C++ Primer

  1. 基本概念
    1. UNIX 系统中,编译器通常默认将可执行文件命名为 a.out(以 .out 为后缀),但其实也可以没有后缀;windows 系统则是以 .exe 为后缀;
    2. 可以通过 echo 获取 main 函数的返回值;如果 main 返回 0,则 echo 显示结果为 0;如果 main 返回 -1,使用 echo 获取显示结果为 255;
    3. 标准库 iostream 定义了两种类型,4个对象,
      1. istream 类型:只有1个对象,cin;
      2. ostream 类型:有3个对象,分别为 cout, cerr, clog;
        1. 写入 cerr 数据一般不缓冲,写到 clog 一般有缓冲;
    4. << 输出运算符,用法:左边为接收数据的输出对象,右边为输出的内容,返回结果仍为输出对象,因此支持链式输出,即右侧可以不断的接收更多的 << 运算符及内容;
      1. 示例: std::cout << “The content to print out” << std::endl;
    5. >> 输入运算符的用法与输入出运算符类似,左侧为输入流对象,右侧为接收数据的对象,从输入流对象将数据赋值给右侧的对象;并返回输入流对象;
    6. std::endl 中的 “endl” 很特殊,它是一个操纵符 manipulator,效果是结束当前行,同时它可以将设备关联的缓冲区的数据写入设备(流)中,而不是仅停留在内存的缓冲区中等待写入流;
      1. 这里面暗含了一个问题,在使用打印语句来调试程序时,当程序出现崩溃的时候,如果打印的内容没有写入流,而只是在缓冲区,则打印语句没有起到效果,这样可能会误导错位位置的推断;
      2. 这么说来,可以考虑使用 endl 确保每次打印的内容会写入流中,而不是丢失在缓冲区中;
    7. 标准库中的定义的所有名字都在 std 命名空间中,因此需要使用 std::cout 来访问 cout,这样可以避免命名冲突;缺点就是书写起来会稍微麻烦一点;
    8. 在 cygwin 下貌似只能用 g++ 编译源文件,无法使用 cc 来编译;但是在 win10 的 ubuntu 子系统则可以;
    9. 错误的注释比完全没有注释更糟糕,因为它会误导其他人;
    10. 假设
      1. int value = 0;
      2. while ( std::cin >> value )
      3. 此处从文件流中取值并赋给变量 value,由于 value 是整数类型,当 std::cin 的值不是整数类型时,istream 对象的状态会变成无效,处于无效状态的 istream 对象会使得 while 条件变为假,从而使得循环终止;(说明 istream 对象为做条件语句时,会触发类型转换,变成一个 bool,而这个布尔值的数据,猜测应该存储在 istream 对象的某个成员中的)
    11. 每个类实际上都定义了一种新的数据类型,其类型名称就是类的名称;
    12. 在动手定义类之前,先手工罗列一下类所允许的操作;
    13. IO 设备通常会将数据先保存在一个缓冲区中,读写缓冲区的动作,与程序中的读写动作是不相关的;可以显式的刷新缓冲区,强制将数据从缓冲区写入设备;默认情况下,读 cin 的时候会刷新 cout;程序异常中止时,也会刷新 cout;
    14. 数据结构:数据以及允许对这些数据进行的操作的一种逻辑组合;
    15. 双冒号 :: 表示作用域运算符,用来访问某个命名空间中的名字(这么说类的内部也算是一个命名空间?);点 . 运算符则用来访问对象的成员;
      1. 由于全局作用域没有名字,因此当出现 ::var 的用法时,表示要访问全局作用域下的 var 变量;
    16. 单冒号:除了和问号组成条件运算符外,还有一种应用场合是在类的构造函数中,用来给数据成员提供默认初始值;(条件运算符也即 js 中的三元运算符);
  2. 变量和基本类型
    1. 基本内置类型
      1. 可寻址的最小内存块称为“字节”;存储的基本单元称为“字”(其实“字”不仅仅出现在存储场合,它应该算是计算机单次操作的一个基本单位);
      2. 如果两个字符串字面值位置相邻在一起,中间仅由空格、换行符、缩进三者隔开,则它们属于同一个字符串;
        1. 示例:“abc” “def”
        2. 表示上看,像是两个字符串,其实它们是一个字符串;最终输出其实是”abcdef”(这种语法规则很是误导人,应该直接报错就好,而不是兼容它);
      3. 转义序列一般是用字母来表示某个特殊字符,但也可以使用泛化的转义序列,即斜杠加数字(8进制或16进制)来表示某个特殊字符;
        1. 这么说可以使用转义序列打出一个有趣的符号组成的图案;
      4. 当要表示一个长整形字面值时,后缀应使用大写的字母 L 而不要用小写,因为小写的字母 l 看起来像是数字壹“1”;
    2. 变量
      1. C++ 中初始化与赋值的区别
        1. 初始化是指创建变量的时候,给变量一个初始值;
        2. 赋值是指擦除对象的当前值,用一个新值取代;
      2. 在 C++ 中变量和对象是同一个意思,都用来表示一段具名的、可供程序操作的内存空间(感觉这一点跟 C 语言很像)(理解这一点非常重要,它涉及了硬件的基本工作原理)
      3. C11 开始支持列表初始化的语法,使用花括号来初始化;
        1. 示例:int units_sold = {10};
        2. 无论是初始化对象,还是为对象赋新值,都可以使用花括号语法(怎么好像是一切皆对象的感觉?)
        3. 列表初始化有一个好处,即会提示无意中产生的类型转换错误,示例如下:
          1. long double ld = 3.1415926536;
          2. int b = {ld}; // 此处编译器会提示类型转换错误,因为双精度转整型,会丢失部分数据;
          3. int b = ld;// 不会报错,但实际上有损失产生;
      4. 在函数体内声明的变量,如果没有初始化,则会产生不可预料的后果;原因:在函数体外声明的变量,如果没有初始化,编译器会给一个默认值,默认值多少取决于变量的类型;因此:强烈建议手工初始化每一个内置类型的变量;
      5. 类的对象如果没有显式的初始化,则其默认值由类来决定;(类会有一个默认构造函数,它可能是类的实现者定义的,也有可能是编译器合成的)
      6. 分离式编译:把程序分成多个文件,然后每个文件独立编译;
      7. 声明操作(declaration)使得变量名字被程序各部分所众知;定义操作(definition)则负责创建与名字关联的实体;定义会申请存储空间,并可能会给变量赋一个初始值;
        1. 这么说声明的时候,是没有申请存储空间的?所以仅有声明的类型,是不可使用的?
      8. 如果想要声明一个变量,但不定义它,可以使用 extern 关键字来实现(貌似是 external 单词的缩写,表示该变量在外部定义);
        1. 示例:extern int i;
        2. 注意,它跟 int i 是不同的,int i 做了声明加定义甚至是初始化的动作;
        3. 如果 extern 语句有给变量赋值,则该语句不再是声明,而是定义了,例如 extern int i = 3; 在这种情况下,extern 的作用被抵销,变得多余了;在函数体内部这么做会报错;
        4. extern 有两个作用
          1. 当在某个文件中初始化变量时使用它时,表示这个变量可以被外部其他文件访问;
          2. 当在某个文件中声明它时(未定义),可表示这个变量是由外部其他文件定义的;
        5. extern 还有一个用法即 extern C,它表示该部分内容(例如某个函数)应按照类 C 的标准,进行编译,这样其他遵循同样标准的语言,就可以调用 C++ 里面的这个函数
      9. 变量仅能被定义一次,但可以多次声明?
        1. 如果想要在多个文件中使用同一个变量,则应该将声明和定义分开;
        2. 此时,变量必须且仅能够在一个文件中进行定义;其他使用这个变量的文件,需要对变量进行声明,但不能重复定义;
        3. 这一段有点绕,待研究一下其使用场景;(看完整本书,大概明白了一点使用的场景,它使用在想要定义某个全局变量的场合)
          1. 假设我们要定义一个全局变量,供多个文件进行引用;
          2. 做法:
            1. 我们创建一个头文件 common.h,在里面使用 extern 关键字声明这个全局变量,例如 extern int global;
            2. 然后在其他想要引用它的源文件中,包括这个头文件 #include “common.h”
            3. 接下来,我们只需要在多个源文件中的某一个,对这个全局变量进行定义,例如 global = 42;
            4. 然后其他同样引用了这个头文件的源文件,就会自动共享获得这个全局变量的定义;
          3. 原理:编译器只负责编译,真正实现共享的是连接器,它将会使用到这个变量的其他源文件,与定义这个变量的源文件建立连接
          4. 注意:
            1. 变量在定义的那个源文件中,需要处于顶层作用域,以便其他文件可以访问得到;而不是局部作用域(例如某个函数内部);
            2. 变量不可以定义为 static 静态类型,因此类型同样会限制访问权限;
      10. 标识符的规范
        1. 只能由字母、数字和开划线组成;
        2. 不能以数字开头;
        3. 不能出现两个连续的下划线;(没测试成功)
        4. 函数体外的标识符,不能出现下划线+大写字母开头(没测试成功);
      11. 名字的有效区域始于声明名字的位置,结束于声明语句所在作用域末尾;
    3. 复合类型
      1. 引用:为对象起了另外一个名字,引用即别名;它的本质是将一个标签,绑定到某个已经存在的对象上面;而一般的变量初始化,是将值拷贝给一个新建的对象;
        1. 当定义了一个引用后,对引用所进行的操作,都会作用到它所绑定的对象上面;
        2. 引用的语法:类型名 &引用名 = 对象名
          1. 示例:int &refVal = ival;
        3. 由于引用是指向一个已经存在的对象,所以创建引用必须同时初始化,因为引用不能单独存在,它必须跟某个对象绑定才有存在的意义;
        4. 引用的类型,需要与绑定的对象的类型严格匹配,不匹配会报错;
        5. 引用只能绑定在已有对象上,不能绑定在值上,例如整数、浮点数、字符等;除非这个引用是一个常量引用类型,常量引用类型可以绑定字面值、非常量对象;
          1. 值是一个临时的对象,但表达式语句结束后,很快会被销毁;
        6. 由于引用本身不是一个对象,所以不能定义引用的引用;
          1. 虽然不能定义,但有可能会间接的创建出来,例如在定义模板参数或类型别名时
          2. 引用的引用可能会触发折叠;
        7. 字面值可以用来初始化一个常量引用,但不能用来初始化一个非常量引用,即 const int &r = 42 是OK的,但 int &r = 42 是错误的;
      2. 指针
        1. 指针与引用的不同点在于,指针本身是一个对象,因此它是可以被赋值的;
        2. 指针在生命周期内可以指向多个不同的对象;(这么说,引用不行?试了下,确实不行;引用并不是对象,没有内存空间,它只是一个别名)
        3. 指针无须在定义时赋初始值,但是,在块作用域内,这么做会带来不可预估的后果;(或许可以让它的初始值为 nullptr)
        4. 由于引用不是对象,因此它没有地址,因此也不能定义指针指向一个引用;
        5. 空指针使用 nullptr 来表示,示例:int *ptr = nullptr(尽量避免使用 NULL),等价于 int *ptr = 0;
        6. 指针如果未被初始化,大多数编译器会默认将指针所在地址的内容当作默认内容,但实际上它本不应存在,如果里面刚好有内容,则会出现不可预料的错误;
        7. 最好的做法:
          1. 先定义好对象,再定义指向对象的指针;
          2. 如果做不到第1条,则指针必须初始化为 nullptr;
        8. void* 是一种特殊类型的指针,它可以存放任意类型的指针地址,但局限是不能通过它来访问地址所指向的对象,因为我们并不知道对象的类型,所以这种访问也没有意义;
        9. 通过星号 * 的个数,可以用来判断指向指针的指针的层次;
        10. 由于指针是一个对象,因此可以定义一个引用指向它,例如:int *&r = p,此处判断 r 的类型的办法对声明语句从右往左阅读,第一个字符是 & 表示它是一个引用,第二个字符是 * 表示它是一个指针引用,第三个字符 int 表示它是一个整数类型的指针引用;
        11. 解引用的英文 dereference,缩写 deref;
    4. const 限定符
      1. const 可以用来声明一个初始化以后不可再修改的常量,因此它在声明的时候必须同时初始化(不然就没有机会赋值了);它可以是任意类型,而且使用方式也同普通变量相同,唯一的不同点是不能修改它的值,否则会报错;
      2. 默认状态下 const 变量仅在当前文件内有效;如果想要在多个文件中共享 const 变量,则应该在变量定义语句前加上 extern 的关键字;
      3. 非常量不可引用常量,因为这样会触发常量被修改的可能:
        1. const int a = 10;
        2. int &b = a // 此处试图使用非常量引用常量,是错误的;
        3. const int &b = a; // 这样就可以
      4. 正常情况下,引用的类型需要与其被引用对象的类型一致,但有一种例外情况,即常量引用在初始化的时候,允许使用任意表达式,只要该表达式的结果,可以转换成常量引用的类型(编译器会生成一个临时量来存储转换化的结果)(即 A 类型常量引用可以被绑定到 B 类型对象上,由于引用是常量,不允许赋值,所以这种绑定可以成立;但如果引用本身不是常量,可以赋值,则这种绑定会报错是);示例:
        1. doubal dval = 3.14;
        2. const int &ri = dval; // 常量引用非常量,这是OK的;
      5. 对于表达式 const int &r1 = 42 来说,怎么感觉 42 在这里像是被当作一个对象来处理?(它不是对象,是一个字面值)
      6. 常量引用仅对引用本身可参与的操作做出了限制,而对引用的那个对象是否是一个常量,却没有限制;即常量引用可能引用的是一个非常量;
      7. 指向常量的指针,也需要定义成常量类型,即
        1. const double val = 3.14;
        2. const double *ptr = val; (注:开头不能少了 const 限定符)
      8. 指针的类型需要与指向的对象的类型相一致,但有一种例外,即允许常量指针指向一个非常量对象,但是无法通过这个常量指针改变非常量对象;(即常量指针与常量引用一样,会被限制操作)
        1. 所谓的常量指针或常量引用都是“自以为是”的家伙,他们以为自己是常量类型了,就应该自觉的不去改变所指向的对象的值;
      9. 常量指针:指针由于是一个对象,所以它自己也可以是一个常量,这个时候,常量指针和指向常量的指针二者是不一样的意思,辨识的方法在于从右往左读,示例如下:
        1. int val1 = 2;
        2. int *const ptr1 = &val; // 从右往左读,依次是 const > * > int,表示 prt1 是一个常量,指针类型,指向整数类型对象;
          1. ptr1 是一个常量,意味着它不可以被重新赋值和修改了;
        3. const int val2 = 4;
        4. const int *const ptr2 = &val2; // 从右往左读,依次是 const > * > int > const,表示ptr2 是一个常量,指针类型,指向整数类型的常量对象;
      10. 指针本身是常量,只是表示它所存的目标对象地址不改变,但不意味目标对象不可改变,只要目标对象是非常量,即可通过该指针改变目标对象;
        1. int dval = 2;
        2. int *const ptr = &dval;
        3. *ptr = 3; // 成立;
      11. 当指针本身是常量类型时,它声明时必须进行初始化;
      12. 普通类型的指针,不能指向常量变量,必须是指向常量类型的指针,才可以指向常量变量;
        1. const int dval = 2;
        2. const int *ptr = &dval; // 正确
        3. int *ptr = &davl; // 错误,非常量指针,不可指向常量对象
        4. int *const ptr = &dval; // 错误,原因同上;ptr 内层有 const ,表示本身是常量没有用,要外层的 const 才能表示它指向一个常量对象
      13. 指向常量的指针,不能赋值给普通指针;
        1. const int dval = 2;
        2. const int *ptr1 = &dval;
        3. int *ptr2 = ptr1; // 错误,ptr2 不是一个指向常量的指针,所以无法匹配 ptr1 指向的对象的类型;
      14. 外层 const 指针要求所指向的对象类型相同,或者除非类型能够转换,例如非常量能转成常量,反之则不行,即常量不可被非常量所指向;
      15. 常量表达式 const expression :在编译间即可获得不会改变的初始化常量值的表达式(有什么用?编译器可以进行检查,确保类型正确)
        1. 在 C11 的标准中,引入了 constexpr 类型来声明一个常量表达式,这样编译器会进行检查,确保类型正确;
        2. 仅在满足 constexpr 函数的情况下,才允许使用函数对常量表达式进行赋值;此类函数比较简单,可以编译期间获得返回值;
        3. const int *p 和 constexpr int *q 中,p 和 q 是两种类型,前者指其指向的整数类型是常量,后者指其本身是一个常量,指向的对象则是整数;
          1. 怎么感觉 constexpr int *q 和 int * const q 是一个意思?
    5. 处理类型
      1. 类型别名:通过使用类型别名,来让类型的名称更有意义,更容易阅读和理解;有两种声明别名的方式
        1. 传统方式,使用 typedef 关键字,示例: typedef double wages; // 此处使用 wages 来做为 double 的别名;
        2. 新标准,使用 using 关键字,示例:using wages = double;
        3. 当使用别名来定义复合类型时,在将其用到声明语句中的时候,记得不要使用简单的替代来理解,而应该将其当作一个独立小个体来理解;示例如下:
          1. typedef char *pstring; // pstring 是一个指向字符的指针;
          2. const pstring cstr = 0; // 由于 pstring 是一个指针,因此此处的 const 是用来修饰指针的,const pstring 是指一个本身为常量的指针,指向字符类型对象;
          3. const pstring *ps; // ps 是一个指针类型,它指向的对象的类型是 pstring 常量指针对象;
            1. 发现 const 是用来修饰它右侧出现的类型的;
      2. auto 类型说明符
        1. 貌似算是新标准引入了动态类型推导,示例:auto item = val1 + val2; // 此处将会根据 val1 和 val2 的相加结果来自动判断 item 所属的类型;
        2. 编译器在做 auto 类型推导时,并不一定会严格等同
          1. 它会忽略内层的 const,而只保留外层的 const ;
          2. 如果希望确保结果是内层 const 类型,则应另外添加 const 修饰符;
          3. 不能将非常量引用绑定到字面值;
            1. 错误示例:auto &h = 42;
            2. 正确示例:const auto &h = 42;
        3. 如果在一条语句中,声明多个变量,则以最左边第一个类型为准,如果类型不同,会引发错误,示例如下:
          1. int i = 42;
          2. auto k = i, &t = i; // 其中 k 是整型,t 是整型引用;
      3. decltype 类型说明符
        1. 使用场景:想要得到一个表达式的类型,用来给某个变量声明相应的类型,但不想用表达式的值来给变量初始化(编译器会分析出返回值的类型以得到想要的结果,且不用将返回值算出来)
        2. decltype 有点跟 auto 不同,如果表达式是一个变量,它会返回变量的类型,包括内层 const 和引用在内;
        3. 引用从来都做为其引用对象的替身出现,唯一的例外情况是在 decltype;
        4. decltype 中的表达式,如果是一个解引用,则结果为引用类型,示例如下:
          1. int i = 0, *p = &i;
          2. decltype(*p) 返回的是 int &,而非 int ;
        5. 当 decltype 中的表达式使用括号(双层括号)时,返回的结果永远是引用类型(当变量被加上括号时,就不再是变量了,而被当成了表达式处理);而当表达式是一个变量时,只有在变量是引用类型时,返回的结果才是引用类型;
        6. 赋值是会产生引用的一种表达式,因此 decltype( 赋值表达式)格式的结果是赋值表达式左值的类型;
          1. 赋值运算符一般返回引用;
    6. 自定义数据结构
      1. 不要忘了在类定义的后面加上分号,不然它后续跟着的其他内容,会被当成声明语句的一部分;
      2. C11 标准允许在定义时给类成员设置初始值,如果没有设置,编译器会自动初始化;
      3. 为了确保使用类的各个文件有一致的类定义,一般将类定义在一个单独的头文件中,给其他文件引用;(头文件一般用来包含那些只能被定义一次的实体,例如类,const,constexpr等);
      4. 某些通用的头文件可能会遇到被多次包含的情况,需要使用预处理器的预处理变量,进行适当处理,以便能够正常工作;
        1. #define 将某个变量设置为预处理变量;
        2. #ifdef 用来检查某个预处理变量是否已经定义;
        3. #ifndef 用来检查某个预处理变量未定义;
        4. #ifdef 或 #ifndef 如果判断条件为真,则会执行后续相应的操作,直到遇到 #endif 为止;
        5. 通过使用以上四个头文件保护符,来避免重复包含的发生;
      5. 为了避免与程序中的其他实体发生冲突,一般预处理变量名字全部使用大写以示区分;事实上预处理变量的名字不太重要,可以自定义,但为了显示出意义,一般将它命名成头文件的名称一致的写法,例如头文件名称为 Sales_data.h ,则预处理变量命名为 SALES_DATA_H
      6. 不管头文件是否已经包含在其他文件,都应该习惯性的加上头文件保护符
  3. 字符串、向量和数组
    1. 命名空间的 using 声明
      1. 字符串 string 和向量 vector,是数组类型(更基础)的某种抽象;
        1. 通过模板来实现
      2. 字符串支持可变长度,向量支持可变长的集合;
      3. 据说数组与硬件的实现直接相关,因此抽象程度不高,灵活性有一些不足;
      4. 使用 using namespace::name 来引入命名空间 namespace 中的某个成员 name,例如做了 using std::cin 的声明后,在代码中就可以直接使用 cin 了,而无须再加上 std:: 的前缀了;
        1. 还有一种引入方法叫 using 指示,它会引入命名空间的所有成员到当前作用域的父空间
    2. 标准库类型 string
      1. 定义和初始化 string 对象
        1. string 对象有好几种初始化的方式,使用哪种方式,是在类里面规定的,可用的方式包括:
          1. string s1; // 定义空字符串,默认初始化
          2. string s2 = s1; // 拷贝赋值运算符
          3. string s3(s1); // 拷贝构造
          4. string s4(“value”); // 构造初始化,形参字面值常量(const char*[ ])
          5. string s5 = “value”; // 拷贝赋值运算符重载,字面值常量版
          6. string s6(n, ‘c’); // 构造初始化,形参 (int, char)
        2. 拷贝初始化:使用等号赋值时,执行的是拷贝初始化;// 编译器有可能会自动优化转成直接初始化
        3. 直接初始化:如果不使用等号,则执行的是直接初始化;
      2. string 对象上的操作
        1. 类除了确定对象的初始化方式外,还用来定义对象上可以执行的操作,包括使用函数调用,或者也可以定义各种 =、<< 等符号在类对象上的新含义(运算符重载);
        2. 可以使用 IO 操作符(输入输出运算符) << 和 >> 来读写 string 对象;string 会忽略空白,会从第一个非空白开始读,并到下一个空白处结束;
        3. getline(is, s) 用来获取一整行,包括空白符;有读入换行符,但没有给 s ;
        4. s.empty( ) 用来判断当前字符串是否为空;
        5. s.size( ) 用来获取当前字符串的长度;
          1. size( ) 函数会返回 string::size_type 类型,它是一个无符号整数类型,因此当表达式中有 size_type 类型时,应避免使用 int 类型一起计算,因为编辑器会默认将 int 类型转换成 unsigned,如果 int 的是负数,则转换后的 unsigned 是一个很大的数,有可能出现意想不到的结果;
        6. ==,!= 用来判断两个字符串是否相等(对大小写敏感);
        7. <, <=, >, >= 用来判断两个字符串的字典顺序(对大小写敏感)
          • 可以用来串接字符串,就像 python 中的那样;
          1. 两个 string 对象,或者 string对象和字面值,都可以相加;(为了与 C 兼容,字符串字面值与 string 对象并不是同一种类型)
            1. 目测实现原理应该是重载了加号运算符,使其接收字符串字面值类型;
            2. 不过好奇 string 对象是否要求必须写在左边?
          2. 但是,两个字面值之间,需要确保至少有一个 string 对象;
            1. 貌似这里应该跟加号的结合顺序有关系?
      3. 处理 string 对象中的字符
        1. for (auto item_name: list_name) 语句可以用来迭代处理 list 中的每一个元素;
          1. 如果想改变 list 中的元素,则 declaration 应该使用引用类型,例如 for ( &c : str ) { … };
          2. 不知 auto 推断出的默认类型是否为常量引用?这样既能提高性能,又不会修改原值;
        2. 下标运算符返回的是引用,即可以通过下标来改变元素的值;除非字符串是常量,不可改变;
        3. 不管什么时候,使用下标访问字符串的元素值,都应该检查一下字符串是否为空;
          1. 原因:如果为空,下标访问会出现不可预知的后果;
          2. 貌似下标运算符的重载函数有设置自动检查?
        4. 使得下标必须严格检查其取值范围不会越界,一般通过下面两条实现
          1. 先声明下标为 decltype(s.size()) 类型(原理:该类型是 无符号整数,这样可以确保下标不会小于0)
          2. 再设置 index < s.size()(原理:可确保下标不会大于 size)
      4. int 转字符串,to_string(int)
    3. 标准库类型 vector
      1. 定义和初始化 vector 对象
        1. vector 表示对象的集合,也叫做容器;
        2. vector 是一种类模板,类模板不是类,类模板的实例化,会生成类;
        3. vector ivec 表示 ivec 是保存 int 类型对象的容器;
        4. 几种初始化 vector 对象的方式
          1. vector v1;
          2. vector v2(v1);
          3. vector v3 = v1;
          4. vector v4(n, val);
          5. vector v5(n);
          6. vector v6{a, b, c, …};
          7. vector v7 = {a, b, c, …};
        5. 圆括号表示使用构造的方式初始化,花括号表示使用列表的方式初始化;
          1. 确切的说,圆括号会调用构造函数;等号和花括号会调用运算符函数;
      2. 向 vector 对象中添加元素
        1. vector 先声明为空,再动态添加元素的性能更好,比一开始就声明长度的性能更好,这点和 C 、Java 不同;(问:背后的原理是什么,为什么性能会更好?)
        2. 可以使用 push_back 往集合中添加元素;
          1. 示例:vec.push_back(val);
      3. vector 的其他操作
        1. 对于 <, <=, >, >= ,只有 vector 的元素本身具备可比性时,例如 string 类型,这种比较才可行,否则会报错;
        2. 使用下标访问时,下标的类型应为 vector::size_type,而不是 vector::size_type,即记得写出 vector 的元素类型;
          1. 原因:vector 本身是一个模板,不是一种类型,需要提供类型参数给它,才能实例化出一种类型
        3. 下标运算符可以用于访问已经存在的元素,但不能用于添加还不存在的元素(此点跟其他语言不同,需要注意);
        4. 如果使用下标运算符访问不存在的元素,就会造成缓冲区溢出,带来安全问题;
        5. == 可以用来判断两个 vector 是否相等;
    4. 迭代器
      1. string 和 vector 可以通过下标访问元素,此外还可以通过迭代器来访问元素;虽然 string 不是容器类型,但它也支持容器相关的操作;vector 是容器类型,但它也支持下标访问;除了 vector 外,其他仅少数几种容器类型支持下标访问,但它们都支持迭代器访问;
      2. 迭代器实现的是容器内元素的间接访问,即访问返回的是一个引用,类似指针类型,而不是元素对象本身;因此,如果要对元素对象进行操作,需要使用解引用符星号 *;示例:(看到这里,终于明白 SW::JSON 库返回的原来是一个迭代器)
        1. ele = v.begin()
        2. *ele = toupper(*ele);
      3. for 循环中使用 != 进行判断,比使用 < 进行判断更好,原因在于,!= 可以支持标准库提供的所有容器类型上,而 < 则只支持数值类型;
      4. 迭代器支持的操作
        1. 使用 ++ 自增运算符来访问下一个元素;
        2. 使用 – 来访问上一个元素;
        3. iter1 == iter2 判断两个迭代器是否相等;如果两个迭代器指向同一个元素,或者都指向尾迭代器,则它们相等,否则它们不等;
        4. 通过 +n 和 -n 可以一次移动 n 个元素;
        5. 通过 += n 或 -= n 可以对某个元素进行加减 n 后赋值;
        6. <, <=, >, >= 四个关系运算符用来表示两个迭代器对象的前后位置关系;
        7. 减号可以用来计算两个迭代器对象的距离;(前提:它们指向同一个容器)
    5. 数组
      1. 定义和初始化内置数组
        1. 与 vector 的不同点在于,数组要求大小固定,不能往里面增加元素;由于大小固定,因此其优点是性能更好;缺点是无法增加元素,不能应对变化,使用上不灵活;
        2. string a[ n ] 此处通过 n 来设定数组的维度,它也是数组的元素数量的上限;
          1. n 可以是一个表达式,但必须是常量表达式;
          2. 可以通过列表来初始化数组,但列表中的元素数量应不大于维度值;也可以不写维度值,如果不写,则默认数组的维度即列表的元素数量;
          3. 如果数组的元素值没有初始化,编译器会默认对其进行初始化;
          4. 数组的元素必须是对象,不能是引用;
          5. 定义数组时,必须指定元素的类型,不允许使用 auto 来推断类型;
            1. 估计应该可以使用 decltype(expression) 来获得类型以供定义;
        3. 字符数组的特殊性在于,末尾需要有一个空字符,用来表示字符数组的结束(这一点估计是为了兼容 C 造成的);
          1. 当使用字符串字面值对字符数组进行初始化时,会自动添加一个空字符做为结束(很好奇,字符串字面值不是本来就自动带一个表示结束的空字符吗?如果是的话,就算不上是自动添加,而是原封不动的拷贝?)(猜测字符串值应该没有带结束符)
          2. 假设使用字符串字面值来初始化字符数组,则字符数组的长度是字符数量加1;
            1. char str[ ] = “abcdef”, sizeof(str) 的值是 7
              1. 注意:数组没有 size() 方法,只能通过 sizeof 来获取长度;
            2. string str = “abcdef”, str.size() 的值是6 而不是 7;
          3. 不允许用一个数组来给另外一个数组赋值(这点倒是跟 C 一样,估计也是需要使用 strcpy 或 memcpy 之类的),也不允许使用一个数组来初始化另一个数组;但可以使用字面值来初始化一个数组;
        4. 数组声明时,如果声明语句的部分有括号,则优先使用由内而外的理解,然后再是从右到左的阅读理解;
          1. int (*arr)[10] 表示 arr 是一个指针,指向数组,数组有10个元素,元素是 int 类型;
          2. int *(&arr)[10] 表示 arr 是一个引用,绑定一个数组,该数组有10元素,元素类型为指针,指针指向 int 类型;
        5. 如果在函数体内定义了某种内置数据类型的数组,如果没有初始化,编译器的默认初始化,会使得数组的元素含有未定义的值;应该避免这种用法,而是在函数内部定义数组时同时进行初始化;
          1. 时刻牢记初始化;
        6. 使用指针和数组定义字符串的区别
          1. const char *cp1 = “Hi”; // 此处定义了一个指针,并非数组;
            1. 注:需要 const,若没有则不合法;
          2. char cp2[ ] = {‘H’, ‘i}; // 此处的数组没有以空字符结束;长度为2;
          3. char cp3[ ] = “Hi”; // 此处的数组是有空字符结束;长度为3;
          4. 区别
            1. 共同点:编译器会将 “Hi” 存储在常量区
            2. 不同点:
              1. 指针:编译器在栈中创建一个指针对象,指向常量区的 “Hi” 的地址,不可更改;
              2. 数组:编译器从常量区复制一份 “Hi” 的拷贝,放在栈中,可以进行更改;
      2. 访问数组元素
        1. 数组支持下标访问和范围 for 访问;
        2. 使用下标访问时,一般将下标定义为 size_t 类型(机器相关的无符号整数),在头文件 cstddef 中定义(奇怪,好像没有引入这份头文件也可以使用 size_t 类型);
        3. 使用数组时,同 string 和 vector 一样,需要仔细检查下标是否越界的问题,因为它是缓冲区溢出的安全隐患;
        4. arr[index] 和 *(arr + index) 是同一个意思;二者都是返回引用;
      3. 指针和数组
        1. 数组的特征:使用数组名字的地方,编译器实际上在编译时,会将其替换为数组首元素的指针;
          1. 如何解释 sizeof(arr) 和 sizeof(ptr) 存在区别?答:数组和指针还是不同的,它们是两种类型; 数组在做为参数传递的时候,会隐式转换为指针; sizeof(ptr) 获取的是指针对象本身占用的内存空间,sizeof(arr) 获取的是整个数组占用的内存空间;
          2. 另外 数组和指针的表现,在 C++ 和 C 中也是有一些差别的; 例如
            1. char *p = “abc” 在 C 里面是可行的,但在 C++ 不可行,需要表示为 const char *p = “abc”;
            2. 原因:C++ 要求只有常量指针才能指向常量字面值;
        2. 在一些情况下,数组的操作实际上是指针的操作,因此它会带来一些预期外的结果,例如使用 auto 来推断新变量的类型时,如果参数是一个数组,实际得到的变量却会是数组首元素类型的指针,例如:
          1. int a1[ ] = {1, 2, 3};
          2. auto a2(a1); // 此处 a2 的类型被推断为 int * 整型指针;因为 a1 在编译时,被实际替换为 &a1[0];
        3. 使用 decltype 则可以避免 auto 的问题,例如
          1. decltype(a1) a2; // a2 得到的类型为整型数组,同 a1 一样;
        4. 指针也是迭代器,支持使用自增运算符来遍历数组,为了让循环能够终止,需要获取数组尾元素之后的地址,例如假设数组有10个元素,则尾元素之后的地址为 &arr[10];
          1. 但这种方法很容易出错,更好的做法是使用 C11引入的 begin 和 end 函数来获取数据的首尾指针地址;示例: int *pbeg = begin(arr), *pend = end(arr);
          2. 注意不要在尾后指针上面使用解引用或者自增操作,将会有溢出缓冲区的风险;
        5. 指针支持类似迭代器的运算,包括相减,自增,自减,与整数的加减,解引用等;
        6. 两个指针相减的结果是一个 ptrdiff_t 的类型;ptrdiff_t 也定义在 cstddef 头文件中;由于相减结果可能为负值,因此 ptrdiff_t 是一个有符号整数;
        7. p[-2] 可以用来表示 p 指针所指向位置的前两个位置的地址中的对象;
          1. 内置类型的下标可以使用负值,而标准库类型(如 string 和 vector)的下标运算只能使用无符号值,但可以使用减 2 来得到前两个位置的对象;
            1. 内置类型下标使用负值是用在什么场合呢?或许是用在当 p 指向某个中间元素的时候(估计要做好越界检查)
      4. C 风格字符串
        1. C 的 string 类型支持 strlen, strcpy, strcmp, strcat 等几个函数,这几个函数不会检查参数的合法性,因此常常需要额外的手工检查;
      5. 与旧代码的接口
        1. 允许使用 C 的字符串数组来初始化 string 对象,示例
          1. const char *s1 = “hello”; // 事实上这里定义的 s1 也不是数组,只是一个指针;
          2. string s2(s1);
        2. 允许使用 C 字符串出现在 string 对象的加减运算中,但字符串的两头至少连着一个 string 对象;
        3. 可以使用 c_str 函数来将 string 对象转换成 C 风格字符串;
        4. 数组的初始化赋值不能使用另一个数组,也不能使用 vector 对象;但是,vector 的初始化赋值可以使用数组(原因:数组由于需要考虑与 C 的兼容性,它是一种内置类型,本身并不具备一些高级的方法,而 vector 则是标准库类型,具备相应的高级方法)
          1. int arr[ ] = {1, 2, 3, 4, 5}; // 正确;
          2. int arr2 = arr; // 错误;不可用一个数组给另一个数组赋值;
          3. vector v(begin(arr), end(arr)); // 正确,vector 支持范围构造初始化;
        5. 如何可以,尽量使用迭代器,vector, string 等标准库类型,避免使用指针,数组,C 字符串等内置类型;
      6. 多维数组
        1. 严格意义上来说,C++ 中没有多维数组,而只有数组的数组;
        2. 可以使用 {0} 来初始化数组中的所有元素为0;【【01
        3. 多维数组的初始化可以使用多层花括号(更直观),也可以使用单层花括号(效果相同,但较不直观);
        4. 如果要使用范围 for 循环来遍历多维数组,则除了最内层的循环外,其他层定义的变量都应用定义成引用类型
          1. 原因:如果不是定义引用类型,由于变量是指向数组,编译器会默认自动把它转换成指针,这样就导致下层循环的定义非法
        5. 当在程序中使用多维数组的名字时,编译器也会将其自动转化为指针(原因:多维数组本质上只是一个数组);
        6. 对于 int (*p)[4] = a[2] 类型的声明,通过使用 auto 或 decltype,可以简化为 auto p = a[2];两种情况下,p 都是一个指向 4 维数组的指针;另外如果再引入 begin 和 end 来判断循环结束,就更加方便不易出错了,即 begin(p), end(p)
        7. 可以使用类型别名来简化多维数组的指针定义,示例:
          1. using int_array = int[4];
          2. typedef int int_array[4]; // 这个用法有点不直观;
          3. 以上两种形式都表示为定义一个 int_array 的新类型来代表 4维整数类型数组
          4. 经过实际安全的测试,发现使用别名真的是很方便,不然在没有用 auto 的情况下,定义数组的指针真是超麻烦,且容易弄错;
          5. 当然,使用 auto 是最简单的(记得除了最内层外,其他层必须定义成引用)
  4. 表达式
    1. 基础
      1. 基本概念
        1. 函数调用也可以看做是一种特殊的运算符,它对要作用的运算对象数量没有限制;
        2. 在表达式求值过程中,一般要求参与运算的对象的类型相一致,如果不一致,运算对象的类型经常会被转化;
        3. 小整数类型(char, bool, short)在自动转化时,经常会被提升成大整数类型 int;
        4. 通过类类型的重新定义,可以实现运算符的重载,让运算符实现想要的运算功能;但是重载无法改变运算符需要满足的个数要求、结合律和运算顺序等原始特性;
        5. 表达式有左值、右值两种类型,一个表达式,它要么是左值,要么是右值;(应该是指返回类型,貌似所有的表达式,都可以看做是某些函数的调用)
        6. 当一个对象被用做右值的时候,使用的是它的内容(即它的值);当被用做左值的时候,使用的是它的身份(即对象在内存中的位置);
        7. 在需要右值的地方,可以用左值替代;但在需要左值的地方,不能使用右值替代;
        8. 使用 decltype 时,表达式是左值还是右值,运算结果会有不同,假设 int *p;
          1. decltype(p) 得到的结果是 int &;(p 表达式返回的结果是一个左值引用)
          2. decltype(&p) 得到的结果是 int **; (&p 表达式返回的结果是 p 这个指针对象的地址)
      2. 优先级与结合律
        1. 不同运算符有不同的优先级,高优先级的运算符,其运算对象优先结合;同等优先级的运算符,其运算顺序则从左到右(左结合律);
        2. 括号可以无视所有运算符的优先级和结合律,括号具备最高结合优先级;
      3. 求值顺序
        1. 大多数情况下,没有明确的求值顺序规定;例如
          1. cout << i << “ “ << ++i << endl; // 此处 ++i 是在 i 输出给 cout 之前求值,还是之后求值,是未定义不可预料的;
          2. 为稳妥起见,应该先把需要的值都求好了,再做下一步的运算,没有必要为了节省几行代码,搞得这么模糊;简单易于理解,才是高质量代码的体现;
    2. 算术运算符
      1. 整数相除的结果还是整数,小数部分将会被丢弃;
      2. % 取余(取模)运算的两个对象必须都是整数类型;
      3. 乘号 *、除号 / 与取余 % 三者的优先级是相同的;
    3. 逻辑和关系运算符
      1. 在范围 for 循环中,声明常量引用有一个好处,即当列表很大时,声明常量引用类型,可以免去复制的开销,提高了效率;同时由于是常量引用,又不会改动原来的值,也很安全;
      2. 做比较运算时,例如 if ( a == b),除非比较的两个对象 a 和 b 都是布尔值,否则不要使用布尔字面值 true 和 false 进行比较,错误示例:if (a == true),正确的做法应该是 if (a);
        1. if (a) 的做法会将 a 转化成布尔值
        2. if (a == true) 的做法会将 true 转换成 a 的类型,可能会发生未定义的行为;
        3. 反思:事实上 a == true 更加的易于理解;对于 if(a),也是存在一定的隐患,因为编译器可能将所有非0的值都视为真,这有可能不是我们想要的结果;
      3. 运算的优先顺序
        1. !
        2. >, >=, <, <=
        3. !=, ==
        4. &&
        5. ||
    4. 赋值运算符
      1. 如果赋值运算符的右侧对象类型与左侧不同,则右侧对象会被自动转换为左侧类型;
      2. C11 支持使用花括号提供初始值列表进行初始化,如果左侧对象是内置类型,则花括号内只能有一个值,且该值所占空间不应大于左侧对象类型所占的空间;
        1. 原因分析:如果左侧对象与花括号内的值的类型不同,会触发类型转换;而一个占用更大空间的对象,转成占更小空间的类型时,显然很有可能会出错,无法正常拷贝初始化;除非有重载列表构造函数或许可以解决这个问题?但是,问题在于内置类型是无法重载的;
      3. 对于类类型来说,则取决于类类型自己如何定义赋值运算符,有可能会实现重载;
      4. 无论左侧是何种类型,花括号中的初始列表都可以为空,此时编译器会创建一个初始化的临时变量并赋值给左对象;
        1. 分析:估计编译器调用了用户定义的构造函数或者合成的构造函数;
      5. 赋值运算符是“右”结合律(注:与其他二元运算符常见的左结合律不同);
      6. 赋值运算符的优先级很低,在复合语句中使用的时候,记得加上括号,不然常常得不到想要的结果;
    5. 递增递减运算符
      1. 前置版本返回运算后的结果,后置版本返回运算前的结果;
      2. 正常情况下,建议优先使用前置版本,因为这样性能的开销更小,不需要为了返回修改前的值,额外存储;除非真的想使用它递增或递减之前的值,才使用后置版本;
      3. 后置递增符的优先级高于解引用符 *;(感觉最好加上括号,让所有人一目了然)
    6. 成员访问运算符
      1. 解引用符的运算优先级低于点运算符;
      2. 箭头运算符作用于指针对象;
    7. 条件运算符
      1. 条件运算符的优先级很低,在复合表达式中使用的时候,要注意加上括号,避免产生预料外的结果;
    8. 位运算符
      1. 位运算符适用于整数类型的运算(意思是其它类型不可用,除非将运算符重载);
      2. 移位运算符要求:右侧运算对象不可为负,且移动位数不可超过左侧对象的位数,否则会产生未定义的行为;
      3. 移位运算符的优先级低于算术运算符,但高于条件、赋值、关系等三种运算符;
    9. sizeof 运算符
      1. sizeof 返回一个表达式或者一个类型名称所占的字节数;
      2. sizeof 满足右结合律,且与解引用符 * 的运算优先级相同;
      3. sizeof 并不会真正去计算表达式的值,因此对于 sizeof(*p),如果 p 是一个无效指针也没有关系,因为并没有真正的执行解引用 *p,故不会产生未定义的行为,同时 sizeof 仍然能够知道 *p 的类型;
      4. sizeof 作用于数组时,会得到整个数组所占空间的大小,相当于对数组的每个元素执行一次 sizeof,并将结果汇总起来;
        1. 所以如果想得到数组的元素数量,还需要除以单个元素所占的空间大小;
      5. 对 string 和 vector 类型,sizeof 只会返回固定部分的大小,不会返回元素占用的空间大小;(原因:string 和 vector 不是内置类型,而是标准库类型)
      6. 由于 sizeof 返回的结果是一个常量表达式,因此可以用 sizeof 的结果来声明一个数组的大小,例如:int[sizeof(arr)];
      7. 对于 int x[10]; int *p = x; p 指向 x 的首地址,因此 *p 获取的是 x 的首个元素;sizeof(*p)获取的是 x 首个元素的大小,此处应为4;
    10. 逗号运算符
      1. 逗号运算符是有求值顺序的,会先求左侧表达式的值,然后再求右侧表达式的值;
      2. 在所有的运算符中,逗号运算符的运算优先级是最低的;
        1. 示例: someValue ? ++x, ++y : –x, –y; // 在这段代码中,由于逗号运算符优先级最低,因此冒号右侧的逗号的右侧内容不会视为条件表达式的一部分;
    11. 类型转换
      1. 如果两种数据类型可以相互转换,则表示它们是关联的;关联的数据类型,在进行计算的时候,有可能会发生隐式转换;转换一般遵循最大程度减少精度损失,即转换成最宽的类型;
      2. 单个字符其实是一个整数类型;
      3. 由于 int 和 float 位数相同,因此它们的算术运算可能会导致提升成 double;
      4. 在大多数用到数组的表达式中,数组会被隐式转换成指向首元素的指针;但在 decltype 参数、sizeof 参数,& 取地址符、typeid 中,这种转换不会发生;
      5. 字面值 nullptr 和常量整数 0 可以转换成任意类型的指针;
      6. 可以定义常量的引用或指针,指向一个非常量对象;但不可以定义一个非常量的指针或引用,指向一个常量;原因:此处发生隐式类型转换,而规则是 not-const 可以转成 const,但反之不行;
      7. 类类型可以自定义转换规则,例如 while(cin >> s),此处 cin >> s 实际上返回的是一个 istream 对象,但在类类型中,规定了其可以转换成布尔类型(当作为条件表达式的时候);
      8. 命名的显式类型转换:cast 强制类型转换,这种操作一般有点危险,相当于直接告诉编译器,请忽略精度损失;格式
        1. 格式:static_cast(expression),其中 type 为要转换的目标类型, expression 为要转换的值;
        2. static_cast 一般用来将大精度转小精度;示例:
          1. int i, j;
          2. static_cast (j) / i; // 将 j 转成 double 精度(原因:两个整数的默认结果也会是整数,所以此处使用强制类型转换为浮点数)
        3. const_cast 用来将常量强制转成非常量;const_cast 只能用来去除 const,不能用来转变类型,比如 char 转成 int 等;
        4. reinterpret_cast 可以在位模式级别,对数据按新类型进行解释,这种操作非常危险,强力避免;
        5. 以上三种是新式的强制类型转换,有命名转换类型,旧式的强制类型转换的语法为 type(exp) 或者 (type) exp,相对于新式,旧式类型转换没有命名,混合了三种转换的特点,语意更加不清晰,应极力避免使用;
  5. 语句
    1. 简单语句
      1. 可以使用空语句来表示啥也不用干,但记得加上注释,不然后续看到的人会以为这里可能写错了;
      2. 复合语句也叫做块,块不以分号作为结束;如果加了分号,反而会被编译器误以为是一条空语句;
      3. 空块的作用等于空语句(不过建议也是要写上注释为妥);
    2. 语句作用域
      1. 在块中定义的变量,其作用域仅限于在块中使用;出了块就不行了;
    3. 条件语句
      1. 严格使用花括号,不要省略,因为它会增加易读性,而且也更加不容易出错;
      2. 即使 else 分支不做任何事情,也写一条空语句,表示该分支的情况已经考虑过了;当然,再得同时写一条注释,避免他人误以为此处写错
        1. 更好的办法是定义一个类型 pass 的函数;
      3. 在 switch 结构中,case 标签必须是一个整形常量表达式;
      4. 通常每个 case 后面要有一个 break,如果出现需要合并几个 case 的情况,可以将多个 case 写成一行,这样可以更容易一眼看出它们是一种情况;
      5. 即使最后一个case,也要养成在其后面写上 break 的习惯,这样即使将来增加新 case 也不容易出现漏写 case 的情况;
      6. 即使没有 default 的情况,最好也把它加上,表示已经考虑过了。更好的做法是可以在里面写入报错语句,这样表示结果没有落入 case 规定的范围;
    4. 迭代语句
      1. 定义在 while 条件部分,或者 while 循环体内的变量,每次循环体的内容都经历从创建到销毁的过程;
      2. 使用 while 循环的两种场景
        1. 不知道循环次数;
        2. 想要在循环结束的时候,继续使用循环控制变量;
      3. for 循环的头语句中,初始化只能使用一条语句(因为只允许出现一次分号),因此如果要初始化多个变量(使用逗号运算符), 这些变量需要具备相同的类型,才可以使用同一个声明语句;
      4. for 循环的头语句中,init, condition, express 三者都可以为空,但为空的时候,为避免无限循环,需要在循环体的其他地方进行作用补偿;
      5. 对于 do-while 循环,由于条件判断前会先执行一次循环体代码,因此不可将变量声明放在条件判断语句中,不然在第一次循环的时候,用不到这个变量(因为它还未声明)
    5. 跳转语句
      1. break 语句可以用来跳出最近的 for, do-while, while, switch;
      2. continue 只能用于 for, do-while, while,不能用于 switch;
      3. goto 可以无条件的从定义的位置,跳转到函数内部其他指定的位置(尽量避免使用 goto);
    6. try 语句块和异常处理
      1. 通过 throw 抛出一个异常,抛出类型为 runtime_error,它同时也是一个函数,在 stdexcept 头文件中定义,这个函数接收一个字符串做为参数(一般用来写错误内容);之后在 catch(runtime_error err) 中可以捕获这个runtime_error 类型的 err 对象,对象有一个 what 的方法,可以用来获得抛出错误时传递的字符串参数;
      2. try…catch 可能存在嵌套行为,throw 抛出错误后,一般的规则是从内到外,逐级向上查找对应的 catch 语句,如果最后都没有找到,则会统一交给标准库的 terminate 函数进行处理,该函数一般会导致程序中止退出;
      3. 有4个头文件定义了异常类,分别是 exception, bad_alloc, bad_cast, stdexcept,其中头3个文件的异常类只能使用默认初始化,不能手工初始化赋值;最后一个 stdexception则相反,需要手工初始赋值,不允许使用默认初始化;
  6. 函数
    1. 函数基础
      1. 函数四要素:返回类型、函数名、形参列表、函数体;
      2. 函数调用过程分两步,其中第一步是用实参初始化形参(以前一直没有注意到这个问题,据说此处的初始化是隐式的,初始化同样遵循变量初始化的规则),第二步是主调函数中止,将控制权交给被调函数;
      3. return 语句的动作也分两步,其中第一步是返回结果,第二步是将控制权交还给主调函数;
      4. 函数的每个形参都需要显式的声明类型;
      5. C++ 中函数能够返回绝大多数类型,但不能返回数组和函数,不过可以返回数组或函数的指针来间接达到目的(意味着需要提前声明好函数的返回类型);另外,由于函数中的局部变量会在函数退出后销毁,因此需要用到两种办法,一个分配一个堆内存避免销毁(但后续需要手工释放);另一个方法是由主调函数提前声明并传入指针,这样会由主调函数结束后自动释放内存);
      6. 在 C++ 中,名字有作用域,对象有生命周期(目测大多数语言都是这样的);
      7. 自动对象:生命周期仅限于执行期间的对象;当执行结束后,自动对象会被销毁;
      8. 函数体内的局部变量是一定会被初始化的,因此如果没有显式的进行初始化,编译器会启用默认初始化,但默认的方式有可能产生未定义的行为(一定会被初始化,猜测可能是因为要分配内存的原因);
      9. 使用 static 可以创建局部静态变量,这种变量的特点是生命周期很长,即使在函数退出后,仍然存在,它会贯穿整个程序的生命周期,直到程序中止时才销毁;(感觉像一个全局变量了,不过目测其访问权限是受的限制的)
      10. 函数使用之前需要先声明,可以多次声明,但只可以定义一次;函数声明据说被叫做函数原型(不知为啥);
      11. 函数的声明建议放在头文件中,定义则放在源文件中,这样有两个好处
        1. 函数可能会被多个源文件引用,如果将来要修改,只需要在被引用的头文件中修改一次即可;
        2. 编译器会根据声明,检查与定义是否匹配,避免犯错;
        3. 注:不是很理解为什么不把定义也放在头文件中,这样不是可以确保所有的定义都一致?经过查询,原来是这样的,声明用一个文件,定义用一个文件,调用用一个文件,总共分放在三个文件;(不过,还是感觉声明和定义可以在同一个文件不是更好吗?)
          1. 今天终于有点明白为什么声明需要单独一个文件了,因为有些类或函数之间可能是相互调用的,所以需要将它们都提前声明好,仅通过定义,由于顺序问题,编译器会提示找不到;
          2. 这个问题也适用于类的函数成员,假设类 A 中的一个函数成员使用到另外一个暂未定义的类B时,此函数成员只能先声明,不能定义;然后等类B声明结束后,再在那个类B的后面,开始对类A 的函数成员进行定义;(这种声明和定义分离的情况,真是令人讨厌,它增加了代码阅读的负担,有没有好的解决办法?)
    2. 参数传递
      1. 形参的初始化,跟变量的初始化过程是一样的;
      2. 如果形参是引用类型,则形参只是实参的别名,此时是引用传递;如果形参不是引用类型,则形参是实参的一份拷贝,此时是值传递;
      3. 如果形参是指针类型,由于指针实际上也是一个对象,因此它也是实参指针的一份拷贝,是值传递,只是它和实参指针都指向同一个对象;
      4. 在 C 语言中,使用指针形参改变主调函数的实参变量的做法很常见,在 C++ 中,则建议使用引用类型的形参,来达到相同的目的;
      5. 当使用引用形参时,可以直接传递实参对象,而非实参对象的地址(指针才需要传递地址);
      6. 使用引用形参的一个好处是可以避免拷贝,尤其是当实参很大或者不可拷贝时(例如 io 类型对象);
      7. 当函数无须改变实参时,函数的引用形参最好定义成常量引用;
      8. 在 C++ 中,函数只能返回一个值,如果要返回多个值,只能将多个值封装成对象的方式返回;当然,还可以通过增加一个引用形参,绕过返回限制,达到相同的目的;
        1. 貌似使用引用形参的话,相当于将对象定义在调用者那里了;
      9. 当实参是右值时,形参无法定义成引用类型;(不知是否可以定义成右值引用,而非常规的左值引用?)
      10. 形参有外层 const 的情况下,传递给它的实参是 const 或者非 const 都是可以的;但是,这也意味着 int func(const int i) 和 int func(int i) 算是同一个函数,因为对于调用方来说,这两种函数没有区别,这在函数重载的规则下面会报错;(感觉这里有个陷阱,当实参是 const 类型时,如果形参是非常量引用(例如 int i),应该会报错)
      11. 使用普通引用,而不是常量引用,会极大的限制函数可以接受的参数类型,同时会给主调者一个误导,认为引用可以改变实参变量的值;在确保不需要改变实参变量值的情况下,应尽量将引用定义成常量引用类型;而且当主调者传入字面值实参时,很容易引发错误,例如 find_char(“hello world”, ‘o’, ctr);(因此应该尽可能定义形参为常量引用)
      12. vector类型迭代器的声明 vector::iterator,如果 vector 已经声明的情况,也可以考虑使用 auto iter = vec.begin() ;
      13. 函数声明时,形参可以只有类型,没有名称,例如 double calc ( double ); int count(const string &, char); 不过还是写上名称更好,因为名称可以传递出更多的信息,让使用者对函数的用途有更加直观的理解;(少写几个单词节省的时间,在易于理解的损失面前,微不足道)
      14. 数组不能拷贝,因此也导致了函数无法实现数组的值传递,以数组名称为实参调用函数时,数组会转换成指向数组首元素的指针;
      15. 由于数组传递的是指针,因此有三种常用的技术对数组长度进行管理:
        1. 传递数组指针的同时,传递数组的长度,例如 void print(const int ia[ ], size_t size)
        2. 借鉴标准库的做法,传递首尾元素的指针,例如 void print(const int *begin, const int *end);(感觉貌似这个做法最值得推荐)
        3. 根据数组特性标记数组结束(此种方法只适用于有明显结束标记的数组,例如字符串),示例:void print(const char *cp) { if (cp) while (*cp) … }
      16. 如果不需要对实参进行写操作,则形参应该定义成常量指针或常量引用,示例 void print(const int *ip);
      17. 形参可以定义成数组的引用,但此时要特别的注意形参的写法,示例:void print(int (&arr)[10])),此处 (&arr) 的括号是必不可少的,它表示定义的是一个数组引用的类型,即顺序为:是一个引用,引用的是一个有10个 元素的数组,元素类型为 int,如果写成 &arr[10],由于[ ] 的高运算优先级,arr 先与[ ]进行结合变成了表示引用的数组类型,顺序为:定义了一个10个元素的数组,数组的元素为 int& 整数引用类型;
      18. 通过将形参设为引用类型,可以达到修改传递和修改数组的目的,但是缺点是限制了数组的固定元素个数,长度不可改,使用起来很不灵活(据说后面16章会有定义可变长度的引用类型形参的办法);
      19. C11 中实现可变形参的函数有三种办法
        1. 使用标准库类型 initializer_list,这种类型实际上是一种数组,因此它有一个前提:即所有实参的类型需要相同;
          1. initializer_list和 vector 一样,是一种模板类型,因此用它来定义变量时,需要注明模板内的元素的类型;
          2. initializer_list对象内的元素永远是常量值,无法被改变;
          3. 当使用 initializer_list进行参数传递时,如果需要传递多个实参,则这些实参应该使用花括号包起来,就像常规的数组一样;
          4. initializer_list可以使用范围 for 进行遍历(原因:它拥有 begin 和 end 方法);
        2. 使用 C 语言风格的省略符;
          1. 示例 void print(…),或者 void print(int x, …);
          2. 仅将省略符形参法应用于与 C 语言交互的代码中,因为大多数标准库类型的实参无法通过省略符被正确拷贝;
        3. 使用函数模板;
          1. template <typename… Args> void g(Args … args)
      20. initializer_list 支持的操作
        1. initializer_list lst,初始化一个元素类型为 T 的 initializer_list;
        2. initializer_list lst{a, b, c, … },用列表初始化一个 initializer_list,元素都是 const 类型,不可改变;
        3. lst2(lst),lst2 = lst;用一个initializer_list 去初始化另外一个 initializer_list,它们的元素不会发生拷贝,而是共享;
        4. lst.size(),返回元素数量;
        5. lst.begin(),返回 lst 的首元素指针;
        6. lst.end(),返回 lst 的尾元素指针;
    3. 返回类型和 return 语句
      1. return 有两种格式
        1. return; // 此种格式仅适用于返回类型为 void 的函数;
        2. return expression;
      2. 如果定义函数的返回类型为引用,则最后 return 的时候,虽然写着 return obj,但其实会返回引用,而不会r返回对象的拷贝;
      3. 避免返回局部对象的引用或者指针,而应该返回拷贝;
      4. 如果一个函数类型是引用,则返回结果是左值;如果不是引用,则返回结果为右值;
      5. 可以为返回结果是非常量引用的函数的返回结果赋值,例如
        1. char &get(string s, int pos);
        2. get(s, pos) = ‘A’
      6. 可以使用列表来返回多个值,类似 return {“abc”, “def”, “ghi”};
        1. 如果定义函数的返回类型是内置类型,则列表内最多只能一个元素,原因:需要函数的返回结果需要用来对同样是内置类型的临时变量进行初始化;
        2. 如果定义函数的返回类型是类类型,则列表内的元素个数由类自定义;
      7. 除了 void 函数外,所有其他类型的函数都需要返回一个值,main 函数除外,因为 main 函数如果没有显式的 return 值,编译器会自动添加;
      8. 通常情况下,main 函数返回 0 表示成功,但失败的返回值则视机器而定,为了解决这个问题,通过引入 cstdlib 头文件,使用其中的预定义宏 EXIT_FAILURE 和 EXIT_SUCCESS 来分别表示成功和失败;
      9. 定义函数返回数组指针的方法
        1. 使用类型别名,例如 typedef int arrInt[10] 或者 using arrInt = int[10],此处 arrInt 表示一个含有10个整数的数组,然后使用方法为 arrInt *func(int t);
        2. 原始声明,例如 int (*func(int t))[10]
        3. 尾置返回类型,示例 auto func(int t) -> int(*)[10] // 注:个人觉得这种类型可以优先使用;
        4. 使用 decltype,示例
          1. int arr[ ] = {1, 2, 3, 4, 5}
          2. decltype(arr) *func(int t) //注意此处的星号别漏了;
    4. 函数重载
      1. 定义函数重载时,不允许两个函数除了返回类型不同外,其他部分都相同(即形参需要不同类型或不同个数);
      2. 如果多个重载函数之间的区分不明显,例如虽然形参类型不同,但可以转换,则容易引用编译器错误;
    5. 特殊用途语言特性
      1. 默认实参
        1. 写法 string print(int ht = 24, int wid = 120, char bakg = ‘ ‘)
        2. 一旦某个形参被赋予了默认初始值,则其后(即右侧)的其他形参也需要赋予默认值;
        3. 设计默认实参函数时,安排形参顺序很重要,经常没有默认值的在左边,有常用默认值的在右边;
        4. 当函数被多次声明时,后面的声明不能修改前面声明的形参默认值,但可以添加形参默认值;
          1. 添加形参默认值,有两种可能
            1. 一种是这个形参已存在,但没有默认值,现在加一个;
            2. 一种是这个形参不存在,现在增加一个形参,并赋予默认值;(感觉不太可能是这种情况,因为形参数量变化,意味着要重载了,估计函数匹配不上)
      2. 内联函数
        1. 内联函数可以避免函数调用的开销,但是函数调用的开销才有多大一点点呢?或许大多数情况下使用内联函数完全没有必要;(正确的做法是将代码写得尽量清晰易懂,是否内联,交由编译器自行优化)
        2. 开销一般由三部分构成
          1. 当前数据保存寄存器,以便返回时可以恢复;
          2. 根据需要拷贝实参;
          3. 跳转到新位置,执行新代码;
      3. constexpr 函数
        1. 定义:能用于常量表达式的函数(有什么用呢?编译的时候,会被替换为字面值,加快执行速度)(暗含的意思是 constexpr 函数会当作内联函数处理?)
        2. 要求:形参和返回结果都必须是字面值类型,且有且仅能有一个 return ;
        3. constexpr 函数不一定返回常量表达式;
      4. 调试帮助
        1. C++ 可以允许在程序中编写调试代码,然后在发布时自动去除调试代码;通过 assert 和 NDEBUG 预处理宏来实现;
          1. 采用自动化测试的方式,是否更好?即代码中不必添加一些调试代码,增加代码的可阅读性;
        2. assert( expr ) 如果表达式为真,则 assert 啥也不做,如果为假,则报错并终止程序;
        3. assert 定义在 cassert 的头文件中;
        4. NDEBUG 表示 no debug,即不需调试的意思,如果定义了 NDEBUG,则 assert 啥也不做;如果没有,则 assert 会检查错误;
    6. 函数匹配
      1. 实参类型与形参类型越接近,其匹配概率越高;
      2. 如果有多个可行函数,编译器比较不出来应该调用谁,会报告二义性的错误;
      3. 重载会根据 const 进行优先匹配;
    7. 函数指针
      1. 函数指针指向的是一个函数而非一个对象,函数也是一种特定的类型;函数这种类型的特征是由其返回类型+形参类型共同决定的,跟函数名无关;
        1. 例如:对于 函数 bool compare(const string &, const string &),该函数的类型为 bool(const string&, const string&)
      2. 当函数名被作为一个值使用时,会被自动转换为指针,示例:
        1. void compare(int t);
        2. void (*pf)(int t); // 声明一个函数指针类型的变量;
        3. pf = compare;当然也可以是 pf = &compare,但 & 不是必须的;
          1. 函数的定义可以写在任意一级作用域,但是此处的 pf = compare 却不能写在顶级作用域,发现会报错“ps does not name a type”,当将它写在某个函数例如 main 里面时才可行;
          2. 猜测原因:void (*pf)(int t) 是一个声明,而 pf = compare 如果在同级作用域,相当于要重新声明(即覆盖前面的声明);但在不同的作用域中的时候,相当于绑定对象,而不是重新声明;
      3. 通过将函数名替换为 (*p) 可以快速的定义一个函数指针;
      4. 可以通过直接使用函数指针来调用函数,无须使用星号 * 进行解引用;(当然,要使用星号解引用也可以)
        1. 示例: pf(5)
        2. 看上去貌似调用运算符(圆括号)是可以直接在指针类型上发生作用的(猜测指针类型莫非设计有调用运算符函数?)
      5. 当形参为一个函数时,它会被自动转换成函数指针类型,就像数组一样;
        1. 但是,定义函数的返回类型时,它不会自动将函数转换成指针类型,需要显式声明;
          1. 是否为 return &compare?
          2. 如果定义让一个函数的返回类型是另外一个函数的指针?
          3. 同时,貌似意味着另外一个函数不可是函数内部创建的局部变量,不然返回的指针无效;
      6. 可以使用自定义别名或 decltype 来简化函数类型定义
        1. using F = int(int*, int);
        2. using PF = int()(int, int);
        3. PF f1(int) 正确
        4. F f1(int) 错误,函数的返回类型不能是函数,需要是函数的指针;
        5. F *f1(int) 正确,显式声明返回类型是函数指针;
      7. 另外还可以使用尾置类型来声明函数的返回类型
        1. auto f(int) -> int()(int, int)
      8. 如果能够提前明确的知道函数返回的是某个函数A 类型,则可以使用 decltype 声明,示例
        1. string sumLength(const string &, const string &)
        2. decltype(sumLength) *get_func(const string &); // 注意:不要把 get_func 左侧的星号给漏了;它表示返回的类型是函数的指针,而不是函数类型本身;
    1. 定义抽象数据类型
      1. 类的成员函数声明在内部,但定义可以在外部,也可以在内部;
        1. 问:什么时候适合定义在内部,什么时候在外部?
        2. 答:看到类的设计的章节后,终于明白为什么要定义在外部了,当类A的某个成员函数返回的类型是另外一个类B时,那么这个类B的定义要在成员函数之前,不然成员函数返回的时候,是找不到类型B的;但是,有可能这个类B又引用了类A,那么导致了类B 只能只类A 之前声明,但不能在类 A 之前定义,应该在类A 之后定义,因为在之前定义的话,类A还没有声明,无法引用类A;因此,此时需要将类A 中用的类B 的那个成员函数提取出来,放在类 B 的定义之后进行定义;
          1. C++ 的这种规则真的是有点操蛋;这个问题在动态语言 Javascipt 或 Scheme 为什么不会存在?是否跟它们在语义分析时,单独分析函数有关?
      2. 类的非成员接口函数的声明和定义都在类的外部(所以叫做非成员函数)(按照这个说法,所有以类为参数的函数,或许都可以叫做类的接口函数);
      3. 据说定义在类内部的函数是隐式的内联函数
        1. 不是很明白这句话的意思,以及它可能产生的后果
        2. 后来发现它的意思是,类的成员函数会被自动 inline ;
        3. 可是 inline 貌似会产生作用域的问题,这个问题是否会带来什么影响?猜测可能也不会,因为成员函数还是函数,它还是会单独开栈的,仍然有自己的独立作用域;
      4. 当我们访问类对象的成员函数时,python 通过引入 self 参数来访问当前对象,而 C++ 则通过引入一个隐式的 this 参数来实现(在定义成员函数的时候,没有显示出来),示例
        1. Sales_data total;
        2. total.isbn( ) 实际在执行的时候,会被编译器转化成 Sales_data::isbn(&total);
        3. 因此,在类中任何自定义为 this 的参数或者变量都会引起冲突,是非法的;
        4. 更有趣的一点是,在 py 中,当访问成员数据时,需要显式的指定 self.var 来访问,但 C++ 则可以不用,直接访问 var 就可以,不需要 this->var;
      5. 由于 this 是隐式的,且还是非常量引用,因此会带来一个麻烦的问题,即当 this 下的某个成员是常量时,由于非常量无法指向常量,会导致无法通过 this 访问它的常量成员,此时需要通过引入 const 关键字,对 this 参数进行转换,但 this 是隐式,导致 const 无法放在常规应该出现的位置,最后 C++ 使用了一个非常规的办法,将 const 放在参数列表后面(看上去像是一种 workaround)(又一个操蛋的设计);
        1. 示例 string isbn() const { // };
        2. 像这种访问常量成员的函数,称为常量成员函数;
      6. 编译器对类的编译分成两步,第一步先编译类的所有数据成员,第二步再编译所有的函数成员(如有),因此,这样使得数据成员的声明的位置可以在函数成员后面,而不出报错;
      7. 如果成员函数在内外部定义,则定义的时候,需要写上类名;
      8. 有些函数属于类的接口的组成部分,但并不属于类的一部分,此时应该将它们定义在类的外部;而且最好把它们与类放在同一个文件中,这样未来也方便查找和阅读;(类的接口函数,不属于类的一部分?思考了一下,感觉这个接口函数的定义有些牵强,貌似所以以类为参数的函数,或许都可以叫做接口函数?以前的理解是类的 public 成员函数才是类的接口函数,用来供他人对类的数据里进行访问用的);
      9. 构造函数
        1. 构造函数的名字和类名相同;构造函数没有返回类型,且不能被声明成 const 类型,因为构造的过程即是修改或初始化类的数据成员的过程;
        2. 类里面可以定义多个构造函数,就像重载函数一样,但每个构造函数的参数必须有所区别;
        3. 如果没有定义构造函数,编译器会自动合成一个默认的构造函数
          1. 不需要任何实参,有可能出错,取决于数据成员类型;
          2. 原则:如非必要,尽量不要去尝试使用自动合成的默认构造函数,除非类非常非常简单;
          3. 如果定义了哪怕只一个的构造函数,就永远不会再合成默认的构造函数;
            1. 一旦写了一个构造函数之后,如果仍然需要编译器帮忙合成一个默认构造函数,那么一定要显式声明出来
            2. 示例: Class_name( ) = default;
          4. 规则:如果存在类内初始值,默认构造函数会使用类内初始值;如果没有,会执行默认初始化(有可能出错);
        4. 可以在类的外部定义构造函数,示例
          1. Sales_data::Sales_data(isream &is);
          2. 说实话,不太明白为什么要搞到外部来;答:有可能这个构造函数中,会用到其他类,所以定义要放到外部,并且要放到其他类声明的后面;
        5. 构造函数有一个所谓的“构造函数初始值列表”的东西,用来对数据成员进行初始化,示例
          1. Sales_data(const istream &is, unsigned n, double p): bookNo(s), units_sold(n), revenue(n*p) { }
            1. 为什么类外部的初始化函数定义,是使用冒号呢?而其他成员函数的定义却不是?答:冒号是用来给数据成员赋初始值用的;仅在构造函数中使用,成员函数中不可用;
          2. 没有出现在初始值列表中的成员,一般将通过类内初始值进行初始化,或者执行默认初始化;
          3. 不太明白为什么不放在函数体中?答:后来发现C++ 的机制是先初始化,再执行函数体,因此不能放在函数体内,因为放在函数体内,其实已经是在默认初始化完成的情况下,再进行赋值的动作,已经晚了;
      10. 类的拷贝、赋值和析构:类对象不可避免需要有拷贝、赋值和析构的动作,幸好编译器会自动帮忙处理这部分的工作,但在某些情况下编译器会无能为力(例如需要管理动态分配的内存的时候)
        1. 正常情况下可以使用编译器自动合成的拷贝、赋值和析构函数;
        2. 少数情况下需要自定义特殊处理方法;
      11. 类的声明记得结尾需要加上分号;
    2. 访问控制与封装
      1. 通过访问说明符来限制类的成员的可见性,public 表示整个程序可见,private 表示仅内部成员可见;
      2. 访问说明符可以多次出现和使用,其有效范围截止到下一个访问说明符或者类结束;
      3. 可以用 struct 和 class 的任意一个来定义类,唯一的区别在于第一个访问说明符出现之前的成员的访问权限不同,struct 是默认可见,class 是默认不可见;
      4. 当类的数据成员是 private 时,可以通过声明友元函数,允许这些函数获得权限访问类的数据成员
        1. 虽然语法支持这么做,但个人不倾向这么做,应该尽量避免使用友元,因为它会扩大错误产生的范围,更好的做法是可以多定义一些 public 成员函数,用来获取所需要的数据;
    3. 类的其他特性
      1. 可以在类中定义类型别名,而且这个类名也一样可以设置 public 或者 private 的权限(有啥用?);不过类型别名需要先定义后才能使用,这点和普通成员不同;因此类型别名一般出现在开始的地方;
      2. 定义在类内部的成员函数是会自动 inline 的(隐式内联);
      3. 成员函数也可以被重载,匹配规则跟普通函数一样
        1. 不知为何,个人不太喜欢重载这种特性,增加了复杂度,还是要写那么多个函数,而好处却仅仅是减少了命名负担);
        2. 后来发现,对于静态类型的语言,重载貌似还是需要的,因为针对每种数据类型,单独声明多个函数来表示相同的处理过程,工作量还是挺大的;
      4. 有些数据成员可以被声明成可变的,这样即使 const 成员函数,也可以改变这个数据成员的值,一旦这个数据成员声明为 mutable,则这永远都不会再是 const;
      5. 当提供一个类内初始值,只允许有两种方式,一种是使用赋值运算符 =,一种是使用花括号的直接初始化方式;示例如下:
        1. int member1 = 5; // 编译的时候,这种拷贝赋值的方式有可能会被编译器优化成直接初始化的方式
        2. vector screens{“abc”};
      6. 通过定义返回类型为引用,并返回 *this 可以实现对对象内容的改变,如果返回的内容不是引用而是对象,则返回的是一个副本,改变内容的操作是发生在副本上面,而不是发生在原对象上面;
      7. 一个 const 成员函数如果返回 *this,那么它返回的是一个常量引用,接下来我们将无法在这个引用上面做修改的操作;
      8. C++ 支持基于 const 的重载:通过限定对象本身是否为 const 类型,然后配合两个版本的函数进行重载判断,选取合适的那个函数来执行(真是有够复杂的啊);示例如下:
        1. string& front();
        2. const string& front() const;
      9. 一个类代表一种新的数据类型,即使存在另外一个类,其所有成员与当前类完全一样,它们也算是两个不同的类;
      10. 假设定义了某个类 Sales_data,那么 Sales_data item 与 class Sales_data item 二者是等价的;
      11. 仅声明类而不定义类,称为前向声明,这种状况用得很少;(是用在什么场景下呢?)
      12. 当一个类被声明后,允许在定义成员的时候,成员指向当前类类型;(貌似链表结构就是这么用的嘛)
      13. 可以用 friend class 来声明友元类,友元类可以访问私有成员;每个类负责控制自己的友元类和友元函数;
      14. 可以将其他类的某个成员函数声明为友元函数,这样当前的权限只对其他类的那个成员函数开放,对其他类的其他成员函数不开放;
      15. 如果有多个重载函数,则在声明友元时,需要逐个声明,否则未声明的不可用;
      16. 注意,友元的声明只是用来控制访问权限,它并不等同于真正的函数声明。因此,相应的友元函数仍然需要在合适的位置进行声明;
        1. 后来发现,当函数以某个类的参数时,它的作用域会增加类所属的作用域,如果在类中有声明某个友元函数,会导致这个友元函数可以被找到;
      17. 在声明其他类的某个成员函数为友元函数时,有一个前提,该友元函数在其他类中需要是 public 成员,如果是 private 成员,则无法声明成功;
        1. 为什么要设置这样的限制呢?答:可能是因为当前类需要对这个成员函数有访问权限?如果是 private 就访问不到了?
    4. 类的作用域
      1. 类有自己的作用域,在类的作用域之外使用类,需要加上类名做为前缀,显式指定类的作用域;
      2. 在类的外部定义一个新的成员函数时,如果该函数的返回类型是在类中定义的,由于返回类型在类作用域外,因此需要显式的写上类名指定作用域,示例
        1. Windows_mgr::ScreenIndex Windows_mgr::addScreen(const Screen &s) { /* … */ } ;
      3. 由于类编译的两阶段(先编译数据成员,再编译函数成员),因此当数据成员中存在与外层作用域的同名变量时,实际优先使用的是类中的变量(即会覆盖外层变量);
      4. 在类中可以使用外层的类型名,但不可以重新定义外层已经定义好的类型名;
      5. 内层的同名变量,会隐藏外层的同名变量,但通过强制使用作用域运算符,指定作用域,可以访问被隐藏的变量,但实际并不推荐这种写法,而应该是起一个不同的变量名;
        1. ::height,只有两个冒号,表示访问全局作用域(即最外层的作用域);
        2. Screen::height,加上类名,表示访问类级别的作用域;
      6. 注意在类外声明的函数必须加上类名才是类的成员函数
        1. class Screen;
        2. Screen::pos verify(); // 这是一个全局函数;只是刚好其返回类型在类中定义,但它不是类的成员函数;
        3. Screen::pos Screen::verify(); // 这是一个 Screen 的成员函数;
      7. 假设在类中定义了一种新类型,则新类型的声明需要放在类的头部,这样才方便后续的数据成员或者函数成员进行调用;如果是放在尾部,则声明前的调用是非法的;
    5. 构造函数再探
      1. 构造函数初始值列表原来是有其原因的,在 C++ 中,由于在执行构造函数的函数体之前,会先编译数据成员,并将其默认初始化,所以如果是在函数体中使用赋值初始化操作,已经晚了,此时默认初始化已经完成;因此,当数据成员包含 const,引用,或者其他未提供默认构造函数的类类型时,编译器会出现报错,因为以上三种类型是无法被默认初始化为空值的,它们必须被显式的初始化为某个已存在的对象;
      2. 最好让初始值列表的成员顺序和类的数据成员顺序一致,同时要避免使用其中一个成员去初始化另外一个成员,这样很容易出现未定义的行为;编译器默认是按类成员出现的顺序来初始化的;
      3. 如果一个构造函数为其所有参数都提供了默认实参,实际上这个构造函数就具备成为类的默认构造函数的功能;
      4. 类中不能定义两个默认构造函数,不然当无实参的对类实例化时,会出现二义性冲突;
      5. C11 引入了委托构造函数来简化构造的过程,提取了共同的部分,原理是设定一个初始的被委托构造函数,然后其他构造函数通过调用它简化一部分工作,之后在自己的函数体内定义被委托函数未完成的部分;整个构造过程的执行顺序是先执行被委托函数的部分,再执行当前构造函数自己的部分;不同的构造函数之间也可以相互调用,逐级嵌套;
        1. 被委托构造函数需要使用什么关键字?
      6. 千万记住,一个类一旦定义了某个构造函数,最好要同时也给它定义一个默认的构造函数;(为什么?猜测原因:一旦自定义了一个构造函数,意味着编译不会再自动合成默认构造函数,因此我们需要手工自定义一个,不然无法应对未提供参数的构造场景)
      7. 不知为何,C++ 允许实现类类型的隐式转换(个人觉得普通变量的隐式转换规则就够麻烦和危险的了),例如某个类的构造函数允许使用一个 string 实参进行实例化,则在使用过程中,如果涉及 string 的运算,编译器会自动将 string 转换成类类型的临时对象;(估计这也是为什么 string 和字面串字面值可以运算的原因)
        1. 注意:隐式转换只会发生在构造函数只需一个实参即可完成实例化的类中;当构造函数需要多个实参时,就不会发生隐式转换了;
        2. 为了避免这种隐式类型转换带来的困扰,C++ 又引入了 explicit 关键字来对构造函数的声明进行限定(唉,C++ 真是会自寻烦恼);
        3. 另外 explicit 只允许在类里面的构造函数声明中使用,在类外面声明定义的构造函数,不允许使用 explicit 关键字;
        4. 被 explicit 声明过的构造函数,只能使用直接初始化的方式,不能使用赋值的方式完成初始化(为什么?因为无法触发隐式类型转换;但是,貌似还可以通过重载赋值运算符的来实现)(突然想到,之前碰到的奇怪现象,即虽然重载了赋值运算符,但是实际赋值的时候,编译器调用的却是拷贝构造函数,看来好像是因为触发了隐式类型转换)
          1. string null_book = “999-999-1323”
          2. Sales_data item = null_book // 如果构造函数声明为 explicit ,则此种初始化方式不可行;
          3. 虽然声明 explicit 后不能隐式转换,但可以通过 static(null_book) 进行显式的强制类型转换;
      8. 聚合类:有什么用?(感觉很像 C 语言中的结构体 struct)
        1. 条件:所有成员都是 public,没有类内初始值,没有构造函数,没有基类,也没有 virtual 函数(什么是 virtual 函数?虚函数,用来实现多态)
        2. 聚合类可以使用花括号括起来的初始值列表进行初始化;但聚合类的初始化跟数组的初始化有点像,元素需要严格按照顺序,初始值数量需要少于或等于成员数量,超过会报错;
        3. 聚合类有点危险,例如假设向聚合类添加一个成员,以前的初始化语句将不再能用,全部需要重新修改(所以搞不明白为什么发明聚合类这种怪东西出来)
      9. 字面值常量类:什么场景下适合使用???
    6. 类的静态成员
      1. 当我们希望某个值是属于类,与类相关,而不是单个对象的时候,可以通过声明静态成员来实现。这样做的好处是,当这个静态成员发生变动时,所有的对象都可以第一时间获得;(注意:当某个数据成员被声明为 static 时,实例化中的对象,是不包含这个数据成员的,我们需要使用类的作用域指针来访问这个属于类的数据成员;
      2. 通过在类的数据成员前面加上 static 关键字实现静态说明;
      3. 由于静态成员是属于类的,因此对象中没有包含静态成员的数据,静态成员是被所有的类对象共享的;因此,静态成员函数是没有 this 指针的,因为它不附属于某个对象;需要使用“类名::数据成员名”的方式来访问;
      4. 类的静态成员函数可以在类的外部定义,但需要先在类的内部使用 static 关键字进行声明,之后在外部定义的时候,不能再次使用 static 关键字,否则会出错;
      5. 据说不能在类内部初始化静态数据成员(声明需要在内部,为什么?),只能在类的外部定义和初始化每个静态数据成员;因此,由于静态成员定义在函数之外,看上去像是全局变量;
      6. 为了确保静态成员对象只被定义一次,比较好的做法是将定义放在一个单独的文件中,其他文件只是引用;
      7. 静态成员可以在类内部做初始化,但即使一个常量静态数据成员在类内部已经初始化了,正常也应该在类外部再次对其进行定义(为什么要这么做呢?)
        1. 类内初始化的要求:必须是 const 整数类型,或者是字面值类型的 constexpr 表达式;原因:编译器才能够算出值,然后进行替换;
      8. 静态成员的某些使用场景
        1. 可以作为成员函数的默认实参,而普通数据成员则不能;(为什么?)
        2. 静态数据成员可以是不完全类型(什么是不完全类型?有声明,但还没有定义的类型),因此,它可以指向类本身,而普通成员则只能声明成当前类的指针或者引用;
      9. 静态成员不会被构造函数初始化,因此它需要单独进行定义和初始化;
      10. 静态成员可以是 public,也可以是 private,类型可以是常量、指针、引用或者类类型;
  7. IO 库
    1. IO 类
      1. 面向不同种类的IO操作,分别定义在三个不同的头文件中,分别是:
        1. iostream,流的读写;
        2. fstream,文件的读写
        3. sstream,内存 string 的读写
      2. IO 对象不能被拷贝或者赋值,因此只能通过引用类型来访问它,由于访问的时候会改变它,因此不能定义成 const 类型;
      3. 流只有在有效的状态下,才能够正常读写,但流有可能处于无效状态,因此读写前有必要判断一下状态,可以通过如下方式判断:while(cin >> word) { // };
        1. 此处的 cin >> word 返回的仍然是流的引用,在 while 条件语句中,会触发流的引用进行隐式的 bool 类型转换;
      4. 标准库定义了一种 isstate 类型来表示流所处的状态,分别有 badbit, failbit, eofbit, goodbit 等状态;当流进入某种状态时,相应的状态变量会被置位;
      5. 通过调用 good() 和 bad() 可以获取流状态是否正常,eof 和 badbit 则用来表示特定类型的异常;
        1. cin.clear(cin.rdstate & ~cin.failbit & ~cin.badbit); // failbit 和 badbit 复位,其他 bit 位保持不变(注意这里的操作使用的是位运算符 & 和 ~,因为这些状态都是使用位模式表示)
      6. 原来流操作的缓冲区的目的,在于操作系统想将多个流操作合并成一个系统级的操作,提高效率(因为写操作很费时,合并后可以得到很大的性能提升);
      7. flush 和 ends 都可以用来刷新缓冲区,区别在于后者会插入一个空字符后再刷新,而前者则直接刷新,其他啥没做;
      8. 通过配对使用 unitbuf 和 nounitbuf 可以实现在二者之间的输出,都是无缓冲的;
      9. 当程序发生崩溃时,缓冲区是不会自动刷新的,因此调试的时候,要确保缓冲区有刷新,这样才能获得实时的程序状态;
      10. 输入流 cin 和输出流 cout 是关联的,意味着当输入流进行操作时,输出流会被刷新(先于输入流的操作发生);
      11. 每个流最多关联一个流,但多个流可以同时关联的同一个输出流 ostream;(虽然关联只能一个,但被关联貌似可以多个的样子)
    2. 文件输入输出
      1. 创建文件流对象时,如果提供文件名,则 open 函数会被自动调用,示例:
        1. ifstream file1(file_name);
        2. ofstream file2(file_name2);
      2. 由于 fstream 是对 iostream 的继承,因此在使用 iostream 的地方,也可以替换使用 fstream;
      3. 如果一开始定义了一个空的文件对象,则后续仍然可以使用对象的 open 方法来关联文件;
        1. 由于 open 有可能执行失败,因此在对文件进行下一步操作前,有必要先检查一下文件流状态,示例 if(file2) { … };
        2. 一个文件流关联打开某个文件后,在关闭前不能再次关联其他文件,需要先关闭才行;
      4. 当一个 fstream 被销毁时,它的 close 方法会被自动调用;
      5. getline(is, s) 一次会读取一行,<< 运算符一次只读取一个单词,即遇到空格会截断;
      6. 文件模式:in, out, app, ate, trunc, binary;
        1. ifstream 默认以 in 模式打开, ofstream 默认以 out 打开,fstream 默认以 in 和 out 模式打开;
        2. out 模式打开已存在的文件时,默认会清空里面的数据;如果要保留数据,必须显式的指定 app 或者 in 模式;
        3. 示例:ofstream fin(“filename”, ofstream::app)
        4. 调用 open 函数时,会有打开模式,如果没有显式指定,会隐式自动默认;
    3. string 流
      1. istringstream 用来写入,ostringstream 用来读取,iostringstream 用来读取和写入;
      2. 创建方法:sstream strm; sstream strm(s)
      3. 读取方法:strm.str() 返回的是一个 string 对象,可以使用 string 的相关方法;
      4. 写入方法:strm.str(s)
      5. string 的操作和 istringstream 拥有的操作是不同的,有时候可以考虑将它们互相转换一下,获取相应的操作办法;
      6. 对于范围 for 循环,记得优先使用 const auto & 的方式,有诸多好处;
  8. 顺序容器
    1. 顺序容器概述
      1. 容器类型:vector, deque, list, forward_list, array, string;
      2. 除非没有很好的理由,不然优先使用 vector,因为 vector 可以使用 sort 进行排序;
      3. 容器的属性
        1. iterator, const_iterator // 迭代器类型
        2. size_type, difference_type // 容器大小(无符号整数),距离(有符号整数)
        3. value_type // 元素的类型
        4. reference, const_reference // 元素引用类型
    2. 容器库概览
      1. 构造函数
        1. C c // 默认构造空容器
        2. C c1(c2) // 拷贝c2 来初始化 c1
        3. C c(b, e) // 使用范围 b/e 来初始化 c
        4. C c{a, b, c} // 使用列表初始化 c
        5. 顺序容器
          1. C seq(n) // 不适用 string 类型
          2. C seq(n, t)
      2. 赋值操作
        1. c1 = c2;
        2. c1 = {a, b, c};
        3. a.swap(b);
        4. swap(a, b)
      3. 大小操作
        1. c.size()
        2. c.max_size()
        3. c.empty()
      4. 容器一般都定义在同名的头文件中;例如 vector 定义在 中,deque 定义中 中;
      5. size_type 是容器的下标类型(注:下标类型不是 int 类型)
      6. size_type, iterator, const_iterator 三者都可以用来访问容器内的元素,但是链表类型只适合于使用后两者进行访问,因为链表不能通过下标访问;
      7. 通过 auto 与 begin/end 配合,可以得到容器的类型,如果容器类型是 const ,就会返回 const iterator,示例如下:
        1. auto it1 = a.begin(); // 获得的迭代器类型依赖于容器
        2. auto it2 = a.end(); // 同上;
        3. auto it3 = a.cbegin(); // 显式指定获得 const 类型的迭代器;
        4. auto it4 = a.cend(); // 同上;
        5. 当不需要写访问时,应该优先使用 cbegin 和 cend;
      8. 当将一个容器初始化为另外一个容器时,两个容器的容器类型和元素类型需要一致;如果使用一对迭代器做为参数,来拷贝容器中的一段元素,则不要求容器类型和元素类型相同;例如可以将 vector 容器的元素,拷贝生成 deque 容器的元素;
      9. 如果容器的元素类型是内置类型,或者具有默认构造函数,则可以只提供一个表示容器大小的参数;如果不是,则还需要再多提供一个元素初始值;
      10. array 不支持普通容器构造函数,它有自己的格式要求;大小是 array 类型的一部分;定义时需要同时显式指定元素类型和元素数量,例如:array<int, 10>, array<string, 20>;
      11. 虽然内置数组类型不支持拷贝或者赋值,但 array 类型则可以支持,示例
        1. int arr1[10] = {0}; // arr1 是内置数组类型
        2. int arr1_copy[10] = arr1; // 错误,因为内置数组类型不支持赋值运算符;
        3. array<int, 10> arr2 = {0}; // arr2 是 array 类型;
        4. array<int, 10> arr2_copy = arr2; // 正确,array 类型支持赋值运算符;
      12. array 类型变量不支持花括号的列表赋值,但可以花括号的列表初始化。除了 array 以外的容器,列表初始化和列表赋值两种操作都可以支持;
        1. array<int, 3> a = {1, 2, 3}; // 正确,使用花括号进行初始化;
        2. array<int, 3> b = {0}; // 正确 ,使用花括号进行初始化;
        3. a = b; // 正确,使用赋值运算符;
        4. a = {0}; // 错误,由于 a 已经初始化过了,故此处是使用花括号赋值,不支持;
      13. 赋值相关运算,会导致原来指向左边容器内部的迭代器,指针和引用失效;而 swap 由于是交换容器内容,不会引起指向内部的迭代器、指针、引用失效;
      14. assign 支持范围段的拷贝,示例:names.assign(oldstype.begin(), oldstyle.end());
      15. swap 可以用来交换两个容器内的内容(除了 string 容器外,据说交换速度会很快)(估计是通过交换指针来实现);
      16. swap 操作不会导致原来的指针、迭代器、引用失效,但会变成指向了交换后的新容器(怎么感觉好像存在越界的风险,例如原来指向第18位,新交换的容器要是少于18位怎么办?);
      17. swap 有成员函数版本和非成员函数版本,推荐使用后者,即 swap(a, b),而不是 a.swap(b);
      18. 容器的关系运算符,实质上是使用元素的关系运算符进行比较,因此容器比较的前提是,元素是相同的类型,且支持某种关系运算符(默认是小于号,如果不是,则需要提供自定义的版本);
      19. forward_list 有一个其他容器没有的 before_begin 独特方法;(原因:它只能单向添加元素,因此有一头是固定的;其他容器两头都可以添加元素,故没有哪头是固定的)
    3. 顺序容器操作
      1. 添加元素
        1. c.push_back(t), c.emplace_back(args) // 返回 void
        2. c.push_front(t), c.emplace_front(t) // 返回 void
        3. c.insert(p, t), c.emplace(p, t) // 返回新添加的元素的迭代器
        4. c.insert(p, n, t) // 返回新添加的第一个元素的迭代器
        5. c.insert(p, b, e) // 返回新添加的第一个元素的迭代器,b/e 不能指向 c 自己;
        6. c.insert(p, il) // il 为花括号包围的列表,返回新添加的第一个元素的迭代器;
        7. 总结发现:所有 insert 操作都跟 p 有关系,都需要提供 p ;
      2. 向 vector, string, deque 插入一个元素,会导致所有原指向该容器的迭代器、指针、引用等都失效;
      3. insert 的迭代器范围参数,不允许指向当前容器自己;(可以先拷贝一份出来临时存储,然后再插入)
      4. 访问元素
        1. c.front(),c.back() // 返回首元素、尾元素的引用
        2. c[n] // 返回第 n 个元素的引用,如果 n 越界,会产生未定义行为;
        3. c.at(n) // 返回第 n 个元素的引用,此方法更安全,如果 n 越界,会抛出 out_of_range 的异常;
      5. front 和 back 可以分别用来获取容器的首尾元素的引用,c.front(), c.back();(注意:forward_list 不支持 back)
      6. 在获取容器的元素前,记得先判断容器非空,以避免引用一些未定义的行为;
      7. auto v = c.back( ),虽然 back 返回引用,但由于 auto v 没有使用 & 符号,故会触发拷贝赋值,即将 back 返回的引用的值拷贝给 v;如果想要实现 v 为引用,则需要定义成 auto &v;
      8. 删除元素
        1. c.pop_back(), c.pop_front() // 分别删除尾元素和首元素;返回 void
        2. c.erase(p) // 删除迭代器 p 所指定的元素,返回 p 之后的迭代器
        3. c.erase(b, e); // 删除 b/e 范围内的元素,返回范围之后的第一个元素;
        4. c.clear() // 删除所有元素,返回 void
      9. 当删除 deque 除首尾外的任何元素时,都会导致原来指向 deque 的所有迭代器、指针、引用等失效;
      10. vector/string 删除点之后的迭代器、指针、引用将失效;
      11. 弹出元素的成员函数 pop 的返回值是 void,所以如果想获得弹出元素的值,应该在弹出前获取;
      12. 非成员函数 begin 和 end 也可以用来获取内置数组类型的首尾指针地址,示例:begin(arr), end(arr);
        1. 所以貌似用这两个元素,可以较好的防范越界行为;
      13. vector 可以使用内置数组类型的首尾指针地址初始化,示例:vector v1(begin(arr), end(arr))
        1. 其中的 begin(arr) 也可由 arr 直接代表,即简写为 vector v1(arr, end(arr));
      14. 在定义函数的 string 类型形参时,应记得在参数列表中尽量使用引用类型;(这样可以减少对 string 的拷贝,减少内存开销)(而且可以处理字符串字面值实参的情况);
      15. void 类型的函数仍然可以使用 return 语句,只不过该语句不能有其他东西,只能有一个 return 单词;
        1. 可以使用在想提前结束退出函数的场景;
      16. 改变容器大小
        1. c.resize(n)
        2. c.resize(n, t) // 每个成员初始化为 t
      17. 如果 resize 变大了容器,则需要通过可选参数提供增加的元素的初始值,如果增加的元素如果是类类型,要求类类型本身自带默认构造函数;
      18. 如果 resize 会缩小容器,则原来的迭代器,指针、引用等都会失效;
      19. 对于 deque,如果在头部添加元素,会导致迭代器失效,但原来的指针和引用仍然有效;
        1. 好奇 deque 的实现方法是否跟 vector 一样提前预分配内存?
        2. 如果是在尾部添加元素,原来的指针、迭代器、引用应该都还会有效吧?
      20. 由于容器的插入和删除操作,都很可能会使用原来的迭代器失效,因此在插入和删除操作的循环中,不应该使用变量保存 end() 返回的尾迭代器,而应该每次循环都重新计算;
        1. insert 的点如果在 begin 前,则也会导致 begin 迭代器失效,因此这种情况下 insert 后应该当即重新赋值 begin;
      21. 复合语句(如 iter += 2) 不适用于 list 和 forward_list 的迭代器运算
        1. 原因:突然想到,迭代器的本质,可能只是对指针的封装,链表的实现原理决定它的内存不需要连续存储,因而迭代器也即指针地址的递增运算对于链表应该是无效的;链表也不支持下标访问;
        2. 注意,只是不适用于链表,对于 vector 和 string 的迭代器,以上运算都是支持的,所以说优先使用 vector,除非有频繁的插入操作;
        3. 由于列表的迭代器不支持加法和减法运算,因此无法通过加减法来计算列表的迭代器之间的距离,不过可以使用 distance 函数来计算,distance(a, b) 和 distance(b, a) 的结果是不一样的;
          1. 那如果要在链表中一次跳转多个元素要如何实现呢?
    4. vector 对象是如何增长的
      1. 实现原理:提前预留,阶段性重新分配;(实现的策略目测好像是按当前的 capacity 进行翻倍)
      2. shrink_to_fit 要求退回内存,但只是一个请求,编译器不一定会执行;
      3. capacity() 告诉已分配能容纳的内存大小;
      4. reserve(n) 要求至少预留的大小,实际编译器的分配可能会更大,原则上是不小于 n 即可;
    5. 额外的 string 操作
      1. 构造 string 的其他方法
        1. 按指定数量截断数组:string s(cp, n)
          1. cp 参数为指向数组的指针,n 是可选参数,但如果没有 n,则 cp 需要有空字符做为结束,不然行为是未定义的;另外此数组需要至少有 n 个字符;
            1. 这里面有一个比较操蛋的地方是,我们设置条件判断 cp 是否有空字符串做为结束;因为如果想通过遍历 cp 来检查,一旦没有结束符,这个检查本身也会触发未定行为;
          2. 目测这个示例代码的功能,好像是通过 C 风格字符串来初始化 string 类型的 s,这么说,还有一种可能的写法为 string s(cp, sizeof(cp) - 1);
        2. 从 string 某个指定位置开始截取余下部分:string s1(s2, pos)
          1. 如果 pos 越界了,会抛出异常;即 pos 应小于等于 s2 的 size;假设要获得首字符之后的内容,可以为 string(s2, 1);
          2. 目测 pos 好像是一个整数下标;
        3. 从 string 某个指定位置开始截取指定长度的字符:string s1(s2, pos, len),假设要取第1到8个字符,可以为 string(s2, 1, 8);
        4. substr 获取子字符串,格式为 s.substr(pos, n) // 这个用法同 string s2(s, pos, len)
        5. string 类型也是容器类型的一种,本身也支持用其他容器进行范围段初始化,只要元素能转换即可,示例
          1. vec vc{‘H’, ‘i’};
          2. string s(vc.cbegin(), vc.cend());
      2. 改变 string 的其他方法
        1. 基本操作
          1. s.insert(pos, args) // 参数 pos 表示下标,返回一个指向 s 的引用;接受迭代器的版本,返回指向第一个插入字符的迭代器;
          2. s.erase(pos, len) // 返回指向 s 的引用
          3. s.assign(args) // 返回指向 s 的引用;
            1. 为什么不直接是 s = args?答:也是可以滴;
          4. s.append(args) // 返回指向 s 的引用;
          5. s.replace(range, args) // 返回指向 s 的引用
            1. range 可以是下标+长度,或者一对迭代器;
            2. args 可以是以下形式之一( assign 和 append 支持所有形式,str 不能是 s 自己,迭代器 b/e 不能指向 s 自己)
              1. str
              2. str, pos, len
              3. cp, len
              4. cp // 此种格式要求 cp 有结束符,不然会出错;
              5. n, c
              6. b, e
              7. 初始化列表
        2. 接受下标为参数
          1. s.insert(下标,字符数量,字符)// 返回一个指向 s 的引用;
          2. s.erase(下标,字符数量)
        3. 接受 C 风格的字符串的指针为参数;
          1. const char *cp = “hello, world.”;
          2. s.assign(cp指针, 字符数量)// 实现 assign 赋值;
          3. s.insert(下标,cp + 7)// 第二个参数 cp + 7 也是表示指针位置
        4. 接受其他 string 为参数
          1. s1.insert(下标, s2)
          2. s1.inser(s1下标, s2, s2下标, 字符数量)
        5. s.append(“abc”),在末尾插入字符串;
        6. s.replace(range, args), range 和 args 支持很多种格式;
          1. s.replace(下标,替换字符数量,待插入字符串)
          2. s.replace(左迭代器,右迭代器,待插入字符串) // 注:迭代器不能指向 s
      3. 搜索 string 的方法
        1. s.find(args) // args 第一次出现的位置,如果没有找到返回 npos (string::npos, unsigned 的最大值);
        2. s.rfind(args) // args 最后一次出现的位置,可以用来实现从右往左搜索;
        3. s.find_first_of(args) // args 任意一个字符第一次出现的位置
        4. s.find_last_of(args) // args 任意一个字符最后一次出现的位置
        5. s.find_first_not_of(args) // 第一个非 args 字符出现的位置
        6. s.find_last_not_of(args) // 最后一个非 args 字符出现的位置
        7. args 格式
          1. c, pos // 从 string 中的位置 pos 开始找字符 c;pos 的默认值为 0,表示从头开始找;
          2. s2, pos // 从 string 中的位置 pos 开始找字符串 s2;
          3. cp, pos // 从 string 中的位置 pos 开始找指针 cp 指向的 C 风格字符串;
          4. cp, pos, n // 同上,n 表示 cp 指向的字符串的前 n 个字符;
      4. compare 函数:
        1. s.compare(s2) // 比较两个字符串,判断大于、等于还是小于;返回0,正数或者负数;
        2. s.compare(pos1, n1, s2) // 将 s 中从位置 pos1 开始的第 n1 个字符串,与 s2 进行比较;
        3. s.compare(pos1, n1, s2, pos2, n2) // 将 s 中从位置 pos1 开始的第 n1 个字符串,与 s2 中从位置 pos2 开始的第 n2 个字符进行比较;
        4. s.compare(cp) // 与指针 cp 指向的 C 风格字符串进行比较;
        5. s.compare(pos1, n1, cp) // 将 s 中从位置 pos1 开始的第 n1 个字符串,与指针 cp 指向的 C 风格字符串进行比较;
        6. s.compare(pos1, n1, cp, n2) // 将 s 中从位置 pos1 开始的第 n1 个字符串,与指针 cp 指向的 C 风格字符串的前 n2 个字符串进行比较
      5. 数值转换
        1. 其他各种类型转 string 直接使用 to_string 函数即可,示例:string s = to_string(i) // 将整数转换成字符串;
        2. double d = stod(s) // 字符串转 double
        3. stoi, stol, stoul, stoll, stoull, stof, stod, stold
    6. 容器适配器
      1. 适配器:就像现实生活中的电源充电器转换头一样,适配器的目的是输入一种类型进行转换,使之产生的效果像另外一样类型;
      2. 容器适配器可以用来生成基于容器构建的新抽象类型,包括栈 stack,队列 queue,优先队列 priority_queue
      3. stack 默认使用 deque 实现,但如果要使用 list 或 vector 也可以,例如 stack<string, vector>;
      4. 初始化方法
        1. 声明空对象,stack<string, vector> stk1
        2. 用一个已有的适配器初始化一个新的 stack<string, vector> stk2(stk1)
      5. 栈的操作
        1. stk.pop()
        2. stk.top()
        3. stk.push(item)
        4. stk.emplace(args)
      6. 队列的操作
        1. que.pop(), que.push(item), que.emplace(args)
        2. que.front(), que.back()
  9. 泛型算法
    1. 概述
      1. 大部分泛型算法定义在头文件 algorithm 中,也有部分数值相关的算法定义在 numeric 头文件中;
      2. 算法通过迭代器参数,实现对容器内元素的遍历操作;
    2. 初识泛型算法
      1. 只读算法
        1. find(b, e, t) 用来实现元素的定位,返回第一个匹配的元素迭代器;
          1. find_if 的第三个参数为可以是一个值,也可以是一个 predictate 谓词,用来判断元素是否满足条件的函数
        2. count(b, e, t) 用来计算出现次数;
        3. accumulate(b, e, init) 用来实现累加;累加的效果取决于元素本身对加法运算符的支持情况
          1. 不知是否可以实现自定义运算符? 答:支持自定义运算,格式为 accumulate(b, e, init, predictate)
          2. accumulate 定义在头文件 中;
        4. equal(b, e, s2.begin()) 用来实现比对是否相等;// 此处 equal 假设第二个迭代序列比第一个长,如果短,则可能发生未定义错误;
        5. 泛型算法本身不会执行容器的操作;注意这句话的意思,是指关于容器本身的操作,例如 resize,push_back 之类;但不是不会执行对容器元素的操作,泛型算法是可以对容器元素进行操作的;如果想对容器进行操作,可以通过一类特殊的迭代器,例如 inserter 来实现;
      2. 写容器元素的算法
        1. 由于算法不会改变容器大小,因此需要确定写入的元素数量应小于等于容器大小;(可以通过 inserter 避开这个问题)
        2. 有些算法接收两个序列,此时第二个序列有两种表示方式,一种使用单一迭代器,一种使用成对迭代器;对于单一迭代器,默认假设第二个序列的长度不小于第一个序列,如果小于,则可能发生未定义行为(访问第二个序列末尾并不存在的元素);
        3. 由于算法不能改变容器大小,因此应该避免使用算法向容器中插入新元素,而只是用来改写容器中已有的元素;如果要插入新元素,应该考虑使用容器本身的方法(如 push_back),或者使用插入迭代器 back_inserter(它本质上也是去调用容器自身的 push_back 方法)
        4. back_inserter 用法(定义在头文件 中)
          1. auto it = back_inserter(vec);
          2. fill_n(it, 10, 0) // 向 vec 插入 10 个 0;fill_n 用来向容器写入指定数量的元素(使用播入迭代器,就可以不用关心容器的大小问题了)
        5. copy 算法
          1. 接受三个迭代器,前两个表示拷贝源的范围,第三个表示目标序列的起始位置;
          2. copy(s1.begin(), s1.end(), s2.begin())
        6. replace
          1. replace(s.begin(), s.end(), oldVal, newVal)
          2. replace_copy(s1.begin, s1.end, back_iterator(s2), oldVal, newVal);
            1. 这个函数的用于将 s1 的内容拷贝一份到 s2 末尾,并将其中的旧值替换为新值;
        7. vector 的 reserve 用来操作内存, resize 用来操作元素数量;为了避免容器越界,应该使用 resize,而不是 reserve;
      3. 重排容器元素的算法
        1. sort(begin, end) // sort 默认使用 < 来比较元素,因此它可以运算的前提是元素类型本身支持 < 运算符,如果不支持,则需要自定义比较操作,否则达不到预期效果;
        2. sort 也可以接受二元谓词做为第三个参数;sort(begin, end, predicate)
        3. stable_sort(begin, end, predicate) 接受一个谓词 predicate 做为参数,它的特点是可以维持相等元素的原有顺序;
        4. unique(begin, end) // 重新排列元素,将不重复项提到前面,重复项放在最后面,返回指向第一个重复项的迭代器(假设叫 first_repeat),之后如果要删除这些重复项,可以调用 v.erase(first_repeat, v.end) 实现;(貌似也可以用拷贝内容到 set 来完成这一系列运作)
        5. for_each 算法
          1. for_each(first, last, function)
    3. 定制操作
      1. 向算法传递函数
        1. 可以通过向算法传递函数来实现定制操作,有些算法支持一元谓词,有些支持二元谓词;不管一元还是二元,对于谓词参数来说,我们可以向算法传递任何可调用的对象(即callable object,支持圆括号表达式);
        2. lambda 的格式 [capture list] (params list) -> return type { func body } // 返回类型需要使用尾置表示;从左到右分别为 捕获列表,参数列表,返回类型,函数体
          1. 示例 auto f = [ ] { return 42; } // 返回类型可以由于返回值进行推断,如果没有返回值,则类型自动为 void;
          2. 调用 f( ) 将返回值 42;
        3. 捕获列表是什么鬼?原来是用来指定 lambda 想要使用的局部变量(当前函数内的局部非 static 变量才需要使用捕获列表,对于 static 变量或当前函数体外的变量,不需捕获列表即可直接在 lambda 中使用);
        4. 据说参数列表和返回类型是可选的,而捕获列表和函数体是必须的,为什么捕获列表是必须的?
        5. 当捕获列表为空时,表示不使用它所在函数中的任何局部变量;
        6. 由于 lambda 不能有默认参数,因此它要求实参与形参的数量必须是一致匹配的;
        7. partition(begin, end, predicate) // 根据 predicate 的布尔结果,将容器分为 true/false 前后两段,返回指向最后一个 true 之后位置的迭代器;
        8. lambda 捕获列表中的变量,是在 lambda 创建时即生成并初始化的变量,它可以是拷贝,或者引用;如果是引用,需要确保在 lambda 执行期间,引用的源对象未被销毁,不然会发生未定义的行为;如果捕获的是指针或迭代器,则要特别小心,确保指针或者迭代器在 lambda 执行期间不会失效,或者值发生未预期的改变;因此,捕获一般应该尽量避免使用指针或者引用,而应多使用值拷贝;(但貌似值拷贝会消耗更的性能)
        9. lambda 也支持隐式捕获变量(即自动推断捕获哪些变量),此时捕获列表使用 = 来表示参数采用值传递([=]),& 来表示引用传递([&]);如果要混合使用,则应该很小心,另外一个参数需要使用不同类型的传递方式,以便能够推断剩下的元素是哪种方式,显式为值,则隐式为引用。反之亦然;
          1. 举个栗子:w = find_if(words.begin(), words.end(), [=](const string &s) {return s.size() >= sz; });
            1. 此处 sz 是需要隐式捕获的变量;而且要求使用值传递;
          2. lambda 一般不会改变传入的形参,如果要改变形参时,应该在形参列表右侧使用 mutable 关键字,示例 auto f = [ v1 ] ( ) mutable { …… }; // 此处 mutable 表示函数体内会修改 v1; 由于此处是值传递,不会影响外层的 v1 变量值;(貌似改变形参也不是一个好习惯啊,正常最好是不要改变)
        10. 如果 lambda 不仅是一条 return 语句,还有其他语句,则编译器会认为 lambda 的返回类型为 void;除非我们显式的使用尾置返回类型进行说明
          1. 示例 [ ](int i) -> int { …… }; // 当没有显式指定返回类型时,是否默认返回类型为 void?还是说如果只有一条 return,会自动判断返回类型?
        11. transform(s1.begin(), s1.end(), s2.begin(), predictate) 其中的 s2.begin 是指转换结果存放的目标位置;
        12. 如果 lambda 的捕获列表为空,正常可以使用函数来替换它,因为不需要使用到局部变量;但如果捕获列表非空,则不好使用函数替代 lambda;
          1. 后来发现可以使用 bind 避开这个限制;bind 很有点 python 中的装饰器的味道;
        13. 由于算法对谓词的参数数量的限制,导致 lambda 更多适合于一些简单的场景,对于需要多个参数的复杂逻辑函数,通过引入 bind 来解决
          1. bind 用法,假设原目标函数 func 的参数数量是5个,例如 func(a, b, c, d, e) ,其中前两个参数的实参由于调用者提供,则需要包装成新的只接受这两个实参的可调用对象;
          2. bind 会返回一个新的可调用对象 gunc,对于算法来说,实际上是跟这个新的可调用对象打交道,如果算法只接受一元谓词,则这个新可调用对象只能有一个形参;如果算法支持二元谓词,则这个新可调用对象,只能有两个形参;形参由算法提供实参进行初始化,然后接下来调用旧的 func,然后按照点位符提前指定好的顺序,将算法提供的实参,与原来已有实参,按顺序排好,传入 func 进行计算
          3. 感觉 lambda 的捕获列表也可以多参数,只是 lambda 可能不太适合放过于复杂的计算逻辑在里面,而且也不太方便于复用;
          4. bind 接收的参数个数取决于被绑定函数,假设被绑定函数的参数个数为n,则 bind 函数的参数个数为 n + 1;因为被绑定函数占用了第一个参数的位置;
          5. auto check6 = bind(check_size, _1, 6);
            1. _1 表示一个占位符,将外层 gunc 的实参按顺序进行编号,并替换相应位置上面的占位符;
            2. placeholder 占位符是定义在 std::placeholder 命名空间中的,使用的时候,需要 using 声明,不然就得写完整的作用域名称出来了
              1. 例如:using std::placeholders::_1; // 这种写法导致每个占位符都得声明一次,如果占位符数量比较多,写出来比较繁琐;
              2. 简单的写法:using namespace std::placeholders;
            3. 事实上,由于已经使用占位符的编号来映射实参,所以我们甚至也可以随意的改变实参想要放置的位置
              1. 例如:auto g = bind(f, a, b, _2, c, _1);
    4. 再探迭代器
      1. 定义在 中的其他几种迭代器:
        1. 插入迭代器:用来向容器中插入元素;
        2. 流迭代器:用来遍历所关联的流;
        3. 反向迭代器:反方向移动的迭代器;(forward_list 不支持,因为它只能单方向移动)
        4. 移动迭代器:专门用来移动元素的迭代器;
      2. 插入迭代器
        1. 三种类型
          1. back_inserter,对应 push_back(t)
          2. front_inserter,对应 push_front(t)
            1. list lst1 = {1, 2, 3, 4};
            2. list lst2, lst3;
            3. copy(lst1.cbegin(), lst1.cend(), front_inserter(lst2)) // 结果得到 {4, 3, 2, 1}
            4. copy(lst1.cbegin(), lst1.cend(), inserter(lst3, lst3.begin()) // 结果得到 {1, 2, 3, 4}
          3. inserter,对应 insert(t, p),p 为一个指向原容器的迭代器,指定在 p 前面插入 t;示例
            1. it = inserter(c, iter) 得到一个指向 iter 的插入器 it,如果对它进行赋值,例如 *it = val,会在 iter 前面插入相应的值 val,效果等同下面的代码:
              1. iter= c.insert(iter, val);
              2. ++iter ;
        2. unique_copy,接受三个迭代器,将指定范围中不重复的元素,拷贝到第三个迭代器指定的位置中;(由于是写操作,应注意第三个迭代器对应的容器大小足够,如果无法确定,则第三个迭代器应该使用插入迭代器);
      3. iostream 流迭代器
        1. 创建流迭代器时,需要指定读取的对象类型,且对象支持输入/输出运算符 << 和 >> 的操作,例如 istream_iterator int_it(cin);
        2. 如果不使用 cin 初始化,而使用默认初始化,例如 istream_iterator int_eof, 则会创建一个尾后值迭代器(什么鬼?有什么用?相当于 v.end(),可以用来作读取结束的判断
          1. while(int_it != int_eof)
          2. 用法二:vector int_v(int_it, int_eof); // 用来做范围初始化还挺方便的;
          3. 用法三:accumulate(int_it, int_eof, 0); // 可以使用迭代算法对流进行操作,此时流看起来跟容器好像区别不大了;
        3. 文件流迭代器
          1. ifstream infile(“../filename”);
          2. istream_iterator str_it(infile); // 可以用来从”filename” 中读取字符串;
        4. 当将一个流迭代器绑定到一个流时,并不能保证立即读取数据,因此,从某种意义上来说,流迭代器貌似支持惰性求值(不过使用惰性求值也不见得是好事)
        5. ostream_iterator 允许绑定一个输出流,但它不能默认初始化,也不支持尾后值迭代器,它可选第二个参数,表示每次输出一个对象时,会将第二个参数也一并输出在后面,该参数必须是一个C风格字符串(即字符串字面值或者以空字符结尾的字符数组)
          1. ostream_iterator out(os)
          2. ostream_iterator out(os, d)
          3. out = val // 将 val 输出到 os 中;
        6. copy(v.begin, v.end, out) 实现了输出 v 容器中元素的功能,写法比循环更加简洁;
      4. 反向迭代器
        1. c.rbegin, c.rend, c.crbegin, c.crend;
        2. reverse_iterator 有个 base 方法,可以将自己变成正向迭代器
        3. 反向迭代器表示的闭合范围与正向迭代器是相反的,因此,当使用正向迭代器初始化一个反向迭代器时,二者指向的并不是相同的元素;
    5. 泛型算法结构
      1. 形参模式
        1. alg(beg, end, other_args)
        2. alg(beg, end, dest, other_args)
        3. alg(beg, end, beg2, other_args)
        4. alg(beg, end, beg2, end2, other_args)
      2. 命名规范
        1. 如果形参数量不同,则接受一个谓词参数
        2. 如果形参数量相同,则使用 _if 版本;
        3. 如果要额外拷贝,使用 _copy 版本
        4. 少数有 copy + if 结合的版本;
    6. 特定容器算法
      1. 链表由于可以在任意位置插入和重新连接元素的特点,应该优先使用其成员函数,而非通用版本的算法,原因:前者效率更高;
      2. 成员函数
        1. lst.merge(lst2) // 使用 == 运算符
        2. lst.merge(lst2, comp) // 使用自定义谓词
        3. lst.remove(val) // 删除 == val 的元素
        4. lst.remove_if(pred) // 删除满足 pred 条件的元素;
        5. lst.reverse() // 反转
        6. lst.sort() // 使用 < 运算符排序
        7. lst.sort(comp) // 使用谓词条件排序
        8. lst.unique() // 使用 == 删除重复元素
        9. lst.unique(pred) // 使用谓词条件删除重复元素;
      3. splice 成员可以用来粘接两段 list
        1. 双向链接 list
          1. lst.splice(p, lst2)
          2. lst.splice(p, lst2, p2)
          3. lst.splice(p, lst2, b, e)
        2. 单向链表 forward_list
          1. flst.splice_after(p, lst2)
          2. flst.splice_after(p, lst2, p2)
          3. flst.splice_after(p, lst2, b, e)
      4. 链表的特点是会改变其参数(比如销毁),这点和通用算法不同,需要特别注意;
  10. 关联容器
    1. 使用关联容器
      1. 关联容器中,无序集合的名字都使用 unordered 开头,例如 unordered_map, unordered_set, unordered_multimap, unordered_multiset;
    2. 关联容器概述
      1. map 支持列表初始化,但每个元素需要使用花括号将其包起来,示例
        1. map<string, int\> m = {{"a", 10}, {"b": 20}}
      2. set 和 multiset 都支持迭代器范围初始化,格式为 set iset(begin, end);,区别在于前者会剔除重复的元素,后者则不会;
      3. 对于 map/set 来说,相同的组成元素是 key 部分,key 称为关键字,它的类型是有一定要求的,即要支持 < 运算符,否则就要注明使用自定义函数进行比较运算
        1. 示例: set<Sales_data, decltype(compareIsbn)*> bookStore(compareIsbn) // 此处不是很理解为什么要在 bookStore 后面再写上 compareIsbn?用来做为构造函数的参数,初始化对象;莫非此种格式即查自定义类型关联容器的写法?
          1. 等同于下面的声明法
            1. using less = bool(*)(Sales_data &a, Sales_data &b);
            2. multiset<Sales_data, less> bookStore2(less);
          2. 相比较之下,看来还是 decltype 比较好用;如果不使用 decltype,就需要使用 using 来自定义类型名称了;假设要声明一个函数指针的类型,可以这样
            1. using less = boo(*)( );
      4. pair 的操作,定义在 的头文件中
        1. pair<T1, T2> p;
        2. pair<T1, T2> p = {v1, v2}; // 支持列表初始化
        3. pair<T1, T2> p(v1, v2); // 支持调用符初始化;
        4. auto p = make_pair(v1, v2);
        5. p.first
        6. p.second
        7. p1 == p2, p1 != p2;
        8. p1 relop p2 // 注:此处的 relop 表示 relationship operation,即关系运算符 <, <=, >, >=等;
    3. 关联容器操作
      1. 关联容器迭代器
        1. 关联容器除了顺序容器的那些类型外,还有一些自己的类型,包括
          1. key_type,关键字类型
          2. mapped_type,map 中与关键字匹配的对象类型,即 key-value 键值对中的 value 对象;set 没有此类型;
          3. value_type,元素类型,对于 map 是 pair;对于 set 则与 key_type 相同;
          4. 可以通过作用域运算符来提取这些成员,例如 map<string, int>::value_type
        2. 没想到关联容器也是有迭代器的,因为可以通过 begin/end 来对关联容器进行遍历,map 成员是字典序的;
          1. 对于 map 来说,通过迭代器可以顺序访问里面的 pair 元素成员,其中 pair 的 key 成员是 const 只读不可修改的,而 value 则可以修改;
          2. 对于 set 来说,其成员是不可修改的,只能通过迭代器进行访问;
        3. 由于关联容器成员的特殊性(例如 set 成员不可修改,map 元素是 pair 且 pair 的首个成员不可修改),因此写操作系列的泛型算法都无法用于关联容器;
        4. 由于关联容器不能通过关键字快速查找,因此普通的搜索泛型算法也不适用于关联容器,调用关联容器自己的 find 成员函数的性能更好;
        5. 在实际编程中,关联容器只在两种情况下参与泛型算法,一种是被当作源序列进行读取操作,另外一种是当作目的位置进行添加成员;
        6. 迭代器解引用的两种方法
          1. (*it).second = val;
          2. it->second = val;
        7. multiset 或 set 不能使用 back_inserter,因为它没有 push_back 的方法;
      2. 添加元素
        1. set 支持 begin/end 和列表两种添加方式,它会自动剔除重复的元素;
          1. set2.insert(v.begin(), v.end());
          2. set2.insert({1, 2, 3, 4})
        2. 向 map 添加元素有四种方法,分别是列表,make_pair,pair,map<T1, T2>::value_type;最后一种即显式的声明成员元素;
        3. 关联容器的 insert 操作
          1. c.insert(v), c.emplace(args) // 返回一个 pair 对象,包含指向对应关键字的迭代器 + 成功与否的 bool 值(true 表示插入成功,false 表示插入失败);
            1. 对于接受重复 key 的 multiset/multimap 来说,只返回迭代器,没有 bool 值;
          2. c.insert(p, v), c.emplace(p, args) // 返回一个迭代器,指向对应的关键字;其中的 p 用来指示从哪里开始搜索新元素的存储位置,感觉这个有点奇怪,因为如果不遍历全部位置,如何避免重复?
          3. c.insert(b, e),c.insert(il) // 返回 void,b/e 是 c::value_type 类型的迭代器,il 是这种类型的列表;对于 map/set,只插入原来不存在的元素,对于 multiset/multimap 则会插入所有元素;
        4. multimap 如何访问具有相同名称的 key? 用 lower_bound 和 upper_bound 获得范围后来访问;或者使用 equal_range;
        5. map[key] = value 与 map.insert({key, value}) 一开始以为它们的效果一样,后来发现二者有所不同;当同一个键出现多次添加时,第一种写法会保留最后一次的结果,相当于重新赋值;第二种写法会保留最早的结果,相当于添加前会先检查键是否存在,如果已经存在,则不会再次添加;
      3. 删除元素
        1. 三种操作
          1. c.erase(k) // 删除所有关键字为 k 的元素,返回被删除元素的数量;
          2. c.erase(p) // 删除迭代器 p 指向的元素,返回指向下一个位置的迭代器,p 不能为 end;若 p 是最后一个元素,则返回 end;
          3. c.erase(b, e) // 删除 b/e 范围中的元素,返回 e;
      4. map 的下标操作
        1. 只有 map 和 unordered_map 支持下标操作,其他类型的关联容器(multimap, set, multiset)都不支持;
        2. 下标操作方式
          1. c[k] // 注:如果在 c 中没有找到 k,则会插入一个 k 的键,并进行值的初始化,因此这种方式只适合用于访问非 const 的 map;
            1. 怎样实现值初始化?估计跟值本身的类型有关;
            2. 返回什么东西?据说是返回了 mapped_type 对象,是一个左值;
          2. c.at[k] // 如果 k 不在容器中,会抛出一个异常,不会自动添加一个元素;
        3. 对于 map 的迭代器,解引用会获得元素的类型,即 pair;而下标操作会得到 mapped_type 类型,即键值对中的值对象类型,而且返回的是左值,意味着可以对它进行读和写;
      5. 访问元素
        1. 操作方式
          1. c.find(k) // 返回第一个指向 k 的迭代器,若找不到,返回 end;
            1. 对于 map,还有一种访问方法为 m[key],如果 key 不在 map 中,会自动添加这个 key;不过这种访问方式竟然也叫做下标操作;
          2. c.count(k) // 返回键等于 k 的元素数量;对于不允许重复的容器,返回要么 0 要么 1;
          3. c.lower_bound(k) // 返回第一个指向关键字不小于 k 的迭代器
          4. c.upper_bound(k) // 返回第一个指向关键字大于 k 的迭代器
          5. c.equal_range(k) // 返回关键字等于 k 的一对迭代器 pair;如果 k 没找到,则 pair 的两个成员都是 end;
        2. 可以通过 find(获得迭代器)和 count 进行配合来遍历 multiset 或 multimap;
        3. 也可以使用 upper_bound 和 lower_bound 配合来完成(当二者返回相同的迭代器时,表示键没有找到);后者的用法比前者更加直观;
        4. 最直观便捷的方法是使用 equal_range,返回相等的键的首尾迭代器;
    4. 无序容器
      1. 据说有4种无序容器,但书上只提到了 unordered_set, unordered_map,还有另外两个不知是什么;
        1. 后来发现是 unordered_multiset, unordered_multimap;
      2. 实现原理:哈希函数+桶;(如果关联容器的关键字不需要固定顺序的话,理论上使用无序容器会得到更好的性能,也更为简单)
      3. 无序容器的操作与有序容器的操作是一样的,正常它们之间可以相互替代,但无序容器也有一些自己的特有操作(主要跟桶有关)
        1. c.bucket_count() // 查询容器当前的桶数量
        2. c.max_bucket_count() // 查询容器能容纳的最多桶数量(好奇这个数字难道不是可以无限增长的吗?)
        3. c.bucket_size(n) // 查询第 n 个桶中的有多少个元素;
        4. c.bucket(k) // 查询关键字 k 在哪个桶中,返回桶的编号;
        5. local_iterator, const_local_iterator // 查询桶中元素的迭代器类型
        6. c.begin(n), c.end(n), c.cbegin(n), c.cend(n) // 第 n 个桶的头尾迭代器
        7. c.load_factor // 查询每个桶的平均元素数量,返回的值为 float 类型;
        8. c.max_load_factor // 查询容器允许每个桶存放的最多元素数量,返回值为 float 类型;当元素数量超过这个值时,容器一般会创建新的桶来安放;
        9. c.rehash(n) // 重新组织桶,使得当前需要使用的桶数量大于等于 n,且大于所有元素总数/每个桶的平均容量;
        10. c.resize(n) // 重新组织桶,使得 c 能够容纳 n 个元素且不用 rehash ;
      4. 无序容器对关键字类型的要求
        1. 关键字的类型需要支持 == 运算符,同时由于需要对关键字进行哈希运算,因此关键字的类型也需要满足哈希函数的要求;如果不自定义自己的哈希函数,则默认支持的类型包括内置类型、标准库 string 类型、智能指针类型等3种;
        2. 如果提供自定义的哈希函数和等号运算符(==)重载函数,则可以使用自定义的关键字类型创建无序容器;
          1. size_t hasher(const Sales_data &sd) { return hash() (sd.isbn()) }; // 自定义哈希函数;
          2. bool eqOp(const Sales_data &lhs, const Sales_data &rhs) { return lhs.isbn() == rhs.isbn() }; // 等号运算符重载
          3. using SD_multiset = unordered_multiset<Sales_data, decltype(hasher)*, decltype(eqOp)*>; // 创建自定义无序容器类型
          4. SD_multiset bookStore(42, hasher, eqOp); // 类对象实例化
  11. 动态内存
    1. 动态内存与智能指针
      1. 两种智能指针,但有三种类型,分别是 shared_ptr(多个指针指向同一个对象), unique_ptr(一个对象只被一个指针关联), week_ptr(指向 shared_ptr 关联的对象);
      2. 智能指针支持的操作(智能指针定义在头文件 中;
        1. share/unique 都支持的操作
          1. shared_ptr sp // 声明一个空的 shared_ptr,指向对象的类型为 T
          2. unique_ptr up // 声明一个空的 unique_ptr,指向对象的类型为 T
          3. p // 将 p 作为一个条件判断,若非指针则返回 true,若为指针则返回 false
          4. *p // 解引用指针指向的对象;
          5. p->m // 相当于 (*p).m
          6. p.get( ) // 获取 p 存储的指针(返回结果为普通的内置指针类型,而非智能指针类型,注:若智能指针释放了对象,则该指针也跟着失效了)
          7. swap(p, q), p.swap(q) // 交换 p, q 中存储的指针
        2. 仅 share_ptr 支持的操作
          1. make_shared(args) // 返回一个 shared_ptr,指向 args 创建的类型为 T 的对象;示例:
            1. auto p1 = make_shared(42);
            2. auto p2 = make_shared(10, ‘9’)
            3. auto p3 = make_shared(); // 初始化对象的值为 0
          2. shared_ptr p(q) // 使用 q 来初始化一个 p,q 的计数器会增加1,p 和 q 必须都是 shared_ptr 类型,而且 q 指向的对象类型需要能够转化为 T*;
          3. p = q // 用 q 给 p 赋值,p 和 q 必须都是 shared_ptr 类型,它们指向的对象类型能够相互转换;有意思的是,此举会递增 q 的引用计数,并递减 p 的引用计数,如果 p 原来的引用计数为 1,递减后变为 0,则 p 原指向的内存会被释放;假设 p 的引用次数原来为 3 呢?这个时候变成了 2,原内存不会释放,但指向了新对象,接下来要如何确保新对象的内存会被释放?猜测 p 在判断完原对象是否需要释放后,新对象的引用计数应该是重新从1开始了;
            1. 新对象的引用计数没有重新开始,而是貌似共享与 q 相同的引用计数,猜测这个引用计数应该是一个 static 变量;
          4. p.unique() // 返回一个布尔值,如果 p.use_count 为 1,表示是唯一的,返回 true;否则返回 false;
          5. p.use_count() // 返回与 p 共享对象的智能指针数量;据说要计算很久,仅用于调试
            1. 好奇如果 p.use_count( ) 要很久,那么意味着 p.unique( ) 也要很久,因为 p.unique 好像会调用 p.use_count?
      3. 正常情况下,当某个 shared_ptr 局部变量离开了它的作用域后,编译器会检查它的 use_count 是否递减为 0,如果是的话,会自动回收它指向的对象;但有一种情况,即 shared_ptr 被存放在某个容器中,而容器一直在用,但实际上里面的 shared_ptr 可能已经不再需要了,此时 shared_ptr 指向的对象会一直存在,直到容器被销毁为止;此种情况的副作用就是会有一些内存被持续无效占用,一个比较好的习惯是使用容器的 erase 方法删除那些已经不再需要的元素;
      4. 使用动态内存的一个常见原因是为了在多个对象之间共享数据(在第12章最后一节的文本查找程序,非常完整的展示了这种用法场景)
      5. 直接管理内存
        1. 使用 new 动态分配和初始化对象
          1. 如果没有显式的初始化,则对于内置类型来说,默认初始化的值是未定义的;对于类类型来说,默认初始值依赖于其构造函数;
            1. int *pi = new int; // 对于内置类型,此种隐式的默认初始化方式的值,是未定义的;但如果使用 int() 的效果则不同了;
          2. 显式初始化的方法很多,可以列表初始化,圆括号初始化,值初始化(空括号)等;
            1. vector *pv = new vector{1, 2, 3};
            2. int *pi = new int(42);
            3. string *ps = new string(10, “hi”);
            4. int *pi2 = new int(); // 空括号的值初始化,值为0;
          3. 括号内只有单一初始器的时候,才可以使用 auto 来自动推断类型,示例
            1. auto p1 = new auto(obj) // 正确
            2. auto p2 = new auto{a, b, c} // 错误,因为括号内非单一的初始化器;
        2. new 支持分配 const 对象,示例: const int *pi = new const int(1024);
        3. 当内存耗尽,无法分配内存时,new 会抛出 bad_alloc 的错误;通过传递 nothrow 参数,可以避免抛出错误,同时分配一个空指针,示例:int *pi = new (nothrow) int(1024);
          1. bad_alloc 和 nothrow 都定义在头文件 中;
          2. 在什么场景下需要用到 nothrow 这个参数呢?
        4. 通过 delete 来释放内存,示例 delete pi;
          1. 相同的指针释放多次,或者释放非 new 创建的指针,都将产生未定义的行为;
            1. 非动态分配的内存,貌似有其他机制来回收,如果我们手工回收了,貌似后续的回收就会出错?
            2. 这里面估计涉及到栈回收的机制,很好奇栈回收是如何实现的?
          2. 传递给 delete 的指针必须指向动态分配的内存,或者是一个空指针;
          3. const 对象虽然不可被改变,但可以被销毁;
        5. 在函数的局部作用域中分配的动态内存,当离开作用域后,由于局部变量被销毁,会导致此块动态内存无法被释放,因此内存的释放需要在局部作用域内进行
          1. 想了下,这句话可能不完全对,如果函数将指针返回,则可以在函数外部销毁动态内存;
        6. 当一个指针指向的动态内存被 delete 显式释放以后,该指针会变成空悬指针,为避免误用空悬指针,在释放后,应该显式的将指针置为 nullptr,这样就可以避免误用发生错误
          1. 仅管如此,这种做法仍然无法避免指向同一块内存的其他指针发生问题,仅仅是对当前指针有效;其他共享同一内存地址的指针依然是空悬指针好像;
      6. shared_ptr 和 new 的结合使用
        1. 由于普通指针和智能指针不能隐式转换,因此不能用一个普通指针来给智能指针赋值,需要使用直接初始化的方法
          1. shared_ptr p1 = new int(1024) // 错误做法
          2. shared_ptr p1(new int(1024)) // 正确做法
          3. 同理,也不可定义返回类型为智能指针的函数后,最后返回的却是普通指针,需要在函数 return 语句中显式的对它们进行转换,例如 return shared_ptr(new int(p))
        2. 定义和改变 shared_ptr 的其他方法
          1. shared_ptr p(q) // p 管理 q 指向的对象,q 必须指向动态分配的内存,且 q 的类型能够转换为 T 类型;这个做法也意味着,p 接下来将接管原来 q 所指向的对象,当 p 的引用计数递减为 0 时,对象将被销毁,q 将变成空悬指针;
          2. shared_ptr p(u) // 让 p 接管原来 unique_ptr 所指向的对象,并将 u 置空;// 貌似实现了 unique_ptr 和 shared_ptr 之间的资源交接?
          3. shared_ptr p(q, d) // 此处的 d 是一个谓词(即函数),与第一条的区别在于这次使用 d 自定义操作来替代 delete 释放内存;
          4. shared_ptr p(p2, d) // p 是 p2 的拷贝,二者的区别在于 p 使用 d 来替代 delete
            1. 怎么感觉跟上一条好像没有什么区别?
          5. p.reset() // 若 p 是唯一指向对象的 shared_ptr,则对象将被销毁并释放内存
          6. p.reset(q) // 令 p 指向 q
          7. p.reset(q, d) // 令 p 指向 q,且使用 d 替代 delete 来释放内存;
        3. 使用一个内置指针来访问智能指针指向的对象是很危险的,因为我们无法预估什么时候对象已经被销毁了;
        4. get 方法可以获取智能指针存储的内置指针,它仅在一种情况下使用,即有些旧代码不能传递智能代码,只接受普通指针;而且,要确保指针传递后,不会被 delete,不然原来的智能指针就空悬了,而且还会发生二次 delete;
        5. 永远不要使用 get 获得的普通指针,去初始化另外一个智能指针;原因:同一块内存,被两个智能指针指向,它们必然发生二次 delete,最后产生未定义行为;
        6. p.unique 可以用来确定当前指针是否是唯一的用户,如果不是的,则可以为它重新赋值指向新对象,同时不会导致原有的对象被销毁;
      7. 智能指针和异常
        1. 普通的内置指针,在发生异常时,不会自动释放资源,需要显式的手动释放;因此,对于某些没有析构函数的类,我们可以借鉴智能指针+自定义构造函数的方式,来完成对资源的自动释放,示例如下:
          1. connection c = connect(&d);
          2. shared_ptr p(&c, disconnect);
          3. 由于自定义了析构的函数,当 p 销毁时,它不会调用默认的 delete,而会使用初始化时传入的 disconnect 函数,来对资源进行释放;
          4. 注意此处的 disconnect 函数在定义的时候,需要以智能指针为参数,如果不是的话,需要另外定义,包裹原来的 disconnect 函数;
        2. 智能指针应避开的陷阱
          1. 避免使用同一个内置普通指针,初始化或 reset 多个智能指针;
          2. 不 delete get 返回的指针(原因:对象由智能指针管理,不用由返回的普通指针操心,否则很容易造成重复释放资源)
          3. 避免使用 get 结果初始化或 reset 另外一个智能指针(感觉同第一条);
          4. 当使用 get 的返回指针时,牢记它随时有可能失效;
          5. 如果智能指针管理的资源,不是使用 new 动态分配的内存,记住在初始化的时候,传递一个专用的删除器,用来释放资源;
      8. unique_ptr 指针
        1. unique_ptr 的初始化只能使用直接初始化的方式
        2. unique_ptr 的操作
          1. unique_ptr u1 // 空指针,指向 T 类型的对象;
          2. unique_ptr<T, D> u2 // 同上,区别在于使用 D 类型的函数来释放内存
          3. unique_ptr<T, D> u3(d) // 同上,区别在于显式指定了 D 类型的函数 d;
          4. u = nullptr // 释放 u 指向的对象,置为空;
          5. u.release() // 置空 u,但同时会返回内部保存的普通指针;注意,对象没有被销毁,因此,接下来如果没有使用另外一个智能指针接替管理对象,对象将会进入手动管理的状态,需要显式的 delete 来释放,以避免内存泄露;
          6. u.reset() // 释放并置空,u 原来的关联对象会被销毁
          7. u.reset(q) // 将 u 改为指向 q 的关联对象,u 原来的关联对象会被销毁
          8. u.reset(nullptr) // 释放并置空,u 原来的关联对象会被销毁,跟 u.reset() 有什么区别?
        3. 由于 unique_ptr 与对象唯一绑定,因此它是不能被拷贝或者赋值的,只能被释放或者 reset;只有一种例外情况,即它在局部作用域结束之前被 return;
        4. unique_ptr 的删除器也是可以被重载的,格式跟 shared_ptr 也相同,示例: unique_ptr<objT, delT> up(new objT, fcn) // 注:其中 fcn 是 delT 类型的函数对象;
      9. weak_ptr 指针
        1. weak_ptr 与 shared_ptr 指向同一个对象,区别在于 weak_ptr 不会管理对象的生命周期,也不会影响 shared_ptr 的引用次数(据说这种行为叫做弱共享),shared_ptr 销毁对象的时候,是不会顾虑是否存在 weak_ptr 指向这个对象;销毁后,会导致 weak_ptr 变成了空悬指针;
          1. weak_ptr 的使用场景是什么?
        2. weak_ptr 的操作方法
          1. weak_ptr wp; // 声明一个空的 weak_ptr
          2. weak_ptr wp(sp) // 使用一个 shared_ptr 来初始化 weak_ptr,它们指向共同的对象,其中 sp 的类型需要能够转换成 T 类型;
          3. w = p; // p 可以是一个 shared_ptr,也可以是一个 weak_ptr,赋值后它们共享对象;
          4. w.reset(); // 将 w 置为空;
          5. w.use_count(); // 查询与 w 共享对象的 shared_ptr 的数量
          6. w.expired() // 若 w.use_count 为0,则返回 true,否则返回 false;貌似 true 意味着指针已经好像无效了,或者被置空了;
          7. w.lock() // 若 w.expired 为 true,则返回一个空 shared_ptr,否则返回一个指向共享对象的非空 shared_ptr;
            1. 由于 weak_ptr 指向的对象不受其控制,随时有可能被销毁,因此使用之前进行检查变成是必要的(若如此,为何不直接使用 shared_ptr?)
      10. 关于动态内存和智能指针(或者指针)的使用,今天有了一个新的理解;当变量在函数体内初始化时,它被放在了栈内存中,当栈的生命周期结束时,变量的内存也会被回收;但动态内存改变了这一局面,它让变量变成了自由人,不再受到栈的控制,它被存储到了堆上面;当我们仍然在函数体内定义智能指针类型的变量,去管理分配在堆上的这块内存;
        1. 事实上,当使用智能指针时,变量被存储在哪里,对使用者来说,变得没有什么区别;但是,由于栈本身的空间有限,当变量本身的数据很大时,存储到堆上变成是必须的了;
        2. 智能指针可以是函数体内的某个变量,其实也可以是函数体内某个对象的某个数据成员,它可以很灵活的出现在各种地方;
        3. 另外,当这些数据需要给不同的对象进行共享时,通过动态内存存放在堆中,貌似也是必须的;不然会局限于某个对象的作用域;
        4. 那为什么不使用 static 变量呢?它也可以实现变量生存在局部作用域外?答:虽然 static 变量离开局部作用域后仍然活着,但对它的访问,貌似只能通过局部作用域进行,还不够自由;
    2. 动态数组
      1. 可以用来批量给对象分配动态内存;动态数组并不是数组类型,只是一种叫法;
        1. int *pia = new int[42] // 使用中括号来表示分配的数量
        2. typedef int arrT[42]; // 感觉使用 using arrT = int[42] 看起来更直观一些;
        3. int *p = new arrT; // 分配一个可容纳42个 int 元素的内存空间;
        4. 可以在中括号后面[ ] 加上一对圆括号或者花括号,来进行值初始化或列表初始化;
          1. int *p = new arrT{1, 2, 3, 4};
          2. 如果列表元素数量小于数组大小,余下的元素会做值初始化;如果多了,则会报错;
        5. 批量销毁动态数组中的元素 delete [ ] p // 与常规做法的区别在于,中间多了对空的中括号;如果忽略了中括号,会发生未定义的行为;
          1. 这点昨天在测试 CUDA 用法有看到,当时还觉得这个写法很奇怪,原来是出自这里;不过 CUDA 使用了 cudaFree 函数进行封装,感觉封装后更加直观容易理解了;
      2. 可以使用 unique_ptr 来管理动态数组,这样确保能够自动释放资源
        1. unique_ptr<int[]> up(new int[10]); // 注意这里使用了 new int[10],而不是 new int;
        2. 由于 up 绑定的是一个数组,因此不能使用点运算符了,也不能使用箭头运算符,但倒是可以使用下标运算符,例如 up[2] 来访问相应的元素;
      3. shared_ptr 不支持管理动态数组,除非自定义删除器;
        1. shared_ptr<int[]> sp(new int[10], [](int *p){ delete [ ] p; }); // 用 lambda 定义的删除器作为第二个参数;
        2. sp.reset(); // 由于 sp 是一个智能指针,所以需要使用 reset 方法,还释放它指向的内存;它会自动调用之前定义的 lambda 方法;
        3. 但 shared_ptr 不支持下标运算符和算术运算符,所以就算成功声明了管理数组的 shared_ptr,貌似也不太好用;使用的时候,还需要用 get 获取内置指针来处理,绕了一个大圈子;
          1. 但是刚又测试了一下,发现可以使用下标运算符,好奇怪;
      4. allocator 类
        1. 目的:为了实现内存分配与对象构造的分离,使得内存管理更加灵活,而使用 new 时,这两个动作是连在一起的,可能会造成内存使用不允分的浪费,另外对于没有默认构造函数的类也无法使用 new;allocator 定义在头文件 中;
        2. allocator 是一个模板类型,它分配的内存是原始的,未构造的;
        3. 支持的操作
          1. allocator a,定义一个 allocator 对象 a,它可以用来为 T 类型对象分配所需的内存空间;
          2. a.allocate(n),分配存储 n 个 T 对象所需的内存空间,这段内存空间是原始的,未构造的;
          3. a.deallocate(p, n),从指针 p 的位置开始,释放之后 n 个 T 类型对象所占的内存空间
            1. p 必须是之前调用 allocate 返回的指针,它指向整段分配内存的起始位置;
            2. 在调用 deallocate 之前,需要先对这 n 个对象执行销毁的动作;
            3. n 必须是先前调用 allocate 时分配的大小;(如果是要释放整段内存,不知为何搞得这么复杂,莫非 a 里面没有保存起始位置和大小?)
          4. a.construct(p, arg),构造一个 T 类型的对象,并存储在 p 指针指向的原始内存位置中;
            1. arg 做为参数(可零个或多个),用来传递给 T 的构造函数,用于构造对象使用;
          5. a.destroy(p),对 p 指针指向的对象,执行销毁动作(即析构);
            1. 奇怪构造的时候,有传递 args 构造函数参数,为什么销毁的时候,却没有传递析构函数?
        4. 在使用 allocator 分配的内存时,需要先使用 construct 方法构造对象;未构造的内存使用会产生未定义的行为;
        5. 拷贝和填充未初始化内存的方法
          1. uninitialized_copy(b, e, b2),拷贝迭代器 b/c 指向的范围内的元素,到 b2 迭代器指向的位置及之位置;需要确保 b2 之间的空间足够大,能够容纳所有 b/e 之间的元素;返回 b2 递增之后的迭代器;
          2. uninitialized_copy_n(b, n, b2),拷贝从迭代器 b 开始之后的 n 个元素,到 b2 指向的位置之后;同样需确保 b2 之后的空间足够大;
          3. uninitialized_fill(b, e, t),将值 t 填充到 b/e 指向的范围段;
          4. uninitialized_fill_n(b, n, t),将值 t 填充到 b 之后的 n 个位置;
          5. 经过测试发现,uninitialized_copy 事实上会调用类的拷贝构造函数;如果类没有正确定义拷贝构造函数,uninitialized_copy 的行为有可能会出错;
        6. 当某个类的静态数据成员声明为 allocator 类时,还需要在类外部定义这个数据成员,定义的方法为
          1. allocator SomeClassName::alloc;
          2. 貌似这种用法是使用默认初始化?
          3. 如果不在类外部进行定义,编译时会报错,提示静态变量未定义;
          4. 猜测原因:由于 allocator 类的目标在于实现内存分配和对象构造的分离,因此声明的时候,仅是分配了内存,并未构造任何类型的对象,这个时候访问这段内存将会是未定义的行为;所以需要另外对其进行定义,才可避免未定义的访问行为;
    3. 使用标准库:文本查询程序
      1. 程序设计
        1. 开始程序设计的一个好的方法,是先罗列出程序允许的操作,然后通过操作可以推导出合适的数据结构,例如选择使用 vector,set,map等;
        2. shared_ptr 可以用于类之间实现共享数据,即 B 类的数据为 A 类数据指针或迭代器;
        3. 但貌似这样两个类对象之间的耦合很深,其中一个对象的有效性,依赖于另外一个对象的生命周期;有时候如果不小心已经销毁了数据对象,则可能会出现未定义行为;
  12. 拷贝控制
    1. 拷贝、赋值、销毁
      1. 类是一种数据的抽象(很像 SICP 第2章的内容),它创建了一种新的数据类型,新数据类型也意味着需要有对应新的运算行为,这样才能够发挥数据类型的抽象作用;除了成员函数外,任何数据类型通常需要有一些通用的基本操作,包括拷贝、赋值、移动、销毁等;如果我们不显式的定义这些行为,编译器会自动定义(但使用效果很可能与我们的预期不一致),因此,在设计类的时候,就要优先先设计好这些基本行为的操作方式;之后再考虑它们的成员函数(好奇当我们只是将类的实例对象,做为一种数据容器进行使用时,设计定义以上四种基本行为,是否仍然有必要性?)(答:有时候有必要,取决于是否可以让计算更加简便,让代码拥有更高层次的抽象);
      2. 拷贝构造函数:只有一个参数,参数是自身类型的引用,其他任何额外参数都有默认值;
        1. 如果我们不自己显式定义一个,编译器就会帮我们合成一个;但有时候合成的可能不是我们想要的;合成的拷贝构造函数,数据成员的类型决定了成员会如何被拷贝,内置类型直接拷贝,类类型由使用其默认构造函数,数组类型则依次拷贝其中的每个元素;
        2. 拷贝构造函数用于拷贝初始化的场合(感觉跟直接初始化很像,只是我们不是直接提供数据成员参数,而是提供一个对象,然后用这个对象的数据成员,来初始化我们新建的对象的数据成员)(听说如果在类里面定义了移动构造函数,则拷贝初始化有时会使用移动构造函数来完成,暂时还不知道为什么这么做)(答:这么做是为了减少拷贝的性能开销,用移动更快)
        3. 参数必须为引用类型的原因在于这样一个使用场景,即某个函数定义的形参是非引用类型,因此当用实参给形参赋值时,会触发拷贝构造行为,此时开始调用形参的拷贝构造函数,如果拷贝构造函数的参数是非引用类型,则意味着需要调用构造函数的形参的拷贝构造函数,这样一层一层的嵌套循环进去,无法终止;因此,只有设置为引用类型,才能避免此问题;(从性能上来说,正常也应该设计为引用类型)
      3. 拷贝初始化触发场景包括:
        1. 使用等号赋值运算符 =
        2. 对象做为实参传给调用函数的形参;
        3. 函数调用返回非引用类型的对象
        4. 用花括号列表初始化数组中的某个元素
        5. 某些类类型(如容器)在新增对象成员时(例如标准库容器的 push_back 之类的操作);
      4. 赋值运算符
        1. 同拷贝构造函数,如果没有自定义赋值运算符,编译器会自动合成一个;
        2. 重载赋值运算符本质上是一个函数,由两部分构成:关键字 operator 加上运算符号,例如 operator=;该函数以赋值等号左侧的运算对象为参数,返回左侧运算对象的类型的引用;左侧运算对象隐式的绑定到函数的 this 参数;
          1. 示例:Foo& operator&(const Foo&);
        3. 如果某个成员变量是指向动态内存的内置类型指针,则在定义赋值运算符时,记得应先释放原来的动态内存,然后再赋值新分配的内存;
        4. 经测试发现,当使用等号赋值时,编译器经常会跳过赋值运算符函数,转为调用拷贝构造函数,不知为何(即使我们没有定义拷贝构造函数也是如此,显然此时编译器会自动帮忙合成一个拷贝构造函数);
      5. 析构函数
        1. 析构函数也是类的成员,由波浪号+类名+圆括号组成,它不接受任何参数,也没有返回值;由于没有参数,也意味着不能实现重载,一个类只有一个析构函数;示例
          1. class Foo {
            1. ~Foo(); // 此处即为析构函数
          2. }
        2. 内置类型的成员,没有析构函数,因为没有销毁的工作;类类型的成员,执行类类型相应的析构函数(好奇如果最终所有成员,包括嵌套的类类型成员,都是由内置类型组成的,那么所谓的销毁释放资源到底做了些什么工作呢?)(在离开变量的作用域的时候,变量使用的内存资源会被释放,估计是使用栈的内存回收机制来实现内置类型的资源回收吧)
        3. 执行析构的五个场景
          1. 当变量离开了作用域;
          2. 临时对象的创建表达式完成了运算;
          3. delete 动态内存;
          4. 容器销毁时,其元素成员跟着销毁;
          5. 类对象销毁时,其数据成员跟着销毁;
        4. 当没有自定义析构函数时,编译器会自动合成一个,合成的析构函数的参数体为空;
        5. 事实上,析构函数本身并不会销毁任何成员,它更像开始析构工作的一个符号;真正的销毁,发现在析构函数执行之后;
        6. 一般来说,析构函数的函数体内为空,但是,当有数据成员是指向动态分配内存的指针时,应记得在函数体内使用 delete 关键字来回收内存;
      6. 三五法则
        1. 如果一个类需要自定义析构函数,几乎可以肯定它也需要自定义自己的拷贝构造函数和赋值运算符函数;
          1. 需要自定义析构函数,基本上意味着手工管理内存,因此,也即意味着拷贝或赋值的时候,也需要参与管理旧内存的释放;
        2. 需要拷贝操作的类,也需要赋值运算符操作,反之亦然;但不一定需要自定义析构函数;
          1. 拷贝操作和赋值操作其实是某种行为的一体两面,它们都是想实现用一个已经存在的对象,去初始化另外一个新对象;
        3. 如果拷贝构造函数具备生成唯一值的数据成员,那么要特别注意以该类类型为参数的成员,形参需要设置为引用类型,不然传递参数的过程中,会触发拷贝,生成一个不一样的新对象;
          1. 示例:应为 void f(number s&) { … } ,而不是 void f(number s) { … };
        4. 三五法则中的“三”猜测是指三个控制类拷贝的操作:拷贝构造、拷贝赋值、析构;在新标准下,增加了移动构造和移动赋值,所以共同有了“五”;
      7. =default
        1. 可以通过使用 =default 关键字,来显式的告知编译器帮忙合成默认的成员函数(如果不这么做,编译器则是隐式操作,正常它会自动合成,但有时候不会);示例
          1. Sales_data() = default; (合成默认构造)
          2. Sales_data(const Sales_data&) = default; (合成拷贝构造)
          3. Sales_data& operator=(const Sales_data&) = default; (合成赋值运算符)
          4. ~Sales_data() = default; (合成析构)
      8. 阻止拷贝
        1. 不管是显式的自定义,还是隐式的默认合成,大多数类应该定义构造函数、拷贝函数和析构函数,但是在某些特殊的场景下,阻止类的拷贝功能有时候是有必要的(为什么?什么时候?);
        2. 可以通过 =delete 关键字,来显式的告知编译器阻止成员函数的执行;( =delete 标注的成员函数,有个奇怪的名称,叫做删除的函数)
        3. =delete 必须出现在函数声明的地方,而不能像 =default 一样定义在函数外部;原因:阻止成员函数的执行,意味着在任何可能发生的调用前,提前知道它是一个删除函数,从而阻止调用的发生;如果定义在函数外部,则在声明和定义之间的调用,不会被阻止;
        4. =default 只能用于构造和拷贝函数,但 =delete 可以用在所有的成员函数身上(但暂时不确定这种用途的目的是什么)
        5. 析构函数不可以标记为 =delete,不然对象资源无法被释放;
        6. 编译器自动合成的拷贝控制成员有可能标记为删除的(为什么?)规则:如果类里面有一个成员不能默认构造、拷贝、复制和销毁,则对应的合成版本的成员函数将被定义为删除的(据说引入这条规则的原因:在于避免创建无法销毁的对象出来;由于无法自动合成,便意味着程序员需要手工编写,如果没有编写,在使用的时候,就会出现报错,从而提醒编写)
        7. 如果类有个成员是 const 类型,则不能使用合成的拷贝函数,因为我们无法给一个 const 成员做赋值操作(这么说只能自定义一个拷贝函数了?);
        8. 如果类有个成员是引用类型,则编译器不会合成默认的构造函数,因为初始化构造需要从外部传入一个对象的引用,但合成的默认构造函数却没有参数;同时合成的拷贝赋值运算符函数也会被定义为删除的,因为对象已经初始化过了,引用类型的成员,已经指向了某个对象,此时再次赋值,并不会改变引用的指向,而是改变了引用指向的对象的值,显然这不是我们所期望的;
        9. 原则:如果类存在不能拷贝、赋值或销毁的成员时,那么合成的拷贝控制函数成员,就会被定义为删除的;(估计是为了更加安全的考虑)
        10. 在 C11 标准之前,为了阻止拷贝,使用将成员定义为 private 的方法(旧方法);对于友元,则通过只声明不定义的方法;(当使用这种方法时,如果某些代码不慎进行拷贝操作,编译器将会抛出错误);在新标准中,直接使用 =delete 关键字来声明就可以了;
          1. 如果 C++ 像 python 一样,没有所谓的自动合成功能,可能就不会有这么多事了;但问题有没有可能是因为 C++ 支持手工管理内存,导致引入了以上一系列操作的复杂性?因为手动分配内存,意味着还要手动的回收内存,从而使得垃圾自动回收机制失效了?
    2. 拷贝控制和资源管理
      1. 据说,管理类外资源的类,需要定义拷贝控制成员(但什么是类外资源呢?可能指资源在类的外部)(动态内存即是一种类外资源,因此如果有成员是内置指针类型的话,意味需要定义专门的析构函数来释放分配的内存空间;而当一个类需要定义析构函数时,也意味着它需要自己的拷贝控制成员函数了);
      2. 类的拷贝操作,有两种语义,一种是像值的独立状态,一种是像指针的共享状态;
      3. 类里面的数据成员分两种,一种是内置类型,一种是指针类型;对于前者,拷贝的时候,应该直接拷贝值;对于后者,则有两种处理方案,即可以让它们像值,也可以让它们像指针;
      4. 定义赋值运算符时,为了能够应对自赋值的情况,在释放资源之前,需要先拷贝一份资源到临时对象,然后再释放;不然如果先释放,就无处可以拷贝了;
        1. 大多数赋值运算符,结合了拷贝函数和析构函数的功能;
      5. 管理类外资源的释放,最好使用 shared_ptr,如果不能用,则只能定义自己的引用计数来管理了(感觉差不多相当于自己实现 shared_ptr 了);
      6. 当使用指针的时候,不管是智能指针,还是普通的内置指针,它都意味着手工管理内存了,差别在于智能指针会将内存释放,而内置指针而不会;当然,内置指针指向的内存,仅为手工动态分配的时候,才需要手工释放,如果是在栈中自动分配的,则不需要手工释放;问题是,要如何解决内置指针的空悬问题?以及,如何才能在栈中内存呢?
    3. 交换操作
      1. 据说管理资源的类,通常还会定义一个 swap 函数,不知为何?答:为了减少移动内存的开销,转而通过移动指针来实现目的;那么对于没有管理资源的类呢?设计一个 swap 函数是否还有必要?(据习题 38 说是没有必要!)
      2. 标准库的 swap 函数,使用的是类的拷贝构造和赋值函数,潜在的内存开销可能很大,因此我们可以通过定义自己版本的 swap 函数,来减少这种开销;
      3. 定义 swap 不是必要的,但是定义了后,有利于优化函数的性能;
      4. 定义 swap 还有另外一个好处是它可以用来实现赋值运算符的重载;正常情况下,赋值运算符的重载,需要小心处理自赋值可能出错的情况,而 swap 原理,使用在销毁旧对象之前,会先做一份副本,所以即使出现自赋值的情况,它也是安全的;
      5. 假设我们通过 using std::swap 进行了声明后,此时在代码中是可以直接使用 swap 进行函数调用,而不再需要前缀 std;但实际上,当 swap 参数的类型有定义自己版本的 swap 函数时,此时编译器会自动优先匹配类类型的自定义版本;
      6. 内置类型是没有自己的自定义版本的 swap 函数的,因此当 swap 的参数是内置类型时,swap 会匹配标准库版本;
    4. 拷贝控制示例
      1. 那么对于没有管理资源的类呢?设计一个 swap 函数是否还有必要?(据习题 38 说是没有必要!)
    5. 动态内存管理类
      1. 终于知道移动函数的用途了,它的出现,是想尽可能的减少值在内存上拷贝带来的开销增加;另外有些类类型本身也不支持拷贝,例如 io 类和 unique_ptr 类;
      2. move 函数定义在标准库头文件 ;
        1. 作用:获取某个左值变量绑定的资源,断开绑定关系,将变量置为可安全销毁的状态;
    6. 对象移动
      1. 右值引用
        1. 左值和右值是相对表达式而言的;一般而言,左值表达式表示对象的身份,右值表达式表示对象的值;
        2. C11 通过引入 && 来表示右值引用;
        3. 左值引用应绑定到左值,右值引用应绑定到右值;唯一的例外是 const 引用可以绑定到右值;
        4. 右值的生命周期是短暂的,它要么是一个字面值常量,要么是一个求值过程中临时创建的对象;
        5. 右值引用的特征:所引用的对象将要被销毁,该对象没有其他用户;
        6. 左值引用(例如变量)的生命周期是持久的,直到离开作用域时,才会被销毁;
        7. 变量是左值,因此,我们不能声明一个右值引用变量,去绑定一个左值变量;但我们可以显式将这个变量转换成右值,然后赋值给一个右值引用变量;相当于将原变量管理的资源,变成临时的了,并交付给新的右值引用变量进行托管,之后很快就会销毁它;原来的左值变量不再指向原资源了,我们可以销毁原来的左值变量,也可以给它赋值,让它指向一个新的资源,但我们永远无法再通过它,找回旧资源了;
        8. 标准库新函数 move 的调用,只能通过 std::move,而不能仅写 move(原因:为了避免命名冲突,引起歧义);
        9. 错误集
          1. 对于 vector a(100),a[0] 本质上是一个左值表达式,它返回的是一个身份,这个身份背后的值是可以随时改变的;
      2. 移动构造函数和移动赋值运算符
        1. 移动构造函数的作用,跟拷贝构造函数差不多,差别在于它的参数是一个一右值引用,意味着它夺取资源后,还要负责善后,即将源变量与资源的绑定断开,并让源对象处于有效的,且可以被安全销毁的状态;
        2. 在编写移动构造函数的时候,我们还可以加上 noexcept 关键字备注(原因:由于移动构造不分配内存,意味着它不会报异常,因此加上这个关键字后,可以通知标准库无需为发生异常进行准备工作,可以减少一些工作开销,提高性能);
          1. 标准库容器的方法中,提供了即使抛出异常,也不会去改变原有值;为了达到这种效果,它需要做隔离措施;这种隔离是有性能成本的;它的隔离措施就是使用类的拷贝构造函数,因为拷贝构造函数总是新建一块内存进行相关操作,不会去改动原来的值;如果想让标准库取消这种保护措施,就需要通过关键字 noexcept 显式的告知它;
        3. noexcept 关键字是 C11 引入的,它的位置一般置于函数参数列表右侧;如果是构造函数,则置于参数列表右侧和初始化列表冒号之间位置;
        4. 仅当一个类没有任何自己版本的拷贝构造函数和赋值运算符,且所有的非 static 成员都是可以移动的时候,编译器才会为这个类合成移动构造函数和移动赋值运算符;
          1. 内置类型都是可以移动的;
          2. 标准库容器有定义自己的移动函数,所以它们也是可以移动的;
        5. 移动操作永远不会隐式的定义为删除的函数;除非要求编译器合成,但有成员不可移动,这时编译器合成的移动操作,会被标注为删除的函数;
        6. 有四条规则会导致合成的移动操作被标注为删除的
          1. 三条都是因为有成员不可移动(例如有成员是 const 或引用)
          2. 一条则是因为类没有析构函数,
        7. 如果一个类只有拷贝构造函数,没有移动构造函数,那么即使参数传递的是一个右值,也会调用拷贝构造函数(事实上:用拷贝构造函数总是安全的)
        8. 拷贝控制成员总共有5个,当需要自定义其中任何一个时,基本上也意味着需要定义其他4个;一般来说,拷贝一个成员会导致一些额外的开销,有时候这种开销是非必要的,此时就可以通过定义移动拷贝成员,来减少开销优化性能;
        9. 移动操作确实可以带来性能提升,但它的使用需要非常小心,因为它也很容易带来不易发现的错误;正常情况下不建议使用,直至出现性能瓶颈时才考虑使用;
        10. 普通的迭代器,解引用返回左值,当我们将这个左值,做为参数传递给函数时,一般会触发拷贝构造函数;为了提高性能,我们可以使用标准库的 make_move_iterator 函数,将普通的迭代器,转换成移动迭代器;移动迭代器的特点是,它的解引用是返回一个右值;然后当我们将这个右值传递给函数时,会触发移动构造函数,从而提高了性能;
      3. 右值引用和成员函数
        1. 类似于拷贝函数,其他普通的成员函数,也可以定义两个版本,一个版本的参数是左值,一个是右值;这样做的目的,也是为了根据情况提高性能;
          1. 事实上,对于 vec.push_back(“done”) 的情况,由于实参是一个右值,所以它实际上就是在调用参数为右值版本的成员函数;
        2. 在旧标准中,有时候会出现莫名其妙的写法,例如 s1 + s2 = “wow”,为了解决这个问题,新标准引入了一个引用限定符”&”或”&&”,来强制要求左值必须是一个引用,不能是一个值;引用限定符放在函数的参数列表右侧;
          1. 示例: Foo& operator=(const Foo&) &
          2. & 表示 this 必须指向左值,&& 表示 this 必须指向右值;
          3. 引用限定符只适用于非 static 函数,且必须同时出现于声明和定义中;
          4. 如果函数还有 const 限定符,则引用限定符的顺序,应该放在 const 限定符的右侧,即 const &;
        3. 同 const,引用限定符也可以用来区分函数的重载版本;但有一点需要特别注意,当我们使用引用限定符来区分成员函数的重载版本时,必须所有相同参数列表的同名函数都加上重载限定符,不管是一个 & 还是两个 &&(参数不同则没有关系);这一点与 const 的使用方式有所不同;
  13. 重载运算与类型转换
    1. 基本概念
      1. 内置类型已经定义了运算符的运算规则,因此它们的运算看起来简单明了;类做为数据的一种更高层级抽象,运算符重载有利于让类的运算,像内置类型一样的简单明了;
      2. 重载运算符不能有默认实参(除 operator() 以外?)(好奇 operator 这个函数有什么用途?运算符重载?)(operator() 相当于要重载圆括号运算符了?)
      3. 当运算符函数做为某个类的成员函数时,它的第一个参数将默认为 this,并做为运算符的左侧运算对象;
      4. 不是所有的运算符都可以被重载的,有少数几个不行(貌似有4个,分别为双冒号”::”,点星号 “.*”,点号 “.”,问冒号”?:”);
        1. && 和 || 运算符由于存在特殊的性质(顺序导致短路求值),因此不建议对它们进行重载,因为重载的版本会失去这些特性,会导致使用者的不适应,很容易用错;
        2. 逗号和取地址运算符 & 也不建议重载,因为它们已经有了内置的含义,重载容易引起歧义;
      5. 运算符重载时,它的参数必须是类类型,不允许是内置类型;也就是说,我们无法对内置类型的运算符进行重载;
      6. 我们也不能重载不存在的运算符;
      7. 运算符函数可以隐式的调用,就像内置类型的运算一样,也可以通过名字进行显式调用,它们是等价的,例如 operator+(a, b) 等价于 a + b;(前者的方式很像 scheme,这种方式事实上更好,歧义更少)
      8. 在设计类的时候,先想好类需要定义哪些操作,然后再思考这些操作是否适用于通过重载运算符来实现;如果适用的话,一般使用重载的方式是推荐的方法;
        1. 不过这里面也有一个问题,即如果定义了某个重载的运算符,很可能意味着要定义一组,比如整组的算术,整组的逻辑等(当能使用一个时,调用人可能想当然的以为其他运算符也可能可以用)
      9. 仅在运算符的含义非常的清晰明了,不会发生任何歧义的情况下,才建议使用重载,不然可能使用良好命名的普通函数更好;
      10. 重载的运算符函数,可以做为类的成员函数,也可以做为普通非成员函数;
        1. 做为成员函数时,其局限性是第一个参数必须是 this,因此这也意味着这种运算符的调用需要特别注意,不能搞反顺序,不然就匹配不上函数;
        2. 而定义为非成员函数时,则不存在这个限制,只要类型能够转换,就能运行;
    2. 输入和输出运算符
      1. 输出运算符不应负责格式,它应只管输出内容即可;格式应该由调用者另外定义,不然容易失去使用的灵活性;
      2. 输出运算符一般返回 ostream 引用,以便与其他输出运算符保持一致;
      3. 输入输出运算符必须定义成非成员函数,然后被声明为友元,以便读取类的数据成员;
      4. 输入运算符重载时,记得它必须处理读入失败的情况,设计当出现失败的情况时,应采取的行为,以便可以从错误中恢复(这一点输出运算符则不需要)
        1. 有时候输入运算符还需要做一些数据验证的工作,当出现不合格的输入时,可以通过设置流的条件状态来标示失败信息;
    3. 算术和关系运算符
      1. 算术和关系运算符一般定义为非成员函数;
      2. 如果类定义了算术运算符,则一般它也会定义一个复合赋值运算符;当有了复合赋值运算符,其实便可以使用它来实现算术运算符;
      3. 当定义了相等运算符后,一般也应该定义一个不等运算符,后者可以基于前者实现,即直接在前者的运算结果上进行取反;
      4. 定义关系运算符需要满足以下两个前提条件(简单的说,如果存在唯一可靠的 < 定义,则应该考虑定义;如果还存在 == ,则当且仅当 < 的定义和 == 产生的结果一致时,才定义 < )
        1. 对象有顺序关系,具备可比性;
        2. 当两个对象不相等时,意味着必然有一个小于另外一个;
    4. 赋值运算符
      1. 除了前面已经学过的拷贝赋值和移动赋值,事实上还可以定义各种赋值运算符,区别在于接受的参数类型不同;比如接受一个其他类型的参数;
        1. 不管哪种情况,赋值运算符都必须定义为成员函数(为什么?)
        2. 赋值运算符应返回一个当前类型的引用;
        3. 复合赋值运算符一般也应定义为成员函数
    5. 下标运算符
      1. 下标运算符必须是成员函数;
      2. 下标运算符通常以所访问元素的引用做为返回值(好处:下标运算符可以出现在赋值运算符左右的任意一侧)
      3. 一般最好定义常量和非常量两个版本的下标运算符;
    6. 递增和递减运算符
      1. 递增和递减运算符一般建议设置为成员函数,因为它们改变的刚好是所操作对象的状态;
      2. 递增和递减应该同时定义前置和后置版本;但是要如何区分呢?通过给后置版本增一个 int 形参来进行区分;当需要显式调用后置版本时,需要提供一个 int 参数,例如 0;
      3. 后置版本应该返回值,前置版本返回引用;
      4. 当定义好前置版本后,在定义后置版本时,可以使用前置版本来实现相应的功能(它还顺便承包了检查有效性的工作);
    7. 成员访问运算符
      1. 一般在迭代器类和智能指针类中需要用到成员访问运算符;
      2. 箭头运算符必须是类的成员;
      3. 解引用运算符一般是类的成员,但非强制要求;
      4. 箭头运算符永远用于获取成员,这点事实不可通过重载变更;
      5. 重载的箭头运算符,要么返回类的指针,要么返回定义了 operator-> 的类的对象,二者只能选其一,除此之外都将报错;
    8. 函数调用运算符
      1. 当类定义了调用运算符时,它的行为很像函数,由于它还能够同时存储状态,相比函数,用起来会更加灵活,被称做函数对象;
      2. 调用运算符必须是类的成员函数;一个类可以定义多个不同版本的函数调用运算符,只需要在参数数量和类型上面有所区分即可;
      3. 函数对象常常用于泛型算法的实参
        1. 例如:for_each(vs.begin(), vs.end(), printString(cerr, “\n”));
      4. 在编译器的眼里,lambda 的本质上其实就是一个未命名类的未命名对象,这个对象定义了一个函数调用运算符
        1. 如果 lambda 捕获的变量是引用类型,则生成的未命名类不需要数据成员;但如果捕获的变量是值类型,则需要数据成员临时保存捕获变量的值;
      5. 标准库定义的函数对象
        1. 标准库中定义了一组表示算术运算符、关系运算符、逻辑运算符的类,这些类中,定义了相应的函数调用运算符,可用来对参数进行相应的运算;
          1. 示例
            1. plus intAdd;
            2. int sum = intAdd(10, 20);
          2. 算术运算函数对象:plus, minus, multiplies, divides, modulus, negate
          3. 关系运算函数对象:equal_to, not_equal_to, greater, greater_equal, less, less_equal
          4. 逻辑运算函数对象:logical_and, logical_or, logical_not
        2. 这些标准库函数对象,有一个非常好的用途,即可以用来替换算法中的默认运算符
          1. sort(sv.begin(), sv.end(), greater())
        3. 关联容器,例如 map 或 set,即是使用 less 对元素进行排序;
      6. C++ 中有多种可调用对象,包括函数、函数指针、bind 函数创建的对象、重载调用运算符的类、lambda 表达式等;这些不同类型的可调用对象,却可能使用相同的调用形式,即返回类型和实参类型的声明格式,例如int(int, int)
        1. 通过引入 function 类型,从返回类型与实参类型入手,对不同的可调用对象,在调用形式维度进行格式化,然后就可以使用 map 容器来管理这些不同类型的可调用对象了;
        2. 不能使用重载函数的名字,来将重载函数直接存入 function 类型容器中,因为它名字可能会带来歧义,更好的做法是使用指针指向要存储的函数,避免名字的歧义问题;
          1. lambda 则不存在歧义的问题了;
    9. 重载、类型转换和运算符
      1. 类型转换运算符
        1. 格式:operator type() const;
        2. 类型转换函数必须是类的成员函数,它不能有返回类型,形参列表为空,由于不改变待转换对象的内容,故一般定义为 const;
        3. 构造函数可以使用其他类型的对象,初始化创建当前类型的新对象;类型转换函数则可以将当前类型,转换成其他想要的类型(一般来说,这种转换是隐式执行的);
        4. 如果待转换的目标类型,与当前类型不存在明显的一一映射关系,则应该谨慎使用类型转换,因为它很容易带来理解上的歧义,从而导致使用上的错误;
        5. 一般来说,很少为类定义类型转换,因为这种转换隐式的发生,经常容易带来困惑,麻烦多于帮助;仅有一种例外情况:,即向 bool 类型的转换;
          1. int i = 42;
          2. cin << i;
          3. 在旧标准中,以上表达式可以编译通过,但会产生意想不到的结果,即 cin 被隐式转换成 bool 类型,然后进一步变成 int 类型,最终结果为 0 或 1 做左移42位的运算;
          4. 为了避免这个问题,新标准引入了 explicit 关键字,来指明类型转换要显式的执行;
            1. explicit operator int() const;
            2. 例外情况:当表达式出现在条件语句中时,显式的类型转换会被隐式的执行;
          5. bool 类型的转换,也应该定义成 explicit 的;
        6. 只定义了单一实参的非显式构造函数,提供了从实参类型到类类型的转换途径;
      2. 避免二义性的类型转换
        1. 最好不要在两个类之间定义相同的类型转换方法,因为当这个两个类出现在同一个表达式当中的时候,编译器将不知道应该使用哪个类型转换的方法;
          1. 当 A 类定义了向 B 类转换的方法,那就不要在 B 类中再定义向 A 类的转换方法;
        2. 也不要在类中定义两个或以上转换源或转换目标是算术类型的转换,原因:编译器不知道用哪个;
          1. 例如两个转换目标分别是 int 和 double,当对象出现在 long double 场合时,两个转换都不能完全匹配,此时就会导致二义性错误;
          2. 例如两个源分别是 int 和 double,当转入的参数是 long 时,由于不能完全匹配,也会出现二义性问题;
        3. 避免转换目标是内置算术类型的转换;
        4. 除了向 bool 类型的转换后,应该尽量避免定义其他类型转换;
        5. 当存在重载的函数时,如果这些函数的参数所涉及的类,本身提供了类型转换,则将使得重载函数的匹配问题变得复杂起来;
          1. struct A { A(int) } // 接收 int 参数实现初始化
          2. struct B { B(int) } // 接收 int 参数实现初始化
          3. void manip { const A& } // 定义了接收 A 类型参数的函数
          4. void manip { const B& } // 定义了接收 B 类型参数的函数
          5. 当出现 manip(10) 的时候,出现二义性问题,不知应该对应匹配上面的那个函数;
      3. 函数匹配与重载运算符
        1. 表达式的运算符,其候选函数集既包括成员函数,也包括非成员函数;(当如果是以函数名进行调用,或者以对象的方法名调用,则不存在此问题)
        2. 在一个类中,既定义了目标是算术类型的转换,也定义了重载的运算符,则会出现二义性问题;
  14. 面向对象程序设计
    1. OOP 概述
      1. 虚函数:当基类要求某些成员函数必须由派生类各自定义自已的版本时,就将它们声明成虚函数;
        1. 目测目的好像是为了实现多态(即所谓的动态绑定)
      2. C++ 使用类派生列表来实现继承,使用方式为在类名右侧加冒号加基类名称
        1. 示例: class child: public parent { … }
      3. 派生类必须在其内部对所有需要重新定义的虚函数进行声明;
    2. 定义基类和派生类
      1. 基类通常需要定义一个虚析构函数,即使它可能不会被用到
        1. 原因:为了实现动态绑定,即析构的时候,调用正确版本的派生类对象的析构函数;
      2. 基类通过 virtual 关键字来控制派生类是否覆盖它的方法;当定义了 virtual 的成员函数时,即显式的要求派生类必须重新定义自己版本的成员函数;
        1. virtual 是实现多态的关键;一旦某个成员函数标注为 virtual,意味着调用该成员函数时,编译器会去相找最合适的版本;
        2. 目测好像派生类也可以不覆盖基类的虚函数?如果不打算覆盖的话,为何要声明成虚函数呢?
      3. 如果基类把某个函数声明成 virtual,则该函数在派生类中,默认也是 virtual 类型;
        1. 这么说在派生类中,是否写出 virtual 关键字就不是必要的了?是的;
      4. 派生类可以访问基类的 public 和 protected 成员,但不能访问 private 成员;普遍代码则只能访问基类的 public 成员;
      5. 派生类继承基类时,对基类的访问有三种说明符,分别是 public/private/protected(它们之间的区别是什么?)
        1. 目的:用于控制从基类继承来的成员,是否对派生类的用户可见;
        2. 原因:派生类对象实际是多个子对象组成的复合对象;每个子对象负责定义自己的成员;
      6. 当派生类对基类的某个方法进行覆盖时,貌似需要使用 override 关键字?
        1. 后来发现不写这个关键字也行,但写了更好,因为它显式的声明当前函数的意图是要覆盖基类的版本;
        2. 如果不写的话,即使两个函数的形参列表不同,也能够编译通过,因为编译器会将其当做一个新的独立函数,不会发生覆盖;但这种结局可能并不是我们想要的,所以说,写上 override 关键字是更稳妥的做法;不然这种错误是很难发现的;
      7. 每个类控制它自己的成员初始化过程;意味着一个对象的基类成员,是由基类完成初始化的;派生类成员则由派生类完成初始化;
        1. 首先初始化基类的部分,然后按照声明的顺序初始化派生类的部分;
        2. 派生类也可以直接赋值所有成员,但这样就失去了抽象性,会使得维护变得困难;
          1. 例如当有一个基类有多个派生类时,如果每个派生类都直接初始化其所有成员,当基类有发生变化时,变成每个派生类都要修改一遍,工作量大且易出错;
      8. 一个派生类的对象,实际上是由多个子对象组成的;一部分是基类的子对象,一部分是派生类自己的子对象;
        1. 由于一个派生可能由多层继承而来,因此它有可能包括多个基类的子对象;这些基类包括直接基类和间接基类;
      9. 派生类的作用域是嵌套在基类的作用域里面的;
      10. 虽然我们可以显式的在派生类的构造函数中,手工给基类成员赋值,但最好不要这么做,而将参数传递给基类的构造函数进行初始化更加合理;即遵循基类接口的原则
        1. 原因:估计是为了将来的维护方便,保持良好的抽象层次性;
      11. 对于基类的静态成员,它只能被定义一次,并且在整个继承体系中,仅有一个实例;即使有多个派生类也是如此;
      12. 派生类的声明格式,跟普通类的声明一样,并没有派生列表;派生列表仅出现于派生类定义的位置;
        1. class Bulk_quote: public Quote (错误)
        2. class Bulk_quote (正确)
      13. 基类在被继承前,应先定义其所有的成员
        1. 原因:不然派生类无法使用基类的成员;
        2. 如果有部分基类的成员,定义在派生类声明的位置之后,会发生什么情况?
      14. 通过在类名后面加上 final 关键字,可以显式的声明此类不可被继承;
        1. 后来发现还可以给成员函数加上 final 关键字,用来声明该成员函数不可被派生类覆盖;
          1. 看来这个关键字的作用跟 virtual 有点相反,virtual 显式要求覆盖,final 则显式要求不可覆盖
      15. 正常情况下,指针或引用所绑定的对象,应该与指针或引用的类型相同;但是,这条规则在有继承关系的类的身上,不适用,基类指针可以指向派生类(因为派生类包括基类的子对象);
        1. 原因:派生类对象包含基类的子对象;
        2. 但是,反方向是不行的,即派生类指针不可以指向基类;
        3. 从派生类到基类的转换,只对指针或引用类型有效(那么意思是说对其他数据类型无效?)
      16. 基类的指针或引用的静态类型,可能与其动态类型不一致
        1. 目的:为了实现动态绑定;让外部用户不用关心差异,使用更加方便;
      17. 当我们使用一个派生类对象,去初始化或赋值一个基类对象时,只会保留基类子对象的成员,并抛弃派生类子对象的成员;
    3. 虚函数
      1. 动态绑定只会在通过指针或引用调用虚函数时,才会发生;
        1. 目测这个指针还必须是指向基类的指针或引用,如果是指向派生类的指针,则也不存在动态绑定的问题;
        2. 而且调用的还必须是虚函数,因为只有虚函数的场景,派生类的函数定义才与基类不同;
      2. 派生类的虚函数的形参列表必须与基类的虚函数完全一致(不然就应该被视为一个新函数了)
        1. 正常情况下返回类型也应该相同,除非是返回一个指向当前类型的指针或引用;
      3. 如果基类的虚函数使用了默认实参,则这个实参是不可被覆盖的,即实际运行的派生类的虚函数,也会使用基类的默认实参;
      4. 可以通过引入作用域运算符,来显式指定到底要调用哪个版本的虚函数,从而回避了编译器的动态判断
        1. double undc = baseP->Quote::net_price(42);
        2. 这种场景一般发生在派生类的成员函数,想要调用基类的虚函数版本;此时如果不指定版本,就会调用它自己的版本,而不是基类的版本;
    4. 抽象基类
      1. 纯虚函数要实现什么样的设计意图?
        1. 为了避免被实例化对象;
      2. 纯虚函数只有声明,无需定义,通过在右侧加上关键字 “=0” 来表示身份;
        1. 如果要定义的话,必须定义在类的外部,不能定义在类的内部;(为什么呢?)
      3. 含有纯虚函数的类,称为抽象基类;抽象基类不能实例化对象;抽象基类的派生类,如果覆盖了纯虚函数,就可以实例化对象;
      4. 重构:重新设计类的继承体系,并将操作或数据从一个类转移到另外一个类中;同时以前使用了该类的代码不需要进行改动;
    5. 访问控制与继承
      1. 每个类除了负责初始化自己独有的成员外,还控制其派生类是否能够访问自己的成员;
      2. protected 成员
        1. 外部调用者,无法直接访问类的实例对象的protected 成员;
        2. 内部调用者(成员函数)和友元,可以访问当前类和实例对象的protected 成员,但不能访问基类对象的 protected 成员;
          1. 相当于只能访问当前类的基类子对象的成员;不能访问普通基类对象的成员;
          2. 事实上,当访问普通基类对象的成员时,相当于外部用户了;
      3. 派生类说明符的作用,在于控制派生类的用户,对基类成员的访问权限
        1. 问题是这个访问权限不是已经在基类的 public/protected/private 中已经规定过了吗?
        2. 经研究发现,这个说明符在前者的基础上,又增加了一层新的控制,可以覆盖前面的设置;而且这层控制是会继承下去的;
          1. 为什么要搞得这么复杂呢?为了更灵活,在一些场合覆盖既定的设置;
        3. 它还会控制派生类向基类转换的可访问性;
          1. 原因:当不能访问基类子对象时,显示派生类到基类的转换就无法成功;
      4. 在设计类的时候,它总共会有三种用户,三者的可访问性各不相同;
        1. 普通用户:只能访问 public 成员,不可访问 protected 和 private 成员
        2. 实现者及其友元:可以访问所有成员;
        3. 派生类及其友元;只能访问 public 和 protected 成员;不可访问 private 成员;
      5. 友元关系不能传递,也不能够继承,这意味着友元的权限,仅在当前类有限,对当前类的派生类是无效的;
        1. 但是,友元如果是某个子对象的友元,则可以访问子对象的成员;
      6. 通过 using 语句,可以单独改变某个成员对于用户的可访问性
        1. 前提:该成员必须是当前派生类的可访问成员;如果当前类访问不到这个成员,则 using 语句并没有鸟用;
        2. using 语句影响到的用户范围,跟 using 语句出现的位置有关
          1. 如果出现在 public 内,则面向所有用户;
          2. 如果出现在 private 内,则仅面向实现者及其友元;
          3. 如果出现在 protected 内,则面向派生类及其友元;
        3. 示例:派生类私有继承基类,此时派生类的用户不可访问基类成员(即使这个基类成员原本是公有的),然后通过在 public 关键字下面,增加声明 using Base::size ,则派生类的用户便获得了基类成员 size 的访问权限;
      7. 使用 class 关键字定义的派生类,默认私有继承,即等同于 class child: private parent;使用 struct 关键字定义的派生类,默认公有继承,即 struct child: public parent;
        1. 不过不管是公有继承,还是私有继承,好的做法是将 public 或 private 关键字显式的写出来,这样可以避免一些不必要的误会;
      8. struct 和 class 关键字只有两点区别:默认派生类访问说明符,默认成员访问说明符;
    6. 继承中的类作用域
      1. 派生类的作用域,是嵌套在基类中;正常情况下,对变量名的搜索顺序是先从派生类开始,然后逐级向上查找;
        1. 因此,当出现类的类型转换时,例如某个基类指针指向了派生类的对象,则通过该指针访问成员时,会直接从基类开始查找,这时候很可能找不到派生类的成员;
      2. 派生类的成员,将隐藏基类的同名成员;原因:在作用域搜索的时候,当前派生类的同名成员优先匹配;
      3. 可以通过作用域运算符来访问一个基类中被隐藏的同名成员;
        1. 原因:相当于指定了搜索的起始位置;
      4. 原则:派生类除了覆盖基类中的同名虚函数外,最好不要覆盖基类中的其他同名成员;
        1. 原因:这样很容易引起混乱,会产生难以发现的BUG;
      5. 编译器对于类的成员函数调用过程解析:确认静态类型 -> 查找成员函数(由下至上逐级查找)-> 类型检查 -> 判断是否虚函数
        1. 如是,且为指针或引用调用,则确定采用哪个虚函数的版本
        2. 如不是,则生成常规调用;
      6. 由于名字查找先于类型检查,意味着派生类的同名函数会直接被调用,而不管基类中是否存在其他同名函数;
        1. 这也是为什么派生类和基类中的虚函数必须有相同的形参列表和返回类型,不然它们无法通过指针或引用实现动态绑定;
      7. 基类可能有多个版本的重载函数,派生类可能只想对其中的某1至2个进行修改,剩下的不修改;为了能够在派生类的作用域中实现正确的重载版本匹配,需要通过 using 声明语句,把基类的函数放在派生类的作用域中,然后派生类只需定义自己想要覆盖的函数即可,这样可以减少很多工作量;
        1. 格式:using base::func;
        2. 不需要提供形参列表,仅需声明一个名字即可;
        3. 感觉这个方法很像在当前作用域打开一个超链接;编译器优先在当前作用域中寻找函数,找到的即是覆盖后的版本;如果没有找到,再从引入的基类同名函数集进行查找;
    7. 构造函数和拷贝控制
      1. 虚析构函数
        1. 当在基类中定义一个虚析构函数时,我们就可以动态的绑定派生类版本的析构函数了;如果不这么做的话,有时候销毁一个对象时,会产生未定义的行为;
          1. 原因:派生类中的资源分配方式,和基类可能不一样,所以需要不同的资源释放方法;
        2. 基类有一条例外,即它一定会有一个析构函数,但它不一定会有拷贝控制成员;对于普通类,则是需要析构也意味着需要拷贝控制成员;
        3. 如果一个类定义了析构函数,则编译器将不再会为它合成移动函数;但如果需要,我们可以手动定义一个移动操作;
          1. 为什么?可能是因为合成的版本无法判断如何正确释放资源;
      2. 合成拷贝控制与继承
        1. 不管是构造、拷贝还是销毁,都会沿着继承体系发生链式发应;
        2. 派生类的对象是由多个子对象组成的,其中的一个子对象即是基类的对象,因此,若基类的某些函数被定义成删除的,则它们在派生类中也会是删除的;
          1. 原因:实现行为表现的一致性,避免发生未定义行为;
        3. 一般来说,如果基类中没有默认、拷贝或移动构造函数,则在派生类中也不会定义它们;
      3. 派生类的拷贝控制成员
        1. 当派生类定义了拷贝控制成员时,这些成员还将负责包括基类部分成员在内的拷贝或移动;
          1. 因此需要调用基类的构造函数,不然基类的成员很可能会使用默认初始化的值,这不一定是我们想要的结果;
          2. 在定义派生类的拷贝控制成员时,需要显式的调用基类对应的拷贝控制成员,用来初始化基类子对象部分;
        2. 派生类可以不用显式调用基类的析构函数,只需负责销毁自己的子对象成员即可,基类子对象的成员会按顺序自动销毁;
        3. 如果我们在构造函数或者析构函数中调用了虚函数,此时要特别小心,因为不管是构造函数还是析构函数,当它们执行的时候,对象都处于一种未完成的半成品状态,而此时调用虚函数的话,由于虚函数的动态绑定特性,它有可能会去绑定未完成的部分,导致调用失败抛出错误;
          1. 因此,如果一定要在构造函数或者析构函数中调用虚函数的话,一定要确保它匹配的版本,是当前类中定义的版本,而不是基类或者派生类的版本;
      4. 继承的构造函数
        1. 派生类只继承基类的构造函数,但不继承默认、拷贝、移动构造函数;
          1. “继承”一词用得也不准确,实际的机制并非真的是继承而来的;它是通过使用 using 语句声明来的;
          2. using 语句通常的作用是让某些东西在当前作用域可见,但此处用法有些不同,它的作用是将基类的构造函数,复制一份到派生类里面来,格式为 child(params): parent(args) { }
          3. using 不会改变构造函数的访问级别;构造函数在基类中是什么访问级别,在派生类中也仍然是相同的访问级别;
          4. using 也不会改变构造函数的 explicit 和 constexpr 属性;
          5. 当基类的构造函数有默认实参时,默认实参不会被继承;但是,派生类会获得多个版本的构造函数,其中一个版本包含默认实参,另外一个版本没有默认实参;但不管哪个版本,默认值都去掉了;
          6. 当派生类中有定义相同形参列表的构造函数,则基类中对应的构造函数会被覆盖,不会被继承;
    8. 容器与继承
      1. 由于容器要求保存的对象具备相同类型,因此,对于存在继承关系的两个对象,它们无法被完整的保存在容器中,派生类对象独有的成员会在复制过程中被切掉;
        1. 解决办法:在容器中存放对象的智能指针即可;
        2. 原因:指向派生类的智能指针,在被插入容器的过程中,会被转换成基类指针;
          1. 目的一:基类指针实际指向的是一个派生类对象,这样当进行解引用调用时,会触发动态绑定,正确访问相应的成员;
          2. 目的二:智能指针全部是基类类型,所以满足容器同类型的要求;
      2. 为了将继承体系中的不同类的对象放在同一个容器中,需要考虑:
        1. 自定义对象的比较函数,以便对象在容器中能够实现排序;
        2. 对于获取对象的数据成员,一般应使用虚函数,以便可以实现动态绑定;
        3. 容器添加元素的过程,实际是拷贝的过程,由于存放的是智能指针,因此拷贝过程不可避免涉及内存的分配;而分配内存需要知道类型,因为拷贝过程相当于也需要实现动态绑定,复制创建一份新对象,然后再使用基类智能指针绑定新创建的对象;最后存放到容器中;
      3. C++ 面向对象的悖论:无法直接使用对象进行面向对象编程,而是需要通过指针和引用;
        1. 原因:只有通过指针和引用,才能够实现动态绑定;
        2. 缺点:指针会增加复杂度,需要小心处理;
    9. 文本查询程序再探
      1. 如果派生类的基类是抽象基类,则如果在派生类没有覆盖纯虚函数,则该派生类仍然是抽象基类,不可实例化对象;
  15. 模板与泛型编程
    1. 定义模板
      1. 函数模板
        1. 格式:template T min(const T&)
        2. 一个函数模板就是一个公式,用来生成针对特定类型的函数;
        3. 模板以关键字 template 开头,随后接着一个模板参数列表,包含1个或多个模板参数,列表不可为空;
        4. 模板参数很像函数的形参,它在实际使用时,会被替换为实参;之后编译器根据推断的参数类型,生成模板的实例,即函数;
        5. 模板类型参数需要使用关键字 class 或者 typename 来表示,例如 template ; 或者 template <typename T, typename U>
          1. class 和 typename 意思相同,可以互换,也可以同时使用;但从直观的角度来说,使用 typename 更好;(保证 class 是历史遗留原因)
        6. 模板还接受非类型的参数,这些参数必须是一个值;当模板实例化时,非类型参数会被用户或者编译器替换为值;
          1. 因此,这些非类型参数需要为常量表达式,这样编译器才有办法推断出它的值;
          2. 非类型参数不再使用 typename 来表示,而是使用普通的类型名;例如 template <int A, int B>
        7. 函数模板可以声明为 inline 或 constexpr,这两个关键字的位置应该放在模板参数列表右侧,返回类型左侧;
          1. template inline T min(const T&)
        8. 泛型编码的两条原则
          1. 模板中的函数参数是 const 的引用(原因:以便模板可以适用于不可拷贝的实参,性能也会更好;因为如果不是引用,在调用函数时,会拷贝实参);
          2. 函数体中的判断仅使用小于号 < 进行比较(原因:避免强制要求实参必须定义其他运算符,使得模板更加通用)(原因跟 for 循环使用 != 做为结束条件判断类似)
        9. 头文件有可能只包含函数声明和类声明,但不包括类定义和函数定义;但模板的声明和定义则通常都放在头文件中,二者不分离;
        10. 模板调用者需要注意在使用模板时,所传入的类型,是否具备模板要求的一些条件,例如比较大小的模板一般要求传入的类型支持 < 运算符;
        11. 对函数模板,在调用的时候,编译器会根据传入的实参,自动推断实参的类型;
      2. 类模板
        1. 格式 template class Blob { … }
        2. 类模板实例化出来的每一个实例,都是一个独立的类;这些类之间没有任何关联,互相之间也没有任何访问权限;
        3. 模板里面还可以嵌套其他模板,而且内部的模板可以使用外部的类型参数;
        4. 类模板的成员函数可以声明在模板内部,也可以声明在模板外部;
          1. 当声明在外部的时候,需要标注该成员函数是属于哪个模板;
          2. 示例:template void Blob::min(const T&) { … }
        5. 通常类模板即使已经实例化了,但如果它的某些成员函数没有马上用到,则该成员函数就暂时不会实例化;需要等到用到时候才开始实例化;
          1. 这意味着即使有些类型不能完全满足类模板的要求,但类模板依然能够成功实例化,只要不要去使用哪些不满足条件的成员函数即可;
          2. 有一个例外,即单独对类模板进行实例化定义时(注:非声明),它会实例化所有成员;
        6. 当我们使用一个类模板类型时,是需要跟着类型实参的,例如 Blob,唯一例外的情况是在类内部,此时可以省略实参,而仅写类模板名即可,例如 Blob,不需要写
          1. 但是,以上例外仅适用于类模板的内部,如果是在类模板的外部,则仍然要写上类型实参;
          2. 事实上,此处严谨的说法应该是在类模板的“作用域”内,而不是类模板的内部;(貌似作用域除了在类内部外,还有其他地方没?)
        7. 如果类模板中有一个非模板友元,则该友元有权访问所有类模板的实例;如果类模板中的友元也是一个模板,则权限可能涵盖所有友元实例,也有可能只涵盖单一类型的友元实例;
          1. 在友元声明的时候,如果使用不同的模板参数名称,则意味着所有友元实例,都具有访问权限;类模板与友元模板之间没有绑定关系;
          2. 如果使用相同的类型参数,则限定只有同一种类型的友元实例,才具有访问权限;
        8. 在 C11 标准中,支持将模板参数声明为友元
          1. 示例
            1. template class Bar {
              1. friend Type;
              2. }
          2. 有什么用?貌似可用于赋予访问权限给友元类的对象;
        9. 新标准引入了模板类型的别名,而且别名可以有一个或多个的参数
          1. template using twin = pair<T, T>
            1. twin 即为 pair<int, int>
          2. template using twin = pair<T, unsigned>
            1. twin 即为 pair<int, unsigned>
        10. 类模板支持 static 成员,实例化后,相同实参类型的类实例,共享同一个 static 数据成员;
          1. static 数据成员跟函数成员一样,也是在使用的时候才会实例化;
      3. 模板参数
        1. 模板参数的作用域同普通的函数参数是一样的,从定义的位置开始,到块的末尾结束;区别在于
          1. 在参数列表中,参数只能出现定义一次,不可重复定义,例如 template <typename T, typename T> 是错误的;
          2. 在作用域内,不可以重用参数名(即不可两次或多次使用同一个参数名来表示不同的类型);
        2. 就像普通函数一样,模板的声明和定义也是可以分开进行的;而且,里面用到的参数名字也可以不同,重点是保持格式相同就可以了,比如参数数量、顺序等;
        3. C++标准假定通过作用域运算符访问的名字不是类型,如果要表示该名字是一个类型,需要通过 typename 关键字显式告知(注:此处使用 class 代替 typename 无效)
          1. template typename T::value_type top(const T&) // 此处表示 value_type 是一个类型;
        4. 就像普通函数支持默认实参一样,模板参数也支持默认实参(C11 之前,只有类模板接受默认实参,函数模板不行);
          1. 使用限制也同普通函数,即某个参数要有默认实参,前提是在参数列表中,该参数右侧的所有其他参数都已经设置了默认实参;
          2. template <typename T, typename F = less> // 此处以 T 实例化后的 less 作为类型 F 的默认值;
          3. int compare(const T &v1, const T &v2, F f = F()) { … }; // 此处以 F 默认初始化后的对象 F() 做为函数 compare 的第三个参数的默认值;
        5. 假设模板为所有参数提供了默认实参,例如 template Blob
          1. 当我们想使用这个模板的实例化类型时,需要提供一对空的尖括号,即 Blob<>;跟函数调用使用默认实参的原理是一样样的;
          2. 当然,我们也可以不使用默认实参的类型,而是自己提供类型,则此时应该将类型放在尖括号中,例如 Blob
          3. 当我们在模板定义的位置中使用模板的实例化类型,即以 Blob 来表示,甚至还可以写成 Blob
      4. 成员模板
        1. 一个普通的类可以包含本身是模板的成员函数
          1. 目的:在调用该函数的时候,可以支持更多的类型;编译器会根据传入的实参自动推断类型;
          2. 当一个类定义了一个模板类型的成员函数,并重载了调用运算符,貌似意味着这个类变成了一个函数对象,并且这个函数对象支持各种参数类型;可以间接的实现将函数像值一样传递;
            1. 事实上貌似只要是函数对象都可以像值一样传递
        2. 一个类模板也可以包含本身是模板的成员函数
          1. template class Blob { // 此处声明了一个类模板
            1. template Blob(It b, It e); // 此处声明了一个构造函数模板
          2. 以上是在类模板的内部定义成员函数模板,我们也可以在类模板的外部,定义成员函数模板,区别在于要同时提供类模板的参数列表和成员模板的参数列表;
            1. template
            2. template
              1. Blob::Blob(It b, It e): …
          3. 在实例化的时候,我们仍然需要显式的提供类型实参给类模板,但成员模板则不用,它会根据传入的实参自动推断并获得类型;
      5. 控制实例化
        1. 不同的源文件使用了相同的模板和类型实参,意味着在不同的文件中会多次实例化,即产生多个实例,这样会产生很大的不必要开销;
        2. 那么,单个文件中多次使用相同的模板和类型实参,是否会产生多个实例,还是只会产生一个?
          1. 貌似只会产生一个;
        3. 使用 extern 关键字,可以显式的告知编译器模板将在其他文件实例化定义,本文件请不要重复实例化;如果没有 extern 关键字的话,则编译器会在本文件进行实例化;
          1. 这也意味着其他文件和本文件需要链接在一起,才能完整使用;
        4. 发现实例化也分成两种,一种叫做普通实例化,一种叫做实例化定义;
          1. 普通实例化:Blob 或 int compare(const int &a, const int &b)
          2. 实例化定义:template class Blob 或 template int compare(const int&, const int&)
          3. 区别:实例化定义会实例化类模板的所有成员,因为它要求传入的类型实参,能够支持所有类模板的成员,否则估计要报错了;
      6. 效率与灵活性
        1. 在运行时绑定类型,会使得类的使用更加灵活;在编译时绑定类型,损失了灵活性,但提高了性能;
    2. 模板实参推断
      1. 类型转换与模板类型参数
        1. 将实参传递给函数模板的形参时,能够自动触发的类型转换只有三种情况:const 转换、数组指针转换、函数指针转换;
          1. 正常是不应该出现类型转换时,直接生成新的函数实例即可;关键的问题是,如果传入两个实参,它们的类型不同,但在定义的时候,却是共同一个形参名字,这个时候编译器要确定推断使用其中一个类型,剩下的另外一个类型就只好进行转换了;
          2. 如果要兼容允许传入不同类型的实参,则更好的做法还不如直接分开定义两种类型的模板参数,例如 template <typename A, typename B> …
            1. 注意:这两种类型需要满足成员函数的一些操作,例如比大小的操作,不然容易出错;
          3. 如果函数的参数类型不是模板参数,则传入实参时,会按照正常的转换规则进行类型转换(如需)
      2. 函数模板显式实参
        1. C++ 也可以允许调用者传入更多的参数,让调用者来显式指定得到某种类型的结果
          1. tempalte <typename A, typename B, typename C> A sum(B, C)
          2. 调用时,正确的做法
            1. sum(int, int) // 第一个 long 对应 A,其他两个自动推断匹配 B 和 C
            2. sum<long, int, int>(int, int) // 不使用自动推断,手工显式全部指定好对应的类型;
          3. 注意:自动推断时,类型匹配是按顺序的,如果函数形参列表的顺序,与函数模板的参数列表顺序不同,则每次调用都要手工指定好对应的类型,无法使用自动推断;
        2. 如果我们不使用自动推断,而是手工指定对应位置的形参类型,则在传入实参时,编译器会将实参转换为对应类型的形参;
          1. long lng;
          2. compare(lng, 1024) // 最后的结果是 long, long,1024 被转化成 long
          3. compare(lng, 1024) // 最后的结果是 int, int, lng 被转换成 int
      3. 尾置返回类型与类型转换
        1. 当我们想从传入的参数,推断出类型,并做为返回的结果的类型时,除了使用显式指定实参的方法(笨拙一些,需要用户手工多写一个参数),还有一种方法是使用尾置返回格式(配合 auto 实现)
          1. template auto fcn(T begin, T end) -> decltype(*beg)
          2. 缺点:这种做法中,如果传入的参数的类型是迭代器,则对迭代器的解引用进行 decltype 只能得到引用类型,得不到值类型;
          3. 克服缺点的办法:引入头文件 ,使用里面的 remove_reference 模板类,去除引用,读取里面的 type 成员,得到值类型
            1. template typename remove_reference<decltype(*beg)>::type
              1. 注:由于作用域运算符读取成员的时候,默认按值处理,但此处我们想获得类型,所以需要使用 typename 关键字;
            2. 这个头文件中还有好几个其他的方法,用来:去引用、加常量符、加左值引用、加右值引用、去指针、加指针、加正负符号、去正负符号;
      4. 函数指针与实参推断
        1. C++ 中允许使用函数模板,来给一个函数指针赋值,或者初始化一个函数指针(暂时不知道为什么允许这么做),由于函数指针实际需要指向一个函数对象,所以模板本质上是需要实例化的,以生产一个对象;此时编译器会根据函数指针里面的参数类型,来推断并提供实参给模板进行实例化
          1. template int compare(const T&, const T&)
          2. int (*p)(const int&, const int&) = compare // 此处使用 compare 模板给指针 p 赋值,编译器会推断 T 的类型为 int;
          3. 如果编译器无法从函数指针的参数类型中进行推断时,就会报错;例如当函数有多个重载的版本时,编译器就不会确定到底选择哪个版本进行推断(二义性);
            1. 解决的办法:显式的手工提供实参类型,让编译器知道选择哪个版本;
            2. func(compare); // 传递 compare(const int&, const int&)
            3. 严谨的表述:当函数的参数,是一个函数模板实例的地址时(即函数指针),程序上下文必须保证每个模板的参数能够有唯一的值或类型;
              1. 假设 compare 就是函数 func 的参数,那么 compare 本质上是一个函数模板指针类型的变量,存的是函数模板的地址?
      5. 模板实参推断与引用
        1. 当函数模板的参数是 T& 类型时,不可接受右值(例如字面值)做为实参;但当它是 const T& 的时候,就可以接受右值实参(难怪在设计函数的时候,前人经验总结是能够使用 const 的要尽量使用,它会使得函数更加具有通用性);
        2. 当函数模板的形参类型是 const T& 时,实参类型如果是 const int&,是自动推断的结果 T 为 int 类型,而不会是 const int 类型;
        3. C++ 的引用在一定条件下会发生折叠,例如:T& &, T& &&, T&& & 都会折叠成 T&;T&& && 则会折叠成 T&&;
        4. 由于折叠的存在,使得当一个函数模板的参数类型定义为右值引用类型时,即 T&&,那么这个函数模板既可以接受左值实参,也可以接受右值实参,但它们的效果不太一样
          1. 传入左值实参,例如 int,T 被推断为左值引用类型 int&
          2. 传入右值实参,T 被推断为原本的值类型 int;
          3. 注意:
            1. 折叠的发生条件局限于间接创建出来的“引用的引用”,例如模板参数或类型别名;
            2. 这种通用性会引入第二个问题,由于推断出来的 T 类型既有可能是引用类型,也有可能是值类型,那么它将我们在模板的代码逻辑带来巨大的混乱,我们很难编写同时适用于引用和值两种类型的代码;
            3. 为了解决以上的问题,C++ 又引入了模板重载来避免类型的多种可能性(但这样也意味着我们可能要同时写两份代码,个人感觉增加了很多的复杂度;如果一开始不允许引用折叠,或许就没有后续的这么多问题了,C++真是会搞事)
      6. 理解 std::move
        1. 实现原理:
          1. remove_reference,将引用去除,得到原本的值类型;
          2. static_cast,引入一条特许规则,可以显式的将左值引用转成右值引用;
      7. 转发
        1. 目标:在函数调用中保持参数的类型信息, 以便转发的时候,能够保持原样,原本是值,转发后还是值;原本是引用,转发后还是引用;
        2. 解决方案:通过引入头文件,使用其中的 std::forward 模板来解决;同时将函数模板的参数定义为右值引用
          1. template <typename F, typename T1, typename T2>
          2. void flip(F f, T1 &&t1, T2 &&t2) {
            1. f(std::forward(t2), std::forward(t1));
          3. }
    3. 重载与模板
      1. C++ 是允许函数模板被另外一个同名函数或同名函数模板重载的,条件同样是参数类型和数量设置不同即可(唉,累不累);
      2. 当有多个函数模板,都可以提供同样质量的匹配时,编译器会选择那个更特例化的版本,放弃更通用的版本;
        1. 这也意味着非模板的函数版本在同等条件下也会优先被编译器匹配;
      3. 定义多个函数模板的时候,有可能其中的某个模板,会调用另外一个模板,这意味着它们需要同时出现在同一个作用域,不然找不到导致调用失败;但由于有多个模板,编译器并不会报错,而是会从剩下的模板中找一个最接近的进行匹配,但这样的结果有可能并不是我们想要的;
    4. 可变参数模板
      1. 可变数目的参数被称为参数包,有两种,分别叫模板参数包和函数参数包(啥区别?)
        1. template <typename T, typename… Args> // Args 表示零个或多个的模板参数
        2. void foo(const T &t, const Args& … rest); // rest 表示零个或多个的函数参数;
      2. 编译器会根据调用函数时传递的实参,来判断函数包中有几个参数,以及对应的类型;
      3. 可以使用 sizeof… 运算符来获取包中的参数数量
        1. template <typename… Args> void g(Args … args) {
          1. cout << sizeof…(Args) << endl; // 获取模板参数包的参数数量
          2. cout << sizeof…(args) << endl; // 获取函数参数包的参数数量;
      4. 编写可变参数的函数模板
        1. 实现原理:递归;定义一个函数的两个版本,第一个版本只接受一个参数,第二个版本接受两个参数,先处理第一个参数,第二个参数为函数包;然后以函数包调用它自己;这样每次调用都会从函数包中取出一个参数进行处理,逐级递减,直到最后调用第一个版本的函数(最后一次时,由于第一个版本更加特例化,编译器会优先匹配它,所以递归可以终止)
      5. 包扩展
        1. 所谓的包扩展,很像函数式编程中的 map 操作,将某种操作映射到列表中的每一个元素,并返回处理结果组成的列表;
          1. template <typename… Args> ostream &errorMsg(ostream &os, const Args&… rest) {
            1. return print(os, debug_rep(rest)…;
          2. }
        2. 事实上,类似 const Args&… 已经算是扩展了,它表示将 Args 中的每个元素进行 const & 处理;
        3. 省略号是用来触发扩展操作的;
      6. 转发参数包
        1. 实现方法:右值参数+forward扩展
          1. template <typename… Args> inline void StrVec::emplace_back(Args&&… args) { // 此处将参数扩展为右值引用的类型
            1. alloc.construct(first_free++, std::forward(args)…); // 此处同时扩展了 Args 和 args;
          2. }
    5. 模板特例化
      1. 目的:当通用模板不适合用于处理某些特殊类型的实参时,通过定义一个特例化的版本来解决问题;
      2. 用空尖括号来表示将为原模板的所有参数提供实参
        1. template <> int compare(const char* const p1, const char const *p2) { return strcmp(p1, p2); }
      3. 注意:一个特例化的版本,本质上是一个函数实例,而非函数模板的一个重载版本;
        1. 那为什么不直接定义一个非模板的普通函数,而是要定义一个模板的实例化版本?二者有什么本质的区别?
      4. 模板特例化有顺序和位置的要求:所有模板及其特例化版本应该放在同一个文件中;同时,所有同名模板应该放在特例化版本声明的位置前面;
      5. 类模板也支持特例化滴
        1. 实现要求:需要在原模板的命名空间中,定义特例化的版本,同时放在包含类模板的文件中(一般为头文件);
      6. 部分特例化:即只为部分模板参数提供实参;我们只能部分特例化类模板,但不能部分特例化函数模板;
        1. template struct remove_reference { typedef T type; }
        2. template struct remove_reference<T&> { typedef T type; }
        3. template struct remove_reference<T&&> { typedef T type; }
      7. 允许特例化某个成员函数,而不是整个类;这种情况下,在实例化的时候,传入的参数如果跟之前部分特例化时提供的实参类型一致,就是触发该特例化的成员函数版本;如果不一致,则按正常规则进行特例化;
  16. 用于大型程序的工具
    1. 命名空间
      1. 命名空间定义
        1. 命名空间可以嵌套于其他命名空间中,但不能定义在类或函数的内部;
        2. 命名空间结束无须使用分号,这一点与类定义不同;
        3. 命名空间中的内容是可以不连续的,即可以分开成几部分单独定义,每一部分都是在前面部分基础上的追加;
        4. 当我们使用类似 namespace nsp { … } 的声明时,它既有可能是表示创建一个新的命名空间,也有可能是向已创建的同名字的命名空间中追加内容;取决于该命名空间之前是否已经创建过了;
        5. #include 操作应该放在命名空间之外;原因:放在内部是指将 include 的文件嵌套于已有的命名空间中,这可能并不是我们想要的;
        6. 同一个命名空间中定义的成员,可以直接使用,而无须加上命名空间的前缀;但命名空间之外的成员就需要加上前缀了;
        7. 命名空间中的成员也可以放在父级命名空间中进行定义,但是不能放在同级及同级的子空间中进行定义;
        8. 全局作用域本质上也是一个命名空间,只是它是隐式声明的;它是所有其他命名空间的父空间;可以通过双冒号显式访问全局命名空间中的成员(一般是因为有局部命名空间中出现同名成员的情况才会这么用,不然后直接用名字就可以访问到了)
        9. 对于嵌套的命名空间,如果出现同名,在内层使用变量时,内层的变量会覆盖外层同名变量;
        10. C11 引入了新东西,叫内联命名空间,该空间的成员可以被外层空间直接访问而无须加上前缀;通过在第一次定义的位置,加上 inline 关键字来声明内联;
          1. 这种特性可以用在什么场合呢?
        11. 未命名的命名空间:享有静态生态周期(也就是说程序销毁的时候才会释放内存)(即使此局部子空间也是这样吗?)
          1. 局限:在单个文件中可以是不连续的,但不能跨越多个文件;
          2. 如果一个头文件中定义了未命名的命名空间,则在包含该头文件中的所有文件,都会有一个独立的未命名空间实体;
          3. 定义在未命令空间的名字可以直接使用(我们也没有名字可以访问它们,故只能直接使用了)
          4. 当未命名空间出现在最外层,也即全局命名空间那一层时,要特别小心,此时未命名空间中的名字要记得避免跟全局空间中的名字出现冲突;
          5. 未命名空间也可以嵌套在某个命名空间A中,此时通过 A:: 即可访问未命名空间的名字;
        12. 在新标准采用命名空间机制之前,一般是使用 static 关键字进行静态声明的作法,来控制某些变量的访问方式;
      2. 使用命名空间成员
        1. 命名空间支持定义别名进行访问
          1. namespace primer = cplusplus_primer;
          2. namespce qlib = cplusplus_primer:QueryLib; // 支持嵌套
          3. 一个命名空间也可以同时好几个别名,每个别名都跟命名空间等价;
        2. using 声明:用来引入命名空间中的单个成员;
        3. using 指示:用来引入命名空间中的所有成员;
          1. using 指示会带来提升效果,即将命名空间的成员全部提升到当前作用域的外层作用域中;
          2. 有可能会引入冲突(如果外层作用域存在同名变量的话)(当前作用域的变量反而不会出现冲突,因为当前作用域变量的优先级更高,会覆盖外层变量);
        4. 头文件最多只能在它的函数或命名空间内使用 using 指示或声明;原因:如果头文件在其顶层作用域使用 using 指示,它会将命名空间引入所有包含该头文件的源文件中;
      3. 类、命名空间和作用域
        1. 实参查找:这里引入一条例外的规则,当我们给函数传递的实参是一个类类型时,除了正常的查找规则外,编译器还会到类类型所属的命名空间中进行查找;
          1. 这个例外规则的好处在于,对于标准库中某些函数的调用,可以不用显示的指定命名空间,带来了一些使用上的简便;
        2. std::move 和 std::forward 的形参是一个右值引用,这种类型的形参意味着它们可以匹配任何种类的实参,因此,也大大提高了出现函数匹配的二义性冲突的风险;因此,每次调用这两个函数时,通过添加 std:: 命名空间名字降低风险;
        3. 在一个类中声明某个友元的时候,其实也是这个友元的一种隐式声明的形式;假设该友元函数以类对象为参数,当调用这个友元函数的时候,根据实参查找规则,编译器还会到类所属的命名空间中查找函数,因此即使在调用友元函数前未显式声明这个友元函数,编译器仍然能够找到这个友元函数;反之,如果友元函数没有参数,则就找不到了;
      4. 重载与命名空间
        1. 调用函数时,如果参数是一个类类型,则查找范围包括该参数所属的命名空间,以及其基类所属的命名空间,此条规则影响深远,会带来很多变化;
        2. using 声明语句,声明的是一个名字,而非一个特定的函数;因此,同名的所有函数都会纳入当前作用域中;
        3. 当 using 声明语句所在作用域中有同名函数时,不可避免会发生冲突或者重载的影响;
        4. 单个或多个 using 指示也会发生重载,要小心;
  17. 其他事宜
    1. 代码变成可执行文件,需要进行编译;而代码文件的编译,需要依照一定的顺序,因为它们之间存在依赖关系,对编译顺序的安排,即是构建 build;即编译器按照构建提供的顺序进行编译,最后得到可执行文件;

C++ Primer
https://ccw1078.github.io/2019/02/20/C++ Primer/
作者
ccw
发布于
2019年2月20日
许可协议