先看一段代码:
1 #include<iostream> 2 3 using namespace std; 4 5 class Base{ 6 public: 7 Base() { cout<<"Base Creted"<<endl; } 8 ~Base() { cout<<"Base Destroyed"<<endl; } 9 }; 10 11 class Derived: public Base { 12 public: 13 Derived() { cout<<"Derived Created"<<endl; } 14 ~Derived() { cout<<"Derived Destroyed"<<endl; } 15 }; 16 17 int main() 18 { 19 Base *pB = new Derived(); 20 delete pB; 21 pB = NULL; 22 return 0; 23 }
运行结果如下,情理之中,意料之内:
C++创建对象的时候先创建基类部分,然后创建派生部分。析构的时候要反过来了,先释放子类部分,然后在释放父类部分。但是这里只释放了父类部分,没有释放派生类的部分。为什么呢?
原因很明确:因为之类pB是基类指针,虽然指向的是派生类,只能调用自己的函数,我们是无法通过基类指针调用到子类的成员函数的(除非采用virtual,也就是迟绑定技术)。
虚函数
为了不造成内存泄漏,我们将上面代码改成:
1 class Base{ 2 public: 3 Base() { cout<<"Base Creted"<<endl; } 4 virtual ~Base() { cout<<"Base Destroyed"<<endl; } //virtual关键字很关键哦 5 };
运行结果正确了:
和之前版本相比,基类的析构函数多了一个 virtual 关键字。这样就使得父类类型的指针可以调用子类的成员函数。虚拟函数就是为了对“如果你以一个基础类指针指向一个衍生类对象,那么通过该指针,你只能访问基础类定义的成员函数”这条规则反其道而行之的设计。如果你打算将某个类作为基类,那么一定要定义一个虚析构函数。
虚函数通过动态绑定技术实现了C++的运行时的多态性。让我们可以通过基类的指针或者引用调用派生类的方法。C++中还有一个多态性是编译时的多态,通过模版实现。
《Effective C++》条款 07 p40:为多态基类声明virtual析构函数。
《Effective C++》条款 07 p44:
- 带多态性质的base classes应该声明一个virtual析构函数。如果class带有任何virtual函数,它就应该拥有一个virtual析构函数
- Classes的设计目的如果不是作为base classes 使用,或不是为了具备多态性,就不该声明virtual析构函数。
动态绑定的是怎么实现的?
1、为每个含义虚函数的类创建一个虚函数表VTable,存到常量区,依次存放虚函数的地址。对于每个派生类来说,如果没有重写基类的虚函数,那么派生类的虚函数表中的函数地址还是基类的那个虚函数地址。
2、为每个含有虚函数的对象创建一个指向VTable的指针VPtr,所以说同类对象的VPtr是一样的。
3、当基类指针指向派生类时,放生了强制转换,基类的指针指向了派生类的VPtr,这样当pBase->func()时,就可以调用派生类的func()了。
4、没有虚函数的类也就没有VTable表了,或者这个表为空。这样基类指针自然调用不到派生类的函数了。
思考:
为何要让父类指针指向派生类对象?
我觉得是为了实现C++的多态性。比如简单工厂模式,一开始我们并不知道需要生产哪种产品,也就不知道返回的是什么类型的产品,此时只能用基类指针去指向返回值。除此之外还没想到其他的使用场景。