您的位置:首页 > 编程语言 > C语言/C++

C++ 虚基类问题、继承体系中的构造函数执行过程。(

2012-06-29 11:45 387 查看
本篇博客是做了网上的一个题目,然后总结而来的,题目并不难,关键是你能否真正理解其中的过程。

解题思路:

(1)不能简单的认为“先执行基类构造函数,再执行子类构造函数”
(2)更本上讲,继承体系中构造函数的执行过程类似于函数的嵌套执行过程。
(3)构造函数复杂在于,它的执行体分两个部分:初始化列表和函数体。初始化列表负责对基类部分和成员部分进行空间的分配和初始化,其实就是调用他们各自的构造函数;函数体在构造函数中的地位并不高。
(4)单个构造函数执行的过程是:先执行初始化列表,后函数体。初始化执行的顺序以基类先、成员后的顺序,而非他们实际排列顺序。
(5)加入了虚基类的继承体系的构造函数执行过程,有点复杂,但是,理解虚基类提出的目的就是解决C++多重继承而出现的问题(水晶结构继承)就可以很好的理解。
(6)虚基类必须被后面的子孙类重写构造函数,否则报错。虚基类会在它第一次出现的时候初始化,之后不会再初始化。
(7)题中,虽然Z的构造函数总,V(f,g)写在后面,但是它仍然是先被执行的(前面指出,初始化列表的执行顺序与他们被写下的顺序无关)

题目:(以被稍改)
#include <iostream>

using namespace std;

class B

{

public:

B(int i)

{

cout<<"Structor B"<<endl;

b=i;

}

int b;

};

class V:public B

{

public:

V(int i,int j):B(i)

{

cout<<"Structor V"<<endl;

v=j;

}

int v;

};

class X:virtual public V

{

public:

X(int i,int j):V(i,j)

{

cout<<"Structor X"<<endl;

x=j;

}

int x;

};

class Y:virtual public V

{

public:

Y(int i, int j):V(i,j)

{

cout<<"Structor Y"<<endl;

y=j;

}

int y;

};

class W:public X,public Y

{

public:

W(int a,int b,int c, int d, int e, int f):

X(a,b),Y(c,d),V(e,f)

{

cout<<"Structor W"<<endl;

w=a;

}

int w;

};

class Z:public W

{

public:

Z(int a,int b,int c,int d, int e,int f, int g):

W(b,c,d,e,f,g),V(f,g)

{

cout<<"Structor Z"<<endl;

z=a;

}

int z;

};

int main()

{

Z obj(1,2,3,4,5,6,7);

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

{

public:

int b;

};

class A:public B

{

public:

int a;

}

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

{

public:

B(int i):b(i)

{

cout<<"structor B"<<endl;

}

int b;

};

class A:public B

{

public:

A(int i, int j):B(i),a(j)

{

cout<<"structor A"<<endl;

}

int a;

};

int main()

{

A a(4,5);

cout<<a.a<<" "<<a.b<<endl;

}

大家都知道执行结果为:

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
B”这句话。

就这样,程序会先将“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++有这种机制后所出现的这种问题,根本原因就是这种多继承不太符合自然界的规律。虚继承为了解决这个问题而出现。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐