第一章 类型推导
基础 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&&并不是一个万能引用,而是右值引用。




