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

C++入门小馆:继承

嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的passion。准备好和我一起冲进代码的奇幻宇宙了吗?Let's go!

我的博客:yuanManGan

我的专栏:C++入门小馆 C言雅韵集 数据结构漫游记  闲言碎语小记坊 题山采玉 领略算法真谛

说起继承,在现实世界中,比如有一天你给你爸爸打了个电话,你跟你爸爸述说你学习的认真,述说你的不容易,而你的爸爸听见了你这么辛苦,决定不再瞒着你了,儿啊其实我们家有几十套房子,你别学了,学出来还没有我出去收租钱多。你没有花费任何代价就继承了你爸的遗产,这就是继承。

1.继承的概念与定义

1.1继承的概念:

继承是针对类的,它的作用是增加复用。比如我们如果要实现一个图书管理系统,涉及到人物这些类中,有老师,学生等。我们描述老师和学生都会用到姓名啊地址电话年龄这些,如果我们分开实现这些就成了:

class Student
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){// ...}// 学习void study(){// ...}
protected:string _name = "peter"; // 姓名string _address; // 地址string _tel; // 电话int _age = 18; // 年龄int _stuid; // 学号
};
class Teacher
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){// ...}// 授课void teaching(){//...}
protected:string _name = "张三"; // 姓名int _age = 18; // 年龄string _address; // 地址string _tel; // 电话string _title; // 职称
};

下面这些都重复出现了。 

我们可以将公共部分放到一个Person类中,Student和Teacher都继承Person,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦。

class Person
{
public:// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证void identity(){cout << "void identity()" << _name << endl;}
protected:string _name = "张三"; // 姓名string _address; // 地址string _tel; // 电话int _age = 18; // 年龄
};
class Student : public Person
{
public:// 学习void study(){// ...}
protected:int _stuid; // 学号
};
class Teacher : public Person
{
public:// 授课void teaching(){//...}
protected:string title; // 职称
};
int main()
{Student s;Teacher t;s.identity();t.identity();return 0;
}

先来看看代码,下面讲解其继承的格式。

1.2.继承的定义:

1.2.1 定义格式

我们上面实现的Person继承给被人的类,称为父类也叫做基类。而Student和Teacher是子类,也称为派生类。

1.2.2继承基类成员访问⽅式的变化

虽然有那么多的继承方式,但真正常用的还是public继承的基类public和protected成员。

public类里面外面都可以访问
protected

类里面可以访问,类外面不能访问,

继承的时候派生类可以访问

private

类里面可以访问,类外面不能访问,

继承的时候派生类可以访问

1. 基类private成员在派⽣类中⽆论以什么⽅式继承都是不可⻅的。这⾥的不可⻅是指基类的私有成员 还是被继承到了派⽣类对象中,但是语法上限制派⽣类对象不管在类⾥⾯还是类外⾯都不能去访问 它。

这里继承的private成员但无法访问它。

2. 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类 中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

3. 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员 在 派 ⽣ 类 的 访 问 ⽅ 式  ==  Min (成员在基类的访问限定符,继承⽅式) ,public > protected > private。

4. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显 ⽰的写出继承⽅式。

这个特点与struct的不写时默认的成员是public和class默认不写时的成员是private一样的。

5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤

protetced/private继承,因为protetced/private继承下来的成员都只能在派⽣类的类⾥⾯使⽤,实

际中扩展维护性不强。

// 实例演⽰三种继承关系下基类成员的各类型成员访问关系的变化
class Person
{
public:void Print(){cout << _name << endl;}
protected:string _name; // 姓名
private:int _age; // 年龄
};
//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected:int _stunum; // 学号
};

1.3 继承类模板

我们可以利用继承写我们的stack:
namespace refrain
{//template<class T>//class vector//{};template<class T>class stack : public std::vector<T>{public:void push(const T& x){// 基类是类模板时,需要指定⼀下类域,// 否则编译报错:error C3861: “push_back”: 找不到标识符// 因为stack<int>实例化时,也实例化vector<int>了
// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到vector<T>::push_back(x);//push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}

但这里没有我们利用适配器写的爽。

2. 基类和派⽣类间的转换

public继承的派⽣类对象 可以赋值给 基类的指针 / 基类的引⽤。这⾥有个形象的说法叫切⽚或者切割寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。
基类对象不能赋值给派⽣类对象。
基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针 是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type
Information)的dynamic_cast 来进⾏识别后进⾏安全转换。(ps:这个我们后⾯类型转换章节再
单独专⻔讲解,这⾥先提⼀下)

class Person
{
public:Person() = default;//{}Person(Person& p): _name(p._name), _sex(p._sex), _age(p._age){}
protected:string _name; // 姓名string _sex; // 性别int _age; // 年龄
};
class Student : public Person
{
public:int _No; // 学号
};
int main()
{Student sobj;// 1.派⽣类对象可以赋值给基类的指针/引⽤Person* pp = &sobj;Person& rp = sobj;// 派⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的Person pobj = sobj;//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错//sobj = pobj;return 0;
}

3. 继承中的作⽤域

3.1 隐藏规则:

基类和派生类都有独立的作用域,意味着我们可以在基类和派生类定义同名的成员,但出现这种情况时,编译器会选择屏蔽掉对基类同名成员的直接访问,这种情况叫做隐藏,但我们可以指定作用域显式访问。(注意如果是成员函数的隐藏,只需要函数名相同即可)。

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

4.1 4个常⻅默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这 ⼏个成员函数是如何⽣成的呢?
1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造 函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。

2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。

3. 派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的
operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域

 

这里传入的是Student类型会进行赋值兼容转化,进行截断然后称为Peson类型进行运算。

4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。因为这样才能保证派 ⽣类对象先清理派⽣类成员再清理基类成员的顺序。
在派生类调用基类对象的析构时为什么会找不到Person的析构函数呢,这里的原因到多态哪里就能了解的更清楚,这里父类和子类的析构函数编译器会把它默认认为是一个名字,所以会构成隐藏,必须指定类域访问:

咦这里怎么会调用两次Person的析构呢?我们编译器它有个逻辑,它先会自己调用子类的析构函数,然后再析构父类的对象,那为什么不能先析构父类的呢?如果我们析构了父类的对象,那如果后面我们还会用到父类的变量呢?我们这里就不用调用父类的析构了编译器会帮我们调用的。

 5. 派⽣类对象初始化先调⽤基类构造再调派⽣类构造。

6. 派⽣类对象析构清理先调⽤派⽣类析构再调基类的析构。

7. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲 解)。那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加
virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系。

4.2 实现⼀个不能被继承的类

一个类怎么让他不被继承呢?

1.将构造放在私有:

我们在不使用Derive对象时不会报错,但我们一旦实例化就会报错。

2.加final关键字:

后面的方法更常用。

5. 继承与友元

继承和友元关系就一句话
友元关系不能继承,也就是说基类友元不能访问派⽣类私有和保护成员。

6. 继承与静态成员

我们的类成员在不同的类里面,继承出来的同名成员是有两份,构成隐藏关系。但静态成员就只有一个了。
基类定义了static静态成员,则整个继承体系⾥⾯只有⼀个这样的成员。⽆论派⽣出多少个派⽣类,都 只有⼀个static成员实例。

 

7. 多继承及其菱形继承问题

7.1 继承模型:

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承
多继承:⼀个派⽣类有两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型 是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯。
一句话总结,你继承了两个或更多的基类,你就是多继承。

 这个就是菱形继承:

我们发现Assistant有两个Person,造成了数据冗余和二义性 。

菱形继承:菱形继承是多继承的⼀种特殊情况。菱形继承的问题,从下⾯的对象成员模型构造,可以 看出菱形继承有数据冗余和⼆义性的问题,在Assistant的对象中Person成员会有两份。⽀持多继承就 ⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议 设计出菱形继承这样的模型的。

这就导致我们编译器不知道去访问那个_name

7.2 虚继承

很多⼈说C++语法复杂,其实多继承就是⼀个体现。有了多继承,就存在菱形继承,有了菱形继承就有 菱形虚拟继承,底层实现就很复杂,性能也会有⼀些损失,所以最好不要设计出菱形继承。多继承可以认为是C++的缺陷之⼀,后来的⼀些编程语⾔都没有多继承,如Java。
我们在继承了同一个类的对象冒号后加个virtual该类就成了虚函数,就能用来解决菱形继承的缺陷,即二义性和数据冗余。

这里的监视窗口能看见三个num但这里的监视窗口进行了优化,其实实际上只有一个name。 

 

我们可以设计出多继承,但是不建议设计出菱形继承,因为菱形虚拟继承以后,⽆论是使⽤还是底层 都会复杂很多。当然有多继承语法⽀持,就⼀定存在会设计出菱形继承,像Java是不⽀持多继承的, 就避开了菱形继承。

8.1 继承和组合

is-a是继承关系,is-c是有组合关系,如果能有is-c就用is-c。

 

 

相关文章:

  • Java 集合线程安全
  • 爬虫的应用
  • P5937 [CEOI 1999] Parity Game 题解
  • Linux54 源码包的安装、修改环境变量解决 axel命令找不到;getfacl;测试
  • 力扣-字符串-468 检查ip
  • XGBoost算法原理及Python实现
  • 使用 Azure DevSecOps 和 AIOps 构建可扩展且安全的多区域金融科技 SaaS 平台
  • 网狐系列三网通新钻石娱乐源码全评:结构拆解、三端实测与本地部署问题记录
  • 软考-软件设计师中级备考 11、计算机网络
  • 数据结构与算法:回溯
  • Redis 数据类型详解(一):String 类型全解析
  • GateWay使用
  • 【CISCO】Se2/0, Se3/0:串行口(Serial) 这里串口的2/0 和 3/0分别都是什么?
  • Python函数完全指南:从零基础到灵活运用
  • [特殊字符]Spring Boot 后台使用 EasyExcel 实现数据报表导出(含模板、样式、美化)
  • **Java面试:技术大比拼**
  • 【人工智能】大模型安全的深度剖析:DeepSeek漏洞分析与防护实践
  • 【C++】Docker常用语法
  • VirtualBox 创建虚拟机并安装 Ubuntu 系统详细指南
  • Ubuntu环境下使用uWSGI服务器【以flask应用部署为例】
  • 黔西市游船倾覆事故发生后,贵州省气象局进入特别工作状态
  • 自我田野|从城市搬到农村生活,我找回了真实和附近
  • 当一群杜克土木工程毕业生在三四十年后怀念大学的历史课……
  • 大众、学术和政治三重框架下的“汉末之变”
  • 研究完蚂蚁搬家,我好像明白了为什么我们总是堵车
  • 辽宁男篮被横扫这一晚,中国篮球的一个时代落幕了