C/C++ 文件操作全攻略:Linux、Windows、macOS 三平台实战

前言

文件操作是每个 C/C++ 开发者的必修课。但说实话,很多人写了几年代码,对文件操作的理解还停留在 fopen("test.txt", "r") 的阶段。更别提跨平台开发时,Linux 和 Windows 那套完全不同的底层 API 能把人搞疯。

这篇文章不讲虚的,直接从三种层面把文件操作讲透:C 标准库C++ 标准库平台系统调用。顺便把 VS2022 那堆安全函数警告的来龙去脉说清楚,以及一个多数人踩过但没深究的坑——Unicode 文件名在 Windows 上的乱码问题


一、C 标准库方式(跨平台通用)

C 标准库的 <stdio.h> 是跨平台文件操作的基础。无论在 Linux、Windows 还是 macOS 上,这套 API 的行为都是一致的。

1.1 传统方式:fopen / fread / fwrite / fclose

这是最经典的一套,从 K&R 时代用到现在。

#include <stdio.h>
#include <string.h>

int main() {
    FILE* fp = fopen("test.txt", "w");
    if (fp == NULL) {
        perror("打开文件失败");
        return 1;
    }
    
    fwrite("Hello World", 1, 11, fp);
    fclose(fp);
    return 0;
}

优点:简单、跨平台、所有编译器都支持。
缺点:错误信息模糊(只能拿到 NULL,不知道具体为啥失败)、缺乏参数校验。

1.2 安全增强方式:fopen_s(C11 Annex K)

VS2022 里直接写 fopen 会报 C4996 警告:

'fopen': This function or variable may be unsafe. Consider using fopen_s instead.

这是微软在 Visual Studio 中把传统 C 库函数标记为“弃用”的结果。fopen_s 是 C11 标准附录 K 定义的边界检查函数,函数原型如下:

errno_t fopen_s(FILE** streamptr, const char* filename, const char* mode);

关键区别:

  • 返回值不再是 FILE*,而是 errno_t 错误码(0 表示成功)
  • 文件指针通过输出参数返回
  • 自动校验参数是否为空指针
#include <stdio.h>
#include <errno.h>

int main() {
    FILE* fp = NULL;
    errno_t err = fopen_s(&fp, "test.txt", "w");
    if (err != 0) {
        printf("打开文件失败,错误码:%d\n", err);
        return 1;
    }
    fwrite("Hello World", 1, 11, fp);
    fclose(fp);
    return 0;
}

注意fopen_s 是 C11 Annex K 的可选特性,不是所有编译器都支持。GCC 和 Clang 默认不实现这套 _s 函数。如果你的代码需要跨平台编译(比如在 Linux 上用 GCC),直接用 fopen 更稳妥——或者用条件编译做适配。

1.3 Windows 平台特有:_fsopen(共享模式)

fopen_s 有个坑:用这个函数打开的文件默认是独占的,其他程序无法同时访问。

如果你需要多进程共享文件,Windows 下要用 _fsopen

#include <stdio.h>
#include <share.h>

FILE* fp = _fsopen("test.txt", "w", _SH_DENYNO);  // 允许其他程序读写

第三个参数控制共享模式:

  • _SH_DENYNO:允许读写共享
  • _SH_DENYRD:拒绝读共享
  • _SH_DENYWR:拒绝写共享

Linux/macOS 下默认就是共享的,不需要这套机制。


二、C++ 标准库方式(面向对象,推荐)

C++ 的 <fstream> 提供了面向对象的文件流操作,是现代 C++ 项目最推荐的方式

2.1 核心类

用途
std::ofstream 输出文件流(写文件)
std::ifstream 输入文件流(读文件)
std::fstream 输入输出文件流(读写均可)

2.2 文本文件读写

#include <iostream>
#include <fstream>
#include <string>

int main() {
    // 写文件
    std::ofstream outFile("example.txt");
    if (!outFile.is_open()) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    outFile << "Hello, C++ File I/O!" << std::endl;
    outFile << "整数: " << 42 << ", 浮点数: " << 3.14159 << std::endl;
    outFile.close();

    // 读文件
    std::ifstream inFile("example.txt");
    if (!inFile.is_open()) {
        std::cerr << "无法打开文件" << std::endl;
        return 1;
    }
    std::string line;
    while (std::getline(inFile, line)) {
        std::cout << line << std::endl;
    }
    inFile.close();
    return 0;
}

2.3 二进制文件读写

#include <fstream>
#include <vector>

struct Data {
    int id;
    double value;
    char name[32];
};

int main() {
    Data d = {1, 3.14, "test"};
    
    // 写二进制
    std::ofstream outFile("data.bin", std::ios::binary);
    outFile.write(reinterpret_cast<const char*>(&d), sizeof(Data));
    outFile.close();
    
    // 读二进制
    Data d2;
    std::ifstream inFile("data.bin", std::ios::binary);
    inFile.read(reinterpret_cast<char*>(&d2), sizeof(Data));
    inFile.close();
    return 0;
}

关键点:二进制模式必须显式指定 std::ios::binary。在 Windows 上,文本模式会自动转换换行符(\n\r\n),二进制模式不会。

2.4 文件打开模式

模式 含义
std::ios::in
std::ios::out 写(默认会清空文件)
std::ios::app 追加
std::ios::ate 打开后定位到末尾
std::ios::trunc 清空文件
std::ios::binary 二进制模式

可以组合使用:std::ios::in | std::ios::out | std::ios::binary


三、C++17 filesystem(目录操作与路径处理)

<filesystem> 是 C++17 引入的跨平台文件系统操作库。它不负责文件内容的读写,但负责路径处理、目录遍历、文件状态查询等。

#include <iostream>
#include <filesystem>
namespace fs = std::filesystem;

int main() {
    // 路径处理 - 自动适配不同系统的路径分隔符
    fs::path p = fs::path("data") / "subdir" / "file.txt";
    std::cout << p.string() << std::endl;
    // Linux:   data/subdir/file.txt
    // Windows: data\subdir\file.txt
    
    // 检查文件/目录是否存在
    if (fs::exists("example.txt")) {
        std::cout << "文件存在,大小: " << fs::file_size("example.txt") << " 字节" << std::endl;
    }
    
    // 遍历目录
    if (fs::exists(".") && fs::is_directory(".")) {
        for (const auto& entry : fs::directory_iterator(".")) {
            std::cout << entry.path().filename().string() << std::endl;
        }
    }
    
    // 创建目录
    fs::create_directory("new_folder");
    fs::create_directories("a/b/c/d");  // 递归创建
    
    // 重命名/移动
    fs::rename("old.txt", "new.txt");
    
    // 删除
    fs::remove("temp.txt");
    fs::remove_all("temp_folder");  // 递归删除
    return 0;
}

编译时注意链接:

  • GCC/Clang:g++ -std=c++17 main.cpp -lstdc++fs
  • MSVC(VS2022):自动链接,无需额外操作
  • macOS 特别说明:Apple Clang 默认使用 libc++,文件系统支持已内置,通常无需额外链接库。若遇到链接错误,尝试 g++ -std=c++17 main.cpp -lc++fs

四、平台系统调用(底层 API)

4.1 Linux / macOS:POSIX 系统调用

POSIX 标准定义了 openreadwriteclose 等系统调用。

#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

int main() {
    // 打开/创建文件(权限 0644)
    int fd = open("test.txt", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    if (fd == -1) {
        fprintf(stderr, "open 失败: %s\n", strerror(errno));
        return 1;
    }
    
    const char* msg = "Hello POSIX\n";
    ssize_t written = write(fd, msg, strlen(msg));
    if (written == -1) {
        fprintf(stderr, "write 失败: %s\n", strerror(errno));
    }
    
    close(fd);
    return 0;
}

常用 flag

  • O_RDONLY:只读
  • O_WRONLY:只写
  • O_RDWR:读写
  • O_CREAT:不存在则创建
  • O_TRUNC:清空
  • O_APPEND:追加
  • O_EXCL:与 O_CREAT 配合,文件存在则报错

4.2 Windows:Win32 API

Windows 下对应的是 CreateFileReadFileWriteFileCloseHandle

#include <windows.h>
#include <iostream>

int main() {
    HANDLE hFile = CreateFileA(
        "test.txt",
        GENERIC_WRITE,
        FILE_SHARE_READ,          // 共享读
        NULL,
        CREATE_ALWAYS,            // 总是创建/覆盖
        FILE_ATTRIBUTE_NORMAL,
        NULL
    );
    
    if (hFile == INVALID_HANDLE_VALUE) {
        std::cerr << "CreateFile 失败,错误码: " << GetLastError() << std::endl;
        return 1;
    }
    
    const char* msg = "Hello Windows\n";
    DWORD bytesWritten;
    WriteFile(hFile, msg, strlen(msg), &bytesWritten, NULL);
    CloseHandle(hFile);
    return 0;
}

绝大部分场景下不需要直接调用系统 API。C 标准库和 C++ 标准库已经封装好了跨平台接口,直接用就行。只有在需要平台特定功能(如 Windows 的文件共享控制、内存映射文件)时才需要深入系统层。


五、VS2022 安全函数与跨平台 Unicode 处理详解

5.1 为什么会报 C4996?

Visual Studio 认为 fopenscanfstrcpy 等传统 C 函数存在安全隐患:

  • 不检查参数是否为空指针
  • 不检查缓冲区大小
  • 错误信息不精确(如 strerror 返回内部静态缓冲区指针,多线程不安全)

所以微软在 MSVC 中将这些函数标记为“已弃用”,建议改用 _s 后缀的安全版本。

5.2 常见 _s 函数对照表

不安全函数 安全替代
fopen fopen_s
freopen freopen_s
fclose fclose_s
scanf scanf_s
printf printf_s
strcpy strcpy_s
strcat strcat_s
strerror strerror_s

5.3 处理 strerror 警告的跨平台方式

MSVC 报 C4996 是因为 strerror 返回的指针指向线程局部静态缓冲区,在多线程环境下可能被覆盖。strerror_s 要求用户传入缓冲区,从根本上杜绝了竞态条件。

跨平台适配代码

#ifdef _MSC_VER
    #define STRERROR_S(buf, size, err) strerror_s((buf), (size), (err))
#else
    // POSIX 标准有 strerror_r(线程安全),但 GNU 和 XSI 版本行为不同
    // 此处为了兼容性,直接使用 strerror,并配合 _CRT_SECURE_NO_WARNINGS
    #define STRERROR_S(buf, size, err) strerror(err)
#endif

更干净的做法是在源文件顶部统一抑制:

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#include <errno.h>

这样既能使用 strerror,又在 MSVC 下不报警。在后面的完整代码中,我采用了这种方案,并额外封装了一个 safe_strerror 辅助函数,在 MSVC 下显式调用 strerror_s,在 POSIX 系统下调 strerror

5.4 Unicode 文件名与内容处理(重点)

为什么 Windows 上中文文件名会乱码或打开失败?

Windows 内核使用 UTF-16 LE 编码存储文件名,而 fopenstd::ifstream 接受的 char* 字符串在 Windows 上默认按 系统代码页(ANSI,如 CP936/GBK) 解释。当你传入一个 UTF-8 编码的 char* 字符串(例如从网络接收或 Linux 移植过来的代码),Windows 会把它当作 ANSI 去解码,导致找不到文件或创建出乱码文件名。

Linux / macOS 则大多使用 UTF-8 作为系统语言环境,char* 直接对应 UTF-8,因此很少出现这个问题。

解决方案

  1. C 标准库:在 Windows 上使用 _wfopen_s(宽字符版本),传入 UTF-16 编码的 wchar_t*。需要先将 UTF-8 字符串转换为 UTF-16(可使用 MultiByteToWideChar)。
  2. C++ 标准库(推荐):使用 std::filesystem::path(C++17),它内部抽象了编码差异。构造 path 时传入 UTF-8 字符串(Linux 下)或宽字符串(Windows 下)。MSVC 的 std::fstream 构造函数重载了 const wchar_t*,因此可以直接传入 path.c_str() 返回的宽字符串。
  3. C++ 通用技巧:统一使用 std::wstring 存储文件路径,在 Linux 下用 std::wstring_convert 转换为 UTF-8(C++11 后可用,但 C++17 起该工具弃用,推荐直接使用 std::filesystem::path)。

实践建议:如果你的项目需要跨平台处理中文路径,C++17 的 <filesystem> 是最省心的选择。下面的完整代码中,C++ 示例统一使用 fs::path 构建路径,C 示例则通过条件编译在 Windows 下调 _wfopen_s,在 Linux/macOS 下调 fopen(假定系统为 UTF-8 环境)。


六、完整代码示例(消除 Emoji,内嵌跨平台注释)

6.1 C 风格完整示例(条件编译 + 手动兼容 macOS/Linux)

/**
 * file_io_c.c - C 风格文件操作完整示例
 * 
 * 跨平台设计思路:
 *   1. Windows (MSVC):使用 _wfopen_s 支持 UTF-16 文件名(原生 Unicode)
 *   2. macOS / Linux (Clang / GCC):使用标准 fopen,依赖系统 UTF-8 语言环境
 *   3. 因 macOS/Linux 不实现 C11 Annex K(_s 安全函数),故在 POSIX 分支中
 *      手动定义 errno_t 为 int,使代码语法与 MSVC 保持一致
 * 
 * 编译指令:
 *   Windows (VS2022):   直接编译为 .c 文件
 *   macOS (Clang):      gcc -std=c11 file_io_c.c -o file_io_c
 *   Linux (GCC):        gcc -std=c11 file_io_c.c -o file_io_c
 */

#define _CRT_SECURE_NO_WARNINGS  // 仅对 MSVC 生效:抑制 fopen/strerror 弃用警告
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

// ==================== 平台适配层 ====================
#ifdef _MSC_VER
    // ---------- Windows 平台 (MSVC) ----------
    // 原因:Windows 内核使用 UTF-16 编码文件名,char* 默认按系统代码页(如 GBK)解释
    //       为了支持中文/日文等 Unicode 路径,必须使用宽字符版本的 _wfopen_s
    #include <wchar.h>
    
    // 安全错误信息函数:MSVC 提供线程安全的 strerror_s
    static const char* safe_strerror(int err, char* buf, size_t size) {
        if (strerror_s(buf, size, err) == 0) {
            return buf;
        }
        return "Unknown error";
    }
    
    // 文件打开宏:使用宽字符版本,模式字符串也需加 L 前缀
    #define FOPEN_W(fp, path, mode) _wfopen_s(&(fp), (path), L##mode)
    
#else
    // ---------- POSIX 平台 (macOS / Linux) ----------
    // 关键修复:macOS 的 Clang 和 Linux 的 GCC 均不实现 C11 Annex K 标准,
    // 即不认识 errno_t、fopen_s、strerror_s 等类型和函数。
    // 此处手动定义 errno_t 为 int,使后续代码中的 errno_t err 能正常编译。
    typedef int errno_t;  
    
    // POSIX 下直接使用标准 strerror(注意:它返回内部静态缓冲区指针,
    // 在多线程环境下不安全,但单线程演示代码中足够使用)
    static const char* safe_strerror(int err, char* buf, size_t size) {
        (void)buf; (void)size;  // 屏蔽未使用参数警告
        return strerror(err);
    }
    
    // 文件打开宏:直接调用标准 fopen,传入 char* 路径(假定系统 LANG 为 UTF-8)
    // 注意:此处强制将模式字符串视为普通 char*,不加 L 前缀
    #define FOPEN_W(fp, path, mode) ((fp) = fopen((path), (mode)), 0)
#endif

// ==================== 功能函数 ====================

// 写文本文件(支持 Unicode 文件名)
void write_text_file(const wchar_t* filename_w) {
    FILE* fp = NULL;
    errno_t err = 0;
    
#ifdef _MSC_VER
    // Windows:使用宽字符版本
    err = _wfopen_s(&fp, filename_w, L"w");
#else
    // POSIX:将 wchar_t* 转为 char*(此处简化,假设文件名只含 ASCII 或系统 UTF-8)
    // 实际项目中可用 wcstombs 或 iconv 进行转换
    char filename_mb[256];
    wcstombs(filename_mb, filename_w, sizeof(filename_mb));
    fp = fopen(filename_mb, "w");
    err = (fp == NULL) ? EIO : 0;
#endif

    if (err != 0 || fp == NULL) {
        char errbuf[128];
        fprintf(stderr, "[ERROR] 打开文件失败: %s\n", safe_strerror(errno, errbuf, sizeof(errbuf)));
        return;
    }
    fprintf(fp, "这是 C 标准库写入的文本,支持中文内容。\n");
    fprintf(fp, "编译日期: %s\n", __DATE__);
    fclose(fp);
    printf("[OK] 文本文件写入成功\n");
}

// 读文本文件
void read_text_file(const wchar_t* filename_w) {
    FILE* fp = NULL;
    errno_t err = 0;
    
#ifdef _MSC_VER
    err = _wfopen_s(&fp, filename_w, L"r");
#else
    char filename_mb[256];
    wcstombs(filename_mb, filename_w, sizeof(filename_mb));
    fp = fopen(filename_mb, "r");
    err = (fp == NULL) ? EIO : 0;
#endif

    if (err != 0 || fp == NULL) {
        char errbuf[128];
        fprintf(stderr, "[ERROR] 打开文件失败: %s\n", safe_strerror(errno, errbuf, sizeof(errbuf)));
        return;
    }
    char buffer[256];
    printf("[READ] 文件内容:\n");
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("  %s", buffer);
    }
    fclose(fp);
}

int main() {
    // 使用宽字符串支持中文文件名(Windows 下为 UTF-16,POSIX 下会被转码)
    wchar_t filename[] = L"c_文本测试.txt";
    
    write_text_file(filename);
    read_text_file(filename);
    return 0;
}

6.2 C++ 风格完整示例(C++17 filesystem 完美处理 Unicode)

/**
 * file_io_cpp.cpp - C++ 风格文件操作完整示例
 * 
 * 跨平台设计思路:
 *   1. 使用 std::filesystem::path 透明处理 Unicode 路径,无需手动转码
 *   2. RAII 自动管理资源,避免忘记 fclose
 *   3. 错误处理同时支持流状态检查和异常捕获
 *   4. 使用条件编译处理 strerror_s / strerror 差异(但 C++ 中更推荐用 std::error_code)
 * 
 * 编译指令:
 *   Windows (VS2022):   直接编译 (C++17)
 *   macOS (Clang):      g++ -std=c++17 file_io_cpp.cpp -o file_io_cpp   (无需 -lstdc++fs)
 *   Linux (GCC):        g++ -std=c++17 file_io_cpp.cpp -o file_io_cpp -lstdc++fs
 *   (若 macOS 提示链接错误,尝试 g++ -std=c++17 file_io_cpp.cpp -lc++fs -o file_io_cpp)
 */

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <filesystem>
#include <iomanip>

namespace fs = std::filesystem;

// ========== 辅助:跨平台错误信息 ==========
// C++ 中更推荐使用 std::error_code,但为演示 strerror 处理,保留此函数
static std::string get_error_message(int err) {
#ifdef _MSC_VER
    char buf[128];
    if (strerror_s(buf, sizeof(buf), err) == 0) {
        return std::string(buf);
    }
    return "Unknown error";
#else
    // POSIX 下使用 strerror(注意:非线程安全,但演示足够)
    return std::string(strerror(err));
#endif
}

// ========== 文本文件操作(Unicode 文件名) ==========
void text_file_demo() {
    std::cout << "\n=== 文本文件操作 ===" << std::endl;
    
    // fs::path 自动处理编码:Windows 转 UTF-16,Linux/macOS 保持 UTF-8
    fs::path filepath = fs::path("cpp_文本") / "测试.txt";
    
    // 写入
    std::ofstream outFile(filepath);
    if (!outFile.is_open()) {
        std::cerr << "[ERROR] 无法打开文件写入" << std::endl;
        return;
    }
    outFile << "Hello, C++ File I/O! 你好,世界!\n";
    outFile << "这是第二行,包含中文。\n";
    outFile << "整数: " << 42 << ", 浮点数: " << 3.14159 << "\n";
    outFile.close();
    std::cout << "[OK] 文本文件写入成功: " << filepath.string() << std::endl;
    
    // 读取
    std::ifstream inFile(filepath);
    if (!inFile.is_open()) {
        std::cerr << "[ERROR] 无法打开文件读取" << std::endl;
        return;
    }
    std::cout << "[READ] 读取内容:\n";
    std::string line;
    while (std::getline(inFile, line)) {
        std::cout << "  " << line << std::endl;
    }
    inFile.close();
}

// ========== 二进制文件操作 ==========
struct PlayerData {
    int id;
    float health;
    float position[3];
    char name[32];
};

void binary_file_demo() {
    std::cout << "\n=== 二进制文件操作 ===" << std::endl;
    
    fs::path filepath = "player.dat";
    
    // 写入
    PlayerData player = {1001, 85.5f, {10.0f, 20.0f, 30.0f}, "Hero"};
    std::ofstream outFile(filepath, std::ios::binary);
    if (!outFile.is_open()) {
        std::cerr << "[ERROR] 无法打开二进制文件写入" << std::endl;
        return;
    }
    outFile.write(reinterpret_cast<const char*>(&player), sizeof(PlayerData));
    outFile.close();
    std::cout << "[OK] 二进制文件写入成功: " << filepath.string() << std::endl;
    
    // 读取
    PlayerData loaded;
    std::ifstream inFile(filepath, std::ios::binary);
    if (!inFile.is_open()) {
        std::cerr << "[ERROR] 无法打开二进制文件读取" << std::endl;
        return;
    }
    inFile.read(reinterpret_cast<char*>(&loaded), sizeof(PlayerData));
    inFile.close();
    
    std::cout << "[READ] 玩家数据:\n";
    std::cout << "  ID: " << loaded.id << "\n";
    std::cout << "  Health: " << loaded.health << "\n";
    std::cout << "  Position: (" << loaded.position[0] << ", " 
              << loaded.position[1] << ", " << loaded.position[2] << ")\n";
    std::cout << "  Name: " << loaded.name << std::endl;
}

// ========== 追加与随机访问 ==========
void append_and_seek_demo() {
    std::cout << "\n=== 追加与随机访问 ===" << std::endl;
    
    fs::path logpath = "log.txt";
    
    // 追加模式
    std::ofstream appendFile(logpath, std::ios::app);
    if (appendFile.is_open()) {
        appendFile << "[LOG] 程序启动于 " << __TIME__ << "\n";
        appendFile.close();
        std::cout << "[OK] 日志追加成功" << std::endl;
    }
    
    // 读写模式 + 随机访问
    fs::path datapath = "data.txt";
    std::fstream rwFile(datapath, std::ios::in | std::ios::out | std::ios::trunc);
    if (!rwFile.is_open()) {
        std::cerr << "[ERROR] 无法打开文件" << std::endl;
        return;
    }
    
    rwFile << "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    rwFile.seekp(5, std::ios::beg);  // 定位到第 5 个字符
    rwFile << "12345";
    rwFile.close();
    
    // 读取验证
    std::ifstream readFile(datapath);
    std::string content;
    readFile >> content;
    std::cout << "[READ] 修改后的内容: " << content << std::endl;
    // 输出: ABCDE12345FGHIJKLMNOPQRSTUVWXYZ
}

// ========== filesystem 目录操作 ==========
void filesystem_demo() {
    std::cout << "\n=== filesystem 目录操作 ===" << std::endl;
    
    fs::path root = "测试文件夹";
    
    // 创建目录
    fs::create_directory(root);
    std::cout << "[OK] 创建目录: " << root.string() << std::endl;
    
    // 创建嵌套目录
    fs::create_directories(root / "sub" / "subsub");
    std::cout << "[OK] 创建嵌套目录: " << (root / "sub" / "subsub").string() << std::endl;
    
    // 创建测试文件
    std::ofstream(root / "文件1.txt") << "File 1 内容";
    std::ofstream(root / "文件2.txt") << "File 2 内容";
    std::ofstream(root / "sub" / "笔记.txt") << "笔记内容";
    
    // 遍历目录(递归)
    std::cout << "[INFO] 遍历 " << root.string() << ":\n";
    for (const auto& entry : fs::recursive_directory_iterator(root)) {
        std::cout << "  " << (fs::is_directory(entry) ? "[DIR]" : "[FILE]")
                  << " " << entry.path().filename().string() << std::endl;
    }
    
    // 文件信息
    auto file1 = root / "文件1.txt";
    if (fs::exists(file1)) {
        auto size = fs::file_size(file1);
        std::cout << "[INFO] 文件1.txt 大小: " << size << " 字节" << std::endl;
    }
    
    // 清理
    fs::remove_all(root);
    std::cout << "[OK] 清理完成" << std::endl;
}

// ========== 错误处理(含 strerror 跨平台适配) ==========
void error_handling_demo() {
    std::cout << "\n=== 错误处理 ===" << std::endl;
    
    fs::path missing = "不存在的文件.txt";
    std::ifstream file(missing);
    if (file.fail()) {
        std::cerr << "[ERROR] 打开失败: " << get_error_message(errno) << std::endl;
    }
    
    // 使用 exceptions() 抛出异常
    std::ifstream file2;
    file2.exceptions(std::ifstream::failbit | std::ifstream::badbit);
    try {
        file2.open(missing.string());
    } catch (const std::ios_base::failure& e) {
        std::cerr << "[ERROR] 异常捕获: " << e.what() << std::endl;
    }
}

int main() {
    std::cout << "========== C++ 文件操作完整示例 ==========" << std::endl;
    
    text_file_demo();
    binary_file_demo();
    append_and_seek_demo();
    filesystem_demo();
    error_handling_demo();
    
    std::cout << "\n========== 所有示例执行完毕 ==========" << std::endl;
    return 0;
}

七、方法对比表格

维度 C 标准库 (fopen) C 安全函数 (fopen_s) C++ fstream + filesystem POSIX 系统调用 Win32 API
跨平台性 完全跨平台 仅 MSVC 原生支持 完全跨平台 (C++17) Linux/macOS 专用 Windows 专用
Unicode 文件名支持 依赖系统代码页 (Windows 易乱码) _wfopen_s 可支持 原生支持 (fs::path) 需 UTF-8 语言环境 原生 UTF-16 (CreateFileW)
C++ 风格 (RAII)
错误信息精度 模糊 (NULL) 精确 (errno_t) 精确 (failbit/异常) 精确 (errno) 精确 (GetLastError)
参数校验
缓冲区安全 需手动保证 内置校验 内置校验 需手动保证 需手动保证
二进制支持 需指定 “b” 需指定 “b” 需指定 binary
文件共享控制 是 (fcntl) 是 (共享参数)
学习曲线 中高
推荐场景 纯 C 项目、嵌入式 Windows 专用 C 项目 现代跨平台 C++ 项目 需要底层控制 Windows 底层开发

八、总结与建议

  1. 跨平台 C++ 项目首选 std::fstream + std::filesystem
    RAII 自动回收资源,fs::path 帮你挡掉 Windows / Linux 路径编码差异。处理中文路径时,直接用 fs::path 构造,无需手动转码。

  2. 纯 C 项目,按平台选 API

    • Windows + VS2022:用 _wfopen_s(宽字符)处理 Unicode 路径,避免 ANSI 代码页乱码。
    • Linux/macOS + GCC/Clang:用 fopen,确保系统 LANG 环境变量为 en_US.UTF-8zh_CN.UTF-8
    • 条件编译是处理这类差异的标准做法。特别提醒:macOS 的 Clang 不识别 errno_t,需在 POSIX 分支中手动 typedef int errno_t;
  3. 别迷信 _s 函数,也别无视警告
    C11 Annex K 的实现不是强制性的。在跨平台代码里,我会在 MSVC 下调用 strerror_s,在 POSIX 下调 strerror,同时用 _CRT_SECURE_NO_WARNINGS 抑制冗余告警,保持代码干净。

  4. 关于 Unicode 内容的读写
    文件内容编码与文件名编码是两回事。写入中文内容时,建议统一使用 UTF-8 编码 存储文本文件(Linux 默认,Windows 上很多编辑器也支持)。C++ 的 std::string 存储 UTF-8 字节序列是安全的,std::wstring 则对应 UTF-16(Windows)或 UTF-32(Linux)。若要在 Windows 控制台正确显示中文,记得调用 SetConsoleOutputCP(CP_UTF8)

  5. 二进制模式别漏掉
    跨平台处理二进制数据时,ios::binary"b" 模式必须显式指定。否则 Windows 会对 \n0x1A 做特殊转换,破坏数据。

  6. macOS 编译小贴士

    • C 代码:直接 gcc -std=c11 file_io_c.c,无需额外链接。
    • C++ 代码:g++ -std=c++17 file_io_cpp.cpp,一般无需 -lstdc++fs(Apple Clang 内置支持),若遇链接错误,可尝试 -lc++fs
    • 若遇到 errno_t 未定义,请参照本文 C 代码中的 typedef int errno_t; 修复。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐