Bootstrap

C++初学者指南-3.自定义类型(第一部分)-析构函数

C++初学者指南-3.自定义类型(第一部分)-析构函数

特殊的成员函数

T::T()默认构造函数当创建新的 T 对象时运行。
T::T(param…)特殊构造函数创建带参数的新 T 对象时运行
T::~T()析构函数当现有的 T 对象被销毁时运行

编译器会在我们没有自己定义的情况下生成一个默认构造函数和一个析构函数。
在后面的章节中,我们将了解到四个特殊的成员,可以用来控制类型的复制和移动行为。

  • copy constructor(拷贝构造函数)  T::T(T const&)
  • copy assignment operator(拷贝赋值操作符函数)  T& T::operator = (T const&)
  • move constructor(移动构造函数)  T::T(T &&)
  • move assignment operator (移动赋值操作符函数) T& T::operator = (T &&)

它们通常也是由编译器自动生成的,在许多/大多数情况下不需要用户自定义。

用户定义的构造函数和析构函数

class Point {};
class Test {
  std::vector<Point> w_;
  std::vector<int> v_;
  int i_ = 0;
public:
  Test() { 
    std::cout << "constructor\n"; 
  }
  ~Test() { 
    std::cout << "destructor\n"; 
  }
  // more member functions …
};

运行上面代码

if (…) {
  …
  Test x;  // prints 'constructor'
  …
}  // prints 'destructor'

销毁时执行顺序
在析构函数体运行完毕后,所有数据成员的析构函数将按照声明的相反顺序执行。这是自动发生的,不能更改(至少不容易改 - 毕竟这是C++,几乎有可以绕过任何事情的方法)。

x 超出作用域范围→执行 ~Test():

  • std::cout << “destructor\n”;
  • x的数据成员被销毁了:
    • i_ 被销毁了(基本类型没有析构函数)
    • v_ 被销毁 → 执行析构函数 ~vector():
      • vector在其缓冲区中销毁整数元素;(基本类型→没有析构函数)
      • 释放堆上的缓冲区内存
      • v_的剩余数据成员已被销毁
    • w_ 被销毁 → 执行析构函数 ~vector():
      • vector在其缓冲区中销毁Point元素
      • 每个~Point()析构函数都会被执行
      • 释放堆上的缓冲区内存
      • w_的剩余数据成员被销毁

RAII

“资源获取即初始化”

  • 对象构建:获取资源
  • 对象销毁:释放资源

示例:std::vector

  • 每个向量对象都拥有一个独立的堆上缓冲区,在那里存储着实际内容。
  • 该缓冲区是根据需要分配的,并且在向量对象被销毁时被释放。
    在这里插入图片描述

所有权
如果一个对象负责资源的生命周期(初始化/创建、终结/销毁),我们就说它是资源(内存、文件句柄、连接、线程、锁……)的所有者。

提醒:C++ 使用值语义
= 变量指向对象本身,而不仅仅是引用/指针。
这是几乎所有编程语言中基本类型(int、double等)的默认行为,也是C++中用户自定义类型的默认行为:

  • 深拷贝:生成一个新的、独立的对象;对象(成员)的值被复制
  • 深层赋值:使目标的值等于源对象的值
  • 深层所有权:成员变量指向与包含对象具有相同生命周期的对象
  • 基于值的比较:如果它们的数值相等/较小,则变量进行相等/小于/… 的比较。

由于成员的生命周期与其包含的对象绑定在一起,所以不需要垃圾回收器。

示例:资源处理

常见情况
我们需要使用一个外部的 © 库,它具有自己的资源管理。这些资源可以是内存,还可以是设备、网络连接、已打开的文件等。
在这样的库中,资源通常是通过初始化和清理函数来处理的,比如 lib_init() 和 lib_finalize() ,用户需要调用这些函数。
问题:资源泄漏
通常在程序庞大且控制流复杂时,经常会忘记调用最终清理函数。这可能导致设备卡住,内存未被释放等问题。
解决方案:RAII 包装器

  • 在构造函数中调用初始化函数
  • 在析构函数中调用清理函数
  • 额外优势:包装类还可以用来存储上下文信息,如连接详情,设备ID等,这些只在初始化和结束之间有效
  • 这样的包装器大多数情况下应该是不可复制的,因为它处理着独特的资源(在后面的章节中会有更详细的解释)
#include <gpulib.h>

class GPUContext {
  int gpuid_;
public:
  explicit
  GPUContext (int gpuid = 0): gpuid_{gpuid} {
    gpulib_init(gpuid_);
  }
  ~GPUContext () {
    gpulib_finalize(gpuid_);
  }
  [[nodiscard]] int gpu_id () const noexcept { 
    return gpuid_;
  }
// make non-copyable:
  GPUContext (GPUContext const&) = delete;
  GPUContext& operator = (GPUContext const&) = delete;
};

int main () {if () {
     // 创建和初始化上下文
    GPUContext gpu;
     // 在这里处理事情} // 自动清理释放!}

示例:RAII记录

  • Device的构造函数获得一个指向UsageLog对象的指针
  • UsageLog 可以用来记录 Device 对象生命周期中的操作
  • 如果Device不再存在,析构函数会通知UsageLog
  • UsageLog 还可以统计活跃设备的数量等等
class File {};
class DeviceID {};

class UsageLog {
public:
  explicit UsageLog (File const&);void armed (DeviceID);
  void disarmed (DeviceID);
  void fired (DeviceID);
};

class Device {
  DeviceID id_;
  UsageLog* log_;public:
  explicit
  Device (DeviceId id, UsageLog* log = nullptr): 
    id_{id}, log_{log},{ 
    if (log_) log_->armed(id_);
  }
  ~Device () { if (log_) log_->disarmed(id_); }
  void fire () {if (log_) log_->fired(id_);
  }};

int main () {
  File file {"log.txt"}
  UsageLog log {file};
  …
  Device d1 {DeviceID{1}, &log};
  d1.fire(); 
  {
    Device d2 {DeviceID{2}, &log};
    d2.fire(); 
  }
  d1.fire(); 
}
log.txt
device 1   armed
device 1   fired
device 2   armed
device 2   fired
device 2   disarmed
device 1   fired
device 1   disarmed

零规则

= 尽量不要编写特殊成员函数

除非你需要进行 RAII 风格的资源管理或基于生命周期的跟踪,否则请避免编写特殊成员函数。
大多数情况下,编译器生成的默认构造函数和析构函数已经足够了。

初始化并不总是需要编写构造函数。
大多数数据成员可以使用成员初始化器进行初始化。

不要为类型添加空析构函数!
用户自定义析构函数的存在会阻止许多优化,并严重影响性能!

你几乎不需要写析构函数。
在C++11之前,使用自定义类并进行显式手动内存管理是非常常见的。然而,在现代C++中,内存管理策略大多数情况下(也应该)封装在专用类(容器,智能指针,分配器等)中。

附上原文地址
如果文章对您有用,请随手点个赞,谢谢!^_^

悦读

道可道,非常道;名可名,非常名。 无名,天地之始,有名,万物之母。 故常无欲,以观其妙,常有欲,以观其徼。 此两者,同出而异名,同谓之玄,玄之又玄,众妙之门。

;