1.多重继承
一个例子
123456789101112131415161718192021222324252627282930313233 | #include <iostream> using namespace std; class Base1{ public : virtual void foo1() {}; }; class Base2{ public : virtual void foo2() {}; }; class MI : public Base1, public Base2{ public : virtual void foo1 () {cout << "MI::foo1" << endl;} virtual void foo2 () {cout << "MI::foo2" << endl;} }; int main(){ MI oMI; Base1* pB1 = &oMI; pB1->foo1(); Base2* pB2 = (Base2*)(pB1); // 指针强行转换,没有偏移 pB2->foo2(); pB2 = dynamic_cast <Base2*>(pB1); // 指针动态转换,dynamic_cast帮你偏移 pB2->foo2(); return 0; } |
你会认为屏幕上会输出什么?是下面的结果吗?
MI::foo1 MI::foo2 MI::foo2 |
这样认为没有什么不对的,因为C++的多态性保证用父类指针可以正确的找到子类实现,并调用。所以会有上面的输出。
但是,现实却不是这样,下面是真实的输出:
为什么会出现上面的情况呢,上面代码中的注释部分也许解释了,这里再来详细的来分析一下。
首先,C++使用一种称之为vtable(google “vtable” for more details)的东西实现virtual函数多态调用。vtable每个类中都有一个,该类的所有对象公用,由编译器帮你生成,只要有virtual函数的类,均会有vtable。在继承过程中,由于类Base1和类Base2都有vtable,所以类MI继承了两个vtable。简单的分析一下对象oMI内存结构,如下:
0 vtable_address_for_Base1 –> [MI::foo1, NULL] 4 vtable_address_for_Base2 –> [MI::foo2, NULL] |
其实很简单,就两个vtable的指针,0和4代表相对地址,指针地址大小为4。
pB1的值为0(pB1 == 0),所以调用“pB1->foo1()”时,可以正确的找到MI::fool这个函数执行。
但是当使用强行转换,将pB1转给pB2,那么实质上pB2的值也是0(pB2 == 0),当调用“pB2->foo2()”时,无法在第一个vtalbe中找到对应的函数,但是却不报错,而是选择执行函数MI::foo1,不知道为什么会有这种行为,但是这种行为却十分恶心,导致结果无法预期的(最后调用的函数会与函数申明的循序有关),不太会引起注意,使得bug十分隐晦。
可以设想,当一个有复杂的业务逻辑的程序,而类似这种函数调用和指针强行转换分布在不同的函数或模块中,可想而知,bug定位十分困难。
当使用动态转换时,也就是“pB2 = dynamic_cast<Base2*>(pB1)”,dynamic_cast函数会根据尖括号中的类型进行指针偏移,所以pB2的值为4(pB2 == 4),这样调用“pB2->foo2()”就会按照期望的方式执行。
结论
上面的现象在单继承中是不会出现的,因为只有一个vtable(子类的virtual函数会自动追加到第一个父类的vtable的结尾)。所以不会出现上面的现象,而多重继承却出现了上面的想象,所以需要注意以下两点:
1. 多重继承需要慎用
2. 类型转换尽量采用c++内置的类型转换函数,而不要强行转换
总结:虚继承,虚函数都会产生相关vptrvptr 带有到基类地址的偏移量通过 dynamic_cast 可以实现多继承不同父类之间的转换。
2.虚基类
教科书上面对C++虚基类的描述玄而又玄,名曰“共享继承”,名曰“各派生类的对象共享基类的的一个拷贝”,其实说白了就是解决多重多级继承造成的二义性问题。例如有基类B,从B派生出C和D,然后类F又同时继承了C和D,现在类F的一个对象里面包含了两个基类B的对象,如果F访问自己的从基类B那里继承过来的的数据成员或者函数成员那么编译器就不知道你指的到底是从C那里继承过来的B对象呢还是从D那里继承过来的B对象。
于是虚基类诞生了,将C和D的继承方式改为虚继承,那么F访问自己从B那里继承过来的成员就不会有二义性问题了,也就是将F对象里的B对象统一为一个,只有一个基类B对象,下面是一段代码说明了对虚基类的使用。
#include <iostream>using namespace std;class A{ public: int i; void showa(){cout<<"i="<<i<<endl;}};class B:virtual public A //此处采用虚继承{ public: int j;};class C:virtual public A //此处采用虚继承{ public: int k;};class D:public B,public C{ public: int m;};int main(){ A a; B b; C c; a.i=1; a.showa(); b.i=2; b.showa(); c.i=3; c.showa(); D d; d.i=4; d.showa(); //cout << "Hello world!" << endl; return 0;}从这个代码我们可以看出B,C,D从A那里继承过来了i这个变量并且它们之间不会有任何影响,如果B和C不是虚继承方式的,那么d.i=4;就不能编译通过了。
3.虚函数和纯虚函数
1. 虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称 $
2. 虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用 $
只有声明而没有定义。
3. 虚函数和纯虚函数都可以在子类(sub class)中被重载,以多态的形式被调用。
4. 虚函数和纯虚函数通常存在于抽象基类(abstract base class -ABC)之中,被继承的子类重载,目的是提供一个统一的接口。
5. 虚函数的定义形式:virtual {method body}
纯虚函数的定义形式:virtual { } = 0;
在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函数却是动态绑定(run-ti$
6. 如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类(ABC)是不能被直接调用的。必须被子类 $
以下为一个简单的虚函数和纯虚寒数的使用演示!
|
新闻热点
疑难解答
图片精选