C语言 — 内存函数和数据的存储

1.memcpy函数

1.1memcpy的使用

memcpy函数是将指定字节的源头数据拷贝到目标数据上,第一个参数是目标数据的起始地址,第二个参数是源头数据的起始地址,第三个数据是需要拷贝的字节个数,返回类型是void*的指针。

给定两个整型数据,将arr2数组的数据拷贝到arr1数组上;

c 复制代码
#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.

c 复制代码
//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函数拷贝数据

c 复制代码
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个元素的位置。

c 复制代码
//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 。

c 复制代码
//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函数,分别测试从后向前拷贝和从前先后拷贝;

c 复制代码
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*。

c 复制代码
//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的模拟实现

c 复制代码
//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.

c 复制代码
#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。

c 复制代码
//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的模拟实现

c 复制代码
//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函数比较两个整型数组的大小。

c 复制代码
#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的补码。

c 复制代码
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. 大小端存储

大端存储是将高字节位的数据存放在低地址处,将低字节位的数据存储在高地址处;

小端存储是将低字节位的数据存储在低地址处,将高字节位的数据存储在高地址处;

判断当前程序是大端还是小端存储可以使用以下程序。

c 复制代码
#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启动调试,打开内存窗口,在搜索栏取出观察元素的地址

c 复制代码
#include<stdio.h>
int main()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	return 0;
}

以下程序运行的结果是什么?

c 复制代码
#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以下程序的运行结果是什么?

c 复制代码
#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;
}
c 复制代码
-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 以下程序输出的结果是什么?

c 复制代码
#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 以下程序的输出结果是什么?

c 复制代码
#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为例子,观察其内存存储的二进制序列

c 复制代码
//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启动调试,打开内存窗口观察

以下程序的输出结果是什么?

c 复制代码
#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;
}
c 复制代码
第一个数是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

程序输出结果如下

相关推荐
ppdkx几秒前
python训练营第33天
开发语言·python
玉笥寻珍26 分钟前
从零开始:Python语言进阶之异常处理
开发语言·python
Java永无止境28 分钟前
JavaSE常用API之Runtime类:掌控JVM运行时环境
java·开发语言·jvm
龙湾开发34 分钟前
C++ vscode配置c++开发环境
开发语言·c++·笔记·vscode·学习
步行cgn42 分钟前
函数式编程思想详解
java·开发语言·windows
大坏波1 小时前
C/C++内存管理
java·c语言·c++
南瓜胖胖1 小时前
R语言科研编程-标准偏差柱状图
开发语言·r语言
编码小笨猪1 小时前
[ Qt ] | 常见控件(一): enable、geometry
开发语言·qt
Eiceblue2 小时前
通过Python 在Excel工作表中轻松插入行、列
开发语言·vscode·python·pycharm·excel