C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++轻量级CSV文件读写库

基于C++ 11标准库实现轻量级CSV文件读写工具库

作者:李迟

本文介绍一个基于 C++11 标准库实现的轻量级 CSV 文件读写工具库,该库提供三个核心 API —— ReadCSV、WriteCSV、AppendCSV,覆盖了 CSV 文件的读取、覆写与追加场景,需要的朋友可以参考下

1. 背景

CSV(Comma-Separated Values)是数据交换领域使用最为广泛的纯文本格式之一。无论是数据库导出、日志采集、配置下发还是跨系统数据同步,CSV 都因其简单、人类可读、工具链成熟而占据重要地位。

在服务器端 C++ 开发中,经常面临这样的需求:

然而,C++ 标准库并未提供开箱即用的 CSV 解析与生成能力。常见的第三方方案(如将数据映射到 std::vector<std::vector<std::string>>)虽然功能完备,但在高频调用或嵌入式环境中会引入不必要的内存开销和模板膨胀。因此,设计一个零依赖、轻量级、调用方自主管理数据格式的 CSV 读写库具有实际工程价值。

2. 目的

本库的设计目标明确且克制:

  1. 零第三方依赖 —— 仅使用 C++11 标准库,可在任意 Linux 环境编译运行;
  2. 调用方控制数据格式 —— 不强制将 CSV 解析为二维数组或结构体,数据内容的构造与解析完全由调用方决定;
  3. 清晰的内存语义 —— 读操作由库内部分配内存,调用方负责释放,边界清晰、无隐式拷贝;
  4. 简单的错误模型 —— 通过整型返回值区分成功与各类失败,不引入异常依赖;
  5. 覆盖三种基本操作 —— 读取、覆写、追加,满足日常工作 90% 的 CSV 场景。

3. 设计

3.1 API 概览

// 读取:函数内部分配 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);

返回值约定:

3.2 内存语义

这是设计中最关键的决策点。对于 ReadCSV

3.3 写入语义

WriteCSVAppendCSV 中的 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 追加时始终从新行开始,避免数据粘连。

4. 实践

4.1 文件结构

文件结构如下,具体的完整代码见附录。

.
├── csv_utils.h      # 头文件:API 声明
├── csv_utils.cpp    # 实现文件
└── main.cpp         # 测试文件

4.2 ReadCSV 实现要点

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;
}

设计取舍分析

4.3 WriteCSV 与 AppendCSV 的实现

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;
}

WriteCSVAppendCSV 的唯一区别在于文件打开模式 —— trunc vs app,其余逻辑保持一致,这遵循了 DRY 原则且便于维护。

4.4 使用示例

场景一:生成数据并读取验证

// 写入
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,任务执行完成");

5. 测试

5.1 编译

g++ -std=c++11 -Wall -Wextra -o csv_test csv_utils.cpp main.cpp
./csv_test

5.1 测试用例

测试编号测试场景验证要点
测试 1WriteCSV + ReadCSV 往返写入后读取内容完全一致
测试 2WriteCSV + AppendCSV 多次追加追加后记录顺序与完整性
测试 3读取不存在的文件返回 -1,输出参数为 nullptr / 0
测试 4空数据写入仅标题行,无数据行的边界情况
测试 5含逗号与引号的转义字段带引号的字段不破坏 CSV 结构

5.2 测试结果

========== 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 通过

========== 全部测试通过 ==========

6. 小结

Golang在语言层支持很多常见的格式或协议的解析,而C++只能找第三方库或自己造轮子。本文件实现的CSV读取库用于临时顶替数据库功能。实际场景并未使用到。

7. 附

头文件csv_utils.h

#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);

实现文件csv_utils.cpp

#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;
}

测试文件main.cpp

#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 —— ReadCSVWriteCSVAppendCSV,覆盖了 CSV 文件的读取、覆写与追加场景。

1. 背景

CSV(Comma-Separated Values)是数据交换领域使用最为广泛的纯文本格式之一。无论是数据库导出、日志采集、配置下发还是跨系统数据同步,CSV 都因其简单、人类可读、工具链成熟而占据重要地位。

在服务器端 C++ 开发中,经常面临这样的需求:

然而,C++ 标准库并未提供开箱即用的 CSV 解析与生成能力。常见的第三方方案(如将数据映射到 std::vector<std::vector<std::string>>)虽然功能完备,但在高频调用或嵌入式环境中会引入不必要的内存开销和模板膨胀。因此,设计一个零依赖、轻量级、调用方自主管理数据格式的 CSV 读写库具有实际工程价值。

2. 目的

本库的设计目标明确且克制:

  1. 零第三方依赖 —— 仅使用 C++11 标准库,可在任意 Linux 环境编译运行;
  2. 调用方控制数据格式 —— 不强制将 CSV 解析为二维数组或结构体,数据内容的构造与解析完全由调用方决定;
  3. 清晰的内存语义 —— 读操作由库内部分配内存,调用方负责释放,边界清晰、无隐式拷贝;
  4. 简单的错误模型 —— 通过整型返回值区分成功与各类失败,不引入异常依赖;
  5. 覆盖三种基本操作 —— 读取、覆写、追加,满足日常工作 90% 的 CSV 场景。

3. 设计

3.1 API 概览

// 读取:函数内部分配 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);

返回值约定:

3.2 内存语义

这是设计中最关键的决策点。对于 ReadCSV

3.3 写入语义

WriteCSVAppendCSV 中的 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 追加时始终从新行开始,避免数据粘连。

4. 实践

4.1 文件结构

文件结构如下,具体的完整代码见附录。

.
├── csv_utils.h      # 头文件:API 声明
├── csv_utils.cpp    # 实现文件
└── main.cpp         # 测试文件

4.2 ReadCSV 实现要点

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;
}

设计取舍分析

4.3 WriteCSV 与 AppendCSV 的实现

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;
}

WriteCSVAppendCSV 的唯一区别在于文件打开模式 —— trunc vs app,其余逻辑保持一致,这遵循了 DRY 原则且便于维护。

4.4 使用示例

场景一:生成数据并读取验证

// 写入
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,任务执行完成");

5. 测试

5.1 编译

g++ -std=c++11 -Wall -Wextra -o csv_test csv_utils.cpp main.cpp
./csv_test

5.1 测试用例

测试编号测试场景验证要点
测试 1WriteCSV + ReadCSV 往返写入后读取内容完全一致
测试 2WriteCSV + AppendCSV 多次追加追加后记录顺序与完整性
测试 3读取不存在的文件返回 -1,输出参数为 nullptr / 0
测试 4空数据写入仅标题行,无数据行的边界情况
测试 5含逗号与引号的转义字段带引号的字段不破坏 CSV 结构

5.2 测试结果

========== 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 通过

========== 全部测试通过 ==========

6. 小结

Golang在语言层支持很多常见的格式或协议的解析,而C++只能找第三方库或自己造轮子。本文件实现的CSV读取库用于临时顶替数据库功能。实际场景并未使用到。

7. 附

头文件csv_utils.h

#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);

实现文件csv_utils.cpp

#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;
}

测试文件main.cpp

#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文件读写库的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文