协会网站建设模板,wordpress文章页个性化定制,wordpress用户密码原理,网站展示怎么做目录 多继承的定义和用法定义多继承多继承中派生类对象的内存布局访问基类成员多继承带来的问题 虚继承虚继承的语法虚继承对象的内存布局虚继承中的构造虚继承的缺点 多继承的定义和用法
C支持多继承#xff0c;即一个派生类可以有多个基类。
很多时候#xff0c;单继承就… 目录 多继承的定义和用法定义多继承多继承中派生类对象的内存布局访问基类成员多继承带来的问题 虚继承虚继承的语法虚继承对象的内存布局虚继承中的构造虚继承的缺点 多继承的定义和用法
C支持多继承即一个派生类可以有多个基类。
很多时候单继承就可以满足开发需求但在特定的情况下就不行。比如有两个类A和B现在要有一个类C它同时具有A和B的属性和行为这种情况下单继承就不能满足要求。
用鸭嘴兽来举例
从形态学上来讲鸭嘴兽应该属于鸟类原因是鸭嘴兽具有扁平的、像鸭子一样的嘴巴而且是角质的不像哺乳动物那种肉质的口唇关键是鸭嘴兽通过下蛋来繁殖后代这明显是鸟类的特征。然而鸭嘴兽也靠乳汁来哺育幼仔浑身密布着浓褐色的短兽毛这又是哺乳动物的重要特征。所以鸭嘴兽既是鸟类又是哺乳动物。
如果要在程序中定义一个鸭嘴兽类采用单继承肯定是不行的。否则鸭嘴兽要么是鸟类要么是哺乳动物显然不符合实际情况。所以此时应当采用多继承让鸭嘴兽同时继承鸟类和哺乳动物类的属性和行为。这样一个鸭嘴兽就既是鸟类又是哺乳动物符合实际情况。
定义多继承
多继承的语法同单继承类似只需要在定义类时在类名后面依次罗列继承方式和基类即可。继承方式同单继承一样也有publicprotected和private。在多继承中针对不同的基类可以使用不同的继承方法。其语法如下 class 派生类名 : 继承方式1 基类名1,继承方式2 基类名2,⋯⋯{派生类新增成员};
多继承的类图如下 例如定义一个鸭嘴兽类应该继承鸟类和哺乳动物类
#include iostream// 鸟类
class Bird
{
public:Bird(){std::cout 鸟类的构造函数 std::endl;}~Bird(){std::cout 鸟类的析构函数 std::endl;}
};// 哺乳动物类
class Mammal
{
public:Mammal(){std::cout 哺乳动物类的构造函数 std::endl;}~Mammal(){std::cout 哺乳动物类的析构函数 std::endl;}
};// 鸭嘴兽类
class Duckbill: public Bird, public Mammal
{
public:Duckbill(): Mammal(), Bird(){std::cout 鸭嘴兽类的构造函数 std::endl;}~Duckbill(){std::cout 鸭嘴兽类的析构函数 std::endl;}
};void Test()
{Duckbill duckbill;
}int main()
{Test();system(pause);return 0;
}vs2022下的运行结果
鸟类的构造函数
哺乳动物类的构造函数
鸭嘴兽类的构造函数
鸭嘴兽类的析构函数
哺乳动物类的析构函数
鸟类的析构函数同定义单继承派生类的构造函数一样定义多继承派生类时也要注意基类的初始化。如果基类没有默认的构造函数那么在派生类构造函数的初始化列表里就要依次调用各个基类的构造函数。无论开发者如何安排基类构造函数的调用次序总是按照其定义时的次序。
我们在初始化列表中先调用哺乳动物的构造函数再调用鸟类的构造函数运行结果按照定义时的顺序进行析构函数相反。
多继承派生类对象在析构时按照与构造相反的顺序进行即先调用派生类自己的析构函数再析构各个数据成员然后按照相反的顺序依次调用各个基类的析构函数
多继承中派生类对象的内存布局
同单继承一样通过多继承派生类将拥有基类所有的属性和行为。在多继承派生类的对象中将依次排列各个基类的非静态数据成员以及派生类新增的数据成员。派生类对象内存中的数据是按照定义时的顺序排列的。也就是说在定义派生类时排在前面的基类其数据在派生类对象中也排在前面。
一个多继承类图如下 它的派生类的内存布局如下
举例说明
#include iostream// 鸟类
class Bird
{
public:Bird(){std::cout 鸟类的构造函数 std::endl;}~Bird(){std::cout 鸟类的析构函数 std::endl;}char a;int b;char c;
};// 哺乳动物类
class Mammal
{
public:Mammal(){std::cout 哺乳动物类的构造函数 std::endl;}~Mammal(){std::cout 哺乳动物类的析构函数 std::endl;}private:char a;char c;char b;
};// 鸭嘴兽类
class Duckbill: public Bird, public Mammal
{
public:Duckbill(): Mammal(), Bird(){std::cout 鸭嘴兽类的构造函数 std::endl;}~Duckbill(){std::cout 鸭嘴兽类的析构函数 std::endl;}
};void Test()
{Duckbill duckbill;std::cout Bird: sizeof(Bird) std::endl;std::cout Mammal: sizeof(Mammal) std::endl;std::cout Duckbill: sizeof(Duckbill) std::endl;
}int main()
{Test();system(pause);return 0;
}vs2022运行结果
鸟类的构造函数
哺乳动物类的构造函数
鸭嘴兽类的构造函数
Bird: 12
Mammal: 3
Duckbill: 16
鸭嘴兽类的析构函数
哺乳动物类的析构函数
鸟类的析构函数Duckbill的内存布局是 char a;int b;char c;char a;char c;char b;按照结构体的内存对齐方式计算得出结果为16而不是123。
派生类对象也可以转换为其基类类型的对象。对于多继承的情况在转换时编译器可以根据要转换的类型进行适当的转换。例如对于上面的多继承类如果要将Derived类对象转换成Base2类的对象编译器会从Derived对象中按照内存排列的顺序从中截取出从Base2类继承来的部分构成新对象。 Derived d;Base2 b2 static_castBase2( d );举例说明 将上述例子中的duckbill转换成基类Mammal对象
Mammal mammal static_castMammal(duckbill);
std::cout Mammal: sizeof(mammal) std::endl; // Mammal: 3访问基类成员
在多继承中如果多个基类拥有同名成员那么在访问基类成员时仅通过成员名并不能区分是哪个基类的成员。解决的方法是在成员名前用域运算符::指明成员所属的基类。通过这种方法访问数据成员和函数成员的语法如下 基类名 :: 数据成员名; // 在派生类成员函数中访问基类成员数据基类名 :: 函数成员名( 参数列表 ); // 在派生类成员函数中访问基类成员函数派生类对象 . 基类名 :: 数据成员名;派生类对象 . 基类名 :: 函数成员名( 参数列表 );派生类指针 - 基类名 :: 数据成员名;派生类指针 - 基类名 :: 函数成员名( 参数列表 );前提是基类的成员变量是公有变量
举例
duckbill.Mammal::a a;
duckbill.Bird::a A;void Duckbill::Foo()
{Mammal::a a;Bird::a A;
}多继承带来的问题
多继承虽然功能强大可以让派生类同时具有多个基类的属性和行为但是多继承同时也会带来一些严重的问题。其中比较常见的问题就是多继承会导致数据重复并由此带来数据不一致的问题。
举例
#include iostream// 动物类
class Animal
{
public:int weight;
};// 鸟类
class Bird: public Animal
{
public:char a;int b;char c;
};// 哺乳动物类
class Mammal: public Animal
{
public:char a;char c;char b;
};// 鸭嘴兽类
class Duckbill: public Bird, public Mammal
{
public:};void Test()
{Duckbill duckbill;std::cout Bird: sizeof(Bird) std::endl;std::cout Mammal: sizeof(Mammal) std::endl;std::cout Duckbill: sizeof(Duckbill) std::endl;}int main()
{Test();system(pause);return 0;
}运行结果
Bird: 16
Mammal: 8
Duckbill: 24很明显Duckbill中的weight有两份。
比较典型的情况是一个派生类D从两个基类B和C中派生而这两个基类又有一个共同的基类A这就会导致A的数据在D中被重复两次如图16-7所示。D多继承B和C将B和C的数据复制到D中。由于A的数据已经分别被B和C继承所以A的数据在D中将重复两次。而且在定义D类的成员函数时或者通过D类对象和指针访问成员数据a时必须用域运算符::指明a所在的类即 B::a 1; // 在D的成员函数中访问A类的数据成员C::a 2;D dObj; // D类对象dObj.B::a 3; // 通过D类对象访问A类的数据成员dObj.C::a 4;D *pObj new D(); // D类指针pObj-B::a 5; // 通过D类指针访问A类的数据成员pObj-C::a 6;从编译器的设计角度来讲当D从B和C继承时并不知道基类A的存在。D只能全盘接受来自B和C的数据而无法区分其中的数据a到底是从B继承而来的还是从C继承而来的。所以要访问数据a只能由用户来指明。
从逻辑的角度来讲在D类的对象中A的数据应当只有一份。比如有一个动物基类Animal它具有重量属性。鸟类Bird和哺乳动物类Mammal都从Animal派生然后鸭嘴兽类DuckBill又从鸟类和哺乳动物类派生。从继承的语义来讲一个DuckBill对象也是一个Animal所以鸭嘴兽应当具有重量属性。但是由于多继承导致数据冗余所以基类的一份数据在其间接派生类中产生了多份副本。所以在上述的鸭嘴兽对象中将具有“两”个重量属性。这显然是不符合逻辑的。而且由于数据冗余也容易导致数据的不一致。例如上例的D类其中继承自B的数据a和继承自C的数据a可以分别访问如果开发者不能始终保证每次修改两个数据使其完全一样那么就很容易导致数据不一致。 D dObj; // 定义D类对象⋯⋯dObj.B::a 1; // 修改继承自B的数据a⋯⋯dObj.C::a 2; // 修改继承自C的数据a显然在上述代码中很容易导致一个数据a有两个不同值而这种情况是多继承无法克服的一个缺点。另外如果A类的构造函数带有参数而且没有默认构造函数那么在B类和C类构造时就必须调用这个构造函数。假设由于开发者的疏忽导致B类和C类在调用A类的构造函数时不一致那么D类中的两个数据a也就会不一致。
为了解决多继承导致的数据冗余和数据不一致的问题可以采用虚拟继承机制也可以禁止最初的基类带有数据。一个不带有任何数据仅有函数成员的基类也称做接口。
虚继承
虚拟继承是解决多继承带来的问题的一个重要机制。通过虚拟继承基类的数据在派生类中将只有一份副本从而避免了多继承导致的数据冗余和数据不一致问题。
虚继承的语法
虚拟继承是在定义派生类时将基类指明为虚基类或者说派生类以虚拟的方式从基类派生。虚拟继承的方法是在普通继承的基类名前加上virtual关键字如下所示 class派生类名 : 继承方式 virtual 基类名{派生类的定义};例如B类从A类虚拟继承则B类定义如下 class B : public virtual A // B类从A类虚拟继承{⋯⋯private:int b; // B类新增的成员数据};虚继承对象的内存布局
虚继承除了常规的数据成员内存还会有虚表指针。
#include iostream// 动物类
class Animal
{
public:int weight;
};// 鸟类
class Bird: public virtual Animal
{
public:int b;
};// 哺乳动物类
class Mammal: public virtual Animal
{
public:char a;int c;int b;
};// 鸭嘴兽类
class Duckbill: public Bird, public Mammal
{
public:};void Test()
{Duckbill duckbill;Bird bird;Mammal mammal;std::cout Bird: sizeof(bird) std::endl;std::cout Mammal: sizeof(mammal) std::endl;std::cout Duckbill: sizeof(duckbill) std::endl;}int main()
{Test();system(pause);return 0;
}vs2022、x64位运行结果
Bird: 24
Mammal: 32
Duckbill: 48Duckbill对象的内存布局如下 与普通继承不同在虚拟继承中派生类对象并不是在其内存中保留一份虚基类数据的副本而是通过一种间接的引用方式即将虚基类子对象的数据单独存放在派生类对象中设置一个指针指向基类子对象。这样当一个派生类通过多个继承路径继承同一个虚基类时并不需要产生多个数据副本而只要维护这个虚基类指针即可。
虚继承中的构造
由于在虚拟继承中虚基类的数据只有一份所以在间接派生类构造时需要特殊处理即只能初始化虚基类一次。
假设Vehicle类有一个带有参数的构造函数而且没有默认构造函数Vehicle ::Vehicleint number那么在中间派生类虚拟继承Tank和Boat的构造函数中都要显式调用Vehicleint number。但是在AmphiTank类多继承自Tank类和Boat类之后如果仍然通过两个基类来初始化Vehicle那么Vehicle将被初始化两次从而可能导致数据不一致。
所以在C中对于虚基类的初始化进行了特殊处理。**如果是在一级派生中比如Tank类虚拟继承Vehicle类那么其初始化同一般继承一样。如果是在多级派生中那么虚基类的初始化将由最终一级的派生类负责。**所以在水陆两栖坦克的类层次结构中虚基类Vehicle的初始化应当由最终一级派生类AmphiTank负责即Vehicle的构造函数应当放在AmphiTank的初始化列表中。
举例
#include iostream// 动物类
class Animal
{
public:Animal(int _num): weight(_num){}int weight;
};// 鸟类
class Bird: public virtual Animal
{
public:Bird(int _num): Animal(_num), b(_num){}int b;
};// 哺乳动物类
class Mammal: public virtual Animal
{
public:Mammal(int _num): Animal(_num), b(_num){}char a ;int c 1;int b 2;
};// 鸭嘴兽类
class Duckbill: public Bird, public Mammal
{
public:Duckbill(int animal, int bird, int mammal): Animal(animal), Bird(bird), Mammal(mammal){}
};void Test()
{Duckbill duckbill(1,2,3);// Bird的weightstd::cout duckbill.Bird::weight std::endl;// Mammal的weightstd::cout duckbill.Mammal::weight std::endl;// Duckbill的weightstd::cout duckbill.weight std::endl;}int main()
{Test();system(pause);return 0;
}vs2022运行结果
1
1
1虽然我们在构造Bird和Mammal时用不同的值都构造了Animal但是只会由最后一级Duckbill负责。
如果一个派生类既有虚基类不一定是直接基类又有非虚基类那么无论初始化列表如何排列虚基类总是先初始化。如果有多个虚基类那么排在前面的先初始化。
派生类的析构顺序总是与构造顺序相反所以如果一个派生类有虚基类则虚基类总是在最后析构。
虚继承的缺点
虚拟继承虽然可以解决多继承带来的数据冗余和数据不一致的缺点但虚拟继承本身也存在一些问题具体问题如下
◆ 增加内存。为了保证虚基类的数据在派生类中只出现一次采用虚继承的方式引入了虚基类指针额外增加了类的占用内存。
◆ 派生类要显式初始化其虚拟基类。通常从开发者的角度来讲设计一个派生类只要初始化其直接基类即可。但是如果在类的派生层次中存在虚拟基类那么派生类始终要负责这些虚拟基类的初始化这在一定程度上导致了设计的复杂化。
多继承容易导致数据冗余和数据不一致而虚拟继承在解决了这个问题的同时又引入了新的问题。对于类层次结构的设计者来讲可以采取另外一种方法来解决多继承的问题即只允许一个基类有数据其他基类只有方法这样就消除了数据冗余和数据不一致的问题。只有方法没有数据的类也称做接口。