深入理解C++的指针和引用

每位学习过C++的程序员都知道指针其实就是地址(这样理解是不完全正确的,但也无可厚非,为了方便暂且这么理解着吧)。在C++中引用的内在表现其实也是地址,但引用和指针的外在表现是不同的。也就是说从汇编语言级和机器语言级来理解,在C++中指针和引用是完全等价的,没有任何的区别。但是反应到C++程序语言的语法上,指针和引用是两个完全不同的概念。之所会如此完全是因为C++编译器程序“搞的鬼”。接下来就让我们一起学习,一层一层的揭开C++中指针和引用的面纱,看看他们的本质到底是什么。

下面我首先来做一个错误的论述,因为这个论述可能已经深入人心了。并且可能是从我们刚学习C++语法时就被灌输了这样的思想。但这种对C++指针和引用的理解只能说是理解了C++中指针和引用的表现形式,而没有真正明白其在计算机内存中的本质。好了,费话有点多,下面进入正题。

在此先郑重声明一下,在接下来的论述中,凡是包含在“{}”中的全是指针和引用在C++语法中的表现形态,而非其在计算机内存中的本质。当我们学习到指针和引用在计算机内存中的本质时,你会发现这些论述可能是错误的。

//注意有花括号

因为指针作为地址指向的是一个用来存储地址的内存空间,即它所指向的内存中存储了“它指向的目标类型变量”的地址(这句话如果用英语定语从句表达就比较好理解了);当要访问指针指向的内容时,需要先通过指针取出其中存储的目标地址,再通过取出来的目标地址去找到我们要访问的内存,从而达到我们的访存目的,这是一个间接寻址的过程,并且属于“二次访存的”间接寻址。而引用没有指针那么复杂,它作为地址指向的是存储目标类型变量的内存空间,可以直接使用它访问目标内存空间;属于直接寻址。

//花括号结束

//虽然下面的比喻是我们学习C++的启蒙,但也改变不了它是错误论述的命运

做个比喻,把地址比作钥匙,把内存空间比作抽屉(在C++经典教材中经常做这种比喻)。一个地址对应一段内存空间,也就是一把钥匙对应一个抽屉。对于引用来说,这把钥匙你是直接拿在手里的,可以直接使用去那个抽屉里取你需要的东西。而对于指针来说,这把钥匙是被锁在一个抽屉里的,你是手中拿着一把可以打开这个抽屉的钥匙(用英语中的定语从句理解更容易懂)。

引用,开一次抽屉就能达到目的,很直接。指针则要开两次抽屉,所以是间接的达到目的。

//当知道这个比喻不正确的时候,我在感性思想上也有些接受不了

//虽然现在知道了这论述是错误的,但我曾经证明过它是对的

如果你对C++程序进行调试,并在Watch窗口中查看指针变量和引用变量的信息,通过分析总结就能够完全理解C++中指针和引用的内涵了。

如果真的这样做了你还会发现一些有趣的东西。凡是玩过汇编语言的都应该知道,我们所声明的变量,在程序段中都是以一个32位的整数表示,其实这就是一个内存空间的地址(对变量的访问属于直接寻址)。对所有变量求地址后,仔细查看Watch窗口中的信息,你会发现,你声明的“引用”所对应的地址和你这“引用”所引用对象所对应的地址是完全一样的。地址一样指向的存储空间就是同一个内存空间,也就是同一个变量了。再看看指针,你会发现指针对应的地址,和指针所指向变量名所对应的地址是不同的。那么它们的联系在哪儿呢?直接在Watch中查看指针名(得到的Value是指针变量所对应地址的内存中存储的内容,也就是指针指向的内存地址),你会发现指针中存放的是一个32位的地址,而这个地址正是指针所指向的变量所对应的地址。

空口大白话也没什么说服力,下面我来写段代码调试一下,有图有真相。

VC6.0中编写代码如图(1)所示

图片

图(1

从图中可以看到,我们在程序结尾处设置了断点。这样做的目的是让程序运行完,所有的变量都有值。如果程序没有运行到最后,那么在一些语句执行之前相关的变量是没有被分配存储空间的。

Watch的内容如图(2)所示

图片

图(2

观察全局变量a,b,c;从图(2)中可以看到对变量a和变量b求地址,得到的值是相同的,也就是说变量a和变量b对应的是同一内存空间。也即他们是同一个变量,只是在C++中的名字不同而已。而指针变量c的地址不是我们关心的,我们关心的是指针变量c中所存储的内容,这内容正是变量a和变量b共同的地址。

再看局部变量aa,bb,cc,从图(2)中可以看到它们之间的关系与变量a,b,c之间的关系相同。这样也就验证了上面的论述。

//其实以上“{}”中的内容不能算是错,只能说是在C++的语法范畴内适用,出了C++就不完全正解了。

//但接下来这个结论中的几句话,就需要特别关注了,尤其是红色字体那一句。

通过以上分析可以得出结论:变量和引用是一样的,引用只是变量的一个别名,其对应的内存空间与变量相同,是同一个变量。用引用访问内存时与变量相同都是直接寻址。指针与引用不同,指针中存储的是变量的地址。使用指针访问内存时,需要先从指针变量中取出变量的地址,然后根据变量地址访问变量对应的内存空间,属于(二次访存)间接寻址。

//现在想想当初证明的是一个错误观点,而且还是有理有据,真的很感慨。但我们都是一路从错误中走来,最终找到了正确的方向

看到上面这么多的花括号,是不是很不服气?里面的论述有理有据,凭什么说人家是错误的呢?证明上面这些花括号里的内容错误,还真不是件容易的事,还是别花时间费精力的证明它错了。接下来我就把另外一种论述分享给大家,并且证明接下来的论述才是正确的。

请仔细听好下面这句话:其实在C++中引用和指针在内存中的表现是完全相同的,并且在汇编语言级和机器语言级上完全一至,没有任何区别。它们都是用来存储其它类型变量地址的内存块,并且都是(二次访存)间接寻址。

看到这句话,你会不会很惊讶?但它确确实实是正解的。

接下来还是有图有真相,开始分析。

程序的代码还是如图(1)所示的VC6.0环境下的代码,同样是编译组建后进行调试。进入调试之后,我们查看C++代码编译后的汇编语言代码来分析。VC6.0对图(1)中的C++代码编译成的汇编代码如图(3)和图(4)所示(这里只截取了需要分析的主要代码)。

图片

图(3

 

从图(3)和图(4)中可以看到,变量的声明部分是没有生成可执行的汇编代码的(在数据库段定义部分肯定要有分配内在的代码了)。而分析引用变量声明并赋值的代码可知,引用是被重新分配了存储空间的,与引用对应的内存中存储的是所引用变量的地址(这好像是指针的特性吧?哈哈,往下会更有意思)。可见仅简单的分析一下生成的汇编代码就能知道引用并不像本文章中最开始论述的那样。

下面集中精力看一下图(4)中的局部变量,局部变量aa声明并初始化,是在stack中分配了存储空间,并把初始值存到了stack中。往下看可以发现变量aa的引用和指针是完全一样的。都是在stack中分配内存并存储变量aa的地址。

图片

图(4

再看看全局变量a,b,c的赋值操作的代码,可以发现,对引用b和指针c的操作完全相同。那对于局部变量呢,比较一下引用bb和指针cc看一下。很明显对他们的操作也完全相同。

通过以上分析可以得出,在C++中引用和指针在汇编语言级是完全相同的,都是以(二次访存间接寻址方式对数据进行操作的。

虽然得到了这样的结论,但这样的结论有什么用呢?我们用C++编程,就是为了避免像汇编语言那种复杂性。而在C++封装后的特性中,引用和指针是完全不同的两个概念。而封装后的引用看起来真的就像和变量一样,可以像使用变量一样使用它。我们完全不用去关心他在汇编语言级的本质,就更不要提机器语言了。

是的,如果不考虑代码的效率问题,的确可以不去关心引用和指针在汇编语言级和机器语言级的本质。如果考虑到代码的执行效率时,就不得不去深入探讨它们在低级语言中的本质了。如果按C++中对引用特性的理解,对它的操作应该归到直接寻址里面,但实际上它却是要二次访存的间接寻址。对于数据的访问速率大家都应该知道,内存快于外存,缓存快于内存,寄存器快于内存。在寻址的时候,直寻址要比间接寻址快,在间接寻址中,访存次数越少速率越快。那么你把引用当成直接寻址,他就要比指针访问的间接寻址速率要快。但实际上,它们的速率是相同的,因为在机器语言级,实现他们的机器指令是一样的,都是二次访存的间接寻址。

至此,C++用的引用和指针的特性已经讨论完毕了,本文不仅对于在C++中如何理解引用和指针进行了详细的说明,还在汇编语言级对二者的本质进行了论述。至于如何灵活巧妙的使用它们,就需要你在编写代码的过程不断的熏陶了,阅读别人的东西是无法真正掌握它们的使用技巧的。

    最后说句题外话,其实 Java 中的对象引用和 C++ 中的引用在使用上是完全相同的(内部实现可能不同,但完全可以当成一样东西来理解,因为外在表现是完全相同的)。关于这部分的分析和论述,可以参阅我将来要写的一篇《 Java C++ 互译——“对象引用”的解决方案》。 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
智能指针C++中的一种特殊类型的指针,它能够自动管理内存的释放,避免了手动释放内存的繁琐和容易出错的问题。C++11引入了几种智能指针,包括带引用计数的智能指针和不带引用计数的智能指针,如auto_ptr, scoped_ptr, unique_ptr, shared_ptr, weak_ptr等。\[1\] 带引用计数的智能指针(shared_ptr)使用引用计数来跟踪指针引用次数,当引用计数为0时,自动释放内存。这种智能指针适用于多个指针共享同一块内存的情况,可以避免内存泄漏和悬挂指针的问题。 不带引用计数的智能指针(unique_ptr)则采用了独占所有权的方式,确保每个指针只能指向一个对象。当指针超出作用域或被显式释放时,内存会被自动释放。这种智能指针适用于需要独占资源的情况,如动态分配的内存、文件句柄等。 自己实现智能指针可以通过重载指针操作符和使用引用计数等技术来实现。但需要注意的是,自己实现智能指针需要考虑线程安全性和异常安全性等问题,确保指针的正确释放和资源的正确管理。 然而,需要注意的是,智能指针并不能解决所有的问题。例如,当存在循环引用时,带引用计数的智能指针(shared_ptr)可能导致内存泄漏。此外,使用智能指针时也需要注意避免裸指针和智能指针混用的情况,以免出现悬挂指针或重复释放内存的问题。\[2\]\[3\] 总之,深入掌握C++智能指针需要了解各种智能指针的原理、用法和适用场景,并注意使用智能指针的注意事项,以确保代码的正确性和安全性。 #### 引用[.reference_title] - *1* *2* [【C++】深入掌握智能指针](https://blog.csdn.net/weixin_45853856/article/details/121184992)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] - *3* [722-深入掌握C++智能指针](https://blog.csdn.net/LINZEYU666/article/details/120982178)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control_2,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值