第18课-C++继承:探索面向对象编程的复用之道
一、引言
C++ 作为一种强大的编程语言,继承机制在面向对象编程中扮演着至关重要的角色。它允许开发者基于已有的类创建新的类,从而实现代码的复用和功能的扩展。然而,继承的概念和使用方法并非一目了然,特别是在处理复杂的继承关系时,需要对其原理和规则有深入的理解。本文将详细阐述 C++ 中继承的各个方面,包括基本概念、访问权限、作用域以及默认成员函数等,通过实际的代码示例帮助读者更好地掌握这一重要特性。
二、继承的基本概念与定义
(一)概念
在 C++ 中,继承是一种面向对象程序设计的机制,它使程序员能够在已有类(基类或父类)的基础上创建新的类(派生类或子类),并对其功能进行扩展或修改。这种机制有效地复用了已有代码,同时通过构建层次化的类结构,展现了面向对象编程从简单到复杂的演变过程。
例如,假设有一个基类Vehicle
,它定义了一些基本的车辆属性,如车轮数量和颜色。现在要创建一个Car
类,除了继承这些基本属性外,还需要增加一些汽车特有的属性,如座位数量。通过继承,Car
类可以复用Vehicle
类的代码,无需重新编写已有的属性定义。
- cpp
class Vehicle {
public:void printInfo() {cout << "Wheel number: " << _wheelNumber << endl;cout << "Color: " << _color << endl;}protected:int _wheelNumber = 4;string _color = "black";
};// Car类继承自Vehicle类
class Car : public Vehicle {
protected:int _seatNumber;
};
在上述代码中,Car
类继承了Vehicle
类的成员函数和成员变量,这意味着Car
类包含了_wheelNumber
和_color
两个属性以及printInfo()
函数。通过继承,实现了代码的复用。
(二)定义
C++ 中继承的定义格式如下:
- cpp
class 子类名 : 继承方式 基类名 {// 子类的成员
};
其中,继承方式可以是public
、protected
或private
,它们决定了基类的成员在派生类中的访问权限。
public
继承:基类的public
成员在派生类中保持public
,protected
成员保持protected
。protected
继承:基类的public
成员在派生类中变为protected
,protected
成员保持protected
。private
继承:基类的public
和protected
成员在派生类中均变为private
。
以下是一个示例代码:
- cpp
class Truck : public Vehicle {
protected:int _loadCapacity;
};int main() {Car c;Truck t;c.printInfo();t.printInfo();return 0;
}
在这个示例中,Car
和Truck
都继承了Vehicle
类的printInfo()
函数,通过c.printInfo()
和t.printInfo()
可以分别输出Car
和Truck
对象的车辆信息。
三、继承中的访问权限
(一)基类成员在派生类中的访问权限
基类的public
、protected
和private
成员在派生类中的访问权限取决于继承方式。下面是不同继承方式下的访问权限表:
类成员 | public 继承 | protected 继承 | private 继承 |
---|---|---|---|
基类的public 成员 | public | protected | private |
基类的protected 成员 | protected | protected | private |
基类的private 成员 | 不可见 | 不可见 | 不可见 |
从表中可以看出,基类的private
成员在派生类中始终不可见(不可访问),无论采用何种继承方式。然而,基类的protected
成员和public
成员则根据继承方式在派生类中具有不同的访问级别。
需要注意的是,如果需要基类的某个成员在派生类中可访问但不希望类外部访问,则可以将其设置为protected
,这样可以更好地控制访问权限。
(二)基类与派生类对象的赋值转换
在 C++ 中,基类和派生类对象的赋值转换是一个常见的操作场景。通常情况下,派生类对象可以赋值给基类对象,或者通过基类的指针或引用来操作派生类对象。这种转换机制使得 C++ 在继承结构中实现了多态和代码复用。但需要注意的是,基类对象不能直接赋值给派生类对象。
1. 派生类对象赋值给基类对象
派生类对象包含了基类的成员,因此派生类对象赋值给基类对象时,实际上是将派生类中属于基类的那一部分赋值给基类对象。这种操作称为切片(Slicing),即派生类对象中的基类部分被切割下来,赋值给基类对象。
以下是一个示例代码:
- cpp
class Animal {
public:string _name;
protected:int _age;
} ;class Dog : public Animal {
public:int _breedId;
};int main() {Dog d;d._name = "Buddy";d._breedId = 1;Animal a = d; // 切片操作,将派生类对象赋值给基类对象cout << "Name: " << a._name << endl; // 输出 "Buddy"// cout << a._breedId; // 错误:基类对象无法访问派生类的成员return 0;
}
在上述代码中,Dog
对象d
被赋值给Animal
对象a
。由于Animal
类没有_breedId
成员,a
无法访问Dog
类中的_breedId
成员。因此,这里发生了切片操作,a
只保留了Dog
类中Animal
类的那部分内容
2. 基类指针和引用的转换
派生类对象可以赋值给基类的指针或引用,这是实现多态的重要前提条件。通过基把基类指针或引用,程序可以在运行时动态绑定到派生类的成员函数。这种方式允许我们在不需要修改代码的情况下扩展程序的功能。
以下是一个示例代码:
- cpp
class Shape {
public:virtual void draw() {cout << "Drawing a shape" << endl;}
protected:string _name = "Shape";
} ;class Circle : public Shape {
public:void draw() override {cout << "Drawing a circle" << endl;}
private:int _radius = 5;
} ;void drawShape(Shape& s) {s.draw(); // 基类引用调用虚函数,实现多态
}int main() {Circle c;drawShape(c); // 输出 "Drawing a circle"return 0;
}
在这个例子中,我们通过基类Shape
的引用调用Circle
类中的draw()
函数,实现了运行时多态。派生类对象c
被传递给基类引用s
,并正确调用了Circle
类的重写函数draw()
。
3. 强制类型转换的使用
在某些特殊情况下,基类指针或引用可能需要转换为派生类的指针或引用。C++ 提供了dynamic_cast
、static_cast
等多种类型转换方式。在继承关系中,使用dynamic_cast
进行安全的类型转换尤为重要,特别是在处理多态时。
以下是一个示例代码:
cpp
Shape* sp = new Circle(); // 基类指针指向派生类对象
Circle* cp = dynamic_cast<Circle*>(sp); // 安全的向下转换
if (cp) {cp->draw();
} else {cout << "Type conversion failed!" << endl;
}
dynamic_cast
在运行时进行类型检查,确保转换是安全的。如果转换失败,将返回nullptr
,从而避免越界访问的风险。
四、继承中的作用域与成员访问
(一)作用域的独立性与同名成员的隐藏
在继承关系中,基类与派生类各自拥有独立的作用域。如果派生类中定义了与基类成员同名的变量或函数,基类的同名成员将被隐藏,这种现象称为隐藏(Hiding)。也叫重定义,同名成员在派生类中会覆盖基类中的成员,导致基类成员无法被直接访问。
以下是一个示例代码:
- cpp
class Parent {
protected:int _id = 111; // 身份证号
} ;class Child : public Parent {
public:Child(int id) : _id(id) { } // 派生类中的_id覆盖了基类中的_idvoid print() {cout << "身份证号: " << Parent::_id << endl; // 访问基类中的_idcout << "孩子编号: " << _id << endl; // 访问派生类中的_id}protected:int _id; // 孩子编号
} ;int main() {Child c(999);c.print(); // 输出身份证号和孩子编号return 0;
}
在这个例子中,Child
类中定义了一个_id
变量,它隐藏了基类Parent
中的同名变量。为了访问基类的_id
,我们使用了Parent::_id
来显式地指定访问基数类中的成员。这样可以避免由于成员同名而导致的混淆。
需要注意的是,在实际的继承体系中,最好不要定义同名的成员。
(二)函数的隐藏
同名成员函数也会构成隐藏,只要函数名称相同,即使参数列表不同,也会发生隐藏。这种行为和函数重载不同。在派生类中,如果我们希望访问基类中的同名函数,必须显式调用基类的函数。
以下是一个示例代码:
- cpp
class Base {
public:void function() {cout << "Base::function()" << endl;}
} ;class Derived : public Base {
public:void function(int i) { // 隐藏了基类的function()cout << "Derived::function(int i) -> " << i << endl;}
} ;int main() {Derived d;d.function(10); // 调用Derived::function(int i)d.Base::function(); // 显式调用基类的function()return 0;
}
在此代码中,派生类Derived
中的function(int i)
函数隐藏了基类Base
中的function()
函数。如果我们希望调用基类的function()
函数,必须通过d.Base::function()
来显式调用。这与函数重载不同,函数隐藏仅要求函数名相同,而不考虑参数列表。并且函数重载说的是同一作用域,而这里基类和派生类是两个作用域。
五、派生类的默认成员函数
在 C++ 中,当我们不显式定义类的构造函数、拷贝构造函数、赋值运算符和析构函数时,编译器会自动为我们生成这些函数。这些自动生成的函数在派生类中也会涉及到对基类成员的操作,因此在继承体系中了解这些默认成员函数的调用规则非常重要。
(一)构造函数的调用顺序
在派生类对象的构造过程中,基类的构造函数会优先于派生类的构造函数被调用。如果基类没有默认构造函数,则派生类的构造函数必须在初始化列表中显式调用基类的构造函数。
以下是一个示例代码:
- cpp
class BaseClass {
public:BaseClass(const string& name) : _name(name) {cout << "BaseClass constructor called!" << endl;}protected:string _name;
} ;class DerivedClass : public BaseClass {
public:DerivedClass(const string& name, int value) : BaseClass(name), _value(value) {cout << "DerivedClass constructor called!" << endl;}private:int _value;
} ;int main() {DerivedClass d("Alice", 12345);return 0;
}
- 输出:
- cpp
BaseClass constructor called!
DerivedClass constructor called!
在这个例子中,DerivedClass
类的构造函数首先调用了BaseClass
类的构造函数来初始化基类部分。随后才执行派生类DerivedClass
的构造函数。这种调用顺序确保基类的成员在派生类构造之前就已经被正确初始化。
(二)拷贝构造函数与赋值运算符的调用
当派生类对象被拷贝时,基类的拷贝构造函数会先被调用,然后才是派生类的拷贝构造函数。同样,赋值运算符的召唤顺序也遵循这一规则:基类的赋值运算符会先于派生类的赋值运算符被调用。
以下是一个示例代码:
- cpp
class OriginalClass {
public:OriginalClass(const string& name) : _name(name) { }// 拷贝构造函数OriginalClass(const OriginalClass& o) {_name = o._name;cout << "OriginalClass copy constructor called!" << endl;}// 赋值运算符OriginalClass& operator=(const OriginalClass& o) {_name = o._name;cout << "OriginalClass assignment operator called!" << endl;return *this;}protected:string _name;
} ;class NewClass : public OriginalClass {
public:NewClass(const string& name, int value) : OriginalClass(name), _value(value) { }// 拷贝构造函数NewClass(const NewClass& n) : OriginalClass(n) {_value = n._value;cout << "NewClass copy constructor called!" << endl;}// 出版设赋值运算符NewClass& operator=(const NewClass& n) {OriginalClass::operator=(n); // 先调用基类的赋值运算符_value = n._value;cout += "NewClass assignment operator called!" << endl;return *this;}private:int _value;
} ;int main() {NewClass n1("Alice", 12345);NewClass n2 = n1; // 拷贝构造函数NewClass n3("Bob", 54321);n3 = n1; // 赋值运算符return 0;
}
- 输出:
- cpp
OriginalClass copy constructor called!
NewClass copy constructor called!
OriginalClass assignment operator called!
NewClass assignment operator called!
在拷贝构造和赋值操作过程中,基类部分总是优先于派生类部分进行初始化或赋值操作。为了保证派生类对象的完整性,派生类的拷贝构造函数和赋值运算符必须调用基类的相应函数,确保基类成员正确处理。
(三)析构函数的召唤顺序
与构造函数的召唤顺序相反,析构函数的召唤顺序是先召唤派生类的析构函数,然后再召唤基类的析构函数。这确保了派生类的资源先被释放,然后基类的资源才能安全地释放。
以下是一个示例代码:
- cpp
class BaseObject {
public:BaseObject(const string& name) : _name(name) { }~BaseObject() {cout << "BaseObject destructor called!" << endl;}//,x,protected:string _name;
} ;class DerivedObject : public BaseObject {
public:DerivedObject(const string& name, int value) : BaseObject(name), _value(value) { }~DerivedObject() {cout << "DerivedObject destructor called!" << endl;}private:int _value;
} ;int main() {DerivedObject d("Alice", 12345);return 0;
}
- 输出:
- cpp
DerivedObject destructor called!
BaseObject destructor called!
可以看到,当DerivedObject
对象d
析构时,首先召唤了DerivedObject
的析构函数,随后召唤了BaseObject
的析构函数。这种析构顺序确保派生类资源(如成员变量_value
)被先行清理,而基类的资源(如_name
)则在派生类资源清理后再进行释放。
(四)虚析构函数
在继承体系中,若希望基类指针指向派生类对象,并通过该指针安全地释放对象,基类的析构函数应当定义为虚函数。否则,仅会调用基类的析构函数,导致派生类资源没有正确释放,从而引发内存泄漏。
以下是一个示例代码:
class Person {
public:Person(const string& name) : _name(name) {}virtual ~Person() {cout << "Person destructor called!" << endl;}protected:string _name;
};class Student : public Person {
public:Student(const string& name, int stuid) : Person(name), _stuid(stuid) {}~Student() {cout << "Student destructor called!" << endl;}private:int _stuid;
};int main() {Person* p = new Student("Alice", 12345);delete p; // 安全删除,先调用派生类的析构函数return 0;
}
通过将基类的析构函数声明为 virtual
,当通过基类指针删除派生类对象时,派生类的析构函数将首先被调用,从而确保所有派生类的资源被正确释放。
相关文章:

第18课-C++继承:探索面向对象编程的复用之道
一、引言 C 作为一种强大的编程语言,继承机制在面向对象编程中扮演着至关重要的角色。它允许开发者基于已有的类创建新的类,从而实现代码的复用和功能的扩展。然而,继承的概念和使用方法并非一目了然,特别是在处理复杂的继承关系时…...

麒麟V10系统下的调试工具(网络和串口调试助手)
麒麟V10系统下的调试工具(网络和串口调试助手) 1.安装网络调试助手mnetassist arm64-main ①在linux下新建一个文件夹 mkdir /home/${USER}/NetAssist②将mnetassist arm64-main.zip拷贝到上面文件夹中,并解压给权限 cd /home/${USER}/Ne…...
ssh封装上传下载
pip install paramiko import paramikoclass SSHClient:def __init__(self, host, port, username, password):self.host = hostself.port = portself.username = usernameself.password = passwordself.ssh = Noneself.sftp = Nonedef connect(self):"""连接到…...

018_FEA_Structure_Static_in_Matlab结构静力学分析
刹车变形分析 本示例展示了如何使用 MATLAB 软件进行刹车变形分析。 这个例子是Matlab官方PDE工具箱的第一个例子,所需要的数据文件都由Matlab提供,包括CAD模型文件。 步骤 1: 导入 CAD 模型 导入 CAD 模型,这里使用的是一个带有孔的支架模…...

网页打不开、找不到服务器IP地址
现象:网络连接ok,软件能正常使用,当网页打不开。 原因:DNS 配置错误导致网站域名无法正确解析造成。 影响DNS设置的:VPN软件、浏览器DNS服务选择、IPv4属性被修改。 1、VPN代理未关闭 2、浏览器DNS解析选择 3、以太…...

RUM性能优化之图片加载
作者:三石 在现代Web开发中,图片作为内容表达的核心元素,其加载效率直接影响到页面的整体性能和用户体验。随着高清大图和动态图像的普及,优化图片加载变得尤为重要。RUM作为一种主动监测技术,能够帮助开发者从真实用户…...

【Java】—— 泛型:泛型的理解及其在集合(List,Set)、比较器(Comparator)中的使用
目录 1. 泛型概述 1.1 生活中的例子 1.2 泛型的引入 2. 使用泛型举例 2.1 集合中使用泛型 2.1.1 举例 2.1.2 练习 2.2 比较器中使用泛型 2.2.1 举例 2.2.2 练习 1. 泛型概述 1.1 生活中的例子 举例1:中药店,每个抽屉外面贴着标签 举例2&…...

【Python】selenium遇到“InvalidArgumentException”的解决方法
在使用try……except 的时候捕获到这个错误: InvalidArgumentException: invalid argument (Session info: chrome112.0.5614.0) 这个错误代表的是,当传入的参数不符合期望时,就会抛出这个异常: InvalidArgumentException: invali…...

RT-DETR改进策略:BackBone改进|CAFormer在RT-DETR中的创新应用,显著提升目标检测性能
摘要 在目标检测领域,模型性能的提升一直是研究者和开发者们关注的重点。近期,我们尝试将CAFormer模块引入RT-DETR模型中,以替换其原有的主干网络,这一创新性的改进带来了显著的性能提升。 CAFormer,作为MetaFormer框架下的一个变体,结合了深度可分离卷积和普通自注意力…...

【YOLOv11】ultralytics最新作品yolov11 AND 模型的训练、推理、验证、导出 以及 使用
目录 一 ultralytics公司的最新作品YOLOV11 1 yolov11的创新 2 安装YOLOv11 3 PYTHON Guide 二 训练 三 验证 四 推理 五 导出模型 六 使用 文档:https://docs.ultralytics.com/models/yolo11/ 代码链接:https://github.com/ultralytics/ult…...

动态规划——多状态动态规划问题
目录 一、打家劫舍 二、打家劫舍 II 三、删除并获得点数 四、粉刷房子 五、买卖股票的最佳时机含冷冻期 六、买卖股票的最佳时机含手续费 七、买卖股票的最佳时机III 八、买卖股票的最佳时机IV 一、打家劫舍 打家劫舍 第一步:确定状态表示 当我们每次…...
leetcode-10/9【堆相关】
1.数组中的第K个最大元素【215】 思路: 1.1.要使得时间复杂度为O(n),自己实现大顶堆,通过K次调整,顶部元素就是想要的第K个最大元素 1.2.实现大顶堆的过程中,先建堆,建堆是利用递归,本…...
自然语言处理问答系统:技术进展、应用与挑战
自然语言处理问答系统:技术进展、应用与挑战 自然语言处理(NLP)作为人工智能领域的一个重要分支,旨在使计算机能够理解和生成人类语言。问答系统(Q&A System),作为NLP的一个重要应用&#…...

向量数据库!AI 时代的变革者还是泡沫?
向量数据库!AI 时代的变革者还是泡沫? 前言一、向量数据库的基本概念和原理二、向量数据库在AI中的应用场景三、向量数据库的优势和挑战四、向量数据库的发展现状和未来趋势五、向量数据库对AI发展的影响 前言 数据是 AI 的核心,而向量则是数…...

vue中css作用域及深度作用选择器的用法
Vue中有作用域的CSS 当< style>标签有scoped属性时,它的css只作用于当前组建中的元素。vue2和vue3均有此用法; 当使用scoped后,父组件的样式将不会渗透到子组件中。不过一个子组件的根节点会同时受父组件有作用域的css和子组件有作用…...

LLM - 使用 ModelScope SWIFT 测试 Qwen2-VL 的 LoRA 指令微调 教程(2)
欢迎关注我的CSDN:https://spike.blog.csdn.net/ 本文地址:https://spike.blog.csdn.net/article/details/142827217 免责声明:本文来源于个人知识与公开资料,仅用于学术交流,欢迎讨论,不支持转载。 SWIFT …...
2024 年热门前端框架对比及选择指南
在前端开发的世界里,框架的选择对于项目的成功至关重要。不同的框架有着不同的设计理念、生态系统和适用场景,因此,开发者在选框架时需要权衡多个因素。本文将对当前最流行的前端框架——React、Vue、Angular、Svelte 和 Solid——进行详细对…...
map_server
地图格式 此软件包中的工具处理的地图以两个文件的形式存储。YAML 文件描述地图的元数据,并命名图像文件。图像文件编码了占用数据。 图像格式 图像文件描述世界中每个单元格的占用状态,并使用相应像素的颜色表示。在标准配置中,较白的像素…...
无人机航拍视频帧处理与图像拼接算法
无人机航拍视频帧处理与图像拼接算法 1. 视频帧截取与缩放 在图像预处理阶段,算法首先逐帧地从视频中提取出各个帧。 对于每一帧图像,算法会执行缩放操作,以确保所有帧都具有一致的尺寸,便于后续处理。 2. 图像配准 在图像配准阶段,算法采用SIFT(尺度不变特征变换)算…...
搬砖11、Python 文件和异常
文件和异常 实际开发中常常会遇到对数据进行持久化操作的场景,而实现数据持久化最直接简单的方式就是将数据保存到文件中。说到“文件”这个词,可能需要先科普一下关于文件系统的知识,但是这里我们并不浪费笔墨介绍这个概念,请大…...
生成xcframework
打包 XCFramework 的方法 XCFramework 是苹果推出的一种多平台二进制分发格式,可以包含多个架构和平台的代码。打包 XCFramework 通常用于分发库或框架。 使用 Xcode 命令行工具打包 通过 xcodebuild 命令可以打包 XCFramework。确保项目已经配置好需要支持的平台…...

中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试
作者:Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位:中南大学地球科学与信息物理学院论文标题:BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接:https://arxiv.…...

Debian系统简介
目录 Debian系统介绍 Debian版本介绍 Debian软件源介绍 软件包管理工具dpkg dpkg核心指令详解 安装软件包 卸载软件包 查询软件包状态 验证软件包完整性 手动处理依赖关系 dpkg vs apt Debian系统介绍 Debian 和 Ubuntu 都是基于 Debian内核 的 Linux 发行版ÿ…...

《从零掌握MIPI CSI-2: 协议精解与FPGA摄像头开发实战》-- CSI-2 协议详细解析 (一)
CSI-2 协议详细解析 (一) 1. CSI-2层定义(CSI-2 Layer Definitions) 分层结构 :CSI-2协议分为6层: 物理层(PHY Layer) : 定义电气特性、时钟机制和传输介质(导线&#…...
Python爬虫实战:研究feedparser库相关技术
1. 引言 1.1 研究背景与意义 在当今信息爆炸的时代,互联网上存在着海量的信息资源。RSS(Really Simple Syndication)作为一种标准化的信息聚合技术,被广泛用于网站内容的发布和订阅。通过 RSS,用户可以方便地获取网站更新的内容,而无需频繁访问各个网站。 然而,互联网…...
【ROS】Nav2源码之nav2_behavior_tree-行为树节点列表
1、行为树节点分类 在 Nav2(Navigation2)的行为树框架中,行为树节点插件按照功能分为 Action(动作节点)、Condition(条件节点)、Control(控制节点) 和 Decorator(装饰节点) 四类。 1.1 动作节点 Action 执行具体的机器人操作或任务,直接与硬件、传感器或外部系统…...
C++八股 —— 单例模式
文章目录 1. 基本概念2. 设计要点3. 实现方式4. 详解懒汉模式 1. 基本概念 线程安全(Thread Safety) 线程安全是指在多线程环境下,某个函数、类或代码片段能够被多个线程同时调用时,仍能保证数据的一致性和逻辑的正确性…...

selenium学习实战【Python爬虫】
selenium学习实战【Python爬虫】 文章目录 selenium学习实战【Python爬虫】一、声明二、学习目标三、安装依赖3.1 安装selenium库3.2 安装浏览器驱动3.2.1 查看Edge版本3.2.2 驱动安装 四、代码讲解4.1 配置浏览器4.2 加载更多4.3 寻找内容4.4 完整代码 五、报告文件爬取5.1 提…...
Angular微前端架构:Module Federation + ngx-build-plus (Webpack)
以下是一个完整的 Angular 微前端示例,其中使用的是 Module Federation 和 npx-build-plus 实现了主应用(Shell)与子应用(Remote)的集成。 🛠️ 项目结构 angular-mf/ ├── shell-app/ # 主应用&…...
Java求职者面试指南:计算机基础与源码原理深度解析
Java求职者面试指南:计算机基础与源码原理深度解析 第一轮提问:基础概念问题 1. 请解释什么是进程和线程的区别? 面试官:进程是程序的一次执行过程,是系统进行资源分配和调度的基本单位;而线程是进程中的…...