基本的内置类型:
char //字符数据类型 //占内存1个字节 short //短整型 //占内存2个字节 int //整型 //占内存4个字节 long //长整型 //占内存4/8个字节 long long //更长的整形 //占内存8个字节 float //单精度浮点数 //占内存4个字节 double //双精度浮点数 //占内存8个字节
类型的意义:
- 类型决定了该数据在内存中开辟空间的大小
- 类型决定了看待内存空间数据的视角
整型家族:
char
signed char
unsigned char
short
signed short [int]
unsigned short [int]
int
signed int
unsigned int
long
signed long [int]
unsigned long [int]
long long
signed long long [int]
unsigned long long [int]
浮点数家族:
float double
构造类型:
> 数组类型 > 结构体类型 struct > 枚举类型 enum > 联合类型 union
指针类型:
char* pc; short* ps; int* pi; long* pl; long long* pll; float* pf; double* pd; void* pv; . . .
空类型:
void 表示空类型(无类型)
通常应用于函数的返回类型、函数的参数、指针类型。
//第一个void代表无返回值
//第二个void代表无需参数
void test1(void)
{
;
}
//void*代表接受任意类型的指针
void test2(void* p)
{
;
}
整形在内存中的存储
变量的创建是要在内存中开辟空间的,而开辟空间的大小由变量的类型决定。
空间开辟好之后,那就必然存在一个存储数据的方式的问题
而在计算机中,整数的存储都是以该数据的二进制的补码形式存储的。
原码、反码、补码计算机中的整数有三种二进制表示方法,即原码、反码和补码。
三种表示方法均有符号位和数值位两部分:
-
符号位:都是用0表示“正”,用1表示“负”。
-
数值位:
- 正数的原码、反码、补码都相同。
- 负数的三种表示方法各不相同。
原码
直接将数值按照正负数的形式翻译成二进制就可以得到原码。
反码
将原码的符号位不变,其它位依次按位取反就可以得到反码。
补码
反码 + 1就可以得到补码。
对于整型来说:数据存放内存中其实存放的是补码
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;
同时,加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
实例:
int a = 20;
十进制:20
二进制:
原码:0000 0000 0000 0000 0000 0000 0001 0100(正数,符号位为0)
由于20是正数,所以原码、反码、补码相同
反码:0000 0000 0000 0000 0000 0000 0001 0100
补码:0000 0000 0000 0000 0000 0000 0001 0100(实际存储的是补码)
int a = -20;
十进制:-20
二进制:
原码:1000 0000 0000 0000 0000 0000 0001 0100(负数,符号位为1)
由于-20是负数,所以遵循负数的三码转换规则
反码:1111 1111 1111 1111 1111 1111 1110 1011(符号位不变,其它位按位取反)
补码:1111 1111 1111 1111 1111 1111 1110 1100(反码+1)(实际存储的是补码)
补码转换为原码有两种方式:
- 按照原码- - ->补码的顺序逆着推:补码-1得到反码,反码符号位不变,其它位按位取反得到原码。
- 还是按照原码- - ->补码的顺序正着推:先是符号位不变,其它位按位取反,然后再+1就得到原码。(这也解释了补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。)
两个整数相加减时:
//相加
int a = 10;
int b = 20;
int c = a + b;
a的补码:0000 0000 0000 0000 0000 0000 0000 1010 //10
b的补码:0000 0000 0000 0000 0000 0000 0001 0100 //20
c的补码:0000 0000 0000 0000 0000 0000 0001 1110 //30
//相减
int a = 10;
int b = -20;
int c = a - b;//由于CPU只有加法器,所以这里实际执行的是int c = a + (-b);
a的补码:0000 0000 0000 0000 0000 0000 0000 1010 //10
-b的补码:1111 1111 1111 1111 1111 1111 1110 1100 //-20
c的补码:1111 1111 1111 1111 1111 1111 1111 0110 //-10
//这也就解释了为什么有补码的存在
在VS2019中查看内存中的存储
可以发现,实际存储在内存中的顺序和我们想象的是有偏差的。这就涉及到大小端的问题了
大小端介绍什么是大端、小端:
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中。
为什么有大端和小端:
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的int型等等大于8bit的数据类型。另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
例如:一个16bit的short型x,在内存中的地址为0x0010,x的值为0x1122,那么0x11为高字节,0x22为低字节。对于大端存储模式,就将0x11放在低地址中,即0x0010中,0x22放在高地址中,即0x0011中。小端模式,刚好相反。我们常用的x86结构是小端模式,而KEIL C51则为大端模式。很多的ARM,DSP都为小端模式。有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
大小端练习题:
设计一个程序判断当前机器的字节序
#includevoid CheckByteOrder() { int a = 1; return *(char*)(&a); } int main() { if(CheckByteOrder()) { printf("小端n"); } else { printf("大端n"); } return 0; }
解释:
大端字节序和小端字节序的区别无非就是高低字节与高低地址的两种排列顺序,而又因为我们取地址取的都是低地址,所以我们只需要知道低地址处存放的是低字节还是高字节。正因为我们只需要知道低地址处的字节信息,那么我们就需要隔离出低地址,所以将取出的地址强制类型转换再解引用。
练习//输出结果是什么? #includeint main() { char a = -1; signed char b = -1; unsigned char c = -1; printf("%d %d %dn", a, b, c);//答案分别是-1、-1、255 return 0; } //解释: -1 原码 10000000 00000000 00000000 00000001 反码 11111111 11111111 11111111 11111110 补码 11111111 11111111 11111111 11111111 又因为a是char型,所以a的补码就是 11111111 用%d来输出char型的a,先看数据类型进行整型提升,再看符号位 由于a是char型,一般默认为有符号char,所以提升原符号位 补码 11111111 11111111 11111111 11111111 整型提升后符号位是1,是负数,转换为原码后再输出 反码 11111111 11111111 11111111 11111110 原码 10000000 00000000 00000000 00000001 所以a输出是-1,由于b和a相同都是有符号char,所以也输出-1 unsigned char c和前两个的区别在于,整型提升时,c看数据类型 是无符号,所以提升0 补码 00000000 00000000 00000000 11111111 而打印%d是由于符号位是0,所以认为是正数,直接打印出补码 所以c打印出255
//输出结果是什么? #include浮点型在内存中的存储int main() { char a = -128; printf("%un",a);//输出4294967168 printf("%dn",a);//输出-128 return 0; } //解释: a 原码 10000000 00000000 00000000 10000000 反码 11111111 11111111 11111111 01111111 补码 11111111 11111111 11111111 10000000 又因为a是char型,所以a的补码就是 10000000 输出时,先根据数据类型整型提升,由于a是char认为有符号 所以提升符号位也就是1 补码 11111111 11111111 11111111 10000000 用%u输出时,认为无符号,直接输出补码,输出4294967168 用%d输出时,认为有符号,先转换位原码,再输出 反码 11111111 11111111 11111111 01111111 原码 10000000 00000000 00000000 10000000 输出原码为-128
常见的浮点数(小数)
3.14159
1E10(科学计数法) 1.0 × 1 0 10 1.0×10^{10} 1.0×1010
浮点数家族包括:float、double、long double类型。
浮点数表示的范围:float.h中定义。
int main()
{
int n = 9;
float* pFloat = (float*)&n;
printf("%dn", n);
printf("%fn", *pFloat);
*pFloat = 9.0f;
printf("%dn", n);
printf("%fn", *pFloat);
return 0;
}
出现这样的结果的原因就是浮点数在内存中的存储规则和整形是完全不同的
浮点数存储规则根据国际标准IEEE(电气和电子工程协会)754,任意一个二进制浮点数 V V V可以表示成下面的形式:
- ( − 1 ) S × M × 2 E (-1)^S×M×2^E (−1)S×M×2E
- ( − 1 ) S (-1)^S (−1)S表示符号位,当 S = 0 S=0 S=0, V V V为正数;当 S = 1 S=1 S=1, V V V为负数。
- M M M表示有效数字, 1 ⩽ M < 2 1leqslant M<2 1⩽M<2
- 2 E 2^E 2E表示指数位。
举例来说:
十进制的 15.0 15.0 15.0,按照科学计数法会写成 1.5 × 1 0 1 1.5×10^1 1.5×101
二进制的 15.0 15.0 15.0就是 1111.0 1111.0 1111.0,按照科学计数法会写成 1.111 × 2 3 1.111×2^3 1.111×23
那么,按照上面 V V V的格式,可以得出 S = 0 , M = 1.111 , E = 3 S=0,M=1.111,E=3 S=0,M=1.111,E=3
二进制的 − 15.0 -15.0 −15.0就是 − 1111.0 -1111.0 −1111.0,科学计数法写出 − 1.111 × 2 3 -1.111×2^3 −1.111×23
可以得出 S = 1 , M = 1.111 , E = 3 S=1,M=1.111,E=3 S=1,M=1.111,E=3
IEEE 754规定:
对于32位的浮点数,最高的一位是符号位 S S S,接着的8位是指数 E E E,剩下的23位为有效数字 M M M.
对于64位的浮点数,最高的一位是符号位 S S S,接着的11位是指数 E E E,剩下的52位为有效数字 M M M.
IEEE 754对有效数字M和指数E,还有一些特别规定:
前面说过, 1 ⩽ M < 2 1leqslant M <2 1⩽M<2,也就是说, M M M可以写成1.xxxxxx的形式,其中xxxxxx表示小数部分。
IEEE 754 规定,在计算机内部保存 M M M时,默认这个数的第一位总是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的时候,只保存01,等到读取的时候,再把第一位的1加上去。这样做的目的是,是节省一位有效数字。以32位浮点数为例,留给 M M M只有23位,将第一位的1舍去以后,等于可以保存24位有效数字。
至于指数E,情况就比较复杂
首先, E E E为一个无符号整数(unsigned int)
这意味着,如果 E E E为8位,它的取值范围为0~255;如果 E E E为11位,它的取值范围为0~2047。但是,我们知道,科学计数法中的 E E E是可以出现负数的,所以IEEE 754规定,存入内存时 E E E的真实值必须再加上一个中间数,对于8位的 E E E,这个中间数是127;对于11位的 E E E,这个中间数是1023。比如 2 10 2^{10} 210的 E E E是10,所以保存成32位浮点数时,必须保存成 10 + 127 = 137 10+127=137 10+127=137,即10001001。
然后,指数E从内存中取出还可以再分成三种情况:
E不全为0或不全为1(一般情况)
这时,浮点数就采用该规则表示:即指数 E E E的计算值减去127(或1023),得到真实值,再将有效数字 M M M前加上第一位的1。
比如:
存入时,0.5的二进制形式为0.1,由于规定正数部分必须为1,即将小数点右移一位,则为 1.0 × 2 − 1 1.0×2^{-1} 1.0×2−1, E E E为 − 1 + 127 = 126 -1+127=126 −1+127=126,表示为01111110,而$M$1.0去掉整数部分为0,补齐0到23位00000000000000000000000,则其二进制表示形式为:
0 01111110 00000000000000000000000
取出时,计算值 E E E01111110减去127得到真实值 E = − 1 E=-1 E=−1,而有效数字 M M M00000000000000000000000加上1得到真实值 M = 1.0 M=1.0 M=1.0,而符号位 S = 0 S=0 S=0
E E E全为0(特殊情况)
这时,令浮点数的指数 E E E等于1-127(或者1-1023)即为真实值,
有效数字 M M M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
E E E全为1(特殊情况)
这时,如果有效数字 M M M全为0,表示±无穷大(正负取决于符号为 S S S)
解释开始时的代码:
9 补码 00000000 00000000 00000000 00001001
用%d打印时,就直接打印出改补码为9
用%f打印时,按照IEEE 754的规定,把该内存分为三个区域,还原为科学计数法
三个区域分别为:S=0,E=00000000,M=00000000000000000001001
而E为全0,则令E=-126,M不再追加第一位的1
科学计数法: V = ( − 1 ) S × M × 2 E V=(-1)^{S}×M×2^{E} V=(−1)S×M×2E即 V = ( − 1 ) 0 × 0.00000000000000000001001 × 2 − 126 V=(-1)^{0}×0.00000000000000000001001×2^{-126} V=(−1)0×0.00000000000000000001001×2−126即 V = 1.001 × 2 − 146 V=1.001×2^{-146} V=1.001×2−146
很显然, V V V是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000.
存储9.0时,先化为二进制1001.0,再化为科学计数法 1.001 × 2 3 1.001×2^{3} 1.001×23.
即 S = 0 , E = 3 , M = 1.001 S=0,E=3,M=1.001 S=0,E=3,M=1.001存入时, E = 3 + 127 = 130 E=3+127=130 E=3+127=130, M M M舍弃掉整数位的1
存入内存中是0 10000010 00100000000000000000000
而用%d打印时,看符号位是0,直接打印出该补码即1091567616
用%f打印时,怎么存进去的就怎么取出来打印,还是9.000000



