变量和基本类型
2.1基本内置类型
2.1.1算数类型
2.1.2类型转换
类型所能表示的值的范围决定了转换的过程(只列出关键项):
- 当赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。例如,8比特大小的
unsigned char
可以表示0到255区间内的值,如果赋了一个区间以外的值,则实际结果是该值对256取模后所得的余数。因此,把-1赋给8比特大小的unsigned char
所得的结果是255。
当赋给带符号类型一个超出它表示范围的值时,结果是未定义的。
含有无符号类型的表达式
尽管不会故意给无符号对象赋一个负值,却可能(特别容易)写出这么做的代码:
unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // 输出-84
std::cout << u + i << std::endl; // 如果int占32位,输出4294967264
在第二个输出表达式里,相加前首先把整数-42转换成无符号数。把负数转换成无符号数类似于直接给无符号数赋一个负值,结果等于这个负数加上无符号数的模。
当从无符号数中减去一个值时,不管这个值是不是无符号数,都必须确保结果不能是一个负值:
unsigned u1 = 42, u2 = 10;
std::cout << u1 - u2 << std::endl; // 正确:输出32
std::cout << u2 - u1 << std::endl; // 正确:不过,结果是取模后的值
无符号数不会小于0这一事实同样关系到循环的写法:
for (unsigned u = 10; u >= 0; --u) {
// 当u等于0时,-1会被自动转换成一个合法的无符号数,例如4294967295
std::cout << u << std::endl;
}
所以,切勿混用带符号类型和无符号类型。
2.1.3字面值常量
字符和字符串字面值
如果两个字符串字面值位置紧邻且仅由空格、缩进和换行符分隔,则它们实际上是一个整体。
指定字面值的类型
布尔字面值和指针字面值
nullptr
是指针字面值。它被自动转换成各种指针类型,但不会被转换为任何整数类型。
2.2变量
2.2.1变量定义
初始值
通常情况下,对象是指一块能存储数据并具有某种类型的内存空间。
在c++中,初始化和赋值是两个完全不同的操作:初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,并以一个新值来替代。
列表初始化
作为c++11新标准的一部分,用花括号来初始化变量得到了全面应用:
int units_sold = {0};
int units_sold {0};
当用于内置类型的变量时,这种初始化形式有一个重要特点:如果使用列表初始化且初始值存在丢失信息的风险,则编译器会报错。
2.2.2变量声明和定义的关系
为了允许把程序拆分成多个逻辑部分来编写,c++语言支持分离式编译机制,该机制允许将程序分割为若干个文件,每个文件可被独立编译。
为了支持分离式编译,c++语言将声明和定义区分开来。声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明。而定义负责创建与名字关联的实体。
变量声明规定了变量的类型和名字,在这一点上定义与之相同。但是除此之外,定义还申请存储空间,也可能会为变量赋一个初始值。
如果想声明一个变量而非定义它,就在变量名前添加关键字extern
,而且不要显式地初始化变量:
extern int i; // 声明i而非定义i
int j; // 声明并定义j
任何包含了显式初始化的声明即成为定义:
extern double pi = 3.1416;
。在函数体内部,如果试图初始化一个有extern
关键字标记的变量,将引发错误(个人理解:可能是因为函数体内部的变量属于局部变量)。需要注意的是,变量只能被定义一次,但是可以被多次声明。
如果要在多个文件中使用同一个变量,就必须将声明和定义分离。此时,变量的定义必须出现在且只能出现在一个文件中,而其他用到该变量的文件必须对其进行声明,并且绝对不能重复定义。
header.h
extern int i;
int i = 100;
test.cc
#include "header.h"
// 如果去掉extern则是对变量的定义
extern int i;
...
C++是一种静态类型语言,其含义是在编译阶段检查类型。其中,检查类型的过程称为类型检查。
在c++语言中,编译器负责检查数据类型是否支持要执行的运算,如果试图执行类型不支持的运算,编译器将报错并不会生成可执行文件。
2.3复合类型
复合类型是指基于其他类型定义的类型。通俗来说,一条声明语句由一个基本数据类型和紧随其后的一个声明符列表组成。每个声明符命名了一个变量并指定该变量为与基本数据类型有关的某种类型。
2.3.1引用
引用不是对象。
int ival = 1024;
int &refVal = ival; // refVal指向ival(是ival的另一个名字)
int &refVal2; // 报错:引用必须初始化
一般在初始化变量时,初始值会被拷贝到新建的对象中。然而定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另外一个对象,因此引用必须初始化。
引用即别名
引用并非对象,相反的,它只是为一个已经存在的对象所起的另外一个名字。定义了一个引用后,对其进行的所有操作都是在与之绑定的对象上进行的:
refVal = 2; // 把2赋给refVal指向的对象,即ival
int ii = refVal; // 与ii = ival执行结果一样
为引用赋值,实际上是把值赋给了与引用绑定的对象。获取引用的值,实际上是获取了与引用绑定对象的值。同理,以引用作为初始值,实际上是以与引用绑定的对象作为初始值:
// 正确:refVal3绑定到了那个与refVal绑定的对象上,即ival
int &refVal3 = refVal;
// 正确:利用与refVal绑定的对象的值初始化变量,i被初始化为ival的值
int i = refVal;
因为引用本身不是一个对象,所以不能定义引用的引用。而且,引用只能绑定在对象上,而不能与字面值或某个表达式的计算结果绑定在一起。另一方面,除了两种例外情况,其他所有引用的类型都要和与之绑定的对象严格匹配:
int &refVal4 = 10; // 错误:引用类型的初始值必须是一个对象
double dval = 3.14;
int &refVal5 = dval; // 错误:此处引用类型的初始值必须是int对象
2.3.2指针
指针与引用相比有很多不同点。
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象。
- 指针无须在定义时赋初值。和其他内置类型一样,在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值。
除了两种例外情况,其他所有指针的类型都要和它所指向的对象严格匹配:
double dval;
double *pd = &dval; // 正确:初始值是double型对象的地址
double *pd2 = pd; // 正确:初始值是指向double对象的指针
int *pi = pd; // 错误:指针pi的类型和pd的类型不匹配
pi = &dval; // 错误:试图把double型对象的地址赋给int型指针
因为在声明语句中指针的类型实际上被用于指定它所指向对象的类型,所以二者必须匹配。如果指针指向了一个其他类型的对象,对该对象的操作将发生错误。
指针的值(即地址)应属于下列状态之一:
- 指向一个对象。
- 指向紧邻对象所占空间的下一个位置。
- 无效指针,也就是上述情况之外的其他值。
解引用操作仅适用于那些确实指向了某个对象的有效指针。
空指针
空指针不指向任何对象,在试图使用一个指针之前代码可以首先检查它是否为空:
// 等价于int* p1 = 0;(c++11),是一种特殊类型的字面值,可以被转化成任意其他的指针类型
int *p1 = nullptr;
int *p2 = 0; // 直接将p2初始化为字面常量0
int *p3 = NULL; // 需要先#include <cstdlib>,等价于int *p3 = 0;(新标准避免使用NULL)
其他指针操作
只要指针拥有一个合法值,就能将它用在条件表达式中。如果指针的值是0,条件取
false
。
对于两个类型相同的合法指针,可以用==
或!=
来进行比较。如果两个指针存放的地址相同,则它们相等;反之不相等。这里两个指针存放的地址相同有三种可能:它们都为空、都指向同一个对象,或者都指向了同一个对象的下一地址。需要注意的是,一个指针指向某对象,同时另一个指针指向另外对象的下一个地址,此时也有可能出现这两个指针相同的情况,即指针相等。
void*指针
void*
是一种特殊的类型,可用于存放任意对象的地址,但是对该地址中到底是个什么类型的对象并不了解。
利用void*
指针能做的事比较有限:拿它和别的指针比较、作为函数的输入或输出,或者赋给另外一个void*
指针。不能直接操作void*
指针所指的对象,因为并不知道这个对象到底是什么类型,也就无法确定能在这个对象上做哪些操作。
2.3.3理解复合类型的声明
指向指针的引用
引用本身不是一个对象,因此不能定义指向引用的指针。但指针是对象,所以存在对指针的引用:
int i = 42;
int *p;
int *&r = p; // r是一个对指针p的引用
r = &i; // r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; // 解引用r得到i,也就是p指向的对象,将i的值改为0
面对一条比较复杂的指针或引用的声明语句时,从右向左阅读有助于弄清楚它的真实含义,离变量名最近的符号对变量的类型有最直接的影响。
2.4const限定符(重点,并不是真正意义上的常量,更多的是只读)
为了满足值不能改变的要求,可以用关键字
const
对变量的类型加以限定。因为const
对象一旦创建后其值就不能改变,所以必须初始化。一如既往,初始值可以是任意复杂的表达式:
const int i = get_size(); // 正确:运行时初始化
const int j = 42; // 正确:编译时初始化
const int k; // 错误:k是一个未经初始化的常量
默认状态下,const对象仅在文件内有效
当以编译时初始化的方式定义一个
const
对象时,编译器将在编译过程中把用到该变量的地方都替换成对应的值。
为了执行上述替换,编译器必须知道变量的初始值。如果程序包含多个文件,则每个用了const
对象的文件都必须得能访问到它的初始值才行。要做到这一点,就必须在每一个用到变量的文件中都有对它的定义。为了支持这一用法,同时避免对同一变量的重复定义,默认情况下,const
对象被设定为仅在文件内有效。当多个文件中出现了同名的const
变量时,其实等同于在不同文件中分别定义了独立的变量。
某些时候有这样一种const
变量,它的初始值不是一个常量表达式,但又确实有必要在文件间共享。这种情况下,不希望编译器为每个文件分别生成独立的变量。相反,想让这类const
对象像其他(非常量)对象一样工作,也就是说,只在一个文件中定义const
,而在其他多个文件中声明并使用它。
解决办法是,对于const
变量不管是声明还是定义都添加extern
关键字,这样只需定义一次就可以了:
header.h
int fcn(void);
extern const int bufSize = fcn();
int fcn() {
return 1024;
}
test.cc
#include "header.h"
...
// 如果去掉extern则是对变量的定义
extern const int bufSize; // 指明bufSize并非本文件所独有,它的定义将在别处出现
int main() {
cout << bufSize << endl;
return 0;
}
2.4.1const的引用
可以把引用绑定到
const
对象上,就像绑定到其他对象一样,称之为对常量的引用。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
const int ci = 1024;
const int &r1 = ci; // 正确:引用及其对应的对象都是常量
r1 = 42; // 错误:r1是对常量的引用
int &r2 = ci; // 错误:视图让一个非常量引用指向一个常量对象
初始化和对const的引用
引用的类型必须与其所引用对象的类型一致,但是也有例外:
- 第一种例外情况就是在初始化常量引用时,允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式:
int i = 42;
const int &r1 = i;
const int &r2 = 42;
const int &r3 = r1 * 2;
int &r4 = r1 * 2; // 错误:r4是一个普通的非常量引用
- 要理解这种例外情况的原因,最简单的办法是弄清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么:
double dval = 3.14;
const int &ri = dval;
// 等价于
const int temp = dval; // 由双精度浮点数生成一个临时的整型常量
const int &ri = temp; // 让ri绑定这个临时量
- 在这种情况下,引用绑定了一个临时量对象,即当编译器需要一个空间来暂存表达式的求值结果时临时创建的一个未命名的对象。
对const
的引用可能引用一个并非const
的对象。必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未做限定。因为对象也可能是个非常量,所以允许通过其他途径改变它的值:
int i = 42;
int &r1 = i;
const int &r2 = i;
r1 = 0; // r1并非常量,i的值修改为0
r2 = 0; // 错误:r2是一个常量引用
2.4.2指针和const
类似于常量引用,指向常量的指针不能用于改变其所指对象的值。要想存放常量对象的地址,只能使用指向常量的指针:
const double pi = 3.14;
double *ptr = π // 错误:ptr是一个普通指针
const double *cptr = π
*cptr = 42; // 错误:不能给*cptr赋值
虽然之前提到,指针的类型必须与其所指对象的类型一致,但是也有例外。其中之一是,允许令一个指向常量的指针指向一个非常量对象:
double dval = 3.14; // dval是一个双精度浮点数,它的值可以改变
cptr = &dval; // 正确:但是不能通过cptr改变dval的值
所谓指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象的值不能通过其他途径改变。
不过需要注意的是,变量的类型必须一致。
const指针
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定为常量。常量指针必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了(不能指向其他对象)。把
*
放在const
关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb = 0;
int *const curErr = &errNumb; // curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = π // pip是一个指向常量对象的常量指针
要想弄清楚这些声明的含义最行之有效的办法是从右向左阅读。
指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型。例如,pip
是一个指向常量的常量指针,则不论是pip
所指的对象值还是pip
自己存储的那个地址都不能改变。相反的,curErr
指向的是一个一般的非常量整数,那么就完全可以用curErr
去修改errNumb
的值:
*pip = 2.72; // 错误:pip是一个指向常量的指针
// 如果curErr所指的对象(也就是errNumb)的值不为0
if (*curErr) {
errorHandler();
*curErr = 0; // 正确:把curErr所指的对象的值重置
}
2.4.3顶层const
指针本身是不是常量以及指针所指的是不是一个常量是两个相互独立的问题。用名词顶层
const
表示指针本身是个常量,而用名词底层const
表示指针所指的对象是一个常量。
更一般的,顶层const
可以表示任意的对象是常量,这一点对任何数据类型都适用,如算数类型、类、指针等。底层const
则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const
也可以是底层const
,这一点和其他类型相比区别明显:
// 自身的值不能被改变的即是顶层const
int i = 0;
int *const p1 = &i; // 不能改变p1的值,这是一个顶层const
const int ci = 42; // 不能改变ci的值,这是一个顶层const
const int *p2 = &ci; // 允许改变p2的值,这是一个底层const
const int *const p3 = p2; // 靠右的const是顶层const,靠左的是底层const
const int &r = ci; // 用于声明引用的const都是底层const
当执行对象的拷贝操作时,常量是顶层
const
还是底层const
区别明显。其中,顶层const
不受什么影响:
i = ci; // 正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2 = p3; // 正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响
执行拷贝操作并不会改变被拷贝对象的值,因此,顶层
const
拷入和拷出的对象是否是常量都没什么影响。另一方面,底层const
的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const
资格,或者两个对象的数据类型必须能够转换(个人理解:由于底层const
表示与之相关连的对象是一个常量,因此,如果进行拷贝操作时,相当于是改变被拷入对象所关联对象的常量属性,因此需要作出限制)。一般来说,非常量可以转换成常量,反之则不行:
int *p = p3; // 错误:p3包含底层const的定义,而p没有
p2 = p3; // 正确:p2和p3都是底层const
p2 = &i; // 正确:int*能转换成const int*
int &r = ci; // 错误:普通的int&不能绑定到int常量上
const int &r2 = i; // 正确:const int&可以绑定到一个普通int上
p3
既是顶层const
也是底层const
,拷贝p3
时可以不在乎它是一个顶层const
,但是必须清楚它指向的对象得是一个常量。因此,不能用p3
去初始化p
,因为p
指向的是一个普通的(非常量)整数。另一方面,p3
的值可以赋给p2
,是因为这两个指针都是底层const
,尽管p3
同时也是一个常量指针(顶层const
),仅就这次赋值而言不会有什么影响。
2.4.4constexpr和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的
const
对象也是常量表达式。
一个对象(或表达式)是不是常量表达式由它的数据类型和初始值共同决定:
const int max_files = 20; // max_files是常量表达式
const int limit = max_files + 1; // limit是常量表达式
int staff_size = 27; // staff_size不是常量表达式
const int sz = get_size(); // sz不是常量表达式,因为具体值直到运行时才能获取到
constexpr变量
在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。当然可以定义一个
const
变量并把它的初始值设为我们认为的某个常量表达式,但在实际使用时,尽管要求如此,却常常发现初始值并非常量表达式的情况。可以这么说,在这种情况下,对象的定义和使用根本是两回事。
C++11新标准规定,允许将变量声明为constexpr
类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr
的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int sz = size(); // 只有当size是一个constexpr函数时才是一条正确的声明语句
所以一般来说,如果认定变量是一个常量表达式,那就把它声明成
constexpr
类型。
字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明
constexpr
时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把它们称为字面值类型。
算数类型、引用和指针都属于字面值类型。自定义类、IO库、string
类型则不属于字面值类型,也就不能被定义成constexpr
。尽管指针和引用都能定义成constexpr
,但它们的初始值却受到严格限制。一个constexpr
指针的初始值必须是nullptr
或者0,或者是存储于某个固定地址中的对象。
需要注意的是,函数体内定义的变量一般来说并非存放在固定地址中,因此constexpr
指针不能指向这样的变量。相反的,定义于所有函数体之外的对象其地址固定不变,能用来初始化constexpr
指针。同样的,允许函数定义一类有效范围超出函数本身的变量(例如static
变量),这类变量和定义在函数体之外的变量一样也有固定地址,因此,constexpr
引用能绑定到这样的变量上,constexpr
指针也能指向这样的变量。
指针和constexpr
必须明确一点,在
constexpr
声明中如果定义了一个指针,限定符constexpr
仅对指针有效,与指针所指的对象无关,其中的关键在于constexpr
把它所定义的对象置为了顶层const
:
const int *p = nullptr; // p是一个指向常量的指针
constexpr int *q = nullptr; // q是一个指向整数的常量指针
与其他常量指针类似,
constexpr
指针既可以指向常量(此时需要加上const
表明指向的是一个常量才行)也可以指向一个非常量:
constexpr int *np = nullptr; // np是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 42; // i的类型是整型常量
// i和j都必须定义在函数体之外,这样地址才是固定的
constexpr const int *p = &i; // p是常量指针,指向整型常量i
constexpr int *p1 = &j; // p1是常量指针,指向整数j
2.5处理类型(重点)
2.5.1类型别名
类型别名是一个名字,它是某种类型的同义词。有两种方法可用于定义类型别名。传统的方法是使用关键字
typedef
:
typedef double wages; // wages是double的同义词
typedef wages base, *p; // base是double的同义词,p是double*的同义词
其中,关键字
typedef
作为声明语句中的基本数据类型的一部分出现。含有typedef
的声明语句定义的不再是变量,而是类型别名。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。
新标准规定了一种新的方法,使用别名声明来定义类似的别名:using SI = Sales_item;
。类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。
指针、常量和类型别名
如果某个类型别名指代的是复合类型或常量,那么把它用到声明语句里就会产生意想不到的后果。例如下面的声明语句用到了类型
pstring
,它实际上是类型char*
的别名:
// 基本数据类型是指针,使用别名不能改变自身基本数据类型的定义
typedef char *pstring;
// const是对给定类型的修饰,由于pstring是指针,因此const修饰的是指针
const pstring cstr = 0; // cstr是指向char的常量指针
// const typedef常见问题,注意const pstring不等于const char*,而是char *const,表明是常量指针
const pstring *ps; // ps是一个指针,它的对象是指向char的常量指针
2.5.2auto类型说明符
编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非那么容易,有时甚至根本做不到。
为了解决这个问题,c++11新标准引入了auto
类型说明符,用它就能让编译器代替去分析表达式所属的类型。和原来那些只对应一种特定类型的说明符(比如double
)不同,auto
让编译器通过初始值来推算变量的类型。显然,auto
定义的变量必须有初始值:
// 由val1和val2相加的结果可以推算出item的类型
auto item = val1 + val2;
使用
auto
也能在一条语句中声明多个变量。因为一条声明语句只能有一个基本数据类型,所以该语句中所有变量的初始基本数据类型都必须一样:
auto i = 0, *p = &i; // 正确:i是整数,p是整型指针
auto sz = 0, pi = 3.14; // 错误:sz和pi的类型不一致
复合类型、常量和auto
编译器推断出来的
auto
类型有时候和初始值的类型并不完全一样,编译器会适当地改变结果类型使其更符合初始化规则:
- 首先,正如所熟知的那样,使用引用其实是使用引用的对象,特别是引用被用作初始值时,真正参与初始化的其实是引用对象的值。此时编译器以引用对象的类型作为
auto
的类型:
int i = 0, &r = i;
auto a = r; // a是一个整数(r是i的别名,而i是一个整数)
- 其次,
auto
一般会忽略掉顶层const
,同时底层const
则会保留下来,比如当初始值是一个指向常量的指针时:
const int ci = i, &cr = ci;
auto b = ci; // b是一个整数(ci的顶层const特性被忽略掉了)
auto c = cr; // c是一个整数(cr是ci的别名,ci本身是一个顶层const,编译器以引用对象的类型作为auto的类型)
auto d = &i; // d是一个整型指针
auto e = &ci; // e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
- 如果希望推断出的
auto
类型是一个顶层const
,需要明确指出:
const auto f = ci; // ci的推演类型是int,f是const int
- 还可以将引用的类型设为
auto
,此时原来的初始化规则仍然适用:
auto &g = ci; // g是一个整型常量引用,绑定到ci
auto &h = 42; // 错误:不能为非常量引用绑定字面值
const auto &j = 42; // 正确:可以为常量引用绑定字面值
- 设置一个类型为
auto
的引用时,初始值中的顶层常量属性仍然保留。和往常一样,如果给初始值绑定一个引用,则此时的常量就不是顶层常量了。
要在一条语句中定义多个变量,切记,符号
&
和*
只从属于某个声明符,而非基本数据类型的一部分,因此,初始值必须是同一种类型:
auto k = ci, &l = i; // k是整数,l是整型引用
auto &m = ci, *p = &ci; // m是对整型常量的引用,p是指向整型常量的指针
// i的类型是int,而p2的类型是const int,两个变量不是同一种类型
auto &n = i, *p2 = &ci;
2.5.3decltype类型指示符
有时会遇到这种情况:希望从表达式的类型推断出要定义的变量的类型,但是不想用该表达式的值初始化变量。为了满足这一要求,c++11新标准引入了第二种类型说明符
decltype
,它的作用是选择并返回操作数的数据类型。在此过程中,编译器分析表达式并得到它的类型,却不实际计算表达式的值:
decltype(f()) sum = x; // sum的类型就是函数f的返回类型
编译器并不实际调用函数
f
,而是使用当调用发生时f的返回值类型作为sum
的类型。
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
处是一个例外。
decltype和引用
如果
decltype
使用的表达式不是一个变量,则decltype
返回表达式结果对应的类型。有些表达式将向decltype
返回一个引用类型。一般来说,当这种情况发生时,意味着该表达式的结果对象能作为一条赋值语句的左值:
// decltype的结果可以是引用类型
int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // 正确:加法的结果是int,因此b是一个未初始化的int
decltype(*p) c; // 错误: c是int&,必须初始化
因为
r
是一个引用,因此decltype(r)
的结果是引用类型。如果想让结果类型是r
所指的类型,可以把r
作为表达式的一部分,如r + 0
,显然这个表达式的结果将是一个具体值而非一个引用。
另一方面,如果表达式的内容是解引用操作,则decltype
将得到引用类型。正如若熟悉的那样,解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,decltype(*p)
的结果类型就是int&
,而非int
。
需要注意的是,decltype((variable))
的结果永远是引用,而decltype(variable)
的结果只有当variable
本身就是一个引用时才是引用。
2.6自定义数据结构
2.6.3编写自己的头文件
头文件通常包含那些只能被定义一次的实体,如类、
const
和constexpr
变量等。头文件也经常用到其他头文件的功能,例如string.h
,所以有必要在书写头文件时做适当处理,使其遇到多次包含的情况也能安全和正常地工作。
预处理器概述
确保头文件多次包含仍能安全工作的常用技术是预处理器,是在编译之前执行的一段程序,可以部分地改变缩写的程序。例如
#include
。
C++程序还会用到的一项预处理功能是头文件保护符,头文件保护符依赖于预处理变量。预处理变量有两种状态:已定义和未定义。#define
指令把一个名字设为预处理变量,另外两个指令则分别检查某个指令的预处理变量是否已经定义:#ifdef
当且仅当变量已定义时为真;#ifndef
当且仅当变量未定义时为真。一旦结果检查为真,则执行后续操作直至遇到#endif
指令为止。使用这些功能就能有效地防止重复包含的发生:
#ifndef SALE_DATA_H
#define SALES_DATA_H
#include <string> // 需要注意的是,string头文件中也有相应的头文件保护符
struct Sales_date {
std::string bookno;
unsigned units_sold = 0;
double revenue = 0.0;
};
#endif
需要注意的是,预处理变量无视c++语言中关于作用域的规则。
总结
- 常量引用可以指向不同类型的对象,只要能够相互转换即可。而指针必须和所指对象的类型保持一致。
- 顶层
const
指的是自身是一个常量,而底层const
则是所指或者所引用的对象是一个常量。对常量的引用永远是一个底层const
,而指针自身既可以是顶层,也可以是底层。- 指向常量的指针是底层
const
,即不能通过该指针改变所指对象的值,但是有可能存在其他指向该对象的普通指针,或者引用,它们不受到限制,可以更改对象的值。- 常量指针可以改变所指对象的值,但是不能再指向其他对象(即指针中的地址不能改变)。
- 当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层
const
资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行。- 一个
constexpr
指针的初始值必须是nullptr
或者0,或者是存储于某个固定地址中的对象。- 关键在于
constexpr
把它所定义的对象置为了顶层const
。const
是对给定类型的修饰,例如typedef char *pstring;
,由于pstring
是指针,因此const
修饰的是指针。- 编译器以引用对象的类型作为
auto
的类型。其中,顶层const
会被忽略,而底层const
则会被保留下来。如果希望推断出的auto
类型是一个顶层const
,需要明确指出。- 如果
decltype
使用的表达式是一个变量,则decltype
返回该变量的类型,包括顶层const
和引用在内。- 解引用指针可以得到指针所指的对象,而且还能给这个对象赋值。因此,
decltype(*p)
的结果类型就是int&
,而非int
。需要注意的是,decltype((variable))
的结果永远是引用,而decltype(variable)
的结果只有当variable本身就是一个引用时才是引用。