Published by orzz.org(). (https://orzz.org/cpp%e5%86%85%e5%ad%98%e7%ae%a1%e7%90%86%e4%b9%8b%e8%87%aa%e5%8a%a8%e5%8c%96%e7%9a%84%e8%b5%84%e6%ba%90%e5%9b%9e%e6%94%b6raii/)
资源回收一直是写C++代码时需要考虑的重点内容之一.比如在出口点较多的函数中,若不使用一些技巧,仅靠机械的手动方式管理资源,往往会导致资源管理的代码与实际的函数逻辑相互纠缠.这样的代码不仅容易出错,维护起来也颇为头疼.
比如下面的这个函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
bool InetDownToFile(LPCTSTR lpszUrl/*下载地址*/, LPCTSTR lpszOut/*输出文件*/) { DWORD ret = 0, ret_siz = sizeof(ret), ret_len = 0; HINTERNET hSession = InternetOpen(NULL, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0); if (!hSession) return false; // 网络连接 HINTERNET hUrl = InternetOpenUrl(hSession, lpszUrl, NULL, 0, INTERNET_FLAG_NO_UI | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS, 0); if (!hUrl) { InternetCloseHandle(hSession); return false; } // 打开文件 HANDLE hFile = CreateFile(lpszOut, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE == hFile) { InternetCloseHandle(hUrl); InternetCloseHandle(hSession); return false; } // 检查服务器返回值是否表示成功 if (HttpQueryInfo(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &ret, &ret_siz, &ret_len)) { if (ret >= 200 && ret < 300) { BYTE buff[512] = {0}; // 分配内存缓冲区 ret_siz = 0; ret_len = 0; do { if (!InternetReadFile(hUrl, (LPVOID)buff, sizeof(buff), &ret_len)) { CloseHandle(hFile); InternetCloseHandle(hUrl); InternetCloseHandle(hSession); return false; } if (ret_len == 0) // read == 0 表示EOF了, iCount >= status 达到上限了 break; else WriteFile(hFile, buff, ret_len, &ret_siz, NULL); } while(1); } else { CloseHandle(hFile); InternetCloseHandle(hUrl); InternetCloseHandle(hSession); return false; } } CloseHandle(hFile); InternetCloseHandle(hUrl); InternetCloseHandle(hSession); return true; } |
仅仅是一段简单的,以HTTP协议从网络上获取文件的函数,代码总共不超过70行.
可是看看这段代码,其中用于资源释放的地方就有5处.不仅大部分代码是相同的,按这个写法,每个函数的出口点都需要罗列一遍前面获得的资源句柄.
仅仅一段简单的HTTP代码就导致了5处必须的资源释放,要是函数逻辑再复杂一点,判断条件再多一点(比如加上几段异常处理),势必会导致代码的臃肿...让人气愤的是,这些臃肿的代码不仅形式雷同,连调用原因都是基本一致的:因为函数要退出了嘛,必须回收局部资源.
有没有什么好办法可以自动回收这些资源呢?C++虽然总要记得释放指针,不过栈上定义的临时变量似乎从来不需要手动去管它们的内存呀.
这就是了.局部直接定义的变量是在栈上的,按C++的语义,当程序执行过了这个局部区域以后(一般是一对大括号的范围)这段临时分配出的资源会被系统自动回收.
那么如何让我们的局部句柄里的资源能够定义到栈上呢?当然,直接强迫系统让栈来管理不知会有多大的资源是不可能的.这时候我们可以用RAII来解决这个问题.
RAII的通常做法,是在一个临时对象构造时传入需要自动释放的资源,然后...就不用管了.这个临时对象不论在这个局部区域的哪个入口点都会被系统自动回收,这时这个对象的析构过程会自动帮我们把需要释放的资源干净的回收掉.
不过很多时候,我们不一定能够在临时对象构造时就传入所有的待释放资源,给这个临时对象加个添加新资源的接口是个简单的选择.
为了改进这段代码,我们需要先定义一个清理类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class CCleaner { protected: typedef list<HINTERNET> list_t; list_t m_ResList; public: ~CCleaner() { list_t::iterator ite; for(ite = m_ResList.begin(); ite != m_ResList.end(); ++ite) if (*ite) InternetCloseHandle(*ite); } public: void Add(HINTERNET hint) { if (NULL == hint) return ; m_ResList.push_front(hint); // 在头部放入新数据, 以保证删除时的顺序(其实这个可能没有必要) } }; |
这个里面用到了STL里的list容器,以便管理多个资源句柄.
下面是改进过的文件下载函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
bool InetDownToFile(LPCTSTR lpszUrl/*下载地址*/, LPCTSTR lpszOut/*输出文件*/) { CCleaner cleaner; DWORD ret = 0, ret_siz = sizeof(ret), ret_len = 0; HINTERNET hSession = InternetOpen(NULL, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0); if (!hSession) return false; cleaner.Add(hSession); // 网络连接 HINTERNET hUrl = InternetOpenUrl(hSession, lpszUrl, NULL, 0, INTERNET_FLAG_NO_UI | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS, 0); if (!hUrl) return false; cleaner.Add(hUrl); // 打开文件 HANDLE hFile = CreateFile(lpszOut, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE == hFile) return false; // 检查服务器返回值是否表示成功 if (HttpQueryInfo(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &ret, &ret_siz, &ret_len)) { if (ret >= 200 && ret < 300) { BYTE buff[512] = {0}; // 分配内存缓冲区 ret_siz = 0; ret_len = 0; do { if (!InternetReadFile(hUrl, (LPVOID)buff, sizeof(buff), &ret_len)) { CloseHandle(hFile); return false; } if (ret_len == 0) // read == 0 表示EOF了, iCount >= status 达到上限了 break; else WriteFile(hFile, buff, ret_len, &ret_siz, NULL); } while(1); } else { CloseHandle(hFile); return false; } } CloseHandle(hFile); return true; } |
新写的代码明显逻辑清晰了很多,繁杂的网络句柄释放不见了,仅仅需要考虑的就是文件句柄的释放.
那么,有没有办法将文件句柄也纳入自动管理的范畴呢?再添加一个类似的清理类?如果这样似乎又在另一个方向上面临代码冗余的问题.
有两个解决这个问题的方法.一是使用统一的基类作为CCleaner的清理逻辑,而将特殊的部分用子类派生,或使用类模板来处理.这样的话其实仍然需要重写一部分不一样的逻辑,如上面的例子,在关闭句柄时调用的函数不一样;二则是将不同的部分交给外部完成,比如在添加资源句柄的时候自定义该资源的清理函数.
我们采用第二种方法改写CCleaner:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class CCleaner { protected: typedef BOOL(WINAPI *cln_proc_t)(void*); struct item_t { void* m_Res; void* m_Cln; item_t(void* res, void* cln) : m_Res(res) , m_Cln(cln) {} }; typedef list<item_t> list_t; list_t m_ResList; public: ~CCleaner() { list_t::iterator ite; for(ite = m_ResList.begin(); ite != m_ResList.end(); ++ite) ((cln_proc_t)(*ite).m_Cln)((*ite).m_Res); } public: void Add(void* pRes, void* pCleanProc) { if (NULL == pRes) return ; m_ResList.push_front ( item_t(pRes, pCleanProc) ); // 在头部放入新数据, 以保证删除时的顺序(其实这个可能没有必要) } }; |
采用结构体定义CCleaner的内部存储结构,结构体内部将资源指针与清理函数指针相关联.这样不论是何种资源,只要其清理接口属于此类定义方式,均可以使用这个清理类做自动化清理.
下面是再次改进过的文件下载函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
bool InetDownToFile(LPCTSTR lpszUrl/*下载地址*/, LPCTSTR lpszOut/*输出文件*/) { CCleaner cleaner; DWORD ret = 0, ret_siz = sizeof(ret), ret_len = 0; HINTERNET hSession = InternetOpen(NULL, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0); if (!hSession) return false; cleaner.Add(hSession, InternetCloseHandle); // 网络连接 HINTERNET hUrl = InternetOpenUrl(hSession, lpszUrl, NULL, 0, INTERNET_FLAG_NO_UI | INTERNET_FLAG_NO_CACHE_WRITE | INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS, 0); if (!hUrl) return false; cleaner.Add(hUrl, InternetCloseHandle); // 打开文件 HANDLE hFile = CreateFile(lpszOut, GENERIC_WRITE, NULL, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); if (INVALID_HANDLE_VALUE == hFile) return false; cleaner.Add(hFile, CloseHandle); // 检查服务器返回值是否表示成功 if (HttpQueryInfo(hUrl, HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER, &ret, &ret_siz, &ret_len)) { if (ret >= 200 && ret < 300) { BYTE buff[512] = {0}; // 分配内存缓冲区 ret_siz = 0; ret_len = 0; do { if (!InternetReadFile(hUrl, (LPVOID)buff, sizeof(buff), &ret_len)) return false; if (ret_len == 0) // read == 0 表示EOF了, iCount >= status 达到上限了 break; else WriteFile(hFile, buff, ret_len, &ret_siz, NULL); } while(1); } else return false; } return true; } |
此时,我们可以专注于业务逻辑的处理,而将资源管理的任务交给清理类全权处理.
其实掌握了此类技巧后,只要是需要在过程退出时完成的操作,都可以自动完成了,比如多线程里的局部加锁解锁;函数内对文件访问后需要在退出函数时还原文件读取位置等等.
比如一个自动的文件位置还原类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
class CFileSeeker { protected: IFileObject* m_pFile; uint64_t m_nTell; public: CFileSeeker(IFileObject* pFile) : m_pFile(pFile) , m_nTell(-1) { if (m_pFile) { m_nTell = m_pFile->Tell(); if (m_nTell == (uint64_t)-1) m_pFile = NULL; } } ~CFileSeeker() { if (m_pFile) m_pFile->Seek(m_nTell, IFileObject::begin); } }; |
只需要定义IFileObject,并实现相应的接口就可以满足各种文件对象的需求.