C语言-数据存储和动态内存分配

数据的存储

C语言中数据的类型

整型

char:signed char(有符号位),unsigned char(无符号位)
short:signed short(有符号位),unsigned short(无符号位)
int:signed int(有符号位),unsigned int(无符号位)
long:signed long(有符号位),unsigned long(无符号位)

其中long和short都是可以与int连用的

#include<stdio.h>
int main(void)
{
	short int i = 1;
	long int j = 2;
}

浮点型

float,单精度浮点型
double,双精度浮点型

构造类型

数组类型,int arr[10]的类型是int[10],int arr[5]的类型是int[5],这两种类型是不一样的
结构体类型 struct
枚举类型 enum
联合类型 union

指针类型

int* pi
char* pc;
float* pf;
void* pv;

空类型

void,void表示空类型

通常用于函数的返回类型,函数的参数,指针类型

变量的创建是要在内存中开辟空间的,开辟的空间大小是根据变量的类型决定的

计算机中的有符号数有三种表示方式,即原码、反码和补码。无符号数也有这三种表示方式,但是他们的值是一样的

原码,直接将正负数转化为二进制即可

反码,原码的符号位不变,其他位次按位取反即可

补码,反码+1就得到补码

#include<stdio.h>
int main(void)
{
	int a = 20;//内存中存储的是14000000
    //00000000 00000000 00000000 00010100 原码,第一个0是符号位
    //00000000 00000000 00000000 00010100 反码
    //00000000 00000000 00000000 00010100 补码
    //转化为16进制:0x00000014,4个二进制位转化成1个十六进制位
	int b = -10;//内存中存储的是F6FFFFFF
    //10000000 00000000 00000000 00001010 原码,第一个0是符号位
    //11111111 11111111 11111111 11110101 反码
    //11111111 11111111 11111111 11110110 补码
    //转化为16进制:0xFFFFFFF6
}

对于整型来说,数据存放在内存中其实存放的是补码

大小端的概念

大端(存储)模式:指数据的低位保存在内存的高地址中,而数据的高位保存在内存的低地址中

小端(存储)模式:指数据的低位保存在内存的低地址中,而数据的高位保存在内存的高地址中

比如,一个数int a = 0x11223344;大端存储模式时,a在内存中的存储方式为11 22 33 44;小端存储模式时,a在内存中的存储方式为44 33 22 11。

char类型的数据在内存中的存储

#include<stdio.h>
#include<stdlib.h>

int main(void)
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;

	printf("%d, %d, %d",a , b, c);
	getchar();
}     

a,b,c的值分别为-1,-1, 255

-1,原码为10000000 00000000 00000000 00000001
   反码为11111111 11111111 11111111 11111110
   补码为11111111 11111111 11111111 11111111
    char类型的只取最后1个字节11111111
%d打印十进制有符号数字
a是有符号的char%d输出时,会用1补位(整形提升,最高位是1就用1补位,最高位是0就用0补位),所以输出a的时候输出的是11111111 11111111 11111111 11111111的原码10000000 00000000 00000000 00000001,即-1
b是有符号的char%d输出时,会用1补位,所以输出a的时候输出的是11111111 11111111 11111111 11111111的原码10000000 00000000 00000000 00000001,即-1    
c是无符号的char%d输出时,会用0补位,所以输出a的时候输出的是00000000 00000000 00000000 11111111的原码00000000 00000000 00000000 11111111,即255

%u打印十进制无符号数字

#include<stdio.h>

int main(void)
{
	char a = -128;
	printf("%u",a);
	getchar();
}  

会输出4294967168

-128,原码为10000000 00000000 00000000 10000000
   反码为11111111 11111111 11111111 01111111
   补码为11111111 11111111 11111111 10000000
char类型的只取最后1个字节10000000%u打印十进制无符号数字,需要整形提升,补位,此时用1补位,补位后,a的补码是11111111 11111111 11111111 10000000,而%u输出的是无符号数,所以补码与原码相同,所以最后输出的是11111111 11111111 11111111 10000000,即4294967168

当char类型的变量中存储的是128,%u输出,输出的也是4294967168

#include<stdio.h>

int main(void)
{
	char a = 128;
	printf("%u",a);
	getchar();
} 
128,原码为00000000 00000000 00000000 10000000
   反码为01111111 11111111 11111111 01111111
   补码为01111111 11111111 11111111 10000000
char类型的只取最后1个字节10000000%u打印十进制无符号数字,需要整形提升,补位,此时用1补位,补位后,a的补码是11111111 11111111 11111111 10000000,而%u输出的是无符号数,所以补码与原码相同,所以最后输出的是11111111 11111111 11111111 10000000,即4294967168
  1. 有符号的char和无符号的char的取值范围

在内存中存储的都是补码

有符号的char

0000 0000   正数,原反补一样,0
0000 0001	1
...
0111 1110	126
0111 1111	127
...
1000 0000	这个数比较特殊,直接当成-128处理
1000 0001	负数,反码为1000 0000,补码为1111 1111-127
...
1111 1110	-2
1111 1111	负数,反码为1111 1110,补码为1000 0001-1

假如用9个字节的数来描述-128,多一个符号位

假如用9个字节的数来描述-128
原码为1 1000 0000
反码为1 0111 1111
补码为1 1000 0000
发现原码和补码是一样的,所以把1000 0000规定成-128

所以有符号char的时候,0111 1111(127)再加1的话,就会变成1000 0000(-128)

取值范围为-128~127

无符号的char,全为正数,原反补都是一样的

0000 0000   正数,原反补一样,0
0000 0001	1
...
0111 1110	126
0111 1111	127
...
1000 0000	128
1000 0001	129
...
1111 1110	254
1111 1111	255

取值范围为0~255

  1. 计算

数据在内存中是以补码的形式存放的,所以i与j的相加是以补码的形式相加,再以有符号整数的形式输出

#include<stdio.h>

int main(void)
{
	int i = -20;
	unsigned int j = 10;
	
	printf("%d",i+j);
	getchar();
} 
10000000 00000000 00000000 00010100  -20的原码
11111111 11111111 11111111 11101011	 -20的反码
11111111 11111111 11111111 11101100  -20的补码
00000000 00000000 00000000 00001010	 10的原码、补码、反码
相加得
11111111 11111111 11111111 11110110	 结果的反码
11111111 11111111 11111111 11110101	 结果的补码
10000000 00000000 00000000 00001010	 结果的原码,-10
  1. 求数组长度
#include<string.h>
#include<stdio.h>

int main(void)
{
	char a[1000];
	int i;
	for(i = 0; i < 1000; i++){
		a[i] = -1-i;
	}
	printf("%d", strlen(a));
	getchar();
} 

输出的值为255

有符号的char的取值范围为-128~127
数组a的第一位是-1,第二位是-2,第三位是-3....-128之后,再减1,变成了127(从-129变成了127int类型的-129的原码:10000000 00000000 00000000 10000001
int类型的-129的反码:11111111 11111111 11111111 01111110
int类型的-129的补码:11111111 11111111 11111110 01111111char类型的只取最后8位,所以-129变成了12701111111,正数的原反补相同)
所以数组又从127开始,126,125....2,1,0
所以数组的长度为128+127=255

浮点型在内存中的存储

1e5或者1E5表示10的5次方

int main(void)
{
	float f = 1e5;
	printf("%lf", f);
	
	getchar();
} 

根据IEEE标准,任意一个二进制浮点数V可以表示为:(-1)^S * M * 2^E
(-1)^S表示符号位,当S为0时,V为正数;当S为1时,V为负数
M表示有效数字,大于等于1,小于2
2E表示指数位(二进制所以是2E,类比于十进制的10^E)

比如,十进制的5.0,写成二进制是101.0。那么按照这种表示方法,可以表示为(-1)^0 * 1.01 * 2^2

IEEE754规定
对于32位的浮点数(单精度浮点数),最高位的1位是符号位S,接着的8位是指数E,剩下的23位是有效数字M
对于64位的浮点数(单精度浮点数),最高位的1位是符号位S,接着的11位是指数E,剩下的52位是有效数字M

对于有效数字M和指数E,还有一些特别的规定:

有效数字M大于等于1,小于2,即M可以写成1.xxxx的形式。所以在计算机内部保存M的时候,默认这个数的第一位总是1,因此可以被舍去,只保留后面的小数部分,等读取的时候,再把1加上去,这样可以节省一位有效数字

指数E,E是一个无符号整数,E为8位的时候,取值0255,如果E为11位,取值02047。但是E是能够有负数的(比如小数的情况),所以存入内存时,E的真实值必须加上一个中间数。对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023

int main(void)
{
	float f = 5.5;	
	getchar();
} 
/* 举例说明浮点数的存储 **/
5.5转为二进制数为101.1,小数部分的1表示21/2次方,即为0.5
可以表示为:(-1)^0 * 1.011 * 2^2
所以在内存中的存储为:0 10000001 01100000000000000000000
内存中数据的显示一般都是以16进制显示,所以01000000 10110000 00000000 00000000变成16进制
则为0x 40 B0 00 00

假如指数E全为0,要知道,内存中指数的8个比特位全为0的时候,那实际E的值为0-127=-127。实际还原回去的E的值是1.xxx * 2^(-127),接近于0的一个数。因此IEEE规定,此时浮点数的指数E的真实值等于1-127(或者1-1023),有效数字M还原的时候也不再加上1,而是还原为0.xxx的小数。这样是为了表示±0以及接近0的很小的数。

同理,指数E全为1的时候,此时实际E的值为255-127=128,实际还原回去的E的值是1.xxx * 2^(128),接近于无穷大的一个数。所以此时,指数E全为1的时候,表示±无穷大的一个数

int main(void)
{
	int n = 9;
	float* f = (float*)&n;	
	printf("%d\n",n);//9
	printf("%f\n",*f);//0.000000

	*f = 9.0;
	printf("%d\n",n);//1091567616
	printf("%f\n",*f);//9.000000
	getchar();
} 
此代码中,9在内存中存储的补码是00000000 00000000 00000000 00001001
当使用一个float类型的指针去解引用时,按照float存储的规则(此时指数位全为0,即指数的真实值为1-127=-126),9的补码转化为
0 00000000 00000000000000000001001,即(-1^0 * 0.00000000000000000001001 * 2^(-126)
这个数无限接近0,所以输出0.000000
当给float类型的指针赋值9.0时,转化为二进制1001.0,科学计数法表示(-1^0 * 1.001 * 2^(3)
此时在内存中存储的值为0 10000010 00100000000000000000000
把这个数当做整型输出的时候,值就为1091567616

动态内存分配

已知的分配内存的方式

1.创建一个变量

int a = 10;如果是局部变量,则在栈区分配空间,如果是全局变量,则在静态区分配空间

2.创建一个数组

int a[10];如果是局部变量,则在栈区分配空间,如果是全局变量,则在静态区分配空间

但是以上两种分配内存的方式,只能开辟固定大小的空间,此外数组在申明的时候,必须指定数组的长度,其所需要的内存在编译时分配。当我们需要在程序运行时,才能知道变量需要的内存空间大小的话,就需要使用动态内存分配了。

动态内存分配,指的是在堆空间申请和分配内存

malloc和free

malloc和free函数的头文件都在stdlib.h头文件中

函数原型:void* malloc(size_t size);

malloc函数用于动态内存开辟,这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针,参数size的单位为字节。

1.如果开辟失败,则返回一个NULL指针,所以malloc的返回值一定要做空指针检查

2.返回值的类型为void*,所以malloc函数并不知道开辟的空间的类型,需要程序员来定义

3.如果参数size为0,则malloc的行为是标准未定义的,取决于编译器

函数原型:void* malloc(void* ptr);

free函数用来释放动态开辟的内存

1.如果参数ptr指针指向的空间不是动态开辟的,那么free函数的行为是未定义的

2.如果参数ptr指针为NULL指针,则函数什么事都不做

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

int main()
{
	//向内存申请10个int类型的空间,即40字节
	int* p = (int*)malloc(10*sizeof(int));
	if(NULL == p){
		printf("%s\n", strerror(errno));//如果分配时出错,则将具体错误写入errno这个全局的错误码中
	}else{//内存分配正常
		int i = 0;
		for(i; i < 10; i++){
			*(p+i) = i;
			printf("%d ", *(p+i));
		}
	}
	//当动态申请的内存不再使用的时候,就应该还给操作系统
	free(p);
	p = NULL;//但是将内存还给操作系统,p指针还保留此内存区域的地址,所以要将p置为空指针
	return 0;
}

calloc

函数原型:void* calloc(size_t num, size_t size);

calloc函数,能为num个大小为size的元素开辟一块空间,并把空间中每个字节都初始化为0,再返回指向这块空间的指针。malloc函数不会将申请的空间初始化为0

realloc

函数原型:void* realloc(void* ptr, size_t size);

realloc函数可以对动态开辟的内存空间进行调整。ptr是指向要调整的内存地址的指针,size是调整之后的总大小(单位:字节),返回值为调整后的指向这块内存空间起始位置的指针。

realloc调整内存空间有两种情况

1.原有空间之后有足够大的空间来分配

则直接在原有空间之后追加开辟新的空间,原有空间的数据不变化,返回的指针为指向原有空间的指针

2.原有空间之后没有足够多的空间来分配

则会在堆空间上,另找一个合适大小的连续空间来使用,此时会将原有空间的数据拷贝至新的内存空间,并释放原有空间的内存,返回的指针为指向新开辟的内存空间的指针

如果调整内存空间失败,则返回NULL空指针,所以使用时要判空

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>

int main()
{
	int i = 0;
	int* ptr = NULL;
	int* p = (int*)malloc(20);
	if(NULL == p){
		printf("%s\n", strerror(errno));
	}else{
		for(i; i < 5; i++){
			*(p+i) = i;
			printf("%d ", *(p+i));
		}
	}
	//将原开辟的20个字节的空间,改为40个字节
	ptr = (int*)realloc(p, 40);
	//如果ptr不为空指针,则将ptr赋值给p,这样新开辟无论是否改变了地址,都可以用p来指向新开辟的空间
	//ptr不为空,说明开辟成功,则可以使用这块内存空间
	if(NULL != ptr){
		p = ptr;
		i = 5;
		for(i; i < 10; i++){
			*(p+i) = i;
			printf("%d ", *(p+i));
		}
	}
	free(p);
	p = NULL;//但是将内存还给操作系统,p指针还保留此内存区域的地址,所以要将p置为空指针
	return 0;
}

动态分配内存要注意的问题

1.对空指针进行解引用操作

动态分配内存函数,有分配失败的情况存在,此时函数返回的就是空指针。所以在使用前,需要进行判空处理

2.对动态开辟内存的越界访问

动态开辟内存后,不能对此内存之外的空间随意进行访问

3.不能对非动态开辟的内存使用free函数释放

4.不能使用free函数释放动态开辟内存的部分内存空间

5.不能对同一块动态开辟的内存空间多次释放

6.动态开辟内存空间忘记释放。导致内存泄漏

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值