一. 继承的概念与定义:怎么让类 “复用” 代码?

先想一个场景:Student 和 Teacher 都需要 “姓名、地址、身份认证”,但 Student 有学号、Teacher 有职称。如果各自写一遍,代码会很冗余 —— 继承就是把 “公共部分” 抽成父类(基类),子类(派生类)直接复用。

本篇博客代码示例中所需头文件

代码语言:javascript

AI代码解释

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<list>
using namespace std;

1.1 继承的核心概念
  • 父类(基类):存放公共成员的类,比如Person类(包含姓名、地址、identity身份认证函数)。
  • 子类(派生类):继承父类并扩展专属成员的类,比如Student(加学号)、Teacher(加职称)。
  • 本质:子类是父类的 “扩展”,能直接用父类的公共 / 保护成员,不用重复定义。

在这里插入图片描述

在这里插入图片描述

1.2 继承的定义格式

关键是 “继承方式 + 父类名”,比如 class Student : public Person。注意两点:

  • class默认私有继承,struct默认公有继承,推荐显式写继承方式(比如public)。
  • 继承方式会影响父类成员在子类中的访问权限(后面会讲)。

在这里插入图片描述

在这里插入图片描述

有了上面的知识储备后,我们来看一段代码示例来加深理解(注意看注释)

代码语言:javascript

AI代码解释

//基类/父类
class Person
{
// 公共成员:子类和类外都能访问
public:
	//进入校园/图书馆/实验室刷二维码等身份认证
	void identity()
	{
		cout << "void identity():" << _name << endl;
	}
	void func()
	{
		cout << _age << endl;
	}

// 保护成员:子类能访问,类外不能访问(专门为继承设计)
protected:
	string _name = "赵四";//姓名
	string _address;//地址
	string _tel;//电话

// 私有成员:子类和类外都不能直接访问(像“爸爸的私房钱”)
private:
	int _age = 18;//年龄
};

// 子类Student:公有继承Person
//class的话不写默认是私有继承,struct是公有继承
// class Student:Person
class Student : public Person
{
public:
	//学习
	void study()
	{
		//……
		//基类私有成员(爸爸的私房钱),派生类	中不可见,语法限制上不能直接使用
		//cout << _age << endl;
		
		//父类公有函数能间接访问私有成员
		func();
	}
protected://在继承中保护用的比较多
	int _stuid;//学号
};

// 子类Teacher:公有继承Person
class Teacher : public Person
{
public:
	//授课
	void teaching()
	{
		//…………
	}
protected:
	string title;//职称
};

// 测试:子类能直接用父类的函数
int main()
{
	Student s;
	Teacher t;
	s.identity();// 用父类的identity,输出“赵四”
	s.study();// 用子类的study,调用父类的func,输出了18

	return 0;
}

在这里插入图片描述

在这里插入图片描述

1.3 继承方式与成员访问权限

父类成员在子类中的访问权限,取决于 “父类的访问限定符” 和 “继承方式”,核心规则是:访问权限 = 两者中更严格(可以理解为Min)的那个public > protected > private)。

我们用表格总结一下(重点记public继承,实际开发最常用):

父类成员类型

public继承(推荐)

protected继承

private继承

父类public成员

子类中为public

子类中为protected

子类中为private

父类protected成员

子类中为protected

子类中为protected

子类中为private

父类private成员

不可见(不可访问)

不可见(不可访问)

不可见(不可访问)

关键提醒

  • 父类private成员无论怎么继承都 “不可见”(但是实际上是存在的)—— 子类想访问,只能通过父类的公有函数(比如上面的func())。
  • protected是为继承设计的:既不让类外访问,又能让子类用。
  • 在实际运用中一般都还是 public 继承,几乎很少使用 protected/private 继承。也不提倡使用后两者,因为它们继承下来的成员实际中扩展维护性不强,受到的限制比公有继承多。

我们这里就拿我们之前的Stack来看,可以使用继承来实现(注意看注释)

代码语言:javascript

AI代码解释

namespace Lotso
{
	template<class T>
	class stack : public vector<T>
	{
	public:
		void push(const T& x)
		{
			// 基类是类模板时,需要指定⼀下类域,
			// 否则编译报错:error C3861: “push_back”: 找不到标识符
			// 因为stack<int>实例化时,也实例化vector<int>了
			// 但是模版是按需实例化,调用了那个成员函数就实例化那个,push_back等成员函数未实例化,所以找不到
			vector<T>::push_back(x);
		}

		void pop()
		{
			vector<T>::pop_back();
		}

		const T& top()
		{
			return vector<T>::back();
		}

		bool empty()
		{
			return vector<T>::empty();
		}
	};
}

int main()
{
	Lotso::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}

	// 但是模版是按需实例化,调用了哪个成员函数,就实例化哪个
	// 构造/析构/push_back, 其他成员函数就不会实例化
	//vector<int> v;
	//v.push_back(1);

	return 0;
}

在这里插入图片描述

在这里插入图片描述


二. 基类与派生类的转换:子类对象能当父类用吗?

这是继承的核心特性之一,简单说:子类对象能隐式转换成父类对象 / 指针 / 引用,反之不行。

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

在这里插入图片描述

在这里插入图片描述

代码示例(注意看注释)

代码语言:javascript

AI代码解释

class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};

class Student : public Person
{
public:
	int _No; // 学号
};

int main()
{
	int i = 1;
	double d = i;
	const double& rd = i;

	string s1 = "1111";
	const string& rs = "111111";
	//上面的转换在之前类和对象以及其它的一些地方都讲到过,这里就不说了,主要是对比

	Student sobj;
	// 1. 子类对象 → 父类指针/引用(隐式转换,安全)
	Person* pp = &sobj;// 父类指针指向子类对象的“父类部分”
	Person& rp = sobj;// 父类引用引用子类对象的“父类部分”

	// 2. 子类对象 → 父类对象(调用父类拷贝构造,只拷贝父类部分)
	// 派生类对象可以赋值给基类的对象是通过调用后面会讲解的基类的拷⻉构造完成的
	Person pobj = sobj;

	//3. 父类对象 → 子类对象(编译报错,不安全)
	//sobj = pobj;//错误:父类没有子类的成员(比如学号)
	//sobj = (Student)pobj;// 强制转换也不行

	return 0;
}

为什么? 子类包含 “父类部分 + 自己的部分”,把子类当父类用,只会用到 “父类部分”,不会越界;但父类没有子类的成员,强行转子类会访问不存在的内容(比如学号),所以禁止。


三. 继承中的作用域:同名成员会冲突吗?

父类和子类有独立的作用域,如果出现同名成员(变量或函数),子类会 “隐藏” 父类的同名成员 ——— 这就是 “隐藏规则”,很容易踩坑。

  • 在继承体系中基类和派生类都有独立的作用域
  • 派生类和基类有同名成员,派生类成员将屏蔽基类对同名成员2的直接访问,这种情况叫做隐藏。(在派生类和成员函数中,可以使用基类::基类成员 显示访问)
  • 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏(参数什么的不重要)。

避坑提醒:继承体系中,尽量不要定义同名成员—— 如果必须同名,访问时一定要加父类作用

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐