C++的核心--继承

C++的核心--继承

 目录

前言

一、继承的概念及定义

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

三、继承中的作用域

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

五、继承与友元

六、继承与静态成员

七、复杂的菱形继承及菱形虚拟继承

(一)单继承与多继承

(二)菱形继承

(三)菱形虚拟继承

八、继承的总结和反思

结语


前言

在C++ 编程世界里,继承是一项极为关键的特性,它为代码的复用和层次化设计提供了强大支持。掌握继承机制,对于编写高效、可维护的C++ 代码至关重要。今天,就让我们一起深入探究C++ 中的继承。

一、继承的概念及定义

继承是面向对象程序设计实现代码复用的重要手段。它允许我们在保持原有类特性的基础上进行扩展,产生新的类,即派生类。这体现了面向对象程序设计的层次结构,从简单到复杂逐步构建。

定义格式上,以 class Student : public Person 为例, Person 是基类(父类), Student 是派生类(子类) , public 是继承方式。继承方式有 public 、 protected 和 private 三种,不同继承方式会改变基类成员在派生类中的访问权限。

比如,基类的 public 成员在 public 继承下,在派生类中仍是 public 成员;但在 protected 继承下,就变为派生类的 protected 成员 。

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

派生类对象和基类对象之间存在特殊的赋值转换关系。派生类对象可以赋值给基类的对象、指针或引用,这就像把派生类中属于基类的那部分“切”出来进行赋值,形象地称为切片。例如:

Student sobj; Person pobj = sobj;  Person* pp = &sobj;  Person& rp = sobj; 

然而,基类对象不能直接赋值给派生类对象。不过,基类的指针或引用可以通过强制类型转换赋值给派生类的指针或引用,但这种转换需要谨慎,若基类是多态类型,可助 RTTI (运行时类型信息)的 dynamic_cast 来确保安全转换。


在 C++ 继承体系中,向上转型和向下转型是绕不开的核心概念,也是面试高频考点与日常编码的常用操作。很多同学对这两个概念模糊不清,分不清使用场景、安全性与语法规则,今天就用最通俗、最清晰的方式,把转型的所有要点一次性讲透。
 
什么是向上/向下转型?
 
首先明确:C++ 没有「向上继承」「向下继承」的语法,这两个词是对「继承体系中类型转换方向」的通俗叫法。
我们默认:基类(父类)在上,派生类(子类)在下,所有转换都围绕父子类指针/引用展开。
 
- 向上转型(Upcast):子类指针/引用 → 父类指针/引用
- 向下转型(Downcast):父类指针/引用 → 子类指针/引用

 
简单记:往上转是子类变父类,往下转是父类变子类。
 
向上转型:永远安全,自动支持
 
向上转型是天然合法、无风险的转换,也是编码中最常用的转型方式。
 
1. 适用条件
 
仅需满足一个核心要求:公有继承(public inheritance)。
因为公有继承代表「is-a」关系(子类是父类的一种),子类对象天然包含父类的所有成员,所以可以直接当作父类使用。
 
2. 语法示例

3. 核心特点
 
✅ 自动转换:编译器直接允许,无需手动强转
✅ 绝对安全:不会出现未定义行为
✅ 多态基础:配合虚函数实现运行时多态,是面向对象的核心用法
 
向下转型:语法允许,风险自负
 
向下转型是逆向转换,编译器不会自动允许,必须手动强制转换,且存在安全风险。
 
1. 安全的向下转型
 
唯一安全场景:被转换的父类指针/引用,原本就指向子类对象(先向上转型,再转回子类)。
 
2. 语法示例

3. 危险的向下转型
 
如果父类指针/引用原本指向的是父类对象,强行向下转型会触发未定义行为(程序崩溃、内存错误等)。
 

4. 安全方案:dynamic_cast 运行时检查
 

一张表总结核心规则
 
转型类型 转换方向 安全性 语法要求 核心场景 
向上转型 子类→父类 绝对安全 公有继承,自动转换 多态、函数传参、接口统一 
向下转型 父类→子类 有风险 需手动强转,原对象为子类 调用子类独有成员 
 
口诀(面试/编码必背)
 
1. 公有继承下,向上转型随便用,自动安全无压力;
2. 向下转型需谨慎,原对象是子类才安全;
3. 多态场景用  dynamic_cast ,运行时检测防崩溃。
 
掌握以上规则,就能彻底搞定 C++ 继承中的转型问题,既避开编码坑,也能轻松应对面试提问。

如果Person&p=s;实际上是将子类对象作为父类的别名。赋值,指针,都是兼容切割,切片。

三、继承中的作用域

在继承体系中,基类和派生类都有各自独立的作用域。当子类和父类存在同名成员时,子类成员会屏蔽父类对同名成员的直接访问,这种现象称为隐藏(重定义) 。比如:

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

在 Student 类的 Print 函数中,通过 Person::_num 明确访问父类的 _num 成员,避免混淆。实际编程中,应尽量避免在继承体系里定义同名成员,以免造成代码理解和维护上的困难。

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

当我们定义派生类时,即便没有显式编写,编译器也会自动生成一些默认成员函数,主要包括以下几个方面:

1. 构造函数:派生类的构造函数必须调用基类的构造函数来初始化基类的那部分成员。若基类没有默认构造函数,就需在派生类构造函数的初始化列表中显式调用合适的基类构造函数。

编译过程中,子类有两组成员,一组是自身的,一组是继承而来,编译过程中会先初始化基类,再初始化派生类(派生类有默认构造才行),派生类只初始化自己的成员,编译器默认把继承而来的的对象交给父类初始化,但是不可以以--子类对象(初始化变量)--显示调用父类的默认构造,如果没有默认构造-->会报错:

需要以person(成员名)的形式调用父类交给他的默认构造,如下所示:

2. 拷贝构造函数:派生类的拷贝构造函数要调用基类的拷贝构造函数,完成基类成员的拷贝初始化。

子类拷贝构造时如果基类无拷贝构造,默认调入默认构造,如果默认构造也没有会报错:

3. 赋值运算符函数:派生类的 operator= 必须调用基类的 operator= 来完成基类成员的复制。(避免隐藏
4. 析构函数:派生类的析构函数在被调用完成后,会自动调用基类的析构函数,以清理基类成员,确保对象资源的正确释放,遵循先派生类后基类的清理顺序。

不需要如下这样显示调用,否则会二次析构。

同时还有一个原因就是,如果析构是先父亲后儿子,子类析构中有调用父类成员函数,就会有坑。

五、继承与友元

友元关系在继承体系中是不能自动继承的。也就是说,基类的友元不能访问子类的私有和保护成员。

父亲的朋友不是你的朋友

你需要和他成为朋友才可以访问(见注释)

例如:

class Student; class Person { public:     friend void Display(const Person& p, const Student& s); protected:     string _name;  }; class Student : public Person { //public:     //friend void Display(const Person& p, const Student& s); protected:     int _stunNum;  }; void Display(const Person& p, const Student& s) {     cout << p._name << endl;     cout << s._stunNum << endl; }

这里 Display 函数作为 Person 类的友元,能访问 Person 类的保护成员,但对于 Student 类,它并不具备天然访问其保护成员的权限。

六、继承与静态成员

若基类定义了 static 静态成员,那么在整个继承体系中,无论派生出多少个子类,都只会存在一个该静态成员的实例。例如:

class Person { public:     Person() { ++_count; } public:     static int _count;  }; int Person::_count = 0; class Student : public Person { };

Person 类中的 _count 静态成员,在 Student 类及其他派生类中都是共享的,可通过类名或对象来访问。

七、复杂的菱形继承及菱形虚拟继承

(一)单继承与多继承

单继承是指一个子类只有一个直接父类,关系简单明了。

而多继承则是一个子类有两个或以上直接父类,这种情况虽然增加了代码复用的灵活性,但也引入了一些复杂问题。

(二)菱形继承

菱形继承是多继承的一种特殊情况。以 class Assistant : public Student, public Teacher 为例, Student 和 Teacher 都继承自 Person ,这样 Assistant 中就会出现 Person 成员的两份拷贝,导致数据冗余和二义性问题。比如在访问 Assistant 对象中来自 Person 的成员时,编译器无法明确确定访问的是哪一个 Person 成员。

数据冗余和二义性: Assistant 中就会出现 Person 成员的两份拷贝

(三)菱形虚拟继承

为解决菱形继承的数据冗余和二义性问题,引入了虚拟继承。在 Student 和 Teacher 继承 Person 时使用虚拟继承(如 class Student : virtual public Person  ),

就能确保在 Assistant 对象中只存在一份 Person 成员的拷贝。虚拟继承通过虚基表指针和虚基表来管理基类成员的存储和访问,有效解决了上述问题,但也增加了一定的实现复杂度。

原理图

示例代码

下图是菱形虚拟继承的内存对象成员模型:这里可以分析出D对象中将A放到的了对象组成的最下面,这个A同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指针叫虚基表指针,这两个表叫虚基表。虚基表中存的偏移量。通过偏移量可以找到下面的A。

八、继承的总结和反思

继承在C++ 中是把双刃剑。它极大地促进了代码复用,构建了清晰的类层次结构,但也带来了一些问题。多继承衍生出的菱形继承和复杂的底层实现,让代码复杂度和维护难度上升,这也是许多其他面向对象语言(如Java)不采用多继承的原因。

在实际编程中,我们要谨慎选择继承和组合。继承是“is - a”关系,每个派生类对象都是一个基类对象;组合是“has - a”关系,一个类包含另一个类的对象。优先考虑对象组合,因为它耦合度低,代码维护性好,更符合“黑箱复用”原则;而继承适用于需要体现类之间层次关系,或实现多态的场景。

Test

#define _CRT_SECURE_NO_WARNINGS #include <iostream> using namespace std; class A { public: A(const char* s) { cout << s << endl; } ~A() {} }; class B : virtual public A { public: B(const char* sa, const char* sb) :A(sa) { cout << sb << endl; } }; class C : virtual public A { public: C(const char* sa, const char* sb) :A(sa) { cout << sb << endl; } }; class D : public B, public C { public: D(const char* sa, const char* sb, const char* sc, const char* sd) :B(sa, sb), C(sa, sc), A(sa) { cout << sd << endl; } }; int main() { D* p = new D("class A", "class B", "class C", "class D"); delete p; return 0; }

结语

C++ 的继承机制丰富而复杂,深入理解它的各个方面,从基本概念到复杂应用,能让我们在编程时更得心应手,编写出结构良好、高效且易于维护的代码。希望通过这篇博客,大家能对C++ 继承有更透彻的认识,在后续编程实践中灵活运用。

Read more

一篇带你速通差分算法(C/C++)

一篇带你速通差分算法(C/C++)

个人主页:摆烂小白敲代码 创作领域:算法、C/C++ 持续更新算法领域的文章,让博主在您的算法之路上祝您一臂之力 欢迎各位大佬莅临我的博客,您的关注、点赞、收藏、评论是我持续创作最大的动力 差分算法是一种在计算机科学中常用的算法,特别是在处理序列数据时,它可以帮助我们快速计算出序列中相邻元素的差值。时间复杂度可以达到O(1),在C++中实现差分算法不仅可以提高程序的效率,还可以简化代码的复杂度。本文将详细介绍差分算法的原理、C++实现方法以及算法例题。  算法原理 上一篇博客一篇带你速通前缀和算法(C/C++)-ZEEKLOG博客我们介绍了前缀和算法,这一篇文章就与前缀和算法相反为差分算法。 一维差分: 差分算法的核心思想是利用已有的数据序列,通过计算相邻元素之间的差值来生成一个新的序列。对于一个给定的序列 a=[a1 ,a2 ,...,an ],其差分序列 d 可以表示为:d[i]=a[i]−a[i-1]。差分数组长度一般为原定序列长度+1,即:

By Ne0inhk
【C++】 —— 笔试刷题day_27

【C++】 —— 笔试刷题day_27

一、kotori和气球 题目解析 这道题,有n中气球,每一种气球有无数多个;现在我们需要将这些气球摆成一排,但是,如果相邻的气球是相同的就会发生爆炸(也就是说,相同的气球相邻的摆法是不合法的); 现在我们要求将气球摆成一排m个一共有多少种摆法;最终结果可能数据过大,我们输出最终结果对于109取模的结果即可。 算法思路 这道题整体来说还是比较简单的: 我们摆放第一个气球时,我们可以随便选取一个气球,那也就有n中可能; 当我们摆放第二个以及后面的气球时,我们不能摆放与上一个气球相同的气球,那也就有n-1种可能。 所以,我们最终结果就等于:n * (n-1)^(m-1)。 代码实现 这里通过查看数据范围我们可以发现:在运算的时候数据就看超出范围,所以在运算的过程中就进行%109操作。 #include<iostream>usingnamespace std;intmain(){int n,m; cin>>n>>m;longlong

By Ne0inhk
C++ 日志系统实战第一步:项目功能与技术架构详解

C++ 日志系统实战第一步:项目功能与技术架构详解

C++-基于多设计模式下的同步&异步日志系统 全是通俗易懂的讲解,如果你本节之前的知识都掌握清楚,那就速速来看我的项目笔记吧~    (全文手敲,受益良多) 本人也是做到项目了,后面会日更项目笔记。  我们要实现日志系统,先了解 什么是日志?    程序运行过程中所记录的程序运行的状态信息。为什么要这些信息呢?便于程序员对程序运行状况判断,进而分析,修改代码子类的。所以 日志作用:记录了程序运行状态信息,便于程序员能够随时根据状态信息,对系统的运行状态,进行分析 项目功能:能够让用户非常简便的进行日志的输出,及其控制。 一.项目介绍 本项目主要实现一个日志系统,其主要支持以下功能: 支持多级别日志消息:(级别有:调试,提示,警告,错误,致命,不同级别对应不同场景:例如调试级别的日志不输出…)  支持同步日志和异步日志:(同步指的是该工作由自己干,异步指的是该工作由别人干,自己等结果就行)同步日志指的是将日志写入到文件或数据库中去,这个操作由业务线程自己完成。异步日志指的是,将日志放到内存中去,由于担心磁盘满了,

By Ne0inhk
【C++ Qt】容器类(GroupBox、TabWidget)内附思维导图 通俗易懂

【C++ Qt】容器类(GroupBox、TabWidget)内附思维导图 通俗易懂

每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry” ✍️绪论 : 本章主要介绍了 Qt 中 QGroupBox 与 QTabWidget 控件。QGroupBox 是带标题的分组框,能容纳其他控件,有标题、对齐方式、是否扁平、是否可勾选等属性,可通过拖拽添加控件测试属性,需借助布局管理器使用。QTabWidget 用于实现标签页切换界面,有标签页位置、当前选中标签页信息、是否可关闭移动等属性,以及切换、点击等信号。实操部分展示了图形化添加标签页及代码实现新增、关闭标签页和感知切换等功能。 ———————— 早关注不迷路🔥,话不多说安全带系好,发车啦(建议电脑🖥️观看)。 文章目录 * 🖼️ QGroupBox 分组框 * 💻实操:了解 flat、checkable属性 * 🎠Tab Widget * ⌨️实操

By Ne0inhk