Loading

【C++笔记】RAII是什么?

RAII是什么

说明:本文是这个视频的笔记,中间可能添加其他来源的资料和自己的理解,请注意区别,如有错漏,敬请指出。

什么是RAII?为什么需要RAII?

RAII,全称资源获取即初始化(英语:Resource Acquisition Is Initialization),它是在一些面向对象语言中的一种惯用法。——维基百科

  1. RAII这个名字比较抽象,所以不要看这个字面意义去理解了。
  2. RAII主要用于面向对象语言,尤其是C++。
  3. RAII就是编程中的一种 Programming Idiom,也就是约定俗成的一些编码方法、编码习惯甚至包括一些代码片段。比如尽量不使用goto语句就是一种Programming Idiom(但不属于RAII)。RAII主要用于程序中的资源(内存、文件描述符、设备、信号量等)的管理。
  4. 之所以需要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;
  1. 如果有很多if,需要加很多释放语句。
  2. 如果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_;}

将资源的生命周期与掌握资源的对象的生命周期绑定

image

让我们再看一下之前的内存泄漏的例子,我们使用另一种方法来防止资源泄露:

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;
	}
};
  1. 析构函数
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
};
  1. 拷贝构造
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
};
  1. 拷贝赋值
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只能保证资源不发生泄露,但不能保证内存安全(多端读写)。

posted @ 2024-03-26 18:41  杨谖之  阅读(34)  评论(0编辑  收藏  举报