C++中有个this关键字,它是“指向本对象的指针”,并且“不占用对象内存空间”。那么this指针到底是什么东东?是不是一个普通指针?能否更改它的值?放在什么地方?能否取它的地址?
在开始讨论之前,想像一下如果由你设计编译器,你会怎么实现this指针?一种可能的方案是在内存中维护一个“this指针链表”或“this指针池”,把程序中所有对象的地址保存起来。这种方案下this指针就是一个普普通通的指针,当然可以取地址,可以更改值。另一种方案是每次用到this指针时(如调用类函数时——之所以说“类函数”而不是“成员函数”,是为了强调“成员函数属于类而不属于对象,调用时要传入对象的地址”这一事实),直接取对象的地址并保存到一个寄存器中。这种方案下,取this指针的地址当然是无意义的,但是更改值呢?这个方案似乎暗示了可以在类成员函数内部更改传入来的this指针。
C++标准并没有对此作出规定,两个都是可行的。一般来说编译器相关的东西讨论下去已经没有什么意义,除非对运行效率有影响,而这个this指针又是无关痛痒的,所以下文大可不看(我本人都觉得无聊)。
我只用过VC,VC肯定是第二个方案,其他编译器就不得而知了,据说主流编译器都采用第二个方案,估计是基于效率和可行性方面的考虑。下面详细讨论VC的this指针实现,以及这种实现的相关问题。
1、为什么说VC采用的是第二个方案,以及这个方案如何把对象地址传给类成员函数。
设有类
class A
{
public:
void foo(int& sum)
{
for (int i = 0; i < 100; i++)
sum += num[i];
};
private:
int num[100];
};
在main()里:
A a;
int n = 0;
a.foo(n);
这样的话,只要在这里下断点,运行,查看汇编,就可以看到编译器是怎样把 a 的地址传给A::foo()。Debug模式下,是:
a.foo(n);
004113FA lea eax,[n] // 把n的地址赋给eax
00411400 push eax // 把eax推入堆栈
00411401 lea ecx,[a] // 把a的地址赋给ecx
00411407 call A::foo (41100Ah) // 调用foo()
可见,调用类成员函数时,参数通过堆栈传递,对象地址通过寄存器ecx传递。这个规则不会随着编译模式而改变,Release版本(完全优化)如下:
a.foo(n);
0040107D lea ecx,[esp+4]
00401081 push ecx
00401082 lea ecx,[esp+0Ch]
00401086 call A::foo (401000h)
可以看到,仅仅是把符号n、a换成地址,参数的传递协议的确没变。由于讨论Debug版本对这个话题意义不大,下面的讨论都是基于Release完全优化版本。此时可以把ecx看作this指针,取其地址当然毫无意义,但是更改其值呢?试着在a.foo(n); 后面加上__asm{inc eax};看看,即:
a.foo(n);
__asm{inc ecx};
a.foo(n);
反汇编是:
a.foo(n);
0040107D lea ecx,[esp+4]
00401081 push ecx
00401082 lea ecx,[esp+0Ch]
00401086 call A::foo (401000h)
__asm{inc ecx};
0040108B inc ecx // 修改this指针的值
a.foo(n);
0040108C lea edx,[esp+4]
00401090 push edx
00401090 lea ecx,[esp+0Ch] // 编译器重新取对象的地址,修改失效
00401094 call A::foo (401000h)
可见编译器把函数调用编译成一个原子操作,在类成员函数外部无法修改this指针的值,或者说这种修改并没有起到修改的效果,是无效的——修改this指针的值后,下一次调用类成员函数并不会传入错误的对象地址。
【未完待续】