这部分内容仅仅是用来记录关于《Inside the C++ Object Model》 一书中的问题。
1.为什么要引入虚拟继承(转自:http://www.cnblogs.com/BeyondAnyTime/archive/2012/06/05/2537451.html)
虚继承是多重继承里面特有的概念。虚继承为了解决菱形继承的问题,如:D继承B1、B2,B1和B2都继承自A,因此会在D中出现两次A的变量和函数,为了节省空间,可以将B1和B2继承定义为虚继承,这样A就变成了虚基类。代码如下:
class A{};
class B1:public virtual A{};
class B2:public virtual A{};
class D:public B1,public B2{};
虚继承主要是用在c++的多重继承里面的。一旦离开了多重继承,虚拟继承也就没有什么意思了,会降低效率和占用更多空间。
2.引入虚继承和直接继承会有什么区别呢
虚继承和直接继承在时间上和空间上存在以下不同:
时间:
在通过继承类对象访问虚基类对象中的成员(包括数据成员和函数成员)时,都必须通过某种间接引用来完成,这样会增加引用寻址时间(就和虚函数一样),其实就是调整this指针以指向虚基类对象,只不过这个调整是运行时间接完成的。
空间:
由于共享所以不必要在对象内存中保存多份虚基类子对象的拷贝,这样较之多继承节省空间。虚拟继承与普通继承不同的是,虚拟继承可以防止出现diamond继承时,一个派生类中同时出现了两个基类的子对象。也就是说,为了保证这一点,在虚拟继承情况下,基类子对象的布局是不同于普通继承的。因此,它需要多出一个指向基类子对象的指针。
3.笔试,面试中常考的C++虚拟继承的知识点
测试例子:
第一种情况: 第二种情况: 第三种情况 第四种情况:
class a class a class a class a
{ { { {
virtual void func(); virtual void func(); virtual void func(); virtual void func();
}; }; char x; char x;
class b:public virtual a class
b :public a }; };
{ { class b:public virtual a
class b:public a
virtual void foo(); virtual void foo(); { {
}; }; virtual void foo(); virtual void foo();
}; };
如果对这四种情况分别求sizeof(a), sizeof(b)。结果是什么样的呢?下面是输出结果:(在codeblock13,使用g++编译器,win64系统)
四种情况的实测输出结果分别为:
第一种:4 4
第二种:4 4
第三种:8 12
第四种:8 8
想想这是为什么呢?
因为每个存在虚函数的类都要有一个4字节的指针指向自己的虚函数表,所以每种情况的类a所占的字节数应该是没有什么问题的,那么类b的字节数怎么算呢?看“第一种”和“第三种”情况采用的是虚继承,那么这时候就要有这样的一个指针vptr_b_a,这个指针叫虚类指针,也是四个字节;还要包括类a的字节数,所以类b的字节数就求出来了。而“第二种”和“第四种”情况则不包括vptr_b_a这个指针,这回应该木有问题了吧。
上述输出结果与事实不符合,第一种和第二种其实是一样的,差别就是第三、四种里面有变量了,可能是由于编译器的优化原因。。这个后面再确认。。之所以用char也是8个字节,是由于存在对齐问题。4 c++重载、覆盖、隐藏的区别和执行方式
既然说到了继承的问题,那么不妨讨论一下经常提到的重载,覆盖和隐藏
4.1成员函数被重载的特征
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。
4.2“覆盖”是指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。
4.3“隐藏”是指派生类的函数屏蔽了与其同名的基类函数,特征是:
(1)如果派生类的函数与基类的函数同名,但是参数不同,此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,但是参数相同,但是基类函数没有virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
小结:说白了就是如果派生类和基类的函数名和参数都相同,属于覆盖,这是可以理解的吧,完全一样当然要覆盖了;如果只是函数名相同,参数并不相同,则属于隐藏。
4.4 三种情况怎么执行:
4.4.1 重载:看参数。
4.4.2 隐藏:用什么就调用什么。(通过指针强制性转换,还可以进行调用的)
4.4.3 覆盖:调用派生类。(也就是说无论如何都没有办法再去调用基类的函数了)
测试实例:#include <iostream>
using namespace std;
class A
{
public:
void func()
{
cout <<"a func" << endl;
}
virtual void foo()
{
cout << "a foo "<<endl;
}
void f(char *c)
{
cout << "a c " << c <<endl;
}
};
class B:public A
{
public:
virtual void func()
{
cout << "b func" <<endl;
}
void foo()
{
cout << "b foo "<<endl;
}
void f(int c)
{
cout << "b int c " << c <<endl;
}
void f(double c)
{
cout << "b double c" << c << endl;
}
};
int main ()
{
B b;
A *a;
b.f('m');
b.f(12.3);
b.foo();
b.func();
// b.f("asdsa"); // error
a = &b;
a->f("as");
//a->f(12.4); // error
a->func();
a->foo();
}
可以看出来,函数f(char*c) 被影藏了,但是可以通过A*指针调用;同样的,函数func也是被隐藏了;函数foo则被覆盖了,不论通过什么方法调用,都是B的版本函数;B中的函数f存在相互的重载。