【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.xdnf.cn/news/1557970.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

leetcode68:文本左右对齐

给定一个单词数组 words 和一个长度 maxWidth &#xff0c;重新排版单词&#xff0c;使其成为每行恰好有 maxWidth 个字符&#xff0c;且左右两端对齐的文本。 你应该使用 “贪心算法” 来放置给定的单词&#xff1b;也就是说&#xff0c;尽可能多地往每行中放置单词。必要时可…

Ubuntu 22.04 安装 KVM

首先检查是否支持 CPU 虚拟化&#xff0c;现在的 CPU 都应该支持&#xff0c;运行下面的命令&#xff0c;大于0 就是支持。 egrep -c (vmx|svm) /proc/cpuinfo安装 Libvirt apt install -y qemu-kvm virt-manager libvirt-daemon-system virtinst libvirt-clients bridge-uti…

DAMA数据管理知识体系(第11章 数据仓库和商务智能)

课本内容 11.1 引言 概要 数据仓库被公认为企业数据管理的核心语境关系图 图11-1 语境关系图&#xff1a;数据仓库和商务智能业务驱动因素 运营支持职能合规需求商务智能活动目标和原则 目标 一个组织建设数据仓库的目标通常有&#xff1a; 1&#xff09;支持商务智能活动。 2&…

易图讯军用VR三维电子沙盘系统

深圳易图讯军用VR三维电子沙盘系统是一种集成了虚拟现实&#xff08;VR&#xff09;技术、三维建模技术、大数据分析、实时动态更新以及高度安全可靠的综合性军事指挥平台。该系统通过高精度三维模型真实再现战场环境&#xff0c;为指挥员提供沉浸式体验和交互操作的可能性&…

数据结构与算法——Java实现 31.阻塞队列

—— 24.10.8 一、问题提出 目前队列存在的问题 1.很多场景要求分离生产者、消费者两个角色、它们需要由不同的线程来担当&#xff0c;而之前的实现根本没有考虑线程安全问题 2.poll方法&#xff0c;队列为空&#xff0c;那么在之前的实现里会返回null&#xff0c;如果就是硬…

构建MySQL健康检查Web应用

构建MySQL健康检查Web应用 在这里将探讨如何将MySQL健康检查功能转换为一个功能完整的Web应用。这个应用允许用户通过简单的Web界面执行MySQL健康检查&#xff0c;并查看详细的结果。我们将逐步介绍代码实现、改进过程以及如何设置和运行这个应用。 1. MySQL健康检查类 首先…

codetop标签双指针题目大全解析(二),双指针刷穿地心!!!!!

复习比学习更重要&#xff0c;如果忘了就跟没学是一样的 1.和为k的子数组2.统计[优美子数组]3.区间列表的交集4.将x减到0的最小操作5.替换子串得到平衡字符串6.划分字母区间7.分隔链表8.通过删除字母匹配到字典里最长单词9.寻找目标值-二维数组10.至多包含两个不同字符的最长子…

麒麟系统串口配置篇

麒麟系统串口配置篇 1.配置串口驱动&#xff08;编译/动态加载串口&#xff09; 解压文件夹,然后在解压后的文件夹所在目录&#xff0c;右键选择打开终端&#xff0c;依次执行以下命令&#xff1a; 以麒麟系统下的CH341串口驱动为例&#xff0c;解压CH341SER_LINUX.zip sudo…

2024_10_8 系统进展

改进位置 发现是label_api里藏了我需要改进的东西 settings.py 数据库 我这边电脑上使用的是windows 192 vue.config.js 陈家强是这样设置的 module.exports {publicPath: process.env.NODE_ENV production? /: /,assetsDir: static,// css: {// extract: false// },…

【C++ 11】for 基于范围的循环

文章目录 【 1. 基本用法 】【 2. for 新格式的应用 】2.1 for 遍历字符串2.2 for 遍历列表2.3 for 遍历的同时修改元素 问题背景 C 11标准之前&#xff08;C 98/03 标准&#xff09;&#xff0c;如果要用 for 循环语句遍历一个数组或者容器&#xff0c;只能套用如下结构&#…

“我养你啊“英语怎么说?别说成I raise you!成人学英语到蓝天广场附近

“我养你啊”这句经典台词出自周星驰自导自演的电影《喜剧之王》。在这部电影中&#xff0c;周星驰饰演的尹天仇对张柏芝饰演的柳飘飘说出了这句深情而动人的台词。这句台词出现在柳飘飘即将离去之时&#xff0c;尹天仇鼓起勇气&#xff0c;用它作为对柳飘飘个人困境的承诺&…

VIP与MPIO,备胎管理谁更强

我爱上班&#xff0c;风雨无阻 大家每天去上班&#xff0c;不可能只有一条路线 可以地铁、也可以开车或公交 万一地铁停运或车子限行&#xff0c;至少还有其他线路选择 企业级存储也是如此 关键业务的存储访问一般有多条路径 网络或单个存储设备故障后 访问路径会自动切换…

集合框架05:List接口使用、List实现类、ArrayList使用

视频链接&#xff1a;13.11 ArrayList使用_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1zD4y1Q7Fw?p11&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 1.List接口使用代码举例 package com.yundait.Demo01;import java.util.ArrayList; import java.util.List;pu…

轻松掌握IP代理服务器设置方法,网络冲浪更自如

在数字化时代&#xff0c;互联网就像是一片浩瀚的海洋&#xff0c;而IP代理服务器就如同我们在这片海洋中航行的指南针。通过使用代理IP&#xff0c;我们可以更方便地访问全球网络资源&#xff0c;提升网络安全性。本文将为您详细介绍IP代理服务器的设置方法&#xff0c;让您在…

指针——指针数组、数组指针

&#xff08;一&#xff09;指针数组 1、本质&#xff1a;指针数组的本质任然是数组 2、基本格式&#xff1a;int* arr[5] 3、应用&#xff1a;如尝试使用指针来模拟二维数组 先来看代码 #include<stdio.h> //指针数组——模拟实现二维数组 int main() {int a[5] {…

本科毕业论文不会写怎么办,论文查重显示80%多

如果本科毕业论文不会写且查重显示 80% 多&#xff0c;可以从以下几个方面着手解决&#xff1a; 一、调整心态&#xff0c;正视问题 首先&#xff0c;不要惊慌和焦虑。高重复率并不意味着无法挽救&#xff0c;要相信自己有能力解决这个问题。把它看作是一个学习和提升的机会&a…

Matlab实现海鸥优化算法优化回声状态网络模型 (SOA-ESN)(附源码)

目录 1.内容介绍 2部分代码 3.实验结果 4.内容获取 1内容介绍 海鸥优化算法&#xff08;Seagull Optimization Algorithm, SOA&#xff09;是一种受海鸥觅食和飞行行为启发的群体智能优化算法。SOA通过模拟海鸥在空中搜寻食物、聚集和分散的行为模式&#xff0c;来探索和开发…

Leecode刷题之路第13天之罗马数字转整数

题目出处 13-罗马数字转整数-题目出处 题目描述 个人解法 思路&#xff1a; todo 代码示例&#xff1a;&#xff08;Java&#xff09; todo复杂度分析 todo 官方解法 13-罗马数字转整数-官方解法 方法1&#xff1a;模拟 思路&#xff1a; 代码示例&#xff1a;&#xff0…

ctf.bugku - game1

题目来源&#xff1a; game1 - Bugku CTF 访问页面&#xff0c;让玩游戏 得到100分&#xff0c;没拿到flag 查看页面源码&#xff0c; GET请求带有 score、IP、sign 三个参数&#xff0c;最后的flag 应该跟分数有关&#xff1b; 给了score一个99999分数&#xff0c; sign 为 …

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-…