目录
1. const修饰普通变量
2. const修饰引用
3. const修饰指针
3.1 * const p
3.2 const * p
3.3 const * const p
4. 顶层const和底层const
5. 用权限的方式理解const
const修饰的变量,本质是变量,但是不能直接修改,有常量的属性,称为常变量。
const float pi = 3.14f;
//float const pi = 3.14f;
pi = 5.14;//err
const修饰的常变量一旦创建后其值就不能再改变,所以必须初始化。
const int a;//err
如果利用一个变量去初始化另外一个变量,则它们是不是const都无关紧要。常变量的常量特征仅仅在执行改变常变量的操作时才会发挥作用。
int i = 42;
const int ci = i//ok
int j = ci;//ok
const修饰的引用,称为常量引用或常引用。不能通过常量引用改变对应的对象的值。
const int ci = 10;
const int &r1 = ci;//ok r1是常量引用,对应的对象ci也是常量
r1 = 20;//err 不能通过常量引用改变对应的对象的值
int &r2 = ci;//err ci不能改变,当然也就不能通过引用去改变ci//假设合法,则可以通过r2来改变ci,这是不合法的
在初始化常量引用时允许用任意表达式作为初始值,只要该表达式的结果能转换成引用的类型即可。尤其,允许为一个常量引用绑定非常量的对象、字面值,甚至是个一般表达式。
int i = 50;
const int& r1 = i;//ok 将const int&绑定到一个普通int对象上
const int& r2 = 10;//ok 将const int&绑定到一个int字面值上
const int& r3 = r1 * 2;//ok 将const int&绑定到一个表达式上
int& r4 = r1 * 2;//err
我们要清楚当一个常量引用被绑定到另外一种类型上时到底发生了什么:
double pi = 3.14;
const int& rpi = pi;//ok 将const int&绑定到一个普通double对象上
为什么rpi能够绑定pi?为了让rpi绑定一个整型对象,编译器把代码处理为:
const int temp = pi;//隐式类型转换 double->const int
const int& rpi = temp;//让rpi绑定这个临时量
所以当一个常量引用被绑定到另外一种类型上时,常量引用绑定的其实是相同类型的临时量。
如果函数的返回值是一个值而非引用,那么返回的是一个临时变量,且具有常量性。如果要用引用接收该值,只能用常量引用。
int Count()
{static int n = 0;n++;return n;
}int& ret = Count();//err
const int& ret = Count();//ok
const放在*的右边,修饰的是指针本身,指针是常量指针。
int m = 5;
int n = 10;
int* const p = &m;//p是常量指针
p = &n;//err 指针本身的值不能改变
*p = 0;//ok 指针指向的值可以改变
常量指针(const pointer)必须初始化,而且一旦初始化完成,则它的值(也就是存放在指针中的那个地址)就不能再改变了。
const放在*的左边,修饰的是指针指向的值,指针是指向常量的指针。
int m = 5;
int n = 10;
const int* p = &m;//p是指向常量的指针
//int const* p = &m;
*p = 0;//err 指针指向的值不能改变
p = &n;//ok 指针本身的值可以改变
const放在*的左右两边,修饰的是既是指针本身,又是指针指向的值,指针是指向常量的常量指针。
const int n = 10;
const int* const p = &n;//p是指向常量的常量指针
p = &n;//err 指针本身的值不能改变
*p = 0;//err 指针指向的值不能改变
如前所述,指针本身是一个对象,它又可以指向另外一个对象。因此,指针本身是不是常量以及指针所指的是不是一个常量就是两个相互独立的问题。用名词顶层const(top-level const)表示指针本身是个常量,而用名词底层const(low-level const)表示指针所指的对象是一个常量。
更一般的,顶层const可以表示任意的对象是常量,这一点对任何数据类型都适用,如算术类型、类、指针等。底层const则与指针和引用等复合类型的基本类型部分有关。比较特殊的是,指针类型既可以是顶层const也可以是底层const,这一点和其他类型相比区别明显:
int i = 0;
int* const p1 = &i; //顶层const,不能改变p1的值
const int ci = 42; //顶层const,不能改变ci的值
const int* p2 = &ci;//底层const,可以改变p2的值
const int* const p3 = p2;//靠右的const是顶层const,靠左的是底层const
const int& r = ci;//用于声明引用的const都是底层const
当执行对象的拷贝操作时,常量是顶层const还是底层const区别明显。其中,顶层const不受什么影响:
i = ci; //ok 拷贝ci的值,ci是一个顶层const,对此操作无影响
p2 = p3;//ok p2和p3指向的对象类型相同,p3顶层const的部分不影响
执行拷贝操作并不会改变被拷贝对象的值,因此,拷入和拷出的对象是否是常量都没什么影响。
另一方面,底层const的限制却不能忽视。当执行对象的拷贝操作时,拷入和拷出的对象必须具有相同的底层const资格,或者两个对象的数据类型必须能够转换。一般来说,非常量可以转换成常量,反之则不行:
int* p = p3;//err p3包含底层const的定义,而p没有
p2 = p3;//ok p2和p3都是底层const
p2 = &i;//ok int*能转换成const int*
int &r = ci;//err 普通的int&不能绑定到int常量上
const int &r2 = i;//ok const int&可以绑定到一个普通int上
p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量。因此,不能用p3去初始化p,因为p指向的是一个普通的(非常量)整数。另一方面,p3的值可以赋给p2,是因为这两个指针都是底层const,尽管p3同时也是一个常量指针(顶层const),仅就这次赋值而言不会有什么影响。
const的功能是将权限缩小,从可读可写缩小到只读。
没有指针或引用时,无关权限,因为不涉及到修改对象的值。
const int m = 1;
int n = m;//ok
指针和引用,赋值/初始化时,权限可以保持或缩小,但是不能放大。
//权限放大 err
const int a = 2;
int& b = a;const int* p1 = nullptr;
int* p2 = p1;//权限保持 ok
const int c = 2;
const int& d = c;const int* p3 = nullptr;
const int* p4 = p3;//权限缩小 ok
int e = 1;
const int& f = e;int* p5 = nullptr;
const int* p6 = p5;