C++复习笔记

[toc]

记录一些容易忘记的C++知识

C++知识体系较为庞杂,学得不够系统,对知识点进行记录和定期复盘是很有必要的。现在想重新过一遍《C++ Primer Plus》,把一些被遗忘了的和还未学习的知识记录下来,方便日后查阅。

正篇开始

1. 头文件

C语言的头文件使用扩展名.h,C++则没有扩展名,但仍然可以使用C语言的.h文件。有些C头文件被转换为C++头文件,并被重新命名,去掉了扩展名.h,并在文件名称前面加上前缀c,例如,C++版本的math.hcmath。没有.h扩展名的头文件可以包含名称空间。

2. 函数

  1. C/C++不允许将函数定义嵌套在另一个函数定义中;

  2. 在C++中,不指定参数列表时应使用省略号void func(...){ },通常,仅当与接受可变参数的C函数交互时才需要这样做;

  3. C++函数不能直接返回数组,但可以将数组作为结构或对象的组成部分来返回;

  4. 函数返回对象时,有可能使得下面的代码能够通过编译:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Vector v1, v2;
    Vector v3;

    // 重载加法运算符
    Vector Vector::operator+(const Vector & rv)
    {
    return Vector(x + rv.x, y + rv.y);
    }

    v3 = v1 + v2; // 正常使用逻辑

    v1 + v2 = v3; // 能够通过编译,先执行operator+,再执行operator=

    为防止在使用赋值运算符的时候出错,可以把返回值设置为const类型。

  5. 传递数组:

    1
    2
    3
    4
    5
    6
    int sum(int [], int);  // 函数原型
    int sum(int *, int); // 另一种形式
    int sum(int array[], int n){return 0;} // 函数定义,sizeof array将等于指针类型长度

    /*为防止函数无意中修改数组的内容,可在声明形参时使用关键字const*/
    int sum(const int *, int); // 这表明该形参指针指向的是常量数据,不允许通过该指针进行修改
  6. 函数不能返回字符串,但是可以在函数体中申请内存空间,返回该内存空间的地址,以此来返回一个字符串;

  7. 函数的地址:

    函数的地址是存储其机器语言代码的内存的开始地址。==函数名==就是函数的地址。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 声明函数指针
    double (*pf)(int); // 可以首先编写函数的原型,然后用(*pf)替换函数名

    // 通过函数指针调用函数
    double y = (*pf)(x);
    double y = pf(x); // C++中也允许像使用函数名那样使用pf

    /*声明函数指针数组,运算符[]的优先级高于*,所以pf[3]表明其是一个包含三个元素的数组,*pf[3]表明其是一个包含三个指针的数组,其他部分的内容则说明这些指针是函数指针。此处不能使用auto关键字,因为自动类型推断只能用于单值初始化,不能用于初始化列表。但是声明数组pf后,可以使用auto来声明同类型的数组
    */
    const double * (*pf[3])(const double *, int) = {f1, f2, f3};
    auto pa = pf;
    pf[0](cpd, 3); // 通过函数指针数组和索引调用函数

    const double * (*(*pd)[3])(const double *, int) = &pf; // 指向函数指针数组的指针,我的天呐

    /*使用typedef进行简化*/
    typedef const double * (*p_fun)(const double *, int);

    p_fun p1 = f1; // 声明函数指针变量,p_fun为新类型的别名
    p_fun p2[3] = {f1, f2, f3}; // 创建函数指针数组,很方便
    p_fun (*pd)[3] = &f1; // 创建指向函数指针数组的指针

以下是C++特有的内容:

  1. 内联函数:

    C++编译器将使用相应的函数代码替换内联函数调用。对于内联代码,程序无需跳到另一个位置处执行代码,再跳回来。因此,内联函数的运行速度比常规函数稍快,但代价是需要占用更多内存。如果程序在10个不同的地方调用同一个内联函数,则该程序将包含该函数代码的10个副本。

    要使用这项特性,必须采取下述措施之一:

    • 在函数声明前加上关键字inline;
    • 在函数定义前加上关键字inline。

    通常的做法是省略原型,将整个定义(即函数头和所有函数代码)放在本应提供原型的地方。

    ==注意==:

    • 如果函数过大,编译器可能不允许将该其作为内联函数。
    • 内联函数不能递归。

    内联函数类似C中的宏,但是内联函数有着按值传参等函数特性,而宏只是简单的替换。例如:

    1
    2
    3
    4
    square(c++);  // 对于内联函数,c只增加一次

    #define SQUARE(x) ((x) * (x))
    SQUARE(c++); // 对于宏,变量c将自增两次,即((c++) * (C++))
  2. 引用变量:

    引用是已定义的变量的别名。

    1
    typeName & ref_name = variable;  // 必须在声明引用时将其进行初始化

    引用与常指针类似,声明之后不能更改为其他变量的引用。如果修改引用,实际上是对原变量进行赋值;

    如果函数中不修改变量的值,同时又想使用引用,则应使用常量引用(即不能通过引用修改原变量的值);

    如果函数的形参类型为引用(非const引用)类型,则传递的参数只能是对应类型的变量,而不能是表达式;

    如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。

    C++ 11新增了右值引用,可指向右值:

    1
    double && ref = 3.14;

    使用函数返回值给变量赋值:int ret = func(var1, var2);,如果函数返回一个结构(或其他类型),则整个结构会被复制到一个临时位置,再将这个拷贝赋值给变量。但在返回值为引用时,将直接把结构复制到变量中,其效率更高。

    假如要使用引用返回值,但又不允许对引用返回值进行赋值,只需将返回值类型声明为const引用:

    1
    2
    const int & sum(int &, int &);
    sum(a, b) = c; // 不允许

    ==返回引用的函数实际上是被引用变量的别名==。

  3. 默认参数:

    只有原型指定了默认值。函数定义与没有默认参数时完全相同。

    1
    2
    void func(int a, int b = 1, int c = 2);  // 带默认参数列表的函数
    void func(int, int = 1, int = 2); // 这种形式实测也可以
  4. 重载函数:

    函数重载的关键是函数的参数列表——也称为函数特征标(==仅返回值不同不是函数重载==)。

    1
    2
    double cube(double x);
    double cube(double &x); // 编译器无法确定究竟选择哪个原型。如果传参是右值cube(3.0),实际上编译也能通过,但是传参是变量就不行了

    当参数不与任何原型匹配,C++将尝试使用标准类型转换进行匹配。如果重载函数中存在唯一的转换方式,则调用该重载函数,否则编译器报错。如:

    1
    2
    3
    4
    double cube(int);
    double cube(long); // 两个重载函数头

    cube(3.14); // 参数3.14为double型,不与任何原型匹配,将进行标准类型转换。在这里可以转int和long,有两种选择,因此编译器将报错

    当同时有带const和非const两个原型时,传递const参数时调用const原型,传递常规参数时,优先调用非const原型(==仅针对指针==):

    1
    2
    int strlen(const char *);
    int strlen(char *); // 同时有带const和非const两个原型

    对于重载引用参数,C++将调用最匹配的版本:

    1
    2
    3
    void show(double &);  // 匹配可修改的左值
    void show(const double &); // 匹配const的左值或右值
    void show(double &&); // 匹配右值

    对于同时出现重载函数和默认参数,如果出现二义性,则编译不通过:

    1
    2
    3
    4
    5
    void show(double, int = 2);  // 带默认参数
    void show(double); // 不带默认参数

    show(3.14, 4); // OK
    show(3.14); // 对重载函数调用不明确,编译器报错
  5. C++将区分常量和非常量函数的特征标;

  6. 函数模板:

    通过将类型作为参数传递给模板,可使编译器生成该类型的函数。模板并不创建任何函数,只是告诉编译器如何定义函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    template <typename AnyType>  // 建立函数模板,typename可以用class代替,AnyType通常使用T。注意,每个模板函数的声明和定义的位置都需要先建立函数模板
    void swap(AnyType &a, AnyType &b)
    {
    AnyType temp;
    temp = a;
    a = b;
    b = temp;
    }

    // 多种类型的函数模板
    template <typename T1, typename T2>
    void swap(T1 &, T2 &);

    // 函数模板与重载结合
    template <typename T>
    void Swap(T a[], T b[], int n)
    {
    T temp;
    for (int i = 0; i < n; i++)
    {
    temp = a[i];
    a[i] = b[i];
    b[i] = temp;
    }
    }

    /* 显式具体化可用于为特定数据类型创建特定的函数定义。
    Q:为什么不直接定义函数呢,非模板版本优先级又高,想不明白。A:其实也可以定义普通的非模板函数
    如果有多个原型,则编译器在选择原型时,非模板版本优先于显式具体化和模板版本,而显式具体化优先于使用模板生成的版本。
    */

    /* 显式具体化语法。注意,该原型必须位于其他模板函数之后。Swap<complex>中的<complex>是可选的,
    因为函数的参数表明,这是complex的一个具体化
    */
    template <> void Swap<complex>(complex &, complex &);
    template <> void Swap(complex &, complex &);
    // 使用显式具体化时,一定要仔细检查函数的特征标和返回值,否则编译出错很难Debug

    // 显式实例化,本质上是手动创建了一个函数定义,而函数模板使编译器可以生成函数定义的方式属于隐式实例化
    template void Swap<int>(int &, int &); // 直接命令编译器创建int类型函数定义实例
    // 也可以在程序中使用函数来创建显式实例化
    template <typename T>
    T add(T a, T b)
    {
    return a + b;
    }
    int m = 6;
    double x = 3.14;
    // 这里的模板与函数调用add(x, m)不匹配,因为该模板要求函数两个参数的类型相同。但通过使用add<double>(x, m),可强制为double类型实例化,并将参数m强制转换为double类型,以便于函数的第二个参数与add<double>(double, double)匹配。
    cout << add<double>(x, m) << endl; // 显式实例化
    cout << add(x, x) << endl; // 隐式实例化

    编译器选择使用哪个函数版本的步骤(内容太多太难记住,需要再查书吧):

    1. 列出所有同名函数;

    2. 列出参数个数匹配的函数;

    3. 确定是否有最佳的可行函数

      通常,从最佳到最差的顺序如下所述:

      1. 完全匹配,但常规函数优于模板;
      2. 提升转换(例如,char和short自动转int,float自动转换为double);
      3. 标准转换(例如,int转换为char,long转换为double)。
    1
    2
    3
    4
    5
    6
    7
    template <typename T>
    T lesser(T, T); // #1
    int lesser(int, int); // #2

    lesser(3, 4); // 调用#2
    lesser(3.14, 2.0); // 调用#1
    lesser<>(3, 4); // <>指出编译器应选择模板函数而不是非模板函数

3. 整型

C++的基本整型有5种,每种类型都有符号版本和无符号版本,因此总共有10种类型。

整型 最小长度 备注
char
short 16位
int 至少与short一样长
long 至少32位,且至少与int一样长
long long 至少64位,且至少与long一样长 C++ 11新增

4. 浮点型

C++有3种浮点类型:float,double和long double。

5. 运算符

sizeof:对类型名(如int)使用sizeof运算符时,应将名称放在括号中;但对变量名使用该运算符,括号是可选的。如果将sizeof运算符用于数组名,得到的将是整个数组中的字节数。

6. 变量初始化

  1. C++特有而C语言没有的初始化语法:int power(100);

  2. C++ 11的大括号初始化器:

    1
    2
    3
    4
    int emus{7};
    int rheas = {12}; // 可以使用等号,也可以不使用

    int rocs = {}; // 大括号内可以不包含任何东西,变量将被初始化为零

7. 输入输出

  1. dechexoct控制符,用于指示以不同进制格式显示整数:cout << hex;。默认为十进制,在修改格式之前,原来的格式将一直有效;

  2. cout.setf(ios::boolalpha);设置了一个标记,该标记命令cout显示true和false,而不是1和0;

  3. cin.eof()cin.fail()用于检测EOF。当用于测试时,需要在读取之后才调用这两个函数(可以二选一);

  4. cin.get(char)的返回值是一个cin对象。cin对象可以转换为bool值,如果最后一次读取成功了,则转换得到的bool值为true,否则为false:

    1
    2
    3
    4
    while (cin);  // 这比!cin.eof()或!cin.fail()更加通用,因为它可以检测到其他失败原因,如磁盘故障
    while (cin.get(ch))
    {
    }

8. 常量

  1. 除非有理由存储为其他类型(如使用了后缀,或值太大,不能存储为int),否则C++将整型常量存储为int类型:
后缀 类型
l或L long
u或U unsigned int
ul或lu unsigned long
ll或LL long long
ull、Ull、uLL、ULL unsigned long long
  1. 在C++中,对十进制整数采用的规则,与十六进制和八进制稍微有些不同。对于不带后缀的十进制整数,将使用intlonglong long中的最小类型来表示;对于不带后缀的十六进制或八进制整数,将使用intunsigned intlongunsigned longlong longunsigned long long中的最小类型来表示;
  2. 在默认情况下,浮点常量都属于double类型,如果希望常量为float类型,使用f或F后缀,对于long double类型,可使用l或L后缀。

9. 字符与字符串

  1. C++ 11新增char16_tchar32_t类型,二者均为无符号类型。C++ 11使用前缀u表示char16_t字符常量和字符串常量,如u'C'u"be good";对于char32_t类型则使用前缀U

  2. char数组只有当最后一个元素为\0时才是字符串;

  3. char bird[11] = "Mr. Cheeps";
    char fish[] = "Bubbles";  // let the compiler count
    
    1
    2
    3
    4
    5
    6
    7

    4. 字符串拼接:任何两个由空白(空格、制表符和换行符)分隔的字符串常量都将自动拼接成一个字符串;

    5. ```c++
    #include <cstring> // for the strlen(), strcpy(), strcat() function
    strcpy(charr1, charr2); // copy charr2 to charr1
    strcat(charr1, charr2); // append contents of charr2 to charr1
  4. cin使用空白(空格、制表符和换行符)来确定字符串结束位置,cin在获取字符数组输入时只读取一个单词;

  5. ```c++
    char c = ‘c’;
    cout << ++c; // 输出为字符
    cout << c + 1; // 输出为ascii编码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    8. 面向行的输入:

    ```c++
    cin.getline(arrayName, arraySize); // 读取并丢弃换行符
    cin.get(arrayName, arraySize); // 不读取换行符
    cin.get(); // 不带参数,可以读取任意一个字符,包括换行符

    char ch;
    cin.get(ch); // Q:ch是如何赋值的?A:使用了引用
  6. string类:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 使用string类,必须包含头文件string,string类位于名称空间std中
    string str = "Hello World";
    string str = {"Hello World"}; // 列表初始化
    string str {"Hello world"}; // 省略等号的列表初始化
    string str ("Hello world"); // 圆括号初始化
    str_copy = str; // 允许赋值
    cin >> str; // 可以使用cin来将键盘输入存储到string对象中,会自动调整str的长度
    cout << str; // 可以使用cout来显示string对象
    cout << str[2]; // 支持索引操作
    str1 == str2; // 支持==、!=、>、<、>=、<=等比较运算

    str3 = str1 + str2; // 字符串合并
    str2 += str1; // 使用+=拼接
    str2 += "Hello World!"; // 拼接C-风格字符串

    int len = str.size(); // 获取str的长度

    getline(cin, str); // 区别于cin.getline(),一个是类成员函数,一个不是
  7. 其他形式的字符串字面值:

    1
    2
    3
    u8"Hello World";  // UTF-8
    R"("Hello World")"; // 原始字符串,将"(和)"用作定界符,字符串中允许出现"",如果字符串中需要出现)",可以自定义定界符,"+*()+*",即"(与)"之间可以插入任意基本字符(空格、左右括号、斜杆和控制字符除外)共同组成定界符,但是左右插入的字符必须相同,如:
    R"+*("(Hello World)")+*";

10. 数组

C++没有提供二维数组类型,但用户可以创建每个元素本身都是数组的数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 创建数组的通用格式
typeName arrayName[arraySize]; // arraySize必须是整型常数或者是const值,也可以是常量表达式
// 初始化
typeName arrayName[arraySize] = {v1, v2, ..., vn};

// 初始化规则
int cards[4] = {3, 6, 8, 10}; // Ok
int cards[4] {3, 6, 8, 10}; // Ok
int hand[4]; // Ok
hand[4] = {5, 6, 7, 9}; // not allowed
hand = cards; // 不允许数组赋值

float hotelTips[5] = {5.0, 2.5}; // 允许,编译器将其他元素置为0
long totals[500] = {0}; // 全部初始化为0
short things[] = {1, 5, 3, 8}; // 编译器计算元素个数

double earnings[4] {1.2e4, 1.6e4, 1.1e4, 1.7e4}; // okey with C++ 11
unsigned int counts[10] = {}; // C++ 11, 所有元素置为0
long plifs[] = {25, 92, 3.0}; // 编译不通过,因为C++ 11列表初始化不允许缩窄

11. 结构体

C语言可以通过函数指针在结构体中包含成员函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
struct newTypeName
{
char e1[20];
int e2;
float e3;
};

struct newTypeName x = {"a", 100, 3.14}; // C语言中不能省略关键字struct
newTypeName x; // C++中可省略struct

// C++ 11列表初始化,注意不能缩窄
struct newTypeName x {"a", 100, 3.14}; // C++ 11列表初始化,可省略等号
struct newTypeName x {}; // 括号内为空,则所有成员将被设置为0

// 成员赋值时,即使成员是数组,也能直接赋值
newTypeName x = {"abc", 100, 3.14};
newTypeName y = x; // 成员赋值

// 声明结构时定义变量
struct newTypeName
{
char e1[20];
int e2;
float e3;
} x, y;

// 声明结构时定义变量并初始化
struct newTypeName
{
char e1[20];
int e2;
float e3;
} x = {"abc", 100, 3.14};

// 创建一次性结构变量
struct
{
char e1[20];
int e2;
float e3;
} x;

// 结构数组及其初始化
newTypeName x[2] =
{
{"a", 100, 3.14},
{"b", 200, 2.71}
};

// 结构体位字段,字段类型应为整型或枚举
struct reg
{
unsigned int SN : 4; // 4 bits for SN value
unsigned int : 4; // 4 bits unused
bool goodIn : 1; // 1 bit
bool finish : 1; // 1 bit
};

12. 共用体

可以利用共用体的特性操作内存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 在结构中声明共用体
struct structType
{
union id
{
long id_num;
char id_char[20];
} id_val;
};
// 访问
structType var;
var.id_val.id_num = 0;
cout << var.id_val.id_char;

// 匿名共用体,共用体成员被视为结构体的两个成员
struct structType
{
union
{
long id_num;
char id_char[20];
};
};

13. 枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
enum spectrum {red, orange, yellow, green, blue, violet, indigo, ultraviolet};

// 只使用枚举常量,不创建枚举变量,可省略枚举类型名称
enum {red, orange, yellow, green, blue, violet, indigo, ultraviolet};

// 枚举在表达式中可以提升为int类型,但int不允许赋值给枚举变量(取决于实现)
int color = red; // 合法操作
spectrum color = spectrum (2); // 合法,使用强制类型转换

// 显式设置枚举量
enum bits {one = 1, two = 2, four = 4, eight = 8};
enum bigstep {first, second = 100, third}; // first = 0, third = 101
enum {zero, null = 0, one, full = 1}; // 可以创建多个值相同的枚举量

14. 指针

==问题==:

1
2
3
4
Q:指针变量所占空间为4个字节,那么超过4G的地址空间如何表示?
A:指针变量不一定是4个字节,得看数据模型。
Q:delete运算符是如何确定释放多少内存空间的?
A:编译器通过分配额外的空间记录已分配的内存空间的长度。

空指针:有些程序员使用(void *) 0来标识空指针(空指针本身的内部表示可能不是零),还有程序员使用NULL,这是一个表示空指针的C语言宏。C++ 11新增了关键字nullptr,用于表示空指针,用法为:pt = nullptr;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int* p1, p2;  // 注意p1为int*类型,p2为int类型

typeName * pointer_name = new typeName; // 动态分配内存
// 使用new运算符,如果内存分配失败,将引发异常,或者返回空指针,取决于实现
delete pointer_name; // 使用delete运算符释放内存

typeName * pointer_name = new typeName [num_elements]; // 使用new创建动态数组
delete [] pointer_name; // 释放一个动态数组的内存
pointer_name[0]; // 可以当做普通数组来访问每个元素
pointer_name += 1; // 指针变量允许此操作,数组名则不允许

// C++编译器对索引操作的转换
arrayname[i] -> *(arrayname + i);
pointername[i] -> *(pointername + i);

short tell[10]; // 数组名tell表示第一个元素的地址,tell == &tell[0],tell + 1会将tell的值增加sizeof(short)
short (*pas)[10] = &tell; // 数组的地址,类型是short (*)[10],pas + 1会将pas的值增加sizeof(short) * 10
short *pas[10]; // 定义十个元素为short *类型的数组

// 数组内指针减法,得到的是两个被指向元素的间隔
int array[20] = {0};
int * start = array;
int * stop = &array[8];
int diff = stop - start; // diff == 8

const int * pt; // 指向常量的指针,指向的值不一定是常量,但对pt而言,这个值是常量。C++禁止将const变量的地址赋值给非const的指针。pt可以指向其他的变量,但是同样不能修改指向的变量
int * const pt = &x; // 常指针,与定义常量不同,定义常指针时const放在类型后面


/*仅当只有一层间接关系(如指针指向基本数据类型)时,才可以将非const地址或指针赋给const指针*/
const int **p2;
int *p1;
const int n = 13;
p2 = &p1; // 第一步:not allowed
*p2 = &n;
*p1 = 10; // 试图用常规指针修改const变量,由于第一步不通过,所以也不会到这一步

/*二维数组传参*/
// 注意:列数不能省略,且传参时只能传递列数相同的二维数组
// 形参没有使用const,因为这里的形参是二级指针,而const只适用于一级指针
int sum(int (*)[4], int); // 函数原型
int sum(int [][4], int); // 这种格式也可以

int sum(int array[][4], int n)
{
array[0][0]; // 直接当做二维数组进行访问
*(*(array + 0) + 0); // 等价的指针操作,比较复杂
}

15. 关键字

  1. const限定符:

    1
    2
    // 创建常量的通用格式
    const type name = value

    在C++(不是C语言)中,const全局变量的链接性为内部,就像是使用了static一样。这也是常量定义可以放在头文件,由多个源文件包含但并不产生冲突的原因。如有必要,可以使用extern关键字来覆盖默认的内部链接性:

    1
    2
    3
    extern const int status = 50;

    extern const int status; // 其他文件引用该变量
  1. auto关键字:

    C++ 11新增了auto关键字,让编译器能够根据初始值的类型推断变量的类型。在初始化声明中,如果使用关键字auto,而不指定变量的类型,编译器将把变量的类型设置成与初始值相同。(注意,auto本来是一个C语言关键字,用于显式地指出变量为自动存储,所谓自动存储,就是代码块中定义的变量。实际上atuo几乎用不上)。

  2. decltype关键字:

    C++ 11新增了decltype关键字,可以将变量指定为与表达式相同的类型。

    1
    2
    3
    4
    5
    6
    7
    decltype(expression) var;
    decltype(x+y) z;

    // 如果需要多次声明,可结合使用typedef和decltype
    typedef decltype(x + y) xytype;
    xytype xy = x + y;
    xytype ab = a + b;

    为确定类型,编译器必须遍历一个核对表:

    1. 如果expression是一个没有用括号括起的标识符:decltype (x) y,则var的类型与该标识符的类型相同,包括const等限定符;
    2. 如果expression是一个函数调用,则var的类型与函数的返回值类型相同;
    3. 如果expression是一个用括号括起的左值,则var为指向其类型的引用:decltype ((x)) y。括号并不会改变表达式的值和左值性;
    4. 如果前面的条件都不满足,则var的类型与expression的类型相同。

    decltype不能解决函数返回值类型的问题,如:

    1
    2
    3
    4
    5
    6
    7
    template <typename T1, typename T2>
    ?type? func(T1 & a, T2 & b)
    {
    return a + b; // a + b类型不确定
    }

    decltype(a+b) func(T1 &, T2 &); // a+b未定义

    使用C++ 11后置返回类型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    double func(int x, float y);  // 原来的函数定义方式

    auto func(int x, float y) -> double; // ->double 被称为后置返回类型,其中auto是一个占位符,表示后置返回类型提供的类型

    // 注意函数参数列表中的形参名a、b不能省略
    template <typename T1, typename T2>
    auto func(T1 & a, T2 & b) -> decltype(a+b)
    {
    return a + b; // decltype(a+b)类型
    }
  3. register关键字:

    关键字register最初是由C语言引入的,它建议编译器使用CPU寄存器来存储自动变量,旨在提高访问变量的速度。

    在C++ 11之前,这个关键字在C++中的用法始终未变。在C++ 11中,register关键字只是显式地指出变量是自动的,与C语言中的auto功能一致。

  4. volatile关键字:

    该关键字的作用是为了改善编译器的优化能力。将变量声明为volatile,相当于告诉编译器,不要进行这种优化。

  5. mutable关键字:

    即使结构(或类)变量为const,其某个成员也可以被修改:

    1
    2
    3
    4
    5
    struct data
    {
    char name[20];
    mutable int accesses;
    };

16. 类型转换

  1. 初始化和赋值时进行的转换:

    转换 潜在的问题
    将较大的浮点类型转换为较小的浮点类型,如将double转换为float 精度(有效数位)降低,值可能超出目标类型的取值范围,在这种情况下,结果将是不确定的
    将浮点类型转换为整型 小数部分丢失,原来的值可能超出目标类型的取值范围,在这种情况下,结果将是不确定的
    将较大的整型转换为较小的整型,如将long转换为short 原来的值可能超出目标的取值范围,通常只复制右边的字节
  2. 以列表初始化{}方式初始化时进行的转换(C++ 11)。列表初始化不允许缩窄,即变量的类型无法表示赋给它的值:

    1
    2
    3
    int x = 66;
    char c = {x}; // 不允许,x是一个变量,可能表示一个很大的值
    char c = x; // 允许
  3. 表达式中的转换:

    1. 整型提升:C++将表达式中的bool,char,unsigned char,signed char和short值转换为int。如果short比int短,则unsigned short类型将被转换为int;如果两种类型的长度相同,则unsigned short类型将被转换为unsigned int。
    2. 当运算涉及两种类型时,较小的类型将被转换为较大的类型。

    C++ 11校验表:

    1. 如果有一个操作数的类型是long double,则将另一个操作数转换为long double;
    2. 否则,如果有一个操作数的类型是double,则将另一个操作数转换为double;
    3. 否则,如果有一个操作数的类型是float,则将另一个操作数转换为float;
    4. 否则,说明操作数都是整型,因此执行整型提升;
    5. 在这种情况下,如果两个操作数都是有符号或者无符号的,且其中一个操作数的级别比另一个低,则转换为级别高的类型;
    6. 如果一个操作数为有符号的,另一个操作数为无符号的,且无符号操作数的级别比有符号操作数高,则将有符号操作数转换为无符号操作数所属的类型;
    7. 否则,如果有符号类型可表示无符号类型的所有可能取值,则将无符号操作数转换为有符号操作数所属的类型;
    8. 否则,将两个操作数都转换为有符号类型的无符号版本。
    
  4. 强制类型转换

    1
    2
    3
    4
    5
    (long) thorn;  // C语言格式
    long (thorn); // C++语言格式

    (type) value; // C语言格式
    type (value); // C++语言格式

17. 数组的替代品

  1. vectorarray

    1
    2
    3
    4
    5
    6
    // vector
    vector<typeName> vt(n_elem); // vector类位于名称空间std中,且使用vector类需包含头文件`vector`
    // array
    array<typeName, n_elem> arr; // array类位于名称空间std中,且使用array类需包含头文件`array`,n_elem必须是常量。使用栈,但是比数组更方便,安全
    array<float, 4> arr = {1.0, 2.0, 3.0, 4.0}; // array初始化
    array<string, 4> arr; // 元素可以是类对象
  2. valarray

    valarray类是由头文件valarray支持的,这个类用于处理数值,它支持诸如将数组中所有元素的值相加以及在数组中找出最大值和最小值等操作,它是一个模板类,能够处理不同的数据类型。基本用法如下:

    1
    2
    3
    4
    5
    6
    7
    double gpa[5] = {3.1, 3.5, 3.8, 2.9, 3.3};
    valarray<double> v1; // an array of double, size 0
    valarray<int> v2(8); // an array of 8 int elements
    valarray<int> v3(10, 8); // an array of int elements, each set to 10
    valarray<double> v4(gpa, 4); // an array of 4 elements, initialized to the first 4 elements of gpa

    valarray<int> v5 = {1, 2, 3, 4, 5}; // C++ 11

18. 流程控制

  1. C++常用的方式是,在for、if、while等流程控制关键字与括号之间加上一个空格,而省略函数名与括号之间的空格;

  2. 在for-init-statement中声明的变量,在程序离开循环后消失;

  3. 在语句块中定义一个新的变量,则仅当程序执行该语句块中的语句时,该变量才存在。执行完该语句块后,变量将被释放;

  4. for (; ;),当省略for循环中的测试表达式时,测试结果将为true,因此循环将一直运行下去;

  5. 基于范围的for循环:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    double prices[5] = {1.0, 2.0, 3.0, 4.0, 5.0};
    for (double x : prices)
    cout << x << endl;

    // 修改数组的值
    for (double &x : prices)
    cout << x << endl;

    // 结合使用for循环和初始化列表
    for (int x : {1, 2, 3, 4, 5})
    cout << x << endl;

19. 递增和递减运算符

  1. 不要在同一条语句对同一个值递增或递减多次,对于这种语句,C++没有定义正确的行为。这种语句在不同的系统上将生成不同的结果:

    1
    y = (4 + x++) + (6 + x++);

    表达式中4+x++不是一个完整表达式,因此,C++不保证x的值在计算子表达式4+x++后立刻增加1。在这个例子中,整条赋值语句是一个完整表达式,而分号标示了顺序点,因此C++只保证程序执行到下一条语句之前,x的值将被递增两次。C++没有规定是在计算每个子表达式之后将x的值递增,还是在整个表达式计算完毕后才将x的值递增。

  2. 前缀递增、前缀递减和解除引用运算符的优先级相同,以从右到左的方式进行结合。后缀递增和后缀递减的优先级相同,但比前缀运算符的优先级高,这两个运算符以从左到右的方式进行结合。

20. 逗号运算符

  1. 用于将多个表达式合并为一个:++j, --i
  2. int i, j;声明变量时的逗号只是分隔符,不是逗号运算符;
  3. i = 20, j = 20 * i;,逗号表达式确保先计算第一个表达式,然后计算第二个表达式;
  4. C++规定,逗号表达式的值是最后一个部分的值;
  5. 逗号运算符的优先级是所有运算符中最低的一个。因此cats = 17, 240将被解释为(cats = 17), 240

21. 关系运算符

关系运算符的优先级比算术运算符低。

22. 获取时间

ctime头文件提供了CLOCKS_PER_SEC常量,可以得到以时钟为单位的时间;clock()函数可用于获取当前系统时间。

24. 类型别名

  1. 使用预处理器

    1
    2
    3
    4
    5
    #define main mian  // 艹

    // 有时候不适合使用#define
    #define FLOAT_POINTER float *
    FLOAT_POINTER pa, pb; // pa是float *类型,而pb是float类型
  2. 使用typedef关键字

    1
    typedef typeName aliasName;

25. 输入重定向

假设在Windows系统中有一个名为gofish.exe的可执行程序和一个名为fishtale的文本文件,则可以在命令提示符模式下输入下面的命令:

1
gofish.exe < fishtale

这样,程序将从fishtale文件获取输入。

26. 逻辑运算符

  1. 由于||&&的优先级比关系运算符低,因此不需要在这些表达式中使用括号,如5 > 3 || 5 > 10
  2. &&的优先级高于||!的优先级高于所有关系运算符和算术运算符;
  3. C++规定,||&&运算符是一个顺序点。也就是说先修改左侧的值,再对右侧的值进行判定。如i++ < 6 || i == j
  4. C++确保程序从左到右进行计算逻辑表达式,并在知道答案后立刻停止(可以通过编写一个返回值为bool类型的函数进行验证)。

27. 文件输入输出

  1. 文件输出
    1. 包含头文件fstream;
    2. 创建一个ofstream对象;
    3. 将该ofstream对象同一个文件关联起来(open函数);
    4. 像使用cout一样使用该ofstream对象;
    5. 调用close函数关闭文件。
  2. 文件输入
    1. 包含头文件fstream;
    2. 创建一个ifstream对象;
    3. 将该ifstream对象同一个文件关联起来(open函数);
    4. 使用该ifstream对象的is_open函数判断文件是否成功打开;
    5. 像使用cin一样使用该ifstream对象;
    6. 调用close函数关闭文件。

28. 变量类型、作用域与链接性

  1. 自动变量

  2. 静态持续变量

    1. 链接性为外部的静态持续变量(如果要在多个文件中使用外部变量,只需在一个文件中包含该变量的定义,但在使用该变量的其他所有文件中,都必须使用关键字extern声明它,且不进行初始化,否则,声明将变为定义,导致分配存储空间);
    2. 链接性为内部的静态持续变量(代码块外加static关键字,将屏蔽其他文件中的外部变量,作用域解析运算符::用于变量名前面,可以解除局部变量对全局变量的屏蔽作用);
    3. 无链接性的静态持续变量(代码块内加static关键字,程序只进行一次初始化)
  3. 动态内存分配

    1. 使用new运算符的初始化:

      在C++98中,使用括号括起的初始值进行初始化

      1
      int * pt = new int (8);

      初始化结构或者数组,需要使用大括号的列表初始化,要求编译器支持C++ 11

      1
      2
      3
      4
      5
      struct where {double x, double y, double z};
      where * point = new where {1.0, 2.0, 3.0};

      int * ar = new int [4] {1, 2, 3, 4};
      int * pt = new int {6};
    2. 分配函数与释放函数:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      // 分配函数,new和new[]分别调用如下函数:
      void * operator new(std::size_t); // std::size_t是一个typedef
      void * operator new[](std::size_t);

      int * pi = new int; // 被转换为int * pi = new(sizeof(int));
      int * pa = new int[40]; // 被转换为int * pa = new(40 * sizeof(int));

      // 释放函数,delete和delete[]分别调用如下函数:
      void * operator delete(void *);
      void * operator delete[](void *);

      delete pi; // 被转换为delete(pi);

      可为new和delete提供替换函数,根据需要对其进行定制。可定义作用域为类的替换函数,并对其进行定制,以满足该类的内存分配需求,定制之后使用new运算符时,将调用自定义的new函数。

    3. 定位new运算符:

      定位new运算符是new运算符的一个变体。定位new运算符可以在特定位置创建对象。要使用定位new特性,需要包含头文件new。使用定位new运算符时,变量后面可以有方括号,也可以没有。

      1
      2
      3
      4
      5
      char buffer1[50];
      char buffer2[500];

      where * pt_buffer1 = new (buffer1) where; // 把where结构体放置于buffer1中
      int * pt_buffer2 = new (buffer2) int [20]; // 把int数组放置于buffer2中

      将定位new运算符用于类对象时,由于定位new运算符对于缓冲区的使用情况一无所知,所以如果使用了动态内存做缓冲区,当该缓冲区被delete []释放时(或常规缓冲区过期时),缓冲区中的对象并不会调用析构函数。对于定位new运算符创建的类对象,需要显式调用析构函数,且必须以后进先出的方式销毁对象,因为后创建的对象可能依赖于先创建的对象。

      1
      2
      3
      4
      char buffer[512];
      Foo * foo = new (buffer) Foo;
      // do something with foo
      foo->~Foo(); // 显式调用析构函数
  4. 函数的链接性:

    函数也有链接性,默认为外部的,在其中一个源文件中使用另一个源文件中定义的函数,可以在函数原型中使用关键字extern(这是可选的)。也可以使用static关键字将函数的链接性设置为内部的,必须同时在原型和函数定义中使用该关键字。内联函数不受单定义规则约束,因此内联函数的定义可以放在头文件中。

  5. 语言链接性:

    链接程序寻找与C++函数调用匹配的函数时,使用的方法与C语言不同。但如果要在C++程序中使用C库中预编译的函数,可以这么做:

    1
    2
    3
    extern "C" void spiff(int);  // 使用C语言版本
    extern "C++" void spiff(int); // 使用C++版本
    extern void spiff(int); // 使用C++版本

29. 名称空间

  1. 名称空间可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中。名称空间是开放的,即可以把新的名称加入到已有的名称空间中。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    namespace Jack {
    double x;
    // 名称空间嵌套
    namespace Jill {
    double z;
    }
    }

    // 往Jack中再添加变量y
    namespace Jack {
    double y;
    }

    using namespace Jack; // using编译指令,与下面两行等价
    using namespace Jack;
    using namespace Jill;

    // 可以给名称空间创建别名
    namespace my_very_favorite_things {...};
    namespace mvft = my_very_favorite_things;
    using mvft::name; // using声明

    // 未命名的名称空间
    namespace
    {
    int ice;
    void show(void);
    }
    // 由于这种名称空间没有名称,因此不能显式地使用using编译指令或using声明,因此可以替代链接性为内部的持续变量。创建完未命名的名称空间后,直接会有使用完using编译指令的效果。
    static int ice; // 与上述名称空间中的名称具有同样的链接性
  2. using声明和using编译指令:

    using声明使特定的标识符可用,using编译指令使整个名称空间可用。

    假设名称空间和声明区域定义了相同的名称。如果试图使用using声明将名称空间的名称导入该声明区域,则这两个名称会发生冲突,从而出错。如果使用using编译指令将该名称空间的名称导入该声明区域,则局部版本将隐藏名称空间版本。

    1
    2
    3
    4
    5
    6
    7
    8
    int cout = 10;  // global cout
    int main()
    {
    using namespace std; // cout in std namespace
    int cout = 20; // local cout
    std::cout; // cout in namespace
    ::cout; // global cout
    }

30. 对象和类

  1. 结构体与类的区别:结构体的默认访问类型是public,而类为private;

  2. 基类的引用可以指向派生类对象,而无需进行强制类型转换,且这种向上强制转换是可传递的。在函数按值传递中,如果形参为基类类型,实参为派生类类型,则只会把派生类中属于基类的部分传递给函数;

  3. 在默认情况下,将一个对象赋给同类型的另一个对象时,C++将源对象的每个数据成员的内容复制到目标对象中相应的数据成员中。(Q:如果是动态分配的内存,这种复制是否导致内存异常?A:这只是在默认的情况下);

  4. 声明类只是描述了对象的形式,并没有创建对象。因此创建对象前,将没有用于存储值的空间(但是C++ 11提供了成员初始化);

  5. 定义成员函数时,==通常==在独立的实现文件中编写,使用作用域解析运算符(::)来标识函数所属的类;

  6. 内联方法:定义位于类声明中的函数都将自动成为内联函数。类声明常将短小的成员函数作为内联函数。在类声明之外定义成员函数,只需在类实现部分中定义函数时使用inline限定符即可使其成为内联函数。内联函数要求在每个使用它们的文件中都对其进行定义,为确保内联定义对多文件程序中的所有文件都可用,最简便的方法是将内联定义放在定义类的头文件中(有些开发系统包含智能链接程序,允许将内联定义放在一个独立的实现文件中);

  7. 构造函数的名称与类名相同。构造函数的原型和函数头没有返回值,但没有被声明为void类型。实际上,构造函数没有声明类型:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    Stock(const string &, long, double);

    // 显式调用构造函数
    Stock stock = Stock("World Cabbage", 250, 1.25);
    Stock * pstock = new Stock("Electroshock Games", 18, 19.0);
    // 隐式调用构造函数
    Stock garment("Furry Mason", 50, 2.5);

    // 第一条语句是初始化,它创建有指定值的对象,可能会创建临时对象(也可能不会,取决于编译器);第二条语句是赋值,会导致在赋值前创建一个临时变量。如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式。通常这种方式的效率更高
    Stock stock = Stock("Boffo Objects", 2, 2.0);
    stock = Stock("Nifty Foods", 10, 50.0);

    // C++ 11的列表初始化语法,参数列表将与某个构造函数的参数列表匹配
    Complex c1 = {2.03.0};
    complex c2{}; // 使用默认构造函数
    Complex pt = new Complex {2.0, 3.0}; // C++ 11列表初始化

    // 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值
    Complex c = 1.0; // Classname object = value;

    // 如果方法通过计算得到一个新的类对象,则应考虑是否可以使用类的构造函数来完成这种工作,这样可以确保新的对象是按正确的方式创建的
    return Vector(x, y);

    如果有多个构造函数,则必须以相同的方式使用new,要么都带中括号,要么都不带。因为只有一个析构函数,所有的构造函数都必须与它兼容。然而,可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++中的nullptr),因为delete或delete []可以用于空指针。

  8. 为了防止类成员与参数名相同,常见的做法是在数据成员中使用m_前缀,或在成员名中使用后缀_

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Stock
    {
    private:
    string m_company;
    lomg m_share;
    };

    class Stock
    {
    private:
    string company_;
    lomg share_;
    };
  9. 默认构造函数:

    默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。它是用于下面这种声明的构造函数:

    1
    Stock fluffy_the_cat;

    如果没有提供任何构造函数,则C++将自动提供默认构造函数。它是默认构造函数的隐式版本,不做任何工作,如:

    1
    Stock::Stock() { }

    当且仅当没有自定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,必须为它提供默认构造函数,否则下面的声明将出错:

    1
    Stock stock;

    创建默认构造函数的方式有两种,由于只能有一个默认构造函数,因此不要同时采用这两种方式:

    1. 使用默认参数:

      1
      Stock::Stock(const string & co = "Error", int n = 0, doouble pr = 0.0);
    2. 使用函数重载:

      1
      Stock::Stock() { }

      使用默认构造函数初始化对象:

    1
    2
    3
    4
    5
    Stock first = Stock();  // 显式调用默认构造函数
    Stock * first = new Stock; // 隐式调用默认构造函数

    Stock first(); // 声明函数,返回值为Stock类型
    Stock second; // 隐式调用默认构造函数
  10. 复制构造函数:

    1. 每当程序生成了对象副本,如在函数按对象值传参(或函数返回对象)以及声明对象时赋值的时候,不会调用构造函数生成新的对象,而是调用了复制构造函数。当使用一个对象来初始化另一个对象时,编译器将自动生成上述构造函数,默认的复制构造函数逐个复制非静态成员,如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。复制构造函数的原型如下:

      1
      2
      3
      4
      5
      6
      7
      Foo(const Foo &);

      Foo foo1; // 对象1
      Foo foo2 = foo1; // 调用复制构造函数
      Foo foo3(foo1); // 调用复制构造函数
      Foo foo4 = Foo(foo1); // 调用复制构造函数
      Foo * foo5 = new Foo(foo1); // 调用复制构造函数

      由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。这样可以节省调用构造函数的时间以及存储新对象的空间。

      如果类中包含了使用new初始化的指针成员,应当定义一个复制构造函数,以复制指向的数据,而不是指针,这被称为深度复制。

    2. 警惕复制构造函数和重载赋值运算符函数:

      如果对象成员引用了动态内存,则必须时刻警惕可能导致调用复制构造函数和重载赋值运算符函数的操作,如创建临时对象(函数按对象传参和返回对象,以及表达式计算存储中间值等),对象赋值等。一个有效的技巧时,将其定义为私有方法:

      1
      2
      3
      4
      5
      class Queue
      {
      private:
      Queue & operator=(const Queue & q) {return *this};
      }

      这样做,可以避免编译器自动生成默认复制构造函数和重载赋值运算符函数,且因为这些方法是私有的,所以在类外编译器将禁止需要调用复制构造函数和重载赋值运算符函数的操作,但仍需警惕在类内执行这些操作,如类方法中返回对象仍无法避免的会创建临时变量。

  11. 析构函数:

    在类名前加上~,即成为析构函数的名称。析构函数跟构造函数一样,可以没有返回值和声明类型。析构函数没有参数。如果没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。

  12. const成员函数:

    1
    2
    3
    4
    5
    6
    const Complex c = Complex(2.0, 3.0);
    c.show(); // 编译器报错,因为show函数的代码可能会修改c对象的成员,但是如果在析构函数中调用不会报错(不知道是不是个bug)

    // 将const关键字放在函数的括号后面,保证函数不会修改调用对象,只要类方法不修改调用对象,就应将其声明为const
    void show() const;
    void Complex::show() const {}
  13. this指针:

    this指针指向用来调用成员函数的对象,*this表示调用成员函数的对象。对象成员x只不过是this->x的简写。

  14. 静态类成员:

    静态类成员有一个特点:无论创建了多少个对象,程序都将只创建一个静态变量的副本,也就是说,类的所有对象共享同一个静态成员。不能在类声明中初始化静态成员变量,这是因为声明描述了如何分配内存,但并不分配内存。对于静态类成员,可以在类声明之外使用单独的语句来初始化:

    1
    2
    3
    4
    5
    6
    7
    class Foo
    {
    private:
    static int mode;
    };

    int Foo::mode = 0; // 静态类成员初始化,初始化语句指出了类型,并使用了作用域解析运算符,但没有使用关键字static

    初始化是在方法文件中进行的,不能在声明文件中进行初始化。这是因为类声明位于头文件中,程序可能将头文件包括在其他几个文件中,导致出现多个初始化语句副本,引发错误。

    静态数据成员在类中声明,在包含类方法的文件中进行初始化,但如果静态成员是const整数类型或枚举型,则可以在类声明中初始化。静态数据成员初始化时使用作用域解析运算符来指出静态成员所属的类。

  15. 静态类成员函数:

    静态类成员函数的声明必须包含关键字static,但如果函数定义是独立的,则其中不能包含关键字static。静态成员函数不能使用this指针。如果静态成员函数是在公有部分声明的,则可以使用类名和作用域解析运算符来调用它。由于静态成员函数不与特定对象相关联,因此只能使用静态数据成员。

    1
    static int getStatus(void);
  16. 作用域为类的常量:

    1. 可以用枚举为整型常量提供作用域为整个类的符号名称:

      1
      2
      3
      4
      5
      class Color
      {
      private:
      enum {Months = 12};
      };

      注意,这种方式声明枚举并不会创建类数据成员。也就是说,所有对象中都不包含枚举。另外,Months只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用12替换它。

      在类声明中声明的结构、类或枚举被称为是被嵌套在类中,其作用域为整个类。这种声明不会创建数据对象,而只是指定了可以在类中使用的类型。如果声明是在类的私有部分进行的,则只能在这个类使用被声明的类型;如果声明是在公有部分进行的,则可以从类的外部通过作用域解析运算符使用被声明的类型。

    2. 使用static关键字:

      1
      2
      3
      4
      5
      class Color
      {
      private:
      static const int Months = 12;
      };

      该常量与其他静态变量存储在一起,而不是存储在对象中。因此,只有一个Months常量,被所有Color对象共享。在C++98中,只能使用这种技术声明值为整数或枚举的静态常量,而不能存储double常量,C++ 11消除了这种限制。

  17. 成员初始化列表:

    对于const数据成员,必须在执行到构造函数体之前,即创建对象时进行初始化。C++提供了成员初始化列表语法来完成数据成员的初始化工作。成员初始化列表由逗号分隔的初始化列表组成(前面带冒号)。它位于参数列表的右括号之后,函数体左括号之前。通常,初值可以是常量或构造函数的参数列表中的参数,也可以是表达式和函数调用。这种方式并不限于初始化常量。只有构造函数可以使用这种语法,对于const类成员和被声明为引用的类成员,也必须使用这种语法。对于本身就是类对象的成员来说,使用成员初始化列表的效率更高。

    如果Classy是一个类,而mem1、mem2和mem3都是这个类的数据成员,则初始化语法规则如下:

    1
    Classy::Classy(int n, int m) : mem1(n), mem2(m), mem3(n * m + 2) {}

    初始化列表中的每一项都调用与之匹配的构造函数,如:

    1
    2
    Student(const char * str, const double * pd, int n) : name(str), scores(pd, n) {}
    // 这里scores是valarray对象,scores(pd, n)将调用valarray(const double *, int)构造函数

    数据成员的被初始化的顺序与它们出现在类声明中的顺序相同,与初始化器中的排列顺序无关。在类中声明函数的时候可以不使用成员初始化列表语法,然后在定义时再进行添加。

    C++ 11的类内初始化

    C++ 11允许以更直观的方式进行初始化:

    1
    2
    3
    4
    5
    class Classy
    {
    int mem1 = 10;
    const int mem2 = 20;
    };

    这与在构造函数中使用成员初始化列表等价:

    1
    Classy::Classy() : mem1(10), mem2(20) {}

    如果调用了使用成员初始化列表的构造函数,则列表参数将覆盖类内初始化的默认初始值:

    1
    Classy::Classy(int n) : mem1(n) {}

    在这里,构造函数将使用n来初始mem1,但mem2仍被设置为20。

  18. 作用域内枚举:

    C++ 11提供了一种新枚举,其枚举量的作用域为类。可以避免两个枚举定义中的枚举量可能发生的冲突。

    1
    2
    enum class egg {Small, Medium, Large, Jumbo};
    enum class t_shirt {Small, Medium, Large, Xlarge};

    也可以用关键字struct代替class。需要使用枚举名来限定枚举量,如t_shirt::Small

    常规枚举将自动转换为整型,如将其赋给int变量或用于比较表达式时,作用域内枚举不能隐式地转换为整型,但可以进行强制类型转换;

    枚举用某种底层整型类型表示,在C++98中,如何选择取决于实现,因此包含枚举的结构的长度可能随系统而异。对于作用域内枚举,C++ 11消除了这种依赖型。默认情况下,C++ 11作用域内枚举的底层类型为int,另外,还可以指定底层整型类型:

    1
    2
    enum class : short pizza {Small, Medium, Large, Xlarge};  // 测试不通过
    enum class pizza : short {Small, Medium, Large, Xlarge}; // 测试通过
  19. 运算符重载:

    1. 重载运算符函数的格式如下:

      1
      2
      3
      operatorop(argument-list){}

      // 当重载运算符函数作为类成员函数时(非友元函数),将通过对象调用;当重载运算符函数作为非成员函数时,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应于运算符函数的第二个参数
    2. op必须是有效的C++运算符,不能虚构一个新的符号,例如不能有operator@()这样的函数。重载运算符与原来的原来的运算符具有相同的优先级;

    3. 为了区分++运算符的前缀版本和后缀版本,C++将operator++()作为前缀版本,将operator++(int)作为后缀版本,其中的参数永远也不会用到,所以不必指定其名称。

    4. 可以像调用Sum()等普通成员函数那样来调用operator+()等方法;

    5. 在运算符表示法中,运算符左侧的对象是调用对象,运算符右边的对象是作为参数被传递的对象;

    6. 因为运算符重载是通过函数来实现的,所以只要运算符函数的特征标不同,使用的运算符与相应的内置C++运算符相同,就可以多次重载同一个运算符,如减法运算符-和负号运算符-

    7. 多对象进行运算:

      1
      2
      3
      t4 = t1 + t2 + t3;
      // 将被转换为如下函数调用
      t4 = t1.operator+(t2.operator+(t3));
    8. 重载赋值运算符:

      赋值运算符的原型如下:

      1
      Class_name & Class_name::operator=(const Class_name &);

      使用赋值运算符应注意的点:

      • 由于目标对象可能引用了以前分配的数据,所以函数应使用delete来释放这些数据;
      • 函数应当避免对象给自身赋值,否则,给对象重新赋值前,释放内存的操作可能删除对象的内容;
      • 函数返回一个指向调用对象的引用,因此可以连续进行赋值。

      一个例子:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      Foo & Foo::operator=(const Foo & foo)
      {
      if (this == &foo)
      {
      return *this;
      }
      delete [] str; // 必须确保str是初始化过了的

      /* 赋值其他内容 */

      return *this;
      }
    9. 重载限制:

      1. 使用运算符时不能违反运算符原来的句法规则。例如,不能将求模运算符号(%)重载成只使用一个操作数的运算符;
      2. 重载的运算符必须至少有一个操作数是用户自定义的类型;
      3. 不能创建新运算符,例如,不能定义operator**()函数来表示求幂;
      4. 不能重载下面的运算符:
        • sizeof
        • .成员运算符
        • ::作用域解析运算符
        • ?:条件运算符
        • typeid一个RTTI运算符
        • const_cast强制类型转换运算符
        • dynamic_cast强制类型转换运算符
        • reinterpret_cast强制类型转换运算符
        • static_cast强制类型转换运算符
      1. 友元有3种:

        • 友元函数
        • 友元类
        • 友元成员函数

        通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。

      2. 非成员重载运算符函数可以解决操作数互换的问题,但是不能访问类的私有数据。如果要为类重载运算符,并将非类的项作为其第一个操作数,则可以使用友元函数来反转操作数的顺序,也不一定需要友元函数,如果不访问私有成员,可以不是友元函数:

        1
        2
        3
        4
        Time operator*(double m, const Time & t)  // 操作数互换(非友元函数)
        {
        return t * m; // use t.operator*(m)
        }
      3. 如果一个函数是多个类的友元函数,则该函数可以同时访问这些类的私有成员。

      4. 创建友元函数

        创建友元函数的第一步是将其原型放在类声明中,并在原型声明前加上关键字friend。虽然该函数是在类声明中进行声明的,但它不是成员函数,因此不能使用成员运算符来调用,但该函数内可以使用类的私有成员。在函数定义中不要使用类名,如Time::限定符,且不要使用friend关键字:

        1
        2
        3
        4
        5
        6
        7
        friend Time operator*(double, const Time &);  // 类中声明

        Time operator*(double m, const Time & t)
        {
        t.hours = 3.0; // 函数定义,可以访问类的私有成员
        return t * m;
        }
      5. 一般来说,要重载<<运算符来显示c_name的对象,可以使用一个友元函数,其定义如下:

        1
        2
        3
        4
        5
        ostream & operator<<(ostream & os, const c_name & obj)
        {
        os << ...;
        return os;
        }
      6. 派生类中的友元函数可以通过强制类型转换成基类对象引用,进而调用基类友元函数,提供了访问私有数据的可能性:

        1
        2
        3
        4
        5
        6
        ostream & operator<<(ostream & os, const derived & obj)
        {
        os << (const base &) obj;
        os << ...; // derived members
        return os;
        }
  20. 类的自动类型转换和强制类型转换:

    1. 在C++中,接受一个参数的构造函数可以作为将类型与该参数相同的值转换为类的转换构造函数,但只有接受一个参数的构造函数才能作为转换函数(带默认参数的函数也可以),这一过程称为隐式转换,因为它是自动进行的,不需要显式强制类型转换:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      Weight(double w);  // 构造函数
      Weight w;
      w = 19.6; // 使用构造函数将19.6转换为Weight对象

      Weight w = 19.6; // 当构造函数只接受一个参数时,可以使用这种格式来初始化类对象


      Star north;
      north = "polaris"; // 调用Star::operator=(const Star *)函数,使用Star::Star(const char *)生成一个对象,该对象将被用作上述赋值运算符函数的参数

      函数原型化提供的参数匹配过程,允许使用Weight(double)构造函数来转换其他数值类型,如int类型。然而当且仅当转换不存在二义性时,才会进行这种二步转换。也就是说,如果这个类还定义了构造函数Weight(long),则编译器将拒绝这些语句,因为调用存在二义性。

    2. 使用隐式转换的情况有:

      • 将对象初始化为如double类型的值时;
      • 将如double类型的值赋给对象时;
      • 将如double类型的值传递给接受类对象参数的函数时;
      • 返回值声明为类对象的函数试图返回如double类型的值时;
      • 在上述任意一种情况下,使用可转换为如double类型的内置类型时(比如说使用int类型数据初始化)。
    3. C++新增了关键字explicit,用于关闭上述自动转换的特性,但仍然允许显式强制类型转换:

      1
      2
      3
      4
      explicit Weight(double w);  // 构造函数
      Weight w;
      w = Weight(19.6); // ok,这不就是显式调用构造函数吗?
      w = (Weight) 19.6; // ok,旧格式
    4. 创建转换函数,将类对象转换为typeName类型,需要使用下面这种形式的转换函数:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      operator typeName();

      // 例子
      operator int();

      int a = obj; // 隐式转换
      int a = int (obj); // 强制类型转换
      int a = (int) obj; // 强制类型转换

      // 如果只定义了一种转换函数,如double转换函数,则下面的语句不会产生二义性,将使用double转换函数和二步转换
      long var = obj;
      // 如果同时定义了多种转换函数,则上述语句将产生二义性,编译器将报错。不过可以用显式强制类型转换来指出要使用哪个转换函数
      // 如果参数类型不匹配,可能会产生二步转换。例如,在不使用explicit关键字的情况下只定义了int转换函数,但是强制转换为double类型,则先将obj转换为int值,再把int值转换为double值

      注意:

      • 转换函数必须是类方法;
      • 转换函数不能指定返回类型;
      • 转换函数不能有参数。
    5. 在C++ 11中,允许将关键字explicit应用于转换函数,可以关闭隐式转换;

    6. 转换函数与友元函数:

      1
      2
      3
      4
      5
      6
      7
      // 假设声明了友元重载加法运算符函数如下:
      friend ClassName operator+(const ClassName &, const ClassName &);
      // 上述声明可能会调用转换构造函数(如传入double类型的值),导致运行性能下降。如果能指定函数参数为特定类型,则无需调用构造函数,运行速度更快些

      // 如果定义了double转换构造函数,并且允许隐式转换,则下面的的代码将调用友元函数,将double的1.0传递给第一个参数,然后调用double转换构造函数生成一个Class对象,再执行对象相加操作。
      ClassName t = 1.0 + obj;
      // 如同时定义了operator double()转换函数,则上述代码将出现二义性,因为可以解释为将obj转换为double,再把两个double值相加
    7. 如果定义了隐式转换函数,但是没有定义友元运算符重载函数,则有时候对象会被隐式地转换。将重载定义为友元函数可以让程序更容易适应自动类型转换,原因在于,两个操作数都成为函数的参数,因此与函数原型匹配,从而调用重载函数:

      1
      ClassName t = 1.0 + obj;  // obj可能被转换,如隐式转换为double
  21. 类继承:

    1. 公有派生:

      使用公有派生,基类的公有成员将成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有和保护方法访问。

      1
      2
      3
      4
      class Engineer : public Person
      {

      };

      创建派生类对象时,程序首先创建基类对象。这意味着基类对象应当在程序进入派生类构造函数之前被创建,C++使用成员初始化列表语法来完成这种工作:

      1
      derived::derived(type1 x, type2 y) : base(x, y) {}

      ==除虚基类外,派生类只能将值传递回相邻的基类==,但后者可以使用相同的机制将信息传递给的其相邻的基类,依此类推。如果没有在成员初始化列表中提供基类构造函数,程序将使用默认的基类构造函数;C++ 11新增了一种能够继承构造函数的机制,默认不打开。

      赋值运算符是不能被继承的,派生类继承的方法的特征标与基类完全相同,但赋值运算符的特征标随类而异,这是因为它包含一个类型为其所属类的形参。这种情况下,基类实现将被屏蔽。

      派生类对象过期时,程序将首先调用派生类析构函数,然后再调用基类析构函数。

    2. 派生类和基类之间的特殊关系:

      基类指针可以在不进行显式类型转换的情况下指向派生类对象;基类引用可以在不进行显式类型转换的情况下引用派生类对象。然而,基类指针或引用只能用于调用基类方法;不可以将基类对象和地址赋给派生类引用和指针;

      引用兼容属性能够将基类对象初始化为派生类对象,这将调用隐式复制构造函数(如果没有定义的话):

      1
      2
      Engineer e;
      Person p (e);

      同样,可以将派生类对象赋给基类对象,这将调用隐式重载赋值运算符函数(如果没有定义的话):

      1
      2
      3
      Engineer e;
      Person p;
      p = e;
    3. 多态公有继承:

      多态:方法的行为取决于调用该方法的对象。

      如果要在派生类中重新定义基类的方法,通常应将基类方法声明为虚的。这样,程序将根据对象类型而不是引用或指针的类型来选择方法版本。方法在基类中被声明为虚的后,它在派生类(包括从派生类派生出来的类)中将自动成为虚方法(派生链中所有函数);

      如果要在派生类中重新定义基类的方法,则在基类中将它设置为虚方法;否则,设置为非虚方法;然而,在派生类声明中使用关键字virtual来指出哪些函数是虚函数也不失为一个好办法(实际上在派生类中virtual可省略)。

      如果没有使用关键字virtual,程序将根据引用类型或指针类型选择方法;如果使用了关键字virtual,程序将根据引用或指针指向的对象的类型来选择方法;

      ==使用基类指针指向派生类对象后,若没有使用virtual关键字,则*object将是基类对象,否则是派生类对象,即typeid(*object).name()将是基类类型,否则是派生类类型==;

      在继承中允许派生类与基类出现同名函数或重载函数(此时将根据类型选择方法),但是非虚函数不具有多态特性;

      如果方法重新进行了定义,那么在派生类中,使用作用域解析运算符::来调用基类方法,若直接调用,可能会导致递归;如果派生类中没有重新定义方法,则可以直接调用基类方法;

      友元不能是虚函数,因为友元不是类成员,而只有类成员才能是虚函数;

      如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本;

      重新定义派生类中的方法并不是重载,无论参数列表是否相同,派生类的新方法将隐藏所有同名的基类方法。有两条经验规则:

      第一,如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变,因为允许返回值类型随类类型的变化而变化;

      第二,如果基类声明被重载了,则应该在派生类中重新定义所有的基类版本。如果不需要修改,则新定义可以只调用基类版本。

      1
      2
      3
      4
      5
      6
      virtual Person getInfo(int) const;  // Base class
      virtual Engineer getInfo() const; // Derived class

      virtual Engineer getInfo() const {return Person::getInfo();} // 重新定义的方法直接返回基类的实现

      Engineer::getInfo(5); // 错误,重新定义的函数与基类原型不一致,将屏蔽基类的版本
    4. 静态联编和动态联编:

      将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编;在编译过程中进行联编被称为静态联编,又称为早期联编;编译器生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编,又称为晚期联编。在C++中,动态联编与通过指针和引用调用方法相关;

      虚函数的工作原理:通常,编译器处理虚函数的方法是,给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。这种数组称为虚函数表,虚函数表存储了为类对象进行声明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;如果派生类没有重新定义虚函数,该虚函数表将保存原始版本的地址。如果派生类定义了新的虚函数,则该函数的地址也将被添加到虚函数表中。

      对使用了虚函数的类对象使用sizeof运算符,可以看到类对象使用了额外的存储空间,用于实现虚函数机制。

    5. 虚析构函数:

      如果析构函数不是虚的,则当使用delete关键字释放基类指针指向的派生类对象所占用的内存时,将会调用基类的析构函数。如果析构函数是虚的,则调用的析构函数取决于指向的对象的类型,而不是指针的类型。对于虚析构函数,如果基类指针指向派生类对象,则释放内存时,先调用派生类的析构函数,再调用基类的析构函数;

      析构函数应当是虚函数,除非类不做基类。

      ==即使虚构函数是纯虚函数,也必须进行实现==。

    6. 访问控制protected:

      关键字protectedprivate相似,在类外只能用公有类成员来访问protected部分中的类成员。派生类的成员可以直接访问基类的保护成员,但不能直接访问基类的私有成员。因此,对于外部世界来说,保护成员的行为与私有成员相似;但对于派生类来说,保护成员的行为与公有成员相似;

      最好对类数据成员采用私有访问控制,不要使用保护访问控制;同时通过基类方法使派生类能够访问基类数据。然而,对于成员函数来说,保护访问控制很有用,它让派生类能够访问类外不能使用的内部函数。

    7. 抽象基类ABC

      为解决继承中出现的信息冗余,可以使用抽象基类。例如,从Ellipse和Circle类中抽象出它们的共性,将这些特性放到一个ABC中,然后从该ABC中派生出Circle和Ellipse类。这样,便可以使用基类指针数组同时管理Circle和Ellipse对象。

      C++通过使用纯虚函数提供未实现的函数(实际上也可以实现虚函数,但是似乎没什么用),纯虚函数声明的结尾处为=0

      1
      virtual double Area() const = 0;  // a pure virtual function

      当类声明中包含纯虚函数时,不能创建该类的对象,只能用作基类。抽象类必须至少包含一个纯虚函数。

31. C++中的代码重用

通常,包含、私有继承和保护继承用于实现has-a关系,即新的类将包含另一个类的对象。

大多数C++程序员倾向于使用包含,因为私有继承比较复杂,可能出现多个独立基类中包含同名方法或共享祖先等问题;使用包含可以包括多个同类的子对象,如3个string对象,而继承则只能使用一个string对象。如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应使用私有继承。

  1. 私有继承:

    使用私有继承,基类的公有成员和保护成员都将成为派生类的私有成员。这意味着基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们,它实现了has-a关系。

    在类继承中,private是默认值,因此省略访问限定符也将导致私有继承:

    1
    2
    3
    4
    class Engineer : private Person  // private可省略
    {

    };

    使用作用域解析运算法来调用基类的方法,如Base::sum();;但如果要使用基类对象本身,可以使用强制类型转换,将Derived对象转换为Base对象:

    1
    2
    3
    4
    const Base & Derived::GetBase() const
    {
    return (const string &) * this; // 为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用
    }

    用类名显式地限定函数名不适合友元函数,这是因为友元不属于类。但是,可以通过显式地转换为基类来调用正确的函数,如:

    1
    2
    3
    4
    ostream & operator<<(ostream & os, const Base & base)  //  Base由string派生而来
    {
    os << (const string &) base << endl;
    }

    派生类的引用不会自动转换为基类的引用,在私有继承中,未进行显式类型转换的派生类引用或指针,无法赋值给基类的引用或指针。

  2. 保护继承

    保护继承是私有继承的变体。保护继承在列出基类时使用关键字protected

    1
    2
    3
    4
    class Engineer : protected Person
    {

    };

    使用保护继承时,基类的公有成员和保护成员都将称为派生类的保护成员。和私有继承一样,基类的接口在派生类中也是可用的,但在继承层次结构之外是不可用的。当从派生类派生出另一个类时,私有继承和保护继承之间的主要区别便呈现出来了。使用私有继承时,第三代类将不能使用基类的接口,这是因为基类的公有方法在派生类中将变成私有方法;使用保护继承时,基类的公有方法在第二代中将变成受保护的,因此第三代派生类可以使用它们。

  3. 使用using重新定义访问权限

    可以使用using声明,指出派生类中可以使用的基类成员,即使采用的是私有派生:

    1
    2
    3
    4
    5
    class Engineer : private Person
    {
    public:
    using Person::Name; // 使用using声明使得基类私有成员函数可用
    };

    注意,using声明只使用成员名,没有圆括号、函数特征标和返回类型,因此,对于operator[]函数(const和非const)都可用。using声明只适用于继承,不适用于包含。

  4. 多重继承

    使用多个基类的继承被称为多重继承(Multiple Inheritance, MI)。

    必须使用关键字来限定每一个基类,否则编译器将认为是私有派生:

    1
    class PineApple : public Pie, public Apple {};

    如果PieApple都有一个共同的基类Food,那么PineApple类将包含两个Food组件,此时将派生类对象的地址赋给基类指针,将会出现二义性:

    1
    2
    PineApple pa;
    Food * pt = &pa; // ambiguous

    通常,这种赋值将把基类指针设置为派生对象中基类对象的地址,但pa中包含两个Food对象,有两个地址可供选择,所以应使用类型转换来指定对象:

    1
    2
    Food *pt = (Pie *)&pa;  // Food in Pie
    Food *pt = (Apple *)&pa; // Food in Apple

    虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象。如:

    1
    2
    3
    4
    5
    // virtual和public的次序无关紧要
    class Pie : virtual public Food {};
    class Apple : public virtual Food {};

    class PineApple : public Pie, public Apple {};

    现在PineApple对象只包含Food对象的一个副本,也因为只有一个副本,所以可以使用多态(没有使用虚基类之前,需要强制类型转换,所以多态只存在于上一层的继承中)。

    如果使用虚基类,那么构造函数中派生类向基类逐级传递信息将不起作用(==除虚基类外,派生类只能将值传递回相邻的基类==),例如:

    1
    Class PineApple(const Food & f, int weight, int color) : Pie(f, weight), Apple(f, color){}  // 存在缺陷

    C++在基类是虚的时,禁止信息通过中间类自动传递给基类。因此,上述构造函数将初始化成员weightcolor,但是f参数中的信息将不会传递给子对象Food。然而,编译器必须在构造派生类对象之前构造基类对象组件,因此,在上述情况下,编译器将使用Food的默认构造函数。如果不希望使用默认构造函数来构造虚基类对象,则需要显式地调用所需的基类构造函数。因此,构造函数应该是这样的:

    1
    Class PineApple(const Food & f, int weight, int color) : Food(f), Pie(f, weight), Apple(f, color){}

    对于虚基类,必须这样做,但对于非虚基类,则是非法的。如果类有间接虚基类,则除非只使用该虚基类的默认构造函数,否则必须显式地调用该虚基类的某个构造函数。

    假如PineApple没有重新定义Show方法,则调用PineApple::Show时将出现二义性,可以通过作用域解析运算符来指定调用函数pa.Pie::Show

    总之,在祖先相同时,使用MI必须引入虚基类,并修改构造函数初始化列表的规则。另外,如果在编写这些类的时候没有考虑到MI,则还可能需要重新进行编写,例如,PieApple中都有Show方法,且都调用了FoodShow方法,如果在PineApple类中要输出所有信息,可以先调用Pie::ShowApple::Show,但是这样会导致Food::Show的重复调用。对此,一种方法是提供一个只显示Food组件的方法和一个只显示Pie组件和Apple组件的方法,然后在PineApple::Show方法中将组件组合起来。

    当类通过多条虚途径和非虚途径继承某个特定的基类时,该类将包含一个表示所有的虚途径的基类子对象和分别表示各条非虚途径的多个基类子对象。假设类B被用作类C和D的虚基类,同时被用作类X和Y的非虚基类,而类M是从C、D、X和Y派生而来的。在这种情况下,类M从虚派生祖先(即类C和D)那里共继承了一个B类子对象,并从每一个非虚派生祖先(即类X和Y)分别继承了一个B类子对象。因此,它包含三个B类子对象。

    如果使用虚基类,从不同的类那里继承了两个或多个同名成员(数据或者方法),则使用该成员名时不一定会导致二义性。在这种情况下,如果某个名称优先于其他所有名称,则使用它时,即使不使用限定符,也不会导致二义性。

    优先的定义为:派生类中的名称优先于间接或祖先类中的相同名称,例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    #include <iostream>

    using namespace std;

    class B
    {
    public:
    short q();
    };

    short B::q()
    {
    cout << "In B::q()" << endl;
    return 0;
    }

    class C : virtual public B
    {
    public:
    long q();
    int omg();
    };

    long C::q()
    {
    cout << "In C::q()" << endl;
    return 0;
    }

    int C::omg()
    {
    cout << "In C::omg()" << endl;
    return 0;
    }

    class D : public C
    {
    };

    class E : virtual public B
    {
    private:
    int omg();
    };

    int E::omg()
    {
    cout << "In E::omg()" << endl;
    return 0;
    }

    class F : public D, public E
    {
    };

    int main()
    {
    F f;
    f.q();
    return 0;
    }

    C中的q()定义优先于类B中的q()定义,因为类C是从类B派生而来的。因此,F中的方法可以使用q()来表示C::q()。另一方面,任何一个omg()定义都不优先于其他omg()定义,==因为CE都不是对方的基类==,所以,在F中使用非限定的omg()将导致二义性。

    ==虚二义性规则与访问规则无关==,不能通过private, public等关键字修改虚二义性优先级规则。

  5. 类模板

    采用模板时,将使用模板定义替换类声明,使用模板成员函数替换类的成员函数。和模板函数一样,模板类如下代码开头:

    1
    2
    template <class Type>
    template <typename Type> // 较新的C++实现可以用typename代替class

    关键字template告诉编译器要定义一个模板。Type为泛型标识符,可以使用自己的泛型名代替Type

    以模板成员函数替换原有的类方法,每个函数都将以上述相同的模板声明打头,还需将成员函数的类限定符从Class::改为Class<Type>::,但是如果在类声明中定义了方法(内联定义),则可以省略模板前缀和类限定符。

    ==注意==:不能将模板函数放在独立的实现文件中(以前,C++标准确实提供了关键字export,能够将模板成员函数放在独立的实现文件中,但支持该关键字的编译器不多;C++ 11不再这样使用关键字export,而将其保留用于其他用途)。由于模板不是函数,它们不能单独编译。==模板必须与特定的模板实例化请求一起使用==。最简单的方法是将所有模板信息放在一个头文件中,并在要使用这些模板的文件中包含该头文件。

    ==注意==:声明成员函数的返回类型时可以为Class,而实际的模板函数定义将类型定义为Class<Type>,前者是后者的缩写,但只能在类中使用。即可以在模板声明或模板函数定义内使用Class,但在类的外面,即指定返回类型或者使用作用域解析运算符时,必须使用完整的Class<Type>。==但是==,经试验,函数的参数类型,如复制构造函数与赋值运算符函数的参数const Class &,在声明和定义时,可以不加<Type>。同时,template <class T>中泛型标识符只是一个占位符,对于不同的成员函数,可以使用不同的标识符。

    非类型参数:

    1
    template<class T, int n>

    int指出n的类型为int,这种参数(指定特殊的类型而不是用作泛型名)称为非类型或者表达式参数。假设有如下声明:

    1
    ClassName<double, 12> obj;

    将导致编译器定义名为ClassName<double, 12>的类。

    ==限制==:表达式参数可以使整型、枚举、引用或指针。因此double m不合法,double & rndouble * pn是合法的。模板代码不能修改参数的值,也不能使用参数的地址,所以,不能使用诸如n++&n等表达式。另外,实例化模板是,用作表达式参数的值必须是==常量表达式==。

    ==优势==:表达式参数使用的是为自动变量维护的内存栈,执行速度更快,适用于使用了很多小型数组的情况。

    ==缺点==:表达式参数不同时,将生成独立的类声明。

    模板的多功能性:可以将用于常规类的技术用于模板类。模板类可用作基类,也可用作组件类,还可用作其他模板的类型参数,即:

    1
    Array < Stack<int> > asi;  // an array of stacks of int

    在C++ 98中要求使用至少一个空白字符将两个>符号分开,以免与运算符>>混淆,C++ 11则不做要求。

    ==递归使用模板==:Array<Array<double, 5>, 10> twodee,可用于构造多维数组。

    ==默认类型模板参数==:可以为类型参数提供默认值:template <class T1, class T2 = int> class Topo {...};,如果省略T2的值,编译器将使用int。若所用模板参数都有默认参数,则可以这样定义对象ClassName<> obj;。==注意==:可以为类模板类型参数提供默认值,但是不能为函数模板参数提供默认值,因为隐式实例化将覆盖默认值。

    ==三种具体化==:

    1. 隐式实例化:通过声明一个或多个类对象,指出所需类型,编译器使用同样模板生成具体的类定义,如:

      1
      Array<int, 10> ar;  // implicit instantiation

      编译器在需要对象之前,不会生成类的隐式实例化:

      1
      2
      Array<int, 10> *pt;  // a pointer, no object needed yet
      pt = new Array<int, 10>; // now an object is needed

      第二条语句导致编译器生成类定义,并根据该定义创建一个对象。

    2. 显式实例化

      当使用关键字template并指出所需类型来声明类时,编译器将生成类声明的显式实例化。声明必须位于模板定义的名称空间中。例如:

      1
      template class Array<int, 10>;  // generate Array<int, 10> class

      这里比函数模板多了关键字class,经测试class关键字可省略(g++不能省略,cl可省略)。虽然没有创建对象,编译器也将生成类声明(包括方法定义),和隐式实例化一样,也将根据同样模板(与下述显式具体化比较)来生成具体化。

    3. 显式具体化

      显式具体化是特定类型(用于替换模板中的泛型)的定义。有时候,可能需要在为特殊类型实例化时,对模板进行修改(比如内置变量类型,如char *不支持的运算符等),使其行为不同。在这种情况下,可以创建显式具体化。

      当具体化模板和通用模板都与实例化请求匹配时,编译器将使用具体化版本。

      具体化类模板定义的格式如下:

      1
      template <> class Classname<specialized-type-name> {...};
    4. 部分具体化

      部分具体化可以给类型参数之一指定具体的类型:

      1
      2
      3
      4
      5
      6
      // general template
      template <class T1, class T2> class Pair {...};
      // explicit specialization
      template <> class Pair <int, int> {...};
      // specialization with T2 set to int
      template <class T1> class Pair <T1, int> {...};

      关键字template后面的<>声明的是没有被具体化的类型参数,如果指定所有类型,则<>内将为空,这将导致显式具体化。

      如果有多个模板可供选择,编译器将使用具体化程度最高的模板。例如,给定上述三个模板,有以下使用情况:

      1
      2
      3
      Pair<double, double> p1;  // use general Pair template
      Pair<double, int> p2; // use Pair<T1, int> partial specialization
      Pair<int, int> p3; // use Pair<int, int> explicit specialization

      也可以通过为指针提供特殊版本来部分具体化现有的模板:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      template <class T>  // general version
      class Foo {...};


      // 书上是这样写的,但是测试时编译不通过,为指针单独设计了一个语法确实有点恶心,不好记
      template <class T*> // pointer partial specialization
      class Foo {...}; // modified code


      // 这样子写倒是可以
      template <class T>
      class Foo <class T*>
      {
      ...
      }

      如果提供的类型不是指针,则编译器将使用通用模板;如果提供但是指针,则编译器将使用指针具体化版本:

      1
      2
      Foo<char> obj;  // use general Foo template, T is char
      Foo<char *> pobj; // use Foo T* template, T is char

      第二个声明将使用具体化模板,将T转换为char(==不带*==)。

      部分具体化特性使得能够设置各种限制,例如,给定以下声明:

      1
      2
      3
      4
      5
      6
      // general template
      template <class T1, class T2, class T3> Class Foo {...};
      // specialization with T3 set to T2
      template <class T1, class T2> class Foo <T1, T2, T2> {...};
      // specialization with T2 and T3 set to T1*
      template <class T1> class Foo <T1, T1*, T1*> {...};

      在上述声明下,编译器将作出如下选择:

      1
      2
      3
      Foo<int, short, char *> foo;  // use general template
      Foo<int, short> foo; // use Foo<T1, T2, T2> template
      Foo<char, char *, char *> foo; // use Foo<T1, T1 *, T1 *> template

    ==成员模板==:模板可用作结构、类或模板类的成员(函数不允许嵌套,类声明允许嵌套):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    template <typename T>
    class beta
    {
    private:
    template <typename V> // nested template class member
    class hold
    {
    private:
    V val;

    public:
    hold(V v = 0) : val(v) {}
    void show() const { cout << val << endl; }
    V Value() const { return val; }
    };
    hold<T> q; // template object
    hold<int> n; // template object
    public:
    beta(T t, int i) : q(t), n(i) {}
    template <typename U> // template method
    U blab(U u, T t)
    {
    return (n.Value() + q.Value()) * u / t;
    }
    void show() const
    {
    q.show();
    n.show();
    }
    };

    blab方法的U类型由该方法被调用时的参数值显式确定,T类型由对象的实例化类型确定,它不是由函数调用设置的(这里是隐式实例化吗?)。

    可以在beta类中声明hold类和blab方法,然后在beta模板外面进行定义:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    template <typename T>
    class beta
    {
    private:
    template <typename V> // nested template class member
    class hold;

    hold<T> q; // template object
    hold<int> n; // template object
    public:
    beta(T t, int i) : q(t), n(i) {}
    template <typename U> // template method
    U blab(U u, T t);

    void show() const
    {
    q.show();
    n.show();
    }
    };

    // member definition
    template <class T>
    template <class V>
    class beta<T>::hold
    {
    private:
    V val;

    public:
    hold(V v = 0) : val(v) {}
    void show() const { cout << val << endl; }
    V Value() const { return val; }
    };

    // member definition
    template <class T>
    template <class U>
    U beta<T>::blab(U u, T t)
    {
    return (n.Value() + q.Value()) * u / t;
    }

    因为模板是嵌套的,所以必须使用下面的语法:

    1
    2
    template <class T>
    template <class V>

    不能用:

    1
    template <class T, class V>

    还必须使用作用域解析运算符::指出holdblabbeta<T>类的成员。这里虽然hold类声明在beta<T>类的私有部分,但是依然可以在模板外面进行定义。

    ==将模板用作参数==:模板还可以包含本身就是模板的参数,例如:

    1
    2
    3
    4
    5
    template <template <typename T> class Things>
    class Foo
    {
    Things<int> bar;
    };

    模板参数是template <typename T> class Things,其中template <typename T> class是类型,Things是参数。使用Foo声明类对象时,模板参数必须是一个模板类:

    1
    2
    3
    4
    Foo<Stack> foo;  // Stack must be a template class

    template <typename T>
    class Stack {...};

    这样,Things<int>将会被实例化为Stack<int>。这里,Things被硬编码为int类型。

    可以混合使用模板参数和常规参数,例如:

    1
    2
    3
    4
    5
    6
    template <template <typename T> class Things, typename U, typename V>
    class Foo
    {
    Things<U> bar;
    Things<V> foo;
    };

    这样,成员barfoo可存储的数据类型为泛型,而不是像之前的硬编码。

    ==模板类和友元==:

    1. 模板类的非模板友元函数

      在模板类中将一个常规函数声明为友元:

      1
      2
      3
      4
      5
      6
      template <class T>
      class HasFriend
      {
      public:
      friend void counts(); // friend to **all** HasFriend instantiations
      }

      友元函数可以像类成员函数一样访问类的数据成员,假设要为友元函数提供模板类参数,可以用如下方式进行友元声明吗?

      1
      friend void report(HasFriend &);  // possible?

      答案是不可以。原因是不存在HasFriend这样的对象,而只有特定的具体化,如HasFriend<Short>。要提供模板类参数,必须指明具体化。例如:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      template <class T>
      class HasFriend
      {
      private:
      static int ct;
      public:
      friend void report(HasFriend<T> &); // bound template friend
      }

      // each specialization has its own static data member
      template <typename T>
      int HasFriend<T>::ct = 0;

      注意,report()函数本身不是模板函数,而只是使用一个模板作参数。因此必须为要使用的友元定义显式具体化:

      1
      2
      void report(HasFriend<short> &) {...};  // explicit specialization for short
      void report(HasFriend<int> &) {...}; // explicit specialization for int
    2. 约束模板友元,即友元的类型取决于类被实例化时的类型

      首先,在类的定义前面声明每个模板函数:

      1
      2
      template <typename T> void counts();
      template <typename T> void reports(T &);

      然后,在类中再次将模板声明为友元:

      1
      2
      3
      4
      5
      6
      template <typename TT>
      class HasFriendT
      {
      friend void counts<TT>();
      friend void reports<>(HasFriendT<TT> &);
      };

      上述声明中的<>指出这是模板的具体化,对于reports<>可以为空,因为可以从函数参数推断出模板类型HasFriendT<TT>,当然,也可以显式指出friend void reports<HasFriendT<TT>>(HasFriendT<TT> &)

      counts()函数没有参数,因此必须使用模板参数语法<TT>来指明其具体化。

      泛型名TTT只是占位符,因此也可以同名。

      接着定义友元函数:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      // template friend functions definitions
      template <typename T>
      void counts()
      {
      }

      template <typename T>
      void reports(T &hf)
      {
      }

      调用友元函数:

      1
      2
      count<int>();  // Q:此时实例化HasFriendT<int>了吗?A:实例化了。
      reports(obj);

      程序将包含多个count()函数,分别对于不同实例化的类类型的友元。同时,count()函数调用没有可被编译器用来推断出所需具体化的函数参数,所以这些调用使用语法cout<int>指明具体化,但是对于reports()调用,编译器可以从参数类型推断出要使用的具体化。

    3. 非约束模板友元,即友元的所有具体化都是类的每一个具体化的友元

      通过在类内声明模板,可以创建非约束友元函数,即每个函数具体化都是每个类具体化的友元。对于非约束友元,友元模板类型参数与模板类类型参数是不同的:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      template <typename T>
      class ManyFriend
      {
      template <typename C, typename D> friend void show (C &, D &);
      };

      template <typename C, typename D>
      void show (C &, D &)
      {
      }

      这种形式的友元可以与任何类型匹配,包括int等内置参数。

    ==模板别名==(C++ 11):可以使用typedef为模板具体化指定别名:

    1
    2
    typedef Array<double, 10> ad;
    typedef Array<int, 10> ai;

    有多少个就得写多少次,然而非模板参数10是固定的。

    C++ 11新增了使用模板提供一系列别名的功能:

    1
    2
    template <typename T>
    using arrtype = std::array<T, 12>; // template to create multiple aliases

    arrtype为模板别名,可使用它来指定类型:

    1
    arrtype<int> ai;

    C++ 11允许将语法using =用于非模板,此时与使用typedef等价:

    1
    2
    3
    4
    typedef const char * pc1;
    using pc2 = const char *;
    typedef const int *(*pa1)[10];
    using pa2 = const int *(*)[10];

32. 类和动态内存分配

如果派生类不使用new,则不需要定义析构函数、复制构造函数和赋值构造函数。对于派生类中的新成员,默认复制或赋值构造函数都可以处理,对于继承得到的基类对象,C++成员复制将根据数据类型采用相应的复制方式,复制类成员或继承的类组件时,使用该类的复制构造函数完成,所以派生类的默认构造函数将使用基类的复制构造函数复制基类对象,这对于赋值也是同理,因此可以不用自行定义。

如果派生类使用了new,则必须为派生类定义显式析构函数、复制构造函数和重载赋值运算符。派生类析构函数自动调用基类的析构函数,故其自身的职责是对派生类构造函数执行的工作进行清理(简单地说,就是各管各的,构造函数增量式更新,析构函数对应撤销更新)。对于复制构造函数,派生类不能访问基类数据,因此必须调用基类复制构造函数来处理共享的基类数据:

1
2
3
4
5
// 必须显式定义复制构造函数
Derived::Derived(const Derived & rd) : Base(rd)
{
// initialize new members
}

在上述代码中,成员初始化列表将一个Derived引用传递给Base构造函数,这是因为Base有一个复制构造函数,而基类引用可以指向派生类,所以没有问题。

对于赋值运算符函数,需要显式调用基类的赋值运算符函数:

1
2
3
4
5
6
7
8
// 必须显式定义赋值运算符函数
Derived & Derived::operator=(const Derived & rd)
{
if (this == &rd)
return *this;
Base::operator=(rd); // copy base portion
// copy new members
}

小结:当基类和派生类都采用动态内存分配时,派生类的析构函数、复制构造函数、赋值运算符都必须使用相应的基类方法来处理基类元素。这种要求是通过三种不同的方式来满足的。对于析构函数,这是自动完成的;对于构造函数,这时通过在初始化成员列表中调用基类的复制构造函数来完成的;如果不这样做,将自动调用基类的默认构造函数。对于赋值运算符,这是通过使用作用域解析运算符显式地调用基类的赋值运算符来完成的。

函数传参、返回值与对象拷贝:

  1. 无论是哪种编译器,当按值传递和返回对象时,都将调用复制构造函数。
  2. 由于按值传递对象将调用复制构造函数,因此应该按引用传递对象。这样可以节省调用构造函数的时间以及存储新对象的空间。
  3. 如果成员本身就是类对象,则将使用这个类的复制构造函数来复制成员对象。
  4. 对象初始化总会调用复制构造函数,而使用=运算符也可能调用赋值运算符函数。

33. 友元、异常和其他

友元类:可以将类作为友元,友元类的的所有方法都可以访问原始类的私有成员和保护成员。

友元类声明:

1
friend class FriendClass;

友元声明可以位于公有、私有或保护部分,其所在的位置无关紧要。

友元成员函数:选择特定的类成员函数成为另一个类的友元。

友元成员函数声明:

1
2
3
4
5
6
class MyClass;  // forward declaration

class Myclass
{
friend void FriendClass:memberFunc();
};

为了让编译器识别FriendClass是一个类,需要使用前向声明,排列顺序如下:

1
2
3
4
class MyClass;

class FriendClass {...};
class MyClass {,..};

被声明为友元的类和方法必须先声明。

如果作为友元的类中的内联方法调用了MyClass中的方法,此时编译器必须看到MyClass类的声明,这样才能知道MyClass类有哪些方法。可以只在FriendClass类中只作声明,然后把实际的的定义放在MyClass类之后。

1
2
3
4
5
6
class MyClass;

class FriendClass {...};
class MyClass {,..};

inline void FriendClass show() const {} // can be inline method

让整个类成为友元不需要前向声明,因为友元语句本身已经指出FriendClass是一个类了:

1
friend class FriendClass;

互为友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyClass
{
friend class FriendClass;
public:
void display(FriendClass & obj);
};

class FriendClass
{
friend class MyClass;
public:
void show(MyClass obj) {obj.show();}
};

inline void MyClass:display(FriendClass & obj) {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#ifndef __MUTUAL_FRIEND_H
#define __MUTUAL_FRIEND_H

#include <iostream>

class MyClass
{
private:
int cnt = 0;
friend class FriendClass;

public:
void show(FriendClass &obj);
};

class FriendClass
{
private:
int cnt = 0;
friend class MyClass;

public:
void show(MyClass &obj)
{
std::cout << "call MyClass::show" << std::endl;
obj.show(*this);
}
};

inline void MyClass::show(FriendClass &obj)
{
if (cnt++ <= 10)
{
std::cout << "call FriendClass::show" << std::endl;
obj.show(*this);
}
else
{
std::cout << "Bye~~" << std::endl;
}
}

#endif

共同友元

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#ifndef COMMON_FRIEND_H
#define COMMON_FRIEND_H

class GPU;

class CPU
{
private:
int data;

public:
friend void sync(CPU &, const GPU &);
friend void sync(GPU &, const CPU &);
};

class GPU
{
private:
int data;

public:
friend void sync(CPU &, const GPU &);
friend void sync(GPU &, const CPU &);
};

inline void sync(CPU &cpu, const GPU &gpu)
{
cpu.data = gpu.data;
}

inline void sync(GPU &gpu, const CPU &cpu)
{
gpu.data = cpu.data;
}

#endif

嵌套类:可以将类声明放在另一个类中,仅当声明位于公有部分时,才能在包含类的外面使用嵌套类,且必须使用作用域解析运算符。嵌套类不是类对象包含,它不创建类成员,而是定义了一种类型,该类型仅在包含嵌套类声明的类中有效。

对类进行嵌套通常是为了帮助实现另一个类,并避免名称冲突。

1
2
3
4
5
6
7
8
9
10
11
12
class OuterClass
{
public:
class InnerClass
{
InnerClass() {};
void show();
}
};

// InnerClass method definition
void OuterClass::InnerClass::show() {}

==嵌套类的作用域==:如果嵌套类A是在另一个类B的私有部分声明的,那么从类B派生出来的类C中,A也是不可见的,因为派生类不能直接访问基类的私有部分。如果嵌套类A是在另一个类B的保护部分声明的,那么从类B派生出来的类C中,A也是可见的。

==模板中的嵌套类==:嵌套类可以使用模板参数类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template <class Item>
class QueueTP
{
private:
enum
{
Q_SIZE = 10
};
// Node is a nested class definition
class Node
{
public:
Item item; // use Item type
Node *next;
Node(const Item &i) : item(i), nxet(0) {}
};
};

Node类不是模板类,但是使用了模板参数Item

假如同时有QueueTP<double> dqQueueTP<char> cq两个声明,则这两个Node类将在两个独立的QueueTP类中定义,因此不会发生名称冲突,即一个节点的类型为QueueTP<double>::Node,另一个节点的类型为QueueTP<char>::Node

异常处理

  1. 引发异常:

    语法:throw typetype可以是任意C++类型,通常是类类型,用于指出异常的特征。

    1
    throw "ValueError"
  2. 使用异常处理程序捕获异常:

    处理程序以关键字catch开头,随后是位于括号中的类型说明,它指出了异常处理程序要响应的异常类型,然后是用一个花括号括起的代码块,指出要采取的措施。catch关键字和异常类型用作标签,指出当异常被引发时,程序应调到这个位置执行。异常处理程序也被称为catch块。程序将寻找与引发的异常类型匹配的异常处理程序(位于try块后面)。

    1
    2
    3
    4
    5
    // similar to function definition
    catch (const char * s) // start of exception handler
    {

    }
  3. 使用try块:

    try块标识其中特定的异常可能被激活的代码块,它后面跟一个或多个catch块。

    1
    2
    3
    try {  // start of try block

    }

    如果函数引发了异常,而没有try块或者匹配的处理程序时,程序最终将调用abort()函数。

异常处理示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void func()
{
throw obj;
}

try {

}
catch (ClassName & obj)
{
// how C++ manages obj?
throw; // rethrows the exception
}

==异常规范==:告诉用户可能需要使用try块,或让编译器添加执行运行阶段检查的代码,检查是否违反了异常规范。

1
2
double func() noexcept;  // func doesn't throw an exception
double func() throw () {} // same as above

==栈解退==:函数由于出现异常而终止,程序将释放栈中的内存,直到找到try块中的返回地址。和函数返回一样,对于栈中的自动类对象,类的析构函数将被调用。

疑问点:如果catch块使用了对象引用,程序不会再回到调用函数里面,而异常类型对象是在调用函数里面创建的,这时候该对象在内存中是如何管理的?

答:引发异常时编译器总是创建一个临时拷贝,即使异常规范和catch块中指定的是引用。

==提示==:如果有一个异常类继承层次结构,应该这样排列catch块:将捕获位于层次结构最下面的异常类的catch语句放在最前面,将捕获基类异常的catch语句放在最后面。

1
2
3
4
5
6
7
8
9
10
class BaseException {};
class ZeroDivisionError : public BaseException {};
class FileNotFoundError : public BaseException {};

try {

}
catch (FileNotFoundError & err) {}
catch (ZeroDivisionError & err) {}
catch (BaseException & err) {}

==提示==:即使不知道异常的类型,也可以通过使用省略号来任何捕获异常:

1
catch (...) {}  // catches any type exception

如果仅知道部分会引发的异常,可以将省略号放至catch块的最后面,有点类似switch语句汇总的default

1
2
3
4
5
6
try {

}
catch {FileNotFoundError & err} {}
catch {ZeroDivisionError & err} {}
catch {...} {} // catch whatever is left

==空指针和new==:很多代码都是在new失败时返回空指针时编写的,为了处理new使用异常的变化,有些编译器提供了一个标记(开关),让用户选择所需的行为,当前,C++标准提供了一种在失败时返回空指针的new

1
int * pi = new (std::nothrow) int;

==注意==:

  1. 如果异常是在带异常规范的函数中引发的,则必须与规范列表中的某种异常匹配(在继承层次结构中,类类型与这个类及其派生类的对象匹配),否则称为意外异常。在默认情况下,这会导致程序异常终止。

  2. 如果异常没有被捕获(在没有try块或没有匹配的catch块时),该异常被称为未捕获异常。在默认情况下,这会导致程序异常终止。未捕获异常不会导致程序立刻异常终止,程序将首先调用函数terminate()。在默认的情况下,terminate()函数调用abort()函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <exception>

    using namespace std;

    void myQuit()
    {
    cout << "Terminating due to uncaught exception\n";
    exit(-1);
    }

    int main()
    {
    set_terminate(myQuit);

    return 0;
    }
  3. 异常规范不适用于模板,因为模板函数引发的异常可能随特定的具体化而异。

RTIIRTII是运行阶段类型识别(Runtime Type Identification)的简称。

==RTII的作用==:从基类指针中获取对象的类型。在公有派生中,如果使用了虚函数,则可以使用基类指针调用派生类对象的虚方法,如果派生类对象有自定义的方法,则无法通过基类指针进行调用。

C++有3个支持RTII的元素:

  1. 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针,否则,该运算符返回0——空指针。
  2. typeid运算符返回一个指出对象的类型的值。
  3. type_info结构存储了有关特定类型的信息。

只能将RTII用于包含虚函数的类层次结构,原因在于只有对这种类层次结构,才应该将派生对象的地址赋给基类指针。

==dynamic_cast语法==:

1
dynamic_cast <type-name> (expression)

如果不能安全执行转换,则结果为空指针。

1
2
3
4
5
6
/*
赋值表达式的值是它左边的值,因此`if`条件的值为pt,如果类型转换成功,则pt的值为非零,如果类型转换失败,则pt的值将为0
*/
if (pt = dynamic_cast<Foo *>(p))
{
}

也可以将dynamic_cast用于引用,但由于没有与空指针对应的引用值用于表示转换失败,dynamic_cast使用bad_cast异常来指出转换失败,该异常类型是从exception类派生出来的,在typeinfo头文件中定义:

1
2
3
4
5
6
7
8
#include <typeinfo>

try {
Foo & foo = dynamic_cast<Foo &>(bar);
}
catch(std::bad_cast &) {

}

==typeid运算符和type_info类==:

typeid运算符功能类似于Python语言中的type,可以接受两种参数:

  1. 类名
  2. 结果为对象的表达式

typeid运算符返回一个对type_info对象的引用,其中type_info是在头文件typeinfo中定义的一个类,它重载了==!=运算符,以便可以使用这些运算符来对类型进行比较。

1
typeid(Foo) == typeid(*foo)

如果*foo指向Foo对象,则上述表达式的值为true。如果foo是一个空指针,程序将引发bad_typeid异常,该异常同样是在typeinfo头文件中声明的。

type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串:通常(但不一定)是类名。

类型转换运算符

==const_cast==:const_cast运算符用于执行只有一种用途的类型转换,即改变值为constvolatile,语法规则为:

1
const_cast<type-name>(expression)

其中,type-nameexpression的类型必须相同。

提供该运算符的原因是,有时候可能需要这样一个值,它在大多数时候是常量,而有时又是可以修改的。在这种情况下,可以将这个值声明为const,并在需要修改它的时候,使用const_cast

const_cast可以修改指向一个值的指针,但修改const值的结果是不确定的,如:

1
2
3
4
5
6
7
8
9
10
int foo = 100;
const int bar = 200;

const int * pt = &foo;
int * pc = const_cast<int *>(pt);
*pc = 20; // Ok

const int * pt = &bar;
int * pc = const_cast<int *>(pt);
*pc = 20; // has no effect

==static_cast==:仅当type-name可被隐式转换为expression所属的类型或expression可被隐式转换为type_name所属的类型时,转换才是合法的,否则将出错。可用于类继承层次中的向上或向下转换,但是不能用于不相关类型的互相转换:

1
static_cast<type-name>(expression)

34. string类和标准模板库

string类

  1. string类实际上是模板具体化basic_string<char>的一个typedef,同时省略了内存管理相关的参数。

  2. string类的一个带模板参数的构造函数:

    1
    2
    template<class Iter>
    string(Iter begin, Iter end)

    string对象初始化为区间[begin, end)内的字符,其中beginend的行为就像指针,用于指定位置,它们指向内存中的两个位置。

智能指针模板类

三个智能指针模板(auto_ptrunique_ptrshared_ptr)都定义了类似指针的对象,可以将new获得的地址赋给这种对象,当智能指针过期时,其析构函数将使用delete来释放内存。

要创建智能指针对象,必须包含头文件memory。创建智能指针对象的语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <memory>

std::auto_ptr<double> pd(new double);
std::unique_ptr<double> pd(new double);
std::shared_ptr<double> pd(new double);


std::unique_ptr<ClassName> pt(new ClassName);
pt->show(); // 对象的方法是如何获取到的呢?


// 数组类型,智能指针将使用delete []
std::unique_ptr<double []> pda (new double [5]);
std::shared_ptr<double []> pda (new double [5]);

假如pt是智能指针对象,则可以对它执行解除引用操作(*pt)、用它来访问结构成员(pt->show)、将它赋给指向同类型的常规指针。

==注意==:避免将智能指针用于非堆内存。

==赋值运算==:

  1. auto_ptrunique_ptr使用所有权管理对象,对于特定的对象,只能有一个智能指针可拥有它,拥有所有权的智能指针的析构函数会删除该对象。赋值操作转让所有权,对于auto_ptr,右值将变成空指针,而对于unique_ptr,如果右值不是临时对象,会产生编译错误:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    #include <iostream>
    #include <string>
    #include <memory>

    std::unique_ptr<std::string> func()
    {
    std::unique_ptr<std::string> pt(new std::string("Hello World"));
    return pt;
    }

    int main()
    {
    using namespace std;

    unique_ptr<string> pu1(new string("Hello"));
    unique_ptr<string> pu2;
    pu2 = pu1; // not allowed

    unique_ptr<string> pu3;
    pu3 = unique_ptr<string>(new string("World")); // allowed

    pu2 = func(); // allowd

    return 0;
    }
  2. shared_ptr使用对象引用计数法。shared_ptr更接近于常规指针。模板shared_ptr包含一个显式构造函数,可用于将右值unique_ptr转换为shared_ptrshared_ptr将接管原来归unique_ptr所有的对象。当unique_ptr是临时右值时,可以将一个unique_ptr赋给shared_ptr

    1
    shared_ptr<string> pt = unique_ptr<string>(new string("Hello"));
  3. 智能指针选择:一般来说,如果程序需要多个指向同一个对象的指针,可以使用shared_ptr,否则使用unique_ptr

标准模板库:STL提供了一组表示容器、迭代器、函数对象和算法的模板。

容器是一个与数组类似的单元,可以存储若干个值。STL容器是同质的,即存储的值的类型相同;迭代器能够用来遍历容器的对象,是广义指针;函数对象是类似于函数的对象,可以是类对象或函数指针(包括函数名)。

  1. 分配器:各种STL容器模板都可接收一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存。如果省略该模板参数值,则容器模板将默认使用allocator<T>类,这个类使用newdelete

  2. 迭代器:迭代器是一个广义指针。事实上,它可以是指针,也可以是一个可对其执行类似指针的操作(如解除引用和递增)的对象。每个容器类都定义了一个合适的迭代器,该迭代器的类型是一个名为iteratortypedef,其作用域为整个类。

    1
    2
    3
    4
    5
    6
    7
    8
    vector<double>::iterator iter;  // iter: an iterator

    vector<double> scores;
    iter = scores.begin(); // have iter point to the first elememt
    *iter = 100;
    ++iter; // make iter point to the next element

    auto iter = scores.begin(); // C++ 11 automatic type deduction

    end()成员函数标识超过结尾的位置,即指向容器最后一个元素后面的那个元素。

    ==注意==:区间[it1, it2)由迭代器it1it2指定,其范围为it1it2(不包含it2)。

    STL定义了5种迭代器,并根据所需的迭代器类型对算法进行了描述。这5种迭代器分别是输入迭代器、输出迭代器、正向迭代器、双向迭代器和随机访问迭代器。

    ==输入迭代器==:

    输入迭代器可被程序用来读取容器中的信息,对输入迭代器解除引用将使程序能够读取容器中的值,但不一定能让程序修改值。因此,需要输入迭代器的算法将不会修改容器中的值。基于输入迭代器的任何算法都应当是单通行的,不依赖于前一次遍历时的迭代器值,也不依赖本次遍历中前面的迭代器值。输入迭代器是单向迭代器,可以递增,但不能倒退。

    ==输出迭代器==:

    程序的输出就是容器的输入。

    对于单通行、只读算法,可以使用输入迭代器,对于单通行、只写算法,可以使用输出迭代器。

    ==正向迭代器==:

    可读写,适用于多次通行算法,每次遍历的顺序都相同,迭代器递增后,仍然可以对前面的迭代器值解除引用,并得到相同的值。

    ==双向迭代器==:

    与正向迭代器类似,但支持递增、递减两种操作。

    ==随机访问迭代器==:

    支持随机访问。

    STL迭代器描述了一系列要求,称为概念,概念的具体实现被称为模型。模型可以使用任何符合要求的实现。因此STL算法可以使用指针来对基于指针的非STL容器进行操作。

    常规指针可以作为迭代器,因此STL算法可以使用指针来对基于指针的非STL容器进行操作。例如,可以将STL算法用于数组。

  3. 算法组:算法通常作为非成员函数,但对于特定的类,可能存在特定算法,其效率比通用算法高。常用的STL函数有:for_each()sort等。使用类方法可以自动调用类的内存管理工具,在需要的时候调整容器的长度,而通用算法不能调用容器类方法,可能需要搭配其他的类方法进行使用,如remove函数和list<int>::erase方法搭配使用等价于使用list<int>::remove方法。

    for_each()函数类似于Python中的map函数,其用法为:

    1
    2
    for_each(vec.begin(), vec.end(), func);
    // func函数接受引用参数时,可以修改容器的内容

    也可以用基于范围的for循环:

    1
    2
    for (auto x : vec) func(x);
    for (auto & x : vec) func(x); // 可修改容器的内容

    sort()函数要求容器支持随机访问,该函数有两个版本:

    • 接受两个定义区间的迭代器参数,并使用为存储在容器中的类型定义的<运算符,对区间中的元素进行操作:

      1
      sort(vec.begin(), vec.end());

      如果是自定义的对象,必须定义operator<()函数。

    • 接收三个参数,前两个参数也是指定区间的迭代器,最后一个参数是指想要使用的函数的指针(函数对象):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      bool func(Ele & e1, Ele & e2)
      {
      if (e1 < e2)
      return true;
      else
      return false;
      }

      sort(vec.begin(), vec.end(), func);

      在全排序中,如果a < bb < a都不成立,则ab必定相同。在完整弱排序中,它们可能相同,也可能只是在某方面相同。

    copy()可以将数据从一个容器复制到另一个容器中。需要保证目标容器有足够的容量进行复制。可以使用ostream_iterator模板将信息复制到显示器上:

    1
    2
    3
    #include <iterator>
    ostream_iterator<int, char> out_iter(cout, " ");
    copy(arr.begin(), arr.end(), out_iter);

    也可以使用istream_iterator模板接收输入信息:

    1
    copy(istream_iterator<int, char>(cin), istream_iterator<int, char>(), vect.begin());  // 可以使用匿名对象

    省略构造函数参数表示输入失败,因此上述代码从输入流中读取,直到文件结尾、类型不匹配或出现其他输入故障为止。

    使用插入迭代器back_insert_iteratorfront_insert_iteratorinsert_iterator,可以自动扩容目标容器。即可以用insert_iterator将复制数据的算法转换为插入数据的算法。

    这些迭代器将容器类型作为模板参数,将实际的容器标识符作为构造函数参数:

    1
    back_insert_iterator<vector<int>> back_iter(dice);

    STL将算法库分为4组:

    • 非修改式序列操作;
    • 修改式序列操作;
    • 排序和相关操作;
    • 通用数字运算。

    前3组在头文件algorithm中描述,第4组在numeric中描述。

    有些算法有两个版本:in-place版本和copy版本,STL的规定是,复制版本的名称将以_copy结尾。

  4. 泛型编程

    面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。泛型编程旨在使用同一个函数来处理数组、链表或任何其他容器类型,即函数不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构。模板提供了存储在容器中的数据类型的通用表示,因此还需要遍历容器中的值的通用表示,迭代器正是这样的通用表示。

  5. 容器

    ==容器概念==:

    存储在容器中的数据为容器所有,这意味着当容器过期时,存储在容器中的数据也将过期。

    ==关联容器==:

    键值对容器,使用键来查找值。STL提供了4中关联容器:setmultisetmapmultimap

    最简单的关联容器是set,其值类型与键相同,对于set来说,值就是键。multiset类似于set,只是可能有多个值的键相同。

  6. 函数对象

    函数对象,也叫函数符。函数符是可以以函数方式与()结合使用的任意对象,包括函数名,指向函数的指针和重载了()运算符的类对象(operator()())。不接受参数的函数对象称为生成器。

    STL通过使用模板解决了函数符传参问题:

    1
    2
    template<class InputIterator, class Function>
    Function for_each(InputIterator first, InputIterator last, Function f);

    注意,函数指针不能解决该问题。

    ==自适应函数符==:携带了标识参数类型和返回类型的typedef成员,如result_typefirst_argument_typesecond_argument_type。接受一个自适应函数符参数的函数可以使用result_type成员来声明一个与函数的返回类型匹配的变量。

    ==函数适配器==:用于改变函数的输入参数个数。STL使用binder1stbinder2nd类自动完成这一过程,将自适应二元函数转换为自适应一元函数:

    1
    2
    #include <functional>
    binder1st(func, val) uni_func;

    也可以使用bind1st()bind2nd()函数:

    1
    bind1st(multiplies<double>(), 2.5);  // 声明了一个匿名multiplies<double>函数对象

    类似于Python中的partial

    C++11已经抛弃了binder1stbinder2nd,可以用bind进行替代。

  7. 其他库

    ==vector、valarray和array==:valarray类是面向数值计算的,有点类似于numpyvalarray没有提供begin()end()迭代器,也不能使用&ar[0]的方式给算法提供参数,C++提供了接受valarray对象作为参数的模板函数begin()end()

    1
    sort(begin(ar), end(ar));
  8. 模板initializer_list

    在定义接受initializer_list参数的构造函数之后,可以使用初始化列表语法将STL容器初始化为一系列值(此时初始化列表语法就只能用于该构造函数):

    1
    std::vector<double> payments {1.0, 2.0, 3.0};

    要在代码中使用initializer_list对象,必须包含头文件initializer_list,这个模板类包含成员函数begin()end()

    1
    2
    3
    4
    5
    6
    7
    double sum(std::initializer_list<double> il)
    {
    double tot = 0;
    for (auto p = il.begin(); p != il.end(); p++)
    tot += *p;
    return tot;
    }

    除了用于构造函数为,还可以将initialize_list用于常规函数的参数。

35 IO

  1. setf(fmtflags, fmtflags)的函数重载中,第一个参数用于设置位,第二个参数用于清除位,可以通过查表获得可用参数值。

  2. unsetf(fmtflags)可以撤销修改。

  3. iomanip头文件提供了设置格式的便捷工具。

  4. 命令行处理:

    1
    int main(int argc, char * argv[])
  5. 二进制读写文件

    1
    2
    fout.write((char *) &structure, sizeof structure);
    fin.read((char *)&structure, sizeof structure);

    适用于不使用虚函数的类,此时类的数据成员被保存,而方法不会被保存。如果类有虚方法,则也将复制隐藏指针(指向虚函数的指针表)。

  6. seekg()用于输入指针,seekp()用于输出指针。fstream对象同步管理读写指针,而如果分开使用ifstreamofstream对象管理同一个文件,则读写指针独立。

  7. 临时文件:

    1
    2
    3
    4
    5
    #include <cstdio>

    char tmp[L_tmpname] = {'\0'};

    tmpnam(tmp);

    tmpname函数创建了一个临时文件名,L_tmpnamTMP_MAX限制了文件名包含的字符数以及在确保当前目录中不生成重复文件名的情况下函数可被调用的最多次数。

  8. sstream提供程序与string对象之间的I/O。读取string对象中的格式化信息或将格式化信息写入string对象中被称为内核格式化。在需要的情况下,ostringstream对象将使用动态内存分配来增大缓冲区。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <string>
    #include <iostream>
    #include <sstream>

    int main()
    {
    using namespace std;
    ostringstream outstr;
    outstr << "some message goes into the string";
    string result = outstr.str(); // freeze the object
    istringstream instr (result);
    instr >> obj;
    return 0;
    }

36 C++11

  1. 右值引用:将右值关联到右值引用将导致该右值被存储到特定的位置,且可以获取该位置的地址。引入右值引用的主要目的之一是实现移动语义。此时右值的生命周期与右值引用一样长。

  2. 移动语义:临时对象(如函数返回值)被删除时,使用所有权转让的方式而不是深度拷贝的方式给新变量赋值。复制构造函数可执行深度拷贝,而移动构造函数只调整记录。移动构造函数的逻辑还是需要自己进行实现的,如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    String::String(String && s) : size(s.size)  // 由于修改了s对象,不能添加const关键字
    {
    pt = s.pt;
    s.size = 0;
    s.pt = nullptr;
    }

    String String::operator+(const String & s)
    {
    String temp;
    // construct temp from *this and s
    return temp;
    }

    String s(s1 + s2); // calls move constructor
    // Note: 程序可能不调用移动构造函数,编译器可能会进行优化,直接完成移动构造函数所做的工作

    Q:多层级动态内存分配的数据结构如何保证正确地修改记录呢?A:将调用各成员的移动构造函数

  3. 移动构造函数解析:

    第一步:根据值的类型(左值或者右值),调用复制构造函数或者移动构造函数。

    第二步:编写移动构造函数。

  4. 移动赋值运算符函数:

    1
    2
    3
    4
    5
    6
    String & String::operator=(String && s)
    {
    pt = s.pt;
    s.size = 0;
    s.pt = nullptr;
    }
  5. 强制使用移动(赋值)构造函数:左值转右值:

    1
    2
    3
    4
    5
    // 选择一
    static_cast<String &&> str;
    // 选择二
    #include <utility>
    str2 = std::move(str1)
  6. 默认的方法和禁用的方法:

    如果提供了某些构造函数(如移动构造函数),编译器不会自动创建默认的构造函数、复制构造函数和赋值构造函数。在这些情况下,可以使用关键字default显式地声明这些方法的默认版本:

    1
    2
    3
    4
    5
    6
    class Someclass
    {
    public:
    Someclass() = default; // use compiler-generated default constructor
    Someclass(Someclass &&);
    };

    关键字delete可用于禁止编译器使用特定方法,例如,要禁止复制对象,可禁用复制构造函数和赋值运算符函数:

    1
    2
    3
    4
    5
    6
    class Someclass
    {
    public:
    Someclass(const Someclass &) = delete;
    Someclass & operator=(const Someclass &) = delete;
    };

    关键字default只能用于6个特殊成员函数,但delete可用于任何成员函数。delete的一种可能用法是禁止特定的转换:

    1
    2
    3
    4
    5
    6
    class Someclass
    {
    public:
    void redo(int) = delete;
    void redo(double) = delete;
    };

    在这种情况下,obj.redo(5)redo(int)匹配,编译器检测到这一点以及redo(int)被禁用后,将这种调用视为编译错误。

  7. 继承基类构造函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class Base
    {
    private:
    int a, b,c;
    public:
    Base();
    Base(int);
    Base(double);
    };

    class Derived : public Base
    {
    private:
    int d;
    public:
    using Base::Base; // C++ 11将using用于构造函数,让派生类继承基类的所有构造函数(默认构造函数、复制构造函数和移动构造函数除外),但不会使用派生类构造函数的特征标匹配的构造函数
    Derived();
    };

    如果没有匹配的派生类构造函数,将调用基类与参数匹配的构造函数,但是派生类的数据成员不会被初始化。

  8. overridefinal

    在继承层次结构中,如果派生类重新定义方法(不管特征标是否相同),则新方法将隐藏旧所有基类方法,导致派生类对象不可调用基类方法。

    虚说明符override指出要覆盖一个虚函数,放置于参数列表后面,如果声明与基类方法不匹配,编译器将视为错误(常用于检测typo):

    1
    virtual void show(char * ch) const override { }

    说明符final用于禁止派生类覆盖特定的虚方法,放置于参数列表后面:

    1
    virtual void show(char * ch) const final { }
  9. Lambda函数:C++不允许嵌套定义函数,函数可以在函数中定义Lambda表达式。

    1
    [](type var) {return expression;}

    仅当lambda表达式完全由一条返回语句组成时,自动类型推断才管用,否则需要使用新增的返回类型后置语法:

    1
    [](type var)->type(statements; return expression;)

    但是可以使用三目运算符:

    1
    [](int x) {return x > 0 ? 1 : 3.14;}

    在多次调用Lambda函数的时候,可以给lambda指定一个名称:

    1
    auto f = [](type var) {return expression;}

    lambda函数可访问作用域内的任何动态变量,要捕获要使用的变量,可将其名称放在中括号内,如果只指定了变量名,将按值访问变量,如果在名称前加上&,将按引用访问变量。[&]能够按照引用访问所有动态变量,而[=]能够按值访问所有动态变量。还可以混合使用这两种方式,如[val1, &val2]能够按值访问val1以及按引用访问val2[&, val]可以按值访问val以及按引用访问其他所有静态变量。[=, &val]可以按引用访问val以及按值访问其他所有动态变量。

  10. 包装器:

    由于函数对象的多样性(函数指针,函数对象,Lambda表达式),将函数对象传递给模板函数时,不同的函数对象会实例化不同的模板函数,导致模板效率低下。使用包装器可以减少实例化次数。

    模板function是正在头文件functional中声明的,它从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针、函数对象或lambda表达式:

    1
    std::function<double(char, int)> fdci = func;  // 接收一个`char`参数和一个`int`参数并返回`double`值的`function`对象

    调用特征标:是由返回类型以及用括号括起并用逗号分隔的参数类型列表定义。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    template <typename T> class function;  // 不能没有主模板,只有偏特化
    template<typename R, typename Arg> // 偏特化
    class function<R(Arg)>
    {
    public:
    R operator()(arg)
    {
    return ...;
    }
    };

    使用包装器,本质上还是会用到多个实例化,但是如果包装器类比接收函数对象的模板函数轻量,还是更加有优势的。

    也可以在模板函数的定义中,用包装器替换模板参数:

    1
    2
    3
    4
    5
    6
    7
    template <typename T>
    T func(T a, std::function<T(T)> f)
    {

    }

    func<double> (3.0, f);

    由于f并不是std::function<T(T)>类型,所以需要显式地指定模板的类型,另一种方法,我觉得可以使用偏特化。

  11. 可变参数模板

    ==模板参数列表和函数参数列表==:

    1
    2
    3
    4
    5
    template <typename T>  // template parameter list
    void func(T t) // function parameter list
    {

    }

    C++ 11提供了一个用省略号表示的元运算符(meta-operator),能够声明表示模板参数包的标识符。模板参数包是一个类型列表,同样地,它可以用于声明函数参数包的标识符,而函数参数包是一个值列表。

    1
    2
    3
    4
    5
    template<typename... Args>  // Args is a template parameter pack
    void func(Args... args) // args is a function parameter pack
    {

    }

    ==展开参数包==:

    可将省略号放在函数参数包的右边,将参数包展开:

    1
    2
    3
    4
    5
    template<typename... Args>  // Args is a template parameter pack
    void func(Args... args) // args is a function parameter pack
    {
    show(args...); // passes unpacked args to show()
    }

    在可变模板中,可指定展开模式:

    1
    2
    3
    func(Args... args)  // pass by value
    func(Args&... args) // pass by reference
    func(Args&... args) // pass by const reference

    ==使用递归处理可变参数==:

    核心理念:将函数参数包展开,对列表中的第一项进行处理,再将余下的内容传递给递归调用,直到列表为空。

    1
    2
    3
    4
    5
    6
    7
    8
    void func() {}  // terminating call

    template<typename T, typename... Args> // Args is a template parameter pack
    void func(T, value, Args... args) // args is a function parameter pack
    {
    // do something with value
    func(args...); // passes unpacked args to func()
    }