跳转到内容

Lesson 16: 字符串拷贝 CNB

练习任务

本课包含两个递进练习:

练习 1(mystrcpy 基础实现):实现 char *mystrcpy(char *dest, const char *src) 函数,将 src 指向的字符串(包括 '\0')逐字符拷贝到 dest 指向的缓冲区,返回 dest 的起始地址。main 从标准输入读取字符串,调用 mystrcpy 后打印结果。

期望输出(输入 copy me):

copy me

练习 2(delta 优化版):实现 char *mystrcpy_delt(char *dst, const char *src),利用 dstsrc 之间的固定偏移量 delt = dst - src,将双指针操作缩减为单指针操作,减少对 dst 指针变量的存取次数。

期望输出(输入 delta copy):

delta copy

提示strcpy 的核心是一行 C 惯用法:while ((*dst++ = *src++) != '\0');。这行代码蕴含了赋值表达式的返回值、后置自增的时机、'\0' 的判断三个关键知识点。delta 优化的思路是:如果 dst 始终在 src 后面偏移 delta 个字节,那么 src[delta] 就等价于 *dst,你只需要移动 src 指针即可。注意 delta 版要求 dstsrc 在同一个数组内。

核心知识点

  • 字符指针 char* — 指向字符或字符串首地址的指针,是字符串操作的核心工具
  • const 限定符 — const char *src 承诺不修改源字符串,编译器帮助检查
  • assert 断言 — 运行时检查空指针 NULL,调试期间快速暴露 bug
  • while (*dst++ = *src++) — C 语言最经典的指针惯用法,将赋值、自增、判断压缩为一行
  • 赋值表达式的返回值 — a = b 的值就是 b 的值,使得 while (a = b) 成为可能
  • \0 终止符的判断 — (*src) != '\0' 等价于 *src,因为 '\0' 的 ASCII 值为 0
  • 后置自增 ++ 的时机 — *p++ 先解引用旧值,再移动指针
  • 函数返回 char* — 返回目标指针实现链式调用,如 printf("%s", mystrcpy(d, s))
  • 指针运算的优先级与结合性 — *dst++ 等价于 *(dst++),解引用优先于自增
  • delta 优化 — 用偏移量 delt = dst - src 将双指针操作化为单指针
  • 内存对齐 — CPU 以 WORD(4 字节)为单位访问内存最快,未对齐访问需两次总线操作
  • memcpy vs strcpymemcpy 按字节数拷贝,不依赖 '\0'strcpy 依赖终止符
  • 安全字符串函数 — strncpysnprintf 限制拷贝长度,防止缓冲区溢出
  • 工业级 strcpy — glibc 按 4 字节对齐批量拷贝,SSE/NEON 指令加速

代码框架

练习 1:基础 mystrcpy

mystrcpy.c
c
#include <stdio.h>
#include <assert.h>

char *mystrcpy(char *dest, const char *src)
{
    assert(dest != NULL && src != NULL);

    // 在这里实现 strcpy:
    //   1. 保存 dest 的起始地址 (用于返回)
    //   2. 用一个循环: 逐字符复制 *src 到 *dest
    //   3. 当复制完 '\0' 后终止循环
    //   4. 返回保存的起始地址
}

int main(void)
{
    char s1[256] = "";
    char s2[256];

    fgets(s2, sizeof(s2), stdin);
    // 去掉末尾换行符
    int i = 0;
    while (s2[i] && s2[i] != '\n') i++;
    s2[i] = '\0';

    mystrcpy(s1, s2);
    printf("%s\n", s1);

    return 0;
}

练习 2:delta 优化版

strcpy_delta.c
c
#include <stdio.h>

char *mystrcpy_delt(char *dst, const char *src)
{
    // 在这里实现 delta 版:
    //   1. 计算偏移量: int delt = dst - src;
    //   2. 用 (char*) 转换 src 的类型
    //   3. 循环: 通过 s[delt] 访问 dst 对应位置
    //   4. 当 *s == '\0' 时停止
    //   5. 最后补上 '\0'
    return dst;
}

// buf 保证 src 和 dst 在同一数组内,delta 运算合法
char buf[512];

int main(void)
{
    char *src = buf;
    char *dst = buf + 256;

    fgets(src, 256, stdin);
    int i = 0;
    while (src[i] && src[i] != '\n') i++;
    src[i] = '\0';

    mystrcpy_delt(dst, src);
    printf("%s\n", dst);

    return 0;
}

填充框架的关键思考:*dst++ = *src++ 的执行顺序是怎样的?赋值表达式的值是什么?循环何时终止?delta 版本中 delt = dst - src 这个减法在什么条件下才合法?

TIP

先不要往下翻看参考解答。尝试自己先实现基础版——把 *dst++ = *src++ 这一行的语义彻底想清楚。做完基础版再挑战 delta 版,它需要你对指针运算有更深的理解。


深度讲解

1. 指针回顾:从字符数组到字符指针

1.1 字符数组回顾(连接 Lesson 09)

在 Lesson 09 中,我们用字符数组存储 itoa 的转换结果:

char_array_review.c
c
char buf[10];           // 10 个连续 char,在栈上分配
buf[0] = '1';
buf[1] = '2';
buf[2] = '3';
buf[3] = '\0';          // 字符串终止符
printf("%s\n", buf);    // 输出: 123

一个 char 数组存放字符串时,末尾必须有一个值为 0 的字节——'\0'(ASCII 码 0,也称为 NUL 终止符)。printf("%s") 会从起始地址开始逐字节打印,直到遇到 '\0' 为止。

内存布局:
┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐
 '1' '2' '3' │'\0'  ?  ?  ?  ?  ?  ?
└─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘
  buf[0] buf[1] buf[2] buf[3] ...                     buf[9]

printf("%s", buf) 读取: '1''2''3''\0' (停止)

1.2 字符指针:指向字符的指针

char_pointer_basics.c
c
char ch = 'A';
char *p = &ch;          // p 指向 ch 的地址

printf("%c\n", *p);     // 解引用: 输出 'A'
printf("%p\n", (void*)p); // 输出 ch 的内存地址

字符指针 char* 可以指向单个字符,也可以指向字符串的首字符:

pointer_to_string.c
c
char str[] = "Hello";       // 字符数组
char *p = str;              // p 指向 str[0] 即 'H'
// 或者直接用字符串字面量:
char *q = "World";          // q 指向只读字符串 "World" 的首字符

printf("%c\n", *p);         // 'H'
printf("%c\n", *(p + 1));   // 'e'  —— 指针算术: p+1 指向下一个字符
printf("%s\n", p);          // "Hello" —— %s 从 p 开始打印直到 '\0'
指针与字符串的关系:
  p ──→ ┌───┬───┬───┬───┬───┐
 H e l l o\0
        └───┴───┴───┴───┴───┴───┘
         str[0]                  str[5]

  *p 'H'    (p 指向的字符)
  *(p+1)  → 'e'    (p+1 指向下一个字符)
  p[0] 'H'    (等价于 *p)
  p[1] 'e'    (等价于 *(p+1))

NOTE

p[i] 本质上就是 *(p + i) 的语法糖。数组下标 [] 运算符在 C 语言中就是指针运算的缩写。回顾 Lesson 09 中 buf[i] 的写法,其实也可以用 *(buf + i) 替代。

1.3 字符指针 vs 字符数组

这是一个重要的区别,贯穿字符串操作始终:

pointer_vs_array.c
c
// 方式 1: 字符数组(可修改)
char s1[32] = "Hello World";
s1[0] = 'h';               // ✅ 合法:修改数组内容
printf("%s\n", s1);         // "hello World"

// 方式 2: 指针指向字符串字面量(只读)
char *s2 = "Hello World";
// s2[0] = 'h';            // ❌ 未定义行为!字符串字面量存储在只读区域
printf("%s\n", s2);         // "Hello World"
特性char s[32] = "Hello"char *s = "Hello"
存储位置栈(或 BSS 段,全局数组)指针在栈,字符串在只读数据段(.rodata)
可修改性可修改数组内容不可修改字符串内容(UB)
大小sizeof(s) = 32(数组总大小)sizeof(s) = 8(指针大小,64 位系统)
赋值不能整体赋值 s = "bye"可以改变指向 s = "bye"
底层实现编译器生成 32 字节空间 + 初始化代码指针指向编译器存储的字符串常量
内存布局对比:

char s1[32] = "Hello";            char *s2 = "Hello";
栈:                                .rodata (只读数据段):
┌──────────────────────┐           ┌───┬───┬───┬───┬───┬───┐
 H e l l o\0│... H e l l o\0
└──────────────────────┘           └───┴───┴───┴───┴───┴───┘
  s1 (数组首地址)                     ▲

                                  栈:
                                  ┌──────┐
 指针  │──┘
                                  └──────┘
                                    s2

CAUTION

使用 char *p = "literal" 时,千万不要尝试修改 p 指向的内容。这会导致段错误(segfault)或更隐蔽的数据损坏。如果需要一个可修改的字符串,用 char buf[N]malloc 分配。


2. const 限定符与防御性编程

2.1 const 的语义:承诺不修改

const_semantics.c
c
// const 在 * 左边:指向的内容不可修改
char *mystrcpy(char *dst, const char *src)
//                        ↑
//                   const char *src 读作:
//                   "src 是一个指针,指向 const char(不可修改的字符)"

// 等价写法:
// const char *src   ← 推荐:const 在类型前
// char const *src   ← 也正确:const 在 * 前

关键区分

const_positions.c
c
const char *p;      // p 指向的内容不可修改(*p 只读)
char * const p;     // p 本身不可修改(p 不能指向别处)
const char * const p; // p 和 *p 都不可修改

mystrcpy 的签名中,const char *src 告诉调用者:我不会修改你传给我的源字符串。这是 API 设计中的承诺——编译器会帮你强制执行。

const_enforcement.c
c
void copy(const char *src, char *dst)
{
    *dst = *src;      // ✅ 读 src 合法
    // *src = 'X';    // ❌ 编译错误: 试图修改 const 数据
}

2.2 为什么源字符串要用 const

why_const.c
c
// 如果没有 const,调用者无法区分方向:
char *bad_strcpy(char *dst, char *src);   // 哪个是输入?哪个是输出?

// 有 const,意图一目了然:
char *good_strcpy(char *dst, const char *src);
//                 ↑ 输出            ↑ 输入(只读)

这是标准库 strcpy 的函数签名:char *strcpy(char *dest, const char *src)const 让代码自文档化——一眼就能看出数据的流向。


3. 空指针检查:assert 断言

3.1 为什么需要检查 NULL

null_pointer_danger.c
c
char *p = NULL;
// *p = 'A';        // ❌ 段错误!访问空指针
// strcpy(p, "hi"); // ❌ 段错误!标准库 strcpy 也不检查 NULL

C 标准库的 strcpy 不会检查 NULL 参数——传入 NULL 是未定义行为。在调试阶段,我们希望尽早暴露这类 bug。

3.2 assert 的工作原理

assert_demo.c
c
#include <assert.h>

char *mystrcpy(char *dst, const char *src)
{
    assert(dst != NULL);    // 如果 dst 为 NULL,程序在此终止
    assert(src != NULL);    // 如果 src 为 NULL,程序在此终止
    // 或者合并:
    // assert(dst != NULL && src != NULL);

    // ... 正常逻辑 ...
}

assert 的行为

  • 条件为真:什么都不发生,程序继续
  • 条件为假:打印错误信息(含文件名、行号、表达式),调用 abort() 终止程序
  • 仅在调试版本生效:定义 NDEBUG 宏后,所有 assert 被编译为空操作
bash
# 调试构建(assert 生效)
gcc -g -O0 mystrcpy.c -o mystrcpy

# 发布构建(assert 失效)
gcc -DNDEBUG -O2 mystrcpy.c -o mystrcpy

IMPORTANT

assert 只应用于编程错误(如传了不该为 NULL 的参数),不应处理运行时错误(如文件打开失败)。运行时错误应该用 if + return 错误码来处理。本课中 assert 的作用是在调试阶段尽早暴露调用者的错误。

3.3 assert 与 if 的选择

assert_vs_if.c
c
// assert: 用于检查「绝不应该发生」的情况
assert(ptr != NULL);         // 调用者传 NULL 是程序员的 bug

// if: 用于处理「可能发生」的运行时错误
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
    perror("fopen");
    return -1;               // 文件不存在不是 bug,需要优雅处理
}

4. C 语言最经典的指针惯用法:while (*dst++ = *src++)

4.1 朴素写法 → 惯用法:逐步演化

让我们从最朴素的写法开始,一步步压缩为经典的一行代码:

strcpy_evolution.c
c
// 版本 1: 最朴素的写法(5 行)
char *mystrcpy_v1(char *dst, const char *src)
{
    int i = 0;
    while (src[i] != '\0') {
        dst[i] = src[i];
        i++;
    }
    dst[i] = '\0';          // 不要忘记拷贝终止符!
    return dst;
}

// 版本 2: 用指针替代下标(4 行)
char *mystrcpy_v2(char *dst, const char *src)
{
    char *ret = dst;
    while (*src != '\0') {
        *dst = *src;
        dst++;
        src++;
    }
    *dst = '\0';
    return ret;
}

// 版本 3: 把赋值和自增合并(3 行)
char *mystrcpy_v3(char *dst, const char *src)
{
    char *ret = dst;
    while (*src != '\0')
        *dst++ = *src++;    // 一行做三件事:赋值 + dst++ + src++
    *dst++ = '\0';          // 拷贝终止符
    return ret;
}

// 版本 4: 利用赋值表达式的返回值(2 行)  ← 经典写法
char *mystrcpy_v4(char *dst, const char *src)
{
    char *ret = dst;
    while ((*dst++ = *src++) != '\0')
        ;                   // 空循环体:赋值 + 判断合二为一
    return ret;
}

// 版本 5: 利用 '\0' == 0 简化条件(2 行)
char *mystrcpy_v5(char *dst, const char *src)
{
    char *ret = dst;
    while ((*dst++ = *src++))
        ;                   // 当复制完 '\0' 时,赋值表达式的值为 0,循环终止
    return ret;
}

版本 5 是最终的 C 惯用法。让我们彻底拆解这一行代码。

4.2 逐层拆解 while ((*dst++ = *src++))

idiom_decomposition.c
c
// 原语句:
while ((*dst++ = *src++))
    ;

// 等价展开:
while (1) {
    // 第一步: 解引用 src,取出当前字符
    char c = *src;

    // 第二步: 把字符写入 dst
    *dst = c;

    // 第三步: 移动两个指针(后置自增)
    src = src + 1;
    dst = dst + 1;

    // 第四步: 赋值表达式 (*dst = c) 的值就是 c
    // while (c) 即 while (c != 0)
    // 当 c == '\0'(即 0)时,循环终止
    if (c == '\0')
        break;
}

关键机制总结

步骤表达式效果
① 解引用 src*src取出源字符
② 赋值*dst = *src将字符写入目标
③ 后置自增dst++, src++两个指针各前进一个 char
④ 返回值赋值表达式返回被赋的值while 用它判断是否为 '\0'

4.3 赋值表达式的返回值

这是理解这行惯用法的核心——C 语言中,赋值表达式 a = b 的值就是 b 的值:

assignment_value.c
c
int x, y;

x = (y = 5);        // y = 5 的值是 5,赋给 x,所以 x = 5, y = 5

if ((x = getchar()) != EOF)  // 赋值后立即比较
    putchar(x);

// 对于 while ((*dst++ = *src++)):
//   赋值 (*dst = *src) 的值就是 *src 的值
//   所以 while 判断的是「刚复制过去的字符是否为 '\0'」
assignment_trace.c
c
#include <stdio.h>

int main(void)
{
    char src[] = "Hi";
    char dst[10];
    char *d = dst, *s = src;

    printf("初始: d=%p, s=%p\n", (void*)d, (void*)s);

    // 手动追踪第一次迭代
    printf("第1步: *s = '%c' (%d)\n", *s, *s);
    *d = *s;                          // dst[0] = 'H'
    printf("第2步: 赋值后 *d = '%c'\n", *d);
    int val = (*d = *s);              // 赋值表达式的值
    printf("第3步: 赋值表达式的值 = %d ('%c')\n", val, val);
    d++; s++;
    printf("第4步: d=%p, s=%p, *s = '%c'\n", (void*)d, (void*)s, *s);

    return 0;
}

输出:

初始: d=0x..., s=0x...
第1步: *s = 'H' (72)
第2步: 赋值后 *d = 'H'
第3步: 赋值表达式的值 = 72 ('H')
第4步: d=..., s=..., *s = 'i'

'H' 的 ASCII 码是 72,非零,循环继续。当复制 '\0'(值为 0)时,赋值表达式的值为 0,while(0) 终止。

4.4 后置自增 ++ 的时机

postfix_increment.c
c
char arr[] = "AB";
char *p = arr;

// *p++ 的执行顺序:
//   1. 先取 *p(得到 'A')
//   2. 再 p = p + 1(指针前进)
//   3. 整个表达式的值是 *p 的旧值('A')

char c = *p++;      // c = 'A', p 现在指向 'B'
printf("%c\n", c);  // A
printf("%c\n", *p); // B
increment_timeline.c
c
// while ((*dst++ = *src++))
// 每次迭代的时间线:
//
//  时刻 1: 取 *src (字符 c)
//  时刻 2: *dst = c (写入目标)
//  时刻 3: src++, dst++ (两个指针都前进)
//  时刻 4: 判断 c != '\0' ? 继续 : 退出
//
// 关键: 指针在「赋值完成之后」才自增
//       所以循环结束时,两个指针都指向 '\0' 之后的位置

NOTE

*p++ 等价于 *(p++),因为 ++ 的优先级高于 *。但 *++p 是先自增再解引用——行为完全不同。本课使用的 *dst++ = *src++ 始终是后置自增(先解引用再自增)。


5. 函数返回值:为什么返回 char*

5.1 链式调用

chain_call.c
c
// 返回 dst 指针使得可以链式调用:
printf("%s\n", mystrcpy(dst, src));

// 等价于:
mystrcpy(dst, src);
printf("%s\n", dst);

标准库 strcpy 返回 dst 起始地址,正是为了支持这种链式调用。实际上,大多数字符串函数(strcatstrncpy 等)都返回目标指针。

chained_operations.c
c
char buf[256];
// 链式操作的典型场景:
printf("结果: %s\n", mystrcpy(buf, "hello"));

// 甚至可以:
mystrcpy(buf + 5, " world");   // 从 buf[5] 开始拷贝
printf("%s\n", buf);           // "hello world"

5.2 保存起始地址的必要性

save_start_address.c
c
char *mystrcpy(char *dst, const char *src)
{
    char *ret = dst;            // 保存起始地址 ← 必须在循环前保存!
    while ((*dst++ = *src++))
        ;
    return ret;                 // 返回起始地址
    // return dst;              // ❌ 错误: dst 已经指向 '\0' 之后了!
}

dst 在循环中不断前进,结束时已经越过 '\0'。必须提前保存起始地址,否则无法返回正确的值。


6. delta 优化:减少指针变量的存取次数

6.1 优化动机

在经典版 strcpy 中,每次循环同时移动两个指针(dst++src++)。如果 dstsrc 的相对位置是固定的,我们只需要移动一个指针,用偏移量计算另一个指针的位置。

delta_concept.c
c
// 经典版: 每次循环操作两个指针
while ((*dst++ = *src++))
    ;
// 每次迭代: 读 *src, 写 *dst, src++, dst++(四次内存/寄存器操作)

// delta 版: 只操作一个指针 + 一个固定偏移量
int delt = dst - src;   // 计算偏移量(以字节为单位)
while ((s[delt] = *s++) != '\0')
    ;
// 每次迭代: 读 *s, 写 s[delt], s++(三次操作)
// s[delt] 由编译器计算: *(s + delt)

优化原理:减少了一次指针变量 dst 的自增操作。在古老的编译器或嵌入式平台上,减少一次变量的存取可能带来可感知的性能提升。

6.2 delta 的完整实现

delta_implementation.c
c
char *mystrcpy_delt(char *dst, const char *src)
{
    assert(dst != NULL && src != NULL);

    char *s = (char *)src;       // 去掉 const 用于计算偏移量
    int delt = dst - src;        // 偏移量 = dst 地址 - src 地址

    while (*s != '\0')           // 遍历源字符串
    {
        s[delt] = *s;            // 写入 dst 对应位置
        s++;                     // 只移动 s 指针
    }
    s[delt] = '\0';              // 补上终止符

    return dst;
}
delta 原理图解:

假设 src = buf[0], dst = buf[256]
delt = dst - src = 256

   src buf[0]   buf[1]   buf[2]   ...   buf[255] | buf[256] ... 
          ┌───┬───┬───┬───────────┬───┐┌───┬───
 H e l  ... o ││\0 ...
          └───┴───┴───┴───────────┴───┘└───┴───
            s   s+1  s+2               s+5

         s[256] s[257] s[258]      s[261]

          ┌───┬───┬───┬───────────┬───┐┌───┬───
 H e l  ... o ││\0 ...
          └───┴───┴───┴───────────┴───┘└───┴───
   dst buf[256]                            buf[261]

s[delt] = s[256] 指向 buf[256],即 dst 的当前位置

WARNING

delta 优化要求 dstsrc 指向同一个数组(或同一块连续分配的内存)内的元素。C 标准规定:两个指针相减只有当它们指向同一个数组内的元素(或数组末尾之后一个位置)时才是合法的。本课练习中 buf[512] 保证了这个前提。

6.3 指针相减的语义

pointer_subtraction.c
c
#include <stdio.h>

int main(void)
{
    char buf[512];
    char *src = buf;           // 指向 buf[0]
    char *dst = buf + 256;     // 指向 buf[256]

    // 指针相减: 结果是两个地址之间的「元素个数」
    int delt = dst - src;      // 256
    printf("delt = %d\n", delt);

    // 地址差 = delt * sizeof(char) = 256 * 1 = 256 字节
    printf("地址差 = %td 字节\n", (char*)dst - (char*)src);

    // 不同类型指针的减法结果不同:
    int arr[100];
    int *p1 = arr;
    int *p2 = arr + 10;
    int diff = p2 - p1;        // 10(个 int),不是 40(个字节)
    printf("diff = %d\n", diff);

    return 0;
}
表达式结果类型含义
p2 - p1ptrdiff_tp1p2 之间的元素个数
(char*)p2 - (char*)p1ptrdiff_t两个地址之间的字节数

7. 从 strcpy 到 memcpy:终止符与字节数

7.1 strcpy 的局限

strcpy_limitation.c
c
// strcpy 依赖 '\0' 作为终止条件
char buf[16];
strcpy(buf, "hello world!");   // ✅ 字符串有 '\0',正常

// 但如果数据中包含 '\0' 怎么办?
char binary[] = {'H', 'e', '\0', 'l', 'o'};  // 中间有 '\0'
strcpy(buf, binary);           // ❌ 只拷贝了 "He"!遇到 '\0' 就停了
printf("%s\n", buf);           // 输出: He

strcpy 只适用于 C 字符串(以 '\0' 结尾的字符序列)。对于包含 '\0' 的二进制数据,需要 memcpy

7.2 memcpy:按字节数拷贝

memcpy_basics.c
c
#include <string.h>

void *memcpy(void *dest, const void *src, size_t n);
//          ↑ 通用指针        ↑                ↑ 拷贝 n 个字节
//          不限于 char*      const void*

// 使用示例:
char src[] = {'A', 'B', '\0', 'C', 'D'};
char dst[16];

memcpy(dst, src, 5);           // 拷贝 5 个字节,不管有没有 '\0'
// dst 现在包含: A, B, 0, C, D

// 注意: memcpy 的参数是 void*,可拷贝任何类型
int a[5] = {1, 2, 3, 4, 5};
int b[5];
memcpy(b, a, 5 * sizeof(int)); // 拷贝 5 个 int
strcpy vs memcpy:

strcpy(dst, "hello"):
  ┌───┬───┬───┬───┬───┬───┐
 h e l l o\0 遇到 '\0' 停止
  └───┴───┴───┴───┴───┴───┘
  拷贝 6 个字节(自动)

memcpy(dst, src, 3):
  ┌───┬───┬───┐
 A B C 不管内容是什么,拷贝 3 个字节
  └───┴───┴───┘
  拷贝 3 个字节(手动指定)

7.3 自己实现 memcpy

my_memcpy.c
c
#include <stddef.h>

void *my_memcpy(void *dest, const void *src, size_t n)
{
    char *d = (char *)dest;       // 转换为字节指针
    const char *s = (const char *)src;

    // 逐字节拷贝(和 strcpy 类似的指针惯用法)
    while (n--)                   // n 次迭代
        *d++ = *s++;

    return dest;
}

// 使用:
char buf[32];
my_memcpy(buf, "hello", 6);       // 拷贝 6 个字节(包括 '\0')
printf("%s\n", buf);              // hello
特性strcpymemcpy
终止条件遇到 '\0'拷贝 n 个字节
参数char *dst, const char *srcvoid *dst, const void *src, size_t n
适用场景C 字符串任意二进制数据
安全性源必须正确终止调用者负责 n 的正确性
返回类型char*void*

8. 安全字符串操作:防止缓冲区溢出

8.1 strcpy 的危险

strcpy_danger.c
c
char buf[8];
strcpy(buf, "this string is way too long!");
//          ↑
//    越界写入!覆盖了 buf 之后的内存
//    可能导致: 段错误、数据损坏、安全漏洞(缓冲区溢出攻击)

缓冲区溢出是 C 语言历史上最严重的安全问题之一。strcpy 不检查目标缓冲区大小,完全信任源字符串。

8.2 安全的替代方案

safe_alternatives.c
c
#include <stdio.h>
#include <string.h>

// 方案 1: strncpy —— 限制拷贝长度
char buf[8];
strncpy(buf, "hello world", sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';    // 手动保证终止
// buf = "hello w" (只拷贝了 7 个字符)

// 方案 2: snprintf —— 格式化写入,自动截断
char buf[8];
snprintf(buf, sizeof(buf), "%s", "hello world");
// buf = "hello w" (自动加 '\0',最多写 sizeof(buf) 字节)

// 方案 3: strlcpy (BSD/非标准,但很多平台可用)
// strlcpy(buf, "hello world", sizeof(buf));
// buf = "hello w" (自动保证 '\0' 终止)
safe_strcpy_comparison.c
c
#include <stdio.h>
#include <string.h>

int main(void)
{
    char buf[8];
    const char *long_str = "this is a very long string";

    // strncpy: 截断但不保证 '\0' 终止!
    strncpy(buf, long_str, sizeof(buf));
    printf("strncpy: [%s]\n", buf);
    // 危险: 如果源长度 >= sizeof(buf),buf 没有 '\0'!

    // 正确做法: 手动补 '\0'
    strncpy(buf, long_str, sizeof(buf) - 1);
    buf[sizeof(buf) - 1] = '\0';
    printf("strncpy safe: [%s]\n", buf);

    // snprintf: 自动保证 '\0' 终止(推荐)
    snprintf(buf, sizeof(buf), "%s", long_str);
    printf("snprintf: [%s]\n", buf);

    return 0;
}
函数安全性\0 终止保证返回值
strcpy❌ 不安全依赖源字符串dst
strncpy⚠️ 有限需手动保证dst
snprintf✅ 安全始终保证如果 buf 足够大,返回写入的字符数
strlcpy✅ 安全始终保证源字符串长度

IMPORTANT

永远不要在新代码中使用 strcpy——这是业界共识。如果必须操作 C 字符串,使用 snprintfstrncpy + 手动 '\0'。本课实现 mystrcpy 是为了理解指针惯用法和底层机制,不是鼓励你在生产代码中使用它。


9. 工业级 strcpy:glibc 的优化实现

9.1 为什么标准库的 strcpy 比我们的快很多

前面我们写的 mystrcpy 逐字节拷贝,简单直观。但现代 CPU 的内存总线是 32 位或 64 位的——每次读写 4 字节(WORD)或 8 字节的效率最高。逐字节拷贝意味着每个字符都要发起一次总线操作。

byte_vs_word.c
c
// 逐字节拷贝(1 字节/次)
char *d = dst;
const char *s = src;
while ((*d++ = *s++))
    ;                       // 拷贝 "hello world!\0" (13 字节) → 13 次循环

// 按 WORD 拷贝(4 字节/次)—— 伪代码
unsigned long *ld = (unsigned long *)dst;
const unsigned long *ls = (const unsigned long *)src;
while (1) {
    unsigned long word = *ls;    // 一次读 4 个字节
    // 检查 word 中是否有 '\0' 字节
    // 如果有,逐个处理剩余字符
    // 否则,一次写入 4 字节
    *ld = word;
    ld++;
    ls++;
}
// 理论上最多需要 ceil(13/4) = 4 次循环

9.2 内存对齐的概念

内存对齐示意图:

地址:  0     1     2     3     4     5     6     7
       ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐

       └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘

对齐的 4 字节读取:
  *(unsigned long*)0x0 → 读 0x0~0x3(一次总线操作,对齐)
  *(unsigned long*)0x4 → 读 0x4~0x7(一次总线操作,对齐)

未对齐的 4 字节读取:
  *(unsigned long*)0x1 → 读 0x1~0x4
    需要两次总线操作: 0x0~0x3 + 0x4~0x7,然后拼接
    性能损失!

NOTE

某些 CPU 架构(如 ARMv5 及更早版本)根本不支持未对齐的内存访问——会直接触发硬件异常。x86 架构支持未对齐访问,但性能会显著下降。

9.3 glibc strcpy 的核心思路

glibc_strcpy_sketch.c
c
// glibc strcpy 的简化思路(非真实代码,仅为概念示意)
char *strcpy_optimized(char *dst, const char *src)
{
    char *ret = dst;

    // 第 1 步: 逐字节拷贝直到 dst 对齐到 4 字节边界
    while (((unsigned long)dst & 3) != 0)   // dst 未对齐?
    {
        if ((*dst++ = *src++) == '\0')
            return ret;
    }

    // 第 2 步: 按 4 字节(WORD)批量拷贝
    unsigned long *ld = (unsigned long *)dst;
    const unsigned long *ls = (const unsigned long *)src;

    while (1)
    {
        unsigned long word = *ls;      // 一次读 4 字节
        // 检查 word 中是否包含 '\0' 字节
        // 魔法公式: (word - 0x01010101) & ~word & 0x80808080
        if (has_zero_byte(word))       // 包含 '\0'?
            break;
        *ld++ = word;                  // 一次写 4 字节
        ls++;
    }

    // 第 3 步: 逐字节拷贝剩余字符(含 '\0')
    dst = (char *)ld;
    src = (const char *)ls;
    while ((*dst++ = *src++))
        ;

    return ret;
}
glibc strcpy 的执行流程:

  输入: src = "Hello World\0"
  
 1 步: 逐字节对齐
    [H] [e] [l] [l]   4 字节,dst 已对齐

     
 2 步: WORD 批量拷贝
    [Hell] [o Wo] [rld\0]   每次 4 字节

     
 3 步: 逐字节收尾
    检测到 '\0' 停止

NOTE

glibc 中检测 WORD 是否包含零字节的魔法公式 (word - 0x01010101) & ~word & 0x80808080 是一个经典的位运算技巧——一次判断就能检查 4 个字节中是否有 0。这比逐个比较高效得多。

9.4 硬件加速:SSE 与 NEON

现代 CPU 提供了 SIMD(单指令多数据)指令集,可以一次处理 16 甚至 32 字节:

平台指令集一次可处理的字节数
x86/x86-64SSE216 字节
x86/x86-64AVX232 字节
ARMNEON16 字节
asm
; x86-64 SSE 版本的 memcpy(示意,非完整代码)
movdqa  xmm0, [rsi]      ; 从 src 一次加载 16 字节到 XMM 寄存器
movdqa  [rdi], xmm0      ; 一次写入 16 字节到 dst
add     rsi, 16           ; src += 16
add     rdi, 16           ; dst += 16

这就是为什么标准库的 memcpy/strcpy 比手写的逐字节版本快 10 倍以上——它们使用了硬件级并行。


10. 字符串操作与指针的全景回顾

10.1 本课在课程体系中的位置

课程中的指针/字符串知识链:

Lesson 08 (数字9的个数)
  └─ 函数定义、逐位提取

Lesson 09 (整型转字符串)
  └─ 字符数组、C字符串、'\0'终止符、双指针逆序

Lesson 10 (约瑟夫环)
  └─ 数组模拟链表、取模实现环形

Lesson 11 (两点距离)
  └─ struct 结构体、按值传递、指针传递性能对比

Lesson 16 (本课: 字符串拷贝)  ← 你现在在这里
  └─ 字符指针、const、指针惯用法、工业级优化

Lesson 17 (统计单词个数)
  └─ 状态机遍历字符串

Lesson 18 (my_printf)
  └─ 格式化输出、变参、字符串解析

10.2 指针惯用法速查表

惯用法含义应用场景
*p++先取 *p,再 p++遍历数组/字符串
*++pp++,再取 *p跳过第一个元素
while (*p++)遍历到 '\0'0字符串/哨兵遍历
while ((*d++ = *s++))拷贝直到 '\0'strcpy 实现
while (n--) *d++ = *s++拷贝 n 个字节memcpy 实现
char *ret = dst; ... return ret;保存并返回起始地址返回目标指针的函数
p[i] 等价于 *(p+i)指针下标数组/字符串随机访问

10.3 strcpy 实现的变化图谱

mystrcpy 的实现层次:

┌─────────────────────────────────────┐
 0 层: 理解 C 字符串与指针
   char buf[] = "hello";             
   char *p = buf; *p = 'H';          
├─────────────────────────────────────┤
 1 层: 朴素下标实现
   int i=0; while(src[i]) dst[i]=...
├─────────────────────────────────────┤
 2 层: 指针惯用法
   while((*dst++ = *src++));          
├─────────────────────────────────────┤
 3 层: 防御性编程
   assert(dst&&src); const char *src; 
├─────────────────────────────────────┤
 4 层: delta 优化
   delt = dst - src; s[delt] = *s++;  
├─────────────────────────────────────┤
 5 层: WORD 对齐批量拷贝
   4 字节一次 + 零字节检测
├─────────────────────────────────────┤
 6 层: 汇编/SIMD 硬件加速
   SSE/NEON 指令, prefetch, cacheline
└─────────────────────────────────────┘

参考解答

练习1: mystrcpy 基础实现
solution_mystrcpy.c
c
#include <stdio.h>
#include <assert.h>

char *mystrcpy(char *dest, const char *src)
{
    assert(dest != NULL && src != NULL);

    char *ret = dest;                    // 保存起始地址

    while ((*dest++ = *src++) != '\0')   // 经典指针惯用法
        ;                                // 空循环体

    return ret;                          // 返回起始地址
}

int main(void)
{
    char s1[256] = "";
    char s2[256];

    fgets(s2, sizeof(s2), stdin);
    // 去掉末尾换行符
    int i = 0;
    while (s2[i] && s2[i] != '\n') i++;
    s2[i] = '\0';

    mystrcpy(s1, s2);
    printf("%s\n", s1);

    return 0;
}

核心在于 while ((*dest++ = *src++) != '\0'); 这一行:每次循环解引用 src 获得一个字符,赋值给 *dest,然后两个指针同时后置自增,当复制的字符是 '\0' 时循环终止。ret = dest 必须在循环前保存——循环结束后 dest 已经越过终止符。

练习2: delta 优化版
solution_strcpy_delta.c
c
#include <stdio.h>

char *mystrcpy_delt(char *dst, const char *src)
{
    char *s = (char *)src;               // 去掉 const 用于计算偏移
    int delt = dst - src;                // 偏移量: dst 和 src 的地址差

    while (*s != '\0')                   // 遍历源字符串
    {
        s[delt] = *s;                    // 写入 dst 对应位置: s[delt] = *(s+delt)
        s++;                             // 只移动一个指针
    }
    s[delt] = '\0';                      // 拷贝终止符

    return dst;
}

// buf 保证 src 和 dst 在同一数组内,delt 运算合法
char buf[512];

int main(void)
{
    char *src = buf;
    char *dst = buf + 256;

    fgets(src, 256, stdin);
    int i = 0;
    while (src[i] && src[i] != '\n') i++;
    src[i] = '\0';

    mystrcpy_delt(dst, src);
    printf("%s\n", dst);

    return 0;
}

delta 优化的核心思想:dstsrc 的偏移量是固定的 delt,因此 dst 当前位置始终等于 src + delt。用 s[delt](等价于 *(s + delt))访问 dst 对应位置,只需移动 s 一个指针。注意指针相减 dst - src 仅在两者指向同一数组内的元素时才是合法的。

对照检查:保存了 ret 起始地址吗?循环条件用的是赋值表达式 (*dst++ = *src++) 吗?循环在 '\0' 之后正确终止了吗?delta 版本中 delt 的计算和 s[delt] 的使用正确吗?


课堂讨论

  1. while ((*dst++ = *src++) != '\0'); 中的 != '\0' 可以省略吗?为什么?
  2. 如果 dstsrc 指向的内存区域重叠(例如 dst = src + 1),mystrcpy 的行为会怎样?
  3. delta 版本中 char *s = (char *)src 为什么要强制转换去掉 const?有什么更好的设计?
  4. assert 在发布版本(-DNDEBUG)中被移除后,如果调用者传了 NULL,会发生什么?如何设计一个不依赖 assert 的错误处理?
  5. char s[32] = "Hello World"char *s = "Hello World" 在内存布局上有什么区别?哪个可以传给 strcpy 作为目标?哪个作为源?

讨论答案

Q1: != '\0' 可以省略吗?

可以省略,因为 '\0' 的值为 0。

omit_null_check.c
c
// 这两种写法等价:
while ((*dst++ = *src++) != '\0')
    ;
while ((*dst++ = *src++))
    ;

// 原理: C 语言中,while 的条件是「不等于 0」
// '\0' 的 ASCII 值为 0,所以 (*dst++ = *src++) != '\0'
// 等价于 (*dst++ = *src++) != 0
// 等价于 (*dst++ = *src++)

但这不等于说省略就是更好的写法。!= '\0' 明确表达了意图——「拷贝直到终止符」——这是一种自文档化的编程风格。K&R 原书中的写法是省略 != '\0' 的,因为当时的 C 程序员对这种惯用法足够熟悉。现代代码中保留 != '\0' 更友好。

Q2: 内存区域重叠时 mystrcpy 的行为?

mystrcpy(以及标准库 strcpy)在内存重叠时行为是未定义的,可能导致数据损坏。

overlap_problem.c
c
char buf[] = "abcdef";
char *src = buf;
char *dst = buf + 1;        // dst 在 src 之后 1 个字节

mystrcpy(dst, src);
// 期望: buf = "aabcdef"
// 实际: 可能产生 "aaaaaaa"(无限循环或数据损坏)
重叠拷贝的失败过程:
  初始:  a  b  c  d  e  f \0
         src dst
  第1次: a  a  c  d  e  f \0   (dst 覆盖了 b,但 src 已经读过 b)
            src dst
  第2次: a  a  a  d  e  f \0   (src 读到了刚写入的 'a'!)
               src dst
  第3次: a  a  a  a  e  f \0
  ...
  结果: a  a  a  a  a  a \0     (全部变成 'a',丢失了原始数据)

解决方案:使用 memmove 而不是 strcpymemmove 能正确处理重叠区域——如果 dst < src,从前向后拷贝;如果 dst > src,从后向前拷贝。

memmove_solution.c
c
#include <string.h>
memmove(dst, src, strlen(src) + 1);   // 安全处理重叠
Q3: 为什么 delta 版要去掉 const?

因为 int delt = dst - src 涉及从 const char*char* 的隐式类型差异。

const_cast_issue.c
c
char *mystrcpy_delt(char *dst, const char *src)
{
    // int delt = dst - src;   // ⚠️ 警告: char* - const char*,类型不完全一致
    // 大多数编译器会接受但给出警告

    char *s = (char *)src;     // 显式转换,消除警告
    int delt = dst - s;        // 现在类型一致了
    // ...
}

更好的设计:使用 ptrdiff_t 类型和中间变量,避免修改 const 限定:

better_delta_design.c
c
char *mystrcpy_delt(char *dst, const char *src)
{
    const char *s = src;            // 保持 const
    ptrdiff_t delt = dst - src;     // 编译器允许 char* - const char* (C 标准未明确禁止)

    while (*s != '\0') {
        // 用 (char *) 转换结果,不影响 s 的 const 属性
        *((char *)s + delt) = *s;   // 等价于 *(dst 对应位置) = *s
        s++;
    }
    *((char *)s + delt) = '\0';

    return dst;
}
Q4: assert 被移除后传 NULL 会怎样?

如果 assert 被编译移除(-DNDEBUG),传入 NULL 会导致段错误或未定义行为。

no_assert_danger.c
c
// 发布版本中:
// assert(dst != NULL);  → 什么都不做!
// 然后:
// *dst = *src;          → 解引用 NULL → 段错误!

更好的设计:用运行时检查替代 assert:

runtime_null_check.c
c
char *mystrcpy_safe(char *dst, const char *src)
{
    if (dst == NULL || src == NULL)
        return NULL;                 // 返回 NULL 表示失败

    char *ret = dst;
    while ((*dst++ = *src++))
        ;
    return ret;
}

// 调用者可以检查返回值:
if (mystrcpy_safe(dst, src) == NULL)
    fprintf(stderr, "Error: NULL pointer\n");

选择指南

  • assert:用于「不可能发生」的编程错误,调试期间快速暴露 bug
  • if + 返回值/错误码:用于「可能发生」的运行时异常,发布版本也需要处理
Q5: char s[32] vs char *s 的区别?
array_vs_pointer_detail.c
c
// 方式 1: 字符数组
char s1[32] = "Hello World";
// - 在栈上分配 32 字节
// - "Hello World" 从只读段复制到栈上
// - s1 是数组名(不可修改的左值),但内容可修改
// - sizeof(s1) = 32
// - 可作为 strcpy 的目标 ✅

// 方式 2: 字符指针
char *s2 = "Hello World";
// - 在栈上分配一个指针(8 字节)
// - 指针指向只读数据段的字符串字面量
// - s2 可以修改(指向其他字符串),但 *s2 不可修改
// - sizeof(s2) = 8(指针大小)
// - 不可作为 strcpy 的目标 ❌(写入只读内存会段错误)
strcpy_target_test.c
c
#include <stdio.h>
#include <string.h>

int main(void)
{
    // 正确的用法:
    char buf[32];
    strcpy(buf, "Hello");               // ✅ buf 是数组,可写
    printf("%s\n", buf);

    // 错误的用法:
    // char *p = "Hello";
    // strcpy(p, "World");              // ❌ p 指向只读内存

    // 正确: 让指针指向可写内存
    char *q = buf;                       // q 指向 buf(可写)
    strcpy(q, "World");                  // ✅
    printf("%s\n", buf);

    return 0;
}
特性char s[32] = "Hello"char *s = "Hello"
存储栈(32 字节)指针在栈(8 字节),字符串在 .rodata
可写性可写 ✅只读 ❌(修改是 UB)
可作为 strcpy 目标❌(除非指向堆/栈内存)
可作为 strcpy 源
sizeof328
&s 类型char (*)[32]char **

课后练习

  1. 实现 strlen:模仿 mystrcpy 的指针惯用法,实现 size_t mystrlen(const char *s),返回字符串的长度(不包括 '\0')。要求使用指针而非下标,只用一行循环体。

    知识点提示while (*p++) 遍历字符串、p - start 计算长度、const char* 表示只读源字符串

  2. 双指针奇偶分离:给定一个整数数组,用双指针(一头一尾向中间靠拢)将奇数放在左边、偶数放在右边,不额外分配空间。打印调整后的数组。

    知识点提示:双指针从两端向中间移动、left < right 循环条件、*left*right 的解引用操作、% 2 判断奇偶

  3. 实现 memcpy:实现 void *my_memcpy(void *dest, const void *src, size_t n),拷贝 n 个字节。思考:为什么参数用 void* 而不是 char*?如何处理 n = 0 的情况?

    知识点提示void* 通用指针(可接受任何类型)、强制转换为 char* 逐字节操作、while (n--) 计数循环、size_t 无符号整型

  4. 字符串安全拷贝函数:实现 size_t safe_strcpy(char *dst, size_t dst_size, const char *src),最多拷贝 dst_size - 1 个字符并始终以 '\0' 终止。返回实际需要的长度(源字符串长度)。

    知识点提示:缓冲区大小检查、'\0' 终止保证、返回值模仿 snprintf 设计、size_t 类型

练习1: mystrlen 实现
ex1_mystrlen.c
c
#include <stdio.h>
#include <stddef.h>

size_t mystrlen(const char *s)
{
    const char *start = s;          // 保存起始地址
    while (*s)                      // 遍历到 '\0'
        s++;
    return s - start;               // 指针相减 = 字符数
}

// 更简洁的一行版本:
size_t mystrlen_v2(const char *s)
{
    const char *p = s;
    while (*p++);                   // 空循环,p 最终指向 '\0' 之后
    return (size_t)(p - s - 1);     // p - s - 1 = 字符数
}

int main(void)
{
    printf("len(\"hello\") = %zu\n", mystrlen("hello"));   // 5
    printf("len(\"\")     = %zu\n", mystrlen(""));         // 0
    return 0;
}

核心技巧与 mystrcpy 一脉相承:用指针遍历直到 '\0',用 指针相减 计算长度。注意 p - s 返回的是 ptrdiff_t 类型(有符号),显式转为 size_t(无符号)。

练习2: 双指针奇偶分离
ex2_odd_even_partition.c
c
#include <stdio.h>

void partition(int arr[], int n)
{
    int *left = arr;                // 左指针
    int *right = arr + n - 1;       // 右指针

    while (left < right)
    {
        // 左边找到第一个偶数
        while (left < right && *left % 2 == 1)
            left++;

        // 右边找到第一个奇数
        while (left < right && *right % 2 == 0)
            right--;

        // 交换
        if (left < right)
        {
            int tmp = *left;
            *left = *right;
            *right = tmp;
            left++;
            right--;
        }
    }
}

int main(void)
{
    int arr[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int n = sizeof(arr) / sizeof(arr[0]);

    partition(arr, n);

    for (int i = 0; i < n; i++)
        printf("%d ", arr[i]);
    printf("\n");
    // 输出: 1 9 3 7 5 6 4 8 2 10(奇左偶右,顺序不要求完全一致)

    return 0;
}

双指针(Two Pointers)是数组/字符串算法中最常见的模式之一——left 从头向右、right 从尾向左,两者相遇时结束。本练习展示了指针不仅仅用于字符串,在数组处理中同样强大。

练习3: my_memcpy 实现
ex3_my_memcpy.c
c
#include <stdio.h>
#include <stddef.h>

void *my_memcpy(void *dest, const void *src, size_t n)
{
    char *d = (char *)dest;            // 转为字节指针
    const char *s = (const char *)src;

    // 处理 n = 0 的情况: 循环体不执行,直接返回
    while (n--)
        *d++ = *s++;

    return dest;
}

int main(void)
{
    // 测试 1: 拷贝字符串
    char str1[32];
    my_memcpy(str1, "hello", 6);       // 6 个字节(包括 '\0')
    printf("%s\n", str1);              // hello

    // 测试 2: 拷贝整数数组
    int a[] = {10, 20, 30, 40, 50};
    int b[5];
    my_memcpy(b, a, 5 * sizeof(int));
    for (int i = 0; i < 5; i++)
        printf("%d ", b[i]);           // 10 20 30 40 50
    printf("\n");

    // 测试 3: n = 0(边界情况)
    char empty[4] = "abc";
    my_memcpy(empty, "xyz", 0);
    printf("%s\n", empty);             // abc(未修改)

    return 0;
}

memcpyvoid* 作为参数类型是为了通用性——同一个函数可以拷贝任何类型的数据。size_t n 是字节数,调用者负责传入正确的值。n = 0while (n--) 直接跳过,函数安全返回。

练习4: safe_strcpy 实现
ex4_safe_strcpy.c
c
#include <stdio.h>
#include <stddef.h>

size_t safe_strcpy(char *dst, size_t dst_size, const char *src)
{
    if (dst == NULL || src == NULL || dst_size == 0)
        return 0;

    size_t i;
    // 拷贝字符,但最多 dst_size - 1 个(为 '\0' 留空间)
    for (i = 0; i < dst_size - 1 && src[i] != '\0'; i++)
        dst[i] = src[i];

    dst[i] = '\0';                      // 始终以 '\0' 终止

    // 计算源字符串的实际长度
    size_t src_len = 0;
    while (src[src_len] != '\0')
        src_len++;

    return src_len;                     // 返回源长度(非实际拷贝长度)
}

int main(void)
{
    char buf[8];

    // 测试 1: 正常情况
    size_t len = safe_strcpy(buf, sizeof(buf), "hello");
    printf("buf=[%s], src_len=%zu\n", buf, len);
    // buf=[hello], src_len=5

    // 测试 2: 截断情况
    len = safe_strcpy(buf, sizeof(buf), "hello world this is long");
    printf("buf=[%s], src_len=%zu\n", buf, len);
    // buf=[hello w], src_len=27

    // 测试 3: 刚好填满
    len = safe_strcpy(buf, sizeof(buf), "1234567");
    printf("buf=[%s], src_len=%zu\n", buf, len);
    // buf=[1234567], src_len=7

    return 0;
}

这个函数模仿了 snprintf 的 API 设计:接受缓冲区大小、保证 '\0' 终止、返回源长度(调用者可以比较返回值与 dst_size 判断是否发生了截断)。这是生产代码中字符串操作的推荐模式。


参考资料

"The most effective debugging tool is still careful thought, coupled with judiciously placed print statements." — Brian Kernighan

Released under the MIT License.