文章目录
C++的对象类型决定了能对该对象进行的操作,一条表达式是否合法依赖于其中参与运算的对象的类型。一些语言,如Smalltalk和Python等,在程序运行时检查数据类型;与之相反,C++是一种静态数据类型语言,它的类型检查发生在编译时。因此,编译器必须知道程序中每一个变量对应的数据类型。
类型分为非常量和常量,一个常量对象必须初始化,而且一旦初始化其值就不能再改变。此外,还可以定义复合类型,如指针和引用等。复合类型的定义以其他类型为基础。
基本内置类型
C++定义了一套包括算术类型(arithmetic type)和空类型(void)在内的基本数据类型。其中算术类型包含了字符、整型数、布尔值和浮点数。空类型不对应具体的值,仅用于一些特殊的场合,例如最常见的是,当函数不返回任何值时使用空类型作为返回类型。
算术类型
整型(integral type,包括字符和布尔类型在内)和浮点型。
基本算数类型:
类型 | 含义 | 最小尺寸 |
---|---|---|
bool | 布尔类型 | 8bits |
char | 字符 | 8bits |
wchar_t | 宽字符 | 16bits |
char16_t | Unicode字符 | 16bits |
char32_t | Unicode字符 | 32bits |
short | 短整型 | 16bits |
int | 整型 | 16bits (在32位机器中是32bits) |
long | 长整型 | 32bits |
long long | 长整型 | 64bits (是在C++11中新定义的) |
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
long double | 扩展精度浮点数 | 10位有效数字 |
带符号类型和无符号类型
除去布尔型和扩展的字符型之外,其他整型可以划分为带符号的(signed)和无符号的(unsigned)两种。带符号类型可以表示正数、负数或0,无符号类型则仅能表示大于等于0的值。
类型int、short、long和long long都是带符号的,通过在这些类型名前添加unsigned就可以得到无符号类型,例如unsignedlong。类型unsigned int可以缩写为unsigned。
如何选择类型
- 当明确知晓数值不可能是负数时,选用无符号类型;
- 使用
int
执行整数运算。一般long
的大小和int
一样,而short
常常显得太小。除非超过了int
的范围,选择long long
。 - 算术表达式中不要使用
char
或bool
。 - 浮点运算选用
double
。
类型转换
- 非布尔型赋给布尔型,初始值为0则结果为false,否则为true。
- 布尔型赋给非布尔型,初始值为false结果为0,初始值为true结果为1。
- 把一个浮点数赋给整数类型时,进行近似处理,结果值将仅保留浮点数中小数点之前的部分。
- 当把一个整数值赋给浮点类型时,小数部分记为0。如果该整数所占的空间超过了浮点类型的容量,精度可能有损失。
- 当赋给带符号类型一个超出它表示范围的值时,结果是未定义的(undefined)。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据。
切勿混用带符号类型和无符号类型。如果表达式里既有带符号类型又有无符号类型,当带符号类型取值为负时会出现异常结果,这是因为带符号数会自动地转换成无符号数。
字面值常量
一个形如42
的值被称作字面值常量(literal)。
-
整型和浮点型字面值。
可以将整型字面值写作十进制数、八进制数或十六进制数的形式。以0开头的整数代表八进制数,以0x或0X开头的代表十六进制数。
-
字符和字符串字面值。
- 使用空格连接,继承自C。
- 字符字面值:单引号,
'a'
- 字符串字面值:双引号,
"Hello World""
- 编译器在每个字符串的结尾处添加一个空字符(′\0′),因此,字符串字面值的实际长度要比它的内容多1。
-
转义序列。
\n
、\t
等。 -
布尔字面值。
true
,false
。 -
指针字面值。
nullptr
变量
变量提供一个具名的、可供程序操作的存储空间。 C++ 中变量和对象一般可以互换使用。
变量定义的基本形式:类型说明符(type specifier)+ 一个或多个变量名组成的列表,其中变量名以逗号分隔,最后以分号结束。
int sum = 0, value, units_sold = 0;
std::string book("0-201")
C++程序员们在很多场合都会使用对象(object)这个名词。通常情况下,对象是指一块能存储数据并具有某种类型的内存空间。
初始值
当对象在创建时获得了一个特定的值,我们说这个对象被初始化(initialized)了。用于初始化变量的值可以是任意复杂的表达式。
在C++语言中,初始化和赋值是两个完全不同的操作。
初始化不是赋值
- 初始化 = 创建变量时 + 赋予初始值
- 赋值 = 把对象的当前值擦除 + 以新值替代
C++语言定义了初始化的好几种不同形式,这也是初始化问题复杂性的一个体现。
// 定义一个名为 price 的int变量并初始化为0,以下的4条语句都可以
int price = 0;
int price = {0};
int price{0};
int price(0);
列表初始化:使用花括号{}
,如int price{0};
默认初始化
如果定义变量时没有指定初值,则变量被默认初始化(default initialized),默认值由变量类型决定,同时定义变量的位置也会对此有影响。
如果是内置类型的变量未被显式初始化,它的值由定义的位置决定:
- 定义于任何函数体之外,变量被初始化为0
- 定义在函数体内部,内置类型变量将不被初始化(uninitialized)
建议初始化每一个内置类型的变量:未被初始化的内置类型变量的值是未定义的,如果试图拷贝或以其他形式访问此类值将引发错误。
变量声明和定义的关系
为了允许把程序拆分成多个逻辑部分来编写,C++语言支持分离式编译(separatecompilation)机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
为了支持分离式编译,C++语言将声明和定义区分开来。
- 声明(declaration)使名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。
- 定义(definition)创建与名字关联的实体。
只声明而不定义:就在变量名前添加关键字extern,如 extern int i;
。但如果包含了初始值,就变成了定义:extern double pi = 3.14;
变量能且只能被定义一次,但是可以被多次声明。
如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,却绝对不能重复定义。
标识符
C++的标识符(identifier)由字母、数字和下画线组成,其中必须以字母或下画线开头。标识符的长度没有限制,但是对大小写字母敏感。
用户自定义的标识符:
- 不能连续出现两个下画线
- 不能以下画线紧连大写字母开头
- 定义在函数体外的标识符,不能以下画线开头。
变量命名规范:
- 标识符要能体现实际含义
- 变量名一般用小写字母,如 index
- 用户自定义的类名一般以大写字母开头,如 Sales_item
- 如果标识符由多个单词组成,则单词间应有明显区分,如 student_loan 或 studentLoan,不要使用 studentloan
作用域
作用域(scope)是程序的一部分,在其中名字有其特定的含义。C++语言中大多数作用域都以花括号分隔。
main 一般定义于所有花括号之外,它和其他大多数定义在函数体之外的名字一样拥有全局作用域(global scope)。一旦声明之后,全局作用域内的名字在整个程序的范围内都可使用。
一般来说,在对象第一次被使用的地方附近定义它是一种好的选择,因为这样做有助于更容易地找到变量的定义。更重要的是,当变量的定义与它第一次被使用的地方很近时,我们也会赋给它一个比较合理的初始值。
嵌套的作用域
#include <iostream>
int global_var = 10;
int main() {
int local_var = 1;
std::cout << global_var << " " << local_var << std::endl;
int global_var = 100;
std::cout << global_var << std::endl;
std::cout << ::global_var << " " << local_var << std::endl;
return 0;
}
// 10 1
// 100
// 10 1
作用域中一旦声明了某个名字,它所嵌套着的所有作用域中都能访问该名字。同时,允许在内层作用域中重新定义外层作用域已有的名字。
复合类型
复合类型(compound type)是指基于其他类型定义的类型。
引用
引用(reference)是另一个对象的别名,引用类型引用(refers to)另外一种类型。通过将声明符写成&d的形式来定义引用类型,其中d是声明的变量名。例如 :
int ivar = 1024;
int &refVar = ivar; // refVar 指向 ivar(是ivar的另一个名字)
引用必须初始化。
int &refVar2; // 报错
引用和它的初始值绑定(bind)在一起,而不是将初始值拷贝给引用。(一般在初始化变量时,初始值会被拷贝到新建的对象中。)
引用即别名
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。
- 给引用赋值 = 给引用绑定的对象赋值
- 引用的值 = 引用绑定的对象的值
- 引用作为初始值 = 引用绑定的对象作为初始值
int ivar = 1024;
int &refVar = ivar; // refVar 指向 ivar(是ivar的另一个名字)
std::cout << ivar << std::endl; // 1024
std::cout << refVar << std::endl; // 1024
refVar = 2; // 把2赋给 refVar 指向的对象,即赋给了 ivar
std::cout << ivar << std::endl; // 2
std::cout << refVar << std::endl; // 2
赋值:
int i2 = refVar; // 与 i2 = ivar 效果一致
i2 = 10;
std::cout << i2 << std::endl; // 10
std::cout << ivar << std::endl; // 2
std::cout << refVar << std::endl; // 2
引用:
int &i2 = refVar;
i2 = 10;
std::cout << i2 << std::endl; // 10
std::cout << ivar << std::endl; // 10
std::cout << refVar << std::endl; // 10
允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号&开头:
int i1 = 1024, i2 = 2048; // i1和i2都是int
int &r1 = i1, r2 = i2; // r1是i1的引用且与i1绑定,r2是int
int i3 = 1024, &r3 = i3; // r3是int,r3是i3的引用且与i3绑定
int &r4 = i3, &r5 = i2; // r4和r5都是引用
指针
指针(pointer)是“指向(point to)”另外一种类型的复合类型。与引用类似,指针也实现了对其他对象的间接访问。
与引用的不同:
- 指针本身是一个对象,允许对指针赋值、拷贝,在指针的生命周期内可以先后指向几个不同的对象
- 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
指针和引用的主要区别:
- 引用是另一个对象的别名,而指针本身就是一个对象
- 引用必须初始化,且定义了引用就无法再绑定到其他对象;指针无须定义时初始化,也可以重新赋值从而指向其他对象
定义指针类型: int *ip1;
,从右向左读,ip1
是指向int
类型的指针。
获取对象的地址
取地址符(操作符**&**)获取指针存放某个对象的地址。
int ivar = 10;
int *p = &ivar; // p 存放变量ivar的地址,或p是指向变量ivar的指针
// int *pi = ivar; // 非法,不能将int变量赋给指针
一般的,指针的类型都要和它所指向的对象严格匹配。
double dvar;
double *pd = &dvar; // 初始值是double类型对象的地址
double *pd2 = pd; // 初始值是指向double对象的指针
// int *pi = pd 错误,指针pi类型和pd类型不匹配
// pi = &dvar 错误,不能将一个指向int的指针指向double。
指针的值的四种状态:
- 指向一个对象;
- 指向紧邻对象的下一个位置;
- 空指针;
- 无效指针。
指针通过解引用符(操作符*****)访问对象:
int ivar = 10;
int *p = &ivar;
cout << *p; // 10
// 为*p赋值实际上是为p所指的对象赋值
*p = 11;
std::cout << ivar << std::endl; // 11
空指针
空指针(null pointer)不指向任何对象。
几个生成空指针的方法:
int *p1 = nullptr; // 字面值nullptr来初始化指针,C++11新标准引入的一种方法
int *p2 = 0;
int *p3 = NULL; // 名为NULL的预处理变量,这个变量在头文件cstdlib中定义,它的值就是0,需要 #include cstdlib
建议:初始化所有指针。尽量等定义了对象之后再定义指向它的指针。如果实在不清楚指针应该指向何处,就把它初始化为 nullptr 或者0。
赋值和指针
指针和引用都能提供对其他对象的间接访问,具体实现细节上二者大不同:
- 引用本身并非一个对象。一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的那个对象。
- 给指针赋值就是令它存放一个新的地址,从而指向一个新的对象。
int i = 10;
int *pi = 0; // pi被初始化,但是没有指向任何对象,是一个空指针
int *pi2 = &i; // pi2被初始化,指向对象i
int *pi3; // 若pi3定义与块内,则pi3的值无法确定
pi3 = pi2; // pi3和pi2指向同一个对象i
pi2 = 0; // pi2此时不指向任何对象
赋值永远改变的是等号左侧的对象:
pi = &ivar; // pi值被改变,pi指向ivar
*pi = 0; // 指针pi指向的对象ivar的值被改变,指针pi并没有改变
只要指针拥有一个合法值,就能将它用在条件表达式中。
int ivar = 1024
int *pi = 0
int *pi2 = &ivar
if (pi)
// ... pi的值为0,因此条件的值是false
if (pi2)
// ... pi2指向ivar,值不为0,条件的值为true
类型相同的合法指针,可用相等操作符(==)或不相等操作符(!=)来比较,结果是布尔类型。
给定指针 p,不能知道它是否指向了一个合法的对象,因为首先要确定这个指针是不是合法的,才能判断它所指向的对象是不是合法的。
void* 指针
void*是一种特殊的指针类型,可用于存放任意对象的地址,指向任何类型的对象。一个 void* 指针也存放着一个地址。
double obj = 3.14;
*pd = &obj;
void *pv = &obj; // void * 可以是任意类型对象的地址
pv = pd; // void * 可以存放任意类型的指针
理解复合类型的声明
一条定义语句可能定义出不同类型的变量:
int i = 1024, *p = &i, &r = i;
// i是int型的数,p是一个int型指针,r是一个int型引用
定义多个变量
int* p1, p2; // p1是指向int的指针,p2是int
// *仅仅是修饰了p1而已,对该声明语句中的其他变量,它并不产生任何作用
两种写法:
-
修饰符和变量标识符写在一起
int *p1, *p2;
-
修饰符和类型名写在一起,并且每条语句只定义一个变量
int* p3; int* p4;
指向指针的指针
指针是内存中的对象,像其他对象一样也有自己的地址,因此允许把指针的地址再存放到另一个指针当中。
int i = 1024;
int *pi = &i;
int **ppi = π
std::cout << i << *pi << **ppi << std::endl;
指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用。
int i = 1;
int *p;
int *&r = p; // r是一个对指针p的引用
r = &i; // r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0
离变量名最近的符号(此例中是&r的符号&)对变量的类型有最直接的影响,因此r是一个引用。声明符的其余部分用以确定r引用的类型是什么,此例中的符号*说明r引用的是一个指针。最后,声明的基本数据类型部分指出r引用的是一个int指针。
int* ip, i, &r = i; // ip是int型指针,i是int型,r是i的引用
int i, *ip = 0; // i是int型,ip是int型指针(空指针)
int* ip, ip2; // ip是int型指针,ip是int型
面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义。
const限定符
目的:希望定义一些不能被改变值的变量。
- const对象必须初始化,且不能被改变。
- const变量默认不能被其他文件访问,非要访问,必须在指定 const 前加 extern 。
// file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufsize = func();
// file_1.h 头文件
extern const int bufsize; // 与file_1.cc中定义的bufsize是同一个
file_1.cc 定义并初始化了 bufsize。因为这条语句包含了初始值,所以它(显然)是一次定义。又因为 bufsize是一个常量,必须用 extern 加以限定使其被其他文件使用。file_1.h 头文件中的声明也由 extern 做了限定,其作用是指明 bufsize 并非本文件所独有,它的定义将在别处出现。
const int buf; // 不合法, const 对象必须初始化
int cnt = 0; // 合法
const int sz = cnt; // 合法
++cnt; ++sz; // 不合法, const 对象不能被改变
const 的引用
对常量的引用:把引用绑定到 const 对象上。
const int ci = 10;
const int &r1 = ci; // 引用及其对应的对象都是常量
r1 = 20; // 错误,r1是对常量的引用
int &r2 = ci; // 错误,非常量引用不能指向常量对象
临时量(temporary)对象:当编译器需要一个空间来暂存表达式的求值结果时,临时创建的一个未命名的对象。
double var = 3.14;
const int &rvar = var;
std::cout << rvar << std::endl; // 3
引用的类型必须与其所引用对象的类型一致,在初始化常量引用时:
- 允许用任意表达式作为初始值,但表达式结果要能转换成引用的类型
- 允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式
int i = 10;
const int &r1 = i;
const int &r2 = 20;
const int &r3 = r1 * 2;
// int &r4 = r * 2; 错误,r4是一个非常量的引用
对 const 的引用可能引用一个并非 const 的对象。
int i = 10;
int &r1 = i;
const int &r2 = i; // 合法,但是不允许通过r2修改i的值
r1 = 11;
std::cout << r1 << std::endl; // 11
std::cout << r2 << std::endl; // 11
// r2 = 12; 错误,r2是一个常量引用
指针和 const
指向常量的指针,不能用于改变其所指对象的值。
const double pi = 3.14;
// double *ptr = π 错误,ptr是一个普通的指针
const double *cptr = π
// *cptr = 2; 错误,不能给*cptr赋值
指针的类型必须与其所指对象的类型一致,也允许令一个指向常量的指针指向一个非常量对象。
和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。
指向常量的指针仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
const double pi = 3.14;
const double *cptr = π
double var = 3.66; // var是一个双精度浮点数,值可变,但是不能通过cptr改变
cptr = &var;
std::cout << *cptr << std::endl; // 3.66
这样理解:所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值。
const 指针
常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。
把*放在 const 关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int num = 0;
int *const cnum = # // cnum 将一直指向 num
const double pi = 3.14;
const double *const cpi = π // cpi 是一个指向常量对象的常量指针
从右向左阅读:离 cnum 最近的符号是 const,意味着 cnum 本身是一个常量对象,对象的类型由声明符的其余部分确定,声明符中的下一个符号是*,意思是 cnum 是一个常量指针,最后,该声明语句的基本数据类型部分确定了常量指针指向的是一个int对象。同样的,cpi 是一个常量指针,它指向的对象是一个双精度浮点型常量。
能否通过指针修改其所指对象的值,在于所指对象的类型。例如,cpi 是一个指向常量的常量指针,cpi 所指的对象值、自己存储的那个地址都不能改变,cnum 指向一个非常量整数,可用 cnum 去修改 num 的值。
顶层 const
顶层 const 表示指针本身是个常量,底层 const 表示指针所指的对象是一个常量。
更一般的,顶层 const 可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。
int i = 0;
int *const p1 = &i; // 不能改变p1的值,顶层const
const int ci = 10; // 不能改变ci的值,顶层const
const int *p2 = &ci; // 允许改变p2的值,底层const
const int *const p3 = p2; // 第一个const底层const,第二个顶层const,所以p3既是顶层const又是底层const
const int &r = ci; // 用于声明引用的const都是底层const
const int v2 = 0; // v2顶层const
int v1 = v2;
int *p1 = &v1, &r1 = v1;
const int *p2 = &v2, *const p3 = &i, &r2 = v2; // p2底层const,p3既是底层又是顶层const,r2底层const
constexpr 和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。
C++11新标准规定,允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式。
一般来说,如果你认定变量是一个常量表达式,那就把它声明成 constexpr 类型。
处理类型
类型别名
类型别名是某种类型的同义词,让复杂的类型名字变得简单明了、易于理解和使用,有助于程序员清楚地知道使用该类型的真实目的。
传统别名:
typedef double wages; // wages是double的同义词
typedef wagers base, *p; // base是double同义词,p是double*的同义词
新标准别名声明(C++11):
using SI = Sales_item; // SI是Sales_item的同义词
SI item; // 等价于 Sales_item item
指针、常量和类型别名
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。
typedef char *pstring; // pstring等价于char*,pstring是指向char的指针
const pstring cstr = 0; // cstr是一个指向char的常量指针
const pstring *ps; // ps是一个指针,它的对象是指向char的常量指针
声明语句中用到 pstring 时,其基本数据类型是指针。用 char*重写了声明语句后,数据类型就变成了char,*成为了声明符的一部分。
遇到一条使用了类型别名的声明语句时,人们往往会错误地尝试把类型别名替换成它本来的样子,以理解该语句的含义:
const char *cstr = 0; // 是对const pstring cstr = 0的错误理解
这样改写的结果是,const char 成了基本数据类型。前后两种声明含义截然不同,前者声明了一个指向 char 的常量指针,改写后的形式则声明了一个指向 const char 的指针。
auto 类型说明符
auto 类型说明符,让编译器去分析表达式所属的类型。auto 定义的变量必须有初始值。
使用 auto 也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i = 0, *p = &i; // 正确,i整数,p是整型指针
auto sz = 0, pi = 3.14; // 错误,sz和pi类型不一致
编译器会适当地改变结果类型使其更符合初始化规则:
int i = 0, &r = i;
auto a = r; // a是一个整数,因为r是i的别名,而i是一个整数
auto 一般会忽略掉顶层 const:
const int ci = i, &cr = ci;
auto b = ci; // b是一个整数(ci的顶层const特性被忽略)
auto c = cr; // c是一个整数(cr是ci的别名,ci本身是一个顶层const)
auto d = &i; // d是一个整型指针(整数的地址就是指向整数的指针)
auto e = &ci; // e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
decltype类型指示符
- 从表达式的类型推断出要定义的变量的类型。
- decltype:选择并返回操作数的数据类型。
decltype(f()) sum = x;
推断sum
的类型是函数f
的返回类型。- 不会忽略
顶层const
。
decltype 处理顶层 const 和引用的方式与 auto 有些许不同。如果 decltype 使用的表达式是一个变量,则 decltype 该变量的类型(包括顶层 const 和引用在内):
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int&, y绑定到变量x
// decltype(cj) z; 错误,z是一个应用,必须初始化
decltype((variable))的结果永远是引用,而 decltype(variable)结果只有当 variable 本身就是一个引用时才是引用。
自定义数据结构
struct
- 类可以以关键字
struct
开始,紧跟类名和类体。 - 类数据成员:类体定义类的成员。
C++11
:可以为类数据成员提供一个类内初始值(in-class initializer)。
struct Sales_data { /* ... */ };
Sales_data accum, trans, *salesptr;
// 等价于:
struct Sales_data { /* ... */ } accum, trans, *salesptr; // 不建议这样写
type(f()) sum = x;推断
sum的类型是函数
f`的返回类型。
- 不会忽略
顶层const
。
decltype 处理顶层 const 和引用的方式与 auto 有些许不同。如果 decltype 使用的表达式是一个变量,则 decltype 该变量的类型(包括顶层 const 和引用在内):
const int ci = 0, &cj = ci;
decltype(ci) x = 0; // x的类型是const int
decltype(cj) y = x; // y的类型是const int&, y绑定到变量x
// decltype(cj) z; 错误,z是一个应用,必须初始化
decltype((variable))的结果永远是引用,而 decltype(variable)结果只有当 variable 本身就是一个引用时才是引用。