【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覆盖了基类中的_num

    void Print() {
        cout << "身份证号: " << Person::_num << endl;  // 访问基类中的_num
        cout << "学号: " << _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.mfbz.cn/a/888518.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

[Algorithm][贪心][可被三整除的最大和][距离相等的条形码][重构字符串]详细讲解

目录 1.可被三整除的最大和1.题目链接2.算法原理详解3.代码实现 2.距离相等的条形码1.题目链接2.算法原理详解3.代码实现 3.重构字符串1.题目链接2.算法原理详解3.代码实现 1.可被三整除的最大和 1.题目链接 可被三整除的最大和 2.算法原理详解 思路&#xff1a;正难则反 贪…

【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;只能套用如下结构&#…

YOLO11改进|卷积篇|引入可变核卷积AKConv

目录 一、AKConv卷积1.1AKConv卷积介绍1.2AKConv核心代码 五、添加MLCA注意力机制5.1STEP15.2STEP25.3STEP35.4STEP4 六、yaml文件与运行6.1yaml文件6.2运行成功截图 一、AKConv卷积 1.1AKConv卷积介绍 AKConv允许卷积参数的数量以线性方式增加或减少&#xff0c;而不是传统的…

大模型生成PPT大纲优化方案:基于 nVidia NIM 平台的递归结构化生成

大模型生成PPT大纲优化方案&#xff1a;基于 nVidia NIM 平台的递归结构化生成 待解决的问题 生成PPT大纲是一种大模型在办公场景下应用的常见需求。 然而&#xff1a; 目前直接让大模型生成大纲往往是非结构化的&#xff0c;输出格式多样&#xff0c;难以统一和规范&#…

数据结构-5.1.树的定义和基本术语

一.树的基本概念&#xff1a; 1.根结点&#xff1a;最顶层的结点&#xff0c;有且仅有一个&#xff0c;没有前驱&#xff1b; 2.叶子结点&#xff1a;不能再有子结点&#xff0c;没有后继&#xff1b; 3.结点&#xff1a;用于存数据&#xff1b; 4.也有前驱和后继的说法&…

制造企业MES管理系统的应用策略与实施路径

在智能制造浪潮的席卷之下&#xff0c;MES管理系统作为连接生产计划与车间操作的核心桥梁&#xff0c;其战略地位愈发显著。本文旨在深入剖析MES管理系统在智能制造转型中的核心价值、实施策略及实践路径&#xff0c;为制造企业探索智能化生产之路提供实践指导与灵感启发。 MES…

JavaScript函数基础(通俗易懂篇)

10.函数 10.1 函数的基础知识 为什么会有函数&#xff1f; 在写代码的时候&#xff0c;有一些常用的代码需要书写很多次&#xff0c;如果直接复制粘贴的话&#xff0c;会造成大量的代码冗余&#xff1b; 函数可以封装一段重复的javascript代码&#xff0c;它只需要声明一次&a…

github 搭建个人导航网

最近搭建了个 个人的导航网&#xff0c;具体内容见下图&#xff0c;欢迎大家访问吖&#xff0c;点击访问 具体实现是使用 vue3 编写&#xff0c;白嫖 github 的 page 部署 首先在 github上创建一个仓库&#xff1a;name.github.io # name是你 github 的名字 然后在本地创建一…

◇【论文_20181020 v6】广义优势估计器 (generalized advantage estimator, GAE)

https://arxiv.org/abs/1506.02438 ICLR 2016 加州伯克利 电子工程与计算机科学系 High-Dimensional Continuous Control Using Generalized Advantage Estimation 文章目录 摘要1 引言2 预备知识3 优势函数估计4 解释为 奖励设计reward shaping5 价值函数估计6 实验6.1 策略优…

九、Drf序列化器

九、序列化器 9.1序列化 从数据库取QuerySet或数据对象转换成JSON 9.1.1序列化器的简易使用 #新建一张部门表 class Depart(models.Model):titlemodels.CharField(verbose_name部门,max_length32)ordermodels.IntegerField(verbose_name顺序)countmodels.IntegerField(verb…

C语言_内存函数

内存函数是 C 标准库中的一组函数&#xff0c;用于管理和操作内存。使用时需要包含头文件<string.h>。 1. memcpy的使用和模拟实现 函数形式如下&#xff1a; void* memcpy(void* destination, const void* source, size_tnum);函数解析和注意事项&#xff1a; memcp…

LinuxO(1)调度算法

概念 在Linux中&#xff0c;O(1)调度算法是一种进程调度算法。O(1)表示算法的时间复杂度是常数级别的&#xff0c;与系统中的进程数量无关。 运行队列结构 他采用了两个运行队列&#xff0c;一个活动队列和一个过期队列。活动队列中的进程是有资格获取CPU时间片的进程&#x…

AWS EC2 部署Echarts大屏展示项目

前言 Echarts简介 ECharts是一个由JavaScript开发的开源可视化库。它能使数据生动、直观、互动、高度个性化数据可视化图表。ECharts适用大部分浏览器&#xff0c;如IE6 、Chrome、Firefox、Safari等&#xff0c;同时支持PC和移动设备。 开源&#xff1a;ECharts是一个开源项目…

【JavaEE】——文件IO

阿华代码&#xff0c;不是逆风&#xff0c;就是我疯 你们的点赞收藏是我前进最大的动力&#xff01;&#xff01; 希望本文内容能够帮助到你&#xff01;&#xff01; 目录 一&#xff1a;认识文件 1&#xff1a;文件的概念 2&#xff1a;文件的结构 3&#xff1a;文件路径…

矩阵求解复数(aniwoth求解串扰)

所以这种求解串扰的格式是因为&#xff0c;有串扰的共轭项在方程组中 复数共轭项的作用&#xff0c;但是这是二次方程&#xff0c;

【深度学习】yolov8n模型的剪枝操作记录

原始 剪枝微调后 可以看到模型大小了&#xff0c; 测试结果显示再cpu 上加速5%-10% from ultralytics import YOLOimport time # Load a pretrained YOLO11n model count_num 500 def test1():model YOLO("/home/justin/Desktop/code/v8_prun/runs/detect/train3/weig…

算法知识点————贪心

贪心&#xff1a;只考虑局部最优解&#xff0c;不考虑全部最优解。有时候得不到最优解。 DP&#xff1a;考虑全局最优解。DP的特点&#xff1a;无后效性&#xff08;正在求解的时候不关心前面的解是怎么求的&#xff09;&#xff1b; 二者都是在求最优解的&#xff0c;都有最优…

微服务实战——ElasticSearch(保存)

商品上架——ElasticSearch&#xff08;保存&#xff09; 0.商城架构图 1.商品Mapping 分析&#xff1a;商品上架在 es 中是存 sku 还是 spu &#xff1f; 检索的时候输入名字&#xff0c;是需要按照 sku 的 title 进行全文检索的检索使用商品规格&#xff0c;规格是 spu 的…

No package nodejs available.No package npm available.

安装nodejs时出现的报错 这个错误的原因是当前的 yum 源没有包含 Node.js 和 npm 的安装包。 解决方法 使用 NodeSource 仓库 curl -fsSL https://rpm.nodesource.com/setup_14.x | bash -运行 yum install 安装 Node.js 和 npm&#xff1a; yum install -y nodejs使用 E…

深入了解Oracle OCP认证,开启数据库专业之旅

使用Oracle数据库的公司内部&#xff0c;经常有员工们在讨论OCP认证(Oracle Certified Professional&#xff0c;Oracle认证专家)&#xff0c;这是甲骨文Oracle公司提供的一种专业认证&#xff0c;认证用于使用者在Oracle技术领域的专业知识和技能。 在这里&#xff0c;有一点…