在 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 标记,接着再正常写入表头和数据就可以了。
