C++中引用变量的探究

浏览: 107 发布日期: 2016-12-01 分类: c++

昨天看了C++primer的2.5节,引用变量,看完之后,对引用变量有一些疑惑,然后自己编写了一些测试程序,想搞清楚引用变量的底层到底做了些什么。下面,首先,我从C++ primer一书的2.5节内容讲起,再进行一些延伸。

一、C++ primer2.5节学习笔记:

注意(1)将普通的引用绑定到const对象是不合法的

  (2)const引用可以初始化为不同类型的对象或者初始化为右值,而非const引用引用不能初始化为右值

  原因在于:一般情况下,应该使非const引用指向同类型的变量,当指向的是不同类型的变量时,会出现以下情况:

例:

编写如下的测试程序:

 1 int main()
 2 
 3 {
 4 
 5         double dval = 3.14;
 6 
 7         const int &ri = dval;
 8 
 9         return 0;
10 
11 }

如上图所示,能够编译成功,但是会显示这么一条warning,将double类型转化为const int类型,可能会loss of data。

首先,看dval存储的地址,是0x0012ff5c,而ri存储的地址是0x0012ff44,而引用所在的地址应该和其指向的变量的地址相同,出现这个问题就是因为引用和其指向的变量类型不同,实际上,编译器会将const int &ri = dval; 转化为如下代码:

1 int temp = dval;
2 
3 const int &ri = temp;

所以ri引用指向的已经不是dval,而是这个过程中创建的一个临时变量temp,进行强制类型转化(转化为了3),这一点从内存中也能够看出,ri指向的变量地址是0x0012ff44,此处,存储着0x0000_0003,即一个整型变量(这里占了4个字节)

所以,如果ri不是const,那么就可以对ri进行改变,但是程序员的原意是通过ri的修改从而改变dval,但是这种情况下,改变ri的值不能改变dval,而只能改变temp(实际上没有这个变量名)的值,这和程序员的原意是违背的,所以C++标准中干脆将这种用法规定为不合法,下面是我们将const int&ri = dval;改成了int &ri = dval;后编译出现的情况:

 

从上图中可以看出,这种用法是不合法的。

二、探究C++的引用:

从一条语句的反汇编看起:

 

1 const int &ri = dval;    // const引用指向不同的类型

 

 

 

首先,dval的地址是在0x0012ff5c,这里的8字节数据存储着浮点数3.14,然后,观察const int &ri = dval;反汇编后的代码:

 

004113E7  fld        qword ptr [dval]      ;浮点协处理器指令

004113EA  call      @ILT+200(__ftol2_sse) (4110CDh)      ; 应该调用函数是进行强制类型转化,结果存在eax寄存器中

004113EF  mov       dword ptr [ebp-24h],eax     ; 将强制类型转化后的结果存在内存中
                                                ; (可理解为C++ primer书中所说的产生了一个temp变量
                                                ; 这个变量的地址就是ebp-24h)

004113F2  lea       eax,[ebp-24h]                  ; 将所存结果的地址(lea)给eax寄存器

004113F5  mov       dword ptr [ri],eax          ;即将上面所说的地址(不妨认为是temp变量的地址)
                                                ;给变量ri所在的内存区域
                                                

 

单步完这一步后,可以看到内存中有两处颜色变红了,表示在这一过程中内存中有两处进行了修改,根据上面的反汇编的分析,的确应该是这样,首先,是内存中的[ebp-24h]处(”temp变量“处)变成了0x0000_0003,然后是将这个临时变量的地址,即ebp-24h的值赋给了ri变量所在的内存区域。

实际上,将鼠标指针放在ebp的上面可以看到其十进制的值,如下所示

 

 

所以此时ebp = 1245032 = 0x0012ff68

 

ebp-24h = 0x0012ff44

 

所以ri变量处的内容应该是0x0012ff44

 

而ri变量的地址应该是0x0012ff50,从上面的内存图中可以看出,0x0012ff50在这一过程中变化了,并且内容是0x0012ff44,实际上,在反汇编的窗口中进行单步也能够看出这一点:

 

”temp变量“改变前:

”temp变量“改变后,ri变量改变前:

ri变量改变后:

 

 

从上面的三幅图中可以看出,我们前面的结论是正确的。

 

但是,在这里出现了一个问题,我们来查看Watch窗口,可以看到:

??????????????????????????

上面和我们前面按照反汇编理解的是不一样的,按照我们的理解,应该是:

ri:存储着被引用变量所在的地址(这里应该是”temp变量“的地址),即0x0012ff5c,

*ri:即0x0012ff5c存储单元的内容,即3

&ri: 即ri变量所在的地址,按照前面调试的结果,应该是0x0012ff50。

但是在Watch窗口中看到的结果是:

ri = 3

*ri = 3

&ri是temp变量的地址,0x0012ff5c

上面的这个问题先放着,先来看这个测试程序的最后两句,是两个普通的非引用变量的初始化,我们来看这部分的反汇编代码:

 

 

可以看到val1变量的初始化很简单,就是把一个常量5放到了内存中变量val1所在处,而val2的初始化也可理解为就是把变量val1的内容搬移到val2所在处,但是考虑到x86系列不能再两个存储器单元之间传送数据,所以这里需要通过一个寄存器eax,把val1中的内容搬到了val2中,注意到,这里用的汇编指令都是mov,从这里,联系前面的分析,就能看出引用变量和非引用变量进行初始化时的最重要区别,非引用变量初始化时,给这个变量开辟了一个内存空间,将等号右边的表达式结果搬移(mov)到了这个变量所在的内存单元处,而引用变量初始化时,也给这个变量开辟了内存空间,但是区别在于是将等号右边的相同类型变量(被引用变量)的地址赋给了这个引用变量(这是就非const变量或者const变量时的一种特殊情况讨论的,若是const变量,并且等号右边和引用变量不是同一类型时,则在中间要加上一步,就是创建一个临时变量(类型和引用变量相同),给这个临时变量开辟一段内存空间,然后把等号右边的表达式的结果存入这个临时变量中,接下来,再把这个临时变量的地址赋给这个引用变量)。

 

所以,从这一点上来看,引用完成的功能和指针很像,但是可能是C++必须要区分开引用这种用法,所以虽然从上面来看引用变量的类型实际上是一个指针类型,但是编译器可能不这样看,而是把在代码中出现的这个引用类型都翻译成引用类型的内容,所以在后面的程序中使用引用类型的时候,比如说改变引用ri的值的时候,实际上改变的是*ri(这里先把ri看成指针类型理解吧)的值,这可能也是前面在Watch窗口中查看时出现问题的原因。

 

上面这段话只是我的推测,在C++方面看的书还比较少

 

下面接着来看下面的语句的反汇编结果和执行情况:

 

1         const double &ri2 = dval;    // const引用指向相同的类型

 

执行后:

 

可以看出,const变量指向相同类型的变量时,实际上在内存中也开辟了一个空间,这个空间中放置着ri2变量,ri2变量的内容是一个地址,即被引用变量dval的地址,但是,如前所述,按照这种指针的方法理解的话,就会把引用变量ri2理解成了指针(其实从这个结果来看,ri2就是一个指针变量),所以,以后理解引用变量时,最好就把引用变量看成一个不占用任何内存空间的”变量“,其地址就是被引用变量的地址(仅仅就两者类型一致时,不一致时有”临时变量“),就好像引用变量是被引用变量的一个别名。

 

接着看下面的语句:

 

 

1 double &ri3 = dval;    // 非const引用指向相同的类型

 

这和上面的const引用指向相同的类型实际上是一样的。

结果截图如下:

最后,看const引用指向右值(常数)的情况:

 

1 const int &ri4 = 4;    // const引用指向右值(常数)

 

 

结果为:

 

 

实际上,这和const变量指向不同类型的情况是相同的。也存在着一个临时变量。

 

下面,对前面的分析进行一个总结:

 

一方面,按照反汇编分析的结果来看:

 

(1)如果引用变量指向的是相同类型的变量,那么,首先会为这个引用变量开辟一段内存空间(实际上这是在编译阶段就完成了),然后把被引用变量的地址放到这段内存空间中,即把被引用变量的地址赋给引用变量,

 

(2)如果引用变量初始化式右端的变量和引用变量是不同类型的,或者初始化式右边就是一个右式(这两种情况都只有const引用是合法的),这时,首先,将开辟一段内存空间,把初始化式右边的结果(可能需要强制类型转化等)放在这个内存空间中(可理解为定义了一个临时变量),然后再把这个临时变量对应的地址赋给引用变量。

 

但是前面已经讲了,按照反汇编的结果理解的话,可能会有将引用和指针混淆的疑惑,而实际上,在C++中使用引用时,也不需要知道这么多的底层细节,所以,可以这样来理解引用:

 

(1)如果引用变量指向的是相同类型的变量,那么这个引用变量就相当于这个被引用变量的一个别名,两者占用相同的内存空间,对引用变量做的任何改变也会改变相应的被引用变量。

 

(2)如果引用变量初始化式右端的变量和引用变量是不同类型的,或者初始化式右边就是一个右式(这两种情况都只有const引用是合法的),这时,首先,编译器会定义一个临时变量,把初始化式右边的结果(可能需要强制类型转化等)赋给这个临时变量,然后再将原引用指向这个类型相同的临时变量(这一步的过程和(1)相同)。

 

 

 

下面是写的这段测试程序的反汇编代码,放在这里方便查看:

 

 

最后,说明一下,这篇文章中很多都是我自己的看法,有些没在书上找到,不一定正确,如果有错,希望大家提醒!

返回顶部