0%

C++ 语法总结

C++ 语法速览

【C++】知识点汇总(上)

【C++】知识点汇总(下)

C++入门基础知识总结

C++速查手册:基本语法与函数使用

C++ 代码基本结构

不包含类的 C++ 代码基本结构

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

int main() {
cout << "Hello World!" << endl;
return 0;
}

包含类的 C++ 代码基本结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

class A {
public:
A() {
cout << "A()" << endl;
}
~A() {
cout << "~A()" << endl;
}
};

int main() {
A a;
return 0;
}

头文件声明

1
2
3
4
5
6
7
8
9
10
#ifndef _A_H_ // 防止头文件被重复包含
#define _A_H_

class A {
public:
A();
~A();
};

#endif

头文件实现

1
2
3
4
5
6
7
8
9
#include "A.h"

A::A() {
cout << "A()" << endl;
}

A::~A() {
cout << "~A()" << endl;
}

引用头文件

1
2
3
4
5
6
#include "A.h"

int main() {
A a;
return 0;
}

STL

容器

vector

创建

1
2
3
4
5
vector<int> v1; // 空
vector<int> v2(10); // 10 个元素,值为 0
vector<int> v3(10, 1); // 10 个元素,值为 1
vector<int> v4(v3); // 拷贝 v3
vector<int> v5(v3.begin(), v3.begin() + 3); // 拷贝 v3 的前 3 个元素

获取容量

1
v.capacity();

获取实际大小

1
v.size();

访问元素

1
2
3
4
v[0];
v.at(0);
v.front();
v.back();

赋值

1
2
v.assign(10, 1); // 10 个元素,值为 1
v.assign(v2.begin(), v2.begin() + 3); // 拷贝 v2 的前 3 个元素

插入

1
2
3
v.insert(v.begin() + 1, 2); // 在第 1 个元素后插入 2
v.insert(v.begin() + 1, 3, 2); // 在第 1 个元素后插入 3 个 2
v.insert(v.begin() + 1, v2.begin(), v2.begin() + 3); // 在第 1 个元素后插入 v2 的前 3 个元素

删除

1
2
v.erase(v.begin() + 1); // 删除第 1 个元素
v.erase(v.begin() + 1, v.begin() + 3); // 删除第 1 个元素到第 3 个元素

尾部插入和删除

1
2
v.push_back(1);
v.pop_back();

获取迭代器

1
2
3
4
5
vector<int>::iterator it = v.begin(); // 迭代器遍历到达尾部时,it == v.end(),指向最后一个元素的下一个位置
vector<int>::const_iterator cit = v.cbegin(); // 常量迭代器,不可修改
vector<int>::reverse_iterator rit = v.rbegin(); // 反向迭代器,指向最后一个元素
vector<int>::const_reverse_iterator crit = v.crbegin(); // 常量反向迭代器,不可修改
// 这里的 :: 是作用域限定符,用于指定 vector<int> 的成员

遍历

1
2
3
for (vector<int>::iterator it = v.begin(); it != v.end(); ++it) {
cout << *it << endl;
}

array

创建

1
2
3
4
5
array<int, 10> a1; // 10 个元素,值为 0
array<int, 10> a2 = {1, 2, 3}; // 10 个元素,前 3 个元素为 1, 2, 3,后 7 个元素为 0
array<int, 10> a3 = {1, 2, 3, 4, 5}; // 10 个元素,前 5 个元素为 1, 2, 3, 4, 5,后 5 个元素为 0
array<int, 10> a4 = a3; // 拷贝 a3
array<int, 10> a5 = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; // 10 个元素,前 10 个元素为 1, 2, 3, 4, 5, 6, 7, 8, 9, 10

获取容量

1
a.size();

访问元素

1
2
3
4
a[0];
a.at(0);
a.front();
a.back();

赋值

1
2
3
a.fill(1); // 所有元素赋值为 1
a = {1, 2, 3}; // 前 3 个元素赋值为 1, 2, 3,后 7 个元素赋值为 0
a = a3; // 拷贝 a3

交换

1
a.swap(a3);

遍历

1
2
3
for (array<int, 10>::iterator it = a.begin(); it != a.end(); ++it) {
cout << *it << endl;
}

虚继承的实现机制

虚继承是为了解决菱形继承问题,代码示例:

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
class Furniture {
public:
int _weight;
};

class Sofa : virtual public Furniture {
public:
int _a;
};

class Bed : virtual public Furniture {
public:
int _b;
};

class SofaBed : public Sofa, public Bed {
public:
int _c;
};

int main() {
SofaBed sb;
sb._weight = 100;
sb._a = 1;
sb._b = 2;
sb._c = 3;
return 0;
}

在虚继承中,每个虚继承的派生类都会增加一个虚基类指针 vbptr,该指针位于派生类对象的顶部。vbptr 指针指向一个虚基类表 vbtable(不占对象内存),虚基类表中记录了基类成员变量相对于 vbptr 指针的偏移量,根据偏移量就可以找到基类成员变量。

当虚基类的派生类被当作基类继承时,虚基类指针 vbptr 也会被继承,因此底层派生类对象中成员变量的排列方式与普通继承有所不同。

上图为对象 sbed 的逻辑存储结构:对象 sbed 顶部是基类 Sofa 的虚基类指针和成员变量;紧接着是基类 Bed 的虚基类指针和成员变量。间接基类 Furniture 的成员变量在对象 sbed 中只有一份拷贝,放在最下面。Sofa 类的虚基类指针 Sofa::vbptr 指向了 Sofa 类的虚基类表,该虚基类表中记录了_wood 与 Sofa::vbptr 的距离,为 16 字节;同样,Bed 类虚基类表记录了_wood 与 Bed::vbptr 的距离,为 8 字节。通过偏移量就可以快速找到基类的成员变量。

虚基类表中的偏移量在继承过程中会变化吗

在虚继承(virtual inheritance)中,虚基类表中的偏移量是在派生类之间共享的,不会因为继承过程中的派生而发生变化。

虚继承的主要目的是解决菱形继承(diamond inheritance)问题,即当一个派生类从两个或多个共同的基类派生而来,并且这些基类有一个共同的基类时,可能会导致多次继承同一个共同基类的问题。为了解决这个问题,C++ 引入了虚基类(virtual base class)的概念,其中虚基类表维护了偏移量的信息,以确保在派生类中只有一份共同基类的实例。

由于虚基类表中的偏移量是在类层次结构中共享的,所以不会因为派生而发生变化。这样一来,不管派生多少次,虚基类在派生类中的位置和偏移量都是一致的,从而确保了共享基类的唯一性和正确性。

共享基类的构造只会发生一次

虚基类表中保存了共享基类的构造标志信息。当派生类的对象被创建时,虚基类的构造函数会根据虚基类表中的构造标志信息判断是否需要进行构造,从而保证共享基类的构造只会发生一次。

虚函数实现多态的机制

虚函数就是通过动态绑定实现多态的,当编译器在编译过程中遇到 virtual 关键字时,它不会对函数调用进行绑定,而是为包含虚函数的类建立一张虚函数表 Vtable。在虚函数表中,编译器按照虚函数的声明顺序依次保存虚函数地址。同时,编译器会在类中添加一个隐藏的虚函数指针 VPTR,指向虚函数表。在创建对象时,将虚函数指针 VPTR 放置在对象的起始位置,为其分配空间,并调用构造函数将其初始化为虚函数表地址。需要注意的是,虚函数表不占用对象空间。

派生类继承基类时,也继承了基类的虚函数指针。当创建派生类对象时,派生类对象中的虚函数指针指向自己的虚函数表。在派生类的虚函数表中,派生类虚函数会覆盖基类的同名虚函数。当通过基类指针或基类引用操作派生类对象时,以操作的对象内存为准,从对象中获取虚函数指针,通过虚函数指针找到虚函数表,调用对应的虚函数。

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Base1       // 定义基类 Base1
{
public:
virtual void func(); // 声明虚函数 func()
virtual void base1(); // 声明虚函数 base1()
virtual void show1(); // 声明虚函数 show1()
};

class Base2 // 定义基类 Base2
{
public:
virtual void func(); // 声明虚函数 func()
virtual void base2(); // 声明虚函数 base2()
virtual void show2(); // 声明虚函数 show2()
};

// 定义 Derive 类,公有继承 Base1 和 Base2
class Derive :public Base1, public Base2
{
public:
virtual void func(); // 声明虚函数 func()
virtual void base1(); // 声明虚函数 base1()
virtual void show2(); // 声明虚函数 show2()
};

上面代码的继承关系如下图:

在编译时,编译器发现 Base1 类与 Base2 类有虚函数,就为两个类创建各自的虚函数表,并在两个类中添加虚函数指针。

如果创建 Base1 类对象(如 base1)和 Base2 类对象(如 base2),则对象中的虚函数指针会被初始化为虚函数表的地址,即虚函数指针指向虚函数表。如下图:

Derive 类继承自 Base1 类与 Base2 类,也会继承两个基类的虚函数指针。Derive 类的虚函数 func()、base1() 和 show2() 会覆盖基类的同名虚函数。如果创建 Derive 类对象(如 derive),则对象 derive 的内存逻辑结构如下图:

通过基类 Base1、基类 Base2 的指针或引用操作 Derive 类对象,在程序运行时,编译器从 Derive 类对象内存中获取虚函数指针,通过指针找到虚函数表,调用相应的虚函数。不同的类,其函数实现都不一样,在调用时就实现了多态。

左值和右值

左值与右值这两个概念是从 C 中传承而来的,左值指既能够出现在等号左边,也能出现在等号右边的变量;右值则是只能出现在等号右边的变量。 左值是可寻址的变量,有持久性; 右值一般是不可寻址的常量,或在表达式求值过程中创建的无名临时对象,短暂性的。

std::move 是一个函数模板,用于将一个左值转换为对应的右值引用,以便在特定情况下实现移动语义。

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

void processValue(int& value) {
std::cout << "Lvalue reference: " << value << std::endl;
}

void processValue(int&& value) {
std::cout << "Rvalue reference: " << value << std::endl;
}

// 在这个函数模板中,T&& 并不表示传入的是右值引用,而是一种通用引用(Universal Reference)的写法。
// 遵循引用折叠规则实现完美转发
template <typename T>
void forwardValue(T&& value) {
processValue(std::forward<T>(value));
}

int main() {
int a = 10;
forwardValue(a); // 传递左值,调用 processValue(int& value)
forwardValue(20); // 传递右值,调用 processValue(int&& value)

return 0;
}

C++ 11 特性

右值引用

与拷贝构造函数相比,移动构造函数是高效的,但它没有拷贝构造函数安全。例如,当程序抛出异常时,移动构造可能还未完成,这样可能会产生悬挂指针,导致程序崩溃。怎么解决这个问题?

在移动构造函数中,确保在移动资源后将源对象置为有效但可识别的状态,这样即使移动构造函数被中断或抛出异常,源对象也处于一个良好定义的状态,不会导致悬挂指针或资源泄漏。

以下是一些解决方案:

  1. 使用资源所有权标记:在移动构造函数中,可以使用某种资源所有权标记来表示源对象已经失去了对资源的所有权。例如,可以将源对象中的指针设置为nullptr,或者将其他标识位设置为无效值。这样,即使移动构造函数被中断,也可以识别出源对象不再持有资源。

  2. 使用智能指针:使用智能指针可以更安全地处理资源的移动。例如,使用std::unique_ptrstd::shared_ptr来管理资源,这样在移动构造函数中移动智能指针时,资源的所有权会自动传递给新对象,并且源对象会被正确地设置为nullptr

  3. 使用异常安全的编程技术:在编写移动构造函数时,可以使用异常安全的编程技术,如使用RAII(资源获取即初始化)原则和异常安全的交换操作来确保移动操作的安全性。这样可以在移动过程中正确地处理异常,保证资源的正确释放和对象状态的正确性。

  4. 使用异常规范:在移动构造函数中,可以使用异常规范(noexcept)来指定该函数不会抛出异常。这样可以帮助编译器进行优化,并且在移动构造函数可能被中断时,确保对象仍然处于良好定义的状态。

需要注意的是,正确处理移动构造函数的异常安全性是开发者的责任。在编写和使用移动构造函数时,要仔细考虑所有可能的异常情况,并确保源对象和目标对象都处于有效且一致的状态,以避免悬挂指针或资源泄漏的问题。

引用折叠规则

引用折叠规则是C++语言中关于引用类型的一种特殊规则,用于确定在特定情况下引用的类型。引用折叠规则适用于以下几种情况:

  1. 当一个左值引用(lvalue reference)与一个左值引用相绑定时,结果仍然是一个左值引用。
  2. 当一个右值引用(rvalue reference)与一个左值引用相绑定时,结果是一个左值引用。
  3. 当一个左值引用与一个右值引用相绑定时,结果是一个右值引用。
  4. 当一个右值引用与一个右值引用相绑定时,结果仍然是一个右值引用。

引用折叠规则的目的是在保持语义一致性的同时提供更灵活的引用使用方式。它使得可以通过统一的方式来处理左值引用和右值引用,简化了代码的编写和理解。

下面是一些示例说明引用折叠规则的应用:

1
2
3
4
5
6
7
8
9
int x = 5;  // x是一个左值

int& lref = x; // lref是一个左值引用,折叠规则1

int&& rref1 = x; // rref1是一个左值引用,折叠规则2

int&& rref2 = 10; // rref2是一个右值引用,折叠规则3

int&& rref3 = std::move(x); // rref3是一个右值引用,折叠规则4

在这个例子中,x是一个左值,lref是一个左值引用,rref1rref2是左值引用(因为它们与左值绑定),而rref3是一个右值引用(因为它与std::move(x)的结果绑定)。

引用折叠规则在模板类型推导、移动语义和完美转发等方面有重要的应用,使得代码可以更加灵活地处理不同类型的引用和值。理解引用折叠规则对于理解C++中的引用类型和引用相关的特性非常重要。

make 和 make install 的区别

make 是编译,make install 是将编译后的包复制到指定文件夹。

参考链接:1

make 和 cmake 的区别

cmake is a system to generate make files based on the platform (i.e. CMake is cross platform) which you can then make using the generated makefiles.

While make is you directly writing Makefile for a specific platform that you are working with.

If your product is crossplatform, then cmake is a better choice over make. Since cmake also supports a lot of other custom commands/rules, even if your product is not crossplatform, there is a good reason to choose cmake as your make system.

参考链接:1 2 3

QT 使用

控制台中文乱码

https://zhuanlan.zhihu.com/p/557844731

CLion 使用

控制台输出中文乱码

main 函数中插入代码

1
system("chcp 65001");

指针

在 C++ 中,this 关键字指向当前对象的指针,即指向调用该成员函数的对象。例如,在成员函数中,this->name 意味着当前对象的 name 成员变量。

*this 则是指向当前对象的引用。在上述代码中,return *this 返回的是当前对象的引用,以便实现链式赋值。

智能指针

如果释放了指针指向的对象,但是其他指针仍然在使用该对象,将造成其他指针无法访问资源,成为悬空指针。为了解决这个问题,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
#include<iostream>
#include<string>

using namespace std;

class Data {
public:
Data(string str) : _str(str) {
cout << "Constructor" << endl;
}

~Data() {
cout << "Destructor" << endl;
}

void dis() {
cout << _str << endl;
}

private:
string _str;
};

int main() {
Data *pstr1 = new Data("I Love China");
Data *pstr2 = pstr1;
Data *pstr3 = pstr1;
pstr1->dis();
delete pstr1;
pstr2->dis();
return 0;
}

以上程序编译会正常通过,但是因为指针指向的原来的对象已经被释放,所以程序运行时会打印乱码。

为了解决这个问题,C++中引入了引用计数的概念,引用计数的思想是:每一个对象都有一个引用计数,当有一个指针指向该对象时,该对象的引用计数加1,当指针不再指向该对象时,该对象的引用计数减1,当该对象的引用计数为0时,该对象被释放。

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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
#include<iostream>
#include<string>

using namespace std;

class Data {
public:
Data(string str) : _str(str) {
cout << "Constructor" << endl;
}

~Data() {
cout << "Destructor" << endl;
}

void dis() {
cout << _str << endl;
}

private:
string _str;
};

class Count //Count类用于存储指向同一资源的指针数量
{
public:
friend class SmartPtr;

Count(Data *pdata) : _pdata(pdata), _count(1) {
cout << "Count类构造函数" << endl;
}

~Count() {
cout << "Count类析构函数" << endl;
delete _pdata;
}

private:
Data *_pdata;
int _count;
};

//使用指针实现智能指针
class SmartPtr //SmartPtr类用于对指向Data类对象的指针实现智能管理
{
public:
SmartPtr(Data *pdata) : _reNum(new Count(pdata)) {
cout << "创建基类对象" << endl;
}

SmartPtr(const SmartPtr &another) : _reNum(another._reNum) {
++_reNum->_count;
cout << "Smartptr类复制构造函数" << endl;
}

~SmartPtr() {
if (--_reNum->_count == 0) {
delete _reNum;
cout << "Smartptr类析构函数" << endl;
}
}

Data *operator->() {
return _reNum->_pdata;
}

Data &operator*() {
return *_reNum->_pdata;
}

int disCount() {
return _reNum->_count;
}

private:
Count *_reNum;
};

int main() {
Data *pstr1 = new Data("I Love China!");
SmartPtr pstr2 = pstr1;
(*pstr1).dis();
SmartPtr pstr3 = pstr2;
pstr2->dis();
cout << "使用基类对象的指针数量:" << pstr2.disCount() << endl;
return 0;
}

以上代码实现了一件事,就是在所有包含原始对象Data的包装对象都被析构之后再释放原始的Data对象,类似于默认拷贝构造函数中的浅拷贝。

这就是智能指针的核心原理。

运算符重载

运算符重载的参数个数决定了运算符的操作数数量。具体来说,有以下两种情况:

  1. 单目运算符重载:单目运算符只需要一个操作数。在重载单目运算符时,将其定义为类的成员函数或非成员函数都是可行的。对于成员函数的重载,操作数就是调用该函数的对象本身;而对于非成员函数的重载,则将操作数作为函数的参数传入。

例如,重载单目取反运算符(-):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass {
public:
int value;

// 成员函数形式的重载
MyClass operator-() {
MyClass result;
result.value = -this->value;
return result;
}
};

// 非成员函数形式的重载
MyClass operator-(const MyClass& obj) {
MyClass result;
result.value = -obj.value;
return result;
}

使用示例:

1
2
3
4
5
MyClass obj;
// 假设obj.value = 5

MyClass result1 = -obj; // 使用成员函数形式的重载
MyClass result2 = operator-(obj); // 使用非成员函数形式的重载
  1. 二元运算符重载:二元运算符需要两个操作数。在重载二元运算符时,将其定义为类的成员函数或非成员函数,参数的传递方式会有所不同。

对于成员函数形式的重载,左侧操作数将成为调用函数的对象本身,右侧操作数将作为函数的参数传入。而对于非成员函数形式的重载,两个操作数都将作为函数的参数传入。

例如,重载二元加法运算符(+):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class MyClass {
public:
int value;

// 成员函数形式的重载
MyClass operator+(const MyClass& other) {
MyClass result;
result.value = this->value + other.value;
return result;
}
};

// 非成员函数形式的重载
MyClass operator+(const MyClass& obj1, const MyClass& obj2) {
MyClass result;
result.value = obj1.value + obj2.value;
return result;
}

使用示例:

1
2
3
4
5
MyClass obj1, obj2;
// 假设obj1.value = 5, obj2.value = 10

MyClass result1 = obj1 + obj2; // 使用成员函数形式的重载
MyClass result2 = operator+(obj1, obj2); // 使用非成员函数形式的重载

需要根据具体情况选择合适的重载方式,保证操作符重载的正确性和符合语义。

友元重载函数的使用形式和成员函数形式类似。

异常

不抛出异常的函数应该声明为 noexcept,这样的函数如果抛出了异常,编译器会调用 terminate 来终止程序的执行。

C++ 11 以前不抛出异常的函数应该声明为 throw(),这样的函数如果抛出了异常,编译器会调用 abort 来终止程序的执行。

模板实例化在编译阶段,所以用 static_assert 来检查模板参数是否满足要求,参数必须是常量表达式,在编译阶段就可以计算出来。

const

const int *p1 可看作是 const 修饰的类型是 int,修饰的内容是 * p1,即 * p1 不允许改变。
int const *p2 可以看作 const 修饰的类型是 int,修饰的内容是 * p2,即 * p2 不允许改变。
int *const p3 可以看作 const 修饰的类型是 int *,修饰的内容是 p3,即 p3 不允许改变

简单来说就是在 * 号左边还是右边,实际应用中,只会出现第一种情况。

记忆方法

  1. const 默认是修饰它左边的符号的,如果左边没有,那么就修饰它右边的符号,比如 const int *p; 左边没有,看右边的一个,是 int,自然就是 p 指针指向的值不能改变
  2. int const *p; 此时左边有 int,其实和上面一样,还是修饰的 int
  3. int* const p : 修饰的是 *,指针不能改变
  4. const int *const p : 第一个左边没有,所以修饰的是右边的 int,第二个左边有,所以修饰的是 * ,因此指针和指针指向的值都不能改变
  5. const int const * p : 这里两个修饰的都是 int 了,所以重复修饰了,有的编译器可以通过,但是会有警告,你重复修饰了,有的可能直接编译不过去,因此,永远记住,看到 const 就看它左边是什么,如果左边没有,才看右边的。

顶层和底层 const

定义(区分重点在修饰的内容,而不是修饰的类型)

  • 被修饰的变量本身无法改变的 const 是顶层 const;
  • 通过指针或引用等间接途径来限制目标内容不可变的 const 是底层 const。

拷贝操作

  • 顶层 const 没有影响。拷贝操作不会改变被拷贝对象的值,因此拷入和拷出的对象是否是常量无关紧要。
  • 底层 const 的限制不能忽视。拷入和拷出的对象必须具有相同的底层 const 资格,或者两个对象的数据类型可以相互转换(一般来说,非常量可以转换成常量,反之则不行)。

形参和实参

  • 和其他初始化过程一样,当用实参初始化形参时会忽略掉顶层 const。换句话说,形参的顶层 const 被忽略掉了。当形参有顶层 const 时,传给它常量对象或者非常量对象都是可以的。
  • 我们可以使用非常量初始化一个底层 const 对象,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。

example

1
2
const int *p; // p 是指针,指向 int,int 是 const, 「*p」类型为 int 并且不可变;是低层 const
int *const p; // p 是 const,是常量指针,指向 int, p 属于「int *」并且不可变;是顶层 const

const 函数

C++ 中的 const 函数指的是在类中声明的成员函数,该函数承诺不会修改类的成员变量,因此可以在函数声明和定义中使用 const 关键字来表示这一点。使用 const 修饰的成员函数被称为 const 成员函数。

const 成员函数可以被声明为常量对象调用,这意味着该对象的成员变量的值不能被修改。如果试图在 const 成员函数中修改成员变量的值,则会导致编译错误。

1
2
3
4
5
6
7
class MyClass {
int value;
public:
int getValue() const {
return value; // 返回值不改变成员变量的值,因此可以使用 const 关键字
}
};

常量表达式

常量表达式是指在编译时值就能确定的表达式。

一般而言,如果你认定变量是一个常量表达式,那就把它声明成 constexpr 类型。constexpr 变量在定义时必须初始化。

1
2
3
4
const int max_num = 20; // max_num 是常量表达式
const int limit = max_num + 1; // limit 是常量表达式
int staff_size = 2; // staff_size 不是常量表达式,因为 staff_size 没有用 const 修饰
const int sz = get_size(); // sz 不是常量表达式,虽然 sz 是个常量,但它的值在运行时才能确定

模板

函数模板显式具体化和显示实例化的区别:显示具体化是提供一个新的实现,显示实例化是为了缩短编译时间。

显示具体化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
template<typename T>
void foo(T t)
{
cout << "General template." << endl;
}

template<>
void foo<int>(int i)
{
cout << "Explicit specialization for int." << endl;
}

int main()
{
foo(42); // 使用显式具体化的版本
foo(3.14); // 使用通用模板
}

显示实例化示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
template<typename T>
void bar(T t)
{
cout << "Template function." << endl;
}

// 显式实例化
template void bar<int>(int);

int main()
{
bar(42); // 使用已经实例化的版本
}

函数模板和类模板的区别
使用类模板时,必须要为模板参数显式指定实参,不存在实参推演过程,也就是说不存在将整型值10推演为int类型再传递给模板参数的过程,必须要在<>中指定int类型,这一点与函数模板不同。

成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部。作为接口组成部分的非成员函数,例如 add、read 和 print 等,它们的定义和声明都在类的外部。

1
2
3
4
5
6
7
// isbn 函数的另一个关键之处是紧随参数列表之后的 const 关键字,这里,const 的作用是修改隐式 this 指针的类型。紧跟在参数列表后面的 const 表示 this 是一个指向常量的指针。像这样使用 const 的成员函数被称作常量成员函数
stdzzstring isbn() const [ return this->bookNo; }

// 类似于
// 此处的 this 是一个指向常量的指针,因为 isbn 是一个常量成员
std::string Salea_dnta::isbn(const Salcs_dntn *const this)
{return this->isbn; }

构造和析构函数的执行顺序

构造函数:基类,成员对象,派生类
析构函数:派生类,成员对象,基类

在main函数中使用的字符常量的生命周期是整个程序的执行周期,而在main函数中使用的string对象的生命周期则是main函数的执行周期。示例代码:

1
2
3
4
5
6
7
int main()
{
const char* constantString = "Hello"
string s = constantString; // string modifiedString(constantString);
cout << s << endl;
return 0;
}

函数

值传递和引用传递

与值传递相比,引用传递的优势主要体现在三个方面:一是可以直接操作引用形参所引的对象;二是使用引用形参可以避免拷贝大的类类型对象或容器类型对象;三是使用引用形参可以帮助我们从函数中返回多个值。

数组形参

数组有两个性质: 不允许数组拷贝,使用数组时通常会被转换为指针
数组以指针的形式传给函数,所以一开始函数并不知道数组的确切尺寸,要解决这个问题有三种常用的技术:

  • 使用标记指定数组长度,函数在处理 C 风格字符串时遇到空字符停止,但是这种方法只适合字符这种有结束标记,想 int 这种所有取值都合理的不适用。

  • 使用标准库范围
    示例代码:

    1
    2
    int j[2] = {0, 1};
    print(begin(j), end(j));
  • 显示传递一个表示数组大小的形参

    1
    2
    3
    4
    5
    void print(const int ia[], size_t size){
    for(size_t i = 0; i != size; ++i){
    cout <<ia[i] << endl;
    }
    }

    数组引用形参,维度是类型的一部分,void print(int (&arr)[10]),&arr 两端的括号必不可少,没有括号的话就变成了引用的数组。但是这样只能将函数作用于大小为 10 的数组。

当我们想把数组作为函数的形参时,有三种可供选择的方式:一是声明为指针,二是声明为不限维度的数组,三是声明为维度确定的数组。

可变形参

  • initializer_list
    和 vector 一样,initializer_list 也是一种模板类型
    和 vector 不一样的是,initializer_list 对象中的元素永远是常量值,我们无法改变 initializer_list 对象中元素的值。
    因为 initializer_list 对象的元素永远是常量值,所以我们不可能通过设定引用类型来更改循环控制变量的内容。只有当 initializer_list 对象的元素类型是类类型或容器类型(比如 string)时,才有必要把范围 for 循环的循环控制变量设为引用类型。

  • 省略符形参
    省略符形参应该仅仅用于 C 和 C++ 通用的类型。特别应该注意的是,大多数类类型的对象在传递给省略符形参时都无法正确拷贝。
    省略符形参只能出现在形参列表的最后一个位置,它的形式无外乎以下两种:

    1
    2
    void foo(parm_list, ...); // , 可以省略,省略符形参所对应的实参无须类型检查
    void foo(...);

返回数组指针

分四种情况,比较复杂:

  • 类型别名
1
2
3
typedef int arrT[10]; // arrT 是一个类型别名
using arrT = int[lO]; // 等价于上一行
arrT* func(int i);
  • 声明一个返回数组指针的函数
1
int (*func(int i))[10];

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

  • 使用尾置返回类型
1
auto func(int i) —> int(*)[10];
  • 使用 decltype
1
2
3
4
5
6
7
int odd[] = {1,3,5,7,9};
int even[] = {O,2,4,6,8};
decltype(odd) *arrPtr(int i)
{

return (i % 2) ? &odd : &even;
}

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

函数重载

顶层不可重载,一个拥有顶层 const 的形参无法和另一个没有顶层 const 的形参区分开来

1
2
3
4
5
Record lookup(Phone);
Record lookup(const Phone); // 重复声明

Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明

底层可以重载,另一方面,如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的 const 是底层的:

1
2
3
4
5
Record lookup(Account&);
Record lookup(const Account&);

Record lookup(Account*);
Record lookup(const Account*);

函数调用的底层实现

在大多数机器上,一次函数调用其实包含着一系列工作:调用前要先保存寄存器,并在返回时恢复;可能需要拷贝实参;程序转向一个新的位置继续执行。

内联函数

将函数指定为内联函数(inline),通常就是将它在每个调用点上 “内联地” 展开。

1
2
3
4
cout <<shorterString(sl, 52) << endl;
// 将在编译过程中展开成类似于下面的形式
cout <<(sl.size() < 32.size() ? 31 : 52) << endl;
// 从而消除了 shorterString 函数的运行时开销。

声明内联函数

1
2
3
4
5
inline const string &
shorterString<const string &s1, const string &s2)
(
return sl.size() <= s2.size() ? sl : s2;
)

constexpr 函数

constexpr 函数(constexpr function)是指能用于常量表达式的函数。

函数的返回类型及所有形参的类型都得是字面值类型,而且函数体中必须有且只有一条 return 语句:

1
2
constexpr int new_sz() { return 42;}
constexpr int foo = new_sz();

执行该初始化任务时,编译器把对 constexpr 函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr 函数被隐式地指定为内联函数。

头文件保护技术

所谓预处理宏其实是一个预处理变量,它的行为有点类似于内联函数。

1
assert(expr);

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

assert 的行为依赖于一个名为 NDEBUG 的预处理变量的状态。如果定义了 NDEBUG,则 assert 什么也不做。默认状态下没有定义 NDEBUG,此时 assert 将执行运行时检查。

打开和关闭调试

1
CC -D NDEBUG main.C it use /D with the Microsoft compiler

这条命令的作用等价于在 main.c 文件的一开始写 #define NDEBUG。

类型提升

即使实参是一个很小的整数值,也会直接将它提升成 int 类型

1
2
3
void ff(int);
void ff(short);
ff('a');

所有算术类型的转换级别都一样

1
2
3
void manip(long);
void manip(float);
manip(3.14); // 错误,二义性调用

类型匹配

1
2
3
4
5
6
7
8
int calc(char*, char*);
int calc(char* const, char* const); // 顶层 const,无法区分这个两个函数

int calc(char*, char*);
int calc(const char*, const char*); // 底层 const,可以区分

int calc(int&, int&);
int calc(const int&, const int&); // 可以区分

函数指针

1
2
3
4
5
bool lengthCompare(const string &, const string &);
// 声明一个函数指针指向函数
bool (*pf) (const string &, const string &);
pf = lengthCompare;
pf = &lengthCompare; // 等价于上一行

使用函数指针

1
2
3
bool bl = pf("hello", "goodbye");
bool b2 = (*pf) ("hello", "goodbye"); // 等价
bool b3 = lenqthCompare("hello", "qoodbve"); // 等价

可以为函数指针赋一个 nullptr 或者值为 0 的整型常量表达式,表示该指针没有指向任何一个函数。

重载函数 的指针,指针类型必须与重载函数中的某一个精确匹配。

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

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

decltype 返回函数类型,此时不会将函数类型自动转换成指针类型。因为 decltype 的结果是函数类型,所以只有在结果前面加上*才能得到指针。

1
2
typedef decltype(lengthCompare)* FuncPZ; // * 号位置可变
void useBigger(const string&, const string&, FuncPZ);

函数类型的形参会自动转换成指针,返回类型不会。

返回指向函数的指针

1
2
int (*fl(int))(int*, int);
auto fl(int) -> int (*)(int*, int); // 等价。尾置返回类型

按照由内向外的顺序阅读这条声明语句:我们看到 f1 有形参列表,所以 f1 是个函数;f1 前面有 *,所以 f1 返回一个指针;进一步观察发现,指针的类型本身也包含形参列表,因此指针指向函数,该函数的返回类型是 int。

示例

编写函数的声明,令其接受两个 int 形参并且返回类型也是 int;然后声明一个 vector 对象,令其元素是指向该函数的指针。

1
2
int func(int, int);
vector<decltype(func)* > vF;