什么是多态
多态是 C++ 编程时的一种特性,多态性即是对一个接口的多种实现,多态可以分为 静态多态和动态多态,所谓静态多态就是通过函数重载、模板、强制类型转换实现的, 静态多态是在函数编译阶段就决定调用的机制,即在编译链接截断将函数的入口地址给出,而 动态多态是在程序运行时刻才决定调用机制,而在 C++ 中动态多态是通过虚函数实现的
那么什么又是函数重载、模板、强制类型转换、虚函数呢
【函数重载】所谓函数重载就是一个同名函数可以完成不同的功能,编译系统在编译时期通过函数参数个数、参数类型不同来区分该调用哪一个函数,其实现静态多态
【模板】模板也是静多态的早绑定是因为模板生成代码的代码,模板特例化出函数之后不同的参数类型就形成了函数的重载,而函数的重载就是早绑定的静多态。因此 模板为静多态的实质就是函数重载为静多态
【虚函数】在类中用 virtual 关键字声明的函数叫虚函数
classObject{public:virtualvoidfunc(intx =10) { cout <<"print object x ="<< x << endl; }};classTest:publicObject{private:voidfunc(inty =10) { cout <<"print test y ="<< y << endl; }};intmain(){ Test t1; Object* p = &t1; p->func();return0;}//print Test y = 10classObject{public:voidfunc(intx =10) { cout <<"print object ="<< x << endl; }};classTest:publicObject{private:voidfunc(inty =20) { cout <<"print test y ="<< y << endl; }};intmain(){ Test t1; Object* p = &t1; p->func();return0;}print object x =10
通过上述代码和两种不同形式下的两种结果,我们能够知道当有 virtual 关键字修饰 函数时,函数是呈动态性的,p 是基类类型的指针,现在指向了派生类,但是在派生 类中有两部分,即派生类自己的部分和虚函数部分,此时的 p 是指向派生类中的基类部分, 所以程序在编译期间程序拿到的是基类中的 func 函数的形参 x = 10,但在运行期间由于 virtual 关键字 的存在形成了动多态,即动态绑定,在派生类中对基类的 func 函数进行了隐藏, 如果要调用Object中的fun()的加上作用域,即p->Object::fun() ),但是此时拿到的参数任然为在基类中 拿到的 10,所以打印的结果为:print Test y = 10。
在不加 virtual 关键字的情况下,我们构造 Test 类的对象时,首先要调用 Object 类 的构造函数去构造 Object 类的对象,然后才调用Test类的构造函数完成自身部分的构造, 从而拼接出一个完整的 Object 对象。当我们将 Test 类的对象转换为 Object 类 型时,该对象就被认为是原对象整个内存模型的上半部分,也就是下图中的 “animal 的对象所占 内存”。那么当我们利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的 方法,由于 p 是基类 Object 的指针,所以他调用的是基类中的 fun() 函数。
编- 译器在编译程序的时候,发现类中存在虚函数,此时编译器会为每个包含虚函数的类创建一个虚表(即虚表是属于类的,一个类的 所有对象共享一个虚表),虚表是一个一维数组,在这个数组中存放着每个虚函数的地址,对于上述程序,Object 和 Test 类都包含了一个虚函数 func,因此 编译器会为这两个类创建虚表,(即使派生类没有 virtual 函数,但是其棋类里面有,所以派生类类也就有了)
那么如何定位虚表呢?编译器另外还为每个类的对象提供了一个虚表指针(一个对象包含一个),这个指针指向了对象所属类的虚表,在程序运行时, 根据对象的类型去初始化 vptr,从而让 vptr 正确的指向所属类的虚表,从而在调用虚函数时,就能找到正确的函数,对于上述程序,由于 p 实际指向的类型是 Test,因 此 vptr 指向的 Test 类的虚表,当调用 p->func() 时,根据虚表中函数地址找到的就是 Test 类的 func() 函数,而在虚表中存有的虚函数指针(即 vptr,每个虚 函数有一个),虚函数指针就指向对应的虚函数
正是由于每个对象调用的虚函数都是通过虚表指针来进行索引的,也就决定了虚表指针的正确初始化是非常重要的,换句话说,在虚表指针没有正 确初始化之前,我们不能够去调用虚函数,那么虚表指针在什么时候进行初始化呢,或者它在什么地方进行初始化
在构造函数中进行虚表的创建和虚指针的初始化,在构造子类对象时,要先调用基类的构造函数,此时编译器 只看到了基类,并不知道后面是否还有继承者,它初始化基类对象的虚表指针,该虚表指针指向基类的虚表,当执行 派生类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表,对于程序来说,当 Test 类的对象构造完毕之后,其内部的虚表指针也就被初始化为指向 Test 类的虚表, 在类型转换后,调用 p->func(),由于 p 实际指向的是 Test 类的对象,该对象内部的虚表指针指向的是 Test 类的虚表,因此最终调用的是 Test 类 的 func 函数
对于虚函数的调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表,所以在程序中,不管你的对象类型如何转换,但 该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是 C++ 多态性实现原理
一般情况下,类的多态性是指用虚函数实现的,函数的多态性是用模板和函数重载实现的
为了防止内存泄漏,基类的析构函数必须声明为虚函数
那么,为什么将基类的析构函数声明为虚函数就可以防止内存泄漏?
如果没有将基类的析构函数声明为虚函数,在释放指向派生类对象的基类指针的时候,只会调用基类的析构函数,而派生类的析构函数不会调用,导致属于 派生类新添加的数据不能释放,从而导致内存泄漏
如果将析构函数声明为虚函数,在释放指向派生类对象的基类指针的时候,会调用派生类的析构函数,而派生类的析构函数会自动调用基类的析构函数,从而使放 所有内存,避免内存泄漏
可是这里会有个问题,为什么将基类析构函数声明为虚函数,在释放指向派生类对象的基类指针时,会调用派生类的析构函数,难道派生类的析构函数重写了基类的析构函数吗?函数名不同啊
其实析构函数是一个特殊的函数,编译器在编译时,析构函数的名字同一命名为 destucter
所以只要将基类的析构函数声明为虚函数即可,不管派生类的析构函数前面是否有 virtual 关键字,都构成重写,这也就可以解释为什么将基类析构函数声明为虚函数,释放指向派生类对象的基类 指针时,会调用派生类的析构函数,因为虚表中的函数指针指向的是派生类的析构函数
我们知道,在调用虚函数前,需要先访问虚表指针,得到虚表,然后在执行虚表中对应的虚函数,假设 现在将构造函数声明为虚函数,调用构造函数前,发现构造函数是一个虚函数,然后去访问虚表指针,可是虚表指针是在构造函数中初始化的,而目前构造函数 还未执行,也就是说,虚表指针还没有初始化,只是一个空值,理所当然,这便找不到构造函数的函数指针,因此无法完成构造任务,所以说,构造函数声明为虚函数是很愚蠢的