首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >《C++ 继承》三大面向对象编程——继承:代码复用与功能扩展的核心机制

《C++ 继承》三大面向对象编程——继承:代码复用与功能扩展的核心机制

作者头像
用户11915063
发布2025-11-20 13:11:46
发布2025-11-20 13:11:46
270
举报

前言:

在面向对象编程的世界里,“避免重复” 与 “灵活扩展” 是开发者始终追求的目标,而 C++ 的继承机制正是实现这两个目标的核心工具。它让我们能够从已有的类(基类)中 “继承” 成熟的成员变量与成员函数,无需重新编写重复代码;同时又能在新类(派生类)中添加专属成员、重写原有函数,让类的功能随需求自然延伸。无论是模拟现实世界中 “动物与猫、狗” 的层级关系,还是开发中 “基础组件与定制组件” 的复用场景,继承都为代码的组织与维护提供了清晰的逻辑框架。理解继承,便是掌握 C++ 面向对象编程的关键一步


一、继承基础与概念解析

1.1 继承的核心概念
1.1.1 继承的本质与定义

继承(inheritance)本质是类设计层次的复用

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

假如我们现在要设计一套图书管理系统/学生管理系统,甚至于说学校的门禁系统(这里插一句题外话,),我们需要做的第一步就是设计定义出有哪些类(如学生、老师、保安、食堂阿姨)

有些信息/方法是独有的,有些则是公共的(大家都有)。同理,有些成员是公共的,每个都写出来就实在是太恶心了,为了方便,我们就要用到接下来我们要介绍的继承——复用——直接用别人的,只不过以前复用的是函数,现在的继承变成了类设计层面的复用

1.1.2 类体系设计方法论——第一步:设计定义出有哪些类

公共的特性单独放到一个类里面——这个类就叫做:基类/父类

1.1.3 继承机制的优势与简洁性

没有继承之前我们设计了两个类Student和Teacher,而这两个类里面都有姓名/地址电话/年龄等成员变量等,设计两个类的话,存在代码冗余,也不简洁,但Student类有自己单独的成员——学号,成员函数——学习,Teacher类有自己单独的成员——职称,成员函数——授课

代码语言:javascript
复制
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 = "Cx330"; // 姓名 

我们就可以把公共的成员放到一个类里面,这样就可以复用这个类,可以省去很多代码,使代码变得简洁

代码演示:

代码语言:javascript
复制
class Person
{
public:
	//进入校园/图书馆/实验室刷二维码等身份认证
	void identity()
	{
		cout << "void indetity" << _name << endl;
	}
	void func()
	{
		cout << _age << endl;
	}
protected:
	string _name = "Cx330";
	string _address;//地址
	string _tel;//电话
private:
	int _age = 18;
};

class Student :public Person
{
public:
	void study()
	{
		//...学习
		//基类私有成员,派生类不可见,语法上限制不能直接使用
		//cout << _age << endl;
		
		//但是可以间接使用
		func();
	}
protected:
	int _stuid;//学号
};

class Teacher : public Person
{
public:
	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
	void identity()
	{
		// ...
	}
	// 授课
	void teaching()
	{
		//...
	}
protected:
	string _title; // 职称
};

int main()
{
	Student s;
	Teacher t;
    
    s.identity();   
    s.study();

	return 0;
}

运行结果:

1.2 继承语法与访问控制
1.2.1 继承定义的标准格式

下面我们看到Person是基类,也称作父类Student是派生类,也称作子类

1.2.2 继承方式与访问权限详解

正是因为有三种继承方式和三种访问,所以才有了继承基类成员访问方式的九种变化

1.3 基类成员访问方式的九种组合变化

类成员/继承方式

public继承

protected继承

private继承

基类的public成员

派生类的public成员

派生类的protected成员

派生类的private成员

基类的protected成员

派生类的protected成员

派生类的protected成员

派生类的private成员

基类的private成员

在派生类中不可见

在派生类中不可见

在派生类中不可见

  1. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  2. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
  3. 实际上面的表格我们进行一下总结会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式 == Min(成员在基类的访问限定符,继承方式),即取Min(权限更小的),权限是public(公有) > protected(保护) > private(私有)
  4. 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式
  5. 在实际运用中一般使用都是public继承,几乎很少使用protetced / private继承,也不提倡使用protetced / private继承,因为protetced / private继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强
1.4 继承与类模板的结合应用
代码语言:javascript
复制
namespace Cx330
{
	template<class T>
	class stack : public vector<T>
	{
	public:
		void push(const T& x)
		{
			// 但是模版是按需实例化的,调用了哪个成员函数,就实例化哪个
			// 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()
{
	Cx330::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << endl;
		st.pop();
	}
 
	//// 但是模版是按需实例化的,调用了哪个成员函数,就实例化哪个
	//// 构造/析构/push_back会实例化,其他成员函数就不会实例化
	//vector<int> v;
	//v.push_back(1);
 
	return 0;
}

模版是按需实例化的,调用了哪个成员函数,就实例化哪个,这里构造/析构/push_back会实例化,其他成员函数就不会实例化


二、类型转换

2.1 基类与派生类类型转换原理
2.1.1 转换机制核心概念

1、通常情况下我们把一个类型的对象赋值给另一个类型的指针或者引用时,存在类型转换,中间会产生临时对象,所以需要加const,例如:

代码语言:javascript
复制
int i=l;
const double&d = i;

public继承中,就是一个特殊处理的例外,派生类对象可以赋值给基类的指针/基类的引用,而不需要加const,这里的指针和引用绑定是派生类对象中的基类部分,如下图所示。也就意味着一个基类的指针或者引用,可能指向基类对象,也可能指向派生类对象

2、派生类对象赋值给基类对象是通过基类的拷贝构造函数或者赋值重载函数完成的(这两个函数的细节后面小节会细讲),这个过程就像派生类自己定义部分成员切掉了一样,所以也被叫做切割或者切片,如下图

3、基类对象不能赋值给派生类对象

4、基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用 RTTI(Run-TimeTypeInformation)的dynamic_cast来进行识别后进行安全转换

2.2 类型转换的特殊处理规则
2.2.1 特殊处理
2.2.2 类型转换实践应用
代码语言:javascript
复制
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 = "11111";
	const string& rs = "11111";

	Student s;
	Person p = s;
	
	//特殊处理
	Person& rp = s;
	Person* ptr = &s;

	//不支持(p = s)父不能传子
	//s = (Student)p;

	return 0;
}

运行结果:


三、继承中的作用域规则

3.1 深度剖析名称“隐藏”规则
  1. 在继承体系中基类和派生类都有独立的作用域;
  2. 派生类和基类中有同名成员,派生类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏(在派生类成员函数中,可以使用基类::基类成员进行显示访问)
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏

注意:参数是否相同不重要

结局方案:

注意在实际中在继承体系里面最好不要定义同名的成员

3.2 同名成员的处理策略

Student的_num和Person的_num构成隐藏关系,这样代码虽然能跑,但是非常容易混淆

代码语言:javascript
复制
//派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问
class Person
{
protected:
	string _name = "⼩李⼦"; // 姓名 
	int _num = 111; // ⾝份证号 
};

class Student : public Person
{
public:
	void Print()
	{
		cout << _num << endl;
		cout <<Person:: _num << endl;
	}
protected:
	int _num = 999; // 学号 
};
3.3 继承作用域选择题精解

1、A和B类中的两个func构成什么关系()

A. 重载 B. 隐藏 C. 没关系

2、下面程序的编译运行结果是什么()

A. 编译报错 B. 运行报错 C. 正常运行

代码演示:

代码语言:javascript
复制
//隐藏
//需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,参数是否相同不重要
class A
{
public:
	void fun()
	{
		cout << "func()" << endl;
	}
};
class B : public A
{
public:
	void fun(int i)
	{
		cout << "func(int i)" << i << endl;
	}
};

int main()
{
	//Student s;
	//s.Print();//999

	B b;
	b.fun(10);
	//b.fun();//error
	b.A::fun();//解决方案

	return 0;
}

运行:

3.4 作用域规则综合总结

1、同名函数,只看函数名,不看参数,参数是否相同不重要; 2、b.fun(); // 报错;解决方案:b.A::fun(); 3、实践中不建议定义同名函数,完全是坑自个儿


四、派生类的默认成员函数专题

4.1 派生类默认成员函数解析(前四类)

6个默认成员函数,我们之前在类和对象就已经见识过了,默认的意思就是指我们不写,编译器会变我们自动生成一个,那么在派生类中,这几个成员函数是如何生成的呢

4.2 成员函数生成的核心机制
  1. 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用
  2. 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  3. 派生类的operator=必须要调用基类的operator=完成基类的复制。需要注意的是派生类的operator=隐藏了基类的operator=,所以显示调用基类的operator=,需要指定基类作用域
  4. 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序
  5. 派生类对象初始化先调用基类构造再调派生类构造
  6. 派生类对象析构清理先调用派生类析构再调基类的析构
  7. 因为多态中一些场景析构函数需要构成重写,重写的条件之一是函数名相同(这个我们多态章节会讲解)。那么编译器会对析构函数名进行特殊处理,处理成destructor),所以基类析构函数不加virtual的情况下,派生类析构函数和基类析构函数构成隐藏关系
4.3 构造函数和析构函数专题
4.3.1 构造与析构实践演练

首先我们思考两个问题:

1、我们不写,默认生成的函数行为是什么?是否符合需求 2、不符合,我们要自己实现,如何实现?

代码语言:javascript
复制
class Person
{
public:
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << endl;
	}

	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}

	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名 
};

本质:可以把派生类当做多一个的自定义类型成员变量(基类)的普通类

代码语言:javascript
复制
class Student : public Person
{
public:
	Student(const char* name = "张三", int num = 18, string address = "郑州")
		:Person(name)
		,_num(num)
		,_address(address)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		:Person(s)
		,_num(s._num)
		,_address(s._address)
	{
		//深拷贝,需要自己实现
	}

	//隐藏基类的赋值重载
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
			_address = s._address;
		}
		return *this;
		//深拷贝,需要自己实现
	}

	~Student()
	{
		//因为多态中⼀些场景析构函数需要构成重写,
		//那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),
		// 所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系
		//不要显示调用基类析构,编译器会在派生类析构结束后自动调用基类析构
		//Person::~Person();
		cout << "~Student()" << endl;
	}
	//构造先父后子,析构先子后父,如果显式调用,无法保证先子后父的析构顺序
protected:
	int _num; //学号 
	string _address;//地址
};

继承的基类成员变量(整体对象)+ 自己的成员变量(遵循普通的规则,跟类和对象部分一样)默认生成的构造,派生类自己的成员,内置类型不确定,自定义类型调用默认构造,基类部分调用默认构造本质上可以把派生类当做多了自定义类型成员变量,(基类)的普通类总,跟普通类原则一样。派生类一般要自己的实现构造,不显示写析构、拷贝析构、赋值重载,除非派生类有深拷贝的资源

4.3.2 【内存泄漏问题】多态环境下的析构函数陷阱

运行:

思考:为什么Person会析构两次呢?

4.3.3 原因

(1)核心的原因就是因为不用显示调用基类析构,编译器会在派生类析构结束之后自动调用析构,如果显示调用父类析构,无法保障先子后父的析构顺序。

(2)隐式的原因就是为了安全性,如下图所示

4.3.4方案验证测试

再来个main函数,我们测试一下构造和析构

运行结果:

4.3.5 构造析构规则总结

1、跟普通类的原则基本上一样; 2、派生类一般要自己实现构造,不需要写拷贝构造、析构、赋值重载; 3、除非派生类有深拷贝的资源


完整代码:

Test.cpp:
代码语言:javascript
复制
#include<iostream>
#include<vector>
#include<list>
using namespace std;

//class Person
//{
//public:
//	//进入校园/图书馆/实验室刷二维码等身份认证
//	void identity()
//	{
//		cout << "void indetity" << _name << endl;
//	}
//	void func()
//	{
//		cout << _age << endl;
//	}
//protected:
//	string _name = "Cx330";
//	string _address;//地址
//	string _tel;//电话
//private:
//	int _age = 18;
//};
//
//class Student :public Person
//{
//public:
//	void study()
//	{
//		//...学习
//		//基类私有成员,派生类不可见,语法上限制不能直接使用
//		//cout << _age << endl;
//		
//		//但是可以间接使用
//		func();
//	}
//protected:
//	int _stuid;//学号
//};
//
//class Teacher : public Person
//{
//public:
//	// 进⼊校园/图书馆/实验室刷⼆维码等⾝份认证
//	void identity()
//	{
//		// ...
//	}
//	// 授课
//	void teaching()
//	{
//		//...
//	}
//protected:
//	string _title; // 职称
//};
//
//int main()
//{
//	Student s;
//	Teacher t;
//
//	s.identity();
//	s.study();
//
//	return 0;
//}

//namespace Cx330
//{
//	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();
//		}
//	};
//}
//
//int main()
//{
//	Cx330::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;
//}

//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 = "11111";
//	const string& rs = "11111";
//
//	Student s;
//	Person p = s;
//	
//	//特殊处理
//	Person& rp = s;
//	Person* ptr = &s;
//
//	//不支持(p = s)父不能传子
//	//s = (Student)p;
//
//	return 0;
//}

//派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问
//class Person
//{
//protected:
//	string _name = "⼩李⼦"; // 姓名 
//	int _num = 111; // ⾝份证号 
//};
//
//class Student : public Person
//{
//public:
//	void Print()
//	{
//		cout << _num << endl;
//		cout <<Person:: _num << endl;
//	}
//protected:
//	int _num = 999; // 学号 
//};

//隐藏
//需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏,参数是否相同不重要
//class A
//{
//public:
//	void fun()
//	{
//		cout << "func()" << endl;
//	}
//};
//class B : public A
//{
//public:
//	void fun(int i)
//	{
//		cout << "func(int i)" << i << endl;
//	}
//};
//
//int main()
//{
//	//Student s;
//	//s.Print();//999
//
//	B b;
//	b.fun(10);
//	//b.fun();//error
//	b.A::fun();//解决方案
//
//	return 0;
//}

class Person
{
public:
	Person(const char* name = "peter")
		: _name(name)
	{
		cout << "Person()" << endl;
	}

	Person(const Person& p)
		: _name(p._name)
	{
		cout << "Person(const Person& p)" << endl;
	}

	Person& operator=(const Person& p)
	{
		cout << "Person operator=(const Person& p)" << endl;
		if (this != &p)
			_name = p._name;

		return *this;
	}

	~Person()
	{
		cout << "~Person()" << endl;
	}
protected:
	string _name; // 姓名 
};
//
class Student : public Person
{
public:
	Student(const char* name = "张三", int num = 18, string address = "郑州")
		:Person(name)
		,_num(num)
		,_address(address)
	{
		cout << "Student()" << endl;
	}

	Student(const Student& s)
		:Person(s)
		,_num(s._num)
		,_address(s._address)
	{
		//深拷贝,需要自己实现
	}

	//隐藏基类的赋值重载
	Student& operator=(const Student& s)
	{
		if (this != &s)
		{
			Person::operator=(s);
			_num = s._num;
			_address = s._address;
		}
		return *this;
		//深拷贝,需要自己实现
	}

	~Student()
	{
		//因为多态中⼀些场景析构函数需要构成重写,
		//那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),
		// 所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系
		//不要显示调用基类析构,编译器会在派生类析构结束后自动调用基类析构
		//Person::~Person();
		cout << "~Student()" << endl;
	}
	//构造先父后子,析构先子后父,如果显式调用,无法保证先子后父的析构顺序
protected:
	int _num; //学号 
	string _address;//地址
};

//继承的基类成员变量(整体对象)+自己的成员变量(遵循普通的规则,跟类和对象部分一样)

//本质可以把派生类当作多了一个自定义类型成员变量(基类)的普通类,跟普通类原则基本一样
//派生类一般要自己实现构造,不需要显示写析构、拷贝、赋值重载,除非派生类由深拷贝的资源需要处理

int main()
{
	Student s1;
	//Student s2("小明", 10);
	//Student s3(s2);

	//s1 = s3;

	return 0;
}

结尾

往期回顾:

《拿下C++ 模板进阶!》:带你从模板分类与特点到实战的每一个细节!

结语:继承作为 C++ 面向对象编程的三大特性(封装、继承、多态)之一,是连接 “通用类” 与 “专用类” 的桥梁。它通过代码复用减少重复开发,通过功能扩展满足个性化需求,同时又通过访问控制和继承方式保障代码的安全性与灵活性。掌握继承的概念、关键要素与使用原则,不仅能提升代码效率,更能帮助我们构建逻辑清晰、易于维护的面向对象系统

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-11-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言:
  • 一、继承基础与概念解析
    • 1.1 继承的核心概念
      • 1.1.1 继承的本质与定义
      • 1.1.2 类体系设计方法论——第一步:设计定义出有哪些类
      • 1.1.3 继承机制的优势与简洁性
    • 1.2 继承语法与访问控制
      • 1.2.1 继承定义的标准格式
      • 1.2.2 继承方式与访问权限详解
    • 1.3 基类成员访问方式的九种组合变化
    • 1.4 继承与类模板的结合应用
  • 二、类型转换
    • 2.1 基类与派生类类型转换原理
      • 2.1.1 转换机制核心概念
    • 2.2 类型转换的特殊处理规则
      • 2.2.1 特殊处理
      • 2.2.2 类型转换实践应用
  • 三、继承中的作用域规则
    • 3.1 深度剖析名称“隐藏”规则
    • 3.2 同名成员的处理策略
    • 3.3 继承作用域选择题精解
    • 3.4 作用域规则综合总结
  • 四、派生类的默认成员函数专题
    • 4.1 派生类默认成员函数解析(前四类)
    • 4.2 成员函数生成的核心机制
    • 4.3 构造函数和析构函数专题
      • 4.3.1 构造与析构实践演练
      • 4.3.2 【内存泄漏问题】多态环境下的析构函数陷阱
      • 4.3.3 原因
      • 4.3.4方案验证测试
      • 4.3.5 构造析构规则总结
  • 完整代码:
    • Test.cpp:
  • 结尾
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档