初始化 `int * p = malloc(1000);` 也可以用 RAII 风格处理吗?

Can initialization `int * p = malloc(1000);` also be dealt in RAII style?

我从 RAII 中了解到,每当您需要使用 new 等手动分配内存时,您也需要释放它。因此,与其手动释放它,不如使用构造函数和析构函数创建 类 来完成这项工作。

那么,下面的人在谈论什么?

发件人:The meaning of the term - Resource Acquisition Is Initialization

The problem is that int * p = malloc(1000); is also initialization of an (integer) object, but it's not the kind of initialization we mean in the context of RAII. ...

@Fred: Indeed. int* is not a RAII type because it doesn't do cleanup. So it's not what RAII means, even though it is what RAII literally says.

嗯,我知道malloc用于C,new用于C++。

销毁 int* 不会释放资源。让它超出范围是不安全的,所以它不是 RAII。

int * 可能是 class 的成员,它在其析构函数中删除了 int*,本质上就是 int 的 unique_ptr 所做的。您可以通过将它们包装在封装删除的代码中来制作类似 RAII 的东西。

使用 malloc 本身不是 RAII,因为当变量超出范围时不会释放资源,从而导致内存泄漏。如果将其包装在 class 中并释放析构函数中的资源,则 可以 使其成为 RAII,因为本地 class 实例在超出范围时确实会死亡.但是,应该注意这里讨论的内容:int * 类型不是 RAII,如果您将它包含在 RAII 类型中,它仍然不是。包装器没有让它成为 RAII,所以这里的 RAII 类型是包装器,而不是指针本身。

根据评论中的要求:RAII 代表资源获取即初始化,它是一种将资源分配与对象的初始化和销毁​​相结合的设计范例。您似乎离理解它不远:当一个对象被实例化时,它会分配所有必要的资源(内存、文件描述符、流等),并在它超出范围或对象被破坏时释放它们。这是 C++ 中的常见范例,因为 C++ classes 是 RAII(即,它们在超出范围时死亡),因此很容易保证正确清理。明显的好处是您无需担心手动清理和跟踪变量的生命周期。

在相关说明中,请注意这是指堆栈分配,而不是堆。这意味着无论您使用何种方式进行分配(new/malloc 与 delete/free),它仍然不是 RAII;动态分配的内存不会神奇地释放,这是给定的。当在堆栈上分配变量(局部变量)时,它们会在范围终止时被销毁。

示例:

class MyObject
{
public:

   MyObject()
   {
      // At this point resources are allocated (memory, files, and so on)
      // In this case a simple allocation.
      // malloc would have been just as fine
      this->_ptr = new int;
   }

   ~MyObject()
   {
      // When the object is destructed all resources are freed
      delete this->_ptr;
   }

private:

   int * _ptr;
};

前面的示例代码在本机指针上实现了 RAII 包装器。使用方法如下:

void f()
{
   MyObject obj;

   // Do stuff with obj, not including cleanup
}

在前面的示例中,int 指针在实例化变量时(在声明时)分配,并在 f 调用终止时释放,导致变量超出范围并调用其析构函数。

注意:正如 Jarod42 的评论中提到的,给定的示例不符合 rule of 3 or the rule of 5, which are common thumb rules in C++. I would rather not add complexity to the given example, and as such I'll complete it here. These rules indicate that, if a method from a given set is implemented, then all methods of the set should be implemented, and those methods are the copy and move constructors, the assignment and move operators, and the destructor. Notice at first that this is a general rule, which means that is not mandatory. For instance, immutable objects should not implement assignment and move operators at all. In this case, if the object is to implement these operators it would probably imply reference counting,因为存在资源的多个副本,所以在销毁所有副本之前,析构函数不得释放资源。我相信这样的实现会超出范围,因此我将其排除在外。

例如

RAII:

void foo()
{
    int* p = malloc(sizeof(int) * N);
    // do stuff
    free(p);
}

RAII:

void foo()
{
    int* p = new int[N];
    // do stuff
    delete[] p;
}

RAII:

struct MyResourceManager
{
    int* p;
    MyResourceManager(size_t n) : p(malloc(sizeof(int) * n)) { }
    ~MyResourceManager() { free(p); }
};

void foo()
{
    MyResourceManager resource(N);
    // doing stuff with resource.p
}

还有 RAII(更好):

struct MyResourceManager
{
    int* p;
    MyResourceManager(size_t n) : p(new int[n]) { }
    ~MyResourceManager() { delete[] p; }
};

void foo()
{
    MyResourceManager resource(N);
    // doing stuff with resource.p
}

还有 RAII(最适合此用例):

void foo()
{
    std::unique_ptr<int[]> p(new int[N]);
    // doing stuff with p
}

是的,您可以使用 RAII 范式处理 int * p = malloc(1000);。智能指针和 std::vector 使用非常相似的技术,尽管它们可能不使用 malloc 而是更喜欢使用 new

下面是对 malloc 可以做什么的简单介绍。 MyPointer 在实际应用中远没有用处。它的唯一目的是演示RAII的原理。

class MyPointer
{
   public:
      MyPointer(size_t s) : p(malloc(s)) {}
      ~MyPionter() { free(p); }

      int& operator[](size_t index) { return p[index]; }

   private:

      int* p;
};

int main()
{
   // By initializing ptr you are acquiring resources.
   // When ptr gets destructed, the resource is released.
   MyPointer ptr(1000);

   ptr[0] = 10;
   std::cout << ptr[0] << std::endl;
}

RAII背后的核心思想是:

  1. 像初始化对象一样对待资源获取。
  2. 确保在销毁对象时释放获取的资源。

您可以在 Wikepedia 阅读更多关于 RAII 的信息。

RAII 没有使用运算符 new 也没有使用 malloc().

本质上就是在初始化一个对象的过程中,分配了该对象发挥作用所需的所有资源。对应的要求是,在销毁对象的过程中,释放其分配的资源。

这个概念适用于内存(最常见),但也适用于任何其他需要管理的资源——文件句柄(在初始化时打开,完成时关闭),互斥量(在初始化时获取,完成时释放),通信端口等

在C++中,RAII通常是通过在对象的构造函数中进行初始化来实现的,资源的释放是在析构函数中完成的。有一些问题,例如其他成员函数可能会重新分配(例如调整动态分配的数组的大小)——在这些情况下,成员函数必须确保它们以某种方式做事,以确保在析构函数完成时适当释放所有分配的资源。如果有多个构造函数,他们需要一致地做事。你会看到这被描述为类似于构造函数设置 class 不变量(即资源被正确分配),成员函数维护那个不变量,并且析构函数能够清理因为不变量被维护。

RAII 的优点 - 如果做得好 - 非静态变量的生命周期由编译器管理(当对象超出范围时,将调用其析构函数)。因此,资源将被正确清理。

但是,要求始终是析构函数执行清理(或者 class 的数据成员有自己的析构函数来执行所需的清理)。如果构造函数使用 malloc() 初始化一个 int *,那么假设析构函数将清理是不够的——析构函数必须将该指针传递给 free()。如果你不这样做,C++ 编译器就不会神奇地找到一些方法来为你释放内存(当析构函数完成时指针将不再存在,但它指向的分配内存将不会被释放 - 所以结果是内存泄漏)。 C++ 本身并不使用垃圾收集(对于习惯垃圾收集语言的人来说,假设会发生垃圾收集,这是一个陷阱)。

并且使用malloc()分配内存,任何形式的运算符delete释放它都是未定义的行为。

通常最好不要在 C++ 中使用 malloc()free(),因为它们不能很好地处理对象构造和析构(调用构造函数和析构函数)。请改用运算符 new(对于您使用的任何形式的运算符 new,请使用相应的运算符 delete)。或者,更好的是,尽可能使用标准 C++ 容器(std::vector 等)以避免手动释放分配的内存。

讨论的是在资源获取时真正进行初始化但不遵循 RAII 设计的代码。

在显示的带有 malloc 的示例中,分配了 1000 字节的内存(资源分配),并用结果初始化变量 p(指向 int 的指针)(初始化)。但是,这显然不是 RAII 的示例,因为对象(int * 类型)不会在其析构函数中处理获取的资源。

所以不,malloc 本身在某些情况下不能是 RAII,它是一个非 RAII 代码的例子,但是 "Initialization on Resource Acquisition" 可能会混淆初看 C++ 程序员。

在 C++ 中,unique_ptr 表示一个指针,"owns" 它指向的对象。您可以提供发布功能作为第二个参数:

std::unique_ptr<int[], std::function<void(void*)>> 
    p( (int *)malloc(1000 * sizeof(int)), std::free );

当然,除了使用 new 之外,没有太多理由这样做(当默认删除器 delete 会做正确的事情时)。

So, what are the following people talking about?

什么是 RAII?

RAII 简而言之是一个非常简单的想法。 除非完全初始化,否则根本不存在任何对象。

为什么这么好?

我们现在有一个 'half built' 对象不会被意外使用的具体保证 - 因为在程序的逻辑流程中,它 不可能存在

我们如何实现它?

a) 总是在他们自己的 class 中管理资源(内存、文件、互斥锁、数据库连接),这是专门为管理该资源而量身定制的。

b) 从 [a]

涵盖的对象集合中构建复杂的逻辑

c) 如果构造函数中的任何内容失败,则始终抛出(以保证失败的对象不存在)

d) 如果我们 在 class 中管理多个资源,我们确保失败的构建清理已经构建的部分(注意: 这很难 [有时是不可能的],为什么此时你应该返回 [a])

听起来很难?

在初始化列表中完全初始化您的对象,同时将所有外部资源包装在管理器中 class(例如文件、内存)毫不费力地实现完美的 RAII。

有什么好处?

您的程序现在可能只包含使推理和阅读更容易的逻辑。编译器将完美地处理所有资源管理。

轻松的复合资源管理

RAII 的一个例子,没有经理 classes 很难,有他们很容易?

struct double_buffer
{
    double_buffer()
    : buffer1(std::nullptr)    // NOTE: redundant zero construction
    , buffer2(std::nullptr)
    {
      buffer1 = new char[100];   // new can throw!
      try {
        buffer2 = new char[100];   // if this throws we have to clean up buffer1
      }
      catch(...) {
        delete buffer1;         // clean up buffer1
        throw;                  // rethrow because failed construction must throw!
      }
    }

    // IMPORTANT! 
    // you MUST write or delete copy constructors, move constructor,
    // plus also maybe move-assignment or move-constructor
    // and you MUST write a destructor!


    char* buffer1;
    char* buffer2;
};

现在是 RAII 版本:

struct double_buffer
{
    double_buffer()
    : buffer1(new char[100])   // memory immediately transferred to manager
    , buffer2(new char[100])   // if this throws, compiler will handle the
                               // correct cleanup of buffer1
    {
      // nothing to do here
    }

    // no need to write copy constructors, move constructor,
    // move-assignment or move-constructor
    // no need to write destructor

    std::unique_ptr<char[]> buffer1;
    std::unique_ptr<char[]> buffer2;
};

它如何改进我的代码?

一些使用 RAII 的安全代码:

auto t = merge(Something(), SomethingElse());   // pretty clear eh?
t.performAction();

不使用RAII的相同代码:

  TargetType t;         // at this point uninitialised.
  Something a;
  if(a.construct()) {
    SomethingElse b;
    if (b.construct()) {
      bool ok = merge_onto(t, a, b);   // t maybe initialised here
      b.destruct();
      a.destruct();
      if (!ok) 
        throw std::runtime_error("merge failed");
    }
    else {
      a.destruct();
      throw std::runtime_error("failed to create b");
    }
  }
  else {
    throw std::runtime_error("failed to create a");
  }

  // ... finally, we may now use t because we can (just about) prove that it's valid
  t.performAction(); 

区别

RAII代码完全按照逻辑编写。

非 RAII 代码是 40% 的错误处理和 40% 的生命周期管理,只有 20% 的逻辑。此外,逻辑隐藏在所有其他垃圾中,甚至这 11 行代码也很难推理。