【C语言】内存函数
摘要:字符串函数遇到
'\0'就停,只能处理文本;结构体、整数数组和二进制缓冲区里随时可能出现零字节,按字符串方式复制会把数据截断。本文讲memcpy、memmove、memset、memcmp和memchr这组按字节操作的内存函数,重点说明长度参数为什么以字节计、memcpy为什么不允许重叠、memmove怎样根据相对位置选择复制方向,最后用动态数组的删除与插入把memmove落到代码里。
文章目录
1. 字符串函数停在 ‘\0’
strcpy 从源地址开始逐字节复制,遇到 '\0' 就停。对文本没有问题,对二进制数据就不成立:整数、浮点数、结构体里都可能有值为 0 的字节。
#include <stdio.h>
#include <string.h>
int main(void)
{
int n = 10000;
char text[sizeof n];
for (size_t i = 0; i < sizeof text; i++)
text[i] = '#';
strcpy(text, (char *)&n);
printf("sizeof(int) = %zu\n", sizeof n);
printf("strlen(text) = %zu\n", strlen(text));
for (size_t i = 0; i < sizeof n; i++)
printf("%02X ", (unsigned char)text[i]);
printf("\n");
return 0;
}
输出:
sizeof(int) = 4
strlen(text) = 2
10 27 00 23
在小端机器上,10000 的 4 个字节是 10 27 00 00。strcpy 复制到第 3 个字节 00 就停了,只写了 10 27 00 三个字节,第 4 个字节仍是初始化时的 '#',十六进制是 23。strlen 也只数出 2。
如果换成 0x01020304 这种 4 个字节都不为 0 的数,strlen 会继续往后读到数组外,属于未定义行为。这段输出还依赖 int 占 4 字节和小端字节序;大端机器上第一个字节就是 00,strlen 会返回 0。
内存函数换了一套接口:参数是 void *,长度是字节数,函数内部按字节处理,不关心数据是什么,也不看 '\0'。
| 函数 | 作用 | 长度参数 |
|---|---|---|
memcpy | 复制一块内存 | 字节数 |
memmove | 复制一块内存,允许重叠 | 字节数 |
memset | 按字节填充一块内存 | 字节数 |
memcmp | 按字节比较两块内存 | 字节数 |
memchr | 在内存块里查找一个字节 | 字节数 |
长度一律写 n * sizeof(元素类型) 或 sizeof 数组,不要写元素个数。sizeof 的结果天然是字节数,和这些函数单位一致。
2. memcpy:按字节复制
void *memcpy(void *destination, const void *source, size_t num);
从 source 复制 num 个字节到 destination,返回目标起始地址。要求两块内存不重叠。
#include <stdio.h>
#include <string.h>
int main(void)
{
int src[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
int dst[10] = {0};
memcpy(dst, src, 5 * sizeof(int));
for (int i = 0; i < 10; i++)
printf("%d ", dst[i]);
printf("\n");
return 0;
}
输出:
1 2 3 4 5 0 0 0 0 0
只复制了 5 个 int,共 20 字节,后 5 个元素保持原值。写成 memcpy(dst, src, 20) 在本机也能跑,但数字 20 依赖 int 占 4 字节,换类型就错了。
自己实现时,void * 不能直接解引用,也不能做指针算术,要先转成 char * 或 unsigned char *,每次前进 1 个字节:
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
void *my_memcpy(void *dst, const void *src, size_t count)
{
void *ret = dst;
unsigned char *d = (unsigned char *)dst;
const unsigned char *s = (const unsigned char *)src;
assert(dst != NULL);
assert(src != NULL);
while (count-- > 0)
*d++ = *s++;
return ret;
}
int main(void)
{
int src[5] = {1, 2, 3, 4, 5};
int dst[5] = {0};
my_memcpy(dst, src, sizeof src);
for (int i = 0; i < 5; i++)
printf("%d ", dst[i]);
printf("\n");
return 0;
}
输出:
1 2 3 4 5
先把 dst 存进 ret,因为循环里 dst 会一路后移,最后要靠 ret 原样返回起始地址。while (count-- > 0) 在 count 为 0 时一次都不执行,不会多复制一个字节。
memcpy 不允许源和目标重叠。原因看下面这段:
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
void *my_memcpy(void *dst, const void *src, size_t count)
{
void *ret = dst;
unsigned char *d = (unsigned char *)dst;
const unsigned char *s = (const unsigned char *)src;
while (count-- > 0)
*d++ = *s++;
return ret;
}
int main(void)
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
my_memcpy(arr + 2, arr, 5 * sizeof(int));
for (int i = 0; i < 10; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
输出:
1 2 1 2 1 2 1 8 9 10
正确结果应该是 1 2 1 2 3 4 5 8 9 10。目标从下标 2 开始,写指针一开始就落在源数据后面;从前往后复制时,写指针追上了还没读走的源字节,把 3、4、5 提前覆盖成了 1、2。标准规定 memcpy 遇到重叠时结果未定义,不同实现可能给出不同结果。

3. memmove:重叠时换方向
void *memmove(void *destination, const void *source, size_t num);
功能和 memcpy 一样,区别是允许源和目标重叠。做法是先判断相对位置,再决定复制方向:
- 目标在源之前(
dst < src):从前往后复制,写指针始终落后于读指针。 - 目标在源之后(
dst > src):从后往前复制,写指针始终领先于读指针。 - 两块完全不重叠:两个方向都可以。
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
void *my_memmove(void *dst, const void *src, size_t count)
{
void *ret = dst;
unsigned char *d = (unsigned char *)dst;
const unsigned char *s = (const unsigned char *)src;
if ((uintptr_t)d < (uintptr_t)s)
{
while (count-- > 0)
*d++ = *s++;
}
else if ((uintptr_t)d > (uintptr_t)s)
{
d += count;
s += count;
while (count-- > 0)
*--d = *--s;
}
return ret;
}
int main(void)
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
my_memmove(arr + 2, arr, 5 * sizeof(int));
for (int i = 0; i < 10; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
输出:
1 2 1 2 3 4 5 8 9 10
目标在源之后,所以从后往前复制:先搬 5 到下标 6,再搬 4 到下标 5,直到 1 落到下标 2。每一步写的都是已经读走的位置,不会破坏还没读的源数据。
反过来,目标在源之前时从前往后复制:
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
void *my_memmove(void *dst, const void *src, size_t count)
{
void *ret = dst;
unsigned char *d = (unsigned char *)dst;
const unsigned char *s = (const unsigned char *)src;
if ((uintptr_t)d < (uintptr_t)s)
{
while (count-- > 0)
*d++ = *s++;
}
else if ((uintptr_t)d > (uintptr_t)s)
{
d += count;
s += count;
while (count-- > 0)
*--d = *--s;
}
return ret;
}
int main(void)
{
int arr[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
my_memmove(arr, arr + 2, 5 * sizeof(int));
for (int i = 0; i < 10; i++)
printf("%d ", arr[i]);
printf("\n");
return 0;
}
输出:
3 4 5 6 7 6 7 8 9 10
前 5 个 int 被下标 2 到 6 的 3 4 5 6 7 覆盖,后面的元素保持原样。
严格按标准,指向不同对象的指针做 <、> 比较是未定义行为。上面的实现先把地址转成 uintptr_t 再比较,避开这个边界;工程里直接用标准库的 memmove 即可。
memcpy 和 memmove 怎么选:
| 情况 | 用哪个 | 原因 |
|---|---|---|
| 能确定两块内存不重叠 | memcpy | 契约更简单,编译器可以更大胆地优化 |
| 存在重叠可能 | memmove | 先判方向,结果一定正确 |
| 拿不准 | memmove | 多一次分支判断,换正确性 |
两者的时间复杂度都是 O ( n ) O(n) O(n),额外空间 O ( 1 ) O(1) O(1)。
4. memset:按字节填充
void *memset(void *ptr, int value, size_t num);
把 ptr 开始的 num 个字节都设成 value。value 先被转换成 unsigned char,也就是只取低 8 位,再逐字节写入。
#include <stdio.h>
#include <string.h>
int main(void)
{
char str[] = "hello world";
memset(str, 'x', 6);
printf("%s\n", str);
return 0;
}
输出:
xxxxxxworld
前 6 个字节被改成 'x',第 7 个字节原本是空格,保留下来。
对字符数组按字节填充没有问题,对 int 数组就要小心:
#include <stdio.h>
#include <string.h>
int main(void)
{
int arr[4] = {0};
memset(arr, 1, sizeof arr);
printf("%d\n", arr[0]);
printf("0x%08X\n", (unsigned)arr[0]);
return 0;
}
输出:
16843009
0x01010101
memset 把每个字节都写成 0x01,4 个字节拼起来就是 0x01010101,十进制 16843009,不是 1。想给 int 数组赋 1,只能用循环逐个赋值。

安全的用法只有两类:
memset(arr, 0, sizeof arr):全 0 位模式在主流实现下就是数值 0。memset(arr, -1, sizeof arr):每个字节都是0xFF,在补码表示下每个int是 -1。这个结果依赖补码,C 标准没有强制。
value 超出 unsigned char 范围时按模 256 截断,例如 memset(buf, 263, 4) 每个字节都是 7。这类写法可读性差,不如直接写 7。
memset 常用来清零结构体或数组,时间复杂度
O
(
n
)
O(n)
O(n)。
5. memcmp 与 memchr
int memcmp(const void *ptr1, const void *ptr2, size_t num);
从两个地址开始比较 num 个字节,按 unsigned char 逐字节比,遇到第一对不同字节就返回。返回值只保证符号:大于 0、等于 0、小于 0。
#include <stdio.h>
#include <string.h>
int main(void)
{
char a[] = "DWgaOtP12df0";
char b[] = "DWGAOTP12DF0";
int r = memcmp(a, b, sizeof(a));
if (r > 0)
printf("%s 大于 %s\n", a, b);
else if (r < 0)
printf("%s 小于 %s\n", a, b);
else
printf("%s 等于 %s\n", a, b);
return 0;
}
输出:
DWgaOtP12df0 大于 DWGAOTP12DF0
第 3 个字节分出大小:'g' 是 0x67,'G' 是 0x47,后面不再比较。具体返回值可能是 1,也可能是两个字节的差 32,调用方只能看符号。
memcmp 和 strcmp 的区别:
| 对比项 | memcmp | strcmp |
|---|---|---|
| 停止条件 | 比满 num 个字节 | 遇到 '\0' |
| 能否处理含零字节的数据 | 能 | 不能 |
| 比较长度 | 由调用方指定 | 由字符串结尾决定 |
| 返回值 | 只看符号 | 只看符号 |
比较整数数组时,memcmp 看到的是内存里的字节序。小端机器上先碰到低位字节,返回值反映字节序列的大小,不一定等于整数数值的大小。结构体也不要整体 memcmp:成员之间的填充字节内容不确定,两个成员完全相同的结构体可能比出不相等。
memchr 在内存块里找第一个等于指定值的字节:
void *memchr(const void *ptr, int ch, size_t num);
#include <stdio.h>
#include <string.h>
int main(void)
{
const char text[] = "hello world";
const char *p = memchr(text, 'o', sizeof text);
if (p != NULL)
printf("下标 %td\n", p - text);
else
printf("没找到\n");
return 0;
}
输出:
下标 4
找到返回指向该字节的指针,找不到返回 NULL。查找范围由 num 决定,不受 '\0' 限制,所以也能在二进制数据里找。
五个函数的开销:
| 函数 | 时间 | 额外空间 |
|---|---|---|
memcpy | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
memmove | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
memset | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
memcmp | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
memchr | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
n 是字节数。memcmp 和 memchr 找到第一处不同或目标字节就停,最好情况小于 n,最坏仍是
O
(
n
)
O(n)
O(n)。
6. 实践:动态数组的删除与插入
顺序表删除一个元素,要把后面的元素整体前移;插入一个元素,要把插入点之后的元素整体后移。两块区间重叠,正好用 memmove。
下面这个动态数组保存 int,支持尾部追加、按下标删除和按下标插入。容量不够时 realloc 翻倍。
#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct
{
int *data;
size_t size;
size_t cap;
} IntVec;
static void vec_init(IntVec *v, size_t cap)
{
v->data = (int *)malloc(cap * sizeof(int));
if (v->data == NULL)
{
perror("malloc");
exit(1);
}
v->size = 0;
v->cap = cap;
}
static void vec_free(IntVec *v)
{
free(v->data);
v->data = NULL;
v->size = 0;
v->cap = 0;
}
static void vec_push(IntVec *v, int x)
{
if (v->size == v->cap)
{
size_t new_cap = v->cap * 2;
int *p = (int *)realloc(v->data, new_cap * sizeof(int));
if (p == NULL)
{
perror("realloc");
free(v->data);
exit(1);
}
v->data = p;
v->cap = new_cap;
}
v->data[v->size++] = x;
}
static int vec_remove(IntVec *v, size_t idx)
{
assert(v != NULL);
assert(idx < v->size);
int old = v->data[idx];
if (idx + 1 < v->size)
memmove(v->data + idx, v->data + idx + 1,
(v->size - idx - 1) * sizeof(int));
v->size--;
return old;
}
static void vec_insert(IntVec *v, size_t idx, int x)
{
assert(v != NULL);
assert(idx <= v->size);
vec_push(v, 0); // 先扩容,并占出一个位置
if (idx < v->size - 1)
memmove(v->data + idx + 1, v->data + idx,
(v->size - 1 - idx) * sizeof(int));
v->data[idx] = x;
}
static void vec_print(const IntVec *v)
{
printf("[");
for (size_t i = 0; i < v->size; i++)
printf("%s%d", i ? ", " : "", v->data[i]);
printf("]\n");
}
int main(void)
{
IntVec v;
vec_init(&v, 4);
for (int i = 1; i <= 5; i++)
vec_push(&v, i);
printf("初始:");
vec_print(&v);
int old = vec_remove(&v, 1);
printf("删除下标 1(值 %d):", old);
vec_print(&v);
vec_insert(&v, 1, 9);
printf("在下标 1 插入 9:");
vec_print(&v);
printf("size = %zu, cap = %zu\n", v.size, v.cap);
vec_free(&v);
return 0;
}
输出:
初始:[1, 2, 3, 4, 5]
删除下标 1(值 2):[1, 3, 4, 5]
在下标 1 插入 9:[1, 9, 3, 4, 5]
size = 5, cap = 8
删除时,memmove 把下标 idx + 1 到末尾的 size - idx - 1 个元素搬到 idx,源区间在目标区间之后,函数自动选择从前往后复制。插入时先把新元素追加到末尾腾出容量,再把 idx 到原末尾的元素后移一位,源区间在目标区间之前,函数自动选择从后往前复制。
两个接口的复杂度:
| 操作 | 时间 | 说明 |
|---|---|---|
vec_push | 均摊 O ( 1 ) O(1) O(1) | 扩容时复制全部元素,摊到每次追加 |
vec_remove | O ( n ) O(n) O(n) | 后面的元素整体前移 |
vec_insert | O ( n ) O(n) O(n) | 插入点之后的元素整体后移 |
memmove 本身是
O
(
n
)
O(n)
O(n),这里没有额外空间。插入和删除慢,是顺序存储的固有代价;需要频繁在中间增删时,应换链表。
7. 小结
- 字符串函数遇到
'\0'就停,二进制数据不能用;内存函数按字节处理,长度参数一律是字节数。 memcpy要求源和目标不重叠,重叠时结果未定义;重叠复制必须用memmove。memmove先看相对位置:目标在源之前从前往后复制,目标在源之后从后往前复制。memset按字节填充,给int设 1 会得到0x01010101;只有全 0 和补码下的全0xFF可以放心用。memcmp比固定字节数,不受'\0'影响,返回值和strcmp一样只看符号;结构体的填充字节会让整体比较不可靠。memchr在指定字节范围内查找,返回首次出现的地址或NULL。- 五个函数的复杂度都是
O
(
n
)
O(n)
O(n);
memcpy和memmove额外空间 O ( 1 ) O(1) O(1)。
更多推荐



所有评论(0)