万字重谈C++——继承篇

继承的概念及定义

继承的概念

继承(Inheritance)机制作为面向对象程序设计中最核心的代码复用方式,它不仅允许开发人员在保留基础类特性的前提下进行功能扩展(从而创建新的派生类),更重要的是体现了面向对象程序设计的分层架构理念,这种架构完美地映射了从简单到复杂的认知过程。与传统函数级别的复用相比,继承提升到了类设计层次的复用,为软件系统的可扩展性和可维护性提供了更强大的支持。

class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter";  // 存储姓名信息int _age = 18;           // 存储年龄信息
};/*
通过继承机制,基类 Person 的所有成员(包括数据成员和成员函数)
都会成为子类的组成部分。以下示例展示了 Student 和 Teacher 类
如何有效地复用 Person 类的成员。开发者可以通过调试工具观察
Student 和 Teacher 对象,直观地验证数据成员的复用情况。
*/class Student : public Person
{
protected:int _stuid;  // 存储学号信息
};class Teacher : public Person
{
protected:int _jobid;  // 存储工号信息
};int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}

继承定义

定义格式

在面向对象系统中,Person 作为父类(也称为基类),而 Student 扮演子类(也称为派生类)的角色,这种层级关系构成了面向对象程序设计的基础架构。

继承关系和访问限定符

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

  1. 基类的 private 成员在派生类中具有不可访问性(无论采用何种继承方式)。这种不可见性并不意味着派生类对象没有继承这些成员,而是从语法层面禁止了派生类对象(无论是在类内部还是外部)的访问权限。

  2. 基类的 private 成员在派生类中无法直接访问。若希望基类成员在派生类中可访问,同时避免类外访问,应当使用 protected 访问限定符。由此可见,protected 限定符的出现正是为了满足继承机制的特殊需求。

  3. 总结访问规则可以发现:基类的私有成员在子类中始终不可见,而基类其他成员在子类中的访问权限取决于

    Min(成员在基类的访问限定符,继承方式)

    其中 public > protected > private

  4. 在 C++ 中,使用 class 关键字定义类时,默认继承方式是 private;使用 struct 时,默认继承方式则为 public。为了代码可读性和维护性,强烈建议显式声明继承方式。

  5. 在工程实践中,public 继承是最常用的继承方式,而 protected/private 继承的应用场景相对较少。这主要是因为 protected/private 继承限制了派生类外部的可访问性,降低了代码的维护性和扩展性。

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

class Person
{
public:void Print(){cout << _name << endl;}
protected:string _name;  // 存储姓名
private:int _age;      // 存储年龄
};// class Student : protected Person
// class Student : private Personclass Student : public Person
{
protected:int _stunum;   // 存储学号
};/*
在面向对象系统中,派生类对象可以赋值给基类对象、基类指针或基类引用,
这一特征常被形象地称为“切片”或“切割”,比喻将派生类中属于父类的部分
“切出”进行赋值。反之,基类对象不能直接赋值给派生类对象。基类指针或引用可以通过强制类型转换赋值给派生类指针或引用,但需要注意:
只有当基类指针确实指向派生类对象时,这种转换才是安全的。在多态类型中,
可以使用 RTTI (Run-Time Type Information) 的 dynamic_cast 进行安全转换
(具体实现将在后续章节讲解)。
*/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;
}

继承中的作用域

  1. 在继承体系中,基类和派生类各自拥有独立的作用域。

  2. 当子类和父类中存在同名成员时,子类的成员会屏蔽父类对该同名成员的直接访问,这种现象叫做隐藏,也称重定义。在子类成员函数中,可以通过 基类::成员 的方式显式访问被隐藏的基类成员。

  3. 成员函数的隐藏只需函数名相同即可构成隐藏,无需参数列表完全一致。

  4. 实际工程中,建议避免在继承体系中定义同名成员,避免带来混淆。例如:

class Person
{
protected:string _name = "小李子";  // 姓名int _num = 111;           // 身份证号
};class Student : public Person
{
public:void Print(){cout << "姓名:" << _name << endl;cout << "身份证号:" << Person::_num << endl;  // 显式访问基类成员cout << "学号:" << _num << endl;}
protected:int _num = 999;  // 学号,隐藏了基类的_num
};void Test()
{Student s1;s1.Print();
}
class A
{
public:void fun(){cout << "func()" << endl;}
};class B : public A
{
public:void fun(int i){A::fun();  // 显式调用基类函数cout << "func(int i)->" << i << endl;}
};void Test()
{B b;b.fun(10);
}

 派生类的默认成员函数

C++中的6个默认成员函数指的是编译器在无显式定义时自动生成的函数。派生类中这些函数的生成及行为:

  1. 构造函数
    派生类构造函数必须调用基类的构造函数来初始化基类部分。如果基类没有默认构造函数,则派生类构造函数须在初始化列表中显式调用相应基类构造函数。

  2. 拷贝构造函数
    派生类的拷贝构造函数必须调用基类的拷贝构造函数来完成基类部分的复制初始化。

  3. 赋值运算符(operator=)
    派生类的赋值运算符必须调用基类的赋值运算符完成基类成员的复制。

  4. 析构函数
    派生类析构函数执行完毕后,自动调用基类的析构函数,保证销毁顺序从派生部分到基类部分。

  5. 对象初始化顺序
    实例化派生类对象时,先调用基类构造,再调用派生类构造。

  6. 对象销毁顺序
    销毁派生类对象时,先调用派生类析构,再调用基类析构。

  7. 析构函数重写注意
    因后续实现多态常重写析构函数,编译器会对析构函数名做特殊处理,形成隐藏关系。若基类析构函数未标记virtual,则子类析构函数和基类析构函数构成隐藏关系,可能导致析构不完全。

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;}~Person(){cout << "~Person()" << endl;}
protected:string _name;  // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator=(const Student& 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;  // 学号
};void Test()
{Student s1("jack", 18);
}

继承与友元

友元关系不能继承,即基类中声明为友元的类或函数,不能访问子类的私有或保护成员。

class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};
class Student : public Person
{
protected:int _stuNum; // 学号
};
void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}
void main()
{Person p;Student s;Display(p, s);
}

继承与静态成员

基类定义的 static 静态成员变量在整个继承体系中只有一份实例。无论有多少派生类对象,都共享同一个静态成员。

class Person
{
public :Person () {++ _count ;}
protected :string _name ; // 姓名
public :static int _count; // 统计人的个数。
};
int Person :: _count = 0;
class Student : public Person
{
protected :int _stuNum ; // 学号
};
class Graduate : public Student
{
protected :string _seminarCourse ; // 研究科目
};
void TestPerson()
{Student s1 ;Student s2 ;Student s3 ;Graduate s4 ;cout <<" 人数 :"<< Person ::_count << endl;Student ::_count = 0;cout <<" 人数 :"<< Person ::_count << endl;
}

菱形继承的问题

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

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

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

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

示例模型(对象成员)揭示菱形继承会引发二义性和数据冗余问题:

class Person
{
public:string _name;  // 姓名
};class Student : public Person
{
protected:int _num;  // 学号
};class Teacher : public Person
{
protected:int _id;   // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse;  // 主修课程
};
void Test()
{Assistant a;a._name = "peter";  // 二义性,编译器无法确认访问哪个基类的_name// 需明确指定访问路径解决二义性,但数据冗余依然存在a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}

虚拟继承

为解决菱形继承中数据冗余和二义性问题,可采用虚拟继承

class Person
{
public:string _name;  // 姓名
};class Student : virtual public Person
{
protected:int _num;  // 学号
};class Teacher : virtual public Person
{
protected:int _id;   // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse;  // 主修课程
};void Test()
{Assistant a;a._name = "peter";  // 无二义性,只有一份 Person 成员
}

继承的总结和反思

  1. C++语法的复杂性,部分来源于多继承及菱形继承的底层实现。虚拟继承虽能解决菱形继承问题,但底层实现复杂且有一定性能开销,因此一般不建议设计多继承结构,更不要设计菱形继承。

  2. 多继承被视为 C++ 的缺陷之一,很多后续面向对象语言(如 Java)均不支持多继承。

  3. 继承与组合关系:公有继承(public inheritance)是is-a关系,即每个派生类对象都是一个基类对象。组合是has-a关系,表示一个类内部包含另一个类的对象。例如:

// is-a 关系
class Car
{
protected:string _colour = "白色";  // 颜色string _num = "陕ABIT00";  // 车牌号
};class BMW : public Car
{
public:void Drive() { cout << "好开-操控" << endl; }
};class Benz : public Car
{
public:void Drive() { cout << "好坐-舒适" << endl; }
};// has-a 关系
class Tire
{
protected:string _brand = "Michelin";  // 品牌size_t _size = 17;            // 尺寸
};class Car
{
protected:string _colour = "白色";  // 颜色string _num = "陕ABIT00";  // 车牌号Tire _t;                   // 轮胎,组合关系
};
  • 建议优先使用组合以降低耦合度,提高代码维护性。继承破坏了基类封装,变化基类实现对派生类有较大影响,导致耦合度高。

  • 继承适用于表达“是一个”关系,且需要实现多态时;组合适用于“拥有一个”关系,是更安全、更灵活的复用手段。

笔试面试题举例

什么是菱形继承?其问题有哪些?

菱形继承(Diamond Inheritance) 是指在多继承中出现的一种特殊继承结构,其中一个派生类同时继承自两个有共同基类的父类,构成菱形结构。具体表现为:

      A/ \B   C\ /D

派生类 D 继承自 B 和 C,而 BC 又都继承自同一个基类 A

问题:

  • 数据冗余:派生类 D 会拥有两个独立的基类 A 子对象,导致内存中有两个 A 成员变量,相当于数据重复。
  • 二义性:在访问基类 A 的成员时,如 D 中调用 A 的成员时是通过 B 继承得到的,还是通过 C 继承得到的?编译器无法确定,导致访问冲突。

什么是菱形虚拟继承?如何解决数据冗余和二义性?

菱形虚拟继承(Virtual Diamond Inheritance) 是解决菱形继承问题的技术手段。通过在所有继承公共基类的路径上使用virtual关键字,确保派生类沿各条路径共享同一个基类子对象,而不是创建多个独立副本。

例如:

class A { ... };class B : virtual public A { ... };class C : virtual public A { ... };class D : public B, public C { ... };

解决方案:

  • 数据冗余解决:通过虚拟继承,派生类 D 只保留一个共享的基类 A 实例,消除多份数据冗余。
  • 二义性解决:访问基类 A 的成员不再因为多继承路径而产生歧义,编译器明确且唯一地解析其位置,无需显式指定路径,避免访问冲突。

虚拟继承底层通过虚基表(VBT)和虚基表指针(VBPtr)实现,维护偏移量以正确定位唯一基类子对象。

继承与组合的区别?何时使用继承,何时使用组合?

特性继承(is-a 关系)组合(has-a 关系)
关系语义“是一个”关系,例如学生是人“拥有一个”关系,例如车有轮胎
封装性破坏部分封装,派生类依赖基类实现封装良好,只依赖公开接口
耦合度高,基类变化影响派生类低,修改部件不影响整体
灵活性较低,类型固定高,可动态组合不同部件
多态支持支持多态,允许重写基类接口一般不支持多态,但可通过接口实现类似效果
使用场景需表达“是一个”的类型继承关系,且关注行为重用组合复杂功能,灵活构建系统,关注模块化和扩展性

何时使用继承?

  • 当类之间存在明确的“是一个”关系。
  • 需要通过多态达到动态绑定和接口统一。
  • 想重用或扩展基类行为。

何时使用组合?

  • 当组件之间是“拥有”的关系。
  • 需降低耦合,提高代码灵活性和可维护性。
  • 希望功能通过组合多个对象实现,便于扩展和替换。

总结:
优先推荐使用组合来实现代码复用,只有在合理且明确的“是一个”关系且多态需求明确时,才采用继承。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/79324.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

移动光猫 UNG853H 获取超级管理员账号密码

注&#xff1a;电脑连接光猫&#xff0c;网线不要接2口&#xff08;2口一般是IPTV网口&#xff09; 首先浏览器打开 192.168.1.1&#xff0c;使用光猫背面的用户名密码登录。&#xff08;user用户名&#xff09; 然后在浏览器中另开一个窗口打开以下地址&#xff1a; http://…

ActiveMQ 可靠性保障:消息确认与重发机制(二)

ActiveMQ 重发机制 重发机制的原理与触发条件 ActiveMQ 的重发机制是确保消息可靠传输的重要手段。当消息发送到 ActiveMQ 服务器后&#xff0c;如果消费者由于某些原因未能成功处理消息&#xff0c;ActiveMQ 会依据配置的重发策略&#xff0c;将消息重新放入队列或主题中&am…

oceanbase设置密码

docker run -p 2881:2881 --name oceanbase-ce -e MODEmini -d oceanbase/oceanbase-ce:4.2.1.10-110010012025041414 先进入镜像再连接数据库的方式 进入镜像 docker exec -it oceanbase-ce bash 修改数据库密码 ALTER USER ‘root’ IDENTIFIED BY ‘123456’; 无密码 obc…

使用Python和Pandas实现的Azure Synapse Dedicated SQL pool权限检查与SQL生成用于IT审计

下面是使用 Python Pandas 来提取和展示 Azure Synapse Dedicated SQL Pool 中权限信息的完整过程&#xff0c;同时将其功能以自然语言描述&#xff0c;并自动构造所有权限设置的 SQL 语句&#xff1a; ✅ 步骤 1&#xff1a;从数据库读取权限信息 我们从数据库中提取与用户、…

tiktok web X-Bogus X-Gnarly 分析

声明 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 逆向过程 部分python代码 import req…

目标文件的段结构及核心组件详解

目标文件&#xff08;如 .o 或 .obj&#xff09;是编译器生成的中间文件&#xff0c;其结构遵循 ELF&#xff08;Linux&#xff09;或 COFF&#xff08;Windows&#xff09;格式。以下是其核心段&#xff08;Section&#xff09;和关键机制的详细解析&#xff1a; 1. 目标文件的…

【软件设计师:复习】上午题核心知识点总结(一)

一、数据结构与算法(高频) 1. 线性数据结构 数组与链表 数组:随机访问(O(1))、插入/删除(O(n))、内存连续。链表:单向链表、双向链表、循环链表;插入/删除(O(1))、随机访问(O(n))。典型问题: 合并两个有序链表(LeetCode 21)。链表反转(迭代/递归实现)。栈与…

【ROS2】 核心概念2——功能包package

官方英文文档&#xff1a;Creating a package — ROS 2 Documentation: Humble documentation 中文参考&#xff1a;古月ROS2 功能包讲解 - 图书资源 省流&#xff0c;就学习一个命令 ros2 pkg create --build-type <build-type> <package_name> ROS2的重要概念…

Java内存对象实现聚合查询

文章目录 什么是聚合查询excel表格演示插入透视表透视表操作 sql聚合查询创建表和插入数据按照国家业务类型设备类型统计总销量按设备类型统计总销量 Java内存对象聚合查询普通对象方式创建对象聚合查询条件查询方法调用方式结果 Record对象方式Recor对象创建对象聚合查询条件查…

VSCode开发调试Python入门实践(Windows10)

我的Windows10上的python环境是免安装直接解压的Python3.8.x老版本&#xff0c;可参见《Windows下Python3.8环境快速安装部署。 1. 安装VSCode 在Windows 10系统上安装Visual Studio Code&#xff08;VS Code&#xff09;是一个简单的过程&#xff0c;以下是详细的安装方法与…

Tomcat DOS漏洞复现(CVE-2025-31650)

免责申明: 本文所描述的漏洞及其复现步骤仅供网络安全研究与教育目的使用。任何人不得将本文提供的信息用于非法目的或未经授权的系统测试。作者不对任何由于使用本文信息而导致的直接或间接损害承担责任。如涉及侵权,请及时与我们联系,我们将尽快处理并删除相关内容。 前…

使用Qt QAxObject解决Visual Fox Pro数据库乱码问题

文章目录 使用Qt QAxObject解决Visual Fox Pro数据库乱码问题一、问题背景&#xff1a;ODBC读取DBF文件的编码困境二、核心方案&#xff1a;通过QAxObject调用ADO操作DBF1. 技术选型&#xff1a;为什么选择ADO&#xff1f;2. 核心代码解析&#xff1a;QueryDataByAdodb函数3. 连…

HTTP知识速通

一.HTTP的基础概念 首先了解HTTP协议&#xff0c;他是目前主要使用在应用层的一种协议 http被称为超文本传输协议 而https则是安全的超文本传输协议 本章节的内容首先就是对http做一个简单的了解。 HTTP是一种应用层协议&#xff0c;是基于TCP/IP协议来传递信息的。 其中…

制作一款打飞机游戏26:精灵编辑器

虽然我们基本上已经重建了Axel编辑器&#xff0c;但我不想直接使用它。我想创建一个真正适合我们当前目的的编辑器&#xff0c;那就是编辑精灵&#xff08;sprites&#xff09;。这将是今天的一个大目标——创建一个基于模板的编辑器&#xff0c;用它作为我们实际编辑器的起点。…

mac下载homebrew 安装和使用git

mac下载homebrew 安装和使用git 本人最近从windows换成mac&#xff0c;记录一下用homebrew安装git的过程 打开终端 command 空格&#xff0c;搜索终端 安装homebrew 在终端中输入下面命令&#xff0c;来安装homebrew /bin/bash -c "$(curl -fsSL https://raw.githu…

【LeetCode Hot100】图论篇

前言 本文用于整理LeetCode Hot100中题目解答&#xff0c;因题目比较简单且更多是为了面试快速写出正确思路&#xff0c;只做简单题意解读和一句话题解方便记忆。但代码会全部给出&#xff0c;方便大家整理代码思路。 200. 岛屿数量 一句话题意 求所有上下左右的‘1’的连通块…

《社交类应用开发:React Native与Flutter的抉择》

社交类应用以令人目不暇接的速度更新迭代。新功能不断涌现&#xff0c;从更智能的算法推荐到多样化的互动形式&#xff0c;从增强的隐私保护到跨平台的无缝体验&#xff0c;每一次更新都旨在满足用户日益增长且多变的需求。面对如此高频的更新需求&#xff0c;选择合适的跨端框…

关于3D的一些基础知识

什么是2D/3D? 2D&#xff08;二维&#xff09;和3D&#xff08;三维&#xff09;是描述空间维度的概念&#xff0c;它们的核心区别在于空间维度、视觉表现和应用场景。以下是详细对比&#xff1a; 1. 定义与维度 • 2D&#xff08;二维&#xff09; • 定义&#xff1a;仅包…

大连理工大学选修课——机器学习笔记(7):集成学习及随机森林

集成学习及随机森林 集成学习概述 泛化能力的局限 每种学习模型的能力都有其上限 限制于特定结构受限于训练样本的质量和规模 如何再提高泛化能力&#xff1f; 研究新结构扩大训练规模 提升模型的泛化能力 创造性思路 组合多个学习模型 集成学习 集成学习不是特定的…

嵌入式产品运行中数据丢失怎么办?

目录 1、数据丢失现象与根源分析 2、硬件层优化 3、系统/驱动层优化 4、应用软件层优化 5、文件系统选型深度解析 5.1、NAND Flash 适用文件系统 5.2、eMMC 适用文件系统 6、系统挂载选项优化实践 嵌入式系统在运行过程中&#xff0c;尤其是在涉及频繁数据写入&#xf…