C++ Primer, 5th Edition 笔记1

<img src="/img/fun/appalled.jpg" width=“100px” style=“float:right”;/>
偶然机会惊骇发现,由于开发面太窄,许多C++的基础知识已经记糊了。

赶紧把C++ Primer拿出来复习一下,当年学的是第4版,现在正好就此机会学习一下第5版,记录一下易忘记的知识点。


第I部分 C++基础

变量和基本类型

基本内置类型

算术类型(字符、整型数、布尔值和浮点数)和空类型。
带符号(signed)和无符号(unsigned)。
C++:算术类型
建议:如何选择类型

unsigned类型使用时要小心。比如下例。当u等于0时,迭代输出0,然后继续执行for语句里的表达式。表达式–u从u中减去1,得到的结果-1并不满足无符号数的要求,于是-1被自动转换成一个合法的无符号数。假设int类型占32位,当u等于0时,–u的结果将是4294967295。
unsigned误用导致死循环
一种解决办法是,用while代替for,因为while能在输出变量之前(而非之后)先减去1。
解决上述错误

字面值常量(literal)。
整型和浮点型字面值
字符和字符串字面值
字符串字面值的类型实际上是由常量字符组成的数组。字面值'A'表示的是单独的字符A,字符串"A"代表一个字符的数组,包含两个字符:一个是字母A、另一个是空字符。
紧邻字符串字面值是一个整体
如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。
转义序列
指定字面值的类型
nullptr是指针字面值。

变量

对象是具有某种数据类型的内存空间。
定义于函数体内的内置类型的对象如果没有初始化,则其值未定义。类的对象如果没有显式地初始化,则其值由类确定。
提示:未初始化变量引发运行时故障
变量能且只能被定义一次,但是可以被多次声明。
如果函数有可能用到某全局变量,则不宜再定义一个同名的局部变量。

复合类型

Compound type如引用和指针。

引用
引用即别名,引用必须被初始化,一旦初始化完成,引用将和它的初始值对象一直绑定在一起,不能再绑定到另外一个对象。因为引用本身不是一个对象,所以不能定义引用的引用。
引用类型的初始值必须是一个对象,而不能是字面值或某个表达式的计算结果。但有两个例外。
int &r1 = i1, &r2 = i2;
const int &r1 = 42 * i;

指针本身就是一个对象,允许对指针赋值和拷贝,可以指向不同的对象。
int *p = &ival; cout << *p; p = nullptr;
关键概念:某些符号有多重含义
赋值永远改变的是等号左侧的对象。
void*是一种特殊的指针类型,可用于存放任意对象的地址。只能做到:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*指针。不能直接操作void*指针所指的对象,因为并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
概括说来,以void*的视角来看内存空间就仅仅是内存空间,没办法访问内存空间中所存的对象。
可以有指针的指针,或者指针的引用。面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清它的真实含义。
int *&r = p; //r是一个对指针p的引用

const限定符

const对象一旦创建后其值就不能再改变,所以const对象必须初始化。
const对象的初始化
默认状态下,const对象仅在文件内有效。如果想让其全局有效,只在一个文件中定义const,而在其他多个文件中声明使用它。解决方法是,对于const变量不管是声明还是定义都添加extern关键字,这样只需定义一次就可以了。
const变量对多文件有效
指针本身是不是常量以及指针所指的是不是一个常量是两个相互独立的问题。顶层const(top-level const)表示指针本身是个常量,底层const(low-level const)表示指针所指的对象是一个常量。
指向常量的指针(pointer to const)。const double pi = 3.14; const double *cptr = &pi;
const指针(const pointer)。不变的是指针本身的值,而非指向的那个值。int *const curErr = &errNumb;

一个对象或表达式是不是常量表达式(const expression)由它的数据类型和初始值共同决定。const int sz = get_size(); //sz不是常量表达式。常量表达式的值需要在编译期就得到计算。
从C++11起,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。如constexpr int mf = 20; //20是常量表达式
在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指的对象无关。
constexpr与指针

处理类型

定义类型别名(type alias)的两种方式:typedef和别名声明。
typedef double wages;
using SI = Sales_item; //C++11后
如果某个类型别名指代的是复合类型或变量,易出错。下图中cstr是指向char的const指针,而非指向const char的指针。
Image Loading

C++11引入了auto,让编译器去分析类型。auto定义的变量必须有初始值。初始值是引用时,auto会以所引用对象的类型为其类型。auto会忽略掉顶层const,如果需要保留需要在auto前自己写上const。

C++11还引入了decltype,作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值。
decltype(f()) sum = x; //sum的类型就是函数f的返回类型
引用从来都作为其所指对象的同义词出现,只有用在decltype处是一个例外,引用还是引用。
Image Loading
如果表达式的内容是解引用操作,则decltype将得到引用类型。decltype(*p)的结果类型是int&,而非int
对于decltype所用的表达式来说,如果变量名加上了一对括号,得到的类型与不加会不同。如果decltype使用的是一个不加括号的变量,得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。变量是一种可以作为赋值语句左值的特殊表达式,所以这样的decltype就会得到引用类型:
Image Loading

字符串、向量和数组

命名空间的using声明

using namespace::name;每个名字都需要独立的using声明。头文件不应包含using声明。

标准库类型string

标准库类型对于一般应用场合来说有足够的效率。
size函数返回的是一个无符号整型数string::size_type。因此切记,如果在表达式中混用了带符号和无符号数可能有意外结果。
cctype头文件中的函数
使用下标执行迭代

其余内容去看博客中已有的STL String总结。

标准库类型vector

原来需要vector<vector<int> >,C++11后那个空格没必要了,vector<vector<int>>即可。
关键概念:vector对象能高速增长
如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。范围for语句体内不应改变其所遍历序列的大小。
vector<int>对象的类型是vector<int>::size_type
提示:只能对确知已存在的元素执行下标操作!

其余内容去看博客中已有的STL Vector总结。

迭代器介绍

去看博客中已有的STL Iterator总结。

数组

数组的维度必须是一个常量表达式,在编译的时候已知。
和vector一样,数组的元素应为对象,因此不存在引用的数组。
字符数组的特殊性,注意结尾的空字符
不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。
要想理解数组声明的含义,最好的办法是从数组的名字开始按照由内向外的顺序阅读。
int *(&arry)[10] = ptrs; //arry是数组的引用,该数组含有10个指针

当使用数组作为一个auto变量的初始值时,推断得出的类型是指针而非数组。
但使用decltype时,推断出的类型是由n个元素组成的数组。

指针也是迭代器。获取数组的尾元素后下一位置指针(尾后指针)的方法。int *e = &arr[10]; //指向arr尾元素的下一位置的指针

C++11引入了两个名为begin和end的函数,定义在iterator头文件中。可以这样用。

1
2
3
int ia[] = {0,1,2,3,4,5,6,7,8,9};
int *beg = begin(ia); //指向ia首元素的指针
int *end = end(ia); //指向arr尾元素的下一位置的指针

数组维度的类型是size_t,两个指针相减的结果类型是ptrdiff_t(带符号类型),都定义在cstddef头文件中。

只要指针指向的是数组中的元素(或者数组中尾元素的下一位置),都可以执行下标运算:

1
2
3
int *p = &ia[2];	//p指向索引为2的元素
int j = p[1]; //p[1]等价于*(p+1),就是ia[3]表示的那个元素
int k = p[-2]; //p[-2]是ia[0]表示的那个元素

内置的下标运算符所用的索引值不是无符号类型,这一点与vector和string不一样。

尽量不要使用C风格字符串,易出错。按此习惯书写的字符串存放在字符数组中并必须以空字符结束。
在cstring头文件中有C标准库提供的处理C风格字符串的函数。这些函数不负责验证其字符串参数。
C风格字符串的函数

现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用string,避免使用C风格的基于数组的字符串。

多维数组

多维数组其实是数组的数组。一个维度表示数组本身大小,另外一个维度表示其元素(也是数组)大小。
多维数组的初始化。

1
2
3
4
int ia[3][4] = { {0,1,2,3}, {4,5,6,7}, {8,9,10,11} };
int ia[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11}; //没有标识每行的花括号,与之前的初始化语句是等价的
int ia[3][4] = { {0}, {4}, {8} }; //显式地初始化每行的首元素
int ix[3][4] = {0,3,6,9}; //显式地初始化第1行,其他元素执行值初始化

多维数组的下标引用和指针。

1
2
3
4
ia[2][3] = arr[0][0][0];	// 用arr的首元素为ia最后一行的最后一个元素赋值
int (&row)[4] = ia[1]; //把row绑定到ia的第二个4元素数组上
int (*p)[3] = ia; //p指向含有3个整数的数组,第一个内层数组的指针
p = &ia[1]; //p指向ia的尾元素

使用范围for语句(C++11)处理多维数组。

1
2
3
4
5
6
size_t cnt = 0;
for (auto &row : ia) //此处row必须为引用,是为了避免数组被自动转为指针,导致内层循环不合法
for (auto &col : row) {
col = cnt;
++cnt;
}

表达式

基础

一元运算符(unary operator)和二元运算符(binary operator)。
当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
建议:处理复合表达式

算术运算符

一元负号运算符对运算对象值取负后,返回其(提升后的)副本。
布尔值不应该参与运算。对大多数运算符来说,布尔类型的运算对象将被提升为int类型。如下例true变成1又变成-1,不等于0于是是true。

1
2
3
4
int i = 1024;
int k = -i; //k是-1024
bool b = true;
bool b2 = -b; //b2是true!。

逻辑和关系运算符

短路求值。

赋值运算符

赋值运算满足右结合律。

递增和递减运算符

除非必须,否则不用递增递减运算符的后置版本。
*pbeg++等价于*(pbeg++),就是解引用pbeg开始时指向的那个元素,并将指针向前移动一个位置。

注意:运算对象可按任意顺序求值。要小心使用。

1
2
3
// 该循环的行为是未定义的!
while (beg != s.end() && !sspace(*beg))
*beg = toupper(*beg++); //错误:该赋值语句未定义

错误在于:赋值运算符左右两端的运算对象都用到了beg,并且右侧的运算对象还改变了beg的值,所以该赋值语句是未定义的。编译器可能按照下面的任意一种思路处理该表达式:

1
2
*beg = toupper(*beg);	//如果先求左侧的值
*(beg+1) = toupper(*beg); //如果先求右侧的值

成员访问运算符

解引用运算符的优先级低于点运算符,执行解引用运算的子表达式两端必须加上括号。

条件运算符

条件运算的嵌套最好别超过两到三层。
条件运算符的优先级非常低,因此当一条长表达式中嵌套了条件运算子表达式时,通常要在其两端加上括号。

位运算符

仅将位运算符用于处理无符号类型。
其余内容去看博客中已有的位运算总结。

sizeof运算符

所得值是一个size_t类型。sizeof不需要真的解引用指针也能知道它所指对象的类型。
sizeof运算符的结果

逗号运算符

逗号运算符首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值。

类型转换

隐式类型转换:算术转换、数组转换成指针、指针的转换、转换成布尔类型、转换成常量、类类型定义的转换。
显式转换:static_cast、dynamic_cast、const_cast、reinterpret_cast(不要用),旧式的强制类型转换。

运算符优先级

参看《C陷阱与缺陷》笔记

语句

简单语句

空语句、小心分号、复合语句(块)。

语句作用域

可以在if/switch/while和for语句的控制结构内定义变量。定义在控制结构当中的变量只在相应的语句内部可见。如while (int i = get_num()) {cout << i << endl;}

条件语句

if语句。switch语句,如果需要为某个case分支定义并初始化一个变量,应该定义在块内,从而确保后面的所有case标签都在变量的作用域之外。

迭代语句

while、for、do while。
使用while循环:1.不确定到底要迭代多少次时。2.在循环结束后访问循环控制变量。
传统for语句头中可定义多个同基础类型的对象。
范围for语句:

1
2
3
4
vector<int> v = {0,1,2,3,4,5,6,7,8,9};
// 范围变量必须是引用类型,这样才能对元素执行写操作
for (auto &r : v) // 对于v中的每一个元素
r *= 2; // 将v中每个元素的值翻倍

do while语句先执行循环体后检查条件,所以不允许在条件部分定义变量。

跳转语句

break/continue/goto(别用)/return。

try语句块和异常处理

C++中的异常处理

函数

函数基础

局部静态对象在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。

参数传递

值、引用。有没有const。只是参数有const不算重载。
不能用字面值初始化一个非常量引用。尽量使用常量引用。

数组形参
管理指针形参有三种常见的技术:使用内部标记指定数组长度、使用标准库规范beg和end、显式传递一个表示数组大小的形参。

数组引用形参

1
2
3
4
5
6
// 正确:形参是数组的引用,维度是类型的一部分
void print(int (&arr)[10])
{
for (auto elem : arr)
cout << elem << endl;
}

传递多维数组

1
2
3
4
// matrix指向数组的首元素,该数组的元素是由10个整数构成的数组
void print(int (*matrix)[10], int rowsize){}
// 或
void print(int matrix[][10], int rowsize){}

含有可变形参的函数
C++11提供两种方法:如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型;如果实参的类型不同,可以写可变参数模板,见16.4。TODO
initializer_list定义在同名头文件中。
initializer_list提供的操作

1
2
3
4
5
6
7
8
9
10
11
void error_msg(initializer_list<string> i1)
{
for (auto beg = i1.begin(); beg != i1.end(); ++beg)
cout << *beg << " ";
cout << endl;
}
//调用。expected和actual是string对象。
if (expected != actual)
error_msg({"functionX", expected, actual});
else
error_msg({"functionX", "okay"});

返回类型和return语句

引用返回左值。main返回0表示成功,其他表示失败,可用cstdlib头文件中的宏EXIT_FAILURE和EXIT_SUCCESS。

声明一个返回数组指针的函数

1
2
3
int arr[10];	//arr是一个含有10个整数的数组
int *p1[10]; //p1是一个含有10个指针的数组
int (*p2)[10] = &arr; //p2是一个指针,它指向含有10个整数的数组

C++11开始的尾置返回类型。
返回类型以->开头写在参数列表后,原来的位置写auto。

1
2
// func接受一个int类型的实参,返回一个指针,该指针指向含有10个整数的数组
auto func(int i) -> int(*)[10];

使用decltype。注意decltype并不负责类型转换。所以下文decltype的结果是个数组,要想表示arrPtr返回指针还必须在函数声明时加一个*符号。

1
2
3
4
5
6
7
int odd[] = {1,3,5,7,9};
int even[] = {0,2,4,6,8};
// 返回一个指针,该指针指向含有5个整数的数组
decltype(odd) *arrPtr(int i)
{
return (i % 2) ? &odd : &even; // 返回一个指向数组的指针
}

函数重载

main函数不能重载。
不允许两个函数除了返回类型外其他所有的要素都相同。
一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。如果形参是某种类型的指针或引用,则此时const是底层的,可以区分其指向的是常量对象还是非常量对象,可以实现函数重载。

特殊用途语言特性

默认实参、内联函数、constexpt
默认实参,顺序要合理。默认实参只在函数声明中指定一次,不能再次指定。
局部变量不能作为默认实参。除此之外,只要表达式的类型能转换成形参所需的类型,该表达式就能作为默认实参:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// wd、def和ht的声明必须出现在函数之外
sz wd = 80;
char def = ' ';
sz ht();
string screen(sz = ht(), sz = wd, char = def);
// 调用screen(ht(), 80, ' ')
string window = screen();
// 内部重新赋值改变默认形参值,但重新声明隐藏原变量不行。
void f2()
{
def = '*'; //改变默认实参的值
sz wd = 100; //隐藏了外层定义的wd,但是没有改变默认值
window = screen(); //调用screen(ht(), 80, '*')
}

内联函数可避免函数调用的开销。
constexpr函数是指能用于常量表达式的函数。返回值类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条return语句。

调试帮助
assert(expr);如果expr为0,输出信息并终止程序的执行。定义在头文件cassert中。常用于检查不能发生的事件。
NDEBUG宏如果定义了则assert什么也不做。也可用其编写自己的条件调试代码。
预处理器定义了5个对于程序调试很有用的名字:

  • __func__存放当前调试的函数的名字。
  • __FILE__存放文件名的字符串字面值。
  • __LINE__存放当前行号的整型字面值。
  • __TIME__存放文件编译时间的字符串字面值。
  • __DATE__存放文件编译日期的字符串字面值。

函数匹配

调用重载函数时应尽量避免强制类型转换。如果在实际应用中确实需要强制类型转换,则说明我们设计的形参集合不合理。

函数指针

1
2
3
4
5
6
bool lengthCompare(const string &, const string &);
bool (*pf) (const string &, const string &);
pf = lengthCompare;
pf = &lengthCompare; //两种赋值语句皆可。
bool b1 = pf("hello", "goodbye"); //调用lengthCompare函数
bool b2 = (*pf)("hello", "goodbye"); //一个等价的调用

在指向不同函数类型的指针间不存在转换规则。可以赋值nullptr或0。
函数指针形参。

1
2
3
4
5
6
7
8
9
// Func和Func2是函数类型
typedef bool Func(const string&, const string&);
typedef decltype(lengthCompare) Func2; //等价的类型
// FuncP和FuncP2是指向函数的指针
typedef bool (*FuncP)(const string&, const string&);
typedef decltype(lengthCompare) *FuncP2; //等价的类型
// 下面两条声明语句声明的是同一个函数,使用了类型别名
void useBigger(const string&, const string&, Func); // 编译器自动将Func表示的函数类型转换成指针
void useBigger(const string&, const string&, FuncP2);

可以返回指向函数的指针。注意必须把返回类型写成指针形式,编译器不会自动地将函数返回类型当成对应的指针类型处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
using F = int(int*, int);	// F是函数类型,不是指针
using PF = int(*)(int*, int); // PF是指针类型
PF f1(int); //正确:PF是指向函数的指针,f1返回指向函数的指针
F f1(int); //错误:F是函数类型,f1不能返回一个函数
F *f1(int); //正确:显式地指定返回类型是指向函数的指针
// 从内到外顺序阅读这条声明语句:f1有形参列表,所以f1是个函数;f1前面有*,所有f1返回一个指针;
// 指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是int。
int (*f1(int))(int*, int);
// 尾置返回类型
auto f1(int) -> int (*)(int*, int);
// 将auto和decltype用于函数指针类型
string::size_type sumLength(const string&, const string&);
decltype(sumLength) *getFcn(const string &);

类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程技术。

定义抽象数据类型

常量对象,以及常量对象的引用或指针都只能调用常量成员函数。
构造,拷贝、赋值和析构。

访问控制与封装

访问说明符public/private,使用class和struct定义类的唯一的区别就是默认的访问权限。
友元函数与类。

1
2
3
friend int test(int* t);
friend int ClassA::test(int* t);
friend class ClassA;

类的其他特性

可变数据成员mutable,即使在一个const对象内也能被修改。
成员函数是否是const的,也可以用来区分进行重载。

1
2
int get_num(){}
const int get_num() const {}

构造函数再探

如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值。
成员初始化顺序与它们在类定义中出现的顺序一致。最好令构造函数初始值的顺序与成员声明的顺序一致。而且如果可能的话,尽量避免使用某些成员初始化其他成员。
如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数。

C++11提供了委托构造函数(delegating constructor)。一个委托构造函数使用它所属类的其他构造函数执行它自己的初始化过程,或者说它把它自己的一些(或者全部)职责委托给了其他构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
class Sales_data
{
public:
// 非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s, unsigned cnt, double price) :
booNo(s), units_sold(cnt), revenue(cnt*price) {}
// 其余构造函数全都委托给另一个构造函数
Sales_data() : Sales_data("", 0, 0) {}
Sales_data(std::string s) : Sales_data(s, 0, 0) {}
Sales_data(std::istream &is) : Sales_data() {read(is, *this);}
// ...
}

隐式的类类型转换。转换构造函数。上例中,接受string的构造函数和接受istream的构造函数分别定义了从这两种类型向Sales_data隐式转换的规则。
通过将构造函数设置为explicit对隐式转换加以阻止。explicit构造函数只能用于直接初始化。

1
2
3
Sales_data item1(null_book);
item.combine(Sales_data(null_book));
item.combine(static_cast<Sales_data>(cin));

类的静态成员

static,关键字只出现在类内部的声明语句。类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据。类的静态成员函数也不与任何对象绑定在一起,不包含this指针。
可以用作用域运算符或者类的对象、引用、指针来访问静态成员。

1
2
3
4
5
6
double r;
r = Account::rate();
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate();
r = ac2->rate();

静态数据成员不属于类的任何一个对象,不是在创建类的对象时被定义的,所以它们不是由类的构造函数初始化的。
静态数据成员可以是不完全类型。特别的,静态数据成员的类型可以就是它所属的类类型。而非静态数据成员则只能声明成它所属类的指针或引用。静态成员可以作为默认实参,非静态数据成员不能,因为它的值本身属于对象的一部分。