对象切片

C++支持将对象储存在栈上,但很多情况,对象不能,或不应该存储在栈上。比如:

  • 对象很大
  • 对象的大小在编译时不能确定
  • 对象是函数的返回值,但由于特殊的原因,不应使用对象的值返回

常见情况之一是,在工厂方法或其他面向对象编程的情况下,返回值类型是基类(的指针或引用)
举例:是对工厂方法的简单演示:

enum class shape_type {
  circle,
  triangle,
  rectangle,
  …
};

class shape { … };
class circle : public shape { … };
class triangle : public shape { … };
class rectangle : public shape { … };

shape* create_shape(shape_type type)
{
  …
  switch (type) {
  case shape_type::circle:
    return new circle(…);
  case shape_type::triangle:
    return new triangle(…);
  case shape_type::rectangle:
    return new rectangle(…);
  …
  }
}

这个create_shape方法会返回一个shape对象,对象的实际类型是某个shape的子类,圆/三角形等。在这种情况下,函数的返回值只能是指针或其变体形式。如果返回类型是shape,实际却返回一个circle,编译器不会报错,但结果多半是错的,这种现象就叫做对象切片(object slicing),是C++特有的一种编码错误。这种错误不是语法错误,而是一个对象复制相关的语义错误,也算是C++的一个陷阱,所以要注意这种情况。

那怎样才能确保在使用create_shape的返回值时不发生内存泄漏呢?
答案就在析构函数和它的栈展开行为上。 只需要把这个返回值存放在一个本地变量里,并确保其析构函数会删除该对象即可。
简单的实现如下:

class shape_wrapper {
public:
  explicit shape_wrapper(
    shape* ptr = nullptr)
    : ptr_(ptr) {}
  ~shape_wrapper()
  {
    delete ptr_;
  }
  shape* get() const { return ptr_; }
private:
  shape* ptr_;
};

void foo()
{
  …
  shape_wrapper ptr_wrapper(
    create_shape(…));
  …
}

delete空指针会发生什么?答案就是这是一个合法的空操作。

在new一个对象和delete一个指针时编译器需要干不少的活,大致翻译如下:

// new circle(…)
{
  void* temp = operator new(sizeof(circle));
  try {
    circle* ptr =
      static_cast<circle*>(temp);
    ptr->circle(…);
    return ptr;
  }
  catch (...) {
    operator delete(ptr);
    throw;
  }
}

new的时候会先分配内存(失败时整个操作失败并向外抛出异常,通常是bad_alloc),然后在这个结果指针上构造对象;构造成功后则new操作整体完成,否则释放刚分配的内存并继续向外抛构造函数产生的异常。

if (ptr != nullptr) {
  ptr->~shape();
  operator delete(ptr);
}

delete时先判断指针是否为空,在指针不为空时调用析构函数并释放之前分配的内存。

回到 shape_wrapper 和它的析构行为。在析构函数里做必要的清理工作,这就是 RAII 的基本用法。这种清理并不限于释放内存,也可以是:

  • 关闭文件(fstream的析构就会这么做)
  • 释放同步锁
  • 释放其他重要的系统资源

例如,我们应该使用:

std::mutex mtx;

void some_func()
{
  std::lock_guard<std::mutex> guard(mtx);
  // 做需要同步的工作
}

而不是:

std::mutex mtx;

void some_func()
{
  mtx.lock();
  // 做需要同步的工作……
  // 如果发生异常或提前返回,
  // 下面这句不会自动执行。
  mtx.unlock();
}
posted @ 2022-09-25 10:55  牛犁heart  阅读(172)  评论(0编辑  收藏  举报