《C++ PrimerPlus》 第一章~第八章

第一章 预备知识

C++简介

  • C++融合了三种不同的变成方式:1、C语言代表的过程性语言 2、带有类的面向对象语言 3、C++模板支持的泛型编程

C++简史

20世纪70年代早期,贝尔实验室的Dennis Ritchie开发了C语言。

20世纪80年代,贝尔实验室的Bjarne Stroustrup开发了C++语言。

可移植性和标准

C++98

C++11

程序创建的技巧

编译和链接

第二章 开始学习C++

进入C++

输入输出:C++能在使用printf()、scanf()和其他所有标准的C输入输出函数,只需要包含常规的C语言的stdio.h文件即可

main函数:

  • main函数是被操作系统调用的,他是程序与操作系统之间的接口。p14
  • int main( void ) 在括号中用void明确指出,函数不接受任何参数,在CPP中,让括号空着与使用void完全等效。但是在C中,让括号空着意味着对于是否接受参数保持沉默。p15
  • 许多程序员喜欢使用下面的函数头,并省略返回语句:void main()。这在逻辑上是可以理解的,大部分系统也适用,但由于它不是当前标准的内容,因此在有些系统上不能工作。新的标准中对这一点作出了让步,如果编译器到达main()函数末尾时没有遇到返回语句,则自动添加return 0语句 (只对main函数有效,对其他函数不会隐含return 0)。p15 (疑问:在测试的时候报错说main函数必须是int返回类型?同时,非main函数也可以不写明return语句,但是返回的值是6295680??)
  • 有一些非标准函数,他们使用_tmain() 形式,这种情况下,有一个隐藏的main()调用它,但是常规的独立程序都需要main()。p15

头文件名,名称空间:

  • 新标准的CPP不适用头文件的.h扩展名,而利用命名空间机制。
  • 新的cout,cin为了避免产生函数名冲突,需要使用std::cout,std::cin来使用
  • 如果使用using namespace std; 则表示std名称空间中的所要名称都可用,但这是一个隐患,推荐使用using std::cout的方式(为了方便,大多会使用using namespace std)
  • p33:using namespace std可以放在main中,表示只有main可以访问其命名空间,也可以放在iostream下面,表示文件中的所有函数都能访问

使用cout进行输出:

  • cout是一个预定义的对象,是某个类的特定实例,<<符号表示它将后面的字符串发送给cout:cout<<string
  • 从概念上看,输出是一个流,即从程序流出的一系列字符。cout对象表示这种流,其属性定义在iostream文件中,<<是cout对象的一个属性,它表示将其右侧的信息插入到流中,该符号与按位左移运算符实际上是重载关系
  • 打印的时候,cout会将整数形式的数字自动转换成字符串形式,注意整数25与字符串25有天壤之别
  • endl确保程序继续运行前刷新输出?
    控制符:诸如endl等对于cout来说有特殊含义的符号(manipulator)
    传统Cpp不能把回车放在字符串中间,但是C++11新增的原始字符串可以包含回车

C++语句

其他C++语句

  • cin.get() 一般需要两条,一条用于接受多余的换行,有一条用于让程序暂停。cin使用>>运算符才输入流中抽取字符。p24

函数

  • C++函数和C一样,不允许嵌套定义。p30
  • main不是关键字,因为它不是语言的组成部分,可以做关键字,但最好别这样,会引起其他错误。cout也不是关键字,而是一个对象名,所以可以在不适用cout的程序中,将cout用作变量名。p31

第三章 处理数据

简单变量

变量名,符号类型:

  • CPP命名规则:以两个下划线或下划线和大写字母打头的名称被保留给实现(编译器及其使用的资源)。p38
  • CPP的字节位数根据字符集的大小而定,对于基本字符集ASCII和EBCDIC来说,一字节为8为,而对于国际编程Unicode来说,一字节可能为16为或32位,int在老式机器中一般为16位,而在现在多为32位。p39
  • 预编译指令#define是c遗留下来的,cpp中多用const关键字p42。
  • cpp中有一种c没有的初始化赋值语法:int a(42)。C++11具有新的初始化方式。p42
  • 常数后缀,ul,lu,uL,LU等等都可以,均表示unsigned long常量。在cpp中,对十进制整数采用的长度规则,与16进制和8进制略有不同。p47
  • 通用字符名,以/u开头 Unicode与ISO 10646。p52
  • char默认情况下既不是无符号,也不是有符号。 wcha_t 与 underlying(底层类型) cin和cout将输入和输出看做是char流,因此不适合用来处理wchar_t类型,以l或L为前缀应用wcin和wcout。cpp11新增的char16_t char32_t分别以u和U为前缀。p53

const限定符

  • p54:const比#define更好,1.它可以明确指定类型,2.cpp的作用于规则将定义限制在特定的函数或头文件中,3.const可用于复杂类型,如数组和结构体

浮点数

C++算术运算符

  • 求模运算符只能用于整形。p59
  • 对于float类型,11.17+50.25=61.419998 具体愿意是float的精度限制所导致的(将操作数转化成二进制即可理解)。p60
  • 数值类型转换,对于精度丢失的情况,最终结果会根据系统的不同而不同。p63
  • c++11中的{}初始化赋值法不允许narrowing缩窄,即只能小赋给大,不能大赋给小(但是const可以,只要能hold住要赋的值即可)。p64
    整型提升:c++在计算表达式时自动将bool char unsigned char signed char short转换为int。如果shot比int短,则unsigned short类型将被转化为int,如果长度相同,则unsigned short将被转化为unsigned int,以此确保在对unsigned short进行提升时不会损失数据。wchar_t被提升为下列类型中第一个宽度足够的类型:int,unsigned int,long,unsigned long。更多转化规则可以查看校验表p64
  • 强制类型转化通用格式:(typeName)value;typeName(value)第一种格式来自C语法,第二种是纯粹C++语法。p65
  • c++11中新增了auto类型声明的用法,让编译器根据初始值的类型推断变量的类型。主要用于复杂类型。p66

第四章 复合类型

数组

  • c++中数组的arraySize只能是常量,const,或常量表达式,不能是变量。p71

字符串

  • c++11初始化数组时,可以省略等号。c++标准模板库(STL)提供了一种数组替代品模板类vector,c++11新增了模板类array。p74
  • ‘s’表示83 “s”表示的是某块内存的地址。 cout会默认自动拼接两段字符串,并且可以不在同一行。p75
  • c++使用空白(空格,制表,换行)来确定字符串的结束位置。为了读取空白可以采用cin的成员函数面向行的输入:cin.getline()和cin.get()。二者以换行为结束,前者会舍弃换行符,后者会将其保留在输入队列中(注意是输入队列,这相当于输入缓冲区,下面读取函数有可能会读到这个换行符)。二者的返回值为cin对象,可以继续调用函数。getline()使用起来更简单方便,但get()更能检查出错误。另外要注意二者读取空行时的区别。p78

string类

  • string对象和字符数组之间的主要区别是string对象可以声明为简单变量,类设计让程序能够自动处理string的大小。p83
  • 原始字符串 raw。p87

结构

  • p89:C++允许在声明结构变量时省略关键字struct。但是C不允许
  • p92:c++的结构特性比C更多。 位字段,共用体(长度为其最大成员长度)

共用体

枚举

  • 对于枚举变量,只有赋值运算符,枚举创建的是符号常量,可以代替const。枚举量的值可以重复。p96

  • 指针:*运算符称为间接值(indirect value)或解除引用(dereferencing)。p101:不管是指向何种类型的指针,其指针变量本身的长度是一定的。p99
    17.10.19

指针和自由存储空间

  • C++利用new关键字代替了malloc()来分配内存:int* p=new int; 用指针和new进行的内存分配是在程序运行时进行的(只有运行时,指针才知道它指向的是哪一块地址)。p102
  • delete关键字只能释放new的内存,不能用于一般变量,同时,不可以重复释放,否则结果未知。 不能用sizeof运算符确定动态数组包含的字节数。p104

指针、数组和指针算术

  • 指向数组的指针和数组名基本等价,区别是:1,指针值可以变,而数组名的值不能变。2,sizeof用在数组名上返回数组长度,用在指针上放回指针的长度。注意short tell[10]; 中tell与&tell的关系。p109
  • cout打印字符数组的关键不在于变量是一个数组名,而在于它是一个char的地址!在cout和多数c++表达式中,char数组名,char指针和双引号下的字符串常量都被解释为字符串第一个字符的地址。p109

第五章 循环和关系表达式

for循环

while循环

do while循环

基于范围的for循环(C++11)

  • c++11新增了一种基于范围的for循环,它简化了一种常见的循环任务:对数组或容器类的循环for(int x:arr)和for(int &x:arr),前者不可以改变x的值,后者可以。5.5节详解cin.get()函数。p152

循环和文本输入

  • cin在获取用户输入的字符时,将忽略空格和换行符,并且,发送给cin的输入会被缓冲,只有在用户按下回车键后,他输入的内容才会被发送给程序,为了读取空格和换行符,可以利用cin.get(char)进行补救,char的函数声明是引用,所以,可以改变char的值。p154

第六章 分支语句和逻辑运算符

if语句

逻辑表达式

字符函数库cctype

?:运算符

switch语句

  • p181 :c++的switch语句中必须是整数表达式,一般为int或char或枚举

break和continue语句

读取数字的循环

简单文件输入/输出

  • 打开已经存在的文件,接受输出时,默认将它的长度截断为零,文件原来的内容会丢失。p194
  • 函数exit()的原型是在头文件cstdlib中定义的,在该头文件中,还定义了一个用于操作系统通信的参数值EXIT_FAILURE。p195
  • windows系统中的文本文件每行都已回车字符和换行符两个字符结尾,在通常情况下,C++在读取文件时将这两个字符转换为换行符,并在写入文件时执行相反的转换。有些文本编辑器不会自动在文件的最后一行加上换行符,因此,需要手动按下回车键再保存文件。p196

第七章 函数——C++的编程模块

复习函数的基本知识

  • 在C++中不能将数组作为函数返回值 (但是可以将数组作为结构或这对象的组成部分返回)。p204
  • 函数定义必须提供标识符,而函数原型不要求,有类型列表就足够了:void cheers(int),通常,在原型的参数列表中,可以包括变量名,也可以不包括。原型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。但是,好的变量名可以帮助理解程序功能,所以一般建议加上。p206
  • C++与接受可变参数的C函数交互时可能用到:void say(…)的形式。p206
  • 通常,函数原型会自动将被传递的参数强制转换为期望的类型。(但函数重载可以导致二义性,因此不允许某些自动强制类型转换)

函数参数和按值传递

  • C++通常按值传递参数,这会让函数在自身的作用域内保持实参的副本,这种方式在一定程度上可以确保数据的完整性和安全性。

函数和数组

  • 在C++中,当且仅当用于函数头或函数原型中,int *arr和int arr[]的含义才是相同的。在其他的环境下,二者的含义并不同,前者代表指向int类型的指针,后者代表数组名。p213
  • 以下程序说明了数组函数一些有趣的地方,首先,cookies和arr指向同一个地址,但sizeof cookies的值是32,而sizeof arr的值是4。sizeof cookies是整个数组的长度,sizeof arr只是指针变量的长度。这也是必须显示传递数组长度,而不能在函数中使用sizeof arr的原因,因为指针本身并没有指出数组的长度。p215

    1
    2
    int cookies[size]={1,2,3,4,5,6,7,8};
    int *arr = cookies
  • 由为防止函数中无意中修改数组的内容,可以在声明形参的时候使用关键字const,但应注意,这并不是意味着原始数组必须是常量而只意味着不能在函数中修改数组中的值。(对于普通变量来说,由于C++默认按值传递的特性,这种保护会自动实现)p217

  • 使用数组区间(range)的函数 :对于处理数组的函数,必须将数组的数据种类、起始位置和元素个数传递给它,传统的方法是传递数组名和数组个数n。另一种方法是传递两个指针,分别标识数组的开头和结尾,即数组区间。STL方法使用“超尾”的概念来指定区间,即end指针的是最后一个元素后面的指针。p220
  • 指针和const:
    • 情况1,pt指向一个const int,因此不能使用pt来修改这个值,但是这并不意味着age是一个常量,而只是说对于pt来说这是一个常量,我们依然可以直接通过age来修改age的值,但不能通过pt来修改它。同时,我们可以修改pt的值,即pt可以重新指向另一个地址。
    • 情况2,finger只能指向age,但是允许使用finger来修改age。简而言之,finger和ps都是const,而*finger和ps不是。
    • 情况3,stick只能指向age,并且不能通过stick修改age的值。p221
      1
      2
      3
      4
      int age=30;
      const int *pt=&age;
      int *const finger=&age;
      const int * const stick=&age;

函数和二维数组

  • 数组作参数的函数,必须牢记,数组名被视为地址,因此,相应的形参是一个指针,正确的函数原型如下所示,二者含义完全相同,后者可读性更强。注意,前者的括号是必不可少的,式子3代表的是指针数组,而不是指向二维数组的指针。
    1
    2
    3
    int sum (int (*arr)[4])
    int sum (int arr[][4])
    int *arr[4]

函数和C-风格字符串

  • C-风格字符串与常规char数组之间的区别:字符串有内置的结束字符’\0’。p225
  • 空字符’\0’值等于0,因此可以直接用于while()里的循环判定。p227
  • 函数无法返回一个字符串,但是可以返回字符串的地址。p227

函数和结构

  • 在涉及到函数时,结构变量的行为更接近与基本的单值变量,默认情况下是按值传递的,函数将使用原始结构的副本。当结构非常大时,这会增加内存要求,因此更推荐使用指针来传递结构体。指针传递时使用间接成员运算符’->’访问,值传递时使用成员运算符’.’访问。p228
  • 当程序在输入循环以后还需要进行输入时,可以使用 cin.clear() 重置输入。p233

函数和string对象

  • 虽然C-风格字符串和string对象的用途几乎相同,但与char数组相比,string对象更像是一个单一变量,可以将string直接复制,也可以直接在函数中传递。

函数和array对象

  • 在C++中,类对象是基于结构的,因此结构变成方面的考虑因素也适用于类,所以可以按值将对象传递给函数。p236
  • array模板并非只能存储基本类型数据,它还可以存储类对象。p237

递归

  • C++函数允许自己调用自己(然而,与C语言不同,C++不允许main()调用自己)

函数指针

  • 与数据项类似,函数也有地址,函数名即为函数的地址,它是存储其机器语言代码的内存的开始地址。p241
  • 使用场景:要在当前函数中使用不同的算法来实现灵活的功能,可以将算法的函数地址作为参数进行传递,这就是函数指针。p241
  • 注意以下代码的区别。p242

    1
    2
    3
    int think ();
    process(think); //传递了函数的地址,process函数能够在其内部调用think函数
    thought(think()); //传递了函数的返回值
  • 声明函数指针,最简单的方法就是,先写出该函数的原型,然后用(*pf)替换函数名即可,如下所示,pf即为函数指针。注意,括号的优先级比星号高,所以这里括号不可少。p242

    1
    2
    3
    4
    double pam(int,double);
    double (*pf)(int,double); //pf是一个指针,指向doubel (int,double)类型的函数
    double *pf(int,double); //pf是一个函数,返回double *类型的数据
    pf = pam; //正确声明函数指针后,便可以将相应的函数赋给它
  • 在使用函数指针时,下面两种方法等价!这很神奇!前者的好处是强调当前正在使用函数指针,后者的好处是使用起来很方便。至于为什么会这样,主要是因为有两种流派的声音,C++对这两种流派进行了折衷,认为二者都正确。p243

    1
    2
    3
    4
    5
    6
    7
    8
    double pam(int);
    double (*pf)(int);
    pf = pam;
    double y;
    y = pam(5);
    //下面两种方法等价
    y = (*pf)(5);
    y = pf(5);
  • C++11的自动类型推断功能在函数指针声明并初始化时十分方便,以下两种声明初始化方式等价。p245

    1
    2
    3
    const double *f1(const double ar[], int n);
    const bouble *(*pf)(const double ar[], int n) = f1;
    auto pf = f1;
  • 函数指针数组,[]的优先级高级星号,所以先指明了这是一个包含3个元素的数组,声明的其他部分指出了元素的类型。所以pa是一个包含三个指针的数组,每个指针都指向一个函数,该函数返回指向double类型的指针。p245

    1
    2
    3
    4
    5
    6
    7
    const double *(*pa[3])(const double *,int) = {f1,f2,f3};
    auto pb = {f1,f2,f3} //非法! auto只能用于单值初始化,不能用于初始化列表。
    auto pb=pa //但可以利用声明好的pa数组,来声明同样类型的数组。

    //使用时,想使用数组一样即可
    const double *px = pa[0](av,3);
    const double *py = (*pb[0])(av,3); //前面的括号必不可少
  • 下面的声明,表示pd首先是一个指针,它指向一个包含三个元素的数组,数组中的元素是函数指针。这里pd其实就是指向pa的地址,pa是上面声明的函数指针数组的名字,也就是函数指针数组的首地址。p245

    1
    2
    3
    4
    5
    6
    const double* (*(*pd)[3])(const double*,int) = &pa;
    //调用方法,用``(*pd)``代替``pa``即可
    (*pd)[i](av,3); //返回指针
    (*(*pd)[i])(av,3); //与上面等价, 返回指针
    *(*pd)[i](av,3); //注意如果不带括号,先返回指针,然和用星号得到指针指向的值
    *(*(*pd)[i])(av,3) //与上一条等价,先返回指针,然和用星号得到指针指向的值
  • 函数指针的声明有时候会很长,此时可使用auto(C++11)或typedef来对代码进行简化,方便编程。p248

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    //下面两条语句等价,前者使用方便,缺点就是无法直观看出pc的类型,后续程序可能会不小心产生类型赋值错误
    auto pc = &pa;
    const double* (*(*pd)[3])(const double*,int) = &pa;

    // 可以用typedef简化声明
    typedef double real; //正常声明变量,前面加上typedef,即可用后者代替前者

    typedef const double* (*p_fun)(const double*, int);
    p_fun pa[3] = {f1,f2,f3};
    p_fun (*pa)[3] = &pa;

第八章 函数探幽

C++内联函数

  • 常规函数与内敛函数之间的主要区别在于C++编译器如何将它们组合到程序中。
    • 传统函数在被调用后,会立即存储该指令的内存地址,并将函数参数复制到堆栈,跳到函数起点的内存单元,然后执行函数的机器代码,之后再跳回到地址被保存的指令处。 来回跳跃并记录跳跃位置需要一定的开销。p253
    • 内联函数的编译代码与其他程序的代码“内联”起来了,即编译器会使用相应的函数代码来替换函数调用(这就省去了来回跳跃的时间开销和内存开销)。 内联函数无需跳跃时间,因此加快了运行速度,但同时增加了存储内联函数的内存开销,如果程序在10个不同的地方调用同一个内联函数,就需要存储10个副本。
  • 当函数的代码执行时间很短(函数很小),则内联调用可以省去调用时间。但是由于这个过程相当快,因此尽管接伸了该调用过程的大部分时间,但节省的时间绝对值并不大,除非该函数被经常调用。p253
  • 使用内联时,在函数声明或定义前加上关键字inline。通常的做法是省略原型,将整个定义放在原型处,并加上内联关键字。p254

    1
    inline double square(double x) { return x*x}
  • inline工具是C++新增的特性,原始的C语言使用#define来实现内联(文本替换)p255

引用变量

  • 引用变量,是 已定义的变量的别名 ,他的主要作用是用作函数的形参,如此一来,函数将使用原始数据,而不是其副本。
  • &符号在变量前(右值)是代表“取地址”,在类型附近时(左值)代表“引用”
  • 引用和指针的区别(引用看上去很像伪装的指针 “&rodents=prats”):

    1
    2
    3
    int rats = 101;
    int & rodents = rats; //rodents是rats的别名,二者指向同一块内存地址
    int * prats = &rats; //prats指向rats的内存地址
    • 引用在声明的同时必须进行初始化(做函数参数时,在函数调用时使用实参初始化),而不能像指针那样,先声明,在赋值 。引用更接近const指针,必须在创建时进行初始化,一旦与某个变量关联起来,就将一直效忠于它。p256
      1
      2
      3
      4
      5
      6
      7
      8
      9
      int rats = 101;
      int & rodents = rats;
      int * const pr = &rats; //上式是该式的伪装表示
      int bunnies = 50;
      rodents = bunnies; //试图将rodents变成bunnies的别名
      cout<<rodents<<endl; //输出50,和rodents值一样
      count<<rats<<endl; //但同时rats的值也变成了50
      cout<<&rodents<<endl;
      cout<<&bunnies<<endl; //二者的内存地址并不相同
  • const double &ra用作函数参数时,在函数内不能修改ra的值(会报错),这在行为上与按值传递类似,但是当ra内存占用比较大时(结构或对象),就会很省内存(按值传递会生成副本,内存消耗大)。p261

  • 对于基本类型,使用按值传递兼容性更好,因为按值传递可以自动强制类型转换,而const引用的限制更严格,因为它是别名,所以不能将表达式赋给引用。p261

    1
    2
    //现代C++中会报错,但早期C++只会警告,会创建一个临时变量,并将其初始化为x+3.0的值
    double & ra = x + 1.0;
  • 临时变量、引用参数和const: 当前,如果实参与引用参数不匹配,仅当引用参数为const引用时,C++将生成临时变量。创建临时变量的两种情况:p262

    • 实参的类型正确,但不是左值。(字面常量,表达式)
    • 实参的类型不正确,但可以转换为正确的类型。(int转double)
  • 左值:左值参数是可以被引用的数据对象,例如,变量、数组元素、结构成员、引用和接触引用的指针都是左值。 非左值:字面常量(用引号扩起的字符串除外,它们由其地址表示)和包含多项的表达式。 (C语言中,左值最初指的是可出现在赋值语句左边的实体,引入const关键字后,const变量,虽然一般不出现在左边,但是可以通过地址访问它们)
  • 非const引用无法生成临时变量,这是因为如果接受引用参数的函数的意图是修改作为参数传递的变量,临时变量将无法实现修改,所以现在的C++标准禁止创建临时变量(老的编译器只会发出警告”Warning: Temporary used for parameter ‘ra’ in call to refcube(double &)”,遇到这种警告,一定要排除)。p263
  • 将引用参数声明为const引用的理由有三个: p263
    • 使用const可以避免无意中修改数据的变成错误;
    • 使用const使函数能够处理cnost和非const实参,否则只能接受非const数据;
    • 使用const引用能使函数能够正确生成并使用临时变量。
  • C++11新增了另一种引用—— 右值引用(rvalue reference) 。这种引用可指向右值,是使用&&声明的。新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现,实例见18章。&声明的叫左值引用。:p263

    1
    2
    3
    double && rref = std::sqrt(36.00); // not allowed for double &
    double j = 15.0;
    double && jref = 2.0*j + 15.6; //not allowed for double &
  • 返回引用与传统返回机制的区别: 传统返回机制是按值传递函数参数类似,计算关键字return后面的表达式,并将结果返回给调用参数。而返回引用是返回return后面的变量的别名,并不会生成新的副本。p267

  • 返回引用需要注意的问题: 最重要的是要避免返回函数终止时不再存在的内存单元的引用。(同样,也应避免返回指向临时变量的指针)。如下面的情况:p267

    1
    2
    3
    4
    5
    6
    const double & clone(double & dref){
    double newguy;
    newguy = dref;
    return newguy; //返回newguy的引用,但是newguy在函数结束时会释放内存,会报错
    return dref; //返回dref的引用,可行
    }
  • 前者可以编译,后者不可以。因为前者是指针,指向x,而后者是变量,是独立于x的副本。指针和副本都会在函数结束时释放,但是x并不会释放。p268

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>
    using namespace std;

    const int & clone(int & x){ //可以编译
    int *y = &x;
    return *y;
    }
    const int & clone(int & x){ //不可以编译
    int y = x;
    return y;
    }
  • 将C-风格字符串用作string对象引用参数,形参类型为const string &时,实参类型可以为char*, const char*, string等(“abc”类型为const char*)。原因如下:p270

    • string类定义了一种char*string的转换功能,这使得可以使用C-风格字符串来初始化string对象
    • const引用形参具有创建临时变量的属性。因此,当类型不符合时,会创建临时变量
  • 对象、继承和引用: 除了可以使用父类的方法外,继承的另一个特征是,基类引用可以指向派生类对象,而无需进行强制类型转换。这种特征的实际结果是,可以定义一个接受基类引用作为参数的函数,调用该函数时,可以将基类对象作为实参,也可以将派生类对象作为实参。p271
  • 使用引用参数两个主要原因: p274
    • 程序员能够修改调用函数中的数据对象;
    • 通过传递引用而不是整个数据对象,可以提高程序的运行速度。
  • 指导原则: p274
    • 对于使用传递的值而不作修改的函数
      • 如果数据对象很小,如内置数据类型或小型结构,则按值传递;
      • 如果数据对象是数组,则使用指针,因为这是唯一的选择,并将指针声明为指向const的指针;
      • 如果数据对象是较大的结构,则是用const指针或const引用,以提高程序的效率。这样可以节省复制结构所需的时间和空间;
      • 如果数据对象是类对象,则是用const引用。传递类对象参数的标准方式是按引用传递。
    • 对于修改调用函数中的数据的函数
      • 如果数据对象是内置数据类型,则是用指针;
      • 如果数据对象是数组,则只能使用指针;
      • 如果数据对象是结构,则使用引用或指针;
      • 如果数据对象是类,则使用引用。

默认参数

  • 对于带参数列表的函数,必须从右向左添加默认值。(即带默认值的参数的右边所有参数都要有默认值)。p275

    1
    2
    int harpo(int n, int m=4, int j=5); //valid
    int chico(int n, int m=6, int j); //invalid
  • 实参按从左到右的顺序一次被赋给相应的形参,而不能跳过任何参数。(这点与python不同) p275

    1
    2
    3
    beeps = harpo(2);  //same as harpo(2,4,5)
    beeps = harpo(1,8); //same as harpo(1,8,5)
    beeps = harpo(3, ,8); /invalid

函数重载

  • “多态”指的是函数有多种形式,“重载”指的是可以有多个同名的函数。二者指的是一回事 。p276
  • 函数重载的关键是函数的参数列表——函数特征标(function signature)。如果两个函数的参数数目和类型相同,同时参数的排列顺序也相同,则它们的特征标相同,而 参数变量名是无关紧要的 。p277
  • 编译器在检查函数特征标时,将把 类型引用和类型本身视为同一个特征标 。如以下两个看起来不同的特征标是不能共存的(它们都接受同一个参数x,会使得程序具有二义性):p277

    1
    2
    double cube(double x);
    double cube(double &x);
  • 函数重载只看特征标是否相同,不关心函数返回类型。p278

  • 当传入参数类型可以被强制转换时,将调用最匹配的版本:p278

    1
    2
    3
    4
    5
    6
    void staff(double & rs); // matches modifiable lvalue
    void staff(const double & rcs); //matches rvalue, const lvalue

    void stove(double & r1); // matches modifiable lvalue
    void stove(const double & r2); //matches const lvalue
    void stove(double && r3); //matches rvalue
  • 名称修饰: C++通过名称修饰(name decoration)或名称矫正(name mangling)来区分重载函数,它会根据函数原型中指定的形参类型对每个函数名进行加密。p289

函数模板

  • 函数模板是通用的函数描述,它们使用泛型来定义函数。模板并不创建任何函数,而只是告诉编译器如何定义函数。在标准C++98添加关键字typename之前,C++使用关键字class来创建模板。p281

    1
    2
    3
    4
    5
    6
    7
    8
    //如果需要多个将同一种算法用于不同类型的函数,可以使用模板
    template <typename AnyType> //注意没有分号;
    void Swap(AnyType &a, AnyType &b){ //可以交换多种类型
    AnyType temp;
    temp = a;
    a = b;
    b = temp;
    }
  • 函数模板不能缩短可执行程序。对于以不同类型多次调用模板的程序来说,最终仍然会生成多个独立的函数定义,就像以手工方式定义一样。 最终的代码不包含任何模板,而只包含了为程序生成的实际函数。p283

  • 重载的模板: 被重载的模板的函数特征标必须不同:p283

    1
    2
    3
    4
    template <typename T>
    void Swap(T &a, T &b);
    template <typename T>
    void Swap(T *a, T *b, int n);
  • 显式具体化: (具体机制随着C++的演变而不断变化,下面是ISO/ANSI C++标准)p286

    • 对于给定的函数名,可以有非模板函数、模板函数和显式具体化模板函数以及它们的重载版本。
    • 显式具体化的原型和定义应以template<>打头,并通过名称来指出类型。
    • 具体化优先于常规模板,而非模板函数优先于具体化和常规模板。
      1
      2
      3
      4
      5
      struct job{...};
      void Swap(job &, job &); //非模板函数
      template <typename T>
      void Swap(T &, T &); //模板函数
      template <> void Swap<job>(job &, job &); //显式具体化
  • 实例化和具体化: 在代码中包含函数模板本身并不会生成函数定义,它只是一个用于生成函数定义的方案。编译器使用模板为特定类型生成函数定义时,得到的是模板实例(instantiation)。也就是说,模板并非函数定义,模板实例才是函数定义。p288

  • 隐式实例化和显式实例化: p288

    • 隐式(implicit):通过函数调用导致编译器生成模板实例(大多数情况下都是隐式)
    • 显示(explicit):直接命令编译器创建特定的实例,方法如下:
      1
      template void Swap<int>(int, int); //explicit instantiation
  • 显式实例化和显式具体化的区别: p288

    • 显式实例化:使用Swap()模板来生成int类型的函数定义

      1
      template void Swap<int>(int, int); //explicit instantiation
    • 显式具体化(explicit specialization):不要使用Swap()模板来生成函数定义,而应使用专门为int类型显式定义的函数定义。这些原型必须有自己的函数定义。

      1
      2
      template <> void Swap<int>(int &, int &);
      template <> void Swap(int &, int &); //这两句声明等价,任选其一
    • 警告: 试图在同一个文件(或转换单元)中使用同一种类型的显式实例化和显式具体化将出错。

  • 隐式实例化、显式实例化和显式具体化统称为具体化(specialization)。 它们的相同之处在于,它们表示的都是使用具体类型的函数定义,而不是通用描述。p289
  • 重载解析(overloading resolution): 对于函数重载、函数模板和函数模板重载,C++需要(且有)一个定义良好的策略,来决定为函数调用使用哪一个函数定义,尤其是有多个参数时。该策略大概过程如下:p289
    • 第一步:创建候选函数列表。其中包含与被调用函数的名称相同的函数和模板函数。
    • 第二步:使用候选函数列表创建可行函数列表。这些都是参数数目正确的函数,为此有一个隐式转换序列,其中包括实参类型与相应的形参类型完全匹配的情况。
    • 第三步:确定是否有最佳的可行函数。如果有,则使用它,否则该函数调用出错。
  • 匹配顺序: p290
    • 完全匹配,但常规函数优先于模板。
    • 提升转换(如,char和shorts自动转换为int,float自动转换为double)。
    • 标准转换(如,int转换为char,long转换为double)。
    • 用户自定义的转换(如,类声明中定义的转换)。
  • 完全匹配与最佳匹配 完全匹配不等于最佳匹配,通常,有两个函数完全匹配是一种错误,但这一规则有两个例外。即有时候,即使两个函数都完全匹配,仍可完成重载解析。p290

    • 指向非const数据的指针和引用,优先与非const指针和引用参数匹配。 下面两个式子都是完全匹配,但程序会选择前者,而不是报错:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      void recycle(blot &);  //#1
      void recycle(const blot &); //#2

      struct blot {int a; char b[10];};
      blot ink = {25,"spots"};
      recycle(ink); //选择#1,因为ink没有被声明为const

      //然而,const和非const之间的区别只适用于指针和引用指向的数据
      //即,如果是如下定义,则将出现二义性错误
      void recycle(blot);
      void recycle(const blot);
    • 两个完全匹配的函数,一个是非模板函数,另一个不是。此时,非模板函数将优先于模板函数(包括显式具体化)。如果两个完全匹配的函数都是模板函数,则较具体的模板函数优先。 C++98新增的特性—— 部分排序规则(partial ordering rules) 可以找出最具体的模板。

  • 8.5小节涵盖的知识点很多,并且由于篇幅原因,没有详细展开,需要多看。