Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >C++类间的 “接力棒“ 传递:继承(上)

C++类间的 “接力棒“ 传递:继承(上)

作者头像
DARLING Zero two
发布于 2025-04-04 01:21:53
发布于 2025-04-04 01:21:53
7000
代码可运行
举报
文章被收录于专栏:C语言C语言
运行总次数:0
代码可运行
本篇将开启 C++ 三大特性中的继承篇章,继承是一种派生类能够复用基类的代码,同时还能添加自己特有的属性和方法,或者对基类的方法进行重写。这种机制可以提高代码的复用性和可维护性

1.什么是继承?

1.1 继承的概念

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

️举个例子

  • 学生和老师都有的共同点(Person): 年龄,性别,名字等
  • 学生特有的(Student): 学号,专业,宿舍号
  • 老师特有的(Teacher): 职工号,职称

共同点就相当于一个基底,称他为基类或者父类,在基类的基础上拓展出来的各种各样的角色称他为派生类或者子类,这样一个拓展的过程就叫继承,所以继承的本质是一种复用

1.2 继承的语法

Person 是父类,也称作基类。Student 是子类,也称作派生类

其语法为:

表示 Studentpublic 继承于 Person,那么这个继承方式和类内部的 public 有何区别?

🚩类内部的 public 这一类的叫访问限定符,表示访问时类内部的变量函数等是以何种方式被访问,只使用访问限定符时 privateprotected 是没有区别的

  • private:成员被声明为 private 后,只能在类的内部被访问和调用,类外部及派生类都无法直接访问
  • protected:类内部可以访问,类的派生类也可以访问,但类外部不能访问

🚩派生类后跟的 public 这一类叫继承方式

🚩那么继承最重要的就是访问限定符和继承方式的组合,组合起来决定了基类成员在派生类中的访问属性

类成员/继承方式

public继承

protected继承

private继承

基类的public成员

派生类的 public 成员

派生类的 protected 成员

派生类的 private 成员

基类的protected成员

派生类的 protected 成员

派生类的 protected 成员

派生类的 private 成员

基类的private成员

在派生类中不可见

在派生类中不可见

在派生类中不可见

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

🔥值得注意的是:

  1. 基类 private 成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它
  2. 基类 private 成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为 protected。可以看出保护成员限定符是因继承才出的
  3. 使用关键字 class 时默认的继承方式是 private,使用 struct 时默认的继承方式是public,不过最好显示的写出继承方式
  4. 在实际运用中一般使用都是 public 继承,几乎很少使用 protetced / private 继承,也不提倡使用 protetced / private 继承,因为 protetced / private 继承下来的成员都只能在派生类的类里面使用,实际中扩展维护性不强

2.基类和派生类的转换机制

Student 是子类,Person 是父类

因为子类包含了父类的内容,且子类其实是父类的一种特殊类型,存在天然的类型兼容性,所以只能子类赋值给父类,且中间不存在类型转换,是以切割 / 切片的形式

为什么说不存在类型转换?举个例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Parent 
{
public:
    int x;

    Parent()
    { }
};

class Child : public Parent 
{
public:
    int y;

    Child()
    { }
};

int main() 
{
    Child child;
    Parent parent;
    cout << "Size of Child: " << sizeof(child) << endl;
    cout << "Size of Parent before assignment: " << sizeof(parent) << endl;
    parent = child;
    cout << "Size of Parent after assignment: " << sizeof(parent) << endl;
    return 0;
}

可以使用 sizeof 运算符获取父类和子类对象的大小,然后将子类对象赋值给父类对象后,再获取父类对象的大小,比较赋值前后父类对象大小是否发生变化,如果是切片,父类对象大小不会改变,因为只是复制了子类中父类部分的成员

🔥值得注意的是:

基类的指针或者引用可以通过强制类型转换赋值给派生类的指针或者引用。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)dynamic_cast 来进行识别后进行安全转换(ps:这个我们后面再讲解,这里先了解一下)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Person
{
protected:
	string _name; // 姓名
	string _sex;  // 性别
	int _age; // 年龄
};

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

void Test()
{
	Student sobj;
	
	// 1.子类对象可以赋值给父类对象/指针/引用
	Person pobj = sobj;
	Person* pp = &sobj;
	Person& rp = sobj;

	//2.基类对象不能赋值给派生类对象
	sobj = pobj;

	// 3.基类的指针可以通过强制类型转换赋值给派生类的指针
	pp = &sobj
	Student * ps1 = (Student*)pp; // 这种情况转换时可以的。
	ps1->_No = 10;

	pp = &pobj;
	Student* ps2 = (Student*)pp; // 这种情况转换时虽然可以,但是会存在越界访问的问题
	ps2->_No = 10;
}

第三种情况的第二种,ppPerson 类型的指针,它指向一个 Person 对象。接着把 pp 强制转换为 Student* 类型并赋值给 ps2。虽然语法上允许这样转换,但实际上 pobj 只是 Person 对象,它并没有 _No 这个成员变量。当执行 ps2->_No = 10; 时,程序会尝试在 pobj 对象的内存区域之后写入 _No 的值,这就造成了越界访问,可能会改写其他重要的数据,从而引发未定义行为

3.继承中的作用域

在继承体系中基类和派生类都有独立的作用域

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Person
{
protected:
	int _num = 111;
};

class Student : public Person
{
public:
	void Print()
	{
		cout << _num << endl;
	}
protected:
	int _num = 999;
};

void Test()
{
	Student s1;
	s1.Print();
};

这里调用 Print 输出的 _num 是多少?根据前面所学有关作用域的知识可知,编译器遵守就近原则,这里优先输出子类类域里的 _num

如果在 Print 局部域里也有 _num 的话,就优先输出局部域 _num;如果想要输出父类的_num 的话,就需要指定类域(Person::_num

这里 C++ 对这种情况取了个名字叫隐藏/重定义:子类和父类有同名成员,子类的成员隐藏了父类的成员(只要函数名相同就构成重定义

具体作用域分析可以回顾前文:

传送门:C++命运石之门代码抉择:C++入门(上)

🔥值得注意的是: 假设子类有个 func(int i),父类有个 func(),这里构成的是重定义,而不是重载,因为重载的前提条件是在同一作用域,同一作用域下就需要根据函数名修饰规则进行区分,虽然只要函数名相同就会进行修饰,但是继承的这种情况根据域的不同就能进行区分了,实际上函数名修饰规则起不到很大作用,因此是重定义,而不是重载

4.派生类的默认成员函数

4.1 构造函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Person
{
public:

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

protected:
	string _name;
};

class Student : public Person
{
public:

	Student(const char* name = "张三", int id = 0)
		:Person(name)
		,_id(id)
	{ }

protected:
	int _id;
};

子类的构造函数只能构造自己的变量,想要构造继承来的父类变量,必须像 Person(name) 这样显示调用父类的构造函数来调用

🔥值得注意的是:

  • 也可以不写 Person(name) 来显示调用,那么就需要调用父类的默认构造函数,即父类的构造函数必须有缺省参数
  • 派生类初始化列表先初始化父类,再初始化子类

4.2 拷贝构造函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
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;
	}

protected:
	string _name;
};

class Student : public Person
{
public:

	Student(const char* name = "张三", int id = 0)
		:Person(name)
		, _id(id)
	{}

	Student(const Student& s)
		: Person(s)
		, _id(s._id)
	{
		cout << "Student(const Student& s)" << endl;
	}

	Student& operator = (const Student& s)
	{
		cout << "Student& operator= (const Student& s)" << endl;
		if (this != &s)
		{
			Person::operator =(s);
			_id = s._id;
		}
		return *this;
	}

protected:
	int _id;
};

同理,拷贝构造的初始化顺序及初始化机制和构造函数基本一致

但是拷贝构造是如何传Person的对象来拷贝构造的呢?

其实直接传子类对象即可,因为前面说过,子类对象可以赋值给父类引用,直接切割就行了

🔥值得注意的是:

  • 当不显式写 Person(s) 时,会调用父类的构造函数初始化父类变量
  • Person::operator =(s)Person:: 必须写,不然根据就近原则,这里构成重定义,子类 operator= 会一直调用自己,造成死循环

4.3 析构函数

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
	~Student()
	{
		Person::~Person();
	}

根据初学经验,一般析构函数我们会写成这样,保证父类和子类都能被析构,这里显式调用父类析构要加 Person:: 是因为在底层,父类和子类的析构都会被统一处理成 destructor 构成重定义(这部分会在多态部分详细解释

但其实这种调用方法是错误的,我们不应该显式调用父类析构,父类析构其实是会被自动调用的,因为必须保证先子后父的调用。如果先析构了父类,那么此时的子类额外的部分可能处于不一致或未定义的状态

️比如: 有可能先把父类析构了,但是子类还在访问父类的内容;但是把子类先析构了,父类是不会去访问子类的内容的,就不会造成访问未定义的情况

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
~Student()
 {
 	cout<<"~Student()" <<endl;
 }

所以这里不需要显式调用,子类完成析构之后就会自动析构父类

希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

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

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

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【c++】C++中的继承&&菱形继承详解
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
用户10925563
2024/06/04
1720
【c++】C++中的继承&&菱形继承详解
【C++阅览室】C++三大特性之继承
继承在C++中是十分重要的,它在面向对象程序设计时使代码可以复用的重要手段。继承可以允许程序员在保持原有类的特性下进行拓展,增加新的功能,这样产生的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简到繁的过程。在此之前,我们接触到的都是函数的复用,继承是类设计之间的复用。
小文要打代码
2024/10/16
900
【C++阅览室】C++三大特性之继承
【C++】类和对象之继承
简单来说就是类似于在一个类中包含了另一个类的成员函数和成员变量以及对应的访问权限。
啊QQQQQ
2024/11/19
850
【C++】类和对象之继承
C++中的继承
继承机制是面向对象程序设计使代码可以复用的最重要的手段, 它允许程序员在保持原有类特性的基础上进行扩展, 增加功能, 这样产生新的类, 称派生类, 继承呈现了面向对象程序设计的层次结构, 体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
用户11317877
2024/10/16
880
C++中的继承
继承
继承机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
海盗船长
2020/08/27
8030
C++:继承与派生
为什么会有继承这样的语法呢??试想这样一个场景:假设我们这个App需要去获取不同类型用户的数据,并进行分类,那么就需要我们去写对应不同的类,比如说学生、老师、军人、公司职工…………每个类都需要有名字、联系方式、家庭住址、年龄……,我们会发现这样每个类都要写一份,非常冗余,于是我们的祖师爷为了解决这个问题,设计出了继承的语法,比如说用户的共同点是都是用户,我们就可以写一个关于人的类,作为基类,而不同类型用户就作为基类的派生类,去继承基类的成员,从而达到我们的目的。
小陈在拼命
2024/03/12
2060
C++:继承与派生
【C++】继承——切片、隐藏、默认成员函数、菱形
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类 。而以前我们接触的复用都是函数复用,继承是类设计层次的复用。
平凡的人1
2023/10/15
4891
【C++】继承——切片、隐藏、默认成员函数、菱形
【C++深度探索】继承机制详解(一)
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类或子类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
大耳朵土土垚
2024/07/01
1010
【C++深度探索】继承机制详解(一)
C++继承(多继承、菱形继承?)
继承(inheritance)机制是面向对象程序设计,使代码可以复用的最重要的手段。它允许程序员在保持原有类特性的基础上进行扩展,以增加功能。这样产生新的类,称为派生类。
利刃大大
2023/04/12
2.1K0
C++继承(多继承、菱形继承?)
类中承上启下的角色——继承
承上:在面向对象编程时,我们通常将我们的需求实例化相关的类对象,在碰到需要处理大量相同的对象或相似的操作时,我们引入了类、函数和模板等标准化的功能,虽然我们可以通过模板等手段来提高上述功能编写时的泛型,但是还有一些其它的情况。
比特大冒险
2023/04/16
7830
类中承上启下的角色——继承
【C++】万字一文全解【继承】及其特性__[剖析底层化繁为简](20)
YY的秘密代码小屋
2024/01/22
1720
【C++】万字一文全解【继承】及其特性__[剖析底层化繁为简](20)
【C++】继承 - 从基类到派生类的代码复用逻辑
 继承机制是面向对象程序设计使代码可以复用的手段,它允许我们在保持原有类基础上面拓展,增加方法(成员函数)和属性(成员变量),这样产生新的类,称为派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的函数层次的复用,继承是类设计层次的复用。  下面是没有继承之前实现的两个类,一个是student一个是teacher,它们两个都有姓名,年龄,电话,地址相同的变量,都有identity身份认证的成员函数,设计到两个类里面就是冗余的。它们也有不同的成员函数和变量,比如老师独有的成员变量是职称,学生独有的成员变量是学号。学生独有的成员函数是学习,老师独有的成员函数是授课。
_孙同学
2025/04/14
1120
【C++】继承 - 从基类到派生类的代码复用逻辑
【c++】继承学习(一):继承机制与基类派生类转换
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用
用户11029103
2024/05/04
4140
【c++】继承学习(一):继承机制与基类派生类转换
C++之继承
本文主要介绍了C++中面向对象三大特性之一的多态的相关概念,包含了单继承、多继承、菱形继承以及虚拟继承,最后比较了继承和组合两种类之间的关系。
摘星
2023/04/28
4250
C++之继承
【C++进阶篇】像传承家族宝藏一样理解C++继承
C++是一个功能强大的面向对象编程语言,它不仅支持过程式编程,还在此基础上提供了许多用于构建复杂软件系统的面向对象特性。继承是C++中最为核心的概念之一,它允许我们通过现有的类(基类)创建新的类(派生类),从而实现代码的重用和扩展。继承是面向对象编程的三个基本特性之一(另外两个是封装和多态),在设计模式、软件架构和大型系统开发中起着至关重要的作用。
熬夜学编程的小王
2024/12/24
1280
【C++进阶篇】像传承家族宝藏一样理解C++继承
【C++】继承
相信大家对于继承这个词应该都不陌生,所以在这篇文章的学习之前,大家可以先联想一下现实生活中的继承是怎么样的。
YIN_尹
2024/01/23
1670
【C++】继承
C++ —— 关于继承(inheritance)
使用类模版模拟实现一个栈,可以使用vector/list/deque来当做底层容器,核心就是类模版的继承
迷迭所归处
2024/11/19
840
C++ —— 关于继承(inheritance)
【C++】继承(定义、菱形继承、虚拟继承)
继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序员在保持原有类特性的基础上进行扩展,增加功能,这样产生新的类,称派生类。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复用,继承是类设计层次的复用。
秦jh
2024/06/28
2030
【C++】继承(定义、菱形继承、虚拟继承)
【C++】你不得不爱的——继承
 凡是面向对象的语言,都有三大特性,继承,封装和多态,但并不是只有这三个特性,是因为者三个特性是最重要的特性,那今天我们一起来看继承!
The sky
2023/04/12
4010
【C++】你不得不爱的——继承
【C++】三大特性之继承
继承 (inheritance) 机制是面向对象程序设计 使代码可以复用 的最重要的手段,它允许程序员在 保
青衫哥
2023/03/31
3760
【C++】三大特性之继承
相关推荐
【c++】C++中的继承&&菱形继承详解
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档