C++易混知识点总结

指针与引用

引用

参考《C++ Primer中文版第五版》P46

1
2
3
int ival = 1024;
int &refVal = ival; // refVal作为ival的引用指向ival(是ival是别名)
int &refVal2; // 报错:引用必须被初始化
  • 引用和初始值绑定在一起,不是拷贝。

  • 引用必须在一出现的时候就初始化(绑上某一个值)。

  • 引用必须被初始化,一旦初始化完成,引用将和它的初始值对象一直绑定在一起,无法重新绑定到另一个对象。

  • 引用不是对象,它只是为一个已经存在的对象所起的另一个名字。

  • 定义一个引用之后,对其所有的操作都是在与之绑定的对象上进行的:为引用赋值,实际上是把值付给了与引用绑定的对象;获取引用的值实际上是获取了与引用绑定的对象的值;以引用作为初始值,实际上是以与引用绑定的对象作为初始值。

  • 引用本身不是一个对象,所以不能定义引用的引用。

  • 定义引用将声明符写成&d的形式,其中d是变量名。

    1
    2
    int &refVal = ival; // 如果是下面多个int&,int混合在一条语句中赋值则看成这种形式便于理解。
    int& refVal = ival; // 自己常用这种,能显示refVal是一个int&便于理解。
  • 允许在一条语句中定义多个引用,每个引用标识符都必须以符号&开头。

    1
    2
    int i1 = 1024, i2 = 2048;
    int &r1 = i1, r2 = i2, &r3 = i2; // r1,r3是引用,r2是int,一条语句可以同时定义多个int&和int
  • 除了 允许为一个常量引用绑定非常量的对象(详见2.1 const的引用)和15.2.3节P534将要介绍的两种例外情况(还没看),其他所有引用的类型都要和与之绑定的对象严格匹配。

    1
    2
    double dval = 3.14;
    int &refVal = dval; // 错误:int&类型的初始值必须是int对象,不能是double对象
  • 引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。

    1
    int &refVal = 10; // 错误:引用类型的初始值必须是一个对象

指针

参考《C++ Primer中文版第五版》P47-51

  • 与引用的相同点:指针也实现了对其他对象的间接访问

    与引用的不同点:

    1. 指针本身就是一个对象,允许对指针赋值和拷贝。引用不是对象,引用只是对象的一个别名。
    2. 在指针的生命周期内它可以先后指向几个不同的对象,但是引用初始化后就不能再对引用赋值。
    3. 指针无须在定义时赋初值,和其它内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。引用一定要在定义时赋初值,且以后值都不能变。
  • 定义指针类型的方法将声明符写成*d的形式,其中d是变量名。

  • 如果在一条语句中定义了几个指针变量,每个变量前面都必须有符号*

    1
    2
    int *ip1, *ip2; // ip1和ip2都是指向int型对象的指针
    double dp, *dp2; // dp2是指向double型对象的指针,dp是double型对象
  • 指针存放的是某个对象的地址,指针就是地址,指针的值就是指针的地址。要想获取该地址,需要使用取地址符&

    1
    2
    int ival = 42;
    int *p = &ival; // 取ival的地址赋给p,p存放ival的地址 => p是指向变量ival的指针
  • 因为引用不是对象,没有实际地址,所以不能定义指向引用的指针。

  • 除了 允许令一个指向常量的指针指向一个非常量对象(详见2.2 指向常量的指针) 和15.2.3节P534将要介绍的两种例外情况(还没看),其他所有指针的类型都要和它所指向的对象严格匹配。

    1
    2
    3
    4
    5
    6
    double dval;
    double *pd = &dval; // 正确:初始值是double型对象的地址
    double *pd2 = pd; // 正确:指针之间的相互赋值,用指向double对象的指针初始化double*

    int *pi = pd; // 错误:指针pi的类型和pd的类型不匹配
    pi = &dval; // 错误:试图把double型对象的地址赋给int型指针,即试图让int*型的pi指向double型的dval
  • 指针的值(即地址)应属下列4种状态之一:

    1. 指向一个对象。
    2. 指向紧邻对象所占空间的下一个位置。
    3. 空指针,意味着指针没有指向任何的对象。
    4. 无效指针,也就是上述情况以外的其他值。

    注意:

    1. 试图拷贝或以其他方式访问无效指针的值都将引发错误。编译器并不负责检查此类错误,这一点和试图使用未经初始化的变量是一样的。
    2. 尽管第2种和第3种形式的指针是有效的,但其使用同样受到限制。显然这些指针没有指向任何具体对象,所以试图访问此类指针(假定的)对象的行为是不被允许的。
  • 如果指针指向了一个对象,则允许使用解引用符 * 来访问该对象。

    1
    2
    3
    int ival = 42;
    int *p = &ival; // p存放着ival的地址,或者说p是指向变量ival的指针
    cout << *p; //由符号*解引用得到指针p所指向的对象,输出42
  • 对指针解引用会得出所指的对象,因此如果给解引用的结果赋值,实际上也就是给指针所指的对象赋值。

    1
    2
    *p = 0; // 由符号*解引用得到指针p所指的对象,*p等价于ival,*p = 0等价于ival = 0,即为*p赋值实际上是为p所指的对象ival赋值
    cout << *p; // 输出0
  • 解引用操作仅仅适用于那些确实指向了某个对象的有效指针。

  • *和&的多重含义

    1
    2
    3
    4
    5
    6
    int i = 42;
    int &r = i; // &紧随类型名出现,因此是声明的一部分,r是一个引用
    int *p; // *紧随类型名出现,因此是声明的一部分,p是一个指针
    p = &i; // &出现在表达式中,是一个取地址符
    *p = i; // *出现在表达式中,是一个解引用符
    int &r2 = *p; // &是声明的一部分,*是一个解引用符
  • 生成空指针的方法

    1
    2
    3
    int *p1 = nullptr; // nullptr是一种特殊类型的字面值,它可以被转换成任意其他的指针类型
    int *p2 = 0; // 直接将p2初始化为字面常量0
    int *p3 = NULL; // 需要首先#include cstdlib引入名为NULL的预处理变量,它的值就是0

    注意:

    1. 2.6.3节P68将稍微介绍一点关于预处理器的知识,现在只要知道预处理器是运行于编译过程之前的一段程序就可以了。
    2. 预处理变量不属于命名空间std,它由预处理器负责管理,因此我们可以直接使用预处理变量而无须在前面加上std::。
    3. 当用到一个预处理变量时,预处理器会自动地将它替换为实际值,因此用NULL初始化指针和用0初始化指针是一样的。在新标准下,现在的C++程序最好使用nullptr,同时尽量避免使用NULL。
  • 把int变量直接赋给指针是错误的操作,即使int变量的值恰好等于0也不行。

  • 使用未经初始化的指针是引发运行时错误的一大原因。建议初始化所有的指针,并且尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为nullptr或者0,这样程序就能检测并知道它没有任何具体的对象了。

  • 和其他任何变量(只要不是引用)一样,给指针赋值就是令它存放一个新的地址,从而指向一个新的对象。

  • 搞清楚一条赋值语句到底是改变了指针的值(改变指针中存放的地址,即该指针从指向a对象变成了指向b对象)还是改变了指针所指对象的值(该指针指向的a对象的值由1变成了2):记住 赋值永远改变的是等号左侧的对象。

    1
    2
    3
    int *pi = 0; // pi被初始化,但没有指向任何对象
    pi = &ival; // pi的值被改变,现在pi指向了ival,意思是为pi赋一个新的值,也就是改变了那个存放在pi内的地址值。
    *pi = 0; // *pi(也就是指针pi指向的那个对象)发生改变,pi指向ival所以是ival的值被改变,指针pi并没有改变,还是指向ival
  • 只要指针拥有一个合法值,就能将它用在条件表达式中,和采用算数值作为条件遵循的规则类似(Java不能用算数值非0为true,0为false的规则作为条件判断),如果指针的值是0,条件取false。任何非0指针(非空指针)对应的条件值都是true。

    1
    2
    3
    4
    5
    6
    7
    int ival = 1024;
    int *pi = 0; // pi合法,是一个空指针
    int *pi2 = &ival; // pi2是一个合法的指针,存放着ival的地址
    if (pi) // pi是一个空指针,pi的值是0,因此条件的值是false
    ...
    if (pi2) // pi2指向ival,因此它的值不是0,条件判断的值是true
    ...
  • void*是一种特殊的指针类型,可用于存放任意类型任意对象的地址。但是我们对该地址中到底是个什么类型的对象并不了解

    1
    2
    3
    double obj = 3.14, *pd = &obj;
    void *pv = &obj; // 正确:void*能存放任意类型对象的地址
    pv = pd; // pv是void*类型,能存放任意类型的指针

    注意:

    1. 利用void指针能做的事情比较有限:那它和别的指针比较,作为函数的输入或输出,赋给另外一个void 指针
    2. 不能直接操作void*指针所指的对象,因为我们并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
    3. 概括地说,以void*的视角来看内存空间也就仅仅是内存空间,没办法访问内存空间中所存的对象。

理解复合类型的声明

参考《C++ Primer中文版第五版》P51-52

  • 变量的定义包括一个基本数据类型(如int)和一组声明符(如i, p, &r)。声明符又由类型修饰符( 和 & )和变量名(i, p, r)组成。在同一条定义语句中,虽然基本数据类型只有一个,但是声明符的形式却可以不同。一条定义语句可能定义出不同类型的变量。

    1
    int i = 1024, *p = &i, &r = i; // i是一个int型的整数,p是一个int型指针,r是一个int型引用
  • 在定义语句中,类型修饰符(*或&)只作用于紧接其后的一个变量,不是作用于本次定义的全部变量。如果把空格写在类型修饰符和变量名中间则容易产生这种误导。

    1
    2
    3
    // 合法但是容易产生误导,基本数据类型是int而非int*,*仅仅是修饰了p而已
    // 对该声明语句中的其他变量并不产生任何作用。p1是指向int的指针,p2是int
    int* p1, p2;
  • 涉及指针或引用的声明的两种写法,第一种把修饰符和变量标识符写在一起,这种形式着重强调变量具有的复合类型。

    1
    int *p1, *p2; // p1和p2都是指向int的指针

    第二种把修饰符和类型名写在一起,并且每条语句只定义一个变量,这种形式着重强调本次声明定义了一种符合类型。

    1
    2
    int* p1; // p1是指向int的指针
    int* p2; // p2是指向int的指针

    上述两种定义指针或引用的方法没有孰对孰错之分,关键是选择并坚持其中的一种写法,不要总是变来变去。

指向指针的指针

参考《C++ Primer中文版第五版》P52

  • 指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针自己的地址再存放到另一个指针当中。

  • 通过的个数可以区分指针的级别,也就是说, 表示指向指针的指针, 表示指向指针的指针的指针,以此类推。

    1
    2
    3
    4
    int ival = 1024;
    int *pi = &ival; // pi指向一个int型的数
    int **ppi = &pi; // ppi指向一个int型的指针(即pi),ppi本身是一个指针,所以ppi是指向int型指针的一个指针,ppi指向pi,pi又指向ival
    cout << ival << *pi << **ppi << endl; // 三种不同方式输出ival的值,两次解引用ppi

指向指针的引用

参考《C++ Primer中文版第五版》P52-53

  • 引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在指向指针的引用。

    1
    2
    3
    4
    5
    int i = 42;
    int *p; // p是一个指向int型的指针
    int *&r = p; // r是一个对指针p的引用
    r = &i; // r引用了一个指针,因此给r赋i的地址就是令p指向i
    *r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0

    要理解r的类型到底是什么,最简单的方法是从右向左阅读r的定义。离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用于确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最有,声明的基本数据类型部分指出r引用的是一个int指针。

    r是一个引用 -> r是指针的引用 -> r引用的这个指针是指向int型的。

const限定符

参考《C++ Primer中文版第五版》P53

  • 希望定义一种变量,它不能被程序改变(任何试图为这种类型的值赋值的行为都将引发错误),只能当我们觉得它的值不再合适了对它进行调整,例如缓冲区的大小。这种变量用关键字const对变量的类型加以限定。

  • 因为const对象一旦创建后其值就不能再改变,所以const对象必须初始化。初始值可以是任意复杂的表达式。

    1
    2
    3
    const int i = get_size(); // 正确:运行时初始化
    const int j = 42; // 正确:编译时初始化
    const int k; // 错误:k是一个未经初始化的常量
  • 只能在const类型的对象上执行不改变其内容的操作,常量特征仅仅在执行改变const类型对象的操作时才会发挥作用。在不改变const对象的操作中还有一种是初始化,如果利用一个对象去初始化另外一个对象,则它们是不是const都无关紧要。拷贝一个对象的值并不会改变它,一旦拷贝完成,新的对象就和原来的对象没什么关系了。

    1
    2
    3
    int i = 42;
    const int ci = i; // 正确:i的值被拷贝给了ci
    int j = ci; // 正确:ci的值被拷贝给了j。注意和2.1中int &r2 = ci对比
  • 默认状态下,const对象仅在文件内有效。如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字。

const的引用

参考《C++ Primer中文版第五版》P54

  • 可以把引用绑定到const对象上,就像绑定到其他对象上一样,称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象。(总之不能改变const对象中的内容)

    1
    2
    3
    4
    const int ci = 1024;
    const int &r1 = ci; // 正确:引用及其对应的对象都是常量
    r1 = 42; // 错误:r1是对常量的引用,不能修改
    int &r2 = ci; // 错误:试图让一个非常量引用r2指向一个常量对象ci,因为不允许直接为ci赋值,当然也不能通过引用去改变ci。假设该初始化合法,则可以通过r2来改变它引用对象ci的值。
  • “常量引用”是“对const的引用”的简称。严格来说,并不存在常量引用。因为引用不是一个对象,所以我们没法让引用本身恒定不变。事实上,由于C++语言并不允许随意改变引用所绑定的对象,所以从这层意义上理解所有的引用又都算是常量。引用的对象是常量还是非常量可以决定其所能参与的操作,但是不会影响到引用和对象的绑定关系本身。

  • 前面提到引用的类型必须与其所引用对象的类型一致的两个例外的第一个就是在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。

    1
    2
    3
    4
    5
    int i = 42;
    const int &r1 = i; // 允许将const int&绑定到一个普通int对象上
    const int &r2 = 42; // 正确:r1是一个常量引用
    const int &r3 = r1 * 2; // 正确:r3是一个常量引用
    int &r4 = r1 * 2; // 错误:r4是一个普通的非常量引用

    常量引用可以绑定(指向)任意非常量的对象,非常量引用不可以绑定(指向)常量对象。

  • 当一个常量引用被绑定到另一种类型上时发生了什么:

    1
    2
    double dval = 3.14;
    const int &ri = dval; // ri是int,但dval是double

    编译器把上述代码变成了如下形式:

    1
    2
    const int tmp = dval; // 由double生成一个临时的int变量
    const int &ri = tmp; // 让ri绑定这个临时量

    在这种情况下,ri绑定了一个临时量对象(当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象),也简称为临时量。假设ri不是常量以上逻辑仍然成立(实际不成立),由于ri不是常量,就允许对ri赋值,这样就会改变ri所引用的对象的值,但是通过int &ri = dval;根据以上逻辑实际ri引用tmp这个临时量,改变一个临时量的值并不是我们的初衷,也没有任何实际意义,C++语言也就把这种行为归为非法。

  • 常量引用(对const的引用)仅仅对引用可参与的操作作出了限定,对于引用的对象本身是不是一个常量未做限定。因为对象也可能是个非常量,所以允许通过其他途径来改变它的值。对const的引用可能引用一个并非const的对象。

    1
    2
    3
    4
    5
    int i = 42;
    int &r1 = i; // 引用r1绑定对象i
    const int &r2 = i; // r2是常量引用(对const的引用),但是它所引用的i不一定是const
    r1 = 0; // r1并非常量,i的值修改为0
    r2 = 0; // 错误:r2是一个常量引用

    r2绑定非常量整数i是合法的行为。

    常量引用只是不允许通过r2改变i的值,但是i的值仍然允许通过其他途径修改,既可以直接给i赋值,也可以通过像r1一样绑定到i的其他引用来修改。

指针和const

指向常量的指针(pointer to const)

参考《C++ Primer中文版第五版》P56

  • 与引用一样,也可以令指针指向常量或非常量。类似于常量引用,指向常量的指针(pointer to const)不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。(右值是const -> 左值也要是const)

    1
    2
    3
    4
    const double pi = 3.14; // pi是个常量,它的值不能改变
    double *ptr = &pi; // 错误:ptr是一个普通指针
    const double *cptr = &pi; // 正确:cptr可以指向一个double常量
    *cptr = 42; // 错误:不能给*cptr赋值
  • 前面提到指针的类型必须与其所指对象的类型一致的两个例外的第一个就是允许令一个指向常量的指针指向一个非常量对象。

    1
    2
    double dval = 3.14; // dval是一个double类型,它的值可以改变
    cptr = &dval; // 正确:但是不能通过cptr改变dval的值

    和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。

const指针

参考《C++ Primer中文版第五版》P56

  • 指针是对象而引用不是,因此对指针就像对其他对象类型一样,允许把指针本身定义为常量。常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。

  • 把*放在const关键字之前用以说明指针是一个常量,也隐含着不变的是指针本身的值(即指针中存的地址,即指向的那个值的地址不变,即一直指向同一个对象)而非指向的那个对象的值

    1
    2
    3
    4
    int errBumb = 0;
    int *const curErr = &errNumb; // curErr将一直指向errNumb
    const double pi = 3.14159; // 常量对象pi
    const double *const pip = &pi; // pip是一个指向常量对象的常量指针

    对于第二行,离curErr最近的符号是const,意味着curErr本身是一个常量对象,下一个符号是*意思是curErr是一个常量指针,最后的int说明这个常量指针指向的是一个int对象。

    double *const pip说明pip是一个const指针,会一直指向同一个对象,这个对象是个double,const double *const pip说明pip一直指向的这个double也是一个const对象,double的值也不会变。

  • 指针本身是一个常量和所指对象是一个常量的区别:

    指针本身是一个常量,int *const curErr,指针的值也就是指针里面存的地址不能改变,所以指针会一直指向同一个对象,但是这个指针指向的这个对象的值可以变,也可以通过这个指针(curErr)来修改它所指的对象(errNumb)的值。

    所指对象是一个常量,const double pi,所指对象的值不能变,但是指针可以指向其他的对象,不指向你这个对象。

    指针本身和所指对象都是常量,const double *const pip,无论是所指对象的值还是自己存储的那个地址都不能变,也就是始终指向同一个对象,这个对象的值也始终不变。

    1
    2
    3
    4
    5
    *pip = 2.72; // 错误:pip是一个指向常量的指针
    if (*curErr) { // 如果curErr所指的对象(也就是errNumb)的值不为0
    errorHandler();
    *curErr = 0; // 正确: 把curErr所指的对象(errNumb)的值重置
    }

顶层const

参考《C++ Primer中文版第五版》P57

  • 如前所述,指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词“顶层const(top-level const)”表示指针本身是个常量,而用名字“底层const(low-level const)”表示指针所指的对象是一个常量。

  • 更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算数类型、类、指针等。底层const则与指针和引用等符合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const,这一点和其他类型相比区别明显:

    1
    2
    3
    4
    5
    6
    7
    /* const修饰的是紧随其后的部分 */
    int i = 0;
    int *const p1 = &i; // 这是一个顶层const,const修饰p1,说明p1是一个const,不能改变p1的值
    const int ci = 42; // 这是一个广义的顶层const,const修饰int,说明ci是一个const,不能改变ci的值
    const int *p2 = &ci; // 这是一个底层const,const修饰int,说明p2所指向的int是一个const,而不是p2本身是一个const
    const int *const p3 = p2; // 靠左的const是底层const,修饰int,说明p3指向的int是一个const;靠右的const是顶层const,修饰p3,说明p3本身是一个const
    const int &r = ci; // 用于声明引用的const都是底层const,因为引用本身不是对象,本身不能用const修饰,所以不存在引用的顶层const
  • 当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响。执行拷贝操作并不会改变被拷贝对象的值,因此拷入和拷出的对象是否是常量都没什么影响。

    1
    2
    i = ci; // 正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
    p2 = p3; // 正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响

    底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,但是常量不能转换成非常量

    1
    2
    3
    4
    5
    int *p = p3; // 错误:p3包含底层const的含义,而p没有
    p2 = p3; // 正确:p2和p3都是底层const
    p2 = &i; // 正确:int*能转换成const int*
    int &r = ci; // 错误:普通的int&不能绑定到int常量上
    const int &r2 = i; // 正确:const int&可以绑定到一个普通int上

    p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它是一个底层const,它所指向的对象是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。

constexpr和常量表达式

参考《C++ Primer中文版第五版》P58

  • 常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。

  • 字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。

  • 一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定:

    1
    2
    3
    4
    const int max_files = 20; // max_files是常量表达式
    const int limit = max_files + 1; // limit是常量表达式
    int staff_size = 27; // staff_size初始值是字面常量,但数据类型是普通int而非const int,所以不是常量表达式
    const int sz = get_size(); // sz本身是常量,但它的具体值直到运行时才能获取到,所以也不是常量表达式

constexpr变量

参考《C++ Primer中文版第五版》P59

  • C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化:

    1
    2
    3
    constexpr int mf = 20; // 20是常量表达式
    constexpr int limit = mf + 1; // mf + 1是常量表达式
    constexpr int sz = size(); // 只有当size是一个constexpr函数时才是一条正确的声明语句

    尽管不能使用普通函数作为constexpr变量的初始值,但是正如6.5.2节P214将要介绍的,新标准允许定义一种特殊的constexpr函数,这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。

  • 一般来说,如果你认定变量是一个常量表达式,那就把它声明成constexpr类型。

字面值类型

参考《C++ Primer中文版第五版》P59

  • 常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制,把声明constexpr时用到的类型称为字面值类型
  • 算数类型、引用、指针都属于字面值类型,自定义类、IO库、string类型不属于字面值类型,也就不能被定义成constexpr。其他一些字面值类型将在7.5.6节P267和19.3节P736(未看)介绍。
  • 尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或者0,或者是存储于某个固定地址中的对象。

指针和constexpr

参考《C++ Primer中文版第五版》P59

  • 在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关

    1
    2
    const int *p = nullptr; // p是一个指向 整型常量 的指针
    constexpr int *q = nullptr; // q是一个指向整数的 常量指针
  • 与其他常量指针类似,constexpr指针既可以指向常量也可以指向一个非常量:

    1
    2
    3
    4
    5
    6
    constexpr int *np = nullptr; // np是一个指向整数的常量指针,其值为空
    /* i和j都必须定义在函数体之外*/
    int j = 0;
    constexpr int i = 42; // i的类型是整型常量
    constexpr const int *p = &i; // p是常量指针,指向整型常量i
    constexpr int *p1 = &j; // p1是常量指针,指向整数

变量

变量/对象

参考《C++ Primer中文版第五版》P39

  • 对象是指具有某种数据类型的内存空间,在这里不严格区分是类还是内置类型,也不区分是否命名或是否只读。
  • 对C++来说,变量(variable)和对象(object)一般可以互换使用。

初始化

参考《C++ Primer中文版第五版》P39

  • 在C++中,初始化和赋值是两个完全不同的操作(尽管在很多编程语言中二者的区别几乎可以忽略不计)。初始化不是赋值,初始化的含义是创建变量时赋予某一个初始值,而赋值的含义是把对象的当前值擦除,而以一个新值来代替。

列表初始化

参考《C++ Primer中文版第五版》P39

  • C++语言定义了初始化的好几种不同形式,例如想要定义一个名为units_sold的int变量并初始化为0,以下的四条语句都可以做到:

    1
    2
    3
    4
    int units_sold = 0;
    int units_sold = {0};
    int units_sold{0};
    int units_sold(0);

    花括号来初始化变量这种初始化的形式被称为列表初始化(list initialization)。现在,无论是初始化对象还是某些时候为对象赋新值,都可以使用这样一组由花括号括起来的初始值了。

    当用于内置类型的变量时,列表初始化有一个重要的特点:如果我们使用列表初始化且初始值存在丢失信息的风险,编译器将报错:

    1
    2
    3
    long double ld = 3.1415926536;
    int a{ld}, b = {ld}; // 错误:转换未执行,因为存在丢失信息的危险
    int c(ld), b = ld; // 正确:转换执行,且确实丢失了部分值

拷贝初始化

参考《C++ Primer中文版第五版》P76

  • 使用等号=初始化一个变量,实际上执行的是拷贝初始化(copy initialization),编译器把等号右侧的初始值拷贝到新创建的对象中去。

    1
    string s5 = "hiya";

直接初始化

参考《C++ Primer中文版第五版》P76

  • 如果使用圆括号,执行的是直接初始化(direct initialization)。

  • 当初始值只有一个,使用直接初始化或拷贝初始化都行。如果像下面的s4,s7那样初始化用到的值有多个,一般来说只能使用直接初始化的方式:

    1
    2
    string s6("hiya"); // 直接初始化
    string s7(10, 'c'); // 直接初始化,s7的内容是cccccccccc

    对于用多个值进行初始化的情况,非要用拷贝初始化的方式来处理也不是不可以,不过需要显式地创建一个(临时)对象用于拷贝,虽然合法但是和直接初始化比没有任何优势:

    1
    2
    3
    4
    string s8 = string(10, 'c'); // 拷贝初始化,s8的内容是cccccccccc
    /* 本质上等效于下面两条语句 */
    string tmp(10, 'c'); // tmp的内容是cccccccccc
    string s8 = tmp; // 将tmp拷贝给s8

默认初始化

参考《C++ Primer中文版第五版》P39

  • 定义于任何函数体之外的变量被默认初始化为0。
  • 定义在函数体内部的内置类型变量将不被默认初始化,一个为被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。

初始化的比较

参考《C++ Primer中文版第五版》P76

1
2
3
4
5
6
string s1; // 默认初始化,s1是一个空串
string s2(s1); // 直接初始化,s2是s1的副本
string s2 = s1; // 拷贝初始化,等价于s2(s1),s2是s1的副本
string s3("value"); // 直接初始化,s3是"value\0"
string s3 = "value"; // 拷贝初始化等价于s3("value)
string s4(n, 'c'); // 直接初始化,把s4初始化为由连续n个字符c组成的串

参考《C++ Primer中文版第五版》P88

  • 大多数情况下这些初始化方式可以相互等价地使用,不过有三种例外情况:

    1. 使用拷贝初始化时(即使用=时),只能提供一个初始值

    2. 如果提供的是一个类内初始值,则只能使用拷贝初始化或使用花括号的形式初始化,不能使用圆括号。

      1
      2
      3
      4
      5
      struct Sales_data {
      std::string bookNo;
      unsigned units_sold = 0; // 拷贝初始化
      double revenue{0.0}; // 花括号列表初始化
      };
    3. 如果提供的是初始元素值的列表,则只能把初始值都放在花括号里进行列表初始化,不能放在圆括号里:

      1
      2
      vector<string> v1{"a", "an", "the"}; // 列表初始化
      vector<string> v2("a", "an", "the"); // 错误

变量声明和定义的关系

参考《C++ Primer中文版第五版》P41

  • 声明(declaration)使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义(definition)负责创建与名字关联的实体。

  • 变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。

  • 如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显式地初始化变量:

    1
    2
    extern int i; // 声明i而非定义i
    int j; // 声明并定义j
  • 任何包含了显式初始化的声明即成为定义。我们能给由extern关键字标记的变量赋一个初始值,但是这么做也就抵消了extern的作用。extern语句如果包含初始值就不再是声明,而变成定义了:

    1
    extern double pi = 3.1416; // 定义

    在函数体内部,如果试图初始化一个由extern关键字标记的变量,将引发错误。???

  • 变量能且只能被定义一次,但是可以被多次声明。如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。

命名空间

参考《C++ Primer中文版第五版》P74

  • 我们用到的库函数基本上都属于命名空间std,而程序也显式地将这一点标示了出来。std::cin表示从标准输入中读取内容。此处使用作用于操作符::的含义是:编译器应从操作符左侧名字所示的作用于中寻找右侧那个名字。因此,std::cin的意思就是要使用命名空间std中的名字cin。

  • 以上方法比较繁琐,可以通过一下三种更简单的途径使用命名空间中的成员。其中3.1介绍的是一种最为安全的方法,也就是使用using声明(using declaration)。

命名空间的using声明

参考《C++ Primer中文版第五版》P74

  • using声明的形式:

    1
    using namespace::name;

    一旦声明了上述语句,就可以直接访问命名空间(namespace)中的名字(name)。

  • 每个名字都需要独立的using声明,每个using声明引入命名空间中的一个成员。

    1
    2
    3
    using std::cin; // 之后可以直接使用cin
    using std::cout; // 之后可以直接使用cout
    using std::endl; // 之后可以直接使用endl

命名空间的别名

参考《C++ Primer中文版第五版》P702

  • 命名空间的别名(namespace alias)使得我们可以为命名空间的名字设定一个短得多的同义词。例如一个很长的命名空间的名字形如:

    1
    namespace cplusplus_primer { /* ... */ };

    我们可以为其设定一个短得多的同义词:

    1
    namespace primer = cplusplus_primer; // namespace 别名名字 = 命名空间原来的名字;

    不能在命名空间还没有定义前就声明别名,否则将产生错误。

  • 一个命名空间可以有好几个同义词或别名,所有别名都与命名空间原来的名字等价。

  • 命名空间的别名也可以指向一个嵌套的命名空间:

    1
    2
    namespace Qlib = cplusplus_primer::QueryLib;
    Qlib::Query q;

using指示

参考《C++ Primer中文版第五版》P702

  • using指示(using directive)和using声明类似的地方是,我们可以使用命名空间名字的简写形式,和using声明不同地方是,我们无法控制哪些名字是可见的,因为所有的名字都是可见的。

  • using指示的形式:

    1
    using namespace std; // using namespace 命名空间的名字;

    如果这里的名字不是一个已经定义好的命名空间的名字,则程序将发生错误。

  • using指示可以出现在全局作用域、局部作用域和命名空间作用域中,但是不能出现在类的作用域中。

  • using指示使得某个特定的命名空间的所有名字都可见,这样我们就无须再为它们添加任何前缀限定符了。简写的名字从using指令开始,一直到using指令所在的作用域结束都能使用。
  • using指令引入的名字的作用域与using声明引入的名字的作用域不同。(详见《C++ Primer中文版第五版》P703)

标准库类型string

参考《C++ Primer中文版第五版》P75

  • 标准库类型string表示可变长的字符序列,使用string类型必须首先包含string头文件。作为标准库的一部分,string定义在命名空间std中。接下来示例都假定已包含了下述代码:

    1
    2
    #include <string>
    using std::string;

定义和初始化string对象

参考《C++ Primer中文版第五版》P76

参见3.2初始化

1
2
3
4
5
6
string s1; // 默认初始化,s1是一个空串
string s2(s1); // 直接初始化,s2是s1的副本
string s2 = s1; // 拷贝初始化,等价于s2(s1),s2是s1的副本
string s3("value"); // 直接初始化,s3是"value\0"
string s3 = "value"; // 拷贝初始化等价于s3("value)
string s4(n, 'c'); // 直接初始化,把s4初始化为由连续n个字符c组成的串

string对象上的操作

参考《C++ Primer中文版第五版》P77

  • string的操作:

    • os << s :将s写到输出流os当中,返回os
    • is >> s :从is中读取字符串赋给s,字符串以空白分隔,返回is
    • getline(is, s) :从is中读取一行赋给s,返回is
    • s.empty():s为空返回true,否则返回false
    • s.size() :返回s中字符的个数
    • s[n]:返回s中第n个字符的引用,位置n从0计起
    • s1 + s2 :返回s1和s2连接后的结果
    • s1 = s2 :用s2的副本代替s1中原来的字符
    • s1 == s2:如果s1和s2中所含的字符完全一样,则它们相等;string对象的相等性判断对字母的大小写敏感
    • s1 != s2
    • <, <=, >, >=: 利用字符在字典中的顺序进行比较,且对字母的大小写敏感
  • 补充字典序:小写字母>大写字母,短的>长的(字典中小写字母在前,短的在前)

读写string对象

参考《C++ Primer中文版第五版》P77

  • 在执行读取操作时,string对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止。

    1
    2
    3
    4
    5
    6
    int main() {
    string s; // 空字符串
    cin >> s; // 将string对象读入s,遇到空白停止
    cout << s << endl; // 输出s
    return 0;
    }

    如果程序输入是” Hello World! “(开头结尾中间都有空格),则输出将是”Hello”,输出结果中没有任何空格。

  • 多个输入或者多个输出可以连写在一起:

    1
    2
    3
    string s1, s2;
    cin >> s1 >> s2; // 把第一个输入读到s1中,第二个输入读到s2中
    cout << s1 << s2 << endl; // 输出两个string对象

    如果程序输入是” Hello World! “(和上面一样),则输出将是”HelloWorld!”,输出结果中还是没有任何空格。

读取未知数量的string对象

参考《C++ Primer中文版第五版》P78

1
2
3
4
5
6
7
int main() {
string word;
while (cin >> word) { // 反复读取,直至到达文件末尾
cout << word << endl; // 逐个输出单词,每个单词后面紧跟一个换行
}
return 0;
}

while中的条件负责在读取时检测流的情况,如果流有效,也就是说没遇到文件结束标记或非法输入,那么执行while语句内部的操作。一旦遇到文件结束标记或非法输入,循环结束。

使用getline读取一整行

参考《C++ Primer中文版第五版》P78

  • 如果希望能在最终得到的字符串中保留输入时的空白符,这时应该用getline函数代替原来的>>运算符。getline函数的参数是一个输入流和一个string对象,函数从给定的输入流中读入内容,知道遇到换行符为止(注意换行符也被读进来了),然后把所读的内容存入到那个string对象中去(不存换行符)。

  • getline只要一遇到换行符就结束读取操作并返回结果,如果输入一开始就是换行符,所得的结果是个空string。

  • 和输入运算符一样,getline也会返回它的流参数。在读取时检测流的情况,如果流有效,也就是说没遇到文件结束标记或非法输入,那么条件判定为真,一旦遇到文件结束标记或非法输入,条件判定为假。

  • 一次输出一整行的程序:

    1
    2
    3
    4
    5
    6
    7
    8
    int main() {
    string line;
    // 每次读入一整行,直至到达文件末尾
    while (getline(cin, line)) {
    cout << line << endl;
    }
    return 0;
    }

    使用endl结束当前行并刷新显示缓冲区。

string::size_type类型

参考《C++ Primer中文版第五版》P79

  • string的size()函数返回的是一个string::size_type类型的值。

  • string类及其他大多数标准库类型都定义了几种配套的类型,这些配套类型体现了标准库类型与机器无关的特性,类型size_type就是其中的一种。在具体使用的时候,通过作用于操作符来表明名字size_type是在类string中定义的。

  • string::size_type是一个无符号类型的值,能足够存放下任何string对象的大小。

  • 允许编译器通过auto或者decltype来判断变量的类型:

    1
    2
    auto len = line.size(); // len的类型是string::size_type
    decltype(f()) sum = x; // f的返回值类型作为sum的类型
  • 由于size函数返回的是一个无符号整型数,如果在表达式中混用了带符号数和无符号数将可能产生意想不到的结果,假设n是一个负值的int,则表达式s.size() < n的判断结果几乎肯定的true,因为负值n会自动地转换成一个比较大的无符号值。所以如果一条表达式中已经有了size()函数就不要再使用int了,这样可以避免混用int和unsigned可能带来的问题。

字面值和string对象相加

参考《C++ Primer中文版第五版》P80

  • C++中的字符串字面值与标准库类型string是不同的类型。

  • 当把string对象和字符字面值以及字符串字面值混在一条语句中使用时,必须确保每个加法运算符(+)的两侧的运算对象至少有一个是string:

    1
    2
    3
    string s1 = "hello";
    string s4 = s1 + ","; // 正确:s1是string对象,","是字面值
    string s5 = "hello" + ","; // 错误:两个都是字面值,没有string对象
    1
    2
    3
    4
    5
    6
    7
    8
    /* 以下三种形式等价且正确 */
    /* 形式1 */
    string s6 = s1 + "," + "world"; // 正确:每个加法运算符都有一个运算对象是string,等价于下面
    /* 形式2 */
    string s6 = (s1 + ",") + "world"; // 子表达式s1 + ","的结果是一个string对象
    /* 形式3 */
    string tmp = s1 + ",";
    string s6 = tmp + "world";
    1
    2
    string s2 = "world";
    string s7 = "hello" + "," + s2; // 错误:不能把两个字面值直接相加

标准库类型vector

参考《C++ Primer中文版第五版》P86

  • 标准库类型vector表示对象的集合,其中所有对象的类型都相同。集合中的每个对象都有一个与之对应的索引,索引用于访问对象。因为vector容纳着其他对象,所以它也常被称作容器(container)。

  • 要想使用vector,必须包含适当的头文件。在后续的例子中,都假定做了如下using声明:

    1
    2
    #include <vector>
    using std::vector;
  • C++语言既有类模板,也有函数模板,其中vector是一个类模板。模板本身不是类或函数,相反可以将模板看作为编译器生成类或函数编写的一份说明。编译器根据模板创建类或函数的过程称为实例化(instantiation),当使用模板时,需要指出编译器应把类或函数实例化成何种类型。

  • 对于类模板来说,我们通过提供一些额外信息来指定模板到底实例化成什么样的类,需要提供哪些信息由模板决定。提供信息的方式总是这样:即在模板名字后面跟一对尖括号,在括号内放上信息。以vector为例,提供的额外信息是vector内所存放对象的类型:

    1
    2
    3
    vector<int> ivec; // ivec保存int类型的对象
    vector<Sales_item> Sales_vec; // 保存Sales_item类型的对象
    vector<vector<string>> file; // 该向量的元素是vector对象

    在上面的例子中,编译器根据模板vector生成了三种不同的类型:vector<int>vector<Sales_item>vector<vector<string>>

  • vector是模板而非类型,由vector生成的类型必须包含vector中元素的类型,例如vector<int>

  • vector能容纳绝大多数类型的对象作为其元素,但是因为引用不是对象,所以不存在包含引用的vector。除此之外,其他大多数内置类型和类类型都可以构成vector对象,组成vector的元素也可以是vector,如vector<vector<int>

定义和初始化vector对象

默认初始化vector

参考《C++ Primer中文版第五版》P87

  • 可以默认初始化vector对象,从而创建一个指定类型的空vector,然后当运行时获取到元素的值后再逐一添加。

    1
    2
    3
    4
    5
    vector<int> v2; // 默认初始化,v2不含任何元素
    for (int i = 0; i != 100; ++i) {
    v2.push_back(i); // 依次把整数值放到v2尾端
    }
    // 循环结束后v2有100个元素,指从0到99

    当然也可以在定义vector对象时指定元素的初始值。例如,允许把一个vector对象的元素拷贝给另外一个vector对象。此时,新vector对象的元素就是原vector对象对应元素的副本。注意两个vector对象的类型必须相同:

    1
    2
    3
    4
    5
    6
    vector<string> svec; // 默认初始化,svec不含任何元素
    vector<int> ivec;
    ... // 在此处给ivec添加一些值
    vector<int> ivec2(ivec); // 把ivec的元素拷贝给ivec2
    vector<int> ivec3 = ivec; // 把ivec的元素拷贝给ivec3
    vector<string> svec(ivec2); // 错误:svec的元素是string对象,不是int

列表初始化vector对象

参考《C++ Primer中文版第五版》P88

  • 用花括号括起来的0个或多个初始元素值赋给vector对象:

    1
    vector<string> articles = {"a", "an", "the"};

    上述vector对象包含三个元素:字符串”a”,”an”,”the”。

创建指定数量的元素

参考《C++ Primer中文版第五版》P88

  • 用vector对象容纳的元素数量和所有元素的统一初始值来初始化vector对象:

    1
    2
    vector<int> ivec(10, -1); // 10个int类型的元素,每个都被初始化为-1
    vector<string> svec(10, "hi!"); // 10个string类型的元素,每个都被初始化为"hi!"

值初始化

参考《C++ Primer中文版第五版》P88

  • 通常情况下,可以只提供vector对象容纳的元素数量而略去初始值。此时库会创建一个值初始化(value-initialized)的元素初值,并把它赋给容器中的所有元素。

  • 值初始化的这个初值由vector对象中元素的类型决定。如果vector对象的元素是内置类型,比如int,则元素初始值自动设为0。如果元素是某种类类型,比如string,则元素由类默认初始化:

    1
    2
    vector<int> ivec(10); // 10个元素,每个都初始化为0
    vector<string> svec(10); // 10个元素,每个都是空string对象

列表初始值还是元素数量?

参考《C++ Primer中文版第五版》P89

  • 初始化的真实含义依赖于传递初始值时用的是花括号还是圆括号。

    • 圆括号是用来构造vector对象的,圆括号里的第一个参数是元素数量,第二个参数如果有的话是每个元素的初始值,第二个参数如果没有元素进行值初始化。

      1
      2
      3
      vector<int> v1(10); // v1有10个元素,每个的值都是0
      vector<int> v3(10, 1); // v3有10个元素,每个的值都是1
      vector<string> v6("hi"); // 错误:不能使用字符串字面值构建vector对象
    • 花括号表明我们想列表初始化vector对象的,希望花括号里的值都是每个元素的值。也就是说,初始化过程会尽可能地把花括号内的值当成是元素初始值的列表来处理,只有在无法执行列表初始化时(当花括号里的值与元素类型不同时)才会考虑用这样的值来构造vector对象。如果确认无法执行列表初始化后,编译器会尝试用默认值初始化vector对象。

      1
      2
      3
      4
      5
      vector<int> v2{10}; // v2有1个元素,该元素的值是10
      vector<int> v4{10, 1}; // v4有2个元素,值分别是10和1
      vector<string> v5{"hi"}; // v5有一个元素,值为"hi"
      vector<string> v7{10}; // 无法把10当成元素初始化的列表,只能当成构造的参数,表示v7有10个默认初始化的元素
      vector<string> v8{10, "hi"}; // 无法把{10,"hi"}当成元素初始化的列表,只能当成构造的参数,表示v8有10个值为"hi"的元素

对vector的操作

参考《C++ Primer中文版第五版》P91

  • vector能在运行时高效快速地添加元素,因此在定义vector对象的时候么必要设定其大小,这么做性能可能更差。只有在所有元素的值都一样时,可以用vector<int> v3(10, 1); // v3有10个元素,每个的值都是1来定义vector。一旦元素的值有所不同,更有效的办法是先定义一个空的vector对象,再在运行时向其中添加具体值。

  • 如果循环内部包含有向vector对象添加元素的语句,则不能使用范围for循环。因为范围for语句体内不应改变其所遍历序列的大小。

    1
    2
    3
    4
    5
    6
    7
    vector<int> v; 
    v.push_back(1);
    v.push_back(2);
    for (auto n : v) {
    cout << n;
    v.push_back(3); // 不能在范围for循环内部改变遍历序列的大小
    }
  • 对vector的操作列表:

    • v.push_back(t):向v的尾端添加一个值为t的元素
    • v.empty():如果v中不含有任何元素返回真;否则返回假
    • v.size():返回v中元素的个数,返回值的类型是由vector定义的size_type类型
    • v[n]:返回v中第n个位置上元素的引用
    • v1 = v2:用v2中元素的拷贝替换v1中的元素
    • v1 = {a, b, c...}:用列表中元素的拷贝替换v1中的元素
    • v1 == v2:v1和v2相等当且仅当它们的元素数量相同且对应位置的元素值都相同
    • v1 != v2:v1和v2不相等
    • <, <=, >, >=:以字典顺序进行比较
  • 使用范围for语句处理vector对象中的所有元素的例子:

    1
    2
    3
    4
    5
    6
    7
    8
    vector<int> v{1, 2, 3, 4};
    for (auto &i : v) { // 对于v中的每个元素(注意:i是一个引用,这样就能通过i给v的元素赋值)
    i *= i; // 求元素值的平方
    }
    for (auto i : v) { // 对于v中的每个元素
    cout << i << " "; // 输出该元素
    }
    cout << endl;
  • vector的size()成员函数的返回值是由vector定义的size_type类型。要使用size_type,需首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型。

    1
    2
    vector<int>::size_type // 正确
    vector::size_type // 错误
  • 不能用下标形式添加元素,只能用v.push_back(t)试图为vector对象ivec添加元素:

    1
    2
    3
    4
    vector<int> ivec; // 空vector对象
    for (decltype(ivec.size()) ix = 0; ix != 10; ++ix) {
    ivec[ix] = ix; // 严重错误:ivec不包含任何元素,不能通过下标去访问任何元素
    }

    正确的方法是使用push_back:

    1
    2
    3
    for (decltype(ivec.size()) ix = 0; ix != 10; ++ix) {
    ivec.push_back(ix); // 正确:添加一个新元素,该元素的值是ix
    }

    vector对象(以及string对象)的下标运算符可用于访问已经存在的元素,而不能用于添加元素。

模板与泛型编程

函数模板

参考《C++ Primer中文版第五版》P578

  • 模板以关键字template开始,后跟一个模板参数列表(template parameter list),这是一个逗号分隔的一个或多个模板参数(template parameter)的列表,用小于号(<)和大于号(>)包围起来。模板参数列表不能为空。

    1
    2
    3
    4
    5
    6
    template <typename T>
    int compare(const T &v1, const T &v2) {
    if (v1 < v2) return -1;
    if (v2 < v1) return 1;
    return 0;
    }
  • 当我们调用一个函数模板时,编译器通常用函数实参来为我们推断模板实参,再用推断出的模板参数为我们实例化(instantiate)一个特定版本的函数。

    1
    2
    3
    4
    5
    // 实例化出int compare(const int&, const int&)
    cout << compare(1, 0) << endl; // T为int
    // 实例化出int compare(const vector<int>&, const vector<int>&)
    vector<int> vec1{1, 2, 3}, vec2{4, 5, 6};
    cout << compare(vec1, vec2) << endl; // T为vector<int>
  • 类型模板参数:

    1
    2
    3
    4
    template <typename T, class U>
    T calc (const T&, const U&) {
    ...
    }

    非类型模板参数:

    1
    2
    3
    4
    template <unsigned N, unsigned M>
    int compare(const char(&p1)[N], const char (&p2)[M]) {
    return strcmp(p1, p2);
    }

    当我们用compare("hi", "mom")调用compare时,编译器会使用字面常量的大小来代替N和M,从而实例化模板。注意,编译器会在字符串字面常量的末尾插入一个空字符串作为终结符,因此编译器会实例化出如下版本:

    1
    int compare(const char (&p1)[3], const char (&p2)[4])

--- 本文到此结束,感谢您的阅读 ---

谢谢小天使请我吃糖果
0%