C++的静态联编和动态联编
最近在看析构函数的内容,看到一些讲的比较好的文章,这里我也有了一些我自己的体会,在这里一并记录一下。
联编是指一个计算机程序自身彼此关联的过程,在这个联编过程中,需要确定程序中的 操作调用(函数调用) 与 执行该操作(函数) 的代码段之间的映射关系。
意思就是这个函数的实现有多种,联编就是把调用和对应的实现进行映射的操作。
按照联编进行的阶段不同,可分为静态联编和动态联编。
静态联编
静态联编工作是在程序编译连接阶段进行的,这种联编又称为早期联编,因为这种联编实在 程序开始运行之前 完成的。在程序编译阶段进行的这种联编在编译时就解决了程序的操作调用与执行该操作代码间的关系。
动态联编
编译程序在编译阶段并不能确切地指导将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切地指导将要调用的函数,要求联编工作在程序运行时进行,这种在 程序运行时进行的 联编工作被称为动态联编。 C++中,动态联编是在虚函数的支持下实现的 。
静态联编和动态联编都是属于多态性的,他们在不同的阶段对不同的实现进行不同的选择。
动态联编需要虚函数的支持,这是因为虚函数的工作原理决定的,而正是因为使用了虚函数来实现动态联编,也让动态联编的效率略低于静态联编。通常,编译器处理虚函数的方法是: 给每个对象添加一个隐藏成员,隐藏成员保存了一个指向函数地址数组的指针 ,这个数组就是虚函数表(virtual function table, vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址,调用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表,如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数,如果使用类声明中的第三个虚函数,程序将使用地址位数组中第三个元素的函数。
虚函数这个概念是C++的精华之一。遇到虚函数时要注意:
定义一个函数为虚函数,不代表函数为不被实现的函数(可以有自己的实现)
定义他位虚函数是为了允许用基类的指针来调用子类的这个函数(提供了基类调用子类函数的方式)
定义一个函数为纯虚函数,才代表函数没有被实现(声明后面接=0 virtual func() = 0 此时派生类必须要实现此虚函数)
具有纯虚函数的类是 抽象类 ,不能用于生成对象(即不能实例化),只能派生,他派生的类如果没有实现纯虚函数,那么他的派生类还是抽象函数。
虚析构函数
虚析构函数顾名思义就是将析构函数定义为虚函数。如果我们在派生中分配了内存空间,但是基类的析构函数不是虚析构函数,就会发生内存泄漏。先看一个例子
#include <iostream> using namespace std; class Base { public: Base(){ data = new char[10];} ~Base(){ cout << "destroying Base data[]\n";delete []data;} private: char *data; }; class Derive: public Base { public: Derive(){ D_data = new char[10];} ~Derive(){ cout << "destroying Derive data[]\n";delete []D_data;} private: char *D_data; }; int main() { Base *basePtr = new Derive(); delete basePtr; return 0; }
输出结果:
$ ./a.out destroying Base data[]
在这个例子中,派生类的析构函数并没有被调用,这在大的项目中就是一个灾难。究其原因是我们在main函数中定义了一个Base的指针,当我们delete一个动态分配的Base指针时,Base指针此时却指向了Derive类型的对象,但编译器还是按照Base类型调用了析构函数,没有执行Derive类型的虚析构函数。修改Base类的析构函数为虚析构函数即可以确保执行正确的析构函数版本。
最后总结一下关于虚函数的一些常见问题:
- 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数,这就是虚函数的基本功能。
- 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还么有构造好,多态此时是被disable的。
- 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。
- 将基类中的一个函数定义为纯虚函数,实际上是将这个类定义位抽象类,不能实例化对象。
- 纯虚函数通常没有定义体,但也可以拥有。(如果Base的析构函数为纯虚函数,那么在类外定义Base::~Base(){…}的方式来定义其定义体)
- 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。
- 非纯的虚函数必须有定义体,不然是一个错误。
- 派生类的override虚函数定义必须和父类完全一致,除了一个特例,如果父类返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。如在Base中定义了virtual Base clone();在Derive中可以定义virtual Derive clone()。