多态在C++中是一个重要的概念,通过虚函数机制实现了在程序运行时根据调用对象来判断具体调用哪一个函数。
具体来说就是:父类类别的指针(或者引用)指向其子类的实例,然后通过父类的指针(或者引用)调用实际子类的成员函数。在每个包含有虚函数的类的对象的最前面(是指这个对象对象内存布局的最前面)都有一个称之为虚函数指针(vptr)的东西指向虚函数表(vtbl),这个虚函数表(这里仅讨论最简单的单一继承的情况,若果是多重继承,可能存在多个虚函数表)里面存放了这个类里面所有虚函数的指针,当我们要调用里面的函数时通过查找这个虚函数表来找到对应的虚函数,这就是虚函数的实现原理。注意一点,如果基类已经插入了vptr, 则派生类将继承和重用该vptr。vptr(一般在对象内存模型的顶部)必须随着对象类型的变化而不断地改变它的指向,以保证其值和当前对象的实际类型是一致的。
以上这些概念都是C++程序员很熟悉的,下面通过一些具体的例子来强化一下对这些概念的理解。
1.
#include<iostream> using namespace std; class IRectangle { public: virtual ~IRectangle() {} virtual void Draw() = 0; }; class Rectangle: public IRectangle { public: virtual ~Rectangle() {} virtual void Draw(int scale) { cout << "Rectangle::Draw(int)" << endl; } virtual void Draw() { cout << "Rectangle::Draw()" << endl; } }; int main(void) { IRectangle *pI = new Rectangle; pI->Draw(); pI->Draw(200); delete pI; return 0; }
该段代码编译失败:
C:\Users\zhuyp\Desktop>g++ -Wall test.cpp -o test -g
test.cpp: In function 'int main()':
test.cpp:29:17: error: no matching function for call to 'IRectangle::Draw(int)'
pI->Draw(200);
^
test.cpp:29:17: note: candidate is:
test.cpp:8:18: note: virtual void IRectangle::Draw()
virtual void Draw() = 0;
^
test.cpp:8:18: note: candidate expects 0 arguments, 1 provided
C:\Users\zhuyp\Desktop>
以上信息表明,在父类IRectangle中并没有Draw(int)这个函数。确实,在父类IRectangle中没有这样签名的函数,但是不是多态吗,new 的不是子类Rectangle吗?我们注意到指针 pI 虽然指向子类,但是本身确是父类 IRectangle 类型,因此在执行 pI->draw(200)的时候查找父类vtable,父类的vtable 中没有Draw(int)类型的函数,因此编译错误。
如果将 pI->draw(200) 这一句修改,将pI进行一个down cast 则编译正常,dynamic_cast<Rectangle *>(pI)->draw(200); 此时调用的是子类的指针,查找的是子类的vtable,该vtable中有签名为 draw(int) 的函数,因此不会有问题。
2.
#include <iostream> using namespace std; class Base { public: ~Base() { cout << "~Base()" << endl; } void fun() { cout << "Base::fun()" << endl; } }; class Derived : public Base { public: ~Derived() { cout << "~Derived()" << endl; } virtual void fun() { cout << "Derived::fun()" << endl; } }; int main() { Derived *dp = new Derived; Base *p = dp; p->fun(); cout << sizeof(Base) << endl; cout << sizeof(Derived) << endl; cout << (void *)dp << endl; cout << (void *)p << endl; delete p; p = NULL; return 0; }
编译并运行程序:
C:\Users\zhuyp\Desktop>test.exe
Base::fun()
1
8
0x3856a0
0x3856a0
~Base()
编译器使用的是gcc4.8.1 可以看出 p 和 pb 的值是相同的,因此可以得出结论,现代C++编译器已经没有为了性能的问题将vptr指针放在类内存模型的最前面了。
3.
#include<iostream> using namespace std; class B { int b; public: virtual ~B() { cout << "B::~B()" << endl; } }; class D: public B { int i; int j; public: virtual ~D() { cout << "D::~D()" << endl; } }; int main(void) { cout << "sizeB:" << sizeof(B) << " sizeD:" << sizeof(D) << endl; char *ch = NULL; B *pb = new D[2]; cout<<"size *pb "<<sizeof(pb)<<"\tend"<<endl; delete [] pb; return 0; }
程序运行出错,在输出 pb 的大小之后。可见是在delete [] pb 的时候出了问题。
我们知道释放申请的数组空间的时候需要使用 delete [] ,那 delete 怎么知道要释放多大的内存呢?delete[] 的实现包含指针的算术运算,并且需要依次调用每个指针指向的元素的析构函数,然后释放整个数组元素的内存。
由于C++中多态的存在,父类指针可能指向的是子类的内存空间。由于上面的例子中delete [] 释放的是多态数组的空间,delete[] 计算空间按照 B 类的大小来计算,每次偏移调用析构函数是按照B类来进行的,而该数组实际上存放的是D类的指针释放的大小不对(由于 sizeof(B) != sizeof(D) ,),因此会崩溃。
C:\Users\zhuyp\Desktop>test.exe
sizeB:16 sizeD:24
size *pb 8 end
注意:本代码在64bit环境中执行的,因此 *pb 是 8.