好久没写博,平时都是把笔记写在有道云笔记里,最近国庆,克服懒癌来写写。距离上次写博,都快半年了,以后要勤快点了。话说霹雳布袋戏又要出新剧集了。
进入正题,写这篇文章的原因
之前看过几本C++的书,只记得C++的引用只是个所谓的“别名”,但是总感觉这东西很奇怪,当时也没具体去深究它,最近在看一本金山的一个哥们写的《深入应用C++ 11》,里面在聊到右值引用的时候,对引用有一句描述:
无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的别名。
这里暂不深入讨论C++的左值引用和右值引用。
书中的这句话又让我想起之前的疑问,什么叫“不拥有所绑定对象的内存”?是说引用不占用内存??比如下面的代码:
#include <stdio.h>
class A{
public:
int i;
int j;
};
int main(){
A * p = new A;
A& q = *p;
printf("%p %p \n",p,&q);
return 0;
}
上面的代码编译后执行,输出结果为:
0x7f98d0404aa0 0x7f98d0404aa0
也就是看起来好像p和q是同一个东西,但是…
这里的q
是一个引用,那到底q
占不占内存呢?看看汇编代码就知道了:
Disassembly of section .text:
0000000100000f20 <_main>:
100000f20: 55 push %rbp
100000f21: 48 89 e5 mov %rsp,%rbp
100000f24: 48 83 ec 20 sub $0x20,%rsp
100000f28: b8 08 00 00 00 mov $0x8,%eax
100000f2d: 89 c7 mov %eax,%edi
100000f2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
100000f36: e8 2f 00 00 00 callq 100000f6a <_main+0x4a>
100000f3b: 48 8d 3d 50 00 00 00 lea 0x50(%rip),%rdi # 100000f92 <_main+0x72>
100000f42: 48 89 45 f0 mov %rax,-0x10(%rbp) #给p赋值
100000f46: 48 8b 45 f0 mov -0x10(%rbp),%rax #
100000f4a: 48 89 45 e8 mov %rax,-0x18(%rbp) #给q赋值
100000f4e: 48 8b 75 f0 mov -0x10(%rbp),%rsi
100000f52: 48 8b 55 e8 mov -0x18(%rbp),%rdx
100000f56: b0 00 mov $0x0,%al
100000f58: e8 13 00 00 00 callq 100000f70 <_main+0x50>
100000f5d: 31 c9 xor %ecx,%ecx
100000f5f: 89 45 e4 mov %eax,-0x1c(%rbp)
100000f62: 89 c8 mov %ecx,%eax
100000f64: 48 83 c4 20 add $0x20,%rsp
100000f68: 5d pop %rbp
100000f69: c3 retq
从上面反汇编的代码看到,至少在我的机器上的g++,对于引用的实现,它就是个普通的变量,它也实实在在地占有栈内存。
那么如果让我写编译器来实现上面这段代码中的引用呢,实际上在这个例子中是可以做到不需要为q
分配额外的内存空间的,因为在编译的阶段,完全可以把用到 q
的地方,采用 栈基址+偏移量(bp + offset)的方式引用到变量p的地址,然后再偷偷解引用即可。
搜索了一下,有一篇类似探讨C++引用的文章 《深入分析C++引用》 这篇文章写的不错,结论也跟我猜的类似:
结论
C++标准并没有规定引用到底占不占内存。大部分编译器对于引用的实现,引用就是一个常量指针,是一种会被编译器自动解引用的指针。
把上面的代码转化一下:
A * p = new A;
A& q = *p; => A* const m = p;
注意关键点!在于意识到自动解引用是编译器干的事!
我说C++很多知识点为什么那么难记,因为编译器干了好多事。
显然上面的引用的写法是比用常量指针的写法更加简洁。那么除了写起来简洁,还有啥优点?
1. 声明引用的同时必须对其进行初始化 。// 我想好处就是对于C++程序员来说,就可以不用像Java程序员那样蛋疼,动不动就Null Pointer Exception, 动不动就得写个 if(null != obj)。引用确实是个好东西。
2. 引用声明结束后,不能作为别的变量的别名。 // 我想好处就是限制程序员把指针搞来搞去把自己都给搞晕了。