今天复习引用(reference)的时候,突然想到,引用既然是对象的别称,那它有没有自己的地址(像指针(pointer))?然后自己测试了下,结果如下:
i的地址和ref的地址一样。。。觉得还是不太对劲,然后上网搜索,找到了下面这篇博客。https://blog.youkuaiyun.com/feigegegegegegegeg/article/details/52613627
1、引用的本质是什么
引用的一般定义为:
1) 引用是对象的一个别称;
2) 引用不是值不占用内存;
3) 引用定义的时候必须初始化,与对象绑定。
引用的本质定义:
1) 引用是通过指针实现的;
2) 引用是一个常量指针;
3) 引用在内存中占4个字节;
4) 引用在定义时必须初始化。
2、探究本质
我们从简单的定义变量开始,看看编译器做了哪些事情。
int var = 42; //[1]
mov dword ptr [var], 2AH //对应的汇编指令[2]
语句[1]表示的是申请一个内存空间,占4个字节,存放了一个int型的变量;内存中的放的是42。
语句[2]是对应的汇编语句,意思是把42写入以var为地址的内存中。var有点像我们理解上的指针,只是编译器没有把它抽象出来,而是让我们更表象的理解:申请一个变量,它的值是42。
那么var这个变量名放在哪里呢?
我们知道程序如果方位内存里面的数据,都是通过地址访问的,所以上面的代码在经过编译器生成目标代码时,用存放42的地址替换了所有的var,所以结论是:目标文件中不存在var,所以变量名本身是不占内存的。
而我们知道,引用是变量的一个别名。那么,编译器在生成目标代码时,会不会也用实际地址替换了引用呢?
答案并非这样!!!
我们接下来看看,当我们定义一个引用时,发生了什么:
int var = 42;
01303AC8 mov dword ptr [var], 2Ah
int &refVar = var;
01303ACF lea eax, [var]
01303AD2 mov dword ptr [refVal], eax
上面的代码显示,当定义一个引用时,编译器现将var的地址赋值给了以refVar为地址的一块内存区域。也就是说refVar其实存放的是var的地址。
我们在看看定义一个指针时发生了什么:
int var = 42;
01303AC8 mov dword ptr [var], 2Ah
int *ptrVar = &var;
01303ACF lea eax, [var]
01303AD2 mov dword ptr [ptrVal], eax
两者的汇编代码完全一致!!!
3、const哪里去了
从上面的分析中,我们看到引用实际上就是一个指针。那么为什么说引用是一个常量指针呢?在目标代码里有什么体现呢?
这个问题其实要从c++底层机制谈起,c++为我们提供了各种存取控制仅仅是在编译阶段给我们的限制,也就是说编译器确保了你在完成任务之前的正确行为,如果你的行为不正确,那么编译器就是给你在编译时提示错误。所谓的const和private等在实际的目标代码中根本不存在,所以在程序运行期间只要你愿意,你可以通过内存工具修改它的任何一个变量的值。
这也就解释了为什么上面的两段代码中引用和指针的汇编指令完全一致。
C++设计引用,并用常量指针来从编译器的角度实现它,目标是为了提供比指针更高的安全性,因为常量指针一旦与变量地址绑定将不能更改,这样降低了指针的危险系数,它提供了一对一的指针。
但是这样就安全了吗?实际上它同样会有与使用指针一样的问题:
int *pVar = new int(42);
int &refVar = *pVar;
delete pVar;
refVar = 42;
这段代码是很不安全的,因为refVar引用的内存区域不合法!!!
为了进一步验证引用与指针在本质上的相同,我们看当引用作为函数参数传递时,编译器的行为:
void Swap(int& v1, int& v2);
void Swap(int* v1, int* v2);
int var1 = 1;
00A64AF8 mov dword ptr [var1],1
int var2 = 2;
00A64AFF mov dword ptr [var2],2
Swap(var1,var2);
00A64B06 lea eax,[var2]
00A64B09 push eax
00A64B0A lea ecx,[var1]
00A64B0D push ecx
00A64B0E call Swap (0A6141Fh)
00A64B13 add esp,8
Swap(&var1, &var2);
00A64B16 lea eax,[var2]
00A64B19 push eax
00A64B1A lea ecx,[var1]
00A64B1D push ecx
00A64B1E call Swap (0A61424h)
00A64B23 add esp,8
上面代码再次证明了引用和指针的行为完全一致,只是编译器在编译时对引用作了更严格的限制。
4、引用占多大内存
因为在表达式中,使用引用实际上就像使用变量本身一样,所以直接用sizeof是得不到引用本身的大小。
double var = 3.14;
double &ref = var;
std::cout << sizeof(var) << std::endl; //8
std::cout << sizeof(ref) << std::endl; //8
我们可以通过定义一个类来解决这个问题:
class refClass {
private:
double &ref;
public:
refClass(double var = 42.0):ref(var){}
};
std::cout << sizeof(refClass) << std::endl; //4
所以结论是引用和指针一样实际占内存空间为4个字节。