欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

初夏小谈:C++继承(二)之菱形虚拟继承

程序员文章站 2024-03-15 17:32:42
...

子类/派生类的六大默认成员函数如何生成?

(一)在说子类/派生类的成员函数生成之前,都有哪六大成员函数?

即1.负责对象初始化和最后清理的。有构造函数和析构函数。

    2.负责拷贝和复制的。有拷贝构造函数和运算符重载函数。

    3.负责取地址重载的。有针对普通对象的和const对象的。

(二)派生类的成员函数的生成规则:

1.子类/派生类初始化时,必须调父类/基类的构造函数来初始化父类/基类的对象。如果父类/基类没有默认的构造函数,这就需要在子类/派生类的构造函数的初始化列表中进行自定义调用。

说明:什么是默认构造函数:有两种就是编译器自己生成的,再一个就是自定义全缺省的构造函数。其它自定义构造函数都是非默认构造函数。需要在派生类的构造函数初始化列表出进行赋值。

2.如果派生类/子类想要完成父类/基类的拷贝构造时,这就必须在派生类的拷贝构造函数调用基类的拷贝构造函数。

3.如果派生类想要完成基类的operator=。就必须在自己的operator=中进行调用。

4.派生类/子类在最后的清理资源时,在清理完自己所有资源后,会自动调用基类的析构函数,完成基类对象的资源清理工作。为什么在最后调用呢?这是因为有可能在子类清理时会用到基类的对象所以待子类清理完后再去清理基类对象资源。

5.派生类的对象初始化是先调用基类的构造函数再调自己的构造函数。

***经典小题:如何设计一个类使之不能被继承?

有两种方法来解决:第一种是将该类的析构函数设置为私有。即private。则将析构函数私有化。

                                第二种是在该类名的后面加上final,说明该类为最终类不可被继承。这是C++11中新添的。

如果将该类的析构函数私有化后仍旧想实例化该类对象。则在类中给一个共有的静态方法来调用析构函数,给成静态的目的是在类外可以通过作用域+方法的方式调用。如果没有static那么就必须创建好对象才能进行调用。

代码如下:

#include<iostream>
using namespace std;

//设计一个类使得它不能被继承
//两种方法:
//         1.将父类的构造函数给成私有化
//         2.在父类类名后加final表明禁止继承(C++11)
class BASE //final
{
public:
	static BASE GetBASE()
	{
		return BASE();
	}
	void SetInfo(int data)
	{
		_data = data;
	}
	void PrintBASE()
	{
		cout << "BASE::_data = " << _data << endl;
	}
private:
//public:
	BASE()
	{}
private:
	int _data;
};

class CHILD : public BASE
{};

int main()
{
	//CHILD c;
	BASE b = BASE::GetBASE();
	b.SetInfo(6);
	b.PrintBASE();
	//CHILD c;
	system("pause");
	return 0;
}

运行结果:

初夏小谈:C++继承(二)之菱形虚拟继承

6.友元关系不能被继承,就是基类的友元函数不能访问子类的保护成员和私有成员。

7.在基类中定义了static成员,则的子类中无论实例化多少个对象都只有这一个static成员。

(三)菱形继承是什么?

由于菱形继承是多继承和单继承的组合。所以在说菱形继承之前,先来说说多继承。

1.多继承顾名思义就是一个类继承了多个类。就是不同继承体系下的对象模型(对象成员在内存中的布局情况)。需要注意的是这个布局是和继承的类的先后顺序一致。

2.知道了多继承后就来看看菱形继承,顾名思义就是类似于菱形的继承。

*父类A,子类B1,B2均继承于A,而C有继承于B1,B2.的这种继承。就是菱形继承,可以结合下图理解:

初夏小谈:C++继承(二)之菱形虚拟继承

在菱形继承的类中有以下几个问题:

1.在C类的大小是多少?

2.在C类对象中能不能继承A的成员,由于B1,B2均继承,如果C可以继承那么将继承谁的?或者又是什么?

3.如果通过B1,B2设置了A类的成员,那么A类成员中的值到底是谁的?

解决办法:

针对问题一:

1.想要得知C类对象的大小那么必须知道它所继承的所有类的大小。来分析以下:在32位操作系统下A类的大小是4个字节,B1,B2,由于继承了A类所以都是8个字节,而C类继承了B1和B2那么就是2*8,16个字节。再加上自己的4个字节就是20个字节。正因为是20个字节而不是16个字节,就引发了一个问题即下面问题。

针对问题二:

只要继承了的类成员都是public和protected修饰的。都可以再子类中访问。说明C类可以继承A的成员。但是是C对象访问A类对象时访问的是B1,还是B2的呢?这就存在二义性的问题? 这个坑编译器表示我不背。那就报错,你们自己处理。所以要想访问A类的成员,

有两种方法:第一种是C对象想访问A的可访问成员时,需要提供作用域说明是谁的。

                     第二种方法是进行虚拟继承。在后面将详细说明。

两种方法的区别:第一种只是明确了是谁的。但是还是存在两份。而第二种将两者合一,只有一份去除二义性。

针对问题三:

如果在C中访问设置了B1类继承的A类的成员,也在C中访问设置了B2类中继承的A类的成员。这时就必须看。C类继承的顺序,如果C先继承B1类再继承B2类,则A的成员将设置和B1一样,否则和B2一样。

4.菱形继承代码实例:

#include<iostream>
using namespace std;

class A
{
protected:
	int _a;
};

class B1 : public A
{
	void SetAInfo(int a)
	{
		_a = a;
	}
protected:
	int _b1;
};

class B2 : public A
{
public:
	void SetAInfo(int a)
	{
		_a = a;
	}
protected:
	int _b2;
};

class C : public B2, public B1
{
public:
	void SetAInfo(int a1, int a2)
	{
		B1::_a = a1;
		B2::_a = a2;
	}
	void SetAB1Info(int a1)
	{
		B1::_a = a1;
	}
	void SetAB2Info(int a2)
	{
		B2::_a = a2;
	}

	void PrintSizeof()
	{
		cout << "sizeof(C) = " << sizeof(C) << endl;
	}

	void PrintAB1B2a()
	{
		cout << "A::_a = " << A::_a << endl;
		cout << "B1::_a = " << B1::_a << endl;
		cout << "B2::_a = " << B2::_a << endl;
	}
protected:
	int _c;
};

//不能再c中直接去设置a的成员的值,存在二义性,务必要区分是设置B1B2中的哪一个
//方法一:(加类名作用域限定符区分),明确是哪一份(根本上没有解决)只存一份就可以了
//方法二:菱形虚拟继承  --- > 可以解决菱形继承中存在的二义性问题
int main()
{

	C c;
	c.PrintSizeof();
	c.SetAInfo(2, 6);
	//c.SetAB2Info(8);
	//c.SetAB1Info(1);
	//在*父类中的成员变量的值取决于第一个继承的类中设置该成员的值
	c.PrintAB1B2a();
	system("pause");
	return 0;
}

运行结果:

初夏小谈:C++继承(二)之菱形虚拟继承

结果验证上述所说。A的成员_a是B2设置的,因为C继承时是先继承B2,后继承B1的。

(四)什么是虚拟继承?

1.虚拟继承是如何实现?

虚拟继承就是在被继承的类的继承方式前面加上关键字virtual。

2.虚拟继承与普通继承的区别?

1.对象模型倒立(成员变量在内存中基类成员在最下面。)

2.对象中多了四个字节--最上面是编译器自己维护。

3.编译器为派生类生成默认的构造函数--2个参数  空间首地址 1代表虚拟继承的标志

3.剖析虚拟继承的过程

初夏小谈:C++继承(二)之菱形虚拟继承

说明:在B类虚拟继承A类时。此时在B中实例化A类中的成员变量时。在内存中将取B对象的前四个字节内容为地址--->在这个地址上+4取到里面的内容data--->从对象起始地址向后偏移data和字节将1赋值给基类成员_a.B对象前四个字节的内容为地址所映射的就是虚基表,第一个字节存当前类对象的偏移量0个字节。第二个字节是当前对象所继承的基类的成员的位置的偏移量。

(五)虚拟继承解决菱形继承的问题

以上面的菱形继承图为例根本原因是虚拟继承将B1和B2继承的A的成员变成了一份。具体做法是B1对象的前四个字节内容为地址所映射的就是虚基表,前四个字节记录了B1对象所在的偏移量。而后四个字节记录了继承A成员的偏移位置。B2也是一样都将指向同一块基类成员。

代码实例:

#include<iostream>
using namespace std;

class A
{
public:
	void PrintA()
	{
		cout << "&A = " << this << endl;
		cout << "&A::_a = " << &(*this)._a << endl;
	}
public:
	int _a;
};

class B1 :  virtual public A
{
public:
	void PrintB1()
	{
		cout << "&B1 = " << this << endl;
		cout << "&B1::_a = " << &(*this)._a << endl;
	}
public:
	int _b1;
};

class B2 : virtual public A
{
public:
	void PrintB2()
	{
		cout << "&B2 = " << this << endl;
		cout << "&B2::_a = " << &(*this)._a << endl;
	}
public:
	int _b2;
};

class C : public B2, public B1
{
	
public:
	int _c;
};


int main()
{
	C c;
	cout << "sizeof(C) = " << sizeof(C) << endl;  //24
	c._a = 5;
	c._b1 = 6;
	c._b2 = 7;
	c._c = 10;
	c.PrintA();
	c.PrintB1();
	c.PrintB2();

	system("pause");
	return 0;
}

运行结果:

初夏小谈:C++继承(二)之菱形虚拟继承

从结果中可以看到虚拟继承可以将存储的两份基类成员变为一份。A类的成员和B1,B2类所从A继承的成员将会是同一份。

(六)继承与组合比较:

1.public继承是一种is-a的关系。也就是说每个派生类对象都可以看作一个基类对象。

2.组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象,但不能把B看作A的基类对象。

3.能使用组合时,尽量使用组合,不使用继承。

4.继承是更关注里面的实现细节,俗称白箱操作。破坏了类的封装。子类和父类依赖关系很强,耦合度高。

5.组合也是复用的一种手段。需要时拿过来,再加上自己需要的特性形成新的对象。组合是黑箱复用,不用关注里面实现了什么,只要它满足我的需求,就直接拿来使用。组合类之间没有很强的依赖关系,耦合度低。

6.尽量使用组合其代码维护性好,耦合度低,少使用继承。具体适合哪一种就使用哪一种。二者都可以时使用组合。

                                                                                                                                                珍&源码