前面提到过C++编译器在遇到const常量时会将字面常量放入符号表,编译中遇到时直接拿符号表里的值替换,const引用会生成一个只读变量,那么我们是否有必要深入分析一下const修饰的标识符什么时候为只读变量?什么时候为常量?
const常量的判别准则:
只有用字面量初始化的const常量才会进入符号表
使用其他变量初始化的const常量仍然是只读变量
被volatile修饰的const常量不会进入符号表,因为volatile修饰的变量是可变的。
所以我们可以总结了:在编译期间不能直接确定初始值的const标识符都被作为只读变量处理。
我们说过const引用会生成一个具有只读属性的变量,那么对const引用进行不同的初始化又会发生什么呢?
若const引用的类型与初始化变量类型相同时,初始化变量成为只读变量
若const引用的类型与初始化变量类型不同时,生成一个新的只读变量。
接下来我们用例子说明。
const int x = 1;
const int& rx = x;
int& nrx = const_cast<int&>(rx);
nrx = 5;
printf("x = %d\n", x);
printf("rx = %d\n", rx);
printf("nrx = %d\n", nrx);
printf("&x = %p\n", &x);
printf("&rx = %p\n", &rx);
printf("&nrx = %p\n", &nrx);
结果会输出些什么呢?让我们先分析一下,首先定义了一个常量x,值为1,接着定义了一个具有只读属性的变量rx,被x初始化了,说明rx = 1;接着rx被强制去除了只读属性后赋值给int型引用nrx,说明rx现在是一个普通变量,nrx是这个变量的一个别名,语句nrx = 5;是有效的,所以最后的打印结果:
x = 1
rx = 5
nrx = 5;
后面打印的三个地址为同一个地址,因为在一定情况下编译器也会为const常量分配空间,他们都指向了同一片空间。
volatile const int y = 2;
int* p = const_cast<int*>(&y);
*p = 6;
printf("y = %d\n", y);
printf("p = %p\n", p);
volatile与const搭配表示y一定是一个只读属性的变量,通过const_cast强制类型转换后y只是一个普通类型的指针变量了,通过指针修改了y所在空间里保存的值,所以输出
y = 6,输出地址肯定是一个合法地址。
char c = 'c';
char& rc = c;
const int& trc = c;
rc = 'a';
printf("c = %c\n", c);
printf("rc = %c\n", rc);
printf("trc = %c\n", trc);
首先我们知道改变rc就相当于改变c,所以c和rc的输出结果都是字符a。但是由于const引用类型和初始化它的变量类型不同,所以会生成一个新的只读变量(注意和初始化变量区别开来,他们指向的空间不同),即trc是一个只读变量,所以对trc来说修改无效,即trc输出仍然为字符c。
引用
前面说过引用在编译器内部实现实际上就是一个指针常量,那么引用和指针是否存在什么关系?
指针是一个变量,具体值为一个内存值,不需要初始化,也可以保存其他地址,通过指针可以访问对应内存地址中的值,指针可以被const修饰成为常量或只读变量。
引用只是一个变量的新名字,在对引用进行一系列操作(赋值、取地址等)都会传递到所代表的变量上,const引用使其代表的变量具有只读属性,引用必须在定义时初始化,并且之后无法再代表别的变量。
站在使用角度上指针和引用完全无关,站在编译器的角度上来说指针常量只是引用的实现方式,因此引用在定义时必须初始化,因为指针不初始化就是一个野指针,容易造成内存错误。
int a = 1;
struct SV
{
int& x;
int& y;
int& z;
};
int main()
{
int b = 2;
int* pc = new int(3);
SV sv = {a, b, *pc};
int& array[] = {a, b, *pc};
delete pc;
分析下代码猜猜看下面两条语句是否有问题?
SV sv = {a, b, *pc};
int& array[] = {a, b, *pc};
第一句表示一个含有三个引用成员的结构体变量分别由全局变量a,栈上的变量b和堆区的变量pc进行初始化。
第二条表示定义一个引用数组,也分别由a、b、pc初始化。
看起来好像都没错,但事实是这样吗?
结构体变量中有三个int&型成员,因为引用的实质是指针常量,占用内存,所以他们的地址是连续的,本质上也就是三个连续相差4个字节的指针常量,由于引用在定义时必须初始化,所以结构体变量就被a,b,pc分别初始化了,成为了他们的别名,打印结构体变量成员的地址就相当于打印它所代表的变量的地址。
数组元素在内存中是连续的,所以不能将三个地址不连续的变量初始化给int&型数组,所以第二条语句是错误的,也间接证明了C++中并没有引用数组这个概念。
#include <stdio.h>
int a = 1;
struct SV
{
int start;
int& x;
int& y;
int& z;
int end;
};
int main()
{
int b = 2;
int* pc = new int(3);
SV sv = {1,a, b, *pc,2};
sv.x = 3;
sv.y = 4;
sv.z = 5;
printf("&sv = %p\n", &sv);
printf("&a = %p\n", &a);
printf("&b = %p\n", &b);
printf("&pc = %p\n", pc);
printf("&sv.start = %p\n", (&sv.start));
printf("&sv.x = %p\n", (&sv.x));
printf("&sv.y = %p\n", (&sv.y));
printf("&sv.z = %p\n", (&sv.z));
printf("&sv.end = %p\n", (&sv.end));
printf("sv.x = %d\n", sv.x);
printf("sv.y = %d\n", sv.y);
printf("sv.z = %d\n", sv.z);
delete pc;
return 0;
}
我们关注下打印结构体变量成员地址的五条语句,为了证明在此处的结构体成员占用的内存是连续的,我设置了start和end成员,将他们的地址打印出来,间接证明引用本质是一个指针常量,并且三个引用占用内存是连续的,看看输出结果:
&sv = 0062FEE4
&a = 0040D000
&b = 0062FEF8
&pc = 00723770
&sv.start = 0062FEE4
&sv.x = 0040D000
&sv.y = 0062FEF8
&sv.z = 00723770
&sv.end = 0062FEF4
sv.x = 3
sv.y = 4
sv.z = 5
可以看到&sv.end - &sv.start = 0x0062FEF4 - 0x0062FEE4 = 16个字节,除去start占用四个字节,那么三个同类型引用占用了12个字节,那么是不是能够间接证明引用本身是占用内存的,而且和指针一样占用四个字节,并且结构体成员之间也是连续的,为什么&sv.x、&sv.y、&sv.z打印出来的地址相差很大,这就是引用的特性了,代替了别的变量,当然需要为它服务,理所当然地址也会指向所代替变量的地址,一般来说编译器是不会让我们直接获取到实现引用的那个指针常量的。