C++-基类的析构函数为什么要加virtual虚析构函数(转)

C++虚析构函数必要性
本文解析了C++中为何基类析构函数应为虚函数,以避免使用基类指针删除派生类对象时导致内存泄漏。通过示例代码说明虚析构函数如何确保正确调用派生类析构函数。

知识背景

         要弄明白这个问题,首先要了解下C++中的动态绑定。 

         关于动态绑定的讲解,请参阅:  C++中的动态类型与动态绑定、虚函数、多态实现

正题

         直接的讲,C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

示例代码讲解

现有Base基类,其析构函数为非虚析构函数。Derived1和Derived2为Base的派生类,这两个派生类中均有以string* 指向存储其name的地址空间,name对象是通过new创建在堆上的对象,因此在析构时,需要显式调用delete删除指针归还内存,否则就会造成内存泄漏。

class Base {
 public:
~Base() {
  cout << "~Base()" << endl;
}
};
复制代码
class Derived1 : public Base {
 public:
  Derived1():name_(new string("NULL")) {}
  Derived1(const string& n):name_(new string(n)) {}

  ~Derived1() {
    delete name_;
    cout << "~Derived1(): name_ has been deleted." << endl;
  }

 private:
  string* name_;
};

class Derived2 : public Base {
 public:
  Derived2():name_(new string("NULL")) {}
  Derived2(const string& n):name_(new string(n)) {}

  ~Derived2() {
    delete name_;
    cout << "~Derived2(): name_ has been deleted." << endl;
  }

 private:
  string* name_;
};
复制代码

我们看下面对其析构情况进行测试:

int main() {
  Derived1* d1 = new Derived1();
  Derived2 d2 = Derived2("Bob");
  delete d1;
  return 0;
}

d1为Derived1类的指针,它指向一个在堆上创建的Derived1的对象;d2为一个在栈上创建的对象。其中d1所指的对象需要我们显式的用delete调用其析构函数;d2对象在其生命周期结束时,系统会自动调用其析构函数。看下其运行结果:

刚才我们说,Base基类的析构函数并不是虚析构函数,现在结果显示,派生类的析构函数被调用了,正常的释放了其申请的内存资源。这两者并不矛盾,因为无论是d1还是d2,两者都属于静态绑定,而且其静态类型恰好都是派生类,因此,在析构的时候,即使基类的析构函数为非虚析构函数,也会调用相应派生类的析构函数。

下面我们来看下,当发生动态绑定时,也就是当用基类指针指向派生类,这时候采用delete显式删除指针所指对象时,如果Base基类的析构函数没有virtual,会发生什么情况?

复制代码
int main() {
  Base* base[2] = {
    new Derived1(),
    new Derived2("Bob")      
  };
  for (int i = 0; i != 2; ++i) {
    delete base[i];    
  }
  return 0;
}
复制代码


        从上面结果我们看到,尽管派生类中定义了析构函数来释放其申请的资源,但是并没有得到调用。原因是基类指针指向了派生类对象,而基类中的析构函数却是非virtual的,之前讲过,虚函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete时候只会调用基类的析构函数,而不会调用派生类的析构函数。这样,在派生类中申请的资源就不会得到释放,就会造成内存泄漏,这是相当危险的:如果系统中有大量的派生类对象被这样创建和销毁,就会有内存不断的泄漏,久而久之,系统就会因为缺少内存而崩溃。

        也就是说,在基类的析构函数为非虚析构函数的时候,并不一定会造成内存泄漏;当派生类对象的析构函数中有内存需要收回,并且在编程过程中采用了基类指针指向派生类对象,如为了实现多态,并且通过基类指针将该对象销毁,这时,就会因为基类的析构函数为非虚析构函数而不触发动态绑定,从而没有调用派生类的析构函数而导致内存泄漏。

        因此,为了防止这种情况下内存泄漏的发生,最好将基类的析构函数写成virtual虚析构函数。

下面把Base基类的析构函数改为虚析构函数:

class Base {
 public:
virtual ~Base() {
  cout << "~Base()" << endl;
}
};

再看下其运行结果:


这样就会实现动态绑定,派生类的析构函数就会得到调用,从而避免了内存泄漏。

 

故: 继承时,要养成的一个好习惯就是,基类析构函数中,加上virtual。

原文地址:http://blog.youkuaiyun.com/iicy266/article/details/11906457

转载于:https://www.cnblogs.com/lemaden/p/10287066.html

<think>好的,我需要回答用户关于C++基类析构函数是否需要virtual关键字的问题。首先,我应该回顾一下C++虚析构函数的作用和必要性。 用户可能已经了解了一些关于继承和内存管理的知识,但可能对虚析构函数的具体应用场景不太清楚。我需要解释为什么基类析构函数需要声明为虚函数,特别是在多态情况下。比如,当通过基类指针删除派生类对象时,如果基类析构函数不是虚的,可能会导致派生类的析构函数不被调用,从而引发内存泄漏。 接下来,我应该考虑用户可能的疑问点。比如,如果基类本身没有其他虚函数,是否还需要虚析构函数?或者,如果用户不打算通过基类指针来删除对象,是否可以省略virtual关键字?这时候需要分情况讨论,说明不同场景下的最佳实践。 另外,我需要提到C++11之后的override和final关键字,虽然这可能超出用户当前的问题范围,但简要提及可以增回答的全面性。同时,需要强调将析构函数声明为虚函数可能带来的开销,比如虚表指针的增,但对于现代编译器来说,这种开销通常是可接受的。 可能还需要举例说明两种情况:基类析构函数是虚的和非虚的,分别会导致什么结果。例如,当基类析构函数不是虚的,使用基类指针删除派生类对象时,只会调用基类析构函数,而不会调用派生类的,从而导致资源泄漏。而声明为虚析构函数后,会正确调用派生类和基类析构函数。 最后,总结何时必须使用虚析构函数,何时可以不用,以及相关的编程准则,如“如果一个类会被继承,并且可能通过基类指针来删除对象,那么基类析构函数应该声明为虚的”。同时,可以提到C++核心指南中的相关建议,增强权威性。 在组织回答结构时,应该先明确回答“需要”还是“不需要”,然后分点详细解释原因,举例说明,最后总结最佳实践。这样用户能快速获取关键信息,同时有详细的解释作为支持。</think>在C++中,**基类析构函数是否需要声明为`virtual`,取决于是否涉及多态销毁操作**。以下是关键要点和具体分析: --- ### 一、核心规则 **若要通过基类指针/引用删除派生类对象,基类析构函数必须声明为`virtual`** 否则会导致派生类的析构函数无法被调用,引发资源泄漏。 #### 示例验证 ```cpp class Base { public: // 非虚析构函数 ~Base() { std::cout << "Base destroyed\n"; } }; class Derived : public Base { public: ~Derived() { std::cout << "Derived destroyed\n"; } // 不会被调用 }; int main() { Base* ptr = new Derived(); delete ptr; // 仅输出 "Base destroyed" } ``` **结果**:派生类的析构函数未执行,可能导致内存泄漏。 --- ### 二、必须使用虚析构的场景 #### 1. 多态基类 - **基类被设计为需要通过指针操作派生类对象时** - **基类已有其他虚函数**(此时虚析构是自然延伸) #### 正确实现 ```cpp class Base { public: virtual ~Base() { std::cout << "Base destroyed\n"; } // 虚析构 virtual void foo() {} // 其他虚函数 }; class Derived : public Base { public: ~Derived() override { std::cout << "Derived destroyed\n"; } // 正确释放 }; int main() { Base* ptr = new Derived(); delete ptr; // 输出 "Derived destroyed" → "Base destroyed" } ``` --- ### 三、无需虚析构的场景 #### 1. 类不会被继承 - **若类明确标记为`final`**(C++11起) ```cpp class NonInheritable final {}; ``` #### 2. 不通过基类指针操作派生类 - **直接通过派生类类型操作对象** ```cpp Derived d; // 自动调用正确析构 ``` --- ### 四、技术原理 1. **虚函数表(vtable)** 虚析构函数使编译器生成虚析构函数指针,确保通过基类指针调用正确的派生类析构函数。 2. **析构顺序** 虚析构保证析构顺序: **派生类析构 → 基类析构**(与构造函数顺序相反) --- ### 五、最佳实践 1. **遵循C++核心指南** - [C.35: 基类析构函数应设为public且virtual,或protected且非虚](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#c35-a-base-class-destructor-should-be-either-public-and-virtual-or-protected-and-non-virtual) 2. **资源管理建议** -基类析构设为`protected`且非虚,可防止通过基类指针误删对象 ```cpp class SafeBase { protected: ~SafeBase() = default; // 禁止基类指针删除 }; ``` 3. **性能考量** - 虚析构会引入虚表指针(每个对象增一个指针大小),但对现代系统影响可忽略 --- ### 六、典型错误模式 ```cpp // 错误:多态基类未声明虚析构 class Shape { public: ~Shape() {} // 非虚析构 }; class Circle : public Shape { public: ~Circle() { /* 清理圆形资源 */ } }; int main() { Shape* s = new Circle(); delete s; // 仅调用Shape::~Shape() } ``` **后果**:`Circle`的资源未释放,内存泄漏。 --- ### 总结 | 场景 | 是否需虚析构 | 原因 | |--------------------------|--------------|--------------------------| | 多态基类(需通过基类指针删除) | ✅ 必须 | 确保派生类资源释放 | | 非多态基类/不会被继承 | ❌ 不需要 | 避免虚表开销 | | 需防止误删的基类 | 设为protected非虚 | 强制通过派生类析构 |
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值