字符串操作是 C 語言程式設計的高頻需求。標準庫 提供的函數雖然好用,但理解其底層實現邏輯對於掌握指標操作、邊界處理、性能優化至關重要。本文從零手寫 7 個核心字符串/內存函數,附帶詳細註解與測試代碼。
'\0' 結束標誌'\0' 判定字符串邊界const 正確性:源字符串應標記 const,目標字符串不可為 const
char* my_strcpy(char* dest, const char* src) {
assert(dest != NULL && src != NULL);
char* ret = dest; // 保存返回目標地址
while ((*dest++ = *src++) != '\0') // 賦值+判斷一體化
; // 空循環體
return ret;
}
關鍵點:返回原始 dest 支援鏈式調用;賦值表達式值即為賦值結果,利用此特性合併複製與判斷。
char* my_strncpy(char* dest, const char* src, size_t n) {
assert(dest != NULL && src != NULL);
char* ret = dest;
size_t i = 0;
// 複製前 n 個字符或遇到 '\0' 為止
while (i < n && (*dest++ = *src++) != '\0')
i++;
// 若 src 長度 < n,補齊 '\0'
while (i < n) {
*dest++ = '\0';
i++;
}
return ret;
}
注意:標準 strncpy 不保證結果以 '\0' 結尾(當 src 長度 ≥ n 時),使用時需手動補終止符。
int my_strcmp(const char* s1, const char* s2) {
assert(s1 != NULL && s2 != NULL);
while (*s1 && *s1 == *s2) { // 相同且未結束則繼續
s1++;
s2++;
}
// 返回差值:>0 表示 s1>s2,=0 相等,<0 表示 s1
細節:轉為 unsigned char 防止有符號字符比較錯誤(如 0xFF > 0x01 在 signed char 中為負)。
size_t my_strlen(const char* str) {
assert(str != NULL);
const char* p = str;
while (*p != '\0')
p++;
return p - str; // 指標相減得元素個數
}
遞歸版(教學用,生產環境慎用棧溢出風險):
size_t my_strlen_rec(const char* str) {
return *str ? 1 + my_strlen_rec(str + 1) : 0;
}
char* my_strstr(const char* haystack, const char* needle) {
assert(haystack != NULL && needle != NULL);
if (*needle == '\0') return (char*)haystack; // 空串匹配開頭
for (; *haystack; haystack++) {
if (*haystack == *needle) { // 找到潛在起始位置
const char* h = haystack;
const char* n = needle;
while (*h && *n && *h == *n) { // 逐字符比對
h++;
n++;
}
if (*n == '\0') // needle 完全匹配
return (char*)haystack;
}
}
return NULL; // 未找到
}
char* my_strcat(char* dest, const char* src) {
assert(dest != NULL && src != NULL);
char* ret = dest;
// 1. 定位到 dest 末尾 '\0'
while (*dest != '\0')
dest++;
// 2. 從此處複製 src
while ((*dest++ = *src++) != '\0')
;
return ret;
}
前提:dest 必須有足夠空間容納拼接結果,否則緩衝區溢出。
void* my_memcpy(void* dest, const void* src, size_t n) {
assert(dest != NULL && src != NULL);
// 按字節複製,支援任意類型數據
unsigned char* d = (unsigned char*)dest;
const unsigned char* s = (const unsigned char*)src;
while (n--)
*d++ = *s++;
return dest;
}
與 strcpy 差異:memcpy 不關心 '\0',按指定字節數複製,適用於結構體、數組等二進制數據。
int main() {
char str1[100], str2[] = "nihao haha";
char str3[50] = "Thanks ,thank you.";
char* s = "you are welcome.";
printf("=== strcpy ===\n");
my_strcpy(str1, str2);
printf("Result: %s\n", str1);
printf("\n=== strncpy ===\n");
my_strncpy(str1, str2, 8);
str1[8] = '\0'; // 手動補終止符
printf("First 8 chars: %s\n", str1);
printf("\n=== strcmp ===\n");
printf("'hello' vs 'hello': %d\n", my_strcmp("hello", "hello"));
printf("'abc' vs 'abd': %d\n", my_strcmp("abc", "abd"));
printf("\n=== strlen ===\n");
printf("Length of '%s': %zu\n", str2, my_strlen(str2));
printf("\n=== strstr ===\n");
char* pos = my_strstr("Hello, world!", "world");
printf("Found at: %s\n", pos ? pos : "NULL");
printf("\n=== strcat ===\n");
my_strcat(str3, s);
printf("Concatenated: %s\n", str3);
printf("\n=== memcpy ===\n");
struct { int a; double b; } src = {42, 3.14}, dst;
my_memcpy(&dst, &src, sizeof(src));
printf("Copied struct: a=%d, b=%.2f\n", dst.a, dst.b);
return 0;
}
| 函數 | 高危場景 | 防範措施 |
|---|---|---|
| strcpy/strcat | 目標緩衝區過小 | 使用 strncpy/strncat 或 snprintf |
| strncpy | 結果無終止符 | 手動設置 dest[n] = '\0' |
| strcmp | 有符號字符比較 | 轉 unsigned char 比較 |
| memcpy | 內存區域重疊 | 重疊場景用 memmove |
手寫標準庫字符串函數是鍛煉指標操作能力的最佳練習。核心模式皆為:指標遍歷 + 邊界判斷 + 數據移動。實際開發中應優先使用經過嚴格測試的標準庫實現,但在嵌入式無標準庫環境、需要定制行為(如區分大小寫比較)、或分析性能瓶頸時,理解底層實現至關重要。