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类,就不需要设置为虚析构函数。

相关文章
|
26天前
|
存储 编译器 数据安全/隐私保护
【C++】多态
多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
32 1
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
47 2
C++入门12——详解多态1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
87 1
|
4月前
|
存储 编译器 C++
|
4月前
|
编译器 C++
C++的基类和派生类构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。 在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private 属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。 这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。 下面的例子展示了如何在派生类的构造函数中调用基类的构造函数:
|
5月前
|
机器学习/深度学习 PyTorch 算法框架/工具
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
C++多态崩溃问题之在PyTorch中,如何定义一个简单的线性回归模型
|
24天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
38 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
83 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
80 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
86 4