首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++ 进阶】继承(上):解锁代码复用的核心密码,体会代码复用的魅力!

【C++ 进阶】继承(上):解锁代码复用的核心密码,体会代码复用的魅力!

作者头像
用户11960591
发布2025-12-23 15:54:17
发布2025-12-23 15:54:17
1360
举报

前言:C++的三大核心特性是封装、继承和多态。在前文中,我们已经通过类和对象讲解了封装特性。接下来,本文将深入探讨C++继承机制的奥秘。

一、继承的概念及定义

1.1继承的概念

继承面向对象编程(OOP)中的核心机制之一,允许一个类(子类/派生类)基于另一个类(父类/基类)来构建子类自动获得父类的属性和方法,并可扩展或修改这些功能。继承呈现了⾯向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们触的函数层次的复⽤,继承是类设计层次的复⽤。

举个例子:

代码语言:javascript
复制
class Student
{
public :
	// 身份验证
	void identity()
	{} 

	// 学习
	void study()
	{
		cout << _name << ":正在学习" << endl;
	}
protected:
	string _name = "张三"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age = 18; // 年龄
	int _stuid; // 学号
};

class Teacher
{
	public :
	// 省份验证
	void identity()
	{}

	// 授课
	void teaching()
	{
		cout << _name << ":正在授课" << endl;
	}
protected:
	string _name = "李四"; // 姓名
	int _age = 25; // 年龄
	string _address; // 地址
	string _tel; // 电话
	string _title; // 职称
};

通过比较教师类和学生类可以发现,它们具有多个相同的特性:

共有成员变量: 姓名、年龄、电话、地址 共有成员函数: 身份验证

而它们的独有特性分别是:

教师类: 职称(成员变量)、授课(成员函数) 学生类: 学号(成员变量)、学习(成员函数)

既然存在这些共性,我们可以将共有属性提取到一个基类中,然后让教师类和学生类通过继承来复用这些属性。

1.2继承的定义

下面我们就来定义一个基类,然后让教师类和学生类来复用这些属性:

代码语言:javascript
复制
//定义一个person类定义共同特性
class Person
{
public:
	//身份验证
	void identity()
	{
		cout << "void identity()" << _name << endl;
	}
protected:
	string _name = "张三"; // 姓名
	string _address; // 地址
	string _tel; // 电话
	int _age; // 年龄
};
//定义一个student类继承person类
class Student : public Person
{
public:
	void print()
	{
		_name = "李四";
		_age = 18;
		cout << "姓名:" << _name << endl;
		cout << "学号:" << _stuid << endl;
	}
	// 学习
	void study()
	{
		cout << _name << ":正在学习" << endl;
	}
protected:
	int _stuid=241610101; // 学号
};
//定义一个teacher类继承person类
class Teacher : public Person
{
public:
	void print()
	{
		_name = "王五";
		_age = 25;
		cout <<"姓名:" << _name << endl;
		cout << "职称:" << _title << endl;
	}
	// 授课
	void teaching()
	{
		cout << _name << ":正在授课中" << endl;
	}

protected:
	string _title="老师"; // 职称
};

int main()
{
	Student s;
	s.print();
	s.study();

	Teacher t;
	t.print();
	t.teaching();

	return 0;
}

这里是引用
这里是引用

我们看到student类和teacher类的姓名都继承至person类,学号,职称,还有学习和授课(成员函数)是自身特有的,这样就实现了代码的复用。

通过上面的代码我们可以看到继承的定义格式:

在这里插入图片描述
在这里插入图片描述

Person是基类,也称作⽗类。Student是派⽣类,也称作⼦类。

在这里插入图片描述
在这里插入图片描述

1.3继承方式与访问方式的组合

以下是C++中基类成员在不同继承方式下于派生类中的访问权限表格:

基类成员访问权限 \ 继承方式

public 继承

protected 继承

private 继承

基类public 成员

派生类 public 成员

派生类protected 成员

派生类private 成员

基类protected 成员

派生类 protected 成员

派生类 protected 成员

派生类 private 成员

基类private 成员

在派生类中不可见

在派生类中不可见

在派生类中不可见

对于上面的这个表格有以下几点注意事项:

1.基类的私有成员无论以何种方式继承在派生类中均不可访问。这里的不可访问性是指,虽然 基类的私有成员确实存在于派生类对象中 ,但从语法层面限制了派生类对象 (无论在类内部还是外部)都无法直接访问这些成员。

2.基类的私有成员在派生类中不可访问。若希望基类成员类外不可直接访问,但在派生类中可访问,则应将其定义为protected。由此可见,保护成员访问修饰符是专门为继承机制而设计的。

3.通过上面的表格我们会发现:基类的私有成员在派生类都是不可见基类的其他成员在派生类的访问⽅式 == Min(成员在基类的访问限定符,继承⽅式),且遵循给public >protected > private。

例如: public继承

  • 父类的public成员,在派生类中任为public成员。
  • 父类的protect成员,在派生类任然为protect成员。
  • 父类的private成员在派生类不可见。

protect继承

  • 父类的public成员,在派生类中任为protect成员。
  • 父类的protect成员,在派生类任然为protect成员。
  • 父类的private成员在派生类不可见。

private继承以此类推

我们会发现,派生类对于基类的访问,要取决于继承方式和基类访问限定最小的那个!

4.在C++中,class默认采用private继承方式,而struct默认采用public继承方式。但为了代码清晰性,建议显式指定继承方式。

1.4继承类模板

代码语言:javascript
复制
#include<iostream>
#include<vector>
using namespace std;
//继承/组合
namespace my_stack
{
//=========================
//类模板:stack<T>
//继承自std::vector<T>的模板
//使用vector的特定来模拟栈
//=========================
	template<class T>
	class stack : public std::vector<T>
	{
		//不写public公有 默认就是私有 私有的话类外面访问不到
	public:
	
		void push(const T& x)
		{
			//C++中栈的底层是使用vector来实现的 所以是栈继承了vector
			//当基类(父类)是模板时,派生类(子类)在使用父类的函数时就要指定类域
			//否则就会编译报错说push_back找不到标识符
			// 因为stack<int>实例化时,也实例化vector<int>了
			// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到
			//push_back(x);
			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()
{
	bit::stack<int> st;
	st.push(1);
	st.push(2);
	st.push(3);
	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	} 
	return 0;
}

二、基类和派生类对象的赋值转换

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

代码语言:javascript
复制
int a=1;
const double& b=a;//不加const编译报错!

而在public继承中存在一个特殊处理:派生类对象可以直接赋值给基类指针或引用,且无需添加const限定。 此时指针或引用绑定的是派生类对象中基类部分(如下图所示)。这意味着基类指针或引用既可以指向基类对象,也可以指向派生类对象。

在这里插入图片描述
在这里插入图片描述

2、派生类对象赋值给基类对象是通过基类的拷贝构造函数或赋值运算符完成的(这两个函数的细节将在后续小节详细讲解)。这个过程类似于将派生类特有的成员部分"切除"因此也被称为对象切割或切片,如下图所示。

在这里插入图片描述
在这里插入图片描述

下面来看代码:

代码语言:javascript
复制
#include <iostream>
using namespace std;

// 基类
class Base {
public:
	int base_data;
};

// 派生类
class Derived : public Base {
public:
	int derived_data;

};

int main() {

	Derived d;
	d.base_data = 10;
	d.derived_data = 20;

	Base b;
	b = d;//发生切片
	cout << b.base_data << endl;//基类b只能看到base_data这一成员!!

	return 0;
}

切片的关键点 派生类对象 d 赋值给基类对象 b 时,编译器仅拷贝 Base 部分的成员(base_data),而 Derived 特有的成员(derived_data)会被丢弃这就是对象切片的本质。

通过指针或引用可以避免切片:

代码语言:javascript
复制
Base& ref = d;  // 通过引用访问,保留派生类完整性
Base* ptr = &d; // 通过指针访问,保留派生类完整性

此时的refptr引用/指向的就是整个派生类对象的基类部分,至于派生类特有的部分虽然看不到,但它还是存在派生类对象中,并没有发生切片!。

举个例子:

代码语言:javascript
复制
class Person
{
public:
	void Display()
	{
		cout << "姓名" << _name << endl;
		cout << "年龄" << _age << endl;
	}
protected:
	string _name; // 姓名
	string _sex; // 性别
	int _age; // 年龄
};
class Student : public Person
{
public:
	void study()
	{
		_name = "张三";
		cout << _name << "正在学习" << endl;
	}
public:
	int _No; // 学号
};
int main()
{
	Student sobj;

	//基类指针/引用指向/引用派生类对象
	Person* p= &sobj; //基类指针指向 student
	Person& rp = sobj;//引用绑定到 student

	//=========基类的指针访问派生类=========
	p->Display();//✅只能访问派生类中基类部分的成员
	//p->stduy();//❌语法上看不到派生类特有的部分

	//=========基类引用访问派生类===========
	rp.Display();//✅同上
	//rp.study();//❌

	//==============赋值问题==============

	// 派生类对象可以赋值给基类的对象是通过调用
	//Person pobj = sobj;

	//2.基类对象不能赋值给派生类对象,这里会编译报错
	//sobj = pobj;
	return 0;
}

注意:子类对象能赋值给父类对象,但父类对象不能赋值给子类对象!

三、继承中的作用域

谈到作用域,作用域机制主要用于解决命名冲突问题,在继承场景中也不例外。当基类和派生类出现同名变量或函数时,究竟会优先调用哪个?这就需要了解隐藏规则了。

3.1隐藏规则

  1. 在继承体系中基类和派生类都有独立的作用域
  2. 派生类基类 存在同名成员时派生类的成员会优先被访问,从而屏蔽基类中的同名成员,这种现象称为"隐藏"(如需访问基类的同名成员,可通过"基类::成员名"的方式显式调用)。
  3. 需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。
  4. 注意在实际中在继承体系里面最好不要定义同名的成员
代码语言:javascript
复制
class Person
{
protected:
	string _name = "张三"; // 姓名
	int _age = 20; // 年龄
};
class Student : public Person
{
public:
	void print()
	{
		cout << "姓名:" << _name << endl;
		//=================隐藏==============
		cout << "年龄:" << _age << endl;//这里的_age与派生类的_age就构成了隐藏关系 默认访问的是派生类的
		cout << "年龄:" << Person::_age << endl;//想要访问基类的_age 指定类域!!
	}
protected:
	int _stuid = 241610101; // 学号
	int _age = 18;
};
int main()
{
	Student t;
	t.print();
	return 0;
}

3.2继承作用域的两道笔试题

在这里插入图片描述
在这里插入图片描述

第一个问题:显然派生类B与基类A出现了同名函数,那么他们之间就构成隐藏关系! ⚠️区别于函数重载: 函数重载是在同一作用域下的同名函数(参数不同)才构成重载!而在继承中 基类派生类都有自己独立的作用域所以不可能构成重载!。 第二个问题: 在main函数中调用了两个fun函数,区别就是一个有参一个无参,有参的fun函数肯定调用的是派生类的fun函数 (未指定默认调用派生类的)。而无参的fun函数 既可以是派生类的fun也可以是基类的fun(函数名相同构成隐藏),编译器不知道是哪个fun函数所以就会报编译错误!

常见的一些报错的原因:

阶段

核心错误类型

典型表现/原因

编译期

语法错误

缺少分号、括号不匹配

类型不匹配

变量/返回值类型不符

符号未定义/冲突

变量未声明、宏冲突

依赖缺失

缺少头文件/库

运行时

内存访问错误

空指针、数组越界

运算错误

除零

资源耗尽

堆栈溢出、文件句柄耗尽

逻辑错误

算法实现错误

输入/数据问题

非法输入未校验、文件操作失败

其他

内存泄漏

未释放动态内存

精度/编码问题

浮点误差、字符编码错误

超时/硬件问题

操作超时、硬件故障

四、总结

本文系统讲解了继承的核心概念,首先明确定义了继承的基本原理,接着详细分析了不同继承方式与访问权限的组合应用。随后深入探讨了基类与派生类对象间的赋值转换问题,最后解析了继承体系中的作用域规则,并通过两道典型笔试题进行实战演练。 由于篇幅问题,后续的继承相关的内容将在下一篇文章介绍!

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

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

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、继承的概念及定义
    • 1.1继承的概念
    • 1.2继承的定义
    • 1.3继承方式与访问方式的组合
    • 1.4继承类模板
  • 二、基类和派生类对象的赋值转换
  • 三、继承中的作用域
    • 3.1隐藏规则
    • 3.2继承作用域的两道笔试题
  • 四、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档