14. C++继承与虚函数


【继承基础概念】

继承可以让本类使用另一个类的非私有成员,提供共用成员的类称为父类或基类,使用共用成员的类称为子类或派生类,子类创建对象时会包含继承自父类的成员。

继承的优势是减少重复定义数据,当本类需要在另一个类的基础上开发新功能时可以使用继承,这样可以简化代码并节省内存。

若父类中的某些成员禁止被外界使用,可以定义为私有成员,若在禁止外界使用时却允许被子类使用,可以定义为保护成员。

重设访问权限

子类继承自父类的成员需要重新设置在子类的访问权限,重设关键词如下:
public,继承的成员保持原有访问权限,公有成员还是公有权限,保护成员还是保护权限。
private,继承的成员设置为私有权限。
protected,继承的成员设置为保护权限。

若不重设访问权限,则默认为私有权限。

子类创建对象

子类创建对象时,并非将继承自父类的成员合并到子类然后创建合并后的对象,而是会首先创建父类对象,然后创建子类对象,在子类对象中调用父类成员时会转换为调用自动创建的父类对象成员,虽然子类不会继承父类私有成员,但是自动创建的父类对象包含父类私有成员,这是为了被父类公有成员调用,每个子类对象都有自己专用的父类对象。

自动创建父类对象时,其构造函数由子类构造函数调用,若父类有构造函数而子类没有,编译器会为子类自动创建一个构造函数,作用只是调用父类构造函数执行,子类构造函数会首先调用父类构造函数执行,之后再执行子类构造函数自身代码,若两个构造函数功能有冲突,则以最后执行的子类构造函数为准。

若父类构造函数无参数,则子类构造函数无需手动管理父类构造函数,编译器会自动在子类构造函数中调用父类构造函数。
若父类构造函数有参数,则子类构造函数需要为父类构造函数参数赋值,此时必须手动定义子类构造函数并为父类构造函数参数赋值,编译器自动生成的子类构造函数无法为父类构造函数参数赋值。

子类对象使用完毕后,子类析构函数负责调用父类析构函数,若父类有析构函数而子类没有则编译器自动创建一个子类析构函数,作用只是调用父类析构函数执行。
子类析构函数会在自身代码执行完毕后调用父类构造函数,若两者功能有冲突则以最后执行的父类析构函数为准。


子类创建对象时不能直接赋值,因为原理上子类需要同时为继承自父类的成员数据赋值,但这会与父类构造函数产生冲突,即使父类没有构造函数也是如此,这样限制的目的是统一语法规则。

子类对象可以使用如下方式赋值:
1.为成员数据设置默认值。
2.使用构造函数赋值。
3.使用其它公有函数赋值,创建子类对象后手动执行此函数。


在C++中有3种对象不能在创建时直接赋值:
1.包含私有成员数据的对象。
2.有构造函数的对象。
3.子类对象。

#include <iostream>
class base
{
protected:int a,b;public:base(int i1, int i2){a = i1;b = i2;printf("父类构造函数\n");}~base(){printf("父类析构函数\n");}void add() const{printf("a+b=%d\n", a+b);}
};/* derive继承base类,继承成员保持原有访问权限 */
class derive : public base
{
public:/* 父类构造函数有参数,需要手动调用父类构造函数,并为其参数赋值 */derive(int i1, int i2) : base(0,0){a = i1;b = i2;printf("子类构造函数\n");}~derive(){printf("子类析构函数\n");}void mul() const{printf("a*b=%d\n", a*b);}
};int main()
{derive derobj(1,2);    //创建子类对象,观察父类与子类构造函数与析构函数的执行顺序derobj.add();derobj.mul();return 0;
}

对象类型转换

子类对象可以转换为父类类型,转换后的子类对象将丢失自建成员,只保留继承自父类的成员,所以父类对象可以引用子类对象赋值,编译器会自动将子类对象转换为父类类型。

父类对象不能转换为子类类型,因为父类中不包含子类成员,子类对象不能引用父类对象赋值。

若需要手动转换子类对象类型,可以使用如下语法: (类型名)对象名。

#include <iostream>
class base
{
public:base(){printf("base构造函数\n");}~base(){printf("base析构函数\n");}void output(){printf("父类\n");}
};class derive : public base
{
public:derive(){printf("derive构造函数\n");}~derive(){printf("derive析构函数\n");}void output()    //子类可以定义与父类同名的函数{printf("子类\n");}
};int main()
{derive derobj;derobj.output();            //调用子类同名函数((base)derobj).output();    //转换为父类类型,调用父类同名函数return 0;
}

子类对象转换为父类类型后,会额外执行一遍父类析构函数,上述代码中,base析构函数会执行两遍。

对象指针类型转换

子类类型指针,可以自动转换为父类类型,但是此时不能通过指针调用子类对象成员,可以将指针再次转换为子类类型从而调用子类对象成员。

父类类型指针,不能自动转换为子类类型,因为父类对象不包含子类成员,若使用代码手动转换则通过指针调用子类成员时会出错。


【多继承】

间接多继承

继承可以一直传递下去,比如A派生B,B派生C,那C也会间接继承A,创建C对象时会自动创建A、B两个对象,C调用B构造函数,B调用A构造函数。

#include <iostream>
class baseA
{
protected:int a,b;public:baseA(int i1, int i2){a = i1;b = i2;printf("baseA构造函数\n");}~baseA(){printf("baseA析构函数\n");}void add() const{printf("a+b=%d\n", a+b);}
};class baseB : public baseA
{
public:baseB(int i1, int i2) : baseA(0,0){a = i1;b = i2;printf("baseB构造函数\n");}~baseB(){printf("baseB析构函数\n");}void sub() const{printf("a-b=%d\n", a-b);}
};class derive : public baseB
{
public:derive(int i1, int i2) : baseB(0,0){a = i1;b = i2;printf("derive构造函数\n");}~derive(){printf("derive析构函数\n");}void mul() const{printf("a*b=%d\n", a*b);}
};int main()
{derive derobj(1,2);derobj.add();derobj.sub();derobj.mul();return 0;
}


间接多继承时,可继承成员由直接继承的父类决定,而非更上层的父类决定。

#include <iostream>
class baseA
{
public:void baseAput(){printf("baseA\n");}
};class baseB : private baseA    //继承成员重设为私有权限
{
public:void baseBput(){printf("baseB\n");}
};class derive : public baseB
{
public:void derput(){//baseAput();    //错误,虽然baseAput定义为公有权限,但是被baseB重设为私有权限,derive继承baseB,不能调用baseB私有成员baseBput();}
};int main()
{derive derobj;derobj.derput();return 0;
}

直接多继承

一个类可以直接继承多个类,此时每个父类都可以单独重设成员访问权限,子类创建对象时会自动创建所有父类的对象,每个父类对象的构造函数都由此子类构造函数调用,析构函数同理。

#include <iostream>
class baseA
{
protected:int a,b;public:baseA(int i1, int i2){a = i1;b = i2;printf("baseA构造函数\n");}int add() const{return a+b;}
};class baseB
{
protected:float a,b;public:baseB(float f1, float f2){a = f1;b = f2;printf("baseB构造函数\n");}float add() const{return a+b;}
};class derive : public baseA, public baseB
{
public:derive(int i1, int i2, float f1, float f2) : baseA(0,0), baseB(0,0){baseA::a = i1;baseA::b = i2;baseB::a = f1;baseB::b = f2;printf("derive构造函数\n");}//......
};int main()
{derive derobj(1, 2, 1.3, 1.5);printf("整数加法结果:%d\n""小数加法结果:%f\n",((baseA)derobj).add(), ((baseB)derobj).add());return 0;
}

直接多继承很容易导致混乱,尤其是在多层继承关系中,在大型项目中经常搞不清一个类的所有上级父类又直接继承了多少个类,类成员的管理非常麻烦,很多高级编程语言都会禁用直接多继承,若你非常注重程序性能,其实使用C语言更合适,而非使用C++直接多继承。

菱形继承

菱形继承是一种复杂的多继承,继承关系网组成一个菱形,具体方式为:A派生出B和C,D又同时继承B和C,D创建对象时会自动创建B、C两个对象,B、C又会各自创建一个A对象,此时D对象就有两个可以使用的A对象,这将导致混乱。

为了解决混乱问题,C++规定在菱形继承关系中B和C继承A时需要添加virtual关键词定义为虚继承,此时创建D对象时只会创建一个A对象,并且三个父类的构造函数都将由D负责调用,B和C的构造函数不再调用A构造函数,等同于将代码转换为D直接继承A、B、C三个类,但是单独创建B或C对象时不受影响。

#include <iostream>
class baseA
{
public:baseA(){printf("baseA构造函数\n");}//......
};class baseB : virtual public baseA    //虚继承
{
public:baseB(){printf("baseB构造函数\n");}//......
};class baseC : virtual public baseA    //虚继承
{
public:baseC(){printf("baseC构造函数\n");}//......
};class derive : public baseB, public baseC
{
public:derive(){printf("derive构造函数\n");}
};int main()
{derive derobj;return 0;
}


【虚函数】

C语言通过函数实现程序功能模块化,程序经常需要使用函数指针调用不同的模块,C++将函数封装在类中管理,函数指针只能指向本类的成员函数,无法像C语言那样使用函数指针随意调用函数,为此C++增加了虚函数功能,虚函数的作用是通过指针调用同源继承关系中所有类的成员函数。

虚函数是父类中定义的特殊函数,子类继承后可以直接使用,也可以重写内部代码,但是不能改变虚函数的参数和返回值,否则就不能实现使用函数指针统一调用。

虚函数使用父类对象指针调用执行,编译器会转换为通过函数指针调用虚函数(而普通函数会转换为直接调用),父类指针可以赋值为子类对象地址,赋值为哪个子类对象地址就会调用哪个子类重写的虚函数,同时虚函数的this参数也会赋值为所在类的对象地址,若子类没有重写虚函数则调用父类定义的虚函数。

虚函数定义方式如下:
1.父类在函数返回值之前使用virtual关键词定义虚函数。
2.子类重写虚函数时可以添加virtual关键词,也可以不添加,为了方便识别虚函数一般会加上。
3.子类重写虚函数时可选在参数之后添加override关键词,用于强制编译器检查重写的虚函数,若重写的虚函数与原型不同(参数、返回值不同),则编译报错。


注:虚函数可以当做普通函数使用,直接使用函数名调用它,此时等于不使用虚函数机制,与使用普通函数无异。

#include <iostream>
class base
{
public:/* 父类定义虚函数 */virtual void f1(){//......printf("通用模块\n");}/* 父类定义虚析构函数,原因之后介绍 */virtual ~base(){}
};class deriveA : public base
{
public:/* 子类重写虚函数 */virtual void f1() override{//......printf("业务模块1\n");}
};class deriveB : public base
{
public:virtual void f1() override{//......printf("业务模块2\n");}
};class deriveC : public base
{
public:virtual void f1() override{//......printf("业务模块3\n");}
};int main()
{base baseobj;deriveA derAobj;deriveB derBobj;deriveC derCobj;base * p1;    //定义父类对象指针int i;scanf("%d", &i);    //模块调用变量if(i == 1){p1 = &derAobj;}else if(i == 2){p1 = &derBobj;}else if(i == 3){p1 = &derCobj;}else{p1 = &baseobj;}p1->f1();    //使用父类指针调用虚函数return 0;
}

上述C++代码功能等同于如下C语言代码:

#include <stdio.h>
struct k
{//......
};
void f0(struct k * this)
{//......printf("通用模块\n");
}
void f1(struct k * this)
{//......printf("业务模块1\n");
}
void f2(struct k * this)
{//......printf("业务模块2\n");
}
void f3(struct k * this)
{//......printf("业务模块3\n");
}
int main()
{void (*p1)();int i;scanf("%d", &i);if(i == 1){p1 = f1;}else if(i == 2){p1 = f2;}else if(i == 3){p1 = f3;}else{p1 = f0;}p1();    //使用函数指针统一调用模块return 0;
}


禁止虚函数重写

虚函数机制会随继承关系遗传下去,若一个类间接继承了提供虚函数的父类,则此类也会有虚函数,也可以使用虚函数机制。

若一个类希望虚函数机制到此为止,其子类不再重写、不再使用虚函数,可以在此类的虚函数中添加final关键词。

virtual void f1() override final { //...... }


虚析构函数

若使用虚函数机制的子类对象需要使用new申请内存进行存储,将此子类对象地址赋值给父类类型指针后,对象使用完毕执行delete释放内存时会出现如下情况。

#include <iostream>
class base
{
public:~base(){printf("base析构函数\n");}virtual void f1(){printf("base虚函数\n");}
};class derive : public base
{
public:~derive(){printf("derive析构函数\n");}virtual void f1() override{printf("derive虚函数\n");}
};int main()
{derive *p1 = new derive;base *p2 = p1;p2->f1();delete p2;    //通过父类指针释放子类对象,只会执行父类析构函数return 0;
}

上面代码中,子类类型指针p1赋值给父类类型指针p2,两个指针指向同一个对象,使用delete释放内存时,原理上通过任何一个指针释放都可以,但是释放对象之后还会涉及到自动执行析构函数的问题,p2是父类类型,通过p2释放内存时编译器只会调用父类析构函数,不会执行子类析构函数,所以实际上只能通过子类类型指针释放内存,或者将父类类型指针强制转换为子类类型再释放,这限制了C++代码的灵活性,为此C++规定使用虚函数机制时父类需要定义一个虚析构函数(即使此函数什么也不做),此时通过父类类型指针释放子类对象时会调用子类析构函数。

#include <iostream>
class base
{
public:/* 父类定义虚析构函数 */virtual ~base(){printf("base析构函数\n");}virtual void f1(){printf("base虚函数\n");}
};class derive : public base
{
public:~derive(){printf("derive析构函数\n");}virtual void f1() override{printf("derive虚函数\n");}
};int main()
{derive *p1 = new derive;base *p2 = p1;p2->f1();delete p2;    //通过父类类型指针释放子类对象,会调用子类析构函数return 0;
}


纯虚函数

使用虚函数机制时,若父类不需要创建对象,父类虚函数也不需要直接调用,父类存在的唯一作用就是提供父类类型指针,从而统一调用子类重写的虚函数,此时可以将父类虚函数定义为纯虚函数,纯虚函数只有函数主体代码,没有任何内容,定义有纯虚函数的类称为抽象类。

纯虚函数与虚函数的区别如下:
1.包含纯虚函数的类不能创建对象。
2.纯虚函数没有内容,子类必须重写纯虚函数,否则子类的虚函数也是纯虚函数,子类也不能创建对象。

class base
{
public:/* 定义纯虚函数,=0表示纯虚函数 */virtual void f1() = 0;/* 抽象类也需要定义虚析构函数 */virtual ~base(){printf("base析构函数\n");}
};


 

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

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

相关文章

L1-009 N个数求和

MD...提交过了好几次才通过。 第三个测试点: 需要使用long long&#xff0c;要求长整型。干脆就把int全部替换成long long。 第五个测试点: 随便试出来的&#xff0c;我输入了2 1/2 -1/2,发现啥都没打印出来。原来是忽略了结果是0的情况&#xff0c;如果整数部分和分子部分都…

LabVIEW高温摩擦磨损测试系统

LabVIEW高温摩擦磨损测试系统 介绍了一个基于LabVIEW的高温摩擦磨损测试系统的软件开发项目。该系统实现高温条件下材料摩擦磨损特性的自动化测试&#xff0c;通过精确控制和数据采集&#xff0c;为材料性能研究提供重要数据支持。 项目背景 随着材料科学的发展&#xff0c;…

git revert 撤回之前的几个指定的提交

文章目录 Intro操作命令-n 选项 参考 Intro 在开发过程中&#xff0c;有的时候一开始只是一个小需求&#xff0c;可以改着改着事情超出了控制&#xff0c;比如说我一开始只是想调整一个依赖包的版本&#xff0c;可是改到后来类库不兼容甚至导致项目无法启动。 这个时候我就想&…

npm市场发布包步骤

1.打开npm官网npm官网 2.创建自己的账号 3.查看当前npm的镜像源&#xff0c; 如果出现淘宝的镜像源则需要切换成官方的镜像源 npm config get registry //查看镜像源 https://registry.npm.taobao.org/ //淘宝的镜像源 https://registry.npmjs.org/ //官方的镜像源 …

Linux文本处理三剑客:awk(常用匹配模式)

在Linux操作系统中&#xff0c;grep、sed、awk被称为文本操作“三剑客”&#xff0c;上三期中&#xff0c;我们将详细介绍grep、sed、awk的基本使用方法&#xff0c;希望能够帮助到有需要的朋友。 1、前言 awk作为一门编程语言还有很多内容&#xff0c;我们继续学习awk。 网…

线程安全之死锁

目录 一、概念 二、例子 三、死锁相关面试题目 一、概念 死锁主要发生在有多个依赖锁存在时,会在一个线程试图以另一个线程相反顺序锁住互斥量时发生 死锁使得一个或多个线程被挂起而无法继续执行,最糟糕的是,这种情况还不容易被发现。 在一个线程中对一个已经加锁的普通锁…

根据标准化开发流程---解析LIN总线脉冲唤醒的测试方法和用例设计思路

前言&#xff1a;本文从标准化开发流程的角度&#xff0c;以LIN总线脉冲唤醒为切入点。从测试工程师的角度来讲测试工作应当如何展开&#xff08;结合我干测试总结出来的测试经验&#xff09;。希望大家都能从中有收获&#xff01;&#xff01;谢谢&#xff01;&#xff01; 1…

FreeROTS day2

总结DMA空闲中断接收数据的使用方法 首先要要选择串口然后配置串口的参数&#xff0c;配置MDA通道选择接受数据&#xff0c;配置空闲中断&#xff0c;定义一个数据接收的容器&#xff0c;启动MDA传输当串口收到数据时MDA将数据传输到容器中,MDA会一直检测是否有数据当有数据并…

【蓝桥杯基础算法】dfs(上)组合数,全排列

刚接触算法&#xff0c;有没有被递归又循环的dfs吓到&#xff1f;没关系&#xff0c;几个例题就可以彻底掌握&#xff01; 1.全排列 1-n的全排列,如输入3&#xff0c;按顺序对1-3进行排列 //枚举 #include<iostream> #include<algorithm> #include<cstring>…

[动态规划][蓝桥杯 2022 省 B] 李白打酒加强版 -- 代码注释含详解

P8786 [蓝桥杯 2022 省 B] 李白打酒加强版(洛谷) 洛谷题目链接 李白打酒很快活&#xff0c;而我打了一晚上代码才把这题弄懂&#x1f972; P8786 [蓝桥杯 2022 省 B] 李白打酒加强版(洛谷)题目描述输入格式输出格式样例 #1样例输入 #1样例输出 #1 提示\***\*\*\*\*\***\*\*\**…

-bash: unzip: 未找到命令的解决方案

遇到 -bash: unzip: 未找到命令 这样的错误信息&#xff0c;表示你的系统中没有安装 unzip 工具。unzip 是一个常用的解压工具&#xff0c;用于解压缩 .zip 文件。你可以通过系统的包管理器安装它。 根据你使用的 Linux 发行版&#xff0c;安装 unzip 的命令会有所不同。下面是…

MUMU模拟器12连logcat的方法

大家好&#xff0c;我是阿赵。   在开发手机游戏的时候&#xff0c;在真机上会出现各种问题&#xff0c;在查询问题的时候&#xff0c;安卓手机需要用adb连接来连接手机看logcat输出分析问题。但由于连接手机比较麻烦&#xff0c;所以我都习惯在电脑用安卓模拟器来测试。   …

通过vue ui创建项目

确认前端环境都安装好之后 打开黑窗口 输入 vue ui 会打开一个vue的网页 在此创建项目 可以选择在那个路径创建 这是我的项目配置 这里是选择vue版本 我要用的是vue2 选好点击创建项目就好了 创建好后的重点的目录结构以及结构的作用 启动前端工程 将创建好的项目导入编译器 我…

跨境干货 | 想“躺平式”经营?TikTok Shop全托管模式入驻了解一下!

在当前跨境电商出海模式不断升级的背景下&#xff0c;全托管模式已经成为各类平台的标准配置。其中&#xff0c;TikTok全托管模式已经上线运行有很长一段时间了&#xff0c;这个模式下&#xff0c;主打一个“仅供货、免运营”&#xff0c;它降低了商家进入和运营TikTok市场的门…

liunx操作系统 环境变量

环境变量 main函数参数 命令行参数环境变量 环境变量的查看环境变量的获取 main函数参数 命令行参数 main函数是有参数的&#xff0c;只是我们一般不适用 这是main函数从bash中读取进程数据使用的一个基本入口。 下面进行简单演示。 o 好oo都是我们输入的命令行参数。其实&a…

【Linux】开始使用gdb吧!

开始使用gdb吧&#xff01; 1 下载安装2 开始使用3 实践运用补充一下 print 的 功能 &#xff08;类似监视窗口的作用&#xff09;和显示堆栈的功能Thanks♪(&#xff65;ω&#xff65;)&#xff89;谢谢阅读&#xff01;&#xff01;&#xff01;下一篇文章见&#xff01;&am…

虽说主业搞前端,看到如此漂亮的网页UI,也是挪不开眼呀。

漂亮的网页UI能够吸引人的眼球&#xff0c;给人留下深刻的印象。作为前端开发人员&#xff0c;可以通过不断学习和掌握设计技巧和工具&#xff0c;提升自己的UI设计能力&#xff0c;为用户提供更好的视觉体验。 以下是一些提升网页UI设计能力的建议&#xff1a; 学习设计基础知…

仿牛客网项目---消息队列的实现

本篇文章讲一讲我们的项目中用到的消息队列。 1.阻塞队列 2.kafka 我的项目为什么要用消息队列&#xff1f; 如果采用消息队列&#xff0c;那么评论、点赞、关注三类不同的事&#xff0c;可以定义三类不同的主题&#xff08;评论、点赞、关注&#xff09;&#xff0c;发生相应…

AI Agents之CrewAI智能体开发框架

一、前言 AI Agents 的开发是当前软件创新领域的热点。随着大语言模型 (LLM) 的不断进步&#xff0c;预计 AI 智能体与现有软件系统的融合将出现爆发式增长。借助 AI 智能体&#xff0c;我们可以通过一些简单的语音或手势命令&#xff0c;就能完成以往需要手动操作应用程序才能…

片上网络(NoC)技术的发展及其给高端FPGA带来的优势

片上网络(NoC)技术的发展及其给高端FPGA带来的优势 1. 概述 在摩尔定律的推动下,集成电路工艺取得了高速发展,单位面积上的晶体管数量不断增加。 片上系统(System-on-Chip,SoC)具有集成度高、功耗低、成本低等优势,已经成为大规模集成电路系统设计的主流方向,解决了…