const
参考书目:《C++ primer》第五版
为何定义const变量?有什么好处?
当我们在特定的场景下使用一个变量的时候,我们希望该变量的值仅仅在初始化能设置其值,且后续操作都不能更改它的值,这时我们就需要使用const限定符。它的好处是一旦初始化,其对象无法进行修改。可以在程序中防止一些误修改操作的发生。
初始化
const int size = 20; //输入一个长度大小
这样就定义了一个int类型的常量。任何试图对size赋值的操作都会引发错误:
size = 30; //错误,试图对size再赋值
由于const对象在创建后就不能更改其值,显然必须在定义的时候对其进行初始化。该初始化的值可以是常数或任意复杂的表达式:
const int a = get_size(); //正确
const int b = 40; //正确
const int k; //错误,未进行初始化
const的主要限制就是只能在const类型的对象上执行不改变其内容的操作,比如const int能够和普通的int一样参与运算操作。
在不改变const对象的操作中还有一种是初始化,如果利用一个对象去初始化另外一个对象,则他们是不是const都无关紧要。
int i = 40;
const int j = i; //正确
int k = j; //正确
虽然j是一个int常量,但是j中的值还是一个整数。j的常量特征仅仅在执行改变j的操作的时候才会体现出来。当用j初始化k时,只需要等号右边是一个同类型的量即可,无需关注它是一个常量还是变量。正如我们拷贝一个值,我们并没有改变它,拷贝完成后,新的对象和原对象没有任何关系。
这里需要注意,默认情况下,const仅在某一个文件内有效。当多个文件中出现了同名的const变量时,等同于在不同的文件中分别定义了独立的常量。想要在多个文件中都使用一个文件定义的const常量,操作和其他(非常量)对象一样,只需要在初始化的文件和需要使用的外部文件都添加extern关键字进行声明。
extern const int size = 20; //初始化一个可被引用的常量size
extern const int size; //引用外部常量
const的引用
和正常的引用一样,const也可以进行引用来绑定到其他对象上,称为对常量的引用。然而它特殊的地方在于,对常量的引用不能用来修改它绑定的对象。
const int a = 10;
const int &b = a; //正确,引用及其对应的的对象都是常量
b = 20; //错误,b为对常量的引用,无法修改其绑定的对象
int &c = a; //错误,试图让一个非常量引用指向一个常量对象
由于a为常量,因此不能对其进行赋值的操作,上述通过引用去改变a的值也是不允许的。若c的初始化正确,那么可以通过c修改其引用的对象a的值,这显然违背了const的初衷。
Tips:实际上并不存在“常量引用”这个说法。因为引用并非一个对象,我们无法让引用本身保持不变。事实上,由于C++并不允许随意更改引用绑定的对象,从这层意义上理解所有的引用都可以认为是常量。引用的对象是常量还是非常量可以决定其所能参与的操作,却无论如何都不会影响到引用和对象的绑定关系本身。简单来说,在引用关系确定后,两者可以认为是同一个对象,这个对象的值能否修改要看其定义是常量还是非常量。
初始化和对const的引用
在C++中,引用的类型必须与其所引用对象的类型一致,但有两种情况除外。初始化常量引用时允许用任意表达式作为初始值(只要表达式最后的结果为对应的类型)。
int a = 10;
const int &b = a; //正确
const int &c = 10; //正确,c为常量引用(右边为一个数)
const int &d = b * 2; //正确,d为常量引用(右边为表达式)
int &e = b * 2; //错误,e为非常量引用(不可通过e引用来修改常量)
有时候可能我们会粗心写成了下面这样:
double a = 3.14;
const int &b = a;
然而更惊讶的是,居然没有报错?原来编译器在执行常量引用的时候,自动将a强制转换成了int类型,然后让b绑定了这个临时量。注意,此时b绑定的对象不是a,而是临时量。如果去掉该const,变量b也会绑定临时量吗?在编译器上运行发现提示错误了。原因很简单,变量的引用,其目的是使用b来更改a,这时b绑定了一个临时量,如何修改对象a的值?因此C++将这种操作认为是非法操作。
对const的引用可能引用一个并非const的对象
前面已经说过,常量引用不可以修改指向的对象,但是如果指向对象的类型并非const呢?那我们可以修改对象的值吗?
int a = 10;
int &b = a; //引用b绑定对象a
const int &c = a; //c绑定对象a,但是不能通过c改变a的值
b = 0; //允许
c = 0; //错误,c为常量引用,不可以通过c修改对象a的值
Tips:我们直接修改a,c的值也会改变(但其指向的对象不变)
指针和const
和引用一样,指针也可以指向常量或者非常量。可以回想一下常量引用的特点,指向常量的指针也不能改变它指向的对象的值。想要存放常量对象的地址,就只能使用指向常量的指针:
const int p = 3; //p为常量,其值不可变
int *a = &p; //错误,普通指针不可以指向常量(a可以改变p的值)
const int *b = &p; //正确,b可以指向一个int常量
*b = 1; //错误,不能给*b赋值(常量)
同样,指针的类型必须与其所指对象的类型一致。但是允许一个指向常量的指针指向一个非常量的对象:
int val = 10;
b = &val; //正确,这里使用上文的b,但不能通过b改变val的值
和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。指向常量的指针仅仅要求不能通过该指针改变对象的值,而没有规定那个对象能不能通过其他途径改变。
const指针
指针是对象,而引用不是对象,因此指针和其他对象类型一样,允许把指针本身定为常量。常量指针必须初始化,在初始化完成后,它的值(指针中存放的地址)就不能被改变了。把*放在const前说明指针是一个常量,这里的意思是指针本身的值不能改变,而不是指针指向的值不能改变。
int num = 0;
int *const err = # //err将一直指向num
const double pi = 3.14;
const double *const pip = π //pip是一个指向常量对象的常量指针
要想更快地理解这些声明的含义,我们可以从右边往左开始解读。在上面的代码第二行err的声明中,离err最近的符号是const,说明err本身是一个常量对象,下一个符号是*,意思是err是一个指针常量,最后类型说明err是一个指向int对象的指针常量。类似地我们可以推断出pip是一个指向double常量的指针常量。
指针本身是一个常量并不意味着不能通过指针修改其指向对象的值,能否更改完全依赖于所指对象的类型。比如pip是一个指向常量的指针常量,那么无论是pip所指的对象的值还是自己存储的地址都不可改变。相反,指针常量err指向的是非常量,那么就可以用err去修改num的值。
**Tips:快速读取和判断指针类型的小技巧**
从左往右 => *和const谁先出现先读谁
eg: int *const->指针常量 int const*->常量指针
从左往右 => const左边第一个出现的对象就是const实际限定的内容,没有则看右边
eg: int *const->const对*做限定,指针(的指向)不能改变
int const*->const对int做限定,指针指向的对象的值不能改变
*pip = 1.1; //错误,pip为指向常量的指针(常量)
*err = 3; //正确,num被修改为3
err = pip; //错误,指针常量不允许在初始化后再赋值
pip = err; //同上
顶层const
指针是一个对象,它可以指向另外一个对象。因此,指针本身是不是常量,以及指针指向的对象是不是常量,实际上是两个完全独立的问题。C++中用顶层const(声明时在最左边)表示指针本身是个常量(即指针常量),用底层const(声明时在指针符号右边)表示指针指向的对象是一个常量(即常量指针)。
指针类型既可以是顶层const也可以是底层const:
int i = 0;
int *const a1 = &i; //不能改变a1的值,顶层const
const int c = 10; //不能改变c的值,顶层const
const int *a2 = &c; //可以改变a2的值,底层const
const int *const a3 = a2; //靠右的const是常量指针顶层const,靠左的是指向常量的底层const
const int &r = c; //用于声明引用的const都是底层const
当我们需要进行对象的拷贝时,常量是顶层const还是底层const的区别就十分明显。顶层const不受任何影响:
i = c; //正确,c是顶层const,拷贝操作不受影响
a2 = a3; //正确,a2和a3指向的对象都是同类型的常量(底层const),a3顶层部分不影响
而另一方面,底层const的限制很大。当执行对象进行拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,后者两个对象的数据类型必须能够转换。一般非常量可以转换成常量,反之不行:
//接上初始化
int *a = a3; // 错误,a3包含底层const定义,而a没有
a2 =a3; // 正确,a2和a3都是底层const
a2 = &i; // 正确,int *可以转换成const int *
int &r = c; // 错误,普通的int&不能绑定到int常量上
const int &r2 = i; // 正确,const int&可以绑定到一个普通int上