HOWTO · C++

在 C++ 显式调用析构函数

本文演示了如何在 C++ 中显式调用析构函数

本页内容

在 C++ 中,使用 obj.~ClassName() 可以显式调用对象的析构函数。这样会结束该对象的生命周期;除非在其存储空间中创建了新对象,否则之后不要再使用它。手动调用析构函数并不常见;通常让对象随作用域结束而自动清理,或使用 std::unique_ptr 这样的所有权类型,会更安全。

使用 obj.~ClassName() 表示法显式调用析构函数

对象的生命周期结束时,析构函数会释放该对象持有的资源。自动存储期对象离开作用域时,编译器会自动调用它的析构函数。对于由 new 返回的指针,delete 会先调用析构函数,再释放对象的存储空间。不要在这些常规清理流程之外再手动调用一次:如果显式销毁一个自动存储期对象,之后又让它离开作用域,就会再次尝试销毁一个生命周期已经结束的对象。这属于未定义行为,不能保证会打印两条消息。同样,手动销毁由 new 创建的对象后,又对同一个指针使用 delete,也不是有效的清理顺序。

在精心管理的底层代码中,显式调用析构函数可能有用,例如对象位于调用者提供的存储空间中,而程序准备在该位置构造另一个对象。下面的示例使用 placement new 在字节缓冲区中开始一个对象的生命周期,调用它的析构函数,然后在同一地址构造替代对象。最后一次显式调用会结束替代对象的生命周期;字节缓冲区本身不会自动销毁其中存放的对象。

调用时,先写对象表达式,再写 .~ 和类名:如果对象名为 obj,类名为 Widget,形式就是 obj.~Widget()。析构函数没有返回值,也不接受参数。它仍受访问控制规则约束,因此无权访问该函数的代码不能调用私有析构函数。调用析构函数会执行清理工作,但不会释放通过分配器或 new 获得的存储空间;管理存储空间是另一项责任。

在示例中,alignas(Widget) 让字节缓冲区满足 Widget 所需的对齐要求,而缓冲区的大小足以容纳一个实例。placement new 会在这个已有缓冲区中构造对象,而不会另行申请存储空间。第一次显式调用会在构造替代对象之前结束第一个实例的生命周期,之后替代对象也会被显式销毁。由于存储空间是局部字节数组,离开作用域时结束的只是数组本身的生命周期;它不会为放置其中的 Widget 调用析构函数。使用这种模式时,程序员必须跟踪该存储空间中当前存活的对象,并确保每个已构造对象恰好销毁一次。

对于一般应用代码,使用局部变量更简单:控制流离开代码块时(包括函数返回时),它的析构函数就会运行。如果需要动态管理所有权,std::unique_ptr<Widget> 是合适的选择,因为所有者的生命周期会带动对象销毁。当对象属于容器或另一个类时,应让所有者管理销毁,而不要手动调用成员对象的析构函数。这样,清理方式会体现在程序结构中,也能避免在对象生命周期结束后继续使用它。

运行此示例:

#include <iostream>
#include <new>
#include <string>
#include <utility>

class Widget {
 public:
  explicit Widget(std::string name) : name_(std::move(name)) {
    std::cout << "created " << name_ << '\n';
  }

  ~Widget() { std::cout << "destroyed " << name_ << '\n'; }

 private:
  std::string name_;
};

int main() {
  alignas(Widget) unsigned char storage[sizeof(Widget)];
  Widget* obj = ::new (static_cast<void*>(storage)) Widget("first");
  obj->~Widget();
  obj = ::new (static_cast<void*>(storage)) Widget("second");
  obj->~Widget();
}
created first
destroyed first
created second
destroyed second

对于不直接管理存储空间和生命周期的代码,应让局部对象自然地随作用域结束。动态分配对象并需要管理其所有权时,优先使用 std::unique_ptr;所有者离开作用域时,它的析构函数会使用 delete。对象销毁后不要再通过指针访问它;也不要仅为了释放成员资源而手动调用析构函数,应改用合适的标准库所有者类型或资源封装类型。