【C++】继承 — 子类默认成员函数、虚继承对象模型 - 详解(下篇)
创始人
2024-03-30 20:17:55

文章目录

  • 📖 前言
  • 1. 派生类的默认成员函数
    • 1.1 子类默认生成的成员函数:
    • 1.2 子类显示写的成员函数:
      • 1.2 - 1 构造函数
      • 1.2 - 2 拷贝构造
      • 1.2 - 3 赋值重载
      • 1.2 - 4 析构函数
  • 2. 如何设计一个不能被继承的类
  • 3. 友元和继承
  • 4. 继承与静态成员
  • 5. 多继承和菱形继承
    • 5.1 菱形继承和虚继承:
    • 5.2 菱形虚拟继承的底层 - 对象模型:
      • 5.2 - 1 菱形继承对象模型
      • 5.2 - 2 菱形虚拟继承对象模型
  • 6. 总结

📖 前言

上篇我们讲了继承的基本语法和使用规范,接下来我们将继续讲解继承的深层次的内容。

前情回顾: 👉 继承 — 上篇回顾


1. 派生类的默认成员函数

在我们之前学类和对象中,已经清楚了基类中默认成员函数的规则。

  • 类和对象默认成员函数复习:👉 传送门

下面我们就要学习派生类中默认成员函数的规则。


1.1 子类默认生成的成员函数:

子类默认生成的成员函数原则:

  1. 调用父类构造函数初始化继承自父类成员
  2. 自己再初始化自己的成员 – 规则参考普通类
  3. 析构、拷贝构造、赋值重载也类似
class Person
{
public:Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}
protected:string _name; 
};class Student : public Person
{
public:
protected:int _num;string _address;
};int main()
{Student s;return 0;
}

在这里插入图片描述

父类调用父类的构造函数处理,子类的内置类型不处理,自定义类型调用该自定义类型的默认构造函数处理。

在这里插入图片描述

1.2 子类显示写的成员函数:

1.2 - 1 构造函数

父类的构造函数完成了父类的那一部分的构造。

(1) 首先不能以下面的方式写:
在这里插入图片描述
在这里插入图片描述
(2) 可以这样写,不初始化在函数体内赋值:
在这里插入图片描述
但是看一下运行结果,还是调用了父类的构造函数,但是我们并没有去显示调用,这是怎么回事?

在这里插入图片描述

  • 这里是在初始化列表中调用的
  • 可以理解为子类把父类那一部分拿下来当成自定义成员
  • 不显示写该自定义类型的构造函,就会调用父类的默认构造

(3) 并且父类的构造函数没有提供全缺省是调不动的:
在这里插入图片描述
在这里插入图片描述
C++的原则是父类的一定要调用父类的构造函数初始化。

(4) 正确写法:

在这里插入图片描述
代码如下:

class Person
{
public:Person(const char* name): _name(name){cout << "Person()" << endl;}~Person(){cout << "~Person()" << endl;}protected:string _name; 
};class Student : public Person
{
public:Student(const char* name = "", int num = 0)//像初始化一个匿名对象一样去写:Person(name),_num(num){_name = name;}
protected:int _num;string _address;
};int main()
{Student s;return 0;
}

补充:
在这里插入图片描述
解释:

  • 初始化列表出现的顺序并不是实际执行的顺序
  • 真正初始化的顺序则是按照声明的顺序
  • 声明中会认为父类在前,子类在后

1.2 - 2 拷贝构造

父类的拷贝构造完成了父类的那一部分的拷贝。

(1) 子类默认生成的拷贝构造:

在这里插入图片描述

在这里插入图片描述

对于子类剩下的那一部分成员,按照之前的规则处理,对于内置类型完成值拷贝,自定义类型去调用该自定义类型的拷贝构造。

(2) 和构造函数一样,同样不支持这样初始化:
在这里插入图片描述
(3) 正确写法:

在这里插入图片描述
问题:

这里要将Person的对象传过去,如何将子类当中父类继承的那一部分拿出来,传过去来拷贝构造呢?

  • 切片 – 子类的对象传给父类对象的引用

1.2 - 3 赋值重载

(1) 父类函数的隐藏导致的栈溢出:
在这里插入图片描述

int main()
{Student s1("李四", 1);Student s2(s1);Student s3("王五", 2);s2 = s3;return 0;
}

栈溢出(爆栈) !!

在这里插入图片描述
我们来看一下堆栈调用:

在这里插入图片描述
从上图可见,一直在递归调用赋值重载,究竟问题何在?

  • 原来子类中的赋值重载和父类中的赋值重载函数名相同
  • 子类和父类中的这两个函数构成了 – 隐藏
  • 我们只需要执行类域,就可以解决这个问题

(3) 正确写法:
在这里插入图片描述

同时赋值重载中,子类调用父类赋值重载时,隐藏的this指针传过去也会被切片,和Student对象一样都要被切片,两个切片。

1.2 - 4 析构函数

  • 按照我们之前的理解,应该显示去调用父类的析构再去完成自己的析构

在这里插入图片描述

int main()
{Student s1("李四", 1);Student s2(s1);Student s3("王五", 2);s2 = s3;return 0;
}

这里编译会报错。

这里有隐藏的很深的问题:

  • 父子类的析构函数构成隐藏关系
  • 原因:下一节多态的需要,析构函数名统一会被处理成destructor( )
  • 所以在无形当中就构成了隐藏

我们显示调用一下:
在这里插入图片描述
看一下运行结果:

在这里插入图片描述

这里我们发现好像多调用了多次父类析构,原因是什么呢?

补充:

为了保证析构顺序,子类的析构完成后,会直接调用父类的

  • 为了保证析构顺序,先子后父
  • 子类析构函数完成后会自动调用父类析构函数,所以不需要我们显示调用
  • ~ 是按位取反,前面加一个~,是和构造函数呼应起来的

析构时要保证先子后父的原因是:

  • 和对象的存储有关系
  • 例如:栈里面的存储对象是,先定义的先初始化,先定义的后析构
  • 如果自己显示调用就很有可能会先父后子的调用析构函数

正确写法:
在这里插入图片描述
看一下运行结果:

在这里插入图片描述


2. 如何设计一个不能被继承的类

  1. 构造函数私有
class A
{
private:A(){}
};class B : public A
{};int main()
{B b;return 0;
}
  • 父类A的构造函数私有化以后,B就无法构造对象
  • 因为规定了子类的成员必须调用父类的构造函数初始化

这时候就还有一个问题A类想单独构造对象也不行了

解决办法:
在这里插入图片描述
这时又有一个问题 —— 先有鸡还是先有蛋的问题:

  • 调用成员函数需要对象,对象创建需要调用成员函数,调用成员函数需要对象…

解决办法:

用一个静态成员函数就能很好的解决问题:

在这里插入图片描述
在这里插入图片描述

3. 友元和继承

友元不能被继承

//友元关系不能被继承 -- 父类的友元不会继承到子类当中
class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; //姓名
};class Student : public Person
{//friend void Display(const Person& p, const Student& s);
protected:int _stuNum; //学号
};void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}int main()
{Person p;Student s;Display(p, s);return 0;
}

在这里插入图片描述

  • 想两个都访问时,只要既变成父类的友元也变成子类的友元就可以了。
  • 不能说是父类的友元你就是子类的友元了。

4. 继承与静态成员

问题:

比如说父类有一个静态成员,那子类继承之后,子类会增加一个静态成员还是和父类共享一个静态成员呢?

答案是共享同一个。

//继承与静态成员
class Person
{
public:Person() { ++_count; }
protected:string _name;       //姓名
public:static int _count;  //统计人的个数。
};int Person::_count = 0;class Student : public Person
{
protected:int _stuNum;        //学号
};class Graduate : public Student
{
protected:string _seminarCourse;  //研究科目
};int main()
{Student s1;Student s2;Student s3;Graduate s4;Person s;//用任何一个类都可以访问 -- 用类域或者是对象都能访问cout << "人数 :" << Person::_count << endl;cout << "人数 :" << Student::_count << endl;cout << "人数 :" << s4._count << endl;//并且地址都是一样的cout << "人数 :" << &Person::_count << endl;cout << "人数 :" << &Student::_count << endl;cout << "人数 :" << &s4._count << endl;return 0;
}

在这里插入图片描述
所以,父类有的静态成员继承下来都是同一个


5. 多继承和菱形继承

5.1 菱形继承和虚继承:

单继承:一个子类只有一个直接父类时称这个继承关系为单继承
在这里插入图片描述
多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承
在这里插入图片描述

早期多继承没什么问题,直到菱形继承的出现。

菱形继承:菱形继承是多继承的一种特殊情况。
在这里插入图片描述
菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。

在Assistant的对象中Person成员会有两份。
在这里插入图片描述
见如下代码:

class Person
{
public:string _name; int _a[10000];
};class Student : public Person
{
protected:int _num;	  
};class Teacher : public Person
{
protected:int _id;	 
};class Assistant : public Student, public Teacher
{
protected:string _major; 
};int main()
{Assistant a;//二义性//a._name = "peter";//通过指定作用域来访问a.Student::_name = "xxx";a.Teacher::_name = "yyy";cout << sizeof(a) << endl;return 0;
}
  • 虽然可以通过指定作用域来访问来解决二义性的问题,但是数据冗余还没有得到解决。
  • 数据冗余带来的问题就是空间的浪费
  • 当父类中的成员变量很大的时候

前人栽树后人乘凉,正是因为多继承会导致很多麻烦,所以java中直接就取消了多继承。

介绍一个新的关键字:

  • virtual – 虚

只需要在菱形继承的腰部加上虚继承,数据冗余的问题就解决了。

在这里插入图片描述
统称:菱形虚拟继承

5.2 菱形虚拟继承的底层 - 对象模型:

Vs的监视窗口在复杂的情况下被处理过,看到的就不准了,此时就需要我们看内存窗口了。

对象模型: 就是其在内存当中到底如何存储

5.2 - 1 菱形继承对象模型

先来代码:

class A
{
public:int _a;//static int _a;
};//int A::_a = 0;class B : public A
{
public:int _b;
};class C : public A
{
public:int _c;
};class D : public B, public C
{
public:int _d;
};int main()
{D d;//d._a = 0;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
  • B 和 C 分别继承 A,D继承了 B 和 C

菱形继承示意图:
在这里插入图片描述
在这里插入图片描述
从内存中可以看出来,数据在内存中是挨个挨个放的,先继承的就在前面。

5.2 - 2 菱形虚拟继承对象模型

菱形虚拟继承解决了数据冗余和二义性的问题。

先来代码:

class A
{
public:int _a;
};class B : virtual public A
{
public:int _b;
};class C : virtual public A
{
public:int _c;
};class D : public B, public C
{
public:int _d;
};int main()
{D d;d._a = 0;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

菱形虚拟继承示意图:
在这里插入图片描述

在这里插入图片描述我们此时发现_a存储到最下面的位置去了

  • 这时A并没有放在B中,也没有放在C中
  • 因为B和C中都有A,干脆直接将A放在一个公共的区域

对象模型和上述模型变了:
在这里插入图片描述
菱形虚拟继承调整了对象的模型。

  • 我们发现B和C对象的开头都存了一个指针, 这种对象模型是省了四个字节(_a),却又增加了两个指针(八个字节),反而变大了四个字节。
  • 但是如果A很大的情况下,剩下来的空间和这两个指针(八个字节)相比
  • 整体空间是节省了不少的空间了

不懂就问,新增的两个指针是用来干嘛的呢?真的是指针?会不会是随机值?

下面我们就来探索一下:

  • 自习观察一下,首先排除随机值的可能
  • 因为它们很相似,并且这两个数值相差不大 —— 相差八个字节
  • 这里可以初步确定B和C开头存的应该是指针

接下来我们拿着这指针去看看其指向的空间中的数据是什么:
在这里插入图片描述

  • 经过探索我们发现通过B和C头上的指针找到的数据都是空,值都是0
  • 但是它们下面一个地址的数据却另有玄机

其实它们都是存着一个叫偏移量的东西:

  • 存的都是自己距离A对象位置的偏移量
  • 分别是B和C距离A的偏移量,20 和 12

为什么要搞这个偏移量呢?

场景一:

  • 在赋值转换 —— 切片的时候就能用得到
  • 假设 D d;B b;b = d;此时就要切片
  • 切片切割的时候,能找到_b,但是要通过偏移量计算出A的位置

场景二:

  • B * ptrb = &d; 这里也是切片,ptrb->_a = 1;
  • B的指针能找到_b,但是找_a是要通过偏移量来算出A的位置

菱形虚拟继承的缺点:

  • 对于编译器和人们的理解都变复杂了
  • 虽然将数据冗余和二义性一概解决了,但是付出了很大的代价 — 多了两层间接
  • 代价就是这个存储模型,该模型也一定程度影响了访问数据的效率

为什么偏移量存储在第二个位置,而不是存在第一个位置:

  • 第一个位置是预留的,可能其他地方要用

模型的优点:

因为不同的编译的设计的不同,A对象存储的位置也会不一样,但是只要有指针去找偏移量,再通过偏移量去找A就能找到,这是通用的方法,统一模型

  • 这个表也叫做 —— 虚基表
  • A叫做虚基类
  • 该指针叫做虚基表指针
    在这里插入图片描述

刨根问底的问题:

  • C++有多继承:
  • 多继承产生的问题 ——》菱形继承;
  • 菱形继承产生的问题 ——》数据冗余和二义性;
  • 数据冗余二义性如何解决 ——》菱形虚拟继承(太复杂了);
  • 菱形虚拟继承是如何解决的 ——》上述菱形虚拟继承对象模型;

下面的成员该如何访问:

在这里插入图片描述

  • _b肯定是在上述指针指向的空间里面
  • _a该如何访问?
  • 如果说B* 类型的指针指向的是d对象,那么就是按照上述的通过虚表指针找偏移量,再通过偏移量找A对象。
  • 如果说B* 类型的指针指向的是b对象,神奇的事情来了,它还是按照虚继承的对象模型来找。

B对象的模型也被改了~

B对象模型:
在这里插入图片描述
原因:

  • ptr1和ptr2不知道自己指向是b对象还是d对象的
  • 在d对象里面,A成员跟B的距离远: 20
  • 在b对象里面,A成员跟B的距离进: 8
  • ptr1和ptr2无法知道也不关心自己指向的是谁!
  • 都使用同样的方式去找A成员,先找到虚基表中偏移量,然后计算A的位置

根本原因是:切片的情况下,距离A的距离是不一样的


6. 总结

  • 很少有人设计菱形继承,但是C++标准库中就有菱形继承,IO流的类就是菱形继承。
  • 实际当中可以设计多继承,但是尽量不要设计菱形继承,更不要设计菱形虚拟继承,太复杂了!还有一定程度的效率损失。

相关内容

热门资讯

埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...
世界上最漂亮的人 世界上最漂亮... 此前在某网上,选出了全球265万颜值姣好的女性。从这些数量庞大的女性群体中,人们投票选出了心目中最美...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
长白山自助游攻略 吉林长白山游... 昨天介绍了西坡的景点详细请看链接:一个人的旅行,据说能看到长白山天池全凭运气,您的运气如何?今日介绍...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
应用未安装解决办法 平板应用未... ---IT小技术,每天Get一个小技能!一、前言描述苹果IPad2居然不能安装怎么办?与此IPad不...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
脚上的穴位图 脚面经络图对应的... 人体穴位作用图解大全更清晰直观的标注了各个人体穴位的作用,包括头部穴位图、胸部穴位图、背部穴位图、胳...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
埃菲尔铁塔在哪 中国仿建埃菲尔... 2019年4月26日,广西南宁市,街头惊现一座巨型山寨版埃菲尔铁塔,高约20米,白色塔身,造型逼真,...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...
北京的名胜古迹 北京最著名的景... 北京从元代开始,逐渐走上帝国首都的道路,先是成为大辽朝五大首都之一的南京城,随着金灭辽,金代从海陵王...