首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C >> C 語言入門

C 語言 memcpy 函數深度解析:從錯誤實現到標準庫級優化

     編輯:C 語言入門

前言:一次失敗的面試經驗

昨天嘗試手寫 memcpy 函數,測試通過後信心滿滿,但細究細節卻發現問題重重。查閱資料後才發現,自己寫的版本簡直「無法直視」。特此總結,以為教訓。

初版代碼:充滿缺陷的實現

char *mymemcpy(char *dest, const char *src, int n) {
    char *pdest = dest;
    char *psrc = src;
    for(; n > 0; n--) {
        *pdest = *psrc;
        pdest++; psrc++;
    }
}

存在的嚴重問題

  1. 無參數合法性檢查:僅在極理想條件下工作,參數錯誤即崩潰
  2. 無返回值:直接修改 dest 指針,違背標準庫約定
  3. 多餘的指針變量:以為需要保護實參指針位置,實則形參指針位置改變不影響實參

關鍵誤區:指針傳參的本質

受「地址傳參可修改數據」影響,誤以為形參指針與實參指針等同。實際上:

  • 形參指針本質是局部變量,佔有獨立內存空間
  • 修改形參指針指向的內容會影響實參指向的內存
  • 修改形參指針自身位置(如 ++)絕不影響實參指針位置

標準 memcpy 實現的五大核心要點

  1. 參數判斷:檢查指針非空、長度合法
  2. 保留首地址:需返回目的地址,必須保存初始 dest
  3. void* 類型處理:源目標類型可能不同,需強轉為 char* 按字節拷貝
  4. void 類型不能作右值:void* 不能直接賦值或運算
  5. 形參指針獨立性:形參指針位置變化不影響實參

參考實現演進過程

版本一:基礎框架(含錯誤)

void *memcpy(void *dest, const void *src, size_t count) {
    if (NULL == dest || NULL == src || count <= 0) return NULL;
    while (count--) *dest++ = *src++;
    return dest;  // 錯誤:返回了遞增後的指針
}

編譯錯誤:error: invalid use of void expression —— void* 不能直接解引用賦值。

版本二:強制類型轉換修正

void *memcpy(void *dest, const void *src, size_t count) {
    if (NULL == dest || NULL == src || count <= 0) return NULL;
    while (count--) *(char *)dest++ = *(char *)src++;
    return dest;  // 仍錯誤:返回值應為原始 dest
}

版本三:專家修正版(網友指正)

void *memcpy(void *dest, const void *src, int count) {
    void *ptr = dest;  // 保存首地址用於返回
    if (NULL == dest || NULL == src || count <= 0) return NULL;
    while (count--) *(char *)dest++ = *(char *)src++;
    return ptr;  // 正確返回起始地址
}

關鍵修正點:

  • 使用 int count 而非 size_t:size_t 為無符號類型,count <= 0 僅判斷 ==0,傳入 -1 會變成巨大無符號數
  • 保存 ptr = dest 確保返回正確起始地址

進階優化:內存對齊與塊拷貝

void *memcpy(void *dst, const void *src, size_t num) {
    int nchunks = num / sizeof(unsigned long);  // 按 CPU 位寬拷貝
    int slice = num % sizeof(unsigned long);    // 剩餘按字節拷貝

    unsigned long *s = (unsigned long *)src;
    unsigned long *d = (unsigned long *)dst;

    while (nchunks--) *d++ = *s++;  // 大塊拷貝
    while (slice--) *((char *)d++) = *((char *)s++);  // 尾部字節拷貝

    return dst;
}

潛在問題:若 dst 和 src 地址未對齊,效率反而下降。是否需先處理對齊?

內存重疊問題:memcpy vs memmove

內存重疊:目的地址落在源內存區間內,導致數據被覆蓋破壞。

情況一:dest ≤ src + size(前向重疊)

前向重疊示意圖

源數據後部被新拷貝數據覆蓋,結果錯誤。

情況二:dest + size ≥ src(後向重疊)

後向重疊示意圖

雖源數據被改變,但拷貝結果正確。

解決方案:逆向拷貝

void *memcpy(void *dest, const void *src, size_t count) {
    char *d, *s;
    if (dest > src && dest < src + count) {  // 重疊且 dest 在 src 後
        d = (char *)(dest + count - 1);
        s = (char *)(src + count - 1);
        while (count--) *d-- = *s--;  // 從高地址向低地址拷貝
    } else {
        d = dest; s = src;
        while (count--) *d++ = *s++;
    }
    return dest;
}

歷史補充:為什麼有 memmove?

  • memcpy 早期版本無重疊保護,為兼容性保留
  • 新版本加入重疊處理,改名為 memmove
  • 注意:memmove 名稱含「move」但實為拷貝,源數據不會消失

總結:寫好 memcpy 的三條鐵律

  1. 不破壞形參:定義臨時變量操作,保留原始指針用於返回
  2. 考慮指針類型:不同類型指針不可直接遞增賦值,必須統一轉為 char* 按字節操作
  3. 重疊情況從高地址向前拷貝:避免數據覆蓋破壞
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved