本章重点
1. 数据类型详细介绍2. 整形在内存中的存储:原码、反码、补码3. 大小端字节序介绍及判断4. 浮点型在内存中的存储解析
Ⅰ、数据类型介绍
前面我们已经学习了基本的内置类型:
空间大小(字节)char //字符数据类型 1short //短整型 2int //整形 4long //长整型 4long long //更长的整形 8float //单精度浮点数 4double //双精度浮点数 8
类型的意义:
1. 使用这个类型开辟内存空间的大小(大小决定了使用范围)。2. 如何看待内存空间的视角。
1.类型的基本归类:
整形家族:
charunsigned charsigned charshortunsigned short [ int ]signed short [ int ]intunsigned intsigned intlongunsigned long [ int ]signed long [ int ]
浮点数家族:
floatdouble
构造类型:
> 数组类型> 结构体类型 struct> 枚举类型 enum> 联合类型 union
指针类型:
int * pi ;char * pc ;float* pf ;void* pv ;
空类型:
void 表示空类型(无类型)通常应用于函数的返回类型、函数的参数、指针类型。
Ⅱ、整形在内存中的存储
一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同的类型而决定的。
接下来我们谈谈数据在所开辟内存中到底是如何存储的。
比如:
int a = 20 ;int b = - 10 ;
我们知道为 a 分配四个字节的空间。
那如何存储?
下来了解下面的概念:
1 .原码、反码、补码
计算机中的整数有三种 2 进制表示方法,即原码、反码和补码。
三种表示方法均有 符号位 和 数值位 两部分,符号位都是用 0 表示 “ 正 ” ,用 1 表示 “ 负 ” ,而数值位
正数的原、反、补码都相同。
负整数的三种表示方法各不相同。
原码直接将数值按照正负数的形式翻译成二进制就可以得到原码。反码将原码的符号位不变,其他位依次按位取反就可以得到反码。补码反码 +1 就得到补码。
对于整形来说:数据存放内存中其实存放的是补码。
在计算机系统中,数值一律用补码来表示和存储。原因在于,使用补码,可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理( CPU 只有加法器 )此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
我们看看内存中的储存:
我们可以看到对于 a 和 b 分别存储的是补码。但是我们发现顺序有点 不对劲 。
这是又为什么?
2. 大小端介绍
什么大端小端:
大端(存储)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中;
小端(存储)模式,是指数据的 低位 保存在内存的 低地址 中,而数据的 高位 , ,保存在内存的 高地址 中。
为什么有大端和小端:
为什么会有大小端模式之分呢?这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为8 bit 。但是在 C 语言中除了 8 bit 的 char 之外,还有 16 bit 的 short型,32 bit 的 long 型(要看具体的编译器),另外,对于位数大于 8 位的处理器,例如 16 位或者 32位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就导致了大端存储模式和小端存储模式。总的来说: 一个地址单元对应一个字节,那一个数据(int,double...) 就可能涉及到多个地址单元,这时候从哪边开始存数据在不同的专家的观点上就出现了不同的意见,从而出现了大小端。
笔试题:
请简述大端字节序和小端字节序的概念,设计一个小程序来判断当前机器的字节序。( 10 分)
这个题我们用两个代码来看,一个是普通的强制类型转换,一个是联合体,联合体我们后面再聊,今天聊强转类型转换:
我们都知道,一个int占4个字节,当我们用int去存储数字1的时候,他只会用其中一个字节去存储他并且数字 1位于32位中的最低位,将数字 1 存入int后,我们将变量的地址取出来强制类型转换为 char* 。由于(char*)类型指针每次解引用只会访问一个字节,他就会访问 int 变量地址的第一个字节,如果我们得到 1 则说明 最低位存到了地位——小端存储,如果得到的是0,则说明高位上的 0 存到了低位——大端存储;
//代码1
#include <stdio.h>
int check_sys()
{int i = 1;return (*(char *)&i);
}
int main()
{int ret = check_sys();if(ret == 1){printf("小端\n");}else{printf("大端\n");}return 0;
}
//代码2
int check_sys()
{union{int i;char c;}un;un.i = 1;return un.c;
}
3 练习:
1.
//输出什么?
#include <stdio.h>
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;
}
2.
#include <stdio.h>
int main()
{char a = -128;printf("%u\n",a);return 0;
}
3.
#include <stdio.h>
int main()
{char a = 128;printf("%u\n",a);return 0;
}
4.
int i= -20;
unsigned int j = 10;
printf("%d\n", i+j);
//按照补码的形式进行运算,最后格式化成为有符号整数
5.
unsigned int i;
for(i = 9; i >= 0; i--)
{printf("%u\n",i);
}
6.
int main()
{char a[1000];int i;for(i=0; i<1000; i++){a[i] = -1-i;}printf("%d",strlen(a));return 0;
}
7.
#include <stdio.h>
unsigned char i = 0;
int main()
{for(i = 0;i<=255;i++){printf("hello world\n");}return 0;
}
详解:
1.
//练习//输出什么?
#include <stdio.h>
int main()
{char a = -1;//10000001 a在内存中的原码//11111110 a的补码//11111111 a的补码--机器中存储的//11111111 11111111 11111111 11111111 整型提升之后的十进制补码//10000000 00000000 00000000 00000001 取反加一后得到原码--打印出来即a= -1signed char b = -1;//在当前编译器环境下,char默认是 有符号char 所以 b和a打印出来是一样的unsigned char c = -1; //虽然c是无符号的char,但是他机器中存的依旧是-1的补码,//因为赋值是先将-1存进内存再根据接收类型的需要的需要进行读取//10000001 a在内存中的原码//11111110 a的补码//11111111 a的补码--机器中存储的// //打印c的时候是要转为整形,进行整形提升,整形提升是按照符号位提升的,这里C是无符号数// 记住::::::这里不是看-1 看的是C //00000000 00000000 00000000 11111111 (无符号) C 进行整形提升后//%d 是按照十进制有符号数打印,他会将最高位看作为符号位 所以打印出来为——255printf("a=%d,b=%d,c=%d", a, b, c);return 0;
}
输出结果
2.
#include <stdio.h>
int main()
{char a = -128;//10000000 补码//11111111 11111111 11111111 10000000 整形提升后的结果//%u是按照10进制无符号数打印 这里将a看作无符号数打印不需要取反加一//按照补码的形式进行运算,最后格式化成为有符号整数printf("%u\n", a);return 0;
}
3.
#include <stdio.h>
int main()
{char a = 128;//此编译器环境下,char默认有符号,128显然超过了有符号char的表示范围,经过编译器处理 a在机器中存的是//10000000 a的机器码//有符号的a整形提升//11111111 11111111 11111111 10000000 整形提升后的结果//按照无符号数打印 按照补码的形式进行运算,最后格式化成为有符号整数printf("%u\n", a);return 0;
}
输出结果跟第二个题一样
4.
int main()
{int i = -20;//10000000 00000000 00000000 00010100 原码//11111111 11111111 11111111 11101100 补码unsigned int j = 10;//00000000 00000000 00000000 00001010 原码也是补码printf("%d\n", i + j);//11111111 11111111 11111111 11101100 //00000000 00000000 00000000 00001010//11111111 11111111 11111111 11110110 i与j的补码相加// 按照有符号十进制数打印,最高位为1 既要取反加一//10000000 00000000 00000000 00001010 结果 ==== -10//按照补码的形式进行运算,最后格式化成为有符号整数return 0;
}
执行结果:
5.
这个题需要了解一些存储规则,一个数的类型是被他的字节限制住的,超过了他能表示的最大的数他就会截断一部分,相当于转圈圈,这里我们以有符号char与无符号char为例:
有符号数char
无符号数char
然后看我们这个题:
int main()
{unsigned int i;for (i = 9; i >= 0; i--){printf("%u\n", i);}return 0;
}
无符号数int,他最小的数就是0,转圈圈永远不可能会走到负数,所以这个代码会死循环。
通过结果我们还可以看见他也是在慢慢减小,当然这里用的是int不容易看出转圈圈效果,换为char就会看的比较清楚。
输出结果:
6.
这一个题也需要上一个题的铺垫
#include<string.h>
int main()
{char a[1000];int i;for (i = 0; i < 1000; i++){a[i] = -1 - i;}printf("%d", strlen(a));return 0;
}
他从-1开始减,有符号char最多可以减到-128,然后再减到0,然后再见到-128,以次循环。不过这里,strlen统计的是 ‘\0' 之前的字符个数,我们第一次走到0的时候就会停止统计(’\0'的ascll码值是0);我们开始计算,-1 ~ -128是128个字符,127 ~ 1 是127个字符(0不统计!统计的是0之前的字符)这样一相加 127+128=255
输出结果:
7.
这个题也是像上面的一样,转圈圈,死循环
#include <stdio.h>
unsigned char i = 0;
int main()
{for (i = 0; i <= 255; i++){printf("hello world\n");}return 0;
}
执行结果:这里为了方便看,我多打印了一个 i :
Ⅲ、浮点型在内存中的存储
常见的浮点数:
3.141591E10浮点数家族包括: float 、 double 、 long double 类型。浮点数表示的范围: float.h 中定义
1 .浮点数存储规则
根据国际标准 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×2^2 。
那么,按照上面 V 的格式,可以得出 S=0 , M=1.01 , E=2 。
十进制的 -5.0 ,写成二进制是 - 101.0 ,相当于 - 1.01×2^2 。那么, S=1 , M=1.01 , 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 。
比如, 2^10 的 E 是 10 ,所以保存成 32 位浮点数时,必须保存成 10+127=137 ,即
10001001 。
然后,指数 E 从内存中取出还可以再分成三种情况:
E 不全为 0 或不全为 1
这时,浮点数就采用下面的规则表示,即指数 E 的计算值减去 127 (或 1023 ),得到真实值,再将
有效数字 M 前加上第一位的 1 。
比如:
0.5 ( 1/2 )的二进制形式为 0.1 ,由于规定正数部分必须为 1 ,即将小数点右移 1 位,则为
1.0*2^(-1) ,其阶码为 -1+127=126 ,表示为
01111110 ,而尾数 1.0 去掉整数部分为 0 ,补齐 0 到 23 位 00000000000000000000000 ,则其二进
制表示形式为 :
0 01111110 00000000000000000000000
E 全为 0
这时,浮点数的指数 E 等于 1-127 (或者 1-1023 )即为真实值,
有效数字 M 不再加上第一位的 1 ,而是还原为 0.xxxxxx 的小数。这样做是为了表示 ±0 ,以及接近于
0 的很小的数字。
E 全为 1
这时,如果有效数字 M 全为 0 ,表示 ± 无穷大(正负取决于符号位 s );
好了,关于浮点数的表示规则,就说到这里。
这里给一个浮点数存储例子:
int main()
{float a = 5.5;//101.1//(-1)^0 * 1.011 * 2^2// S=0 E=2+127=129=10000001 M =01100000000000000000000 // 01000000 10110000 0000000000000000 // 40 b0 00 00// return 0;
}
下面有一个题:
int main()
{int n = 9;//00000000 00000000 00000000 00001001 整数视角存整数 9float* pFloat = (float*)&n;//0 00000000 00000000000000000001001 用浮点数时间看待9//S=0//E=0-127=-127//M= 00000000000000000001001//*pFloat= (-1)^0 * 00000000000000000001001 * 2^(-127)//这个数是远小于0的 而flost只能打印小数点后六位,所以打印结果为 0.000000printf("n的值为:%d\n", n);//9printf("*pFloat的值为:%f\n", *pFloat);//*pFloat = 9.0;//9.0 //(-1)^0 * 1.001 * 2^3 S=0 E=3+127=130= 10000010 M= 00100000000000000000000//01000001 00010000 00000000 00000000 浮点数视角存储 9.0 ——机器码 // 41 10 00 00printf("num的值为:%d\n", n);//01000001 00010000 00000000 00000000按照10进制打印为1091567616printf("*pFloat的值为:%f\n", *pFloat);//9.0return 0;
}