C++继承详解三 ----菱形继承、虚继承
转载:http://blog.csdn.net/pg_dog/article/details/70175488
今天呢,我们来讲讲菱形继承与虚继承。这两者的讲解是分不开的,要想深入了解菱形继承,你是绕不开虚继承这一点的。它俩有着什么关系呢?值得我们来剖析。
菱形继承也叫钻石继承,它是多继承的一种特殊实例吧,它的基本架构如下图:
在我们的设想中,D所对应的对象模型应该如下图所示:
下面我们来用一段代码验证一下:
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
char a;
};
class B :public A
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
char b;
};
class C :public A
{
public:
C()
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
int c;
};
class D :public B, public C
{
public:
D()
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
int d;
};
int main()
{
cout << sizeof(A)<< endl; //1
cout << sizeof(B)<< endl; //2
cout << sizeof(C)<< endl; //8
cout << sizeof(D)<< endl; //16
system("pause");
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
上面显示的大小似乎证实了我们的猜想,但实际上对象模型不是这样的,如下图所示
但是你会发现,这里面存在一个问题,对象D中有两个‘a’,存在数据冗余的问题,如果对象B,C中有两个同名的函数或同名成员变量(本例中的变量‘a’),那么对象D在调用该函数或该成员变量时,该选择调用哪个呢?这也就可以看出还存有二义性问题。那么该如何处理呢?
解决二义性问题很简单,你在调用函数时加上作用域运算符(::),但是数据冗余问题还是没有解决。那么编译器是如何处理这两个问题的呢?
为了解决二义性问题和数据冗余问题,C++引入了虚继承这一概念。下面重点来看虚继承。
虚继承
虚继承又称共享继承,是面向对象编程的一种技术,是指一个指定的基类,在继承体系结构中,将其成员数据实例共享给也从这个基类直接或间接派生的其他类。虚拟继承是多重继承中特有的概念,虚拟继承就是为了解决多重继承而出现的。
这里我想引入《C++ Primer》这本书中对虚继承的有关描述。
在C++语言中我们通过虚继承的机制来解决共享问题。虚继承的目的是令某个类作出声明,承诺共享它的基类。其中,共享的基类子对象称其为虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只含有唯一一个共享的虚基类子对象。
这里还有一个概念,虚基类。虚基类是通过virtual继承而来的派生类的基类。例如:B虚继承了A,所以A是B的虚基类,而不是说A是虚基类。
看下图了解普通基类与虚基类的区别:
按照上面的说法,在对象D中应该只含有一个共享的虚基类子对象,也就是例子中的_a。确实,这样就解决了数据冗余与二义性问题。我们来验证上面的的说法。(为了计算简单,我将上例中每个类成员变量变为整形int)
下面我们来看一段代码:
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
void print()
{
printf("A");
}
int _a;
};
class B :virtual public A //B虚继承A
{
public:
B()
{
cout << "B()" << endl;
}
~B()
{
cout << "~B()" << endl;
}
int _b;
};
class C :virtual public A //C虚继承A
{
public:
C()
{
cout << "C()" << endl;
}
~C()
{
cout << "~C()" << endl;
}
int _c;
};
class D :public B, public C
{
public:
D()
{
cout << "D()" << endl;
}
~D()
{
cout << "~D()" << endl;
}
int _d;
};
int main()
{
cout << sizeof(A)<< endl;
cout << sizeof(B)<< endl;
cout << sizeof(C)<< endl;
cout << sizeof(D)<< endl;
B bb;
C cc;
D dd;
dd.B::_a = 1;
dd.C::_a = 2;
dd._b = 3;
dd._c = 4;
dd._d = 5;
system("pause");
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
B和C都是虚拟继承,
按照我们之前的推理,对象D的结构应该如图所示:
我们来通过vs2013调试中的内存窗口来验证一下:
看到这个结果是不是吓坏宝宝了?和我们预测的完全不一样,对象A和B中的_a跑到了最底部,这种结构明显没有了数据冗余和二义性问题了,这是怎么实现的呢?这就要引入一新的概念——虚基类表。
虚基类表:又称虚基表,编译器会给虚继承而来的派生类生成一个指针vbptr指向一个虚基表,而虚基表中存放的是偏移量。
我们来看对象D中的对象B,它的第一部分(第一行)就是虚基类表指针vbptr,它存的是虚基表的地址,虚基表中存的是共享基类成员变量_a的相对此位置的偏移量,我们来看看,“01259b60”是个地址,利用内存窗口我们可以发现里面存着两部分第一行“00 00 00 00”和第二行“00 00 00 14”,虚基表中分两部分:第一部分存储的是对象相对于存放vptr指针的偏移量(在这就是“00 00 00 00”,偏移量为0),第二部分存储的是对象中基类对象部分相对于存放vbptr指针的地址的偏移量(在这就是“00 00
00 14”),
即20(十六进制下14就是十进制的20),也就是说偏移量是20个字节,你可以用他们的地址相减验证一番。你可以看图数一下,而对象D中的C的第一部分也是一样,是个虚基表,存的一样也是偏移量,它存的地址“00369b68”,里面是“00 00 00 0c”即十进制的12,即偏移量为12字节。可以看下图:
下面我再讲一个概念——虚函数,这会在下篇文章多态中重点讲解,但是这里有必要了解一下。
虚函数——类的成员函数前面加上virtual关键字,则这个函数被称为虚函数。
虚函数:用于定义类型特定行为的成员函数。通过引用和指针对虚函数的调用直到运行时才被解析,依据是引用或指针所绑定对象的类型。(《C++ Primer》中定义)
虚函数重写(覆盖):当在子类定义了一个和父类完全相同的虚函数时,则称这个这个子类的函数重写了(覆盖了)父类的虚函数。
既然说到这,就有必要区分一下几个概念:
重载:在同一作用域内,函数名相同,参数不同,返回值可不同的一对函数被称为重载。
隐藏(重定义):在不同作用域(一般指基类和派生类),函数名相同,参数列表也相同,但不需要virtual关键字的一组函数称为隐藏。
覆盖:不在同一作用域(一般指派生类和基类),完全相同(协变除外)基类中函数必须有virtual关键字的一对函数被称为重定义。
注:
1,基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
2,只有类的成员函数才能定义为虚函数。
3,静态成员函数不能定义为虚函数。
4,如果在类外定义虚函数,只能在声明处加virtual关键字,类外定义函数时不能加virtual关键字。
5,构造函数不能为虚函数。
6,最好不要将赋值运算符重载定义为虚函数,因为使用容易混淆。
7,不要在构造函数和析构函数调用虚函数,在构造函数和析构函数中对象是不完整的,可能会发生未定义的行为。
8,最好将基类的析构函数定义为虚函数。(注:虽然基类的析构函数和派生类的析构函数名称不一样,但构成覆盖,因为编译器做了特殊处理)
9,虚继承只对虚继承子类后面派生出的子类有影响,对虚继承自雷本身没有影响。
纯虚函数
纯虚函数——在成员函数的后面加上=0,则成员函数为纯虚函数。一个纯虚函数无需定义,但也可以定义,但是必须在类外,也就是说我们不能在类内部为一个带有=0的函数提供函数体。包含纯虚函数的类被称为抽象类,也叫接口类。抽象类不能实例化出对象。他只是作为基类服务于派生类,如果派生类不对基类的虚函数进行覆盖,那他仍将是抽象基类。
class Father //抽象类(接口类)
{
public:
virtual void fun() = 0; //定义纯虚函数
protected:
int _a;
};
class Child
{
public:
virtual void fun() = 0; //覆盖,否则Child也是抽象类(接口类)
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
继承和友元
友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员。
继承和静态成员
基类中定义了静态成员,则整个继承体系中只有一个这样的成员。无论派生出多少的子类,都只有一个静态成员实例。