谈到数据的类型,我们就会想到我们用过的最多的整型和浮点型,就以这两个数据类型为例,我们知道在计算机内存中的整数与小数不像在现实生活中可以无限大,它们的大小往往都是受制约的,比如 int 类型只能占4个字节,double 类型只能占8个字节等等。可以说,具体的类型开辟了存储空间的大小,而大小决定了使用范围。还有一个问题就是,我们应该以怎样的视角去看待内存空间的数据。首先我们从数据的基本类型开始了解。
char
int
short
long
long long
整型可以理解为整数。所有的整型都分为有符号整型与无符号整型,正是因为符号位的影响,所以有符号数与无符号数的范围就有了差别,至于有怎样的差别,我们会在第二节中提到。
一般来说,在不注明该整型是否有无符号是,默认此类型是有符号的,所以 signed 是可省略的。char 类型的而本质也是整型。
一般的,在短整型与长整型、超长整型中,我们也可以将后面的 int 省略。所以在表示有符号整型时,int, short, long, long long 是最简便的写法。
单精度浮点型 float
双精度浮点型 double
浮点数可以理解为计算机存储的小数。浮点数可以分为 float 和 double 类型。float 在内存中占4个字节,double 类型在内存中占8个字节。
数组类型
结构体类型 struct
枚举类型 enum
联合类型 union
char * p 指向字符类型的指针
short * p 指向短整型的指针
int * p 指向整型的指针
unsigned int * p 指向无符号整型的指针
long * p 指向长整形的指针
float * p 指向单精度浮点型的指针
double * p 指向双精度浮点型的指针
void * p 指向空类型的指针
void
void 表示空类型(无类型),通常应用于函数的返回类型、函数的参数、指针类型。
我们知道,整型在内存中存储是要开辟存储空间的,那么开辟的存储空间的大小取决于类型。如 int 需要开辟 4 个字节的空间 ,也就是 32 个比特位;double 需要开辟 8 个字节的空间,也即是 64 个比特位……
要想弄清楚整型如何在内存中存储,我们就先得明白下面一段代码是如何存储的。
int a = 10;
int b = -10;
整型分为有符号与无符号。
计算机中的有符号数有三种表示方法:原码、反码、补码。
三种表示方法均有符号位与数值位,符号位都是用 0 表示正,1 表示负。数值位三种表示方法各不相同。
原码
直接将二进制按照正负数的形式翻译成二进制就是原码。例如:
int a = 10;
因为 int 类型占4个字节,32个比特位,第一个数表示符号位
其原码为: 00000000 00000000 00000000 00001010
int b = -10;
因为 int 类型占4个字节,32个比特位,第一个数表示符号位
其原码为: 10000000 00000000 00000000 00001010
正数的原、反、补码都相同。
无符号数的原、反、补码都相同。
反码
将原码的符号位不变,其他位依次按位取反就可以得到反码。例如:
int a =10 ;
其原码为: 00000000 00000000 00000000 00001010
其反码为: 00000000 00000000 00000000 00001010
int b = -10 ;
其原码为: 10000000 00000000 00000000 00001010
其反码为: 11111111 11111111 11111111 11110101
补码
反码+1就得到补码。
int a =10 ;
其原码为: 00000000 00000000 00000000 00001010
其反码为: 00000000 00000000 00000000 00001010
其补码为: 00000000 00000000 00000000 00001010
int b = -10 ;
其原码为: 10000000 00000000 00000000 00001010
其反码为: 11111111 11111111 11111111 11110101
其补码为: 11111111 11111111 11111111 11110110
对于整形来说:数据存放内存中其实存放的是补码。
既然有以上的法则,我们就来看看内存中的存储到底是不是这样的。
a的补码: 00000000 00000000 00000000 00001010
转化为 16 进制:00 00 00 0a
b 的补码: 11111111 11111111 11111111 11110110
转化为 16 进制:ff ff ff f6
我们可以看到,内存中的结果和我们推算出的结果基本一致,只是顺序上不同,这是我们后面会谈到的大小端存储问题,我们下节再做介绍。
上一节的结果让我们很困惑,为什么我们推算出来的结果却与实际结果顺序相反?难道还有什么特殊的约定吗?
其实,计算机存储数据的方式分为两种模式:大端存储模式和小端存储模式。
大端存储模式
大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中。
小端存储模式
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。
可见,我们刚刚编译代码的机器采用的是小端存储模式。
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8bit。但是在C语言中除了8bit的char之外,还有16bit的short型,32bit的long型(要看具体的编译器),另外,对于位数大于8位的处理器,例如16位或者32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如果将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。
那么我们怎样设计一个程序来判断当前机器是大端存储还是小端存储呢?
#include
int main()
{
int a = 1;
int ret = 0;
ret = *((char*) &a); //从左到右取第一个字节的内容
if(ret == 1) //如果是 1 ,则是小端
{
printf("小端存储\n");
}
else //如果不是 1 ,则是大端
{
printf("大端存储\n");
}
return 0;
}
整型提升是C程序设计语言中的一项规定:在表达式计算时,各种整形首先要提升为int类型,如果int类型不足以表示则要提升为unsigned int类型;然后执行表达式的运算。下面我们用几个例子来说明。
例1
输出什么?
#include
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;
}
我们先从a 看起。
-1是一个整数,整型在内存中占32个比特位,而char类型只有8个比特位,这样存储时必会发生截断:
-1的原码:10000000 00000000 00000000 00000001
-1的反码:11111111 11111111 11111111 11111110
-1的补码:11111111 11111111 11111111 11111111
存储到 a 中的补码:
a的补码:11111111
而在内存中的确也表现为这样
好,现在我们知道存储到 a 中的是什么了,那么对 a 进行整型打印又会发生什么呢?整型需要32个比特位,而现在的 a 只有8个比特位,所以这时需要整型提升。
因为 a 是有符号数,所以就按符号位进行整型提升:
a发生整型提升(补码):11111111 11111111 11111111 11111111
整型提升之后,就可以对 a 进行打印了,对 a 的原码进行打印:
a的反码:11111111 11111111 11111111 11111110
a的原码:10000000 00000000 00000000 00000001
所以最后打印出的结果是 -1。
因为b也是有符号整数,所以原理同a,打印出的结果也是-1。
我们来看c。
c是一个无符号整数,占8个比特位,-1是有符号整数,占32个比特位,因此也会发生截断。
-1的原码:10000000 00000000 00000000 00000001
-1的反码:11111111 11111111 11111111 11111110
-1的补码:11111111 11111111 11111111 11111111
存储到 c 中的补码:
c的补码:11111111
此时对 c 进行整型打印,需要整型提升,因为 c 是无符号数,提升时高位补0:
c进行整型提升后(补码):00000000 00000000 0000000 11111111
因为无符号数的原码、反码、补码都相同,所以打印出来的结果就是255。
例2
输出什么?
#include
int main()
{
char a = -128;
printf("%u\n",a);
return 0;
}
-128是一个有符号整型数,要存到有符号字符类型中去,因此要发生截断:
-128的原码:10000000 00000000 00000000 10000000
-128的反码:11111111 11111111 11111111 01111111
-128的补码:11111111 11111111 11111111 10000000
发生截断后
a 的补码为:10000000
再将a 进行无符号整型打印,首先要进行整型提升,因为a是有符号类型,所以高位补符号位:
a 发生整型提升后(补码): 11111111 11111111 11111111 10000000
以无符号数的形式打印出来,原码、反码、补码都相同,最高位1是数值位,所以打印出来的值为4294967168。
例3
输出什么?
#include
int main()
{
char a = 128;
printf("%u\n",a);
return 0;
}
这一题与上一题类似,只不过赋给a的值改变了符号。
128的原码、反码、补码都相同:
00000000 00000000 00000000 10000000
发生截断之后:
a 的补码: 10000000
按符号位进行提升:
a发生整型提升之后(补码):11111111 11111111 11111111 10000000
因为要按照无符号整数进行打印,所以原码、反码、补码相同,打印出的结果为4294967168。
例4
输出什么?
#include
int main()
{
int i = -20;
unsigned int j = 10;
printf("%d\n",i+j);
return 0;
}
首先我们写出-20的补码和无符号数10的补码:
-20的原码:10000000 00000000 00000000 00010100
-20的反码:11111111 11111111 11111111 11101011
-20的补码:11111111 11111111 11111111 11101100
10的补码: 00000000 00000000 00000000 00001010
将-20的补码与10的补码相加:
相加后的结果: 11111111 11111111 11111111 11110110
要想按有符号整型打印:
相加后的反码:11111111 11111111 11111111 11110101
相加后的原码:10000000 00000000 00000000 00001010
例5
输出什么?
#include
int main()
{
unsigned int i;
for(i=9;i>=0;i--)
{
printf("%u\n",i);
}
return 0;
}
因为 i 是一个无符号整型变量,所以它的值不可能为负,随着循环的不断进行,当 i=0 时,打印出0,i - -, i 不会变成 -1 那会变成什么呢?我们从内存的角度来看问题。
无符号整数 0 的补码为:
00000000 00000000 00000000 00000000
可以看作 1 00000000 00000000 00000000 00000000被截断32位
在此基础上减去1 :
11111111 11111111 11111111 11111111
而无符号数的原反补码相同,所以就会输出以上的无符号数:4,294,967,295
因为仍然是大于等于0的数,所以会一直无限循环下去。
例6
输出什么?
int main()
{
char a[1000];
int i;
for(i=0; i<1000; i++)
{
a[i] = -1-i;
}
printf("%d",strlen(a));
return 0;
}
这一题让我们求字符串的长度,我们知道 strlen 函数什么时候得到返回值取决于什么时候遇到 ‘\0’ , a 是一个存放字符类型的数组,数组内元素的范围应该在 -128~127内,否则就会发生截断。
当 i = 126 时,a[126] = -127,
-127的原码:11111111
-127的反码:10000000
-127的补码:10000001
进行下一次循环时,i=127, a[127]又会在a[126]的基础上减一,得到a[127]的补码:
10000000
有人就会问了,这不是0吗?
从表面上来说,它的值的确是0,但是
00000000
也是0,这样0的表示不就不唯一了吗?这可就麻烦了,出大事了。因此C语言规定 10000000 在这种情况下的值为 -128 ,所以a[127] = -128。再进行下一次循环, i=128, a[128] = a[127] -1 ,会是-129吗?不会,因为 char 的范围是 -128~127,我们再从内存上来看:
10000000 减去1
得到 01111111 ------ 127
因为这次减法之后,符号位变成了0,而正数的原码、反码、补码相同,结果 a[128] = 127,这样一直循环,a[255] = 0,当 strlen 函数查找到 0 ,就会返回 0~254的长度,结果为255。
例7
输出什么?
#include
unsigned char i = 0;
int main()
{
for(i = 0;i<=255;i++)
{
printf("hello world\n");
}
return 0;
}
i 是一个全局的无符号字符类型变量,在for循环内,i 从0增加到255,打印256次 hello world ,因为无符号字符型的取值范围是 0~255 ,如果将 255 再加一,会怎么样呢?
255的原反补码:11111111
若在此基础上加1,结果会变成
1 00000000
此时最高位会溢出,char 类型截取了低8位,得到:
0000 0000
所以255 + 1 的结果是 0,这样 ,0 <= 255,又可以循环下去,因此该程序会死循环打印 hello world。
经过了上面若干道题的思考,整型的提升与截断应该很简单了吧。
在进行整型提升时,观察其为有符号数还是无符号数,若是有符号数,则按符号位进行提升;若是无符号数,则在高位补0;截断时,要知道应该截取多少位,从低字节到高字节进行截取。另外我们还可以发现字符类型的加减有以下规律:
既然我们了解了整型在内存中是如何存储的,那我们思考,浮点数与整数的最大不同就是要表示小数点与小数,浮点数的存储是否也像整型一样呢?我们来看接下来的一个例子:
#include
int main()
{
int n = 9;
float * p = (float*)&n;
printf("n的值为%d\n",n);
printf("*p的值为%f\n",*p);
*p = 9.0;
printf("n的值为%d\n",n);
printf("*p的值为%f\n",*p);
return 0;
}
如果浮点型数据的存储与整型数据的存储一样,那么我们会很自信的说出程序的结果是 :
n的值为9
*p的值为9.000000
n的值为9
*p的值为9.000000
我们来看一下结果:
结果出乎我们的意外,这样的结果说明了一件事:就是浮点型的存储方式与整型的存储方式是不同的。接下来我们来详细了解浮点型的存储方式。
根据国际标准IEEE(电气和电子工程协会) 754,任意一个二进制浮点数V可以表示成下面的形式:
(-1)^S * M * 2^E
(-1)^S表示符号位,当S=0,V为正数;当S=1,V为负数。
M表示有效数字,大于等于1,小于2。
2^E表示指数位。
我们举以下的例子来说明:
十进制数 5.0
用二进制表示为101.0
相当于 1.01×22
S=0 ,M =1.01 ,E=2
十进制数 -7.25
用二进制表示为-111.01
相当于 -1.1101×22
S=1 ,M=1.1101 ,E=2
IEEE 754规定: 对于32位的浮点数,最高的1位是符号位S,接着的8位是指数E,剩下的23位为有效数字M。
对于64位的浮点数,最高的1位是符号位S,接着的11位是指数E,剩下的52位为有效数字M。
IEEE 754对有效数字M和指数E,还有一些特别规定。 前面说过, 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。比如210的E是10,所以保存为32位浮点数时,必须保存成10+127=137,即10001001。
然后,指数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)。
现在我们再用学过的知识来解释以下这一节刚开始的例子。为什么0x00000009 还原成浮点数,就成了0.000000呢?我们将9拆分为二进制数:
0000 0000 0000 0000 0000 0000 0000 1001
得到第一位符号位S=0,后面8位的指数E=00000000 ,最后23位的有效数字M=000 0000 0000 0000 00001001。浮点数就写成:(-1)0 × 0.00000000000000000001001×2(-126)=1.001×2(-146) 显然,这是一个很小的接近于0的正数,所以用十进制小数表示就是0.000000。
请问浮点数9.0,如何用二进制表示?还原成十进制又是多少? 首先,浮点数9.0等于二进制的1001.0,即1.001×23。
9.0 -> 1001.0 ->(-1)0×1.00123
S=0, M=1.001,E=3+127=130
得到二进制序列:
0 10000010 001 0000 0000 0000 0000 0000
还原成十进制数,即为1091567161。
这些就是浮点型在内存中的存储。
以上就是该篇文章的全部内容,感谢读者老爷们的观看,别忘了点赞支持下噢~