购买
下载掌阅APP,畅读海量书库
立即打开
畅读海量书库
扫码下载掌阅APP

1.3 从C++代码的执行过程看编译器支持面向对象语言

大家都知道,Java语言作为面向对象编程语言中的后来者,吸收了其他高级语言的特点,特别是吸收、借鉴了C++的很多特性。JVM作为字节码执行器,在对字节码进行编译和解释时也借鉴了C++编译器的实现。与面向过程的语言不同,面向对象的语言有三大特点:封装、继承和多态。下面从一个具体的实例出发,看一下编译器是如何支持这三大特点的。C++的代码示例如下:

struct CPoint{
    double xAxis;
    double yAxis;
};

class CShape {
private:
    double xAxis;
    double yAxis;
public:
    void setCenter(double xAxis, double yAxis) {
        this->xAxis = xAxis;
        this->yAxis = yAxis;
    }

    void setCenter(CPoint point) {
        this->xAxis = point.xAxis;
        this->yAxis = point.yAxis;
    }
    virtual string getType() {
            string s("Unknown");
            return s;
    }
};

class CCircle : CShape {
private:
    double radius;
public:
    virtual string getType() {return string("Circle");}
    void setRadius(double radius) {
        this->radius = radius;
    }
};

注意

C++的语法非常复杂,有静态成员函数、多继承、虚继承、模板等。这里只是为了简单演示编译器如何处理面向对象语言,所以仅仅包含了单继承、函数的重载和重写。

1.3.1 封装支持

封装是面向对象方法的重要原则——把对象的属性和行为(数据操作)结合为一个独立的整体,并尽可能地隐藏对象的内部实现细节,外部只能通过对象的公有成员函数访问对象。编译器对于封装的处理相对来说比较简单,只要确定好怎么处理成员函数和成员变量就能正确地处理类。

编译器对于成员函数的处理方法是把成员函数转化成类似于C语言中的普通函数,转化之后编译器就能像编译C语言的函数一样编译成员函数。转化的规则也非常简单,就是为成员函数增加一个额外的参数。例如我们前面提到的CShape类中有一个成员函数void setCenter(double xAxis, double yAxis),编译器首先对这个函数进行转化,然后再进行编译。转化后的函数形式为void setCenter(CShape * const this, double xAxis, double yAxis),这就解决了成员函数的编译问题。

注意

这也是在面向对象语言的成员函数中可以通过this指针访问对象成员变量的原因。因为每一个this指针实际上指向一个具体的对象,这个对象是成员函数的隐式参数之一。

编译器对成员变量的处理非常简单,直接按照对象的内存布局产生对象即可。比如CPoint类实例化的对象布局如图1-7所示。

图1-7 简单对象的内存布局

另外需要提到的是,编译器按照对象的成员变量组织对象的内存布局,在这个过程中并不关心对象成员变量的修饰符(如private、protected和public)。也就是说,当内存布局组织好以后,编译器无法控制内存的访问,那么private的成员变量可以通过“某些特殊”手段被非本类的成员函数访问。成员变量和成员函数的修饰符的访问规则是编译器在编译过程进行处理,不涉及程序运行时。

因为CShape中存在虚函数,所以编译器在实例化对象的时候会增加一个额外指针的空间用于存储虚函数表的地址。虚函数表中存放的是函数的地址,这个指针的目的是支持多态,下面会详细介绍。CShape类实例化的对象布局如图1-8所示。

图1-8 包含虚函数对象的内存布局

注意

vptr的位置和编译器实现有关,有些编译器将vptr放在对象布局的起始位置,有些则将vptr放在对象内存布局的最后。

1.3.2 继承支持

继承是面向对象最显著的一个特性,继承是从已有的类中派生出新的类,称为子类。子类继承父类的数据属性和行为,并能根据自己的需求扩展出新的行为,提高了代码的复用性。

编译器对于继承的实现也不复杂。还是从两个方面考虑,继承对于成员函数的处理并不影响,也无关成员函数是不是虚函数。对于成员变量的处理,编译器需要把父类的成员变量全部复制到子类中。在上例中,CCircle继承于CShape,CCircle类实例化的对象布局如图1-9所示。

图1-9 对象继承后的内存布局

C++中还支持多继承,如果多个父类都定义了虚函数,即对象布局可能都需要一个vptr,大多数编译器会将多个vptr合并成一个。当然这也与编译器的实现有关,由于这些内容涉及C++编译器的实现细节,且与本书内容关系不密切,因此不再进一步介绍,有兴趣的读者可以参考其他书籍。

1.3.3 多态支持

多态指的是一个接口多种实现,同一接口调用可以根据对象调用不同的实现,产生不同的执行结果。多态有两种形式,一种是静态多态,另一种是动态多态。

静态多态也称为函数重载(overlap)。在早期的C语言中,每个函数的名字都不相同,所以可以直接通过函数名唯一地确定函数。例如,在CShape中有两个函数名字相同的setCenter,所以不能通过函数名来唯一地确定函数。编译器采用的方法是对函数名进行编码(称为name mangling),编码的规则不同,编译器的实现也不同,原则是把函数名、参数个数、参数类型等信息编码成唯一的一个函数名(也称为函数的签名)。在Linux中对上述文件进行编译,然后可以通过nm命令查看编译后的函数签名。可以得到两个不同的函数签名,分别为:

关于Name Mangling的具体编码规则,可以参考其他书籍或文章。

动态多态也称为函数重写(override),该机制主要通过虚函数实现。编译器对于虚函数的实现主要通过增加虚函数指针和虚函数表的方式来实现。编译器会在数据段中增加一个数据空间,称为虚函数表,虚函数表中存放的是编译后函数的地址,同时在类的构造函数中把实例化对象的虚指针指向虚函数表。CShape示例化对象的布局如图1-10所示。

图1-10 CShape示例化对象布局

CCircle示例化对象的布局如图1-11所示。

图1-11 CCirle示例化对象布局

从编译器的角度来看,当CCircle重写了CShape的虚函数(此处为getType),编译器会在CCircle对应的虚函数表中修改函数的地址,此函数的地址为CCircle中函数的地址。若CCircle仅仅继承CShape的虚函数,但并没有重写,则CCircle的虚函数表中函数的地址仍然指向CShape中函数的地址。

另外,在图1-10和图1-11中都指出虚函数表(vtbl)位于数据段中,这样设计主要是因为使用该数据时只需要读权限,而不需要执行权限。但这并不意味着虚函数表会动态地变化,实际上虚函数表在编译时唯一确定,在程序执行过程中并不会变化。

编译器支持封装、继承和多态的特性以后,也会按照与C语言一样的方式生成可执行文件,并且也按照对应的调用约定支持函数调用。 hZ3zosFEpUV5qTbewrlLc8u5Tjj9ivhp3isuigaHznL3cPC/hF/3gOjgLvdLQkX3

点击中间区域
呼出菜单
上一章
目录
下一章
×