C++中为多态基类声明虚析构函数

简介: C++中为多态基类声明虚析构函数

1.何谓析构函数?


它是用来释放对象所占用的资源。当对象的使用周期结束后(例如:当某对象的范围结束时或动态分配的对象被delete关键字销毁时),对象的析构函数会被自动调用,对象所占用的资源就会被释放。像文章C++类中默认生成的函数中所述,假如在你的类中不声明析构函数,编译器也会为你自动生成一个。


2.何谓多态基类?


多态(polymorphism)是C++面向对象的基本思想(封装,继承,多态)之一。如果我们希望仅仅通过基类指针就能操作它所有的子类对象,这时候就需要用到多态啦。如下例所示:


1#include <iostream>
 2
 3// 多态基类
 4class TimeKeeper{
 5public:
 6    TimeKeeper();
 7    ~TimeKeeper();
 8    // ...
 9};
10
11// 子类
12class AtomicClok: public TimeKeeper{  // 原子钟
13    // ...
14};
15
16class WaterClock: public TimeKeeper{   // 水钟
17    // ...
18};
19
20class WristWatch: public TimeKeeper{  // 手表
21    // ...
22};
23
24
25TimeKeeper* getTimeKeeper(){  // 用来返回一个基类指针
26    // ...
27}
28
29TimeKeeper* ptk = getTimeKeeper();  
30....      // 使用这个基类指针ptk指向它的子类对象
31delete ptk;  // 使用完毕,释放资源


上例中代码存在的问题:getTimeKeeper()返回的指针ptk指向一个子类对象(例如WristWatch),而这个子类对象却通过一个基类指针ptk被删除。同时,基类中的析构函数是non-virtual的。C++中规定:当子类对象通过一个基类指针被删除时,而该基类带有一个非虚的析构函数时,其结果会出现未定义。实际执行过程中,通过发生的情况是子类对象中仅销毁了基类成分,而子类成分并没有被销毁,从而出现一个“局部销毁”对象,造成内存泄漏

解决方法:给基类中的析构函数增加virtual关键字修饰,使其成为虚析构函数。这样子类就允许拥有自己的析构函数,从而保证被占用的所有资源都会被释放。


1#include <iostream>
 2
 3// 多态基类
 4class TimeKeeper{
 5public:
 6    TimeKeeper();
 7    virtual ~TimeKeeper();  // 虚析构函数,注意这句!
 8    // ...
 9};
10
11// 派生类
12class AtomicClok: public TimeKeeper{  // 原子钟
13    // ...
14};
15
16class WaterClock: public TimeKeeper{   // 水钟
17    // ...
18};
19
20class WristWatch: public TimeKeeper{  // 手表
21    // ...
22};
23
24
25TimeKeeper* getTimeKeeper(){  // 用来返回一个动态分配的基类对象
26    // ...
27}
28
29TimeKeeper* ptk = getTimeKeeper();  
30....      // 使用这个指针操作它的子类
31delete ptk;  // 使用完毕,释放资源


像Timekeeper这样的基类中可能会含有除析构函数外的函数,通常还有其他的虚函数。任何类只要带有虚函数都几乎确定应该也有一个虚析构函数

3.不要盲目将析构函数设置为virtual


如果一个类中不含有虚函数,通过表示这个类并不意图被用于一个基类。当类不打算被当作基类时,令其析构函数为virtual往往是一个馊主意。这与虚函数的运行机制有关:如果想实现出虚函数,对象必须携带某些信息,它主要用来在运行期间决定哪一个虚函数应该被调用。这份信息通常由一个vptr(虚函数表指针)指出。vptr指向一个由函数指针构成的数组(即虚函数表vtbl)。每个带有virtual函数的类都有一个对应的虚函数表。当对象调用某个虚函数时,实际被调用的函数取决于该对象的虚函数表指针所指向的那个虚函数表,编译器在其中寻找合适的函数指针。盲目使用虚析构函数很造成问题,如下例所示:


1class Point{
2public:
3    Point(...);
4    ~Point();
5private:
6    int x;
7    int y;
8};


上例中,如果int占用32位,那么Point对象可以放入一个64位缓存器中。甚至,这样一个Point对象可以被当作一个64位的量传给其他语言编写的函数。然而当Point的析构函数是virtual,情况就发生了变化。如果Point类中含有虚函数时,其对象的体积会增加。32位计算机体系结构中将占用64位(为了存放两个int)至96位(两个int加vptr)。在64位计算机体系结构中可能占用64~128位,因为指针在这样的计算机结构中占64位(8字节)。因此,为Point类添加一个vptr会增加其对象大小达50%~100%。Point对象不再能够塞入一个64位的缓存器中,而C++的Point也不再和其他语言内的相同声明有一样的结构(因为其他语言中没有vptr),所以也不可能把它传递到其他语言所写的函数中。


4.即使类完全不带虚函数,也会受虚函数影响


标准的string中不含有虚函数,但有时候我们也会错误地把它当做基类。如下例所示:


1class SpecialString : public std::string{...};      // 某个继承自标准字符串的类,std::string有个非虚析构函数
2
3SpecialString* pss = new SpecialString("Hi");
4std::string* ps;
5...
6ps = pss;
7delete ps;                                          //使用完后从基类删除内存


上面的写法中,同样会导致第2节中所讲的内存泄漏问题,因为标准库的字符串并没有把析构函数定义为虚函数,它们并不是用来拿去继承的,所以不能随便继承,包括STL。虽然C++不像java有final和C#有sealed来阻止某些类被继承的机制,我们也要拒绝这种写法。


5.抽象类与虚析构函数的完美结合


对于抽象类(abstract class),抽象类是包含至少一个纯虚函数的类(pure virtual function),而且它们不能被实例化,只能通过指针来操作,是纯粹被用来当做多态基类的。


它们相比于具体类(concrete class),虽然都可以通过父类指针来操作子类对象,但抽象类有更高一层的抽象,从设计的角度来说,它们能更好的概括某些类的共同特性,比如"狗"相对于"边牧","柴犬","斗牛",把"狗"当做基类显然要比把某个品种当做基类要好。


因为多态基类需要有虚析构函数,抽象类又需要有纯虚函数,那么在抽象类中就要把析构函数声明为纯虚函数。如下例所示:


1class AWSL{
2public:
3  virtual ~AWSL() =0;     // 声明纯虚函数
4};


注意:你必须为这个纯虚的析构函数提供一个定义。这是因为析构函数的运行机制是:最深层的子类中的析构函数最先被调用,然后在再调用基类中的析构函数。因此,编译器会在AWSL的子类的析构函数中创建一个对~AWSL的调用动作,所以必须为这个纯虚的析构函数提供一个定义。否则,链接器会报错。


1AWSL::~AWSL(){}                     // 基类的析构函数要有一个空的定义


6.理性对待虚析构函数


给一个基类的析构函数声明为virtual,这个规则只适用于带有多态性质的基类身上。这样的基类设计出来的目的是为了通过基类中的接口处理子类的对象。并不是所有基类的设计目的都是为了多态。例如文章如果不想使用编译器默认生成的函数,请明确拒绝它!中,它们并非被设计用来通过基类接口处理子类对象的,因此基类中的析构函数不用声明为virtual。


7.总结


(1).用来实现多态的基类应该声明为虚(virtual)的析构函数。如果一个基类中含有虚函数,那它就是被用来实现多态的,就需要有一个虚析构函数。

(2).某些类不是被用来当做基类的,比如std::string和STL,或者某些不是用来实现多态的基类,比如文章如果不想使用编译器默认生成的函数,请明确拒绝它!中的Uncopyable类,就不需要设置为虚析构函数。

相关文章
|
2天前
|
编译器 C++
c++中的多态
c++中的多态
|
3月前
|
存储 编译器 数据安全/隐私保护
【C++】多态
多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
41 1
|
4月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
67 2
C++入门12——详解多态1
|
4月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
101 1
|
6月前
|
存储 编译器 C++
|
2天前
|
编译器 C语言 C++
类和对象的简述(c++篇)
类和对象的简述(c++篇)
|
1月前
|
C++ 芯片
【C++面向对象——类与对象】Computer类(头歌实践教学平台习题)【合集】
声明一个简单的Computer类,含有数据成员芯片(cpu)、内存(ram)、光驱(cdrom)等等,以及两个公有成员函数run、stop。只能在类的内部访问。这是一种数据隐藏的机制,用于保护类的数据不被外部随意修改。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。成员可以在派生类(继承该类的子类)中访问。成员,在类的外部不能直接访问。可以在类的外部直接访问。为了完成本关任务,你需要掌握。
68 19
|
1月前
|
存储 编译器 数据安全/隐私保护
【C++面向对象——类与对象】CPU类(头歌实践教学平台习题)【合集】
声明一个CPU类,包含等级(rank)、频率(frequency)、电压(voltage)等属性,以及两个公有成员函数run、stop。根据提示,在右侧编辑器补充代码,平台会对你编写的代码进行测试。​ 相关知识 类的声明和使用。 类的声明和对象的声明。 构造函数和析构函数的执行。 一、类的声明和使用 1.类的声明基础 在C++中,类是创建对象的蓝图。类的声明定义了类的成员,包括数据成员(变量)和成员函数(方法)。一个简单的类声明示例如下: classMyClass{ public: int
50 13
|
1月前
|
编译器 数据安全/隐私保护 C++
【C++面向对象——继承与派生】派生类的应用(头歌实践教学平台习题)【合集】
本实验旨在学习类的继承关系、不同继承方式下的访问控制及利用虚基类解决二义性问题。主要内容包括: 1. **类的继承关系基础概念**:介绍继承的定义及声明派生类的语法。 2. **不同继承方式下对基类成员的访问控制**:详细说明`public`、`private`和`protected`继承方式对基类成员的访问权限影响。 3. **利用虚基类解决二义性问题**:解释多继承中可能出现的二义性及其解决方案——虚基类。 实验任务要求从`people`类派生出`student`、`teacher`、`graduate`和`TA`类,添加特定属性并测试这些类的功能。最终通过创建教师和助教实例,验证代码
50 5
|
1月前
|
存储 算法 搜索推荐
【C++面向对象——群体类和群体数据的组织】实现含排序功能的数组类(头歌实践教学平台习题)【合集】
1. **相关排序和查找算法的原理**:介绍直接插入排序、直接选择排序、冒泡排序和顺序查找的基本原理及其实现代码。 2. **C++ 类与成员函数的定义**:讲解如何定义`Array`类,包括类的声明和实现,以及成员函数的定义与调用。 3. **数组作为类的成员变量的处理**:探讨内存管理和正确访问数组元素的方法,确保在类中正确使用动态分配的数组。 4. **函数参数传递与返回值处理**:解释排序和查找函数的参数传递方式及返回值处理,确保函数功能正确实现。 通过掌握这些知识,可以顺利地将排序和查找算法封装到`Array`类中,并进行测试验证。编程要求是在右侧编辑器补充代码以实现三种排序算法
40 5