RAII这个词看着很吓人,但其实很easy,这不是一个特性,也不是一个技术,它是一种编程思想(或者说习惯),类似"多写注释有助于以后重新熟悉代码"。
为了解释RAII的优越性,我们先看一段没有使用RAII的代码。
- // 检查文件是否符合某种规范
- bool CheckFile(const std::string file_path) {
- std::string file_content;
- ifstream ifs;
- ifs.open(file_path, std::ifstream::binary); // 打开文件
-
- // 检查文件是否能打开成功
- if (!ifs.is_open())
- return false;
-
- // 检查文件大小
- int64 file_size = ifs.seekg(0, ifs.end).tellg();
- if (file_size > 1024) {
- ifs.close();
- return false;
- }
- ifs.seekg(0, ifs.beg);
-
- // 获取文件第一行内容
- if (!getline(ifs, file_content)) {
- ifs.close();
- return false;
- }
-
- // 检查文件内容
- if (file_content != "this file is legal") {
- ifs.close();
- return false;
- }
-
- ifs.close(); // 关闭文件
- return true;
- }
注意这段代码是没有bug的,可以正常运行,但看起来非常拧巴,就这几个return前的close,写的罗嗦不说,以后再改代码还容易出bug,随便哪次return时忘了close文件就可能出问题。
为此有很多方式可以将多个return合并为一个,比如:
- bool CheckFile(const std::string file_path) {
- std::string file_content;
- ifstream ifs;
- ifs.open(file_path, std::ifstream::binary); // 打开文件
-
- bool is_check_ok = true;
-
- // 检查文件是否能打开成功
- if (is_check_ok)
- is_check_ok = ifs.is_open();
-
- // 检查文件大小
- if (is_check_ok) {
- int64 file_size = ifs.seekg(0, ifs.end).tellg();
- ifs.seekg(0, ifs.beg);
- is_check_ok = file_size <= 1024;
- }
-
- // 获取文件第一行内容
- if (is_check_ok)
- is_check_ok = (bool)getline(ifs, file_content);
-
- // 检查文件内容
- if (is_check_ok)
- is_check_ok = file_content == "this file is legal";
-
- ifs.close(); // 关闭文件
- return is_check_ok;
- }
- bool CheckFile(const std::string file_path) {
- std::string file_content;
- ifstream ifs;
- ifs.open(file_path, std::ifstream::binary); // 打开文件
-
- bool is_check_ok = false;
-
- do {
- // 检查文件是否能打开成功
- if (!ifs.is_open())
- break;
-
- // 检查文件大小
- int64 file_size = ifs.seekg(0, ifs.end).tellg();
- ifs.seekg(0, ifs.beg);
- if (file_size > 1024)
- break;
-
- // 获取文件第一行内容
- if (!getline(ifs, file_content))
- break;
-
- // 检查文件内容
- if (file_content != "this file is legal")
- break;
-
- is_check_ok = true;
- } while (false);
-
- ifs.close(); // 关闭文件
- return is_check_ok;
- }
- bool CheckFile(const std::string file_path) {
- std::string file_content;
- ifstream ifs;
- ifs.open(file_path, std::ifstream::binary); // 打开文件
-
- // lambda
- auto check_file_fun = [&]() -> bool {
- // 检查文件是否能打开成功
- if (!ifs.is_open())
- return false;
-
- // 检查文件大小
- __int64 file_size = ifs.seekg(0, ifs.end).tellg();
- if (file_size > 1024)
- return false;
-
- ifs.seekg(0, ifs.beg);
-
- // 获取文件第一行内容
- if (!getline(ifs, file_content))
- return false;
-
- // 检查文件内容
- if (file_content != "this file is legal")
- return false;
-
- return true;
- };
-
- bool is_check_ok = check_file_fun();
- ifs.close(); // 关闭文件
- return is_check_ok;
- }
- // 仅作演示用,实际工作中不推荐使用goto
- bool CheckFile(const std::string file_path) {
- std::string file_content;
- ifstream ifs;
- ifs.open(file_path, std::ifstream::binary); // 打开文件
-
- bool is_check_ok = false;
-
- // 检查文件是否能打开成功
- if (!ifs.is_open())
- goto Exit;
-
- // 检查文件大小
- int64 file_size = ifs.seekg(0, ifs.end).tellg();
- ifs.seekg(0, ifs.beg);
- if (file_size > 1024)
- goto Exit;
-
- // 获取文件第一行内容
- if (!getline(ifs, file_content))
- goto Exit;
-
- // 检查文件内容
- if (file_content != "this file is legal")
- goto Exit;
-
- is_check_ok = true;
-
- Exit:
- ifs.close(); // 关闭文件
- return is_check_ok;
- }
哈哈哈抱歉多写了几个。
但这样用起来还是很不方便,这三种方式虽然实现了功能,也杜绝了手误写bug的可能性,但这种代码写法还是不如第一种随时return的方式直观。
如果这个ifs能自动close就好了,像一个普通变量一样,超出作用域就自动释放,这样我们就不需要专门处理它的清理工作了。
那么,有办法实现这个效果吗?答案是有,思路是用一个变量把ifs包裹起来,把ifs当初普通变量使用。比如这样:
- // 智能文件类
- class SmartFile {
- private:
- ifstream ifs;
- public:
- SmartFile(const std::string file_path) {
- ifs.open(file_path, std::ifstream::binary); // 构造函数里加载文件
- }
- ~SmartFile() {
- ifs.close(); // 析构函数里释放文件
- }
- ifstream& getIfs() {
- return ifs; // 获取ifs
- }
- };
-
- bool CheckFile(const std::string file_path) {
- std::string file_content;
- SmartFile smartIfs(file_path); // 使用智能ifs
-
- // 检查文件是否能打开成功
- if (!smartIfs.getIfs().is_open())
- return false;
-
- // 检查文件大小
- int64 file_size = smartIfs.getIfs().seekg(0, smartIfs.getIfs().end).tellg();
- if (file_size > 1024)
- return false;
- smartIfs.getIfs().seekg(0, smartIfs.getIfs().beg);
-
- // 获取文件第一行内容
- if (!getline(smartIfs.getIfs(), file_content))
- return false;
-
- // 检查文件内容
- if (file_content != "this file is legal")
- return false;
-
- return true;
- }
注意看这个代码,新增一个SmartFile类专门处理ifstream,在构造的时候初始化,在析构的时候close,这样我们就可以像使用普通变量一样使用ifstream了。
无论我们在任何时候(全局变量、函数内、if内)使用SmartFile,只要他超出作用域,SmartFile必然被销毁,析构函数被调用,ifstream自动被close了,无需我们手动处理。
上面代码已经无限接近第一版代码了,非常符合我们日常的思维习惯,但getIfs还是有点绕口,重载运算符来让代码更简洁:
- // 智能文件类
- class SmartFile {
- private:
- ifstream ifs;
- public:
- SmartFile(const std::string file_path) {
- ifs.open(file_path, std::ifstream::binary); // 构造函数里加载文件
- }
- ~SmartFile() {
- ifs.close(); // 析构函数里释放文件
- }
- ifstream* operator->() { return &ifs; }
- operator ifstream* () { return &ifs; }
- };
-
- bool CheckFile(const std::string file_path) {
- std::string file_content;
- SmartFile smartIfs(file_path); // 使用智能ifs
-
- // 检查文件是否能打开成功
- if (!smartIfs->is_open())
- return false;
-
- // 检查文件大小
- int64 file_size = smartIfs->seekg(0, smartIfs->end).tellg();
- if (file_size > 1024)
- return false;
- smartIfs->seekg(0, smartIfs->beg);
-
- // 获取文件第一行内容
- if (!getline(*smartIfs, file_content))
- return false;
-
- // 检查文件内容
- if (file_content != "this file is legal")
- return false;
-
- return true;
- }
是不是看起来就顺眼许多啦哈哈哈。
这就是RAII,非常的优雅。
几乎所有一切需要清理资源的,或者需要成对调用的接口,我们都可以使用RAII把它封装起来。
比如CRITICAL_SECTION,使用的时候需要Enter和Leave,可以封装:
- class SmartCritSec {
- CRITICAL_SECTION* cs;
- public:
- explicit SmartCritSec(CRITICAL_SECTION* cs) : cs(cs) {
- EnterCriticalSection(cs);
- }
- ~SmartCritSec() {
- LeaveCriticalSection(cs);
- }
- };
- void Test() {
- SmartCritSec scope(&m_access); // m_access为已经初始化过的CRITICAL_SECTION
- // work
- }
比如打开文件的HANDLE,结束时需要CloseHandle,可以封装:
- class SmartHandle {
- private:
- HANDLE handle;
- public:
- explicit SmartHandle(HANDLE handle) : handle(handle) { }
- ~SmartHandle() { CloseHandle(handle); }
- operator HANDLE() const { return handle; }
- };
-
- void Test() {
- ScopedHandle file(CreateFile(file_path, GENERIC_READ | GENERIC_WRITE, OPEN_EXISTING));
- // work
- }
比如Windows的GDI对象HBRUSH创建完需要DeleteObject,可以封装:
- class SmartHBrush {
- HBRUSH obj;
- public:
- SmartHBrush(HBRUSH obj) : obj(obj) { }
- ~SmartHBrush() { DeleteObject(obj); }
- operator HBRUSH() const { return obj; }
- };
-
- void Test() {
- SmartHBrush brush(CreateSolidBrush(RGB(255, 0, 0)));
- // work
- }
比如使用com组件时需要CoInitialize和CoUninitialize,可以封装:
- class SmartCom {
- public:
- SmartCom() { (void)CoInitialize(nullptr); }
- ~SmartCom() { CoUninitialize(); }
- };
-
- void Test() {
- SmartCom com;
- // work
- }
再也不用惦记着return前执行关闭操作了。
这时有聪明的同学要问了,我很少用你说的这些handle、com、brush,有没有什么我们更常用的代码用RAII实现的,等等你这个Smart单词看起来很眼熟呀~
眼熟就对了,RAII最大的应用,就是处理指针。
比如我们写一个正常的裸指针代码如下:
- bool Check() {
- std::string* s = new std::string;
- *s = "abc";
- // work
- if (!s) {
- delete s;
- return false;
- }
- if (*s == "") {
- delete s;
- return false;
- }
- delete s;
- return true;
- }
那么我们就可以写一个RAII指针如下:
- class SmartString {
- private:
- std::string* obj;
- public:
- SmartString(std::string* obj) : obj(obj) {}
- ~SmartString() { delete obj; }
- operator std::string* () const { return obj; }
- std::string* operator->() const { return obj; }
- };
-
- bool Check() {
- SmartString s(new std::string);
- *s = "abc";
- // work
- if (!s)
- return false;
- if (s->length() == 0)
- return false;
- if (*s == "")
- return false;
- return true;
- }
是不是就顺眼很多啦~
来,我们把它变成一个模板,让它更通用:
- template <class T>
- class SmartPtr {
- T* obj;
- public:
- SmartPtr(T* obj) : obj(obj) {}
- ~SmartPtr() { delete obj; }
- operator T* () const { return obj; }
- T* operator->() const { return obj; }
- };
-
- bool Check() {
- SmartPtr<std::string> s(new std::string);
- *s = "abc";
- SmartPtr<int> i(new int);
- SmartPtr<Bitmap> d(new Bitmap);
- SmartPtr<MyClassA> c(new MyClassA);
- return true;
- }
这就是一个最简单的智能指针,其他所有一切的智能指针,核心原理都是这样的。
当然一个智能指针要在各种复杂环境下都正常工作,仅有这些是不够的,还需要处理其他的逻辑操作,包括重载更多运算符,包括获取裸指针等,有兴趣的小伙伴请研究一下主流智能指针的实现源码。
在智能指针的基础上再多谈一点。
我上面实现的这个智能指针,再丰富一下操作接口,就是std::unique_ptr。
假如现在我在A线程有一个智能指针,指向一个对象(即一块内存),那如果我把这个指针传递给B线程时,我需要复制这个指针,也需要复制对象本身,这样两个线程各自持有各自的智能指针,也指向各自的对象。如果不这么操作,两个线程的指针指向同一对象,那当其中一个线程退出时,智能指针将对象销毁,第二个线程访问对象必然出错。
那么,有什么办法来解决这个问题呢,答案是std::shared_ptr,智能指针内部有一个计数器,初始化时为1,每复制一次指针就将计数器+1,而销毁指针时,并不直接销毁对象,而是将计数器-1,当计数器为0时才真正销毁对象。这样不管我复制多少次智能指针,销毁多少次智能指针,只有最后一个智能指针销毁时才真的销毁对象,非常的安全,高效,更优雅了。
那么,shared_ptr有什么缺点吗,当然有的,它的缺点叫循环引用,看下面这段代码(不再详细讲shared_ptr的实现逻辑了):
- struct Node {
- std::shared_ptr<Node> child_node;
- };
-
- void Test() {
- std::shared_ptr<Node> item = std::make_shared<Node>();
- item->child_node= item;
- }
运行一下就会出现内存泄漏,详细解释一下为什么会出问题:
- make_shared时new了Node对象,此时计数器为1
- item->_node = item时将智能指针赋给了child_node,此时既不new也不delete对象,只是计数器变成2
- 当Test执行完成后,智能指针item销毁,然后shared_ptr本来计数器为2,现在item销毁了,计数器就变成1了,shared_ptr一看计数器没有变成0,也就不会真的执行对象的销毁操作,那么第1步new出来的对象就永远存在了。
硬要解释的话就是本来new出来的对象有2个入口,销毁了一个,第二个入口还在,但我们再也拿不到这个入口了,因此造成内存泄露。
那么,有什么办法来解决shared_ptr循环引用的问题吗,当然有的,那就是weak_ptr。
weak_ptr,看名字就知道,它是一个弱引用智能指针,就是为了解决shared_ptr循环引用问题的,请看代码
- struct Node {
- std::weak_ptr<Node> _node;
- };
-
- void Test() {
- std::shared_ptr<Node> item = std::make_shared<Node>();
- item->_node = item;
- }
如此,便不会内存泄露了。
至于shared_ptr和weak_ptr的详细用法,大家还请继续研究。
看上面智能指针的逻辑体系,其实非常的直白,符合我们的日常思维直觉,一点都不高深,哪怕让我们自己设计一套智能指针系统,我们最终写出来的,肯定也是shared_ptr与weak_ptr这一套。
现在C++已经不推荐使用裸指针了,除非万不得已,不然还是老老实实的使用智能指针,能极大的降低bug概率和程序员的心智负担。