跳转到内容

Lesson 26: 实现 my_memcpy — 标准内存拷贝

练习任务

难度:易-中

实现自己的 memcpy 函数——my_memcpy(dest, src, n),将 n 字节从 src 逐字节拷贝到 dest,返回 dest。函数原型与标准库一致:

c
void *my_memcpy(void *dest, const void *src, size_t n);
  • dest:目标缓冲区(可写)
  • src:源缓冲区(只读,const 修饰)
  • n:要拷贝的字节数(size_t 类型)
  • 返回值:dest(目标缓冲区的起始地址)

本课有 3 组测试用例——程序从标准输入读取一行字符串,去掉末尾换行符后将其拷贝并打印:

输入 "hello\n" 输出 "hello"
输入 "abc\n" 输出 "abc"
输入 "\n" 输出 ""     (空行,拷贝 '\0')

提示void* 是 C 语言的"万能指针",可以接收任何类型的指针——但代价是不能直接做算术运算。实现 memcpy 的关键一步是:把 void* 强转为 char*(或 unsigned char*),然后逐字节循环拷贝。为什么偏偏是 char*,不是 int*short*?想一想每个类型的一个 ptr++ 分别跳过几个字节。


核心知识点

  • void* 万能指针 — 可以接收任意类型指针,但不能做算术运算(+/++/-)也不能解引用(*
  • void*char* 转换 — 逐字节操作内存的唯一路径,char 恰好 1 字节是 C 语言的最小可寻址单位
  • const 限定符在指针转换中的保留 — const void *src 转为 const char *s,不能丢掉 const(编译器警告 + 承诺破坏)
  • size_t 类型 — 无符号整型,定义在 <stddef.h><string.h> 隐式包含),能保证容纳任何对象的大小
  • 逐字节循环 — for (i=0; i<n; i++) d[i]=s[i]while (n--) *d++ = *s++
  • 返回值保留 dest — 返回转换前的原始指针,而非 ds(语义一致,与标准库对齐)
  • unsigned char* 的严谨性 — 避免 char 有符号时的符号扩展问题
  • 浅拷贝 vs 深拷贝 — memcpy 对含指针成员的结构体只拷贝指针值(浅拷贝),两个对象指向同一内存
  • memcpy vs strcpy vs 逐元素赋值 — 粒度、类型通用性、终止条件的三维对比

代码框架

26_my_memcpy.c
c
#include <stdio.h>
#include <string.h>

/*
 * 实现 my_memcpy — 将 void* 强转为 char*,逐字节拷贝
 * 返回 dest(写入后的目标空间起始地址)
 */
void *my_memcpy(void *dest, const void *src, size_t n)
{
    // 将 void* dest 强转为 char* d(目标,可读写)
    // char *d = (char *)dest;
    // 在这里转换 dest

    // 将 const void* src 强转为 const char* s(源,只读)
    // const char *s = (const char *)src;
    // 注意:src 是 const void*,转换后必须保留 const!
    // 在这里转换 src

    // 逐字节拷贝:循环 n 次,每次拷贝一个字节
    // 方法 1:下标版本  for (size_t i = 0; i < n; i++) d[i] = s[i];
    // 方法 2:指针版本  while (n--) *d++ = *s++;
    // 在这里实现循环

    // 返回 dest(原始指针,不是 d)
    // return dest;
}

int main(void)
{
    char src[256], dest[256] = {0};

    // 用 fgets 读取一行输入到 src
    // 在这里读取输入
    // fgets(src, sizeof(src), stdin);

    // 去掉 src 末尾的换行符 '\n'
    // 在这里去掉换行符(遍历到 '\n' 替换为 '\0')

    // 计算需要拷贝的字节数 = strlen(src) + 1
    // +1 是把 '\0' 也拷贝过去,保证 dest 是一个完整字符串
    // size_t len = strlen(src) + 1;
    // 在这里计算长度并调用 my_memcpy
    // my_memcpy(dest, src, len);

    // 打印 dest 验证结果
    // printf("%s\n", dest);
    // 在这里打印

    return 0;
}

框架中的核心思考:

  • 为什么 void* 必须先转换为 char*?直接 *src 会发生什么?
  • 为什么目标指针转换不加 const,源指针转换保留了 const
  • size_tint 在循环中有什么区别?

TIP

先不要往下翻看参考解答。把 void*char* 的转换写好,然后逐字节循环——这是内存操作的根基。本课看似简单,但它是后续 Lesson 27(memmove 重叠拷贝)、Lesson 45(realloc 动态扩容)的基础。


深度讲解

1. void* —— C 语言的"万能指针"

1.1 万能的一面:接收任意指针

void_ptr_assign.c
c
int    x = 42;
double y = 3.14;
char   z = 'A';

void *p;
p = &x;    // ✅ int*    → void*  无需强制转换
p = &y;    // ✅ double* → void*  无需强制转换
p = &z;    // ✅ char*   → void*  无需强制转换

void* 也可以无声地转换回原始类型(在 C 中,不在 C++ 中):

void_ptr_cast_back.c
c
void *p = &x;
int *ip = p;            // ✅ void* → int*   在 C 中隐式转换合法
// C++ 中必须写 int *ip = (int *)p;

// 但这是 C,所以 memcpy 可以直接 return dest 而不用强制类型转换

1.2 致命的限制:不能做算术,不能解引用

void_ptr_limitations.c
c
void *p = &x;
p++;       // ❌ 编译错误!void 没有大小,编译器不知道 +1 跳几个字节
p + 1;     // ❌ 编译错误!同上
*p;        // ❌ 编译错误!不能解引用(不知道读几个字节、当成什么类型来读)

void* 存在的原因就是为了"通用"——库函数不知道调用者会传什么类型,所以用 void* 作为入口,在函数内部再转为已知大小的具体类型。

┌────────────────────────────────────────────────┐
  void* 的契约:

  入口:接收任意类型的指针(隐式转换)
  出口:返回任意类型的指针(隐式转换)
  内部:必须转为具体类型(如 char*)才能操作

  类比:void* 是一个"未贴标签的盒子"
        不知道里面装什么,但知道它在哪
        要开盒子,必须知道里面是什么类型
└────────────────────────────────────────────────┘

2. 为什么偏偏转 char*

2.1 char = 1 字节——最小可寻址单位

C 标准保证 sizeof(char) == 1,这是 C 语言中唯一大小固定为 1 字节的类型memcpy 的定义就是"逐字节拷贝"——所以 char* 是唯一正确的选择。

C 类型大小对照(64 位系统典型值):
┌───────────┬──────────┬────────────────────────────┐
   类型   大小  ptr+1 ptr++ 跳过几个字节
├───────────┼──────────┼────────────────────────────┤
   char  1 字节  1 memcpy 的语义基础
   short  2 字节  2
   int  4 字节  4
   long  8 字节  8
   double  8 字节  8
   void  N/A  编译错误
└───────────┴──────────┴────────────────────────────┘

2.2 如果用 int* 会怎样?

wrong_int_ptr.c
c
// ❌ 用 int* 做 memcpy — 隐患极大
void *bad_memcpy(void *dest, const void *src, size_t n)
{
    int *d = (int *)dest;
    const int *s = (const int *)src;

    // 问题 1: n 是按"字节"计数的d++ 跳 4 字节 — 单位不匹配
    for (size_t i = 0; i < n; i++)   // ← i 仍是字节计数!
        d[i] = s[i];                 // ← d[i] 访问第 i 个 int,即第 i*4 个字节!
    // 拷贝了 4n 个字节!越界!

    // 问题 2: n 不是 4 的倍数时,最后一轮越界
    // memcpy(buf, "hello", 6) → 6 个字节,用 int* 拷贝 2 个 int = 8 字节

    // 问题 3: 源/目标地址可能未按 int 边界对齐
    // 某些 CPU 对此会触发 bus error
}
 char* 拷贝 6 字节: int* 拷贝 6 字节(错误):
                                          
 字节:  0  1  2  3  4  5                  字节:  0  1  2  3 | 4  5
 内容: [H][e][l][l][o][\0\x]               内容: [H][e][l][l]|[o][\0] → d[0]=4B
       └────逐字节────┘                                └───int──┘
       6 次操作,100% 正确                          d[1] 越界!只写了 2B 但读了 4B

2.3 char* vs unsigned char* — 哪个更严谨?

unsigned_char_rigor.c
c
// 标准写法:char*(大多数教材)
char *d = (char *)dest;
const char *s = (const char *)src;

// 更严谨的写法:unsigned char*(C 标准实际上用这个)
unsigned char *d = (unsigned char *)dest;
const unsigned char *s = (const unsigned char *)src;

C 标准规定 memcpy 的行为"等价于"将 srcdest 视为 unsigned char 数组来拷贝。使用 unsigned char* 的好处:

  • 避免有符号 char 在比较或赋值时的符号扩展(在 char 为有符号的系统上,0x80int 扩展为 0xFFFFFF80
  • 与标准库的内部实现更一致

本练习中 char*unsigned char* 等价——编译器优化后无差异。工业级代码倾向 unsigned char*


3. const 限定符的跨转换保留

3.1 不能丢掉 const

preserve_const.c
c
// ✅ 正确:保留 const
const char *s = (const char *)src;
// src 声明为 const void*,转换后 src 的数据仍受 const 保护

// ❌ 错误:丢掉 const
char *s = (char *)src;              // 编译器警告:discards 'const' qualifier
// 这不仅产生警告——更严重的是,现在可以通过 s 修改调用者的只读数据
why_const_matters.c
c
const char result[] = "result";    // 调用者声明为只读

// 如果你的 my_memcpy 内部丢掉了 const:
char *s = (char *)src;             // ← 丢掉 const
s[0] = 'X';                        // ← 修改了调用者的只读数据!UB!

// 保留 const 则编译器会在 s[0]='X' 处报错,防止此类 bug

const 不仅是编译器的检查工具——它是 API 设计中的"契约":const void *src 告诉所有读代码的人,"我不会动你的数据"。在转换中丢失 const 等于破坏了承诺。

3.2 目标指针不加 const

dest_non_const.c
c
char *d = (char *)dest;             // dest 可写——不加 const
const char *s = (const char *)src;  // src 只读——必须 const

目标缓冲区 dest 是输出——函数需要向它写入数据——所以转换时不加 const。这是 C 标准库签名的一致性设计:输入参数加 const,输出参数不加。


4. size_t —— 为什么用它而不是 int

4.1 size_t 的语义和范围

size_t_intro.c
c
#include <stddef.h>   // size_t 定义于此
#include <string.h>   // 但也包含了 <stddef.h>

// size_t 的特征:
// - 无符号整型(unsigned integer type)
// - 保证能表示任何对象的最大大小(取决于平台字长)
// - 64 位系统: 8 字节(最大值 ~18 EB)
// - int 典型: 4 字节(最大值 ~2 GB)
size_t_vs_int.c
c
// ❌ 用 int 的问题
void *bad_memcpy_int(void *dest, const void *src, int n)
{
    // 问题 1: n 为负怎么办?memcpy(d, s, -1) 会拷贝什么?
    // 问题 2: 最大只能传 ~2GB,拷贝 3GB 文件时 n 溢出为负数
    // 问题 3: 有符号/无符号比较警告: for(int i=0; i<n; i++) 当 n 为 size_t 时
}

// ✅ 用 size_t
void *memcpy(void *dest, const void *src, size_t n);
// 无符号 → n 永远 ≥ 0(语义自明)
// 64 位 → 可处理任意现代系统上能分配的内存

4.2 循环中的有符号/无符号

signed_unsigned_loop.c
c
// ✅ 好:循环变量用 size_t 匹配参数类型
for (size_t i = 0; i < n; i++)
    d[i] = s[i];

// ⚠️ 可行但会有警告:int i 和 size_t n 类型不匹配
for (int i = 0; i < (int)n; i++)   // -Wsign-compare
    d[i] = s[i];

// ✅ 指针版本——完全避开了索引类型问题
while (n--)
    *d++ = *s++;

指针版本 while (n--) 是最简洁的写法——它既规避了类型匹配问题,又是代码行数最少的形式。


5. 逐字节拷贝——memcpy 的语义本质

5.1 三种等价写法

three_ways.c
c
// 方式 1: 下标版本(最直观)
char *d = (char *)dest;
const char *s = (const char *)src;
for (size_t i = 0; i < n; i++)
    d[i] = s[i];
return dest;

// 方式 2: 指针版本(最经典)
char *d = (char *)dest;
const char *s = (const char *)src;
while (n--)
    *d++ = *s++;
return dest;

// 方式 3: 先保存 d 再返回(等价于方式 2,但更明确)
char *d = (char *)dest;
const char *s = (const char *)src;
char *ret = d;             // 保存 dest 副本用于返回
while (n--)
    *d++ = *s++;
return ret;                // 或 return dest; 都可以

5.2 为什么返回 dest 而不是 ds

return_dest.c
c
// dest 是调用者传进来的原始指针——返回它就是返回"目标缓冲区的起点"
// d 和 s 在循环中不断前进,循环结束后已越过末尾
// 返回 dest 语义清晰、行为一致

char *d = (char *)dest;
// ... 循环中 d++ 不断前进 ...
// 循环结束:d 指向 dest + n(末尾之后)
return (void *)dest;       // ✅ 返回起点
// return (void *)d;       // 功能上没错(d 同样指向 dest 的原始地址,如果没移动的话)
//                          // 但语义上不清晰——d 是"工作指针",dest 是"锚点"

实际上,如果没在循环中移动 d(像方式 1 用了下标而非指针增减),return destreturn d 值相同。但在指针版本中 d 已经被移动了,必须返回 dest 或提前保存的副本。


6. memcpy vs strcpy vs 逐元素赋值 —— 三维对比

维度memcpy逐元素 a[i]=b[i]strcpy
适用类型任意(通过 void*需知道具体元素类型只适用 C 字符串
操作粒度字节级元素级字节级,遇 '\0' 停止
长度指定n 字节(显式传入)循环次数(显式)自动(遇 '\0' 停机)
泛型能力✅ 一份实现处理所有类型❌ 每种类型独立实现❌ 只处理 char
终止符处理包含(若 n 包含 '\0'包含(若 n 包含 '\0'自动包含('\0' 是停车信号)
安全性调用者负责 n 不越界调用者保证循环不越界调用者保证 dest 足够大
二进制数据✅ 支持✅ 支持❌ 遇到 '\0' 提前终止
strcpy vs memcpy 的终止条件差异:

strcpy(dst, "hel\0lo"):
  ┌───┬───┬───┬───┐
 h e l\0 遇到 '\0' 停止,后面的 "lo" 未拷贝
  └───┴───┴───┴───┘
  拷贝 4 字节(自动停机)

memcpy(dst, "hel\0lo", 7):
  ┌───┬───┬───┬───┬───┬───┬───┐
 h e l\0 l o\0 不管有无 '\0',拷贝满 7 字节
  └───┴───┴───┴───┴───┴───┴───┘
  拷贝 7 字节(手动指定)

7. 浅拷贝 vs 深拷贝 —— 结构体的陷阱

7.1 简单结构体:memcpy = 逐字段赋值

pod_struct.c
c
// POD(Plain Old Data)结构体——无指针成员
struct Point { int x, y; };
struct Point a = {10, 20};
struct Point b;

memcpy(&b, &a, sizeof(struct Point));
// b.x = 10, b.y = 20 ✓
// 等价于 b = a; ✓

对于不含指针的简单结构体,memcpy 等价于逐字段赋值——每个字段是独立的值,拷贝后互不影响。

7.2 含指针的结构体:浅拷贝的危险

shallow_copy_danger.c
c
struct Person {
    char *name;     // 指向堆上的字符串
    int   age;
};

struct Person alice;
alice.name = malloc(16);
strcpy(alice.name, "Alice");
alice.age = 30;

struct Person bob;
memcpy(&bob, &alice, sizeof(struct Person));
// bob.name = alice.name(两个指针指向同一块堆内存!)
// bob.age  = 30 ✓

// 危险!
free(alice.name);          // alice.name 指向的内存已释放
printf("%s\n", bob.name);  // ❌ 悬垂指针!bob.name 仍指向已释放的内存
memcpy 后的内存状态(浅拷贝):

  栈(alice)                   栈(bob)
  ┌──────────┐      ┌──────────────┐      ┌──────────┐
 name ────────────→│ A l i c e \0 │←─────┼─── name
 age: 30      └──────────────┘ age: 30
  └──────────┘                            └──────────┘
 两个指针
 指向同一块内存!

  free(alice.name) 后,bob.name 变成悬垂指针 → 段错误或 UB

对于含指针成员的结构体,需要深拷贝——对指针指向的内容也进行拷贝:

deep_copy.c
c
struct Person deep_copy_person(const struct Person *src)
{
    struct Person dst;
    dst.name = malloc(strlen(src->name) + 1);  // 为新对象独立分配
    strcpy(dst.name, src->name);                // 拷贝名字内容
    dst.age = src->age;
    return dst;
}

CAUTION

memcpy 对含指针成员的结构体只做浅拷贝——拷贝指针值,不拷贝指针所指内容。两个对象共享同一块堆内存,一旦一方 free,另一方变成悬垂指针。JavaScript 的 Object.assign、Python 的 copy.copy 同理——深拷贝需用专用函数(copy.deepcopy 等)。


参考解答

练习: my_memcpy 完整实现
solution_26_my_memcpy.c
c
#include <stdio.h>
#include <string.h>

void *my_memcpy(void *dest, const void *src, size_t n)
{
    char *d = (char *)dest;
    const char *s = (const char *)src;

    while (n--)
        *d++ = *s++;

    return dest;
}

int main(void)
{
    char src[256], dest[256] = {0};

    fgets(src, sizeof(src), stdin);

    /* 去掉末尾的 '\n' */
    int i = 0;
    while (src[i] && src[i] != '\n')
        i++;
    src[i] = '\0';

    /* +1 把 '\0' 也拷贝过去,确保 dest 是完整 C 字符串 */
    size_t len = strlen(src) + 1;
    my_memcpy(dest, src, len);

    printf("%s\n", dest);

    return 0;
}

核心设计决策:

  1. while (n--):最简洁的逐字节循环。n 每次递减,直到 0 自动退出。无需额外循环变量。
  2. 返回 dest:调用者传入的原始目标指针,语义清晰。如果返回 d,它已被移动了 n 字节。
  3. len = strlen(src) + 1+1 确保 '\0' 也被拷贝。忘记 +1 是常见错误——dest 会是未终止的字符数组,printf("%s") 会读越界。

对照检查void* 转换成了 char* 吗?src 转换保留了 const 吗?循环条件用的是 n 递减吗?返回的是 dest(原始指针)吗?


课堂讨论

  1. 为什么 memcpy 的参数顺序是 (dest, src, n) 而不是 (src, dest, n)?这种顺序有什么工程哲学支撑?
  2. 如果 n = 0my_memcpy 应该做什么?为什么这样是安全的?
  3. size_tint 有什么区别?假设在一个超大文件拷贝场景(3.5 GB),用 int 做参数会发生什么?
  4. memcpy 能用来拷贝结构体吗?什么时候安全,什么时候危险?为什么?
  5. 为什么 C 标准要同时提供 memcpymemmove 两个函数?它们的功能有重叠吗?

讨论答案

Q1: 参数顺序 (dest, src, n) 的工程哲学

类比赋值语句 dest = src,目的在左,源在右。

这是 UNIX 系统编程的经典约定——几乎所有标准库的拷贝系函数都遵循"目的在先"的顺序:

  • strcpy(dest, src) → 目的在前
  • memcpy(dest, src, n) → 目的在前
  • strcat(dest, src) → 目的在前
  • shell 命令:cp src dest → 源在前!与 C 库相反(一个著名的"不一致")

C 语言的这个选择可能是从赋值语句的逻辑自然导出——a = ba(目的)写在左边。当函数需要表达类似语义时,dest 放在第一个参数位置。

Q2: n = 0 时的行为

直接返回 dest,不拷贝任何字节。

循环条件 while (n--) 在第一轮即因 n=0 而退出——不进入循环体。这是安全的,因为:

  • 没有读取 src(即使 src 为 NULL 也不会触发错误——只要进入循环体之前 n 已经是 0)
  • 没有写入 dest
  • 返回值 dest 始终有效

这也意味着 memcpy(dest, NULL, 0) 是定义良好的——虽然传 NULL 到第 2 个参数看起来可怕,但因为 n=0 没有实际访问,行为安全。当然,不建议特意这样写。

Q3: size_t vs int 在超大场景的差异

size_t(64 位系统 8 字节)最大约 18 EB,int(4 字节)最大约 2 GB。

拷贝一个 3.5 GB 文件的场景:

c
// ❌ 用 int
int n = 3.5 * 1024 * 1024 * 1024;   // ≈ 3,758,096,384
                                     // 但 int 最大值约 2,147,483,647
                                     // → 溢出!n 变成负数!
memcpy(dest, src, n);                // 参数为负 → UB

// ✅ 用 size_t
size_t n = (size_t)3.5 * 1024 * 1024 * 1024; // 正确存储为无符号大整数
memcpy(dest, src, n);                         // 正常工作

额外的细微差异:

  • size_t 是平台相关类型——32 位系统上它是 4 字节,64 位系统上是 8 字节。用 size_t 写出的代码跨平台更安全。
  • int 有符号,n 为负的语义是不明确的——库里需要防御性地检查。
Q4: memcpy 拷贝结构体的安全边界

对简单(POD)结构体安全,对含指针或资源句柄的结构体危险。

安全:

c
struct Point { int x, y; };          // 纯值类型,无指针
struct Point a = {1, 2}, b;
memcpy(&b, &a, sizeof(a));           // ✅ 等价于 b = a

危险:

c
struct Buffer {
    char *data;                       // 指针——需深拷贝
    size_t size;
};
struct Buffer a = {malloc(100), 100};
struct Buffer b;
memcpy(&b, &a, sizeof(a));           // ❌ b.data 和 a.data 指向同一堆内存

判断标准:如果结构体没有指针成员、没有 FILE* 等资源句柄、没有 union 中包含指针的类型——memcpy 是安全的。其他情况需要自定义深拷贝函数。

Q5: 为什么同时需要 memcpy 和 memmove?

分工不同:memcpy 假设不重叠、追求最快;memmove 保证重叠正确处理。

memcpy:  "我保证 src 和 dest 不重叠 → 你(编译器/硬件)可以无脑正向拷贝、用块指令优化"
memmove: "可能重叠 → 你必须判断方向然后正确处理"

对比:
         memcpy(restrict)              memmove( restrict)
重叠处理   不保证(UB)                   保证正确处理
方向判断                           有(d < s 正向 / d > s 反向)
性能       略快(无条件分支)             多一次比较 + 可能反向拷贝
使用场景   拷贝到全新缓冲区                数组内移动元素、不确定是否重叠

这是 C 语言设计的哲学——给予程序员选择权:你知道不重叠,就可以用更快的 memcpy;你不确定,就用安全但稍慢的 memmove。C99 为 memcpy 添加了 restrict 关键字正式声明"不重叠"的契约。

下一课(Lesson 27)你将亲手实现 memmove,体验方向判断和反向拷贝的精妙。


课后练习

  1. 用指针版本替换下标版本。将参考解答中的 while (n--) *d++ = *s++ 展开为等价的下标版本,并比较两种写法在"编译产出的汇编"上可能有什么差异。

    知识点提示:现代编译器(gcc -O2)通常会将两种形式优化为相同的汇编代码(rep movsbmemcpy 内建),所以性能上无差异。差异主要在代码可读性——指针版本更紧凑但需对 *d++ 的优先级有直觉。

    参考解答
    subscript_version.c
    c
    void *my_memcpy_subscript(void *dest, const void *src, size_t n)
    {
        char *d = (char *)dest;
        const char *s = (const char *)src;
    
        for (size_t i = 0; i < n; i++)
            d[i] = s[i];                    /* 等价于 *(d+i) = *(s+i) */
    
        return dest;
    }

    编译器优化后两者几乎相同——*d++ = *s++d[i] = s[i] 在 -O2 下通常生成相同指令。差异在于:

    • 指针版本对老编译器或 -O0 可能更快(按地址读写,无需 i×sizeof(char) 的加法)
    • 下标版本更易读,尤其代码评审中不需要判读 *d++ 的运算符优先级
  2. 实现 my_memset。仿照 my_memcpy 的实现,编写 void *my_memset(void *s, int c, size_t n)——将 s 指向的前 n 个字节全部设置为值 c。注意 cint 但写入时按 unsigned char 截断。

    知识点提示:标准 memsetunsigned char 方式写入 c 的低 8 位。如果 c = 0x12345678,实际写入 0x78。循环用 while (n--) *p++ = (unsigned char)c;

    参考解答
    my_memset.c
    c
    #include <stdio.h>
    
    void *my_memset(void *s, int c, size_t n)
    {
        unsigned char *p = (unsigned char *)s;
        unsigned char byte = (unsigned char)c;    /* 仅低 8 位 */
    
        while (n--)
            *p++ = byte;
    
        return s;
    }
    
    int main(void)
    {
        char buf[32];
        my_memset(buf, 'X', 10);
        buf[10] = '\0';
        printf("%s\n", buf);   /* XXXXXXXXXX */
        return 0;
    }
  3. memcpy 实现 swap 泛型版。标准的两数交换依赖具体类型(intdouble 等)。用 memcpy 写一个 generic_swap,支持交换任意类型的两个变量(通过 void*sizeof 和临时缓冲区)。

    知识点提示:需要一个临时缓冲区(unsigned char tmp[MAX_SIZE]malloc)来保存中间值。这是 C++ std::swap 的底层实现思想——C++ 模板生成的代码实际上就是按 sizeof(T) 做的 memcpy 序列。

    参考解答
    generic_swap.c
    c
    #include <string.h>
    #include <stdio.h>
    
    #define MAX_SWAP_SIZE 1024
    
    void generic_swap(void *a, void *b, size_t size)
    {
        unsigned char tmp[MAX_SWAP_SIZE];   /* 或动态分配 */
    
        my_memcpy(tmp, a, size);            /* tmp = a   */
        my_memcpy(a,   b, size);            /* a   = b   */
        my_memcpy(b, tmp, size);            /* b   = tmp */
    }
    
    int main(void)
    {
        int x = 42, y = 99;
        printf("before: x=%d y=%d\n", x, y);
        generic_swap(&x, &y, sizeof(int));
        printf("after:  x=%d y=%d\n", x, y);
    
        double a = 3.14, b = 2.71;
        printf("before: a=%.2f b=%.2f\n", a, b);
        generic_swap(&a, &b, sizeof(double));
        printf("after:  a=%.2f b=%.2f\n", a, b);
    
        return 0;
    }

    要点:

    • tmpunsigned char[MAX_SWAP_SIZE] 是固定大小方案,省去 malloc/free
    • 调用 my_memcpy 而非标准 memcpy,巩固本课所学
    • C++ std::swap 本质上做着完全相同的事——只是生成模板特化时编译器内联了拷贝
  4. 阅读 material:glibc 的 memcpy 实现。在 glibc 源码(sourceware.org)中查看工业级 memcpy 的实现。它如何对不同的拷贝规模选择不同的策略(小 → 逐字节、中 → WORD 对齐、大 → SIMD/块传输)?为什么一个看似简单的函数有这么多优化?

    知识点提示:关注 glibc 对"对齐拷贝"(WORD-aligned copy)的处理——先用逐字节拷贝将 src 对齐到 WORD 边界,然后用 unsigned long 批量拷贝,末尾再用逐字节收尾。这是"先对齐、后批量、再收尾"的三段式优化模式。

  5. 比较 memcpy 的不同实现策略的性能。为自己实现的逐字节 my_memcpy、标准库 memcpy、和一篇使用块拷贝(一次拷贝 8 字节的 unsigned long)的"优化版 my_memcpy_fast"做一次性能对比——拷贝 1KB、1MB、10MB 各 10000 次,测量各自耗时。

    知识点提示:用 clock() 测量时间,用 volatile 防止编译器优化掉无用调用。小数据量时逐字节和块传输差异不大(数据已在 L1 cache),大数据量时差异可达数倍(低效逐字节 vs 高效块/SIMD)。


参考资料

  • man memcpy — Linux 手册页,查看标准库 memcpy 的函数签名与行为契约
  • C 标准 §7.24.2.1 — memcpy 函数的正式规范(C99 引入 restrict 限定符)
  • C99 标准 §6.2.5 — void 类型的定义与使用限制
  • K&R《C 程序设计语言》§5.4 指针与地址算术 — 指针运算的经典讲解
  • glibc memcpy 源码 — 工业级实现,理解多级优化策略

"Premature optimization is the root of all evil." — Donald Knuth

Released under the MIT License.