一 .内存函数
1.memcpy(内存拷贝函数)的使用和模拟实现
void * memcpy(void * dest , const void * src , size_t num);
(1)函数memcpy从src的空间位置开始复制num个字节的数据到dest指定的内存位置。
(2)这个函数遇到‘\0’不会停下来。
(3)如果src和dest有任何的重叠,那么拷贝结果都是未定义的。
我们来看看这个函数的使用:
#include <stdio.h>
#include <sting.h>
//使用memcpy函数
int main()
{
int a1[] = { 1,2,3,4,5 };
int a2[20] = { 0 };
memcpy(a2, a1, sizeof(int) * 5);
for(int i=0;i<5;i++)
printf("%d ", a2[i]);//1 2 3 4 5
return 0;
}
这个函数的使用还得包含<string.h>这个头文件
并且最重要的一点是:dest目标必须有足够大的内存来存放将要放进去的空间大小!!!
模拟实现memcpy:
#include <stdio.h>
#include <sting.h>
void* my_memcpy(void* dest,const void* src,size_t num)
{
void* ret = dest;
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
return ret;
}
int main()
{
int a1[] = { 1,2,3,4,5 };
int a2[10] = { 0 };
int* ret=(int*)my_memcpy(a2, a1, sizeof(int) * 5);
for (int i = 0; i < 5; i++)
{
printf("%d ", ret[i]); //1 2 3 4 5
}
return 0;
}
这个函数的模拟实现也不是太难,我们传入参数以后,用num作为循环结束条件,然后将源头指针和目的地指针强转成char*类型,这样是为了方便拷贝各种类型的数据,因为char*每次走一个字节,这样可以兼容很多类型,而转换成别的类型就不方便了
返回的时候因为dest已经在循环中指向别的地址了,我们要返回的是dest的初始地址,所以应在循环开始之前就找到初始地址,在整个函数结束后,将初始地址ret返回,我们也可以将返回的ret强转成int*类型,便于打印,因为void*指针不能被用于类似解引用和加减的这种操作。
2.memmove(可拷贝重叠的内存)的使用和模拟实现
void * memmove(void * dest , const void * src , size_t num);
这个函数和memcpy相似,有一点差别就是memmove可以拷贝源内存块和目标内存块重复的情况
所以遇到要拷贝重复的内存空间就用memmove这个函数!
memmove的使用:
#include <stdio.h>
#include <string.h>
//memmove的使用
int main()
{
int a1[] = { 1,2,3,4,5,6,7,8,9,10 };
memmove(a1 + 2, a1, sizeof(int) * 5);
for (int i = 0; i < 10; i++)
{
printf("%d ", a1[i]);//1 2 1 2 3 4 5 8 9 10
}
return 0;
}
memmove模拟实现:
#include <stdio.h>
#include <string.h>
//模拟实现memmove函数
void* my_memmove(void* dest, const void* src, size_t num)
{
void* ret = dest;
if (dest < src)
{
while (num--)
{
*(char*)dest = *(char*)src;
dest = (char*)dest + 1;
src = (char*)src + 1;
}
}
else
{
while (num--)
{
*((char*)dest+num) = *((char*)src+num);
}
}
return ret;
}
int main()
{
int a1[] = { 1,2,3,4,5,6,7,8,9,10 };
int* ret=(int *)my_memmove(a1+2, a1, sizeof(int) * 5);
for (int i = 0; i < 10; i++)
{
printf("%d ", a1[i]);//1 2 1 2 3 4 5 8 9 10
}
return 0;
}
这个函数的模拟有点难想,但想通了以后也很简单就实现了
如果有重复的空间的话,其实是分两种情况的,一种是dest大于src,一种是dest小于src
我们先画图在讲解:
第一种情况:dest大于src的时候,这时正常的拷贝已经不能满足需求了,因为从1开始拷贝的话,到2拷贝完的时候就会覆盖掉后面的空间,所以我们可以换一种思路,从src的最后面也就是5开始,向dest的最后面也就是7开始拷贝,这样就不会覆盖掉原有空间,并且还能正常拷贝。
第二种情况:dest小于src的时候,这时就像第一种情况一样也不行了,一个反着来,从src的第一块空间也就是3开始,拷贝到dest的起始地址,这个好像跟memcpy的方法又一样了哈哈。
然后代码在实现的时候要找到源内存和目标内存的末尾地址就需要起始地址的指针+num在解引用就可以啦!
3.memset(申请内存空间)函数的使用
void* memset(void * ptr , int value , size_t num);
其中ptr是指向要设置内存的空间的起始地址 ,value是要设置成什么样的值,num是字节个个数
memset是用来设置内存的,将内存中的值以字节为单位设置成想要的内容。
使用memset也需要包含头文件<string.h>
下面我们来看memset的使用
这是设置字符型内存时:
#include <stdio.h>
#include <string.h>
//memset的使用
int main()
{
char a[] = "hello world";
memset(a,'w',5);
printf("%s", a); // wwwww world
return 0;
}
那么能不能设置整形空间呢?
看代码:
#include <stdio.h>
#include <string.h>
//memset的使用
int main()
{
int a[] = { 1,2,3,4,5 };
memset(a,1,20);
int i = 0;
for (i = 0; i < 5; i++)
{
printf("%d ", a[i]); //16843009 16843009 16843009 16843009 16843009
}
return 0;
}
那么为什么结果会变成这么大的数字呢?我们设置的可是1啊,接下来看看内存里面是什么情况吧
原来是因为memset是按字节设置的,所以把每个字节都变成了1,四个字节的1合起来变成了一个超大的整形,所以变成了这样的结果
因此我们得知,memset是按照字节来设置空间内存的。
4.memcmp(内存空间比较)函数的使用
int memcmp(const void * ptr1 , const void * ptr2 , size_t num);
从ptr1和ptr2指向的位置开始比较num个字节
但其实num是最大的比较字节,如果在num之前就已经比较出大小,就不会在往后继续比了
返回值如下:
要是ptr1指向的地址和ptr2比较,没有ptr2大的话,返回一个小于0的数字。
要是ptr1指向的地址和ptr2比较,和ptr2一样大的话,返回0。
要是ptr1指向的地址和ptr2比较,比ptr2大的话,返回一个大于0的数字。
来看图:
我们可以看到图中明显是arr2大,所以arr1与arr2比较,小于arr2,最后返回小于0的数字。
二.数据在内存中的存储
1.整数在内存中的存储
我们都知道整数的二进制表示方法有三种:原码,反码和补码。
而对整形来说,在二进制中存的是补码
原因在于使用补码可以将符号位和数值域统一处理,同时,加法和减法也可以统一处理
并且补码和原码可以相互转换,不需要额外的硬件电路
2.大小端字节序和判断
超过一个字节的数据在内存中存储的时候就有存储顺序的问题,按照不同的存储顺序,我们分为大端字节序和小端字节序
大端字节序:是指数据的低位字节内容保存在内存的高位地址处,而数据的高位字节内容保存在内存的低位地址处
小端字节序:是指数据的高位字节内容保存在内存的高位地址处,而数据的低位字节内容保存在内存的低位地址处
我们可以这样判断数据的高低位,按数学上的个十百千来看,比如:0x11223344 里面44就属于低位,因为看做一个数字的话,最高位是11
那么为什么要有大小端呢?
对于位数大于8位的处理器,例如16或者32位的处理器,由于寄存器宽度大于一个字节,那么就必然存在着一个如何将多个字节安排的问题,因此就导致了大小端模式
那么我们来设置一个小程序判断所在处理器是大端还是小端吧
看代码:
#include <stdio.h>
int check_dx()
{
int a = 1;
if (*(char*)&a == 1)
{
return 1;// 小端
}
else
{
return 0; //大端
}
}
int main()
{
int ret=check_dx();
printf("%d ", ret);
}
我们这个代码以1放在内存中的位置为例判断所在处理器是大端还是小端,最后得出结果为1也就是小端,那么代码里的方法是什么呢?
就是取出a的地址然后char*访问一个字节,就找到最低位里放的是1还是0,如果是1,说明最低位放在了最低位,则是小端,为0则是大端。
我们来仔细观察一下内存:
如图可见,01放在了最低位,所以是小端字节序
下面来看一些数据存放的例子吧:
1.
#include <stdio.h>
int main()
{
char a = -1;
signed char b = -1;
unsigned char c = -1;
printf("%d %d %d", a, b, c);// -1 -1 255
1000000000000000000000000000000000000001 //原
1111111111111111111111111111111111111110 //反
1111111111111111111111111111111111111111 //补
11111111
1111111111111111111111111111111111111111 //整形提升
1000000000000000000000000000000000000000 //反
1000000000000000000000000000000000000001 //补 -1
/* 10000000000000000000000000000001
11111111111111111111111111111110
11111111111111111111111111111111
11111111
00000000000000000000000011111111
//看变量类型整形提升 有符号类型补符号位,无符号补0
*/
}
前两个变量都是有符号char,所以存进-1以后拿到11111111,然后整形提升补符号位,最后得到-1的补码,因为是%d打印,所以还得继续原反补,最后打印出-1
而最后一个无符号char c就不一样了,它刚开始截断以后存进变量里面的是11111111,但是因为是无符号char,所以得补0,打印的时候把补位的0默认为正数,所以打印出255
2.
#include <stdio.h>
int main()
{
char a = -128;
printf("%u ", a); //
/*10000000000000000000000010000000 原
11111111111111111111111101111111 反
11111111111111111111111110000000 补
10000000
11111111111111111111111110000000 整形提升
*/
return 0;
}
这次是无符号打印,结果可大不一样了
截断以后存进去的是10000000,整形提升以后,因为是无符号打印,所以不用原反补,直接打印出结果,为4,294,967,168,
3. 下面再来看看关于取值范围的一道题:
#include <stdio.h>
#include <string.h>
int main()
{
//char 取值范围为-128 - 127
char a[1000];
for (int i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a)); //255
}
char里面最大存的数字是127,最小为-128,所以循环里面从-1开始一直到-128,然后就变成了127,在到0正好是255个数字,如图:
这也告诉我们每个类型的数据都有自己的取值范围,我整理了一下常用的类型放在图片中,以便大家查阅:
加油,知识就是这样一点一滴积累起来的,现在的辛苦是为了以后的惬意!!!