C++ 写入 CSV 文件

在 C++ 中,如果我们想把程序里的学生成绩单、商品清单或统计结果导出成表格文件保存起来,生成 CSV 文件是最简单省事的做法。

与前面读取 CSV 文件时需要拆分字段和转换类型相比,生成 CSV 文件要直接得多。因为 CSV 本质上就是普通的纯文本文件,我们先写入第 1 行表头,再把每一行数据的各个字段使用英文逗号(,)隔开并写入文件就可以了。

写入基础 CSV 文件

向 CSV 文件写入数据时,我们可以直接使用 <fstream> 头文件中的输出文件流 std::ofstream。拼接每一行时记住一个基本规则:同一行相邻的两个字段之间使用英文逗号(,)隔开,整行最后一个字段的后面直接跟换行符 \n。

语法:

std::ofstream file("文件路径");
file << "列名1,列名2,列名3\n";
file << val1 << ',' << val2 << ',' << val3 << '\n';

说明:

第 1 次写入的 "列名1,列名2,列名3\n" 是表头行,后面每一行数据的字段个数和顺序都要与表头保持一致。如果在打开文件时只传入文件名(比如 "scores.csv"),文件默认保存在程序运行时的当前目录下,运行结束后可以直接使用记事本或 Excel 打开查看。

示例 1:创建并写入 CSV 文件

#include <fstream>
#include <iostream>

int main() {
    std::ofstream file("scores.csv");
    if (!file.is_open()) {
        std::cout << "无法创建文件 scores.csv\n";
        return 1;
    }

    // 写入表头
    file << "id,name,score\n";

    // 写入学生数据
    file << 1 << ',' << "Jack" << ',' << 95 << '\n';
    file << 2 << ',' << "Lucy" << ',' << 88 << '\n';
    file << 3 << ',' << "Tom" << ',' << 92 << '\n';

    std::cout << "scores.csv 写入完成!\n";
    return 0;
}

运行结果如下。

scores.csv 写入完成!

分析:

在上面代码中,file << "id,name,score\n"; 表示先向文件写入第 1 行表头,紧接着的 3 条输出语句依次写入了 3 名学生的学号、姓名和成绩。运行程序后,打开当前目录下生成的 scores.csv 文件,可以看到里面的文本内容如下:

id,name,score
1,Jack,95
2,Lucy,88
3,Tom,92

初学的小伙伴在拼接 CSV 数据行时要特别注意:只有相邻两个字段之间才加英文逗号,每一行最后一个字段的后面直接跟换行符 '\n' 即可。千万不要在行末多写一个逗号,否则会导致每一行末尾多出一列空字段。

// 正确方式
file << id << ',' << name << ',' << score << '\n';

// 错误方式
file << id << ',' << name << ',' << score << ",\n";

批量导出与追加 CSV 数据

在实际开发中,需要导出的表格数据通常保存在 std::vector 容器和自定义结构体中,而不是一行一行直接写死在输出语句里。下面我们就来看看批量导出结构体数据与追加记录的具体写法。

1. 批量导出结构体数据

当内存中有一个 std::vector<Student> 容器时,我们先写入一行表头,再配合范围 for 循环遍历容器,把每个结构体变量的成员按顺序写入文件就可以了。下面我们来看一个例子。

示例 2:批量导出结构体数据

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

struct Student {
    int id;
    std::string name;
    int score;
};

int main() {
    std::vector<Student> students = {
        {1, "Jack", 95},
        {2, "Lucy", 88},
        {3, "Tom", 92}
    };

    std::ofstream file("students_output.csv");
    if (!file.is_open()) {
        std::cout << "无法创建文件 students_output.csv\n";
        return 1;
    }

    // 写入表头
    file << "id,name,score\n";

    // 遍历容器写入每条记录
    for (const auto& s : students) {
        file << s.id << ',' << s.name << ',' << s.score << '\n';
    }

    std::cout << "导出记录数:" << students.size() << '\n';
    return 0;
}

运行结果如下。

导出记录数:3

分析:

在这个例子中,每一个 Student 结构体变量恰好对应 CSV 文件中的一行记录,而结构体内部的 id、name、score 三个成员变量则对应同一行里的 3 列。通过范围 for 循环遍历 students 容器,哪怕容器里有成百上千条记录,我们也能快速将其导出为标准的 CSV 文件。

2. 向已有 CSV 文件追加记录

默认情况下,std::ofstream 打开文件时会先清空原有内容。如果磁盘上已经有了一份包含表头和历史数据的 CSV 文件,我们只想往文件末尾继续添加新行,可以在打开文件时传入 std::ios::app 追加模式,其中 app 是 “append(追加)” 的缩写。

这里小伙伴们要注意一点:因为原文件的第 1 行已经有了表头,所以在追加模式下直接写入新的数据行即可,不要再重复写入表头了。请看下面例子。

示例 3:向 CSV 文件追加记录

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

int main() {
    // 创建初始 CSV 文件
    std::ofstream initFile("students_log.csv");
    initFile << "id,name,score\n";
    initFile << "1,Jack,95\n";
    initFile << "2,Lucy,88\n";
    initFile.close();

    // 以追加模式写入第 3 条记录
    std::ofstream appendFile("students_log.csv", std::ios::app);
    if (!appendFile.is_open()) {
        std::cout << "无法打开文件 students_log.csv\n";
        return 1;
    }
    appendFile << 3 << ',' << "Tom" << ',' << 92 << '\n';
    appendFile.close();

    // 读取文件验证结果
    std::ifstream inFile("students_log.csv");
    std::string line;
    while (std::getline(inFile, line)) {
        std::cout << line << '\n';
    }
    return 0;
}

运行结果如下。

id,name,score
1,Jack,95
2,Lucy,88
3,Tom,92

分析:

在这个例子中,打开 appendFile 时传入的 std::ios::app 参数表示在文件末尾继续追加内容,不会清空原有数据。从运行结果可以看出,原有的表头和前两条数据完好保留,新写入的 3,Tom,92 直接排在了文件最后一行。

另外,当我们在同一个函数里先写入文件、紧接着又打开同一个文件读取验证时,记得像上面代码那样先调用 close() 关闭输出流。这样可以确保内存缓冲区中的数据已经完整写入磁盘,避免后面读取时拿到不完整甚至空白的内容。

特殊字符的转义处理

如果所有字段都是纯数字或普通文本,直接通过逗号拼接没有任何问题。但如果某个字符串字段本身就包含英文逗号或双引号,比如地址 "Guangzhou, China" 或商品名称 "15\" Laptop",直接输出就会生成下面这样的错误文本:

id,title,city
1,15" Laptop,Guangzhou, China

可以看到,原本属于同一个地址的 "Guangzhou, China" 因为中间带有逗号,读取时就会拆成两列,导致整行变成 4 列。

按照通用 CSV 格式规范,当写入的字段内容可能包含特殊字符时,我们需要遵循以下 2 条转义规则:

  • 外层加双引号包裹:只要字段内容中包含英文逗号(,)、双引号(")或换行符,我们就需要使用一对英文双引号把整个字段括起来。
  • 内部双引号成对转义:如果字段内容本身包含英文双引号("),我们需要将其替换成两个连续的双引号 ""。

我们可以把这两条规则封装成一个简单的辅助函数 escapeCsvField(),在写入字符串字段时直接调用。

示例 4:转义并写入特殊字段

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

// 转义包含特殊字符的 CSV 字段
std::string escapeCsvField(const std::string& field) {
    if (field.find_first_of(",\"\n") == std::string::npos) {
        return field;
    }

    std::string result = "\"";
    for (char ch : field) {
        if (ch == '"') {
            result += "\"\"";    // 双引号转义为两个
        } else {
            result += ch;
        }
    }
    result += "\"";
    return result;
}

int main() {
    std::ofstream outFile("items.csv");
    if (!outFile.is_open()) {
        std::cout << "无法创建文件 items.csv\n";
        return 1;
    }

    std::string title = escapeCsvField("15\" Laptop");
    std::string city = escapeCsvField("Guangzhou, China");

    outFile << "id,title,city\n";
    outFile << 1 << ',' << title << ',' << city << '\n';
    outFile.close();

    // 读取文件查看转义效果
    std::ifstream inFile("items.csv");
    std::string line;
    while (std::getline(inFile, line)) {
        std::cout << line << '\n';
    }
    return 0;
}

运行结果如下。

id,title,city
1,"15"" Laptop","Guangzhou, China"

分析:

在 escapeCsvField() 函数中,field.find_first_of(",\"\n") 用来检查字符串中是否出现了指定字符集里的任意一个字符。这里的参数 ",\"\n" 一共包含 3 个特殊字符:英文逗号 ,、双引号 "(在 C++ 字符串字面量中写作 \")以及换行符 \n。

如果查找结果等于 std::string::npos,说明字段里不含这些特殊字符,直接原样返回即可。一旦发现特殊字符,函数就会在首尾各拼上一个双引号,并通过 result += "\"\""; 把内部的单个双引号替换成两个连续的双引号 ""。

比如内存中的商品名 15" Laptop,内部的双引号先替换成 "",首尾再套上一对双引号。写入文件后就会变成标准的 "15"" Laptop",使用 Excel 打开时能够准确识别为 3 列。

常见问题

1. 为什么生成的 CSV 文件使用 Windows 版 Excel 直接打开时,中文会显示乱码?

在现代 C++ 开发中,源代码和输出的字符串通常采用 UTF-8 编码。但 Windows 版的 Excel 在直接双击打开不带 BOM(字节顺序标记)的 CSV 文件时,默认会按系统本地的 GBK 编码去读取,因此会把 UTF-8 中文显示为乱码。

如果我们希望生成的 CSV 文件在代码编辑器和 Windows Excel 中都能正常显示中文,可以在创建文件后、写入第 1 行表头前,先执行一行 file << "\xEF\xBB\xBF"; 写入 3 个字节的 UTF-8 BOM 标记,接着再正常写入表头和数据就可以了。

上一篇:

下一篇:

给站长反馈

绿叶网正在不断完善中,小伙伴们如果发现任何问题,还望多多给站长反馈,谢谢!

邮箱:lvyenet@vip.qq.com

「绿叶网」服务号
绿叶网服务号放大
关注服务号,微信也能看教程。
绿叶网服务号