01 基本概念
在现实世界中,任何一个概念(类(类型))都不是独立存在的,都存在与之相关的概念,这些概念之间存在各种各样的关系。在面向对象的方法中,使用类表示概念,那么概念和概念之间的关系,就是类和类之间的关系,也是类型与类型之间的关系
如:
当提到“计算机”这个概念的时候
很快就会想到CPU,内存,主板,显卡......
也会想到台式机,笔记本,游戏本,平板,手机......
在上面的例子中,类和类之间的关系主要存在两种:(1) 组合(Composite)
如计算机是由CPU,内存,主板,显卡 ...... 等类型的对象组合而成的(如:矩形类拥有Size,Point,Color)
也称为has-a的关系(有一个的关系)
(2) 继承(Inheritance)如台式机,笔记本,游戏本,平板,手机 ...... 都是"计算机",都是由计算机引申出来的概念(是一个特别的计算机)
称为is-a的关系(是一个的关系),它们都具有"计算机"共同的属性
那么什么是继承呢?用来做什么?有什么好处呢?
在已有类型的基础上创建新的类型,新类型拥有(继承了)已有类型的所有特性
继承主要用来描述那些非常相似,只有细微差别的类之间的关系
继承可以实现代码重用,减少代码的冗余,提高开发效率
通过继承联系在一起的类,构成了一种层次关系
处于层次关系中根部的类,称为基础类(Basic class),简称基类,也称为父类处于层次关系中其他的类(直接或者间接从基类继承而来的),这些继承而来的类,称为派生类(Derived class),也称为子类,派生类拥有基类的所有属性和行为
基类和派生类应该分别完成什么事情(定义哪些属性)呢?
基类负责定义层次关系中所有的类共同拥有的那些行为和特征派生类自动获取基类的行为和属性,并且负责定义各派生类自己特有的行为和属性
02 定义基类
定义基类的方式和定义单个类的语法规则相同(基类就是一个普通的类型)
class 类名 {......
};
但是设计基类时,要注意:1. 基类的成员函数(行为)
一种是基类希望派生类直接继承而不需要改变的行为(所有的计算机都可以开机)
一种是基类希望派生类提供自己的新定义,以覆盖从基类继承过来的函数定义
这种函数,基类通常将他们定义为虚函数(virtual function)
如:所有的计算机都可以输入,但是输入的方式不一样
2. 基类成员的访问控制
在派生类中对基类成员的访问权限
基类的公有成员在任何地方(成员函数/全局函数/派生类)都可以直接访问,包括派生类(派生类拥有基类的所有属性)基类的私有成员只有友元函数和本类的成员函数可以访问,其他地方都不可以直接访问,派生类也不行(派生类拥有基类的私有成员,但是不能直接访问)
如果有些成员不希望对外公开,但是又希望它的派生类能够直接访问,则可以使用protected来修饰这些成员(只有友元,本类的成员函数,派生类中的成员函数可以直接访问
03 定义派生类
由于派生类是在基类的基础上创建的(拥有基类的一切),所以在定义派生类的同时,需要指明基类是什么并且指明是以什么方式继承
语法规则: class 派生类名称 : 继承方式1 基类名1, 继承方式2 基类名2... { 派生类自己新增的属性和行为; };
如果派生类只有一个基类,则这种继承方式叫做单继承
如果派生类有多个基类,则这种继承方式叫做多继承
继承方式:指定基类成员在派生类内部(派生类自己的成员函数)和派生类外部(通过对象)的访问权限C++中有三种继承方式: public 公有继承(一定需要掌握) private 私有继承 protected 保护继承
继承方式可以省略,class的默认的继承方式是private继承,struct的默认继承方式是public继承,虽然class和struct都有默认的继承方式,但是为了程序的可读性,我们应该显示的说明继承方式
派生类继承自基类,拥有基类的所有成员(一切),可以看做是派生类中拥有一个基类子对象
在构造派生类前要先构造基类子对象,先构造的后析构public 公有继承 基类的公有成员,通过公有继承,成为了派生类自己的公有成员 基类的私有成员,通过公有继承,成为了派生类自己的一部分, 但是派生类不可以直接访问它 基类的保护成员,通过公有继承,成为了派生类自己的保护成员 private 私有继承(限制比较大,用得少,因为基类的成员基本上都成为了私有成员) 基类的公有成员,通过私有继承,成为派生类的私有成员 基类的私有成员,通过私有继承,成为了派生类自己的一部分, 但是派生类不可以直接访问它 基类的保护成员,通过私有继承,成为派生类的私有成员 protected 保护继承 基类的公有成员,通过保护继承,成为了派生类自己的保护成员 基类的私有成员,通过保护继承,成为了派生类自己的一部分, 但是派生类不可以直接访问它 基类的保护成员,通过保护继承,成为了派生类自己的保护成员
关于保护成员:
类内部(成员函数)可以直接访问,友元函数可以直接访问,派生类内部(派生类的成员函数)也可以直接访问类的外部(通过对象名/对象指针)不可以直接访问
关于私有成员:private成员在子类中依然存在,但是却无法直接访问,无论哪一种继承方式,派生类都不能直接使用基类的私有成员
派生类占用的空间的大小:以windows编译器为准,Linux编译器g++会有优化
等于基类占用的存储空间大小(把基类看成一个整体)加上派生类自己的成员大小(派生类中看做是拥有一个基类,考虑字节对齐)
#include <iostream> using namespace std; class A { public: char a; double b; int c; }; class B : public A { public: int d; }; int main() { cout << sizeof(B) << endl; // 32 return 0; } ========================================= #include <iostream> using namespace std; class A { private: int a; char c; }; class B : public A { private: char c; }; int main() { cout << sizeof(B) << endl; // 12 return 0; } ========================================== #include <iostream> using namespace std; class A { public: int c; int d; char e; }; class B : public A { public: double f; }; int main() { cout << sizeof(B) << endl; // 24 return 0; }
04 派生类的组成
一个派生类对象从整体看,由两部分组成:
a. 从基类继承过来的成员(“可以理解为一个基类子对象,是一个整体”)
b. 派生类自己的新增成员
由于在派生类中,含有基类的所有成员(具备基类的一切的能力),所以,我们能够把公有继承的派生类对象当成基类对象来使用
具体表现在:1. 基类的引用可以绑定到派生类对象(基类的引用可以成为派生类的别名)
Derived d; // 派生类对象 Base &pd = d; // 基类的引用成为派生类的别名 pd.show(); // 调用的还是基类本身的show函数 // 当使用派生类对象得到一个基类对象/引用/指针的时候 // 该基类对象/引用/指针是无法访问到派生类的新增成员的 // 只能访问到派生类从基类继承过来的成员 // 在编译器看来,基类对象/引用/指针的类型依然是基类
2. 基类的指针可以指向派生类对象
Derived d; // 派生类对象 Base *pd = d; // 基类的引用成为派生类的别名 pd->show(); // 调用的还是基类本身的show函数 // 当使用派生类对象得到一个基类对象/引用/指针的时候 // 该基类对象/引用/指针是无法访问到派生类的新增成员的 // 只能访问到派生类从基类继承过来的成员 // 在编译器看来,基类对象/引用/指针的类型依然是基类 ======================================================== 把基类指针向下转换为派生类指针 Derived *p = static_cast<Derived *>(pd); p->show(); // 此时调用的是派生类自己的show函数
3. 可以使用派生类对象初始化基类对象(基类的拷贝构造函数)
Derived d; // 实例化一个派生类对象 Base b = d; // 使用派生类对象初始化基类对象 // 创建的是基类,调用的是基类的拷贝构造函数 // Base(const Base &pb); ------> const Base &pb = d; // 基类的引用可以绑定到派生类对象 // 使用派生类中拥有的基类数据去初始化新创建的基类成员 b.show(); // 调用基类的show函数,因为b是一个实实在在的基类对象
4. 可以使用派生类对象给基类对象赋值(基类的拷贝赋值函数)
Derived d; // 实例化一个派生类对象 Base b; // 实例化一个基类对象 b = d; // 使用派生类对象给基类对象赋值 // b.operator=(d) // Base & operator=(const Base &b); // const Base &b = d; // 使用派生类中含有的基类数据给基类对象赋值
在没有虚函数的情况下!!!
当使用派生类对象得到一个基类对象或者基类引用或者基类指针的时候
该基类对象/引用/指针是无法访问到派生类的新增成员的,只能访问到派生类从基类继承过来的成员
在编译器看来,基类对象/引用/指针类型仍然是基类!!!
注意:引用和指针虽然调用的是基类版本的成员函数,但是访问的数据还是派生类中的数据Derived d; // 派生类对象
d.setValue(2, 3, 4);
Base &pd = d; // 基类的引用成为派生类的别名
pd.show(); // 调用了基类自己的show函数
// void show();------>void show(Base * const this)
// pd.show();----->show(&pd)---> Base * const this = &pd;#include <iostream> using namespace std; // 定义基类 class Base { private: int m_a; public: int m_b; // 构造函数 Base(int a = 100, int b = 200, int c = 300) : m_a{a}, m_b{b}, m_c{c} { cout << "Base::constructor" << endl; } // 拷贝构造函数 Base(const Base & b) : m_a{b.m_a}, m_b{b.m_b}, m_c{b.m_c} { cout << "Base::copy-constructor" << endl; } // 赋值运算符重载函数 Base & operator=(const Base &b) { cout << "Base::operator=" << endl; // 防止自赋值 if (this == &b) { return *this; } m_a = b.m_a; m_b = b.m_b; m_c = b.m_c; return *this; } // 析构函数 ~Base() { cout << "Base::de-structor" << endl; } void show() { cout << "Base:" << m_a << "," << m_b << "," << m_c << endl; } int getA() { return m_a; } void setValue(int a, int b, int c) { m_a = a; m_b = b; m_c = c; } protected: int m_c; }; // 定义派生类 class Derived : public Base { public: int m_d = 1024; Derived() { cout << "Derived::constructor" << endl; } ~Derived() { cout << "Derived::de-structor" << endl; } // 此处不是覆盖,而是隐藏!!! void show() { cout << "Derived:" << endl; // cout << this->m_a << endl; 基类的私有成员在派生类中不可以直接访问 cout << getA() << endl; // 间接访问 cout << this->m_b << endl; cout << this->m_c << endl; } }; void fun(Base &pd) { pd.show(); // 调用的还是基类本身的show函数 // 当使用派生类对象得到一个基类对象/引用/指针的时候 // 该基类对象/引用/指针是无法访问到派生类的新增成员的,只能访问到派生类从基类继承过来的成员 // 在编译器看来,基类对象/引用/指针的类型依然是基类 } void fun2(Base *pd) { pd->show(); // 调用的还是基类本身的show函数 // 当使用派生类对象得到一个基类对象/引用/指针的时候 // 该基类对象/引用/指针是无法访问到派生类的新增成员的,只能访问到派生类从基类继承过来的成员 // 在编译器看来,基类对象/引用/指针的类型依然是基类 } void fun3(Base b) { // fun3(d) Base b = d; b.show(); // 肯定调用基类本身的show函数 // 当使用派生类对象得到一个基类对象/引用/指针的时候 // 该基类对象/引用/指针是无法访问到派生类的新增成员的,只能访问到派生类从基类继承过来的成员 // 在编译器看来,基类对象/引用/指针的类型依然是基类 } int main() { // 1.基类的引用可以绑定到派生类对象(基类的引用可以成为派生类的别名) Derived d; // 派生类对象 d.setValue(2, 3, 4); // cout << d.m_d << endl; // YES 派生类中本就拥有m_d Base &pd = d; // 基类的引用成为派生类的别名 // cout << pd.m_d << endl; // ERROR 基类中没有m_d cout << pd.m_b << endl; // 3 pd.show(); // 调用了基类自己的show函数 // 基类引用虽然绑定到了派生类对象,但是在编译器看来,pd的类型依然是基类 cout << "----------------------" << endl; d.setValue(11, 22, 33); pd.show(); d.show(); fun(d); // void fun(Base &pd); // Base &pd = d; cout << "----------------------" << endl; // reinterpret_cast<目标类型>(表达式) Derived &p = reinterpret_cast<Derived &>(pd); p.show(); /* Base::constructor Derived::constructor 3 Base:2,3,4 ---------------------- Base:11,22,33 Derived: 11 22 33 Base:11,22,33 ---------------------- Derived: 11 22 33 Derived::de-structor Base::de-structor */ // =========================================================================== // 2.基类的指针可以指向派生类对象 // Derived d; // 派生类对象 // // cout << d.m_d <<endl; // YES 派生类中本就拥有m_d // Base *pd = &d; // 基类的指针保存派生类的地址 // // cout << pd->m_d << endl; // ERROR.基类中没有m_d // pd->show(); // 调用了基类自己的show函数 // // 基类指针虽然保存派生类的地址,但是在编译器看来,pd的类型依然是基类 // d.setValue(1, 2, 3); // pd->show(); // cout << "----------------------" << endl; // fun2(&d); // void fun(Base *pd); // Base *pd = &d; // cout << "----------------------" << endl; // // 把基类指针向下转换为派生类指针 // Derived *p = static_cast<Derived *>(pd); // p->show(); // 此时调用的是派生类自己的show函数 /* Base::constructor Derived::constructor Base:100,200,300 Base:1,2,3 ---------------------- Base:1,2,3 ---------------------- Derived: 1 2 3 Derived::de-structor Base::de-structor */ // =========================================================================== // 3.可以使用派生类对象初始化基类对象(基类的拷贝构造函数) // Derived d; // 实例化一个派生类对象 // Base b = d; // 使用派生类对象初始化基类对象 // // 创建的是基类,调用的是基类的拷贝构造函数 // // Base(const Base &pb); ------> const Base &pb = d; // // 基类的引用可以绑定到派生类对象 // // 使用派生类中拥有的基类数据去初始化新创建的基类成员 // b.show(); // 调用基类的show函数,因为b是一个实实在在的基类对象 // cout << "----------------------" << endl; // fun3(d); // void fun(Base b); // Base b = d; /* Base::constructor Derived::constructor Base::copy-constructor Base:100,200,300 ---------------------- Base::copy-constructor Base:100,200,300 Base::de-structor Base::de-structor Derived::de-structor Base::de-structor */ // =========================================================================== // 4.可以使用派生类对象给基类对象赋值(基类的拷贝赋值函数) // Derived d; // 实例化一个派生类对象 // Base b; // 实例化一个基类对象 // b = d; // 使用派生类对象给基类对象赋值 // // b.operator=(d) // // Base & operator=(const Base &b); // const Base &b = d; // // 使用派生类中含有的基类数据给基类对象赋值 // d.setValue(1, 2, 3); // d.show(); // b.show(); /* Base::constructor Derived::constructor Base::constructor Base::operator= Derived: 1 2 3 Base:100,200,300 Base::de-structor Derived::de-structor Base::de-structor */ return 0; }
05. 派生类中的成员隐藏
如果在派生类中增加一个与基类同名的成员,当使用派生类对象使用这个同名成员的时候,默认调用的是派生类自己新增加的版本,而不是从基类继承过来的版本,基类的同名成员
被派生类中的同名成员隐藏了
如果需要访问从基类继承过来的同名成员,只需要指定作用域就可以了
如:
d.Base::show();cout << d.m_b << endl; // 访问的是派生类自己的成员
cout << d.Base::m_b << endl; // 通过作用域的方式访问指定的成员
可以认为派生类的作用域是嵌套在基类中的
06. 继承中的构造函数和析构函数
构造函数和析构函数都不能被继承
虽然派生类中含有从基类继承过来的成员,但是有可能没有访问权限(如:基类的私有成员)所以派生类是不能直接初始化基类的这些成员的
在C++中规定,每一个类控制它自己的初始化和销毁过程
也就是说基类的成员由基类自己的构造函数和析构函数分别执行初始化和销毁过程
所以在构造派生类前会调用基类的构造函数以初始化基类对象(基类子对象)
对于构造函数:如果基类有默认的构造函数(不需要提供参数就可以调用的构造函数),则实例化派生类对象时,会自动调用基类的默认构造函数以初始化从基类继承而来的成员
如果基类没有无参构造函数或者希望基类调用其他的构造函数,怎么办?
可以在派生类的构造函数初始化列表中,以初始化列表的方式调用基类指定的构造函数, 以初始化从基类继承而来的成员格式如下:
派生类的构造函数名(参数列表) : 基类构造函数名{参数列表}..... {
派生类自己的构造函数体;
}
如:
Derived(int x, int y, int z) : Base{x, y, z} {
cout << "Derived::constructor" << endl;
}对于拷贝构造函数:
如果派生类中没有显示的声明拷贝构造函数,编译器会自动生成一个拷贝构造函数
自动生成的拷贝构造函数,函数体不为空,执行对象的逐成员赋值(浅拷贝)!!!格式如下:
派生类名(const 派生类名 &oth) : 基类名{oth}..... {
派生类自己的拷贝构造函数体;
}
对于派生类的拷贝构造函数,需要拷贝两部分
a. 基类子对象的数据(从基类继承而来的数据)
b. 派生类自己的成员使用构造函数初始化列表,但是从基类继承而来的那一部分数据应该由基类自己完成初始化动作,数据来自于派生类中含有的基类数据
Derived(const Derived &oth) : Base{oth} // 通过构造函数初始化列表指定调用基类的拷贝构造函数
{
cout << "Derived::copy-constructor" << endl;
// 需要拷贝两部分的内容
// a.基类子对象(由自己的拷贝构造函数完成)
// b.派生类自己新增的成员
m_d = oth.m_d;
}
对于析构函数:当销毁派生类对象时,先调用派生类自己的析构函数释放自己的成员
再调用基类的析构函数释放基类的成员(可以看做是在派生类中的析构函数默认调用了基类的析构函数)
子类对象在创建时,会自动调用父类的构造函数
子类对象在销毁时,会自动调用基类的析构函数
继承的过程:1. 获取基类所有的行为和属性
2. 一定程度上的调整
3. 添加派生类自己的属性
问题:基类中的友元函数能不能被继承? 不能
不能!!!
友元函数不属于基类,只是基类授予它特别的访问权限
基类的赋值运算符重载函数能够被继承吗? 不能C++规定不能继承,因为相较于基类,派生类往往多了一些自己的成员
如果允许赋值运算符重载函数能够被继承(派生类继承了基类的赋值运算符重载函数)
派生类不提供自己的赋值运算符重载函数,就只能调用从基类继承而来的版本
但是基类的版本只能处理基类的成员,所以赋值运算符重载函数不能被继承
如果不提供派生类的赋值运算符重载函数,派生类会自己生成自己的赋值运算符重载函数this->Base::operator=(oth); // 手动调用基类的赋值运算符重载函数
// 其实派生类继承了基类的赋值运算符重载函数
我们所说的不能继承是:派生类在继承了基类的赋值运算符重载函数的同时(operator=),还会生成自己的赋值运算符重载函数(operator=),执行浅拷贝,两个函数的函数名相同,可以认为是派生类的赋值运算符重载函数把基类的赋值运算符给隐藏了
如果派生类没有自己写赋值运算符重载函数,那么编译器就会默认生成赋值运算符重载函数,默认生成的赋值运算符重载函数是浅拷贝格式如下:
派生类名 & operator=(派生类名 &oth) {
// 防止自赋值
if (this == &oth) {
return *this;
}
基类名::operator=(oth);
// 执行派生类资源的浅拷贝
......
}
基类的移动构造函数,移动赋值函数 同 赋值运算符重载函数 都不能被继承
#include <iostream> using namespace std; class Base { private: int m_a; public: int m_b; Base(int a, int b, int c) : m_a(a), m_b(b), m_c(c) { cout << "Base::有参构造函数" << endl; } Base(Base &b) : m_a(b.m_a), m_b(b.m_b), m_c(b.m_c) { cout << "Base::拷贝构造函数" << endl; } ~Base() { cout << "Base::~析构函数" << endl; } // 赋值运算符重载 Base & operator=(Base &oth) { cout << "Base::赋值运算符重载" << endl; // 防止自赋值 if (this == &oth) { return *this; } m_a = oth.m_a; m_b = oth.m_b; m_c = oth.m_c; return *this; } int getA() { return m_a; } protected: int m_c; }; class Derived : public Base { private: int m_d; public: Derived(int a, int b, int c, int d) : Base(a, b, c), m_d(d) { cout << "Derived::有参构造函数" << endl; } Derived(Derived &d) : Base(d), m_d(d.m_d) { cout << "Derived::拷贝构造函数" << endl; } ~Derived() { cout << "Derived::~析构函数" << endl; } // 赋值运算符重载 Derived & operator=(Derived &oth) { // 防止自赋值 if (this == &oth) { return *this; } Base::operator=(oth); cout << "Derived::赋值运算符重载" << endl; m_d = oth.m_d; return *this; } void show() { cout << "m_a = " << getA() << endl; cout << "m_b = " << m_b << endl; cout << "m_c = " << m_c << endl; cout << "m_d = " << m_d << endl; } }; int main() { Derived d{1, 2, 3, 4}; d.show(); cout << "-----------------" << endl; Derived d2 = d; d2.show(); cout << "-----------------" << endl; Derived d3{5, 6, 7, 8}; d3 = d; d3.show(); return 0; }
07 多重继承和虚继承
1. 多重继承(多继承)
是指从多个直接基类中产生派生类的能力,多重继承的派生类继承了所有直接基类的属性和行为
语法规则:class 派生类名称 : 继承方式1 基类1,继承方式2 基类2 ...... {
派生类自己新增的特性;
};
#include <iostream> using namespace std; // 基类1 class A { public: int a; A(int a) : a{a} { cout << "A::构造函数" << endl; // this->a = a; } ~A() { cout << "A::析构函数" << endl; } void show() { cout << "A::show:" << a << endl; } }; // 基类2 class B { public: int a; B(int a) : a{a} { cout << "B::构造函数" << endl; // this->a = a; } ~B() { cout << "B::析构函数" << endl; } void show() { cout << "B::show:" << a << endl; } }; // 派生类,先声明的基类先构造,并且放到前面 class C : public A, public B { public: int a; C(int a) : A{100}, B{200} { cout << "C::构造函数" << endl; this->a = a; } ~C() { cout << "C::析构函数" << endl; } /* void show() { cout << "C::show:" << a << endl; } */ }; int main() { C c{10}; // 实例化一个C类型对象 // c.show(); // 成员的查找在基类中是同时进行的 c.A::show(); // 通过作用域指明函数版本 c.B::show(); // 派生类中拥有所有基类的数据 cout << "A:" << sizeof(A) << endl; // 4 cout << "B:" << sizeof(B) << endl; // 4 cout << "C:" << sizeof(C) << endl; // 12 cout << c.a << endl; // 派生类的成员隐藏了基类的成员 return 0; }
2. 多重继承下的类成员作用域
在只有一个基类的情况下,派生类的作用域是嵌套在基类作用域之中,名字的查找过程是沿着继承体系自下向上进行,直到找到所需要的名字,同时,派生类的名字将隐藏基类的同名成员!!!在多重继承的情况下,名字的查找过程是在所有直接基类中同时进行的,如果要查找的名字在多个直接基类中都可以被找到,则对该名字的访问具有二义性,编译器报错
解决方法:
只需要在使用某一个具有二义性的名字时,明确的指出它的作用域即可
c.A::show();// 通过作用域指明函数版本
c.B::show();
C c{0};
cout << c.A::a << endl;
cout << c.B::a << endl;
在普通多重继承的情况下,在构造派生类对象的时候,会先调用基类的构造函数
有多个基类的时候,是按照继承声明顺序调用的class A {}; class B : public A, public A {}; // error: duplicate base type ‘A’ invalid =========================================================================== "菱形继承"/"钻石继承" class A{}; class B : public A {}; class C : public A {}; class D : public B, public C {};
虽然在派生类中同一个基类只能出现一次,但是在某些特殊情况下,派生类可能会多次继承同一个基类
最典型的情况就是"菱形继承"即一个派生类D,有两个直接基类B和C,然后两个直接基类又继承自同一个间接基类A,这种情况下,间接基类A的成员会在底层派生类D中出现多份副本
// cout << d.a << endl; // 二义性
cout << d.B::a << endl;
cout << d.C::a << endl;
D
{
<B> = {
<A> = {a = 100},
b = 300
},
<C> = {
<A> = {a = 200},
c = 400
},
d = 0
}#include <iostream> using namespace std; // 基类 class A { public: int a; A(int a): a{a} { cout << "A::构造函数" << endl; } ~A() { cout << "A::析构函数" << endl; } void show() { cout << "A::show:" << a << endl; } }; class B: public A { public: int b; B(int b): A{100} { // 指定基类的构造函数 cout << "B::构造函数" << endl; this->b = b; } ~B() { cout << "B::析构函数" << endl; } void show() { cout << "B::show:" << b << endl; } }; class C: public A { public: int c; C(int c): A{200} { cout << "C::构造函数" << endl; this->c = c; } ~C() { cout << "C::析构函数" << endl; } void show() { cout << "C::show:" << c << endl; } }; class D: public B, public C { public: int d; D(int d): B{300}, C{400} { cout << "D::构造函数" << endl; this->d = d; } ~D() { cout << "D::析构函数" << endl; } void show() { cout << "D::show:" << d <<endl; } }; int main() { cout << "A:" << sizeof(A) << endl; // 4 cout << "B:" << sizeof(B) << endl; // 8 cout << "C:" << sizeof(C) << endl; // 8 cout << "D:" << sizeof(D) << endl; // 20 D d{0}; d.show(); // cout << d.a << endl; // 存在二义性 cout << d.B::a << endl; cout << d.C::a << endl; return 0; }
在C++中,建议使用虚继承来解决上面的问题!
3. 虚继承虚继承的目的是让某一个类做出声明,承诺愿意共享它的基类,被共享的那个基类称为虚基类
不论虚基类在继承体系中出现了多少次,在底层的派生类中都只包含唯一的一份共享的虚基类对象
格式:
class 派生类名 : virtual 继承方式 基类名称 // 派生类承诺可以和其他类共享基类
{
....
};虚继承不影响虚继承的派生类本身(B、C)的使用(但是会在B、C里面添加一个虚基类指针(vptr),记录虚基类的偏移量),只会影响从指定的虚基类的派生类中进一步派生出来的D,为了达到共享的目的,派生类D的所有直接基类子对象(B、C)中都不再包含虚基类子对象(A),而是把虚基类子对象放到了所有成员变量的最后面,然后直接基类子对象(B、C)只需要记录指定虚基类子对象偏移当前基类子对象多少个字节就可以了!!!
虚基类子对象(A)的偏移量是放在一个"虚基类表"中,每一个直接基类子对象(B、C)中会插入一个虚基类表指针,指向虚基类表,以找到这个虚基类子对象(A)对于当前基类子对象(B、C)的偏移量
这个虚基类表指针是直接基类的一部分(B、C),会被它的派生类D继承,所以我们在计算底层派生类大小时需要考虑虚基类表指针大小和字节对齐
4. 构造函数和虚继承
如果按照普通的规则去初始化,即每一个派生类(B、C)调用它直接基类(A)的构造函数,以初始化从基类继承过来的成员(A),在没有虚继承时,是没有问题的
在虚继承时,B和C是共享A这个子对象的,那么如果还交给B和C去初始化,根部的基类成员会被重复初始化
所以,C++规定,在虚继承体系中,虚基类的成员由最底层的派生类(D)初始化也就是说,当含有虚基类的派生类(D)实例化对象时,先调用虚基类的构造函数(由D调用),再按照直接基类在派生类列表中出现的顺序,依次调用直接基类的构造函数
析构函数的调用顺序与构造函数的调用顺序相反!!!
#include <iostream> using namespace std; // 基类 class A { public: int a; A(int a): a{a} { cout << "A::构造函数" << endl; } ~A() { cout << "A::析构函数" << endl; } void show() { cout << "A::show:" << a << endl; } }; class B: virtual public A { // 派生类承诺可以和其他类共享基类 public: int b; B(int b): A{100} { // 指定基类的构造函数 cout << "B::构造函数" << endl; this->b = b; } ~B() { cout << "B::析构函数" << endl; } void show() { cout << "B::show:" << b << endl; } }; class C: virtual public A { // 派生类承诺可以和其他类共享基类 public: int c; C(int c): A{200} { cout << "C::构造函数" << endl; this->c = c; } ~C() { cout << "C::析构函数" << endl; } void show() { cout << "C::show:" << c << endl; } }; class D: public B, public C { public: int d; D(int d): B{300}, C{400}, A{10} { // 指定虚基类A的构造函数 cout << "D::构造函数" << endl; this->d = d; } ~D() { cout << "D::析构函数" << endl; } void show() { cout << "D::show:" << d << endl; } }; int main() { cout << "A:" << sizeof(A) << endl; // 4 cout << "B:" << sizeof(B) << endl; // 16 cout << "C:" << sizeof(C) << endl; // 16 cout << "D:" << sizeof(D) << endl; // 40 D d{0}; d.show(); cout << d.a << endl; // 不会存在二义性 return 0; }
08 练习
1. 实现一个带乘法功能的字符串类型
2. 定义一个形状类,以它作为基类,派生出一个圆类,能够计算圆的面积,再从圆派生出一个圆柱类也能够计算它的表面积