C++之virtual(虚)关键字:虚基类(虚继承);虚函数和纯虚函数

@著作权归作者所有:来自CSDN博客作者大胡子的艾娃的原创作品,如需转载,请注明出处https://blog.csdn.net/qq_43148810,否则将追究法律责任。
如有错误的地方欢迎指正,谢谢!

一、虚基类

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;

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

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

四、补充(二、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行,显然派生类对象中的动态分配的内存没有被释放,导致内存泄漏。所以一般将析构函数声明为虚函数。

更多内容请关注个人博客:https://blog.csdn.net/qq_43148810

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章