C++入门小馆:继承

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

我的博客:yuanManGan

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

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

1.继承的概念与定义

1.1继承的概念:

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

cpp 复制代码
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,就可以复用这些成员,就不需要重复定义了,省去了很多麻烦。

cpp 复制代码
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成员但无法访问它。

  1. 基类private成员在派⽣类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派⽣类 中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。
  2. 实际上⾯的表格我们进⾏⼀下总结会发现,基类的私有成员在派⽣类都是不可⻅。基类的其他成员 在 派 ⽣ 类 的 访 问 ⽅ 式 == Min (成员在基类的访问限定符,继承⽅式) ,public > protected > private。
  3. 使⽤关键字class时默认的继承⽅式是private,使⽤struct时默认的继承⽅式是public,不过最好显 ⽰的写出继承⽅式。

这个特点与struct的不写时默认的成员是public和class默认不写时的成员是private一样的。
5. 在实际运⽤中⼀般使⽤都是public继承,⼏乎很少使⽤protetced/private继承,也不提倡使⽤

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

际中扩展维护性不强。

cpp 复制代码
// 实例演⽰三种继承关系下基类成员的各类型成员访问关系的变化
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:

cpp 复制代码
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:这个我们后⾯类型转换章节再
单独专⻔讲解,这⾥先提⼀下)

cpp 复制代码
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的析构呢?我们编译器它有个逻辑,它先会自己调用子类的析构函数,然后再析构父类的对象,那为什么不能先析构父类的呢?如果我们析构了父类的对象,那如果后面我们还会用到父类的变量呢?我们这里就不用调用父类的析构了编译器会帮我们调用的。

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

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

  3. 因为多态中⼀些场景析构函数需要构成重写,重写的条件之⼀是函数名相同(这个我们多态章节会讲 解)。那么编译器会对析构函数名进⾏特殊处理,处理成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。

相关推荐
.格子衫.23 分钟前
014枚举之指针尺取——算法备赛
java·c++·算法
明月看潮生38 分钟前
青少年编程与数学 02-018 C++数据结构与算法 24课题、密码学算法
c++·算法·青少年编程·密码学·编程与数学
步行cgn41 分钟前
GZIPOutputStream 类详解
java·开发语言·intellij-idea
wuqingshun3141591 小时前
蓝桥杯 17. 通电
c++·算法·职场和发展·蓝桥杯·深度优先·动态规划
HelloZheQ1 小时前
Java:从入门到精通,你的编程之旅
java·开发语言
烦躁的大鼻嘎1 小时前
【Linux】深入理解Linux基础IO:从文件描述符到缓冲区设计
linux·运维·服务器·c++·ubuntu
李匠20241 小时前
C++负载均衡远程调用学习之HOOK注册机制
java·c++·学习·负载均衡
清同趣科研1 小时前
R绘图|3分钟复现瑞士“苏黎世大学”Nature全球地图——基于R包ggplot2+sf等
开发语言·r语言
purrrew1 小时前
【Java ee初阶】多线程(5)
java·java-ee
Cyanto2 小时前
Java使用JDBC操作数据库
java·开发语言·数据库