http://www.cnblogs.com/fte99923/archive/2011/04/24/2026517.html
晚自习和秦多铎讨论了C++里多态的问题,发现自己在这方面存在两个严重问题:
1.以前只是知其然,但不知所以然,只知道怎么实现多态,但不明白编译器内部大概是怎么实现的,也就导致了编程时很多细节处需要翻书。
2.C#后遗症很严重,用VC6写小程序验证自己想法的时候好几次编译不通过。
多铎在网上找了一篇技术贴(http://user.qzone.qq.com/624576542/blog/1303304079)
帖子内容非常赞,但是重点不够突出(什么是多态、怎么用多态这样的基础问题这帖子也不厌其烦地讲),也有说得不太明白、模棱两可的地方。
按照里边的知识点,编了几个小程序测试了下,梳理了一下思路,总结如下:
(零)VTABLE机制
当一个类中有virtual函数时,编译器会为这个类建立且仅建立一个VTABLE
这个VTABLE大概是个数组的感觉,其元素是“函数指针”。
所以说,下边这段代码:
1 class A 2 { 3 public : 4 virtual void f0() {cout << " a0 " << endl}; 5 6 virtual void f1() {cout << " a1 " << endl}; 7 8 virtual void f2() {cout << " a2 " << endl}; 9 10 }; 11 12 class B: public A 13 { 14 public : 15 virtual void f0() {cout << " b0 " << endl}; 16 17 virtual void f1() {cout << " b1 " << endl}; 18 19 virtual void f2() {cout << " b2 " << endl}; 20 21 };
编译器会给类A和类B各自建一个VTABLE:
VTABLE_A的0,1,2个元素分别放的是指向A::f0() , A::f1() , A::f2()的指针
VTABLE_B的0,1,2个元素分别放的是指向B::f0() , B::f1() , B::f2()的指针
而对每个实际的类对象,编译器会增加一个vptr字段
(注:这也是很多笔经面经里说的sizeof(A)和sizeof(B)里多出4的问题的原因,多出来这4个Byte就是给vptr指针分配的空间,由此引发的sizeof对齐问题经常被问到)
而下边这段代码:
A * a = new B; a -> f1(); // 这里,编译器实际上做的是a->vptr[1]();这样,实际上运行的是VTABLE_B[1]指向的函数
这样看来,以前很多死记硬背的细节,哪里是override,哪里是overload,调用的到底是哪个函数,用vptr和VTABLE的概念就很好理解了
(一)多层次类中的virtrual
看下边的代码:
class A { public : virtual void f(){cout << " a " << endl;} void g(){cout << " ag " << endl;} }; class B: public A { public : void f(){cout << " b " << endl;} // B::f()没有virtual关键字! }; class C: public B // C继承的是B,而B中没有显式的virtual { public : void f(){cout << " c " << endl;} // C中的f也没有virtual关键字! void g(){cout << " cg " << endl;} }; void main() { A * a = new C(); a -> f(); // 这里调用了C::f(),实际上是a->vptr[0](); a -> g(); // 这里调用的是A::g(),因为g在A中不是virtual的 delete a; a = new B(); a -> f(); // (这行语句加得有点蛋疼,只是为了测试得更完整)这里调用了B::f(),a->vptr[0](); delete a; }
A::f()是virtual的,但B::f()和C::f()都没有显式声明virtual
而实际上,C::f()是多态了A::f()的。
说穿了很简单,编译器给A建立了VTABLE,也就会给A的每个派生类都建立VTABLE(哪怕是孙子辈的C)
而A,B,C类中VTABLE中元素的顺序都是一样的(这个例子只有1个f(),如果A还有virtual的f2,f3,那么B和C的VTABLE中的相应位置中也会存放B,C版本的f2,f3)
(二)virtual析构函数
基本上,C++的多态,都要“虚”一下析构函数(这1年来C#用得多,人都变傻了,前几天电面的时候,面试官和我聊多态,“虚析构”这地方还被鄙视了下=。=)
1 class A 2 { 3 public : 4 A() { cout << " A() " << endl;ptra_ = new char [ 10 ];} 5 virtual ~ A() { cout << " ~A() " << endl; delete[] ptra_;} // 注意:这里如果不“虚”一下,main函数里调用的顺序是A(),B(),~A() 6 7 // “虚”了之后,main里调用顺序是A(),B(),~B(),~A() 8 9 private : 10 char * ptra_; 11 }; 12 13 class B: public A 14 { 15 public : 16 B() { cout << " B() " << endl;ptrb_ = new char [ 20 ];} 17 ~ B() { cout << " ~B() " << endl; delete[] ptrb_;} 18 private : 19 char * ptrb_; 20 }; 21 22 void main() 23 { 24 A * a = new B; 25 delete a; 26 }
由此可见,C++里用多态,除非你的程序很土,从来没new过堆空间,否则“虚析构”是必用的。
另:构造函数不能虚!
(三)private虚函数
class A { public : void foo() { bar();} // 实际上这里是调用了this->vptr[0],具体调用了谁,要看“this”指向的谁 private : virtual void bar() {cout << " a " << endl;} }; class B: public A { private : virtual void bar() { cout << " b " << endl;} }; void main() { A * a = new B(); a -> foo(); // a指向的是B,所以a->vptr[0]调用的是VTABLE_B[0]指向的函数,即B::bar() }
(四)构造函数和析构函数中调用virtual函数
class A { public : A() { cout << " A() " << endl;foo();} // 在这里,无论如何都是A::foo()被调用! ~ A() { cout << " ~A() " << endl;foo();} // 同上 virtual void foo(){cout << " a " << endl;} }; class B: public A { public : virtual void foo(){cout << " b " << endl;} }; class C: public B { public : virtual void foo(){cout << " c " << endl;} }; void main() { A * a = new B; delete a; B * b = new C; delete b; cout << sizeof (A) << endl; cout << sizeof (B) << endl; // 2个构造、2个析构都调用的a::foo() // 无责任猜想,应该是构造函数被调用时,B的vptr还没做成;而析构函数被调用时,vptr已被撤销,所以不能通过B::vptr饮用B的VTABLE }
(五)C#后遗症两条
1.多态的实现是用指针,而不是对象
Base base = new Derived; // 这是多态不了的,因为实际上是进行了Derived到Base的强制类型转换,转完之后实际上还是Base类的VTABLE Base * base = new Derived; // 这里base指针指向了Derived对象,base->f()调用的是“被指向的Derived对象”的vptr字段,即Derived类的VTABLE中的元素
2.C#的base关键字和Java的super关键字,C++里木有
想实现类似的功能,直接用父类的类名和“::”运算符,即:
Class B : public A { virtual void f() { A::f(); cout << " f() in B " << endl; }
这样。