C++ 继承和派生 万字长文超详解

本文章内容来源于C++课堂上的听课笔记

继承和派生基础

继承是一种概念,它允许一个新创建的类(称为子类或派生类)获取另一个已经存在的类(称为父类或基类)的属性和行为。这就好比是子类继承了父类的特征。想象一下,如果有一个“动物”类,它有一些通用的特征和行为,然后我们有一个“猫”类,它可以继承“动物”类的这些通用特征,如四条腿、呼吸等。这样,“猫”类就继承了“动物”类

派生是指从已有类创建新类的过程。在这个过程中,新类继承了已有类的属性和方法。派生类可以添加新的属性或方法,也可以覆盖或修改继承的属性和方法。这就允许我们基于现有的代码构建和扩展新的功能。在上面的例子中,“猫”类就是通过派生(或继承)从“动物”类创建出来的

继承分为单继承和多重继承,如果一个派生类只有一个基类,就是单继承,否则是多重继承

派生类是基类的具体化,基类是派生类的抽象

假设已经声明了一个基类A,想要声明派生类B的一般形式为:

举个例子:

#include <iostream>
using namespace std;// 定义基类 Animal
class Animal {
public:void makeSound() {cout << "Some generic sound...\n";}
};// 定义派生类 Dog,它继承了 Animal 类
class Dog : public Animal {
public:void bark() {cout << "Woof! Woof!\n";}
};int main() {// 创建一个 Dog 对象Dog myDog;// 调用继承自 Animal 类的方法myDog.makeSound();// 调用派生类 Dog 自己的方法myDog.bark();return 0;
}

对于继承方式,一般有三种,分别是public,protected,private,如果不写继承方式,默认是private

派生类中的成员包括从基类继承的成员和新增的成员两大部分

构造派生类时,会涉及下面的一些事件:

1.从基类接收成员。派生类把基类全部的成员接收过来

2.调整从基类接收的成员。接收基类成员是程序人员不能选择的,但是程序人员可以对这些成员作某些调整。如:  通过指定继承方式,改变基类成员在派生类中的访问属性;  可以在派生类中声明一个与基类成员同名的成员,覆盖基类的同名成员(与重载是不同的,这里叫做重写override

3.在声明派生类时增加的成员

4.在声明派生类时,一般还应当自己定义派生类的构造函数和析构函数,因为构造函数和析构函数是不能从基类继承的

继承方式

上面说过,在声明派生类的时候要加一个名叫“继承方式”的东西,这里就对其进行相关的解释

公有继承

基类的公用成员和保护成员在派生类中仍然保持其公用成员和保护成员的属性; 而基类的私有成员在派生类中并没有成为派生类的私有成员,它仍然是基类的私有成员,只有基类的成员函数可以引用它,而不能被派生类的成员函数引用,因此就成为派生类中的不可访问的成员

#include <iostream>
using namespace std;// 定义基类 Vehicle
class Vehicle {
public:void startEngine() {cout << "Engine started.\n";}void stopEngine() {cout << "Engine stopped.\n";}
private:int m;
};// 定义派生类 ElectricCar,它公有继承自 Vehicle
class ElectricCar : public Vehicle {
public:void chargeBattery() {cout << "Battery is charging.\n";cout << m << endl;//无法访问}
};int main() {// 创建一个 ElectricCar 对象ElectricCar myElectricCar;// 使用继承自 Vehicle 类的方法myElectricCar.startEngine();// 使用 ElectricCar 类自己的方法myElectricCar.chargeBattery();// 使用继承自 Vehicle 类的方法myElectricCar.stopEngine();return 0;
}

私有继承

私有基类的公用成员和保护成员在派生类中的访问属性相当于派生类中的私有成员,即派生类的成员函数能访问它们,在派生类外不能访问它们。 私有基类的私有成员在派生类中成为不可访问的成员,只有基类的成员函数可以引用它们

对于不需要再往下继承的类的功能可以用私有继承方式把它隐蔽起来,这样,下一层的派生类无法访问它的任何成员

#include <iostream>
#include <string>
using namespace std;// 定义基类 Person
class Person {
private:string name;int age;public:Person(const string& n, int a) : name(n), age(a) {}void displayInfo() {cout << "Name: " << name << ", Age: " << age << endl;}
};// 定义派生类 Employee,私有继承自 Person
class Employee : private Person {
private:string jobTitle;public:// 在 Employee 构造函数中初始化 Person 的成员Employee(const string& n, int a, const string& title): Person(n, a), jobTitle(title) {}void displayEmployeeInfo() {// 在 Employee 类的成员函数中,可以访问继承的 Person 成员displayInfo();cout << "Job Title: " << jobTitle << endl;}
};int main() {// 创建一个 Employee 对象Employee employee("John", 30, "Software Engineer");// 无法直接访问 Person 的私有成员// employee.displayInfo(); // 这行代码会导致编译错误// 但可以通过 Employee 的成员函数间接访问继承的 Person 成员employee.displayEmployeeInfo();return 0;
}

保护继承

保护成员:保护成员和私有成员类似,都不能直接被外界访问,但唯一不同的是保护成员可以被对应类的派生类的成员函数所访问

保护基类的公用成员和保护成员在派生类中都成了保护成员,其私有成员仍为基类私有。也就是把基类原有的公用成员也保护起来,不让类外任意访问

三种继承方式总结如下

派生类的构造函数和析构函数

上面说过子类不能继承父类的构造函数和析构函数,但有一种方法是可以让子类去调用父类的构造函数的,这样,就可以同时初始化父类成员和子类成员了

一般形式如下:

派生类构造函数名(总参数表列): 基类构造函数名(参数表列){派生类中新增数据成员初始化语句}

举一个例子:

Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s) //派生类构造函数
{    age=a;                        //在函数体中只对派生类新增的数据成员初始化addr=ad;
}

其中age,addr是派生类新增成员

也可以在类外实现派生类的构造函数,在类内仅仅声明即可

Student1∷Student1(int n,string nam,char s,int a,string ad):Student(n,nam,s)
{age=a;     addr=ad;}

在类中对派生类构造函数作声明时,不包括基类构造函数名及其参数表列

不仅可以利用初始化表对构造函数的数据成员初始化,而且可以利用初始化表调用派生类的基类构造函数,实现对基类数据成员的初始化。也可以在同一个构造函数的定义中同时实现这两种功能。

Student1(int n, string nam,char s,int a, string ad):Student(n,nam,s),age(a),addr(ad){}

在派生类对象释放时,先执行派生类析构函数~Student1( ),再执行其基类析构函数~Student( )

有子对象的构造函数和组合

什么是子对象?简单来说就是一个类的成员中包含一个类对象,这个类对象就是子对象(subobject),这个子对象也是可以直接在构造函数中一起初始化的,非常方便

注意,如果这个子对象是本派生类的基类,还是正常的继承

如果这个子对象是另外一个已定义的类称为类的组合

类的组合和继承一样,是软件重用的重要方式。组合和继承都是有效地利用已有类的资源。但二者的概念和用法不同。继承是纵向的,组合是横向的

#include <iostream>
#include <string>
using namespace std;// 定义基类 Student
class Student {
private:string name;int age;public:// 基类 Student 的构造函数Student(const string& n, int a) : name(n), age(a) {cout << "Student constructor called." << endl;}void displayInfo() {cout << "Name: " << name << ", Age: " << age << endl;}
};// 定义子对象 Grade
class Grade {
private:char grade;public:// Grade 构造函数Grade(char g) : grade(g) {cout << "Grade constructor called." << endl;}void displayGrade() {cout << "Grade: " << grade << endl;}
};// 定义派生类 Student1,包含一个子对象 Grade
class Student1 : public Student {
private:Grade studentGrade;public:// Student1 构造函数,调用基类 Student 和子对象 Grade 的构造函数Student1(const string& n, int a, char g): Student(n, a), studentGrade(g) {cout << "Student1 constructor called." << endl;}// Student1 类的成员函数,可以访问基类 Student 和子对象 Grade 的成员void displayStudent1Info() {displayInfo();        // 访问基类成员函数studentGrade.displayGrade();  // 访问子对象成员函数}
};int main() {// 创建一个 Student1 对象Student1 student1("Alice", 20, 'A');// 调用 Student1 类的成员函数,间接调用基类 Student 和子对象 Grade 的成员函数student1.displayStudent1Info();return 0;
}

执行派生类构造函数的顺序是: ① 调用基类构造函数,对基类数据成员初始化; ② 调用子对象构造函数,对子对象数据成员初始化; ③ 再执行派生类构造函数本身,对派生类数据成员初始化

基类构造函数和子对象的次序可以是任意的,如果有多个子对象,派生类构造函数的写法依此类推,应列出每一个子对象名及其参数表列。

多级派生时的构造函数

#include <iostream>
#include <string>using namespace std;// 基类 Person
class Person {
private:string name;public:// 基类构造函数Person(const string& n) : name(n) {cout << "Person constructor called." << endl;}void displayName() {cout << "Name: " << name << endl;}
};// 派生类 Student,继承自 Person
class Student : public Person {
private:int studentID;public:// Student 构造函数,调用基类 Person 构造函数Student(const string& n, int id) : Person(n), studentID(id) {cout << "Student constructor called." << endl;}void displayStudentInfo() {displayName(); // 访问基类成员函数cout << "Student ID: " << studentID << endl;}
};// 派生类 GraduateStudent,继承自 Student
class GraduateStudent : public Student {
private:string researchTopic;public:// GraduateStudent 构造函数,调用直接基类 Student 构造函数GraduateStudent(const string& n, int id, const string& topic): Student(n, id), researchTopic(topic) {cout << "GraduateStudent constructor called." << endl;}void displayGraduateStudentInfo() {displayStudentInfo(); // 访问直接基类 Student 的成员函数cout << "Research Topic: " << researchTopic << endl;}
};int main() {// 创建一个 GraduateStudent 对象GraduateStudent gradStudent("John Doe", 12345, "Machine Learning");// 调用 GraduateStudent 类的成员函数,间接调用基类 Person 和直接基类 Student 的成员函数gradStudent.displayGraduateStudentInfo();return 0;
}

main 函数中创建 GraduateStudent 对象时,首先调用 Person 类的构造函数,然后调用 Student 类的构造函数,最后调用 GraduateStudent 类的构造函数

派生类构造函数和析构函数注意点

1.当不需要对派生类新增的成员进行任何初始化操作时,派生类构造函数的函数体可以为空,即构造函数是空函数

2.如果在基类中没有定义构造函数,或定义了没有参数的构造函数,那么在定义派生类构造函数时可不写基类构造函数

3.如果在基类和子对象类型的声明中都没有定义带参数的构造函数,而且也不需对派生类自己的数据成员初始化,则可以不必显式地定义派生类构造函数

4.如果在基类或子对象类型的声明中定义了带参数的构造函数,那么就必须显式地定义派生类构造函数

5.如果在基类中既定义无参的构造函数,又定义了有参的构造函数(构造函数重载),则在定义派生类构造函数时,既可以包含基类构造函数及其参数,也可以不包含基类构造函数

6.在执行派生类的析构函数时,系统会自动调用基类的析构函数和子对象的析构函数,对基类和子对象进行清理

7.调用的顺序与构造函数正好相反: 先执行派生类自己的析构函数,对派生类新增加的成员进行清理,然后调用子对象的析构函数,对子对象进行清理,最后调用基类的析构函数,对基类进行清理

多重继承

注意派生类中的构造函数怎么写的

#include <iostream>
#include <string>using namespace std;// 基类 Shape(形状)
class Shape {
protected:int width;int height;public:// Shape 类构造函数Shape(int w, int h) : width(w), height(h) {cout << "Shape constructor called." << endl;}void displayShapeInfo() {cout << "Shape - Width: " << width << ", Height: " << height << endl;}
};// 基类 Color(颜色)
class Color {
protected:string color;public:// Color 类构造函数Color(const string& c) : color(c) {cout << "Color constructor called." << endl;}void displayColor() {cout << "Color: " << color << endl;}
};// 派生类 ColoredShape(有颜色的形状),继承自 Shape 和 Color
class ColoredShape : public Shape, public Color {
public:// ColoredShape 类构造函数,显式调用基类构造函数ColoredShape(int w, int h, const string& c,int id) : Shape(w, h), Color(c) id(id){cout << "ColoredShape constructor called." << endl;}void displayColoredShapeInfo() {displayShapeInfo(); // 访问 Shape 类的成员函数displayColor();     // 访问 Color 类的成员函数}int id;
};int main() {// 创建一个 ColoredShape 对象ColoredShape coloredShape(10, 5, "Red");// 调用 ColoredShape 类的成员函数,间接调用基类 Shape 和 Color 的成员函数coloredShape.displayColoredShapeInfo();return 0;
}

虚基类

如果一个派生类有多个直接基类,而这些直接基类又有一个共同的基类,则在最终的派生类中会保留该间接共同基类数据成员的多份同名成员

C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员

虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。 声明虚基类的一般形式为

class 派生类名: virtual 继承方式 基类名

#include <iostream>// 基类:动物
class Animal {
public:Animal(const std::string& name) : name(name) {}void eat() const {std::cout << name << " is eating." << std::endl;}void sleep() const {std::cout << name << " is sleeping." << std::endl;}private:std::string name;
};// 虚基类:飞行动物
class FlyingAnimal : virtual public Animal {
public:FlyingAnimal(const std::string& name) : Animal(name) {}void fly() const {std::cout << Animal::getName() << " is flying." << std::endl;}
};// 派生类:哺乳动物
class Mammal : virtual public Animal {
public:Mammal(const std::string& name) : Animal(name) {}void giveBirth() const {std::cout << Animal::getName() << " is giving birth." << std::endl;}
};// 派生类:既是哺乳动物又可以飞的动物
class Bat : public FlyingAnimal, public Mammal {
public:Bat(const std::string& name) : Animal(name), FlyingAnimal(name), Mammal(name) {}
};int main() {Bat bat("Batman");// 通过虚基类,避免了二义性问题bat.eat();bat.sleep();bat.fly();bat.giveBirth();return 0;
}

基类和派生类的转换

在C++中,基类和派生类之间的转换主要涉及两种类型:向上转型(Upcasting)和向下转型(Downcasting)。这些转型可能涉及到指针和引用。

向上转型(Upcasting):

向上转型是将派生类的指针或引用转换为基类的指针或引用。这是一个安全的操作,因为派生类对象包含基类的部分。这样的转换通常是自动进行的,不需要显式的转换操作

#include <iostream>class Base {
public:virtual void display() const {std::cout << "Base class display" << std::endl;}
};class Derived : public Base {
public:void display() const override {std::cout << "Derived class display" << std::endl;}void additionalFunction() const {std::cout << "Additional function in the derived class" << std::endl;}
};int main() {Derived derivedObj;Base* basePtr = &derivedObj; // 向上转型// 调用的是 Derived 类的 display 方法basePtr->display();// 无法直接调用 Derived 类的 additionalFunction// basePtr->additionalFunction(); // 这行代码会产生编译错误return 0;
}

向下转型(Downcasting):

向下转型是将基类的指针或引用转换为派生类的指针或引用。这是一个潜在的危险操作,因为基类可能无法包含派生类的所有信息。因此,向下转型时通常需要进行显式的类型转换,并且应该在确保安全性的情况下进行

class Base {//...
};class Derived : public Base {//...
};int main() {Base* basePtr = new Derived();// 向下转型,需要显式的类型转换Derived* derivedPtr = dynamic_cast<Derived*>(basePtr);if (derivedPtr) {// 转换成功,可以使用 derivedPtr 操作 Derived 类的成员} else {// 转换失败,basePtr 实际上不指向 Derived 类的对象}delete basePtr;return 0;
}

在上述代码中,使用了 dynamic_cast 运算符进行向下转型。这个运算符在运行时检查转型的有效性,如果转型合法,则返回指向派生类对象的指针;否则,返回空指针。

总的来说,向上转型是安全的,而向下转型需要谨慎并使用适当的手段进行检查,以避免潜在的运行时错误。在进行向下转型时,通常使用 dynamic_cast 运算符进行类型检查,或者在一些情况下,可以使用 static_cast 运算符,但要确保转型是安全的。

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

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

相关文章

【LeetCode刷题-字符串】--6.N字形变换

6.N字形变换 方法&#xff1a;使用二维矩阵模拟 根据题意&#xff0c;当在矩阵中填写字符时&#xff0c;会向下填写r个字符&#xff0c;然后向右继续填写r - 2个字符&#xff0c;最后回到第一行&#xff0c;因此Z字形变换的周期是t r r - 2 2r - 2&#xff0c;是|/,每个周期…

选择java商城开发商需要注意哪些方面?

Java商城开发是一项庞大而复杂的任务&#xff0c;选择一家合适的开发商至关重要。那么&#xff0c;我们在选择Java商城开发商时&#xff0c;需要注意哪些方面呢&#xff1f; 1、专业经验 选择具有丰富经验的开发商是至关重要的。开发商应该拥有多年的Java开发经验&#xff0c;…

Linux常见命令手册

目录 文件命令 文件和目录命令 文件的权限命令 文件搜索命令 进程命令 查看进程命令 关闭进程命令 用户和群组命令 网络命令 firewall-cmd 网络应用命令 高级网络命令 网络测试命令 网络安全命令 网络配置命令 软件管理命令 系统信息命令 vi编辑器 关机命令…

【前端学java】java中的日期操作(12)

往期回顾&#xff1a; 【前端学java】JAVA开发的依赖安装与环境配置 &#xff08;0&#xff09;【前端学 java】java的基础语法&#xff08;1&#xff09;【前端学java】JAVA中的packge与import&#xff08;2&#xff09;【前端学java】面向对象编程基础-类的使用 &#xff08…

JS进阶——高阶技巧

1、深浅拷贝 浅拷贝和深拷贝只针对引用类型 1.1 浅拷贝 浅拷贝&#xff1a;拷贝的是地址 如果是简单数据类型拷贝值&#xff0c;应用数据类型拷贝的是地址&#xff08;简单理解&#xff1a;如果是单层对象&#xff0c;没问题&#xff0c;如果有多层就有问题&#xff09; 常…

#gStore-weekly | gBuilder功能详解之结构化数据抽取

上一个weekly中已经详细讲解了schema的设计&#xff0c;在schema设计好了之后&#xff0c;gBuilder支持将结构化和非结构化数据转化为RDF图数据。其中结构化数据支持数据的无损转化。 1. 技术介绍 gBuilder的结构化数据抽取采用D2RQ技术实现。 DR2Q是一个能够将关系数据库中…

API接口测试工具为什么尤其重要

在现代软件开发中&#xff0c;API接口测试工具扮演着关键的角色&#xff0c;连接不同的软件组件&#xff0c;实现数据传递和功能调用。为确保API的可靠性、安全性和性能&#xff0c;此工具成为不可或缺的一部分。本文将介绍API接口测试工具的重要性! 1. 自动化测试的效率 API接…

mongodb——概念介绍(文档,集合,固定集合,元数据,常用数据类型)

mongodb 层级结构 实例&#xff1a;系统上运行的进程及节点集&#xff0c;一个实例可以有多个库&#xff0c;默认端口 27017。 库&#xff1a;多个集合组成数据库&#xff0c;每个数据库都是独立的&#xff0c;有自己的用户、权限信息&#xff0c;独立的存储文件集 合。 集合&…

【【VDMA彩条显示实验之四 含C语言代码】】

VDMA彩条显示实验之四 含C语言代码 VTC 手册简介 所有的视频都需要有时序 有时序的地方就需要有 时序控制器 VTC的 主要作用是 产生 视频时序 相对于上一节 在这里 我们会理解的更多 观察 这个 HB 信号 其实这个和上一节的图片差不多 在 行同步信号 前面就是前沿 在 行同步…

Java爬虫框架下代理使用中的TCP连接池问题及解决方案

引言 当使用Java爬虫框架进行代理爬取时&#xff0c;可能会遇到TCP连接池问题&#xff0c;导致"java.net.BindException: Cannot assign requested address"等错误。本文将介绍如何以爬取小红书为案例&#xff0c;解决Java爬虫框架中代理使用中的TCP连接池问题&…

关于漏洞:检测到目标SSL证书已过期【原理扫描】

这个漏洞是监听443端口的&#xff0c;如果我们是正式的网站且使用了https那么更新证书即可。 但是我的服务器上面几乎是个空服务器&#xff0c;谁会用443呢&#xff1f; 找不到谁用了那我就部署一个nginx&#xff0c;用nginx代理443&#xff0c;找个证书配一下呗。 然而当我…

Netty源码学习4——服务端是处理新连接的netty的reactor模式

零丶引入 在前面的源码学习中&#xff0c;梳理了服务端的启动&#xff0c;以及NioEventLoop事件循环的工作流程&#xff0c;并了解了Netty处理网络io重要的Channel &#xff0c;ChannelHandler&#xff0c;ChannelPipeline。 这一篇将学习服务端是如何构建新的连接。 一丶网络包…