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

本篇博客是做了网上的一个题目,然后总结而来的,题目并不难,关键是你能否真正理解其中的过程。
 
解题思路:
 
(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 Apublic 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类型的成员,我们知道虽然typeclass在现代面向对象语言中得到同意,但是在C++中,Type就是type,没有什么构造函数的概念。这个时候,分配的空间,进行了初始化。这个时候B构造函数的初始化列表结束(其实只有一个),下一步是该构造函数的函数体,也就是输出“structor  B”这句话。

就这样,程序会先将“structor B”输出来。

执行完B构造函数后,系统控制回到A的构造函数(跟普通的函数调用过程一样),这个时候,需要执行下一个空间分配和初始化过程,也就是a5)。执行完后,同样A的构造函数的初始化列表也执行完了,下一步就是A构造函数的函数体。

于是,程序接着输出了“ structor A”。

这给人的感觉就好像是先调用了构筑函数B,然后调用构造函数A,其实我认为,这种说法给那些初学者还算可以,如果想要深入了解,还是不要这么简单的像好。

 

 

 

 

有了上面的思想,我们分析所有的继承体系构造过程,就容易了,不过。只是当遇到了virtual继承的时候,问题就不同了。Virtual就像是一个捣乱者,它放在函数前面,让函数成为虚函数,导致我们常理分析的东西不再合理了(多态性)。它放在继承基类前面,就成了虚继承。

虚继承的引入其实就是为了解决C++特有的多重继承,所带来的麻烦。也就是我们通常所说的水晶型问题。B,C都继承了A,然后,D继承BC。这样按照前面的“常理”推论,D的实例化中应该有两个A对象空间。这是不合理的。

其实,多重继承,本来就有点不合理(所以javaC#都放弃了这种机制),但是C++有这种机制后所出现的这种问题,根本原因就是这种多继承不太符合自然界的规律。虚继承为了解决这个问题而出现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值