33人参与 • 2026-07-29 • C/C++
csv(comma-separated values)是数据交换领域使用最为广泛的纯文本格式之一。无论是数据库导出、日志采集、配置下发还是跨系统数据同步,csv 都因其简单、人类可读、工具链成熟而占据重要地位。
在服务器端 c++ 开发中,经常面临这样的需求:
然而,c++ 标准库并未提供开箱即用的 csv 解析与生成能力。常见的第三方方案(如将数据映射到 std::vector<std::vector<std::string>>)虽然功能完备,但在高频调用或嵌入式环境中会引入不必要的内存开销和模板膨胀。因此,设计一个零依赖、轻量级、调用方自主管理数据格式的 csv 读写库具有实际工程价值。
本库的设计目标明确且克制:
// 读取:函数内部分配 buffer,调用方 delete[] 释放
int readcsv(const std::string& filename, char*& buffer, size_t& len);
// 覆写:先写 title(表头),再写 buffer(数据行)
int writecsv(const std::string& filename,
const std::string& title,
const std::string& buffer);
// 追加:在文件末尾追加数据
int appendcsv(const std::string& filename, const std::string& buffer);
返回值约定:
这是设计中最关键的决策点。对于 readcsv:
delete[],生命周期由使用者掌控,不存在悬挂指针风险;\0 结尾:虽按二进制方式读取,但额外追加一个空字符,方便调用方直接作为 c 字符串使用。writecsv 和 appendcsv 中的 buffer 参数由调用方自由构造——可以是一个完整的 csv 多行字符串,也可以是一行记录。库不做任何格式校验,保证了最大的灵活性:
// 调用方自行拼接 csv 行
std::string data = "1,alice,95\n2,bob,87\n3,charlie,92";
writecsv("out.csv", "id,name,score", data);
库仅负责一项防御性处理:若 title 或 buffer 不以换行结尾,自动补一个 \n。这确保了 appendcsv 追加时始终从新行开始,避免数据粘连。
文件结构如下,具体的完整代码见附录。
. ├── csv_utils.h # 头文件:api 声明 ├── csv_utils.cpp # 实现文件 └── main.cpp # 测试文件
int readcsv(const std::string& filename, char*& buffer, size_t& len)
{
// ① 以二进制 + 末尾定位模式打开,直接获取文件大小
std::ifstream ifs(filename, std::ios::binary | std::ios::ate);
if (!ifs.is_open()) { buffer = nullptr; len = 0; return -1; }
const std::streamsize filesize = ifs.tellg();
// ② 分配 buffer,多 1 字节用于 '\0'
try {
buffer = new char[static_cast<size_t>(filesize) + 1];
} catch (const std::bad_alloc&) { buffer = nullptr; len = 0; return -2; }
// ③ 回到文件头,一次性读取全部内容
ifs.seekg(0, std::ios::beg);
if (!ifs.read(buffer, filesize)) { delete[] buffer; buffer = nullptr; len = 0; return -3; }
len = static_cast<size_t>(filesize);
buffer[len] = '\0';
return 0;
}
设计取舍分析:
std::ios::ate 模式打开后在文件尾,一次 tellg() 即可获得文件大小,避免 seekg 到末尾再回来的二次 io 开销;std::bad_alloc,确保不会因 new 抛出异常而破坏调用方的异常安全策略;delete[] buffer 并将输出参数归零,保证失败后调用方拿到的是干净状态。int writecsv(const std::string& filename,
const std::string& title,
const std::string& buffer)
{
// trunc 模式:覆盖写入
std::ofstream ofs(filename, std::ios::out | std::ios::trunc);
if (!ofs.is_open()) return -1;
ofs << title;
if (title.empty() || title.back() != '\n') ofs << '\n';
if (!buffer.empty()) {
ofs << buffer;
if (buffer.back() != '\n') ofs << '\n';
}
return ofs.good() ? 0 : -2;
}
int appendcsv(const std::string& filename, const std::string& buffer)
{
// app 模式:追加写入
std::ofstream ofs(filename, std::ios::out | std::ios::app);
if (!ofs.is_open()) return -1;
if (!buffer.empty()) {
ofs << buffer;
if (buffer.back() != '\n') ofs << '\n';
}
return ofs.good() ? 0 : -2;
}
writecsv 与 appendcsv 的唯一区别在于文件打开模式 —— trunc vs app,其余逻辑保持一致,这遵循了 dry 原则且便于维护。
// 写入
writecsv("report.csv",
"交易id,金额,状态",
"t001,1500,成功\nt002,2300,成功\nt003,800,失败");
// 读取
char* buf = nullptr;
size_t len = 0;
if (readcsv("report.csv", buf, len) == 0) {
std::cout << std::string(buf, len) << std::endl;
delete[] buf; // ← 调用方负责释放
}
writecsv("log.csv", "时间,级别,消息", "");
// 运行中持续追加
appendcsv("log.csv", "2026-07-27 10:00,info,服务启动");
appendcsv("log.csv", "2026-07-27 10:05,warn,内存使用率超过80%");
appendcsv("log.csv", "2026-07-27 10:30,info,任务执行完成");
g++ -std=c++11 -wall -wextra -o csv_test csv_utils.cpp main.cpp ./csv_test
| 测试编号 | 测试场景 | 验证要点 |
|---|---|---|
| 测试 1 | writecsv + readcsv 往返 | 写入后读取内容完全一致 |
| 测试 2 | writecsv + appendcsv 多次追加 | 追加后记录顺序与完整性 |
| 测试 3 | 读取不存在的文件 | 返回 -1,输出参数为 nullptr / 0 |
| 测试 4 | 空数据写入 | 仅标题行,无数据行的边界情况 |
| 测试 5 | 含逗号与引号的转义字段 | 带引号的字段不破坏 csv 结构 |
========== csv utils 测试 ========== === 测试 1:writecsv + readcsv === 文件长度: 51 bytes 文件内容: id,name,score 1,alice,95 2,bob,87 3,charlie,92 ✓ 测试 1 通过 === 测试 2:writecsv + appendcsv + readcsv === id,name,score 1,alice,95 2,bob,87 3,charlie,92 4,diana,88 5,eve,100 ✓ 测试 2 通过 === 测试 3:读取不存在的文件 === 返回码: -1(预期 -1) ✓ 测试 3 通过 === 测试 4:空数据写入 === 文件内容: [col1,col2,col3 ] ✓ 测试 4 通过 === 测试 5:含转义字段的 csv === id,name,description 1,"smith, john","he said ""hello""" 2,jane doe,normal text ✓ 测试 5 通过 ========== 全部测试通过 ==========
golang在语言层支持很多常见的格式或协议的解析,而c++只能找第三方库或自己造轮子。本文件实现的csv读取库用于临时顶替数据库功能。实际场景并未使用到。
#pragma once
#include <cstddef>
#include <string>
/**
* @brief 读取csv文件全部内容到内存缓冲区
* @param filename 文件路径
* @param buffer 输出参数,函数内部分配内存,调用方负责释放(使用 delete[])
* @param len 输出参数,读取到的数据长度(不含结尾'\0')
* @return 0 成功
* -1 文件打开失败
* -2 内存分配失败
* -3 读取失败
*/
int readcsv(const std::string& filename, char*& buffer, size_t& len);
/**
* @brief 写入csv文件(覆盖模式),包含标题行和数据内容
* @param filename 文件路径
* @param title 标题行(csv表头字符串)
* @param buffer 数据内容(csv格式行数据,由调用方构造)
* @return 0 成功
* -1 文件打开失败
* -2 写入失败
*/
int writecsv(const std::string& filename,
const std::string& title,
const std::string& buffer);
/**
* @brief 追加数据到已有csv文件末尾
* @param filename 文件路径
* @param buffer 要追加的数据内容(由调用方构造)
* @return 0 成功
* -1 文件打开失败
* -2 写入失败
*/
int appendcsv(const std::string& filename, const std::string& buffer);
#include "csv_utils.h"
#include <cstring>
#include <fstream>
// ──────────────────────────────────────────────
// readcsv – 读取整个文件到调用方可释放的缓冲区
// ──────────────────────────────────────────────
int readcsv(const std::string& filename, char*& buffer, size_t& len)
{
// 1. 以二进制方式打开,并在末尾定位以获取文件大小
std::ifstream ifs(filename, std::ios::binary | std::ios::ate);
if (!ifs.is_open()) {
buffer = nullptr;
len = 0;
return -1; // 文件打开失败
}
const std::streamsize filesize = ifs.tellg();
if (filesize < 0) {
buffer = nullptr;
len = 0;
return -1;
}
// 2. 分配缓冲区(多一个字节存放 '\0',方便调用方当作 c 字符串使用)
try {
buffer = new char[static_cast<size_t>(filesize) + 1];
} catch (const std::bad_alloc&) {
buffer = nullptr;
len = 0;
return -2; // 内存分配失败
}
// 3. 回到文件头并读取全部内容
ifs.seekg(0, std::ios::beg);
if (!ifs.read(buffer, filesize)) {
delete[] buffer;
buffer = nullptr;
len = 0;
return -3; // 读取失败
}
len = static_cast<size_t>(filesize);
buffer[len] = '\0'; // 以 null 结尾,方便作为 c 字符串使用
return 0;
}
// ──────────────────────────────────────────────
// writecsv – 覆盖写入标题行 + 数据内容
// ──────────────────────────────────────────────
int writecsv(const std::string& filename,
const std::string& title,
const std::string& buffer)
{
std::ofstream ofs(filename, std::ios::out | std::ios::trunc);
if (!ofs.is_open()) {
return -1;
}
// 写入标题行,末尾确保换行
ofs << title;
if (title.empty() || title.back() != '\n') {
ofs << '\n';
}
// 写入数据缓冲区
if (!buffer.empty()) {
ofs << buffer;
// buffer 不以换行结尾时补一个换行,保证后续 appendcsv 从新行开始
if (buffer.back() != '\n') {
ofs << '\n';
}
}
if (!ofs.good()) {
return -2;
}
return 0;
}
// ──────────────────────────────────────────────
// appendcsv – 追加数据到文件末尾
// ──────────────────────────────────────────────
int appendcsv(const std::string& filename, const std::string& buffer)
{
std::ofstream ofs(filename, std::ios::out | std::ios::app);
if (!ofs.is_open()) {
return -1;
}
if (!buffer.empty()) {
ofs << buffer;
if (buffer.back() != '\n') {
ofs << '\n';
}
}
if (!ofs.good()) {
return -2;
}
return 0;
}
#include "csv_utils.h"
#include <cassert>
#include <cstring>
#include <iostream>
#include <sstream>
#include <string>
// ──────────────────────────────────────────────
// 测试 1 :writecsv → readcsv,验证写入与读取一致
// ──────────────────────────────────────────────
static int test_write_and_read()
{
const std::string filename = "test_write.csv";
// 构造 csv 数据(调用方决定 buffer 内容)
const std::string title = "id,name,score";
std::ostringstream oss;
oss << "1,alice,95\n"
<< "2,bob,87\n"
<< "3,charlie,92";
const std::string data = oss.str();
// 写入
int ret = writecsv(filename, title, data);
assert(ret == 0);
// 读取
char* buf = nullptr;
size_t len = 0;
ret = readcsv(filename, buf, len);
assert(ret == 0);
assert(buf != nullptr);
assert(len > 0);
std::cout << "=== 测试 1:writecsv + readcsv ===" << std::endl;
std::cout << "文件长度: " << len << " bytes" << std::endl;
std::cout << "文件内容:" << std::endl;
std::cout << buf << std::endl;
// 调用方负责释放
delete[] buf;
// 验证内容正确
ret = readcsv(filename, buf, len);
assert(ret == 0);
std::string content(buf, len);
assert(content.find("id,name,score") != std::string::npos);
assert(content.find("1,alice,95") != std::string::npos);
assert(content.find("2,bob,87") != std::string::npos);
assert(content.find("3,charlie,92") != std::string::npos);
delete[] buf;
std::cout << "✓ 测试 1 通过" << std::endl << std::endl;
return 0;
}
// ──────────────────────────────────────────────
// 测试 2 :appendcsv 追加内容,验证追加后文件完整
// ──────────────────────────────────────────────
static int test_append()
{
const std::string filename = "test_append.csv";
// 先写入基础内容
const std::string title = "id,name,score";
const std::string data = "1,alice,95\n2,bob,87";
int ret = writecsv(filename, title, data);
assert(ret == 0);
// 追加内容
const std::string appenddata = "3,charlie,92\n4,diana,88";
ret = appendcsv(filename, appenddata);
assert(ret == 0);
// 再追加一条
ret = appendcsv(filename, "5,eve,100");
assert(ret == 0);
// 读取并验证
char* buf = nullptr;
size_t len = 0;
ret = readcsv(filename, buf, len);
assert(ret == 0);
std::string content(buf, len);
delete[] buf;
std::cout << "=== 测试 2:writecsv + appendcsv + readcsv ===" << std::endl;
std::cout << content << std::endl;
assert(content.find("id,name,score") != std::string::npos);
assert(content.find("1,alice,95") != std::string::npos);
assert(content.find("2,bob,87") != std::string::npos);
assert(content.find("3,charlie,92") != std::string::npos);
assert(content.find("4,diana,88") != std::string::npos);
assert(content.find("5,eve,100") != std::string::npos);
std::cout << "✓ 测试 2 通过" << std::endl << std::endl;
return 0;
}
// ──────────────────────────────────────────────
// 测试 3 :读取不存在的文件,验证错误码
// ──────────────────────────────────────────────
static int test_read_nonexistent()
{
const std::string filename = "nonexistent.csv";
char* buf = nullptr;
size_t len = 0;
int ret = readcsv(filename, buf, len);
assert(ret == -1);
assert(buf == nullptr);
assert(len == 0);
std::cout << "=== 测试 3:读取不存在的文件 ===" << std::endl;
std::cout << "返回码: " << ret << "(预期 -1)" << std::endl;
std::cout << "✓ 测试 3 通过" << std::endl << std::endl;
return 0;
}
// ──────────────────────────────────────────────
// 测试 4 :空数据写入与读取
// ──────────────────────────────────────────────
static int test_empty_data()
{
const std::string filename = "test_empty.csv";
// 仅写入标题,数据为空
int ret = writecsv(filename, "col1,col2,col3", "");
assert(ret == 0);
char* buf = nullptr;
size_t len = 0;
ret = readcsv(filename, buf, len);
assert(ret == 0);
assert(len > 0);
std::string content(buf, len);
delete[] buf;
std::cout << "=== 测试 4:空数据写入 ===" << std::endl;
std::cout << "文件内容: [" << content << "]" << std::endl;
assert(content.find("col1,col2,col3") != std::string::npos);
std::cout << "✓ 测试 4 通过" << std::endl << std::endl;
return 0;
}
// ──────────────────────────────────────────────
// 测试 5 :含逗号和引号的 csv 字段(转义场景)
// ──────────────────────────────────────────────
static int test_escaped_fields()
{
const std::string filename = "test_escape.csv";
// 包含引号和逗号的字段
const std::string title = "id,name,description";
const std::string data = "1,\"smith, john\",\"he said \"\"hello\"\"\"\n"
"2,jane doe,normal text";
int ret = writecsv(filename, title, data);
assert(ret == 0);
char* buf = nullptr;
size_t len = 0;
ret = readcsv(filename, buf, len);
assert(ret == 0);
std::string content(buf, len);
delete[] buf;
std::cout << "=== 测试 5:含转义字段的 csv ===" << std::endl;
std::cout << content << std::endl;
assert(content.find("id,name,description") != std::string::npos);
assert(content.find("\"smith, john\"") != std::string::npos);
assert(content.find("\"\"hello\"\"") != std::string::npos);
std::cout << "✓ 测试 5 通过" << std::endl << std::endl;
return 0;
}
// ──────────────────────────────────────────────
// main
// ──────────────────────────────────────────────
int main()
{
std::cout << "========== csv utils 测试 ==========" << std::endl << std::endl;
test_write_and_read();
test_append();
test_read_nonexistent();
test_empty_data();
test_escaped_fields();
std::cout << "========== 全部测试通过 ==========" << std::endl;
return 0;
}
title: 一种轻量级c++ csv文件读写库的实现方案
urlname: a-lightweight-c+±csv-file-library-implementation
tags:
本文介绍一个基于 c++11 标准库实现的轻量级 csv 文件读写工具库。该库提供三个核心 api —— readcsv、writecsv、appendcsv,覆盖了 csv 文件的读取、覆写与追加场景。
csv(comma-separated values)是数据交换领域使用最为广泛的纯文本格式之一。无论是数据库导出、日志采集、配置下发还是跨系统数据同步,csv 都因其简单、人类可读、工具链成熟而占据重要地位。
在服务器端 c++ 开发中,经常面临这样的需求:
然而,c++ 标准库并未提供开箱即用的 csv 解析与生成能力。常见的第三方方案(如将数据映射到 std::vector<std::vector<std::string>>)虽然功能完备,但在高频调用或嵌入式环境中会引入不必要的内存开销和模板膨胀。因此,设计一个零依赖、轻量级、调用方自主管理数据格式的 csv 读写库具有实际工程价值。
本库的设计目标明确且克制:
// 读取:函数内部分配 buffer,调用方 delete[] 释放
int readcsv(const std::string& filename, char*& buffer, size_t& len);
// 覆写:先写 title(表头),再写 buffer(数据行)
int writecsv(const std::string& filename,
const std::string& title,
const std::string& buffer);
// 追加:在文件末尾追加数据
int appendcsv(const std::string& filename, const std::string& buffer);
返回值约定:
这是设计中最关键的决策点。对于 readcsv:
delete[],生命周期由使用者掌控,不存在悬挂指针风险;\0 结尾:虽按二进制方式读取,但额外追加一个空字符,方便调用方直接作为 c 字符串使用。writecsv 和 appendcsv 中的 buffer 参数由调用方自由构造——可以是一个完整的 csv 多行字符串,也可以是一行记录。库不做任何格式校验,保证了最大的灵活性:
// 调用方自行拼接 csv 行
std::string data = "1,alice,95\n2,bob,87\n3,charlie,92";
writecsv("out.csv", "id,name,score", data);
库仅负责一项防御性处理:若 title 或 buffer 不以换行结尾,自动补一个 \n。这确保了 appendcsv 追加时始终从新行开始,避免数据粘连。
文件结构如下,具体的完整代码见附录。
. ├── csv_utils.h # 头文件:api 声明 ├── csv_utils.cpp # 实现文件 └── main.cpp # 测试文件
int readcsv(const std::string& filename, char*& buffer, size_t& len)
{
// ① 以二进制 + 末尾定位模式打开,直接获取文件大小
std::ifstream ifs(filename, std::ios::binary | std::ios::ate);
if (!ifs.is_open()) { buffer = nullptr; len = 0; return -1; }
const std::streamsize filesize = ifs.tellg();
// ② 分配 buffer,多 1 字节用于 '\0'
try {
buffer = new char[static_cast<size_t>(filesize) + 1];
} catch (const std::bad_alloc&) { buffer = nullptr; len = 0; return -2; }
// ③ 回到文件头,一次性读取全部内容
ifs.seekg(0, std::ios::beg);
if (!ifs.read(buffer, filesize)) { delete[] buffer; buffer = nullptr; len = 0; return -3; }
len = static_cast<size_t>(filesize);
buffer[len] = '\0';
return 0;
}
设计取舍分析:
std::ios::ate 模式打开后在文件尾,一次 tellg() 即可获得文件大小,避免 seekg 到末尾再回来的二次 io 开销;std::bad_alloc,确保不会因 new 抛出异常而破坏调用方的异常安全策略;delete[] buffer 并将输出参数归零,保证失败后调用方拿到的是干净状态。int writecsv(const std::string& filename,
const std::string& title,
const std::string& buffer)
{
// trunc 模式:覆盖写入
std::ofstream ofs(filename, std::ios::out | std::ios::trunc);
if (!ofs.is_open()) return -1;
ofs << title;
if (title.empty() || title.back() != '\n') ofs << '\n';
if (!buffer.empty()) {
ofs << buffer;
if (buffer.back() != '\n') ofs << '\n';
}
return ofs.good() ? 0 : -2;
}
int appendcsv(const std::string& filename, const std::string& buffer)
{
// app 模式:追加写入
std::ofstream ofs(filename, std::ios::out | std::ios::app);
if (!ofs.is_open()) return -1;
if (!buffer.empty()) {
ofs << buffer;
if (buffer.back() != '\n') ofs << '\n';
}
return ofs.good() ? 0 : -2;
}
writecsv 与 appendcsv 的唯一区别在于文件打开模式 —— trunc vs app,其余逻辑保持一致,这遵循了 dry 原则且便于维护。
// 写入
writecsv("report.csv",
"交易id,金额,状态",
"t001,1500,成功\nt002,2300,成功\nt003,800,失败");
// 读取
char* buf = nullptr;
size_t len = 0;
if (readcsv("report.csv", buf, len) == 0) {
std::cout << std::string(buf, len) << std::endl;
delete[] buf; // ← 调用方负责释放
}
writecsv("log.csv", "时间,级别,消息", "");
// 运行中持续追加
appendcsv("log.csv", "2026-07-27 10:00,info,服务启动");
appendcsv("log.csv", "2026-07-27 10:05,warn,内存使用率超过80%");
appendcsv("log.csv", "2026-07-27 10:30,info,任务执行完成");
g++ -std=c++11 -wall -wextra -o csv_test csv_utils.cpp main.cpp ./csv_test
| 测试编号 | 测试场景 | 验证要点 |
|---|---|---|
| 测试 1 | writecsv + readcsv 往返 | 写入后读取内容完全一致 |
| 测试 2 | writecsv + appendcsv 多次追加 | 追加后记录顺序与完整性 |
| 测试 3 | 读取不存在的文件 | 返回 -1,输出参数为 nullptr / 0 |
| 测试 4 | 空数据写入 | 仅标题行,无数据行的边界情况 |
| 测试 5 | 含逗号与引号的转义字段 | 带引号的字段不破坏 csv 结构 |
========== csv utils 测试 ========== === 测试 1:writecsv + readcsv === 文件长度: 51 bytes 文件内容: id,name,score 1,alice,95 2,bob,87 3,charlie,92 ✓ 测试 1 通过 === 测试 2:writecsv + appendcsv + readcsv === id,name,score 1,alice,95 2,bob,87 3,charlie,92 4,diana,88 5,eve,100 ✓ 测试 2 通过 === 测试 3:读取不存在的文件 === 返回码: -1(预期 -1) ✓ 测试 3 通过 === 测试 4:空数据写入 === 文件内容: [col1,col2,col3 ] ✓ 测试 4 通过 === 测试 5:含转义字段的 csv === id,name,description 1,"smith, john","he said ""hello""" 2,jane doe,normal text ✓ 测试 5 通过 ========== 全部测试通过 ==========
golang在语言层支持很多常见的格式或协议的解析,而c++只能找第三方库或自己造轮子。本文件实现的csv读取库用于临时顶替数据库功能。实际场景并未使用到。
#pragma once
#include <cstddef>
#include <string>
/**
* @brief 读取csv文件全部内容到内存缓冲区
* @param filename 文件路径
* @param buffer 输出参数,函数内部分配内存,调用方负责释放(使用 delete[])
* @param len 输出参数,读取到的数据长度(不含结尾'\0')
* @return 0 成功
* -1 文件打开失败
* -2 内存分配失败
* -3 读取失败
*/
int readcsv(const std::string& filename, char*& buffer, size_t& len);
/**
* @brief 写入csv文件(覆盖模式),包含标题行和数据内容
* @param filename 文件路径
* @param title 标题行(csv表头字符串)
* @param buffer 数据内容(csv格式行数据,由调用方构造)
* @return 0 成功
* -1 文件打开失败
* -2 写入失败
*/
int writecsv(const std::string& filename,
const std::string& title,
const std::string& buffer);
/**
* @brief 追加数据到已有csv文件末尾
* @param filename 文件路径
* @param buffer 要追加的数据内容(由调用方构造)
* @return 0 成功
* -1 文件打开失败
* -2 写入失败
*/
int appendcsv(const std::string& filename, const std::string& buffer);
#include "csv_utils.h"
#include <cstring>
#include <fstream>
// ──────────────────────────────────────────────
// readcsv – 读取整个文件到调用方可释放的缓冲区
// ──────────────────────────────────────────────
int readcsv(const std::string& filename, char*& buffer, size_t& len)
{
// 1. 以二进制方式打开,并在末尾定位以获取文件大小
std::ifstream ifs(filename, std::ios::binary | std::ios::ate);
if (!ifs.is_open()) {
buffer = nullptr;
len = 0;
return -1; // 文件打开失败
}
const std::streamsize filesize = ifs.tellg();
if (filesize < 0) {
buffer = nullptr;
len = 0;
return -1;
}
// 2. 分配缓冲区(多一个字节存放 '\0',方便调用方当作 c 字符串使用)
try {
buffer = new char[static_cast<size_t>(filesize) + 1];
} catch (const std::bad_alloc&) {
buffer = nullptr;
len = 0;
return -2; // 内存分配失败
}
// 3. 回到文件头并读取全部内容
ifs.seekg(0, std::ios::beg);
if (!ifs.read(buffer, filesize)) {
delete[] buffer;
buffer = nullptr;
len = 0;
return -3; // 读取失败
}
len = static_cast<size_t>(filesize);
buffer[len] = '\0'; // 以 null 结尾,方便作为 c 字符串使用
return 0;
}
// ──────────────────────────────────────────────
// writecsv – 覆盖写入标题行 + 数据内容
// ──────────────────────────────────────────────
int writecsv(const std::string& filename,
const std::string& title,
const std::string& buffer)
{
std::ofstream ofs(filename, std::ios::out | std::ios::trunc);
if (!ofs.is_open()) {
return -1;
}
// 写入标题行,末尾确保换行
ofs << title;
if (title.empty() || title.back() != '\n') {
ofs << '\n';
}
// 写入数据缓冲区
if (!buffer.empty()) {
ofs << buffer;
// buffer 不以换行结尾时补一个换行,保证后续 appendcsv 从新行开始
if (buffer.back() != '\n') {
ofs << '\n';
}
}
if (!ofs.good()) {
return -2;
}
return 0;
}
// ──────────────────────────────────────────────
// appendcsv – 追加数据到文件末尾
// ──────────────────────────────────────────────
int appendcsv(const std::string& filename, const std::string& buffer)
{
std::ofstream ofs(filename, std::ios::out | std::ios::app);
if (!ofs.is_open()) {
return -1;
}
if (!buffer.empty()) {
ofs << buffer;
if (buffer.back() != '\n') {
ofs << '\n';
}
}
if (!ofs.good()) {
return -2;
}
return 0;
}
#include "csv_utils.h"
#include <cassert>
#include <cstring>
#include <iostream>
#include <sstream>
#include <string>
// ──────────────────────────────────────────────
// 测试 1 :writecsv → readcsv,验证写入与读取一致
// ──────────────────────────────────────────────
static int test_write_and_read()
{
const std::string filename = "test_write.csv";
// 构造 csv 数据(调用方决定 buffer 内容)
const std::string title = "id,name,score";
std::ostringstream oss;
oss << "1,alice,95\n"
<< "2,bob,87\n"
<< "3,charlie,92";
const std::string data = oss.str();
// 写入
int ret = writecsv(filename, title, data);
assert(ret == 0);
// 读取
char* buf = nullptr;
size_t len = 0;
ret = readcsv(filename, buf, len);
assert(ret == 0);
assert(buf != nullptr);
assert(len > 0);
std::cout << "=== 测试 1:writecsv + readcsv ===" << std::endl;
std::cout << "文件长度: " << len << " bytes" << std::endl;
std::cout << "文件内容:" << std::endl;
std::cout << buf << std::endl;
// 调用方负责释放
delete[] buf;
// 验证内容正确
ret = readcsv(filename, buf, len);
assert(ret == 0);
std::string content(buf, len);
assert(content.find("id,name,score") != std::string::npos);
assert(content.find("1,alice,95") != std::string::npos);
assert(content.find("2,bob,87") != std::string::npos);
assert(content.find("3,charlie,92") != std::string::npos);
delete[] buf;
std::cout << "✓ 测试 1 通过" << std::endl << std::endl;
return 0;
}
// ──────────────────────────────────────────────
// 测试 2 :appendcsv 追加内容,验证追加后文件完整
// ──────────────────────────────────────────────
static int test_append()
{
const std::string filename = "test_append.csv";
// 先写入基础内容
const std::string title = "id,name,score";
const std::string data = "1,alice,95\n2,bob,87";
int ret = writecsv(filename, title, data);
assert(ret == 0);
// 追加内容
const std::string appenddata = "3,charlie,92\n4,diana,88";
ret = appendcsv(filename, appenddata);
assert(ret == 0);
// 再追加一条
ret = appendcsv(filename, "5,eve,100");
assert(ret == 0);
// 读取并验证
char* buf = nullptr;
size_t len = 0;
ret = readcsv(filename, buf, len);
assert(ret == 0);
std::string content(buf, len);
delete[] buf;
std::cout << "=== 测试 2:writecsv + appendcsv + readcsv ===" << std::endl;
std::cout << content << std::endl;
assert(content.find("id,name,score") != std::string::npos);
assert(content.find("1,alice,95") != std::string::npos);
assert(content.find("2,bob,87") != std::string::npos);
assert(content.find("3,charlie,92") != std::string::npos);
assert(content.find("4,diana,88") != std::string::npos);
assert(content.find("5,eve,100") != std::string::npos);
std::cout << "✓ 测试 2 通过" << std::endl << std::endl;
return 0;
}
// ──────────────────────────────────────────────
// 测试 3 :读取不存在的文件,验证错误码
// ──────────────────────────────────────────────
static int test_read_nonexistent()
{
const std::string filename = "nonexistent.csv";
char* buf = nullptr;
size_t len = 0;
int ret = readcsv(filename, buf, len);
assert(ret == -1);
assert(buf == nullptr);
assert(len == 0);
std::cout << "=== 测试 3:读取不存在的文件 ===" << std::endl;
std::cout << "返回码: " << ret << "(预期 -1)" << std::endl;
std::cout << "✓ 测试 3 通过" << std::endl << std::endl;
return 0;
}
// ──────────────────────────────────────────────
// 测试 4 :空数据写入与读取
// ──────────────────────────────────────────────
static int test_empty_data()
{
const std::string filename = "test_empty.csv";
// 仅写入标题,数据为空
int ret = writecsv(filename, "col1,col2,col3", "");
assert(ret == 0);
char* buf = nullptr;
size_t len = 0;
ret = readcsv(filename, buf, len);
assert(ret == 0);
assert(len > 0);
std::string content(buf, len);
delete[] buf;
std::cout << "=== 测试 4:空数据写入 ===" << std::endl;
std::cout << "文件内容: [" << content << "]" << std::endl;
assert(content.find("col1,col2,col3") != std::string::npos);
std::cout << "✓ 测试 4 通过" << std::endl << std::endl;
return 0;
}
// ──────────────────────────────────────────────
// 测试 5 :含逗号和引号的 csv 字段(转义场景)
// ──────────────────────────────────────────────
static int test_escaped_fields()
{
const std::string filename = "test_escape.csv";
// 包含引号和逗号的字段
const std::string title = "id,name,description";
const std::string data = "1,\"smith, john\",\"he said \"\"hello\"\"\"\n"
"2,jane doe,normal text";
int ret = writecsv(filename, title, data);
assert(ret == 0);
char* buf = nullptr;
size_t len = 0;
ret = readcsv(filename, buf, len);
assert(ret == 0);
std::string content(buf, len);
delete[] buf;
std::cout << "=== 测试 5:含转义字段的 csv ===" << std::endl;
std::cout << content << std::endl;
assert(content.find("id,name,description") != std::string::npos);
assert(content.find("\"smith, john\"") != std::string::npos);
assert(content.find("\"\"hello\"\"") != std::string::npos);
std::cout << "✓ 测试 5 通过" << std::endl << std::endl;
return 0;
}
// ──────────────────────────────────────────────
// main
// ──────────────────────────────────────────────
int main()
{
std::cout << "========== csv utils 测试 ==========" << std::endl << std::endl;
test_write_and_read();
test_append();
test_read_nonexistent();
test_empty_data();
test_escaped_fields();
std::cout << "========== 全部测试通过 ==========" << std::endl;
return 0;
}
以上就是基于c++ 11标准库实现轻量级csv文件读写工具库的详细内容,更多关于c++轻量级csv文件读写库的资料请关注代码网其它相关文章!
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论