前置引述
有时我们希望定义这样一种变量,它的值不能被改变。为了满足这一要求,可以用关键字const对变量的类型加以限定:
const int bufSize = 512 ;
一个常量,是不能被修改的,任何对常量的修改都会报错。
bufSize = 512 ; //错误,bufSize在上面已被初始化
因为const对象一旦创建后其值就不能改变,所以const对象必须初始化。一如既往,初始值可以是任意复杂的表达式:
const int i = get_size();
const int j = 42;
const int k ;//错误,k是一个未经初始化的常量
const对象和跨文件
当以编译时初始化的方式定义一个const对象时,就如对bufSize的定义一样:
const int bufSize = 512;
编译器将在编译过程中把用到该变量的地方都替换成对应的值。也就是说,编译器会找到代码中所有用到bufSize的地方,然后用512替换。
为了能执行上述替换,编译器必须知道变量的初始值。程序如果包含多个文件,则每个用了const对象的文件都必须得访问到它的初始值才行。所以,解决方法就是用extern关键字,将const对象声明与定义(必须初始化)分离(在1.C++–关于文件间共享代码的方法 有提及)。
//file.cpp
extern const int bufSize = fcn();
//file.h
extern const int bufSize;//就是上述bufSize的声明
声明和定义都必须加extern关键字(跟一般变量不同)。(但我用vs2013测试了一下,发现定义的时候并不需要加上extern关键字,艹)(看来C++ Primer有些文字不严谨,对了)(不要买Primer第六版(中文)翻译太烂了,买第五版)。
值得注意的是:在头文件中声明并定义且初始化一个const对象,多个.cpp文件包含这个头文件,但并不会像普通变量一样引起重复定义的错误。在c++的编译机制中,默认情况下,const对象对象设定为仅在文件内有效,即在多个文件中穿线了同名的const变量时,其实等同于在不同文件中分别定义了独立的变量。
const的引用
声明:引用并不是一个对象。
可以把引用绑定到const对象上,就像绑定到其他对象上一样,我们称之为对常量的引用(reference to const)。与普通引用不同的是,对常量的引用不能被用作修改它所绑定的对象:
const int ci = 1024;
const int &r1 = ci;//正确,引用及其对应的对象都是常量
r1 = 42;//错误,r1是对常量的引用,不能修改
int &r2 = ci;//错误,非常量引用不能指向常量
C++程序员们经常把词组“对const的引用”简称为“常量引用”,这一简称还是挺靠谱的,不过前提是你得时刻记得这只是一个简称而已。
严格来说,并不存在常量引用(文法,常量在这里修饰引用,表示引用是不可修改的且是一个对象,的确引用不可修改,但引用并不是一个对象,所以常量引用并不严谨,对const的引用才是严谨的)。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。
对const的引用的初始化
引用的类型必须与其所引用对象的类型一致。但这个规则在对const的引用的初始化中有点”小小“的不同。
在对”对const的引用“的初始化中,允许两个特例(普通引用一下特例不适用):
1.在初始化常量引用(就是对const的引用,以后都用常量引用表达)时允许用任意表达式作为初始值,只要改表达式的结果能转换成引用的类型即可。
2.允许为一个常量引用绑定非常量的对象、字面值,甚至是一个一般表达式。
int i = 42;
const int& r1 = i;//允许将const int&绑定到一个普通int对象上
const int& r2 = 42;//允许r1是个常量引用
const int& r3 = r1 * 2;//r3是一个常量引用
int& r4 = r1 * 2;//错误:r4是一个普通的非常量引用
要想理解这种例外情况的原因,最简单的办法是弄清楚当一个常量引用被绑定到另外一个类型上时到底发生了什么:
double dval = 3.14;
const int& ri = dval;
此处ri引用了yigeint型的数。对ri的操作应该是整数运算,但dval却是一个双精度浮点数而非整数。因此为了确保让ri绑定一个整数,编译器把上述代码编程了如下形式:
const int temp = deval;//由双精度浮点数生成一个临时的整型常量
const int& ri = temp;//让ri绑定这个临时量
在这种情况下,ri绑定了一个临时量对象。所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时创建的一个未命名的对象。C++程序员们常常把临时量对象简称为临时量。
当然,常量引用通过这个机制可以接受不用类型的变量的初始化,但普通引用是不适用这个特例的,比如你不能将普通引用用其他类型初始化,更不用说将普通引用用常量初始化。
对const的引用可能引用一个并非const的对象
必须认识到,常量引用仅对引用可参与的操作做出了限定,对于引用的对象本身是不是一个常量未作限定。因为对象也可能是个非常量,所以允许通过其他途径来改变它的值:
int i = 42;
int& r1 = i;//引用ri绑定对象i
const int&r2 = i;//r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0;//r1并非常量,i的值修改为0
r2 = 0;//错误:r2是一个常量引用
指针和const
同引用一样,指针也可以指向常量或者非常量。同常量引用一样(对const对象的引用),指向常量的指针不能用于改变其所指向对象的值:
const double pi = 3.14;//pi是个常量,它的值不能改变
double* ptr = π//错误:ptr是一个普通指针
const double* cptr = π//正确:cptr可以指向一个双精度常量
*cptr = 42;//错误:不能给*cptr赋值
可以得知,只有指向常量的指针才能保存对应类型常量的地址,且该指向常量的指针不能修改所指向对象的内容。
const double pi = 3.14;
const double* piptr = π//定义了一个指向常量的指针
但事实上指针与引用的最大不同是,指针是个对象,而引用不是。所以,虽然指向常量的指针并不能修改指向对象的内容,但指向常量的指针本身可以修改所指向的对象。
const double pi = 3.14;
const double e = 2.71;
const double* ptr = π//定义了一个指向常量的指针
ptr = &e;//修改指向的对象
值得注意的是,虽然常量对象在定义的时候必须被初始化,但指向常量的指针本身并不是一个常量,只是说明它所指向的对象是个常量而已。所以,指向常量的指针定义的时候并不一定要初始化。
const double* ptr;//正确
const指针
指针是对象而引用不是,因此就像其他对象类型一样,允许把指针本身定义为常量。常量指针必须初始化,而且一旦初始化完成,则它的值就不能改变了。把*放在const关键字之前用以说明指针是一个常量,这样的书写形式隐含着一层意味,即不变的是指针本身的值而非指向的那个值:
int errNumb = 0;
int *const curErr = &errNumb;//curErr将一直指向errNumb
const double pi = 3.13159;
const double *const pip = π//pip是一个指向常量的常量指针
常量指针,也就是const指针,本身就是一个常量,所以定义时必定要初始化,否则编译器就会报错。但要明白的是,常量指针仅仅只是说明指针是常量,但指向的内容却不是常量,所以常量指针是可以修改指向对象的内容的。
术语:顶层const/底层const
顶层const(top-level const)表示指针本身是个常量,更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都使用,如算数类型、类、指针等。
底层const(low-level const)表示指针所指的对象是一个常量。更一般的,底层const 则与指针和引用等复合类型的基本类型部分有关。
特别的,指针既可以是顶层const,也可以是底层const。
底层const拷贝要特别小心,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能相互转换。
constexpr和常量表达式
常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。显然,字面值属于常量表达式,用常量表达式初始化的const对象也是常量表达式。后面会提到,c++语言中有几种情况下要用到常量表达式的。
一个对象(或表达式)是不是常量表达式由他的数据类型和初始值共同决定,例如:
const int max_files = 20;//max_files是常量表达式
const int limit = max_files + 1;//是
int staff_size = 27;//不是
cconst int sz = get_size();//不是
constexptr变量
在一个复杂系统中,很难(几乎肯定不能)分辨一个初始值到底是不是常量表达式。
c++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明constexptr的变量一定是一个常量,而且必须用常量表达式初始化:
constexpr int mf = 20;
constexpr int limit = mf + 1;
constexpr int sz = size();
尽管不能使用普通函数作为constexpr表娘的初始值,但在C++ primer(第五版)214页将要介绍的,新标准允许定义一种特殊的constexpr函数。这种函数应该足够简单以使得编译时就可以计算其结果,这样就能用constexpr函数去初始化constexpr变量了。
字面值类型
常量表达式的值需要在编译时就得到计算,因此对声明constexpr时用到的类型必须有所限制。因为这些类型一般比较简单,值也显而易见、容易得到,就把他们称为“字面值类型”(literal type)。
一般的,算数类型、引用和指针等都属于字面值类型。自定义类、io库、string类型则不属于字面值类型,也就不能被定义成constexpr了。其他的看书(C++ primer第五版)。
尽管指针和引用都能定义成constexpr,但它们的初始值却受到严格限制。一个constexpr指针的初始值必须是nullptr或0,或者是存储与某个固定地址中的对象。
以后将会提到,存放于函数体中的变量的地址并不是固定不变的,所有不能在函数体中用constexpr限定变量。当然,也有机制允许函数定义一类有效范围超出函数本身的变量,这类变量和定义在函数体之外的变量一样也有固定地址。因此,constexpr引用能绑定到这样的变量上,constexpr指针也能指向这样的变量。
指针和constexpr
要之指出的是,constexpr修饰符仅对指针有效,而对指针所指向的对象是没有限制的。
const int* p = nullptr;//p是一个指向整型常量的指针
constexpr int* q = nullptr;//q是一个指向整型的常量指针
附表
//const和常量引用
//要引用常量,必须使用常量引用
const double pi = 3.14;
const double& pi_ref = pi;
//不同于普通引用,常量引用的初始化可以用任意表达式,只要该表达式能够转换为常量引用对应的类型
double &ref00 = 4.12;//错误,普通常量不能用字面值初始化
const double &ref02 = 4.12;//正确,只要表达式能转换为对应常量引用的类型就可以初始化
const double &ref03 = 4.12 + ref02;//嗯,这样也可以,只要能转换
//不同于普通引用,常量引用不一定要指向常量,也可以指向非常量
double pi2 = 3.14;
const double &ref_pi2 = pi2;//当然,只能读,不能写
//当然有一种比较综合的写法,比如
int random = 5;
const double &ref03 = random;//这是上面两条特性的结合,实际上可以理解成这样
/*****************************************
double random_dou = random;
const double &ref03 = random_dou;
*****************************************/
//常量引用属于底层const,用于拷贝的时候要特别小心
//常量引用只能对常量引用进行赋值(初始化),不能对非常量引用初始化,不管这个常量引用指向的是不是常量
const double& hp = ref_pi2;
double &hp02 = ref_pi2;//错误
//常量引用对类型变量进行拷贝,只要能将内容转换为对应类型,即可
int value = ref_pi2;
//常量引用对指针的赋值
const double* ptr = &ref_pi2;//正确,将pi2的地址拷贝给了ptr
double* ptr02 = &ref_pi2;//错误,即使ref_pi2指向的不是一个常量,但编译器只看赋值算式的左右,只要将常量引用引用对象的地址赋值给非常量指针,就会报错。
const int* ptr_int = &ref_pi2;//错误,因为类型也要对应
//总结:
//常量引用对类型变量进行拷贝限制很少,只要常量引用引用的对象的值能合法的转换为被拷贝对象就行
//对引用的赋值中,常量引用只能对常量引用赋值(初始化)
//对指针的赋值中,常量引用只能对常量指针赋值,且类型要对应
//const和指针
//指向常量的指针
const double pi = 3.14; //双精度常量
double* ptr = π //普通双精度指针并不能指向该常量
const double* ptr_const = π //指向常量的指针就可以
//指向常量的指针本身并不是一个常量,所以,它定义的时候可以不被初始化
const double* ptr_02;
//就像常量引用一样,并没有规定常量引用或指向常量的指针一定要指向对应类型的常量,变量同样可以存储指向常量的指针中,只是不能通过指向常量的指针来修改这个变量的内容而已
double e = 2.71;
const double* ptr_e = &e;//正确
e = 3;//可以通过非指向常量的指针修改对象的内容
//当然,指向常量的指针包含的对象必须和指针有相同的类型
const int* ptr_int = π//错误,类型不匹配
//指向常量的指针是一个底层const,所以用它进行拷贝的时候应格外小心
double* ptr_03 = ptr_const;//错误,ptr_const指向常量,但ptr_03显然不是指向常量的指针
ptr_03 = ptr_e;//错误,由此可见,指向常量的指针并不能给对应类型的普通指针赋值(拷贝)