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时,

arri = - 129;8位的二进制原码:10000001 -> 01111110(反码)-> 01111111(127的补码),以此类推可以 i = -129时,arri = 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

程序输出结果如下

相关推荐
Dovis(誓平步青云)31 分钟前
《QT学习第四篇:常见事件与UDP、TCP、文件系统、(锁、信号量、条件变量》
c语言·开发语言·汇编·qt
isyangli_blog9 小时前
OpenDayLight (Carbon 版本) 启动与组件安装
开发语言·php
vb2008119 小时前
FastAPI APIRouter
开发语言·python
Benszen9 小时前
KVM虚拟化解决方案
开发语言·perl
会编程的土豆9 小时前
Go 语言反射(Reflection)详解
开发语言·后端·golang
東雪木9 小时前
多线程与并发编程 专属复习笔记
java·开发语言·笔记·java面试
杨充10 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
噜噜噜阿鲁~10 小时前
python学习笔记 | 11.3、面向对象高级编程-多重继承
java·开发语言
basketball61610 小时前
Go 语言从入门到进阶:4. 数组和MAP使用方法总结
开发语言·后端·golang
春生野草10 小时前
反射、Tomcat执行
java·开发语言