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

【C++】—— 一篇文章解决面试 继承菱形继承

目录

前言

继承 && 继承方式

重定义

友元类 继承?

继承与静态成员

菱形继承


前言

在C++这个广袤而神秘的编程世界里,继承宛如一颗璀璨的明珠,散发着独特的魅力。它是连接代码世界的纽带,也是实现代码复用与扩展的神奇魔法。当我们深入探索C++继承的奥秘时,就仿佛开启了一段奇妙的旅程,在这个旅程中,我们将领略到面向对象编程的博大精深,感受到代码在继承的力量下焕发出的无限活力。

每一行代码,都是我们对世界的理解和表达;每一次继承,都是我们对程序设计的又一次探索和创新。在这个过程中,我们不仅能够提高代码的效率和质量,更能够培养自己严谨的逻辑思维和创造力。现在,就让我们一同踏上这段探索C++继承的奇妙之旅,去发现那些隐藏在代码背后的惊喜和智慧吧。


继承 && 继承方式

继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能。

一个类,继承了其他类,我们将这种类称为派生类,或者子类

一个类,被其他类继承,我们将这种类称为基类,也称为父类

继承的基本语法:

  • 定义一个继承关系的类时,使用冒号(:)后跟继承方式(public  protected 或 private),然后是基类的名称

示例:

class person {
   public:
      //
      void FirstPrint() {
		std::cout << "姓名:" << name << std::endl;
		std::cout << "年龄:" << age << std::endl;
	  }
      std::string name="小鱼同学";
           int age = 18;
};
class student :public person{
   public:
      //
      std::string num="2209109016";
};

student 类 继承了 person类, 继承方式为 public

我们说过,继承是一种代码复用的手段,上述例子中我们无法体现代码复用

先说结论:

继承的代码复用,就是派生类可以融合基类的成员,将其当成自己的成员

怎么说?

定义一个student对象 ,调用一下基类中的FirstPrint函数,并用监视窗口查看其值

可以发现,student对象xy内部值会多出来一个person类,向下查看,会看到name和age

这代表,person类的成员变量name和age,也是student类的成员变量

同时,从student对象可以直接调用person类的FirstPrint函数可以看出,Person的成员函数也通过继承成为了student类的成员函数

即,继承就是将基类的成员融合进派生类,使派生类中有基类一样的代码片段,从而达到代码复用的效果

派生类继承基类的成员,并不是全部继承,会受到基类访问限定符的限制

  • 基类内部,凡是private限定符内的成员,均不可被继承

基类的成员能不能被继承,被派生类当成自己的成员,关键在于其在基类内部,是不是private成员

基类的成员被派生类继承,就是派生类的新成员

派生类就需要为这些新成员进行访问限定符限制,这个操作则是由继承方式来进行限制的

基类不同限制符受继承方式在派生类中的变化

 总结:

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected可以看出保护成员限定符是因继承才出现的
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式)public > protected> private
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public不过最好显示的写出继承方式
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承,也不提倡使用protetced/private继承,因为protetced/private继承下来的成员都只能在派生类的类里面使用

重定义

在基类限制符允许的情况下,派生类所有的成员,但其中一些成员,继承会应发调用的二义性

如与派生类原生成员重名的基类成员

这种基类成员被派生类,会引发调用的二义性

为了防止调用歧义,派生类对基类重名成员的处理是,一律进行隐藏

派生类不用基类的重名成员,而是用自己的成员,这种隐藏也可以被称作重定义!

示例:

class Base {
public:
    void display() {
        std::cout << "Base display" << std::endl;
    }
};

class Derived : public Base {
public:
    void display() { // 参数列表不同,隐藏了基类的display()
        std::cout << "Derived display" << std::endl;
    }
};

隐藏,并不是拒绝继承,拒绝融合

派生类要使用被隐藏的重名成员,还是能使用的

使用 基类:: 重名成员名 的方式进行使用

示例:

int main() {
    Derived d;
    d.display();//Derived自己的类成员函数
    d.Base::display();//基类Base的类成员函数
    return 0;
}

运行结果: 

 注意:

使用 类名:: 即可访问隐藏的基类成员,而 类名 :: 一般都是用来突破类域进行访问的,说明基类和派生类的作用域,并没有融合,两者的作用域仍独立!

友元类 继承?

基类的友元关系,是不会被派生类继承的

也就是说,基元友元不能访问子类的私有和保护成员

这个很好理解,继承就是父传子,你父亲的朋友不一定是你的朋友

继承与静态成员

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

如何证明这一点呢?

首先,派生类继承基类,会继承基类所有的成员,将其作为自己的一部分

我们可以直接访问派生类的基类static成员,打印其地址

同时,对基类的static成员也进行访问,打印其地址

以static成员变量为例:

class person {
public:

	void FirstPrint() {
		std::cout << "姓名:" << name << std::endl;
		std::cout << "年龄:" << age << std::endl;
	}
	std::string name="小鱼同学";
	int age=18;
	static std::string value ;//person类中声明一个静态变量
};
std::string person::value = "静态变量";//类外对其进行初始化

class student :public person{
public:
	void Print() {
		FirstPrint();
		std::cout << name << std::endl;
		std::cout << age << std::endl;
		std::cout << num << std::endl;
	}
	std::string num="2209109016";
};

打印student的value的地址 和 person中的value的地址

void test_6() {
	std::cout <<"student类中的静态变量地址->" << &student::value << std::endl;
	std::cout << "person类中的静态变量地址->" << &person::value << std::endl;
}

运行结果:

菱形继承

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

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

多继承意味着融合更多成员,复用更多代码

从好的方面看,其帮助我们节省了很多重复代码的编写

但其也会带啦隐患,如菱形继承

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

菱形继承示例:

说明:

  • 类Assistant继承 类Student 和 类Teacher,
  • 类Student 和 类Teacher 均继承 类Person 

继承意味着子类融合父类的成员,Assistant 同时融合 Student 和 Teachaer 的成员

但这两个类中,都有一份相同的Person成员,此时 Assistant 中就有两份一模一样的成员

当Assistant对象调用Person类中的成员时,就会有调用二义性的问题

同时,Assistant中相当于有两份一摸一样的成员,造成了数据冗余

解决菱形继承,就需要使用此 virtual 关键字,将普通继承变成虚拟继承!

在上面的继承关系中,在 Student 和 Teacher 的继承 Person 时使用虚拟继承

class student :virtual public person
class teacher :virtual public person

即可解决调用二义性与数据冗余

虚拟继承是如何解决菱形继承的呢?

准备四个类,它们之间的继承关系如代码所示

class A {
public:
	int A_value;
};
//class B :public A
class B :virtual public A
{
public:
	int B_value;
};
//class C :public A 
class C:virtual public A
{
public:
	int C_value;
};
class D :public B, public C {
public:
	int D_value;
};

 要想看看虚拟内存如何解决菱形继承,最直观的方式就是查看内存地址

菱形继承下的内存

说明:

  • A类对象的地址存在两份,也就是说,此时D类对象中,存在两份A对象

虚拟继承下的内存

  • A类对象的地址只有一份了,此时D类对象中,只有一份A对象
  • 而B类对象和C类对象中,原本存储A类对象地址对应的值发生改变,存储了一份地址

查看这B类对象中存储的地址

它指向的地址的下一个地址,存储的是一个偏移量:14

前面仔细观察就会发现

  1. B类对象的地址为0x012FFD48
  2. A类对象的地址为0x012FFD5C
  3. 0x012FFD48+14 = 0x012FFD5C

B类对象可以通过这个偏移量找到A类对象

同样,C类对象也可以通过这个方法寻找到A类对象

此时,B类对象和C类对象中的A类对象就是同一个

在D类中也就不会存在调用二义性,已经数据冗余的问题了 

无论是对类B还是类C的类A对象操作,或者调用函数,都是对同一份内存地址的A类对象操作!

B类对象和C类对象原本存储A类对象的内存地址,现在存储的是一个指针

这个指针会指向一张表,我们将这张表称为虚基表,将指针称为 虚基表指针

虚基表上存储的是当前 派生类对象地址 相对于 基类对象地址 的偏移量

派生类对象可以通过这个偏移量,找到对应的 基类对象 ,从而操作!

注意:

虚拟继承只能用来解决菱形继承问题,不可用在其他地方!


结语

   正如任何技术在实践中都并非完美无缺,继承也并非适用于所有场景。我们在享受其带来的便捷之时,也需时刻警惕过度继承可能引发的问题。这就要求我们在设计之初,就需深思熟虑继承的合理性,结合具体的项目需求和场景,权衡利弊,灵活运用继承。

  希望通过对继承的系统学习与实践尝试,大家能在未来的编程之路上,以继承为有力工具,精心雕琢出更加简洁、高效且稳固的软件架构,让代码在我们的笔下绽放更加绚烂的光彩,为解决实际问题提供更坚实的技术支撑。


http://www.kler.cn/a/587054.html

相关文章:

  • A SURVEY ON POST-TRAINING OF LARGE LANGUAGE MODELS——大型语言模型的训练后优化综述——第一部分
  • 加密算法逆向与HOOK技术实战
  • OpenHarmony子系统开发 - ArkCompiler开发指导
  • matlab 控制系统GUI设计-PID控制超前滞后控制
  • 打靶日记Kioptix Level 4
  • vue项目如何实现条件查询?
  • 贪吃蛇小游戏-简单开发版
  • 【实战ES】实战 Elasticsearch:快速上手与深度实践-附录-2-性能调优工具箱
  • 路由器和网关支持边缘计算
  • 无需归一化的Transformers:神经网络设计的突破
  • 单元测试、系统测试、集成测试
  • MySQL数据库知识总结
  • Java 大视界 -- Java 大数据在智能金融资产定价与风险管理中的应用(134)
  • install of jenkins-2.501-1.1.noarch conflicts with file
  • 【redis】hash 类型应用场景
  • 面试系列|蚂蚁金服技术面【2】
  • The workbook already contains a sheet named
  • docker 增加镜像(忘记什么bug了)
  • Linux下的正则表达式应用与sed、printf、awk等命令的使用
  • 大模型架构全景解析:从Transformer到未来计算范式