C++ 继承与多态(二)

本文围绕C++中虚函数和多重继承展开。探讨析构、构造、静态、内联函数与虚函数的关系,指出基类指针指向派生类对象时析构函数需为虚函数。介绍多重继承的二义性和菱形继承问题,引出虚继承和虚基类概念,还从内存层面分析虚继承实现原理。

1、析构函数能不能实现成虚函数?

      可以。

    在析构函数中调用virtual虚函数,是动态绑定。

2、不能在构造函数前加virtual,因为只有通过调用构造函数才有对象。

       在构造函数中调用virtual虚函数,不能发生动态绑定。因为动态绑定需要访问对象的前四个字节(即虚函数表的地址),构造函数还没结束的时候对象还没有产生。

3、类的静态成员方法,能不能写成虚函数?    不能,因为静态成员方法的调用不需要对象

4、内联函数能不能写成虚函数?

      老版C++编译器不能写,但是现在的编译器是可以的,但是inline函数写成虚函数之后它自己就变成普通函数无法内联了

5、什么时候,析构函数必须写成虚析构函数?

     当基类指针或引用指向堆上的派生类对象时,如果基类的析构函数不是虚函数,那么代码只执行基类的析构而不执行派生类的析构。
     导致资源泄漏,所以,在这种情况下,需要将基类的析构函数写成虚函数。

6、调用虚函数一定会发生动态绑定吗?

不一定。

如果类中有虚函数,则代码在编译阶段产生虚函数表,并将虚函数表的地址放入类中。普通函数调用都是静态绑定,通过打断点可以看到,call函数名是静态绑定动态绑定call的是寄存器,即虚函数的地址,在运行时才能调用。

对象调用虚函数永远是静态绑定,因为对象的类型已经说明了作用域。
指针或者引用调用虚函数则是动态绑定

7、基类函数必须实现为虚函数的情况:

class Base
{
public:
	Base(int a) :ma(a) 
	{ 
		cout << "Base()" << endl; 
		clear();
	}
	~Base() { cout << "~Base()" << endl; }
	void clear() { memset(this, 0, sizeof(*this)); }
	virtual void show() { cout << "Base::show()" << endl; }
protected:
	int ma;
};
class Derive : public Base
{
public:
	Derive(int data) :Base(data), mb(data) 
	{
		cout << "Derive()" << endl; 
	}
	~Derive() { cout << "~Derive()" << endl; }
	virtual void show() { cout << "Derive::show()" << endl; }
private:
	int mb;
};
int main()
{
	//Base *p = new Base(0);
	//p->show();

	Base *p2 = new Derive(0);  // 问题1:这里为什么没挂掉
	/*
	new的是派生类的对象,但是派生类的对象在构造之前先会构造基类对象,所以此时往派生类的*vfptr写的是基类的虚函数表的地址。
	基类在构造函数中调用clear(),却导致vfptr被清空。返回来再来构造派生类对象时,会将派生类虚函数表的地址写进派生类的*vfptr。
	所以可以执行。
	*/
	p2->show();
	delete p2; //问题2:这里为什么不调用派生类的析构函数
	/*
		当基类指针或引用指向堆上的派生类对象时,如果基类的析构函数不是虚函数,那么代码只执行基类的析构而不执行派生类的析构。
		导致资源泄漏,所以,在这种情况下,需要将基类的析构函数写成虚函数。		
	*/
	return 0;
}

多重继承

之前所写的继承都是单一继承,单一继承是指:一个派生类只继承一个基类。

多重继承指的是一个类可以同时继承多个不同基类的行为和特征功能

class Base1
{
public:
	Base1() { cout << "Base1()" << endl; }
	~Base1() { cout << "~Base1()" << endl; }
};
class Base2
{
public:
	Base2() { cout << "Base2()" << endl; }
	~Base2() { cout << "~Base2()" << endl; }
};

/*
 : 之后称为类派生表,表的顺序决定基类构造函数
   调用的顺序,析构函数的调用顺序正好相反
*/
class Derive : public Base2, public Base1
{};
int main()
{
	Derive derive;
	return 0;
}

根据代码运行结果我们可以看出是先构造Base2,再构造Base1。析构的顺序则相反。

多重继承有好处但也有坏处,会带来各种各样的问题:

  • 二义性问题
  • 菱形继承,导致代码重复

1、二义性问题

使用多重继承很容易导致二义性问题,就是说如果派生类的基类中有同名的成员方法或成员变量,那么在派生类或者主函数中使用派生类的对象来调这个成员方法时,因为不清楚到底要调哪个基类的方法而导致二义性问题。

解决办法之一当然是可以在调用的这个成员方法前加上作用域,之二就是在派生类中重新实现这个成员方法来覆盖掉基类的这个成员方法。

2、菱形继承问题

代码实现:

class A
{
public:
	A(int data = 0) :ma(data) { cout << "A()" << endl; }
	~A(){cout << "~A()" << endl; }
private:
	int ma;
};
class B :public A 
{
public:
	B(int data = 0) :A(data), mb(data) { cout << "B()" << endl; }
	~B(){cout << "~B()" << endl; }
private:
	int mb;
};
class C : public A
{
public:
	C(int data = 0) :A(data), mc(data) { cout << "C()" << endl; }
	~C(){cout << "~C()" << endl; }
private:
	int mc;
};

class D : public B, public C
{
public:
	D(int data=0):B(data), C(data), md(data) { cout << "D()" << endl; }
	~D(){cout << "~D()" << endl; }
private:
	int md;
};

int main()
{
	D d;
	return 0;
}

类继承关系如下:

代码运行结果:

根据类继承关系可知,在D这个派生类中,它继承到了BC两个类的所有成员变量,以至于D的成员变量中有2个ma。从代码运行结果中也可以看到调用了两次类A的构造,也被析构了两次。

这样的结果代码有重复,并不是我们想要的。所以为了解决代码重复,C++中引出了虚继承和虚基类的概念。

派生类继承基类时,在基类继承方式前加virtual,被继承的类叫虚基类,派生类的此种继承方式称为虚继承。虚基类的出现就是为了解决菱形继承下,导致代码继承重复的问题。虚基类使得从多个类(它们的基类相同)派生出的对象只继承一个基类对象

代码改进如下:

class A//成为B和C的虚基类
{
public:
	A(int data = 0) :ma(data) { cout << "A()" << endl; }
	~A(){cout << "~A()" << endl; }
private:
	int ma;
};
class B :virtual public A //虚继承A
{
public:
	B(int data = 0) :A(data), mb(data) { cout << "B()" << endl; }
	~B(){cout << "~B()" << endl; }
private:
	int mb;
};
class C :virtual public A//虚继承A
{
public:
	C(int data = 0) :A(data), mc(data) { cout << "C()" << endl; }
	~C(){cout << "~C()" << endl; }
private:
	int mc;
};

class D : public B, public C
{
public:
	D(int data=0):B(data), C(data), md(data) { cout << "D()" << endl; }
	~D(){cout << "~D()" << endl; }
private:
	int md;
};

int main()
{
	D d;
	return 0;
}

 

根据代码运行结果可以看出,这次只调用了一次类A的构造和析构 。

虚基类和抽象类的区别:

抽象类:有纯虚函数的类称为 “抽象类”,不能实例化对象,只是作为基类为派生类服务,但是可以定义指针或者引用。除非在派生类中完全实现基类中所有的的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象

虚基类:用于多重继承导致代码重复的情况下。这种情况下,虚继承可以使得在派生类中只保留虚基类的一份成员方法或成员变量,解决代码重复的问题。

从内存层面来看虚继承的实现原理

类中有虚函数时,类中会在编译时期自动添加一个虚函数表即vftable来存放虚函数的地址,而这个虚函数表的起址用虚函数指针来存放即vfptr。而虚基类的实现是产生虚基类表指针 vbptr 与虚基类表 vbtable

1、vbptr(virtual base table  pointer)  和 vbtable(virtual table)

虚继承的底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的派生类都会有一个虚基类指针vbptr(占用一个指针的存储空间,4字节)和虚基类表vbtable(不占用类对象的存储空间)。

虚基类依旧会在派生类里面进行拷贝,但是只有一份,虚继承可以使得在派生类中只保留虚基类的一份成员方法或成员变量。

当虚继承的派生类被当做基类继承时,虚基类指针也会被继承。

虚继承的情况下,会在派生类的最开始加vbptr,而把基类继承过来的成员变量放在最后vbptr里存放的是(向上的偏移量(0),向下的偏移量(离虚基类变量的偏移量))。通过偏移地址,这样就可以找到虚基类成员,而虚继承也不用像普通多重继承那样拥有公共基类(虚基类)的2份以上的成员方法或成员变量,从而节省了存储空间。

2、vfptr/vftable、vbptr/vbtable的区别

  • 虚函数的实现和虚基类的实现都是在编译时期在其类内产生一个指针(占用存储空间)和一个相应的表(不占用存储空间)。
  • vftable存储的是类中所有虚函数的地址;vbtable存储的是虚基类相对直接继承类的偏移
  • 虚函数不占用存储空间;虚基类依旧存在继承类中,只占用存储空间
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值