在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>
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:处理重叠内存的复制函数
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>
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
类型。 - 返回值表示两个内存区域的大小关系:
<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);
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字节的数据(如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访问第一个字节
}
三、浮点数在内存中的存储
举个例子:
#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,由此说明浮点数和整数在内存中的存储不同
浮点数(float
、double
)的存储方式与整数完全不同,遵循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 存储与读取规则
存储过程:
- 规范化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); // 输出: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;
}
char
和signed 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
,继续减到0strlen
统计\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