C++独孤九剑第六式——洞若观火(深入对象操作)

在前面的几式中,虽说我们已经比较详细的探讨了对象的种种操作(构造、复制构造、赋值操作符、析构),但是我感觉还是差了一点深度,所以在这一式中进一步加深,直击对象内部的操作过程。真正做到“知其然,且知其所以然”。

叙述终究是无法面面俱到的,但是我相信,在我们讨论的几种情况的基础上,小伙伴们也能对我们尚未探讨的情况作出正确的判断(*^-^*)

一、对象直接构造

1.无继承简单类

对于类中全部是基本数据类型,且没有虚函数、虚继承的情况,其操作和C语言中的struct是一样的,只需要普通的位操作,即程序的默认内存管理方法就够了。这种情况就不多加说明了。

2.有继承情况下的类

继承体系下的对象构造时,构造函数内部可能含有大量的隐藏代码,因为编译器会扩充每一个构造函数,各自的扩充程度视类的继承情况而定。一般,编译器所做的扩充操作大致如下:

记录在成员初始化列表中的数据成员初始化操作会被放进构造函数中,以成员们的声明顺序为顺序进行操作。

如果有一个成员并没有出现在成员初始化列表中,但它有一个默认构造函数,那么该默认构造函数将被调用(调用的位置依然取决于该成员的声明顺序)。

有如下类代码:

class A
{
public:
	int _a;
	A(int a = 0):_a(a)
	{
		cout<<"In A constructor:"<<this->_a<<endl;
	}
};
class B
{
public:
	A a1;
	A a2;
	B(int b = 1):a2(A(b))//这个函数是重点
	{
		cout<<"In B constructor"<<endl;
	}
};

我们在上面初始化列表中只初始化a2,当然a1也会被调用默认构造函数。此时构造类B的对象输出如下:

a1(默认值为0)的构造依然先于a2执行,最后是用户显示定义的语句。

在那之前(指的是在第步之前,此处开启了有虚函数的情况),如果类对象有虚函数表指针,指针(们)必须被设定初值,指向适当的虚函数表(们)。

在那之前(在第步之前,此处开启了继承的情况,但不包含虚继承),所有上一层的基类构造函数必须被调用,以基类的声明顺序为顺序(与它们在成员初始化列表中的顺序无关):

■如果该基类的初始化被列于成员初始化列表中,那么任何显示指定的参数都应该传递过去。

■如果该基类没有在成员初始化列表中的操作,而它有默认构造函数(编译器合成或显示定义的),那么该函数将被调用。

■如果该基类是多重继承下的第二或后继的基类,那么this指针必须有所调整(因为构造完后,this指针指向的位置与我们要构造的该派生类对象的整体起始位置不符)。

在那之前(在第之前,此处开启了虚继承的情况),所有虚基类的构造函数必须被调用,顺序为从左到右,从最深到最浅:

■如果该类别列于成员初始化列表中,那么如果有任何显示指定的参数,都应该传递过去。若没有列于成员初始化列表中,而该类有一个默认构造函数,它将被调用。

■类中的每一个虚基类部分的偏移位置必须在执行期可被存取。

■在最底层的类中,如果该类的构造函数被调用,用于支持上述行为的操作必须被加进来。

如果我们定义如下的继承链:

class VB1
{
public:
	int vb1;
	VB1(){cout<<"In VB1 constructor"<<endl;}
};
class VB2
{
public:
	int vb2;
	VB2(){cout<<"In VB2 constructor"<<endl;}
};
class Base1
{
public:
	int base1;
	Base1(){cout<<"In Base1 constructor"<<endl;}
	virtual void vfun(){cout<<"In virtual function!"<<endl;}
};
class Base2:public virtual VB1
{
public:
	int base2;
	Base2(){cout<<"In Base2 constructor"<<endl;}
};
class Base3:public virtual VB2
{
public:
	int base3;
	Base3(){cout<<"In Base3 constructor"<<endl;}
};
class Child:public Base1,public Base2,public virtual Base3
{
public:
	Child()
	{
		cout<<"In Child constructor"<<endl;
	}
};

则生成Child类对象的时候,输出如下:

C++独孤九剑第六式——洞若观火(深入对象操作)_第1张图片

将Child类的继承情况改为下面的情况

classChild:public Base1,public virtual Base3,public Base2

则有如下输出:

由上面的结果可以看出:首先虚基类的构造要先于普通基类;虚基类之间是从左到右顺序构造;同一个水平顺序中,当遇到有更深的虚基类时,先构造深层次的虚基类;普通基类也是从左到右,在水平顺序有多个普通基类时,先构造较深层次的基类。

好了,大体上的步骤相信大家都清楚了。下面就具体看一下Child对象的构造函数执行的操作。这里就拿如下的继承情况来描述:

classChild:public Base1,public Base2,public virtual Base3

则其操作如下:

1.  构造VB1部分,此时可以初始化成员变量vb1;

2.  构造VB2部分,此时可以初始化成员变量vb2;

3.  构造Base3部分。设置了Base3部分的虚基类指针(__vbptr_Base3),并可初始化成员变量base3。到此为止虚基类全部构造完毕。

上面3个步骤的内存变化情况如下(下面是已经可以初始化的内存示意图):

C++独孤九剑第六式——洞若观火(深入对象操作)_第2张图片

虚基类表的数据含义详见第一式。上面着色的部分,作为Base3的虚基类部分vb2并没有出现在其最底部。原因是这样安排可以在构造过程中由上而下一气呵成,而不必再在中间执行其它的计算来确定成员的位置,无疑,这样的策略效率会更高(当然这只是我的推测,因为我目前还没有发现那本书讲到这方面的内容,而且不同的编译器也不一定都是这种安排)。

1.  构造Base1部分。首先设置其虚函数指针(__vptr_Base1),然后可执行成员变量base1的初始化。

2.  构造Base2部分。首先设置其虚基类指针(__vbptr_Base2),然后可执行成员变量base2的初始化。

3.  构造Child整体。此处Child并没有新增成员变量,只是重新设置了Base1的虚函数指针和Base2的虚基类指针,使它们指向自己类中的相应数据表。

上面3个步骤的内存变化情况如下:

C++独孤九剑第六式——洞若观火(深入对象操作)_第3张图片

说明:其实在编译的时候就已经确定了该类对象中的各个成员的布局情况。定义类对象时,程序为我们分配合适的内存,而构造函数只是在相应的内存上做设置值的工作。前3个步骤我并没有画出全部成员内存,但其实从一开始它们就已经存在了(可以在基类构造函数中输出某个成员变量的地址,然后和最终形成对象的相应成员地址进行比较,可以发现它们是一样的)。最后__vptr_Child对应的表中中只有一个虚函数;而__vbptr_Child指向的表内容为:(0,8,12,16,0)。

 

二、对象复制构造

若是同一个类的不同对象之间调用复制构造,此时对象中的数据只需按照原来的内容“复制”即可。

关键是当一个基类对象以其派生类对象的内容做初始化时,此时需要保证vptr指针、vbptr指针与虚基类部分内容的正确性。

有虚基类的话,会在合适的位置进行构造,并设置vbptr指针,指向本类的虚基类表。有虚函数的话,会重置vptr,使其指向本类的虚函数表。

如果有两个基类,这两个基类又有共同的虚基类,此时为了保证该虚基类只被构造一次,可以使用一个标识变量,只在某个合适的类中调用该虚基类的构造函数。

当然,这些操作编译器都会帮我们实现,我们只需要放心地使用就行了o( ̄▽ ̄)d

 

三、对象赋值操作

赋值操作和复制构造一直以来进行的工作内容都是大同小异。需要注意的情况也和前面说到的相同,只是这里是调用赋值操作符函数。当然它们本身是有不同之处的,比如复制构造可以有成员初始化列表,而赋值操作没有;赋值操作可以取到函数地址,而复制构造不能。

男人嘛,每个月都有30来天想偷偷懒,我想大家都懂得。


四、对象析构操作

构造不是分配对象内存,析构也不是释放对象内存,它们都是在指定的对象上执行的操作。

例如有如下代码:

	Child *pc;
	{
		Child c;
		c.base1 = 10;
		pc = &c;
	}//此时c对象被析构
	cout<<pc->base1<<endl;//输出依然是10

程序正常运行并且输出的值依然是c对象存在时设置的值。说明对象虽被析构,但是它曾经使用的内存还是可以用的,而且保存的值也不会因为析构而被清除。析构函数的存在,主要还是在必要的时候归还我们从系统中获取的资源,当然得自己显示定义这些操作。






你可能感兴趣的:(C++独孤九剑第六式——洞若观火(深入对象操作))