移动语义

lvalue and rvalue

左值,即赋值符号左边的值,当然也可以出现在赋值符号右边,可以通过去地址运算符获取其地址是左值的根本特点;

右值,即赋值符号右边的值,只有数值的概念,在内存中没有一块固定的空间用于存放,如表达式中的数值、表达式运算的结果等等,如果熟悉汇编语言,可以理解为右值暂时存放在寄存器中。c++11为了引入右值引用,将右值进一步划分为纯右值将亡值

  • 纯右值,即纯粹的右值,包括没有标识符、不可取地址的表达式和字面量(注意与字符串字面量区分,双引号括起来的字符串是存放在内存的可读区的)。
  • 将亡值,临时对象,暂时存放在内存中,一般马上会被销毁,如函数返回的一个对象等等,这部分内存是可以被移动的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int a;
a = 2; // true. a is lvalue and 2 is rvalue
a = 3; // true. a is lvalue and 3 is rvalue
2 + 3 = 9; // error. 2+ 3 is rvalue

int b = 3;
int *pb = &b; // true. b is lvalue,可以取地址操作
&b = 12; // error.取地址操作返回的是b的地址,是一个右值

int & foo(); // foo返回一个左值引用
foo() = 12; // true. 可以通过一个左值引用给左值赋值
int * pf = &foo(); // true. 对左值引用取地址等同于对其引用的对象取地址

int bar(); // bar返回一个int类型的值,为右值
int c = bar(); // 将bar返回的值拷贝给c
int * pb = &bar(); // error. bar返回的值是临时存在的

左值引用与右值引用

左值引用,即引用对象是左值的引用;右值引用,即引用对象是右值的引用。以往我们直接称左值引用为引用,并使用&表示,为了区分左值引用,使用&&表示右值引用。使用规则如下:

  • 左值引用,使用T&,只能绑定左值
  • 右值引用,使用T&&,只能绑定右值
  • 常量左值引用,const T&,可以绑定左值或右值,但是不能对其修改。
1
2
3
4
5
6
7
int i = 10;
int & ri = i;
int && rri1 = i; // error. i is a lvalue
int && rri2 = 10; // true. i is a rvalue
int && rri3 = 8 + 10 * i; // true. 8 + 10 * i is a rvalue
const int & cri1 = i; // const T&绑定到左值
const int & cri2 = 2 + 3; // const T&绑定到右值

Note:右值引用本身也是一个左值。如下fun为重载函数,一个接受左值参数,另一个接受右值参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void fun(int &a) {
std::cout << "in fun(int &)" << std::endl;
}

void fun(int &&a) {
std::cout << "in fun(int &&)" << std::endl;
}

int main()
{
int a = 1;
int &&b = 1;

fun(a);
fun(b);
fun(std::move(a)); // 将左值转换为右值引用类型
fun(1+3);

return 0;
}

编译运行结果如下,可见fun(b)调用的是左值版本。

1
2
3
4
5
6
> g++ b.cc -g -o a
> ./a
in fun(int &)
in fun(int &)
in fun(int &&)
in fun(int &&)

std::move

左值具有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。绑定到临时对象的右值引用,其引用的对象将要被销毁且无其他用户,因此我们可以接管其占有的资源。

虽然不能将一个右值引用绑定到左值上,但是可以通过move显示地将一个左值转换为对应的右值引用。上一节最后,使用了move作用于a,使得其从一个左值转换为右值引用类型。需要注意的是,可以销毁一个移后源对象,也可以赋予它新值,但是不能使用其值,因为其值已经和之前的状态不同了(比如被设置成初始状态)。

Why 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
25
class Obj
{
public:
Obj()
{
cout << "Obj default-constructor\n";
}
Obj(const Obj &)
{
cout << "Obj copy-constructor\n";
}
~Obj()
{
cout << "Obj destructor\n";
}
};

int main()
{
vector<Obj> vo;
vo.reserve(4);
vo.push_back(Obj());

return 0;
}

编译运行,输出如下:

1
2
3
4
5
> g++ b.cc -O0 -o a && ./a
Obj default-constructor
Obj copy-constructor
Obj destructor
Obj destructor

可见,首先调用了默认构造函数,生成一个临时对象,然后将该临时对象拷贝一份存入vector,再销毁该临时对象。如果临时对象本身占有大量的资源,这些资源的拷贝和销毁会成为降低程序的性能。

由于临时对象本身没有其他用户且马上需要被销毁,我们可以直接接管其资源,既可以减轻销毁临时对象的工作,也无需拷贝一份重复的资源。或者有时候我们不需要使用一个左值了,此时move就派上了用场。类似拷贝构造函数和拷贝赋值运算符,c++11允许我们为类定义移动构造函数和移动赋值运算符,它们从给定的对象中“窃取”资源而非拷贝。

move-constructor and move-assign

重新设计一个类名为Array,其动态管理一个int数组,因此它需要管理自身占有的资源,首先思考这个类需要什么功能,即需要对外提供的接口:

  • 初始化:根据给定的size和默认值初始化一个Array
  • 拷贝构造和拷贝赋值:支持深拷贝
  • 添加元素:类似vector的push_back
  • 返回size:返回array对象的元素个数
  • 返回容量:返回array对象的capacity
  • 销毁:析构函数需要负责销毁自身管理的int数组
1
2
3
4
class Array
{

};

编写main函数测试该类:

1
2
3
4
int main()
{

}

生成时机

上一节讨论了移动语义在实际编程中的用法和优点,类似拷贝操作,编译器有时会为我们默认合成移动操作,这些默认的移动操作有时候就能满足我们的编程需求。但是有时候编译器不会提供默认合成的移动操作,而将其定义为删除的函数,此时如果我们在程序中显示要求使用移动操作,则编译器会默认使用拷贝操作替代,从而程序的行为不符合我们的预期。本节讨论如下内容:

  • 默认移动操作的行为:移动每一个数据成员,其中内置类型的成员编译器可以移动,类类型的成员需要有对应的移动操作。
  • 何时编译器会为类合成移动操作:类如果定义了自己的拷贝构造函数、拷贝赋值运算符或析构函数,则编译器不会为它合成移动操作。如果我们没有定义移动操作,且编译器也没有为我们合成时,即使我们指定使用移动操作,编译器会默默使用拷贝操作替代。此时,我们可以自定义移动操作,或者显式要求编译器为我们合成默认操作(=default)。
  • 何时移动操作被定义为删除,即显式要求一个移动操作而编译器无法为其生成,编译器会将移动操作定义为删除的函数。此时如果我们需要移动操作,必须自定义,而不能显示要求编译器默认为我们合成。
  • 显示定义移动操作会对默认合成的拷贝操作有什么影响:定义了移动操作(移动拷贝函数和移动赋值运算符之一),则拷贝操作默认被定义为删除。必须自定义或者显式要求编译器生成。

Note:需要区分编译器为我们合成移动操作时,是不会还是不能

Prepare

设计三个类,分别为Dad、Son和Daughter,其中Dad类拥有一个Son类型的数据成员和一个Daughter类型的数据成员,可以理解为父亲类有一对儿女。

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
class Daughter
{
public:
Daughter()=default;
Daughter(const Daughter &)
{
cout << "Daughter copy-constructor\n";
}
Daughter(Daughter &&)
{
cout << "Daughter move-constructor\n";
}
Daughter & operator=(const Daughter &)
{
cout << "Daughter copy-assign\n";
return *this;
}
Daughter & operator=(Daughter &&)
{
cout << "Daughter move-assign\n";
return *this;
}
};

class Son
{
public:
Son()=default;
Son(const Son &)
{
cout << "Son copy-constructor\n";
}
Son(Son &&)
{
cout << "Son move-constructor\n";
}
Son & operator=(const Son &)
{
cout << "Son copy-assign\n";
return *this;
}
Son & operator=(Son &&)
{
cout << "Son move-assign\n";
return *this;
}
};

class Dad
{
public:
Dad()=default;
Dad(const Dad &)=default;
Dad(Dad &&)=default;
Dad & operator=(const Dad &)=default;
Dad & operator=(Dad&&d)=default;
~Dad()=default;
public:
Son son;
Daughter daughter;
};

我们通过如下测试程序,根据打印的信息,探索移动操作的行为。c++默认的拷贝操作和移动操作,是拷贝或移动每个非static成员,因此,如果拷贝或移动了Dad对象,则其数据成员也会被拷贝或移动;如果拷贝赋值或者移动赋值了Obj对象,则其mem成员也会被拷贝赋值或者移动赋值。此时会打印处提示信息,我们就知道了Dad对象是被拷贝还是移动了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
vo.push_back(Dad()); // 临时对象,移动
Dad dad;
vo.push_back(dad); // 左值,拷贝
vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
o2 = o1;
o2 = std::move(o1);

return 0;
}

编译运行,输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
> g++ b.cc -std=c++11 -O2 -o b && ./b
========Construct========
Son move-constructor
Daughter move-constructor
Son copy-constructor
Daughter copy-constructor
Son move-constructor
Daughter move-constructor
========Assign========
Son copy-assign
Daughter copy-assign
Son move-assign
Daughter move-assign

与预期结果相同,此时我们显式要求了编译器为我们提供移动操作和拷贝操作,并且编译器通过匹配调用了我们需要的操作。

Verify 1

验证:一个类定义了拷贝构造函数、拷贝赋值运算符或析构函数,则不会合成移动操作,编译器使用拷贝操作替代

注释掉拷贝操作、移动操作和析构函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dad
{
public:
Dad()=default;
// Dad(const Dad &)=default;
// Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
// Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
Son son;
Daughter daughter;
};

再次编译运行:

1
2
3
4
5
6
7
8
9
10
11
12
13
> g++ b.cc -std=c++11 -O2 -o b && ./b
========Construct========
Son move-constructor
Daughter move-constructor
Son copy-constructor
Daughter copy-constructor
Son move-constructor
Daughter move-constructor
========Assign========
Son copy-assign
Daughter copy-assign
Son move-assign
Daughter move-assign

此时编译器的输出和最初的情况相同,即默认为我们生成了拷贝操作和移动操作,并在合适的地方调用了。

然后,显式要求编译器合成默认的拷贝构造函数、拷贝赋值运算符和析构函数中的任一几个(为节省篇幅仅给出显式合成析构函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
14

class Dad
{
public:
Dad()=default;
// Dad(const Dad &)=default;
// Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
// Dad & operator=(Dad&&d)=default;
~Dad()=default;
public:
Son son;
Daughter daughter;
};

编译运行,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
> g++ b.cc -std=c++11 -O2 -o b && ./b
========Construct========
Son copy-constructor
Daughter copy-constructor
Son copy-constructor
Daughter copy-constructor
Son copy-constructor
Daughter copy-constructor
========Assign========
Son copy-assign
Daughter copy-assign
Son copy-assign
Daughter copy-assign

此时,编译器使用拷贝操作替代了移动操作,即使我们没有显式要求合成拷贝操作。此时,其对象是通过拷贝操作来实现“移动”的。

Verify 2

验证:定义了移动操作的类必须定义拷贝操作,否则拷贝操作被定义为删除的

显式要求合成移动构造函数,其余保持不变:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dad
{
public:
Dad()=default;
// Dad(const Dad &)=default;
Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
// Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
Son son;
Daughter daughter;
};

编译,出现报错:

1
2
3
4
5
6
7
8
9
10
> g++ b.cc -std=c++11 -O2 -o b && ./b
b.cc: In function ‘int main()’:
b.cc:101:10: error: use of deleted function ‘Dad& Dad::operator=(const Dad&)’
101 | o2 = o1;
| ^~
b.cc:74:7: note: ‘Dad& Dad::operator=(const Dad&)’ is implicitly declared as deleted because ‘Dad’ declares a move constructor or move assignment operator
74 | class Dad
| ^~~
b.cc:102:22: error: use of deleted function ‘Dad& Dad::operator=(const Dad&)’
102 | o2 = std::move(o1);

此时main函数中的如下两行,被编译器认定使用了删除的拷贝赋值运算符:

1
2
o2 = o1;
o2 = std::move(o1);

可见,仅仅定义了移动构造函数,移动复制运算符会被拷贝赋值运算符替代。尝试定义移动赋值而注释掉移动构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dad
{
public:
Dad()=default;
// Dad(const Dad &)=default;
// Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
Son son;
Daughter daughter;
};

尝试编译仍然出错,因为编译器也未默认合成移动构造函数,而是使用拷贝构造函数替代之,但是声明了移动赋值运算符,拷贝操作均被定义为删除。

因此我们必须同时定义两种移动操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Dad
{
public:
Dad()=default;
// Dad(const Dad &)=default;
Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
Son son;
Daughter daughter;
};

同时,我们修改main函数,仅仅测试拷贝赋值是否成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
vo.push_back(Dad()); // 临时对象,移动
Dad dad;
// vo.push_back(dad); // 左值,拷贝
// vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
o2 = o1;
o2 = std::move(o1);

return 0;
}

编译,出现错误(一般IDE也会提示报错而无需编译):

1
2
3
4
5
6
7
8
> g++ b.cc -std=c++11 -O2 -o b && ./b
b.cc: In function ‘int main()’:
b.cc:101:10: error: use of deleted function ‘Dad& Dad::operator=(const Dad&)’
101 | o2 = o1;
| ^~
b.cc:74:7: note: ‘Dad& Dad::operator=(const Dad&)’ is implicitly declared as deleted because ‘Dad’ declares a move constructor or move assignment operator
74 | class Dad
| ^~~

提示我们使用了删除的拷贝赋值,因为Dad类声明了移动构造函数或移动赋值运算符。

再次修改main函数测试拷贝构造函数,首先验证移动构造可以成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
vo.push_back(Dad()); // 临时对象,移动
Dad dad;
// vo.push_back(dad); // 左值,拷贝
vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
// o2 = o1;
o2 = std::move(o1);

return 0;
}

编译运行:

1
2
3
4
5
6
7
8
9
> g++ b.cc -std=c++11 -O2 -o b && ./b
========Construct========
Son move-constructor
Daughter move-constructor
Son move-constructor
Daughter move-constructor
========Assign========
Son move-assign
Daughter move-assign

再次修改main函数测试拷贝构造:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
// vo.push_back(Dad()); // 临时对象,移动
Dad dad;
vo.push_back(dad); // 左值,拷贝
// vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
// o2 = o1;
// o2 = std::move(o1);

return 0;
}

编译运行:

1
2
3
4
5
6
/usr/include/c++/11/ext/new_allocator.h:162:11: error: use of deleted function ‘Dad::Dad(const Dad&)’
162 | { ::new((void *)__p) _Up(std::forward<_Args>(__args)...); }
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
b.cc:74:7: note: ‘Dad::Dad(const Dad&)’ is implicitly declared as deleted because ‘Dad’ declares a move constructor or move assignment operator
74 | class Dad
| ^~~

同上,提示我们因为Dad类声明了一个移动构造函数或移动赋值运算符,拷贝构造函数被定义为删除。

Verify 3

验证:类成员的移动操作不可见或者被定义为删除时,类的移动操作也被定义为删除

Verify 4

验证:类似拷贝赋值,如果存在类成员是被const修饰的常量或者一个引用,则其移动赋值被定义为删除

为Dad类添加一个int类型常量成员i:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Dad
{
public:
Dad() {}
// Dad(const Dad &)=default;
// Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
// Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
const int i{};
Son son;
Daughter daughter;
};

修改main函数,测试构造函数部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
vo.push_back(Dad()); // 临时对象,移动
Dad dad;
vo.push_back(dad); // 左值,拷贝
vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
// o2 = o1;
// o2 = std::move(o1);

return 0;
}

编译运行:

1
2
3
4
5
6
7
8
9
> g++ b.cc -std=c++11 -O2 -o b && ./b
========Construct========
Son move-constructor
Daughter move-constructor
Son copy-constructor
Daughter copy-constructor
Son move-constructor
Daughter move-constructor
========Assign========

修改main函数测试移动赋值部分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
cout << "========Construct========\n";
vector<Dad> vo;
vo.reserve(4);
vo.push_back(Dad()); // 临时对象,移动
Dad dad;
vo.push_back(dad); // 左值,拷贝
vo.push_back(std::move(dad)); // 移动

cout << "========Assign========\n";
Dad o1;
Dad o2;
// o2 = o1;
o2 = std::move(o1);

return 0;
}

编译出错,提示我们移动赋值操作被定义为删除的:

1
2
3
4
5
6
7
8
9
> g++ b.cc -std=c++11 -O2 -o b && ./b
b.cc: In function ‘int main()’:
b.cc:105:22: error: use of deleted function ‘Dad& Dad::operator=(Dad&&)’
105 | o2 = std::move(o1);
| ^
b.cc:76:7: note: ‘Dad& Dad::operator=(Dad&&)’ is implicitly deleted because the default definition would be ill-formed:
76 | class Dad
| ^~~
b.cc:76:7: error: non-static const member ‘const int Dad::i’, cannot use default assignment operator

同样的步骤我们可以为Dad类添加一个引用类型成员,并逐步修改main函数测试,编译出错的原因是相同的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int i;

class Dad
{
public:
Dad() : ci(i) {}
// Dad(const Dad &)=default;
// Dad(Dad &&)=default;
// Dad & operator=(const Dad &)=default;
// Dad & operator=(Dad&&d)=default;
// ~Dad()=default;
public:
int & ci;
Son son;
Daughter daughter;
};

编译器提示的错误信息如下:

1
2
3
4
5
6
7
8
9
> g++ b.cc -std=c++11 -O2 -o b && ./b
b.cc: In function ‘int main()’:
b.cc:105:22: error: use of deleted function ‘Dad& Dad::operator=(Dad&&)’
105 | o2 = std::move(o1);
| ^
b.cc:76:7: note: ‘Dad& Dad::operator=(Dad&&)’ is implicitly deleted because the default definition would be ill-formed:
76 | class Dad
| ^~~
b.cc:76:7: error: non-static reference member ‘int& Dad::ci’, cannot use default assignment operator

可见,当存在类成员为引用类型或者是const,此时不允许赋值,这是显而易见的。

类型转换

有时候我们需要在表达式中显示地将对象强制转换为另一种类型参与运算,称作**强制类型转换(cast)。一个命名的强制类型的形式为cast-name(expression)**,其中type是转换的目标而expression是要转换的值。

static_cast

任何具有明确定义的类型转换,且不包含底层const,都可以使用static_cast。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// convert int to double
int a = 9;
int b = 8;
cout << a / b << endl;
cout << static_cast<double>(a) / b <<endl;


// 具有底层const,无法使用static_cast
const char * p = "helloworld";
char * q = static_cast<char *>(p); // compile error
*q = 'H';
cout << q << endl;

// 顶层const可以使用static_cast
char * const x = "aloha";
char * y = static_cast<char *>(x); // 去掉顶层const
y = "hello";

cout << x << endl;
cout << y << endl;
// 转换为string
cout << static_cast<string>(x) << endl;
cout << static_cast<string>(y) << endl;
cout << static_cast<const string>(y) << endl; // 添加顶层const

const_cast

const_cast只能改变运算对象的底层const。对于将常量对象转换为非常量对象,称为去掉const性质。如果对象本身不是常量,则获得写权限是合法的行为,反之对其进行写操作会产生未定义的后果。 const_cast接受的操作对象只能是指针、引用或者指向类类型对象的成员指针。
const_cast与static_cast对比

1
2
3
4
5
const char * sp;
char *q = static_cast<char *>(sp); // compile error
static_cast<string>(sp); // true.字符串字面值转换为string类型
const_cast<string>(sp); // compile error.只能改变常量属性
char * l = const_cast<char*> (sp); // true

使用const_cast获得常量的写操作权限

1
2
3
4
5
void write_const(const char * cp)
{
char * q = const_cast<char*> (cp);
*q = 'H';
}

该函数通过将指向常量的指针参数,去掉其底层const,尝试进行写操纵。
当cp指向真正的常量时,如下main函数

1
2
3
4
5
6
7
int main()
{
const char * sp = "helloworld"; // sp指向字符串常量
write_const(sp);

return 0;
}

编译通过,运行。触发segment fault:

1
2
> g++ main.cc -g -o main && ./main 
fish: Job 1, './main' terminated by signal SIGSEGV (Address boundary error)

若sp指向的是非常量

1
2
3
4
5
6
7
8
9
int main()
{
char arr[] = "helloworld";
const char * sp = arr; // sp指向字符串常量
write_const(sp);
cout << sp << endl;

return 0;
}

编译通过,运行,写入成功:

1
2
> g++ main.cc -g -o main && ./main 
Helloworld

如下程序通过去掉const特性修改string的内容:

1
2
3
4
5
6
7
8
9
10
11
12

int main(int argc, char **argv)
{
const string s = "Hello";
const string & crs = s;
string & rs = const_cast<string &>(crs);
cout << s << ' ' << crs << ' ' << rs<<endl;
rs = "world";
cout << s << ' ' << crs << ' ' << rs<<endl;

return 0;
}

运行结果如下:

1
2
3
> g++ main.cc -g -o a && ./a
Hello Hello Hello
world world world

const_cast操作对象不是指针或引用时会编译出错:

1
2
3
4
5
6
7
8

int main(int argc, char **argv)
{
string s = "hello";
const string cs = const_cast<const string>(s); // the type in a const_cast must be a pointer, reference, or pointer to member to an object type

return 0;
}

reinterpret_cast

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

1
2
3
4
// 将一段存储数字的内存的地址转换为字符串的地址,并输出
int arri[] = {0x6c6c6568, 0x726f776f, 0x646c};
char * p = reinterpret_cast<char *>(arri);
cout << p << endl;

运行结果为

1
2
> g++ main.cc -g -o main && ./main
helloworld

dynamic_cast

dynamic_cast运算符,用于将基类指针或引用安全转换为派生类的指针或引用。使用形式如下

  • dynamic_cast<type *>(e),e必须是一个有效的指针

  • dynamic_cast<type &>(e),e必须是一个左值

  • dynamic_cast<type &&>(e),e不能是左值

其中,type必须是一个类类型,并且通常情况下该类型应该含有虚函数。e的类型必须符合如下三个条件中任一一个:

  • e的类型是目标type的公有派生类
  • e的类型是目标typpe的公有基类
  • e的类型就说目标type的类型
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
class base
{
public:
base()
{
cout << " \n base constructor \n";
}
};

class derived : public base
{
public:
derived()
{
cout << " \n derived constructor \n";
}
};

int main()
{

base *obj = new derived; // case 1: explicitly upcasting
derived *OBJ = dynamic_cast<derived *>(obj); // case 2: error

return 0;
}

此种情况下,base类没有虚函数,编译器会提示错误信息

1
the operand of a runtime dynamic_cast must have a polymorphic class typeC/C++(698)

任一添加虚函数后,程序通过编译(note: 虚函数必须有定义,不能只声明)。

对于转换类型是指针和引用,当转换失败时的行为不同:若一条 dynamic_cast语句的转换目标是指针类型,则结果为0;若转换目标是引用类型,则dynamic_cast运算符抛出bad_cast异常。

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
class base
{
public:
base()
{
cout << " \n base constructor \n";
}
virtual void foo()
{
}
};

class derived : public base
{
public:
derived()
{
cout << " \n derived constructor \n";
}
};

int main()
{
// case 1: pointer
base *obj = new derived; // case 1: explicitly upcasting
derived *OBJ = dynamic_cast<derived *>(obj); // case 2: error

// case 2: reference
derived d;
base &rb = d;
try
{
derived &rd = dynamic_cast<derived &>(rb);
}
catch (const std::exception &e)
{
std::cerr << e.what() << '\n';
}

// case 3: rvalue
// derived &&rrd = dynamic_cast<derived &&>(base());

delete obj;
return 0;
}

上述例子可以运行成功。下面这个例子中的指针和引用转换失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

int main()
{
// case 1: pointer
base *obj = new base;
derived *OBJ = dynamic_cast<derived *>(obj); // fail: return nullptr
assert(OBJ == nullptr);

// case 2: reference
base b;
base &rb = b;
try
{
derived &rd = dynamic_cast<derived &>(rb);
}
catch (const std::exception &e)
{
std::cerr << e.what() << '\n';
}

delete obj;
return 0;
}

输出结果如下

1
std::bad_cast

Ring

ring,即rte_ring,是dpdk中的一种用于存取数据的基础数据结构,本质上是一种无锁实现的环形队列(lock-free)。

Feature

  • FIFO
  • maximum size是固定的,指针存放在一个table中
  • 无锁实现
  • 支持多消费者或单消费者出队
  • 支持多生产者或单生产者入队
  • Bulk operate:入队或出队指定数量的对象,或成功或失败
  • Burst operate:入队或出队指定数量的对象,或成功或返回最大可用对象

Advantages

  • Faster,操作很快只需要简单的CAS指令
  • 比完全无锁实现的队列简单
  • 适用于批量入队/出队操作(bulk operations)。通过操作一个table中的指针完成操作,不会造成大量cache miss。

Disadvantages

  • 大小固定不够灵活
  • 内存使用效率不如链表组织的队列 ,一个空的ring也需要占用内存

Use cases

  • DPDK中应用程序的通信
  • 用于内存池分配器(见mempool章)

Operations

Single Producer Enqueue

初始状态:

  • Step-1: 共享内存中的变量prod_head和cons_tail被复制到本地局部变量prod_head和cons_tail,局部变量prod_next指向表中接下来的一个或几个元素;
  • Step-2: 通过cons_tail判断是否没有足够的空闲空间;
  • Step-3: 取出prod_head处的元素,通过CAS指令判断是否轮到当前线程继续执行,若是则将全局prod_head的值指向局部变量prod_next,反之继续从第一步开始执行;
    中间状态:
  • Step-4: 修改全局prod_tail的值指向全局prod_head。

最终状态:

Single Consumer Dequeue

初始状态:

Steps:

  • Step-1: 将全局cons_head和全局prod_tail复制到本地局部变量cons_head和prod_tail,局部变量cons_next指向table中的下一个或几个元素,如果没有足够空间返回error;
  • Step-2: 通过prod_tail判断是否没有足够的空闲空间;
  • Step-3: 通过CAS指令判断是否由当前线程执行,若是则修改全局cons_head的值执行局部变量cons_next,反之则重新开始循环,执行第一步;
    中间状态:
  • Step-4: 修改全局cons_tail指向cons_head。
    最终状态:

Multiple Producer Enqueue

  • Step-1: 将全局prod_head和全局cons_tail复制到本地局部变量prod_head和cons_tail,局部变量prod_next指向table中的下一个或几个元素,如果没有足够空间返回error;
    初始状态:
  • Step-2: 修改全局prod_head的值,使其指向prod_next,通过CAS指令,原子执行如下操作:
    • 如果全局prod_head和当前局部变量prod_head不同,则CAS指令执行失败,重新执行第一步;
    • 否则,更新全局prod_head指向prod_next,继续执行。

中间状态:

  • Step-3: 直到全局的prod_tail等于当前局部变量prod_tail时,更新全局的prod_tail等于全局的prod_head。

最终状态:

Multiple Consumer Dequeue

  • Step-1: 将全局prod_tail和全局cons_head复制到本地局部变量prod_tail和cons_head,局部变量cons_next指向table中的下一个或几个元素,如果没有足够空间返回error;
  • Step-2: 修改全局cons_head的值,使其指向cons_next,通过CAS指令,原子执行如下操作:
    • 如果全局cons_head和当前局部变量cons_head不同,则CAS指令执行失败,重新执行第一步;
    • 否则,更新全局cons_head指向cons_next,继续执行。
  • Step-3: 直到全局的cons_tail等于当前局部变量cons_tail时,更新全局的cons_tail等于全局的cons_head。

Implementation

struct

两对<head,tail>,包括生产者相关的br_prod_head和br_prod_tail,以及消费者相关的br_cons_head和br_cons_tail,均用volatile修饰,保证每次读取时都是内存中的最新值。

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
struct buf_ring {
volatile uint32_t br_prod_head;
volatile uint32_t br_prod_tail;
int br_prod_size;
int br_prod_mask;
uint64_t br_drops;
uint64_t br_prod_bufs;
uint64_t br_prod_bytes;
/*
* Pad out to next L2 cache line
*/
uint64_t _pad0[11];

volatile uint32_t br_cons_head;
volatile uint32_t br_cons_tail;
int br_cons_size;
int br_cons_mask;

/*
* Pad out to next L2 cache line
*/
uint64_t _pad1[14];
#ifdef DEBUG_BUFRING
struct mtx *br_lock;
#endif
void *br_ring[0];
};

Single Producer Enqueue

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
```
## Single Consumer Dequeue
因为没有其他的consumer,每次唯一的consumer只需要判断当前是否存在可用element即可。
```c

/*
* single-consumer dequeue
* use where dequeue is protected by a lock
* e.g. a network driver's tx queue lock
*/
static __inline void *
buf_ring_dequeue_sc(struct buf_ring *br)
{
// local variable, copy from br_cons_head和prod_tail
uint32_t cons_head, cons_next, cons_next_next;
uint32_t prod_tail;
void *buf;

cons_head = br->br_cons_head;
prod_tail = br->br_prod_tail;

// 下一个和下下一个元素位置
cons_next = (cons_head + 1) & br->br_cons_mask;
cons_next_next = (cons_head + 2) & br->br_cons_mask;

// 没有元素,cons_head指向的是下一个需要消费的"对象"
// prod_tail指向下一个加入的对象的位置
if (cons_head == prod_tail)
return (NULL);

#ifdef PREFETCH_DEFINED
// profetch
if (cons_next != prod_tail) {
prefetch(br->br_ring[cons_next]);
if (cons_next_next != prod_tail)
prefetch(br->br_ring[cons_next_next]);
}
#endif
// 更新全局cnns_head
br->br_cons_head = cons_next;
buf = br->br_ring[cons_head];

#ifdef DEBUG_BUFRING
br->br_ring[cons_head] = NULL;
if (!mtx_owned(br->br_lock))
panic("lock not held on single consumer dequeue");
if (br->br_cons_tail != cons_head)
panic("inconsistent list cons_tail=%d cons_head=%d",
br->br_cons_tail, cons_head);
#endif
// 更新全局cons_tail
br->br_cons_tail = cons_next;
return (buf);
}

Multiple Producer Enqueue

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
/*
* multi-producer safe lock-free ring buffer enqueue
*
*/
static __inline int
buf_ring_enqueue_bytes(struct buf_ring *br, void *buf, int nbytes)
{
// local variable
uint32_t prod_head, prod_next;
uint32_t cons_tail;
int success;
#ifdef DEBUG_BUFRING
int i;
// Check whether buf has enqueued
for (i = br->br_cons_head; i != br->br_prod_head;
i = ((i + 1) & br->br_cons_mask))
if(br->br_ring[i] == buf)
panic("buf=%p already enqueue at %d prod=%d cons=%d",
buf, i, br->br_prod_tail, br->br_cons_tail);
#endif
critical_enter();
do {
// 读取最新的全局prod_head和prod_tail
prod_head = br->br_prod_head;
cons_tail = br->br_cons_tail;

prod_next = (prod_head + 1) & br->br_prod_mask;

// check是否还有空闲空间
if (prod_next == cons_tail) {
critical_exit();
return (ENOBUFS);
}

// 如果当前全局prod_head和本地局部prod_head相同,则更新全局prod_head为prod_next
success = atomic_cmpset_int(&br->br_prod_head, prod_head,
prod_next);
} while (success == 0);
#ifdef DEBUG_BUFRING
if (br->br_ring[prod_head] != NULL)
panic("dangling value in enqueue");
#endif
// 入队,保存buf
br->br_ring[prod_head] = buf;
wmb();

/*
* If there are other enqueues in progress
* that preceeded us, we need to wait for them
* to complete
*/
// 等待已经更新完的线程执行完
while (br->br_prod_tail != prod_head)
cpu_spinwait();
br->br_prod_bufs++;
br->br_prod_bytes += nbytes;
br->br_prod_tail = prod_next;
// 退出临界区
critical_exit();
return (0);
}

Multiple Consumer Dequeue

存在多个consumer,通过实现CAS指令功能的atomic_cmpset_int,同步对全局变量的更新操作。有可能存在一个线程,它在当前线程之前已经完成对全局cons_head更新,该线程的局部cons_next的值比当前线程的局部cons_next小,因此该线程需要先执行完毕对全局cons_tail的修改(否则当前线程更新完全局cons_tail后,这个值会被另一位线程修改为一个更小的值)。

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
 * multi-consumer safe dequeue 
*
*/
static __inline void *
buf_ring_dequeue_mc(struct buf_ring *br)
{
// local variable
uint32_t cons_head, cons_next;
uint32_t prod_tail;
void *buf;
int success;

// 进入临界区
critical_enter();
do {
// 读取最新的全局cons_head和cons_tail
cons_head = br->br_cons_head;
prod_tail = br->br_prod_tail;

// update cons_next
cons_next = (cons_head + 1) & br->br_cons_mask;

// 没有可用元素,退出
if (cons_head == prod_tail) {
critical_exit();
return (NULL);
}

// 更新全局cons_head
success = atomic_cmpset_int(&br->br_cons_head, cons_head,
cons_next);
} while (success == 0);

buf = br->br_ring[cons_head];
#ifdef DEBUG_BUFRING
br->br_ring[cons_head] = NULL;
#endif
rmb();

/*
* If there are other dequeues in progress
* that preceeded us, we need to wait for them
* to complete
*/
// 等待在当前线程更新全局cons_head之前的线程执行完毕
while (br->br_cons_tail != cons_head)
cpu_spinwait();

// update global cons_next and exit
br->br_cons_tail = cons_next;
critical_exit();

return (buf);
}

Another

为什么同时需要cons_head、cons_tail、prod_head和prod_tail?只需要一个消费者头部cons_head和生产者头部prod_head会有什么问题?

生产者线程首先修改prod_head,但是不会修改prod_tail,可以理解为,生产者“预定”了这个位置,然后生产者在该位置写入指向待入队的对象的地址(指针),最后修改prod_tail可以理解为,生产者线程完成了入队操作。因此,仅仅查看prod_head是否移动无法得知生产者是否真正完成了入队操作。尤其对于消费者线程来说,如果消费者线程通过判断prod_head查看是否对内有元素可使用,则会尝试出队一个,生产者还未写入实际对象指针的位置处的指针。

如下是多消费者场景下的出队操作最后更新cons_tail的代码,为什么需要这个while循环?

1
2
3
4
5
6
7
// 等待在当前线程更新全局cons_head之前的线程执行完毕
while (br->br_cons_tail != cons_head)
cpu_spinwait();

// update global cons_next and exit
br->br_cons_tail = cons_next;
critical_exit();

该循环等待在当前线程出队元素之前已经尝试出队的线程,完成出队操作。

reference

[1] DPDK官方文档https://doc.dpdk.org/guides-19.11/prog_guide/ring_lib.html

auto decltype

有时候程序中会出现难以拼写的复杂类型,或者是有时候程序不知道所需要的变量的类型是什么,C++提供了类型别名、auto类型说明符和decltype类型指示符以处理相应的情况。

类型别名

类型别名(type alias)是一个名字,是某种类型的同义词,它让复杂的类型名字变得简单易用。
有两种方法可以用于定义类型别名

  • 传统方法,使用关键字typedef
  • 新标准,使用别名声明,using
    类型别名和类型的名字等价。但是如果某个类型别名指代的是复合类型或常量,可能会产生意想不到的结果。
    1
    2
    3
    4
    5
    6
    7
    typedef char *pstring;  
    char ps[] = "hello";
    const pstring cstr = ps; // cstr是指向char的常量指针
    const char *p = ps; // p是指向常量字符的指针
    cstr[0] = 'J';
    cout << ps << endl; // output Jello
    p[0] = 'K'; // error! cannot change its content

auto类型说明符

有时候需要把一个较为复杂的表达式或者类型难以拼写的变量赋值给一个变量,C++11引入了auto类型说明符,可以让编译器分析表达式的值所属的类型,来推算变量的类型,显然,auto定义的变量必须有初始值。

1
auto item = val1 + val2; // item初始化为val1和val2相加的结果,根据结果可以推断出item的类型

使用auto也能在一条语句中声明多个变量,但语句中所有变量的初始基本数据类型必须一样,即使是可以相互转换的类型也不行

1
2
3
4
5
6
7
8
auto i = 0, *p = &i;     // 正确,i是int类型,p是指向int的指针
auto sz = 0, pi = 3.14; // 错误,sz和pi的类型不一致
float x = 2.1;
auto a = x, b = 3.14; // 错误,x是float类型,而b是double类型
Base1 base1; // Base1是TopLevel的父类
TopLevel t;
Base1 * pb =&t; // 正确,基类类型的指针可以指向子类类型对象
auto u = base1, v = t; // 错误,u和v类型不一致

复合类型、常量和auto

编译器推断出来的auto类型有时和初始值的类型并不完全一样,编译器会适当改变结果类型使其更加符合初始化规则。
使用引用其实是使用引用的对象,因此引用作为初始值时,参与初始化的是引用对象的值,编译器以此为auto的类型。

1
2
int i = 0, &r = i;
auto a = r; // a是int类型

其次,auto一般会忽略掉顶层const,底层const则会保留。

1
2
3
4
5
const int ci = i, &cr = ci; // ci是const int类型,cr是const int&类型
auto b = ci; // b为int,忽略了ci的顶层const
auto c = cr; // c为int,cr是ci的别名,忽略了ci的顶层const
auto d = &i; // d为int*
auto e = &ci; // e为const int*,对常量对象取地址是一种底层const

如果希望推断出的auto类型是一个顶层const,需要明确指出(如上面例子中的cr):

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

可以将引用的类型设为auto,初始值中的顶层const仍然保留。

1
2
3
auto & g = ci; // g是一个整型常量的引用
auto &h = 42; // 错误,非常量引用无法绑定到字面值
const auto &j = 42; // 常量引用可以绑定到字面值

一条语句中定义多个变量时,符合&和*只从属于某个声明符,而非数据类型的一部分。

1
2
3
auto k = ci, &l = i;        // k是int,l是int &
auto &m = ci, *p = &ci; // m是const int &,p是const int *
auto &n = i, *p2 = &cr; // 错误,n是int&,p2是const int *

多维数组循环遍历中的auto

c++11新增了范围for语句,因此可以使用其遍历二维数组:

1
2
3
4
5
6
7
int ia[][4] = {{1,2,3,4}, {5,6,7,8}};
for (const auto& row : ia)
{
for (auto c : row)
cout << c << ' ';
cout << endl;
}

除了最内层的循环外,外层循环的控制变量必须是引用类型,否则auto推断类型会把数组转为指针。

decltype类型指示符

有时候希望从一个表达式的类型推断出所需要定义的变量的类型,但又不想用该表达式的值初始化变量。c++11引入了类型说明符decltype
它的作用是选择并返回操作数的数据类型。编译器并不计算表达式的值。

1
2
3
4
5
6
7
8

int f() { return 1;}
void g() {}
// ……
decltype(f()) i;
decltype(f())* pi = &i;
*pi = 3;
cout << i << endl; // 3

decltype处理顶层const和引用的方式与auto不同,如果decltype使用的表达式是一个变量,则decltype返回该变量的类型(包括顶层const和引用)。

1
2
3
4
const int ci = 0, &cj = ci;
decltype(ci) x=0; // const int
decltype(cj) y = x; // const int &
decltype(cj) z; // const int &,必须初始化

其次,如果decltype使用的表达式不是一个变量,则返回该表达式结果对应的类型,如果表达式结果可赋值,则得到引用类型。

1
2
3
4
5
6
7
8
9
10
int i = 42, *p = &i, &r=i;
decltype(r+0)b; // int
decltype(*p) c; // int &,必须初始化
decltype(*p+0) d; // int
int i = 9;
const int * p = &i;
decltype(*p) u= 9; // const int &,必须初始化
decltype(p) v; // const int *
const int * const q = &i;
decltype(q) w= NULL; // const int * const

如果表达式的结果是左值类型,则返回一个引用,注意还有不可修改的左值——数组:

1
2
3
4
5
6
7
8
9
10
int ia[][4] = {{1,2,3,4}, {5,6,7,8}};
int ib[] = {1,2,3,4};
decltype(ia[1]) x= ib; // x是int(&)[4] ia[1]是一个不可修改的左值
decltype(ib) y; // int[4]
for (decltype(ia[0]) row : ia) // int (&)[4]
{
for (auto c : row)
cout << c << ' ';
cout << endl;
}

对于变量,如果加上了括号,则被当作表达式,而变量是可赋值的特殊表达式,因此会返回引用类型。

1
2
decltype(((i))) d; // int &
decltype((*p + 0)) e; // int,*p+0是一个临时量无法赋值

另外与auto不同的地方是,当数组作为一个auto变量的初始值时,推断得到的类型是指针,而decltype关键字不会发生上述转换:

1
2
3
4
5
int ia[] = {1,2,3,4};
auto ia2(ia); // ia2 是int*,等同于auto ia2(&ia[0])
decltype(ia) ia3 = {0,1,2,3}; // ia3是int[4]
ia3[1] = 9;
return 0;

Avl Tree

有时候我们需要用一个变量存储一个不变的值,例如缓冲区的大小、数组的元素个数等等。好处是我们可以根据需求很容易对其进行调整,并且也能防止程序不小心修改了这个值,此时可以使用关键字const对变量的类型加以限定。

初始化

const对象一旦创建后,其值就无法修改,因此必须初始化。初始值可以是任何复杂的表达式,包括常量和变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
int i = 10; // i是一个已初始化的变量
int j; // j是一个未初始化的变量
const int k = 9; // k是一个常量,必须初始化
cin >> j;
const int a = i;
const int b = j;
const int c = k;
cout << "i = " << i <<endl;
cout << "j = " << j <<endl;
cout << "k = " << k <<endl;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
6
i = 10
j = 6
k = 9
a = 10
b = 6
c = 9
```
当用i、j和k去初始化a、b和c时,根本无须在意是不是用常量初始化,常量特征仅仅在执行改变该对象的操作时才会发生作用。任何试图改变const对象的内容的行为都是非法的。
```c++
a = b; // 错误,不能修改常量的值
const int d; // 错误,常量必须初始化

文件作用域

当以编译时初始化的方式定义一个const对象时,如下

1
const int bufSize = 512;

编译器将在编译过程中把用到该变量的地方都替换成对应的值。为了执行替换,编译器必须知道变量的初始值,如果程序包含多个文件,则每个用了const对象的文件必须能够访问到它的初始值。因此,就必须在每个用到变量的文件中都有对它的定义,为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量,其实等同于在不同文件中分别定义了独立的变量。
main.cc中声明了func()函数,其定义在a.cc中,初始化bufSize = 512,在main函数中打印它的值,并调用func()。

1
2
3
4
5
6
7
8
9
10
11
12
// main.cc


const int bufSize = 512;
void func(); // define in a.cc

int main()
{
cout<< "main.c: " << bufSize<<endl;
func(); // call func
return 0;
}

a.cc中定义了func()函数,其输出定义在本文件中的const变量bufSize。

1
2
3
4
5
6
7
8
// a.cc

const int bufSize = 1024;

void func()
{
std::cout << "a.cc: " << bufSize << endl;
}

命令行编译并执行:

1
2
3
4
rda@pa ~/Downloads> g++ a.cc main.cc -g -o main
rda@pa ~/Downloads> ./main
main.c: 512
a.cc: 1024

可见,定义在不同文件中的同名const对象是不同的。
若需要在文件之间共享const变量,则对于其声明和定义都添加extern关键字,这样只需定义一次就可以了。
为main.cc中的bufSize添加extern关键字。

1
2
3
4
5
6
7
8
9
10
11
12
// main.cc


extern const int bufSize = 512;
void func(); // define in a.cc

int main()
{
cout<< "main.c: " << bufSize<<endl;
func(); // call func
return 0;
}

为a.cc中的bufSize添加extern关键字,并舍去其初始值。

1
2
3
4
5
6
7
8
// a.cc

extern const int bufSize;

void func()
{
std::cout << "a.cc: " << bufSize << endl;
}

再次编译并执行:

1
2
3
rda@pa ~/Downloads> g++ a.cc main.cc -g -o main && ./main
main.c: 512
a.cc: 512

此时bufSize在两个文件之间共享。

const的引用

可以把引用绑定到const对象上,称之为对常量的引用。对常量的引用无法修改它所绑定的对象:

1
2
3
4
const int ci = 1024;
const int &r1 = ci;
r1 = 42; // 错误,常量引用无法修改绑定的对象
int &r2 = ci; // 错误,非常量引用无法绑定到一个常量对象

一般来说,引用的类型必须与其所引用的对象的类型一致,但当初始化常量引用时允许使用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。允许一个常量引用绑定到非常量的对象、字面值甚至是一般表达式:

1
2
3
4
5
int i = 42;
const int & r1 = i; // 常量引用绑定到非常量对象
const int &r2 = 42; // 常量引用绑定到字面值
const int &r3 = 2 * r1; // 常量引用绑定到表达式
int &r4 = r1 * 3; // 错误,非常量引用无法绑定到一个表达式

当一个常量引用绑定到另一种类型上时,如下:

1
2
double dval = 3.14;
const int &ri = dval;

编译器把上述代码变成如下格式:

1
2
const int temp =dval; // temp的值为3
const int &ri = temp;

这种情况下,ri绑定到了一个临时量(temporary)对象,所谓的临时对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。

指针和const

可以令指针指向常量或非常量。指向常量的指针不能用于改变其所指的对象的值,想要存放常量对象的地址,只能使用指向常量的指针。

1
2
3
4
const double pi = 3.14;
double *ptr = &pi; // 错误,常量对象的地址只能使用指向常量的指针保存
const double *cptr = &pi;
*cptr = 4.2; // 错误,指向常量的指针无法改变其所指对象的值

指针的类型必须与其所指对象的类型一致,但是有两个例外,第一个例外是,允许一个指向常量的指针指向一个非常量对象。

1
2
double dval = 3.14;
const double *cptr = &dval;

所谓指向常量的指针或引用,仅仅是自身无法修改所指向的对象或所绑定的对象,而没有规定那个对象的值无法修改。
指针是对象,而引用不是,因此允许把指针本身定义为常量,即常量指针,意味着不变的是指针本身的值而非指向的地址中存储的值。和其他常量一样,常量指针必须初始化,且一旦初始化完成,则它的值(存放在指针中的地址)不能再改变。

1
2
3
4
5
6
7
8
9
10
11
int errNumb = 0;
int anoNum = 1;
int *const curErr = &errNumb; // 常量指针
int *curAno = &errNumb;
const double pi = 3.14159;
const double *const pip = &pi; // 指向常量对象的常量指针

curErr = &anoNum; // 错误,常量指针的值不能修改
curAno = &anoNum; // 非常量指针的值可以修改
*curErr = 23;
*pip = 9.8; // 指向常量对象的指针无法修改指向对象的值

顶层const和底层const

对于指针来说,其本身是一个对象,且指向另一个对象(存储该对象的地址),因此指针本身是不是一个常量以及其指向的是不是一个常量对象是独立的。名词顶层const表示指针本身是个常量,而底层const表示指针所指向的对象是一个常量。更为一般的,顶层const可以表示任意的对象是一个常量。

1
2
3
4
5
6
7
8
9
    int i = 0;
int *const pi = &i; // 顶层const,pi本身的值无法改变
const int ci = 6; // 顶层const,ci本身的值无法改变
const int *p2 = &ci; // 底层const,p2所指向的对象的值无法改变
const int *const p3 = p2; // 左边的const是底层const,右边的const是顶层const
const int &r = ci; // 底层const
// 进行拷贝操作时,拷入和拷出的对象是否为常量没有影响
i = ci;
p2 = p3;

拷贝时,两个对象必须具有相同的底层const资格,或者两个对象的数据类型可以转换,一般来说,非常量可以转换成常量,反之则不行。

1
2
3
4
5
int *p = p3; // 错误,p3和p的底层const不同且常量无法转换为非常量
p2 = p3;
p2 = &i;
int &r=ci; // 错误,普通int&无法绑定到int常量
const int &r2 = ci; // const int & 可以绑定到非常量

重载函数中的const

实参初始化形参时会忽略顶层const,当形参含有顶层const时,传递给它常量对象或非常量对象都是可以的,因此如下重载函数会发生错误。

1
2
3
4
5
6
7
8

void fcn(const int i){
cout << "call const fcn, i = "<< i << endl;
}
void fcn(int i) // 错误,重复定义了fcn
{
cout << "call non-const fcn, i = "<< i << endl;
}

但底层const可以用来区分不同的重载函数,此时,常量对象调用底层const对应版本的函数。

1
2
3
4
5
6
7
8
const int i = 3;
const int &ri =i;
int j = 9;
int &cj = j;
fcn(i);
fcn(ri);
fcn(j);
fcn(cj);

输出结果为:

1
2
3
4
call const fcn, i = 3
call const fcn, i = 3
call non-const fcn, i = 9
call non-const fcn, i = 9

注意,如下情况编译不会报错,但运行时会有错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

void fcn(const int& i){
cout << "call const fcn, i = "<< i << endl;
}
void fcn(const int i) // 错误,重复定义了fcn
{
cout << "call non-const fcn, i = "<< i << endl;
}

int main()
{
const int i = 3;
int j = 5;
fcn(i); // ambiguous
fcn(j); // ambiguous
return 0;
}

无法得知调用的是哪个版本的重载函数。

const限定符

有时候我们需要用一个变量存储一个不变的值,例如缓冲区的大小、数组的元素个数等等。好处是我们可以根据需求很容易对其进行调整,并且也能防止程序不小心修改了这个值,此时可以使用关键字const对变量的类型加以限定。

初始化

const对象一旦创建后,其值就无法修改,因此必须初始化。初始值可以是任何复杂的表达式,包括常量和变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
int i = 10; // i是一个已初始化的变量
int j; // j是一个未初始化的变量
const int k = 9; // k是一个常量,必须初始化
cin >> j;
const int a = i;
const int b = j;
const int c = k;
cout << "i = " << i <<endl;
cout << "j = " << j <<endl;
cout << "k = " << k <<endl;
cout << "a = " << a << endl;
cout << "b = " << b << endl;
cout << "c = " << c << endl;

输出结果如下:

1
2
3
4
5
6
7
6
i = 10
j = 6
k = 9
a = 10
b = 6
c = 9

当用i、j和k去初始化a、b和c时,根本无须在意是不是用常量初始化,常量特征仅仅在执行改变该对象的操作时才会发生作用。任何试图改变const对象的内容的行为都是非法的。

1
2
a = b; // 错误,不能修改常量的值
const int d; // 错误,常量必须初始化

文件作用域

当以编译时初始化的方式定义一个const对象时,如下

1
const int bufSize = 512;

编译器将在编译过程中把用到该变量的地方都替换成对应的值。为了执行替换,编译器必须知道变量的初始值,如果程序包含多个文件,则每个用了const对象的文件必须能够访问到它的初始值。因此,就必须在每个用到变量的文件中都有对它的定义,为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const对象被设定为仅在文件内有效。当多个文件中出现了同名的const变量,其实等同于在不同文件中分别定义了独立的变量。
main.cc中声明了func()函数,其定义在a.cc中,初始化bufSize = 512,在main函数中打印它的值,并调用func()。

1
2
3
4
5
6
7
8
9
10
11
12
// main.cc


const int bufSize = 512;
void func(); // define in a.cc

int main()
{
cout<< "main.c: " << bufSize<<endl;
func(); // call func
return 0;
}

a.cc中定义了func()函数,其输出定义在本文件中的const变量bufSize。

1
2
3
4
5
6
7
8
// a.cc

const int bufSize = 1024;

void func()
{
std::cout << "a.cc: " << bufSize << endl;
}

命令行编译并执行:

1
2
3
4
rda@pa ~/Downloads> g++ a.cc main.cc -g -o main
rda@pa ~/Downloads> ./main
main.c: 512
a.cc: 1024

可见,定义在不同文件中的同名const对象是不同的。
若需要在文件之间共享const变量,则对于其声明和定义都添加extern关键字,这样只需定义一次就可以了。
为main.cc中的bufSize添加extern关键字。

1
2
3
4
5
6
7
8
9
10
11
12
// main.cc


extern const int bufSize = 512;
void func(); // define in a.cc

int main()
{
cout<< "main.c: " << bufSize<<endl;
func(); // call func
return 0;
}

为main.cc中的bufSize添加extern关键字,并舍去其初始值。

1
2
3
4
5
6
7
8
// a.cc

extern const int bufSize;

void func()
{
std::cout << "a.cc: " << bufSize << endl;
}

再次编译并执行:

1
2
3
rda@pa ~/Downloads> g++ a.cc main.cc -g -o main && ./main
main.c: 512
a.cc: 512

此时bufSize在两个文件之间共享。

const的引用

可以把引用绑定到const对象上,称之为对常量的引用。对常量的引用无法修改它所绑定的对象:

1
2
3
4
const int ci = 1024;
const int &r1 = ci;
r1 = 42; // 错误,常量引用无法修改绑定的对象
int &r2 = ci; // 错误,非常量引用无法绑定到一个常量对象

一般来说,引用的类型必须与其所引用的对象的类型一致,但当初始化常量引用时允许使用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。允许一个常量引用绑定到非常量的对象、字面值甚至是一般表达式:

1
2
3
4
5
int i = 42;
const int & r1 = i; // 常量引用绑定到非常量对象
const int &r2 = 42; // 常量引用绑定到字面值
const int &r3 = 2 * r1; // 常量引用绑定到表达式
int &r4 = r1 * 3; // 错误,非常量引用无法绑定到一个表达式

当一个常量引用绑定到另一种类型上时,如下:

1
2
double dval = 3.14;
const int &ri = dval;

编译器把上述代码变成如下格式:

1
2
const int temp =dval; // temp的值为3
const int &ri = temp;

这种情况下,ri绑定到了一个临时量(temporary)对象,所谓的临时对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。

指针和const

可以令指针指向常量或非常量。指向常量的指针不能用于改变其所指的对象的值,想要存放常量对象的地址,只能使用指向常量的指针。

1
2
3
4
const double pi = 3.14;
double *ptr = &pi; // 错误,常量对象的地址只能使用指向常量的指针保存
const double *cptr = &pi;
*cptr = 4.2; // 错误,指向常量的指针无法改变其所指对象的值

指针的类型必须与其所指对象的类型一致,但是有两个例外,第一个例外是,允许一个指向常量的指针指向一个非常量对象。

1
2
double dval = 3.14;
const double *cptr = &dval;

所谓指向常量的指针或引用,仅仅是自身无法修改所指向的对象或所绑定的对象,而没有规定那个对象的值无法修改。
指针是对象,而引用不是,因此允许把指针本身定义为常量。和其他常量对象相同,常量指针必须初始化,且初始化后不能改变指针的值。把*放在const前说明指针本身是一个常量。

1
2
3
4
5
6
7
int errNum = 0;
int i;
int *const curErr = &errNum; // curErr是一个常量指针,将一直指向errNum
*curErr = 4; // 可以修改指向的对象,因为该对象不是常量
curErr = &i; // 错误 无法修改该指针的值
const double pi = 3.14;
const double *const pip = &pi; // pip是一个指向常量对象的常量指针

指针本身是一个常量并不意味着无法通过指针修改其指向对象的值,能否这样做取决于其指向的对象是否为常量。

顶层const和底层const

指针本身是一个对象,因此指针本身是否为常量和其指向的对象是否为常量是相互独立的。名词顶层const表示指针本身是常量,名词底层const表示指向的对象是一个常量。

更一般的,顶层const可以表示任何对象是常量,而底层const则与指针和引用等复合类型的基本类型部分有关。

1
2
3
4
5
6
int i = 0;
int * const p1 = &i; // 顶层const,不能改变p1的值
const int ci = 42; // 顶层const,不能改变ci的值
const int *p2 = &ci; // 底层const,可以改变p2的值但不能通过p2改变其指向对象的值
const int *const p3 = p2; // 左边的是底层const,修饰其指向的对象;右边的是顶层const,修饰其本身
const int &r = ci; // 引用的const都是底层const

执行对象拷贝操作时,常量是顶层const还是底层const有明显区别,顶层const不受影响:

1
2
i = ci;
p2 = p3; // p2和p3指向的对象类型相同

底层const具有限制作用,拷入和拷出的对象必须具备相同的底层const资格,或者两个对象的数据类型必须能够转换,一般来说,非常量可以转换成常量,反之则不行:

1
2
3
4
5
6
7
8
9
10
int *p = p3;  // 错误,p3包含底层const
p2 = p3;
p2 = &i;
int &r = ci; // 错误,普通int&不能绑定到常量
const int &r2 = r;
const int &cr = i;
p2 = &cr;
p = &cr; // 错误,const int *无法转换为int *
int &ri = i;
p2= &ri;

const形参和实参

VS C

c语言中也有const关键字,c++虽然与c保持兼容,但还是存在一定差别,而const关键字上表现出的不同特性就是其一。

本身含义

严格来说,c语言中const修饰的变量并不是常量,只不过其值不允许被改变,因此称为只读变量更合适;因此如下代码是可以通过编译的:

1
2
3
4
5
6
7
8
9
10
11
12
> cat a.c
#include <stdio.h>

const int global;
int main()
{
const int local;
printf("global: %d; local: %d\n", global, local);
return 0;
}⏎
> gcc a.c -o main && ./main
global: 0; local: 22013

可以不初始化const变量的值。

c++中的const修饰的是真正的常量,在compiler-time或者run-time必须被初始化:

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

const int global;
int main()
{
const int local;
cout << "global: " << global << "; local: " << local << endl;
return 0;
}⏎
> g++ a.cc b.cc -g -o main && ./main
a.cc:4:11: error: uninitialized ‘const global’ [-fpermissive]
4 | const int global;
| ^~~~~~
a.cc: In function ‘int main()’:
a.cc:7:15: error: uninitialized ‘const local’ [-fpermissive]
7 | const int local;
| ^~~~~

文件作用域

c++中const修饰的变量文件作用域为本文件内,即internal linkage,因此两个不同的文件中相同的全局const变量是不相互影响的,通过上述描述也可以看出;而c语言中const修饰的变量具有外部链接,即external linkage。分别准备两个c++源文件(a.cc和b.cc)和两个c文件(a.c和b.c),功能类似:

a.cc

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

const int a = 12;
void func();
int main()
{
cout << "a.cc: " << a << endl;
func();
return 0;
}

b.cc

1
2
3
4
5
6
7
8
#include <iostream>
using namespace std;
const int a = 0;

void func()
{
cout << "b.cc: " << a << endl;
}

a.c

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

const int a = 12;
void func();
int main()
{
printf("a.cc: %d\n", a);
func();
return 0;
}

b.c

1
2
3
4
5
6
7
#include <stdio.h>
const int a = 12;

void func()
{
printf("b.cc: %d\n", a);
}

编译运行c++程序,如下:

1
2
3
4
> g++ a.cc b.cc -g -o main
> ./main
a.cc: 12
b.cc: 0

分别打印出各自源文件内定义的const变量的值。

编译运行c程序,如下:

1
2
3
> gcc a.c b.c -g -o main
/usr/bin/ld: /tmp/ccA7931K.o:/home/rda/Templates/b.c:2: multiple definition of `a'; /tmp/ccWlwmJW.o:/home/rda/Templates/a.c:3: first defined here
collect2: error: ld returned 1 exit status

提示链接错误:重复定义变量。