C++ 继承,多态

看前须知:

本篇博客是作者听课时的笔记,不喜勿喷,若有疑问可以评论区一起讨论。

继承

定义:

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

继承格式

继承基类成员访问格式

1.基类(父类)的private成员在派生类(子类)中无论是以什么样的继承方式都是不可见的,但可以通过基类的成员函数进行访问。

2.protected:派生类(子类)可以继承且访问基类(父类)的成员函数,但是其他类中不能被访问。

3.基类的私有成员在派生类中访问是不可见的,其他成员在派生类中的访问方式==MIn(成员在基类的访问限定符,继承方式),public>protected>private

4.在实际运用中一般使用都是public继承

应用(利用继承实现栈)

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
#include<vector>
#include<list>
using namespace std;
//用继承模拟实现栈
//#define CONTAINER std::vector
#define CONTAINER std::list
namespace lph {template<class T>class stack :public CONTAINER<T> {public:void push(const T& x) {//基类是类模板,需要指定类域否者编译报错CONTAINER<T>::push_back(x);}void pop() {CONTAINER<T>::pop_back();}T& top() {return CONTAINER<T>::back();}size_t size() {return CONTAINER<T>::size();}bool empyt() {return CONTAINER<T>::empty();}};
}
int main() {lph::stack<int> s1;//进行实例化时调用模板的实例化,但并不会实例化基类(父类)的成员函数,所有在调用基类(父类)的成员函数时需要指定类域。s1.push(1);s1.push(2);s1.push(3);s1.push(4);while (!s1.empyt()) {cout<<s1.top()<<" ";s1.pop();}return 0;
}

基类和派⽣类间的转换

public继承的派⽣类对象,可以赋值给基类(父类)的指针/基类的引⽤。这⾥有个形象的说法叫切⽚或者切 割。

寓意把派⽣类中基类那部分切出来,基类指针或引⽤指向的是派⽣类中切出来的基类那部分(父类不能赋值给子类)

继承中的作⽤域

隐藏规则

1.继承体系中基类(父类)和派生类(子类)都有独立的作用域

2.派生类(子类)和基类(父类)中有同名成员,派生类将屏蔽基类对基类对同名成员的直接访问(在派生类成员函数中,可以使用基类::基类成员 显示访问)

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
class person {
protected:int num = 999;//同名成员变量
};
class student : public person {
public:void print() {cout << num << endl;//访问到的是派生类的成元变量100cout<<person::num<<endl;//访问基类成员变量999}
protected:int num = 100;//同名成员变量
};
int main() {student s1;s1.print();return 0;
}

 3.对于成员函数的隐藏规则,只要派生类中有与基类中同名函数就会进行隐藏。

using namespace std;
class person {
public:void fun() {cout << "fun()" << endl;}
};
class student : public person {
public:void fun(int i) {cout << "fun(int i)" << endl;}
};int main() {student s1;s1.fun(1);//带参数的直接进行访问派生类s1.person::fun();//不带参数需要指定类域return 0;
}
//只要子类有函数名与基类函数名像似就把基类进行隐藏

 4.在实际继承体系中,最好不要定义同名成员或则成员函数。

派⽣类的默认成员函数

6个默认成员函数,默认的意思就是指我们不写,编译器会变我们⾃动⽣成⼀个,那么在派⽣类中,这 ⼏个成员函数是如何⽣成的呢?

类和对象中默认成员函数规则。

1.对于内置类型是否初始化不确定。

2.对与自定义类型会调用他的默认构造。

3.派生类中的默认成员函数调用其父类的默认成员函数

对应规则

1.默认构造

派⽣类的默认构造函数必须调⽤基类的默认构造函数初始化基类的那⼀部分成员。如果基类没有默认的构造 函数,则必须在派⽣类构造函数的初始化列表阶段显⽰调⽤。

由上图可知在父类我们并没有显示实现默认构造,派生类无法调用父类默认构造,因此必须在派生类初始化列表显示写默认构造,但对于父类成员变量要调用父类的初始化列表进行初始化(person(name)相当于一个匿名对象)

2.拷贝构造

派⽣类(子类)的拷⻉构造函数必须调⽤基类(父类)的拷⻉构造完成基类的拷⻉初始化。

对于该函数,成员变量有一个内置类行变量和一个person(父类)变量,对于自定义变量(person)调用person的拷贝构造即可,但对于内置变量直接进行值拷贝,所以该子类并不需要直接写拷贝构造。

如果有资源开辟那么我们就需要进行自己写子类的拷贝钩爪

3.赋值重载

派⽣类的operator=必须要调⽤基类的operator=完成基类的复制。需要注意的是派⽣类的 operator=隐藏了基类的operator=,所以显⽰调⽤基类的operator=,需要指定基类作⽤域

与拷贝构造分析类似

4.析构

对于派生类调用基类的析构,如果有要释放的资源则需要自己写就行。

自己写的话显示调用父类析构(也可以不写因为子类进行析构后会自动调用父类析构,这样保证先清理子类资源在清理父类资源 ),需要注意的是子类析构和父类析构构成隐藏关系。

5.子类的初始化先调用父类在子

6.析构 先子后父

构造析构图解

实现不能被继承的类

方法一:

c++98规定:只要把构造函数私有化就不能被继承

原因:因为实例化派生类要调用基类的构造函数,如果没有就不能继承

方法二:

在父类后边加一个final说明这个类是最后一个类

class parent final{};

继承与友元

基类的友元关系不能被继承如果想被继承需要在两个类中都加友元

继承与静态成员

普通成员变量定义在基类和派生类则两则是两个不同的变量,静态成员变量如果继承下来则两个类是公用该同一个变量。

多继承及其菱形继承问题

继承类型

单继承:只继承一个父类

多继承:一个子类继承两个父类

菱形继承:多继承继承的两个父类又继承了同一个父类

菱形继承出现的问题:

当实例化菱形继承时存在二议性,和冗余性,冗余性体现在所继承继承继承的两个类代码重复,二义性表现在实例化时不清楚用的时那个类进行实例化的。

虚继承

虚继承可以解决以上问题,在多继承中继承的两个父类加上关键字virtual即可解决。

总结:

现实中尽量不要使用或则设计菱形继承,因为很复杂。

多继承中指针偏移问题?

class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;Derive* p3 = &d;return 0;
}

多态

概念:简单来说就是多种形态。

分类:编译时多态(静态多态)和运行时多态(动态多态)(重点)

编译时多态

类似于前面学的函数重载和函数模板

以下面例子来讲:

template<class T>
void fun(int,int);
void fun(double,double)

利用函数模板进行传参时,进行不同类型实例化时,函数在编译时就会生成不同的函数。

运行时多态(重点)

一句话概括:不同的对象去完成不同的行为 这样就表现出多态。

多态的构成条件

多态是一个继承关系下的类对象,去调用同一个函数,产生了不同的行为。

虚函数

虚函数:加在类中成员函数前面一个virtual构成虚函数

实现多态的两个必须重要条件

先看以下代码

class person {
public:virtual void Buyticket() {cout << "全价票" << endl;}
};
class student :public person {virtual void Buyticket() {cout << "半价票" << endl;}
};
void fun(person& p) {//必须为父类引用p.Buyticket();//如果不满足多态指向谁就调用谁
}
int main() {person p1;student s1;fun(p1);fun(s1);return 0;
}

1.必须是基类指针或则引用调用虚函数。

2.被调用的的函数必须是虚函数

说明:

要实现多态效果,第一必须是基类的指针或者引用如下:

void fun(person& p) {//必须为父类引用p.Buyticket();
}
int main() {person p1;student s1;fun(p1);fun(s1);return 0;
}

原因:学过继承应该知道只有时基类的指针或则引用才能指向自己或则时子类(对其进行切割)。

第二点就i是派生类必须对基类的虚函数完成重写覆盖,只有重写/覆盖基类和派生类才能有不同的函数,多态的不同形态才能体现出来。

虚函数重写规则:派生类虚函数与基类虚函数返回值相同,函数参数相同,函数名相同)。如下代码即是重写/覆盖。

class person {
public:virtual void Buyticket() {cout << "全价票" << endl;}
};
class student :public person {virtual void Buyticket() {cout << "半价票" << endl;}
};

注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承 后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样 使⽤,不过在考试选择题中,经常会故意买这个坑,让你 判断是否构成多态(笔试常考)。

加深理解:

多个类构成的多态(只是为了能加深理解)

class animal {
public:virtual void talk() {cout << "0" << endl;};
};
class dog:public animal {
public:virtual void talk() {cout << "汪汪" << endl;}
};
class cat :public animal {
public:virtual void talk() {cout << "喵喵" << endl;}
};
void fun1(animal& a) {a.talk();
}
int main() {animal a;dog d;cat c;fun1(a);fun1(d);fun1(c);return 0;
}

关于多态的一道面试题目

class A
{
public:virtual void func(int val = 1) {cout << "A->" << val << endl; }virtual void test() {func();  //把对象b的指针传给A*//原因是对于继承只是一种形象化的说法,并不会真的把代码拷贝到b对象中,只是存在一种搜索规则,在b类中搜索不到后就去a类中进行搜索。所以这个地方还是A*}
};class B : public A
{
public:void func(int val = 0) {//虽然没加virtual 但与基类构成重写cout << "B->" << val << endl;}
};
int main(int argc, char* argv[])
{B* p = new B;//实例化一个B对象 pp->test();//相当于对象A指针 preturn 0;
}
//A:A->0 B:B->1 C:A->1 D:B->0 E:编译出错 F:以上都不正确---B
//不是D的原因是因为真正的重写是由基类的声明部分加派生类构造部分(重写的本质就是重写虚函数) 所以真正的写法因该是
virtual void func(int val = 1){cout << "B->" << val << endl};//B->1

虚函数重写的⼀些其他问题

协变(了解)

定义:基类虚函数返回基类对象的指针或者引用,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变

class person {
public:virtual person* Buyticket() {//返回值为personcout << "全价票" << endl;return nullptr;}
};
class student :public person {virtual student* Buyticket() {//返回值为studentcout << "半价票" << endl;return nullptr;}
};
void fun(person& p) {//必须为父类引用p.Buyticket();//如果不满足多态指向谁就调用谁
}
int main() {person p1;student s1;fun(p1);fun(s1);return 0;
}
//运行结果与多态一致 像这种情况称之为协变只需了解即可
析构函数的重写(面试考题:)

问题:为什么基类中的析构 函数建议设计为虚函数

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键之,都与基类析构函数构成重写,实际上编译器对析构函数的名称构成了重写,编译后的名称统一处理成destructor,因此符合重写条件,所以就构成重写。

c++设计初衷:

class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};
class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

对于以上程序,基类指针既能指向自己也能指向派生类,如果指向派生类(不构成重写的情况下),并且派生类中由资源需要释放,那么就不能去释放派生类中的资源,照成内存泄漏,只有两则构成多泰才能更好的解决这里的问题。

override和final关键字

override,可以帮助⽤⼾检测是否重写。

先看一段程序

class Car {
public:virtual void Dirve(){}
};
class Benz :public Car {
public:virtual void Drive(){ cout << "Benz-舒适" << endl; }
};
int main()
{return 0;
}

 细心的你可能不能发现以上函数并不构成重写(仔细观察Dirve Drive)函数名不同,不构成重写,这时候方便快速检查可以在派生类虚函数后边加一个override既能快速发现错误

class Car {
public:virtual void Dirve(){}
};
class Benz :public Car {
public:virtual void Drive()override{ cout << "Benz-舒适" << endl; }
};
int main()
{return 0;
}

 如果我们不想让 派⽣类重写这个虚函数,那么可以⽤final去修饰

virtual void Drive() final {}//基类的虚函数加final

重载/重写/隐藏三者对比(常考)

虚函数和抽象类

定义:在虚函数的后⾯写上=0,则这个函数为纯虚函数,只要在基类声明即可,无意义。

包含纯虚函数的类叫做抽象类,抽象类不能实例 化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。

纯虚函数某种程度上强制了 派⽣类重写虚函数,因为不重写实例化不出对象。

virtual void Drive() = 0;//基类中的虚函数
//基类成为抽象类 强制派生类必须重写

多态的原理

话题引入

虚函数表指针

class Base
{
public:virtual void Func1(){cout<<"Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};
int main()
Base b;
cout << sizeof(b) << endl;
return 0;
}
//A.编译报错 B.运⾏报错 C.8 D.12----D

想必大家根据内容对齐的知识都会选择c,但是在该题中存在的virtual函数是一个指针(4个字节)所以一共是12bytes.

⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针(该指针指向一个指针数组),因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

多态是如何实现的

问题引入:

从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调⽤Person::BuyTicket, ptr指向Student对象调⽤Student::BuyTicket的呢?

通过下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址(虚拟函数地址会进行重写)。所以通过这种方式传不同对象就能调用不同的虚函数,完成不同的行为。

虚函数表

注意点:

1.基类对象的虚函数表中存放基类中所有的虚函数地址。

问:为什么一个类中会存放一张虚函数表?

答:为了多个类能够共享一张虚函数表,一定角度节约了资源。

结论:同类型的对象虚表共用,不同对象虚表各自独立。

2.派生类有两部分构成,继承下来的基类和自己的成员变量,继承下来的基类中有虚函数表指针,自己就不在生成虚函数表指针(但这里的虚函数表指针进行的重写/覆盖 ,指针地址也发生了变化)。

3.派生类的虚函数表包含,基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址。

4.虚函数的存放地址与普通函数的地址是一样的,编译好后是一段指令,都是存放在代码段的,只是虚函数的地址又存放在虚表中。

5.虚函数表存放在哪里并不确定。

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

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

相关文章

【C++】list 链表的使用+模拟实现

目录 文章目录 前言 一、list的简介 二、list的使用方法 三、list的模拟实现 1.基本框架&#xff1a; 2.迭代器实现 3.常用接口实现 四、完整代码 总结 前言 本文主要介绍C【STL】容器中的 list&#xff0c;包括接口说明和模拟实现。其中讲解了迭代器功能上的分类&am…

2. EXCEL中函数和公式《AI赋能Excel》

欢迎来到滔滔讲AI。今天我们来学习和讨论下函数和公式是什么&#xff0c;以及它们之间的区别。 点击图片查看视频 2、AI赋能EXCEL-函数和公式 一、什么是函数 首先&#xff0c;我们来了解一下函数。函数是Excel中预定义的计算工具&#xff0c;能够帮助我们快速进行各种计算。 …

基于AT89C51单片机的教室智能照明控制系统

点击链接获取Keil源码与Project Backups仿真图&#xff1a; https://download.csdn.net/download/qq_64505944/90419908?spm1001.2014.3001.5501 C16 部分参考设计如下&#xff1a; 摘 要 本项目的智能教室灯光控制系统通过合理的软硬件设计&#xff0c;有效地提升了教室…

商业化运作的“日记”

晴&#xff0c;2025年2月24日 看到这张图&#xff1a; 将其放大&#xff1a; 建立表格&#xff1a; 原话翻译一些点市场中的万物现出本相&#xff0c;无非世人的需求有需求才有市场商品交换需求交换⇆孕育平台产品价值功能价值情绪价值资产价值解决实际问题 情感经济价值/增…

Matlab R2024B软件安装教程

一、新版本特点 MATLAB R2024B版本带来了众多新特性和改进&#xff0c;旨在提升用户的内容创作体验和工程效率。以下是该版本的一些主要特点&#xff1a; 1. 性能提升和优化&#xff1a;R2024B版本在性能上进行了显著优化&#xff0c;无论是在提问、回答问题、发布新技巧还是…

本地部署AI模型 --- DeepSeek(二)---更新中

目录 FAQ 1.Failed to load the model Exit code: 18446744072635812000 FAQ 1.Failed to load the model Exit code: 18446744072635812000 问题描述&#xff1a; &#x1f972; Failed to load the model Error loading model. (Exit code: 18446744072635812000). Unkn…

项目一 - 任务3:搭建Java集成开发环境IntelliJ IDEA

本次实战任务通过搭建 Java 集成开发环境 IntelliJ IDEA&#xff0c;深入体验了 Java 开发的全流程。从安装 IntelliJ IDEA 到配置开发环境&#xff0c;再到创建 Java 项目、包和类&#xff0c;每一步都为后续开发奠定了基础。通过编写简单的 Java 程序&#xff0c;如 Welcome …

【uni-app】对齐胶囊容器组件

代码碎片 <template><div><view :style"{ height: ${statusBarHeight}px }"></view><viewclass"":style"{height: ${menuButtonHeight menuButtonPadding * 2}px,width: ${menuButtonInfo.left}px,}"><slot …

OpenCV(8):图像直方图

在图像处理中&#xff0c;直方图是一种非常重要的工具&#xff0c;它可以帮助我们了解图像的像素分布情况。通过分析图像的直方图&#xff0c;我们可以进行图像增强、对比度调整、图像分割等操作。 1 什么是图像直方图&#xff1f; 图像直方图是图像像素强度分布的图形表示&am…

攻防世界 Reversing-x64Elf-100

进入题目&#xff0c;先下载附件 将下载好的RE文件拖入ExeinfoPE查看&#xff0c;有无壳 “Diagnose” 一栏的信息 “NOT WIN EXE -o - ELF executable [64bit obj. Exe file - CPU: AMD x86” 可知&#xff0c;这是一个 ELF 格式的可执行文件&#xff08;通常用于 Linux 系统…

【C】堆的应用1 -- 堆排序

之前学习了堆&#xff0c;堆的一棵以顺序结构存储的完全二叉树&#xff0c;堆本身又氛围大根堆和小根堆&#xff0c;假设以大根堆为例&#xff0c;由于堆顶部元素是一棵二叉树里面最大的元素&#xff0c;所以如果每次都取堆顶的元素&#xff0c;那么取出的元素就是一个降序排列…

CentOS 7配置YOLOv8环境指南:无显卡版教程 - 幽络源

看本篇教程前请确保Centos7系统已安装配置Python3环境&#xff0c;参考幽络源上一篇文章>CentOS 7安装Python3环境详细指南&#xff1a;从源码编译到PIP配置 步骤1&#xff1a;建立python虚拟环境项目 在home目录下执行如下命令新建虚拟环境python项目 python3 -m venv y…

Confluence知识库管理系统安装步骤(Windows版本)

我们介绍的是安装7.15.1以下版本的安装方式,8.0以上的安装方式暂不支持。 如果你要安装8.0以上的版本,请参考本文末尾的附录中提供的相关网址。 首先我们安装之前需要准备安装所需文件以上文件可以在这里下载:【https://download.csdn.net/download/Elegant_Kevin/90412040】…

Uniapp 开发中遇到的坑与注意事项:全面指南

文章目录 1. 引言Uniapp 简介开发中的常见问题本文的目标与结构 2. 环境配置与项目初始化环境配置问题解决方案 项目初始化注意事项解决方案 常见错误与解决方案 3. 页面与组件开发页面生命周期注意事项示例代码 组件通信与复用注意事项示例代码 样式与布局问题注意事项示例代码…

学习笔记--电磁兼容性EMC

一、基本概念 电磁兼容性&#xff08;Electromagnetic Compatibility&#xff0c;EMC&#xff09;是电子电气设备在特定电磁环境中正常工作的能力&#xff0c;同时不会对其他设备产生不可接受的电磁干扰。其核心目标是确保设备在共享的电磁环境中既能抵抗干扰&#xff0c;又能避…

Unity百游修炼(2)——Brick_Breaker详细制作全流程

一、项目简介 Brick Breaker 是一款经典的打砖块游戏&#xff0c;本次案例将使用 Unity 引擎来实现该游戏的核心功能。 游戏画面如下&#xff1a; Brick_ breaker 二、项目结构概览和前期准备 &#xff08;1&#xff09;在 Unity 项目视图中&#xff0c;我们可以看到几个重要…

Java基础常见的面试题(易错!!)

面试题一&#xff1a;为什么 Java 不支持多继承 Java 不支持多继承主要是为避免 “菱形继承问题”&#xff08;又称 “钻石问题”&#xff09;&#xff0c;即一个子类从多个父类继承到同名方法或属性时&#xff0c;编译器无法确定该调用哪个父类的成员。同时&#xff0c;多继承…

算法题(77):数组中的第k个最大元素

审题&#xff1a; 需要我们在时间复杂度O(n)的前提下找到数组中第k个最大元素 思路&#xff1a; 方法一&#xff1a;建堆实现 首先写一个dowmset函数&#xff0c;实现对第i个索引位置的向下调整。然后创建build函数&#xff0c;利用dowmset实现向下调整建堆&#xff0c;再根据k…

PCIe学习笔记1:PCIe体系架构——PCIe简介

目录 一、PCIe简介 1.1 串行传输 1.1.1 相对于并行传输的优化 1.1.2 带宽计算 1.1.3 差分信号传输 1.1.4 基于数据包的传输协议 1.2 PCIe的系统拓扑结构 1.2.1 根组件&#xff08;Root Complex&#xff0c;RC&#xff09; 1.2.2 上行端口与下行端口 1.2.3 交换机与桥 …

一天记20个忘10个之4:man

据说&#xff0c;给你一个支点&#xff0c;你就能撬起地球。 那好&#xff0c;今天&#xff0c;我给你一个 man&#xff0c;如果你能完成记20个忘10个的任务&#xff0c;你就真的很 man 了。 零、热身 young manold manmedical man 一、man之复合词 1.1 man复合词 chairm…