目录
- 一、数据类型
- 1.1 数据类型介绍
- 1.2 类型的意义
- 1.3 类型的基本归类
- 整形家族
- 浮点数家族
- 构造类型(自定义类型)
- 指针类型
- 空类型
- 二、整型在内存中的存储
- 2.1 原反补
- 2.2 为什么内存中要存补码?
- 2.3 大小端介绍
- 2.4 为什么会有大小端之分
- 2.5 写一个程序 判断当前机器的字节序
- 三、练习
- 3.1 %d打印需要整型提升
- 3.2 %u打印char
- 3.3 %u打印溢出的char
- 3.4 理解%u和%d
- 3.5 unsigned int的范围
- 3.6 signed char的取值范围
- 3.7 unsigned char的取值范围
- 四、浮点数在内存中的存储
- 4.1 常见的浮点数
- 4.2 浮点数的取值范围
- 4.3 问题引入
- 4.4 浮点数在计算机内部的表示方法
- 4.5 具体怎么存入内存
- 4.6 以5.5为例
- 4.7 以9.0为例
- 4.8 以3.14为例(为什么不能精确保存?)
- 4.9 回到4.3问题引入
一、数据类型
1.1 数据类型介绍
基本的内置类型:
1.2 类型的意义
- 使用这个类型开辟内存空间的大小(大小决定了使用范围)
- 类型决定了看待内存空间的视角(同样是4字节 是整型还是浮点型呢?)
1.3 类型的基本归类
整形家族
- 字符在存储的时候 存储的是其对应的ASCII码值对应的二进制序列 所以char也被归为整形家族
- 平时写的short num 其实是short int num 短整型 int可以省略
- long long也可以加进来 unsigned/signed long long [int]
- int/long/long long num 其实等价于 signed int/long/long long num 默认是有符号的
- 但是char ch 并不一定等价于 signed char ch 这里的char到底是signed还是unsigned C语言没有明确规定 这取决于编译器 常见的编译器char一般都是等价于signed char的
浮点数家族
构造类型(自定义类型)
- 数组也是自定义类型 int [10]; int [11]; char [5]…都是不同的类型
指针类型
空类型
二、整型在内存中的存储
2.1 原反补
对整数来说
在内存里存的是补码(下图显示的是二进制补码对应的十六进制形式)
计算机中的整数有三种2进制表示方法 即原码 反码和补码
三种表示方法均有符号位和数值位两部分
符号位都是用0表示“正” 用1表示“负”
正数的原、反、补码都相同
负整数的三种表示方法各不相同:
原码: 直接将数值按照正负数的形式翻译成二进制就可以得到原码
反码: 将原码的符号位不变 其他位依次按位取反就可以得到反码
补码: 反码+1就得到补码
正数负数(只要是整数) 内存里存的都是补码
2.2 为什么内存中要存补码?
在计算机系统中
数值一律用补码来表示和存储
原因在于:
使用补码 可以将符号位和数值域统一处理(下面有例子 包括符号位在内直接一起算)
同时 加法和减法也可以统一处理(CPU只有加法器)
此外 补码与原码相互转换 其运算过程是相同的 不需要额外的硬件电路
如下图 计算1-1的时候
其实就是1+(-1) 因为CPU只有加法器
如果用原码直接算 结果显然错误
用补码计算就对了
补码相加的结果给int 最高位那个1被截断了
计算结果就是:000000…00 其实也是补码
发现补码最高位是0 则正数的原反补相同 结果就是0
2.3 大小端介绍
- 大端(存储)模式:
是指数据的低位保存在内存的高地址中;而数据的高位 保存在内存的低地址中; - 小端(存储)模式:
是指数据的低位保存在内存的低地址中;而数据的高位 保存在内存的高地址中。 - 注意:大小端字节序指的是数据在电脑上存储的 字节顺序
放进去还要拿出来 所以放进去的规则尽量要简单
如果是乱放 还要记住放进去的规则 才能拿出来
所以存储的时候 只有大小端两种方式 因为最简单
2.4 为什么会有大小端之分
就是因为在内存中存储超过1字节的数据的时候 必然存在字节序的问题
在计算机系统中 我们是以字节为单位的 每个地址单元都对应着一个字节 一个字节为8 bit
但是在C语言中除了8 bit的char之外 还有16 bit的short型 32 bit的long型(要看具体的编译器)
另外 对于位数大于8位的处理器 例如16位或者32位的处理器 由于寄存器宽度大于一个字节 那么必然存在着一个如何将多个字节安排的问题
因此就导致了大端存储模式和小端存储模式
例如:
一个 16bit 的 short 型 x ,在内存中的地址为 0x0010 , x 的值为 0x1122 ,那么 0x11 为高字节, 0x22 为低字节。
对于大端模式,就将 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。
小端模式,刚好相反。
我们常用的 X86 结构是小端模式,而 KEIL C51 则为大端模式。
很多的ARM,DSP都为小端模式。
有些ARM处理器还可以由硬件来选择是大端模式还是小端模式。
2.5 写一个程序 判断当前机器的字节序
就直接看看怎么存1的 1默认是int 32位:
00000000 00000000 00000000 00000001(补码)
即00 00 00 01
如果是大端:00 00 00 01
如果是小端:01 00 00 00
看低地址处第一个字节存的是00还是01即可
可以简化:
三、练习
3.1 %d打印需要整型提升
- 在VS中 char默认是signed char
- 有符号:整型提升按照符号位提升
- 无符号:整型提升高位直接补0
- %d打印的是有符号整数 对abc打印的时候 首先需要整型提升
char a 和 signed char b的过程一模一样
%d打印a的时候 发现补码是1111…1111 最高位是1 所以要计算得出原码才是打印的结果 补码111…1111对应原码就是-1需要注意的是无符号的c:
-1是一个int 32位 他的补码是1111…111 把他给一个无符号的c 需要截断成11111111
用%d打印c的时候 也就是把c当成一个有符号的整数来打印
首先c被整型提升成–>0000…000011111111(因为c是unsigned的 所以直接补0 提升的结果还是补码)
%d把此补码视为一个有符号数来打印 发现最高位也就是符号位为0 所以是正数 所以原反补相同
所以打印的就是000…000011111111对应的数值 也就是256-1=255
答案是 -1 -1 255
3.2 %u打印char
-128存入char 存的补码是10000000(背下来 本质上也是截断了再给char 的)
用%u %d打印a 也算参与运算了 a首先也需要整型提升(整型提升的规则都一样 跟%u还是%d没关系)
既然要整型提升 那就看看a是不是有符号的 发现a是有符号的(char默认是signed char) 所以按照最高位1提升
最终a会被提升为11111111 11111111 11111111 10000000
如果是%u打印 当成无符号数打印 就直接打印其对应的数值 很大一个数
如果是%d打印 当成有符号数打印 一看最高位是1 要计算出原码 再打印原码对应的数值 即-128
总结:
1.%u %d打印之前 都需要整型提升
2.提升完 %u直接当成无符号数打印 不讨论原反补 或者说原反补同码 直接打印对应的值
3.提升完 %d当成有符号数打印 需要翻译成原码打印
3.3 %u打印溢出的char
我的理解:
char a = 127 + 1 = -128(操作符那一章讲过)
其实和3.2没区别了
下面是详细过程:
发现截断之后 a确实放的也还是1000000
%u打印是很大的数
%d打印也是-128
3.4 理解%u和%d
把一个数存到内存之后 不管该数据本身是有符号还是无符号
%u就是把内存存的补码当做无符号(原反补同码)
%d就是把内存存的补码当做有符号(打印的是原码 所以如果最高位是1 还需要计算)
i+j是补码计算 计算机里都是补码在计算!!!
计算的结果 仍然是个补码!!!
%d认为内存里存的那个二进制序列(补码) 是有符号数 所以打印的时候要计算原码 (符号位0 不需要算 符号位1 需要算)
%u认为内存里存的那个二进制序列(补码) 是无符号数 直接打印对应的值(原反补同码)
注意:严格来说 i+j是发生了算术转换的 int–>unsigned int
但是虽然计算的结果应该是个无符号的数 但是这里并不影响结果
因为 %u不管你存的是不是无符号 反正它就把内存的二进制序列看做无符号数来打印
3.5 unsigned int的范围
其实理解了之前说的char和unsigned char 这题就很好理解
脑海里类比那个表格 0 1 2 3 4 5…255
其实也是一个轮回
9 8 7 6 5 4 3 2 1 0 232-1 232-2 … 9 8 7 6 … 2 1 0
这里对应到 unsigned int 就是[0,232-1]
事实上
当00000000 00000000 00000000 00000000 打印完 再-1
得到的是32个1
其实就是把-1放到i里去 放到unsigned int 全给你当做有效位 是一个很大的数
3.6 signed char的取值范围
'\0’对应的码值就是0 所以把0赋给char[i] 0就会被解析成字符串结束标志
想象之前画的表格 char数组里会放进去如下内容:
-1 -2 -3 -4 -5 … -128 127 126 125 … 1 0 -1 -2 -3 …
第一次0之前 出现了128+127=255个元素
那么strlen求到的就是255
0后面的元素 strlen就不关心了
3.7 unsigned char的取值范围
unsigned char的范围是[0,255] 255+1=0 会死循环
或者理解成:i肯定是<=255的 判断条件恒成立
所以说如果循环的判断条件的变量是无符号数 一定要当心
四、浮点数在内存中的存储
4.1 常见的浮点数
- 1E10 = 1.0 × 1010
- 3.1415926 字面量
- 浮点数家族有float; double; long double
4.2 浮点数的取值范围
4.3 问题引入
下图能看出整型放进去 整型拿出来 或者 浮点型放进去 浮点型拿出来 才是正常的结果
也可以看出指针类型的作用 *float被解引用的时候 权限也是4字节 但是被当做一个浮点型了
接下来就探讨浮点数到底怎么存在内存中的
为什么浮点型9.0放进去之后 用%d当做整型拿出来 差异会这么大?
4.4 浮点数在计算机内部的表示方法
根据国际标准IEEE754(电气和电子工程协会)
任意一个二进制浮点数V可以表示成下面的形式:
V = (-1)^S * M * 2^E
- (-1)^S表示符号位 当S=0 V为正数;当S=1 V为负数
- M表示有效数字 M大于等于1 小于2
- 2^E表示指数位
- 任何一个浮点数用这种方式表示出来 只是SME不一样 那么就把SME存起来即可
- 注意二进制小数的权重
举例来说:
- 十进制的5.0 写成二进制是:101.0 相当于(-1)0×1.01×22
按照上面的格式 可以得出
S=0 M=1.01 E=2 - 十进制的-5.0 写成二进制是 -101.0 相当于(-1)1×1.01×22
S=1 M=1.01 E=2 - 十进制的5.5 写成二进制是:101.1(1的权重是2-1) 相当于(-1)0×1.011×22
S=0 M=1.011 E=2
4.5 具体怎么存入内存
前面说每个浮点数对应一个SME的表示方法 那就把SME存进去 相当于把每个浮点数都存进去了
IEEE 754规定:
对于32位的(4字节float)浮点数
最高的1位是符号位S 接着的8位是指数E 剩下的23位为有效数字M
对于64位的(8字节double)浮点数
最高的1位是符号位S 接着的11位是指数E 剩下的52位为有效数字M
S就是0或者1 直接拿起来存在第一位即可 需要特殊讨论一下M和E
M:
前面说过 1≤M<2
也就是说 M可以写成 1.xxxxxx 的形式 其中xxxxxx表示小数部分
因此IEEE754规定 在计算机内部保存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是可以出现负数的(比如V=0.5的例子)
所以IEEE 754规定:
存入内存时 E的真实值必须再加上一个中间数(拿出来的时候再减去中间数)
对于8位的E 这个中间数是127
对于11位的E 这个中间数是1023
比如 2^10的E是10 所以保存成32位浮点数时
必须保存成10+127=137 即10001001
然后 指数E从内存中取出还可以再分成三种情况:
- E不全为0 或 不全为1(就是有1也有0)
指数E的计算值减去127(或1023)
得到真实值 再将有效数字M前加上第一位的1
就可以还原出真实值
比如:
0.5的二进制形式为0.1 即1.0*2^(-1)
其阶码为-1+127=126
表示为01111110
而尾数1.0去掉整数部分后为0
补齐0到23位00000000000000000000000
则其二进制表示形式为:
0 01111110 00000000000000000000000
- E全为0(比如2-127 2-383)
这时 浮点数的指数E等于1-127(或者1-1023)
有效数字M不再加上第一位的1 而是还原为0.xxxxxx的小数
这样做是为了表示±0 以及接近于0的很小的数字
- E全为1(比如2128)
这时 表示±无穷大(正负取决于符号位S)
4.6 以5.5为例
再往外拿的时候
S:0
E:发现有1有0 按照规则1 先减去127 再拿出来 拿到2
M:拿出011 再补个1 也就是1.011
则可以得到V=(-1)0 × 1.011 × 22 = 101.1(二进制)
翻译成十进制:5.5
可以在内存观察计算的对不对:
4.7 以9.0为例
9.0 = V = 1001.0 = (-1)0 × 1.001 × 23
则:
S=0 存0
M=1.001 存001再补全0到23位 存00100000000000000000000
E=3 加上127 得到130 存10000010
综上存:0 10000010 00100000000000000000000
十六进制:41 10 00 00
4.8 以3.14为例(为什么不能精确保存?)
求3.14的二进制的时候 3很好算是11
但是0.14 怎么用2-1 2-2 2-3…2-n给他精确的凑出来呢? 很困难!!!
而且不管是32位的float 还是64为的double
能给M用的最多是23或者52位 是有限的位数
有时候就是无法精确表示出一个小数!!!
所以我们之前遇到的下图的疑惑就解开了 有些浮点数就是不能精确保存!!!
同样也是由于double的M的位数更多
所以相对来说 double更精确!!
4.9 回到4.3问题引入
-
n的补码是00000000 00000000 00000000 00001001 用%d打印没有任何问题 就是9
-
解引用*pFloat 把上面的补码看成浮点数SEM的规则了
也就是
0(S) 00000000(E) 00000000000000000001001(M)
发现E全是0 回头看规则2
M还原的时候不要补1 而是直接补0
翻译成十进制小数就是:
(-1)0 × 0.00000000000000000001001 × 21-127
是一个非常小的数 打印出来只能看到0.000000 -
*pFloat = 9.0 就是正常把9.0以浮点数的SME放进去
这里4.7已经详细说过了 存的就是:
0(S) 10000010(E+127) 00100000000000000000000(M)
然后用%f打印解引用pFloat 就是符合SME的规则的 拿到9.0 -
但是用%d打印的话 3.存的SME的规则的二进制序列 又被解读成了一个32位的有符号整型的补码:
01000001 00010000 00000000 00000000
发现最高位是0 则原反补同码
直接打印其对应的数值