news 2026/10/2 19:23:04

一文带你吃透C++继承

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一文带你吃透C++继承

1.1继承的概念

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

继承&组合(也称合成): 是C++实现代码重用的2种主要方法。

🍇1.2图解继承例子

代码示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

classPerson

{

public:

voidPrint()

{

cout <<"name:"<< _name << endl;

cout <<"age:"<< _age << endl;

}

protected:

string _name ="jojo";// 姓名

int_age = 18;// 年龄

};

// 继承后父类的Person的成员(成员函数+成员变量)都会变成子类的一部分。这里体现出了Student和

//Teacher复用了Person的成员。下面我们使用监视窗口查看Student和Teacher对象,可以看到变量的复用。

//调用Print可以看到成员函数的复用。

classStudent :publicPerson

{

private:

int_stuid;// 学号

};

classTeacher :publicPerson

{

protected:

int_jobId;// 工号

};

intmain()

{

Student s;

Teacher t;

s.Print();

t.Print();

return0;

}

🍇1.3 继承的语法形式

🍇1.3.1继承基类成员访问方式的变化

总结:

  • 基类private成员在派生类中无论以什么方式继承都是不可见的。这里的不可见是指基类的私有成员还是被继承到了派生类对象中,但是语法上限制派生类对象不管在类里面还是类外面都不能去访问它。
  • 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的
  • 使用关键字class时默认的继承方式是private,使用struct时默认的继承方式是public,不过最好显示的写出继承方式。
  • 在实际运用中一般使用都是public继承,几乎很少使用protetced/private继承。
  • 如果不想让一个类作为其它类的基类,可以用final关键字阻止它被继承。

🍎2、 基类&派生类的关系

通过继承,派生类拥有了基类的数据成员和函数成员。

🍉派生类在基类的基础上可实施以下改变🍉:

  • 增加新的成员(数据+函数)。
  • 重载(overload)基类的函数成员。
  • 重定义(override,覆盖)基类已有的函数成员。
  • 改变基类成员在派生类中的访问属性。

🍉派生类不能继承基类的以下内容🍉:

1)析构函数。
2) 基类的友元函数。
3)静态成员(数据+函数)
4)针对基类定义的一些特殊运算符,如new等。

注意:

派生类继承了基类的所有成员,但派生类能够直接访问从基类继承来的公有和保护成员,且只能通过这两类成员访问从基类继承来的私有成员。

🍎3、基类和派生类对象赋值兼容转换

  • 派生类对象可以赋值给基类的对象 / 基类的指针 / 基类的引用。这里有个形象的说法叫切片或者切割。寓意把派生类中父类那部分切来赋值过去。
  • 基类对象不能赋值给派生类对象。
  • 基类的指针可以通过强制类型转换赋值给派生类的指针。但是必须是基类的指针是指向派生类对象时才是安全的。这里基类如果是多态类型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 来进行识别后进行安全转换。
  • 基类和派生类的赋值转换的基础是建立在公有继承的基础上的
  • 引用类型通常是用.来访问的,指针类型的访问就要通过->

代码示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

classPerson

{

protected:

string _name;// 姓名

string _sex;// 性别

int_age;// 年龄

};

classStudent :publicPerson

{

public:

int_No;// 学号

};

intmain()

{

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;

}

🍎4、继承中的作用域

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

隐藏关系代码举例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

classBase {//基类

public:

Base(inta = 0) { k = a; }

intk;//为演示,将数据成员声明为公有

voidfn1() { cout <<"Base::fn1()"<< endl; }

voidfn2() { cout <<"Base::fn2()"<< endl; }

};

classDerived :publicBase {//派生类

public:

voidfn1() { cout <<"Devired::fn1()"<< endl; }//构成隐藏

voidfn2() { cout <<"Devired::fn2()"<< endl; }

//如果我们想访问基类的fn1函数可以指定作用域

};

intmain() {

Derived d;

cout <<"k="<< d.k << endl;

d.fn1();

d.fn2();

d.Base::fn1();

}

🍎5、派生类的默认成员函数

  • 派生类的构造函数必须调用基类的构造函数初始化基类的那一部分成员。如果基类没有默认的构造函数,则必须在派生类构造函数的初始化列表阶段显示调用.
  • 派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
  • 派生类的operator=必须要调用基类的operator=完成基类的复制
  • 派生类的析构函数会在被调用完成后自动调用基类的析构函数清理基类成员。因为这样才能保证派生类对象先清理派生类成员再清理基类成员的顺序。
  • 派生类对象初始化先调用基类构造再调派生类构造
  • 派生类对象析构清理先调用派生类析构再调基类的析构。

已知父类代码Person如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

classPerson

{

public:

Person(constchar* name ="peter")//有参的构造函数

: _name(name)

{

cout <<"Person()"<< endl;

}

Person(constPerson& p)//Person给拷贝构造函数

: _name(p._name)

{

cout <<"Person(const Person& p)"<< endl;

}

Person& operator=(constPerson& p)//operator重载=

{

cout <<"Person operator=(const Person& p)"<< endl;

if(this!= &p)

_name = p._name;

return*this;

}

~Person()//析构函数

{

cout <<"~Person()"<< endl;

}

protected:

string _name;// 姓名

};

如何实现子类的拷贝构造?派生类的拷贝构造函数必须调用基类的拷贝构造完成基类的拷贝初始化
子类有一个Int 类型的成员_num。

子类完整代码示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

classStudent :publicPerson

{

public:

Student(constchar* name,intnum)

: Person(name)

, _num(num)

{

cout <<"Student()"<< endl;

}

Student(constStudent& s)

: Person(s)

, _num(s._num)

{

cout <<"Student(const Student& s)"<< endl;

}

Student& operator = (constStudent& s)

{

cout <<"Student& operator= (const Student& s)"<< endl;

if(this!= &s)

{

Person::operator =(s);

_num = s._num;

}

return*this;

}

~Student()

{

cout <<"~Student()"<< endl;

}

protected:

int_num;//学号

};

  • 子类里面要不要显示调用父类析构函数的问题?

答案是不用,因为父子类的析构函数构成隐藏关系,子类会自动调用父类的析构函数。
析构名称会被统一处理成destructor()

  • 如何设计一个不能被继承的类?

答案:构造函数私有,这样子类就无法初始化基类对象,从而不能被继承。

  • 派生类对象的构造和析构顺序

派生类对象的构造顺序:
步骤1:先构造基类;
步骤2:再构造对象成员;
步骤3:最后构造派生类自身;

  • 派生类什么时候可以不定义构造函数?

当基类 和 所有 对象成员具有无参构造函数时!
无参构造函数细分为3种情况: (1)没有定义任何构造函数; (2)具有[重定义的]无参构造函数; (3)具有缺省参数的构造函数。

  • 派生类什么时候“必须”定义构造函数?

答案:当基类或对象成员所属类只含有带参数的构造函数时。即使派生类本身没有数据成员要初始化,它也必须定义构造函数!以“构造函数初始化列表”的方式向基类和对象成员的构造函数传递参数,以实现基类子对象和对象成员的初始化。

🍎6、继承与友元、静态函数

友元关系不能继承,也就是说基类友元不能访问子类私有和保护成员

**如果一个类继承了其它类,则它声明的友元也只能访问它自己的全体成员,以及从基类继承到的public和protected成员。**而它的基类和派生类并不认可这种友元关系,按照规则只能访问公有成员。

继承和静态的关系:

  • 基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。
  • 基类静态成员为继承层次结构所有类共享;

代码示例:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

classPerson

{

public:

Person () {++ _count ;}

protected:

string _name ;// 姓名

public:

staticint_count;// 统计人的个数。

};

intPerson :: _count = 0;

classStudent :publicPerson

{

protected:

int_stuNum ;// 学号

};

classGraduate :publicStudent

{

protected:

string _seminarCourse ;// 研究科目

};

voidTestPerson()

{

Student s1 ;

Student s2 ;

Student s3 ;

Graduate s4 ;

cout <<" 人数 :"<< Person ::_count << endl;

Student ::_count = 0;

cout <<" 人数 :"<< Person ::_count << endl;

}

🍎7、复杂的菱形继承及菱形虚拟继承

单继承:一个子类只有一个直接父类时称这个继承关系为单继承

多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况。

菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。在Assistant的对象中Person成员会有两份。

需要显示指定访问哪个父类的成员可以解决二义性问题

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

classPerson

{

public:

string _name;// 姓名

};

classStudent :publicPerson

{

protected:

int_num;//学号

};

classTeacher :publicPerson

{

protected:

int_id;// 职工编号

};

classAssistant :publicStudent,publicTeacher

{

protected:

string _majorCourse;// 主修课程

};

voidTest()

{

// 这样会有二义性无法明确知道访问的是哪一个

Assistant a;

//a._name = "peter";

// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决

a.Student::_name ="xxx";

a.Teacher::_name ="yyy";

}

解决二义性的方法除了指定访问的父类,也可以通过虚拟继承.
虚拟基类:使得派生类中只存在同一基类的一份拷贝,解决了基类 数据成员的二义性问题;
🍉虚拟继承的定义🍉

举例:

1

2

3

classStudent:virtualpublicPerson{……}//Person为虚基类

classEmployee:virtualpublicPerson{……}//Person为虚基类

classStuEmployee:publicStudent,publicEmployee{……}

代码示例:

1

2

3

4

5

6

7

8

9

10

11

12

classA {

public:

voidvf() {

cout<<"I come from class A"<<endl; }

};

classB:virtualpublicA{ };

classC:virtualpublicA{ };

classD:publicB,publicC{ };

voidmain(){

D d;

d.vf();// 正确

}

继承关系如图所示——虚基类!

🍎总结

本文和大家总结了C++继承的几个要点,从继承的概念、定义出发,再到基类和派生类之间的关系,赋值兼容转换,再到继承中的作用域、成员函数等,再浅谈了C++的复杂菱形继承等,希望本文对大家有所帮助


版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/2 19:22:34

无需计算p(x)的贝叶斯分类器:从生成式到判别式的统一视角

1. 从“没有 p(x)”说起&#xff1a;贝叶斯分类器到底在算什么 很多人第一次学贝叶斯分类器&#xff0c;脑子里都会被一个公式钉死&#xff1a;后验概率正比于似然乘以先验&#xff0c;也就是 (p(y|x) \propto p(x|y)p(y))。然后紧接着教材就会告诉你&#xff0c;分母 (p(x)) 是…

作者头像 李华
网站建设 2026/10/2 19:22:11

Java遍历全攻略:从数组到二叉树,彻底搞懂遍历方式与坑

提到Java遍历&#xff0c;我第一反应不是去背API&#xff0c;而是先问一句&#xff1a;你要遍历的到底是什么结构&#xff1f;是数组、List、Set还是Map&#xff1f;遍历过程中要不要删除元素&#xff1f;数据量大不大&#xff1f;需不需要并行&#xff1f;这些听起来像面试题&…

作者头像 李华
网站建设 2026/10/2 19:21:52

机器人空间描述与坐标变换:旋转矩阵、欧拉角与齐次变换

1. 先把问题摆清楚&#xff1a;机器人为什么非要和坐标系较劲带过几届做机器人方向的学生和实习生&#xff0c;我发现一个挺有意思的规律&#xff1a;真正让大家在入门阶段卡住的&#xff0c;往往不是后面的雅可比矩阵&#xff0c;也不是动力学方程&#xff0c;而是第一章的空间…

作者头像 李华
网站建设 2026/10/2 19:21:42

5G高负荷判定:联合流量与用户数的资源利用率计算

简介&#xff1a;针对5G网络高负荷小区的识别与优化需求&#xff0c;这份由通信网络工程师整理的规范文档&#xff0c;系统定义了第五代移动通信网络中高负荷状态的判定标准&#xff0c;面向无线优化工程师、网络规划与运维人员&#xff0c;可辅助完成容量评估、扩容决策及多场…

作者头像 李华
网站建设 2026/10/2 19:20:03

Agentic合成与清洗:SFT、mid-training、RL训练数据管线实战

1. 为什么“合成清洗”成了训练数据的新主线过去两年&#xff0c;我参与过好几个从零起步的模型训练项目&#xff0c;从最早的纯人工标注&#xff0c;到后来的规则清洗&#xff0c;再到现在的 agentic 合成加自动清洗&#xff0c;最大的感受就是&#xff1a;数据工程的重心正在…

作者头像 李华
网站建设 2026/10/2 19:18:30

Claude Agent Skills 实战:从 SKILL.md 到工作流自动化

1. 从“skills”这个热词说起&#xff1a;它到底是什么&#xff0c;为什么突然火了如果你最近在开发者社区、技术群或者视频平台上频繁刷到“skills”这个词&#xff0c;大概率不是指传统意义上的“技能”泛称&#xff0c;而是特指围绕 Claude 生态、尤其是 Claude Code 和 Age…

作者头像 李华