基于C++ 11標準庫實現(xiàn)輕量級CSV文件讀寫工具庫
1. 背景
CSV(Comma-Separated Values)是數(shù)據(jù)交換領域使用最為廣泛的純文本格式之一。無論是數(shù)據(jù)庫導出、日志采集、配置下發(fā)還是跨系統(tǒng)數(shù)據(jù)同步,CSV 都因其簡單、人類可讀、工具鏈成熟而占據(jù)重要地位。
在服務器端 C++ 開發(fā)中,經(jīng)常面臨這樣的需求:
- 將程序運行狀態(tài)或計費結果導出為 CSV 供下游消費;
- 讀取上游系統(tǒng)生成的 CSV 配置文件;
- 在已有 CSV 文件尾部追加新紀錄,實現(xiàn)滾動日志式存儲。
然而,C++ 標準庫并未提供開箱即用的 CSV 解析與生成能力。常見的第三方方案(如將數(shù)據(jù)映射到 std::vector<std::vector<std::string>>)雖然功能完備,但在高頻調用或嵌入式環(huán)境中會引入不必要的內(nèi)存開銷和模板膨脹。因此,設計一個零依賴、輕量級、調用方自主管理數(shù)據(jù)格式的 CSV 讀寫庫具有實際工程價值。
2. 目的
本庫的設計目標明確且克制:
- 零第三方依賴 —— 僅使用 C++11 標準庫,可在任意 Linux 環(huán)境編譯運行;
- 調用方控制數(shù)據(jù)格式 —— 不強制將 CSV 解析為二維數(shù)組或結構體,數(shù)據(jù)內(nèi)容的構造與解析完全由調用方?jīng)Q定;
- 清晰的內(nèi)存語義 —— 讀操作由庫內(nèi)部分配內(nèi)存,調用方負責釋放,邊界清晰、無隱式拷貝;
- 簡單的錯誤模型 —— 通過整型返回值區(qū)分成功與各類失敗,不引入異常依賴;
- 覆蓋三種基本操作 —— 讀取、覆寫、追加,滿足日常工作 90% 的 CSV 場景。
3. 設計
3.1 API 概覽
// 讀?。汉瘮?shù)內(nèi)部分配 buffer,調用方 delete[] 釋放
int ReadCSV(const std::string& filename, char*& buffer, size_t& len);
// 覆寫:先寫 title(表頭),再寫 buffer(數(shù)據(jù)行)
int WriteCSV(const std::string& filename,
const std::string& title,
const std::string& buffer);
// 追加:在文件末尾追加數(shù)據(jù)
int AppendCSV(const std::string& filename, const std::string& buffer);
返回值約定:
- 0:操作成功
- -1:文件打開失敗
- -2:內(nèi)存分配失敗 / 寫入失敗
- -3:讀取失敗
3.2 內(nèi)存語義
這是設計中最關鍵的決策點。對于 ReadCSV:
- 分配在庫內(nèi):確保 buffer 大小與文件內(nèi)容精確匹配,調用方無需預分配或二次擴容;
- 釋放在庫外:調用方在消費完數(shù)據(jù)后主動
delete[],生命周期由使用者掌控,不存在懸掛指針風險; - buffer 總是以
\0結尾:雖按二進制方式讀取,但額外追加一個空字符,方便調用方直接作為 C 字符串使用。
3.3 寫入語義
WriteCSV 和 AppendCSV 中的 buffer 參數(shù)由調用方自由構造——可以是一個完整的 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 追加時始終從新行開始,避免數(shù)據(jù)粘連。
4. 實踐
4.1 文件結構
文件結構如下,具體的完整代碼見附錄。
. ├── csv_utils.h # 頭文件:API 聲明 ├── csv_utils.cpp # 實現(xiàn)文件 └── main.cpp # 測試文件
4.2 ReadCSV 實現(xiàn)要點
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 字節(jié)用于 '\0'
try {
buffer = new char[static_cast<size_t>(fileSize) + 1];
} catch (const std::bad_alloc&) { buffer = nullptr; len = 0; return -2; }
// ③ 回到文件頭,一次性讀取全部內(nèi)容
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 開銷;- 內(nèi)存分配失敗時捕獲
std::bad_alloc,確保不會因new拋出異常而破壞調用方的異常安全策略; - 讀取失敗時立即
delete[] buffer并將輸出參數(shù)歸零,保證失敗后調用方拿到的是干凈狀態(tài)。
4.3 WriteCSV 與 AppendCSV 的實現(xiàn)
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 的唯一區(qū)別在于文件打開模式 —— trunc vs app,其余邏輯保持一致,這遵循了 DRY 原則且便于維護。
4.4 使用示例
場景一:生成數(shù)據(jù)并讀取驗證
// 寫入
WriteCSV("report.csv",
"交易ID,金額,狀態(tài)",
"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", "時間,級別,消息", "");
// 運行中持續(xù)追加
AppendCSV("log.csv", "2026-07-27 10:00,INFO,服務啟動");
AppendCSV("log.csv", "2026-07-27 10:05,WARN,內(nèi)存使用率超過80%");
AppendCSV("log.csv", "2026-07-27 10:30,INFO,任務執(zhí)行完成");
5. 測試
5.1 編譯
g++ -std=c++11 -Wall -Wextra -o csv_test csv_utils.cpp main.cpp ./csv_test
5.1 測試用例
| 測試編號 | 測試場景 | 驗證要點 |
|---|---|---|
| 測試 1 | WriteCSV + ReadCSV 往返 | 寫入后讀取內(nèi)容完全一致 |
| 測試 2 | WriteCSV + AppendCSV 多次追加 | 追加后記錄順序與完整性 |
| 測試 3 | 讀取不存在的文件 | 返回 -1,輸出參數(shù)為 nullptr / 0 |
| 測試 4 | 空數(shù)據(jù)寫入 | 僅標題行,無數(shù)據(jù)行的邊界情況 |
| 測試 5 | 含逗號與引號的轉義字段 | 帶引號的字段不破壞 CSV 結構 |
5.2 測試結果
========== CSV Utils 測試 ========== === 測試 1:WriteCSV + ReadCSV === 文件長度: 51 bytes 文件內(nèi)容: 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:空數(shù)據(jù)寫入 === 文件內(nèi)容: [col1,col2,col3 ] ? 測試 4 通過 === 測試 5:含轉義字段的 CSV === id,name,description 1,"Smith, John","He said ""Hello""" 2,Jane Doe,Normal text ? 測試 5 通過 ========== 全部測試通過 ==========
6. 小結
Golang在語言層支持很多常見的格式或協(xié)議的解析,而C++只能找第三方庫或自己造輪子。本文件實現(xiàn)的CSV讀取庫用于臨時頂替數(shù)據(jù)庫功能。實際場景并未使用到。
7. 附
頭文件csv_utils.h
#pragma once
#include <cstddef>
#include <string>
/**
* @brief 讀取CSV文件全部內(nèi)容到內(nèi)存緩沖區(qū)
* @param filename 文件路徑
* @param buffer 輸出參數(shù),函數(shù)內(nèi)部分配內(nèi)存,調用方負責釋放(使用 delete[])
* @param len 輸出參數(shù),讀取到的數(shù)據(jù)長度(不含結尾'\0')
* @return 0 成功
* -1 文件打開失敗
* -2 內(nèi)存分配失敗
* -3 讀取失敗
*/
int ReadCSV(const std::string& filename, char*& buffer, size_t& len);
/**
* @brief 寫入CSV文件(覆蓋模式),包含標題行和數(shù)據(jù)內(nèi)容
* @param filename 文件路徑
* @param title 標題行(CSV表頭字符串)
* @param buffer 數(shù)據(jù)內(nèi)容(CSV格式行數(shù)據(jù),由調用方構造)
* @return 0 成功
* -1 文件打開失敗
* -2 寫入失敗
*/
int WriteCSV(const std::string& filename,
const std::string& title,
const std::string& buffer);
/**
* @brief 追加數(shù)據(jù)到已有CSV文件末尾
* @param filename 文件路徑
* @param buffer 要追加的數(shù)據(jù)內(nèi)容(由調用方構造)
* @return 0 成功
* -1 文件打開失敗
* -2 寫入失敗
*/
int AppendCSV(const std::string& filename, const std::string& buffer);
實現(xiàn)文件csv_utils.cpp
#include "csv_utils.h"
#include <cstring>
#include <fstream>
// ──────────────────────────────────────────────
// ReadCSV – 讀取整個文件到調用方可釋放的緩沖區(qū)
// ──────────────────────────────────────────────
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. 分配緩沖區(qū)(多一個字節(jié)存放 '\0',方便調用方當作 C 字符串使用)
try {
buffer = new char[static_cast<size_t>(fileSize) + 1];
} catch (const std::bad_alloc&) {
buffer = nullptr;
len = 0;
return -2; // 內(nèi)存分配失敗
}
// 3. 回到文件頭并讀取全部內(nèi)容
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 – 覆蓋寫入標題行 + 數(shù)據(jù)內(nèi)容
// ──────────────────────────────────────────────
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';
}
// 寫入數(shù)據(jù)緩沖區(qū)
if (!buffer.empty()) {
ofs << buffer;
// buffer 不以換行結尾時補一個換行,保證后續(xù) AppendCSV 從新行開始
if (buffer.back() != '\n') {
ofs << '\n';
}
}
if (!ofs.good()) {
return -2;
}
return 0;
}
// ──────────────────────────────────────────────
// AppendCSV – 追加數(shù)據(jù)到文件末尾
// ──────────────────────────────────────────────
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 數(shù)據(jù)(調用方?jīng)Q定 buffer 內(nèi)容)
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 << "文件內(nèi)容:" << std::endl;
std::cout << buf << std::endl;
// 調用方負責釋放
delete[] buf;
// 驗證內(nèi)容正確
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 追加內(nèi)容,驗證追加后文件完整
// ──────────────────────────────────────────────
static int test_append()
{
const std::string filename = "test_append.csv";
// 先寫入基礎內(nèi)容
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);
// 追加內(nèi)容
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 :空數(shù)據(jù)寫入與讀取
// ──────────────────────────────────────────────
static int test_empty_data()
{
const std::string filename = "test_empty.csv";
// 僅寫入標題,數(shù)據(jù)為空
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:空數(shù)據(jù)寫入 ===" << std::endl;
std::cout << "文件內(nèi)容: [" << 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文件讀寫庫的實現(xiàn)方案
urlname: a-lightweight-C+±CSV-file-library-implementation
tags:
- 工程庫代碼
categories: - 我的程序代碼
date: 2026-07-28 00:00:00
photos: - gallery/tech/c2.jpg
本文介紹一個基于 C++11 標準庫實現(xiàn)的輕量級 CSV 文件讀寫工具庫。該庫提供三個核心 API —— ReadCSV、WriteCSV、AppendCSV,覆蓋了 CSV 文件的讀取、覆寫與追加場景。
1. 背景
CSV(Comma-Separated Values)是數(shù)據(jù)交換領域使用最為廣泛的純文本格式之一。無論是數(shù)據(jù)庫導出、日志采集、配置下發(fā)還是跨系統(tǒng)數(shù)據(jù)同步,CSV 都因其簡單、人類可讀、工具鏈成熟而占據(jù)重要地位。
在服務器端 C++ 開發(fā)中,經(jīng)常面臨這樣的需求:
- 將程序運行狀態(tài)或計費結果導出為 CSV 供下游消費;
- 讀取上游系統(tǒng)生成的 CSV 配置文件;
- 在已有 CSV 文件尾部追加新紀錄,實現(xiàn)滾動日志式存儲。
然而,C++ 標準庫并未提供開箱即用的 CSV 解析與生成能力。常見的第三方方案(如將數(shù)據(jù)映射到 std::vector<std::vector<std::string>>)雖然功能完備,但在高頻調用或嵌入式環(huán)境中會引入不必要的內(nèi)存開銷和模板膨脹。因此,設計一個零依賴、輕量級、調用方自主管理數(shù)據(jù)格式的 CSV 讀寫庫具有實際工程價值。
2. 目的
本庫的設計目標明確且克制:
- 零第三方依賴 —— 僅使用 C++11 標準庫,可在任意 Linux 環(huán)境編譯運行;
- 調用方控制數(shù)據(jù)格式 —— 不強制將 CSV 解析為二維數(shù)組或結構體,數(shù)據(jù)內(nèi)容的構造與解析完全由調用方?jīng)Q定;
- 清晰的內(nèi)存語義 —— 讀操作由庫內(nèi)部分配內(nèi)存,調用方負責釋放,邊界清晰、無隱式拷貝;
- 簡單的錯誤模型 —— 通過整型返回值區(qū)分成功與各類失敗,不引入異常依賴;
- 覆蓋三種基本操作 —— 讀取、覆寫、追加,滿足日常工作 90% 的 CSV 場景。
3. 設計
3.1 API 概覽
// 讀取:函數(shù)內(nèi)部分配 buffer,調用方 delete[] 釋放
int ReadCSV(const std::string& filename, char*& buffer, size_t& len);
// 覆寫:先寫 title(表頭),再寫 buffer(數(shù)據(jù)行)
int WriteCSV(const std::string& filename,
const std::string& title,
const std::string& buffer);
// 追加:在文件末尾追加數(shù)據(jù)
int AppendCSV(const std::string& filename, const std::string& buffer);
返回值約定:
- 0:操作成功
- -1:文件打開失敗
- -2:內(nèi)存分配失敗 / 寫入失敗
- -3:讀取失敗
3.2 內(nèi)存語義
這是設計中最關鍵的決策點。對于 ReadCSV:
- 分配在庫內(nèi):確保 buffer 大小與文件內(nèi)容精確匹配,調用方無需預分配或二次擴容;
- 釋放在庫外:調用方在消費完數(shù)據(jù)后主動
delete[],生命周期由使用者掌控,不存在懸掛指針風險; - buffer 總是以
\0結尾:雖按二進制方式讀取,但額外追加一個空字符,方便調用方直接作為 C 字符串使用。
3.3 寫入語義
WriteCSV 和 AppendCSV 中的 buffer 參數(shù)由調用方自由構造——可以是一個完整的 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 追加時始終從新行開始,避免數(shù)據(jù)粘連。
4. 實踐
4.1 文件結構
文件結構如下,具體的完整代碼見附錄。
. ├── csv_utils.h # 頭文件:API 聲明 ├── csv_utils.cpp # 實現(xiàn)文件 └── main.cpp # 測試文件
4.2 ReadCSV 實現(xiàn)要點
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 字節(jié)用于 '\0'
try {
buffer = new char[static_cast<size_t>(fileSize) + 1];
} catch (const std::bad_alloc&) { buffer = nullptr; len = 0; return -2; }
// ③ 回到文件頭,一次性讀取全部內(nèi)容
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 開銷;- 內(nèi)存分配失敗時捕獲
std::bad_alloc,確保不會因new拋出異常而破壞調用方的異常安全策略; - 讀取失敗時立即
delete[] buffer并將輸出參數(shù)歸零,保證失敗后調用方拿到的是干凈狀態(tài)。
4.3 WriteCSV 與 AppendCSV 的實現(xiàn)
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 的唯一區(qū)別在于文件打開模式 —— trunc vs app,其余邏輯保持一致,這遵循了 DRY 原則且便于維護。
4.4 使用示例
場景一:生成數(shù)據(jù)并讀取驗證
// 寫入
WriteCSV("report.csv",
"交易ID,金額,狀態(tài)",
"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", "時間,級別,消息", "");
// 運行中持續(xù)追加
AppendCSV("log.csv", "2026-07-27 10:00,INFO,服務啟動");
AppendCSV("log.csv", "2026-07-27 10:05,WARN,內(nèi)存使用率超過80%");
AppendCSV("log.csv", "2026-07-27 10:30,INFO,任務執(zhí)行完成");
5. 測試
5.1 編譯
g++ -std=c++11 -Wall -Wextra -o csv_test csv_utils.cpp main.cpp ./csv_test
5.1 測試用例
| 測試編號 | 測試場景 | 驗證要點 |
|---|---|---|
| 測試 1 | WriteCSV + ReadCSV 往返 | 寫入后讀取內(nèi)容完全一致 |
| 測試 2 | WriteCSV + AppendCSV 多次追加 | 追加后記錄順序與完整性 |
| 測試 3 | 讀取不存在的文件 | 返回 -1,輸出參數(shù)為 nullptr / 0 |
| 測試 4 | 空數(shù)據(jù)寫入 | 僅標題行,無數(shù)據(jù)行的邊界情況 |
| 測試 5 | 含逗號與引號的轉義字段 | 帶引號的字段不破壞 CSV 結構 |
5.2 測試結果
========== CSV Utils 測試 ========== === 測試 1:WriteCSV + ReadCSV === 文件長度: 51 bytes 文件內(nèi)容: 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:空數(shù)據(jù)寫入 === 文件內(nèi)容: [col1,col2,col3 ] ? 測試 4 通過 === 測試 5:含轉義字段的 CSV === id,name,description 1,"Smith, John","He said ""Hello""" 2,Jane Doe,Normal text ? 測試 5 通過 ========== 全部測試通過 ==========
6. 小結
Golang在語言層支持很多常見的格式或協(xié)議的解析,而C++只能找第三方庫或自己造輪子。本文件實現(xiàn)的CSV讀取庫用于臨時頂替數(shù)據(jù)庫功能。實際場景并未使用到。
7. 附
頭文件csv_utils.h
#pragma once
#include <cstddef>
#include <string>
/**
* @brief 讀取CSV文件全部內(nèi)容到內(nèi)存緩沖區(qū)
* @param filename 文件路徑
* @param buffer 輸出參數(shù),函數(shù)內(nèi)部分配內(nèi)存,調用方負責釋放(使用 delete[])
* @param len 輸出參數(shù),讀取到的數(shù)據(jù)長度(不含結尾'\0')
* @return 0 成功
* -1 文件打開失敗
* -2 內(nèi)存分配失敗
* -3 讀取失敗
*/
int ReadCSV(const std::string& filename, char*& buffer, size_t& len);
/**
* @brief 寫入CSV文件(覆蓋模式),包含標題行和數(shù)據(jù)內(nèi)容
* @param filename 文件路徑
* @param title 標題行(CSV表頭字符串)
* @param buffer 數(shù)據(jù)內(nèi)容(CSV格式行數(shù)據(jù),由調用方構造)
* @return 0 成功
* -1 文件打開失敗
* -2 寫入失敗
*/
int WriteCSV(const std::string& filename,
const std::string& title,
const std::string& buffer);
/**
* @brief 追加數(shù)據(jù)到已有CSV文件末尾
* @param filename 文件路徑
* @param buffer 要追加的數(shù)據(jù)內(nèi)容(由調用方構造)
* @return 0 成功
* -1 文件打開失敗
* -2 寫入失敗
*/
int AppendCSV(const std::string& filename, const std::string& buffer);
實現(xiàn)文件csv_utils.cpp
#include "csv_utils.h"
#include <cstring>
#include <fstream>
// ──────────────────────────────────────────────
// ReadCSV – 讀取整個文件到調用方可釋放的緩沖區(qū)
// ──────────────────────────────────────────────
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. 分配緩沖區(qū)(多一個字節(jié)存放 '\0',方便調用方當作 C 字符串使用)
try {
buffer = new char[static_cast<size_t>(fileSize) + 1];
} catch (const std::bad_alloc&) {
buffer = nullptr;
len = 0;
return -2; // 內(nèi)存分配失敗
}
// 3. 回到文件頭并讀取全部內(nèi)容
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 – 覆蓋寫入標題行 + 數(shù)據(jù)內(nèi)容
// ──────────────────────────────────────────────
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';
}
// 寫入數(shù)據(jù)緩沖區(qū)
if (!buffer.empty()) {
ofs << buffer;
// buffer 不以換行結尾時補一個換行,保證后續(xù) AppendCSV 從新行開始
if (buffer.back() != '\n') {
ofs << '\n';
}
}
if (!ofs.good()) {
return -2;
}
return 0;
}
// ──────────────────────────────────────────────
// AppendCSV – 追加數(shù)據(jù)到文件末尾
// ──────────────────────────────────────────────
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 數(shù)據(jù)(調用方?jīng)Q定 buffer 內(nèi)容)
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 << "文件內(nèi)容:" << std::endl;
std::cout << buf << std::endl;
// 調用方負責釋放
delete[] buf;
// 驗證內(nèi)容正確
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 追加內(nèi)容,驗證追加后文件完整
// ──────────────────────────────────────────────
static int test_append()
{
const std::string filename = "test_append.csv";
// 先寫入基礎內(nèi)容
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);
// 追加內(nèi)容
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 :空數(shù)據(jù)寫入與讀取
// ──────────────────────────────────────────────
static int test_empty_data()
{
const std::string filename = "test_empty.csv";
// 僅寫入標題,數(shù)據(jù)為空
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:空數(shù)據(jù)寫入 ===" << std::endl;
std::cout << "文件內(nèi)容: [" << 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標準庫實現(xiàn)輕量級CSV文件讀寫工具庫的詳細內(nèi)容,更多關于C++輕量級CSV文件讀寫庫的資料請關注腳本之家其它相關文章!
相關文章
C語言初識動態(tài)內(nèi)存管理malloc calloc realloc free函數(shù)
動態(tài)內(nèi)存是相對靜態(tài)內(nèi)存而言的。所謂動態(tài)和靜態(tài)就是指內(nèi)存的分配方式。動態(tài)內(nèi)存是指在堆上分配的內(nèi)存,而靜態(tài)內(nèi)存是指在棧上分配的內(nèi)存2022-03-03

