目录
[1 · memcpy](#1 · memcpy)
[2 · memmove](#2 · memmove)
[3 · memset](#3 · memset)
[4 · memcmp](#4 · memcmp)
[5 · 大小端字节序和字节序判断](#5 · 大小端字节序和字节序判断)
[5 - 1 · 什么是大小端](#5 - 1 · 什么是大小端)
[5 - 2 · 为什么要有大小端](#5 - 2 · 为什么要有大小端)
[5 - 3 · 如何判断当前机器的字节序](#5 - 3 · 如何判断当前机器的字节序)
[6 · 浮点数在内存中的存储](#6 · 浮点数在内存中的存储)
[6 - 1 · 浮点数的存储](#6 - 1 · 浮点数的存储)
[6 - 2 · 浮点数存的过程](#6 - 2 · 浮点数存的过程)
[6 - 3 · 浮点数取的过程](#6 - 3 · 浮点数取的过程)
[6 - 4 · 解析代码](#6 - 4 · 解析代码)
上一篇我们介绍了关于字符串系列的函数,C语言也提供了内存操作函数,它们操作的是内存块,也就可以对任意类型的数据进行操作。
1 · memcpy
作用是针对内存块进行拷贝。使用需包含头文件 string.h
原型如下:
void * memcpy ( void * destination, const void * source, size_t num );
有三个参数,最后一个参数是 需要拷贝的字节数。
效果是将从 source 开始的 num 个字节的数据拷贝到 destination 中。
这个函数在遇到 '\0' 的时候并不会停下来
注意:source 和 destination 的数据不能有重叠,如果有重叠,结果是不保证一定正确的。对于有重叠的内存块,最好使用memmove。
下面我们模拟实现一下:
cpp
#include <stdio.h>
#include <string.h>
#include <assert.h>
void* MyMemcpy(void* dst, const void* src, size_t num)
{
assert(dst);
assert(src);
char* pdst = (char*)dst;
char* psrc = (char*)src;
while (num--)
{
//一次交换一个字节
*pdst = *psrc;
pdst++;
psrc++;
}
return dst;
}
int main()
{
int arr1[] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr1) / sizeof(arr1[0]);
int arr2[20] = { 0 };
int arr3[20] = { 0 };
int i = 0;
memcpy(arr2, arr1, 20);
printf("memcpy:");
for (i = 0; i < 10; i++)
{
printf("%d ", arr2[i]);
}
printf("\n");
MyMemcpy(arr3, arr1, 20);
printf("MyMemcpy:");
for (i = 0; i < 10; i++)
{
printf("%d ", arr3[i]);
}
return 0;
}
运行一下看看:

一个整型占4个字节,这里拷贝20个字节也就是5个整型数据。
由于不知道要拷贝的数据是什么类型的,所以进行一个字节一个字节拷贝。这里的思想其实与之前指针下那篇中优化冒泡排序类似。
2 · memmove
memmove 的作用和 memcpy 类似,只是memmove 处理的源内存块和目标内存块是可以重叠的。使用需包含头文件 string.h
原型如下:
void * memmove ( void * destination, const void * source, size_t num );
我们先来看看我们写的 MyMemcpy 拷贝重叠内存块会发生什么:
int main()
{
int arr1[] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr1) / sizeof(arr1[0]);
int i = 0;
MyMemcpy(arr1 + 2, arr1, 20);
for (i = 0; i < sz; i++)
{
printf("%d ", arr1[i]);
}
}
我们想将 arr1 的前5个元素 从arr1 的第3个元素开始放,那我们理想的效果是:
0 1 0 1 2 3 4 7 8 9
运行一下看看:

这是因为我们准备将第3个元素拷贝的时候,第3个元素已经被拷贝成 0 了。
而我们想将 arr1 的从第3个位置开始的5个元素从首元素开始放是没有问题的:
cpp
int main()
{
int arr1[] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr1) / sizeof(arr1[0]);
int i = 0;
MyMemcpy(arr1, arr1 + 2, 20);
for (i = 0; i < sz; i++)
{
printf("%d ", arr1[i]);
}
}
运行一下:

所以当无重叠或有重叠,但dst < src 时,我们的 MyMemcpy 是能够完成任务的
那么下面我们模拟实现一下 memmove:
cpp
#include <stdio.h>
#include <string.h>
#include <assert.h>
void* MyMemmove(void* dst, const void* src, size_t num)
{
assert(dst);
assert(src);
char* p1 = (char*)dst;
char* p2 = (char*)src;
if (dst < src || dst >(char*)src + num)
{
//从前往后(低地址到高地址)拷贝
while (num--)
{
*p1 = *p2;
p1++;
p2++;
}
}
else
{
//从后往前(高地址到低地址)拷贝
p1 = p1 + num - 1;
p2 = p2 + num - 1;
while (num--)
{
*p1 = *p2;
p1--;
p2--;
}
}
return dst;
}
int main()
{
int arr[] = { 0,1,2,3,4,5,6,7,8,9 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
MyMemmove(arr + 2, arr, 20);
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
运行一下:

当无重叠或有重叠,但dst < src 时 我们 从前往后(低地址到高地址)拷贝
当有重叠 且 dst > src 时,从前往后拷贝就会出问题,所以我们可以从后往前(高地址到低地址)拷贝,这样是不会出问题的,因为 src 不会走到已被拷贝过的内存块。
注意:我们从后往前拷贝时, p1 p2 + num 后要 -1。
拿上面的例子来说,我们是整型数组,一个元素占4个字节,p1 指向第3个元素的首字节(第一个字节),p2 指向第1个元素的首字节(第一个字节),而p1 和 p2 都 + num 后,它们分别指向第8个元素的首字节和第6个元素的首字节,而我们要从后向前拷贝,所以要让 p1 指向第7个元素的尾字节(第四个字节) p2 指向第5个元素的尾字节。
3 · memset
memset是用来设置内存的,将内存中的值以字节为单位设置成想要的内容。使用需包含头文件 string.h
原型如下:
cpp
void * memset ( void * ptr, int value, size_t num );
效果是 将从ptr 开始的num个字节改成 value
我们使用一下看看:
cpp
#include <stdio.h>
#include <string.h>
int main()
{
char arr[] = { "hello world" };
memset(arr, 'x', 5);
printf("%s", arr);
return 0;
}
运行一下看看:

这里将前五个字符改成了 x ,如果想从第三个开始改,第一个参数就可以写 arr+2。
那么如果是整型数据,我们想将一个初始化为0的整型数组的前五个元素设置成1,是不是只需要memset(arr,1,20)呢
试一试:
cpp
int main()
{
int arr[10] = { 0 };
int sz = sizeof(arr) / sizeof(arr[0]);
int i = 0;
memset(arr, 1, 20);
for (i = 0; i < sz; i++)
{
printf("%d ", arr[i]);
}
return 0;
}
运行一下:

可以看到,结果并不是我们预期的那样,这是因为memset 是以字节为单位进行设置的,20个字节全部设置成了1。可以通过调试看看:

一个整型占4字节,每个字节都设置为 1 那么这个16进制数 0x01010101 就是 16843009
所以我们在使用 memset 时要注意是否能按照预期那样进行设置。
4 · memcmp
memcmp 是用来进行内存块比较的。使用需包含头文件 string.h
原型如下:
cpp
int memcmp ( const void * ptr1, const void * ptr2, size_t num );
比较 从ptr1 和 ptr2 指向的位置开始 往后的 num个字节。
如果前者大,返回一个大于0的值
如果两者相等,返回0
如果前者小,返回一个小于0的值
我们使用一下:
cpp
#include <stdio.h>
#include <string.h>
int main()
{
int arr1[] = { 1,2,3,4,5,6,7,8,9 };
int arr2[] = { 1,2,3,4,5,9,9,9,9 };
int ret = memcmp(arr1, arr2, 20);
printf("第一次比较:%d\n", ret);
ret = memcmp(arr1, arr2, 21);
printf("第二次比较:%d", ret);
return 0;
}
运行一下:

第21个字节的比较其实是 05 和 08 的比较


我们正常的 16 进制表示5 和 8 应该是 0x 00 00 00 05 和 0x 00 00 00 08,VS中给人一种反过来放的感觉,这其实就与数据在内存中的存储有关了。
5 · 大小端字节序和字节序判断
我们在详解操作符那篇中介绍了 整数在内存中的存储存的是补码
上面我们通过内存看了 5 和 8 ,下面我们看个更加清晰的 0x 11 22 33 44 :

可以看到,是按照字节为单位,倒着存储的。为什么呢?这就涉及到大小端了。
5 - 1 · 什么是大小端
其实超过⼀个字节的数据在内存中存储的时候,就有存储顺序的问题,按照不同的存储顺序,我们分为大端字节序存储和小端字节序存储。
字节序指的是以字节为单位
下面是具体概念:
大端(存储)模式:
是指数据的低位字节内容保存在内存的高地址处,而数据的高位字节内容,保存在内存的低地址处。
小端(存储)模式:
是指数据的低位字节内容保存在内存的低地址处,而数据的高位字节内容,保存在内存的高地址处。
低位字节可以当作是看整数的个位
0x 11 22 33 44 中,44就是低位字节。

那么我们就可以知道,上面我们演示用的 VS2022 是小端模式
5 - 2 · 为什么要有大小端
其实只要保证是怎么样放进内存的,并且能按照正确的顺序取出来,都是可行的。
比如将 0x11223344
可以存成 22 11 44 33 或者 33 11 44 22 等等,只要能正确取出来,理论上都是可行的。但是这样的写法肉眼可见的混乱,取出来感觉十分变扭,所以最终只有两种存储方式,大端和小端。
为什么会有大小端模式之分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着⼀个字节,⼀个字节为8bit 位,但是在C语言中除了8 bit 的 char 之外,还有16 bit 的 short 型,32 bit 的 long 型(要看
具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于⼀个字节,那么必然存在着⼀个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
5 - 3 · 如何判断当前机器的字节序
我们可以写一段代码,来判断当前的字节序是大端还是小端:
cpp
#include <stdio.h>
int check()
{
int a = 1;
return *(char*)&a;
}
int main()
{
int ret = check();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
return 0;
}
将整型变量a 初始化为1 内存中放的是 0x 00 00 00 01,如果是小端,那么01就会放到低地址处,如果是大端,01就会放到高地址处。所以此时访问 a 所占的地址的首字节位置,如果访问到的是1,说明1被存到了低地址,此时机器是小端。如果访问到的是0,说明1不在低地址,那么此时就是大端。
6 · 浮点数在内存中的存储
常见的浮点数:3.14159、1E10等,浮点数家族包括: float 、 double 、 long double 类型。
我们先来看一段代码:
cpp
#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;
}
运行一下看看:

有的结果可能与我们预想中有些出入,那么这也说明整数和浮点数在内存中的存储方式是不一样的。
要理解这个结果,我们就要搞懂浮点数在内存中的存储。
6 - 1 · 浮点数的存储
存储到内存中的是二进制浮点数。
根据国际标准IEEE(电气和电子工程协会) 754,任意⼀个⼆进制浮点数V可以表示成下面的形式:
V = (−1)^ S * M * 2^ E
(−1)^S表示符号位,当S=0,V为正数;当S=1,V为负数
M 表示有效数字,M是大于等于1,小于2的
2^E表示指数位
所以浮点数的存储,其实就是存储 S M E 相关的值
举个栗子:
十进制的 5.5,转换成二进制是 101.1
转换成科学计数法,小数点要前移两位,所以 101.1 相当于 1.011 * 2^2
其中 S==0,M==1.011,E==2
IEEE 754规定:
对于32位的浮点数(float),最高的1位存储符号位S,接着的8位存储指数E,剩下的23位存储有效数字M
对于64位的浮点数(double),最高的1位存储符号位S,接着的11位存储指数E,剩下的52位存储有效数字M
float 内存分配:

double 内存分配:

6 - 2 · 浮点数存的过程
IEEE 754 对有效数字M和指数E,还有⼀些特别规定。
对于M:
前面说过, 1 ≤ M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中 xxxxxx 表示小数部分。
IEEE 754 规定,在计算机内部保存M时,默认这个数的第⼀位总是1,因此可以被舍去,只保存后⾯的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第⼀位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第⼀位的1舍去以后,等于可以保存24位有效数字。
对于E:
E为⼀个⽆符号整数
这意味着,如果E为8位,它的取值范围为0~255;如果E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存⼊内存时E的真实值必须再加上⼀个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。
比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
6 - 3 · 浮点数取的过程
简单来说:怎么存的就怎么取出来
其中:指数E从内存中取出还可以再分成三种情况
- E不全为0或不全为1
这时指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第⼀位的1。
比如:
0.5 的⼆进制形式为0.1,由于规定正数部分必须大于1,即将小数点右移1位,则为1.0*2^(-1),其
E的计算值为-1+127(中间值)=126,表示为01111110,而尾数1.0去掉整数部分为0,补齐0到23位
00000000000000000000000,则其⼆进制表示形式为:
0 01111110 00000000000000000000000
2.E为全0
我们存进去的E是真实值还要加上一个中间值的,此时E的计算值为0,说明E是-127,代表这是一个极小的接近于0的数。
这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第⼀位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
3.E为全1
对于32位浮点数来说,有8位存储E,此时E的计算值为255,那么真实值就为128,这说明此时是一个极大值
这时,如果有效数字M全为0,表示±⽆穷大(正负取决于符号位s)
6 - 4 · 解析代码
那么此时回到我们的代码
cpp
#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 以整型的形式存储,为
00000000000000000000000000001001
那么将这个二进制序列当作浮点数
S==0,E==00000000,M==00000000000000000001001
此时E全为0
那么按照计算:
V=(-1)^0 × 0.00000000000000000001001×2^(-126)=1.001×2^(-146)
显然,V是⼀个很小的接近于0的正数,所以用十进制小数表示就是0.000000
然后 9.0 以浮点数的形式存储
转换为二进制是:1001.0
科学计数法就是 1.001 * 2^3
那么存储的S为0,E为3+127==130,M为001后加20个0,二进制序列为
01000001000100000000000000000000
这个二进制被当作整数解析就是1091567616
总结
以上简单介绍了内存函数以及数据在内存中的存储相关内容,关于C语言的其余内容,请期待后续更新
以上内容如有错误或不准确之处,欢迎指出,或者你有更好的想法,也欢迎交流。