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

news2024/11/27 12:27:24

文章目录

  • 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.coloradmin.cn/o/2197922.html

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

相关文章

多端同步的收银系统源码

随着经济的不断发展&#xff0c;很多门店越来越趋向连锁品牌化&#xff0c;收银系统自然也成为很多连锁门店必不可少的软件工具。希望通过一套软件可以帮助门店解决门店线下销售、会员管理、连锁多门店管理、线下线上一体化、商品库存管理等难题实现降本增效&#xff0c;为了方…

MySQL连接查询:联合查询

先看我的表结构 emp表 联合查询的关键字&#xff08;union all, union&#xff09; 联合查询 基本语法 select 字段列表 表A union all select 字段列表 表B 例子&#xff1a;将薪资低于5000的员工&#xff0c; 和 年龄大于50 岁的员工全部查询出来 第一种 select * fr…

大模型微调技术之 LoRA:开启高效微调新时代

一、LoRA 简介 LoRA&#xff0c;即低秩适应&#xff08;Low-Rank Adaptation&#xff09;&#xff0c;是一种用于微调大型语言模型的技术&#xff0c;旨在以较小的计算资源和数据量实现模型的快速适应特定任务或领域。 LoRA 方法通过引入低秩近似的思想&#xff0c;对大型预训…

NFS共享文件系统(将文件目录挂载到别的机器上)

我们创建的磁盘是否都必须挂载到本机上&#xff1f;并不是。在 Linux 和其他操作系统中&#xff0c;有一种叫做 NFS&#xff08;网络文件系统&#xff09;的工具&#xff0c;它允许跨网络共享文件系统资源。通过使用 NFS&#xff0c;我们可以将多个客户端服务器的数据目录挂载到…

Java中常见的等待唤醒机制及实践

JDK自带的等待唤醒机制 在Java中&#xff0c;有一个JDK维度的等待唤醒机制。Object类的wait和notify,notifyAll 需要在synchronized同步代码块内并且对象必须获取到锁才能调用。否则会抛IllegalMonitorStateException异常。 当线程在尝试获取锁时失败&#xff0c;会被封装成节…

Mybatis-plus做了什么

Mybatis-plus做了什么 Mybatis回顾以前的方案Mybatis-plus 合集总览&#xff1a;Mybatis框架梳理 聊一下mybatis-plus。你是否有过疑问&#xff0c;Mybatis-plus中BaseMapper方法对应的SQL在哪里&#xff1f;它为啥会被越来越多人接受。在Mybatis已经足够灵活的情况下&…

《强烈推荐一个强大的书签管理工具》

在信息爆炸的时代&#xff0c;我们每天都会浏览大量的网页&#xff0c;收藏各种各样的书签。然而&#xff0c;随着书签数量的增加&#xff0c;管理起来也变得越来越困难。这时&#xff0c;一个强大的书签管理工具就显得尤为重要。今天&#xff0c;我要向大家推荐一款备受好评的…

EtherCAT学习笔记

文章目录 前言一、EtherCAT介绍二、EtherCA系统组成2.1 ESC(EtherCAT从站控制器)2.2 从站控制微处理器2.3 物理层器件2.4 其它应用层器件 三、EtherCAT数据帧结构3.1 寻址方式3.2 时钟3.3 通信模式 四、状态机和通信初始化五、应用层协议六、ESC概述6.1 EtherCAT从站控制芯片6.…

基于SpringBoot+Vue+MySQL的美食信息推荐系统

系统展示 用户前台界面 管理员后台界面 系统背景 在数字化时代&#xff0c;随着人们对美食文化的热爱与追求不断增长&#xff0c;美食信息推荐系统成为了连接食客与美食之间的重要桥梁。面对海量的美食信息&#xff0c;用户往往难以快速找到符合个人口味和需求的美食。因此&…

Java-数据结构-Lambda表达式 (✪ω✪)

文本目录&#xff1a; ❄️一、背景&#xff1a; ➷ 1、Lambda表达式的语法&#xff1a; ➷ 2、函数式接口&#xff1a; ❄️二、Lambda表达式的基本使用&#xff1a; ➷ 1、语法精简&#xff1a; ❄️三、变量捕获&#xff1a; ❄️四、Lambda在集合中的使用&#xff1a; …

Chromium 中js navigator对象c++实现分析

一、Navigator 对象 Navigator 对象包含有关浏览器的信息。 前端测试例子&#xff1a; <!DOCTYPE html> <html> <head> <meta charset"utf-8"> <title>接口测试</title> </head> <body><div id"example&q…

爱心表达公式

脚本公式 local r (math.sin(angle) * math.sqrt(math.abs(math.cos(angle)))) / (math.sin(angle) 1.4) - 2 * math.sin(angle) 2

IOT-Tree连接西门子PLC S7 200 Smart竟然如此简单

最近一个项目需要把用户现场控制柜接入到云端&#xff0c;控制柜使用西门子PLC Smart 200 SR40型号&#xff0c;已经运行多年&#xff0c;PLC通过以太网接口对接一个触摸屏。 按照我以往的经验&#xff0c;觉得触摸屏以太网接口已经被占用&#xff0c;那么只能通过剩余的RS485…

通过一个实际的例子,介绍 Java 的自动装箱与拆箱机制

Java 中 1000 1000 返回 false&#xff0c;但 100 100 返回 true&#xff0c;这一现象背后隐藏了 Java 对于对象和基本类型的内存管理机制。为了理解这个现象&#xff0c;我们需要从 Java 的自动装箱与拆箱机制、对象引用和数值缓存策略等角度深入探讨。让我们一步一步通过 J…

电脑怎么卸载软件?学会这6个卸载软件技巧就够了(精选)

电脑怎么卸载软件&#xff1f;在日常的办公生活中&#xff0c;我们需要下载一些工具来辅助工作&#xff0c;当不需要这些工具的时候&#xff0c;我们就需要卸载这些软件了。很多小伙伴表示卸载软件卸载不干净&#xff0c;还是回残留一些文件&#xff0c;或者是卸载不了&#xf…

Verilog开源项目——百兆以太网交换机(九)表项管理模块设计

Verilog开源项目——百兆以太网交换机&#xff08;九&#xff09;表项管理模块设计 &#x1f508;声明&#xff1a;未经作者允许&#xff0c;禁止转载 &#x1f603;博主主页&#xff1a;王_嘻嘻的CSDN主页 &#x1f511;全新原创以太网交换机项目&#xff0c;Blog内容将聚焦整…

新型僵尸网络针对 100 个国家发起 30 万次 DDoS 攻击

近日&#xff0c;网络安全研究人员发现了一个名为 Gorilla&#xff08;又名 GorillaBot&#xff09;的新僵尸网络恶意软件家族&#xff0c;它是已泄露的 Mirai 僵尸网络源代码的变种。 网络安全公司 NSFOCUS 在上个月发现了这一活动&#xff0c;并称该僵尸网络在今年 9 月 4 日…

【Java 循环控制实例详解【While do... while】】

Java 循环控制详解【While & do… while】 在 Java 中&#xff0c;循环控制是程序设计中非常重要的部分&#xff0c;主要包括 while 循环和 do...while 循环。本文将详细介绍这两种循环的基本语法、执行流程及相关示例。 1. while 循环控制 基本语法 循环变量初始化; wh…

在uniapp中实现长按聊天对话框可以弹出对话框然后可以删除该条对话,单击可以进入该条对话框的对话页面

效果展示 效果描述 长按【大于1s】某一条对话框会弹出一个对话框&#xff0c;点击确定按钮就可以将当前对话框从列表中进行删除&#xff0c;如果点击取消则不做额外操作。 如果只是点击了一下&#xff0c;时间【小于1s】的情况下会直接引入到与该用户的对话框详情页面。 代码…

ai绘画变现方式全解析,教你如何通过AI绘画赚钱

*AI绘画变现方式全解析&#xff0c;教你如何通过AI绘画赚钱* *为什么选择AI绘画&#xff1f;* 你是否曾经梦想过成为一名画家&#xff0c;但现实却让你无从下手? 或者你已经是一位艺术家&#xff0c;但苦于作品没能带来足够的收入&#xff1f;随着AI技术的飞速发展&#xff…