C++的继承机制精讲

前言

在c++的入门章节中,我们就介绍了面向对象编程的三大特征,那就是封装,继承,多态,我们在讲了封装后,并没有像学校课程一般,讲解继承和多态的内容,而是通过几个经典的STL容器的实现,让大家感受c++编写的基础知识,和逻辑,现在,我认为实际已成熟,我们"千呼万唤始出来"的继承和多态,也可以学习了。

1.继承的概念定义

1.1什么是继承

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

现在我们如果要实现,一个学校的教务管理系统,既然是面向对象编程,首先就要定义几个类,分别是学生,老师,门卫,食堂阿姨等等,我们在设计时会发现前面几个类,我们都要设计诸如,姓名,年龄,电话,身份证等等特征,难道我们每设计一个类,就要帮他们初始化这些特征吗?所以此时聪明面向对象的前辈们,就开始想能不能,设计出一个成员为这些公共特征的类,使得,我们后面的学生,老师类可以复用他们,而不再准们的初始化一遍,实现这个功能的过程,就是继承。

1.2c++中的继承要求

class Person

{public:

Person(string name="zhangsan")

:_name(name)

,_number()

{

cout << "Person的构造" << endl;

}

Person(const Person& s)

{

_name = s._name;

_number = s._number;

cout << "Person的拷贝构造" << endl;

}

Person& operator=(const Person& s)

{

_name = s._name;

_number = s._number;

cout << "person的赋值重载" << endl;

}

~Person()

{

复制代码
}

protected:

string _name;

int _number;

};

class Student :public Person

{

public:

Student(const int& id = 0)

:Person("zhoujielun")

, _id(id)

{

cout << "Student 的构造" << endl;

}

Student(const Student& s)

:_id(s._id)

,Person(s)

{

cout << "Student的拷贝构造" << endl;

}

Student& operator=(const Student& s)

{

Person::operator=(s);

_id = s._id;

cout << "Student的赋值重载" << endl;

}

protected:

int _id;

};

int main()

{

Student s(1);

复制代码
Student s2 = s;

return 0;

}

上面是我们实现的两个类,分别是Person类和Student类,很明显是后者继承前者,这时我们成Person为基类,Student为派生类,两者的继承通过:

public,protectde,private三个继承方式来实现。

既然有不同的访问限定符,那肯定有不同的区别。这时让我们复习一下上面三个关键字对成员的意义。

public就是类内和类外,都能访问,private就是只能类能访问,protected是类内可以以访问,对于继承其的派生类,也能访问。所以:我们在写继承类时通常使用protected限定成员,而不是private.

上面这张表就是,基类成员,通过不同的继承方式,继承到派生类后的权限,我们通常都是用的时public。

总结:

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

2.C++中的基类派生类对象赋值转换

派生类和基类的赋值方式,时向上的类型转换,就像可以让子类赋值给父类,而不能让父类赋值给基类,可以这样记忆,只能是儿子像爹,不是爹像儿子。

接下来我们就要知道向上转换的具体细节,是一种切片似的转换:

我们就可以这样:

Student s;

Person *p=s;

即子类对象可以赋值给父类指针引用等。

而且这种并不是之前我们学过的:

int i;

double j=i;

这种时先生成i的double类型的临时变量,然后赋值,而向上继承的方式,没有临时变量的出现。

3.继承中的作用域问题

  1. 在继承体系中基类和派生类都有独立的作用域。
  2. 子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的直接访问,这种情况叫隐藏,
    也叫重定义。(在子类成员函数中,可以使用 基类::基类成员 显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员
    所以我们在平时写继承时,最好不要让子类和父类有同名的成员。

    如图所示,上面的两个函数并不够构成重载,因为函数重载的定义前提是:在同一作用域的两个同名函数,这里的两个func跟不在同一个作用域,如果我们直接调用
    b.func(10)他会直接默认为当前B域找,如果B域没有再扩大范围再A域里面找。

4.基类和派生类的默认成员函数关系

  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用。
    即我们必须要在派生类中调用基类的构造函数:

    调用方式如同匿名对象一般Person(" ")
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化。
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。
    如下:

    总之意思就是对于派生类中的基类成员的默认函数操作,都必须调用他本身的默认函数。
    4.通常是基类先构造,然后派生类再构造,如果是析构就是派生类先析构,然后基类再析构,就如同我们之前说的类里面的成员是自定义类型成员的情况一样。

5.继承和友元

我们知道,友元函数还是帮助函数可以在类外访问类内成员的一种方式,但是再继承体系中,派生类,并不能继承基类的友元函数,即友元函数不能访问派生类的类内成员。

举个例子:就像你爸爸的朋友,并不是你的朋友。

6.继承中的static类

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

也就是说,一般的成员变量,我们的派生类一般会拷贝一份,而对于static类所有继承类都只共享一份,没有发生拷贝,也就是说只有派生类只有该成员的使用权,并不具有该成员。

7.菱形继承

7.1什么是菱形继承

面向对象就是模仿一个虚拟的世界,于是我们的祖师爷,就想既然要搞继承,那就彻底一点,如自然界中有的既有动物的特征,又有植物的特征,如植物人(呵呵,开个地狱冷笑话),所以我们的继承应当也满足,一个类可以继承多个不同的类。

于是就出现了:

此时,再进一步,如果student和teacher又同时继承一个相同的类person呢?

于是:

这不就是一个菱形吗?所以上面的这个继承方式,就是一个菱形继承模型。

于是我们的祖师爷半夜将要睡觉之时,意识到这样有点不对劲,然后突然坐起来,惊叹道,问题大了去了。

7.2菱形继承的问题

这个问题是什么呢?

如图所示,意思是有一个人叫jay他是学校的博士,所以它既是一名学生,也是一名教师,所以同时继承student类和Teacher类,看似很合理,但其实有一个大坑,就是我们看jay这个类,他有两个相同的内嵌成员,那就是person,这不就是重复了吗,浪费空间吗?这就是数据的冗余。 ,还有,如果我们改变了student类中的person,那么teacher类中的person是不是也被改变,而且如果我:

Jay t;

t._name

那么他是访问的student的_name还是teacher的_name呢?这就是数据的二义性。

7.3解决菱形继承问题的方法

其实解决菱形继承的方法思路很简单,我们只要知道我们的目的是为了结局数据的冗余和二义性,那可以用什么方法呢?

我们可以将student类中的person和teacher类中的person拿出来

底层实现就是:

jay类中原本student和teacher的公有成员person位置不再存储person,而是存储一个地址,这个地址指向一个数据表,表的内容是该位置距离新的person距离,那我们就可以根据这个表来确定新的person在jay中的位置了,这个表就是虚拟继承表,即最终我们确定了以虚拟继承表中的偏移量来确定新person的位置的继承方式。

这种继承方式就是,虚拟继承,使用虚拟继承,就只用在菱形继承的腰部类加上virtual即可.

如上图所示。

8.总结

我认为cpp的复杂就体现在了继承和多态部分的内容,但是只要我们知道,**一切语法都是以解决问题为目的设计的理念,**所以不妨带着疑惑,来一步一步的学习继承语法,也可以利用编译器,通过观察底层内存分来学习继承。

相关推荐
用户298698530148 分钟前
.NET 文档自动化:Spire.Doc 设置奇偶页页眉/页脚的最佳实践
后端·c#·.net
码路飞36 分钟前
GPT-5.3 Instant 终于学会好好说话了,顺手对比了下同天发布的 Gemini 3.1 Flash-Lite
java·javascript
序安InToo39 分钟前
第6课|注释与代码风格
后端·操作系统·嵌入式
xyy12339 分钟前
C#: Newtonsoft.Json 到 System.Text.Json 迁移避坑指南
后端
洋洋技术笔记42 分钟前
Spring Boot Web MVC配置详解
spring boot·后端
JxWang0542 分钟前
VS Code 配置 Markdown 环境
后端
navms1 小时前
搞懂线程池,先把 Worker 机制啃明白
后端
JxWang051 小时前
离线数仓的优化及重构
后端
Nyarlathotep01131 小时前
gin01:初探gin的启动
后端·go
JxWang051 小时前
安卓手机配置通用多屏协同及自动化脚本
后端