第一章 类型推导
基础1:顶层const和底层const
在 const int* const p = new int(10); 中,同时具有顶层const与底层const。顶层const表示修饰的元素本身不可变,如 const int a = 10;,底层const表示指向的内容不可变,常量引用与常量指针相同,如 const int &ra = 10;。

《Primer C++》P58提到,当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响;另一方面,底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行。
我们可以看到指针pa不是任意一种const,因此并不能被p赋值。我们并不关心pa的指向是否可变,反而更注重pa指向的数据是否可变,也就是底层const,这关系到赋值是否合法。

对于const int a = 10;,虽然a是顶层const,但对a取地址(const int* pb = &a;)时将变为底层const。这是因为常量a从本身不能改变变为指针指向的数据不能改变,因此仍然需要增加底层const来适配。
注:
- 引用不是对象也不进行拷贝,不满足上面的原则。
- 常量引用在左侧时右侧可以跟任何元素:
const int &ra = 10; // ...,但去除const后会报错。有一说法是在常量引用被字面量赋值时会创建一临时变量tmp,引用的是变量的引用&tmp。并且对于常量a是本身适配的,对于变量int b = 10;也仍然适配,意味ra是b的别名且仍然可以对b进行修改。 - 用常量给非常量引用赋值会引发报错:
int &rb = a; // ERROR,如果允许非常量引用对数据进行修改,则常量失去了意义。 - 引用在等号右侧时忽略引用:在忽略
&ra的&后,ra为常量,这与3是一样的,int &rb = ra; // ERROR。引用本质上是别名,当我们使用引用时,也就是在引用原始的数据。 - 非常量可以被常量引用赋值,这是因为顶层const在元素的赋值中不受什么影响。
基础2:值类型与右值引用
在这节开始前,我们做出如下思考:
1 | int getA(){ |
当我们从函数获取a时,执行了几次拷贝?
答案是2次。当我们从getA中获取数据时,若不进行优化,编译器会先创建一个不具名对象,也叫将亡值(tmp),将a赋值给tmp,最终释放getA的栈帧,将tmp赋值给x,销毁tmp。相当于int tmp = a; int x = tmp;。
C++98的表达式类型中,由是否可以取地址分为左值和右值。左值是指向特定内存的具名对象,右值是临时对象,字符串常量除外。
在int* p = &x++;和int* p = &++x;中,第一个会引发错误。

这是因为我们无法对临时对象取地址,也就是x++无法取地址的原因,而++x直接返回加一后的本身,因此可以进行取地址操作。
值类型
C++11将表达式类型详细的分为泛左值,右值,又将泛左值分为左值和与右值同划分的将亡值,右值除了划分出将亡值外,还划分出纯右值的概念。

| 类别 | 英文全称 | 通俗理解 | 典型例子 |
|---|---|---|---|
| lvalue | left value | 有名字、可以放在等号左边的 “持久” 值 | int a = 10; 中的 a;函数返回左值引用 |
| prvalue | pure rvalue | 纯临时值,生命周期短暂,通常是计算结果 | 10;a + b;std::string("hello") |
| xvalue | eXpiring value | “将亡” 的临时值,生命周期即将结束 | std::move(a);函数返回右值引用 |
| glvalue | generalized lvalue | 泛左值,包含所有有身份的值 | lvalue + xvalue |
| rvalue | right value | 可以被移动的值,是 “临时” 或 “将亡” 的值 | prvalue + xvalue |
在研究右值引用前,还需要探讨一个问题,对于赋值操作,在C++中只有拷贝一种方法吗?
右值引用
很明显以高效为著称的一个语言必然有着其他的赋值方式:拷贝,引用和移动。
- 拷贝意味着我拥有和原数据的全部“经历”,因此十分耗时:
int A = 1;int B = A;。 - 引用操作意味着我与原数据共享“经历”,二者捆绑:
int rA = &A;。 - 移动操作意味着我把原数据的内容拿过来后,原数据死亡。若原数据不死亡的情况下,需要使用
std::move使其死亡:int C = getA();int B = std::move(A);。
那么右值引用和移动语义之间具有什么样的关系?因为左值引用不接受右值,因此出现了右值引用这一概念(Type &&),移动语义(std::move)可以将左值变为右值。
我们可以通过将泛左值转化为将亡值:强制转换 static_cast<type &&>(...); 或使用移动语义std::move(...); ,也可以通过C++17引用的临时量实质化,将纯右值转换为临时对象实现。
移动语义没移动,完美转发不完美。
注:
- 即使是纯右值,也可以进行
std::move - 若类中未实现移动构造,即使使用
std::move也仍然采用拷贝 - 右值的引用仍然是左值:
int x = 10;int &&z = std::move(x);,&z一定可以实现,z作为x的引用,共同指向x的存储地址。 - 如果将右值绑定到右值引用上,连移动都不会发生:
int &&E = std::move(x);意为为x创建一个别名,这与int &rx = x;是一样的,编译器并不会在创建一个别名的时候做些什么。 - 常量引用(
const &&):const int x = 10;int &&z = std::move(x);与基础一提到的(注3)一致,非常量引用被常量赋值时会报错,需要添加底层const。
最后举个例子:
1 | int getNum(const int &num){ |
1 | int makeNum(){ |
1 | int num = makeNum(); |
因此当我们实现移动构造函数后可以大大提升程序的运行效率,否则例子中的一次copy两次move会变为三次copy。
在实践以上内容时,需要关闭编译器的返回值优化(set(CMAKE_CXX_FLAGS "-fno-elide-constructors")),如果关闭禁止返回值优化,即使未实现移动拷贝构造,也只会有一次copy。
基础3:数组指针与函数指针
由于数组的相关数据类型过于复杂,因此C++中建议使用标准模板库替换。在表达式 int array[5] = {1,2,3,4,5}; 的数据类型为 int[5](不是 int*)。
数组指针
对于 int* ptr = array; ,数组名退化为指针,也可以说&array与array表示的意思是一样的。但他与 int (*ptr2)[5] = &array; 不同,后者被称为数组指针,表示数组名取地址的类型,指向数据类型为int,大小为5的数组。
倘若去除括号(int* ptr2[5] = &array;)则表示一个存放指针的数组,被称为指针数组:int *ptr3[5] = {&a,&a,&a,&a,&a};。
ptr类型为 int(*)[5],对数组的引用与数组指针类似,为 int (&ref)[5] = array;,数据类型为 int(&)[5];。
需要注意的是,C++有指针数组但是没有引用数组。因为引用在等号右侧时会被忽略,这又变成了原值本身,这是不合理的。
在前文提到字符串字面值如“hello world”是左值,且数组可以退化为指针,因此我们可以用指针接受它的地址:const int* p = "hello world"; 同样也可以用数组指针 const char (*strptr)[12] = &"hello world"; 和数组引用 const char (&strref)[12] = "hello world"; 来表示。
在汇编中,当我们要给变量赋值时,只有将值存放进寄存器后进行,因为不进入内存所以不会有地址。若用此方法对字符串常量进行赋值,要先构造字符,再进行拷贝(char作为内置类型没有移动)。赋值成功后被清除,若是一直进行构造则会导致效率低下,因此编译器将字符串字面量放进内存省去了重复构造的过程。因为发生了拷贝,显而易见的是 &“hello world” 与 char str[12] = "hello world"; 拿到的地址并不相同。
hello world 除了空格外,还有空终止字符(’\0’),因此是str[12]而不是str[11],否则会报错 “const char [12]” 类型不能用于初始化 “char [11]” 类型。对于const为什么能省去,详情参照基础1顶层const。请注意 const int* p = "hello world"; 中的const虽然去除后只会发出警告,但这里并不建议去除,因为字符串字面量存储在 .rodata 中,并不能进行修改。
数组名作为参数传递
数组名作为参数传递时会发生变化 void fun(int a[100]); 与 void fun(int a[5]);,void fun(int* a); 等价,但与 void fun2(int (*a)[100]);,void fun2(int (*a)[5]); 不等价。这是因为数组作为参数传递时会退化为指针,而 void fun2(int (*a)[5]); 传入的数组指针与只传入数组类型不同。
而函数相关的数据类型中,bool fun(int a,int b); 作为函数数据类型bool(int,int),bool (*funptr)(int a,int b);作为函数指针,类型为bool(*)(int,int)。
- 函数指针的赋值:
funptr = &fun; // &可省略 - 函数指针的使用:
bool c = (*funptr)(1,2); // 可省略为funptr(1,2); - 函数指针作为形参:
void fun2(int c,bool (*funptr)(int,int)); //可省略为void fun2(int c,bool funptr(int,int)); - 函数指针做返回值:
bool (*fun3(int c))(int,int);,需要注意的是,函数指针做返回值时只能用指针返回,不能返回本身。 - 函数的引用:
bool (&funref)(int,int) = fun;
这看起来也太复杂了,特别是函数指针作为返回值时,因此我们接下来引入类型别名的概念。
类型别名
与using比起来,typedef在这里的使用反而令人有些费解:
typedef bool FUNC(int,int); 表示 FUNC == bool(int,int); , typedef bool (*FUNCPTR)(int,int); 表示 FUNCPTR == bool(*)(int,int);
奇怪的点在于类型别名的声明不应该是 typedef bool (*)(int,int) FUNCPTR; 之类的吗?而using则更容易理解:using FUNC = bool(int,int);,using FUNCPTR = bool(*)(int,int);,其中 FUNC* == FUNCPTR。
接上部分的函数指针做返回值,倘若用类型别名简化后便大大方便了返回值的书写方式:FUNC* fun3(int c); 或 FUNCPTR fun3(int c);。
条款1:理解模板类型推导
在开始这一章节前,还需要补充一些知识。
template<typename T>中void f(const T param);与void f(T const param);等价,如果 T 推导为 int*,则两者均为顶层const。顶层const不构成重载,如
fun(int a);与fun(const int a);。指针的引用表示为:i
nt* a = &b;int *&c = a;。在指针与函数引用中,函数指针的底层const只能用类型别名表示:
int func(int a){ return 10; },若函数指针定义为const int (*funcptr)(int) = func;则会报错 “int(*)(int)"类型不能用于初始化 “const int(*)(int)“ 类型。他的意思为int作为传入的参数,const int做返回值的const int (*funptr)(int) = func;与int const (*funptr)(int) = func;均不能被正确定义。CPP认为函数具有语法原子性,不可分割,函数天然不可改变,所以函数指针不需要底层const。
若仍然想要使用底层const呢?可以考虑类型别名的方法:
using T = int(int);后,便可以使用顶层与底层const。另外,函数引用的底层const会被编译器忽略。
在模板中:

ParamType分别为:T,T*,T&,T&&,const T,const T*,const T&,const T&&,T* const,const T* const。

这是因为顶层const不构成重载,因此只讲述黑框中的一个类型。
1 | template<Typename T> |
对于:void f(T param){ std::cout << param; }
| 语句 | 推导 | 类型 |
|---|---|---|
int a = 10; |
int |
void f<int>(int param) |
int *aptr = &a; |
int* |
void f<int*>(int* param) |
int &aref = a; |
int |
void f<int>(int param) |
int &&arref = std::move(a); |
int |
void f<int>(int param) |
值传递时会发生 引用消失(reference removed),因此 int& 与 int&& 都被推导为 int。
| 语句 | 推导 | 类型 |
|---|---|---|
const int ca = 10 |
int |
void f<int>(int param) |
const int *captr = &ca; |
const int* |
void f<const int*>(const int* param) |
const int &caref = ca; |
int |
void f<int>(int param) |
const int &&caref = std::move(ca); |
int |
void f<int>(int param) |
int* const acptr = &a; |
int* |
void f<int*>(int* param) |
const int* const cacptr = &ca; |
const int* |
void f<const int*>(const int* param) |
10 |
int |
void f<int>(int param) |
const int ca = 10 中,顶层const被忽略,因此推导为int,const int *captr = &ca; 底层则不能忽略。const int &caref = ca; 与 const int &&caref = std::move(ca); 均为引用且是顶层const,因此推导出 int,int* const acptr = &a; 为顶层const,推导出int。
| 语句 | 推导 | 类型 |
|---|---|---|
int array[2] = {0,1}; |
int* |
void f<int*>(int* param) |
"hello world" |
const char* |
void f<const int*>(const char *param) |
int (*arrayptr)[2] = &array; |
int(*)[2] |
void f<int (*)[2]>(int (*param)[2]) |
int (&arrayref)[2] = array; |
int* |
void f<int*>(int* param) |
func; // void(int,int) |
void(*)(int,int) |
void f<void(*)(int,int)>(void (*param)(int,int)) |
void (*funcptr)(in,int) = func; |
void(*)(int,int) |
void f<void(*)(int,int)>(void (*param)(int,int)) |
void (&funcref)(int,int) - func; |
void(*)(int,int) |
void f<void(*)(int,int)>(void (*param)(int,int)) |
int array[2] = {0,1}; 退化为指针,推导出int*,"hello world" const数组退化为const int*指针。int (*arrayptr)[2] = &array; 不过数组指针不会退化,为int(*)[2],int (&arrayref)[2] = array; 也会退化,和原始的没有区别。func; // void(int,int) 函数会退化为指针,void (*funcptr)(in,int) = func; 不会退化,引用则是被编译器忽略,同上。
对于:void f(T *param){ std::cout << param; }
| 语句 | 推导 | 类型 |
|---|---|---|
int a = 10; |
- | - |
int *aptr = &a; |
int |
void f<int>(int* param) |
int &aref = a; |
- | - |
int &&arref = std::move(a); |
- | - |
T* 表示 param 必须传入一个指针,int a = 10; 传入报错,int *aptr = &a; 则退出 int,与 * 共同组合成 ParamType。int &aref = a; int &&arref = std::move(a); 与 int a = 10; 相同,均应该传入一个指针。
| 语句 | 推导 | 类型 |
|---|---|---|
const int ca = 10 |
- | - |
const int *captr = &ca; |
const int* |
void f<const int>(const int* param) |
const int &caref = ca; |
- | - |
const int &&caref = std::move(ca); |
- | - |
int* const acptr = &a; |
int |
void f<int>(int* param) |
const int* const cacptr = &ca; |
const int* |
void f<const int>(const int* param) |
10 |
- | - |
const int ca = 10 const int &caref = ca; const int &&caref = std::move(ca); 均报错,const int* const cacptr = &ca; 中底层const不能忽略,推导出const int,ParamType为const int*,int* const acptr = &a; 中顶层const可以忽略,推出int*。const int* const cacptr = &ca; 与底层const一样,传入10则报错。
| 语句 | 推导 | 类型 |
|---|---|---|
int array[2] = {0,1}; |
int |
void f<int>(int* param) |
"hello world" |
const char |
void f<const char>(const char* param) |
int (*arrayptr)[2] = &array; |
int[2] |
void f<int[2]>(int (*param)[2]) |
int (&arrayref)[2] = array; |
int |
void f<int>(int* param) |
func; // void(int,int) |
void(int,int) |
void f<void(int,int)>(void (*param)(int,int)) |
void (*funcptr)(int,int) = func; |
void(int,int) |
void f<void(int,int)>(void (*param)(int,int)) |
void (&funcref)(int,int) = func; |
void(int,int) |
void f<void(int,int)>(void (*param)(int,int)) |
int array[2] = {0,1}; 退化为指针,T推出int,ParamType为int*,"hello world" 退化为 const char*,底层const不可忽略。func; 退化为指针,T推出void(int,int),推导为void(*param)(int,int)。
对于:void f(T ¶m){ std::cout << param; }
| 语句 | 推导 | 类型 |
|---|---|---|
int a = 10; |
int |
void f<int>(int& param) |
int *aptr = &a; |
int* |
void f<int*>(int*& param) |
int a = 10; 中T为int,ParamType推出为引用,int *aptr = &a; 中T为int*类型,则会有 int*¶m 的指针引用的类型。
| 语句 | 推导 | 类型 |
|---|---|---|
const int ca = 10 |
const int |
void f<const int>(const int& param) |
const int *captr = &ca; |
const int* |
void f<const int*>(const int*& param) |
int* const acptr = &a; |
int* const |
void f<int* const>(int* const& param) |
const int* const cacptr = &ca; |
const int* const |
void f<const int* const>(const int* const& param) |
10 |
- | - |
const int ca = 10 中虽然拷贝时为顶层const,但随着引用被转换为底层,所以T必须是const int。const int *captr = &ca; 同理,推导为 const int*,后加&。int* const acptr = &a; 顶层const推出 int* const ¶m,顶层底层const都存在则都推导:const int* const。 传入10会因为用右值给左值引用赋值而报错。
| 语句 | 推导 | 类型 |
|---|---|---|
int array[2] = {0,1}; |
int[2] |
void f<int[2]>(int (¶m)[2]) |
"hello world" |
const char[12] |
void f<const char[12]>(const char (¶m)[12]) |
int (*arrayptr)[2] = &array; |
int (*)[2] |
void f<int (*)[2]>(int (*¶m)[2]) |
func; // void(int,int) |
void(int,int) |
void f<void(int,int)>(void (¶m)(int,int)) |
void (*funcptr)(int,int) = func; |
void(*)(int,int) |
void f<void(*)(int,int)>(void (*¶m)(int,int)) |
void (&funcref)(int,int) = func; |
void(int,int) |
void f<void(int,int)>(void (¶m)(int,int)) |
int array[2] = {0,1}; 中数组的引用不会退化,因此为 int(¶m)[12] 而不是 int[12],"hello world" 为 const char (¶m)[12],void (*funcptr)(in,int) = func; 为指针的引用。
对于:void f(const T *param){ std::cout << param; }
| 语句 | 推导 | 类型 |
|---|---|---|
int a = 10; |
- | - |
int *aptr = &a; |
int |
void f<int>(const int* param) |
int &aref = a; |
- | - |
int &&arref = std::move(a); |
- | - |
int a = 10; 报错,int *aptr = &a; 由于是拷贝,所以可以将非常量指针赋给常量,T为int,推出const int*。
| 语句 | 推导 | 类型 |
|---|---|---|
const int ca = 10 |
- | - |
const int *captr = &ca; |
int |
void f<int>(const int* param) |
const int &caref = ca; |
- | - |
const int &&caref = std::move(ca); |
- | - |
int* const acptr = &a; |
int |
void f<int>(const int* param) |
const int* const cacptr = &ca; |
int |
void f<int>(const int* param) |
10 |
- | - |
const int *captr = &ca; 为底层const,但由于模板本身为底层const,因此推导为int。int* const acptr = &a; 顶层const被忽略,仍为const int*。
| 语句 | 推导 | 类型 |
|---|---|---|
int array[2] = {0,1}; |
int |
void f<int>(const int* param) |
"hello world" |
char |
void f<char>(const char* param) |
int (*arrayptr)[2] = &array; |
int[2] |
void f<int[2]>(const int (*param)[2]) |
int (&arrayref)[2] = array; |
int |
void f<int>(const int* param) |
func; // void(int,int) |
void(int,int) |
void f<void(int,int)>(void (*param)(int,int)) |
void (*funcptr)(int,int) = func; |
void(int,int) |
void f<void(int,int)>(void (*param)(int,int)) |
void (&funcref)(int,int) = func; |
void(int,int) |
void f<void(int,int)>(void (*param)(int,int)) |
func; // void(int,int) 函数因为会退化为指针,而函数指针的底层const只能用类型别名表示,因此T什么都推不出来,报错。
对于:void f(const T ¶m){ std::cout << param; }
因为常量引用在左侧,因此可以传入任何值。func; 虽不会退化,但函数的底层const会被忽略,因此推导为void f<void (int,int)>(void (¶m)(int,int))。
对于:void f(T &¶m){ std::cout << param; }
需要引入引用折叠的概念:int&与int&&可以折叠为int&,如int a = 10;,f(a) 中T被推导为int&,类型为int ¶m,三个引用引发变量折叠。而const T&&并不是一个万能引用,而是右值引用。




