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

时间:2015-12-24 08:03:15

标签: c++ raii

我对RAII的理解是,无论何时需要使用new等手动分配内存,您都需要将其释放。因此,不应手动释放它,而应使用构造函数和析构函数创建类来完成工作。

那么,以下人们在谈论什么?

来自:The meaning of the term - Resource Acquisition Is Initialization

  

问题是int * p = malloc(1000);也是(整数)对象的初始化,但它不是我们在RAII上下文中所指的那种初始化。   ...

      @Fred:的确如此。 int *不是RAII类型,因为它不进行清理。所以它不是RAII的意思,即使它是RAII的字面意思。

嗯,我知道在C中使用了malloc,在C ++中使用了new

8 个答案:

答案 0 :(得分:4)

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

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

在相关的说明中,请注意这是指堆栈分配,而不是堆。这意味着无论你使用什么方式进行分配(new / malloc vs 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 3rule of 5,它们是C ++中的常见拇指规则。我宁愿不为给定的例子增加复杂性,因此我将在这里完成它。这些规则表明,如果实现了给定集合中的方法,则应实现集合的所有方法,并且这些方法是复制和移动构造函数,赋值和移动运算符以及析构函数。首先请注意,这是一般规则,这意味着这不是强制性的。例如,不可变对象根本不应该实现赋值和移动运算符。在这种情况下,如果对象要实现这些运算符,则可能意味着reference counting,因为存在资源的多个副本,析构函数必须不释放资源,直到销毁所有副本。我相信这样的实施将超出范围,因此我将其排除在外。

答案 1 :(得分:3)

以示例

RAII:

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

NOT 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
}

答案 2 :(得分:2)

RAII不使用运算符new,也不使用malloc()

它本质上意味着,在初始化对象的过程中,分配了对象需要运行的所有资源。对应的要求是,在销毁对象的过程中,它已分配的资源被释放。

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

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

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

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

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

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

答案 3 :(得分:1)

是的,您可以使用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. 确保在对象被破坏时释放所获取的资源。
  3. 您可以在Wikepedia了解有关RAII的更多信息。

答案 4 :(得分:1)

在C ++中,unique_ptr表示&#34;拥有&#34;的指针。它指向的东西。您可以将释放函数作为第二个参数提供:

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

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

答案 5 :(得分:1)

  

那么,以下人们在谈论什么?

什么是RAII?

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

为什么那么好?

我们现在有一个具体的保证,即半建造的&#39;对象不能被意外使用 - 因为程序逻辑流程中的任何一点都不能可能存在

我们如何实现它?

a)始终在自己的类中管理资源(内存,文件,互斥锁,数据库连接),这些资源专门用于管理该资源。

b)从[a]

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

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

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

听起来很难?

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

有什么优势?

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

轻松的复合资源管理

RAII的一个例子,如果没有经理人课程并且很容易与他们一起工作?

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行代码很难推理。

答案 6 :(得分:0)

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

int *可以是一个类的成员,它在析构函数中删除int *,这实际上是int的unique_ptr。你可以通过将它们包装在封装删除的代码中来制作这样的RAII。

答案 7 :(得分:0)

讨论是关于在资源获取时逐字进行初始化的代码,但不遵循RAII设计。

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

所以不,malloc本身在某些情况下不能成为RAII,它是非RAII代码的一个例子,然而&#34;资源获取的初始化&#34; 可能会令人困惑对于新的c ++程序员来说,乍一看。