我对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
。
答案 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 3或rule 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背后的核心理念是:
您可以在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)
那么,以下人们在谈论什么?
我们现在有一个具体的保证,即半建造的&#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 ++程序员来说,乍一看。