前言

    前面4节我们已经完成了对4种C++对象布局的分析,本文试图覆盖更多的,常见的C++面向对象的概念。所以,最后2节将继续阐述2个主题:接口和抽象类以及构造函数、虚构函数和虚析构函数。

C++对象的内存分析(5)_主题 接口

    这里我准备只主要阐述接口,而不谈一般的抽象类。因为在C++中,是没有“接口”这种类型的,所有的接口事实上是定义为纯抽象类。所谓纯抽象类,就是没有 成员变量,没有实现了的函数,只有纯虚函数的抽象类。我相信,理解了接口这种特殊的抽象类,再去理解一般的抽象类是很容易的。

    来看一个例子,我们有接口IAnimal来表示一般的动物行为,代码如下:

class IAnimal
{
public:
    virtual void Eat()=0;
    virtual void Move()=0;
    virtual void Sleep()=0;
};

    这个接口在内存中的布局可以看作下图中的样子:

C++对象的内存分析(5)_休闲_02

 

    接口在内存中的布局可以看作一个虚函数指针指向一个虚函数表,虚函数表中的所有元素指向的地址为0,因为所有纯虚函数都没有被实现。

    上面的描述只是为了大家理解方便,事实上这种说法是很不准确的。因为,一个接口是绝对不能被构造的,这一点很重要。接口中的纯虚函数都没有被实现,如果允许构造他们,那么在调用这些方法的时候,将造成非法访问异常。

    我们的问题来了,既然接口不允许被构造,那么我们为什么经常看到各种接口类型的对象呢。很简单,这些接口类型的对象都是由从该接口继承的子类对象通过类型 转换而来的。在子类中,我们需要实现接口的所有方法,否则,该子类仍是一个抽象类。下图中,我们描述了继承了IAnimal接口的Horse类的内存结构 图,这个类还继承了IVehicle接口:

C++对象的内存分析(5)_主题_03

    关于Horse类如果进行指针调整,怎么转换为IAnimal接口类型,并实现多态特性的,我们在前面的章节中不止一次涉及到,即使是接口,也没有什么不同,我们这里不打算再重复。

    下面,我们要谈的是,为什么我们要使用接口呢? 由于本文主要研究的是对象的内存布局,所以,关于这个涉及设计模式的主题,我只准备简单的讲述,这个主题太大了,因此我只希望下面的描述能给你一些关于接 口应用场景的灵感。我们经常听到关于接口的描述是“只要实现了某某接口,对象就能实现某某功能/流程”,或者“就能被某某方法/模块调用来实现某某功能 /流程”。 我们来看下面的代码:

void AnimalHappyDay(IAnimal* animal)
{
    animal->Eat();
    animal->Move();
    animal->Sleep();
}

   类似的,我们要说,任何类只要实现了IAnimal接口,AnimalHappyDay函数就可以调用该类的对象,来实现动物快乐的一天!我们不用关心这 个类是猫,狗还是独角兽,不用关心它是否还实现了其他的接口,不用关心它是直接实现IAnimal接口的,还是间接的。他们只有一个共同点,就是实现了 IAnimal接口。那么,它就可以被AnimalHappyDay函数调用,Eat,Move,Sleep,来实现动物快乐的一天这个功能。

    一个接口,应该是最小粒度的。所谓最小粒度,首先,它没有任何的实现,只是用来描述一种标准的调用规范,完全由子类去实现它。其次,它只包含实现流程需要 的最小的方法集合,比如,我们在IAnimal接口中不应该加入Grow方法,因为AnimalHappyDay流程不需要该方法,如果我们加入Grow 方法,所有继承IAnimal的类就不得不去实现它。应该把Grow方法放在其他接口中,比如ILIfe接口,这个接口中或许定义了Both,Dead方 法,用来实现动物的生命周期,类作者可以选择是否要去实现该接口。