Lesson 26: 实现 my_memcpy — 标准内存拷贝
练习任务
难度:易-中
实现自己的 memcpy 函数——my_memcpy(dest, src, n),将 n 字节从 src 逐字节拷贝到 dest,返回 dest。函数原型与标准库一致:
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— 返回转换前的原始指针,而非d或s(语义一致,与标准库对齐) unsigned char*的严谨性 — 避免char有符号时的符号扩展问题- 浅拷贝 vs 深拷贝 —
memcpy对含指针成员的结构体只拷贝指针值(浅拷贝),两个对象指向同一内存 memcpyvsstrcpyvs 逐元素赋值 — 粒度、类型通用性、终止条件的三维对比
代码框架
#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_t和int在循环中有什么区别?
TIP
先不要往下翻看参考解答。把 void*→char* 的转换写好,然后逐字节循环——这是内存操作的根基。本课看似简单,但它是后续 Lesson 27(memmove 重叠拷贝)、Lesson 45(realloc 动态扩容)的基础。
深度讲解
1. void* —— C 语言的"万能指针"
1.1 万能的一面:接收任意指针
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 *p = &x;
int *ip = p; // ✅ void* → int* 在 C 中隐式转换合法
// C++ 中必须写 int *ip = (int *)p;
// 但这是 C,所以 memcpy 可以直接 return dest 而不用强制类型转换1.2 致命的限制:不能做算术,不能解引用
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* 会怎样?
// ❌ 用 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 但读了 4B2.3 char* vs unsigned char* — 哪个更严谨?
// 标准写法: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 的行为"等价于"将 src 和 dest 视为 unsigned char 数组来拷贝。使用 unsigned char* 的好处:
- 避免有符号
char在比较或赋值时的符号扩展(在 char 为有符号的系统上,0x80→int扩展为0xFFFFFF80) - 与标准库的内部实现更一致
本练习中 char* 和 unsigned char* 等价——编译器优化后无差异。工业级代码倾向 unsigned char*。
3. const 限定符的跨转换保留
3.1 不能丢掉 const
// ✅ 正确:保留 const
const char *s = (const char *)src;
// src 声明为 const void*,转换后 src 的数据仍受 const 保护
// ❌ 错误:丢掉 const
char *s = (char *)src; // 编译器警告:discards 'const' qualifier
// 这不仅产生警告——更严重的是,现在可以通过 s 修改调用者的只读数据const char result[] = "result"; // 调用者声明为只读
// 如果你的 my_memcpy 内部丢掉了 const:
char *s = (char *)src; // ← 丢掉 const
s[0] = 'X'; // ← 修改了调用者的只读数据!UB!
// 保留 const 则编译器会在 s[0]='X' 处报错,防止此类 bugconst 不仅是编译器的检查工具——它是 API 设计中的"契约":const void *src 告诉所有读代码的人,"我不会动你的数据"。在转换中丢失 const 等于破坏了承诺。
3.2 目标指针不加 const
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 的语义和范围
#include <stddef.h> // size_t 定义于此
#include <string.h> // 但也包含了 <stddef.h>
// size_t 的特征:
// - 无符号整型(unsigned integer type)
// - 保证能表示任何对象的最大大小(取决于平台字长)
// - 64 位系统: 8 字节(最大值 ~18 EB)
// - int 典型: 4 字节(最大值 ~2 GB)// ❌ 用 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 循环中的有符号/无符号
// ✅ 好:循环变量用 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 三种等价写法
// 方式 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 而不是 d 或 s?
// 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 dest 和 return 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(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 含指针的结构体:浅拷贝的危险
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对于含指针成员的结构体,需要深拷贝——对指针指向的内容也进行拷贝:
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 完整实现
#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;
}核心设计决策:
while (n--):最简洁的逐字节循环。n每次递减,直到 0 自动退出。无需额外循环变量。- 返回
dest:调用者传入的原始目标指针,语义清晰。如果返回d,它已被移动了n字节。 len = strlen(src) + 1:+1确保'\0'也被拷贝。忘记+1是常见错误——dest会是未终止的字符数组,printf("%s")会读越界。
对照检查:
void*转换成了char*吗?src转换保留了const吗?循环条件用的是n递减吗?返回的是dest(原始指针)吗?
课堂讨论
- 为什么
memcpy的参数顺序是(dest, src, n)而不是(src, dest, n)?这种顺序有什么工程哲学支撑? - 如果
n = 0,my_memcpy应该做什么?为什么这样是安全的? size_t和int有什么区别?假设在一个超大文件拷贝场景(3.5 GB),用int做参数会发生什么?memcpy能用来拷贝结构体吗?什么时候安全,什么时候危险?为什么?- 为什么 C 标准要同时提供
memcpy和memmove两个函数?它们的功能有重叠吗?
讨论答案
Q1: 参数顺序 (dest, src, n) 的工程哲学
类比赋值语句 dest = src,目的在左,源在右。
这是 UNIX 系统编程的经典约定——几乎所有标准库的拷贝系函数都遵循"目的在先"的顺序:
strcpy(dest, src)→ 目的在前memcpy(dest, src, n)→ 目的在前strcat(dest, src)→ 目的在前- shell 命令:
cp src dest→ 源在前!与 C 库相反(一个著名的"不一致")
C 语言的这个选择可能是从赋值语句的逻辑自然导出——a = b 中 a(目的)写在左边。当函数需要表达类似语义时,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 文件的场景:
// ❌ 用 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)结构体安全,对含指针或资源句柄的结构体危险。
安全:
struct Point { int x, y; }; // 纯值类型,无指针
struct Point a = {1, 2}, b;
memcpy(&b, &a, sizeof(a)); // ✅ 等价于 b = a危险:
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,体验方向判断和反向拷贝的精妙。
课后练习
用指针版本替换下标版本。将参考解答中的
while (n--) *d++ = *s++展开为等价的下标版本,并比较两种写法在"编译产出的汇编"上可能有什么差异。知识点提示:现代编译器(gcc -O2)通常会将两种形式优化为相同的汇编代码(
rep movsb或memcpy内建),所以性能上无差异。差异主要在代码可读性——指针版本更紧凑但需对*d++的优先级有直觉。参考解答
cvoid *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++的运算符优先级
实现
my_memset。仿照my_memcpy的实现,编写void *my_memset(void *s, int c, size_t n)——将s指向的前n个字节全部设置为值c。注意c是int但写入时按unsigned char截断。知识点提示:标准
memset以unsigned char方式写入c的低 8 位。如果c = 0x12345678,实际写入0x78。循环用while (n--) *p++ = (unsigned char)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; }用
memcpy实现swap泛型版。标准的两数交换依赖具体类型(int、double等)。用memcpy写一个generic_swap,支持交换任意类型的两个变量(通过void*、sizeof和临时缓冲区)。知识点提示:需要一个临时缓冲区(
unsigned char tmp[MAX_SIZE]或malloc)来保存中间值。这是 C++std::swap的底层实现思想——C++ 模板生成的代码实际上就是按sizeof(T)做的memcpy序列。参考解答
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; }要点:
tmp用unsigned char[MAX_SWAP_SIZE]是固定大小方案,省去malloc/free- 调用
my_memcpy而非标准memcpy,巩固本课所学 - C++
std::swap本质上做着完全相同的事——只是生成模板特化时编译器内联了拷贝
阅读 material:glibc 的 memcpy 实现。在 glibc 源码(sourceware.org)中查看工业级
memcpy的实现。它如何对不同的拷贝规模选择不同的策略(小 → 逐字节、中 → WORD 对齐、大 → SIMD/块传输)?为什么一个看似简单的函数有这么多优化?知识点提示:关注 glibc 对"对齐拷贝"(WORD-aligned copy)的处理——先用逐字节拷贝将 src 对齐到 WORD 边界,然后用
unsigned long批量拷贝,末尾再用逐字节收尾。这是"先对齐、后批量、再收尾"的三段式优化模式。比较 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