【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则

文章目录

  • C++ 继承详解:初阶理解与实战应用
    • 前言
    • 第一章:继承的基本概念与定义
      • 1.1 继承的概念
      • 1.2 继承的定义
    • 第二章:继承中的访问权限
      • 2.1 基类成员在派生类中的访问权限
      • 2.2 基类与派生类对象的赋值转换
        • 2.2.1 派生类对象赋值给基类对象
        • 2.2.2 基类指针和引用的转换
        • 2.2.3 强制类型转换的使用
    • 第三章:继承中的作用域与成员访问
      • 3.1 作用域的独立性与同名成员的隐藏
        • 3.1.1 函数的隐藏
      • 3.2 派生类的默认成员函数
        • 3.2.1 构造函数的调用顺序
        • 3.2.2 拷贝构造函数与赋值运算符的调用
        • 3.2.3 析构函数的调用顺序
        • 3.2.4 虚析构函数
    • 总结

C++ 继承详解:初阶理解与实战应用

💬 欢迎讨论:在学习过程中,如果有任何疑问或想法,欢迎在评论区留言一起讨论。

👍 点赞、收藏与分享:觉得这篇文章对你有帮助吗?记得点赞、收藏并分享给更多的朋友吧!你们的支持是我不断进步的动力!
🚀 分享给更多人:如果你觉得这篇文章对你有帮助,欢迎分享给更多对 C++ 感兴趣的朋友,一起学习进步!


前言

C++ 继承机制是面向对象编程的重要组成部分,能够帮助开发者实现代码的复用和扩展。通过继承,开发者可以基于已有的类创建新的类,从而避免重复代码编写,提升开发效率。然而,继承的使用并不总是那么简单,特别是在涉及到复杂继承关系时,容易导致一些新手难以理解的困惑。本篇文章将通过细致入微的分析,帮助大家从初阶的角度理解 C++ 中继承的基本原理,并结合实际的代码示例,逐步深入剖析继承中的难点和注意事项。


第一章:继承的基本概念与定义

1.1 继承的概念

在C++中,继承(Inheritance) 是面向对象程序设计中的一种机制,它允许程序员在已有类(即基类或父类)的基础上,扩展或修改功能,从而形成新的类(即派生类或子类)。这种机制能够复用已有的代码,并且通过层次化的类结构,展示了面向对象编程由简单到复杂的认知过程。

举个例子,假设有一个基类 Person,定义了基本的个人信息,如姓名和年龄。现在需要创建一个 Student 类,除了拥有基本的个人信息外,还需要增加学号。通过继承,Student 类可以复用 Person 类中的代码,而不必重新编写这些属性。

class Person {
public:void Print() {cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name = "peter";  // 姓名int _age = 18;           // 年龄
};// Student类继承自Person类
class Student : public Person {
protected:int _stuid;  // 学号
};

在以上代码中,Student 类继承了 Person 类的成员函数和成员变量,这意味着 Student 类中包含了 _name_age 两个属性,以及 Print() 函数。通过继承,我们实现了代码的复用。

1.2 继承的定义

继承在 C++ 中的定义主要通过以下格式实现:

class 子类名 : 继承方式 基类名 {// 子类的成员
};

其中,继承方式 可以是 publicprotectedprivate,它们决定了基类的成员在派生类中的访问权限。

  • public 继承:基类的 public 成员在派生类中保持 publicprotected 成员保持 protected
  • protected 继承:基类的 public 成员在派生类中变为 protectedprotected 成员保持 protected
  • private 继承:基类的 publicprotected 成员在派生类中均变为 private
    在这里插入图片描述

示例代码:

class Teacher : public Person {
protected:int _jobid;  // 工号
};int main() {Student s;Teacher t;s.Print();t.Print();return 0;
}

在这个示例中,StudentTeacher 都继承了 Person 类的 Print() 函数,通过 s.Print()t.Print() 可以分别输出 StudentTeacher 对象的姓名和年龄。


第二章:继承中的访问权限

2.1 基类成员在派生类中的访问权限

基类的 publicprotectedprivate 成员在派生类中的访问权限取决于继承方式。下面是不同继承方式下的访问权限表:

类成员public 继承protected 继承private 继承
基类的 public 成员publicprotectedprivate
基类的 protected 成员protectedprotectedprivate
基类的 private 成员不可见不可见不可见

从表中可以看出,基类的 private 成员在派生类中始终不可见(不可访问),无论采用何种继承方式。然而,基类的 protected 成员和 public 成员则根据继承方式在派生类中具有不同的访问级别。

注意如果需要基类的某个成员在派生类中可访问但不希望类外部访问,则可以将其设置为 protected,这样可以更好地控制访问权限

在这里插入图片描述


2.2 基类与派生类对象的赋值转换

在C++中,基类和派生类对象的赋值转换是一个比较常见的操作场景。通常情况下,派生类对象可以赋值给基类对象,或者通过基类的指针或引用来操作派生类对象。这种转换机制使得C++在继承结构中实现了多态和代码复用。但需要注意的是,基类对象不能直接赋值给派生类对象。

2.2.1 派生类对象赋值给基类对象

派生类对象包含了基类的成员,因此派生类对象赋值给基类对象时,实际上是将派生类中属于基类的那一部分赋值给基类对象。这种操作称为切片(Slicing),即派生类对象中的基类部分被切割下来,赋值给基类对象。

在这里插入图片描述

示例代码如下:

class Person {
public:string _name;
protected:int _age;
};class Student : public Person {
public:int _stuid;
};int main() {Student s;s._name = "John";s._stuid = 1001;Person p = s;  // 切片操作,将派生类对象赋值给基类对象cout << "Name: " << p._name << endl;  // 输出 "John"// cout << p._stuid;  // 错误:基类对象无法访问派生类的成员return 0;
}

在上面的代码中,Student 对象 s 被赋值给 Person 对象 p。但是由于 Person 类没有 stuid 成员,p 无法访问 Student 类中的 _stuid 成员。因此,这里发生了切片操作,p 只保留了 Student 类中 Person 类的那部分内容。

2.2.2 基类指针和引用的转换

派生类对象可以赋值给基类的指针或引用,这是实现多态的重要前提条件。通过基类指针或引用,程序可以在运行时动态绑定到派生类的成员函数。这种方式允许我们在不需要修改代码的情况下扩展程序的功能。

class Person {
public:virtual void Print() {cout << "Person: " << _name << endl;}
protected:string _name = "Alice";
};class Student : public Person {
public:void Print() override {cout << "Student: " << _name << ", ID: " << _stuid << endl;}
private:int _stuid = 123;
};void PrintPersonInfo(Person& p) {p.Print();  // 基类引用调用虚函数,实现多态
}int main() {Student s;PrintPersonInfo(s);  // 输出 "Student: Alice, ID: 123"return 0;
}

在这个例子中,我们通过基类 Person 的引用调用 Student 类中的 Print() 函数,实现了运行时多态。派生类对象 s 被传递给基类引用 p,并正确调用了 Student 类的重写函数 Print()

2.2.3 强制类型转换的使用

在某些特殊情况下,基类指针或引用可能需要转换为派生类的指针或引用。C++ 提供了 dynamic_caststatic_cast 等多种类型转换方式。在继承关系中,使用 dynamic_cast 进行安全的类型转换尤为重要,特别是在处理多态时。

Person* pp = new Student();  // 基类指针指向派生类对象
Student* sp = dynamic_cast<Student*>(pp);  // 安全的向下转换
if (sp) {sp->Print();
} else {cout << "Type conversion failed!" << endl;
}

dynamic_cast 在运行时进行类型检查,确保转换是安全的。如果转换失败,将返回 nullptr,从而避免越界访问的风险。


第三章:继承中的作用域与成员访问

3.1 作用域的独立性与同名成员的隐藏

在继承关系中,基类与派生类各自拥有独立的作用域。如果派生类中定义了与基类成员同名的变量或函数,基类的同名成员将被隐藏,这种现象称为隐藏(Hiding)也叫重定义同名成员在派生类中会覆盖基类中的成员,导致基类成员无法被直接访问。

示例代码:

class Person {
protected:int _num = 111;  // 身份证号
};class Student : public Person {
public:Student(int num) : _num(num) {}  // 派生类中的_num覆盖了基类中的_numvoid Print() {cout << "身份证号: " << Person::_num << endl;  // 访问基类中的_numcout << "学号: " << _num << endl;  // 访问派生类中的_num}protected:int _num;  // 学号
};int main() {Student s(999);s.Print();  // 输出身份证号和学号return 0;
}

在这个例子中,Student 类中定义了一个 _num 变量,它隐藏了基类 Person 中的同名变量。为了访问基类的 _num,我们使用了 Person::_num 来显式地指定访问基类中的成员。这样可以避免由于成员同名而导致的混淆。

注意在实际中在继承体系里面最好不要定义同名的成员。

3.1.1 函数的隐藏

同名成员函数也会构成隐藏,只要函数名称相同,即使参数列表不同,也会发生隐藏。这种行为和函数重载不同。在派生类中,如果我们希望访问基类中的同名函数,必须显式调用基类的函数。

class A {
public:void fun() {cout << "A::fun()" << endl;}
};class B : public A {
public:void fun(int i) {  // 隐藏了基类的fun()cout << "B::fun(int i) -> " << i << endl;}
};int main() {B b;b.fun(10);  // 调用B::fun(int i)b.A::fun();  // 显式调用基类的fun()return 0;
}

在此代码中,派生类 B 中的 fun(int i) 函数隐藏了基类 A 中的 fun() 函数。如果我们希望调用基类的 fun() 函数,必须通过 b.A::fun() 来显式调用。这与函数重载不同,函数隐藏仅要求函数名相同,而不考虑参数列表。并且函数重载说的是同一作用域,而这里基类和派生类时两个作用域


3.2 派生类的默认成员函数

在 C++ 中,当我们不显式定义类的构造函数、拷贝构造函数、赋值运算符和析构函数时,编译器会自动为我们生成这些函数。这些自动生成的函数在派生类中也会涉及到对基类成员的操作,因此在继承体系中了解这些默认成员函数的调用规则非常重要。
在这里插入图片描述

3.2.1 构造函数的调用顺序

在派生类对象的构造过程中,基类的构造函数会优先于派生类的构造函数被调用如果基类没有默认构造函数,则派生类的构造函数必须在初始化列表中显式调用基类的构造函数

class Person {
public:Person(const string& name) : _name(name) {cout << "Person constructor called!" << endl;}protected:string _name;
};class Student : public Person {
public:Student(const string& name, int stuid) : Person(name), _stuid(stuid) {cout << "Student constructor called!" << endl;}private:int _stuid;
};int main() {Student s("Alice", 12345);return 0;
}

输出

Person constructor called!
Student constructor called!

在这个例子中,Student 类的构造函数首先调用了 Person 类的构造函数来初始化基类部分。随后才执行派生类 Student 的构造函数。这种调用顺序确保基类的成员在派生类构造之前就已经被正确初始化。

3.2.2 拷贝构造函数与赋值运算符的调用

当派生类对象被拷贝时,基类的拷贝构造函数会先被调用,然后才是派生类的拷贝构造函数。同样,赋值运算符的调用顺序也遵循这一规则:基类的赋值运算符会先于派生类的赋值运算符被调用。

class Person {
public:Person(const string& name) : _name(name) {}// 拷贝构造函数Person(const Person& p) {_name = p._name;cout << "Person copy constructor called!" << endl;}// 赋值运算符Person& operator=(const Person& p) {_name = p._name;cout << "Person assignment operator called!" << endl;return *this;}protected:string _name;
};class Student : public Person {
public:Student(const string& name, int stuid) : Person(name), _stuid(stuid) {}// 拷贝构造函数Student(const Student& s) : Person(s) {_stuid = s._stuid;cout << "Student copy constructor called!" << endl;}// 赋值运算符Student& operator=(const Student& s) {Person::operator=(s);  // 先调用基类的赋值运算符_stuid = s._stuid;cout << "Student assignment operator called!" << endl;return *this;}private:int _stuid;
};int main() {Student s1("Alice", 12345);Student s2 = s1;  // 拷贝构造函数Student s3("Bob", 54321);s3 = s1;  // 赋值运算符return 0;
}

输出

Person copy constructor called!
Student copy constructor called!
Person assignment operator called!
Student assignment operator called!

在拷贝构造和赋值操作过程中,基类部分总是优先于派生类部分进行初始化或赋值操作。为了保证派生类对象的完整性,派生类的拷贝构造函数和赋值运算符必须调用基类的相应函数,确保基类成员正确处理。

3.2.3 析构函数的调用顺序

与构造函数的调用顺序相反,析构函数的调用顺序是先调用派生类的析构函数,然后再调用基类的析构函数。这确保了派生类的资源先被释放,然后基类的资源才能安全地释放。

class Person {
public:Person(const string& name) : _name(name) {}~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() {Student s("Alice", 12345);return 0;
}

输出

Student destructor called!
Person destructor called!

可以看到,当 Student 对象 s 析构时,首先调用了 Student 的析构函数,随后调用了 Person 的析构函数。这种析构顺序确保派生类资源(如成员变量 _stuid)被先行清理,而基类的资源(如 _name)则在派生类资源清理后再进行释放。

3.2.4 虚析构函数

在继承体系中,若希望基类指针指向派生类对象,并通过该指针安全地释放对象,基类的析构函数应当定义为虚函数。否则,仅会调用基类的析构函数,导致派生类资源没有正确释放,从而引发内存泄漏。

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;
}

输出

Student destructor called!
Person destructor called!

通过将基类的析构函数声明为 virtual,当通过基类指针删除派生类对象时,派生类的析构函数将首先被调用,从而确保所有派生类的资源被正确释放。

在这里插入图片描述


总结

通过本篇文章的学习,我们深入了解了 C++ 中继承的基本概念、继承方式对成员访问的影响、对象赋值转换的机制,以及如何处理同名成员的隐藏问题。我们还讨论了派生类默认成员函数的调用顺序和析构函数的正确使用方式。

继承机制使得我们能够有效地复用代码,同时为程序设计提供了层次结构。但在实际开发中,继承的设计需要谨慎,避免出现复杂的层次结构。在下一篇文章中,我们将进一步探讨 虚拟继承 的使用,解决多继承中常见的问题,敬请期待!

💬 讨论区:如果你在学习过程中有任何疑问,欢迎在评论区留言讨论。
👍 支持一下:如果你觉得这篇文章对你有帮助,请点赞、收藏并分享给更多 C++ 学习者!你的支持是我继续创作的动力。


以上就是关于【C++篇】继承之韵:解构编程奥义,领略面向对象的至高法则的内容啦,各位大佬有什么问题欢迎在评论区指正,或者私信我也是可以的啦,您的支持是我创作的最大动力!❤️

在这里插入图片描述

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

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

相关文章

OkHttp

OkHttp是一个用于Android和Java应用的高效HTTP客户端库。它具有以下优点&#xff1a; 优点 高效连接池&#xff1a; 支持连接复用&#xff08;Connection Pooling&#xff09;减少延迟。有效管理HTTP/2多路复用。 透明压缩&#xff1a; 自动处理Gzip压缩&#xff0c;减少传输…

Label Studio 半自动化标注

引言 Label Studio ML 后端是一个 SDK,用于包装您的机器学习代码并将其转换为 Web 服务器。Web 服务器可以连接到正在运行的 Label Studio 实例,以自动执行标记任务。我们提供了一个示例模型库,您可以在自己的工作流程中使用这些模型,也可以根据需要进行扩展和自定义。 1…

dotnet7==windows ZIP方式安装和web demo和打包

下载ZIP Download .NET 7.0 (Linux, macOS, and Windows) 解压 创建项目 mkdir MyWebApp cd MyWebApp "C:\Users\90816\Downloads\dotnet-sdk-7.0.317-win-x64\dotnet.exe" new webapp -n MyWebApp 运行项目 "C:\Users\90816\Downloads\dotnet-sdk-7.0.317-…

k8s的简介和部署

一、k8s简介 在部署应用程序的方式上面&#xff0c;主要经历了三个阶段&#xff1a; 传统部署:互联网早期&#xff0c;会直接将应用程序部署在物理机上优点:简单&#xff0c;不需要其它技术的参与缺点:不能为应用程序定义资源使用边界&#xff0c;很难合理地分配计算资源&…

Docker 实践与应用举例

Docker 实践与应用举例 Docker 已经成为现代软件开发和部署中的重要工具&#xff0c;通过容器化技术&#xff0c;开发者可以轻松管理应用的依赖环境、简化部署流程&#xff0c;并实现跨平台兼容性。本篇博客将详细介绍 Docker 的基本概念、实践操作以及应用场景&#xff0c;帮…

【软件部署安装】OpenOffice转换PDF字体乱码

现象与原因分析 执行fc-list查看系统字体 经分析发现&#xff0c;linux默认不带中文字体&#xff0c;因此打开我们本地的windows系统的TTF、TTC字体安装到centos机器上。 安装字体 将Windows的路径&#xff1a; C:\Windows\Fonts 的中文字体&#xff0c;如扩展名为 TTC 与TT…

shell脚本写代码

用简单的test语句来判断是否闰年 #! /bin/bash read -p "sd " yearif [ $((year%4)) -eq 0 -a $((year%100)) -ne 0 -o $((year%400)) -eq 0 ]thenecho "是润年"elseecho "不是闰年" fi判断一个数是否为偶数 #! /bin/bash read -p "…

LINUX——内核移植、内核编译教程

Linux内核编译是一个将内核源代码转换成可在特定硬件架构上运行的二进制文件的过程。以下是编译Linux内核的一般步骤&#xff1a; 1、准备工作&#xff1a; 确保安装了必要的编译工具&#xff0c;如gcc、make、ncurses库&#xff08;用于make menuconfig&#xff09;等。 2、…

数据结构-八大排序之堆排序

堆排序 1.1 基础知识 原理&#xff1a; 1. 利用完全二叉树构建大顶堆 2. 堆顶元素和堆底元素进行交换&#xff0c;除堆底元素之外其余元素继续构建大顶堆 3. 重复2&#xff0c;直到所有元素都不参与构建 整个数组排序完成 完全二叉树&#xff1a; 数据从上到下&#x…

雷池+frp 批量设置proxy_protocol实现真实IP透传

需求 内网部署safeline&#xff0c;通过frp让外网访问内部web网站服务&#xff0c;让safeline记录真实外网攻击IP safeline 跟 frp都部署在同一台服务器&#xff1a;192.168.2.103 frp client 配置 frpc只需要在https上添加transport.proxyProtocolVersion "v2"即…

基于SpringBoot的设备管理系统源码带本地搭建教程

技术框架&#xff1a;SpringBoot mybatis thymeleaf Mysql5.7 Fastjson Druid Shiro 运行环境&#xff1a;jdk8 IntelliJ IDEA maven 宝塔面板 系统功能&#xff1a;登陆&#xff0c;注册&#xff0c;系统用户管理&#xff0c;角色&#xff0c;部门管理&#xff0c;…

软件测试之压力测试

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 压力测试 压力测试是一种软件测试&#xff0c;用于验证软件应用程序的稳定性和可靠性。压力测试的目标是在极其沉重的负载条件下测量软件的健壮性和错误处理能力&…

《恋与深空》陷抄袭争议,但不影响它登顶App Store畅销总榜

伴随着《恋与深空》全新混池而来的&#xff0c;是文案疑似抄袭的负面新闻。 9月23日&#xff0c;《恋与深空》上线了第一个国风混池“欲揽旖旎色”&#xff0c;但比玩家的夸奖与反馈更先来的&#xff0c;是男主角之一秦彻的剧情文案抄袭的争议&#xff0c;#恋与深空 抄袭#火速…

渗透测试入门学习——使用python脚本自动跟踪csrf_token实现对网站登录界面的暴力破解

目录 写在前面 使用方法 相关代码 写在前面 最近在学习使用Burp Suite时发现其intruder模块无法实现多种模式的混合使用&#xff0c;就如想要暴力破解账号和口令两个区域并同时跟踪网页的csrf_token时BP似乎不能很方便的实现这一功能&#xff0c;于是自己在练习时就想到了用…

三 星 SCX-4521F 硒 鼓 清 零 及 一 般 故 障 维 修 浅 谈

基本参数 耗材容量:SCX-4521D3/XIL(3000页) 功 率:平均功率350W、休眠模式10W 一般故障讲解 一、三星SCX-4521F打印机更换硒鼓(或加粉)后仍显示墨粉用尽 (加粉清零、关闭碳粉通知) 按菜单------#1934(快速按完)------屏幕会有TECH字母显示------菜单------向…

0基础跟德姆(dom)一起学AI 机器学习04-逻辑回归

逻辑回归简介 应用场景 逻辑回归是解决二分类问题的利器 数学知识 sigmoid函数 概率 极大似然估计 核心思想&#xff1a; 设模型中含有待估参数w&#xff0c;可以取很多值。已经知道了样本观测值&#xff0c;从w的一切可能值中&#xff08;选出一个使该观察值出现的概率为…

Java8新特性, 函数式编程及Stream流用法大全

用了多少年的java8了&#xff0c;Lambda表达式和stream流也经常用&#xff0c;但是也仅限于某些用法比较熟练&#xff0c;看见了 Function、Consumer 等函数式接口还是一脸懵逼&#xff0c;现在来全面总结一下java8这些新特性&#xff0c;也为自己后续查找做个备忘。如果你只是…

启用vnc访问Dell 服务器IDRAC 7虚拟控制台

Dell IDRAC 7 版本太老&#xff0c;SSL证书过期&#xff0c;IDRAC的Java和本地远程虚拟机控制台访问不了&#xff0c;怎么办&#xff1f; 可以启用vnc访问IDRAC 虚拟控制台

解决雪花ID在前端精度丢失问题

解决雪花ID在前端精度丢失问题 在现代分布式系统中&#xff0c;雪花算法&#xff08;Snowflake&#xff09;被广泛用于生成唯一的ID。这些ID通常是Long类型的整数。然而&#xff0c;当这些ID从后端传递到前端时&#xff0c;JavaScript的精度限制可能会导致精度丢失&#xff0c…

Leetcode: 0021-0030题速览

Leetcode: 0021-0030题速览 本文材料来自于LeetCode solutions in any programming language | 多种编程语言实现 LeetCode、《剑指 Offer&#xff08;第 2 版&#xff09;》、《程序员面试金典&#xff08;第 6 版&#xff09;》题解 遵从开源协议为知识共享 版权归属-相同方式…