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

news2024/11/25 12:41:44

文章目录

  • 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/2207738.html

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

相关文章

Spark练习-RDD创建,读取hdfs上的数据,指定rdd分区

目录 RDD的创建 读取HDFS上文件数据 RDD分区指定 RDD的创建 将python数据转为rdd # 将Python数据转为rdd data [1,2,3,4] res sum(data) # 使用python的方法计算时&#xff0c;采用的单机资源计算&#xff0c;如果数据量较大时&#xff0c;可以将python数据转为spark的r…

QD1-P2 HTML 编辑器:HBuilderX

本节学习&#xff1a; HTML课程内容介绍HBuilderX编辑器的使用 本节视频 www.bilibili.com/video/BV1n64y1U7oj?p2 HTML 内容 基础语法 标签整体架构DOCTYPE 常用标签 标题和水平线段落和换行列表div 和 span格式化标签图片超链接标签表格表单字符实体 编辑器 HBuilder…

C/C++逆向:函数逆向分析-总体流程(整型指针)

函数的初始化 在逆向工程中&#xff0c;函数的初始化操作是函数在开始执行时&#xff0c;为正确运行而进行的准备工作。通常&#xff0c;这些操作发生在函数的序言&#xff08;Prologue&#xff09;阶段&#xff0c;具体的内容和顺序会因编译器、调用约定和目标平台&#xff0…

【AIGC】ChatGPT提示词Prompt高效编写模式:思维链、Self-Consistency CoT与Zero-Shot CoT

博客主页&#xff1a; [小ᶻZ࿆] 本文专栏: AIGC | ChatGPT 文章目录 &#x1f4af;前言&#x1f4af;思维链 (Chain of Thought, CoT)如何工作应用实例优势结论 &#x1f4af;一致性思维链 (Self-Consistency CoT)如何工作应用实例优势结论 &#x1f4af;零样本思维链 (Ze…

MC802单片机:触控未来,8位高性能与多IO接口的完美结合

MC802单片机&#xff1a;开启智能生活新篇章 MC802 &#xff08;2 Touch Key 4 I/O&#xff09; MC802是由厦门晶尊微电子科技有限公司&#xff08;ICman&#xff09;推出的一款高性能8位单片机&#xff0c;它集成了2个自校正容性触摸按键和4个I/O口&#xff0c;专为需要多…

地级市-制造业集聚水平数据(2008-2021年)

制造业集聚水平是衡量一个地区制造业发展程度的重要指标&#xff0c;它不仅反映了制造业在地理上的集中程度&#xff0c;还体现了该地区制造业的专业化水平。 制造业集聚水平通常通过以下几个量化指标来衡量&#xff1a; 年末单位从业人员数&#xff1a;反映了制造业的劳动力…

如何替换OCP节点(二):使用 antman脚本 | OceanBase应用实践

前言&#xff1a; OceanBase Cloud Platform&#xff08;简称OCP&#xff09;&#xff0c;是 OceanBase数据库的专属企业级数据库管理平台。 在实际生产环境中&#xff0c;OCP的安装通常是第一步&#xff0c;先搭建OCP平台&#xff0c;进而依赖OCP来创建、管理和监控我们的生…

02_安装jmeter

windows&#xff1a; 安装jdk1.8.0&#xff1a; 1、下载安装包&#xff0c;双击运行安装&#xff0c;点击“下一步”直到完成 2、配置环境变量&#xff1a; JAVA_HOME的值配置为jdk安装目录如D:\java\jdk1.8.0_201 系统变量的Path中添加"%JAVA_HOME%\bin" 3、验证安装…

海外市场充电桩需求激增:充电基础设施展望

报告显示&#xff0c;在大多数欧盟国家的路网中&#xff0c;充电桩数量存在不足、不支持快速充电且分布不均匀的问题。具体而言&#xff0c;有6个欧洲国家的平均每百公里充电桩数量不足1个&#xff0c;17个国家的平均每百公里充电桩数量少于5个&#xff0c;仅有5个国家的平均每…

【Axure原型分享】标签管理列表

今天和大家分享通过标签管理列表的原型模板&#xff0c;包括增删改查搜索筛选排序分页翻页等效果&#xff0c;这个模板是用中继器制作的&#xff0c;所以使用也很方便&#xff0c;初始数据我们只要在中继器表格里填写即可&#xff0c;具体效果可以观看下方视频或者打开预览地址…

单片机(学习)2024.10.11

目录 按键 按键原理 按键消抖 1.延时消抖 2.抬手检测 通信 1.通信是什么 2.电平信号和差分信号 3.通信的分类 (1)时钟信号划分 同步通信 异步通信 (2)通信方式划分 串行通信 并行通信 (3)通信方向划分 单工 半双工 全双工 4.USART和UART&#xff08;串口通信&a…

selenium工具的几种截屏方法介绍(9)

在使用selenium做自动化的时候&#xff0c;可以对于某些场景截图保存当时的执行情况&#xff0c;方便后续定位问题或者作为一些证据保留现场。 获取元素后将元素截屏 我们获取元素后&#xff0c;使用函数screenshot将元素截屏&#xff0c;参数filename传入完整的png文件名路径…

最近 3 个 yyds 的开源项目!

01 电脑屏幕、麦克风记录工具 ScreenPipe 是一个开源的全天候本地屏幕与麦克风记录工具&#xff0c;为 AI 应用程序提供全方位上下文数据的支持。 该项目旨在成为 Rewind.ai 的替代方案&#xff0c;支持 Windows、Linux 和 macOS 等多平台应用&#xff0c;并且使用 Rust 语言构…

学习Ultralytics(获取yolov8自带的数据集并开始训练)

今天小编带大家学习一下YOLOv8 配置文件&#xff0c;用来定义不同数据集的参数和配置。这些文件包含了关于每个数据集的路径、类别数、类别标签等信息&#xff0c;帮助模型正确地加载和解析数据集&#xff0c;以便进行训练和推理。 具体来说&#xff0c;这些 YAML 文件的作用如…

AIGC时代的程序员生存法则:如何在AI辅助编程工具普及的背景下保持并提升核心竞争力

随着AIGC&#xff08;AI-Generated Content&#xff0c;如ChatGPT、MidJourney、Claude等&#xff09;技术的迅猛发展&#xff0c;特别是大型语言模型的不断涌现&#xff0c;程序员的工作方式正发生深刻变革。AI辅助编程工具的普及给编程行业带来了前所未有的挑战和机遇。一方面…

SwiftUI 6.0(iOS 18)将 Sections 也考虑进自定义容器子视图布局(上)

概述 在 WWDC 24 新推出的 SwiftUI 6.0 中,苹果对于容器内部子视图的布局有了更深入的支持。为了能够未雨绸缪满足实际 App 中所有可能的情况,我们还可以再接再厉,将 Sections 的支持也考虑进去。 SwiftUI 6.0 对容器子视图布局的增强支持可以认为是一个小巧的容器自定义布…

Wordpress—一个神奇的个人博客搭建框架

wordpress简介 在当今数字化的时代&#xff0c;拥有一个属于自己的个人博客&#xff0c;不仅可以记录生活点滴、分享专业知识&#xff0c;还能展示个人风采。而在众多的博客搭建框架中&#xff0c;Wordpress 以其强大的功能和灵活性脱颖而出。今天&#xff0c;就让我们一起深入…

spring boot项目日志怎么加?

使用源码LoggerFactory&#xff08;日志工厂类&#xff09; 使用方法&#xff1a;getlogger()中间传入1个类 加在过滤里所以需要传入的是过滤这个类&#xff08;reqfilter.class) 用这个对象调info方法 logger.error是打印错误信息 logger.debug打印debug 结果会增加时间名称等…

LQB焊接超声波部分原理图和焊接说明(勘误)

1、自制的板子的原理图&#xff0c;有一个错误的地方&#xff0c;导致超声波不能正常使用。 下图是实物的原理图存在错误&#xff0c;不小心&#xff0c;自我批评一下。 图中的C6电容330pF的一端接到了VCC&#xff0c;是错误的。 蓝桥杯的原理图是下图&#xff0c;接到GND 因…

【机器学习(十三)】机器学习回归案例之股票价格预测分析—Sentosa_DSML社区版

文章目录 一、背景描述二、Python代码和Sentosa_DSML社区版算法实现对比(一) 数据读入(二) 特征工程(三) 样本分区(四) 模型训练和评估(五) 模型可视化 三、总结 一、背景描述 股票价格是一种不稳定的时间序列,受多种因素的影响。影响股市的外部因素很多,主要有经济因素、政治因…