memcpy 與 memmove 是 C 標準庫 <string.h> 中最基礎的記憶體複製函數,看似簡單卻暗藏「記憶體重疊」這一經典陷阱。本文從函數簽名、實現原理、重疊場景分析、汎用實現代碼、效能差異、工程選擇指南五個維度深度解析,並提供面試高頻手寫代碼模板。
// 標準庫宣告 (string.h / cstring)
void* memcpy(void* restrict dest, const void* restrict src, size_t n);
void* memmove(void* dest, const void* src, size_t n);
| 特性 | memcpy | memmove |
|---|---|---|
關鍵字 restrict | ✅ C99 保證 dest/src 不重疊 | ❌ 無 restrict,允許重疊 |
| 重疊行為 | 未定義行為 (UB) | 定義良好,正確處理 |
| 實現複雜度 | 簡單正向複製 | 需判斷方向,雙向複製 |
| 效能 | 略快(無分支判斷) | 略慢(含方向判斷) |
| 適用場景 | 確定不重疊的高效複製 | 可能重疊、未知來源的安全複製 |
原始記憶體: [A][B][C][D][E][F][G][H]
0 1 2 3 4 5 6 7 (索引)
memcpy(dst=2, src=0, count=5) // 從 0 複製 5 bytes 到 2
// src 區間: [0,1,2,3,4] → A B C D E
// dst 區間: [2,3,4,5,6] → 目標位置
// 重疊區間: [2,3,4] (C D E)
步驟 0: [A][B][C][D][E][F][G][H]
步驟 1: 複製 [0]→[2] [A][B][A][D][E][F][G][H]
步驟 2: 複製 [1]→[3] [A][B][A][B][E][F][G][H]
步驟 3: 複製 [2]→[4] [A][B][A][B][A][F][G][H] ← 已被覆寫的 A 被複製!
步驟 4: 複製 [3]→[5] [A][B][A][B][A][B][G][H]
步驟 5: 複製 [4]→[6] [A][B][A][B][A][B][A][H]
結果: [A][B][A][B][A][B][A][H] ← 數據損壞!
memmove(dst=2, src=0, count=5) // 檢測 dst > src,選擇反向複製
步驟 0: [A][B][C][D][E][F][G][H]
步驟 1: 複製 [4]→[6] [A][B][C][D][E][F][E][H]
步驟 2: 複製 [3]→[5] [A][B][C][D][E][D][E][H]
步驟 3: 複製 [2]→[4] [A][B][C][D][C][D][E][H]
步驟 4: 複製 [1]→[3] [A][B][C][B][C][D][E][H]
步驟 5: 複製 [0]→[2] [A][B][A][B][C][D][E][H]
結果: [A][B][A][B][C][D][E][H] ← 正確!
void* my_memcpy(void* dest, const void* src, size_t count) {
// 1. 參數合法性斷言
assert(dest != nullptr && src != nullptr);
// 2. 類型轉換為字節指標
char* d = static_cast<char*>(dest);
const char* s = static_cast<const char*>(src);
// 3. 逐字節複製 (正向)
while (count--) {
*d++ = *s++;
}
// 4. 返回目的地址 (鏈式調用慣例)
return dest;
}
void* my_memmove(void* dest, const void* src, size_t count) {
assert(dest != nullptr && src != nullptr);
char* d = static_cast<char*>(dest);
const char* s = static_cast<const char*>(src);
if (d == s || count == 0) return dest; // 無操作快速返回
// 關鍵判斷:目標在源前方 → 正向複製;目標在源後方 → 反向複製
if (d < s) {
// 正向:低地址→高地址,不覆蓋未讀取數據
while (count--) {
*d++ = *s++;
}
} else {
// 反向:高地址→低地址,避免覆蓋未讀取數據
d += count;
s += count;
while (count--) {
*--d = *--s;
}
}
return dest;
}
char*?void* 無法進行指標算術運算(不知道步長)char 佔 1 byte,char* 移動 1 即前進 1 byte,精確控制字節級複製char (signed/unsigned) 可別名訪問任意類型物件表示size_t/uint64_t 批量複製 (8 bytes/次)movntdq 避免污染快取memmove 判斷方向極其廉價void* fast_memcpy(void* dest, const void* src, size_t count) {
char* d = static_cast<char*>(dest);
const char* s = static_cast<const char*>(src);
// 1. 前導字節對齊到 sizeof(size_t)
while (count && reinterpret_cast<uintptr_t>(d) % sizeof(size_t)) {
*d++ = *s++;
--count;
}
// 2. 批量複製 size_t (8 bytes on 64-bit)
size_t* dw = reinterpret_cast<size_t*>(d);
const size_t* sw = reinterpret_cast<const size_t*>(s);
while (count >= sizeof(size_t)) {
*dw++ = *sw++;
count -= sizeof(size_t);
}
// 3. 餘數字節複製
d = reinterpret_cast<char*>(dw);
s = reinterpret_cast<const char*>(sw);
while (count--) *d++ = *s++;
return dest;
}
| 場景 | 推薦函數 | 理由 |
|---|---|---|
| 確定不重疊 (如不同分配區塊) | memcpy | 無分支判斷,編譯器易優化為 SIMD,最快 |
| 可能重疊 / 同一緩衝區內移動 | memmove | 語義正確,方向判斷開銷極小 (1-2 指令) |
| 結構體賦值 / 陣列複製 | memcpy | 通常不重疊,且編譯器可優化為單指令 |
| 字串操作 (含結束符) | strcpy/strncpy | 語義更清晰,自動處理 '\0' |
| C++ 類類型 | 禁用 | 繞過建構/析構/拷貝語義,導致 UB;用 std::copy/拷貝構造 |
memcpy (glibc) : ~0.8 ns/byte (AVX2 32B/cycle)
memmove (glibc) : ~0.9 ns/byte (含方向判斷)
手寫 byte-wise : ~3.5 ns/byte (無向量化)
手寫 word-wise : ~1.2 ns/byte (部分向量化)
| 陷阱 | 代碼示例 | 後果 | 修正 |
|---|---|---|---|
| 重疊用 memcpy | memcpy(buf+2, buf, 10); | 數據損壞 (UB) | 改用 memmove |
| 結構體含指標/虛表 | memcpy(&obj2, &obj1, sizeof(obj1)); | 淺拷貝、雙重釋放、虛表指針錯誤 | 實作拷貝構造/賦值運算符 |
| 類類型對象 | memcpy(&vec2, &vec1, sizeof(vector)); | 容器內部指標重複擁有、崩潰 | 用 vec2 = vec1 或 std::copy |
| count 計算錯誤 | memcpy(dst, src, sizeof(ptr)); 指標大小非對象大小 | 複製不完整或越界 | sizeof(*ptr) 或 sizeof(Type) |
| const 正確性 | memcpy(const_cast<void*>(dst), src, n); | 修改只讀記憶體 → 段錯誤 | 確保目標可寫 |
#include <algorithm>
#include <vector>
#include <string>
// 1. std::copy - 迭代器抽象,型別安全,支援重疊 (同 memmove 語義)
vector<int> v1 = {1,2,3,4,5}, v2(5);
copy(v1.begin(), v1.end(), v2.begin()); // 非重疊
copy(v1.begin()+1, v1.end(), v1.begin()); // 重疊安全 (向前複製)
// 2. std::copy_backward - 明確反向複製 (重疊安全)
copy_backward(v1.begin(), v1.end()-1, v1.end()); // 向後移動
// 3. std::move - 移動語義 (C++11),資源轉移而非複製
vector<int> v3 = move(v1); // v1 變空,v3 接管資源
// 4. std::string - 字串專用,自動管理記憶體
string s1 = "hello", s2;
s2 = s1; // 深拷貝,安全
// 5. span (C++20) - 非擁有視圖,配合 copy
span<int> src(v1), dst(v2);
copy(src.begin(), src.end(), dst.begin());
dest < src 正向,dest > src 反向,相等/零長度直接返回dest < src 時反向會覆蓋未讀數據 (對稱情況)memcpy 與 memmove 的核心差異在於記憶體重疊語義:
restrict),正向複製,**極致效能**,違反假設即 UB工程鐵律:
std::copy/std::move/拷貝構造char* 字節級操作本質記憶體複製是系統程式設計的基石操作,徹底掌握其重疊語義、實現細節與現代替代方案,能避免 90% 以上的記憶體操作 Bug,並寫出高效、安全、可維護的基礎設施代碼。