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改为xxxxx
memset(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("<");
else
printf("==");
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;
}
//不相等直接返回做差的值
else
return *(char*)ptr1 - *(char*)ptr2;
}
//此时已经将全部字节比较,返回0
if (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("<");
else
printf("==");
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("小端");
else
printf("大端");
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^E
V = (-1)^0 * 1.01 ^ 2 ^2
S = 0, E = 2 ,M = 1,01
首位是符号位 : 0
E的存储是8为,加上中间值127为129:1000 0001
M的小数点前的1不存储,剩下23bit位存储01,在有效位01后补0即可
01 000000000000000000000(21个bit位)
将 S E M 结合
0 10000001 01000000000000000000000
01000000 10100000 00000000 00000000
40 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.625000
V = -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
程序输出结果如下