使用C++實(shí)現(xiàn)的錯(cuò)誤信息收集器
1. 背景
在大型C++項(xiàng)目的開發(fā)過程中,錯(cuò)誤處理是一個(gè)不容忽視的核心環(huán)節(jié)。年初筆者接手了一個(gè)工程,近幾個(gè)月在修改,發(fā)現(xiàn)其中的錯(cuò)誤信息處理比較凌亂,考慮到方便日后的自己的維護(hù),因此要進(jìn)行必要的重構(gòu)。缺乏統(tǒng)一錯(cuò)誤收集機(jī)制帶來以下一些困擾:
- 信息碎片化:不同模塊各自定義錯(cuò)誤格式,日志散落在不同文件中,排查問題時(shí)需要手動(dòng)關(guān)聯(lián)
- 重復(fù)記錄:同一個(gè)錯(cuò)誤可能被多個(gè)層級(jí)重復(fù)記錄,導(dǎo)致日志膨脹
- 特殊字符干擾:錯(cuò)誤消息中包含的換行符、制表符破壞日志格式,影響后續(xù)分析
- 缺乏分類:無法區(qū)分致命錯(cuò)誤、警告和普通提示信息
為了解決這些問題,筆者著手設(shè)計(jì)并實(shí)現(xiàn)了一個(gè)輕量級(jí)的ErrorCollector類。本文將從需求分析、設(shè)計(jì)思路、實(shí)現(xiàn)細(xì)節(jié)到測(cè)試驗(yàn)證,完整分享這個(gè)組件的開發(fā)過程。
PS:近年來,筆者基本都會(huì)因其他共事的人升職、轉(zhuǎn)崗等原因接手不同性質(zhì)代碼工程,但還沒能力向他人移交工程或事務(wù)。從技術(shù)角度看,需要寫不同類別的代碼,是件好事,但從個(gè)人成長(zhǎng)角度看,不是個(gè)事,特別是處于年齡越來越大,頭發(fā)越來越白的尷尬的人生階段。
2. 設(shè)計(jì)思路
2.1 核心需求
經(jīng)過需求梳理,我們確定了以下核心功能點(diǎn):
- 去重存儲(chǔ):相同的錯(cuò)誤消息只保留一份,避免冗余
- 分類標(biāo)記:每條消息關(guān)聯(lián)一個(gè)錯(cuò)誤碼(0表示普通信息,非0表示錯(cuò)誤)
- 字符清洗:自動(dòng)移除
\t、\n、\r等特殊字符 - 靈活查詢:支持按類型(全部/僅錯(cuò)誤)、按錯(cuò)誤碼過濾查詢
- 空值安全:對(duì)空字符串和空指針有防護(hù)
- 簡(jiǎn)單易用:提供直觀的接口,且只有頭文件,減少使用成本
2.2 數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
采用兩個(gè)并行的std::vector分別存儲(chǔ)錯(cuò)誤碼和消息文本:
std::vector<int> m_code; // 錯(cuò)誤碼列表 std::vector<std::string> m_list; // 消息列表(與m_code一一對(duì)應(yīng))
這種設(shè)計(jì)的優(yōu)勢(shì)有以下幾點(diǎn):
- 索引對(duì)應(yīng):通過相同下標(biāo)關(guān)聯(lián)錯(cuò)誤碼和消息,無需額外的映射結(jié)構(gòu)(注意索引需大于等于0)
- 順序保留:保持添加順序,便于追溯問題發(fā)生的時(shí)間線
- 內(nèi)存友好:相比
std::map或std::unordered_map,內(nèi)存開銷更小
去重機(jī)制則通過std::find在m_list中線性查找實(shí)現(xiàn)。雖然時(shí)間復(fù)雜度為O(n),但考慮到錯(cuò)誤收集場(chǎng)景下消息數(shù)量通常有限(一般不超過幾十條),這種實(shí)現(xiàn)足夠高效且代碼簡(jiǎn)潔。
2.3 接口設(shè)計(jì)原則
- 重載友好:同時(shí)支持
std::string和const char*參數(shù),適應(yīng)不同使用場(chǎng)景 - 默認(rèn)參數(shù):
code參數(shù)默認(rèn)為0,簡(jiǎn)化普通信息記錄的調(diào)用 - 防御性編程:在函數(shù)入口進(jìn)行參數(shù)校驗(yàn),提前返回避免無效操作
- const正確性:查詢接口標(biāo)記為
const,確保不修改對(duì)象狀態(tài)
3. 實(shí)踐
3.1 源碼
ErrorCollector.h文件代碼如下:
#include <algorithm>
#include <vector>
#include <unordered_set>
#include <string>
// 錯(cuò)誤信息、過程信息記錄器
class ErrorCollector {
public:
// 構(gòu)造函數(shù),初始化變量為空
ErrorCollector() {
m_code.clear();
m_list.clear();
}
// 析構(gòu)函數(shù)(可選)
~ErrorCollector() {
m_code.clear();
m_list.clear();
}
void AddInfo(const std::string& msg1, int code = 0) {
// 判斷字符串是否為空
if (msg1.empty()) {
return;
}
std::string msg = msg1;
msg.erase(std::remove_if(msg.begin(), msg.end(),
[](char c) { return c == '\t' || c == '\n' || c == '\r'; }),
msg.end());
// 刪除特殊字符后再次判斷是否為空
if (msg.empty()) {
return;
}
if (std::find(m_list.begin(), m_list.end(), msg) == m_list.end()) {
m_code.push_back(code);
m_list.push_back(msg);
}
}
void AddInfo(const char* msg1, int code = 0) {
// 判斷指針是否為空,以及字符串是否為空
if (msg1 == nullptr || msg1[0] == '\0') {
return;
}
std::string msg = msg1;
AddInfo(msg, code);
}
// 返回豎線分隔的字符串,格式:錯(cuò)誤1 | 錯(cuò)誤2 | 錯(cuò)誤3
// all 表示所有的信息,如只返回錯(cuò)誤,設(shè)置為false
std::string GetInfos(bool all = true) const {
std::string result;
int cnt = 0;
for (size_t i = 0; i < m_list.size(); i++) {
if (!all && m_code[i] == 0) {
continue;
}
if (cnt > 0) {
result += " | ";
}
cnt++;
result += m_list[i];
}
if (!result.empty() && result.back() == '|') {
result.pop_back();
}
return result;
}
// 同上,指定code
std::string GetInfoByCode(int code) const {
std::string result;
int cnt = 0;
for (size_t i = 0; i < m_list.size(); i++) {
if (m_code[i] != code) {
continue;
}
if (cnt > 0) {
result += " | ";
}
cnt++;
result += m_list[i];
}
if (!result.empty() && result.back() == '|') {
result.pop_back();
}
return result;
}
std::string GetCodes() const {
std::string result;
int cnt = 0;
for (size_t i = 0; i < m_code.size(); i++) {
if (m_code[i] == 0) {
continue;
}
if (cnt > 0) {
result += "|"; // 不要空格緊湊一些
}
cnt++;
result += std::to_string(m_code[i]);
}
if (!result.empty() && result.back() == '|') {
result.pop_back();
}
return result;
}
// 可指定忽略的code
std::string GetCodes(int skipCode) const {
std::string result;
int cnt = 0;
for (size_t i = 0; i < m_code.size(); i++) {
if (m_code[i] == 0 || m_code[i] == skipCode) {
continue;
}
if (cnt > 0) {
result += "|"; // 不要空格緊湊一些
}
cnt++;
result += std::to_string(m_code[i]);
}
if (!result.empty() && result.back() == '|') {
result.pop_back();
}
return result;
}
bool Empty() {return m_list.empty();}
private:
std::vector<int> m_code;
std::vector<std::string> m_list;
};
3.2 關(guān)鍵實(shí)現(xiàn)解析
特殊字符清洗:
msg.erase(std::remove_if(msg.begin(), msg.end(),
[](char c) { return c == '\t' || c == '\n' || c == '\r'; }),
msg.end());
這里使用了經(jīng)典的"erase-remove"慣用法,高效地移除了三種常見的格式干擾字符。清洗后再判斷消息是否為空,確保不會(huì)存儲(chǔ)僅由特殊字符組成的無效消息。
去重邏輯:
if (std::find(m_list.begin(), m_list.end(), msg) == m_list.end()) {
m_code.push_back(code);
m_list.push_back(msg);
}
值得注意的是,此處考慮的設(shè)計(jì)決策是:去重基于消息內(nèi)容,而不是消息+錯(cuò)誤碼的組合。這意味著如果同一個(gè)錯(cuò)誤消息先后以不同的錯(cuò)誤碼添加,只有第一次會(huì)被保留,后續(xù)的添加會(huì)被忽略。這種設(shè)計(jì)在某些場(chǎng)景下可能丟失錯(cuò)誤碼信息,但換來的是消息列表的簡(jiǎn)潔性。在實(shí)際使用中,我們約定錯(cuò)誤消息應(yīng)該包含足夠的上下文信息,使得重復(fù)消息確實(shí)不需要再次記錄。
輸出格式控制:
if (cnt > 0) {
result += " | ";
}
cnt++;
result += m_list[i];
此處通過計(jì)數(shù)器控制分隔符的插入位置,避免在字符串開頭或結(jié)尾出現(xiàn)多余的分隔符。GetCodes方法中則使用更緊湊的"|"(無空格)作為分隔符,方便后續(xù)解析。
3.3 使用示例
ErrorCollector collector;
// 記錄普通信息
collector.AddInfo("開始數(shù)據(jù)遷移");
// 記錄錯(cuò)誤
collector.AddInfo("連接數(shù)據(jù)庫失敗", 1001);
collector.AddInfo("讀取配置文件異常", 1002);
// 獲取所有信息(包含提示和錯(cuò)誤)
std::string all = collector.GetInfos(true);
// 輸出: "開始數(shù)據(jù)遷移 | 連接數(shù)據(jù)庫失敗 | 讀取配置文件異常"
// 僅獲取錯(cuò)誤信息
std::string errors = collector.GetInfos(false);
// 輸出: "連接數(shù)據(jù)庫失敗 | 讀取配置文件異常"
// 獲取錯(cuò)誤碼列表
std::string codes = collector.GetCodes();
// 輸出: "1001|1002"
// 按錯(cuò)誤碼查詢
std::string info = collector.GetInfoByCode(1001);
// 輸出: "連接數(shù)據(jù)庫失敗"
3.4 應(yīng)用場(chǎng)景
ErrorCollector在以下場(chǎng)景中特別有用:
- API參數(shù)校驗(yàn):收集所有校驗(yàn)失敗的信息,一次性返回給調(diào)用方
- 批量操作:執(zhí)行多個(gè)獨(dú)立操作時(shí),收集所有異常而非遇到第一個(gè)錯(cuò)誤就中斷
- 配置加載:加載配置文件時(shí),記錄所有格式錯(cuò)誤或缺失的配置項(xiàng)
- 單元測(cè)試:收集測(cè)試過程中的斷言失敗信息
4. 測(cè)試
4.1 測(cè)試代碼
測(cè)試代碼文件main.cpp:
#include <iostream>
#include "ErrorCollector.h"
void printTestResult(const std::string& testName, bool passed) {
std::cout << (passed ? "[PASS]" : "[FAIL]") << " " << testName << std::endl;
}
int main() {
ErrorCollector collector;
// 測(cè)試1:基本添加和獲取
std::cout << "\n=== 測(cè)試1:基本功能 ===" << std::endl;
collector.AddInfo("數(shù)據(jù)庫連接失敗", 1001);
collector.AddInfo("配置文件缺失", 1002);
collector.AddInfo("用戶認(rèn)證超時(shí)", 0); // code為0表示提示信息
std::string infos = collector.GetInfos(true);
std::cout << "所有信息: " << infos << std::endl;
std::cout << "僅錯(cuò)誤: " << collector.GetInfos(false) << std::endl;
std::cout << "錯(cuò)誤碼: " << collector.GetCodes() << std::endl;
printTestResult("基本功能", infos == "數(shù)據(jù)庫連接失敗 | 配置文件缺失 | 用戶認(rèn)證超時(shí)");
// 測(cè)試2:去重功能
std::cout << "\n=== 測(cè)試2:去重功能 ===" << std::endl;
ErrorCollector collector2;
collector2.AddInfo("重復(fù)錯(cuò)誤", 100);
collector2.AddInfo("重復(fù)錯(cuò)誤", 200); // 相同消息,不同code
collector2.AddInfo("唯一錯(cuò)誤", 300);
std::string result = collector2.GetInfos(true);
std::cout << "去重后結(jié)果: " << result << std::endl;
std::cout << "錯(cuò)誤碼: " << collector2.GetCodes() << std::endl;
printTestResult("去重功能", result == "重復(fù)錯(cuò)誤 | 唯一錯(cuò)誤" && collector2.GetCodes() == "100|300");
// 測(cè)試3:空字符串和特殊字符
std::cout << "\n=== 測(cè)試3:空值和特殊字符過濾 ===" << std::endl;
ErrorCollector collector3;
collector3.AddInfo("", 100);
collector3.AddInfo("\t\n\r", 200);
collector3.AddInfo("有效消息", 300);
collector3.AddInfo(nullptr, 400);
std::string result3 = collector3.GetInfos(true);
std::cout << "過濾后結(jié)果: " << result3 << std::endl;
printTestResult("空值過濾", result3 == "有效消息");
printTestResult("空狀態(tài)檢測(cè)", !collector3.Empty());
// 測(cè)試4:特殊字符清理(制表符、換行符)
std::cout << "\n=== 測(cè)試4:特殊字符清理 ===" << std::endl;
ErrorCollector collector4;
collector4.AddInfo("消息\t包含\t制表符", 100);
collector4.AddInfo("第一行\(zhòng)n第二行\(zhòng)r\n", 200);
std::string result4 = collector4.GetInfos(true);
std::cout << "清理后結(jié)果: " << result4 << std::endl;
printTestResult("制表符清理", result4 == "消息包含制表符 | 第一行第二行");
// 測(cè)試5:按錯(cuò)誤碼過濾
std::cout << "\n=== 測(cè)試5:按錯(cuò)誤碼獲取 ===" << std::endl;
ErrorCollector collector5;
collector5.AddInfo("錯(cuò)誤A", 100);
collector5.AddInfo("提示B", 0);
collector5.AddInfo("錯(cuò)誤C", 200);
collector5.AddInfo("錯(cuò)誤D", 300);
std::cout << "錯(cuò)誤碼100的信息: " << collector5.GetInfoByCode(100) << std::endl;
std::cout << "跳過錯(cuò)誤碼200: " << collector5.GetCodes(200) << std::endl;
printTestResult("按碼獲取", collector5.GetInfoByCode(100) == "錯(cuò)誤A");
printTestResult("跳過碼獲取", collector5.GetCodes(200) == "100|300");
// 測(cè)試6:格式驗(yàn)證
std::cout << "\n=== 測(cè)試6:輸出格式驗(yàn)證 ===" << std::endl;
ErrorCollector collector6;
collector6.AddInfo("A", 1);
collector6.AddInfo("B", 2);
collector6.AddInfo("C", 3);
std::string result6 = collector6.GetInfos(true);
std::cout << "格式結(jié)果: " << result6 << std::endl;
bool formatOk = (result6 == "A | B | C") &&
(result6.back() != '|') &&
(result6.front() != '|');
printTestResult("格式正確", formatOk);
// 測(cè)試7:空狀態(tài)檢測(cè)
std::cout << "\n=== 測(cè)試7:空狀態(tài)檢測(cè) ===" << std::endl;
ErrorCollector collector7;
std::cout << "初始Empty: " << (collector7.Empty() ? "true" : "false") << std::endl;
collector7.AddInfo("測(cè)試", 100);
std::cout << "添加后Empty: " << (collector7.Empty() ? "true" : "false") << std::endl;
printTestResult("空狀態(tài)檢測(cè)", collector7.Empty() == false);
// 測(cè)試8:大量數(shù)據(jù)性能
std::cout << "\n=== 測(cè)試8:大量數(shù)據(jù)性能 ===" << std::endl;
ErrorCollector collector8;
for (int i = 0; i < 1000; i++) {
collector8.AddInfo("錯(cuò)誤_" + std::to_string(i), i);
}
std::string result8 = collector8.GetInfos(true);
bool hasError500 = result8.find("錯(cuò)誤_500") != std::string::npos;
std::cout << "包含第500條: " << (hasError500 ? "是" : "否") << std::endl;
printTestResult("大量數(shù)據(jù)處理", hasError500);
std::cout << "\n=== 所有測(cè)試完成 ===" << std::endl;
return 0;
}
編譯命令:
g++ -std=c++11 main.cpp
執(zhí)行輸出結(jié)果:
# ./a.out === 測(cè)試1:基本功能 === 所有信息: 數(shù)據(jù)庫連接失敗 | 配置文件缺失 | 用戶認(rèn)證超時(shí) 僅錯(cuò)誤: 數(shù)據(jù)庫連接失敗 | 配置文件缺失 錯(cuò)誤碼: 1001|1002 [PASS] 基本功能 === 測(cè)試2:去重功能 === 去重后結(jié)果: 重復(fù)錯(cuò)誤 | 唯一錯(cuò)誤 錯(cuò)誤碼: 100|300 [PASS] 去重功能 === 測(cè)試3:空值和特殊字符過濾 === 過濾后結(jié)果: 有效消息 [PASS] 空值過濾 [PASS] 空狀態(tài)檢測(cè) === 測(cè)試4:特殊字符清理 === 清理后結(jié)果: 消息包含制表符 | 第一行第二行 [PASS] 制表符清理 === 測(cè)試5:按錯(cuò)誤碼獲取 === 錯(cuò)誤碼100的信息: 錯(cuò)誤A 跳過錯(cuò)誤碼200: 100|300 [PASS] 按碼獲取 [PASS] 跳過碼獲取 === 測(cè)試6:輸出格式驗(yàn)證 === 格式結(jié)果: A | B | C [PASS] 格式正確 === 測(cè)試7:空狀態(tài)檢測(cè) === 初始Empty: true 添加后Empty: false [PASS] 空狀態(tài)檢測(cè) === 測(cè)試8:大量數(shù)據(jù)性能 === 包含第500條: 是 [PASS] 大量數(shù)據(jù)處理 === 所有測(cè)試完成 ===
4.2 關(guān)鍵測(cè)試用例
用例1:基本功能驗(yàn)證
collector.AddInfo("數(shù)據(jù)庫連接失敗", 1001);
collector.AddInfo("配置文件缺失", 1002);
collector.AddInfo("用戶認(rèn)證超時(shí)", 0);
用例2:去重驗(yàn)證
collector2.AddInfo("重復(fù)錯(cuò)誤", 100);
collector2.AddInfo("重復(fù)錯(cuò)誤", 200); // 相同消息,不同code
collector2.AddInfo("唯一錯(cuò)誤", 300);
用例3:特殊字符清理
ErrorCollector collector4;
collector4.AddInfo("消息\t包含\t制表符", 100);
collector4.AddInfo("第一行\(zhòng)n第二行\(zhòng)r\n", 200);
4.3 測(cè)試結(jié)論
所有測(cè)試用例全部通過,驗(yàn)證了實(shí)現(xiàn)的正確性和健壯性。
| 測(cè)試項(xiàng) | 預(yù)期結(jié)果 | 實(shí)際結(jié)果 | 狀態(tài) |
|---|---|---|---|
| 基本添加和獲取 | 正確存儲(chǔ)和檢索 | 通過 | ? |
| 消息去重 | 相同消息只保留一個(gè) | 通過 | ? |
| 空值過濾 | 空字符串被忽略 | 通過 | ? |
| 特殊字符清理 | 移除\t\n\r | 通過 | ? |
| 按錯(cuò)誤碼獲取 | 正確過濾 | 通過 | ? |
| 跳過指定錯(cuò)誤碼 | 正確排除 | 通過 | ? |
| 輸出格式 | 豎線分隔,無多余符號(hào) | 通過 | ? |
| 空狀態(tài)檢測(cè) | 狀態(tài)正確 | 通過 | ? |
| 性能測(cè)試 | 1000條數(shù)據(jù)正常處理 | 通過 | ? |
5. 其它說明
ErrorCollector雖然是一個(gè)簡(jiǎn)單的工具類,但它在實(shí)際項(xiàng)目中發(fā)揮了重要作用。以下是一些要點(diǎn)說明和未來改進(jìn)方向:
- 其它信息收集:
ErrorCollector雖然名為錯(cuò)誤收集器,但通過創(chuàng)建多個(gè)副本,可實(shí)現(xiàn)其它信息的收集。另外,可以根據(jù) - 線程安全:在多線程環(huán)境下,需要添加互斥鎖保護(hù)
- 容量限制:防止錯(cuò)誤消息無限增長(zhǎng),可以設(shè)置最大容量
- 持久化:支持將錯(cuò)誤信息序列化到文件或數(shù)據(jù)庫
6. 小結(jié)
錯(cuò)誤收集看似簡(jiǎn)單,但設(shè)計(jì)一個(gè)好用、健壯的錯(cuò)誤收集器需要對(duì)使用場(chǎng)景有深入理解。ErrorCollector的實(shí)踐證明,即使是小型工具類,只要設(shè)計(jì)得當(dāng),也能在項(xiàng)目中發(fā)揮"四兩撥千斤"的效果。
在軟件開發(fā)中,我們常常追求復(fù)雜而全面的解決方案,但有時(shí)候,一個(gè)簡(jiǎn)潔、專注的工具可能更符合"夠用就好"的工程哲學(xué)。正如這個(gè)錯(cuò)誤收集器所展示的:在正確的抽象層次上,解決正確的問題,就是好的設(shè)計(jì)。
以上就是使用C++實(shí)現(xiàn)的錯(cuò)誤信息收集器的詳細(xì)內(nèi)容,更多關(guān)于C++錯(cuò)誤信息收集器的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C語言實(shí)現(xiàn)簡(jiǎn)單酒店管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)簡(jiǎn)單酒店管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03
數(shù)據(jù)結(jié)構(gòu)與算法中二叉樹子結(jié)構(gòu)的詳解
這篇文章主要介紹了數(shù)據(jù)結(jié)構(gòu)與算法中二叉樹子結(jié)構(gòu)的詳解的相關(guān)資料,需要的朋友可以參考下2017-04-04
c語言通過opencv實(shí)現(xiàn)輪廓處理與切割
這篇文章主要介紹了c語言通過opencv實(shí)現(xiàn)輪廓處理與切割,具有一定借鑒價(jià)值,需要的朋友可以參考下2018-01-01
C語言實(shí)現(xiàn)學(xué)生籍貫信息記錄簿
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)學(xué)生籍貫信息記錄簿,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-06-06
解析如何在C語言中調(diào)用shell命令的實(shí)現(xiàn)方法
本篇文章是對(duì)如何在C語言中調(diào)用shell命令的方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05
Qt6遠(yuǎn)程連接MySQL數(shù)據(jù)庫的簡(jiǎn)單易上手版
在Qt應(yīng)用程序里,可實(shí)現(xiàn)遠(yuǎn)程MySQL服務(wù)器的連接操作,本文就來介紹一下Qt6遠(yuǎn)程連接MySQL數(shù)據(jù)庫,具有一定的參考價(jià)值,感興趣的可以了解一下2023-11-11

