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


在C语言中,内存操作是编程的核心环节之一。四个常用的内存函数: memcpymemmovememsetmemcmp

内存函数

一、memcpy:内存复制函数

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

void * memcpy ( void * destination, const void * source, size_t num );

函数特点

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

使用示例

#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;
}

模拟实现

#include <assert.h>

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

注意

  • 通过将指针强制转换为char*,每次访问1字节,实现按字节操作,确保对任意类型的内存都能正确复制。
  • 循环内部如果想写成自增要这样写:((char*)dest)++;

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

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

void * memmove ( void * destination, const void * source, size_t num );

函数特点

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

使用示例

#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;
}

模拟实现

#include <assert.h>

char* my_memmove(void* dest, void* src, size_t num)
{
    assert(dest && src);
	char* ret = dest;
	if (dest < src)//从前向后
	{
		while (num--)
		{
			*(char*)dest = *(char*)src;
			((char*)dest)++;
			((char*)src)++;
		}
	}
	if (dest > src)//从后向前
	{
		while (num--)
		{
			*((char*)dest + num) = *((char*)src + num);
		}
	}
	return ret;
}
int main()
{
	int arr[20] = { 1,2,3,4,5,6,7,8,9,10 };
	char* ret = my_memmove(arr + 2, arr, 20);
	return 0;
}

在这里插入图片描述

注意

  • dest地址小于src时应该正向复制,dest地址大于src时应逆向赋值,避免覆盖未复制的数据

三、memset:内存设置函数

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

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

函数特点

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

使用示例

#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个字节,函数原型如下:

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

函数特点

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

使用示例

#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;
}
//输出:'DWgaOtP12df0' is greater than 'DWGAOTP12DF0'

说明:示例中因小写字母的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))。
  • 节省硬件:补码与原码的转换规则统一,无需额外电路。

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

在这里插入图片描述
存储0x11223344这样一个数据,我们调试打开内存发现:

  • 整数在内存中存储的是二进制补码,调试窗口为了方便展示转化为16进制的值
  • 存储的顺序是倒过来的

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

2.1 大小端的定义

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

2.2 为什么存在大小端?

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

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

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

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

方法1:指针法
#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:联合体法
#include <stdio.h>

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

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

举个例子:

#include <stdio.h>
int main()
{
 int n = 9;
 float *pFloat = (float *)&n;
 printf("n的值为:%d\n",n);
 printf("*pFloat的值为:%f\n",*pFloat);
 
 *pFloat = 9.0;
 printf("num的值为:%d\n",n);
 printf("*pFloat的值为:%f\n",*pFloat);
 return 0;
}

运行结果:在这里插入图片描述
以浮点型指针访问整数得到的结果不是9,以整型打印浮点数得到的结果不是9.0,由此说明浮点数和整数在内存中的存储不同

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

3.1 IEEE 754标准格式

任意二进制浮点数V可表示为: [ V = (-1)^S * M * 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 示例解析

#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:字符类型的符号影响

#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。
  • char是有符号(signed char)还是无符号(unsigned char)取决于编译器,在VS2022上是有符号的
  • %d打印有符号整数,把最高位当做符号位
  • %u %zd打印无符号整数,把最高位当做有效位

练习2:循环陷阱

#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,循环永不结束。

练习3

int main()
{
   char[1000];
   for(int i = 0 ; i < 1000 ; i++{
       a[i] = -1 - i;
     }//-1 -2 -3 -4 ... -128 127 126 125 ....4 3 2 1 0
   printf("%zd ",,strlen(a);//255
   return 0;
}

在这里插入图片描述

  • -128-1的值是127,继续减到0
  • strlen统计\0前的字符个数,字符个数是128+127=255

练习4

x86环境 小端字节序

#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", ptr1[-1], *ptr2);//4     2 00 00 00
 return 0;
}
  • 指针+1:取决于指针的类型
  • 整数+1 :直接+1
  • %x用于打印十六进制的整数
  • &a取出整个数组的地址,+1指向数组后一个地址ptr1[-1] == *(ptr1-1),向前跳过一个字节,指向4
  • 将a转化为整型再+1向后跳过一个字节,在x86,小端字节序的环境下,ptr2指向00 00 00 02再转化为02 00 00 00
    在这里插入图片描述
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值