C语言 — 内存函数和数据的存储
1.memcpy函数
1.1memcpy的使用
memcpy函数是将指定字节的源头数据拷贝到目标数据上,第一个参数是目标数据的起始地址,第二个参数是源头数据的起始地址,第三个数据是需要拷贝的字节个数,返回类型是void*的指针。
给定两个整型数据,将arr2数组的数据拷贝到arr1数组上;
#include<string.h>
int main()
{int arr1[10] = { 0 };int arr2[10] = { 1,2,3,4,5,6,7,8,9,10 };//将arr2 拷贝到 arr1上,拷贝个数是 整个arr2数组的大小int* pa = (int*)memcpy(arr1, arr2, sizeof(arr2));//memcpy的返回类型强制类型转换为int*类型的指针,访问//整型数组的数据return 0;
}
按F10打开启动调试,打开调试窗口观察观察拷贝前后的数据,拷贝前的数据arr1全是0;
拷贝后arr1数组的内容与arr2数组的内容一致。
1.2 memcpy的模拟实现
需要注意的是,不能直接将强制类型转换后的指针进行后置++操作,不然编译器会报警告,因为强制类型转换是临时的,转换成功后只能使用一次,使用后指针就转换为原有类型,所以强制类型转换使用指针后,需要使指针向后指向新的地址,可以使强制类型转换后的指针+1,然后赋给原来指向的指针,即dest = (char*) dest + 1.
//memcpy的模拟实现
#include<stdio.h>
#include<assert.h>
//返回类型void* 目标数据指针dest
//源头数据指针src 拷贝个数num,单位字节
void* my_memcpy(void* dest, const void* src,size_t num)
{void* tem = dest;//存放目标空间的起始地址assert(dest && src);//dest 和 src 不为空指针//一对字节一对字节拷贝,使用while循环while (num--)//num为后置--,循环次数刚好为拷贝次数{//因为dest 和 src是void*类型,所以需要先强制类型转换为char**(char*)dest = *(char*)src;//首次拷贝//拷贝后,指针指向下一个字节的地址//因为强制类型转换是临时的,所以不能强制类型转换后使用后置++//即(char*)dest++,可以使用赋值//指向下一字节的地址dest = (char*)dest + 1;src = (char*)src + 1;}return tem;//返回起始地址
}
使用模拟实现的memcpy函数拷贝数据
int main()
{double a[] = { 99.0,85.5,90.00 };double b[] = { 0,0,0 };//将a数组的数据拷贝到b数组中double* pd = (double*)my_memcpy(b, a, sizeof(a));//输出拷贝后的b数组for (int i = 0; i < sizeof(b)/sizeof(b[0]); i++){printf("%.2lf ", pd[i]);//保留小数点后两位}return 0;
}
2.memmove函数
2.1 memmove函数的使用
memmove函数是将源头数据拷贝到目标数据中,第一个参数是目标数据的起始地址,第二个参数是源头数据的起始地址,第三个参数是拷贝的字节个数,返回类型是void*;memmove函数拷贝的可以是有重叠的内存空间,如在同一个数组中拷贝。
给定一个数组,将数据第三个元素起的5个元素拷贝到数组前5个元素的位置。
//mememove函数的使用
#include<string.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };//将第3个元素起的5个元素拷贝到数组前五个元素memmove(arr, arr + 2, sizeof(arr[0]) * 5);//arr是起始位置的地址,arr+2是第三个元素的地址//sizeof(arr[0])是一个数据的大小,*5是5个数据的大小return 0;
}
按F10启动调试,打开监视窗口观察拷贝前后数组arr的数据情况
2.2 memmove函数的模拟实现
第一种情况:目标数据的起始地址在源头数据地址后面,需要从后往前拷贝;
假设数据的数据是1-10,拷贝5个元素大小,如果从前往后拷贝,预期拷贝的结果是1 2 3 1 2 3 4 5 9 10,但是实际上拷贝的是1 2 3 1 2 3 1 2 9 10,这是因为目标空间的 4 和 5 被源头数据覆盖变成 1 和 2,导致最后两个元素拷贝将 1 和 2 拷贝给目标空间。
第二种情况:目标数据的起始地址 dest 在源头数据的起始地址 src 前面,需要从前往后拷贝;
假设数组元素是1 - 10,拷贝4个元素,将第二元素起的4个元素拷贝到数组前4个元素,如果是从后往前拷贝,预期拷贝的数据是 3 4 5 6 5 6 7 8 9 10,实际上拷贝的结果是5 6 5 6 5 6 7 8 9 10,这是因为,目标数据的后两个元素被源头数据的后两个元素覆盖,当源头数据使用前两个元素时,此时的3 和 4 数据已经被拷贝成 5 和 6 ,导致目标数据的前两个元素也是拷贝5 和 6 。
//memmove的模拟实现
#include<stdio.h>
#include<assert.h>
void* my_memmove(void* dest, void* src, size_t num)
{void* tem = dest;//存放目标数据的起始地址assert(dest && src);//dest和src不为空指针//第一种情况 ;src < dest 从后往前拷贝if (src < dest){while (num--)//拷贝次数{//将dest 和 src的类型先强制类型转换为char*//因为需要从后往前拷贝,因此需要指向最后一个字节//+num刚刚好指向最后一个字节,对其解引用后赋值*((char*)dest + num) = *((char*)src + num);}}//第二种情况,从前往后拷贝else {while (num--)//拷贝次数{//强制类型转换后赋值*(char*)dest = *(char*)src;//指向下一字节dest = (char*)dest + 1;src = (char*)src + 1;}}return tem;//返回目标数据起始地址
}
}
使用模拟实现的memmove函数,分别测试从后向前拷贝和从前先后拷贝;
int main()
{int arr1[8] = { 1,2,3,4,5,6,7,8 };int arr2[6] = { 2,3,4,5,6,7 };//第一种情况:src < dest 从后往前拷贝//拷贝3个数据int* p1 = (int*)my_memmove(arr1+2, arr1, 12);//第二种情况,src > dest 从前往后拷贝//拷贝4个数据int* p2 = (int*)my_memmove(arr2, arr2 + 2, 16);int len1 = sizeof(arr1) / sizeof(arr1[0]);int len2 = sizeof(arr2) / sizeof(arr2[0]);//输出for (int i = 0; i < len1; i++){printf("%d ", arr1[i]);//p1是arr1+2的地址}//此处是遍历数组打印,所以使用arr1[i]printf("\n");//换行for (int i = 0; i < len2; i++){printf("%d ", p2[i]);//p2是arr2的首元素地址}return 0;
}
3.memset函数
3.1memset函数的使用
memset函数是将指定个数的元素拷贝到指定位置;第一个参数是需要拷贝内存块的起始地址,第二个参数是拷贝元素的值,第三个参数是拷贝给个数,单位字节,返回类型是void*。
//memstet函数的使用
#include<stdio.h>
#include<string.h>
int main()
{char ch[] = "Hello World";//将Hello改为xxxxxmemset(ch, 'x', 5);//ch是首元素地址//'x'是将ASCII码值进行传递//5是改变的字节个数return 0;
}
按F10启动调试,打开监视窗口,观察memset使用前后数组ch的元素。
3.2 memset的模拟实现
//memset的模拟实现
#include<assert.h>
void* my_memset(void* ptr, int value, size_t num)
{void* tem = ptr;assert(ptr != NULL);//指针不为空//拷贝while (num--){//强制类型转换*(char*)ptr = (unsigned char)value;ptr = (char*)ptr + 1;}return tem;//返回起始地址}
使用模拟实现的memset函数设置内存空间,将一个字符数组内容全部设置为a.
#include<stdio.h>
int main()
{char ch[10];//创建数组//设置数组内容char* ptr =(int*)my_memset(ch, 97, sizeof(ch));//输出for (int i = 0; i < sizeof(ch) / sizeof(ch[0]); i++){printf("%c ", ptr[i]);}return 0;
}
4.memcmp函数
4.1 memcmp函数的使用
memcmp函数是用于比较两块内存块的大小;第一个参数是第一块内存块的起始地址,第二个参数是第二块内存块的起始地址,第三个参数是比较的字节个数,返回类型为int。
//memcmp的使用
#include<string.h>
#include<stdio.h>
int main()
{char a[] = "abababcabcf";char b[] = "abababcabce";int tem = memcmp(a, b,strlen(a));//比较大小if (tem > 0)printf(">");else if (tem < 0)printf("<");elseprintf("==");return 0;
}
4.2 memcpy的模拟实现
//memcmp模拟实现
#include<assert.h>
int my_memcmp(void* ptr1, void* ptr2, size_t num)
{assert(ptr1 && ptr2);while (num--){if (*(char*)ptr1 == *(char*)ptr2){ptr1 = (char*)ptr1 + 1;ptr2 = (char*)ptr2 + 1;}//不相等直接返回做差的值elsereturn *(char*)ptr1 - *(char*)ptr2;}//此时已经将全部字节比较,返回0if (num == 0)return 0;
}
使用模拟实现的memcmp函数比较两个整型数组的大小。
#include<stdio.h>
int main()
{int a[] = { 1,2,3 };int b[] = { 1.2,2 };int r = my_memcmp(a, b, 12);if (r > 0)printf(">");else if (r < 0)printf("<");elseprintf("==");return 0;
}
5. 整型数据在内存的存储
整型数据在内存中存储的是二进制序列的补码,补码是通过原码和反码转换后得来的。
正数的整型数据的原码,补码,反码是一样的。
原码:整形数据的二进制序列;
反码:与原码相同;
补码:与原码相同;
负数的整型数据的原码,补码,反码。
原码: 整型数据的二进制序列;
补码:符号位不变,其它位按位取反;
补码:补码加一。
例如 1 和 -1的补码。
1的原码:00000000 00000000 00000000 00000001
1的反码:00000000 00000000 00000000 00000001
1的补码:00000000 00000000 00000000 00000001
在内存中存储的是十六进制的序列,每4个二进制位转换一个16进制位
转换后:00 00 00 01 或者 01 00 00 00
第一种情况是小端存储,第二种情况是大端存储-1的原码: 10000000 00000000 00000000 00000001
-1的反码:11111111 11111111 11111111 11111110(符号位是首位不变,其它位是1变0,是0变1)
-1的补码:11111111 11111111 11111111 11111111(反码+1)
在内存的存储:FF FF FF FF 或者 ff ff ff ff(大小写取决于编译器)
6. 大小端存储
大端存储是将高字节位的数据存放在低地址处,将低字节位的数据存储在高地址处;
小端存储是将低字节位的数据存储在低地址处,将高字节位的数据存储在高地址处;
判断当前程序是大端还是小端存储可以使用以下程序。
#include<stdio.h>//整型数据在内存的存储
//判断是大端还是小端
int check_sys()
{int tem = 1;//小端:01 00 00 00//大端:00 00 00 01//对比首字节即可return *((char*)&tem);//取出tem的地址,强制类型转换//位char*,解引用操作访问第一//个字节,将得到的结果返回
}int main()
{int r = check_sys();if (r == 1)printf("小端");elseprintf("大端");return 0;
}
当需要观察多个数据时,可以按F10启动调试,打开内存窗口,在搜索栏取出观察元素的地址
#include<stdio.h>
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };return 0;
}
以下程序运行的结果是什么?
#include <stdio.h>
//X86环境 ⼩端字节序
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);
return 0;
}
&a取出的是整个数组的地址,+1跳过整个数组,指向数组后面的地址,ptr1[-1]等价于*(ptr1-1),指向第四个元素的地址,%x是按照16进制输出,所以输出00 00 00 04,VS编译器默认首位不为0输出打印,所以输出4;a是数组名表示首元素地址,将其强制类型转换为int类型,即转换为一个整数,+1相当于整数的加法,假设此时地址为0x 00 00 00 10,+1后地址为0x 00 00 00 11,将此地址强制类型转换为int*类型的指针,相当于在原有的地址出跳过一个字节,指向新的地址,将此地址赋给ptr2的指针变量,解引用操作时可以访问4个字节,即 00 00 00 20,按照小端存储的方式存放,按照%x输出打印
02 00 00 00,首位0去除,2 00 00 00;
7. 整型数据存储范围
char类型的数据有8个bit位,取值范围:-128 - 127 (- 2^7 - 2^7 -1)
unsigned char 的取值范围:0 - 255 (127+128)
short类型的数据又16个bit位,取值范围:-32768 - 32767
unsigned short 的取值范围:0 - 65535 (32768 + 32767)
int类型的数据有16个bit位,取值范围:-2147483648 - 2147483647
unsigned int的取值范围:0 - 4294967295
long类型的数据有16个bit位,取值范围:-2147483648 - 2147483647
unsigned long的取值范围:0 - 4294967295
long long类型的数据有16个bit位,取值范围:-9223372036854775808 - 9223372036854775807
unsigned int的取值范围:0 - 18446744073709551615
7.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);
return 0;
}
-1的补码:11111111 11111111 11111111 11111111
char类型的数据只能存储8个bit位,存储最后8位
a存储的二进制序列补码:11111111
b的类型与a的默认类型一样
b存储的二进制序列补码:11111111
c存储的二进制序列补码:11111111
输出打印:
%d是将数据通过整数的形式打印
a数据是char类型,需要先进行整型提升,char类型默认是有符号的
首位是符号位,默认补一
提升后:11111111 111111111 11111111 11111111(补码)
%d输出的是原码
取反:10000000 00000000 00000000 00000000
+1 : 10000000 00000000 00000000 00000001(-1)的原码
所以第一个输出的是 a = -1;
由于b和a的类型相同,输出b = -1;
c的类型是unsigned char类型,需要进行整型提升
unsigned char类型默认是无符号整型,提升时补0
提升后:00000000 000000000 00000000 11111111(原 反 补 相同)
输出:c = 255
7.2 以下程序输出的结果是什么?
#include <stdio.h>
#include <string.h>
int main()
{char a[666];int i;for (i = 0; i < 666; i++){a[i] = -1 - i;}printf("%d", strlen(a));return 0;
}
char类型数据的取值范围是-128 - 127,-1 - i 的值是整型的,但是赋给char类型的a数组元素只能存储8个bit位,a = -128时,即i = 127,a数组前128个元素存放的数据是-1 到 -128,i = 128时,
arr[i] = - 129;8位的二进制原码:10000001 -> 01111110(反码)-> 01111111(127的补码),以此类推可以 i = -129时,arr[i] = 126,直到arr[ i ] = 0 时(i =255),第256个元素存放0,strlen函数是求\0(0)前面的元素个数,有255个(-1到-128有128个,127 -1 有127个),输出255.
7.3 以下程序的输出结果是什么?
#include<stdio.h>
int main()
{unsigned int i = 0;for (i = 0; i >= 0; i--){printf("Hello!\n");}return 0;
}
因为unsigned int 类型的取值范围是0 - 2^32 -1, i >= 0的条件永远成立,导致死循环。
8. 浮点数的存储
根据国际标准IEEE(电⽓和电⼦⼯程协会) 754,任意⼀个⼆进制浮点数V可以表⽰成下⾯的形式:
V = (−1)^S ∗ M ∗ 2^E
S 表⽰符号位,当S=0,V为正数;当S=1,V为负数
M 表⽰有效数字,M是⼤于等于1,⼩于2的
E 表⽰指数位
float类型的数据有32个bit位,存放规则如下:
double类型的数据有64个bit位,首位存放S的值(0 或者 1),后面的11个bit位存放E的值,剩下的bit位存放M的值;
浮点数的数值位
5.0按照转换规则是V = (-1)^ 0 * 1.01 * 2^2 ,S = 0, E = 2, M = 1.01;
5的二进制序列是101,小数点后是0,不需要转换,因为是正数,S =0,M是小于2大于1的小数,
将5的二进制序列写成小数的形式是1.01 * 2^2(跟十进制类似,*10进一位小数,二进制是 *2进一位小数点),因此E是2,M是1.01。
-0.625按照转换规则是V = (-1)^ 1 * 1.01 * 2^-1 ,S =1,E = 0,M = 1.01;
小数点前是0,二进制序列是0(简写),0.625 = 2^(-1) + 2^(-3) = 101, 0.625写成二进制序列
0101,首位是符号位,S =1,0101写成小数形式是1.01 * 2(-1),所以E = -1,M =1.01。
在实际存储中会将E的值加上中间数后进行存储,E是8位是会将E的值加上127后进行存储,如果是11位会加上中间数1023后进行存储;对于M的存储,因为M的取值范围是1 -2的小数,可以将小数点后面的数转换位二进制序列进行存储,小数点前的1可以不存储,转换时再补充即可,这样就可以多出一个bit位进行存储,提高精度;
以5.0为例子,观察其内存存储的二进制序列
//5.0的内存的存储序列
#include<stdio.h>
int main()
{float f = 5.0f;转换:V = (-1)^S * M * 2^EV = (-1)^0 * 1.01 ^ 2 ^2S = 0, E = 2 ,M = 1,01首位是符号位 : 0E的存储是8为,加上中间值127为129:1000 0001M的小数点前的1不存储,剩下23bit位存储01,在有效位01后补0即可01 000000000000000000000(21个bit位)将 S E M 结合0 10000001 0100000000000000000000001000000 10100000 00000000 0000000040 a0 00 00 }
按F10启动调试,打开内存窗口观察
以下程序的输出结果是什么?
#include <stdio.h>
int main()
{int n = 5;float* pFloat = (float*)&n;printf("n的值为:%d\n", n);printf("*pFloat的值为:%f\n", *pFloat);*pFloat = 1.625;printf("*pFloat的值为:%f\n", *pFloat);printf("n的值为:%d\n", n);return 0;
}
第一个数是n = 5 ,取地址后赋给float*类型的指针变量pFloat,%d形式打印n的值,输出5;5的二进制序列是:00000000 000000000 00000000 00000101
%f形式打印*pFloat,按照浮点数的使用规则转换:0 00000000 0000000000000000000000101
首位是符号位表示正数,后面8位是E,真实的E需要减去中间数127,E = -127,最后是M,需要小数点前补1,
M = 1.0000000000000000000000101,所以*pFloat =-1^0 * 1.0000000000000000000000101 * 2^(-127),
是一个非常小的数字,输出的结果是0.000000;*pFloat = 1.625,按照%f输出的是1.625000V = -1^0 * 1.101 * 2^0; S = 0,E = 0,M =1.101;
转换二进制序列:0 01111111 101 000000000000000000000(20个0)
%d形式输出*pFloat,是将存储在内存中的二进制当成补码转换为原码后输出
0 01111111 101 00000000000000000000的十进制数字是:1,070,596,096
0011 1111 1101 0000 0000 0000 0000 0000
程序输出结果如下