zoukankan      html  css  js  c++  java
  • [2017.02.15] 《C++Primer5》 复习笔记

    编程语言主要是提供一个框架,用计算机能够处理的方式来表达问题的解决方法。
    自定义数据类型class的基本思想是数据抽象dataabstraction和封装encapsulation。数据抽象是一种依赖于接口interface和实现implementation分离的编程技术。类的接口包括了用户所能够执行的各种操作;类的实现则包括类的数据成员、负责接口实现的函数体以及定义类所需要的各种是私有函数。
    封装实现了类的接口和实现的分离。封装时使用访问权限控制,设定成员的访问权限。封装后的类隐藏了实现细节,用户只能够使用接口而无法访问实现细节。
    要想实现数据抽象和封装,首先需要定义一个抽象数据类型(abstract data type)。在抽象数据类型中,有类的设计者负责考虑实现细节,也就是定义API的具体实现;而使用该类的程序员只需要抽象地思考类型做了什么,无序了解类型的工作细节。
    STL标准模板库,定义了一组模板化的ADT,核心是很多模板化的容器类和一簇泛型算法,包括容器(线性容器如vector、list、dequeue、queue、stack、map、set、undered_map、undered_set)、迭代器iterator、算法algorithms、functional等。 STL标准库会关注那些簿记操作的细节,特别是内存管理,这样我们编写程序的时候就可以专心投入到要解决的问题上,而不是一些底层细节的琐碎实现。
    
    要想开发一款成功的应用,作者必须充分了解并实现用户的需求。作为一个良好设计的类,既要具有直观且易用的接口,又要具备高效的实现过程。
    
    
    OOP的三大原则:封装、继承、多态。
    
    使用继承,复用已定义的操作。
    重载overload就是简单复用一个现存的名字,但是可以通过参数列表来区分。重载是编译期间进行解析的。
    而重写override是子啊有派生关系的两个类中,父类含有virtual函数,子类通过重写父类同名同参数列表的虚函数来实现。重写实现运行时多态。当父类指针或引用支指向的是派生类的对象的时候,如果同时调用了虚函数,则编译器无法在编译期间确定该函数是哪个版本的,运行时决议。而根据C++对象模型的设计原理,可以知道一个类的成员是按照其声明顺序来在内存中分配对象和初始化的。静态变量和函数属于整个类,分配到静态存储空间。而普通成员变量根据访问区域来设定在内存中的布局。普通成员函数的布局是怎样的?每个类都有一个虚函数表,记录该类类型的所有虚函数的地址。而每一个类实体都有至少含有一个指向虚函数表的指针。使用这种方法,该类的所有对象共享实现代码。虚函数表的布局是预先设置好的,某个成员函数的函数指针在该类的所有子类的虚拟函数表中的偏移地址都一样。在运行时,对虚拟成员函数的调用是通过vptr指针根据适当的偏移量调用虚拟函数表中适合的函数指针来实现的,是一种间接的调用。
    
    
    多态分为两种,编译时静态多态和运行时动态多态。
    其中编译时静态多态是通过函数重载或者模板类/模板函数来实现的,因为编译器在编译的时候就能够决定调用那个版本。而动态多态这是通过虚函数来实现的,当父类指针或引用指向子类实例的时候,如果调用虚函数,编译器如法决议使用父类的函数还是子类的函数。通过运行期间根据指针或者对象所指向的实例的实际类型来确定所调用的函数。
    
    虚函数是 C++ 实现动态单分派子类型多态(dynamic single-dispatch subtype polymorphism)的方式。    动态:在运行时决定的(相对的是静态,即在编译期决定,如函数重载、模板类的非虚函数调用)    单分派:基于一个类型去选择调用哪个函数(相对于多分派,即由多个类型去选择调用哪个函数)    子类型多态:以子类型-超类型关系实现多态(相对于用参数形式,如函数重载、模版参数)。
    
    内置数据类型,也就是bool、char、int、long、double等类型,它们说声明的变量如果是全局变量,则可以被初始化为0。如果是函数体内部的局部变量,则不能够被自动初始化,没有被初始化,那么在使用的时候其值是未定义的。对于自定义数据类型如class、struct,它们所声明的变量也就是实例的初始化工作是由构造函数来实现的。其中如果实例中含有静态成员变量,则需要在类外部进行初始化。该初始化过程在构造函数调用前已经被完成。实例中的其他成员变量则按照成员的声明顺序进行初始化。特别地,当该类类型含有非静态的内置类型或者复合类型(如数组或指针),如果采用编译器合成的默认构造函数来进行默认初始化,则数据的初值将是未定义的。
    
    
    //io类属于不能够被拷贝的类型,只能够通过引用来传递。而输出的时候,应该尽量减少对格式的控制,以保证给用户最大的自由度。
    istream &read(istream &is, Sale_data &item) {
      double price = 0;
      is >> item.bookNO >> item.units_sold >> price;
      item.revenue = price * item.units_sold;
      return is;
    }
    
    ostream &print(ostream &os, const Sale_data &item) {
      os << item.isbn() << " " << item.units_ << " " << item.revenue << " "
         << item.avg_price();
      return os;
    }
    
    
    
    程序越复杂,编译器在编译期间进行的静态类型检测就越重要,当然前提是编译器知道该实体对象的类型,这也就要我们在使用某个变量之前需要声明变量的类型。新版的C++复用了auto关键字,因此可以让编译器根据表达式来推断所声明的类型。
    
    编写软件的时候,多动脑。遵守一些best practices/coding styles,让代码风格具有持续性。
    
    C++中作用域使用花括号来声明。分为全局作用域(global scope)和块作用域(block scope)。作用域可以被嵌套,但是内部作用域中的变量会隐藏外部作用域中的同名变量。
    
    
    #define 宏定义,在过去的C语言中,常用于定义一些常量、小函数或者一些条件编译选项。当定义一些常量或者小函数的时候,由于宏替换操作仅仅是预处理器将宏替换为对应的文本,没有类型检查,无视C++语言中关于作用域的规则,不受作用域限制(除非使用undef取消定义),因此在C++语言中不推荐经常使用宏定义。可用选择的有inline小函数、lambda表达式、const/constexpr/enum class等来代替。
    
    在头文件的开始和结尾写上条件宏,作为头文件保护符号,防止头文件被重复包含。 因为#include头文件,在预编译的时候会把整个头文件都拷贝到该文件中,如果重复包含,可能会出现重复定义,如一个类定义了两次,或者一个全局静态变量被定义了两次。
    #ifndef XXX_H
    #define XXX_H
    
    #endif
    }
    
    确定一个内置类型是否是无符号的?
    分析:有符号和无符号的区别在于溢出后是否会改变符号。
    #define ISUNSIGNED(type) (((type)0 - 1) >0)
    
    分离式编译是把程序分割为多个单独的文件然后再整体编译。
    临时值是编译器在计算表达式结果时创建的无名对象。为某个表达式创建一个临时值,则此临时值将一直存在知道包含该表达式的最大表达式计算完毕位置。
    
    
    this指针:每一个非静态成员函数都有一个this指针,指向对象自身。C++编译器会在编译期间,对函数进行一些额外的处理。拓展函数参数列表,插入this作为第一个参数。由于this指向的是该对象实例的地址,因为唯一所以可以用来区分该类的不同实例。
    
    static关键字,字面意思是“静态的” ,主要作用是:限定作用域,拓展生存期,保证唯一性。
    
    static关键字修饰的变量(不管是局部还是全局的),都被存放在静态存储区,在main函数执行前已经被初始化。
    static关键字可以用来修饰局部变量,该变量在初次运行的时候被初始化,变量的生存期是初始化直到程序结束。
    static关键字也可以用来修饰全局变量或者函数,用来限定该变量或者函数的作用域为本文件,并保证其生存期直到程序结束。如果想要在其他文件访问,需要使用extern声明修饰。
    static变量和全局变量都存储在静态存储区。而static变量可以被自动初始化,全局变量需要用户自己初始化。
    
    static修饰C++成员变量,表示该变量属于类。该变量在类外部初始化,该类类型有且仅有唯一的一个该变量,被类和类的所有对象共享。
    static修饰类的成员函数,则该函数只能访问类的静态成员(变量或者函数),因为static函数属于类而不属于单个对象,因此没有this指针,没法通过this指针来访问类的成员变量。
    
    对类的静态成员函数或者静态成员变量访问可以通过类域运算符::来获取。在发生继承关系的时候,如果子类隐藏了父类中的变量,则可以使用类域运算符来获取该变量。
    
    指针和引用在实现上都是地址的概念, 指针指向一块内存,它的内容是所指向内存的地址;引用是某块内存的别名。但是指针和引用之间存在着一些显著差别:
    引用是变量的别名而不是一个实体,定义的时候必须初始化也就是绑定到对象实体上,所指向的内存块不能为nullptr,运行时不必要检测其合法性(因为必定合法),且在运行的时候不能被修改引用关系。
    
    指针是一个实体,指向一块内存块,定义和初始化可以分离,可以重新赋值,可以为nullptr。在使用指针的使用,如果要获取所指向内存块的内容,需要解引用*。注意解引用前必须验证指针的合法性,防止其是悬空指针或者nullptr。当使用指针/迭代器来遍历数组的时候,一定要注意防止越界(上越界和下越界)。指针本身的值(也就是指针自身的地址)是pass-by-value传递的,可以改变指针的值,也就是修改指针所指向的内存块的位置。
    
    int a = 12;
    int &ri =a, *pi=&a;
    int &ri2 = *pi,*&rpi=pi,**ppi=&pi;
    
    //P84 数组与指针
    对数组的引用中可以写成对指针的引用,确实存在一种指针和数组的定义完全相同的上下文环境,但是并非所有的情况下都是如此。而人们却想当然地归纳并假定所有的情况下数组和指针都是相等的。
    
    X = Y;
    这个上下文环境中,符号X是左值,代表X所在的地址;符号Y是右值,代表Y所指向地址中的内容。左值在编译的时候可以知道,是存储结果的地方,且该变量在运行时一直保存这个地址。右值知道运行时才得知其内容。当需要用到变量中存储的值的时候,编译器就发出指令,从它指定的地址读取变量的值并保存在寄存器中。
    
    // 对指针的引用
    char *pc = 'a'; // 符号表中 pc的地址是 &pc=4624,指向的内存地址是""5081", &’a'=5081
    char c = *pc;
    运行时步骤1:取得地址[4621]的内容,也就是“5081”
    运行时步骤2:取得地址[5081]的内容
    
    
    // 对数组的下标引用
    int i = 4;
    char arr[9] = "abcdefgh";  //设 &arr = 9980
    char c = arr[i];
    char c = *(arr+i);
    编译器符号表中,arr地址是9980。
    运行时步骤1:取得i的值,将它与9980相加,
    运行时步骤2:取得[9980+i]的内容。
    也就是获取数组首元素地址加上偏移量后所在地址的内容。
    
    // 对指针的下标引用
    int i = 4;
    char *parr = "abcdefgh"; //  &parr = 4024, &"abcdefgh" = 5081
    char c = parr[i];
    char c = (parr+i); //
    编译器符号表中,parr的地址是4624,parr的内容是5081,"abcdefgh"的地址是5081
    运行时步骤1:取得地址[4624]的内容,即"5081"。
    运行时步骤2:取得i的值,并将它和5081相加。
    运行时步骤3:取得地址[5081+i]的内容。
    
    arr[4] = '4'; //OK,字符串初始化的数组是可以修改的
    parr[4] = '4'; //ERROR,字符串初始化的指针所指向的是字符串常量,是不可以修改的
    
    专业的C程序员必须使用熟练掌握malloc()函数,并且学会使用指针来操纵匿名内存。
    
    //P205 C专家编程
    数组作为下标形式,总是可以改写成带偏移量的指针表达式。
    一个数组是一个地址,一个指针是一个地址的地址。
    在C语言中,所有的非数组形式的数据都是以传值的形式调用。然而如果函数的参数是数组形式,要拷贝整个数组会造成较大的时间和空间开销。
    因此C语言约定,所有的数组在作为参数传递的时候,都转化为数组起始地址的指针,而其他参数均采用传值调用。这样的约定可以简化版编译器。
    类似的,函数的返回值绝对不能是一个函数数组,而只能是指向数组或者函数的指针。
    
    C编译器会对数组下标表达式进行改写,改写成数组首地址加上偏移量,然后解引用的操作。
    int arr[10];
    arr[i] => *(arr+i)
    
    int matrix[10][20];  //直接声明一个10x20的多维数组
    
    typedef int ivec[10]; //声明一个 int[10]数组的别名为ivec
    ivec matrix[20];      //声明一个数组的数组,也就是多维数组
    
    matrix[row][col] => *(*(matrix+row)+col)
    
    //P239
    能否不适用临时变量,达到交换两个数的目的?
    
    常用做法是执行3个连续的异或操作:
    a^=b;
    b^=a;
    a^=b;
    但是这种方法只能对相同字节长度的变量进行异或操作,不一定能够节省时间或空间。
    
    
    //P52/P103
    想要了解变量的类型,最简单的办法是从右往左读变量的定义,这样有助于弄清楚变量的真实含义。
    注意圆括号必不可少,否则改变了与运算符的优先级,进而影响了程序的解释。
    int *ptrs[10]; //含有10个整型指针的数组
    int &refs[10]; //错误,引用不是对象,不存在引用的数组
    int (*parray)[10] = &arr; //指针指向含有10个整数的数组
    int (&arrRefs)[10] = &arr; //引用指向含有10个整数的数组
    int *(&array)[10] = ptrs; //引用指向含有10个整数指针的数组
    
    现代C++中应当尽量使用vector和迭代器,避免使用内置数组和指针;尽量使用string,避免使用C风格的给予数组的字符串。
    
    关于const,默认情况下只有在本文件中有效。如果需要在文件之间共享,则需要在该const对象的声明前加上extern关键字修饰。
    所谓的指向const的指针或者引用,只不过是让指针或者引用“自以为”是const罢了,让它们不去主角修改所指向对象的值。其实可以通过其他方式来修改值。
    
    constexpr是值值不会改变且在编译期间就能得到计算结果的表达式。字面值(XXX区域)是常量表达式,而运行时捕获的表达式的值,则不是常量表达式。一个constexpr的初始值必须指向nullptr或者0或者固定地址的对象。可以用static修饰后的变量(存放在静态存储区)来初始化constexpr。
    
    decltype((variable)) 双层括号的结果永远是引用,而decltype(variable)的结果只有在variable本身是一个引用或者解引用的时候才是引用。
    
    
    数据结构就是把一组相关的数据元素组织起来,然后使用它们的策略和方法。C++11可以使用类内初始化。
    
    在C语言里面,指针并不是什么所谓的精髓。它只是一扇门,推开门,后面是一整个世界。获取了指针,如果有访问权限,那么可以随意在内存空间中进行自由的驰骋,也就是指针的加减和解引用。信息等于位和上下文,有指针获取了需要的位,然后程序员可以让程序“随性”的解释位(位序列)的含义。
    
    没有什么是不能通过增加一个抽象层解决的。但抽象层并不是免费的,一旦你和什么东西之间被加上了一个抽象层,那你就一定得在每次访问它时受到某种限制、或者付出某些代价。
    
    
    int main(){
        string line;
        while(getline(cin,line)){
            //使用逻辑运算符和短路求值策略,确保访问合法
            if(!line.empty() && line.size() > 80)
            //触发getline返回的那个换行符被丢弃了
            cout<<line<<endl;
        }
    }
    
    C语言的头文件name.h,C++的头文件cname。cname头文件中的名字从属于std命名空间。
    
    //使用下标执行迭代和随机访问
    //注意检查下标的合法性,防止越界
    string s("hello,world!");
    for(decltype(s.size()) index = 0;
        index!=s.size() && !isspace(s[index]));
        ++index){
            s[index] = toupper(s[index]);
        }
    
    //使用范围for语句
    for(auto & c : s){
        c = toupper(c);
    }
    
    
    模板本身不是类或者函数,可以看做是编译器实例化生成类或者函数的一份说明。当使用模板的时候,需要指出编译器应该把函数或者类实例化为何种类型。
    vector<T> v1; //默认初始化
    vector<T> v2(v1); //拷贝初始化
    vector<T> v3=v1; //等价于v3(v1),拷贝初始化而不是赋值操作
    vector<T> v4(n,val); //v4包含n个重复元素,每个元素都是val
    vector<T> v5(n); //包含n个重复元素,每个元素执行值初始化
    vector<T> v6{a,b,c,..}; //列表初始化
    vector<T> v7 = {a,b,c,..}; //等价于v7{a,b,c,..},列表初始化
    
    注意花括号是列表初始化,而圆括号是值初始化。
    
    vector<int> vec{0,1,2,3,4,5};
    
    范围for循环
    for(auto &i :vec){
        i*=2; //翻倍
    }
    
    等价于传统的for循环
    for(auto beg = vec.begin(), end = vec.end();
        beg!=end; ++beg){
            auto &i = *beg;
            i*=2;
        }
    
    //P91/P168/P316
    注意范围for循环在循环开始的时候,预存了end()的值,且每次迭代都会重新定义循环控制变量,并将其初始化为序列中的下一个值。如果在序列中增删元素,end的值可能会变得无效。
    
    //P96
    标准容器迭代器的运算符
    *iter : 解引用运算符
    iter->mem : 成员访问运算符,等价于 (*iter).mem
    ++iter/--iter:递增递减迭代器
    iter1 == iter2:判断迭代器是否相等
    iter1 != iter2:判断迭代器是否不相等
    注意尾后迭代器不实际指向元素,因此不能被解引用。
    
    
    //使用迭代器运算完成二分搜索
    auto beg = vec.begin(), end = vec.end();
    auto mid = begin + (end-beg)/2;
    while(mid!=end && *mid != target){
        if(target <*mid ){
            end = mid;
        }else{
            beg = mid+1;
        }
        mid = begin + (end-beg)/2;
        //注意不要使用 mid = (beg+end)/2; 因为会改变类型!!
    }
    
    
    //P114 使用for处理多维度的数组
    cosntexpr size_t cols = 4, rows = 3;
    int ia[rows][cols];
    for(size_t i=0; i!=rows; ++i){
        for(size_t j=0; j!=cols; ++j){
            ia[i][j] = i*cols + j;
        }
    }
    
    size_t cnt = 0;
    for(const auto & row : ia){
        for(const auto & i: row){
            cout << i<<" ";
            ++cnt;
        }
    }
    
    //使用begin和end函数
    for(auto p = begin(ia); p != end(ia); ++p){
        for(auto q=begin(p); q !=end(p); ++q){
            cout <<*q<<" ";
        }
    }
    
    //使用using声明
    using int_arr = int[4];
    typedef int int_arr[4];
    for(int_arr *p = ia; p!=ia+rows; ++p){
        for(int *q = *p; q!=*p+4; ++q){
            cout << *q<<" ";
        }
    }
    
    
    size_t 是机器无关的类型,在cstddef中定义。
    
    
    //P121
    左值和右值:
    右值使用的是对象的值(内容),左值使用的是对象的身份(内存中的地址)。
    int ia = 10;
    int *pi = &ia;
    typeid(decltype(*pi)) == typeid(int&);
    typeid(decltype(&pi)) == typeid(int**);
    
    *++iter; //先前置递增,再解引用,返回的是iter向前移动一个位置后解引用的结果
    *iter++; //先后置递增,再解引用,返回的是iter开始时指向的值,然后iter向前移动一个位置
    
    赋值运算符的左侧对象必须是一个可以修改的左值,赋值的结果是它的左侧运算对象(也是一个左值)。
    int idx;
    while((idx = get_idx()) != 42){
        // ...
    }
    
    //P138 移位运算符满足左结合律,其优先级比算术运算符低,比关系运算符高
    cout << 1 + 2; // OK
    cout << (1<2); // OK
    cout << 1 < 2; // Error!, 试图比较cout和2的大小
    
    //P139
    sizeof运算符,解引用一个无效指针仍然是一个安全行为,因为不会真正执行运算就可以知道其类型。
    sizeof运算符不会把数组指针转化为指针来处理,但是函数会把数组退化为指针。
    
    
    //P141 隐式类型转换,由编译器自动决定的类型转换
    1. 大多表达式中,比int小的整型值会首先提升为较大的整数类型(整形提升)。不论是否需要,总会整形提升。
    2. 条件表达式中,非bool型会首先转化为bool型。
    3. 如果算术运算或者关系运算的运算对象有多种类型,则首先会转化为同一种类型。
    4. 初始化过程中,初始值转化为变量的类型;赋值语句中,右侧运算对象会转化为左侧运算对象的类型。
    5. 函数调用会发生类型转换。
    6. 大多数用到数组的表达式中,数组会自动转化为指向其首元素的指针。
    7. 类类型转换,有编译器自动执行转换,但是每次只能执行一分钟类类型转换。
    如果构造函数只接受一个实参,则实际上定了转化为此类类型的隐式转换机制(可以称为转换构造函数)。在类内部成员函数声明的时候使用explict修饰函数,只对具有一个实参的构造函数有效。
    
    //P263
    class A{};
    A a(); //错误,声明了一个函数而非对象
    A a;   //正确,是对象而非函数
    
    class A{
    public:
        A() = default;
        explicit A(int i): a(i){}
        explicit A(istream& is);
        combine(A& rhs);
        int a;
    };
    
    A::A(istream& is){
        read(is,*this);
    }
    
    A null_A(1); //直接初始化
    a.combine(A(null_A)); //显示构造函数
    a.combine(static_cast<A>(cin)) ; //satic_cast可以使用explicit显式构造函数
    
    
    //P144 强制类型转换是一种显式类型转换
    cast-name < type> (expression);
    static_cast, dynamic_cast, const_cast, reinterpret_cast
    static_cast 只要不包含底层const,都可以使用static const。注意强制类型转换的结果必须保证与原来的地址值相等,一旦类型不符合,将产生未定义后果。
    const_cast 只能改变底层const,去掉const性质。但是如果一个const变量去掉了const属性,再进行写操作的时候就会发生未定义的行为。常用于有重载函数的上下文中。
    
    //P209
    顶层const形参(指针是常量)无法与普通形参区分开来 。而底层const可以区分,因此可以用来实现函数重载。   当我们传递一个非常量对象或者指向非常量对象的指针的时候,编译器就会优先选择非常量版本。
    
    int fun1(int);
    int fun1(const int); //重复
    
    int fun2(int *);
    int fun2(int * const);//重复
    
    int fun3(int *);
    int fun3(const int *); //重载
    注意优先选择重载函数中的非常量版本。
    
    
    const string &shorterString(const string & s1, const string &s2){
        return s1.size()<s2.size()?s1:s2;
    }
    string &shorterString(string &s1, string& s2){
        const string & = shorterString(const_cast<const string&>(s1),
                                const_cast<const string&>(s2));
        return const_cast<string&>(r);
    
    }
    reinterpret_cast为运算对象在位模式下提供较低层次的重新解释,本质上是机器相关的操作,不具备可移植性。
    
    
    //P730
    运行时类型识别(RunTimeTypeIdentification),当我们想要使用基类对象的指针或者引用来执行某个派生类对象的操作,但是该操作不是虚函数。此时使用RTTI可以帮助我们完成任务,但是必须清楚地知道要转换的目标类型,并且检查类型转换是否被成功执行。如果有可能,还是尽量使用虚函数,而不是直接接管类型管理的重任。
    type运算符,用于返回表达式(是对象)的类型。当运算符对象不属于类类型或者是一个不包含任何虚函数的类的时候,typeid运算符不进行求值,得到的是运算对象的静态类型。如用于指针(而不是指针所指向的对象)时返回的结果是该指针的静态类型。而当运算对象是定义了至少一个虚函数的类的指针或引用左值的时候,typeid会进行求值,其结果直到运行时才会求得。
    dynamic_cast运算符具有如下形式:
    dynamic_cast<type*>(expr);  //指针
    dynamic_cast<type&>(expr);  //左值引用
    dynamic_cast<type&&>(expr); //右值引用
    其中,type必须是一个类类型,通常具有虚函数。expr可以是type的公有派生类、type的公有基类或者expr本身就是type类型。如果转换成功,返回目标类型的指针或引用;如果转换失败,若是指针返回0,若是引用跑出bad_cast异常。
    
    
    为具有继承关系的类实现相等运算符时,使用RTTI非常凑效。类的继承体系中,每个派生类负责添加自己的数据成员,因此派生类的相等运算符必须把派生类的新成员考虑进来。
    
    
    //P150
    对于有超过一个运算符的表达式,要理解其含义,就要理解优先级、结合律和求值顺序。
    短路求值描述逻辑与和逻辑或运算符的执行过程。如果根据第一个运算符对象就能够确定整个表达式的结果,求值将终止,而第二个表达式将不会被求值。
    
    
    //P154
    C++提供了一组kongzhiliu(flow-of-control)语句用于支持复杂的执行路径。
    如条件执行语句、用于重复执行相同代码的循环语句、用于中断当前控制流跳转语句。
    
    // if-else if- else 语句
    if(condition1){
        // statement1
    }else if(condition1){
        // statement2
    }else{
        // statement3
    }
    
    // switch-case语句
    enum class Color { Red, Green, Blue, Default };
    Color c = Color::Green;
    switch (c) {
    case Color::Red: {
      cout << "Red";
    } break;
    case Color::Green: {
      cout << "Green";
    } break;
    case Color::Blue: {
      cout << "Blue";
    } break;
    default: { cout << "Default"; } break
    }
    
    //传统的for循环
    string s("hello");
    for(decltype(s.size()) idx=0, sz = s.size();
        idx != sz && !isspace(s[idx]); ++idx){
        s[idx]=toupper(s[idx]);
    }
    
    //注意范围for循环在循环开始的时候,预存了end。因此不能对容器执行增删操作。
    for(auto & c: s){
        if(!isspace(c)){
            c = toupper(c);
        }
    }
    
    // do{statement;}while(condition);
    string rsp;
    do{
        cout<<"Please enter two values:
    ";
        int val1(0),val2(0);
        cin >> val1>>val2;
        cout<<"The sum of "<< val1 <<" and " << val2 << " is "<< val1+val2<<"
    "
        <<"More? Enter yes or no: ";
        cin >> rsp;
    }while(!rsp.empty() && rsp[0]!='n');
    
    
    // goto
    label_name:
        statement;
        goto label_name;
    
    // P173/P176/P684
    // try-throw-catch
    // 异常是存在于运行时的反常行为,会中断程序的正常流程,典型异常包括失去数据库连接、遇到意外输入等。
    // try语句块用于处理可能会带有异常的语句;throw表达式抛出异常抛出异常,会终止当前执行路径/控制流,将当前的控制权限转交给能够处理该异常的代码,也即是最近的catch子语句;catch子语句捕获异常,如果能够匹配异常类,则被处理,否则异常将被外围的try语句块处理或者终止程序。
    // 编写异常安全exception safe的代码,也就是抛出异常后程序仍然能够正确执行。
    // 必须时刻清楚何时发生异常,异常发生后程序应该如何确保对象有效、资源无泄漏、程序处于合理的状态等等。
    
    exception异常类含有what()虚函数,返回与异常对象类型有关的C风格的字符串。
    exception包括动态的runtime_error(运行时错误,如range_error、overflow_error、underflow_error)和静态的logic_error(程序逻辑错误,如invalid_argument、length_error、out_of_range等)。
    
    //P694 自定义异常类,继承于标准异常,用于表示异常发生时的错误。
    class myerror:public std::runtime_error{
    public:
        explicit myerror(const std::string &msg):std::runtime_error(msg){}
    };
    
    int fun(vector<>int>& vec){
        assert(vec.size()>0);
        try{
            for(auto &i : vec){
                if(i<0){
                    throw myerror("element should be positive");
                }
            }
        }catch(const myerror &e){
            cerr << e.what()<<endl;
        }
    
    }
    
    
    //P182 函数
    函数是命名的代码块/计算单元,对于程序的结构化至关重要。典型函数定义包括返回值类型、函数名、形参(parameter)列表和函数体。
    通过函数调用运算符(也就是一对圆括号)来执行函数。调用运算符作用于函数或者指向函数的指针的表达式;圆括号内是一个用逗号隔开的实参(argument)列表。函数调用完成的工作是:第一步是隐式地定义形参并用实参来初始化函数对应的形参,并然后将控制权转移给被调用的函数。
    当遇到函数体中的return或者函数体被执行完毕,如果return语句有返回值,则返回相应的值,同时将控制权限从被调用的而函数转移回主调函数。
    函数的返回值类型不能是数组或者函数类型,但是可以是指向数组或者函数的指针。
    
    类似于其他名字,在使用前都需要声明。只能够定义一次,但是可以声明多次。声明描述对象会在其他地方被创建,定义则是给对象分配内存。
    对于函数体外的全局变量。程序启动,创建初始化全局变量对象,调用main函数,main函数执行完毕,清理工作,销毁对象,结束程序。
    对于只存在于块执行期间的对象,称之为自动对象。当函数的控制路径经过变量的定义语句的时候创建该对象,到达定义所在的块末尾的时候销毁它。
    内置类型的未初始化局部变量会产生未定义的值。
    对于局部静态变量,其生命周期贯穿函数调用以及之后的时间。在程序执行路径第一次经过对象定义语句的时候初始化,并直到程序终止的时候才被销毁。
    
    
    //193 数组形参
    由于实际上没有真正的二维数组,不允许拷贝数组因此数组传递的时候无法值传递;使用数组的时候通常会转化为指针,传递给函数的数组实际上是指向数组首元素的指针。数组作为形参的函数必须保证使用数组的时候不会发生越界。可以有下列技术:
    1. 使用标记为指定数组的终止范围。如C风格的字符串。
    2. 使用标准库规范,传递数组的首元素和尾后元素指针。
    3. 显式地传递一个表示数组大小的形参。
    4. 使用模板函数,让编译器自动推导数组的维度。
    void print(const char* pstr){
        if(pstr){
            while(*pstr){
                cout<< *pstr++;
            }
            cout<<"
    ";
        }
    }
    
    void print(const char*beg, const char*end){
        while(beg!=end){
                cout<< *beg++;
            }
            cout<<"
    ";
    }
    void print(const char str[],size_t sz){
        for(int i=0; i<sz;++i){
            cout<<str[i];
        }
        cout<<"
    ";
    }
    
    //使用模板函数传递数组,注意必要的括号必不可少
    template <typename T,size_t N>
    void print(const (&str)[N]){
        cout<<"N:"<<N<<endl;
        for(auto elem:str){
            cout<< elem;
        }
        cout<<endl;
    }
    const char *pstr="abcdefg";
    const char str[7]={'a','b','c','d','e','f','g'};
    print(pstr);
    print(begin(str),end(str));
    print(str,end(str)-begin(str));
    print(str);
    
    //C++/C语言中没有真正的多维数组,多维数组其实是数组的数组。
    //作为函数参数传递的时候,其实传递的是指向数组的指针。
    //只有第一维度可以忽略大小,其他维度都必须指明。
    void print(int (*matrix)[10], int rows){
        //...
    }
    void print(int matrix[][10], int rows){
        //...
    }
    
    
    //P201  函数的返回值
    函数返回的值用来初始化调用点的一个临时变量,该临时变量就是函数调用的结果。注意不要返回局部对象/栈对象的指针或者引用,否则函数调用结束后,栈对象销毁,其所指的位置可能会被重用,因此会产生未定义行为。如果返回堆对象,那么函数的调用者需要对返回堆对象负责,确保不会发生资源泄露。
    
    //直接返回类型对象,会创建一个拷贝
    string make_plural(size_t cnt, const string &word, const string& ending){
        return (cnt>1)?(word+ending):word;
    }
    
    const string& shorterString(const string& str1, const string &str2){
        return s1.size()<s2.size()?s1:s2;
    }
    
    //P205 返回数组的指针
    由于数组不能被拷贝,所以函数不能返回数组。但是可以返回数组的指针或者引用。使用数组的类型别名来辅助定义。
    typedef int ArrayT[10];
    using ArrayT = int[10];
    ArrayT* func(int i); //返回一个指向具有10个整数的数组的指针
    
    int (*func(int i))[10]; //返回一个指向具有10个整数的数组的指针
    func(int i); //表示func函数调用的时候需要一个int类型的实参
    (*func(int i)); //可以对函数调用的结果执行解引用操作
    (*func(int i))[10]; //表示解引用的结果是大小为10的数组
    int (*func(int i))[10]; //表示数组的类型是int型
    
    //P223 返回指向函数的指针
    int (*func(int))(int*,int);
    func(int); //func是一个函数
    (*func(int)); //可以对返回值解引用,也就是返回的是指针
    (*func(int))(int*,int);// 该指针指向的是一个函数类型,形参列表是 (int*,int)
    int (*func(int))(int*,int);// 该指针指向的是函数类型,形参列表是 (int*,int), 返回值类型是int
    
    auto func(int) ->int(*)(int*,int); //尾置返回值类型,在参数列表后面指定返回类型。
    using F = int(int*,int); //F是函数类型,不是指针
    using PF = int(*)(int*,int); //PF是指针类型
    F func(int); // 错误,F是函数类型,func不能返回一个函数
    F* func(int); //正确
    PF func(int); //正确
    
    // P64/P78  C专家编程,分析C语言的声明
    声明应该从它的名字开始读取,然后按照优先级顺序依次读取。
    
    char *const*(*func)(); //func是一个指向函数的指针,该函数返回另一个指针,该指针指向一个只读的指向char的指针。
    *func; //标示可以被解引用
    (*func)(); //解引用结果是函数
    *(*func)(); //函数的返回值是指针类型
    char *const*(*func)();  // 指针指向的是 char * const,也就是一个只读的指向char的指针
    
    
    char*(*func[10])(int**); //func是一个数组[0..9],它的元素类型是函数指针,指向的函数的返回值是指向char的指针。
    func[10]; //标示func是一个数组[0..9]
    (*func[10]); //标示可以对func解引用,也就是数组的内容是指针
    (*func[10])(int**); //指针所指向的是函数,函数的形参是int**型。
    char* (*func[10])(int**); //函数的返回值类型是char*型。
    //综述:func是一个函数指针的数组,指针指向的所有函数都把一个指向指针的指针作为函数的唯一参数,函数的返回值类型是指向char的指针。
    
    
    // P70 C专家编程 理解typedef struct foo{ int foo;}foo;的含义
    typedef struct foo { int foo;} foo;
    struct foo val1;
    foo val2;
    
    typedef struct my_tag{ int myvar;}; mytype;
    struct my_tag var1;
    mytype var2;
    
    
    typedef 引入了mytype作为 struct my_tag{int myvar;};的别名,同时引入了结构标签my_tag(也就是结构体名字空间的名字为my_tag),在my_tag前加上struct关键字可以标示同样的意思。
    在不同的命名空间/代码块中可以使用相同的名字。
    
    //P277 C专家编程 库函数调用和系统调用的区别
    //P482 深入理解计算机系统 异常控制流
    库函数调用是语言或者应用程序的一部分,而系统调用是操作系统的一部分。
    函数库调用在用户地址空间中执行,它的运行时间属于“用户”时间,属于过程调用,开销比较小,常见的C函数库调用有system、fprintf、malloc等。而系统调用是调用的系统内核的服务,是操作系统的一个接入点,在内核地址空间中执行,它的运行时间属于“系统时间”,需要切换到内核上下文环境然后再切换回来,开销比较大。当用户需要向内核请求服务,比如切换工作目录chdir、读取文件read、定位文件偏移量lseek、将存储器映射到文件mmap、创建进程fork、加载一个新的程序execv、终止当前进程exit等,都是系统调用。
    系统调用是四种异常(中断interrupt、陷阱trap、故障fault和终止abort)中的trap陷阱异常,是有意而为的。主要用于用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
    
    最佳匹配是从一组重载函数中选择出来一个函数,如果存在最佳匹配,则选出的函数和与其他所有可行函数相比,至少在一个实参上是更加优良的匹配,同时在其他实参的匹配上也不会太差。
    二义性调用是一种编译时错误,原因是在函数匹配的时候多个函数提供的匹配一样好,编译器找不到唯一的最佳匹配。
    自动对象是仅存在于函数执行过程中的对象。当程序的控制流经过对象的定义语句的时候,创建该对象;当到达了定义所在的块的末尾的时候,销毁该对象。
    隐藏名字是指某个作用域类声明的名字会隐藏掉外层作用域中的声明的同名实体。可以使用与运算符暴露名字。如实现swap的时候,把std::swap暴露出来。
    对象的生命周期。块内部的费静态对象的生命周期是从它的定义开始,直到所在的块末尾为止。程序启动后创建全局对象,程序控制流经过局部静态对象的定义时,创建该局部静态对象;当main函数结束的时,销毁全局对象和局部静态对象。
    operator()函数调用运算符,用于执行某个函数。当一个类重载了函数调用运算符之后,就会变成函数对象。
    //P217 函数匹配与实参类型的转换
    当有几个重载函数的形参数量相等以及某些形参的类型可以由其他类型转换得来的时候,确定本次函数调用的版本就比较困难,需要做以下工作。
    1. 确定候选函数集(同名且调用点可见)和可行函数(形参个数符合且形参类型与实参类型一致或者能够转换)。
    2. 寻找最佳匹配(实参类型与形参类型越接近,匹配越好),若没有优势的匹配函数,那么会出现函数调用的二义性。
    3. 实参类型的转换分为几个等级。
    a. 精确匹配:实参和形参类型相同,实参从数组类型或者函数类型转化为对应指针类型,对实参添加或者删除顶层const(指针本身是const)。
    b. 通过底层const转换(所指向的对象是const)实现匹配。用非常量的实参初始化常量的形参需要类型转换。
    c. 通过类型提升实现转换实现的匹配。
    d. 通过算术类型转换或者指针转换实现的匹配。
    e. 通过类类型转换实现的匹配。
    
    //P514 类型转换运算符
    类型转换运算符是一个特殊的函数,负责将一个类类型转化为其他类型。类型转换运算符没有显式的返回类型,也没有形参,并且必须定义为成员函数,通常是const。类型转换运算符是隐式执行的,无法给这些函数传递实参,当然也就无法在类型转换运算符的定义中使用任何形参。一个类最好不要定义多重类型转换,防止出现二义性调用。
    operator type() const;
    explicit operator bool() const;  //operator bool一般定义为explicit的
    
    class SmallInt {
    public:
      SmallInt(int i = 0) : val(i) {
        if (i < 0 || i > 255) {
          throw std::out_of_range("Bad SmallInt value");
        }
      }
      opertor int() const { return val; }
      firend SmallInt operator+(const SmallInt& lhs, const SmallInt& rhs);
    
    private:
      std::size_t val;
    }
    
    //P521 函数匹配与重载运算符
    重载运算符也是重载函数,因此适用函数匹配规则。如果对同一个类提供了重载运算符,同时也提供了转换目标是算术类型的类型转换,那么可能会遇到重载运算符和内置运算符的二义性问题。
    
    SmallInt s1,s2;
    SmallInt s3 = s1 + s2; // 使用重载运算符 operator+
    int i = s3 + 0;        // 产生二义性错误
    
    
    //P250 类的声明
    类的前向声明之后,在类被具体定义之前,是不完全类型。此时可以定义该类类型的指针或者引用,但是不能定义该类类型的对象。因为创建对象之前必须定义过而不能仅仅是声明,因为编译器需要知道创建一个对象需要多少存储空间。同时一旦一个类的名字出现之后就是声明过了,因此一个类允许包含指向它自身类型的引用或者指针。
    被隐藏的变量可以通过域运算符来获取。
    
    关于友元friend,友元函数破坏了类的封装性,改变了对成员的访问权限。友元函数或者友元类最好在类内部集中声明,然后在内外部再次集中声明一次,最后才是友元函数的定义。当一个类内部含有对自身的友元,则该类的对象之间都是友元关系。
    很多类中,初始化可赋值的区别事关底层效率的问题:前者直接初始化数据成员,后者先初始化再赋值。一些苏旭成员必须被初始化如const成员,需要使用初始化表来初始化。
    
    C++11拓展了构造函数初始值的功能,使得我们可以定义所谓的委托构造函数delegating constructor。一个委托构造函数使用它所属类的其他构造函数来执行它的初始化过程,或者它把自己的一些或者全部职责委托给其他构造函数。
    
    
    //
    iostream 处理控制台IO
    fstream 文件流,处理文件IO,用来读写命名文件的流对象。除了普通的iostream操作外,文件流还定义了open和close函数,用来打开和关闭文件。
    stringstream 字符串流,处理内存stringIO,用来读写string的流对象。除了普通的iostream的操作外,字符串流还定义了一个名为str()的重载函数成员。调用str()的无参数版本会返回与字符串流对象相关联的string。调用时传递给它一个string参数,则会将字符串流与该string的一个拷贝关联起来。
    由于里氏替换原则,派生类可以替换基类,因此可以使用fstream/sstream替换iostream&。
    每一组IO对象都维护了一组条件状态,用来指出此对象是否可以进行IO操作。如果遇到了错误,如输入流遇到了文件末尾,则此对象的状态会变成失效,后续的输入操作都不能够被执行,直到错误被纠正为止。标准库提供了一组函数用来设置了检测这些状态。
    对文件进行操作,按照指定的文件模式(如in/out/app/ate/trunc/binary),打开open(),检测状态是否有效is_open()/good()/fail()/eof(),关闭文件close()。 当一个fstream对象被销毁的时候,close()函数会被自动调用。
    保留ofstream打开文件中已有的数据的唯一方法是显式地指明app或者in模式。
    
    //P284/P286 处理文件
    ifstream input(argv[1]);
    ofstream output(argv[2], ofstream::app | ofstream::out);
    output.close();
    output.open(argv[2]+std::string(".log", ofstream::app);
    if(output.is_open() && input.is_open()){
      Sale_data total;
      if (read(input, total)) {
        Sale_data trans;
        while (read(input, trans)) {
          if (total.isbn() == trans.isbn()) {
            total.combine(trans);
          } else {
            print(output, total);
            total = trans;
          }
        }
        print(output, total);
      } else {
        cerr << " No date ?!" << endl;
      }
    }
    
    // P288/P289 使用istringstream/ostringstream
    struct PersonInfo{
        string name;
        vector<string> phones;
    };
    string line,word;
    vector<PersonInfo> people;
    while(getline(cin,line)){
        PersonInfo info;
        istringstream record(line);
        record >> info.name;
        while(record >> word){
            info.phones.push_back(word);
        }
        people.push_back(info);
    }
    
    for(const auto & entry:people){
        ostringstream formatted, badNums;
        for(const auto &nums:entry.phones){
            if(!valid(nums)){
                badNums << " "<<nums;
            }else{
                formatted <<" "<<format(nums);
            }
        }
        if(badNums.str().empty()){
            cout<<entry.name<<" "<<formatted.str()<<endl;
        }else{
            cerr<<"error:" << entry.name<<" invalid number(s) " <<badNums.str()<<endl;
        }
    }
    
    
    
    //P292 顺序容器
    所有的容器都共享公共接口,不同的容器按照不同的方式对其拓展。这个公共接口使得容器的学习更加容易,基于某种容器所学习的内容适用于其他容器。每种容器都提供了性能和功能的权衡。
    顺序容器提供了快速访问元素的能力。
    array :固定大小的数组,支持快速随机访问,不支持插入和删除操作。
    vector: 可变长度的数组,支持快速随机访问。尾部之外的插入和删除可能会很慢。
    string: 与vector相似的容器,专门用于保存字符,随机访问很快,尾部插入和删除很快。
    deque : 双端队列,支持快速随机访问。在头尾位置插入和删除速度很快。
    list  : 双向链表,只支持双向顺序访问,在list中任何位置进行插入和删除操作的速度都很快。
    forward_list: 单向链表,只支持党项顺序访问,在链表任何位置进行插入和删除操作的速度都很快。
    
    容器的内存布局,决定了容器中元素的访问方式以及对容器中元素能否进行特定的操作。
    如vector/string存储在线性连续空间中,支持快速随机访问,但是在中间插入和删除元素会很耗时,需要移动插入或者删除位置之后的所有元素来保证连续存储,会使得一些迭代器失效。而且添加元素可能会导致容器所需要的内存超过了预先分配的内存,因此需要执行”分配新内存空间-移动/拷贝所有的元素-删除旧空间“的步骤,导致某次操作非常耗时。
    list双向链表和forword_list单向链表这两个容器设计的目的是令容器中任何位置的添加和删除操作都很快。作为代价是不支持随机访问;为了访问一个元素,我们需要遍历整个容器。且这两个容器由于需要保存内存块的指针,也就需要额外的内存开销。
    deque是一个更为复杂的数据结构,以分段线性连续来模拟线性连续,支持快速随机访问,在两端插入和删除元素的速度很快,但是在中间位置则很慢。
    
    通常使用vector是最好的选择,除非有很好的理由来选择其他容器。
    一般来说,应用中占主导地位的操作(执行访问的操作多还是执行插入/删除的操作多)决定了容器类型的选择。
    如果不确定使用那种容器,那么在程序中只使用vector和list的公共操作,也就是使用迭代器而不是使用下标操作来避免随机访问。这样在必要时选择使用vector或者list都很方便。
    
    
    //P295 容器的常用操作
    //类型别名
    iterator        //容器的迭代器类型
    const_iterator  //容器的制度迭代器类型
    size_type       //容器的大小,无符号整型
    difference_type //带符号整型,足够保存两个迭代器之间的距离
    value_type      //元素类型
    reference       //元素的左值类型,与value_type&含义相同
    const_reference //元素的const左值类型,即 const value_type&
    
    //P299 构造与赋值
    C c;      //默认构造函数
    C c(c1);  //拷贝构造函数
    C seq(n);              //顺序容器类型的seq包含n个元素
    C seq(n,init);         //顺序容器类型的seq包含n个由init初始化的值
    C c = c1;              //拷贝构造函数
    c = c1;                //赋值操作符
    C c(iter1,iter2);      //将迭代器iter1和iter2之间的元素拷贝到c中
    C c{a,b,c,...};        //列表初始化,c初始化为初始化列表中元素的拷贝,任何遗漏的元素都将进行值初始化
    c = {a,b,c,...};       //将c中的元素替换为列表中的元素
    c1.swap(c2);           //等价于swap(c1,c2);交换容器c1和c2中的元素,swap通常比直接拷贝元素快的多
    
    //大小
    c.size(); //a中元素的数目,不支持forword_list(为了达到了手写list相当的性能,没有维护size的操作)
    c.empty();             //判断时候为空
    c.max_size();          //最大元素数目
    
    //P305 添加删除元素,注意操作后返回的迭代器是第容器改变部分的一个有效的迭代器位置或者待插入的位置
    c.insert(args);        //将args中的元素拷贝到c中
    c.emplace(init);       //使用init初始化构造c中的一个元素
    c.clear();             //清除c中的所有元素,返回void
    c.erase(iter);         //删除iter迭代器所指示的内容,返回当前迭代器的下一个迭代器
    c.erase(b,e);          //删除[b,e)迭代器范围类的元素。返回的是指向被删除元素之后元素的迭代器
    c.push_back(t);        //值传递,拷贝到容器
    c.emplace_back(t);     //使用参数在容器管理的内存空间中直接构造元素,比push_back/insert高效
    c.insert(p,t);         //位置和单元素,在位置p之前创建一个值为t的元素,返回新添加的元素的迭代器
    c.insert(p,n,t);       //位置和长度,在位置p之前插入n个值为t的元素,返回新添加的第一个元素的迭代器;若n=0,返回p。
    c.insert(p,b,e);       //位置和迭代器范围,将迭代器b和e之间的元素插入到迭代器p所指向的元素之前
    c.insert(p,il);        //位置和初始化表,使用初始化列表,将这些元素插入到迭代器p之前
    
    //顺序容器中元素的访问
    c.front()   //返回c中首元素的引用,c为空则未定义
    c.back()    //返回c中尾元素的引用,c为空则未定义
    c[n]        //返回c中下标为n的元素的引用,n>=c.size()则未定义
    c.at(n)     //返回下标为n的元素的引用,如果下标越界,则抛出out_of_range异常
    
    //关系运算符
    ==, !=                 //所有的容器都支持相等性测试
    <,<=,>,>=              //关系运算符,无序容器不支持
    //获取迭代器
    c.begin(),c.end()      //返回c的首元素和尾后元素的迭代器
    c.cbegin(),c.cend()    //返回const_iterator
    //反向容器的额外成员,不支持forward_list
    reserve_iterator       //按照逆序寻址元素的迭代器
    const_reserve_iterator //按照逆序寻址元素的常量迭代器
    c.rbegin(),c.rend()    //返回c的首尾元素和首前元素的迭代器
    c.crbegin(),c.crend()  //返回const_reserve_iterator
    
    //管理容器大小的成员函数 P318
    c.reserve(n); //预先分配至少能够容纳n个元素的内存空间
    c.capacity(); //在不扩张容器的情况下,容器当前能够容纳的元素个数
    c.size();     //当前容器中元素的个数
    c.shrink_to_fit(); //将申请退回内存空间,将capacity()减少为size()相同的大小
    c.resize(n);      //调整c的代销为n个元素。只会改变元素的个数,而不影响vector预先分配的内存大小。
    
    注意元素访问的时候,保证下标的有效性是程序员的责任,下表运算不检查下标是否合法。
    使用越界的下标是一种严重的程序设计失误!
    如果想要保证下标的合法性,可以使用at成员函数,当下标越界的时候会抛出out_of_range异常。
    删除元素的成员函数并不检查其参数。删除元素之前,程序员必须确保他们是存在的。
    
    容器中元素的增删操作,可能会使得指向容器的指针或者引用或者迭代器失效,如果失效之后又使用,会导致严重的程序设计错误。必须保证每次改变容器的操作之后都能够正确地重新定位更新迭代器/指针/引用,防止使用失效的东西。对vector/string/deque,如果在循环中需要插入/删除迭代器,那么不要缓存end返回的迭代器,并且要更新。
    
    在需要修改迭代器的操作中,最好不要保存end迭代器。因为增删元素后,原来返回的迭代器总是失效的。
    必须反复用end()函数更新迭代器。
    // P316 删除偶数元素,并复制每个奇数元素
    vector<int> vi{-5,-4,-3,-2,-1,0,1,2,3,4,5};
    //删除偶数元素,并复制每个奇数元素
    for(auto curr = vi.begin();curr!=vi.end();){
        if(*curr %2 !=0){
            // 插入奇数元素,更该迭代器位置
            curr = vi.insert(curr,*curr);
            curr+=2;
        } else{
            //删除偶数元素,更改迭代器位置
            curr = vi.erase(curr);
        }
    }
    
    // P313 在forward_list中添加或者删除元素,必须关注两个迭代器
    // 一个指向当前要处理的元素,一个指向其前驱
    forward_list<int> flst{-5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6};
    auto prev = flst.before_begin(); //首前迭代器
    auto curr = flst.begin();        //首个迭代器
    while (curr != flst.end()) {
      if (*curr % 2 != 0) {
        // 插入奇数元素,更该迭代器位置
        curr = flst.emplace_after(curr, *curr);
        prev = curr;
        curr++;
      } else {
        //删除偶数元素,更改迭代器位置
        curr = flst.erase_after(prev);
      }
    }
    
    for_each(flst.begin(),flst.end(),[](int& item){cout<<item<<"
    ";});
    for (auto &item : flst) {
      cout << item << endl;
    }
    
    
    
    //P470
    对象的移动。在程序中,有些对象没必要进行拷贝,重新分配内存时,将旧内存中的元素拷贝到新内存是不必要的,更好的方式是移动元素。同时一些IO类或者unique_ptr类包含了不能被共享的资源如指针或者/IO缓冲,因此这些对象不能拷贝但是可以移动。
    
    可移动可拷贝:标准容器,string,shared_ptr。
    可移动不可拷贝:IO类(包含IO缓冲),unique_ptr(包含不可以共享的指针)。
    
    int && rr1 =  42; //正确,字面值常量是右值
    int && rr2 = rr1; //错误,表达式rr1是左值
    int && rr3 = std::move(rr1); //正确,move函数承诺,除了对rr1赋值或者销毁它之外,我们将不再使用它。
    
    
    
    //P319
    reserve 申请预先分配空间
    capacity 实际分配的空间
    shrink_to_fit 请求退换多余的内存
    size 已使用的空间
    vector可以有自己的内存分配策略,必须遵守的是,只有当迫不得已的时候才可以分配新的内存空间。使用内存池的分配策略,预先分配一系列固定大小的内存块,当需要使用的时候从中取出合适的内存块。如果内存空间不够用了,则分配新的空间,拷贝元素,删除旧空间。虽然看上去很浪费时间和空间,但是摊还之后对额代价为O(1)。
    
    
    注意string::npos是string的静态成员变量,定义为const string::size_type也就是const unsigned型,并初始化为-1也就是最大大小。
    
    
    // P321 string的一些操作
    查找find/rfind/find_first_of/find_last_of/find_first_not_of/find_last_not_of(args)
    修改insert/erase/assign/append/replace
    获取substr/assign/
    比较compare,等于(0)、大于(>0)、小于(<0)
    
    // P328 新标准库中string的数值转换
    int i = 42;
    string s = to_string(i); //转化为字符
    int ii = stoi(s);        //将字符串转化为整数
    string s2 = "pi = 3.14159";
    double d = stod(s2.substr(s2.find_first_of("+-.0123456789")));
    
    double pi = 3.14159;
    ostringstream oss;
    oss << pi;
    double pi2 = stod(to_string(pi));
    double pi3 = stod(oss.str());
    
    cout << ii << endl << d << endl << pi2 << endl << pi3 << endl;
    
    
    //P329 容器适配器
    适配器是一种机制,使得某物的行为看起来像是另一种事物一样。一个容器适配器能够接受一种已有的容器类型,使其行为看起来相是另一种事物一样。有3种容器适配器:stack、queue、priority_queue。默认情况下,stack和queue使用deque来实现,而priority_queue是给予vector来实现的。
    
    // P336 泛型算法 概述
    标准库没有给每个容器添加大量功能,而是提供了一组泛型算法,大多数是独立于任何特定容器的,可以用于不同类型的容器和不同类型的元素。
    在头文件algorithm中定义了大部分的算法。一般情况下,算法不直接操作容器,而是遍历两个迭代器指定的一个元素范围来进行操作。迭代器令算法不依赖于容器,如find算法可以在未排序的元素序列中查找特定的元素,元素序列可以是vector、list等;但是算法依赖于特定元素类型的操作,如find查找的时候需要支持==操作,来实现每个元素和给定元素的比较操作,我们可以使用自定义操作来替代默认的运算符。
    算法不会执行迭代器操作,但是会执行迭代器的操作。
    
    // 初识泛型算法
    大多数标准库算法都对一个范围类的元素执行操作。称元素范围为“输入范围”。接受输入范围的算法总是使用前两个参数标示范围(分别表示起始迭代器和尾后迭代器)。理解算法最基本的方法就是了解他们是否读取元素、改变元素或者重排元素顺序。
    
    弄清楚决定一个泛型算法实例化的关键因素是什么,是迭代器范围?参数类型?是否发生替换?是否发生写操作?
    
    //P343
    
    
    
  • 相关阅读:
    Git -- 撤销修改
    Git -- 管理修改
    Git -- 相关命令
    Git -- 工作区 和 暂存区
    Git -- 基本操作 之 版本回退
    Git -- 创建版本库
    Git -- 安装
    Git -- 简介
    word文档下划线无法显示的解决方法
    The data protection operation was unsuccessful. This may have been caused by not having the user profile loaded for the current thread's user context,
  • 原文地址:https://www.cnblogs.com/ausk/p/6401589.html
Copyright © 2011-2022 走看看