目录
数据类型介绍
基本内置类型:
char | 字符数据类型 |
short | 短整型 |
int | 整型 |
long | 长整型 |
long long | 更长的整型 |
float | 单精度浮点数 |
double | 双精度浮点数 |
类型的意义:
- 使用这个类型开辟内存空间的大小(大小决定了使用范围);
- 如何看待内存空间的视角
类型的基本归纳:
整型家族:
- char:unsigned char,signed char
- short:unsigned short [int],signed int
- int:unsigned int,signed int
- long:unsigned long [int],signed long [int]
浮点数家族:
- float
- double
构造类型:
- 数组类型
- 结构体类型 sturct
- 枚举类型 enum
- 联合类型 union
指针类型:
- int* pi
- char* pc
- float* pf
- void* pv
空类型:
void:表示空类型(无类型) ,通常应用于函数的返回类型,函数的参数,指针类型
整型在内存中的存储
原码,反码和补码:
计算机中的有符号数有三种表示方式,即原码,反码和补码。三种表示方式均有符号位和数值位两部分,符号位都是用0表示“正”,用1表示“负”,而数值位三种表示方法各不相同。
正数:原,反,补码都相同
负数:原,反,补码需要按照如下规则进行运算
- 原码:直接将数值按照正负数的形式翻译成二进制形式就可以得到原码
- 反码:将原码的符号位不变,其他位一次按位取反就可以得到反码
- 补码:反码+1
注意:对于整型来说,数据存放在内存中存放的其实是数据的补码
原因如下:在计算机系统中,数值一律用补码来表示和存储。原因在于使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理(CPU只有加速器)。此外,补码和原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
我们通过以下程序来查看数据在内存中的存储:
我们发现数据在内存中确实是以16进制的补码形式进行存储的,但是我们发现数据的存放顺序是存在差异的,那这又是为何?
大小端存储模式:
何为大小端?
大端(存储)模式:是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式:是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中
大小端产生原因:
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
案例一:设计一个小程序来判断当前机器的字节序。
//基础版
int ckeck_sys()
{
int a = 1;//00 00 00 01
char* p = (char*)&a;//强制类型转换,取第一个字节
if (*p == 1)
{
return 1;//小端
}
else
{
return 0;//大端
}
return 0;
}
//进阶版
int check_sys()
{
int a = 1;
return *(char*)&a;
}
int main()
{
int ret = check_sys();
if (ret == 1)
{
printf("小端\n");
}
else
{
printf("大端\n");
}
}
运行结果:
案例二:下例程序输出什么?
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;
}
分析:
int main()
{
//char是signed char还是unsigned char,C语言并没有明确规定,取决于编译器,大多数编译器下是signed char
char a = -1;
//原:10000000 00000000 00000000 00000001
//反:11111111 11111111 11111111 11111110
//补:11111111 11111111 11111111 11111111
//取8位:11111111
signed char b = -1;
//同a
unsigned char c = -1;
//原:10000000 00000000 00000000 00000001
//反:11111111 11111111 11111111 11111110
//补:11111111 11111111 11111111 11111111
//取8位:11111111
printf("a=%d,b=%d,c=%d\n",a,b,c);//-1,-1,255
//%d是打印有符号数,%d是用来输出十进制的整数,对应的数据类型是 int
//当我们打印a时,要发生整型提升,有符号数则直接补符号位,则11111111->11111111 11111111 11111111 11111111->转原码:10000000 00000000 00000000 00000001得-1
//b同a一样
//当我们打印c时,要发生整型提升,无符号数则直接补0,则11111111->00000000 00000000 00000000 11111111->转原码:00000000 00000000 00000000 11111111得255
return 0;
}
运行结果:
案例三:下例程序输出什么?
int main()
{
char a = -128;
printf("%u\n", a);
return 0;
}
分析:
int main()
{
char a = -128;
//原:10000000 00000000 00000000 10000000
//反:11111111 11111111 11111111 01111111
//补:11111111 11111111 11111111 10000000
//取8位:10000000
printf("%u\n",a);
//%u是打印无符数,%u以无符号的十进制形式输出整数,对应的数据类型是unsigned int
//当我们打印a时,要发生整型提升,有符号数则直接补符号位,则11111111 11111111 11111111 10000000,打印的是无符号数,所以原反补相同,所以直接将二进制转换成十进制输出:4294967168
return 0;
}
运行结果:
案例四:下例程序输出什么?
int main()
{
char a = 128;
printf("%u\n", a);
return 0;
}
分析:
int main()
{
char a = 128;
//原:00000000 00000000 00000000 10000000
//反:00000000 00000000 00000000 10000000
//补:00000000 00000000 00000000 10000000
//取8位:10000000
printf("%u\n", a);
//%u打印的是无符号的整数,%u以无符号的十进制形式输出整数,对应的数据类型是unsigned int
//当我们打印a时,要发生整型提升,有符号数则直接补符号位,则11111111 11111111 11111111 10000000,打印的是无符号数,所以原反补相同,所以直接将二进制转换成十进制输出:4294967168
return 0;
}
运行结果:
案例五:下例程序输出什么?
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d\n", i + j);
return 0;
}
分析:
int main()
{
int i = -20;
//原:10000000 00000000 00000000 00010100
//反:11111111 11111111 11111111 11101011
//补:11111111 11111111 11111111 11101100
unsigned int j = 10;
//原:00000000 00000000 00000000 00001010
//反:00000000 00000000 00000000 00001010
//补:00000000 00000000 00000000 00001010
printf("%d\n",i+j);
//俩补码相加:
//11111111 11111111 11111111 11101100
//00000000 00000000 00000000 00001010
//11111111 11111111 11111111 11110110:补
//10000000 00000000 00000000 00001001:反
//10000000 00000000 00000000 00001010:原->-10
return 0;
}
运行结果:
案例六:下例程序输出什么?
int main()
{
unsigned int i;
for(i = 9; i >= 0; i--)
{
printf("%u\n", i);
}
return 0;
}
分析:
#include<windows.h>
int main()
{
unsigned int i;
for (i = 9; i >= 0; i--)
{
printf("%u\n",i);
//9 8 7 6 5 4 3 2 1 0 (-1的补码:11111111 11111111 11111111 11111111对应的无符号数:4294967295) (-2的补码对应的无符号数:4294967294) 依次循环并进入死循环
//i是个无符号类型的,也就是i>=0恒成立,所以这个循环的条件就恒成立
Sleep(1000);//单位是毫秒
}
return 0;
}
运行结果:
案例七: 下例程序输出什么?
int main()
{
char a[1000];
int i;
for(i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d", strlen(a));
return 0;
}
分析:
int main()
{
char a[1000];
int i;
for (i = 0; i < 1000; i++)
{
a[i] = -1 - i;
}
printf("%d\n",strlen(a));//255
//即-1 -2 -3...-128 127(-129对应的二进制截段) 126 125...1 0,strlen读取到0的时候则停止
//-129的原码:10000000 00000000 00000000 10000001->反码:11111111 11111111 11111111 01111110->补码:11111111 11111111 11111111 01111111,取8位:01111111得127
//strlen是求字符串的长度,找的是\0的位置,统计的是\0之前出现了多少哥字符,注意'\0'的ASCII值是0
return 0;
}
运行结果:
案例八:下例程序输出什么?
unsigned char i = 0;
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");
}
return 0;
}
分析:
unsigned char i = 0;//0-255
int main()
{
for (i = 0; i <= 255; i++)
{
printf("hello world\n");//死循环,00000000-11111111,+1得100000000,舍弃最高位1得00000000
}
return 0;
}
运行结果:死循环
char小结:
char虽然是字符类型,但是字符类型存储时,存储的是字符的ASCII码,而ASCII的值为整数;
char究竟是有符号数还是无符号数,这是不确定的,要取决于对应的编译器;
无符号char:0-255,有符号char:-128-127,-128的补码是:1000 0000
char的具体意义只取决于读取内存数据并解释的编译器,现在大多数编译都将char解释成signed char
浮点型在内存中的存储
浮点数家族包括:float,double,long double类型,其表示的范围是由float.h定义
案例分析:
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("n的值为:%d\n", n);
printf("*pFloat的值为:%f\n", *pFloat);
*pFloat = 9.0;
printf("num的值为:%d\n", n);
printf("pFloat的值为:%f\n", *pFloat);
return 0;
}
运行结果:
num和*pFloat在内存中明明是同一个数,为什么浮点数和整数的解读结果会差这么大?要理解这个结果,一定要明白浮点数在计算机内部的表示方法。
整数和浮点数在内存中的存储方式是有差异的
任何一个二进制浮点数V都可以表示成:(-1)^S*M*2^E
- (-1)^S表示符号位,当S=0时,V为正数;当S=1时,V为负数
- M表示有效数字,大于等于1,小于2
- 2^E表示指数位
举例:5.5->二进制表示:101.1->(-1)^0*1.011*2^2->S=0;M=1.011;E=2
9.0->二进制表示:1001.0->(-1)^0*1.001*2^3->S=0;M=1.001;E=3
IEEE 754规定:
- 单精度浮点数存储模型:对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M
- 双精度浮点数存储模型:对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M
对有效数字M:
前面说过, 1≤M<2 ,也就是说,M可以写成 1.xxxxxx 的形式,其中xxxxxx表示小数部分。IEEE 754规定,在计算机内部保存M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的,是节省1位有效数字。以32位浮点数为例,留给M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
对指数E:
首先,E为一个无符号整数(unsigned int),这意味着,如果E为8位,它的取值范围为0 ~ 255;如果E为11位,它的取值范围为0 ~ 2047。但是,我们知道,科学计数法中的E是可以出现负数的,所以IEEE 754规定,存入内存时E的真实值必须再加上一个中间数,对于8位的E,这个中间数是127;对于11位的E,这个中间数是1023。比如,2^10的E是10,所以保存成32位浮点数时,必须保存成10+127=137,即10001001。
案例一:
int main()
{
float f = 5.5;
//101.1
//(-1)^0*1.011*2^2
//S=0
//E=2
//M=1.011
//存储到内存:
//E+127=2+127=129
//0 10000001 01100000000000000000000
//S E M
//0x40b00000
return 0;
}
指数E从内存中取出还可以在分成三种情况:
- E不全为0或不全为1:这时,浮点数就采用下面的规则表示,即指数E的计算值减去127或1023,得到真实值,再将有效数字M前加上第一位的1;
- E为全0:这时,浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字;
- E全为1:这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s)
案例二:
int main()
{
float f = 0.5;
//0.1
//(-1)^0*1.0*2^-1
//S=0
//M=1.0
//E=-1
//存储到内存:
//E+127=-1+127=126
//0 01111110 00000000000000000000000
//S E M
//0x3f000000
return 0;
}
在了解完浮点数的存储方式之后,我们再来回顾之前的案例分析,
int main()
{
int n = 9;
//00000000 00000000 00000000 00001001
//以浮点数的存储方式进行读取
//0 00000000 00000000000000000001001
//S E M
//E为全0:浮点数的指数E等于1-127(或者1-1023)即为真实值,有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数
//E=1-127=-126
//M=0.00000000000000000001001
//(-1)^0*0.00000000000000000001001*2^-126=1*0.00000000000000000001001*2^-126,是一个无限接近0的数字,保留八位得0.000000
float* pFloat = (float*)&n;
printf("n的值为:%d\n",n);//9
printf("*pFloat的值为:%f\n", *pFloat);//0.000000
*pFloat = 9.0;
//9.0
//1001.0
//1.001*2^3
//(-1)^0*1.001*2^3
//S=0
//M=1.001
//E=3
//存储到内存中:
//E=3+127=130
//0 10000010 00100000000000000000000
//S E M
//以整数形式打印:1091567616
printf("n的值为:%d\n", n);//1091567616
printf("*pFloat的值为:%f\n", *pFloat);//9.000000
return 0;
}
运行结果: