C++语言学习(十六)——多继承

一、多继承简介

1、多继承简介

C++语言支持多继承,一个子类可以有多个父类,子类拥有所有父类的成员变量,子类继承所有父类的成员函数,子类对象可以当作任意父类对象使用。

2、多继承语法规则

class Derived : public BaseA,public BaseB,public BaseC
{};

3、多继承派生类的内存布局

通过多重继承得到的派生类对象可能具有不同的地址。

#include <iostream>using namespace std;class BaseA
{
public:BaseA(int a){ma = a;}
private:int ma;
};class BaseB
{
public:BaseB(int b){mb = b;}
private:int mb;
};class Derived : public BaseA,public BaseB
{
public:Derived(int a, int b, int c):BaseA(a),BaseB(b){mc = c;}
private:int mc;
};struct Test
{int a;int b;int c;
};int main(int argc, char *argv[])
{Derived d(1,2,3);cout << sizeof(d) << endl;//12Test* p = (Test*)&d;cout << p->a << endl;//1cout << p->b << endl;//2cout << p->c << endl;//3cout << &p->a << endl;//1cout << &p->b << endl;//2cout << &p->c << endl;//3BaseA* pa = &d;BaseB* pb = &d;//子类对象的地址、首位继承类的成员地址cout << &d << endl;cout << pa << endl;cout << &p->a <<endl;//子类对象的地址、次位继承类的成员地址cout << pb << endl;cout << &p->b << endl;return 0;
}

上述代码中,Derived类对象的内存布局如下:

Derived类对象从基类继承而来的处成员变量将根据继承的声明顺序进行依次排布。基于赋值兼容原则,如果BaseA类型指针pa、BaseB类型指针pb都指向子类对象d,pa将得到BaseA基类成员变量ma的地址,即子类对象的地址;pb将得到BaseB类成员变量mb的地址;因此,pa与pb的地址不相同。

4、菱形多继承导致的成员冗余


上述类图中,Teacher类和Student类都会继承People的成员,Doctor会继承Teacher类和Student类的成员,因此Doctor将会有两份继承自顶层父类People的成员。

#include <iostream>
#include <string>using namespace std;class People
{
public:People(string name, int age){m_name = name;m_age = age;}void print(){cout << "name: " << m_name<< " age: " << m_age <<endl;}
private:string m_name;int m_age;
};class Teacher : public People
{
public:Teacher(string name, int age):People(name, age){}
};class Student : public People
{
public:Student(string name, int age):People(name, age){}
};class Doctor : public Teacher, public Student
{
public:Doctor(string name, int age):Teacher(name + "_1", age),Student(name + "_2", age){}
};int main(int argc, char *argv[])
{Doctor doc("Bauer", 30);//doc.print();//error//error: request for member 'print' is ambiguous//Doctor继承了从Teacher,Student继承来的print函数。//doc.People::print();//error//error: 'People' is an ambiguous base of 'Doctor'//People被继承了两次doc.Teacher::print();//name:bauer_1 age:30doc.Student::print();//name:bauer_2 age:30return 0;
}

二、虚继承

1、虚继承简介

在多继承中,保存共同基类的多份同名成员,可以在不同的数据成员中分别存放不同的数据,但保留多份数据成员的拷贝,不仅占有较多的存储空间,增加了成员的冗余,还增加了访问的困难。C++提供了虚基类和虚继承机制,实现了在多继承中只保留一份共同成员。
C++对于菱形多继承导致的成员冗余问题的解决方案是使用虚继承。
虚继承中,中间层父类不再关注顶层父类的初始化,最终子类必须直接调用顶层父类的构造函数。
虚继承的语法如下:
class 派生类名:virtual 继承方式 基类名

2、虚继承示例

#include <iostream>
#include <string>using namespace std;class People
{
public:People(string name, int age){m_name = name;m_age = age;}void print(){cout << "name: " << m_name<< " age: " << m_age <<endl;}
private:string m_name;int m_age;
};class Teacher : virtual public People
{
public:Teacher(string name, int age):People(name, age){}
};class Student : virtual public People
{
public:Student(string name, int age):People(name, age){}
};class Doctor : public Teacher, public Student
{
public://最终子类必须调用顶层父类的构造函数Doctor(string name, int age):People(name, age),Teacher(name + "_1", age),Student(name + "_2", age){}
};int main(int argc, char *argv[])
{Doctor doc("Bauer", 30);doc.print();//name:bauer age:30doc.People::print();//name:bauer age:30doc.Teacher::print();//name:bauer age:30doc.Student::print();//name:bauer age:30return 0;
}

上述代码中,使用虚继承解决了成员冗余的问题。
虚继承解决了多继承产生的数据冗余问题,但是中间层父类不再关心顶层父类的初始化,最终子类必须直接调用顶层父类的构造函数。

三、多继承派生类的对象模型

1、多继承派生类对象的内存布局


上述类图中,Derived类继承自BaseA和BaseB类,funcA和funcB为虚函数,Derived对象模型如下:

#include <iostream>
#include <string>using namespace std;class BaseA
{
public:BaseA(int a){m_a = a;}virtual void funcA(){cout << "BaseA::funcA()" <<endl;}
private:int m_a;
};class BaseB
{
public:BaseB(int b){m_b = b;}virtual void funcB(){cout << "BaseB::funcB()" <<endl;}
private:int m_b;
};class Derived : public BaseA, public BaseB
{
public:Derived(int a, int b, int c):BaseA(a),BaseB(b){m_c = c;}
private:int m_c;
};struct Test
{void* vptrA;int a;void* vptrB;int b;int c;
};int main(int argc, char *argv[])
{cout << sizeof(Derived) << endl;Derived d(1,2,3);Test* pTest = (Test*)&d;cout << pTest->a <<endl;//1cout << pTest->b <<endl;//2cout << pTest->c <<endl;//3cout << pTest->vptrA <<endl;//cout << pTest->vptrB <<endl;//return 0;
}

2、菱形继承派生类对象的内存布局

菱形继承示例代码如下:

#include <iostream>
#include <string>using namespace std;class People
{
public:People(string name, int age){m_name = name;m_age = age;}void print(){cout << "name: " << m_name<< " age: " << m_age <<endl;}
private:string m_name;int m_age;
};class Teacher : public People
{string m_research;
public:Teacher(string name, int age, string research):People(name + "_1", age + 1){m_research = research;}
};class Student : public People
{string m_major;
public:Student(string name, int age,string major):People(name + "_2", age + 2){m_major = major;}
};class Doctor : public Teacher, public Student
{string m_subject;
public:Doctor(string name, int age,string research, string major, string subject):Teacher(name, age,research),Student(name, age, major){m_subject = subject;}
};struct Test
{string name1;int age1;string research;string name2;int age2;string major;string subject;
};int main(int argc, char *argv[])
{Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC");cout << "Doctor size: " << sizeof(doc) << endl;Test* pTest = (Test*)&doc;cout << pTest->name1 << endl;cout << pTest->age1 << endl;cout << pTest->research << endl;cout << pTest->name2 << endl;cout << pTest->age2 << endl;cout << pTest->major << endl;cout << pTest->subject << endl;return 0;
}
// output:
// Doctor size: 28
// Bauer_1
// 31
// Computer
// Bauer_2
// 32
// Computer Engneering
// HPC

上述代码中,底层子类对象的内存局部如下:

底层子类对象中,分别继承了中间层父类从顶层父类继承而来的成员变量,因此内存模型中含有两份底层父类的成员变量。
如果顶层父类含有虚函数,中间层父类会分别继承顶层父类的虚函数表指针,因此,底层子类对象内存布局如下:

#include <iostream>
#include <string>using namespace std;class People
{
public:People(string name, int age){m_name = name;m_age = age;}virtual void print(){cout << "name: " << m_name<< " age: " << m_age <<endl;}
private:string m_name;int m_age;
};class Teacher : public People
{string m_research;
public:Teacher(string name, int age, string research):People(name + "_1", age + 1){m_research = research;}
};class Student : public People
{string m_major;
public:Student(string name, int age,string major):People(name + "_2", age + 2){m_major = major;}
};class Doctor : public Teacher, public Student
{string m_subject;
public:Doctor(string name, int age,string research, string major, string subject):Teacher(name, age,research),Student(name, age, major){m_subject = subject;}virtual void print(){}
};struct Test
{void* vptr1;string name1;int age1;string research;void* vptr2;string name2;int age2;string major;string subject;
};int main(int argc, char *argv[])
{Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC");cout << "Doctor size: " << sizeof(doc) << endl;Test* pTest = (Test*)&doc;cout << pTest->vptr1 << endl;cout << pTest->name1 << endl;cout << pTest->age1 << endl;cout << pTest->research << endl;cout << pTest->vptr2 << endl;cout << pTest->name2 << endl;cout << pTest->age2 << endl;cout << pTest->major << endl;cout << pTest->subject << endl;return 0;
}// output:
// Doctor size: 28
// 0x405370
// Bauer_1
// 31
// Computer
// 0x40537c
// Bauer_2
// 32
// Computer Engneering
// HPC

3、虚继承派生类对象的内存布局

虚继承是解决C++多重继承问题的一种手段,虚继承的底层实现原理与C++编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4(8)字节)和虚基类表(不占用类对象的存储空间)(虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
在虚继承情况下,底层子类对象的布局不同于普通继承,需要多出一个指向中间层父类对象的虚基类表指针vbptr。
vbptr是虚基类表指针(virtual base table pointer),vbptr指针指向一个虚基类表(virtual table),虚基类表存储了虚基类相对直接继承类的偏移地址;通过偏移地址可以找到虚基类成员,虚继承不用像普通多继承维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

#include <iostream>
#include <string>using namespace std;class People
{
public:People(string name, int age){m_name = name;m_age = age;}void print(){cout << "this: " << this <<endl;}
private:string m_name;int m_age;
};class Teacher : virtual public People
{string m_research;
public:Teacher(string name, int age, string research):People(name + "_1", age + 1){m_research = research;}void print(){cout << "this: " << this <<endl;}
};class Student : virtual public People
{string m_major;
public:Student(string name, int age,string major):People(name + "_2", age + 2){m_major = major;}void print(){cout << "this: " << this <<endl;}
};class Doctor : public Teacher, public Student
{string m_subject;
public:Doctor(string name, int age,string research, string major, string subject):People(name, age),Teacher(name, age,research),Student(name, age, major){m_subject = subject;}
};struct Test
{void* vbptr_left;string research;void* vbptr_right;string major;string subject;string name;int age;
};int main(int argc, char *argv[])
{Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC");cout << "Doctor size: " << sizeof(doc) << endl;Test* pTest = (Test*)&doc;cout << pTest->vbptr_left << endl;cout << *(int*)pTest->vbptr_left << endl;cout << pTest->research << endl;cout << pTest->vbptr_right << endl;cout << *(int*)pTest->vbptr_right << endl;cout << pTest->major << endl;cout << pTest->subject << endl;cout << pTest->name << endl;cout << pTest->age << endl;return 0;
}// output:
// Doctor size: 28
// 0x40539c
// 12
// Computer
// 0x4053a8
// 0
// Computer Engneering
// HPC
// Bauer
// 30

上述代码没有虚函数,在G++编译器打印结果如上,底层子类对象的内存布局如下:

#include <iostream>
#include <string>using namespace std;class People
{
public:People(string name, int age){m_name = name;m_age = age;}virtual void print(){cout << "this: " << this <<endl;}
private:string m_name;int m_age;
};class Teacher : virtual public People
{string m_research;
public:Teacher(string name, int age, string research):People(name + "_1", age + 1){m_research = research;}void print(){cout << "this: " << this <<endl;}virtual void func1(){}
};class Student : virtual public People
{string m_major;
public:Student(string name, int age,string major):People(name + "_2", age + 2){m_major = major;}void print(){cout << "this: " << this <<endl;}virtual void func2(){}
};class Doctor : public Teacher, public Student
{string m_subject;
public:Doctor(string name, int age,string research, string major, string subject):People(name, age),Teacher(name, age,research),Student(name, age, major){m_subject = subject;}void print(){cout << "this: " << this <<endl;}virtual void func3(){}
};struct Test
{void* vbptr_left;char* research;void* vbptr_right;char* major;char* subject;void* vptr_base;char* name;long age;
};int main(int argc, char *argv[])
{Doctor doc("Bauer", 30, "Computer", "Computer Engneering", "HPC");cout << "Doctor size: " << sizeof(doc) << endl;Test* pTest = (Test*)&doc;cout << pTest->vbptr_left << endl;cout << std::hex << *(int*)pTest->vbptr_left << endl;cout << std::dec << *((int*)pTest->vbptr_left+8) << endl;cout << std::dec << *((int*)pTest->vbptr_left+16) << endl;cout << std::dec << *((int*)pTest->vbptr_left+24) << endl;cout << pTest->research << endl;cout << pTest->vbptr_right << endl;cout << pTest->major << endl;cout << pTest->subject << endl;cout << pTest->vptr_base << endl;cout << pTest->name << endl;cout << pTest->age << endl;return 0;
}

上述代码中,使用了虚继承,因此不同的C++编译器实现原理不同。
对于GCC编译器,People对象大小为char + int + 虚函数表指针,Teacher对象大小为char+虚基类表指针+A类型的大小,Student对象大小为char+虚基类表指针+A类型的大小,Doctor对象大小为char + int +虚函数表指针+char+虚基类表指针+char+虚基类表指针+char*。中间层父类共享顶层父类的虚函数表指针,没有自己的虚函数表指针,虚基类指针不共享,因此都有自己独立的虚基类表指针。
VC++、GCC和Clang编译器的实现中,不管是否是虚继承还是有虚函数,其虚基类指针都不共享,都是单独的。对于虚函数表指针,VC++编译器根据是否为虚继承来判断是否在继承关系中共享虚表指针。如果子类是虚继承拥有虚函数父类,且子类有新加的虚函数时,子类中则会新加一个虚函数表指针;GCC编译器和Clang编译器的虚函数表指针在整个继承关系中共享的。
G++编译器对于类的内存分布和虚函数表信息命令如下:

g++ -fdump-class-hierarchy main.cpp
cat main.cpp.002t.class

VC++编译器对于类的内存分布和虚函数表信息命令如下:
cl main.cpp /d1reportSingleClassLayoutX
Clang编译器对于类的内存分布和虚函数表信息命令如下:
clang -Xclang -fdump-record-layouts

4、多继承派生类的虚函数表

所有的虚函数都保存在虚函数表中,多重继承可能产生多个虚函数表。多继承中,当子类对父类的虚函数重写时,子类的函数覆盖父类的函数在对应虚函数表中的虚函数位置;当子类有新的虚函数时,新的虚函数被加到第一个基类的虚函数表的末尾。当dynamic_cast对子类对象进行转换时,子类和第一个基类的地址相同,不需要移动指针,但当dynamic_cast转换子类到其他父类时,需要做相应的指针调整。

四、多继承的指针类型转换

1、多继承中指针类型转换的陷阱

C++语言中,通常对指针进行类型转换,不会改变指针的值,只会改变指针的类型(即改变编译器对该指针指向内存的解释方式),但在C++多重继承中并不成立。

#include <iostream>using namespace std;class BaseA
{
public:BaseA(int value = 0){data = value;}virtual void printA(){cout << "BaseA::print data = " << data << endl;}
protected:int data;
};class BaseB
{
public:BaseB(int value = 0){data = value;}virtual void printB(){cout << "BaseB::print data = " << data << endl;}
protected:int data;
};class Derived : public BaseA, public BaseB
{
public:Derived(int value = 0){data = value;}virtual void printA(){cout << "Derived printA data = " << data << endl;}virtual void printB(){cout << "Derived printB data = " << data << endl;}
protected:int data;
};int main(int argc, char *argv[])
{Derived* dpd = new Derived(102);cout << dpd << endl;//0x8d1190BaseA* bpa = (BaseA*)dpd;cout << bpa << endl;//0x8d1190BaseB* bpb = (BaseB*)dpd;cout << bpb << endl;//0x8d1198cout << (dpd == bpb) << endl;//1return 0;
}

上述代码中,指向Derived对象的指针转换为基类BaseA和BaseB后,指针值并不相同。dpd指针、bpa指针与bpb指针相差8个字节的地址空间,即BaseA类虚函数表指针与data成员占用的空间。
将一个派生类的指针转换成某一个基类指针,C++编译器会将指针的值偏移到该基类在对象内存中的起始位置。


cout << (dpd == bpb) << endl;//1

上述代码打印出1,C++编译器屏蔽了指针的差异,当C++编译器遇到一个指向派生类的指针和指向其某个基类的指针进行==运算时,会自动将指针做隐式类型提升以屏蔽多重继承带来的指针差异。

2、多继承中派生类、基类指针类型转换

派生类对象指针转换为不同基类对象指针时,C++编译器会按照派生类声明的继承顺序,转换为第一基类时指针不变,以后依次向后偏移前一基类所占字节数。
多继承下,指针类型转换需要考虑this指针调整的问题。

五、多继承应用示例

多继承中,如果中间层父类有两个以上父类实现了虚函数,会造成子类产生多个虚函数表指针,可以使用dynamic_cast关键字作类型转换。
工程实践中通常使用单继承某个类和实现多个接口解决多继承的问题。
代码实例:

#include <iostream>
#include <string>using namespace std;class Base
{
protected:int mi;
public:Base(int i){mi = i;}int getI(){return mi;}bool equal(Base* obj){return (this == obj);}
};class Interface1
{
public:virtual void add(int i) = 0;virtual void minus(int i) = 0;
};class Interface2
{
public:virtual void multiply(int i) = 0;virtual void divide(int i) = 0;
};class Derived : public Base, public Interface1, public Interface2
{
public:Derived(int i) : Base(i){}void add(int i){mi += i;}void minus(int i){mi -= i;}void multiply(int i){mi *= i;}void divide(int i){if( i != 0 ){mi /= i;}}
};int main()
{Derived d(100);Derived* p = &d;Interface1* pInt1 = &d;Interface2* pInt2 = &d;cout << "p->getI() = " << p->getI() << endl;    // 100pInt1->add(10);pInt2->divide(11);pInt1->minus(5);pInt2->multiply(8);cout << "p->getI() = " << p->getI() << endl;    // 40cout << endl;cout << "pInt1 == p : " << p->equal(dynamic_cast<Base*>(pInt1)) << endl;cout << "pInt2 == p : " << p->equal(dynamic_cast<Base*>(pInt2)) << endl;return 0;
}

在程序设计中最好不要出现多继承,要有也是继承多个作为接口使用抽象类(只声明需要的功能,没有具体的实现)。因为出现一般的多继承本身就是一种不好的面向对象程序设计。

转载于:https://blog.51cto.com/9291927/2164576

C++语言学习(十六)——多继承相关推荐

  1. OpenCV与图像处理学习十六——模板匹配

    OpenCV与图像处理学习十六--模板匹配 一.模板匹配介绍 二.代码应用 一.模板匹配介绍 模板匹配是一种最原始.最基本的模式识别方法,研究某一特定目标的图像位于图像的什么地方,进而对图像进行定位. ...

  2. PyTorch框架学习十六——正则化与Dropout

    PyTorch框架学习十六--正则化与Dropout 一.泛化误差 二.L2正则化与权值衰减 三.正则化之Dropout 补充: 这次笔记主要关注防止模型过拟合的两种方法:正则化与Dropout. 一 ...

  3. Python的学习(十六):对文件的操作

    Python的学习(十六):对文件的操作 编码格式的介绍 Python中的解释器使用的是Unicode(内存) .py文件在磁盘上使用UTF-8存储(外存) 如何修改文件格式?不写的话默认为UTF-8 ...

  4. 强化学习(十六) 深度确定性策略梯度(DDPG)

    在强化学习(十五) A3C中,我们讨论了使用多线程的方法来解决Actor-Critic难收敛的问题,今天我们不使用多线程,而是使用和DDQN类似的方法:即经验回放和双网络的方法来改进Actor-Cri ...

  5. c++代码转为go_Go语言学习笔记六--string编码

    分解探索string编码 转为byte数组 func main() {s := "Hi小智加油!"fmt.Println("len(s):",len(s)) / ...

  6. map赋值给另一个map_Java学习(十六): Lambda操作Map

    1. Java8新加入的lambda方法 由于继承关系,他们相应的子类也都会继承这些新方法. 2. forEach() 该方法签名为void forEach(BiConsumer super K,? ...

  7. C语言试题十六之写删除字符串中指定下标的字符。其中,a指向原字符串,删除后的字符串存放在b所指的数组中,n中存放指定的下标。

    1. 题目 请编写一个函数void function(char a[],char b[], int n),其功能是:删除字符串中指定下标的字符.其中,a指向原字符串,删除后的字符串存放在b所指的数组中 ...

  8. c++面向对象高级编程 学习十六 vptr和vtbl

    当一个类中有一个或多个虚函数时,内存中会多一个虚指针(vptr,virtual pointer),指向一个虚表(vtbl,virtual table) 父类有虚函数,则子类一定有虚函数 在下图示意图中 ...

  9. c++面向对象高级编程 学习十五 组合继承关系下的构造和析构

    文章目录 继承关系 组合关系 继承和组合 继承关系 构造由内而外,析构由外而内,内即是父类 组合关系 A拥有B, 构造由内而外,析构由外而内,内即是B 继承和组合 构造和析构顺序如图:

  10. Go语言学习十二 变量和常量

    本文最初发表在我的个人博客,查看原文,获得更好的阅读体验 Go 使用var关键字声明变量:使用关键字const声明常量.变量可以像常量一样初始化. 一 变量 1.1 变量声明 语法: var 变量名 ...

最新文章

  1. .NET C# 发送邮件内容嵌入图片
  2. Spring来装配组件
  3. python classmethod用处_Python classmethod类方法修饰符
  4. c++ const常量的实现机制(转载)2
  5. mysql数据库+查询+sequence_MySQL数据库InnoDB存储引擎Log漫游
  6. aes key iv从mysql_OpenSSL AES 算法中 Key 和 IV 是如何生成的?
  7. 同步机制应遵循的规则
  8. 用JAVA写一个俄罗斯方块游戏tetrisGame
  9. c语言间隔符号的作用,C语言教学(二)常见的符号
  10. 【贪心】(雾)小Y的炮
  11. oracle10g centos 安装,Centos下静默安装oracle10g
  12. dorado 刷新_dorado5 datatree 刷新问题
  13. GSM技术类有哪些最新发表的毕业论文呢?
  14. 终于有人把标签设计讲明白了
  15. 计算机技术考长沙理工大学难不难,长沙理工大学难考吗?长沙理工大学值得上吗?...
  16. 原腾讯QQ技术总监、T13专家,黄希彤被“离职”,原因竟是……
  17. 教你5分钟做成一个ASP论坛
  18. FM 发射模块QN8027软件android 5.1实现分析
  19. linux 打印环境变量命令,echo命令的打印环境变量
  20. 2017年php还能火多久,网红品牌 还能火多久?

热门文章

  1. MyBatis执行原理图
  2. [基础知识]在PeopleSoft中SMTP设置不生效如何查找问题
  3. Ubuntu配置 PPTP 服务器端
  4. 生活中的思维风暴——读《谁是谷歌想要的人才》精彩题目小记(二)
  5. Oracle 9i的标量数据类型
  6. 机器学习基础:K近邻算法(Machine Learning Fundamentals: KNN)
  7. Initial Audio Trap Phantom Heatup3 Expansion for mac(Heatup3扩展预设)
  8. 解决sketch文件导出没有背景色的问题
  9. 《拥抱机器人时代——Servo杂志中文精华合集》——3.6 物联网有多么重要
  10. Android 色彩设计理念