#include <iostream>
using namespace std;
class B
{
};
class V:public B
{
};
class X:virtual public V
{
};
class Y:virtual public V
{
};
class W:public X,public Y
{
};
class Z:public W
{
};
int main()
{
cout<<obj.b<<obj.v<<obj.x<<obj.y<<obj.w<<obj.z<<endl;
}
实际结果
Structor B
Structor V
Structor X
Structor Y
Structor W
Structor Z
673521
随想(甚看)
通常,许多课本会告诉你先执行基类的构造函数在执行本类的构造函数。但其实这样解释构造函数的构造过程,是不利于更深刻的理解类实例化过程的。
这里,我们从新分析一下,继承体系中,构造函数的执行过程。
首先,让我们来梳理一下,有关构造函数以及相关的一下基础知识。
1) 事实上,继承作为构造一个新类的方法,如果从实例化类的角度上看,它与类的元素存于同等地位。
class B
{
};
class A:public B
{
}
A类继承了B类,A类中还有一个成员a。我们从实例化A类的角度出发。系统会先给B类实例开辟一个空间。然后,在开辟A类实例对象的空间。A所能控制的领域为B内部的所有东西,以及a元素。
实际上,继承和组合是构建类的两种基本方法。我们前面说过。当你想要新建一个类的时候,你的目的是为了将来建立对应的对象。你希望这个对象中有哪些东西,你就会往你现在正在建立的类中加。你有两种选择来丰富你的类。
1, 以成员的方式,这是最基本的方式,当某个类被以成员的方式加入,那么它自成一个小空间。不管他是复杂类型还是基本类型。它有自己的名字(成员名),
2, 以基类的方式。这是一个往往被我们误解的方式,而实际上,它仍然是一个丰富现有类的方法。与成员的方式不同的是,进入你的类之后,会与你的类合并,而不是从属于你的一个项,原则上,它没有自己的名字,它为你带来的就是它的成员。
这两种方法的不同就是我们通常所说的“is ”和“has”的区别。
那么从实例化空间来讲两种方式基本没有什么区别。
2) 构造函数,再怎么特殊,它仍然是函数,虽然它的形式,它的执行过程,它的调用方法,与普通的函数都有不同,但是一句话,它仍然是一个函数。
函数的要素构造函数都有
1, 名称。特殊在它的名称就是类的名称,这个不是乱规定的,而是有原因的,因为我们实例化类的方法是使用类名(或者是对象名),后面加上一个参数列表,这是不是很像一个函数调用过程。对了,这就是一个函数调用过程,调用的就是构造函数。构造函数返回时什么,是该类的对象。
2, 参数列表。这个参数列表很容易理解。
3, 函数体。这也是我们最容易误解的地方,事实上,构造函数中最不重要的就是函数体。
4, 初始化列表。这是构造函数与普通函数最大的区别。普通函数没有这个概念。根据构造函数的功能(建立对象),那么构造函数就需要实现空间分配、数据初始化两个基本功能。初始化列表完成了这个功能。
3) 初始化列表。初始化列表是构造函数固有的东西,如果你不写,它会默认自己添加。基本的结构就是:
类名(参数列表):初始化列表{ 函数体}
不管你写不写,怎么写,什么顺序,初始化列表的完整就是
基类1名(参数列表),基类2名(参数列表),成员名(参数列表),成员名(参数列表)
从上面也可以看出,基类和成员的关系了。
我们还可以看出。初始化列表中的元素,基本上就是调用了他们的构造函数。所以如果那些类没有对应的构造函数,就会报错。通常不会出现这种问题,主要的原因是,通常类都有默认构造函数(没有参数的构造函数)
初始化列表的元素的顺序无所谓,但是,执行初始化的顺序却是一定的,先基类(从做到又),然后成员(从上到下)
4) 我们所这么多什么用呢?
#include <iostream>
#include <string>
using namespace std;
class B
{
};
class A:public B
{
};
int main()
{
}
大家都知道执行结果为:
structor B
structor A
5 4
那么构造函数的执行过程,真的就可以用这种输出的方式来表达吗?不对,事实上,应该是A的构造函数先执行,那么为什么又是先输出structor B呢?
其实,这就是有关前面我们所说的,构造函数的特殊性,构造函数,它是一种用于创建并初始化对象的函数,它的主体功能不是函数体,而是初始化列表。
当执行A a(4,5)语句时,我们知道,这是一个标准的直接初始化方法,调用普通的构造函数。还有一种初始化叫着赋值初始化,它与赋值区别很大,但仍然是调用的对应的构造函数。注意,赋值初始化的唯一特殊就是它有一个特殊调用模式就是使用=号。
A a(4,5)调用的构造函数只是A中定义的那个构造函数。这个构造函数执行的过程是:
调用初始化列表进行,构造基类和成员对象,顺序前面已经说过来。
我们在来看看初始化类表的结构。对于基类,我们发现它什么使用的是直接构造函数的方法,而成员也是,所不同的是他们一个使用的是类名,一个使用的是变量名。
不过这两种方法,其实都是一样的,都是直接初始化方法。
看到没有,对于A a(4,5)这个过程,下一步要做的就是,调用B(4)这个构造函数,这个时候其实就是进入了B类对象的构建过程。嵌套的往里面走(我们知道,它必须是深度优先),到了底层,我么所说的底层,就是那些只有成员,没有基类,并且成员属于基本数据类型。对于我们现在这个例子,就是B这个类。B只有b这个int类型的成员,我们知道虽然type和class在现代面向对象语言中得到同意,但是在C++中,Type就是type,没有什么构造函数的概念。这个时候,分配的空间,进行了初始化。这个时候B构造函数的初始化列表结束(其实只有一个),下一步是该构造函数的函数体,也就是输出“structor
就这样,程序会先将“structor B”输出来。
执行完B构造函数后,系统控制回到A的构造函数(跟普通的函数调用过程一样),这个时候,需要执行下一个空间分配和初始化过程,也就是a(5)。执行完后,同样A的构造函数的初始化列表也执行完了,下一步就是A构造函数的函数体。
于是,程序接着输出了“ structor A”。
这给人的感觉就好像是先调用了构筑函数B,然后调用构造函数A,其实我认为,这种说法给那些初学者还算可以,如果想要深入了解,还是不要这么简单的像好。
有了上面的思想,我们分析所有的继承体系构造过程,就容易了,不过。只是当遇到了virtual继承的时候,问题就不同了。Virtual就像是一个捣乱者,它放在函数前面,让函数成为虚函数,导致我们常理分析的东西不再合理了(多态性)。它放在继承基类前面,就成了虚继承。
虚继承的引入其实就是为了解决C++特有的多重继承,所带来的麻烦。也就是我们通常所说的水晶型问题。B,C都继承了A,然后,D继承B,C。这样按照前面的“常理”推论,D的实例化中应该有两个A对象空间。这是不合理的。
其实,多重继承,本来就有点不合理(所以java和C#都放弃了这种机制),但是C++有这种机制后所出现的这种问题,根本原因就是这种多继承不太符合自然界的规律。虚继承为了解决这个问题而出现。