C++之virtual(虚)关键字:虚基类(虚继承);虚函数和纯虚函数_c 基类 虚函数-程序员宅基地

技术标签: virtual  C++基础  纯虚函数  虚基类  虚继承  虚函数  

一、虚基类

1、为什么要虚基类或者说虚继承:
a、直接二义性可以用作用域与同名覆盖的方法来消除(看程序注释),但是间接二义性(同名数据成员在内存中同时拥有多个拷贝,同一个成员函数会有多个映射,菱形继承或称钻石继承)只能通过虚继承来消除。

#include<iostream>
using   namespace std;
class Automobile              //汽车类
{
private:
	int power;   //动力
public:
	Automobile(int power);
	void show();
};
class Car : public Automobile      //小客车类
{
	private:
	int seat;     //座位
public:
	Car(int power, int seat) :Automobile(power);
	void show();
};
class Wagon : public Automobile //小货车类
{
	private:
	int load;     //装载量
public:
	Wagon(int power, int load) :Automobile(power);
	void show();
};
class StationWagon :public Car, public Wagon  //客货两用车类
{
public:
	StationWagon(int CPower, int WPower, int seat, int load)
		:Wagon(WPower, load), Car(CPower, seat);
	void show()     //3,同名覆盖基类中的show()函数
	{ Car::show(); //2,作用域限定,若不限定会不断递归调用StationWagon中的show()函数本身
		Wagon::show();}
};
int main()
{
	StationWagon SW(105, 108, 3, 8);
	SW.show();    //1,若StationWagon类中没有show()函数将会产生直接二义性
	return 0;
}

b、以上代码中 StationWagon SW(105, 108, 3, 8);时,调用构造函数过程,StationWagon对象SW回溯自己的构造函数;发现有继承, 回溯Car构造函数,发现有继承,回溯Automobile构造函数,没有继承了,依次运行Automobile构造函数、Car构造函数;回溯Wagon构造函数,发现有继承,回溯Automobile构造函数,没有继承了,依次运行Automobile构造函数、Wagon构造函数;最后调用StationWagon构造函数。析构和构造顺序相反。
提示:“”“”的依据是继承被声明的先后顺序。
c、仔细分析后,我们知道:Ca::power为105,Wagon::power为108,但是不知道StationWagon::power为多少。StationWagon类对象中,具有多个从不同途径继承来的同名的数据成员power。 占据了内存空间,由于在内存中有不同的拷贝而可能造成数据不一致。这就产生了间接二义性。

2、虚基类的作用
a、不同的路径继承过来的同名数据成员在内存中就只有一个拷贝,同一个函数名也只有一个映射,解决了二义性问题,也节省了内存,避免了数据不一致的问题。
b、声明了虚基类之后,虚基类在进一步派生过程中始终和派生类一起,维护同一个基类子对象的拷贝。

3、虚基类的定义
虚基类的定义是在融合在派生类的定义过程中的,其定义格式如下:
class 派生类名:virtual 继承方式 基类名

4、虚基类的构造和虚构
a、规定,在初始化列表中同时出现对虚基类和非虚基类构造函数的调用,虚基类的构造函数先于非虚基类的构造函数的执行。

b、虚基类的构造函数调用分三种情况:
(1) 虚基类没有定义构造函数
程序自动调用系统缺省的构造函数来初始化派生类对象中的虚基类子对象。
(2) 虚基类定义了缺省构造函数
程序自动调用自定义的缺省构造函数和析构函数。
(3) 虚基类定义了带参数的构造函数
这种情况下,虚基类的构造函数调用相对比较复杂。因为虚基类定义了带参数的构造函数,所以在整个继承结构中,直接或间接继承虚基类的所有派生类,都必须在构造函数的初始化表中列出对虚基类的初始化。但是,只有用于建立派生类对象的那个最远派生类的构造函数才调用虚基类的构造函数,而派生类的其它非虚基类中所列出的对这个虚基类的构造函数的调用被忽略,从而保证对公共虚基类子对象只初始化一次

c、虚基类的构造和虚构举例
对以上程序做以下修改

class Car: virtual public Automobile      //小客车类
class Wagon: virtual public Automobile //小货车类
StationWagon(int CPower,int WPower, int seat,int load) 
	:Automobile(CPower),Wagon(WPower,load), Car(CPower,seat)
                 {}

(1)调用构造函数过程,StationWagon对象SW回溯自己的构造函数;发现虚继承, 回溯Automobile构造函数,没有继承直接运行,回溯Car构造函数,发现虚继承,不在回溯,运行Car构造函数;回溯Wagon构造函数,发现虚继承,不在回溯,运行Wagon构造函数;最后调用StationWagon构造函数。析构和构造顺序相反。
(2)最远派生类StationWagon的直接基类Car和Wagon的构造函数对虚基类构造函数的嵌套调用将自动被忽略,这样,power只会被初始化一次。

二、虚函数

1、virtual关键字说明该成员函数为虚函数。在定义虚函数时要注意:
a、 虚函数不能是静态成员函数,也不能是友元函数。因为静态成员函数和友元函数不属于某个对象。
b 、内联函数是不能在运行中动态确定其位置的,即使虚函数在类的内部定义,编译时,仍将其看作非内联的。
c 、构造函数不能是虚函数(对象无法实例化),析构函数可以是虚函数,而且通常声明为虚函数。
d、只有类的成员函数才能声明为虚函数,虚函数的声明只能出现在类的定义中。因为虚函数仅适用于有继承关系的类对象,普通函数不能说明为虚函数。
e、虚函数有函数体。
f、虚函数可以派生,如果在派生类中没有重新定义虚函数,虚函数就充当了派生类的虚函数。

2、虚函数的访问与其它成员函数不同
在正常情况下,完全一样。只有通过子类指针初始化的指向基类的指针或引用来调用虚函数时才体现虚函数与一般函数的不同

3、要实现动态联编,需要满足三个条件:
a、应满足类型兼容规则。
b、在基类中定义虚函数, 并且在派生类中要重新定义虚函数。
c、要由成员函数或者是通过指针、引用访问虚函数。
注意:调用虚函数不一定发生动态联编,调用虚函数在静态联编是当成一般成员函数使用。

举例说明:

#include<iostream>
using namespace std;
class Point
{
private:
	int X, Y;
public:
	Point(int X = 0, int Y = 0)
	{
		this->X = X, this->Y = Y;
	}
	virtual double area()   //求面积
	{
		return 0.0;
	}
};
const double PI = 3.14159;
class Circle :public Point
{
private:
	double radius;   //半径
public:
	Circle(int X, int Y, double R) :Point(X, Y)
	{
		radius = R;
	}
	double area()   //求面积
	{
		return PI*radius*radius;
	}
};
int main()
{
	Point P1(10, 10);
	cout << "P1.area()=" << P1.area() << endl;
	Circle C1(10, 10, 20);
	cout << "C1.area()=" << C1.area() << endl;
	Point *Pp;
	Pp = &C1;
	cout << "Pp->area()=" << Pp->area() << endl;	//Pp->area()发生动态联编
	Point & Rp = C1;
	cout << "Rp.area()=" << Rp.area() << endl;    	// Rp.area()发生动态联编
	return 0;
}
/*
运行结果:
P1.area()=0
C1.area()=1256.64
Pp->area()=1256.64
Rp.area()=1256.64
*/

程序说明:
(1)如果去除virtual对Point::area()函数说明, Pp->area()将调用Point::area()函数;不去除,发生动态联编,调用Circle::area()函数。
(2)当在派生类中未重新定义虚函数(去除Circle::area()函数),虽然虚函数被派生类继承,但通过基类、派生类类型指针、引用调用虚函数时,不实现动态联编,调用的是基类的虚函数。
(3)没有重新定义虚函数时,并且不满足类型兼容规则(派生类中定义了虚函数的重载函数),与虚函数同名的重载函数覆盖了派生类中的虚函数。此时试图通过派生类对象、指针、引用调用派生类的虚函数时,不实现动态联编,调用的是基类的虚函数。

三、纯虚函数

1、定义:
a、纯虚函数(pure virtual function)是一个在基类中说明的虚函数,它在该基类中没有定义具体实现,要求各派生类根据实际需要定义函数实现。纯虚函数的作用是为派生类提供一个一致的接口。
b、 带纯虚函数的类叫虚基类也叫抽象类,这种基类不能直接生成对象,只能被继承,重写虚函数后才能使用,运行时动态绑定!
c、定义的形式为:
virtual 函数类型 函数名(参数表)=0;

2、与一般的虚函数的区别
a、原型在书写格式上的不同就在于后面加了=0。
b、纯虚函数根本就没有函数体,而虚函数一定有函数体,空虚函数的函数体为空。
c、纯虚函数所在的类是抽象类,不能直接进行实例化,空虚函数所在的类是可以实例化的。

3、与一般的虚函数共同的特点
都可以派生出新的类,然后在新类中给出新的虚函数的实现,而且这种新的实现可以具有多态特征。

四、补充(二、1、c):虚析构函数

1、当基类的析构函数被声明为虚函数,则派生类的析构函数,无论是否使用virtual关键字进行声明,都自动成为虚函数。
2、析构函数声明为虚函数后,程序运行时采用动态联编,因此可以确保使用基类类型的指针就能够自动调用适当的析构函数对不同对象进行清理工作。
3、当使用delete运算符删除一个对象时,隐含着对析构函数的一次调用,如果析构函数为虚函数,则这个调用采用动态联编,保证析构函数被正确执行。

class A
{
    
public:
   virtual ~A()            //虚析构函数
   {
      
        cout<<"A::~A() is called."<<endl; 
   }
   A() 
   {
    
        cout<<"A::A() is called."<<endl; 
   }
};
class B: public A          //派生类
{
    
private:
      int  *ip;
public:
      B(int size=0)
     {
    	
          ip=new int[size]; 
          cout<<"B::B() is called."<<endl;
     }
     ~B()
     {
     
          cout<<"B::~B() is called."<<endl; 
          delete []  ip;
     }
};
int main()
{
    
    A *b=new B(10);          //类型兼容
    delete b;
    return 0; 
}

运行结果:
1:A::A() is called.
2:B::B() is called.
3:B::~B() is called.
4:A::~A() is called.
由于定义基类的析构函数是虚析构函数,所以当程序运行结束时,通过基类指针删除派生类对象时,先调用派生类析构函数,然后调用基类的析构函数。
如果基类的析构函数不是虚析构函数,则程序运行结果会少了第3行,显然派生类对象中的动态分配的内存没有被释放,导致内存泄漏。所以一般将析构函数声明为虚函数。

如有错误或不足欢迎评论指出!创作不易,转载请注明出处。如有帮助,记得点赞关注哦(⊙o⊙)
更多内容请关注个人博客:https://blog.csdn.net/qq_43148810

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/qq_43148810/article/details/83514044

智能推荐

在ubuntu 8.04下安装Oracle 11g二-程序员宅基地

文章浏览阅读408次。 在ubuntu 8.04下安装Oracle 11g2008年05月22日 星期四 11:02oracle 11g 数据库虽然提供了linux x86的版本,但是支持的linux版本只有Red Hat,Novell and Solaris 这几个,debian 和 ubuntu 不在支持之列,所以在ubuntu下安装就相对麻烦一些,请照着下文的方法一步一步的安装,不

初一计算机知识点下册,初一英语下册语法知识点全汇总-程序员宅基地

文章浏览阅读166次。新东方在线中考网整理了《初一英语下册语法知识点全汇总》,供同学们参考。一. 情态动词can的用法can+动词原形,它不随主语的人称和数而变化。1. 含有can的肯定句:主语+can+谓语动词的原形+其他。2. 含有can的否定句:主语+can't+动词的原形+其他。3. 变一般疑问句时,把can提前:Can+主语+动词原形+其他? 肯定回答:Yes,主语+can。否定回答:No,主语+can't...._七年级下册计算机知识点

NX/UG二次开发—其他—UFUN函数调用Grip程序_uf调用grip-程序员宅基地

文章浏览阅读3k次。在平时开发中,可能会遇到UFUN函数没有的功能,比如创建PTP的加工程序(我目前没找到,哪位大神可以指点一下),可以使用Grip创建PTP,然后用UFUN函数UF_call_grip调用Grip程序。具体如下截图(左侧UFUN,右侧Grip程序):..._uf调用grip

Android RatingBar的基本使用和自定义样式,kotlin中文教程_ratingbar样式修改-程序员宅基地

文章浏览阅读156次。第一个:原生普通样式(随着主题不同,样式会变)第二个:原生普通样式-小icon第三个:自定义RatingBar 颜色第四个:自定义RatingBar DrawableRatingBar 各样式实现===============原生样式原生样式其实没什么好说的,使用系统提供的style 即可<RatingBarstyle="?android:attr/ratingBarStyleIndicator"android:layout_width=“wrap_cont.._ratingbar样式修改

OpenGL环境搭建:vs2017+glfw3.2.1+glad4.5_vs2017的opengl环境搭建(完整篇)-程序员宅基地

文章浏览阅读4.6k次,点赞6次,收藏11次。安装vs2017:参考vs2017下载和安装。安装cmake3.12.3:cmake是一个工程文件生成工具。用户可以使用预定义好的cmake脚本,根据自己的选择(像是Visual Studio, Code::Blocks, Eclipse)生成不同IDE的工程文件。可以从它官方网站的下载页上获取。这里我选择的是Win32安装程序,如图所示:然后就是运行安装程序进行安装就行。配置glfw3...._vs2017的opengl环境搭建(完整篇)

在linux-4.19.78中使用UBIFS_ubifs warning-程序员宅基地

文章浏览阅读976次。MLC NAND,UBIFS_ubifs warning

随便推点

计算机系统内存储器介绍,计算机系统的两种存储器形式介绍-程序员宅基地

文章浏览阅读2.2k次。计算机系统的两种存储器形式介绍时间:2016-1-6计算机系统的存储器一般应包括两个部分;一个是包含在计算机主机中的主存储器,简称内存,它直接和运算器,控制器及输入输出设备联系,容量小,但存取速度快,一般只存放那些急需要处理的数据或正在运行的程序;另一个是包含在外设中的外存储器,简称外存,它间接和运算器,控制器联系,存取速度虽然慢,但存储容量大,是用来存放大量暂时还不用的数据和程序,一旦要用时,就..._计算机存储器系统采用的是主辅结构,主存速度快、容量相对较小,用于 1 分 程序,外

西门子PLC的编程工具是什么?_西门子plc编程软件-程序员宅基地

文章浏览阅读5.6k次。1. STEP 7(Simatic Manager):STEP 7或者Simatic Manager是西门子PLC编程最常用的软件开发环境。4. STEP 7 MicroWin:STEP 7 MicroWn是一款专门针对微型PLC(S7-200系列PLC)的编程软件,是Simatic Manager的简化版。如果需要与PLC系统配合使用,则需要与PLC编程工具进行配合使用。除了上述软件之外,西门子还提供了一些配套软件和工具,如PLC模拟器、硬件调试工具等,以帮助PLC编程人员快速地进行调试和测试。_西门子plc编程软件

HashMap扩容_hashma扩容-程序员宅基地

文章浏览阅读36次。【代码】HashMap扩容。_hashma扩容

Eclipse maven项目中依赖包不全,如何重新加载?_maven资源加载不全,怎么重新加载-程序员宅基地

文章浏览阅读2.9k次。1mvn dependency:copy-dependencies2 项目右键 -> Maven -> Disable Maven Nature3 项目右键 -> Configure -> Convert to Maven Project_maven资源加载不全,怎么重新加载

mysql dml全称中文_MySQL语言分类——DML-程序员宅基地

文章浏览阅读527次。DMLDML的全称是Database management Language,数据库管理语言。主要包括以下操作:insert、delete、update、optimize。本篇对其逐一介绍INSERT数据库表插入数据的方式:1、insert的完整语法:(做项目的过程中将字段名全写上,这样比较容易看懂)单条记录插入语法:insert into table_name (column_name1,......_dml的全称是

【小工匠聊Modbus】04-调试工具-程序员宅基地

文章浏览阅读136次。可以参考: http://git.oschina.net/jrain-group/ 组织下的Java Modbus支持库Modbus-系列文章1、虚拟成对串口(1)下载虚拟串口软件VSPD(可在百度中搜索)image.png(2)打开软件,添加虚拟串口。在设备管理中,看到如下表示添加成功。..._最好用的 modebus调试工具

推荐文章

热门文章

相关标签