Bootstrap

原码反码补码详解 -浮点数的表示方法 -数据截断、溢出和提升(全网最全)

刚开始发现自己对于整形和浮点数的二进制表示不是很了解的时候,就打算自己整理一下这方面的知识,但是看了一些大神写的文章后,感觉自己写不出这么简洁易懂的文章,所以就直接推荐大神写的文章吧,本文主要做一个知识的整理和相关内容的一个补充。


一、整形的原码反码补码表示,浮点数的表示方法

原码, 反码, 补码 详解

IEEE 754浮点数标准详解

浮点数的表示和基本运算

上面这三篇内容基本上已经把整形和浮点数在计算机中的二进制表示方法说的很清晰了。


二、C语言中打印数值的二进制表示

我们知道,对于数值的表示方法,我们经常使用二进制、十进制、十六进制进行表示。

在C语言中,我们可以使用 %x 来打印一个数值的十六进制,用 %d 来打印一个数值的十进制,就是没有提供可以打印一个数值的二进制的方法(原因未知?有知道的大佬可以在评论区指出)

没有的话当然就要想办法呀,我们都学过用辗转相除法来获得一个数值的二进制表示,那么用代码应该怎么实现呢?

感谢热爱分享的大佬们,感谢开源精神,我又找到一篇写的非常好的文章,呵呵,不是我写不出,只是我不想重复造轮子,哈哈哈,安慰一下自己。

C语言打印数据的二进制格式-原理解析与编程实现

为了方便阅读,我在下面放一下代码

void printf_bin(int num)
{
    int i, j, k;
    unsigned char *p = (unsigned char*)&num + 3;//p先指向num后面第3个字节的地址,即num的最高位字节地址
    for (i = 0; i < 4; i++) //依次处理4个字节(32位)
    {
        j = *(p - i); //取每个字节的首地址,从高位字节到低位字节,即p p-1 p-2 p-3地址处
        for (int k = 7; k >= 0; k--) //处理每个字节的8个位,注意字节内部的二进制数是按照人的习惯存储!
        {
            if (j & (1 << k))//1左移k位,与单前的字节内容j进行或运算,如k=7时,00000000&10000000=0 ->该字节的最高位为0
                printf("1");
            else
                printf("0");
        }
        printf(" ");//每8位加个空格,方便查看
    }
    printf("\r\n");
}

有了上面的代码,我们现在可以在C语言中打印出一个数值二进制。


三、有符号数和无符号数在计算机中的二进制表示

在整形中,我们经常使用的char、short、int都是有符号数,这就意味着其最高位表示符号位。

而在一些不存在负值的情况下,经常使用一种叫着无符号数的数据类型,其二进制表示中不存在符号位,因此相比有符号数可以表示更大的数值,有可能可以节省存储空间且使数值的含义更加明确。在C语言中,unsigned char、unsigned short、unsigned int代表三种无符号整形。

下面我们用一段代码来测试一下,无符号数的二进制表示方法。

void printf_bin_8(unsigned char num)
{
    int k;
    unsigned char *p = (unsigned char*)&num;

    for (int k = 7; k >= 0; k--) //处理8个位
    {
        if (*p & (1 << k))
            printf("1");
        else
            printf("0");
    }
    printf("\r\n");
}

int main() {
	unsigned char a = X;
	printf_bin_8(a);
}

当X = +0/-0,其二进制表示为 00000000

当X= 64,其二进制表示为 01000000

当X= 127,其二进制表示为 01111111

当X= 128,其二进制表示为 10000000

当X= 129,其二进制表示为 10000001

当X= 255,其二进制表示为 11111111

当X = 256,其二进制表示为 00000000(发生了溢出)

当然一些小伙伴也会疑问,我给无符号数赋值一个负数会怎么样呢,嘿嘿,我也不知道,试试看。

当X= -1,其二进制表示为 11111111 ,可以看到-1和255的二进制表示方法是一样的。

我们来分析一下 -1 = (10000001)原码 = (11111111)补码

因为在计算机中所有整形都是通过补码的形式进行存储的 ,所以 -1会被表示成 (11111111)补码

如果我们使用 printf("%d",X) 打印该值,我们会发现打印出来的是255,因为打印X时,会将其认定为 unsigned char型,而-1补码和255的二进制表示是一样的,所以会打印出255。

为了验证,我们再对一个负数进行测试

当X = -15,其二进制表示为 11110001,使用 printf("%d",X) 打印该值打印出的值为241

再看一下当X= 241,其二进制表示为 11110001

X= -15 = (10001111)原码 = (11110001)反码

可以看出和我们上面的想法是一致的。

通过上面我们也可以得出一个结论:在C语言中,整型数据的存储都是使用补码来jing

四、溢出和截断,数据类型的转换

首先我们将C语言中所有的基本数值数据都列举出来,来看看它们之间的运算转换。

  1. 整形类的

    • 有符号数:char, short, int, long
    • 无符号数:unsigned char, unsigned short, unsigned int, unsigned long
  2. 浮点类的

    • float,double

不同数值类型之间的运算

一般我们都是对相同数据类型的数值之间进行+,-,*,/运算。比如:int和int之间进行加减乘除运算,但是不同数据类型之间进行加减乘除运算也是很常见的。下面我们就来了解一下不同数据类型之间运算的规则。

  1. 若一个运算符两侧的数据类型不同,则先自动进行类型转换,使两者具有同一类型,然后进行运算。

  2. 自动类型转换的规则是将低精度的数据转换成高精度的数据

    数据低精度和高精度的概念:对于整型数据char的精度最低,long的精度最高

    整型数据之间的精度由低到高排序为:char < short < int < long

    浮点数据之间的精度由低到高排序为:float < double

    任何整型的数据精度都小于浮点数据,因此总的数据类型精度排序为:char < short < int < long < float < double

    上面的排序还没有考虑到无符号数的精度排序,因为无符号数这边稍微有点复杂我们后面再单独讲。

  3. 具体运算规则:

    • 当两个不同整型数据类型进行运算时,将低精度的整型数据转换成高精度的整型数据再进行运算;

    • 当整型和浮点型进行运算,不管浮点是float还是double,都将进行运算的整型和浮点转成double进行运算;

    • Char型与int型数据进行运算,就是把字符的ASCII码与整型数据进行运算

      如:12+‘A’=12+65=77

    • 两个int型相除,不管是否有余数,结果都为整型;如:5/10 输出是整数部分:0

下面我们用代码来测试一下:

#include <stdio.h>
int main ()
{
	char a1 = 'A', a2 = 'B';
	short s1 = 55, s2 = 66;
	int i1 = 555, i2 = 666;
	float f1 = 2.72, f2 = 22.72;
	double d1 = 3.14, d2 = 31.45;
	
    printf("%d",sizeof(X));
}

sizeof(char) = 1 sizeof(short) = 2 sizeof(int) = 4 sizeof(float) = 4 sizeof(double) = 8

sizeof(a1 + a2) = 4 两个char相加后变成了int,4个字节

sizeof(a1 + s1) = 4 char和short相加变成了int,4个字节

sizeof(s1 + s2) = 4 short和short相加变成了int,4个字节

sizeof(a1 + i1) = 4 char和int相加变成了int,4个字节

sizeof(i1 + i2) = 4 int和int相加变成了int,4个字节

sizeof(i1 + f1) = 4 float和float相加变成了float,4个字节

sizeof(i1 + d1) = 8 int和double相加变成了double,8个字节

sizeof(a1 + d1) = 8 char和double相加变成了double,8个字节

可以看出,当不同整型数据运算时,都优先向int转换;当涉及到浮点数时,则转成浮点数。

整型提升是C程序设计语言中的一项规定:在表达式计算时,各种整形首先要提升为int类型,如果int类型不足以表示则要提升为unsigned int类型;然后执行表达式的运算。


数据的溢出、截断、提升


**溢出:**指给某个变量赋值时,超出了该类型变量所能表达的范围,比如:

unsigned char a;
a = 260;//a的取值范围是0~255,使a=260就会导致数据溢出,a最终得到的是一个错误的值;

对于整型溢出,分为无符号整型溢出和有符号整型溢出。

对于unsigned整型溢出,C的规范是有定义的——“溢出后的数会以2^(8*sizeof(type))作模运算”,也就是说,如果一个unsigned char(1字符,8bits)溢出了,会把溢出的值与256求模。例如:

unsigned char x = 0xff;
printf("%d\n", ++x);

对于signed整型的溢出,C的规范定义是“undefined behavior”,也就是说,编译器爱怎么实现就怎么实现。对于大多数编译器来说,算得啥就是啥。比如:

signed char x =0x7f; //注:0xff就是-1了,因为最高位是1也就是负数了
printf("%d\n", ++x);

上面的代码会输出:-128,因为0x7f + 0x01得到0x80,也就是二进制的1000 0000,符号位为1,负数,后面为全0,就是负的最小数,即-128。

另外,千万别以为signed整型溢出就是负数,这个是不定的。比如:

signed char x = 0x7f;
signed char y = 0x05;
signed char r = x * y;
printf("%d\n", r);

提升:提升是将占字节小的元素赋给占字节大的元素时出现的补位现象。

#include <stdio.h>
int main ()
{
    char a = -50;//a的补码为11001110
    short b = a;
    int c = a;
    
	printf("%d\n",b);//打印值为-50,补码为11111111 11001110
	printf("%d\n",c);//打印值为-50,补码为11111111 11111111 11111111 11001110
}

可以看出对于负数来说,提升是使用低位填充,高位补1的操作。

对于正数,则是低位填充,高位补0的操作。

#include <stdio.h>
int main ()
{
    char a = 50;//a的补码为 00110010
    short b = a;
    int c = a;
    
	printf("%d\n",b);//打印值为50,补码为00000000 00110010
	printf("%d\n",c);//打印值为50,补码为00000000 00000000 00000000 00110010
}

截断:指给某个变量赋值时,超出了该类型能表达的范围,如果采取截断策略,则变量只保留数据中低字节的数据,高字节的数据则会丢弃,即截断是将所占字节大的元素赋给所占字节小的元素时会出现数值的舍去现象。比如

#include <stdio.h>
int main ()
{
	int a = 1431655765; //a的补码为01010101 01010101 10110101 01010101
	short b = a;
	char c = a;
	printf("%d\n",b);//b打印的值为-19115,二进制补码为10110101 01010101
	printf("%d\n",c);//c打印的值为85,二进制补码为01010101
}

可以看出截断的规则是先填充低位,截断高位。


五、联合体实例

下面我们以联合体为例,来讲解一下上面知识的应用。

首先看两段代码:

#include <stdio.h>
union UN
{
	char c;
	int i;
};
int main ()
{
    UN test = {555};
    printf("%d\n",test.i); //打印的test.i的值为43
}
#include <stdio.h>
union UN
{
	int i;
	char c;
};
int main ()
{
    UN test = {555};
    printf("%d\n",test.i);//打印的test.i的值为555
}

上面两段代码就只有联合体UN内部的char和int元素定义的顺序发生了一下变化而已。

其实上面这个和联合体初始化的性质有关

UN test = {555};//这条语句就是会给第一个元素赋值,而555超过了char所能容纳的最大值,所以会发生截断。

555 = (0000001000101011)补码

截断之后只剩下(00101011)补码 = 43

;