我们知道,在C++的非静态成员函数中,有一个隐含的参数,即this指针,利用它,我们可以访问相应对象的数据成员,那么究竟this指针是如何作用的呢?下面先来看一个例子。有下面的一个简单的类:
- class CNullPointCall
- {
- public:
- static void Test1();
- void Test2();
- void Test3(int iTest);
- void Test4();
- private:
- static int m_iStatic;
- int m_iTest;
- };
- int CNullPointCall::m_iStatic = 0;
- void CNullPointCall::Test1()
- {
- cout << m_iStatic << endl;
- }
- void CNullPointCall::Test2()
- {
- cout << "Very Cool!" << endl;
- }
- void CNullPointCall::Test3(int iTest)
- {
- cout << iTest << endl;
- }
- void CNullPointCall::Test4()
- {
- cout << m_iTest << endl;
- }
那么下面的代码都正确吗?都会输出什么?
- CNullPointCall *pNull = NULL; // 没错,就是给指针赋值为空
- pNull->Test1(); // call 1
- pNull->Test2(); // call 2
- pNull->Test3(13); // call 3
- pNull->Test4(); // call 4
你肯定会很奇怪我为什么这么问。一个值为NULL的指针怎么可以用来调用类的成员函数呢?!可是实事却很让人吃惊:除了call 4那行代码以外,其余3个类成员函数的调用都是成功的,都能正确的输出结果,而且包含这3行代码的程序能非常好的运行。经过细心的比较就可以发现,call 4那行代码跟其他3行代码的本质区别:类CNullPointCall的成员函数中用到了this指针。
在编绎阶段,当遇到通过指针调用成员函数时,编绎器会首先检查该成员函数是否为虚函数,如果不是(是虚函数的情况待会儿再讲),编绎器会再此插入一些实现代码,它会在函数调用之前,首先将对象的首地址放入到ECX寄存器中,然后才是函数调用的语句(注意,不同的编绎器在this指针的实现上可能会有所区别,但是C++编译器必须遵守C++标准,因此对于this指针的实现应该都是差不多的)。在我们这个程序中对象的首地址就是NULL,由于前三个函数并没有使用到时this指针,所以自然不会出问题,而对于test4,由于要访问m_iTest数据成员,程序将从ECX寄存器中取出对象的首地址,也就是NULL,然后再通过NULL访问m_iTest数据成员,这才是出问题的原因。
了解了这一点后,我们再来看调用函数为虚函数的情况。要了解这一情况发生时的机理,我们必需首先了解虚表的概念。C++中的虚函数的作用主要是实现了多态的机制,而这一机制的实现靠的就是虚表,简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。对于含有虚函数的类,它的每个对象分配的内存空间的最前端就保存有指向该类的虚表的地址。如下图所示:
为了更好的理解它,我们来看一个比较奇怪的例子。
假设我们有这样的一个类:
- class A {
- public:
- A(const int& p_d): d(p_d) {
- }
- virtual void fun1() {
- cout<< "fun1"<< endl;
- }
- virtual void fun2() {
- cout<< "fun2"<< endl;
- }
- virtual void fun3() {
- cout<< d<< endl;
- }
- private:
- int d;
- };
按照上面的说法,我们可以通过A的实例来得到虚函数表。 下面是实际例程:
- typedef void (*FUNC)();
- int main(int argc, char** argv) {
- A a(2);
- FUNC* fs = *(FUNC**)&a;
- cout << fs<< endl;
- fs[0]();
- fs[1]();
- return 0;
- }
在这个实例里面,fs实际上就指向了虚表的首地址,我们可以把它想象成一个数组,这样就可以对虚函数进行调用了,而且这一方法还可以绕过它对访问修饰的限制,即使是私有虚函数,我们也可以照调用不误。
关于虚表就说到这里,下面还是回到对通过指针调用的函数是虚函数的讨论中来。这在种情况下,编绎器插入的代码完成的功能大概如下:还是首先将对象的首地址放到ECX寄存器中,然后插入函数调用的代码,只不过这里不再是一个简单的函数首地址完事,它会插入一些代码通过前面提到的虚表机制找寻真正要调用的函数的入口地址。
在上一个例子中,如果我们调用fs[2]()函数会怎么样呢?因为我们是直接通过虚表调用的成员函数,所以它没有事先将对象的首地址放到ECX寄存器中,而该成员函数是会通过EXC寄存器中存储的原先的值去访问数据成员d的,那么它输出的就将是一个没有意义的随机值罢了。
通过以上对this指针以及虚表的讨论,回想C++中关于成员函数指针的使用,我们就可以理解它到底和普通的函数指针有何不同了,一切都是this指针的缘故,通过成员指针调用函数会事先将对象的首地址放到ECX寄存器中,这也就是它们的根本区别所在。