求解C++中引用的实现原理

TheNewIpad 2014-03-11 10:35:29
突然对引用有所好奇,于是就拿vs2008验证了如下代码

int i = 0;
int &j = i;
j = 10;
int *p = &j;


在debug模式下,查看其汇编代码。 如下

;int i = 0;
mov dword ptr [i],0 ; i所在空间赋值为0;

;int &j = i;
lea eax, [i] ; 将i的地址放入eax寄存器
mov dword ptr[j],eax ; 将i的地址放在赋值给j. 这很费解。 疑点1

; j = 10;
mov eax, dword ptr[j] ; 取j的地址放入eax, j的地址跟i的地址相同
mov dword ptr [eax], 0Ah ; 将j指向的地址修改为10;

;int *p = &j;
mov eax,dword ptr [j] ; 疑点2,
mov dword ptr [p],eax


这怎么看 怎么想引用跟指针一样。 但是有如下疑问

1、 在调试时,进入上述代码所在的函数,哪怕还没有执行到对应的代码,我都可以使用watch1查看i的地址,却看不到j的地址,在执行到int &j = i这个引用初始化时,看到了&j的地址跟&i的地址一致。 可是,通过memory工具,可以发现在疑点1执行完毕后,确实有一个栈空间被赋值了&i(我又理由相信这跟j关联密切的一个地址); 我暂且认为疑点1处的eax的值为i的地址,并且被赋值给了j。 即,j占用的4个字节的空间存放的值是&i;
那为啥&j的值跟&i的值一致, 我在memory中看到的疑点1时是同&i不一样的一个地址。

2、 我以为引用同指针,编译器实际上为引用变量分配了空间,用于存储被应用对象的地址,(见疑点一), 那么int *p = &j中疑点2,我就又不理解了。此处真的用了j的地址。而不是j的值
...全文
745 25 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
25 条回复
切换为时间正序
请发表友善的回复…
发表回复
wswxfwps 2016-08-24
  • 打赏
  • 举报
回复
我认为引用可能类似 一个常量指针池,用户需要用的时候直接获取一个指针去指向需要指向的对象,而不用临时去开辟,因此才能提高效率
kissSimple 2014-08-24
  • 打赏
  • 举报
回复
问题问的很好,回答的也挺好。我喜欢
赵4老师 2014-04-09
  • 打赏
  • 举报
回复
为什么不参考g++相关源代码呢?
神-气 2014-04-08
  • 打赏
  • 举报
回复
17楼是对的,C++的标准中只规定了指针和引用的语义,并没有规定两者的实现方式。而实际上引用基本上都是通过指针实现的。 这个指针可能是指向堆上,也可能是指向栈上。 void show(const int& val) { cout << val<<endl; } int main(int argc, char* argv[]) { //int val = 1; show(1); return 0; } 如这段代码,show的参数实则是栈上的一个值 “1”的地址。
shiguojie19892 2014-03-13
  • 打赏
  • 举报
回复
太深奥了,围观下
TheNewIpad 2014-03-13
  • 打赏
  • 举报
回复
引用 18 楼 menzi11 的回复:
lea eax, [i] ; 将i的地址放入eax寄存器,此时eax保存着i的地址 mov dword ptr[j],eax ; 将i的地址存入j.如果把j看做指针,那j里面存的不本来就应该i的地址吗,有何疑点? ;* j = 10; mov eax, dword ptr[j] ; 此处你原话有误,这句的意思是把j的,即i的地址存入eax mov dword ptr [eax], 0Ah ; 把i所在的数据,即j指向的地址的值改为10. ;int *p = j; mov eax,dword ptr [j] ; 这里我也没看出有什么疑点,此句的意思是把j的,即i的地址存入eax mov dword ptr [p],eax 这里就是把j的,即i的地址存入p所在的位置了,有何不对? [/code]
妥妥滴,没问题了。 当时蒙了,因为我发现我通过watch查看j的地址,在还没有初始化的时候,居然不显示地址。 但是在初始化之后,取地址显示的i的地址,通过memory却能发现j确实有一个真实的地址。 我给矛盾了。 我估计是vs为了维持C++语法,在watch 和 memory上实现了上述上我纠结的地方。
TheNewIpad 2014-03-12
  • 打赏
  • 举报
回复
咋没个牛人给肯定下啊。 也不知道正确与否。 待我新开一贴。 重新说这事
TheNewIpad 2014-03-12
  • 打赏
  • 举报
回复
引用 12 楼 u010832643 的回复:
昨天也被问到了这个问题,有点搞不清楚。 只是觉得引用像一个能自动被编译器间接引用的常量指针。
看看7楼的描述, 感觉vs9的实现引用的特性如下 1、 引用变量在32位系统下占用4个字节,其目的是保存被引用对象的地址。 2、 当在访问引用变量时,首先取得特性1保存的被引用对象的地址。 3、 然后调用相关方法对引用对象进行访问。
coffeecato 2014-03-12
  • 打赏
  • 举报
回复
昨天也被问到了这个问题,有点搞不清楚。 只是觉得引用像一个能自动被编译器间接引用的常量指针。
赵4老师 2014-03-12
  • 打赏
  • 举报
回复
计算机组成原理→DOS命令→汇编语言→C语言(不包括C++)、代码书写规范→数据结构、编译原理、操作系统→计算机网络、数据库原理、正则表达式→其它语言(包括C++)、架构…… 对学习编程者的忠告: 眼过千遍不如手过一遍! 书看千行不如手敲一行! 手敲千行不如单步一行! 单步源代码千行不如单步对应汇编一行! 单步类的实例“构造”或“复制”或“作为函数参数”或“作为函数返回值返回”或“参加各种运算”或“退出作用域”的语句对应的汇编代码几步后,就会来到该类的“构造函数”或“复制构造函数”或“运算符重载”或“析构函数”对应的C/C++源代码处。 VC调试时按Alt+8、Alt+7、Alt+6和Alt+5,打开汇编窗口、堆栈窗口、内存窗口和寄存器窗口看每句C对应的汇编、单步执行并观察相应堆栈、内存和寄存器变化,这样过一遍不就啥都明白了吗。 对VC来说,所谓‘调试时’就是编译连接通过以后,按F10或F11键单步执行一步以后的时候,或者在某行按F9设了断点后按F5执行停在该断点处的时候。 (Turbo C或Borland C用Turbo Debugger调试,Linux或Unix下用GDB调试时,看每句C对应的汇编并单步执行观察相应内存和寄存器变化。)
TheNewIpad 2014-03-12
  • 打赏
  • 举报
回复
引用 8 楼 buyong 的回复:
memory工具这里显示不准确吧。j和i当然是一个地址。
是7楼写的。
TheNewIpad 2014-03-12
  • 打赏
  • 举报
回复
引用 8 楼 buyong 的回复:
memory工具这里显示不准确吧。j和i当然是一个地址。
你看看8楼写的。 而且当我把8楼里面的引用类型从int & 改为double &后, objT的大小仍然为8. 从这点看,这是多么明显的指针特性的。
buyong 2014-03-12
  • 打赏
  • 举报
回复
memory工具这里显示不准确吧。j和i当然是一个地址。
TheNewIpad 2014-03-12
  • 打赏
  • 举报
回复
引用 6 楼 zhao4zhong1 的回复:
指针即地址 引用即别名
谢谢赵老师回复,我是想搞明白,引用在某个具体编译下,在汇编角度是怎么实现的。 还了一个中实现方式,我大概搞清楚了。
class CTest
{
public:
    CTest(int &nVal)
        : m_nRefVal(nVal)
        , m_nVal(nVal * 1000)
    {

    }

public:
    int m_nVal;
    int &m_nRefVal;
};

int _tmain(int argc, _TCHAR* argv[])
{

    int i = 10;

    CTest objT(i);

    objT.m_nRefVal = 1000;


    system("pause");
    return 0;
}
objT的大小为8,根据其声明顺序,objt的内存布局是 m_nVal -> m_nRefVal; 其中m_nVal = 2710; m_nRefVal地址存放的是i的地址,我的例子中i的地址是0x002dfef8 所以objT的内存布局是, 10, 27, 00, 00, f8, fe, 2d, 00 再执行objT.m_nRefVal = 1000时, 执行如下代码 mov eax,dword ptr [ebp-14h] // 取objT.m_nRefVal的值,即i的地址放入eax寄存器 mov dword ptr [eax],3E8h // eax的值指向的地址赋值为0x3e8; 这不就是典型的指针么?
赵4老师 2014-03-12
  • 打赏
  • 举报
回复
指针即地址 引用即别名
TheNewIpad 2014-03-12
  • 打赏
  • 举报
回复
引用 4 楼 menzi11 的回复:
楼主首先,不要去假设编译器如何去优化变量,但是引用和指针之前的区别确实如楼主所说, 曾经有一篇老外文章"如何编写一个比C语言更快的语言"中,提到了C语言缺少引用类型 所可能导致的效率降低.其中详细描述了引用和指针的区别,例如,正如大多数教材所误导你的, "引用和指针在很多情况下实现一致",都是通过一个指针来实现的,但以下情况不同:

int i=1;
int* p=&i;
*p=1;

int i=1;
int& p=i;
p=1;
这两段代码的区别,在于第一段代码中,编译器并不了解我们对i取地址"&"的行为 实际上只是希望之后可以对i赋值,而并不关心取地址本身这个操作,由于它无法 了解我们的目的所在(这段代码中可能能了解,但再复杂一点就不行了),因此编译器 将不得不让变量i持有一个栈上的内存空间,以便使i确实拥有一个地址可以被"&", 而在第二段代码中,由于我们使用了引用,编译器将了解我们只是想通过某种方式 访问到i这个变量,而完全不关心i的地址,因此编译器可以根本不给变量i申请内存地址, 而是让i从头到尾都存放在某个寄存器中,使得对i的读写实际上都变成对寄存器的读写. 而根本不需要和内存交互. 也就是说,任何对取地址的操作,都会使编译器产生一种:"这个被取地址的变量一定要在内存里"的错觉. 例如,有时我们需要强制类型转换,经常遇见这种代码:

int32 i=0x313671;
//.................
float k=*(float*)&i;
我们的目的无非就是希望i中的每一个bit都被完整地复制到k中,而编译器不这样认为,编译器认为 你对i进行了&操作,那么i必然在内存中,这段代码就会产生内存读写操作. C++中上例的正确方法应该是:

int32 i=0x313671;
union {
  int a;
  float b;
} conv;
conv.a=i;
float k=conv.b;
没有取地址操作,则编译器能对这段代码更好地优化.将i和k全部放入寄存器中. 同理的,C语言中经常有函数是如下形式:

typedef struct Data_tag {
.........
.........
.........
} Data;

void process(Data* y,Data* x){...};

.............
Data a,b;
process(&a,&b);
其中无论编译器决策process函数是否inline,C语言(C++同理)都会认为无论如何,对变量 a和b必须进行取地址的操作,在process函数中代表a和b的必须是指针而不是其他形式. 而这样:

struct Data {
.........
.........
.........
};

void process(Data& y,Data& x){...};

.............
Data a,b;
process(a,b);
时,如果编译器认为process函数不能inline,那么这两段代码的行为是一致的,但一旦遇到 可以inline process函数的情况,编译器会马上了解到我们在process函数中只是需要处理 a和b,而不在乎a和b的地址.编译器不需要创建一个指针来指向a和b的地址,而是直接在process函数 中使用a和b所在的地址就可以了. 所以在一些对性能要求严格的位置,到底是使用引用还是使用指针,是很有讲究的. 对于编译器来说,使用引用可能能够带来更多的优化空间(虽然自由度下降了).
谢谢耐心回复。 确实涨见识了。 可是,我本意并不是想弄清楚如何优化之类的。 我就是想知道类似VS编译器,是如何实现引用的。 特别是疑点1, 把i的地址,复制给了j所在的地址。 从这一点看,貌似i 和 j的地址不一样。 但是, 在疑点二处,取j的地址却和i相同。 这叫我感觉矛盾。
lyramilk 2014-03-12
  • 打赏
  • 举报
回复
引用就是指针换个操作方式。至于你说的疑点跟本不存在。 C/C++是在语言的层面上,有i和j之分,到了汇编里,没有i对应谁j对应该的说法。指针需要一个sizeof(void*)大小的空间存储目标地址,引用它也需要。另外,编译器为你标的i和j不是完全正确的。 关于引用和指针这个东西,没必要求证。你知道引用就是指针就可以了,只不过编译器为了你操作简便而让你直接访问而不需要取值后再访问。
menzi11 2014-03-12
  • 打赏
  • 举报
回复
引用 5 楼 TheNewIpad 的回复:
[quote=引用 4 楼 menzi11 的回复:] 楼主首先,不要去假设编译器如何去优化变量,但是引用和指针之前的区别确实如楼主所说, 曾经有一篇老外文章"如何编写一个比C语言更快的语言"中,提到了C语言缺少引用类型 所可能导致的效率降低.其中详细描述了引用和指针的区别,例如,正如大多数教材所误导你的, "引用和指针在很多情况下实现一致",都是通过一个指针来实现的,但以下情况不同:

int i=1;
int* p=&i;
*p=1;

int i=1;
int& p=i;
p=1;
这两段代码的区别,在于第一段代码中,编译器并不了解我们对i取地址"&"的行为 实际上只是希望之后可以对i赋值,而并不关心取地址本身这个操作,由于它无法 了解我们的目的所在(这段代码中可能能了解,但再复杂一点就不行了),因此编译器 将不得不让变量i持有一个栈上的内存空间,以便使i确实拥有一个地址可以被"&", 而在第二段代码中,由于我们使用了引用,编译器将了解我们只是想通过某种方式 访问到i这个变量,而完全不关心i的地址,因此编译器可以根本不给变量i申请内存地址, 而是让i从头到尾都存放在某个寄存器中,使得对i的读写实际上都变成对寄存器的读写. 而根本不需要和内存交互. 也就是说,任何对取地址的操作,都会使编译器产生一种:"这个被取地址的变量一定要在内存里"的错觉. 例如,有时我们需要强制类型转换,经常遇见这种代码:

int32 i=0x313671;
//.................
float k=*(float*)&i;
我们的目的无非就是希望i中的每一个bit都被完整地复制到k中,而编译器不这样认为,编译器认为 你对i进行了&操作,那么i必然在内存中,这段代码就会产生内存读写操作. C++中上例的正确方法应该是:

int32 i=0x313671;
union {
  int a;
  float b;
} conv;
conv.a=i;
float k=conv.b;
没有取地址操作,则编译器能对这段代码更好地优化.将i和k全部放入寄存器中. 同理的,C语言中经常有函数是如下形式:

typedef struct Data_tag {
.........
.........
.........
} Data;

void process(Data* y,Data* x){...};

.............
Data a,b;
process(&a,&b);
其中无论编译器决策process函数是否inline,C语言(C++同理)都会认为无论如何,对变量 a和b必须进行取地址的操作,在process函数中代表a和b的必须是指针而不是其他形式. 而这样:

struct Data {
.........
.........
.........
};

void process(Data& y,Data& x){...};

.............
Data a,b;
process(a,b);
时,如果编译器认为process函数不能inline,那么这两段代码的行为是一致的,但一旦遇到 可以inline process函数的情况,编译器会马上了解到我们在process函数中只是需要处理 a和b,而不在乎a和b的地址.编译器不需要创建一个指针来指向a和b的地址,而是直接在process函数 中使用a和b所在的地址就可以了. 所以在一些对性能要求严格的位置,到底是使用引用还是使用指针,是很有讲究的. 对于编译器来说,使用引用可能能够带来更多的优化空间(虽然自由度下降了).
谢谢耐心回复。 确实涨见识了。 可是,我本意并不是想弄清楚如何优化之类的。 我就是想知道类似VS编译器,是如何实现引用的。 特别是疑点1, 把i的地址,复制给了j所在的地址。 从这一点看,貌似i 和 j的地址不一样。 但是, 在疑点二处,取j的地址却和i相同。 这叫我感觉矛盾。[/quote] 我写这个是因为我实在搞不明白你哪里不明白.....这段汇编没有任何不对啊,按照我说的,不优化的情况下 一般引用就等于指针了,那么就相当于:

int i  = 0;
int* j = i;
*j      = 10;
int* p = j;
我们再看看汇编:

;int i = 0;
mov dword ptr [i],0   ; i所在空间赋值为0;
 
;int* j = i;
lea  eax, [i]            ; 将i的地址放入eax寄存器,此时eax保存着i的地址
mov  dword ptr[j],eax    ; 将i的地址存入j.如果把j看做指针,那j里面存的不本来就应该i的地址吗,有何疑点?
 
;* j = 10;
mov eax, dword ptr[j]     ; 此处你原话有误,这句的意思是把j的,即i的地址存入eax
mov dword ptr [eax], 0Ah  ; 把i所在的数据,即j指向的地址的值改为10.
 
;int *p = j;
mov eax,dword ptr [j]     ; 这里我也没看出有什么疑点,此句的意思是把j的,即i的地址存入eax
mov dword ptr [p],eax  这里就是把j的,即i的地址存入p所在的位置了,有何不对?
lm_whales 2014-03-12
  • 打赏
  • 举报
回复
1)指针即地址(附加了所指对象的类型信息,地址的信息是不完整的, 需要指针指向的对象的类型信息,类型信息才完整) 引用即别名(附加了被引用对象的类型信息,简单的别名不能确认类型, 所以引用类型,需要被引用对象的类型信息才完整) 这是概念 2)实现方式 引用一般就是隐藏一个指针,来实现的, 当然,也可以用别的方式实现,不过如果那样的话, 引用的效率(空间占用效率,和存取速度)就很成问题,至少比指针要差。 引用可以不占用任何内存,因为引用即别名,但是常常必须占用内存, 因为引用并不总是,即用即丢的,经常需要在引用中,保存所引用的目标(即储存被引用对象的地址)。 其实这么处理一下,就可以看到引用,这种数据的实际内存地址了;
#inlcude<cstdio>

int main(){
struct  ref_int 
{
      ref_int(int &var):r(var){};
int &r;
 };
int a;
ref_int  ref(a);
printf("&a =%p &ref.r =%p &ref = %p, ",&a,&ref.r,&ref );
//  结果如下
//   &a  == & ref.r ,这是对引用取地址的结果,根据引用即别名,引用的地址就是被引用对象的地址。所以二者地址相同
//   &ref 就是储存引用这种数据的地址,和&a 是不同的
}
menzi11 2014-03-12
  • 打赏
  • 举报
回复
楼主首先,不要去假设编译器如何去优化变量,但是引用和指针之前的区别确实如楼主所说, 曾经有一篇老外文章"如何编写一个比C语言更快的语言"中,提到了C语言缺少引用类型 所可能导致的效率降低.其中详细描述了引用和指针的区别,例如,正如大多数教材所误导你的, "引用和指针在很多情况下实现一致",都是通过一个指针来实现的,但以下情况不同:

int i=1;
int* p=&i;
*p=1;

int i=1;
int& p=i;
p=1;
这两段代码的区别,在于第一段代码中,编译器并不了解我们对i取地址"&"的行为 实际上只是希望之后可以对i赋值,而并不关心取地址本身这个操作,由于它无法 了解我们的目的所在(这段代码中可能能了解,但再复杂一点就不行了),因此编译器 将不得不让变量i持有一个栈上的内存空间,以便使i确实拥有一个地址可以被"&", 而在第二段代码中,由于我们使用了引用,编译器将了解我们只是想通过某种方式 访问到i这个变量,而完全不关心i的地址,因此编译器可以根本不给变量i申请内存地址, 而是让i从头到尾都存放在某个寄存器中,使得对i的读写实际上都变成对寄存器的读写. 而根本不需要和内存交互. 也就是说,任何对取地址的操作,都会使编译器产生一种:"这个被取地址的变量一定要在内存里"的错觉. 例如,有时我们需要强制类型转换,经常遇见这种代码:

int32 i=0x313671;
//.................
float k=*(float*)&i;
我们的目的无非就是希望i中的每一个bit都被完整地复制到k中,而编译器不这样认为,编译器认为 你对i进行了&操作,那么i必然在内存中,这段代码就会产生内存读写操作. C++中上例的正确方法应该是:

int32 i=0x313671;
union {
  int a;
  float b;
} conv;
conv.a=i;
float k=conv.b;
没有取地址操作,则编译器能对这段代码更好地优化.将i和k全部放入寄存器中. 同理的,C语言中经常有函数是如下形式:

typedef struct Data_tag {
.........
.........
.........
} Data;

void process(Data* y,Data* x){...};

.............
Data a,b;
process(&a,&b);
其中无论编译器决策process函数是否inline,C语言(C++同理)都会认为无论如何,对变量 a和b必须进行取地址的操作,在process函数中代表a和b的必须是指针而不是其他形式. 而这样:

struct Data {
.........
.........
.........
};

void process(Data& y,Data& x){...};

.............
Data a,b;
process(a,b);
时,如果编译器认为process函数不能inline,那么这两段代码的行为是一致的,但一旦遇到 可以inline process函数的情况,编译器会马上了解到我们在process函数中只是需要处理 a和b,而不在乎a和b的地址.编译器不需要创建一个指针来指向a和b的地址,而是直接在process函数 中使用a和b所在的地址就可以了. 所以在一些对性能要求严格的位置,到底是使用引用还是使用指针,是很有讲究的. 对于编译器来说,使用引用可能能够带来更多的优化空间(虽然自由度下降了).
加载更多回复(5)

65,186

社区成员

发帖
与我相关
我的任务
社区描述
C++ 语言相关问题讨论,技术干货分享,前沿动态等
c++ 技术论坛(原bbs)
社区管理员
  • C++ 语言社区
  • encoderlee
  • paschen
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
  1. 请不要发布与C++技术无关的贴子
  2. 请不要发布与技术无关的招聘、广告的帖子
  3. 请尽可能的描述清楚你的问题,如果涉及到代码请尽可能的格式化一下

试试用AI创作助手写篇文章吧