【C语言】内存函数与数据在内存中的存储
文章目录
- 内存函数
- 一、memcpy:内存复制函数
- 函数特点
- 使用示例
- 模拟实现
- 二、memmove:处理重叠内存的复制函数
- 函数特点
- 使用示例
- 模拟实现
- 三、memset:内存设置函数
- 函数特点
- 使用示例
- 四、memcmp:内存比较函数
- 函数特点
- 使用示例
- 总结
- 数据在内存中的存储
- 一、整数在内存中的存储
- 1.1 原码、反码、补码的定义
- 1.2 为什么使用补码?
- 二、大小端字节序和字节序判断
- 2.1 大小端的定义
- 2.2 为什么存在大小端?
- 2.3 如何判断机器的字节序?
- 方法1:指针法
- 方法2:联合体法
- 三、浮点数在内存中的存储
- 3.1 IEEE 754标准格式
- 3.2 存储结构
- 3.3 存储与读取规则
- 存储过程:
- 读取过程:
- 3.4 示例解析
- 四、经典练习解析
- 练习1:字符类型的符号影响
- 练习2:循环陷阱
在C语言中,内存操作是编程的核心环节之一。本文将详细介绍四个常用的内存函数:
memcpy
、
memmove
、
memset
和
memcmp
,包括它们的使用方法、模拟实现以及适用场景,帮助你更好地理解和运用这些函数进行内存操作。
内存函数
一、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>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:处理重叠内存的复制函数
memmove
与memcpy
功能类似,但它支持源内存和目标内存重叠的场景,函数原型如下:
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>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
用于将指定内存区域的每个字节设置为指定值,函数原型如下:
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); // 输出:xxxxxxworldreturn 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
类型。 - 返回值表示两个内存区域的大小关系:
<0
:ptr1
的第一个不同字节小于ptr2
的对应字节;0
:两个内存区域的前num
个字节完全相同;>0
:ptr1
的第一个不同字节大于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);elseprintf("'%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字节的数据(如short
、int
),不同硬件架构对字节排列顺序有不同约定:
- 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访问第一个字节
}
三、浮点数在内存中的存储
浮点数(float
、double
)的存储方式与整数完全不同,遵循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 存储与读取规则
存储过程:
- 规范化M:M默认省略整数位1,仅存小数部分(如
1.001
存为001
),节省1位空间。 - 调整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); // 输出:9printf("*pFloat = %f\n", *pFloat); // 输出:0.000000*pFloat = 9.0;printf("n = %d\n", n); // 输出:1091567616printf("*pFloat = %f\n", *pFloat); // 输出:9.000000return 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=255return 0;
}
char
和signed char
:-1的补码为11111111
,打印时符号扩展为11111111 11111111 11111111 11111111
(仍为-1)。unsigned char
:-1的补码为11111111
,无符号解读为255。
练习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,循环永不结束。