数据的存储
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
- 有符号的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
- 计算
数据在内存中是以补码的形式存放的,所以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
- 求数组长度
#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变成了127)
int类型的-129的原码:10000000 00000000 00000000 10000001
int类型的-129的反码:11111111 11111111 11111111 01111110
int类型的-129的补码:11111111 11111111 11111110 01111111
而char类型的只取最后8位,所以-129变成了127(01111111,正数的原反补相同)
所以数组又从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表示2的1/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.动态开辟内存空间忘记释放。导致内存泄漏