【C++笔记】RAII是什么?
RAII是什么
说明:本文是这个视频的笔记,中间可能添加其他来源的资料和自己的理解,请注意区别,如有错漏,敬请指出。
什么是RAII?为什么需要RAII?
RAII,全称资源获取即初始化(英语:Resource Acquisition Is Initialization),它是在一些面向对象语言中的一种惯用法。——维基百科
- RAII这个名字比较抽象,所以不要看这个字面意义去理解了。
- RAII主要用于面向对象语言,尤其是C++。
- RAII就是编程中的一种 Programming Idiom,也就是约定俗成的一些编码方法、编码习惯甚至包括一些代码片段。比如尽量不使用goto语句就是一种Programming Idiom(但不属于RAII)。RAII主要用于程序中的资源(内存、文件描述符、设备、信号量等)的管理。
- 之所以需要RAII,就是为了在保持代码简洁的情况下,保证代码的异常安全性。RAII要求,资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄漏问题。
什么是资源?What is a resource?
在一个程序的执行环境中,任何访问有限制的虚拟成分,都是资源。如:
- 内存
- 信号量
- 普通文件、socket、管道等
由于资源的访问是有限制的,所以我们不能无限制索取资源,也不能一直占着资源不放,所以资源在程序中往往涉及到“申请”和“释放”,这二者应该成对出现,不“申请”就无法将资源拿到手里,而一直拿着资源不“释放”,就会导致其他需要使用资源的进程无法正常运行,甚至导致更严重的后果,也就是所谓的“资源泄露”。
什么是资源管理?What is the resource management?
资源管理就是让循环地对资源进行获取(acquire)、访问(access)、释放(release)等操作。
char *p = new char[len]; // 获取
cin.getline(p, len); //访问
cout << p << endl;
delete[] p; //释放
一个资源泄露的例子
char *p = new char[len];
// some operations...
if (stat(p, &sb))
return; // nice shot! Memory leaked.
delete[] p;
指针p获取了内存后,程序可能无法执行到释放内存操作的那一行代码就结束了,就会造成内存泄露。
第一种fix方法
char *p = new char[len];
// some operations...
if (stat(p, &sb)) {
delete[] p; // before leaving the scope
return;
}
delete[] p;
- 如果有很多if,需要加很多释放语句。
- 如果some operations抛出异常,无法执行到释放语句。
第二次fix
char *p = new char[len];
try {
// some operations...
if (stat(p, &sb)) {
delete[] p; // before leaving the scope
return;
}
} catch (.../* BAD! */) {
delete[] p;
throw;
}
delete[] p;
如果some operations中包含其他资源的申请操作,还是会造成内存泄露:
char *p = new char[len];
try {
auto *t1 = new T;
auto *t2 = new T;
if (stat(p, &sb)) {
delete[] p; // before leaving the scope
return;
}
delete t2; delete t1;
} catch (.../* BAD! */) {
delete[] p;
throw;
}
delete[] p;
如果t1成功获取内存,t2申请内存的时候抛异常了,那么还需要去清理t1。
总结:可以看出来,我们申请了资源后,就必须考虑任何一条程序执行路径,并进行资源释放。
有构造必有析构
RAII就是有构造必有析构:
RAIIPtr::RAIIPtr(int *ptr) : ptr_(ptr) {}
~RAIIPtr::RAIIPtr() {delete ptr_;}
将资源的生命周期与掌握资源的对象的生命周期绑定
让我们再看一下之前的内存泄漏的例子,我们使用另一种方法来防止资源泄露:
auto std::make_unique<char[]>(len); // char *p = new char
// some operations...
throws std::runtime_error("exceptions..."); // no problem~
// dtor called()
if (stat(p.get(), &sb))
return; //dtor called, no leaked
在这种方法下,我们把申请的内存和只能指针绑定在一起,由于智能指针本身在栈上分配,所以不会造成内存泄漏。
- 在return的时候,程序退栈会自动触发智能指针的析构函数,释放内存。
- 在抛出异常的时候,只要智能指针已经分配完毕,程序退栈的时候就会自动触发智能指针的析构函数。
C++中遵循RAII的设计
Files
void printFile () {
ifstream input("hamlet.txt");
// read file
// no close call needed!
}
// stream destructor
// releases access to file
Locks
void cleanDatabase (mutex& databaseLock, map<int, int>& database) {
lock_guard<mutex>(database);
// other threads will not modify database
// modify the database
// if exception thrown, that's fine!
// no release call needed
} // lock always unlocked when function exit
Smart pointers
- std::unique_ptr
- std::shared_ptr
- std::weak_ptr
- boost::scoped_ptr
- boost::intrusive_ptr
但是智能指针依然可能会造成内存泄漏:
void fun(std::unique_ptr<A>, std::unique_ptr<B>);
int main() {
fun(std::unique_ptr<A>(new A),
std::unique_ptr<B>(new B));
}
编译器可能先申请A和B需要的内存,并构造A和B,然后再构造二者对应的只能指针,而如果已经申请了内存,还未完全构造两个智能指针,可能会造成内存泄漏,具体构造顺序由编译器的具体实现决定,是一个未定义的行为。解决方法如下:
void fun(std::unique_ptr<A>, std::unique_ptr<B>);
int main() {
std::unique_ptr a(new A);
std::unique_ptr b(new B);
fun(a, b);
}
这种写法之下,人工控制了程序执行顺序。另一种解法:
void fun(std::unique_ptr<A>, std::unique_ptr<B>);
int main() {
fun(std::make_unique<A>(),
std::make_unique<B>());
}
如何实现RAII
我们试着实现一个简单的Vector:
class NaiveVector {
int *ptr_;
size_t size_;
public:
NaiveVector() : ptr_(nullptr), size_(0) {}
void push_back(int newValue) {
int *newptr = new int[size_ + 1];
std::copy(ptr_, ptr_ + size_, newptr);
delete [] ptr_; ptr_ = newptr;
ptr_[size++] = newvalue;
}
};
- 析构函数
class NaiveVector {
int *ptr_;
size_t size_;
public:
NaiveVector() : ptr_(nullptr), size_(0) {}
void push_back(int newValue) {
int *newptr = new int[size_ + 1];
std::copy(ptr_, ptr_ + size_, newptr);
delete [] ptr_; ptr_ = newptr;
ptr_[size++] = newvalue;
}
~NaiveVector() {delete [] ptr_; } // OK! No leak
};
- 拷贝构造
class NaiveVector {
int *ptr_;
size_t size_;
public:
NaiveVector() : ptr_(nullptr), size_(0) {}
void push_back(int newValue) {
int *newptr = new int[size_ + 1];
std::copy(ptr_, ptr_ + size_, newptr);
delete [] ptr_; ptr_ = newptr;
ptr_[size++] = newvalue;
}
NaiveVector(const NaiveVector& rhs) {
ptr_ = new int[rhs.size_];
size_ = rhs.size_;
std::copy(rhs.ptr_, rhs.ptr_ + size_, ptr);
}
~NaiveVector() {delete [] ptr_; } // OK! No leak
};
- 拷贝赋值
class NaiveVector {
int *ptr_;
size_t size_;
public:
NaiveVector() : ptr_(nullptr), size_(0) {}
void push_back(int newValue) { ... }
NaiveVector(const NaiveVector& rhs) { ... }
NaiveVector& operator=(const NaiveVector& rhs) {
NaiveVector copy = rhs;
copy.swap(*this);
return *this;
}
~NaiveVector() {delete [] ptr_; } // OK! No leak
};
总结:三个规则
- 如果你的类直接管理一些资源,你应当保证手工实现三个特殊的成员函数:
- 析构函数:用于释放资源
- 拷贝构造函数:用于复制资源
- 拷贝赋值函数:释放左边的资源并拷贝右边的。
- 使用copy-and-swap习语来实现赋值
为什么要用copy-and-swap?
如果不使用copy-and-swap可能会导致资源提前被删除:
NaiveVector& NaiveVector::operator=(const NaiveVector& rhs) {
delete ptr_;
ptr_ = new int[rhs.size_];
size_ = rhs.size_;
std::copy(rhs.ptr, rhs.ptr_ + size_, ptr_);
return *this;
}
如果删除ptr_后,复制rhs前发生了异常,那么原来的数据也丢失了!
C++中RAII的一些问题
RAII只能保证资源不发生泄露,但不能保证内存安全(多端读写)。