c++:面向对象三大特性--继承

ops/2024/11/27 7:11:52/

在这里插入图片描述

面向对象三大特性--继承

  • 一、继承的概念及定义
    • (一)概念
    • (二)继承格式
      • 1、继承方式
      • 2、格式写法
      • 3、派生类继承后访问方式的变化
    • (三)普通类继承
    • (四)类模板继承
  • 二、基类和派生类的转换
    • (一)基类转换派生类
    • (二)派生类转换基类
  • 三、几个重要细节
    • (一)继承与作用域
      • 1、作用域
      • 2、隐藏
    • (二)继承与友元
    • (三)继承与静态成员
  • 四、继承中派生类的构造函数
  • 五、多继承与菱形继承
    • (一)多继承
      • 多继承的指针偏移问题
    • (二)菱形继承
    • (三)虚继承
  • 六、继承和组合
    • 结束语:

一、继承的概念及定义

(一)概念

继承是⾯向对象程序设计使代码可以复用的最重要的⼿段,它允许我们在保持原有类特性的基础上进⾏扩展,增加⽅法(成员函数)和属性(成员变量),这样产⽣新的类,称派⽣类。

(二)继承格式

1、继承方式

我们前面对类的成员有三种限制方式,这里也就对应了三种继承方式
在这里插入图片描述

2、格式写法

在这里插入图片描述

3、派生类继承后访问方式的变化

1、通过表格可以发现,如果是private成员,那么无论哪种继承方式都不可以访问到这个权限
2、此外,structclass这两个关键字在继承时也有差距,struct默认继承方式为公有,而class默认继承方式为私有。

我们如果将权限的大小定义为 public > protected > private, 那么其余访问方式变化就是将大于该继承方式的权限降到继承方式的权限即可

在这里插入图片描述

(三)普通类继承

这里用到的是继承最基本的语法,采用public继承,那么除了父类的private变量不可访问以外,成员的权限保持不变。

class Person
{
public:void Print(){cout << _name << endl;cout << _age << endl;}
protected:string _name = "张三"; // 姓名
private:int _age = 18; // 年龄
};class Student : public Person
{
public:void func(){Print();}
protected:int _stunum; // 学号
};

(四)类模板继承

在之前我们实现stack时,采用的是新建了一个容器类型,在这里我们亦可以采用继承的方式来实现。
需要注意的是,派生类在继承时,如果需要访问父类的成员函数,需要指定类域,模板的成员函数采用的是按需实例化

namespace wgm
{template<class T>class stack : public std::vector<T>{public:void push(const T& x){// 基类是类模板时,需要指定⼀下类域,// 否则编译报错:error C3861: “push_back”: 找不到标识符// 因为stack<int>实例化时,也实例化vector<int>了// 但是模版是按需实例化,push_back等成员函数未实例化,所以找不到vector<T>::push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
}

二、基类和派生类的转换

(一)基类转换派生类

1、基类对象不能赋值给派⽣类对象。
2、基类的指针或者引⽤可以通过强制类型转换赋值给派⽣类的指针或者引⽤。但是必须是基类的指针
是指向派⽣类对象时才是安全的。这⾥基类如果是多态类型,可以使⽤RTTI(Run-Time Type Information)dynamic_cast 来进⾏识别后进⾏安全转换。

(二)派生类转换基类

1、public继承的派⽣类对象 可以赋值给基类的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切
。寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分。

值得注意的是,之前在隐式类型转换时会生成临时变量,因此在应用时需要加上const,而在切片时不会生成中间的临时变量

class Person
{
protected:string _name; // 姓名string _sex; // 性别
public:int _age = 18; // 年龄
};class Student : public Person
{
public:int _No; // 学号
};int main()
{string s1 = "11111";const string& s2 = "11111";Student sobj;// 赋值兼容转换,特殊处理// 1.派生类对象可以赋值给基类的指针/引用Person* pp = &sobj;Person& rp = sobj;rp._age++;return 0;
}

在这里插入图片描述

接下来通过下面的例子发现,继承后的基类私有变量虽然访问不到,但是我们可以发现它在派生类的对象中依旧占据相应的空间,而经过赋值兼容转换变量的大小为基类的大小

在这里插入图片描述

在这里插入图片描述

接下来更加深层的来了解赋值兼容,发现基类的指针或引用在调用重名函数的时候,调用的是父类的函数,而派生类调用时因为隐藏的特点,派生类对象调用的是派生类的函数

class A
{
public:void func(){cout << "A::func()" << endl;}protected:int _a;int _b;
private:int _c;
};class B : public A
{public:void func(){cout << "B::func()" << endl;}public:int _d;
};int main() {B obj_b;A* ptr_a = &obj_b;A& ref_a = obj_b;obj_b.func();ptr_a->func();ref_a.func();return 0;
}

2、子类的变量可以复制给父类。

	Person pobj = sobj;

在这里插入图片描述

三、几个重要细节

(一)继承与作用域

1、作用域

在继承体系中基类和派⽣类都有独⽴的作⽤域。

2、隐藏

派⽣类和基类中有同名成员,派⽣类成员将屏蔽基类对同名成员的直接访问,这种情况叫隐藏(在派⽣类成员函数中,可以使⽤ 基类::基类成员 显⽰访问

需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏。

(二)继承与友元

在继承时,友元关系是不接受继承的。所以如果友元函数需要访问派生类的成员,需要重新声明友元。

(三)继承与静态成员

在继承后,静态成员变量始终只有基类在定义的这一份。通过下面的代码可以发现,我们可以用类域加静态变量的方式来访问静态变量,但是打印的地址是同一份。

class Person
{
public:string _name;static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:int _stuNum;
};
int main()
{Person p;Student s;cout << &p._count << endl;cout << &s._count << endl;cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}

在这里插入图片描述

四、继承中派生类的构造函数

  1. 派⽣类的构造函数必须调⽤基类的构造函数初始化基类的那⼀部分成员。基类没有默认的构造函数必须在派⽣类构造函数的初始化列表阶段显⽰调⽤
  2. 派⽣类的拷⻉构造函数必须调⽤基类的拷⻉构造完成基类的拷⻉初始化。
  3. 派⽣类的operator=必须要调⽤基类的operator=。需要注意的是派⽣类的operator=隐藏了基类的operator=,所以指定基类作⽤域显⽰调⽤基类的operator=
  4. 派⽣类的析构函数会在被调⽤完成后⾃动调⽤基类的析构函数清理基类成员。才能保证先清理派⽣类成员再清理基类成员。因为多态中⼀些场景析构函数需要构成重写。,那么编译器会对析构函数名进⾏特殊处理,处理成destructor(),所以基类析构函数不加virtual的情况下,派⽣类析构函数和基类析构函数构成隐藏关系
class Person
{
public:Person(const char* name): _name(name){cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}// destructor()~Person(){cout << "~Person()" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public:Student(int num, const char* address, const char* name):_num(num), _address(address), Person(name){cout << "Student()" << endl;}Student(const Student& s): Person(s), _num(s._num), _address(s._address){cout << "Student(const Student& s)" << endl;}Student& operator = (const Student& s){cout << "Student& operator= (const Student& s)" << endl;if (this != &s){_num = s._num;_address = s._address;Person::operator=(s);}return *this;}// destructor()~Student(){// 不需要写,子类析构函数结束后,会自动调用父类析构//Person::~Person();cout << "~Student()" << endl;}protected:int _num; //学号string _address;
};

五、多继承与菱形继承

(一)多继承

单继承:⼀个派⽣类只有⼀个直接基类时称为单继承
多继承:⼀个派⽣类有两个或以上直接基类时称为多继承

多继承的指针偏移问题

多继承对象在内存中的模型是,先继承的基类在前⾯,后⾯继承的基类在后⾯,派⽣类成员在放到最后⾯
在这里插入图片描述
通过上面的例子,我们可以清晰的认识到基类在派生类的储存情况。

(二)菱形继承

菱形继承:菱形继承是多继承的⼀种特殊情况,有数据冗余和⼆义性的问题

在这里插入图片描述

class Person
{
public:string _name; // 姓名
};class Student : public Person
{
protected:int _num; //学号
};class Teacher : public Person
{
protected:int _id; // 职工编号
};//给类加上 virtual 关键字,解决菱形继承造成的二义性和数据冗余。//class Student : virtual public Person
//{
//protected:
//	int _num; //学号
//};
//
//class Teacher : virtual public Person
//{
//protected:
//	int _id; // 职工编号
//};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};int main() {Assistant obj;obj.Student::_name = "张三";obj.Teacher::_name = "李四";return 0;
}

在这里插入图片描述
通过调试窗口,可以发现我们在继承时同时继承了来自Person和来自Teacher_name我们在写代码时无法处理这个二义性,同时也形成了数据冗余。

(三)虚继承

为了解决这个现象,我们只需要在继承同一个基类成员的派生类加上一个virtual关键字,底层会自行加工,使得我们后面访问的_name只是一份数据。

class Person
{
public:string _name; // 姓名
};//给类加上 virtual 关键字,解决菱形继承造成的二义性和数据冗余。
class Student : virtual public Person
{
protected:int _num; //学号
};class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};int main() {Assistant obj;obj.Student::_name = "张三";obj.Teacher::_name = "李四";return 0;
}

下列窗口显示出来的_name实则是同一份数据,最开始指定类域Student::初始化_name为张三
在这里插入图片描述
我们通过Teacher::修改数据为李四,那么数据被修改为李四。
在这里插入图片描述

切记,尽量不用使用菱形继承,因为virtual关键字在解决问题的同时造成了效率的降低,代价有点大。

六、继承和组合

继承组合
定义public继承是⼀种is-a的关系。也就是说每个派⽣类对象都是⼀个基类对象。组合是⼀种has-a的关系。假设B组合了A,每个B对象中都有⼀个A对象。
复用方式白箱复用:在继承⽅式中,基类的内部细节对派⽣类可⻅⿊箱复⽤:通过调用对象的接口实现,对象的内部细节是不可⻅的
耦合度继承⼀定程度破坏了基类的封装,基类的改变,对派⽣类有很⼤的影响。派⽣类和基类间的依赖关系很强,耦合度高组合类之间没有很强的依赖关系,耦合度低

我们可以发现,组合的好处要大于继承,在两种都可以的情况下,优先使⽤组合,⽽不是继承。实际尽量多去⽤组合,组合的耦合度低,代码维护性好。

结束语:

感谢一直以来支持的朋友,支持一路走来披荆斩棘的道友,或许不识,一路同行!


http://www.ppmy.cn/ops/137019.html

相关文章

Java面试题、八股文学习之JVM篇

1、知识点汇总 JVM是Java运行基础,面试时一定会遇到JVM的有关问题,内容相对集中,但对只是深度要求较高。 重点包括内存模型、类加载机制和垃圾回收&#xff08;GC&#xff09;。性能调优侧重应用实践&#xff0c;编译器优化与执行模式侧重理论基础。需掌握内存模型的各部分功能…

【强化学习的数学原理】第02课-贝尔曼公式-笔记

学习资料&#xff1a;bilibili 西湖大学赵世钰老师的【强化学习的数学原理】课程。链接&#xff1a;强化学习的数学原理 西湖大学 赵世钰 文章目录 一、为什么return重要&#xff1f;如何计算return&#xff1f;二、state value的定义三、Bellman公式的详细推导四、公式向量形式…

【纪念365天】我的创作纪念日

过去的一年 没有注意加入csdn已经有一年了。 这几天翻看小猴儿的通知才发现时间来到了一年的纪念日。稍稍思索想要将这一段时间的学习到的知识以及偶然遇到的机遇做一下总结。 上一次写纪念日是来到csdn128天的时候&#xff0c; 200天前我的学习状态是非常疯狂的。 只记得我当时…

Python人工智能项目报告

一、实践概述 1、实践计划和目的 在现代社会&#xff0c;计算机技术已成为支撑社会发展的核心力量&#xff0c;渗透到生活的各个领域&#xff0c;应关注人类福祉&#xff0c;确保自己的工作成果能够造福社会&#xff0c;同时维护安全、健康的自然环境&#xff0c;设计出具有包…

SpringBoot(三十九)SpringBoot集成RabbitMQ实现流量削峰添谷

前边我们有具体的学习过RabbitMQ的安装和基本使用的情况。 但是呢&#xff0c;没有演示具体应用到项目中的实例。 这里使用RabbitMQ来实现流量的削峰添谷。 一&#xff1a;添加pom依赖 <!--rabbitmq-需要的 AMQP 依赖--> <dependency><groupId>org.springfr…

卷积神经网络学习记录

目录 神经网络基础定义&#xff1a; 基本组成部分 工作流程 卷积层&#xff08;卷积定义&#xff09;【CONV】&#xff1a; 卷积层&#xff08;Convolutional Layer&#xff09; 特征提取&#xff1a;卷积层的主要作用是通过卷积核&#xff08;或滤波器&#xff09;运算提…

esp32触发相机

esp32触发相机&#xff0c;测试成功上升沿触发 串口发送命令 up 20000 1 20000 触发 #include <Arduino.h>const int outputPin 12; // 输出引脚 String inputCommand ""; // 串口输入缓冲区// 解析命令参数&#xff0c;例如 "up 10 5" 解析为…

基于nxp LS1046+fpga的嵌入式系统中虚拟化设备的设计与实现

3 虚拟化设备仿真平台设计 本文需要设计和实现的虚拟化设备需要搭建一个仿真平台&#xff0c;一个完善的仿真平台才 是一种虚拟化设备能搭建起来的关键&#xff0c;仿真平台的搭建需要一定条件的硬件环境&#xff0c;更为 主要的是软件环境&#xff0c;下文就要详细介绍此虚…