深入浅出 C++ 继承:从基础概念到模板、转换与作用域的实战指南

目录

  • 前言
  • 一、继承的概念及定义
    • [1.1 继承的概念](#1.1 继承的概念)
    • [1.2 继承的定义](#1.2 继承的定义)
      • [1.2.1 定义格式](#1.2.1 定义格式)
      • [1.2.2 继承基类成员访问方式的变化](#1.2.2 继承基类成员访问方式的变化)
    • [1.3 继承类模板](#1.3 继承类模板)
  • 二、基类和派生类之间的转换
  • 三、继承中的作用域
    • [3.1 隐藏规则](#3.1 隐藏规则)
    • [3.2 考察继承作用域相关选择题](#3.2 考察继承作用域相关选择题)
      • [3.2.1 A和B类中的两个func构成什么关系()](#3.2.1 A和B类中的两个func构成什么关系())
      • [3.2.2 下面程序的编译运行结果是什么()](#3.2.2 下面程序的编译运行结果是什么())
  • 结语

🎬 云泽Q个人主页
🔥 专栏传送入口 : 《C语言》《数据结构》《C++》《Linux》《蓝桥杯系列

⛺️遇见安然遇见你,不负代码不负卿~


前言

大家好啊,我是云泽Q,欢迎阅读我的文章,一名热爱计算机技术的在校大学生,喜欢在课余时间做一些计算机技术的总结性文章,希望我的文章能为你解答困惑~

一、继承的概念及定义

1.1 继承的概念

一、继承的核心定义

继承(inheritance)是面向对象程序设计中实现代码复用 的最重要手段,它允许我们在保持原有类(父类 / 基类)特性的基础上,派生出新的类(子类 / 派生类)。子类可以直接复用父类的属性(成员变量)和行为(成员函数),同时还能扩展新的属性和行为。

继承体现了面向对象的层次化设计思想 ,反映了 "从简单到复杂" 的认知规律,是类设计层次的复用(区别于函数层次的复用)。

二、继承解决的痛点:代码冗余

在没有继承的场景中,多个相似类会存在大量重复的成员,导致代码冗余、维护成本高:

  • 比如图片中的Student、Teacher、食堂阿姨等类,都包含姓名、年龄、电话、地址等重复的成员变量,以及identity()身份认证等重复的成员函数。
  • 这些重复代码不仅增加了开发工作量,也容易出现逻辑不一致的问题。

三、继承的实现思路

  1. 提取公共特性
    将多个相似类的公共成员(变量 + 函数)抽取到一个公共的父类(基类)中,比如图片中的Person类,包含id、姓名、年龄、电话、家庭地址等公共属性,以及identity()公共方法。
  2. 子类继承父类
    让需要复用公共特性的类(如Student、Teacher)作为子类,通过继承语法(如 C++ 中class Student : public Person)继承父类的所有成员(受访问权限控制)。
  3. 子类扩展独有特性
    子类只需在父类基础上,添加自己独有的成员变量和成员函数即可。
    例如Student类增加学号、父母信息、study()方法;
    Teacher类增加职称、紧急联系人电话、teaching()方法。

四、代码对比:无继承 vs 有继承

  1. 无继承的冗余代码
cpp 复制代码
// Student类
class Student
{
public:
    void identity() { /* 身份认证逻辑 */ }
    void study() { /* 学习逻辑 */ }
protected:
    string _name;
    string _address;
    string _tel;
    int _age;
    int _stuid; // 独有属性
};

// Teacher类
class Teacher
{
public:
    void identity() { /* 重复的身份认证逻辑 */ }
    void teaching() { /* 授课逻辑 */ }
protected:
    string _name; // 重复属性
    string _address; // 重复属性
    string _tel; // 重复属性
    int _age; // 重复属性
    string _title; // 独有属性
};
  1. 有继承的优化代码
cpp 复制代码
#include<iostream>
using namespace std;

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 teacher()
	{
		//...
	}
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. 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管是在类里面还是类外面都不能去访问它

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

  3. 实际上面的表格我们进行一下总结就会发现,基类的私有成员在派生类都是不可见。基类的其他成员在派生类的访问方式==Min(成员在基类的访问限定符,继承方式),public > protected > private。

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

  5. 在实际运用中一般使用都是 public 继承,几乎很少使用 protected/private 继承,也不提倡使用 protected/private 继承,因为 protected/private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强

1.3 继承类模板

假设现在要自己实现一个栈,我前面的文章中栈的内部是用适配器来实现的(用vector或list来存储操作),现在也可以通过继承的方式来实现。

cpp 复制代码
#include<iostream>
#include<vector>
#include<list>
using namespace std;

namespace yunze
{
	template<class T>
	class stack : public std::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()
{
	yunze::stack<int> st;
	st.push_back(1);
	st.push_back(2);
	st.push_back(3);
	while (!st.empty())
	{
		cout << st.top() << endl;
		st.pop();
	}
	//模板是按需实例化,调用了哪个成员函数,就实例化哪个
	//这里实例化构造,析构,push_back,其他成员函数就不会实例化
	vector<int> v;
	v.push_back(1);
	return 0;
}

继承类模板和继承普通类是有区别的,下面结合代码说一下二者的区别

  1. 基类的本质:具体类 vs 模板类
  • 继承普通类
    基类是已经定义完成的具体类 (比如 Person 类),它的成员变量、成员函数都是确定的,不需要额外参数就能直接使用。
    例如:class Student : public Person 中,Person 是一个具体的类,结构和行为完全明确。
  • 继承类模板
    基类是类模板 (比如 std::vector< T >),它本身不是一个可直接实例化的类,必须传入模板参数(如 int、string)才能生成具体的类。
    例如:template< class T > class stack : public std::vector< T > 中,std::vector< T > 是模板,只有传入 T(如 int)后,才会实例化为 std::vector< int > 这个具体类。
  1. 派生类的定义形式:普通类 vs 模板类
  • 继承普通类
    派生类可以是普通类,语法简洁,直接继承具体类即可。
cpp 复制代码
// 普通类继承
class Student : public Person { ... };

继承类模板

派生类必须是模板类,因为要和基类的模板参数关联。需要先声明模板参数,再继承带参数的基类模板。

cpp 复制代码
// 继承类模板:派生类必须是模板类
template<class T>
class stack : public std::vector<T> { ... };
  1. 基类成员的访问方式:直接访问 vs 显式域限定
  • 继承普通类
    子类可以直接访问基类的成员(受访问权限控制),编译器能明确找到基类的成员,无需额外限定。
    例如:Student 类中可以直接调用基类的 identity(),或者访问 _name(如果是 protected 修饰)。
  • 继承类模板
    由于基类是模板,编译器在模板实例化前无法确定基类的具体成员(模板是 "按需实例化 " 的),因此访问基类成员时需要显式指定基类的域,否则会出现编译错误( error C3861: "push_back": 找不到标识符)。
cpp 复制代码
void push(const T& x)
{
    // 必须显式指定基类域:vector<T>::push_back
    vector<T>::push_back(x);
    // 不能直接写 push_back(x),编译器无法确定该成员来自基类
}

(也可以用 this->push_back(x) 来解决依赖名称的查找问题,效果等价)

再补充一下继承的概念,继承并不是把父类的代码往子类中拷贝一份,只是实例化子类的对象时,子类的对象由两部分构成,一部分是父类的,一部分是自己的,其次调用子类的函数时,首先会在派生类找,派生类找不到才会去父类中找。

  1. 实例化的时机与方式:直接实例化 vs 模板参数驱动实例化
  • 继承普通类
    基类是具体类,派生类可以直接实例化对象,无需额外参数。
cpp 复制代码
Student s; // 直接创建对象
  • 继承类模板
    派生类是模板类,必须先传入模板参数实例化出具体类,才能创建对象。同时,基类的实例化会跟随派生类的模板参数自动触发
cpp 复制代码
// 先实例化 stack<int>,同时自动实例化基类 vector<int>,且只会实例化vector的构造和析构
yunze::stack<int> st;
st.push(1);

二、基类和派生类之间的转换

三、继承中的作用域

3.1 隐藏规则

3.2 考察继承作用域相关选择题

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

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


结语

相关推荐
玖釉-2 小时前
[Vulkan 实战] 深入解析 Vulkan Compute Shader:实现高效 N-Body 粒子模拟
c++·windows·图形渲染
a***59262 小时前
C++跨平台开发:挑战与实战指南
c++·c#
Li_yizYa2 小时前
谈谈Java集合中的fail-fast和fail-safe
java·开发语言
十五年专注C++开发2 小时前
CMake进阶:模块模式示例FindOpenCL.cmake详解
开发语言·c++·cmake·跨平台编译
蜜汁小强2 小时前
macOS 上管理不同版本的python
开发语言·python·macos
肥硕之虎2 小时前
从原理到实操:php://filter 伪协议玩转文件包含漏洞
开发语言·网络安全·php
a努力。2 小时前
中国电网Java面试被问:RPC序列化的协议升级和向后兼容
java·开发语言·elasticsearch·面试·职场和发展·rpc·jenkins
csbysj20202 小时前
Bootstrap4 徽章(Badges)
开发语言
码农水水2 小时前
得物Java面试被问:大规模数据的分布式排序和聚合
java·开发语言·spring boot·分布式·面试·php·wpf