【C语言】内存函数与数据在内存中的存储

文章目录

在C语言中,内存操作是编程的核心环节之一。本文将详细介绍四个常用的内存函数: memcpymemmovememsetmemcmp,包括它们的使用方法、模拟实现以及适用场景,帮助你更好地理解和运用这些函数进行内存操作。

内存函数

一、memcpy:内存复制函数

memcpy函数用于从源内存地址向目标内存地址复制指定字节数的数据,其函数原型如下:

c 复制代码
void * memcpy ( void * destination, const void * source, size_t num );

函数特点

  • source指向的内存位置开始,复制num个字节到destination指向的内存位置。
  • 不会因遇到\0而停止复制,严格按照指定的字节数操作。
  • 不处理重叠内存:如果源地址和目标地址的内存区域有重叠,复制结果是未定义的。

使用示例

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int arr2[10] = {0};
    // 复制20个字节(即5个int元素,每个int占4字节)
    memcpy(arr2, arr1, 20);
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr2[i]); // 输出:1 2 3 4 5 0 0 0 0 0
    }
    return 0;
}

模拟实现

c 复制代码
#include <assert.h>

void * memcpy(void * dst, const void * src, size_t count) {
    void * ret = dst;
    assert(dst && src); // 确保指针非空
    // 按字节逐个复制
    while (count--) {
        *(char *)dst = *(char *)src;
        dst = (char *)dst + 1;
        src = (char *)src + 1;
    }
    return ret;
}

说明 :通过将指针强制转换为char*,实现按字节操作,确保对任意类型的内存都能正确复制。

二、memmove:处理重叠内存的复制函数

memmovememcpy功能类似,但它支持源内存和目标内存重叠的场景,函数原型如下:

c 复制代码
void * memmove ( void * destination, const void * source, size_t num );

函数特点

  • memcpy的核心区别:可以安全处理重叠的内存区域
  • 当源地址和目标地址重叠时,使用memmove可保证复制结果正确。

使用示例

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    int arr1[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    // 将arr1的前5个元素(20字节)复制到arr1+2的位置(从第3个元素开始)
    memmove(arr1 + 2, arr1, 20);
    for (int i = 0; i < 10; i++) {
        printf("%d ", arr1[i]); // 输出:1 1 2 3 4 5 6 7 8 9 10(原文档此处代码笔误,arr2应为arr1)
    }
    return 0;
}

模拟实现

c 复制代码
#include <assert.h>

void * memmove(void * dst, const void * src, size_t count) {
    void * ret = dst;
    assert(dst && src);
    if (dst <= src || (char *)dst >= (char *)src + count) {
        // 内存不重叠或目标地址在源地址之后,从低地址到高地址复制
        while (count--) {
            *(char *)dst = *(char *)src;
            dst = (char *)dst + 1;
            src = (char *)src + 1;
        }
    } else {
        // 内存重叠且目标地址在源地址之间,从高地址到低地址复制
        dst = (char *)dst + count - 1;
        src = (char *)src + count - 1;
        while (count--) {
            *(char *)dst = *(char *)src;
            dst = (char *)dst - 1;
            src = (char *)src - 1;
        }
    }
    return ret;
}

说明:通过判断内存区域是否重叠,选择正向复制(低到高)或反向复制(高到低),避免覆盖未复制的源数据。

三、memset:内存设置函数

memset用于将指定内存区域的每个字节设置为指定值,函数原型如下:

c 复制代码
void * memset ( void * ptr, int value, size_t num );

函数特点

  • 字节为单位 设置内存值,value会被转换为unsigned char后填充。
  • 常用于初始化数组或清空内存区域。

使用示例

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char str[] = "hello world";
    // 将前6个字节设置为'x'
    memset(str, 'x', 6);
    printf(str); // 输出:xxxxxxworld
    return 0;
}

注意memset按字节操作,若用于设置int数组,可能无法达到预期效果(如memset(arr, 1, 4)会将int值设为0x01010101,而非1)。

四、memcmp:内存比较函数

memcmp用于比较两个内存区域的前num个字节,函数原型如下:

c 复制代码
int memcmp ( const void * ptr1, const void * ptr2, size_t num );

函数特点

  • 按字节比较,每个字节视为unsigned char类型。
  • 返回值表示两个内存区域的大小关系:
    • <0ptr1的第一个不同字节小于ptr2的对应字节;
    • 0:两个内存区域的前num个字节完全相同;
    • >0ptr1的第一个不同字节大于ptr2的对应字节。

使用示例

c 复制代码
#include <stdio.h>
#include <string.h>

int main() {
    char buffer1[] = "DWgaOtP12df0";
    char buffer2[] = "DWGAOTP12DF0";
    int n = memcmp(buffer1, buffer2, sizeof(buffer1));
    if (n > 0) 
        printf("'%s' is greater than '%s'\n", buffer1, buffer2);
    else if (n < 0) 
        printf("'%s' is less than '%s'\n", buffer1, buffer2);
    else
        printf("'%s' is the same as '%s'\n", buffer1, buffer2);
    return 0;
}

说明 :示例中因小写字母的ASCII值大于大写字母,buffer1会被判定为大于buffer2

总结

函数 功能 特点 适用场景
memcpy 复制内存 不处理重叠内存,效率较高 非重叠内存的复制
memmove 复制内存 处理重叠内存,安全性高 可能重叠的内存复制
memset 设置内存值 按字节操作,用于初始化或填充 内存初始化、批量设置值
memcmp 比较内存 按字节比较,返回差值关系 任意类型内存的比较

数据在内存中的存储

一、整数在内存中的存储

整数在内存中以补码形式存储,这是由计算机系统的特性决定的。要理解补码,需先掌握原码、反码的概念:

1.1 原码、反码、补码的定义

  • 符号位:最高位为符号位,0表示正数,1表示负数。
  • 数值位:剩余位表示数值大小。
  • 正整数 :原码、反码、补码完全相同。
    • 示例:int a = 5(32位)
      原码:00000000 00000000 00000000 00000101
      反码:00000000 00000000 00000000 00000101
      补码:00000000 00000000 00000000 00000101
  • 负整数
    • 原码:直接翻译二进制(符号位为1)。
    • 反码:符号位不变,数值位按位取反。
    • 补码:反码 + 1。
    • 示例:int b = -5(32位)
      原码:10000000 00000000 00000000 00000101
      反码:11111111 11111111 11111111 11111010
      补码:11111111 11111111 11111111 11111011

1.2 为什么使用补码?

  • 统一符号位和数值位:补码让符号位参与运算,无需额外处理。
  • 简化运算 :CPU只有加法器,补码可将减法转换为加法(如a - b = a + (-b))。
  • 节省硬件:补码与原码的转换规则统一,无需额外电路。

二、大小端字节序和字节序判断

当数据占多个字节时,会涉及字节在内存中的排列顺序,即字节序

2.1 大小端的定义

  • 大端字节序 :数据的高位字节存于内存低地址,低位字节存于高地址。
    • 示例:0x11223344 存储为 11 22 33 44(低地址到高地址)。
  • 小端字节序 :数据的低位字节存于内存低地址,高位字节存于高地址。
    • 示例:0x11223344 存储为 44 33 22 11(低地址到高地址)。

2.2 为什么存在大小端?

计算机以字节为内存单位,对于超过1字节的数据(如shortint),不同硬件架构对字节排列顺序有不同约定:

  • X86、ARM(默认)等架构采用小端。
  • KEIL C51、部分ARM配置采用大端。

2.3 如何判断机器的字节序?

通过代码可快速判断当前系统的字节序:

方法1:指针法
c 复制代码
#include <stdio.h>

int check_sys() {
    int i = 1;
    // 取i的地址并强制转换为char*,仅访问第一个字节
    return *(char*)&i; 
}

int main() {
    if (check_sys() == 1) {
        printf("小端\n"); // 小端中第一个字节为0x01
    } else {
        printf("大端\n"); // 大端中第一个字节为0x00
    }
    return 0;
}
方法2:联合体法
c 复制代码
#include <stdio.h>

int check_sys() {
    union {
        int i;
        char c;
    } un;
    un.i = 1;
    return un.c; // 联合体成员共用内存,c访问第一个字节
}

三、浮点数在内存中的存储

浮点数(floatdouble)的存储方式与整数完全不同,遵循IEEE 754标准

3.1 IEEE 754标准格式

任意二进制浮点数V可表示为:

V = (-1)\^S \\times M \\times 2\^E

  • S:符号位(0为正,1为负)。
  • M:有效数字(1 ≤ M < 2)。
  • E:指数位(整数)。

3.2 存储结构

  • 32位float:1位S + 8位E + 23位M。
  • 64位double:1位S + 11位E + 52位M。

3.3 存储与读取规则

存储过程:
  1. 规范化M :M默认省略整数位1,仅存小数部分(如1.001存为001),节省1位空间。
  2. 调整E :E为无符号整数,存储时需加上中间值(float加127,double加1023)。
    示例:E=3(float)→ 存储为3+127=130(二进制10000010)。
读取过程:
  • E不全为0且不全为1:E真实值 = 存储值 - 中间值,M前补1。
  • E全为0:E真实值 = 1 - 中间值,M前补0(表示接近0的小数)。
  • E全为1:M全0表示±无穷大,M非0表示NaN(非数值)。

3.4 示例解析

c 复制代码
#include <stdio.h>

int main() {
    int n = 9;
    float *pFloat = (float*)&n;
    printf("n = %d\n", n); // 输出:9
    printf("*pFloat = %f\n", *pFloat); // 输出:0.000000

    *pFloat = 9.0;
    printf("n = %d\n", n); // 输出:1091567616
    printf("*pFloat = %f\n", *pFloat); // 输出:9.000000
    return 0;
}
  • 第一步 :整数9的补码为00000000 00000000 00000000 00001001,按float解读时E全为0,结果接近0。
  • 第二步 :9.0的float存储为0 10000010 00100000000000000000000,按整数解读时为1091567616。

四、经典练习解析

练习1:字符类型的符号影响

c 复制代码
#include <stdio.h>

int main() {
    char a = -1;
    signed char b = -1;
    unsigned char c = -1;
    printf("a=%d, b=%d, c=%d", a, b, c); 
    // 输出:a=-1, b=-1, c=255
    return 0;
}
  • charsigned char:-1的补码为11111111,打印时符号扩展为11111111 11111111 11111111 11111111(仍为-1)。
  • unsigned char:-1的补码为11111111,无符号解读为255。

练习2:循环陷阱

c 复制代码
#include <stdio.h>

unsigned char i = 0;
int main() {
    for (i = 0; i <= 255; i++) {
        printf("hello world\n"); // 无限循环
    }
    return 0;
}
  • unsigned char范围为0~255,i++后始终≤255,循环永不结束。
相关推荐
鹿野素材屋36 分钟前
C#中对于List的多种排序方式
开发语言·c#
whxnchy40 分钟前
C++刷题 - 7.27
开发语言·c++
金融小师妹1 小时前
AI量化模型解析黄金3300关口博弈:市场聚焦“非农数据”的GRU-RNN混合架构推演
大数据·人工智能·算法
金融小师妹1 小时前
基于LSTM-GRU混合网络的动态解析:美联储维稳政策与黄金单日跌1.5%的非线性关联
大数据·人工智能·算法
白日梦想家-K1 小时前
题单【模拟与高精度】
开发语言·c++·算法
鹦鹉0072 小时前
IO流中的字节流
java·开发语言·后端
haaaaaaarry2 小时前
Element Plus常见基础组件(二)
开发语言·前端·javascript
AI_RSER2 小时前
第一篇:【Python-geemap教程(三)上】3D地形渲染与Landsat NDVI计算
开发语言·python·3d·信息可视化·遥感·gee
Hello_Embed2 小时前
嵌入式 C 语言入门:循环结构学习笔记 —— 从语法到实用技巧
c语言·笔记·stm32·学习