这一篇解决什么问题
上一篇用几个结构体搞清楚了对象内部的布局。接下来我想弄清楚两件事:
Point a(1);和Point* b = new Point(2);这两种写法,对象的内存到底在哪,生命周期差在哪。- 成员函数里随手就用的
this,它到底是个什么东西,从哪来的。
还是一个最小工程,打印地址看现象。
栈与堆:两种对象,两套生命周期
规则很简单:
- 栈对象(局部变量):内存在栈上,构造发生在声明处,离开作用域自动析构。不用管,也轮不到你管。
- 堆对象(
new出来的):内存在堆上,构造发生在new这一刻,只有delete才析构。忘了delete,对象就一直在那,这就是内存泄漏。
一个容易混淆的点:Point* b 这个指针变量本身是栈对象,它指向的 Point 才是堆对象。&b 和 b 是两个地址,分属两个世界。
this 指针:一个隐藏的函数参数
非静态成员函数在编译时会被改写:show() 实际变成类似 show(const Point* this) 的普通函数,调用处 a.show() 实际传入 &a。
在 MSVC x64 下,这个隐藏参数通过 rcx 寄存器传递(x64 调用约定的第一个参数)。所以 this 不是对象的组成部分,不占对象任何字节——它是每次调用成员函数时,由调用方临时递进来的一个地址。上一篇的布局实验也印证了这点:sizeof 里从来没有 this 的位置。
验证工程
1 | // 栈对象 vs 堆对象, this 指针本质. |
运行结果与解读
先给出我的预测(VS2022 x64 Debug,地址为预测的形态示意):
- 栈对象的 this、&a、&x 三者完全相同(x 偏移为 0),且是
000000xx....FF....形态的栈地址。 - 堆对象的 this、b、&x_ 三者相同,是另一种前缀的堆地址;而 &b(指针本身)仍是栈地址。
- 栈对象的 dtor 在作用域结束处自动出现;堆对象的 dtor 只在 delete 之后出现。
实际运行输出:
1 | --- stack object --- |
逐行比对,三条预测全部命中:
第一,this 和 &a 是同一个地址。 show() 里打印的 this 和外部打印的 &a 完全相同(都是 ...F8E4),堆对象那边 this 也和 b 的值相同。这直接证实了”this 就是调用时传进来的对象地址”。
第二,栈和堆的地址形态肉眼可分。 栈地址是 00000037... 开头,堆地址是 0000017F... 开头,两个区域离得很远。同时注意 &b(指针本身)是 00000037...——指针变量确实住在栈上,和它指向的堆对象天各一方。顺带一提,&b - &a = F908 - F8E4 = 0x24,两者都在 main 的同一个栈帧里,具体距离是编译器的帧布局决定的,不用深究。
第三,析构时机完全不同。 栈对象 a 的 dtor 紧跟在作用域结束的 } 之后自动打印;堆对象的 dtor 只在 delete b 之后出现。把 delete 注释掉再跑,第二个 dtor 就永远消失了。
另外 &x_ 和 this 相同,因为 x_ 是 Point 的第一个(也是唯一一个)成员,偏移为 0——这正是上一篇布局规则的延续:成员地址 = this + 偏移。
在反汇编里看 this 的传递
想看隐藏的参数传递,最直接的办法是看汇编。在 VS 里 F5 调试,在 a.show() 这一行打断点,命中后右键选择「转到反汇编」(快捷键 Ctrl+K,G)。这是本次运行的实际反汇编:
1 | Point a(1); |
这段汇编证实了三件事。
1. lea rcx, [rbp+4] 出现了两次。 构造和 show() 调用前,都是先把 a 的地址(rbp+4,a 在栈帧里的位置)装进 rcx,再 call。this 作为隐藏参数经 rcx 传递,和预测一致。
2. 意外收获:mov edx,1。 构造调用的完整序列是 mov edx,1 + lea rcx,[rbp+4] + call Point::Point——构造函数也是普通成员函数,同样收 this(rcx),显式参数 1 被挤到了第二个寄存器 rdx。这就是 x64 调用约定的参数顺序:rcx、rdx、r8、r9,this 占了第一个位置,其余参数顺次后移。这个细节是预测时没想到的,是看汇编带来的新发现。
3. printf 那三行是同一个约定的对照组。 lea rcx,[格式串] + lea rdx,[rbp+4]——rcx 放第一个参数(格式串),rdx 放第二个(&a)。成员函数调用和普通函数调用没有本质区别,只是编译器替成员函数多塞了一个 this 参数。
小结
1 | 栈对象: 声明即构造, 离开作用域自动析构, 地址在栈区 |