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

目录
打赏
0
0
0
0
6
分享
相关文章
【c++】多态(多态的概念及实现、虚函数重写、纯虚函数和抽象类、虚函数表、多态的实现过程)
本文介绍了面向对象编程中的多态特性,涵盖其概念、实现条件及原理。多态指“一个接口,多种实现”,通过基类指针或引用来调用不同派生类的重写虚函数,实现运行时多态。文中详细解释了虚函数、虚函数表(vtable)、纯虚函数与抽象类的概念,并通过代码示例展示了多态的具体应用。此外,还讨论了动态绑定和静态绑定的区别,帮助读者深入理解多态机制。最后总结了多态在编程中的重要性和应用场景。 文章结构清晰,从基础到深入,适合初学者和有一定基础的开发者学习。如果你觉得内容有帮助,请点赞支持。 ❤❤❤
198 0
【C++】多态
多态是面向对象编程中的重要特性,允许通过基类引用调用派生类的具体方法,实现代码的灵活性和扩展性。其核心机制包括虚函数、动态绑定及继承。通过声明虚函数并让派生类重写这些函数,可以在运行时决定具体调用哪个版本的方法。此外,多态还涉及虚函数表(vtable)的使用,其中存储了虚函数的指针,确保调用正确的实现。为了防止资源泄露,基类的析构函数应声明为虚函数。多态的底层实现涉及对象内部的虚函数表指针,指向特定于类的虚函数表,支持动态方法解析。
63 1
|
6月前
|
C++入门12——详解多态1
C++入门12——详解多态1
86 2
C++入门12——详解多态1
|
6月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
117 1
【C++篇】深度解析类与对象(下)
在上一篇博客中,我们学习了C++的基础类与对象概念,包括类的定义、对象的使用和构造函数的作用。在这一篇,我们将深入探讨C++类的一些重要特性,如构造函数的高级用法、类型转换、static成员、友元、内部类、匿名对象,以及对象拷贝优化等。这些内容可以帮助你更好地理解和应用面向对象编程的核心理念,提升代码的健壮性、灵活性和可维护性。
【c++11】c++11新特性(上)(列表初始化、右值引用和移动语义、类的新默认成员函数、lambda表达式)
C++11为C++带来了革命性变化,引入了列表初始化、右值引用、移动语义、类的新默认成员函数和lambda表达式等特性。列表初始化统一了对象初始化方式,initializer_list简化了容器多元素初始化;右值引用和移动语义优化了资源管理,减少拷贝开销;类新增移动构造和移动赋值函数提升性能;lambda表达式提供匿名函数对象,增强代码简洁性和灵活性。这些特性共同推动了现代C++编程的发展,提升了开发效率与程序性能。
40 12
【C++进阶】特殊类设计 && 单例模式
通过对特殊类设计和单例模式的深入探讨,我们可以更好地设计和实现复杂的C++程序。特殊类设计提高了代码的安全性和可维护性,而单例模式则确保类的唯一实例性和全局访问性。理解并掌握这些高级设计技巧,对于提升C++编程水平至关重要。
49 16
类和对象(中 )C++
本文详细讲解了C++中的默认成员函数,包括构造函数、析构函数、拷贝构造函数、赋值运算符重载和取地址运算符重载等内容。重点分析了各函数的特点、使用场景及相互关系,如构造函数的主要任务是初始化对象,而非创建空间;析构函数用于清理资源;拷贝构造与赋值运算符的区别在于前者用于创建新对象,后者用于已存在的对象赋值。同时,文章还探讨了运算符重载的规则及其应用场景,并通过实例加深理解。最后强调,若类中存在资源管理,需显式定义拷贝构造和赋值运算符以避免浅拷贝问题。
类和对象(上)(C++)
本篇内容主要讲解了C++中类的相关知识,包括类的定义、实例化及this指针的作用。详细说明了类的定义格式、成员函数默认为inline、访问限定符(public、protected、private)的使用规则,以及class与struct的区别。同时分析了类实例化的概念,对象大小的计算规则和内存对齐原则。最后介绍了this指针的工作机制,解释了成员函数如何通过隐含的this指针区分不同对象的数据。这些知识点帮助我们更好地理解C++中类的封装性和对象的实现原理。

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等