## 题目:C++ 里如何使用 RAII 实现安全的文件操作

在 C++ 中,RAII(资源获取即初始化)是一种重要的编程技巧,它通过对象的生命周期来管理资源,避免泄漏。下面以文件操作为例,演示如何使用 RAII 实现一个安全、易用且符合现代 C++ 风格的文件处理类。

1. 需求分析

  • 打开文件后立即获取句柄,文件关闭时自动释放句柄。
  • 支持读写操作,且错误信息能被用户捕获。
  • 不需要用户手动调用 close(),避免忘记关闭导致的资源泄漏。
  • 兼容 POSIX 与 Windows 两大平台。

2. 设计思路

  • 创建一个 FileHandle 类,在构造函数中打开文件,在析构函数中关闭文件。
  • 把文件描述符(int on POSIX, HANDLE on Windows)包装成私有成员,防止外部直接操作。
  • 提供 write()read() 等成员函数,内部使用系统调用完成实际操作。
  • 对错误情况抛出 std::runtime_error 或使用错误码返回,视情况而定。

3. 关键实现代码

// file_handle.hpp
#pragma once
#include <string>
#include <stdexcept>
#include <vector>
#include <system_error>

#ifdef _WIN32
#include <windows.h>
#else
#include <fcntl.h>
#include <unistd.h>
#endif

class FileHandle {
public:
    enum Mode { Read, Write, Append };

    FileHandle(const std::string& path, Mode mode) {
#ifdef _WIN32
        DWORD dwDesiredAccess = 0;
        DWORD dwCreationDisposition = 0;
        if (mode == Read) {
            dwDesiredAccess = GENERIC_READ;
            dwCreationDisposition = OPEN_EXISTING;
        } else if (mode == Write) {
            dwDesiredAccess = GENERIC_WRITE;
            dwCreationDisposition = CREATE_ALWAYS;
        } else { // Append
            dwDesiredAccess = FILE_APPEND_DATA;
            dwCreationDisposition = OPEN_ALWAYS;
        }
        hFile = CreateFileA(path.c_str(), dwDesiredAccess,
                            FILE_SHARE_READ, nullptr,
                            dwCreationDisposition,
                            FILE_ATTRIBUTE_NORMAL, nullptr);
        if (hFile == INVALID_HANDLE_VALUE)
            throw std::system_error(GetLastError(), std::system_category(), "Failed to open file");
#else
        int flags = 0;
        if (mode == Read) flags = O_RDONLY;
        else if (mode == Write) flags = O_WRONLY | O_CREAT | O_TRUNC;
        else flags = O_WRONLY | O_CREAT | O_APPEND;
        fd = open(path.c_str(), flags, 0666);
        if (fd < 0)
            throw std::system_error(errno, std::generic_category(), "Failed to open file");
#endif
    }

    // 禁止拷贝
    FileHandle(const FileHandle&) = delete;
    FileHandle& operator=(const FileHandle&) = delete;

    // 移动构造与赋值
    FileHandle(FileHandle&& other) noexcept {
#ifdef _WIN32
        hFile = other.hFile;
        other.hFile = INVALID_HANDLE_VALUE;
#else
        fd = other.fd;
        other.fd = -1;
#endif
    }
    FileHandle& operator=(FileHandle&& other) noexcept {
        if (this != &other) {
            close();
#ifdef _WIN32
            hFile = other.hFile;
            other.hFile = INVALID_HANDLE_VALUE;
#else
            fd = other.fd;
            other.fd = -1;
#endif
        }
        return *this;
    }

    ~FileHandle() { close(); }

    std::size_t write(const std::vector <char>& buf) {
#ifdef _WIN32
        DWORD written = 0;
        if (!WriteFile(hFile, buf.data(), static_cast <DWORD>(buf.size()), &written, nullptr))
            throw std::system_error(GetLastError(), std::system_category(), "Write failed");
        return written;
#else
        ssize_t res = ::write(fd, buf.data(), buf.size());
        if (res < 0)
            throw std::system_error(errno, std::generic_category(), "Write failed");
        return res;
#endif
    }

    std::size_t read(std::vector <char>& buf, std::size_t count) {
#ifdef _WIN32
        DWORD read = 0;
        if (!ReadFile(hFile, buf.data(), static_cast <DWORD>(count), &read, nullptr))
            throw std::system_error(GetLastError(), std::system_category(), "Read failed");
        return read;
#else
        ssize_t res = ::read(fd, buf.data(), count);
        if (res < 0)
            throw std::system_error(errno, std::generic_category(), "Read failed");
        return res;
#endif
    }

private:
    void close() {
#ifdef _WIN32
        if (hFile != INVALID_HANDLE_VALUE) {
            CloseHandle(hFile);
            hFile = INVALID_HANDLE_VALUE;
        }
#else
        if (fd >= 0) {
            ::close(fd);
            fd = -1;
        }
#endif
    }

#ifdef _WIN32
    HANDLE hFile = INVALID_HANDLE_VALUE;
#else
    int fd = -1;
#endif
};

4. 使用示例

#include "file_handle.hpp"
#include <iostream>
#include <vector>

int main() {
    try {
        // 写入文件
        FileHandle writer("test.txt", FileHandle::Append);
        std::string data = "Hello, RAII!\n";
        writer.write(std::vector <char>(data.begin(), data.end()));

        // 读取文件
        FileHandle reader("test.txt", FileHandle::Read);
        std::vector <char> buffer(1024);
        std::size_t n = reader.read(buffer, buffer.size());
        std::string content(buffer.begin(), buffer.begin() + n);
        std::cout << "File content:\n" << content << std::endl;
    } catch (const std::system_error& e) {
        std::cerr << "系统错误: " << e.what() << std::endl;
        return 1;
    } catch (const std::exception& e) {
        std::cerr << "错误: " << e.what() << std::endl;
        return 1;
    }
    return 0;
}

5. 优点与扩展

  • 安全:资源在对象生命周期结束时自动释放,避免泄漏。
  • 可维护:将平台差异隐藏在实现细节中,调用者无需关心。
  • 可扩展:可以在此基础上添加 seek、文件大小查询、锁定等功能。

6. 结语

RAII 通过让资源的拥有者成为 C++ 对象的生命周期来简化错误处理与资源释放。文件操作是最直观的示例之一,掌握好后可以在更大范围内(如网络套接字、内存映射文件等)运用相同思路,编写出更可靠、更易维护的系统代码。

发表评论