《C++ Primer》提纲梳理

我把 《C++ Primer》也给看完了。相比于之前的《C++ Primer Plus》,明显感觉这本书的质量更高一些。这本书中的代码已经完全采用了 C++11 的风格,对于新手来讲可能上手会相对难一点,但明显我还是更推荐看这本书。在这里我也是做了一个提纲整理,但说实在这种归纳方式只是方便于我复习,对于大家来讲还是要自己去把书籍完整地看过才行。而且想提升自己代码水平光看书的话其实帮助不大,多敲代码练习才是重点。 另外整理的过程中我已经省略掉了一些与《C++ Primer Plus》提纲中相同的部分。

基础

  • 一个 函数 的定义包含四个部分:返回类型、函数名、一个括号包围的形参列表(允许为空)以及函数体

  • main 函数的返回类型必须为 int

  • 标准输入: 一个名为 cinistream 类型的对象。标准输出: 以一个名为 coutostream 类型的对象。标准错误: 名为 cerr 的对象。

  • 操纵符:endl ,效果是结束当前行,并将于设备关联的 缓冲区 中的内容刷到设备中。缓冲刷新操作可以保证到目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流。

  • 算数类型 分为两类:整型(包括字符和布尔类型在内)和 浮点型

  • 当我们赋给 无符号类型 一个超出它表示范围的值时,结果是 初始值对无符号类型表示数值总数取模后的余数

  • 当我们赋给 带符号类型 一个超出它表示范围的值时,结果是 未定义的 。此时,程序可能继续工作、可能崩溃、也可能生成垃圾数据。

    1
    2
    3
    4
    unsigned u = 10;
    int i = -42;
    std::cout << i + i << std::endl; //输出 -84
    std::cout << u + i << std::endl; //如果 int 占 32 位,输出 4294967264
  • 如果两个字符串 字面值 位置紧邻且仅由 空格、缩进和换行符 分隔,则它们实际上是一个整体。

  • 在 C++ 中,初始化赋值 是完全不同的操作。

    1
    2
    3
    4
    5
    // C++ 初始化
    int units_sold = 0;
    int units_sold = {0};
    int units_sold {0}; //列表初始化
    int units_sold(0);
  • 如果使用 列表初始化 且初始值存在丢失信息的风险,则编译器将报错。

  • 定义于任何函数体之外的变量被初始化为 0,定义在函数体内部的内置类型变量将 不被初始化

  • string 类规定如果没有指定初值则生成一个空串。

  • 如果想 声明 一个变量而非定义它,就在变量名前添加关键字 extern ,而不要显式地初始化变量。在函数体内部,如果试图初始化一个由 extern 关键字标记的变量,将引发错误。

  • 位于 头文件 的代码一般来说不应该使用 using 声明。

  • break 语句负责终止离它最近的 whiledo whileforswitch 语句,并从这些语句之后的第一条语句开始继续执行。

  • continue 语句终止最近的循环中的当前迭代并立即开始下一次迭代。continue 语句只能出现在 forwhiledo while 循环的内部,或者嵌套在此类循环里的语句或块的内部。

复合类型

引用

  • 引用: 为对象起了另外一个名字,通过将声明符写成 &d 的形式来定义引用类型。

    1
    2
    3
    int ival = 1024;
    int &refVal = ival;
    int &refVal2; // 报错:引用必须被初始化
  • 因为引用本身不是一个对象,所以不能定义引用的引用。

  • 引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。

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

指针

  • 指针: 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。指针无须在定义时赋初值。
  • 指针存放某个对象的 地址 。要想获取该地址,需要使用 取地址符(&
  • 指针的值(即地址)应属于下列 4 种状态之一:
    1. 指向一个对象。
    2. 指向紧邻对象所占空间的下一个位置。
    3. 空指针,意味着指针没有指向任何对象。
    4. 无效指针,也就是上述情况之外的其他值。
  • 如果指针指向了一个对象,则允许使用 解引用符(操作符 * 来访问对象。
  • void* 是一种特殊的指针类型,可用于存放任意对象的地址。

指针是对象,所以存在对指针的引用

1
2
3
4
5
6
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 引用的是一个 int 指针。

const 限定符

  • 默认情况下,const 对象被设定为仅在文件内有效。当多个文件中出现了同名的 const 变量时,其实等同于在不同文件中分别定义了独立的变量。

  • 初始化常量引用 时允许用任意表达式作为初始值。

    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
    3
    4
    5
    int i = 42;
    int &r1 = i; // 引用 r1 绑定对象 i
    const int &r2 = i; // r2 也绑定对象 i,但是不允许通过 r2 修改 i 的值
    r1 = 0; // r1 并非常量,i 的值修改为 0
    r2 = 0; // 错误:r2 是一个常量引用。

  • 指向常量的指针(pointer to const) 不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针。

    1
    2
    3
    4
    const double pi = 3.14; // pi 是个常量,它的值不能改变
    double *ptr = &pi; // 错误:ptr 是一个普通指针
    const double *cptr = &pi; // 正确:cptr 可以指向一个双精度常量
    *cptr = 42; // 错误:不能给 *cptr 赋值
  • 一种例外情况是 允许令一个指向常量的指针指向一个非常量对象


  • 常量指针(const pointer) 必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了,即不变的是指针本身的值而非指向的那个值。

  • 顶层 const 表示 指针本身是个常量底层 const 表示 指针所指的对象是一个常量

  • 顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用。底层 const 则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层 const 也可以是底层 const

    1
    2
    3
    4
    5
    6
    int i = 0;
    int *const p1 = &i; // 不能改变 p1 的值,这是一个顶层 const
    const int ci = 42; // 不能改变 ci 的值,这是一个顶层 const
    const int *p2 = &ci; // 允许改变 p2 的值,这是一个底层 const
    const int *const p3 = p2; // 靠右的 const 是顶层 const,靠左的是底层 const
    const int &r = ci; // 用于声明引用的 const 都是底层 const

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

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

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

    1
    2
    const int *p = nullptr; // p 是一个指向整型常量的指针
    constexpr int *q = nullptr; // q 是一个指向整数的常量指针

处理类型

类型别名

  • 类型别名 是一个名字,是某种类型的同义词。

  • 传统的方法是使用关键字 typedef

    1
    2
    typedef double wages; // wages 是 double 的同义词
    typedef wages base, *p; // base 是 double 的同义词,p 是 double* 的同义词
  • 使用 别名声明 来定义类型别名,这种方法用关键字 using 作为别名声明的开始,其后紧跟别名和等号。

    1
    using SI = Sales_item; // SI 是 Sales_Item 的同义词

auto 类型说明符

  • C++11 新标准引入了 auto 类型说明符,用它就能让编译器替我们去分析表达式所属的类型。

  • auto 定义的变量必须有初始值。

  • auto 一般会忽略掉顶层 const ,同时底层 const 则会保留下来。

    1
    2
    3
    4
    5
    const int ci = i, &cr = ci;
    auto b = ci; // b 是一个整数(ci 的顶层 const 特性被忽略掉了)
    auto c = cr; // c 是一个整数(cr 是 ci 的别名,ci 本身是一个顶层 const)
    auto d = &i; // d 是一个整型指针(整数的地址就是指向整数的指针)
    auto e = &ci; // e 是一个指向整数常量的指针(对常量对象取地址是一种底层 const)
  • 如果希望推断出的 auto 类型是一个顶层 const ,需要明确指出

    1
    const auto f = ci; // ci 的推演类型是 int, f 是 const int

decltype 类型指示符

  • 希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。C++11 新标准引入类型说明符 decltype ,它的作用是 选择并返回操作数的数据类型

    1
    decltype(f()) sum = x; // sum 的类型就是函数 f 的返回类型
  • 如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上了一层或多层括号,编译器就会把它当成是一个表达式。

    1
    2
    3
    // decltype 的表达式如果是加上了括号的变量,结果将是引用
    decltype((i)) d; // 错误:d 是 int&,必须初始化
    decltype(i) e; // 正确:e 是一个(未初始化的) int

标准库类型 string

  • 使用 string 类型必须包含 string 头文件,string 定义在命名空间 std 中。

    1
    2
    3
    4
    string s1; // 默认初始化,s1 是一个空字符串
    string s2 = s1; // s2 是 s1 的副本
    string s3 = "hiya"; // s3 是该字符串字面值的副本
    string s4(10, 'c'); // s4 的内容是 cccccccccc
  • 如果使用 等号 初始化一个变量,实际上执行的是 拷贝初始化 ,编译器把等号右侧的初始值拷贝到新创建的对象中去。如果不使用等号,则执行的是 直接初始化


  • 在执行读取操作时,string 对象会自动忽略开头的空白(即空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇见下一处空白为止。
  • getline 函数的参数是一个 输入流 和一个 string 对象,函数从给定的输入流中读入内容,直到遇到换行符为止(注意换行符也被读进来了)。getline 只要一遇到 换行符 就结束读取操作并返回结果。
  • size 函数返回的是一个 string::size_type 类型的值,它是一个 无符号类型 的值而且足够存放下任何 string 对象的大小。
  • 假设 n 是一个具有 负值int,则表达式 s.size() < n 的判断结果几乎肯定是 true 。因为负值 n 会自动转换成一个比较大的无符号值。

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

    1
    2
    3
    4
    5
    string s1 = "hello", s2 = "world";
    string s4 = s1 + ","; // 正确:把一个 string 对象和一个字面值相加
    string s5 = "hello" + ", "; // 错误:两个运算对象都不是 string
    string s6 = s1 + ", " + "world"; // 正确:每个加法运算符都有一个运算对象是 string
    string s7 = "hello" + ", " + s2; // 错误:不能把字面值直接相加

cctype 头文件中定义了一组标准库函数处理某个字符。

函数 定义
isalnum(c) c 是字母或数字时为真
isalpha(c) c 是字母时为真
isdigit(c) c 是数字时为真
islower(c) c 是小写字母时为真
ispunct(c) c 是标点符号时为真
isspace(c) c 是空白时为真
isupper(c) c 是大写字母时为真
tolower(c) 如果 c 是大写字母,输出对应的小写字母;否则原样输出 c
toupper(c) 如果 c 是小写字母,输出对应的大写字母;否则原样输出 c

下标运算符([] 接受的输入参数是 string::size_type 类型的值。如果某个索引是带符号类型的值将自动转换成由 string::size_type 表达的无符号类型。

标准库类型 vector

标准库类型 vector 表示 对象的集合 ,其中所有对象的类型都相同。使用 vector 类型必须包含 vector 头文件,vector 定义在命名空间 std 中。

1
2
3
4
5
6
7
vector<T> v1;
vector<T> v2(v1);
vector<T> v2 = v1;
vector<T> v3(n, val); // v3 包含了 n 个重复的元素,每个元素的值都是 val
vector<T> v4(n); // v4 包含了 n 个重复地执行了值初始化的对象
vector<T> v5{a, b, c...}; // v5 包含了初始值个数的元素,每个元素被赋予相应的初始值
vector<T> v5 = {a, b, c...};

如果用的是 圆括号 ,可以说提供的值是用来 构造 vector 对象的。如果用的是 花括号,可以表述成我们想 列表初始化vector 对象。

1
2
3
4
5
vector<int> v1(10); // v1 有 10 个元素,每个的值都是 0
vector<int> v2{10}; // v2 有 1 个元素,该元素的值都是 10

vector<int> v3(10, 1); // v3 有 10 个元素,每个的值都是 1
vector<int> v4{10, 1}; // v4 有 2 个元素,值分别是 10 和 1

  • push_back 函数向 vector 的尾端添加元素
  • size 函数返回 vector 对象中的元素,返回值的类型是由 vector 定义的 size_type 类型。
  • 不能用 下标 形式添加元素

迭代器

  • 迭代器 的类型同时拥有返回迭代器的成员。这些类型都拥有名为 beginend 的成员。begin 成员负责 返回指向第一个元素的迭代器end 成员则负责 返回指向容器 “尾元素的下一个位置” 的迭代器。
  • 迭代器使用 递增运算符 来从一个元素移动到下一个元素。
  • 所有标准库容器的迭代器都定义了 ==!= ,但是它们中的大多数都没有定义 < 运算符。
  • 拥有迭代器的标准库类型使用 iteratorconst_iterator 来表示迭代器的类型。
  • beginend 返回的具体类型由对象是否是 常量 决定,如果对象是常量,beginend 返回 const_iterator ;如果对象不是常量,返回 iterator
  • 为了便于专门得到 const_iterator 类型的返回值,C++11 新标准引入了两个新函数,分别是 cbegincend
  • 凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
  • difference_type带符号整型数 表示右侧的迭代器向前移动多少位置就能追上左侧的迭代器的距离。

数组

  • 默认情况下,数组的元素被默认初始化。
  • 定义数组的时候必须制定数组的 类型 ,不允许使用 auto 关键字由初始值的列表推断类型。另外和 vector 一样,数组的元素应为对象,因此不存在引用的数组。
  • 不能将数组的内容拷贝给其他数组作为其初始值,也不能用数组为其他数组赋值。

1
2
3
4
5
6
int *ptrs[10]; // ptrs 是含有 10 个整型指针的数组
int &refs[10] = /*?*/; // 错误:不存在引用的数组
int (*Parray)[10] = &arr; // Parray 指向一个含有 10 个整数的数组
int (&arrRef)[10] = arr; // arrRef 引用一个含有 10 个整数的数组

int *(&array)[10] = ptrs; // array 是数组的引用,该数组含有 10 个指针

当使用数组下标的时候,通常将其定义为 size_t 类型。size_t 是一种机器相关的无符号类型,它被设计得足够大以便能表示内存中任意对象的大小。在 cstddef 头文件中定义了 size_t 类型。


在很多用到数组名字的地方,编译器都会自动地将其替换为一个指向数组首元素的指针。

1
string *p2 = nums; // 等价于 p2 = &nums[0];

当使用数组作为一个 auto 变量的初始值时,推断得到的类型是指针而非数组。

1
2
3
int ia[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // ia 是一个含有 10 个整数的数组
auto ia2(ia); // ia2 是一个整型指针,指向 ia 的第一个元素
ia2 = 42; // 错误:ia2 是一个指针,不能用 int 值给指针赋值
1
auto ia2(&ia[0]); // 显然 ia2 的类型是 int*

当使用 decltype 关键字时上述转换不会发生,decltype(ia) 返回的类型是由 10 个整数构成的数组。

1
2
3
4
// ia3 是一个含有 10 个整数的数组
decltype(ia) ia3 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
ia3 = p; // 错误:不能用整型指针给数组赋值
ia3[4] = i; // 正确:把 i 的值赋给 ia3 的一个元素

C++11 新标准引入了两个名为 beginend 的函数,这两个函数不是成员函数,正确的使用形式是将数组作为它们的参数。

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

两个指针相减的结果的类型是一种名为 ptrdiff_t 的标准库类型,ptrdiff_t 也是一种定义在 cstddef 头文件中的机器相关的类型。因为差值可能为负值,所以 ptrdiff_t 是一种 带符号类型


C 标准库 string 函数,这些函数可用于操作 C 风格字符串,它们定义在 cstring 头文件中。

函数 定义
strlen(p) 返回 p 的长度,空字符不计算在内
strcmp(p1, p2) 比较相等性。如果相等,返回 0;如果 p1 > p2 ,返回正值;如果 p1 < p2 ,返回负值
strcat(p1, p2) p2 附加到 p1 之后,返回 p1
strcpy(p1, p2) p2 拷贝给 p1 ,返回 p1

不能使用 string 对象直接初始化指向字符的指针。string 专门提供了一个名为 c_str 的成员函数。

1
2
char *str = s; // 错误:不能用 string 对象初始化 char*
const char *str = s.c_str(); // 正确

允许使用数组来初始化 vector 对象。只需要拷贝区域的首元素地址和尾后地址就可以。

1
2
int int_arr[] = {0, 1, 2, 3, 4, 5};
vector<int> ivec(begin(int_arr), end(int_arr));

表达式

  • 当一个对象被用作 右值 的时候,用的是对象的值(内容);当对象被用作 左值 的时候,用的是对象的身份(在内存中的位置)。
  • 一个重要的原则是 在需要右值的地方可以用左值来代替,但是不能把右值当成左值使用
  • C++ 语言的早期版本允许结果为负值的商向上或向下取整,C++11 新标准则规定 商一律向 0 取整(即直接切除小数部分) 。隐含的意思是,如果 m%n 不等于 0,则它的符号和 m 相同。
  • C++11 新标准允许使用 花括号 括起来的初始值列表作为赋值语句的右侧运算对象。
  • 赋值运算符满足 右结合律
  • 后置递增运算符 的优先级高于解引用运算符,因此 *pbeg++ 等价于 *(pbeg++)
  • 箭头运算符 作用于一个指针类型的运算对象,结果是一个 左值

sizeof 运算符返回 一个表达式一个类型名字所占的字节数sizeof 运算符满足右结合律,其所得的值是一个 size_t 类型的常量表达式。

1
2
sizeof (type)
sizeof expr

强制类型转换 具有如下形式

1
cast-name<type>(expression);

  • 任何具有明确定义的类型转换,只要不包含底层 const ,都可以使用 static_cast
  • 当需要把一个较大的算术类型赋值给较小的类型时,static_cast 非常有用。
  • 可以使用 static_cast 找回存在于 void* 指针中的值。
  • 虽然不能隐式地将一个左值转换为右值引用,但可以用 static_cast 显式地将一个左值转换为一个右值引用。
1
2
void* p = &d; // 正确:任何非常量对象的地址都能存入 void*
double *p = static_cast<double*>(p); // 正确:将 void* 转换回初始的指针类型

const_cast 只能改变运算对象的底层 const

1
2
const char *pc;
char *p = const_cast<char*>(pc); // 正确:但是通过 p 写值是未定义的行为

只有 const_cast 能改变表达式的常量属性,不能用 const_cast 改变表达式的类型。

1
2
3
4
const char *cp;
char *q = static_cast<char*>(cp); // 错误:static_cast 不能转换掉 const 性质
static_cast<string>(cp); // 正确:字符串字面值转换成 string 类型
const_cast<string>(cp); // 错误:const_cast 只改变常量属性

reinterpret_cast 通常为运算对象的位模式提供较低层次上的重新解释。

函数

  • 形参函数体内部定义的变量 统称为 局部变量 ,同时局部变量还会 隐藏 在外层作用于中同名的其他所有声明。
  • 只存在于块执行期间的对象称为 自动对象
  • 局部静态对象 在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁,在此期间即使对象所在的函数结束执行也不会对它有影响。
  • 如果局部静态变量没有显示的初始值,它将执行值初始化,内置类型的局部静态变量初始化为 0 。
  • 当形参是引用类型时,我们说它对应的实参被 引用传递 或者函数被 传引用调用 。当实参的值被拷贝给形参时,形参和实参是两个相互独立的对象。我们说这样的实参被 值传递 或者函数被 传值调用
  • 当形参有顶层 const 时,传给它常量对象或者非常量对象都是可以的。
  • 使用引用而非 常量引用 会极大限制函数所能接受的实参类型。
  • C++11 新标准规定,函数可以 返回花括号包围的值的列表

指针形参

当执行指针拷贝操作时,拷贝的是指针的值。

1
2
3
4
5
6
7
8
9
void reset(int *ip)
{
*ip = 0; // 改变指针 ip 所指对象的值
ip = 0; // 只改变了 ip 的局部拷贝,实参未被改变
}

int i = 42;
reset(&i); // 改变 i 的值而非 i 的地址
cout << "i= " << i << endl; // 输出 i = 0

数组形参

当我们为函数传递一个数组时,实际上传递的是指向数组首元素的指针。

1
2
3
4
5
// 尽管形式不同,但这三个 print 函数是等价的
// 每个函数都有一个 const int* 类型的形参
void print(const int*);
void print(const int[]);
void print(const int[10]);

因为数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。

main 函数

1
int main(int argc, char *argv[]){...}

第二个形参 argv 是一个数组,它的元素是指向 C 风格字符串的 指针 ;第一个形参 argc 表示数组中的字符串的数量。因为第二个形参是 数组 ,所以 main 函数也可以定义成:

1
int main(int argc, char **argv){...}

当实参传递给 main 函数之后,argv 的第一个元素指向 程序的名字 或者 一个空字符串 ,接下来的元素依次传递命令行提供的实参。最后一个指针之后的元素值保证为 0 。


假定 main 函数位于可执行文件 prog 之内,可以向程序传递下面的选项:

1
prog -d -o ofile data0

argc 应该等于 5,argv 应该包含如下的 C 风格字符串:

1
2
3
4
5
6
argv[0] = "prog";
argv[1] = "-d";
argv[2] = "-o";
argv[3] = "ofile";
argv[4] = "data0";
argv[5] = 0;

main 函数的返回值可以看做是状态指示器。返回 0 表示执行成功,返回其他值表示执行失败。为了使返回值与机器无关,cstdlib 头文件定义了两个预处理变量 EXIT_FAILUREEXIT_SUCCESS

initializer_list 形参

  • 如果函数的实参数量未知但是全部实参的类型都相同,我们可以使用 initializer_list 类型的形参,该类型定义在同名的头文件中。
  • initializer_list 对象中的元素永远是常量值,我们无法改变 initializer_list 对象中元素的值。
  • 如果想向 initializer_list 形参中传递一个值的序列,则必须把序列放在一对花括号内。
  • 含有 initializer_list 形参的函数也可以同时拥有其他形参。

返回数组指针

因为数组不能被拷贝,所以函数不能返回数组。因为我们无法返回数组,所以将返回类型定义成数组的指针。

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

如果我们想定义一个返回数组指针的函数,则数组的维度必须跟在函数名字之后。

1
Type (*function(parameter_list))[dimension]

1
int (*func(int i))[10];
  • func(int i) 表示调用 func 函数时需要一个 int 类型的实参。
  • (*func(int i)) 意味着我们可以对函数调用的结果执行解引用操作。
  • (*func(iny i))[10] 表示解引用 func 的调用将得到一个大小是 10 的数组。
  • int (*func(int i))[10] 表示数组中的元素是 int 类型。

尾置返回类型

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

函数重载

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

constexpr 函数

  • constexpr 函数是指能用于常量表达式的函数。函数的返回类型及所有形参的类型都是字面值类型,而且函数体中必须有且只有一条 return 语句。
  • 允许 constexpr 函数的返回值并非一个常量。

assert 预处理宏

所谓 预处理宏 其实是一个预处理变量,它的行为有点类似于内联函数。定义在 cassert 头文件中。预处理名字由预处理器而非编译器管理,所以可以直接使用 assert 而不是 std::assert ,也不需要为 assert 提供 using 声明。

1
assert(expr);

首先对 expr 求值,如果表达式为假(即 0),assert 输出信息并终止程序的执行。如果表达式为真(即非 0),assert 什么也不做。

函数指针

  • 函数指针 指向的是函数而非对象。
  • 要想声明一个可以指向该函数的指针,只需要用指针替换函数名即可。
1
bool (*pf)(const string &, const string &);

当把函数名作为一个值使用时,该函数自动地转换成指针。

1
2
pf = lengthCompare; // pf 指向名为 lengthCompare 的函数
pf = &lengthCompare; // 等价的赋值语句:取地址符是可选的

还能直接使用指向函数的指针调用该函数,无需提前解引用指针。

1
2
3
bool b1 = pf("hello", "goodbye"); // 调用 lengthCompare 函数
bool b2 = (*pf)("hello", "goodbye"); // 一个等价的调用
bool b3 = lengthCompare("hello", "goodbye"); // 另一个等价的调用

当使用重载函数时,上下文必须清晰地界定到底应该选用哪个函数。

1
2
3
4
void ff(int *)
void ff(unsigned int);

void (*pf)(unsigned int) = ff; // pf1 指向 ff(unsigned)

  • 成员函数 通过一个名为 this 的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化 this
  • 在 C++11 新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上 =default 来要求编译器生成构造函数。
  • =default 既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。
  • 如果 =default 在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下不是内联的。
  • 使用 访问说明符 加强类的分装性。
  • 定义在 public 说明符之后的成员在整个程序内可被访问,public 成员定义类的接口。
  • 定义在 private 说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private 部分封装了类的实现细节。
  • 如果我们使用 struct 关键字,则成员默认是 public 的;如果我们使用 class 关键字,则成员默认是 private 的。

友元

  • 类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的 友元
  • 友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员也不受它所在区域访问控制级别的约束。

可变数据成员

我们希望能修改类的某个数据成员,即使是在一个 const 成员函数内。可以通过在变量的声明中加入 mutable 关键字做到这一点。

隐式的类类型转换

  • 如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作 转换构造函数
  • 在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为 explicit 加以阻止。
  • 关键字 explicit 只对一个实参的构造函数有效。

C++ 标准库

IO 库

IO 类

  • istreamostream 之外,标准库还定义了其他一些 IO 类型。iostream 定义了用于读写流的基本类型,fstream 定义了读写命名文件的类型,sstream 定义了读写内存 string 对象的类型。
  • IO 对象 无拷贝赋值 。进行 IO 操作的函数通常以引用方式传递和返回流。
条件状态
查询流的状态
  • IO 库定义了一个与机器无关的 iostate 类型,它提供了表达 流状态 的完整功能。
  • badbit 表示 系统级错误 ,如不可恢复的读写错误。通常情况下,badbit 被置位,流就无法再使用了。
  • 在发生 可恢复错误 后,failbit 被置位,如期望读取数值却读出一个字符等错误。
  • 如果到达文件结束位置,eofbitfailbit 都会被置位。goodbit 的值为 0,表示流未发生错误。如果 badbitfailbiteofbit 任一个被置位,则检测流状态的条件会失败。
  • 操作 good 在所有错误位均未置位的情况下返回 true ,而 badfaileof 则对应错误位被置位时返回 true
管理条件状态
  • 流对象的 rdstate 成员返回一个 iostate 值,对应流的当前状态。setstate 操作将给定条件位置位,表示发生了对应错误。
  • clear 不接受参数的版本清除(复位)所有 错误标志位 。执行 clear() ,调用 good 会返回 true
1
2
3
4
5
// 记住 cin 的当前状态
auto old_state = cin.restate(); // 记住 cin 的当前状态
cin.clear(); // 使 cin 有效
process_input(cin); // 使用 cin
cin.setstate(old_state); // 将 cin 置为原有状态
管理输出缓冲
刷新输出缓冲区

除了 endl ,IO 库中还有两个类似的操纵符 flushendsflush 刷新缓冲区,但不输出任何额外的字符;ends 向缓冲区插入一个空字符,然后刷新缓冲区。

1
2
3
cout << "hi!" << endl; // 输出 hi 和一个换行,然后刷新缓冲区
cout << "hi!" << flush; // 输出 hi,然后刷新缓冲区,不附加任何额外字符
cout << "hi!" << ends; // 输出 hi 和一个空字符,然后刷新缓冲区
unitbuf 操作符

如果想在每次输出操作后都刷新缓冲区,我们可以使用 unitbuf 操纵符。它告诉流在接下来的每次写操作之后都进行一次 flush 操作。而 nounitbuf 操纵符则重置流,使其恢复使用正常的系统管理的缓冲区刷新机制。

1
2
3
cout << unitbuf; // 所有输出操作后都会立即刷新缓冲区
// 任何输出都立即刷新,无缓冲
cout << nounitbuf; // 回到正常的缓冲方式

文件输入输出

头文件 fstream 定义了三个类型来支持文件 IO:ifstream 从一个给定文件读取数据,ofstream 向一个给定文件写入数据,以及 fstream 可以读写给定文件。

使用文件流对象
  • 每个文件流都定义了一个名为 open 的成员函数,它完成一些系统相关的操作,来定位给定的文件,并视情况打开为读或写模式。
  • 创建 文件流对象 时,我们可以提供文件名。如果提供了一个文件名,则 open 会自动被调用。
  • 在 C++11 标准中,文件名可以是库类型 string 对象,也可以是 C 风格字符数组。

  • 如果我们定义了一个空文件流对象,可以随后调用 open 来将它与文件关联起来。
  • 如果调用 open 失败,failbit 会被置位。因为调用 open 可能失败,进行 open 是否成功的检测通常是个好习惯。
  • 当一个 fstream 对象被销毁时,close 会自动被调用。
文件模式

每个流都有一个关联的 文件模式 ,用来指出如何使用文件。

函数 定义
in 以读方式打开
out 以写方式打开
app 每次写操作前均定位到文件末尾
ate 打开文件后立即定位到文件末尾
trunc 截断文件
binary 以二进制方式进行 IO

ifstream 关联的文件默认以 in 模式打开;与 ofstream 关联的文件默认以 out 模式打开;与 fstream 关联的文件默认以 inout 模式打开。

out 模式打开文件会丢弃已有数据
  • 默认情况下,当我们打开一个 ofstream 时,文件的内容会被丢弃。
  • 阻止一个 ofstream 清空给定文件内容的方法是同时指定 app 模式
1
2
3
4
5
6
7
// 在这几条语句中,file1 都被截断
ofstream out("file1"); // 隐含以输出模式打开文件并截断文件
ofstream out2("file1", ofstream::out); // 隐含地截断文件
ofstream out3("file1", ofstream::out | ofstream::trunc);
// 为了保留文件内容,我们必须显式制定 app 模式
ofstream app("file2", ofstream::app); // 隐含为输出模式
ofstream app2("file2", ofstream::out | ofstream::app);

string

  • sstream 头文件定义了三个类型来支持内存 IO,这些类型可以向 string 写入数据,从 string 读取数据。
  • istringstreamstring 读取数据,ostringstreamstring 写入数据,而头文件 stringstream 既可以从 string 读数据也可向 string 写数据。

顺序容器

顺序容器概述

函数 定义
vector 可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢。
deque 双端队列。支持快速随机访问。在头尾位置插入、删除速度很快。
list 双向链表。只支持双向顺序访问。在 list 中任何位置进行插入、删除操作速度都很快。
forward_list 单项链表。只支持单项顺序访问。在链表任何位置进行插入、删除操作速度都很快。
array 固定大小数组。支持快速随机访问。不能添加或删除元素。
string vector 相似的容器,但专门用于保存字符。随机访问很快。在尾部插入、删除速度很快。
  • forward_listarray 是 C++11 标准增加的类型。
  • array 对象的大小是固定的。因此 array 不支持添加和删除元素以及改变容器大小的操作。
  • forward_list 的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此,forward_list 没有 size 操作。

容器库概览

  • 每个容器都定义在一个头文件中,文件名与类型名相同。

  • forward_list 迭代器不支持 递减运算符

  • 如果需要元素类型,可以使用容器的 value_type 。如果需要元素类型的一个引用,可以使用 referenceconst_reference


将一个新容器创建为另一个容器的拷贝的方法有两种:可以直接拷贝整个容器 ,或者(array 除外)拷贝由一个迭代器对指定的元素范围

1
2
3
4
5
6
7
8
list<string> authors = {"Milton", "Shakespeare", "Austen"};
vector<const char*> articles = {"a", "an", "the"};

list<string> list2(authors); // 正确:类型匹配
deque<string> authList(authors); // 错误:容器类型不匹配
vector<string> words(articles); // 错误:容器类型必须匹配
// 正确:可以将 const char* 元素转换为 string
forward_list<string> words(articles.begin(), articles.end());

  • 当定义一个 array 时,除了指定 元素类型 ,还要指定容器 大小 。与其他容器不同,一个默认构造的 array 是非空的,它包含了与其大小一样多的元素。
  • 标准库 array 类型允许赋值,不支持 assign ,也不允许用 花括号 包围的值列表进行赋值。
1
2
3
4
array<int, 10> a1 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array<int, 10> a2 = {0}; // 所有元素值均为 0
a1 = a2; // 替换 a1 中的元素
a2 = {0}; // 错误:不能将一个花括号列表赋予数组

顺序容器(array 除外)还定义了一个名为 assign 的成员,允许我们 从一个不同但相容的类型赋值,或者从容器的一个子序列赋值

1
2
3
list<string> names;
vector<const char*> oldstyle;
names.assign(oldstyle.cbegin(), oldstyle.cend());

assign 的第二个版本接受一个整型值和一个元素值。它用 指定数目且具有相同给定值的元素替换容器中原油的元素

1
2
list<string> slist1(1); // 1 个元素,为空 string
slist1.assign(10, "Hiya!"); // 10 个元素,每个都是 "Hiya!"

  • swap 操作 交换两个相同类型容器的内容
  • array 外,swap 不对任何元素进行 拷贝、删除或插入操作 ,因此可以保证在常数时间内完成。
  • 与其他容器不同,对一个 string 调用 swap 会导致 迭代器、引用和指针失效
  • swap 两个 array 会真正交换它们的元素。因此,交换两个 array 所需的时间与 array 中元素的数目成正比。

  • 成员函数 size 返回 容器中元素的数目emptysize 为 0 时返回布尔值 true ,否则返回 falsemax_size 返回 一个大于或等于该类型容器所能容纳的最大元素的值
  • forward_list 支持 max_sizeempty ,但不支持 size

顺序容器操作

添加元素
  • arrayforward_list 之外,每个顺序容器(包括 string 类型)都支持 push_back
  • 当我们用一个对象来初始化容器时,或将一个对象插入到容器中时,实际上放入到容器中的是对象值的一个拷贝

listforward_listdeque 容器还支持名为 push_front 的操作。此操作将元素插入到容器 头部


  • insert 成员提供了更一般的添加功能,它允许我们 在容器中任意位置插入 0 个或多个元素vectordequeliststring 都支持 insert 成员。
  • 每个 insert 函数都接受一个迭代器作为其第一个参数。迭代器指出了在容器中什么位置放置新元素。
  • 在 C++11 标准下,接受元素个数或范围的 insert 版本返回 指向第一个新加入元素的迭代器 。如果范围为空,不插入任何元素,insert 操作会将第一个参数返回。

  • C++11 引入了三个新成员 —— emplace_frontemplaceemplace_back ,这些操作 构造 而不是拷贝元素。
  • 当我们调用一个 emplace 成员函数时,则是将参数传递给元素类型的构造函数。emplace 成员使用这些参数在容器管理的内存空间直接构造元素。
访问元素
  • 包括 array 在内的每个顺序容器都有一个 front 成员函数,而除 forward_list 之外的所有顺序容器都有一个 back 成员函数。这两个操作分别返回 首元素和尾元素的引用
  • 在容器中访问元素的成员函数(即,frontback 、下标和 at)返回的都是 引用
  • 如果我们希望确保下标是合法的,可以使用 at 成员函数。at 成员函数类似下标运算符,但如果下标越界,at 会抛出一个 out_of_range 异常。
删除元素
  • pop_frontpop_back 成员函数分别删除 首元素尾元素 。这些操作返回 void
  • vectorstring 不支持 pop_frontforward_list 不支持 pop_back
  • 成员函数 erase 从容器中指定位置删除元素。我们可以删除由一个迭代器指定的单个元素,也可以删除由一对迭代器指定的范围内的所有元素。两种形式的 erase 都返回 指向删除的(最后一个)元素之后位置的迭代器
  • 为了删除一个容器中的所有元素,我们既可以调用 clear ,也可以用 beginend 获得的迭代器作为参数调用 erase
forward_list 操作

forward_list 并未定义 insertemplaceerase,而是定义了名为 insert_afteremplace_aftererase_after 的操作。forward_list 也定义了 before_begin,它返回一个 首前迭代器 ,允许我们在链表首元素之前并不存在的元素 “之后” 添加或删除元素。

改变容器大小

可以使用 resize 来增大或缩小容器,array 不支持 resize 。如果当前大小大于所要求的大小,容器后部的元素会被删除;如果当前大小小于新大小,会将新元素添加到容器后部。

容器管理

函数 定义
c.shrink_to_fit() 请将 capacity() 减少为与 size() 相同大小
c.capacity() 不重新分配内存空间的话,c 可以保存多少元素
c.reserve(n) 分配至少能容纳 n 个元素的内存空间

shrink_to_fit 只适用于 vectorstringdequecapacityreverse 只适用于 vectorstring

额外的 string 操作

  • substr 操作返回一个 string ,它是原始 string 的一部分或全部的拷贝。可以传递给 substr 一个可选的开始位置和计数值。
  • 除了接受迭代器的 inserterase 版本外,string 还提供了接受下标的版本。下标指出了开始删除的位置,或是 insert 到给定值之前的位置。
  • append 操作是在 string 末尾进行插入操作的一种简写形式。
  • repalce 操作是调用 eraseinsert 的一种简写形式。
  • find 函数完成最简单的 搜索 。它查找参数指定的字符串,若找到,则返回第一个匹配位置的下标,否则返回 npos 。搜索(以及其他 string 操作)是大小写敏感的。
  • rfind 成员函数搜索最后一个匹配,即 子字符串最靠右的出现位置
函数 定义
s.find(args) 查找 sargs 第一次出现的位置
s.rfind(args) 查找 sargs 最后一次出现的位置
s.find_first_of(args) s 中查找 args 中任何一个字符第一次出现的位置
s.find_last_of(args) s 中查找 args 中任何一个字符最后一次出现的位置
s.find_first_not_of(args) s 中查找第一个不在 args 中的字符
s.find_last_not_of(args) s 中查找最后一个不在 args 中的字符
  • 根据 s 是等于、大于还是小于参数指定的字符串,s.compare 返回 0、整数或负数。
  • to_string 转换类型。

容器适配器

  • 标准库定义了三个 顺序容器适配器stackqueuepriority_queue
  • 每个适配器都定义两个构造函数:默认构造函数创建一个空对象,接受一个容器的构造函数拷贝该容器来初始化适配器
  • 默认情况下,stackqueue 是基于 deque 实现的,priority_queue 是在 vector 之上实现的。
  • stack 类型定义在 stack 头文件中。
栈函数 定义
s.pop() 删除栈顶元素,但不返回该元素值
s.push(item) 创建一个新元素压入栈顶,该元素通过拷贝或移动 item 而来,或者由 args 构造
s.emplace(args) 同上
s.top() 返回栈顶元素,但不将元素弹出栈顶

queuepriority_queue 适配器定义在 queue 头文件中。

队列函数 定义
q.pop() 返回 queue 的首元素或 priority_queue 的最高优先级的元素,但不删除此元素
q.front() 返回首元素或尾元素,但不删除此元素
q.back() 只适用于 queue
q.top() 返回最高优先级元素,但不删除该元素。只适用于 priority_queue
q.push(item) queue 末尾或 priority_queue 中恰当的位置创建一个元素,其值为 item,或者由 args 构造。
q.emplace(args) 同上

泛型算法

大多数算法都定义在头文件 algorithm 中。标准库还在头文件 numeric 中定义了一组数值泛型算法。

初识泛型算法

只读算法
  • findcount 函数。
  • 另一个是 accumulate ,它定义在头文件 numeric 中。accumulate 函数接受 三个参数,前两个指出了需要求和的元素的范围,第三个参数是和的初值
  • equal 用于确定两个序列是否保存相同的值。它将第一个序列中的每个元素与第二个序列中的对应元素进行比较 。此算法接受三个迭代器,前两个表示第一个序列中的元素范围,第三个表示第二个序列的首元素。
写容器元素的算法
  • fill 接受一对迭代器表示一个范围,还接受一个值作为第三个参数。fill 将给定的这个值赋予输入序列中的每个元素。
  • fill_n 接受一个单迭代器、一个计数值和一个值。它将给定值赋予迭代器指向的元素开始的指定个元素。
  • 一个初学者非常容易犯的错误是在一个 空容器 上调用 fill_n

back_inserter 定义在头文件 iterator 中的一个函数。back_inserter 接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器。

1
2
3
vector<int> vec; // 空向量
auto it = back_inserter(vec); // 通过它赋值会将元素添加到 vec 中。
*it = 42; // vec 中现在有一个元素,值为 42

我们常常使用 back_inserter 来创建一个迭代器,作为算法的目的位置来使用。

1
2
3
vector<int> vec; // 空向量
// 正确:back_inserter 创建一个插入迭代器,作为算法的目的位置来使用。
fill_n(back_inserter(vec, 10, 0); // 添加 10 个元素到 vec

  • 传递给 copy 的目的序列至少要 包含与输入序列一样多的元素 ,这一点很重要。
  • copy 返回的是其目的位置迭代器(递增后)的值。
  • replace 算法读入一个序列,并 将其中所有等于给定值的元素都改为另一个值 。此算法接受 4 个参数:前两个是迭代器,表示输入序列,后两个一个是要搜索的值,另一个是新值。它将所有等于第一个值的元素替换为第二个值。
  • 如果我们希望保留原序列不变,可以调用 replace_copy 。此算法接受额外第三个迭代器参数,指出调整后序列的保存位置。

  • 调用 sort 会重排输入序列的元素,使之有序,它是利用元素类型的 < 元素符来实现排序的。sort 接受两个迭代器,表示要排序的元素范围。
  • 为了保持相同长度的单词按字典序排列,可以使用 stable_sort 算法。这种稳定排序算法维持相等元素的原有顺序。
  • unique 算法重排输入序列,将相邻的重复项消除,并返回一个指向不重复值范围末尾的迭代器。

定制操作

  • 谓词 是一个可调用的表达式,其返回结果是一个能用作条件的值 。标准库算法所使用的谓词分为两类:一元谓词二元谓词
  • find_if 算法接受一对迭代器,表示一个范围。find_if 的第三个参数是一个谓词。它返回第一个使谓词返回非 0 值得元素,如果不存在这样的元素,则返回尾迭代器。
lambda 表达式
  • 一个 lambda 表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数。一个 lambda 具有一个返回类型、一个参数和一个函数体。 lambda可能定义在函数内部。

    1
    [capture list](parameter list) -> return type{function body}
  • capture list(捕获列表)是一个 lambda 所在函数中定义的局部变量的列表(通常为空);return typeparameter listfunction body 与任何普通函数一样,分别表示返回类型、参数列表和函数体。但是,lambda 必须使用 尾置 返回来指定返回类型。

  • 我们可以忽略 参数列表返回类型 ,但必须永远包含 捕获列表函数体

  • 如果忽略返回类型,lambda 根据函数体中的代码推断出返回类型。如果函数体只是一个 return 语句,则返回类型从返回的表达式的类型推断出来。否则,返回类型为 void

  • lambda 不能有默认参数,一个 lambda 调用的实参数目永远与形参数目相等。

  • 一个 lambda 通过 将局部变量包含在其捕获列表中来指出将会使用这些变量 。 捕获列表指引 lambda 在其内部包含访问局部变量所需的信息。

  • 与传值参数类似,采用 值捕获 的前提是变量可以拷贝。与参数不同,被捕获的变量的值是在 lambda 创建时拷贝,而不是调用时拷贝。

  • 为了指示编译器推断捕获列表,应在捕获列表中写一个 &=& 告诉编译器采用捕获引用方式,= 则表示采用值捕获方式。

  • 当我们混合使用隐式捕获和显式捕获时,捕获列表中的第一个元素必须是一个 &=

参数绑定
  • bind 函数定义在头文件 functional 中。它接受一个可调用对象,生成一个新的可调用对象来适应原对象的参数列表。
1
auto newCallable = bind(callable, arg_list);
  • arg_list 中的参数可能包含形如 _n 的名字,其中 n 是一个整数。这些参数是 “占位符”,表示 newCallable 的参数,它们占据了传递给 newCallable 的参数的位置。数值 n 表示生成的可调用对象中参数的位置:_1newCallable 的第一个参数,_2 为第二个参数…
  • 名字 _n 都定义在一个名为 placeholders 的命名空间中,而这个命名空间本身定义在 std 命名空间中。
1
2
3
using std::placeholders::1;

using namespace std::placeholders;
1
2
3
4
// 按单词长度由短至长排序
sort(words.begin(), words.end(), isShorter);
// 按单词长度由长至短排序
sort(words.begin(), words.end(), bind(isShorter, _2, _1));
  • 如果我们希望传递给 bind 一个对象而又不拷贝它,就必须使用标准库 ref 函数。函数 ref 返回一个对象,包含给定的引用,此对象是可以拷贝的。标准库中还有一个 cref 函数,生成一个保存 const 引用的类。refcref 定义在头文件 functional 中。

再探迭代器

iostream 迭代器

istream_iterator 读取输入流,ostream_iterator 向一个输出流写数据。

istream_iterator 操作
1
2
3
4
istream_iterator<int> int_it(cin); // 从 cin 读取 int
istream_iterator<int> int_eof; // 尾后迭代器
ifstream in("afile");
istream_iterator<string> str_it(in); // 从 "afile" 读取字符串
1
2
3
4
5
6
7
istream_iterator<int> in_iter(cin); // 从 cin 读取 int
// eof 被定义为空的 istream_iterator,从而可以当做尾后迭代器来使用
istream_iterator<int> eof; // istream 尾后迭代器。
while(in_iter != eof) // 当有数据可供读取时
// 后置递增运算符读取流,返回迭代器的旧值
// 解引用迭代器,获得从流读取的前一个值
vec.push_back(*in_iter++);
ostream_iterator 操作

当创建一个 ostream_iterator 时,我们可以提供(可选的)第二参数,它是一个字符串,在输出每个元素后都会打印此字符串。此字符串必须是一个 C 风格字符串。

1
2
3
4
ostream_iterator<int> out_iter(cout, " ");
for(auto e : vec)
*out_iter++ = e; // 赋值语句实际上将元素写到 cout
cout << endl;

该程序将 vec 的每个元素写到 cout ,每个元素后加一个空格。每次向 out_iter 赋值时,写操作都会被提交。

当我们向 out_iter 赋值时,可以 忽略解引用和递增运算

1
2
3
for(auot e : vec)
out_iter = e;
cout << endl;
反向迭代器
  • 反向迭代器 就是在容器中从尾元素向首元素反向移动的迭代器。
  • 可以通过调用 rbeginrendcrbegincrend 成员函数来获得反向迭代器。

特定容器算法

listforward_list 定义了独有的 splicesortmergeremovereverseunique

关联容器

  • 两个主要的关联的 关联容器 类型是 mapset
  • 类型 mapmultimap 定义在头文件 map 中;setmultiset 定义在头文件 set 中;无序容器则定义在头文件 unordered_mapunordered_set 中。
  • 关联容器 不支持顺序容器的位置相关的操作 ,例如 push_frontpush_back
  • 关联容器的迭代器都是双向的。
  • mapset 中的关键字必须是唯一的,multimapmultiset 允许多个元素具有相同的关键字。

关联容器操作

  • pair 类型定义在头文件 utility 中。一个 pair 保存两个数据成员。
  • pair 的数据成员是 public 的。两个成员分别命名为 firstsecond 。我们用普通的成员访问符号来访问它们。
  • 可以使用 make_pair 来生成 pair 对象。

关联容器额外的类型别名
key_type 此容器类型的关键字类型
mapped_type 每个关键字关联的类型;只适用于 map
value_type 对于 set ,与 key_type 相同
对于 map ,为 pair<const key_type, mapped_type>
1
2
3
4
5
set<string>::value_type v1; // v1 是一个 string
set<string>::key_value v2; // v2 是一个 string
map<string, int>::value_type v3; // v3 是一个 pair<const string, int>
map<string, int>::key_type v4; // v4 是一个 string
map<string, int>::mapped_type v5; // v5 是一个 int
关联容器迭代器
  • map 而言,value_type 是一个 pair 类型,其 first 成员保存 const 的关键字,second 成员保存值。
  • 虽然 set 类型同时定义了 iteratorconst_iterator 类型,但两种类型都只允许只读访问 set 中的元素。
添加元素
  • 关联容器的 insert 成员向容器中添加一个元素或一个元素范围。由于 mapset (以及对应的无序类型)包含不重复的关键字,因此插入一个已存在的元素对容器没有任何影响。
  • 对一个 map 进行 insert 操作时,必须记住元素类型是 pair
1
2
3
4
5
// 向 map 插入的 4 种方法
word_count.insert({word, 1});
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word, 1));
word_count.insert(map<string, size_t>::value_type(word, 1));
  • insert (或 emplace)返回值依赖于容器类型和参数。对于不包含重复关键字的容器,添加单一元素的 insertemplace 版本返回一个 pair ,告诉我们插入操作是否成功。pairfirst 成员是一个迭代器,指向具有给定关键字的元素;second 成员是一个 bool 值,指出元素是插入成功还是已经存在于容器中。如果关键字已在容器中,则 insert 什么事情也不做,且返回值中的 bool 部分为 false 。如果关键字不存在,元素被插入容器中,且 bool 值为 true
删除元素
  • 通过传递给 erase 一个 迭代器 或一个 迭代器对 来删除一个 元素 或者一个 元素范围 。指定的元素被删除,函数返回 void
  • 关联容器提供一个额外的 erase 操作,它接受一个 key_type 参数。此版本删除所有匹配给定关键字的元素(如果存在的话),返回实际删除的元素的数量。
访问元素
  • 对于不允许重复关键字的容器,可能使用 find 还是 count 没什么区别。
  • 对于允许重复关键字的容器,count 会做更多的工作:如果元素在容器中,它还会统计有多少个元素有相同的关键字。
  • 使用下标操作有一个严重的 副作用 :如果关键字还未在 map 中,下标操作会插入一个具有给定关键字的元素。
  • 如果关键字在容器中,lower_bound 返回的迭代器将指向 第一个具有给定关键字的元素 ,而 upper_bound 返回的迭代器则指向 最后一个匹配给定关键字的元素之后的位置
  • equal_range 接受一个关键字,返回一个迭代器 pair若关键字存在,则第一个迭代器指向第一个与关键字匹配的元素,第二个迭代器指向最后一个匹配元素之后的位置。若未找到匹配元素,则两个迭代器都指向关键字可以插入的位置。

无序容器

  • C++11 定义了 4 个 无序关联容器 。这些容器使用一个 哈希函数 和关键字类型的 == 运算符来组织元素。
  • 无序容器在存储上组织为一组桶,每个桶保存零个或多个元素。无序容器使用一个哈希函数将元素映射到桶。为了访问一个元素,容器首先计算元素的哈希值,它指出应该搜索哪个桶。容器将具有一个特定哈希值的所有元素都保存在相同的桶中。
  • 默认情况下,无序容器使用关键字类型的 == 运算符来比较元素,它们还是用一个 hash<key_type> 类型的对象来生成每个元素的哈希值。

动态内存

  • C++11 提供了两种 智能指针 类型来管理动态对象。智能指针的行为类似常规指针,重要的区别是 它负责释放所指向的对象
  • shared_ptr 允许 多个指针指向同一个对象unique_ptr独占所指向的对象weak_ptr 伴随类是一种弱引用,指向 shared_ptr 所管理的对象 。这三种类型都定义在 memory 头文件中。
  • 智能指针也是模板,创建一个智能指针时,必须提供额外的信息 —— 指针可以指向的类型。
  • 默认初始化的智能指针中保存着一个空指针。

shared_ptr

  • 最安全的分配和使用动态内存的方法是调用一个名为 make_shared 的标准库函数。此函数在动态内存中分配一个对象并初始化它,返回指向此对象的 shared_ptrmake_shared 也定义在头文件 memory 中。
1
2
// 指向一个值为 42 的 int 的 shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
  • 当进行拷贝或赋值操作时,每个 shared_ptr 都会记录有多少个其他 shared_ptr 指向相同的对象。通常称为 引用计数 。无论何时拷贝一个 shared_ptr ,计数器都会递增。一旦一个 shared_ptr 的计数器变为 0,它会自动释放自己所管理的对象。
  • 可以用 reset 来将一个新的指针赋予一个 shared_ptr
1
2
p = new int(1024); // 错误:不能将一个指针赋予 shared_ptr
p.reset(new int(1024)); // 正确:p 指向一个新对象

unique_ptr

  • 某个时刻只能有一个 unique_ptr 指向一个给定对象。当 unique_ptr 被销毁时,它所指向的对象也被销毁。
  • 当我们定义一个 unique_ptr 时,需要将其绑定到一个 new 返回的指针上。类似 shared_ptr ,初始化 unique_ptr 必须采用 直接初始化 形式。
1
unique_ptr<int> p2(new int(42));
  • unique_ptr 不支持普通的拷贝或赋值操作。
1
2
3
4
unique_ptr<string> p1(new string("ssss"));
unique_ptr<string> p2(p1); // 错误:unique_ptr 不支持拷贝
unique_ptr<string> p3;
p3 = p1; // 错误:unique_ptr 不支持赋值
  • 虽然不能拷贝或赋值 unique_ptr ,但可以通过调用 releasereset 将指针的所有权从一个 (非 constunique_ptr 转移给另一个 unique
1
2
3
4
5
// 将所有权从 p1 转移给 p2
unique_ptr<string> p2(p1.release()); // release 将 p1 置为空
unique_ptr<string> p3(new string("Trex"));
// 将所有权从 p3 转移给 p2
p2.reset(p3.release()); // reset 释放了 p2 原来指向的内存
  • reset 成员接受一个可选的指针参数,令 unique_ptr 重新指向给定的指针。如果 unique_ptr 不为空,它原来指向的对象被释放。
  • 可以拷贝或赋值一个将要被销毁的 unique_ptr 。最常见的例子是从函数返回一个 unique_ptr
1
2
3
4
5
unique_ptr<int> clone(int p)
{
// 正确:从 int* 创建一个 unique_ptr<int>
return unique_ptr<int>(new int(p));
}

weak_ptr

  • weak_ptr 是一种不控制所指向对象生存期的智能指针,它指向由一个 shared_ptr 管理的对象。

  • 将一个 weak_ptr 绑定到一个 shared_ptr 不会改变 shared_ptr 的引用计数。一旦最后一个指向对象的 shared_ptr 被销毁,对象就会被释放。即使有 weak_ptr 指向对象,对象也还是会被释放。

  • 当创建一个 weak_ptr 时,要用一个 shared_ptr 来初始化它。

    1
    2
    auto p = make_shared<int>(42);
    weak_ptr<int> wp(p); // wp 弱共享 p;p 的引用计数未改变
  • 由于对象可能不存在,不能使用 weak_ptr 直接访问对象,而必须调用 lock 。此函数检查 weak_ptr 指向的对象是否仍存在。如果存在,lock 返回一个指向共享对象的 shared_ptr

    1
    2
    3
    4
    if (shared_ptr<int> np = wp.lock()) // 如果 np 不为空则条件成立
    {
    // 在 if 中,np 与 p 共享对象
    }

动态数组

new 和数组
  • 为了让 new 分配一个对象数组,要在类型名之后跟一对方括号,在其中指明要分配的对象的数目。

    1
    2
    // 调用 get_size 确定分配多少个 int
    int *pia = new int[get_size()]; // pia 指向第一个 int
  • 也可以用一个表示数组类型的类型别名来分配一个数组,这样 new 表达式中就不要方括号了。

    1
    2
    typedef int arrT[42]; // arrT 表示 42 个 int 的数组类型
    int *p = new arrT; // 分配一个 42 个 int 的数组;p 指向第一个 int
  • 当用 new 分配一个数组时,并未得到一个数组类型的对象,而是 得到一个数组元素类型的指针

  • 当用 new 分配一个大小为 0 的数组时,new 返回一个合法的非空指针。此指针保证与 new 返回的其他任何指针都不相同。对于零长度的数组来说,此指针就像尾后指针一样,但此这指针不能解引用。

  • 为了释放动态数组,使用一种特殊形式的 delete —— 在指针前加上一个 空方括号

  • 为了用一个 unique_ptr 管理动态数组,必须在对象类型后面跟一对 空方括号

    1
    2
    unique_ptr<int[]> up(new int[10]);
    up.release(); // 自动用 delete[] 销毁其指针
  • unique_ptr 不同,shared_ptr 不直接支持管理动态数组。如果希望使用 shared_ptr 管理一个动态数组,必须提供自己定义的删除器。

    1
    2
    3
    // 为了使用 shared_ptr, 必须提供一个删除器
    shared_ptr<int> sp(new int[10], [](int *p){ delete[] p; });
    sp.reset(); // 使用我们提供的 lambda 释放数组,它使用 delete[]
allocator
  • 标准库 allocator 类定义在头文件 memory 中,它帮助我们将内存分配和对象构造分离开来。

  • allocator 是一个模板。为了定义一个 allocator 对象,必须指明这个 allocator 可以分配的对象类型。

    1
    2
    allocator<string> alloc; // 可以分配 string 的 allocator 对象
    auto const p = alloc.allocate(n); // 分配 n 个未初始化的 string
  • allocator 分配的内存是 未构造的 ,按需要在此内存中构造对象。在新标准库中,construct 成员函数 接受一个指针和零个或多个额外参数,在给定位置构造一个元素

    1
    2
    3
    4
    auto q = p; // q 指向最后构造的元素之后的位置
    alloc.construct(q++); // *q 为空字符串
    alloc.construct(q++, 10, 'c'); // *q 为 cccccccccc
    alloc.construct(q++, "hi"); // *q 为 hi!
  • 当用完对象后,必须对每个构造的元素调用 destroy 来销毁它们。函数 destroy 接受一个指针,对指向的对象执行析构函数。

    1
    2
    while (q != p)
    alloc.destroy(--q); // 释放我们真正构造的 string
  • 一旦元素被销毁后,就可以重新使用这部分内存来保存其他 string ,也可以将其归还给系统。释放内存通过调用 deallocate 来完成。

    1
    alloc.deallocate(p, n);
  • 标准库为 allocator 类定义了两个伴随算法,可以在未初始化内存中创建对象。它们都定义在头文件 memory 中。

    1
    2
    3
    4
    5
    6
    // 分配比 vi 中元素所占用空间大一倍的动态内存
    auto p = alloc.allocate(vi.size() * 2);
    // 通过拷贝 vi 中的元素来构造从 p 开始的元素
    auto p = uninitialized_copy(vi.begin(), vi.end(), p);
    // 将剩余元素初始化为 42
    uninitialized_fill_n(q, vi.size(), 42);
  • uninitialized_copy 接受三个迭代器参数。前两个表示 输入序列 ,第三个表示这些元素将要拷贝到的 目的空间 。传递给 uninitialized_copy 的目的位置迭代器必须指向未构造的内存。uninitialized_copy 在给定目的位置构造元素。

  • uninitialized_copy 返回(递增后的)目的位置迭代器。

类设计者的工具

拷贝控制

当定义一个类时,我们显式地或隐式地指定在此类型的对象拷贝、移动、赋值和销毁时做什么。一个类通过定义五中特殊的成员函数来控制这些操作,包括:拷贝构造函数拷贝赋值运算符移动构造函数移动赋值运算符析构函数 。拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。我们称这些操作为 拷贝控制操作

拷贝构造函数

  • 如果一个构造函数的第一个参数是 自身类类型的引用 ,且任何额外参数都有默认值,则此构造函数是 拷贝构造函数 。拷贝构造函数通常不应该是 explicit 的。
  • 拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。
  • 如果使用的初始化值要求通过一个 explicit 的构造函数来进行类转换,那么使用拷贝初始化还是直接初始化就不是无关紧要的了。

三 / 五法则

  • 当决定一个类是否要定义它自己版本的拷贝控制成员时,一个基本原则是首先确定这个类是否需要一个 析构函数 。如果这个类需要一个析构函数,几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符。
  • 如果一个类需要一个 拷贝构造函数 ,几乎可以肯定它也需要一个 拷贝赋值运算符 。然而,无论是需要拷贝构造函数还是需要拷贝赋值运算符都不必然意味着也需要析构函数。
  • 一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。

使用 =default

  • 可以通过拷贝控制成员定义为 =default 来显式地要求编译器生成合成的版本。
  • 当在类内使用=default 修饰成员的声明时,合成的函数将隐式地声明为内联的。

使用 =delete

  • 在新标准下,可以通过将拷贝构造函数和拷贝赋值运算符定义为 删除的函数 来阻止拷贝。删除的函数是这样的一种函数:虽然声明了它们,但不能以任何方式使用它们。在函数的参数列表后面加上 =delete 来指出希望将它定义为删除的。
  • =delete 必须出现在函数第一次声明的时候。可以对任何函数指定 =delete
  • 不能删除析构函数。

右值引用

  • 所谓 右值引用 就是必须绑定到右值的引用。通过 && 而不是 & 来获得右值引用。

  • 右值引用智能绑定到一个将要销毁的对象。

  • 一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。

    1
    2
    3
    4
    5
    6
    int i = 42;
    int &r = i; // 正确:r 引用 i
    int &&rr = i; // 错误:不能将一个右值引用绑定到一个左值上
    int &r2 = i * 42; // 错误:i * 42 是一个右值
    const int &r3 = i * 42; // 正确:我们可以将一个 const 的引用绑定到一个右值上
    int &&rr2 = i * 42; // 正确:将 rr2 绑定到乘法结果上
  • 返回非引用类型的函数,连同算数、关系、位以及后置递增 / 递减运算符,都生成右值。

  • 可以将一个 const 的左值引用或者一个右值引用绑定到这类表达式上。


  • 可以显式地将一个左值转换为对应的右值引用类型。可以通过调用一个名为 move 的新标准库函数来获得绑定到左值上的右值引用,此函数定义在头文件 utility 中。
  • move 不提供 using 声明。直接调用 std::move 而不是 move

移动构造函数和移动赋值运算符

  • 移动构造函数的第一个参数是该类类型的一个 引用 。这个引用参数在移动构造函数中是一个右值引用。
  • 移动构造函数必须确保移后源对象处于这样一个状态 —— 销毁它是无害的。
  • 移动操作 不应抛出任何异常,不分配任何新内存
  • 如果类定义了一个移动构造函数和 / 或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。

重载运算与类型转换

当把运算符定义成成员函数时,它的左侧运算对象必须是运算符所属类的一个对象。

1
2
3
string s = "world";
string t = s + "!"; // 正确:我们能把一个 const char* 加到一个 string 对象中
string u = "hi" + s; // 如果 + 是 string 的成员,则产生错误

重载输出运算符 <<

  • 通常情况下,输出运算符的第一个形参是一个非常量 ostream 对象的引用,之所以 ostream 是非常量是因为向流写入内容会改变其状态;而该形参是引用是因为我们无法直接复制一个 ostream 对象。
  • 第二个形参一般来说是一个常量的引用,该常量是我们想要打印的类类型。
  • 为了与其他输出运算符保持一致,operator<< 一般要返回它的 ostream 形参。

重载输入运算符 >>

通常情况下,输入运算符的第一个形参是运算符将要读取的流的引用,第二个形参是将要读入到的(非常量)对象的引用。

下标运算符

  • 为了与下标的原始定义兼容,下标运算符通常以所访问元素的引用作为返回值,这样做的好处是下标可以出现在赋值运算符的任意一端。
  • 最好同时定义下标运算符的常量版本和非常量版本,当作用于一个常量对象时,下标运算符返回常量引用以确保我们不会给返回的对象赋值。

递增和递减运算符

后置版本接受一个额外的(不被使用)int 类型的形参。当我们使用后置运算符时,编译器为这个形参提供一个值为 0 的实参。这个形参的唯一作用就是区分前置版本和后置版本的函数。

标准库 function 类型

function 定义在 functional 头文件中。function 是一个模板,当创建一个具体的 function 类型时必须提供额外的信息。

1
2
3
4
5
6
7
8
9
function<int(int, int)> // 表示接受两个 int、返回一个 int 的可调用对象。

function<int(int, int)> f1 = add; // 函数指针
function<int(int, int)> f2 = divide(); // 函数对象类的对象
function<int(int, int)> f3 = [](int i, int j){ return i * j; }; // lambda

cout << f1(4, 2) << endl; // 打印 6
cout << f2(4, 2) << endl; // 打印 2
cout << f3(4, 2) << endl; // 打印 8
1
2
3
4
5
6
7
map<string, function<int(int, int)>> binops = {
{"+", add}, // 函数指针
{"-", std::minus<int>()}, // 标准库函数对象
{"/", divide()}, // 用户定义的函数对象
{"*", [](int i, int j){ return i * j; }}, // 未命名的 lambda
{"%", mod} // 命名了的 lambda 对象
};

不能(直接)将重载函数的名字存入 function 类型的对象中。

1
2
3
4
int add(int i, int j){ return i + j; }
Sales_data add(const Sales_data&, const Sales_data&);
map<string, function<int(int, int)>> binops;
binops.insert( {"+", add}); // 错误,二义性

解决上述二义性问题的一条途径是 存储函数指针而非函数的名字

1
2
int (*fp)(int, int) = add;  // 指针所指的 add 是接受两个 int 的版本
binops.insert( {"+", fp} ); // 正确:fp 指向一个正确的 add 版本

同样,也可以使用 lambda 来消除二义性。

1
binops.insert( {"+", [](int a, int b){return add(a, b)}; });

类型转换运算符

  • 类型转换运算符 是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。type 表示某种类型。

    1
    operator type() const;
  • 类型转换运算符可以面向任何类型(除了 void 之外)进行定义,只要该类型能作为函数的返回类型。不允许转换成 数组 或者 函数类型 ,但允许转换成 指针(包括数组指针及函数指针) 或者 引用类型

  • 类型转换运算符 既没有显式的返回类型,也没有形参,而且必须定义成类的成员函数 。类型转换运算符通常不应该改变待转换对象的内容,因此,类型转换运算符一般被定义成 const 成员。

面向对象程序设计

面向对象程序设计 的核心思想是 数据抽象继承动态绑定 。通过使用 数据抽象 ,可以将 类的接口与实现分离 ;使用 继承 ,可以 定义相似的类型并对其相似关系建模 ;使用 动态绑定 ,可以在 一定程度上忽略相似的类型的区别,而以统一的方式使用它们的对象

继承

  • 对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成 虚函数
  • 类派生列表 的形式是:首先是一个 冒号 ,后面紧跟以 逗号 分隔的 基类列表 ,其中每个基类前面可以有 访问说明符
  • 派生类必须在其内部对所有重新定义的虚函数进行声明。
  • C++11 允许派生类显式地注明它将使用哪个成员函数改写基类的虚函数,具体措施是在该函数的形参列表之后增加一个 override 关键字。

基类

  • 基类通过在其成员函数的声明语句之前加上关键字 virtual 使得该函数执行 动态绑定
  • 如果基类把一个函数声明成虚函数,则该函数在派生类中隐式地也是虚函数。
  • 成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时。

派生类

  • 派生类必须通过使用 类派生列表 明确指出它是从哪个(哪些)基类继承而来。
  • 类派生列表的形式是:首先是一个 冒号 ,后面紧跟以 逗号 分隔的 基类列表 ,其中每个基类前面可以有以下三个访问说明符中的一个:publicprotected 或者 private
  • C++11 允许派生类显式地注明它使用某个成员函数覆盖了它继承的虚函数。具体做法是在形参列表后面、或者在 const 成员函数的 const 关键字后面、或者在引用成员函数的引用限定符后面添加一个关键字 override
  • 派生类的声明与其他类差别不大,声明中包含类名但是不包含它的派生列表。
  • C++11 提供了一种防止继承发生的方法,即在类名后跟一个关键字 final

虚函数

  • 一个派生类的函数如果覆盖了某个继承而来的虚函数,则它的形参类型必须与被它覆盖的基类函数完全一致。

  • 派生类中虚函数的返回类型也必须与基类函数匹配。

  • 在 C++11 中我们可以使用 override 关键字来说明派生类中的虚函数。这么做的好处是在使得程序员的意图更加清晰地同时让编译器可以为我们发现一些错误。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    struct B{
    virtual void f1(int) const;
    virtual void f2();
    void f3();
    };

    struct D1 : B{
    void f1(int) const override; // 正确:f1 与基类中的 f1 匹配
    void f2(int) override; // 错误:B 没有形如 f2(int) 的函数
    void f3(int) override; // 错误:f3 不是虚函数
    void f4() override; // 错误:B 没有名为 f4 的函数
    };
  • 如果已经把函数定义成了 final 了,则之后任何尝试覆盖该函数的操作都将引发错误。

    1
    2
    3
    4
    5
    6
    7
    8
    struct D2 : B {
    // 从 B 继承 f2() 和 f3(),覆盖 f1(int)
    void f1(int) const final; // 不允许后续的其他类覆盖 f1(int)
    };
    struct D3 : D2 {
    void f2(); // 正确:覆盖从间接基类 B 继承而来的 f2
    void f1(int) const; // 错误:D2 已经将 f2 声明成 final
    };
  • 虚函数也可以拥有 默认实参

抽象基类

  • 通过在函数体的位置(即在声明语句的分号之前)书写 =0 就可以将一个虚函数说明为纯虚函数。其中,=0 只能出现在类内部的虚函数声明语句处。
  • 也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。也就是说,不能在类的内部为一个 =0 的函数提供函数体。
  • 含有(或者未经覆盖直接继承)纯虚函数的类是 抽象基类 。不能(直接)创建一个抽象基类的对象。

访问控制

受保护的成员

一个类使用 proteceted 关键字来说明那些它希望与派生类分享但是不想被其他公共访问使用的成员。

  • 和私有成员类似,受保护的成员对于类的用户来说是不可访问的。
  • 和公有成员类似,受保护的成员对于派生类的成员和友元来说是可访问的。
  • 派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权。
友元与继承

友元关系不能被传递与继承。

改变个别成员的可访问性
  • 有时需要改变派生类继承的某个名字的访问级别,通过使用 using 声明可以达到这个目的。
  • 通过在类的内部使用 using 声明语句,可以将该类的直接或间接基类中的任何可访问成员标记出来。
  • 如果一条 using 声明语句出现在类的 private 部分,则该名字只能被类的成员和友元访问;如果 using 声明语句位于 public 部分,则类的所有用户都能访问它;如果 using 声明语句位于 protected 部分,则该名字对于成员、友元和派生类都是可访问的。

继承中的类作用域

当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义。

构造函数与拷贝控制

  • 基类通常应该定义一个虚析构函数,析构函数的虚属性也会被继承。
  • 如果一个类定义了析构函数,即使它通过 =default 的形式使用了合成的版本,编译器也不会为这个类合成移动操作。

  • 一个类只继承其直接基类的构造函数。类不能继承默认、拷贝和移动构造函数。
  • 派生类继承基类构造函数的方式是提供了一条注明了(直接)基类名的 using 声明语句。
  • 当一个基类构造函数含有默认实参时,这些实参并不会被继承。相反,派生类将获得多个继承的构造函数,其中每个构造函数分别省略掉一个含有默认实参的形参。

模板与泛型编程

模板定义以关键字 template 开始,后跟一个 模板参数列表 ,这是一个 逗号分隔 的一个或多个 模板参数 的列表。

函数模板

非类型模板参数
  • 可以在模板定义 非类型参数 。一个非类型参数表示一个值而非一个类型。通过一个特定的类型名而非关键字 classtypename 来指定非类型参数。

  • 当一个模板被实例化时,非类型参数被一个用户提供的或编译器推断出的值所代替。这些值必须是常量表达式,从而允许编译器在编译时实例化模板。

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

类模板

  • 当使用一个类模板类型时必须提供模板实参。在类模板自己的作用域中,可以直接使用模板名而不提供实参。
  • 如果一个类模板包含一个非模板友元,则友元被授权可以访问所有模板实例。如果友元自身是模板,类可以授权给所有友元模板实例,也可以只授权给特定实例。

模板参数

如果希望使用一个模板类型参数的类型成员,就必须显式告诉编译器该名字是一个类型。通过使用关键字 typename 来实现这一点。

1
2
3
4
5
6
7
8
template <typename T>
typename T::value_type top(const T& c)
{
if (!c.empty())
return c.back();
else
return typename T::value_type();
}

模板实参推断

1
2
3
// 编译器无法推断 T1,它未出现在函数参数列表中
template <typename T1, typename T2, typename T3>
T1 sum(T2, T3);

在本例中,没有任何函数实参的类型可用来推断 T1 的类型。每次调用 sum 时调用者都必须为 T1 提供一个 显式模板实参

显式模板实参在尖括号中给出,位于函数名之后,实参列表之前。

1
2
// T1 是显式指定的,T2 和 T3 是从函数实参类型推断而来的
auto val3 = sum<long long>(i, lng); // long long sum(int, long)

此调用显式指定 T1 的类型。而 T2T3 的类型则由编译器从 ilng 的类型推断出来。

1
2
3
4
5
6
7
8
// 糟糕的设计:用户必须指定所有三个模板参数
template <typename T1, typename T2, typename T3>
T3 alternative_sum(T2, T1);

// 错误:不能推断前几个模板参数
auto val3 = alternative_sum<long long>(i, lng);
// 正确:显式指定了所有三个参数
auto val2 = alternative_sum<long long, int, long>(i, long);

高级主题

标准库特殊设施

tuple 类型

  • 一个 tuple 可以有任意数量的成员。每个确定的 tuple 类型的成员数目是固定的,但一个 tuple 类型的成员数目可以与另一个 tuple 类型不同。

  • tuple 类型及其伴随类型和函数都定义在 tuple 头文件中。

  • 当定义一个 tuple 时,需要指出每个成员的类型。

  • 当创建一个 tuple 对象时,可以使用 tuple 的默认构造函数,它会对每个成员进行值初始化。tuple 的这个构造函数是 explicit 的,因此必须使用直接初始化语法。

    1
    2
    tuple<size_t, size_t, size_t> threeD = {1, 2, 3}; // 错误
    tuple<size_t, size_t, size_t> threeD{1, 2, 3}; // 正确
  • 标准库定义了 make_tuple 函数,还可以用它来生成 tuple 对象。make_tuple 函数使用初始值的类型来判断 tuple 的类型。

  • 因为一个 tuple 类型的成员数目是没有限制的。因此,tuple 的成员都是未命名的。要访问一个 tuple 的成员,就要使用一个名为 get 的标准函数模板。

  • 为了使用 get ,我们必须指定一个显式模板实参,它指出我们想要访问第几个成员。我们传递给 get 一个 tuple 对象,它返回指定成员的引用。

    1
    2
    3
    4
    auto book = get<0>(item); // 返回 item 的第一个成员
    auto cnt = get<1>(item); // 返回 item 的第二个成员
    auto price = get<2>(item) / cnt; // 返回 item 的最后一个成员
    get<2>(item) *= 0.8; // 打折 20%
  • 如果不知道一个 tuple 准确的类型细节信息,可以使用两个辅助类模板来查询 tuple 成员的数量和类型。

    1
    2
    3
    4
    5
    typedef decltype(item) trans; // trans 是 item 的类型
    // 返回 trans 类型对象中成员的数量
    size_t sz = tuple_size<trans>::value; // 返回 3
    // cnt 的类型与 item 中第二个成员相同
    tuple_element<1, trans>::type cnt = get<1>(item); // cnt 是一个 int
  • tuple_size 有一个名为 valuepublic static 数据成员,它表示给定 tuple 中成员的数量。tuple_element 模板除了一个 tuple 类型外,还接受一个索引值。它有一个名为 typepublic 类型成员,表示给定 tuple 类型中指定成员的类型。

  • 只有两个 tuple 具有相同数量的成员时,我们才可以比较它们。

正则表达式

  • 正则表达式 是一种描述字符序列的方法。C++ 正则表达式(RE 库) 定义在头文件 regex 中。

  • 函数 regex_matchregex_search 确定一个给定字符序列与一个给定 regex 是否匹配。如果整个输入序列与表达式匹配,则 regex_match 函数返回 true;如果输入序列中一个子串与表达式匹配,则 regex_search 函数返回 ture

  • 函数 regex_search 在输入序列中只要找到一个匹配子串就会停止查找。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 查找不在字符 c 之后的字符串 ei
    string pattern("[^c]ei");
    // 我们需要包含 parrern 的整个单词
    pattern = "[[:alpha:]]*" + pattern + "[["alpha:]]";
    regex r(pattern); // 构造一个用于查找模式的 regex
    smatch results; // 定义一个对象保存搜索结果
    // 定义一个 string 保存于模式匹配和不匹配的文本
    string test_str = "receipt friend theif receive";
    // 用 r 在 test_str 中查找于 pattern 匹配的子串
    if (regex_search(test_str, results, r)) // 如果有匹配子串
    cout << results.str() << endl; // 打印匹配的单词
  • 正则表达式 [^c] 表面我们希望匹配任意不是 c 的字符,而 [^c]ei 指出我们想要匹配这种字符后接 ei 的字符串。

  • 默认情况下,regex 使用的正则表达式语言是 ECMAScript。模式 [[:alpha:]] 匹配任意字母,符号 +* 分别表示我们希望 “一个或多个” 或 “零个或多个” 匹配。因此 [[:alpha:]]* 将匹配零个或多个字母。

  • 如果编写的正则表达式存在错误,则在运行时标准库会抛出一个类型为 regex_error 的异常。regex_error 有一个 what 操作来描述发生了什么错误。regex_error 还有一个名为 code 的成员,用来返回某个错误类型对应的数值编码。

随机数

定义在头文件 random 中的随机数库通过一组协作的类来解决这些问题:随机数引擎类随机数分部类

随机数引擎和分布

随机数引擎是函数对象类,它们定义了一个调用运算符,该运算符不接受参数并返回一个随机 unsigned 整数。

1
2
3
4
default_random_engine e; // 生成随机无符号数
for (size_t i = 0; i < 10; ++i)
// e() “调用” 对象来生成下一个随机数
cout << e() << " ";
随机数引擎操作
Engine e 默认构造函数;使用该引擎类型默认的种子
Engine e(s) 使用整型值 s 作为种子
e.seed(s) 使用种子 s 重置引擎的状态
e.min() 此引擎可生成的最小值和最大值
e.max()
Engine::result_type 此引擎生成的 unsigned 整型类型
e.discard(u) 将引擎推进 u 步,u 的类型为 unsigned long long
分布类型和引擎

为了得到一个指定范围内的数,使用一个分布类型的对象。

1
2
3
4
5
6
7
// 生成 0 到 9 之间(包含)均匀分布的随机数
uniform_int_distribution<unsigned> u(0, 9);
default_random_engine e; // 生成无符号随机整数
for (size_t i = 0; i < 10; ++i)
// 将 u 作为随机数源
// 每个调用返回在指定范围内并服从均匀分布的值
cout << u(e) << " ";

我们将 u 定义为 uniform_int_distribution<unsigned> 。此类型生成均匀分布的 unsigned 值。

注意,我们传递给分布对象的是引擎对象本身,即 u(e) 。如果我们将调用写成 u(e()) ,含义就变为将 e 生成的下一个值传递给 u ,会导致一个变异错误。我们传递的是引擎本身,而不是它生成的下一个值,原因是某些分布可能需要调用引擎多次才能得到一个值。

引擎生成一个数值序列
1
2
3
4
5
6
7
8
9
10
11
// 几乎肯定是生成随机整数 vector 的错误方法
// 每次调用这个函数都会生成相同的 100 个数!
vector<unsigned> bad_randVec()
{
default_random_engine e;
uniform_int_distribution<unsigned> u(0, 9);
vector<unsigned> ret;
for (size_t i = 0; i < 100; ++i)
ret.push_back(u(e));
return ret;
}

正确的方法是将引擎和关联的分布对象定义为 static 的。

1
2
3
4
5
6
7
8
9
10
11
12
// 返回一个 vector,包含 100 个均匀分布的随机数
vector<unsigned> good_randVec()
{
// 由于我们希望引擎和分布对象保持状态,因此应该讲它们
// 定义为 static 的,从而每次调用都生成新的数
static default_random_engine e;
static uninform_int_distribution<unsigned> u(0, 9);
vector<unsigned> ret;
for (size_t i = 0; i < 100; ++i)
ret.push_back(u(e));
return ret;
}
生成随机实数

可以定义一个 uniform_real_distribution 类型的对象,并让标准库来处理从随机整数到随机浮点数的映射。

1
2
3
4
5
default_random_engine e; // 生成无符号随机整数
// 0 到 1(包含) 的均匀分布
uniform_real_distribution<double> u(0, 1);
for (size_t i = 0; i < 10; ++i)
cout << u(e) << " "

IO 库再探

1
2
3
4
bool bool_val = get_status();
cout << boolalpha; // 设置 cout 的内部状态
<< bool_val
<< noboolalpha; // 将内部状态恢复为默认格式

1
2
3
4
5
6
cout << showbase; // 当打印整型值时显式进制
cout << "default: " << 20 << " " << 1024 << endl;
cout << "in octal: " << oct << 20 << " " << 1024 << endl;
cout << "in hex: " << hex << 20 << " " << 1024 << endl;
cout << "in decimal: " << dec << 20 << " " << 1024 << endl;
cout << noshowbase; // 恢复流状态
1
2
3
4
default: 20 1024
in octal: 024 02000
in hex: 0x14 0x400
in decimal: 20 1024

1
2
3
4
5
6
7
8
9
10
11
// cout.precision 返回当前精度值
cout << "Precision: " << cout.precision()
<< ", Value: " << sqrt(2.0) << endl;
// cout.precision(12) 将打印精度设置为 12 位数字
cout.precision(12);
cout << "Presion: " << cout.preision()
<< ", Value: " << sqrt(2.0) << endl;
// 另一种设置精度的方法是使用 setprecision 操作符
cout << setprecision(3);
cout << "Precision: " << cout.precision()
<< ", Value: " << sqrt(2.0) << endl;
1
2
3
Precision: 6, Value: 1.41421
Precision: 12, Value: 1.41421356237
Precision: 3, Value: 1.41

1
2
3
cout << 10.0 << endl; // 打印 10
cout << showpoint << 10.0 // 打印 10.0000
<< noshowpoint << endl; // 恢复小数点的默认格式

1
2
3
4
cin >> noskipws; // 设置 cin 读取空白符
while (cin >> ch)
cout << ch;
cin >> skipws; // 将 cin 恢复到默认状态,从而丢弃空白符

特殊工具与技术

类成员指针

  • 成员指针 是指可以指向类的非静态成员的指针。
  • 成员指针的类型囊括了 类的类型 以及 成员的类型 。当初始化一个这样的指针时,我们令其指向类的某个成员,但是不指定该成员所属的对象;直到使用成员指针时,才提供成员所属的对象。
数据成员指针
  • 和其他指针一样,在声明成员指针时我们也使用 * 来表示当前声明的名字是一个指针。

  • 成员指针必须包含成员所属的类。因此,我们必须在 * 之前添加 classname:: 以表示当前定义的指针可以指向 classname 的成员。

    1
    2
    // pdata 可以指向一个常量(非常量)Screen 对象的 string 成员
    const string Screen::*pdata;
  • 当初始化一个成员指针(或者向它赋值)时,需指定它所指的成员。

    1
    pdata = &Screen::contents;
  • 当初始化一个成员指针或为成员指针赋值时,该指针并没有指向任何数据。成员指针指定了成员而非该成员所属的对象,只有当解引用成员指针时我们才提供对象的信息。

  • 有两种成员指针访问运算符:.*->* ,这两个运算符使得我们可以解引用指针并获得该对象的成员。

    1
    2
    3
    4
    5
    Screen myScreen, *pScreen = &myScreen;
    // .* 解引用 pdata 以获得 myScreen 对象的 contents 成员
    auto s = myScreen.*pdata;
    // ->* 解引用 pdata 以获得 pScreen 所指对象的 contents 成员
    s = pScreen->*pdata;
成员函数指针

使用 classname::* 的形式声明一个指向成员函数的指针。

声明一个指针,令其指向含有两个形参的 get

1
2
char (Screen::*pmf2)(Screen::pos, Screen::pos) const;
pmf2 = &Screen::get;

处于优先级的考虑,上述声明中的 Screen::* 两端的括号必不可少。如果没有这对括号的话,编译器将认为该声明是一个(无效的)函数声明。


使用 .* 或者 ->* 运算符作用于指向成员函数的指针,以调用类的成员函数。

1
2
3
4
5
Screen myScreen, *pScreen = &myScreen;
// 通过 pScreen 所指的对象调用 pmf 所指的函数
char c1 = (pScreen->*pmf)();
// 通过 myScreen 对象将实参 0, 0 传给含有两个形参的 get 参数
char c2 = (myScreen.*pmf2)(0, 0);