虚继承
1 class parent 2 { 3 public: 4 virtual void foo(){cout < <"foo from parent";}; 5 void foo1(){cout < <"foo1 from parent";}; 6 }; 7 8 class son:public parent 9 { 10 void foo(){cout < <"foo from son";}; 11 void foo1(){cout < <"foo1 from son";}; 12 }; 13 14 int main() 15 { 16 parent *p=new son(); 17 p->foo(); 18 p->foo1(); 19 return 0; 20 }
其输出结果是: foo from son,foo1 from parent
注意fool1函数还是调用的基类的。
1、真正意义上的虚函数调用,是运行时绑定的;
1.为什么要引入虚拟继承
虚拟继承是多重继承中特有的概念。虚拟基类是为解决多重继承而出现的。如:类D继承自类B1、B2,而类B1、B2都继 承自类A,因此在类D中两次出现类A中的变量和函数。为了节省内存空间,可以将B1、B2对A的继承定义为虚拟继承,而A就成了虚拟基类。实现的代码如 下:
class A
class B1:public virtual A;
class B2:public virtual A;
class D:public B1,public B2;
虚拟继承在一般的应用中很少用到,所以也往往被忽视,这也主要是因为在C++中,多重继承是不推荐的,也并不常用,而一旦离开了多重继承,虚拟继承就完全失去了存在的必要因为这样只会降低效率和占用更多的空间。
2.引入虚继承和直接继承会有什么区别呢
由于有了间接性和共享性两个特征,所以决定了虚继承体系下的对象在访问时必然会在时间和空间上与一般情况有较大不同。
2.1时间:在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。
2.2空间:由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之 多继承节省空间。虚拟继承与普通继承不同的是,虚拟继承可以防止出现diamond继承时,一个派生类中同时出现了两个基类的子对象。也就是说,为了保证 这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针。
一个例子
1 class CA 2 { 3 int k; //如果基类没有数据成员,则在这里多重继承编译不会出现二义性 4 public: 5 void f() {cout << "CA::f" << endl;} 6 }; 7 class CB : public CA 8 { 9 }; 10 class CC : public CA 11 { 12 }; 13 class CD : public CB, public CC 14 { 15 }; 16 void main() 17 { 18 CD d; 19 d.f(); 20 }
1 class CA 2 { 3 int k; 4 public: 5 void f() {cout << "CA::f" << endl;} 6 }; 7 class CB : virtual public CA //也有一种写法是class CB : public virtual CA 8 { //实际上这两种方法都可以 9 }; 10 class CC : virtual public CA 11 { 12 }; 13 class CD : public CB, public CC 14 { 15 }; 16 void main() 17 { 18 CD d; 19 d.f(); 20 }
虚继承:子类has虚基类,所以带来的问题是虚基类必须先于子类构造,虚基类要有独立性,如拥有虚函数表指针。
一般继承:子类is基类,属于关系,虚函数表自然可以共享。
首先,说说GCC的编译器.
它实现比较简单,不管是否虚继承,GCC都是将虚表指针在整个继承关系中共享的,不共享的是指向虚基类的指针。
1 class A { 2 int a; 3 virtual ~A(){} 4 }; 5 class B:virtual public A{ 6 virtual void myfunB(){} 7 }; 8 class C:virtual public A{ 9 virtual void myfunC(){} 10 }; 11 class D:public B,public C{ 12 virtual void myfunD(){} 13 };
以上代码中sizeof(A)=8,sizeof(B)=12,sizeof(C)=12,sizeof(D)=16.
解释:A中int+虚表指针。B,C中由于是虚继承因此大小为A+指向虚基类的指针,B,C虽然加入了自己的虚函数,但是虚表指针是和基类共享的,因此不会有自己的虚表指针。D由于B,C都是虚继承,因此D只包含一个A的副本,于是D大小就等于A+B中的指向虚基类的指针+C中的指向虚基类的指针。
如果B,C不是虚继承,而是普通继承的话,那么A,B,C的大小都是8(没有指向虚基类的指针了),而D由于不是虚继承,因此包含两个A副本,大小为16.注意此时虽然D的大小和虚继承一样,但是内存布局却不同。
然后,来看看VC的编译器
vc对虚表指针的处理比GCC复杂,它根据是否为虚继承来判断是否在继承关系中共享虚表指针,而对指向虚基类的指针和GCC一样是不共享,当然也不可能共享。
代码同上。
运行结果将会是sizeof(A)=8,sizeof(B)=16,sizeof(C)=16,sizeof(D)=24.
解释:A中依然是int+虚表指针。B,C中由于是虚继承因此虚表指针不共享,由于B,C加入了自己的虚函数,所以B,C分别自己维护一个虚表指针,它指向自己的虚函数。(注意:只有子类有新的虚函数时,编译器才会在子类中添加虚表指针)因此B,C大小为A+自己的虚表指针+指向虚基类的指针。D由于B,C都是虚继承,因此D只包含一个A的副本,同时D是从B,C普通继承的,而不是虚继承的,因此没有自己的虚表指针。于是D大小就等于A+B的虚表指针+C的虚表指针+B中的指向虚基类的指针+C中的指向虚基类的指针。
同样,如果去掉虚继承,结果将和GCC结果一样,A,B,C都是8,D为16,原因就是VC的编译器对于非虚继承,父类和子类是共享虚表指针的。
利用visual studio 命令提示(2008),到xx.cpp 文件目录下 运行cl /d1 reportSingleClassLayoutB xx.cpp
第一个vfptr 指向B的虚表,第二个vbptr指向A,第三个指向A的虚表,因为是虚拟继承,所以子类中有一个指向父类的虚基类指针,
防止菱形继承中数据重复,这样在菱形继承中,不会出现祖先数据重复,而只指向祖先数据的指针。
虚函数继承
带 有虚函数的普通继承,前面一个是父类,后面一个是子类,多态实现只需要更新虚函数表即可.,虚函数指针没有变,变得只是覆盖的虚函数表中的函数,你可以把 虚函数指针想象成普通的成员变量,编译的时候自动插入类中,用的是同一个虚函数指针,只不过子类对虚函数表中对应的虚函数进行了更新。
3.执行顺序
1 class Base 2 { 3 public: 4 Base(){cout<<"Base called : " << gFlag++ << endl;} 5 void print(){cout<<"Base print" <<endl;} 6 }; 7 8 class Mid1 : public Base 9 { 10 public: 11 Mid1(){cout<<"Mid1 called" << endl;} 12 }; 13 14 class Mid2 : public Base 15 { 16 public: 17 Mid2(){cout<<"Mid2 called" << endl;} 18 }; 19 20 class Child:public Mid1, public Mid2 21 { 22 public: 23 Child(){cout<<"Child called" << endl;} 24 }; 25 26 int main(int argc, char* argv[]) 27 { 28 Child d; 29 30 //不能这样使用,会产生二意性 31 //d.print(); 32 33 //只能这样使用 34 d.Mid1::print(); 35 d.Mid2::print(); 36 37 system("pause"); 38 return 0; 39 }
//output
Base called : 0
Mid1 called
Base called : 1
Mid2 called
Child called
Base print
◇虚拟继承
在派生类继承基类时,加上一个virtual关键词则为虚拟继承
1 1 class Base 2 2 { 3 3 public: 4 4 Base(){cout<<"Base called : " << gFlag++ << endl;} 5 5 void print(){cout<<"Base print" <<endl;} 6 6 }; 7 7 8 8 class Mid1 : virtual public Base 9 9 { 10 10 public: 11 11 Mid1(){cout<<"Mid1 called" << endl;} 12 12 }; 13 13 14 14 class Mid2 : virtual public Base 15 15 { 16 16 public: 17 17 Mid2(){cout<<"Mid2 called" << endl;} 18 18 }; 19 19 20 20 class Child:public Mid1, public Mid2 21 21 { 22 22 public: 23 23 Child(){cout<<"Child called" << endl;} 24 24 }; 25 25 26 26 int main(int argc, char* argv[]) 27 27 { 28 28 Child d; 29 29 30 30 31 31 d.print(); 32 32 33 33 34 34 d.Mid1::print(); 35 35 d.Mid2::print(); 36 36 37 37 system("pause"); 38 38 return 0; 39 39 }
//output
1: Base called : 0
Mid1 called
3: Mid2 called
5: Base print
7: Base print
通过输出的比较
2.声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。
虚拟继承与虚函数有一定相似的地方,但他们之间是绝对没有任何联系的。
4.c++重载、覆盖、隐藏的区别和执行方式
既然说到了继承的问题,那么不妨讨论一下经常提到的重载,覆盖和隐藏
4.1成员函数被重载的特征
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
4.2“覆盖”是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
4.3“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,特征是:
(1)如果派生类的函数与基类的函数同名,但是参数不同,此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,但是参数相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
小结:说白了就是如果派生类和基类的函数名和参数都相同,属于覆盖,这是可以理解的吧,完全一样当然要覆盖了;如果只是函数名相同,参数并不相同,则属于隐藏。