將二進位檔案轉為十六進位文字輸出時,發現 lstrcat 導致效能隨字串增長急劇下降。本文深入分析字串連接效能陷阱,從 C 執行庫 strcat 到 MFC CString 內部實作,揭示「黑箱」背後的真相。
// 初版:使用 lstrcat
void Save()
{
// ... 讀取檔案到 lpbySrc ...
for (DWORD i = 0; i < dwSize; i++)
{
wsprintf(szByte, (i%16==15) ? _T("%02X\r\n") : _T("%02X "), lpbySrc[i]);
lstrcat(lpDst, szByte); // 每次都要掃描至字串尾端!
}
// 進度條越跑越慢...
}
char* strcat(char* dst, const char* src)
{
char* cp = dst;
while (*cp) cp++; // 1. 尋找結束符 O(n)
while (*cp++ = *src++); // 2. 複製內容
return dst;
}
每次連接都要從頭掃描找 \0,總複雜度 O(n²)。資料量大時明顯拖慢。
void SaveOptimized()
{
// ... 讀取檔案 ...
DWORD j = 0; // 尾端索引
for (DWORD i = 0; i < dwSize; i++)
{
int k = (i%16==15) ? 4 : 3; // "\r\n" 4 字元,否則 3 字元
wsprintf(szByte, (i%16==15) ? _T("%02X\r\n") : _T("%02X "), lpbySrc[i]);
lstrcpy(&lpDst[j], szByte); // 直接寫入尾端
j += k;
}
// O(n) 線性複雜度
}
void CString::ConcatCopy(int nSrc1Len, LPCTSTR lpszSrc1,
int nSrc2Len, LPCTSTR lpszSrc2)
{
int nNewLen = nSrc1Len + nSrc2Len;
if (nNewLen != 0)
{
AllocBuffer(nNewLen); // 1. 配置精確大小緩衝區
memcpy(m_pchData, lpszSrc1, nSrc1Len*sizeof(TCHAR)); // 2. 複製第一段
memcpy(m_pchData + nSrc1Len, lpszSrc2, nSrc2Len*sizeof(TCHAR)); // 3. 複製第二段
}
}
operator+= 呼叫 ConcatCopy → 2 次 memcpy)| 方法 | 複雜度 | 記憶體操作 | 適用場景 |
|---|---|---|---|
| strcat/lstrcat | O(n²) | 掃描尾端 + 複製 | 極小字串、偶爾連接 |
| 手動尾端索引 | O(n) | 單次複製 | 大量資料串流、高效能需求 |
| CString::operator+= | O(n²) * | 重新配置 + 2×memcpy | 一般用途、程式碼可讀性優先 |
| CString::Format / 陣列收集後一次 Format | O(n) | 單次格式化 | 已知片段數量、批次處理 |
* 因每次 += 都重新配置並複製現有內容
「黑箱」並非完美萬能。C 風格字串的「零結尾」機制、CString 的「複製語意」,在特定場景下都會成為效能瓶頸。理解內部實作,才能在關鍵路徑做出正確選擇。
Format 或 memcpy 組裝CString 可讀性優先,效能非瓶頸時無需優化