13. C++继承 | 详解 | 虚拟继承及底层实现

目录

1.定义

1.1继承的概念

1.2 继承的定义

2. 对象赋值转换

3. 继承中的作用域

a. 隐藏/重定义 (Hiding/Redefinition)

b. 重载 (Overloading)

c. 重写/覆盖 (Overriding)

d. 编译报错 (Compilation Error)

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

构造

拷贝构造

运算符重载

析构

5. 继承与友元

6. 继承与静态成员

7.菱形继承与虚拟继承

难点

虚拟继承

大端存放(Big-Endian)

小端存放(Little-Endian)

8. 继承反思

9.常见问题answer

1. 什么是菱形继承?菱形继承的问题是什么?

菱形继承(Diamond Inheritance)

菱形继承的问题

2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的问题?

菱形虚拟继承(Virtual Inheritance)

解决的数据冗余和二义性的问题

3. 继承和组合的区别?什么时候用继承?什么时候用组合?

继承(Inheritance)和组合(Composition)

继承

组合

什么时候用继承?什么时候用组合?


1.定义

1.1继承的概念

继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用

我们可以以下面录入学校学生和教职工信息为例

对于公共信息,每次都要初始化一遍吗?

写一个 person 来存储公共信息,student,teacher...继承 person 即可

// 定义Person基类
class Person {
public:// 构造函数Person(const std::string& name, int age) : _name(name), _age(age) {}// 打印个人信息void Print() const {std::cout << "_name: " << _name << std::endl;std::cout << "_age: " << _age << std::endl;}
protected:std::string _name;int _age;
};// 继承自Person的Student类
class Student : public Person {
public:// 构造函数Student(const std::string& name, int age, int stuid): Person(name, age), _stuid(stuid) {}// 打印学生信息void PrintStudentInfo() const {Print();std::cout << "_stuid: " << _stuid << std::endl;}
private:int _stuid;
};// 继承自Person的Teacher类
class Teacher : public Person {
public:// 构造函数Teacher(const std::string& name, int age, int jobid): Person(name, age), _jobid(jobid) {}// 打印教师信息void PrintTeacherInfo() const {Print();std::cout << "_jobid: " << _jobid << std::endl;}
private:int _jobid;
};// 主函数
int main() {Student s("张三", 18, 12345);Teacher t("李四", 30, 67890);// 打印学生信息s.PrintStudentInfo();// 打印教师信息t.PrintTeacherInfo();return 0;
}

这样一个对象就有两份数据了,一份是自己的,一份是父类的

继承:复用 的好处?

简化代码

1.2 继承的定义

Person是父类,也称作基类。Student是子类,也称作派生类。

访问方式

  1. 基类的私有成员在子类都是不可见。基类的其他成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected >private。

   2.不可见,语法上限制访问(类里面和外面都不能用),private 是类外面不能使用,类里面可以

基类的 private 在派生类中不可见,即(基类) 父类的私有成员,子类无论如何都用不了

#include <iostream>
#include <string>class Person {
protected:std::string _name = "zhangsan";int _age = 18;
public:void Print() const {std::cout << "_name: " << _name << std::endl;std::cout << "_age: " << _age << std::endl;}
};class Student : public Person {
public:void Func() const {std::cout << "name: " << _name << std::endl;std::cout << "age: " << _age << std::endl;}
protected:int _stuid;
};int main() {Student s;// 测试Student的Func()方法s.Func();// 测试继承自Person的Print()方法s.Print();return 0;
}

所以父类中不想被子类使用的部分,就可以设置为 private

  1. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过显示的写出继承方式。
  2. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强。

2. 对象赋值转换

派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用(子给父)。这里有个形象的说法叫切片 或者切割。寓意把派生类中父类那部分切来赋值过去。

测试:

#include <iostream>
#include <string>using namespace std;class Person {
protected:string _name; // 姓名string _sex;  // 性别
public:int _age = 20;     // 年龄// 构造函数Person() = default;Person(const string &name, const string &sex, int age): _name(name), _sex(sex), _age(age) {}// 虚析构函数,确保正确调用派生类的析构函数virtual ~Person() = default;// 显示信息的方法virtual void display() const {cout << "Name: " << _name << ", Sex: " << _sex << ", Age: " << _age << endl;}
};class Student : public Person {
public:int _No; // 学号// 构造函数Student() = default;Student(const string &name, const string &sex, int age, int No): Person(name, sex, age), _No(No) {}// 显示信息的方法void display() const override {cout << "Name: " << _name << ", Sex: " << _sex << ", Age: " << _age << ", No: " << _No << endl;}
};int main() {Person p("Alice", "Female", 30);Student s("Bob", "Male", 22, 1001);// 派生类对象赋值给基类对象p = s;// 显示基类对象的信息p.display(); // 只会显示基类的成员信息// 我们知道d赋值给i,会产生临时变量double d = 1.1;int i = static_cast<int>(d); // 显式类型转换cout << "Double value: " << d << ", Integer value: " << i << endl;return 0;
}

运行:

基类对象不能赋值给派生类对象。

student s;
//向上兼容//	Person p1 = s;
//	Person& rp = s;
//	rp._name = "张三";
//
//	Person* ptrp = &s;
//	ptrp->_name = "李四";

切割没有产生中间变量

赋值兼容转换(切割/切片)

子可以给给父,向上转换都是可以的(缩小),生成了别名

3. 继承中的作用域

遵循规则:

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 就近原则 //先在子类当中找

子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏
也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)

  1. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏
  2. 注意在实际中在继承体系里面最好不要定义同名的成员。//要注意避免

猜猜下面代码身份证号打印的是 111 还是 999?

// Student的_num和Person的_num构成隐藏关系,可以看出这样代码虽然能跑,但是非常容易混淆
#include <iostream>
#include <string>using namespace std;class Person {
protected:string _name = "张三"; // 姓名int _num = 111;        // 身份证号
};class Student : public Person {
public:void Print() const {cout << "姓名: " << _name << endl; // 访问基类的_namecout << "身份证号: " <<_num << endl; // 明确访问基类的_num}
protected:int _num = 999; // 身份证号
};int main() {Student s;s.Print();return 0;
}

会发现是就近子类当中的 999

这个地方有非常多的考点,我们不妨来看一下下面的例题

例题 1:

两个fun构成什么关系?

a、隐藏/重定义 b、重载 c、重写/覆盖 d、编译报错

class A
{
public:void fun(){cout << "A::func()" << endl;}
};
class B : public A
{
public:void fun(int i)//类型不相同了,是不是感觉有点像重载{cout << "B::func(int i)->" << i << endl;}
};

答案:a (父子类域中,成员函数名相同就构成隐藏)

这里一定不能和重载搞混淆了,重载要在同一作用域
上面是构成隐藏,成员函数满足函数名相同就构成隐藏!

以下是对隐藏/重定义、重载、重写/覆盖以及编译报错的解释和示例:

a. 隐藏/重定义 (Hiding/Redefinition)

隐藏/重定义是指在派生类中定义一个与基类成员同名的新成员。这样会隐藏基类的成员,使其在派生类对象中不可见。隐藏可以发生在数据成员、成员函数和类型别名等方面。

示例:

#include <iostream>class Base {
public:void func() {std::cout << "Base func()" << std::endl;}
};class Derived : public Base {
public:void func() { // 隐藏基类的 func()std::cout << "Derived func()" << std::endl;}
};int main() {Derived d;d.func(); // 调用的是 Derived::func()d.Base::func(); // 明确调用 Base::func()return 0;
}

b. 重载 (Overloading)

重载是指在同一个类中定义多个同名的函数,但这些函数具有不同的参数列表(参数个数或类型不同)。重载不依赖于继承关系。

示例:

#include <iostream>class Example {
public:void func(int x) {std::cout << "func(int x): " << x << std::endl;}void func(double y) {std::cout << "func(double y): " << y << std::endl;}void func(int x, double y) {std::cout << "func(int x, double y): " << x << ", " << y << std::endl;}
};int main() {Example ex;ex.func(10);ex.func(3.14);ex.func(10, 3.14);return 0;
}

c. 重写/覆盖 (Overriding)

重写/覆盖是指在派生类中重新定义基类中已经存在的虚函数。重写函数的签名必须与基类中被覆盖的虚函数的签名完全一致。

示例:

#include <iostream>class Base {
public:virtual void func() { // 虚函数std::cout << "Base func()" << std::endl;}
};class Derived : public Base {
public:void func() override { // 覆盖基类的虚函数std::cout << "Derived func()" << std::endl;}
};int main() {Base* b = new Derived();b->func(); // 调用的是 Derived::func()delete b;return 0;
}

d. 编译报错 (Compilation Error)

编译报错是指代码在编译阶段出现的错误,通常是由于语法错误、类型不匹配或其他规则违背。

示例:

#include <iostream>class Example {
public:void func(int x) {std::cout << "func(int x): " << x << std::endl;}
};int main() {Example ex;// ex.func("Hello"); // 编译报错,类型不匹配return 0;
}

在这个示例中,ex.func("Hello") 会导致编译报错,因为 func 期望一个 int 类型的参数,而不是 const char* 类型的字符串。

通过以上示例,可以更清楚地理解隐藏/重定义、重载、重写/覆盖以及编译报错的概念及其在 C++ 中的应用。


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

这里为什么只有派生类的默认成员函数,而没有基类的默认成员函数呢?
这是因为基类的默认成员函数和其他类无任何差别。

有六个默认成员函数,分别是构造,析构,拷贝构造,赋值重载,取地址,const取地址,我们一一来看。

遵循创造:先父后子

       消灭:先子后父

构造

派生类的构造函数必须调用父类的构造函数初始化基类的那一部分成员。

自己写构造,不能在派生类的构造函数中直接对父类成员初始化,父类成员的初始化只能调用父类的构造函数完成初始化。

构造:先父后子

class Person
{
public:Person(const char* name="zhangsan"): _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; //学号
};int main()
{Student s;return 0;
}

测试:

修改:

派生类相比于普通类的构造函数,多了一步对基类成员的处理

会默认构造,但不建议,还是显示的调用构造更好

拷贝构造

拷贝构造函数是构造函数的重载,所以它们的特性几乎是一样的。

#include <iostream>
#include <string>using namespace std;class Person {
public:Person() {} // 默认构造函数// 拷贝构造函数Person(const Person& p): _name(p._name) {cout << "Person(const Person& p)" << endl;}protected:string _name;
};class Student : public Person {
public:Student() {} // 默认构造函数// 拷贝构造函数Student(const Student& s): _num(s._num),Person(s) { // 调用基类的拷贝构造函数cout << "Student(const Student& s)" << endl;}protected:int _num; // 学号
};void checkCopyConstruction() {Student originalStudent;Student copiedStudent(originalStudent); // 这里会调用拷贝构造函数
}int main() {checkCopyConstruction(); // 调用函数检查拷贝构造函数的行为return 0;
}

运行

  • 派生类的拷贝构造函数先调用基类的拷贝构造函数。
  • 派生类的拷贝构造函数不能直接处理基类的成员,必须显示调用基类的拷贝构造函数。

运算符重载

就近原则:基类和派生类的运算符重载函数构造了隐藏/重定义
父亲干父亲的活,孩子干孩子的活

#include <iostream>
#include <string>using namespace std;class Person {
public:Person() = default;Person(const Person& p): _name(p._name) {}Person& operator=(const Person& p) {cout << "Person& operator==(const Person& p)" << endl;if (this != &p) {_name = p._name;}return *this;}protected:string _name;
};class Student : public Person {
public:Student() = default;Student(const Student& s): Person(s), _num(s._num) {}//基类部分调用基类来构造Student& operator=(const Student& s) {cout << "Student& operator=(const Student& s)" << endl;if (this != &s) {Person::operator=(s);//!指定一下,父类部分调用父类来实现_num = s._num;}return *this;}// Setter methods to initialize the objectvoid setName(const string& name) {_name = name;}void setNum(int num) {_num = num;}protected:int _num; // 学号
};void checkAssignment() {Student student1;student1.setName("John Doe");student1.setNum(12345);Student student2;student2 = student1; // 这里会调用赋值运算符// 输出应显示两个赋值运算符都被调用过
}int main() {checkAssignment();return 0;
}

运行:

注意子类重载时的调用,要指定一下 Person::operator=(s)

析构

需要在子类中显示构造基类的析构函数吗

#include <iostream>
#include <string>using namespace std;class Person {
public:~Person() {cout << "~Person()" << endl;}
protected:string _name;
};class Student : public Person {
public:~Student() {cout << "~Student()" << endl;}
protected:int _num; // 学号
};int main() {Person a;Student b;// 等待用户按键后退出,以便观察析构函数的调用cout << "Press any key to exit..." << endl;cin.get();return 0;
}

不需要

按照之前几个默认成员函数的做法,在派生类的析构函数中显示调用基类的析构函数,但是发现基类的析构函数一共调用了两次。这显然是不行的,一块动态空间只能被释放一次。

由于后面多态的原因(具体后面讲),析构函数的函数名被特殊处理了,统一处理成destructor

显示调用父类析构,无法保证析构的先子后父,所以子类析构函数完成就自动调用,默认调用父类析构,这样就保证了先子后父

总结:

先的构造后析构的原理,先构父,但先析子

派生类相比于普通类的四类默认成员函数,多了一步对基类成员的处理,而且只能通过基类的默认成员函数去处理,不能由派生类自行处理,重载则遵循就近原则

5. 继承与友元

友元关系不能继承

那些叔叔是爸爸的朋友,但不是我的朋友

测试:

#include <iostream>
#include <string>using namespace std;
class Student;
class Person {
public:Person(const string& name) : _name(name) {}friend void Display(const Person& p, const Student& s);protected:string _name; // 姓名
};class Student : public Person {
public:Student(const string& name, int stuNum) : Person(name), _stuNum(stuNum) {}// 在类体中再次声明Display为友元friend void Display(const Person& p, const Student& s);protected:int _stuNum; // 学号
};void Display(const Person& p, const Student& s) {cout << "Person name: " << p._name << endl;cout << "Student name: " << s._name << endl;cout << "Student number: " << s._stuNum << endl;
}int main() {Person person("John Doe");Student student("Jane Doe", 12345);Display(person, student);return 0;
}

一个小细节:

定义一个Display函数,它是基类的友元函数,可以访问基类内部的保护成员。

  • 由于基类中的友元声明中包含派生类,但是编译器只会向上寻找,所以必须在友元声明之前加上派生类的声明。
  • 否则会报Student未声明的错误。

  • 若想让基类中的友元也成为派生类中的友元,需要在派生类中也进行友元声明

不声明就会报错:

注意: 一般不建议使用友元,因为它会破坏类的封装。


6. 继承与静态成员

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例。

继承的是使用权,在派生类中,不用再单独拷贝

#include <iostream>using namespace std;class Person {
public:int _Pnum = 1;static int _count; // 静态成员变量的声明
};int Person::_count = 0; // 静态成员变量的定义和初始化class Student : public Person {
public:int _Snum = 1; // 学号
};int main() {Person p;Student s1;Student s2;cout << "Initial Person::_count: " << Person::_count << endl;Person::_count = 5; // 修改静态成员变量的值cout << "Modified Person::_count: " << Person::_count << endl;// 各自是各自的cout << "Address of p._Pnum: " << &p._Pnum << endl;cout << "Address of s1._Pnum: " << &s1._Pnum << endl;cout << "Address of s2._Pnum: " << &s2._Pnum << endl;// 都是同一个地址cout << "Address of Person::_count (via p): " << &p._count << endl; // 引用调用cout << "Address of Person::_count (via s1): " << &s1._count << endl;cout << "Address of Person::_count (via s2): " << &s2._count << endl;cout << "Address of Person::_count (via class name): " << &Person::_count << endl; // 类名调用cout << "Address of Student::_count (via class name): " << &Student::_count << endl;return 0;
}

应用:如何实现学生人数的计数

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 ; // 研究科目
};
void TestPerson()
{Student s1 ;Student s2 ;Student s3 ;Graduate s4 ;cout <<" 人数 :"<< Person ::_count << endl;Student ::_count = 0;cout <<" 人数 :"<< Person ::_count << endl;
}

计数成功啦

借用了全局调用,static 只有一个 的特性


7.菱形继承与虚拟继承

难点

多继承-棱形继承-虚拟继承

面向对象,就是现实世界的描述

单继承:只有一个直接父类

多继承:多个父类,用逗号隔开

一个人有多个身份,例如即是程序员,又是外卖员

由于多继承的存在,就会引起菱形继承的问题

在Assistant的对象中Person成员会有两份。

菱形继承的问题:

从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 在Assistant的对象中Person成员会有两份。

一个人正常的信息,一份就够了,二义性也容易发生报错不明确

可以通过指定作用域的方法来解决二义性的问题,如上图所示,但是并不符合实际情况,一个人虽然有多种角色,但是名字怎么会有两个甚至多个呢?

单继承并不会形成菱形,如下

class A
class B : public A
class C : public B

二义性已经带来了很多坑,写出一个下面的继承,可能就将会被逐出 C++界了 hhh

虚拟继承

虚拟继承就是专门用来解决菱形继承导致的数据冗余和二义性问题的。

运用:class B : virtual public A

底层:有点小复杂

我们可以建立以下结构来测试一下:

#include <iostream>
#include <string>using namespace std;class A
{
public:int _a;
};class B : public A
//class B : virtual public A
{
public:int _b;
};class C : public A
//class C : virtual public A
{
public:int _c;
};class D : public B, public C
{
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

首先我们来看,不使用虚拟继承时的内存模型:

  • 在D对象创建后,通过内存窗口来看它内部的成员分别情况,如上图所示。
  • 最外边的紫色框是整个d对象,它一共有5个int类型的变量。
  • 中间的蓝色框中的成员是分别是从B,C中继承下来的,都有两个int类型的变量。
  • 红色细框中的变量都是从A中继承下来的。

虚拟继承后的内存模型:

菱形继承中原本冗余的成员最后只有一个,而且放在最终派生类对象中的最后位置。

此时数据冗余和二义性是解决了,因为派生类对象中只有一个从A继承下来的成员了,但是相比原来不用虚拟继承多出来4个字节不说,还将原本是成员所在位置内容也发生了改变。

在两个新内存窗口中看到的两个新的框被称为虚基表

我用的是小端存储方式,按照小端模式得到d对象中存放的两个地址。

所以第一行的虚基表到底存的是什么呢?

存找基类偏移量的表,距离 A 的偏移量(相对距离)

补充:

大端存放(Big-Endian)

在大端存放方式中,数据的高位字节存放在内存的低地址处,而数据的低位字节存放在内存的高地 BCBC 址处。可以形象地理解为数据从“大头”开始存放。

例如,对于32位整数 0x12345678

textCopy code
内存地址    值
0x1000      0x12
0x1001      0x34
0x1002      0x56
0x1003      0x78

在大端存放中,最高有效字节(0x12)存放在最低地址(0x1000),依次排列。

小端存放(Little-Endian)

在小端存放方式中,数据的低位字节存放在内存的低地址处,而数据的高位字节存放在内存的高地址处。可以形象地理解为数据从“小头”开始存放。

例如,对于同样的32位整数 0x12345678

textCopy code
内存地址    值
0x1000      0x78
0x1001      0x56
0x1002      0x34
0x1003      0x12

在小端存放中,最低有效字节(0x78)存放在最低地址(0x1000),依次排列。


虚基表中,第一个int类型的数据存放的是0,具体什么意义在多态的时候再讲。

  • 虚基表中第二个int类型的数据存放的是0x14,它是一个偏移量

再看d对象的内存模型:

  • 从B继承下来的成员,起始地址是0x00F9FA98。
  • 从A继承下来的成员,它的地址是0x00F9FAAC。

这两个地址之间相差0x14(十六进制)(所以 B 小端存放的地址指向的也是 14),也就是20。

当使用d.B::_a来访问A继承下来的成员时,就从B继承下来的成员的起始地址处,根据偏移量去访问具体的_a。

同理可以算出:C区域和A的偏移量是12。

由于使用了虚拟继承,所以B对象和C对象同样采用有虚基表的结构,将从A继承下来的成员放在最后,原本的位置存放对应虚基表的地址,虚基表中存放偏移量。

虚基表存在的原因:

现在有个疑问,为什么要根据偏移量来找从A中继承下来的那个成员?B对象C对象,或者是D对象,它们自己肯定会知道自己成员的位置啊。

B的指针拿到的是对象b的地址时,解引用访问_a,此时只是在自己内部寻找,不用偏移量也可以理解。

B的指针拿到的是对象d的地址时,此时会发生切片,但是d中的_a仍然会保留下来,但是此时站在B指针的角度来看,它根本不知道_a在哪里,因为这是d对象安排的。

所以此时就需要通过虚基表获取_a距离B的偏移量来访问_a。

思考:

为什么不直接存 A 的地址,要偏移?

虚基表,一个类可以有很多个对象,方便大家都可以用

虚拟继承是否有节省空间

有,当 A 对象大一些的时候,就可以体现啦

例题:

题目 1:

题目 2:

下面代码 A 调用几次?

#include <iostream>
using namespace std;class A {
public:A(const char* s) { cout << s << endl; }~A() {}
};class B : virtual public A {
public:B(const char* sa, const char* sb) : A(sa) { cout << sb << endl; }
};class C : virtual public A {
public:C(const char* sa, const char* sb) : A(sa) { cout << sb << endl; }
};class D : public B, public C {
public:D(const char* sa, const char* sb, const char* sc, const char* sd) : A(sa), B(sa, sb), C(sa, sc) {cout << sd << endl;}
};int main() {D d("Constructor of A", "Constructor of B", "Constructor of C", "Constructor of D");return 0;
}

按照声明的顺序来调用

打印结果:

Constructor of A
Constructor of B
Constructor of C
Constructor of D

A 调用了一次

解析:

  1. D 类对象创建时
    • 构造函数调用顺序从最基类开始,然后逐步向派生类调用。
  1. A 类的构造函数
    • 由于 A 类是通过虚继承被 BC 继承的,所以在创建 D 类对象时,会先调用 A 类的构造函数。
    • A(sa) 被调用,打印 "Constructor of A"。
  1. B 类的构造函数
    • B 类的构造函数 B(sa, sb) 被调用。因为 A(sa) 已经在上一步调用过,这里不会再次调用。
    • 打印 "Constructor of B"。
  1. C 类的构造函数
    • C 类的构造函数 C(sa, sc) 被调用。因为 A(sa) 已经在上一步调用过,这里不会再次调用。
    • 打印 "Constructor of C"。
  1. D 类的构造函数
    • 最后调用 D 类的构造函数,并打印 "Constructor of D"。

通过上面题目,可以感受到了比较复杂,所以项目中尽量不要写菱形继承。

应用:

库函数以身试法,使用了菱形继承


8. 继承反思

  1. 多继承可以认为是C++的缺陷之一,很多后来的OO(面向对象)语言都没有多继承,如Java。
  2. 继承和组合

什么时候用继承 or 组合?

  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。

实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有
些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用
继承,可以用组合,就用组合。

优先使用对象组合,而不是类继承 是为什么?

  • 继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称
    白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的
    内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很
    大的影响。派生类和基类间的依赖关系很强,耦合度高。
  • 对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象
    来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱复
    用(
    black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。
    组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被
    封装。

黑盒测试:功能上的

白盒测试:不仅要实现功能上,还要管内部实现

共有越少,耦合度低越好,例如进行增删查改就会更方便

软件工程:高内聚 低耦合 

9.常见问题answer

1.什么是菱形继承?菱形继承的问题是什么?

2.什么是菱形虚拟继承?如何解决数据冗余和二义性的

3.继承和组合的区别?什么时候用继承?什么时候用组合?

解答:

1. 什么是菱形继承?菱形继承的问题是什么?

菱形继承(Diamond Inheritance)

菱形继承是一种特定的多重继承形式,在这种继承结构中,一个基类被两个派生类继承,而这两个派生类又被另一个派生类继承,形成一个菱形结构。

例如:

class A {};
class B : public A {};
class C : public A {};
class D : public B, public C {};

在这个例子中,类 B 和类 C 都继承自类 A,而类 D 同时继承自类 B 和类 C,形成了一个菱形结构。

菱形继承的问题
  1. 数据冗余(Data Redundancy):由于 D 类从 BC 继承,而 BC 又都从 A 继承,这会导致 D 类中包含两份 A 类的成员。这种重复继承会造成内存浪费和数据冗余。
  2. 二义性(Ambiguity):在 D 类中访问 A 类的成员时,编译器会不知道该选择 B 类中的 A 还是 C 类中的 A,导致二义性错误。

2. 什么是菱形虚拟继承?如何解决数据冗余和二义性的问题?

菱形虚拟继承(Virtual Inheritance)

菱形虚拟继承是一种解决菱形继承问题的技术。在 C++ 中,通过虚继承(virtual inheritance)可以确保最基类在菱形继承结构中只被继承一次,从而避免数据冗余和二义性问题。

使用虚继承:

class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};
解决的数据冗余和二义性的问题
  1. 数据冗余:虚继承确保基类 A 在整个继承链中只存在一份。无论多少次继承 A,最终的派生类 D只会有一份 A 的数据成员(虚基表存地址实现)
  2. 二义性:由于 A 只存在一份,访问 A 的成员时不会产生二义性。编译器明确知道该访问唯一的 A 实例。

例如:

class A {
public:int value;
};class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};int main() {D d;d.value = 10; // 访问的是唯一的 A::valuereturn 0;
}

3. 继承和组合的区别?什么时候用继承?什么时候用组合?

继承(Inheritance)和组合(Composition)
继承

继承是一个类从另一个类派生而来的关系,表示“是一个”(is-a)的关系。继承主要用于重用代码,通过基类和派生类的层次结构实现功能的扩展和重载。

例如:

class Animal {
public:void eat() { cout << "Eating" << endl; }
};class Dog : public Animal {
public:void bark() { cout << "Barking" << endl; }
};

在这个例子中,Dog 类继承自 Animal 类,表示 Dog 是一种 Animal

组合

组合是一个类包含另一个类的对象,表示“有一个”(has-a)的关系。组合主要用于构建复杂的对象,从其他对象中组装而成,从而实现功能的复用。

例如:

class Engine {
public:void start() { cout << "Engine started" << endl; }
};class Car {
private:Engine engine;
public:void start() { engine.start(); }
};

在这个例子中,Car 类包含一个 Engine 对象,表示 Car 有一个 Engine

什么时候用继承?什么时候用组合?

在实际开发中,通常推荐优先使用组合,因为组合更加灵活和耦合度低,只有在明确表示 is-a 关系并且需要重用基类行为时才考虑使用继承,多和多态一起使用。


小知识: hc = head count

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/47644.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

win11将bat文件固定到“开始“屏幕

一、为bat文件创建快捷方式 (假设bat文件的全名为运行脚本.bat) 右键bat文件&#xff0c;点击显示更多选项 右键菜单选择发送到(N)-桌面快捷方式 二、获取快捷方式的路径 返回桌面&#xff0c;选中创建好的快捷方式&#xff0c;按AltEnter&#xff0c;切换到安全选项卡 鼠…

JCR一区级 | Matlab实现PSO-Transformer-LSTM多变量回归预测

JCR一区级 | Matlab实现PSO-Transformer-LSTM多变量回归预测 目录 JCR一区级 | Matlab实现PSO-Transformer-LSTM多变量回归预测效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现PSO-Transformer-LSTM多变量回归预测&#xff0c;粒子群优化Transformer结合LST…

Nginx的核心功能

1. Nginx的核心功能 1.1 nginx反向代理功能 正向代理 代理的为客户端&#xff0c;对于服务器不知道真实客户的信息。例如&#xff1a;翻墙软件 反向代理服务器 代理的为服务器端。对于客户来说不知道服务器的信息。例如&#xff1a;nginx 项目部署图 web项目部署的虚拟机和Ng…

鸿蒙语言基础类库:【@system.notification (通知消息)】

通知消息 说明&#xff1a; 从API Version 7 开始&#xff0c;该接口不再维护&#xff0c;推荐使用新接口[ohos.notification]。本模块首批接口从API version 3开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。 导入模块 import notification fro…

httpx 的使用

httpx 是一个可以支持 HTTP/2.0 的库 还有一个是&#xff1a; hyper 库 这里有一个由HTTP/2.0的网站&#xff1a; https://spa16.scrape.center/ 使用 requests 库 进行爬取 import requests url https://spa16.scrape.center/ response requests.get(url) print(response…

达梦数据库的系统视图v$arch_file

达梦数据库的系统视图v$arch_file 在达梦数据库中&#xff0c;V$ARCH_FILE 是一个动态性能视图&#xff0c;用于显示当前数据库的归档日志文件信息。这个视图可以帮助数据库管理员监控和管理归档日志文件&#xff0c;确保数据库的备份和恢复过程顺利进行。 查询本地归档日志信…

Unity UGUI Image Maskable

在Unity的UGUI系统中&#xff0c;Maskable属性用于控制UI元素是否受到父级遮罩组件的影响。以下是关于这个属性的详细说明和如何使用&#xff1a; Maskable属性 Maskable属性&#xff1a; 当你在GameObject上添加一个Image组件&#xff08;比如UI面板或按钮&#xff09;时&…

ctfshow-web入门-php特性(web127-web131)

目录 1、web127 2、web128 3、web129 4、web130 5、web131 1、web127 代码审计&#xff1a; $ctf_show md5($flag); 将 $flag 变量进行 MD5 哈希运算&#xff0c;并将结果赋值给 $ctf_show。 $url $_SERVER[QUERY_STRING]; 获取当前请求的查询字符串&#xff08;que…

开源防病毒工具--ClamAV

产品文档&#xff1a;简介 - ClamAV 文档 开源地址&#xff1a;Cisco-Talos/clamav&#xff1a;ClamAV - 文档在这里&#xff1a;https://docs.clamav.net (github.com) 一、引言 ClamAV&#xff08;Clam AntiVirus&#xff09;是一个开源的防病毒工具&#xff0c;广泛应用…

【算法专题】归并排序

目录 1. 排序数组 2. 交易逆序对的总数 3. 计算右侧小于当前元素的个数 4. 翻转对 总结 1. 排序数组 912. 排序数组 - 力扣&#xff08;LeetCode&#xff09; 今天我们使用归并排序来对数组进行排序&#xff0c;实际上&#xff0c;归并排序和快速排序是有一定相似之处的&a…

Android View的绘制流程

1.不管是View的添加&#xff0c;还是调用View的刷新方法invalidate()或者requestLayout()&#xff0c;绘制都是从ViewRootImpl的scheduleTraversals()方法开始 void scheduleTraversals() {if (!mTraversalScheduled) {mTraversalScheduled true;mTraversalBarrier mHandler…

Linux中nohup(no hang up)不挂起,用于在系统后台不挂断地运行命令,即使退出终端也不会影响程序的运行。

nohup的英文全称是 no hang up&#xff0c;即“不挂起”。这个命令在Linux或Unix系统中非常有用&#xff0c;主要用于在系统后台不挂断地运行命令&#xff0c;即使退出终端也不会影响程序的运行。默认情况下&#xff08;非重定向时&#xff09;&#xff0c;nohup会将输出写入一…

C++之类与对象(1)

目录 前言 1.类的定义 1.1类定义的格式 1.2访问限定符 1.3类域 1.3.1类定义一个作用域 1.3.2类成员在类的作用域中 1.3.3在类体外定义成员 2.实例化 2.1实例化概念 2.2对象大小 3.this指针 4.选择题补充练习 结束语 前言 Hello&#xff0c;友友们&#xff0c;好久…

Linux安装mysql(超详细版)

步骤1&#xff1a;新建一个文件夹&#xff0c;专放从网络下载的文件 [rootiZ2zeh6vyxsq620zifz8jaZ home]#mkdir soft #在根目录下创建也可以 步骤2&#xff1a;切换目录&#xff0c;进入soft文件中 [rootiZ2zeh6vyxsq620zifz8jaZ /]# cd home/ #若第一步文件建在根目…

牛客周赛 Round 51

目录 A.小红的同余 B.小红的三倍数 C.小红充电 D.小红的gcd E.小红走矩阵 F.小红的数组 这次周赛题目比较简单&#xff0c;算法题也基本上是板子题&#xff0c;出得很好(&#xff5e;&#xffe3;▽&#xffe3;)&#xff5e; A.小红的同余 思路&#xff1a;签到题&am…

Android Studio 不再支持windows 7

Android Studio 一打开就报错&#xff1a; 无法找到入口 无法定位程序输入点 CreateAppContainerProfle 于动态链接库USERENV.dII 上。 截图如下&#xff1a; 经调查&#xff0c;是因为系统版本不兼容。 我目前的电脑环境&#xff1a;windows 7,但是现在的Android Studio要…

24年Hvv准备,6大方向,33篇技战法

进去不少小伙伴后台留言说需要技战法&#xff0c;因此小编对市面上的技战法进行了收集和总结&#xff0c;并对收集来的技战法进行了分类&#xff0c;总共分了6大类&#xff0c;共计33篇&#xff1a; 有需要的小伙伴关注我&#xff0c;点击在看&#xff0c;并私信回复“技战法”…

基于Java的原创歌曲分享平台

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBootMyEclipse 工具&#xff1a;MyEclipse、B/S架构 系统展示 首页 用户注册界面 音乐分享…

【python】OpenCV—Coordinates Sorted Clockwise

文章目录 1、需求介绍2、算法实现3、完整代码 1、需求介绍 调用 opencv 库&#xff0c;绘制轮廓的矩形边框&#xff0c;坐标顺序为右下→左下→左上→右上&#xff0c;我们实现一下转化为熟悉的 左上→右上→右下→左下 形式 按照这样的顺序组织边界框坐标是执行透视转换或匹…

21天学通C++:第十三、十四章节

第十三章&#xff1a;类型转换运算符 类型转换是一种机制&#xff0c;让程序员能够暂时或永久性改变编译器对对象的解释。注意&#xff0c;这并不意味着程序员改变了对象本身&#xff0c;而只是改变了对对象的解释。可改变对象解释方式的运算符称为类型转换运算符。 为何需要…