【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成员
基类的成员被派生类继承,就是派生类的新成员
派生类就需要为这些新成员进行访问限定符限制,这个操作则是由继承方式来进行限制的
基类不同限制符受继承方式在派生类中的变化
总结:
- 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面 都不能去访问它。
- 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
- 实际上面的表格我们进行一下总结会发现,基类的私有成员在子类都是不可见。基类的其他 成员在子类的访问方式 == Min(成员在基类的访问限定符,继承方式),public > protected> private。
- 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
- 在实际运用中一般使用都是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
前面仔细观察就会发现
- B类对象的地址为0x012FFD48
- A类对象的地址为0x012FFD5C
- 0x012FFD48+14 = 0x012FFD5C
B类对象可以通过这个偏移量找到A类对象
同样,C类对象也可以通过这个方法寻找到A类对象
此时,B类对象和C类对象中的A类对象就是同一个
在D类中也就不会存在调用二义性,已经数据冗余的问题了
无论是对类B还是类C的类A对象操作,或者调用函数,都是对同一份内存地址的A类对象操作!
B类对象和C类对象原本存储A类对象的内存地址,现在存储的是一个指针
这个指针会指向一张表,我们将这张表称为虚基表,将指针称为 虚基表指针
虚基表上存储的是当前 派生类对象地址 相对于 基类对象地址 的偏移量
派生类对象可以通过这个偏移量,找到对应的 基类对象 ,从而操作!
注意:
虚拟继承只能用来解决菱形继承问题,不可用在其他地方!
结语
正如任何技术在实践中都并非完美无缺,继承也并非适用于所有场景。我们在享受其带来的便捷之时,也需时刻警惕过度继承可能引发的问题。这就要求我们在设计之初,就需深思熟虑继承的合理性,结合具体的项目需求和场景,权衡利弊,灵活运用继承。
希望通过对继承的系统学习与实践尝试,大家能在未来的编程之路上,以继承为有力工具,精心雕琢出更加简洁、高效且稳固的软件架构,让代码在我们的笔下绽放更加绚烂的光彩,为解决实际问题提供更坚实的技术支撑。