资源管理 ######################################## 一般而言,所谓资源,指的是内存、CPU、硬件、文件、句柄等资源。操作系统为每种资源的管理都提供了相关的接口。要想获得并访问资源,需要先获得资源,然后需要拥有可操纵资源的句柄,能否获得资源由操作系统决定,而程序员能做的就是通过资源句柄去操纵对象,并决何时获得/释放资源。 .. important:: 资源的管理第一原则为:谁获取资源,谁释放资源 决定函数的返回值 **************************************** 上面已经说过了:谁获取资源,谁释放资源,但是有些时候我们确实需要返回一个指针怎么办呢?例如需要将两个字符串进行拼接,那么函数必定要分配空间,但是指针返回后生命周期就不再归函数管理了。 一种直观的方法是使用智能指针,另一种则是使用输出指针 将指针分为输入指针和输出指针,函数从输入指针获取数据,然后将数据写入到输出指针中,输出指针指向的内存的尺寸由用户指定,这样就避免了指针逃出控制的问题 获取资源 **************************************** 以文件资源为例: .. code-block:: c // 打开文件并判断是否成功 FILE *hFile = fopen("D:\\main.cpp", "r"); if (!hFile) exit(EXIT_FAILURE); // 获取文件长度 fseek(hFile, 0, SEEK_END); long len = ftell(hFile); // 在内存中开辟缓冲区 char *buf = (char *) malloc(len + 1); rewind(hFile); // 读取文件内容到缓冲区 fread(buf, 1, len, hFile); buf[len] = '\0'; // 释放文件资源 fclose(hFile); printf("%s", buf); // 释放缓冲区资源 free(buf); 在上面的代码中,我们首先通过 ``fopen`` 打开文件 ``D:\main.cpp`` 并获取其句柄(hfile),然后获取文件的大小并通过 ``malloc`` 开辟缓冲区,读取文件内容后 **立即** 通过 ``fclose`` 关闭文件,打印缓冲区内容后 **立即** 释放缓冲区资源。 .. important:: 注意:这里我用到了两个 **立即** 来说明 ``何时释放资源``。对于资源的获取和释放我们有以下约定: 尽晚获取资源,尽早释放资源 对于 C++ 而言,我们可以通过类将上述操纵进行封装,从而简化操作: .. code-block:: cpp class File { char *buf = nullptr; FILE *hFile = nullptr; public: File(const char *_FileName, const char *_Mode) { hFile = fopen(_FileName, _Mode); } bool success() { return hFile != nullptr; } const char *read() { fseek(hFile, 0, SEEK_END); long len = ftell(hFile); buf = (char *) malloc(len + 1); rewind(hFile); fread(buf, 1, len, hFile); buf[len] = '\0'; return buf; } ~File() { if (hFile) fclose(hFile); if (buf) free(buf); } }; // 用法 File *objFile = new File("D:\\main.cpp", "r"); if (!objFile->success()) exit(EXIT_FAILURE); cout << objFile->read(); delete objFile; 在上述代码中,要点为: - 在构造函数中获取资源,在析构函数中释放资源 - 使用 new/delete 操作符自动执行构造函数和析构函数 - 我们通过对象操纵资源 可以看到,通过对文件句柄操作的简单封装,我们在使用时大幅度减少了代码的数量。更重要的是我们现在通过对象操纵资源,而不是通过句柄操纵资源。这样,我们将资源封装到一个一个类中,然后统一使用对象操纵资源(而不是文件句柄、设备句柄等) .. note:: 这种在构造函数获取资源并初始化对象,在析构函数释放资源的手段称为 :abbr:`RAII (Resource Acquisition Is Initialization)` 验证资源的有效性 **************************************** 现在我思考另外一个问题:如果我们的 read() 函数中某行代码执行失败了怎么办?比如内存分配失败了,文件读取失败了,文件定位失败了。更糟糕的是可能文件打开失败了,而我们却不加校验地调用了 read() 函数,导致我们错误地对空指针进行了操作。最简单的方法当然是直接返回一个空指针,但是这意味着我们在调用 read() 后又要进行一次资源有效性的判断。 这样,我们调用的代码就变成了: .. code-block:: cpp File *objFile = new File("D:\\main.cpp", "r"); if (!objFile->success()) exit(EXIT_FAILURE); const char *content = objFile->read(); if (!content) exit(EXIT_FAILURE); cout << content; delete objFile; 看吧,我们只是简单地读取一下文件的内容,但是由于要检测错误却出现了这么多行的代码,而且我们甚至不知道为什么代码出错了,代码不仅丑陋而且对于错误检测也毫无意义。 现在我们考虑 ``异常`` ,当我们函数体内的代码失败后我们 **立即** 中断当前函数的执行并抛出一个异常,在异常中我们可以详细叙述代码出错的时间、地点、原因等附加信息,而在调用函数前,我们通过捕获异常来查明代码出现的错误。通过核查异常的附加信息,我们可以很方便地找到错误的原因。 现在我们在 File 中添加异常: .. code-block:: cpp class File { char *buf = nullptr; FILE *hFile = nullptr; public: File(const char *_FileName, const char *_Mode) { hFile = fopen(_FileName, _Mode); if (!hFile) throw runtime_error("文件打开失败"); } const char *read() { fseek(hFile, 0, SEEK_END); long len = ftell(hFile); buf = (char *) malloc(len + 1); if (!buf) throw bad_alloc(); rewind(hFile); size_t size = fread(buf, 1, len, hFile); if (size < len && ferror(hFile)) throw runtime_error("文件读取失败"); buf[len] = '\0'; return buf; } ~File() { if (hFile) fclose(hFile); if (buf) free(buf); } }; // 使用 try { File *objFile = new File("D:\\Documents\\projects\\clion\\test\\main.cpp", "r"); cout<read(); delete objFile; } catch (bad_alloc&e) { cout<obj = obj; this->close = close; } ~CharGuard() { close(obj); } private: char *obj; hClose close; }; void closeFunc(char *str) { delete[] str; } // 用法 char* s = new char[20]; CharGuard guard(s, closeFunc); 我们将堆对象的生命周期绑定到了栈对象上,这意味我们无需担心资源发生泄露的问题。关于 CharGuard 更好的实现参见 `ScopeGuard `_ C++ 提供了 ``智能指针`` ,通过指针指针,我们可以做到整份 C++ 代码不再出现 new/delete ,通过这种“智能”手段管理资源可以大大减少资源泄露的可能。 使用引用管理对象声明周期 **************************************** 引用提供了一种“智能”管理对象生命周期的方法,其可以简单地分为左值引用和右值引用,其中右值引用可以用来延长对象的生命周期,左值引用可以用来消除代码中的指针变量。 具体做法为: - 在函数中返回指针变量的解引用,则函数返回值为左值引用 - 在函数中使用 ``std::move`` 移动临时对象,返回值为右值引用 .. important:: 如果不使用 ``std::move`` 移动临时对象,而返回值却为引用,则得到的引用为 ``悬空引用`` ,结果未定义。 例如: .. code-block:: cpp File& getChar(File* file){ return *file; } File&& getFile(const char *_FileName, const char *_Mode){ File file(_FileName, _Mode); return move(file); } .. seealso:: - `Does ScopeGuard use really lead to better code? `_ - `你应该掌握的C++ RAII手法:Scopegaurd `_ - `ScopeGuard 介绍和实现 `_ - `Boost.ScopeExit `_ - `C++11(及现代C++风格)和快速迭代式开发 `_ - `Generic: Change the Way You Write Exception-Safe Code — Forever `_ - `C++中的RAII介绍 `_