当前位置: 首页 > news >正文

C++继承(下)

目录

一、继承与友元

二、继承与静态成员

三、菱形继承及菱形虚拟继承

1. 继承的方式

2. 菱形继承的问题 

3. 菱形虚拟继承 

4. 虚拟继承解决数据冗余和二义性的原理

4.1 普通菱形继承的内存布局

4.2 虚拟继承的内存布局

四、继承的总结和反思

1. 多继承的复杂性

2. 继承与组合

2.1 继承(is-a关系)

2.2 组合(has-a关系)

3. 优先使用组合

4. 小结

五、笔试面试题解析

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

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

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


一、继承与友元

        友元关系不能被继承。也就是说,基类中声明的友元函数或友元类不能访问派生类的私有或保护成员。因为友元关系是类之间的一种特殊访问权限约定,它不具备继承性,派生类并不会自动继承基类的友元所拥有的特殊访问权。

        在下面代码中,Display 函数是 Person 类的友元,因此它可以访问 Person 类的保护成员 _name。然而,Display 函数并不是 Student 类的友元,因此它不能访问 Student 类的保护成员 _stuNum。这表明友元关系不具备继承性,派生类不会自动继承基类的友元声明。

#include <iostream>
#include <string>
using namespace std;class Person
{
public:friend void Display(const Person& p, const Student& s); // 声明友元函数
protected:string _name; // 姓名
};class Student : public Person
{
protected:int _stuNum; // 学号
};void Display(const Person& p, const Student& s)
{cout << p._name << endl; // 可以访问,因为Display是Person的友元cout << s._stuNum << endl; // 编译错误,Display不是Student的友元
}void main()
{Person p;Student s;Display(p, s);
}

        若想让 Display 函数能够访问派生类 Student 的私有或保护成员,需要在 Student 类中显式地声明 Display 函数为友元: 

class Student : public Person
{
public:friend void Display(const Person& p, const Student& s); // 声明Display是Student的友元
protected:int _stuNum;
};

二、继承与静态成员

        基类中定义的静态成员在整个继承体系中是共享的,无论派生出多少个子类,都只有一个该静态成员的实例。静态成员属于类而非某个具体对象,所以在继承体系中,所有类和对象都共用这一个静态成员,对它的访问和修改会影响整个继承体系中的所有相关部分。

        在基类Person中定义了一个静态成员变量_count,用于统计实例化的对象数量:

#include <iostream>
#include <string>
using namespace std;class Person
{
public:Person() { ++_count; } // 构造函数中自增_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; // 通过Student类修改_count的值cout << "人数 : " << Person::_count << endl; // 输出修改后的值
}int main()
{TestPerson();return 0;
}

        在上述代码中,无论创建多少个StudentGraduate对象,Person::_count始终是一个实例。这是因为静态成员变量属于类本身,而不是类的某个特定对象。我们可以通过打印Person类和Student类中静态成员_count的地址来证明它们是同一个变量: 

cout << &Person::_count << endl; // 输出Person类中_count的地址
cout << &Student::_count << endl; // 输出Student类中_count的地址

三、菱形继承及菱形虚拟继承

1. 继承的方式

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承 

菱形继承:菱形继承是多继承的一种特殊情况 


2. 菱形继承的问题 

        在多继承情况下,若一个类有两个或以上基类,而这些基类又有一个共同的基类,就可能形成菱形继承。这种情况下容易出现数据冗余(同一个基类被多次继承,导致其成员在派生类对象中重复出现)和二义性(访问共同基类的成员时,编译器无法确定具体访问哪一个基类路径中的成员)问题。

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

class Person
{
public:string _name; // 姓名
};class Student : public Person
{
protected:int _num; // 学号
};class Teacher : public Person
{
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};void Test()
{Assistant a;// 这里会报错:二义性成员访问,因为 Assistant 同时从 Student 和 Teacher 继承了 _namea._name = "peter";
}

        通过显式指定父类可以解除二义性,但会导致数据冗余(每个父类都有自己的 _name 成员)。

a.Student::_name = "xxx";
a.Teacher::_name = "yyy"; 

3. 菱形虚拟继承 

        为了解决菱形继承的二义性和数据冗余问题,出现了虚拟继承。通过将继承方式改为虚拟继承,可以让派生类共享同一个基类的实例,从而解决数据冗余和二义性问题。在虚拟继承中,编译器会采用特殊的机制,如虚基表虚基表指针,来确保基类在内存中只出现一次,并且正确地解析对基类成员的访问路径。

#include <iostream>
#include <string>
using namespace std;class Person
{
public:string _name; //姓名
};class Student : virtual public Person //虚拟继承
{
protected:int _num; //学号
};class Teacher : virtual public Person //虚拟继承
{
protected:int _id; //职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; //主修课程
};int main()
{Assistant a;a._name = "peter"; //不再二义性cout << a.Student::_name << endl; // 输出 "peter"cout << a.Teacher::_name << endl; // 输出 "peter"cout << &a.Student::_name << endl; // 地址相同cout << &a.Teacher::_name << endl; // 地址相同return 0;
}

4. 虚拟继承解决数据冗余和二义性的原理

        为了研究虚拟继承原理,我们写一个简化的菱形继承体系,再借助内存窗口观察对象成员的模型。 

4.1 普通菱形继承的内存布局

#include <iostream>
using namespace std;class A
{
public:int _a;
};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.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

在普通菱形继承中,类D对象的内存布局如下:

  • 首先是类B继承自类A的部分,包含 _a(来自A)和 _b

  • 然后是类C继承自类A的部分,再次包含 _a(来自A)和 _c

  • 最后是类D自己的成员 _d

这种布局导致了两个_a成员的存在。

4.2 虚拟继承的内存布局

#include <iostream>
using namespace std;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.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

使用虚拟继承后,类D对象的内存布局调整为:

  • B和类C各自包含一个虚基表指针(vbptr),这些指针指向虚基表(vbtbl)。

  • 虚基表中存储了从类B和类C到共享类A实例的偏移量。

  • D对象中只包含一份类A的成员 _a,并且这个_a被放置在内存布局的最后。

        这里可以分析出D对象中将A放到了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。


下面是上面的Person关系菱形虚拟继承的原理解释: 

四、继承的总结和反思

1. 多继承的复杂性

        很多人觉得C++语法复杂,多继承就是一个典型例子。多继承引入后,很容易形成菱形继承结构,而菱形继承又需要通过虚拟继承来解决数据冗余和二义性问题。这些机制在底层实现上非常复杂。因此,一般不建议设计多继承结构,尤其是菱形继承。它不仅会增加代码的复杂度,还可能对性能产生负面影响。

2. 继承与组合

        在面向对象编程中,继承组合是两种重要的复用机制。它们各有特点,适用于不同的场景。

2.1 继承(is-a关系)

        继承体现的是“is-a”的关系。例如,BMW继承自Car,因为BMW是一种汽车。通过继承,BMW类可以复用Car类的功能和属性。然而,继承也被称为“白箱复用”,因为基类的内部实现对子类是可见的。这种高可见性意味着基类的任何修改都可能影响到所有子类,导致子类和基类之间的耦合度很高。

class Car 
{
protected:string _colour = "白色"; // 颜色string _num = "甘EBIT00"; // 车牌号
};class BMW : public Car 
{
public:void Drive() {cout << "好开-操控" << endl;}
};

2.2 组合(has-a关系)

        组合体现的是“has-a”的关系。例如,Car类组合了Tire类,因为汽车有一个或多个轮胎。组合被称为“黑箱复用”,因为被组合对象的内部实现对组合类是不可见的。这种方式耦合度低,组合类之间的依赖关系较弱。

class Tire 
{
protected:string _brand = "Michelin";  // 品牌size_t _size = 17;         // 尺寸
};class Car 
{
protected:string _colour = "白色"; // 颜色string _num = "甘EBIT00"; // 车牌号Tire _t; // 组合的轮胎对象
};

3. 优先使用组合

        尽管继承在某些场景下非常有用,但在实际开发中,我们应优先使用组合。组合的低耦合特性使得代码更易于维护和扩展。继承则更适合用于那些具有明确“is-a”关系的场景,或者当需要实现多态性时。

4. 小结

  • 继承:适合“is-a”关系,复用基类实现,但耦合度高。

  • 组合:适合“has-a”关系,通过组合对象实现功能,耦合度低。

五、笔试面试题解析

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

菱形继承:

        菱形继承是一种多继承的特殊情况,其类层次结构形如菱形。具体来说,一个基类派生出两个或多个子类,然后另一个类又同时继承自这两个子类。例如:

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

        在这里,类 D 同时继承自类 B 和类 C,而类 B 和类 C 又都继承自类 A,从而形成了菱形的继承结构。

主要问题:

  1. 数据冗余:由于多继承路径,基类 A 的成员在派生类 D 中会存在多个副本(每个继承路径各一份)。这不仅浪费内存空间,还可能导致数据不一致的问题。

  2. 二义性:在访问基类 A 的成员时,编译器无法确定应该通过哪条继承路径(BC)来访问该成员,从而引发编译错误。例如

    D d;
    d.a_member = 1; // 错误:无法确定是通过 B 还是 C 访问 a_member

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

菱形虚拟继承:

        菱形虚拟继承是一种通过使用 virtual 关键字来解决菱形继承问题的机制。具体来说,在继承基类时使用 virtual 关键字,可以让所有派生类共享同一个基类实例。例如:

class A {};
class B : virtual public A {}; // 虚拟继承
class C : virtual public A {}; // 虚拟继承
class D : public B, public C {};

解决数据冗余和二义性的原理

  1. 共享基类实例:虚拟继承确保在派生类 D 中,基类 A 的成员只存在一个副本。这样就消除了数据冗余。

  2. 消除二义性:通过共享同一个基类实例,访问基类成员时不再需要指定继承路径,编译器可以明确地找到唯一的成员实例。例如

    D d;
    d.a_member = 1; // 正确:直接访问共享的 a_member

内存布局的变化

        使用虚拟继承后,派生类 BC 中会包含虚基表指针(vbptr),这些指针指向虚基表(vbtbl),而虚基表中存储了从派生类到共享基类的偏移量。这样,派生类 D 对象中只包含一份基类 A 的成员,并且可以通过虚基表指针找到该成员。

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

继承(is-a关系)

  • 特点:派生类是基类的特化(如“猫是动物”),可复用基类接口并扩展功能。

  • 适用场景

    • 需要实现多态(如虚函数)。

    • 明确逻辑上的层次关系(如GUI控件继承自基类Widget)。

  • 缺点:高耦合性,基类修改可能影响所有派生类。

组合(has-a关系)

  • 特点:类通过包含其他类的对象实现功能复用(如“汽车包含引擎”)。

  • 适用场景

    • 需要复用功能但无需继承接口(如Stack类组合vector实现存储)。

    • 降低耦合,提升代码灵活性(组合类可替换成员对象实现不同功能)。

  • 优点:封装性好,维护成本低。

选择原则

  • 优先组合:除非需要多态或明确的层次关系,否则优先使用组合。

  • 谨慎继承:避免复杂的继承链,尤其是菱形继承,尽量使用虚继承解决冗余问题。

相关文章:

  • 监听滚动事件
  • Ubuntu平台使用aarch64-Linux交叉编译opencv库并移植RK3588S边缘端
  • 新手小白如何查找科研论文?
  • Nginx匹配规则详细解析
  • 快充诱骗协议芯片的工作原理及应用场景
  • Python3(19)数据结构
  • [SystemVerilog] Enum
  • UDP/TCP协议知识及相关机制
  • 【使用小皮面板 + WordPress 搭建本地网站教程】
  • 私有知识库 Coco AI 实战(五):打造 ES 新特性查询助手
  • 线上婚恋相亲小程序源码介绍
  • ES基本使用方式
  • 基于策略模式实现灵活可扩展的短信服务架构
  • 美团优选小程序 mtgsig 分析 mtgsig1.2
  • Vue3源码学习-提交限制
  • pytorch中的原地与非原地操作
  • 软件系统验收报告:功能、性能稳定性如何?数据导出卡顿咋回事?
  • GPU集群中的超节点
  • 【Hot 100】 148. 排序链表
  • AI Engine Kernel and GraphProgramming--知识分享1
  • 美国季度GDP时隔三年再现负增长,特朗普政府关税政策对美国经济负面影响或将持续
  • 首开股份:一季度净利润亏损约10.79亿元,签约金额63.9亿元
  • 山西太原一处居民小区发生爆炸,现场产生大量浓烟
  • 人民日报:在大有可为的时代大有作为
  • 广西干旱程度有所缓解,未来一周旱情偏重地区降水量仍不足
  • 诗词文赋俱当歌,听一听古诗词中的音乐性