Hello,大家好! 这里是小J,本篇文章是我在学习C语言内存操作函数和数据存储时的笔记整理,包含 memcpy / memmove / memset / memcmp 的模拟实现、大端小端字节序、整型提升、浮点型存储以及多道经典例题,希望能帮助大家彻底搞懂内存中的那些事。


一、内存操作函数

<string.h> 头文件中提供了一组直接操作内存字节的函数,它们不关心数据类型,只以字节为单位进行读写。

1. memcpy – 内存拷贝

void *memcpy(void *dest, const void *src, size_t num);

· 功能:将 src 指向的内存块的前 num 个字节拷贝到 dest 指向的内存块。
· 返回值:返回 dest。
· 注意事项
· dest 和 src 不能有重叠(标准规定重叠时行为未定义)。
· 不关心数据类型,逐字节拷贝。
· 常用于拷贝数组、结构体等任意类型的数据。

模拟实现

#include <assert.h>

void* my_memcpy(void* dest, const void* src, size_t num)
{
    assert(dest && src);
    void* ret = dest;
    while (num--)
    {
        *(char*)dest = *(char*)src;
        dest = (char*)dest + 1;
        src = (char*)src + 1;
    }
    return ret;
}

为什么用 char* ?
char 类型占1个字节,强制转换为 char* 后可以逐字节操作,实现“泛型”拷贝。

2. memmove – 内存移动(支持重叠)

void *memmove(void *dest, const void *src, size_t num);

· 功能:与 memcpy 类似,但允许源和目标内存区域重叠,能正确处理重叠情况。
· 返回值:返回 dest。

模拟实现

核心思路:根据 dest 和 src 的相对位置,决定从前向后还是从后向前拷贝,避免覆盖。

void* my_memmove(void* dest, const void* src, size_t num)
{
    assert(dest && src);
    void* ret = dest;
    
    if (dest < src)
    {
        // 从前向后拷贝
        while (num--)
        {
            *(char*)dest = *(char*)src;
            dest = (char*)dest + 1;
            src = (char*)src + 1;
        }
    }
    else
    {
        // 从后向前拷贝
        while (num--)
        {
            *((char*)dest + num) = *((char*)src + num);
        }
    }
    return ret;
}

图解重叠拷贝:

重叠拷贝的两种情况与拷贝方向

memmove 的核心在于处理 srcdest 内存区域重叠时,如何避免数据在拷贝完成前被覆盖。关键在于根据 destsrc 的起始地址关系,决定拷贝方向。

情况一:dest 在 src 之前 (dest < src) -> 从前向后拷贝

  • 场景:目标区域在源区域的前面(低地址侧)。
  • 安全方向从前向后 (front to back)。
  • 原因:从低地址开始拷贝,即使有重叠,也是先拷贝源区域的前部(非重叠部分)到目标区域。当拷贝到重叠部分时,源数据已经被复制到了更前面的目标位置,所以不会被后续操作破坏。

示例:将数组 arr[3..6] 拷贝到 arr[0..3]

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

int main() {
    int arr[] = {10, 20, 30, 40, 50, 60, 70, 80};
    // 目标:将 arr[3], arr[4], arr[5], arr[6] (40,50,60,70) 移动到 arr[0], arr[1], arr[2], arr[3]
    // src = &arr[3], dest = &arr[0], num = 4 * sizeof(int)
    memmove(arr, arr + 3, 4 * sizeof(int));

    // 打印结果
    for (int i = 0; i < 8; i++) {
        printf("%d ", arr[i]);
    }
    // 输出:40 50 60 70 50 60 70 80
    return 0;
}

拷贝过程图解(从前向后)

初始数组索引: [0] [1] [2] [3] [4] [5] [6] [7]
初始值:       10  20  30  40  50  60  70  80
              dest         src
               |           |
               V           V
步骤1: 拷贝 arr[3] (40) -> arr[0], 数组变为: [40, 20, 30, 40, 50, 60, 70, 80]
步骤2: 拷贝 arr[4] (50) -> arr[1], 数组变为: [40, 50, 30, 40, 50, 60, 70, 80]
步骤3: 拷贝 arr[5] (60) -> arr[2], 数组变为: [40, 50, 60, 40, 50, 60, 70, 80]
步骤4: 拷贝 arr[6] (70) -> arr[3], 数组变为: [40, 50, 60, 70, 50, 60, 70, 80]

可以看到,即使 srcdest 有重叠(arr[3] 既是源也是目标),因为是从前向后拷贝,源数据 40, 50, 60, 70 在被覆盖前就已经被正确读取并复制到了前面。


情况二:dest 在 src 之后 (dest > src) -> 从后向前拷贝

  • 场景:目标区域在源区域的后面(高地址侧)。
  • 安全方向从后向前 (back to front)。
  • 原因:如果从前向后拷贝,会先覆盖源区域的前部数据,导致后续拷贝读取到错误的值。从后向前拷贝可以确保重叠部分的源数据在被覆盖前已被读取。

示例:将数组 arr[0..3] 拷贝到 arr[3..6]

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

int main() {
    int arr[] = {10, 20, 30, 40, 50, 60, 70, 80};
    // 目标:将 arr[0], arr[1], arr[2], arr[3] (10,20,30,40) 移动到 arr[3], arr[4], arr[5], arr[6]
    // src = &arr[0], dest = &arr[3], num = 4 * sizeof(int)
    memmove(arr + 3, arr, 4 * sizeof(int));

    // 打印结果
    for (int i = 0; i < 8; i++) {
        printf("%d ", arr[i]);
    }
    // 输出:10 20 30 10 20 30 40 80
    return 0;
}

拷贝过程图解(从后向前)

初始数组索引: [0] [1] [2] [3] [4] [5] [6] [7]
初始值:       10  20  30  40  50  60  70  80
              src         dest
               |           |
               V           V
步骤4: 拷贝 arr[3] (40) -> arr[6], 数组变为: [10, 20, 30, 40, 50, 60, 40, 80]
步骤3: 拷贝 arr[2] (30) -> arr[5], 数组变为: [10, 20, 30, 40, 50, 30, 40, 80]
步骤2: 拷贝 arr[1] (20) -> arr[4], 数组变为: [10, 20, 30, 40, 20, 30, 40, 80]
步骤1: 拷贝 arr[0] (10) -> arr[3], 数组变为: [10, 20, 30, 10, 20, 30, 40, 80]

如果错误地使用从前向后拷贝:

步骤1: 拷贝 arr[0] (10) -> arr[3], 数组变为: [10, 20, 30, 10, 50, 60, 70, 80] (arr[3]的40被覆盖)
步骤2: 拷贝 arr[1] (20) -> arr[4], 但此时 arr[1] 的值?(实际上还是20,但假设继续...)
... 最终结果错误。

因此,当 dest > src 时,必须从后向前拷贝。


情况三:destsrc 地址相同或区域不重叠

dest == src:源和目标为同一块内存,无需进行任何拷贝操作。从前或从后拷贝结果都一样。-* 区域不重叠:无论 destsrc 之前还是之后,只要两个内存块没有交集,从前或从后拷贝都是安全的。但为了代码统一,通常按 dest < srcdest >= src 来分支。-

总结与模拟实现逻辑

void* my_memmove(void* dest, const void* src, size_t num) {
    // ... 参数检查
    if (dest < src) {
        // 情况一:dest 在 src 之前 -> 从前向后拷贝
        // 逐字节从低地址向高地址拷贝
    } else {
        // 情况二:dest 在 src 之后或相等 -> 从后向前拷贝
        // 逐字节从高地址向低地址拷贝
    }
    // ... 返回 dest
}

记忆口诀“前向后,后向前”。即目标(dest)在源(src) 前面向前拷,目标在源 后面向后拷。
`dest```

3. memset – 内存填充

void *memset(void *ptr, int value, size_t num);

· 功能:将 ptr 指向的内存块的前 num 个字节全部设置为 value(value 转换为 unsigned char)。
· 返回值:返回 ptr。
· 注意:以字节为单位设置,不能用来初始化 int 数组为 1(因为1会变成 0x01010101)。

示例

int arr[10];
memset(arr, 0, sizeof(arr));  // 正确,全部置0
memset(arr, 1, sizeof(arr));  // 错误,每个字节变成1,int值变成0x01010101

4. memcmp – 内存比较

int memcmp(const void *ptr1, const void *ptr2, size_t num);

· 功能:比较两个内存块的前 num 个字节。
· 返回值
· 0:相等
· >0:ptr1 大于 ptr2
· <0:ptr1 小于 ptr2

与 strcmp 的区别:memcmp 不关心 \0,直接比较字节;strcmp 遇到 \0 就停止。


二、数据在内存中的存储

1. 字节序(大端/小端)

大端:数据的高位字节存储在低地址。
小端:数据的低位字节存储在低地址。

例如整数 0x12345678 在内存中的存储:

· 小端:78 56 34 12
· 大端:12 34 56 78

判断当前机器的字节序

int check_sys()
{
    int n = 1;
    return *(char*)&n;  // 取第一个字节,若为1则是小端,0则是大端
}

int main()
{
    if (check_sys() == 1)
        printf("小端\n");
    else
        printf("大端\n");
    return 0;
}

2. 整型在内存中的存储(补码)

· 正数:原码、反码、补码相同。
· 负数:补码 = 原码取反 + 1。

计算机中统一使用补码存储。

3. char 类型的深入探讨

char 到底是有符号还是无符号取决于编译器。
在 VS 上,char 等价于 signed char,取值范围 -128 ~ 127。
unsigned char 取值范围 0 ~ 255。

3.1 经典例题1:char a = -1 打印

int main()
{
    char a = -1;
    signed char b = -1;
    unsigned char c = -1;
    printf("a=%d, b=%d, c=%d\n", a, b, c);
    // 输出:a=-1, b=-1, c=255
    return 0;
}

解析

· -1 的补码全是 1:11111111(8位)。
· a 和 b 作为 signed char,高位为符号位,%d 打印时会整型提升,高位补符号位1,得到 0xFFFFFFFF,即 -1。
· c 作为 unsigned char,整型提升时高位补0,得到 0x000000FF,即 255。

3.2 经典例题2:char a = -128 打印 %u

int main()
{
    char a = -128;
    // -128 原码: 10000000 00000000 00000000 10000000
    // 补码: 11111111 11111111 11111111 10000000
    // 截断到 char: 10000000
    printf("%u\n", a);
    // 整型提升: 有符号 char 10000000 -> 11111111 11111111 11111111 10000000
    // 作为无符号整数输出: 4294967168
    return 0;
}

输出:4294967168(即 0xFFFFFF80)

3.3 经典例题3:char a = 128 打印 %u

int main()
{
    char a = 128;
    // 128 原码: 00000000 00000000 00000000 10000000
    // 截断: 10000000(与 -128 的补码相同)
    printf("%u\n", a);
    // 结果同 -128,输出 4294967168
    return 0;
}

注意:128 超出了 signed char 的范围,会发生截断,实际存储为 10000000,即 -128 的补码。

3.4 经典例题4:char 数组循环

int main()
{
    char a[1000];
    int i;
    for (i = 0; i < 1000; i++)
    {
        a[i] = -1 - i;
    }
    printf("%zd\n", strlen(a));  // 求字符串长度,遇到 \0 停止
    return 0;
}

解析

· -1 - i 的值:-1, -2, -3, …, -128, 127, 126, …, 1, 0, -1, -2 …
· 当值为 0 时,对应 ASCII 码 \0,strlen 停止。
· 从 -1 到 -128 共128个数,再从 127 到 1 共127个数,之后遇到 0。
· 总长度 = 128 + 127 = 255。


4. 浮点型在内存中的存储(IEEE 754)

以 float(32位)为例:

· 最高位1位:符号位 S
· 之后8位:指数位 E(移码表示,偏移127)
· 最后23位:尾数位 M(隐含整数部分1)

公式:(-1)^S × 1.M × 2^(E-127)

示例:float f = 5.5;

· 二进制:101.1 → 1.011 × 2^2
· S=0, E=2+127=129(10000001), M=011
· 存储:0 10000001 01100000000000000000000
· 十六进制:0x40B00000


三、综合例题解析(指针与整数转换)

#include <stdio.h>
int main()
{
    int a[4] = {1, 2, 3, 4};
    int* ptr1 = (int*)(&a + 1);
    int* ptr2 = (int*)((int)a + 1);
    printf("%x,%x\n", ptr1[-1], *ptr2);
    return 0;
}

分析(X86 小端环境)

· &a 是整个数组的地址,类型 int()[4]。
· &a + 1 跳过整个数组,指向数组末尾后面一个位置。
· ptr1 = (int
)(&a + 1) → ptr1 指向 a[4] 之后的地址。
· ptr1[-1] 等价于 *(ptr1 - 1),指向前一个 int,即 a[3] = 4。
· 输出 4(十六进制 4)。
· (int)a 将数组首地址强制转换为整数,然后 +1 表示地址值加1字节。
· ptr2 指向 a[0] 的第二个字节(小端环境下 a[0] 存储为 01 00 00 00,偏移1字节后指向 00 00 00 02 的开头)。
· *ptr2 读取4个字节:00 00 00 02 → 值为 0x2000000(即 33554432)。
· 输出 2000000(十六进制)。

最终输出:4,2000000


四、总结与记忆技巧

函数/概念核心要点记忆口诀
memcpy不重叠拷贝,逐字节“拷贝不重叠,字节逐个搬”
memmove处理重叠,分向前后“重叠不用慌,前后分开搬”
memset按字节填充“字节填充,注意整型陷阱”
memcmp字节比较,无视 \0“比较不看零,按字节判断”
大小端高低地址存高位/低位“小端低低,大端高低”
整型提升有符号补符号位,无符号补0“提升看符号,无符号补零”
浮点存储IEEE 754:S+E+M“符号指数加尾数,偏移127要记住”

希望这篇博客能帮你彻底理清C语言内存操作和数据存储的知识点。如果有任何疑问,欢迎在评论区交流讨论!

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐