C++中引用的本质

原创 2015年07月08日 16:33:22

代码运行环境:Windows7 32bits+VS2012。

引用是C++引入的重要机制,它使原来在C中必须用指针实现的功能有了另一种实现的选择,在书写形式上更为简洁。那么引用的本质是什么,它与指针又有什么关系呢?


1.引用的底层实现方式

引用被称为变量的别名,它不能脱离被引用对象独立存在,这是在高级语言层面的概念和理解,并未揭示引用的实现方式。常见错误说法是“引用“自身不是一个变量,甚至编译器可以不为引用分配空间。

实际上,引用本身是一个变量,只不过这个变量的定义和使用与普通变量有显著的不同。为了解引用变量底层实现机制,考查如下代码:

int i=5;
int &ri=i;
ri=8;

在Visual Studio 2012环境的debug模式调试代码,反汇编查看源码对应的汇编代码的步骤是:调试->窗口->反汇编,即可得到如下原码对应的汇编代码:

int i=5;
00A013DE  mov        dword ptr [i],5    //将文字常量5送入变量i
int &ri=i;
00A013E5  lea        eax,[i]            //将变量i的地址送入寄存器eax
00A013E8  mov        dword ptr [ri],eax  //将寄存器的内容(也就是变量i的地址)送入变量ri
ri=8;
00A013EB  mov         eax,dword ptr [ri]  //将变量ri的值送入寄存器eax
00A013EE  mov         dword ptr [eax],8   //将数值8送入以eax的内容为地址的单元中
return 0;
00A013F4  xor         eax,eax    

考查以上代码,在汇编代码中,ri的数据类型为dword,也就是说,ri要在内存中占据4个字节的位置。所以,ri的确是一个变量,它存放的是被引用对象的地址。由于通常情况下,地址是由指针变量存放的,那么,指针变量和引用变量有什么区别呢?使用指针常量实现上面的代码功能。考查如下代码:

int i=5;
int * const pi=&i;
*pi=8;

按照相同的方式,在VS2012中得都如下汇编代码:

int i=5;
011F13DE  mov         dword ptr [i],5  
int * const pi=&i;
011F13E5  lea         eax,[i]  
011F13E8  mov         dword ptr [pi],eax  
*pi=8;
011F13EB  mov         eax,dword ptr [pi]  
011F13EE  mov         dword ptr [eax],8  

观察以上代码可以得出如下结论:
(1)只要将pi换成ri,所得汇编代码与第一段所对应的汇编代码完全一样。所以,引用变量在功能上等于一个指针常量,即一旦指向某一个单元就不能在指向别处。

(2)在底层,引用变量由指针按照指针常量的方式实现。


2.高级语言层面引用与指针常量的关系

(1)在内存中都是占用4个字节(32bits系统中)的存储空间,存放的都是被引用对象的地址,都必须在定义的同时进行初始化。

(2)指针常量本身(以p为例)允许寻址,即&p返回指针常量(常变量)本身的地址,被引用对象用*p表示;引用变量本身(以r为例)不允许寻址,&r返回的是被引用对象的地址,而不是变量r的地址(r的地址由编译器掌握,程序员无法直接对它进行存取),被引用对象直接用r表示。

(3)凡是使用了引用变量的代码,都可以转换成使用指针常量的对应形式的代码,只不过书写形式上要繁琐一些。反过来,由于对引用变量使用方式上的限制,使用指针常量能够实现的功能,却不一定能够用引用来实现。

例如,下面的代码是合法的:

int i=5,j=6;
int* const array[]={&i,&j};

而如下代码是非法的:

int i=5,j=6;
int& array[]={i,j};

也就是说,数组元素允许是指针常量,却不允许是引用。C++语言机制如此规定,原因是避免C++语法变得过于晦涩。加入定义一个“引用的数组”,那么array[0]=8;这条语句该如何理解?是将数组元素array[0]本身的值变成8呢,还是将array[0]所引用的对象的值变成8呢?对于程序员来说,这种解释上的二义性对正确编程是一种严重的威胁,毕竟程序员在编写程序的时候,不可能每次使用数组时都要回过头去检查数组的原始定义。


3.非正常的使引用变量指向别的对象

C++语言规定,引用变量在定义的时候就必须初始化,也即是将引用变量与被引用对象进行绑定。而这种引用关系一旦确定就不允许改变,直到引用变量结束其生命期。这种规定是在高级语言的层面上,由C++语言和编译器所做的检查来保障实施的。在特定的环境下,利用特殊的手段,还是可以在运行时动态地改变一个引用变量与被引用对象的对应关系,使引用变量指向一个别的对象。见下面的程序:

#include <iostream>
using namespace std;
int main(int argc,char* argv[])
{
    int i=5,j=6;
    int &r=i;
    void *pi,*pj;
    int* addr;
    int dis;

    pi=&i;    //取整型变量i的地址
    pj=&j;    //取整型变量j的地址
    dis=(int)pj-(int)pi;//计算连续两个整型变量的内存地址之间距离
    addr=(int*)((int)pj+dis);//计算引用变量r在内存中的地址

    cout<<"&i:"<<pi<<endl;
    cout<<"&j:"<<pj<<endl;
    cout<<"&pi:"<<&pi<<endl;
    cout<<"&pj:"<<&pj<<endl;
    cout<<"&addr:"<<&addr<<endl;
    cout<<"&dis:"<<&dis<<endl;
    cout<<"distance:"<<dis<<endl;

    (*addr)=(int)&j;//将j的地址赋给引用r(此处把r看作指针)

    cout<<"addr:"<<addr<<endl;
    r=100;
    cout<<i<<" "<<j<<endl;
    getchar();
    return 0;
}

这个程序在Debug模式下输出结果如下:

仔细观察代码和输出结果可以得出如下结论:
(1)Win32(Windows 32bits)平台下,int型变量和指针变量都占用4个字节,但是&i-&j=-12,并非想象中的4。
原因有二:
一是局部变量存储在栈空间,栈在主存中的生长方向是从高地址到低地址,因此i和j的地址差为负数;
二是Debug模式下,int变量前后均添加4个字节的调试信息,故一个int占用了12字节。模式设为Release,就会发现栈上连续定义的int变量,地址相差4个字节。

(2)指针变量pi与int变量j地址间相差了24字节,按照推理,如果引用r不占用内存空间,那么地址差应该为12字节,这也说明了引用变量在内存占用空间。

(3)将引用变量r理解成指针,间接的获取r的地址并修改r的值,使r指向变量j。从引用的角度理解就是将引用r与j绑定。对r赋值,结果显示j的值被修改。
以上代码是较为诡异,实际编程绝不提倡大家模仿。利用以上程序可以看出“引用“本身的确是一个变量,它存放被引用对象的地址。并且,利用特殊手段能够找到这个引用变量的地址并修改其自身在内存中的值,从而实现与其他对象的绑定。

这个程序在VS环境下的Release模式,编译不通过,会出现内存访问冲突,无法通过引用变量r修改j的值,可能与 Release模式下编译器对引用的优化有关。与此同时,该程序可移植性很差,在64为平台上,由指针转换为int可能会发生截断从而丢失数据。其次,如果引用变量前的变量不是int型,考虑到内存对齐等因素,要准确计算引用变量的地址不是一件容易的事,很可能跟具体的编译器和运行环境相关。因此,研究此程序的目的是为了对引用变量的底层实现机制有所了解。在实际使用中,还是要遵循C++语言对引用制定的规范。


参考文献

[1]陈刚.C++高级进阶教程[M].武汉:武汉大学出版社,2008.
[2]http://blog.chinaunix.net/xmlrpc.php?r=blog/article&uid=28541347&id=4598089

版权声明:感谢您对博文的关注!2017年秋季校招已经开始,有需要内推腾讯的可以QQ(1589276509)联系我哈,期待你的加入。

c++引用与指针的区别(着重理解)

★ 相同点:     1. 都是地址的概念;     指针指向一块内存,它的内容是所指内存的地址;引用是某块内存的别名。     ★ 区别:     1. 指针是一个实体,而引用仅是个别名; ...

c++之引用的本质

引用变量是c++引入的重要机制。错误观念:引用本质只是别名,在符号表中ri和i对应于相同的变量地址int i=5; 0100437E mov dword ptr [i],5 ...

C++引用本质

在看这篇文章之前,请你先要明白一点:那就是c++为我们所提供的各种存取控制仅仅是在编译阶段给我们的限制,也就是说是编译器确保了你在完成任务之前的正确行为,如果你的行为不正确,那么你休想构造出任何可执行...
  • zhubosa
  • zhubosa
  • 2013年09月09日 20:58
  • 1983

C++ : 引用的实质理解 !!!!

引用的定义:在类型和标识符之间加上一个取地址符,说明该标识符为指定类型的引用         如: int &ref = num;   就是ref是一个int型的引用     引用有什么作用呢?...

C++引用和指针的本质差别

以下所讨论,都是C++中的概念.   编译代码的时候,在符号表中,引用的地址是引用的变量的地址,指针的地址,是指针自身的地址.也就是说,引用本质上是不存在的,而指针是确实存在的. int a = ...

C++之引用的详解

详细剖析c++中的引用,消除心中久疑虑,拨开云雾见天明。交流学习,共同进步。...

深入分析C++引用

Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE ...

C++为什么要提出引用

直接给出结论:引用可以理解成在被一些条件束缚住的指针,这里并不是说引用是指针,我的意思是,当指针被一些条件束缚住,他的属性等于引用。这些条件是:1,不是空指针,2,指向的内存必须存在,3,...

C++中引用(reference)的用法详解

C++中引用(reference)的用法详解 TOC 1.简介 2.引用的语法 3.引用使用技巧     3.1 引用和多态     3.2 作为参数     3.3 作为返回值     3....

C++拾遗--引用(左值引用、右值引用)

C++拾遗--引用 前言 引用就是别名(alias)。所谓别名,就是对已存在的对象另起一个名字。本身含义并不难理解,但与其它概念一组合,就成了使用难点。再加上新标准提出了新的一种引用-右值引用...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:C++中引用的本质
举报原因:
原因补充:

(最多只允许输入30个字)