C++的那些事:你真的了解引用吗

一、引用的本质是什么

说到引用,一般C++的教材中都是这么定义的:

1,引用就是一个对象的别名。

2,引用不是值不占内存空间。

3,引用必须在定义时赋值,将变量与引用绑定。

那你有没有想过,上面的定义正确吗?编译器是如何解释引用的?

这里先给出引用的本质定义,后面我们再进一步论证。

1,引用实际是通过指针实现的。

2,引用是一个常量指针。

3,引用在内存中占4个字节。

4,在对引用定义时,需要对这个常量指针初始化。

二、探究本质

我们从最简单的变量的定义开始,看编译器会做哪些事情。

int var = 42;
mov         dword ptr [var],2Ah  // 对应汇编代码

上面语句申请了一块内存空间,占4个字节,存放了一个int型的变量。内存里放的是42的二进制码。

汇编代码向我们表达的意思就是把42写入以var为地址的内容区域。var有点像我们理解上的指针,只是编译器并没有把它抽象出来,而是让我们更表象的理解:申请一个变量,它的值为42。

那么var这个变量名放在哪呢

我们知道程序如果访问内存里的数据,需要通过地址来进行访问,所以上面的代码在经过编译器生成目标代码时,用存放42的地址了所有的var,所以结论时,目标文件中不存在var,所以变量名本身是不占内存的

而我们知道,引用是变量的一个别名。那么,从这很多人会联想到,引用会不会也只是一个名字而已,编译器在生成目标代码的时候,会用实际地址替换引用呢?

答案并非这样!

那我们接下来看看,当我们定义一个引用时,发生了什么:

1     int var = 42;
2 01303AC8  mov         dword ptr [var],2Ah
3     int&  refVar = var;
4 01303ACF  lea         eax,[var]
5 01303AD2  mov         dword ptr [refVar],eax 

上面的代码显示,当定义一个引用时,编译器将var的地址赋给了以refVar为地址的一块内存区域。也就是说refVar其实存放的是var的地址。

这让我们联想到了指针,那么我们看看定义一个指针是发生了什么:

1     int var = 42;
2 01213AC8  mov         dword ptr [var],2Ah
3     int* ptrVar = &var;
4 01213ACF  lea         eax,[var]
5 01213AD2  mov         dword ptr [ptrVar],eax 

没错,没有任何差别,定义一个引用和一个指针的汇编代码完全一致!

三、const哪里去了

相信从上面的分析时,你可能已经相信了,引用实际上就是一个指针。那么为什么说引用是一个常量指针呢,在目标代码里有什么体现呢?

这个问题其实要从C++底层机制谈起,C++为我们提供的各种存取控制仅仅是在编译阶段给我们的限制,也就是说编译器确保了你在完成任务之前的正确行为,如果你的行为不正确,那么编译器就是给你在编译时提示错误。所谓的const和private等在实际的目标代码里根本不存在,所以在程序运行期间只要你愿意,你可以通过内存工具修改它的任何一个变量的值。

这也就解释了为什么上面的两段代码中引用和指针的汇编代码完全一致。

C++设计引用,并用常量指针来从编译器的角度实现它,目标是为了提供比指针更高的安全性,因为常量指针一旦与变量地址绑定将不能更改,这样降低了指针的危险系数,它提供了一种一对一的指针。

但是你觉得使用引用就安全了吗?它同样会有与使用指针一样的问题

1 int *var = new int(42);
2 int &ref = *var;
3 delete var;
4 ref = 42;
5 return 0;

上面这段代码就很不安全,因为ref引用的内存区域不合法。

为了进一步验证引用与指针在本质上的相同,我们看当引用作为函数参数传递时,编译器的行为:

 1 void Swap(int& v1, int& v2);
 2 void Swap(int* v1, int* v2);
 3
 4     int var1 = 1;
 5 00A64AF8  mov         dword ptr [var1],1
 6     int var2 = 2;
 7 00A64AFF  mov         dword ptr [var2],2
 8     Swap(var1,var2);
 9 00A64B06  lea         eax,[var2]
10 00A64B09  push        eax
11 00A64B0A  lea         ecx,[var1]
12 00A64B0D  push        ecx
13 00A64B0E  call        Swap (0A6141Fh)
14 00A64B13  add         esp,8
15     Swap(&var1, &var2);
16 00A64B16  lea         eax,[var2]
17 00A64B19  push        eax
18 00A64B1A  lea         ecx,[var1]
19 00A64B1D  push        ecx
20 00A64B1E  call        Swap (0A61424h)
21 00A64B23  add         esp,8 

上面代码再次证明了,引用与指针的行为完全一致,只是编译器在编译时对引用作了更严格的限制。

四、引用占多大的内存空间

因为在在表达式中,使用引用实际上就像使用变量本身一样,所以直接用sizeof是得不到引用本身的大小的。

double var = 42.0;
double& ref = var;

cout << sizeof var << endl;  // print 8
cout << sizeof ref << endl;   // print 8

我们可以通过定义一个只含有引用的类来解决这个问题:

1 class refClass{
2 private:
3     double& ref;
4 public:
5     refClass(double var = 42.0) :ref(var){}
6 };
7
8 cout << sizeof refClass << endl;  // print 4

所以结论就是引用和指针一样实际占内存空间4个字节。

参考文章:http://www.cnblogs.com/rollenholt/articles/1907408.html

C++的那些事:你真的了解引用吗,码迷,mamicode.com

时间: 2024-10-14 21:21:53

C++的那些事:你真的了解引用吗的相关文章

不加班的实践(1)——这真的该用try-catch吗?

前言 我有个技能,就是把“我”说的听起来特别像“老子”. 以前是小喽啰的时候,会跟领导说“我!不加班.”,听起来就像“老子不加班!”一样.到最后发现,我确实没有把计划内的工作拖到需要加班才能完成,这个“老子”也就慢慢的被承认了!到后来我带队的时候,我说“我不让你们加班!”,这个时候听起来绝对不像“老子不让你们加班!”,后来他们也真的不加班就把项目漂亮的做完了,相信他们愿意承认我有“老子”的本事. 哈哈!这个有点妄自尊大了哟.做到就可以了,千万别这么讲啊!低调,低调.不过什么事情我都能做到不加班,

&lt;C++&gt; 函数默认参数 函数重载 引用

一.函数默认参数 1.缺省参数:就是在声明函数的某个参数的时候 给他一个默认值 1 #include<iostream> 2 using namespace std; 3 4 void Show(int a = 1,int b = 2,int c = 3) 5 { 6 cout << a << endl; 7 cout << b << endl; 8 cout << c << endl; 9 } 10 11 int mai

这一次,彻底解决Java的值传递和引用传递

本文旨在用最通俗的语言讲述最枯燥的基本知识 学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨论的话题又是充满争议:有的论坛帖子说Java只有值传递,有的博客说两者皆有:这让人有点摸不着头脑,下面我们就这个话题做一些探讨,对书籍.对论坛博客的说法,做一次考证,以得出信得过的答案. 其实,对于值传递和引用传递的语法和运用,百度一下,就能出来可观的解释和例子数目,或许你看一下例子好像就懂,但是当

堆栈详解 + 彻底理解Java的值传递和引用传递

本文旨在用最通俗的语言讲述最枯燥的基本知识 学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨论的话题又是充满争议:有的论坛帖子说Java只有值传递,有的博客说两者皆有:这让人有点摸不着头脑,下面我们就这个话题做一些探讨,对书籍.对论坛博客的说法,做一次考证,以得出信得过的答案. 其实,对于值传递和引用传递的语法和运用,百度一下,就能出来可观的解释和例子数目,或许你看一下例子好像就懂,但是当

什么是紧急的事呢?

什么是紧急的事呢?这里引用极客时间<技术管理实战36讲>专栏作者的一个观点来回答这个问题: 对于“计划内工作”,看收益是否足够大.收益越大越重要,也就越需要给予相匹配的优先级.资源和关注度:收益相对不大,就放入“To do list”,作为待办任务处理. 对于“计划外的工作”,看损失是否足够大.损失够大,就按照紧急任务安排,以止损为核心目的:如果损失可控,就放入“计划内工作”列表. 原文地址:https://www.cnblogs.com/syw20170419/p/11244839.html

Binder学习笔记(十一)—— 智能指针

轻量级指针 Binder的学习历程爬到驱动的半山腰明显感觉越来越陡峭,停下业务层的学习,补补基础层知识吧,这首当其冲的就是智能指针了,智能指针的影子在Android源码中随处可见.打开frameworkds/rs/cpp/util,RefBase.h和StrongPointer.h两个文件,代码多读几遍都能读懂,可是串起来总感觉摸不到骨架,把不住主线.闭上眼零零星星的点串不成一条线.究其原因应该是此处使用了模式,最好先剔除掉业务层的皮肉,把模式的骨架摸个门清,再回来看代码就会势如破竹了. 不是多

14 个折磨人的 JavaScript 面试题

前端工程师有时候面试时会遇到一类面试官,他们问的问题对于语言本身非常较真儿,往往不是候选人可能期待的面向实际的问题(有些候选人强调能干活就行,至于知不知道其中缘由是无关痛痒的).这类题目,虽然没有逻辑,但某种程度说,确实考察了候选人对于javascript这门语言的理解. 突然想到这个话题是无聊在翻自己的Github,看看以前都写过什么丑货.然后翻到了这篇解释Javascript quiz的文章quiz-legend,反正没事儿,就想搬过来供大家学习.理解.背诵.批判. 问题一 (functio

WebService的事务处理 (转)

因为这个问题讨论起来内容比较多一些,所以另开一个话题.     如果你只是要解决两个系统之间的事务同步问题,可以采用判断服务是否成功的办法来解决,即:        * A系统开始自己的事务,处理自己的数据,然后...    * 在提交之前调用B系统的服务.    * B系统开始自己的事务B,在事务中处理数据,再提交事务.    * B系统把自己事务的提交成功与否的信息做为返回值回馈A系统.    * A系统根据B的事务成功情况决定自己的事务是否提交或是回滚.     但是,在继续深入讨论这个问

如何从计算机相关专业转为前端工程师且能在两年内月薪过万?

最近了解到有几个从事计算机相关专业的朋友,他们想转前端却又不知道该怎么做,甚至觉得心有余而力不足,正好自己一年前也有这样的打算,自己刚好也做了好多准备,所以就冒昧的也是时候改给那段经历做个了解了,也希望能给他人一点参考. 先讲讲自己的那段时间的经历和想法吧. 以前在搜房时那时项目组前后端没有完全分开,自己又做前端又做后端开发,只是不懂的或者不会的,网上又没找到刚刚好的,就去问问公司大牛,或者直接让他们帮忙敲代码,这个过程很重要,因为别人在帮你敲代码的时候你可以问很多问题,比如怎么调试这段代码,怎