目录
原码、反码、补码
计算机中的有符号整数有三种二进制表示方法 , 即原码、反码和补码。
三种表示方法均有符号位和数值位两部分,符号位都是用 0 表示 “正” , 用 1 表示 “负”
无符号的数据类型的原码、反码、补码是相同的
有符号的数据类型的反码、补码要经过原码转换得到
原码
按照一个数的正负 , 直接写出它的二进制表示形式得到的就是原码
反码
反码是原码的符号位不变 , 其他位按位取反
补码
补码是反码+1
例:
整型占4个字节 (32bit)
如 10 的:
00000000000000000000000000001010 - 原码
00000000000000000000000000001010 - 反码
00000000000000000000000000001010 - 补码
-10 的
10000000000000000000000000001010 - 原码
111111111111111111111111111111110101 - 反码
111111111111111111111111111111110110 - 补码
原码、反码、补码的互相转化:
(取反:符号位不变 , 其他位按位取反,减去一不好得出结果时,也可以取反+1)
在计算机中,为什么要用用补码存储一个数呢?
在计算机系统中 , 数值一律用补码来表示和存储。原因在于,使用补码 , 可以将符号位和数值域统一处理 ; 同时 , 加法和减法也可以统一处理(CPU只有加法器)此外,补码与原码相互转换,其运算过程是相同的,不需要额外的硬件电路。
一个简单的例子:
如果计算机用原码存储一个数:
00000000 00000000 00000000 00000001 - 1 的原码
10000000 00000000 00000000 00000001 - 负1的原码
我们计算 1 - 1 也就是 1 + (-1)
10000000 00000000 00000000 00000010 - 结果是 -2 ???
这就出现问题了,结果显然不对。
那如果我们使用补码呢?
00000000 00000000 00000000 00000001 - 1 的补码
11111111 11111111 11111111 11111111 - 负1的补码
现在用 1 的补码和 -1 的补码相加:
1 00000000 00000000 00000000 00000000
由于整形占 4 个字节,所以最高位的那个 1 会溢出后被截断,最终的结果是:
00000000 00000000 00000000 00000000
这样结果就正确了,所以计算机要用补码存储一个数,并且用补码计算。
大端存储和小端存储
大端字节序存储
是指数据的低位保存在内存的高地址中 , 而数据的高位保存在内存的低地址中;
小端字节序存储
是指数据的低位保存在内存的低地址中 , 而数据的高位保存在内存的高地址中。
例如,对于十六进制数 0x 11223344:
调试并查看内存:
想出大小端字节序存储的灵感
《格列佛游记》中的一个故事被用来类比计算机科学中的大小端字节序存储问题。在小说中,小人国的臣民为水煮蛋应该从大的一端(Big-End)剥开还是小的一端(Little-End)剥开而争论不休。这个故事被英国计算机科学家丹尼·科恩(Danny Cohen)引用,用来类比数据在内存中的存放序列(字节序)的问题。这导致了“大端(Big-Endian)”和“小端(Little-Endian)”字节序的概念的提出。
为什么会有大小端模式之分呢?
这是因为在计算机系统中,我们是以字节为单位的,每个地址单元都对应着一个字节,一个字节为 8 bit。但是在C语言中除了 8 bit 的 cha r之外,还有 16 bit 的 short 型,32 bit 的 long 型(要看具体的编译器),另外,对于位数大于 8 位的处理器,例如 16 位或者 32 位的处理器,由于寄存器宽度大于一个字节,那么必然存在着一个如何将多个字节安排的问题。因此就产生了大端存储模式和小端存储模式。
例如:一个 16bit 的 short 型 x,在内存中的地址为 0x0010 , x 的值为 0x1122 , 那么 0x11 为高字节 , 0×22 为低字节。对于大端模式 , 就将 0x11 放在低地址中 , 即 0x0010 中, 0×22 放在高地址中 , 即0x0011中。小端模式 , 刚好相反。我们常用的 x86 结构是小端模式,而 KEIL C51 则为大端模式。很多的 ARM , DSP都为小端模式。有些 ARM 处理器还可以由硬件来选择是大端模式还是小端模式。
检查当前机器是大端存储还是小端存储的函数:
int check_sys()
{
int a = 1;
return *(char*)&a;
}
将数据从内存中取出时,要复原数据:
两个头文件
整形类数据(int 、long、long、)的取值范围可以在 limits.h 这个头文件里查阅:
//
// limits.h
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The C Standard Library <limits.h> header.
//
#pragma once
#define _INC_LIMITS
#include <vcruntime.h>
#pragma warning(push)
#pragma warning(disable: _VCRUNTIME_DISABLED_WARNINGS)
_CRT_BEGIN_C_HEADER
#define CHAR_BIT 8
#define SCHAR_MIN (-128)
#define SCHAR_MAX 127
#define UCHAR_MAX 0xff
#ifndef _CHAR_UNSIGNED
#define CHAR_MIN SCHAR_MIN
#define CHAR_MAX SCHAR_MAX
#else
#define CHAR_MIN 0
#define CHAR_MAX UCHAR_MAX
#endif
#define MB_LEN_MAX 5
#define SHRT_MIN (-32768)
#define SHRT_MAX 32767
#define USHRT_MAX 0xffff
#define INT_MIN (-2147483647 - 1)
#define INT_MAX 2147483647
#define UINT_MAX 0xffffffff
#define LONG_MIN (-2147483647L - 1)
#define LONG_MAX 2147483647L
#define ULONG_MAX 0xffffffffUL
#define LLONG_MAX 9223372036854775807i64
#define LLONG_MIN (-9223372036854775807i64 - 1)
#define ULLONG_MAX 0xffffffffffffffffui64
#define _I8_MIN (-127i8 - 1)
#define _I8_MAX 127i8
#define _UI8_MAX 0xffui8
#define _I16_MIN (-32767i16 - 1)
#define _I16_MAX 32767i16
#define _UI16_MAX 0xffffui16
#define _I32_MIN (-2147483647i32 - 1)
#define _I32_MAX 2147483647i32
#define _UI32_MAX 0xffffffffui32
#define _I64_MIN (-9223372036854775807i64 - 1)
#define _I64_MAX 9223372036854775807i64
#define _UI64_MAX 0xffffffffffffffffui64
#ifndef SIZE_MAX
// SIZE_MAX definition must match exactly with stdint.h for modules support.
#ifdef _WIN64
#define SIZE_MAX 0xffffffffffffffffui64
#else
#define SIZE_MAX 0xffffffffui32
#endif
#endif
#ifndef RSIZE_MAX
#define RSIZE_MAX (SIZE_MAX >> 1)
#endif
_CRT_END_C_HEADER
#pragma warning(pop) // _VCRUNTIME_DISABLED_WARNINGS
浮点型数据的取值范围可以在 float.h 这个头文件里查阅:
截断
截断通常指的是将一个数据类型的值赋给另一个数据类型时,由于目标类型的表示范围小于源类型,导致数据的部分信息丢失的现象。这种情况通常发生在将一个较大的数据类型转换为一个较小的数据类型时。
例如,将一个 int 类型的值赋给一个 char 类型的变量时,由于 int 通常是 32 位,而 char 通常是 8位,因此 int 类型的值的高 24 位将被截断,只保留低 8 位的值。这可能会导致数据的精度丢失。
下面是一个简单的例子:
int main()
{
char a = -1;
// -1s是整数,在内存中以32位二进制补码形式存储:11111111 11111111 11111111 11111111
// 由于a是char类型,只能存储8位二进制,所以将-1存储到a时要发生截断,存放到a中的数实际上是:11111111
return 0;
}
溢出
在C语言中,溢出是指当一个变量的值超出了其数据类型所能表示的范围时发生的情况。C语言中的整数类型(如int、short、long等)都有固定的范围,这些范围由编译器和平台决定,但通常是基于标准的(如ISO C标准)”。
溢出的类型
● 无符号整型溢出:
对于unsigned整型溢出,C的规范是有定义的 -- “溢出后的数会以2^(8 *sizeof(type))作模运算”。例如unsigned char (1字节,8 bits)溢出了,会把溢出的值与2 56求模”。
● 符号整型溢出:
对于signed char,正整数最大值为127,负整数最小值为-128,十进制转二进制运算时直接将高于八位的数字直接舍弃。由于整数运算在计算机内部都是通过补码进行处理,因此需要将十进制整型转换为二进制补码进行运算(正数的原码补码相同,负数原码取反(符号位不动),加一得到补码)。运算完成得到的是补码,有以下两种情况:”
● 如果第八位(符号位)为0,得到的是一个正数,则舍去溢出的高位,直接将二进制
转换十进制即可 ”。
● 如果第八位(符号位)为1,说明得到的是一个负数,则舍去溢出的高位,将此补码转换成原码(逆运算,减一再取反)”。
溢出的影响
整数溢出可能导致不可预测的结果。在某些情况下,程序可能会崩溃或产生错误的输出。在一些安全关键的系统中,整数溢出可能导致严重的安全漏洞”。
处理溢出的方法
● 选择合适的数据类型:在进行编程时,根据可能出现的数值范围,选择足够大的整数类型
来避免溢出。例如,如果预计数值可能会很大,优先考虑使用long long类型”。
● 使用条件判断检测溢出:在进行运算之前,可以先判断运算是否会导致溢出。例如,在进行加法运算之前,可以先判断是否会发生溢出,如果会发生溢出,则返回 -1 并打印提示信息”。
● 使用库函数:一些标准库提供了处理整数运算的函数,可以检测和处理溢出。例如,在< stdint.h>头文件中,提供了一些固定宽度的整数类型(如int16_t、int32_t等)以及相应的运算函数”。
● 使用大数运算库:如果需要处理非常大的数字,可以考虑使用大数运算库,如GNU MP库、BigInt库等。这些库提供了高精度的数值计算功能,可以处理超出常规数据类型表示范围的数据”。
隐式类型转换(整形提升)
C的整型算术运算总是至少以缺省(默认)整型类型的精度来进行的。
为了获得这个精度,表达式中的字符(1个字节)和短整型(两个字节)操作数在使用之前被转换为普通整型(int), 这种转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数的字节长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。
因此,即使两个char类型的相加,在CPU执行时实际上也要先转换为CPU内整型操作数的标准长度。通用CPU(general-purpose CPU)是难以直接实现两个8比特字节直接相加运算(虽然机器指令中可能有这种字节相加指令)。
所以,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
例如:
char a = 1;
char b = 2;
char c = a + b;
a 和 b 的值被提升为整形,然后再执行加法运算。
加法运算完成之后,结果将被截断,然后存储于 c 中。
如何进行整形提升?
整形提升是按照变量的数据类型的符号位来提升的,有符号的数据类型整形提升时在补码的左边补充符号位的数(是0就补0,是1就补1),无符号的数据类型整形提升时在补码的左边补充0(是0是1都补0),补充到 32 位(四字节)。
以 char 为例:
char c1 = - 1;
假设现在 c1 要发生整形提升了
变量c1的二进制位(补码)中只有8个比特位:
11111111
因为 char 为有符号的 char 所以整形提升的时候 , 高位补充符号位 , 即为1
提升之后的结果是:
11111111111111111111111111111111
char c2 = 1;
变量c2的二进制位(补码)中只有8个比特位:
00000001
因为 char 为有符号的 char 所以整形提升的时候 , 高位补充符号位 , 即为0
提升之后的结果是:
00000000000000000000000000000001
//无符号整形提升,高位补0
分析以下代码:
计算过程:
char a = 3 ;
3 的原码:00000000000000000000000000000011
由于 char 类型只占 1 个字节,所以将 3 存储到 a 时要发生截断,因此:
a 的补码:00000011(内存中以补码形式存储)
char b = 127;
b 的补码:01111111
char c = a + b;
要先对 a、b 进行整形提升:
a 整形提升后:00000000000000000000000000000011
b 整形提升后:00000000000000000000000001111111
a + b : 00000000000000000000000010000010
截断后存储到 c :
c 的补码:10000010(最高位是 1 ,c 已经是负数了)
printf("%d", c);
%d 是以有符号十进制整数形式打印二进制原码,所以要将 c 进行整形提升,再找到 c 的原码。
c 进行整形提升: 11111111111111111111111110000010
取反+1得到原码:100000000000000000000001111110 (这是 -126 的原码)
所以打印的是 -126 。
有符号的 char 类型(signed char)的取值范围是 -128 到 127 ,如何得来的?
signed char 类型占 1 个字节即 8 bit ,1 个 bit 有 0 或者 1 两种可能,8 个 bit 可以有 2 的 8 次方即 256 种组合,可以表示十进制的0 到 257,但最高位为符号位,即表示数值实际有 7 个 bit 位(128 种组合),当最高位为 0 (表示正数)时可以表示十进制的 0 到 127 ,当最高位为 1 (表示负数)时可以表示十进制的 -128(10000000 规定为 -128) 到 -1 (11111111),所以,有符号的 char 类型(signed char)的取值范围是 -128 到 127 。其他类型,如 int 类型可以依此类推。
对于 unsigned char (无符号字符型)最高位不再表示符号,此时 8 个 bit 位都表示数值,所以 unsigned char 的取值范围是 0 到 255 ,其他类型,如 unsigned int 类型也可以依此类推。
char a = 127;(a:01111111)此时对 a 加一,(a + 1:10000000,这个二进制数规定为 -128)a 的值为 -128,再对 a 加一,(a + 2:10000001,-127)a 的值为 -127,一直对 a 加一,a 的值会从 -127 到 -126 ... 一直到 -1(11111111)再加一(100000000,1 溢出,a 的值变为00000000,即0)a 的值变为 0。
所以说,对一个 char 类型的数无休止的加一,这个数不会一直增加,它的值呈现为在一定范围内周期性的变化。(从 1 数到 100 ,个位总是在 0-9 循环变化)
请分析以下代码:
最后输出了 255 ,就是因为上文介绍的规律(strlen 碰到数字 0 停下)
请分析以下代码:
最后只打印出了 c ,请思考为什么
(tip:0xb6 表示十六进制数字 b6 ,二进制形式为10110110,十六进制数字每一位占半个个字节, ==运算有时也要整形提升 )
下面这个例子也说明了整形提升的存在:( “+”也是操作符,对 c 要整形提升)
数据的输出
%d - 以有符号十进制整数形式输出二进制原码
printf(“%d”,a)
首先判断 a 的数据类型是不是 4 个字节的(32位)
不满 32 位:
对 a 按照整形提升的规则(有符号的数据类型整形提升时补码的高位补充符号位的数,无符号的数据类型整形提升时补码的高位补充0,补充到 32 位(四字节))进行整形提升。
整形提升后再看 a 是有符号的还是无符号的。
如果是有符号的,算出 a 的原码(整形提升后的二进制数是补码)(正数的原码就是补码,负数的原码是补码取反加 1 ),算出的原码再看它的最高位,如果是 1 ,就会输出负数,如果是 0 ,就会输出正数。输出的具体数值就要看算出的原码的数值位(后 31 位)。
如果是无符号的,最高位不再表示符号位,直接输出算出的原码的表示的十进制数值
比 32 位多:
将最左边多于 32 位的 bit 位截断,作为要输出的数的补码,之后的过程与上述进行整形提升后相同。
其他输出格式控制符如 %u、%ld、%lld 可与上述类比。
%f 的读取方式见下文。
浮点数在内存中的存储
引例:
从这个例子可以看出用整数的方式存储一个数但用浮点数的方式输出一个数,或者用浮点数的方式存储一个数但用整数的方式输出一个数,得到的结果往往与预期不同。
浮点数存储规则
num 和 *pFloat 在内存中明明是同一个数 , 为什么浮点数和整数的解读结果会差别这么大?
要理解这个结果,一定要搞懂浮点数在计算机内部的表示方法。
详细解读:
根据国际标准IEEE(电气和电子工程协会) 754 , 任意一个二进制浮点数 V 可以表示成下面的形式:
(-1)^S * M * 2^E ((-1)^S 表示 -1 的 S 次方)
● (-1) ^ S 表示符号位 , 当 S = 0 , V 为正数,当 S = 1 ,V 为负数;
● M 表示有效数字 , 大于等于1 , 小于2。
● 2^E 表示指数位。
例子:
v = 5.5(十进制)
= 101.1 (二进制,小数点后的1表示 2^(-1)即十进制 0.5)
= 1.011 * 2^2
= (-1)^ 0 * 1.011 * 2^2 (S = 0;M = 1.011;E = 2)
每一个浮点数都唯一对应一对 S、M 和 E,因此要存储一个浮点数,只要存储它的 S、M 和 E。
IEEE 754 规定:
对于 32 位的浮点数 , 最高的 1 位是符号位 S , 接着的 8 位是指数 E , 剩下的 23 位为有效数字 M。
| | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | | |
S(1bit) E(8bit) M(23bit)
而对于 64 位的浮点数,S占1bit,E占11bit,M占52bit。
S、M 和 E 怎么存储到内存里去
S = 0(二进制),是正数,S = 1(二进制),是负数。
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 位浮点数时 , E必须保存成 10+127=137, 即10001001 , 再放入 E 的存储单元 ,等到要用到这个浮点数时 ,把 E 取出来时再减去这个中间数就行。
比如:
0.5 的二进制形式为 0.1 , 由于规定 M 部分必须为 1.xxxxxxx 这样的形式 , 现将小数点右移 1 位 , 则为 1.0 * 2 ^ (-1) 而 1.0 去掉 1 后为 0 , 在 0 的右边补齐 0 到 23 位 00000000000000000000000 , 其中 E = -1 加上 127 后为 126 , 二进制表示为 01111110 , 则 0.5 在内存中表示形式为:
0 01111110 00000000000000000000000
转化为十六进制表示为:0x 3F 00 00 00
在内存中验证:
E 、M 怎么从内存中取出
指数E从内存中取出可以分成三种情况:
E不全为 0 或不全为 1
这时,取出 E 、M时指数E的计算值减去 127 (或1023) , 得到真实值 , 再将有效数字 M 前加上第一位的1。
E全为0
这时 , 浮点数的指数 E 直接认为等于 -126 (或者 -1023),这是一个非常非常小的数字,有效数字 M 不再加上第一位的 1 , 而是还原为 0.xxxxxx 的小数。这样做是为了表示这是一个接近于0的很小的数字。
E全为1
这时 , 如果有效数字 M 全为 0 , 表示 ± 无穷大 (正负取决于符号位 S );
%f 格式控制符:
假设定义了单精度浮点数 a :float a = 0.5;
现在用printf(“%f”,a)打印 a:
0 01111110 00000000000000000000000 - a 在内存中的表示
%f 首先读取最左边的0 ,即 S = 0,是正数,再读取 01111110 ,01111110 是十进制 126,减去127 后等于 -1,即 E = -1,然后读取 00000000000000000000000 ,在它前面加上 1 就是 1.00000000000000000000000即为 M,所以 %f 读取到的二进制数字就是:
(-1)^ 0 * 1.00000000000000000000000 * 2 ^ (-1) , 即二进制 0.1,转化为十进制就是 0.5了
如果 %f 读取的是整形类数据:
int a = 21
printf(“%f”,a),则也按照上述的读取规律读取,这就可能产生错误,所以打印一个数据要用相应的格式控制符。
相似的,如果用 %d 读取一个单精度浮点数,如:
float a = 0.5;
printf(“%d”,a);
根据 %d 的读取方式,%d 认为 a 的二进制补码是:
0 01111110 00000000000000000000000
最高位是 0 ,说明是正数,原码就是补码,%d 就会输出 0 01111110 00000000000000000000000
表示的十进制数字,这是一个很大的数字。
精度问题
二进制小数点后 n 位的位权是 2 ^ (-n),
由于计算机用采用二进制存储数据,二进制有时不能精确地表示十进制小数,例如,十进制的0.1在二进制中是无限循环小数 0.00011001100110011……,由于浮点数位数有限,无法精确表示,只能近似存储,这就产生了精度问题。