补码编码、有符号无符号转化、字符扩展与截断


1. 无符号数的编码

无符号数的编码其实就是我们平时认知的二进制表示形式,比如\(0b0000 1101 = 1*2^{3}+1*2^{2}+1*2^{0} = 13\)

其一般定义为:

对于向量 $\vec{x}=[x_{w-1}, x_{w-2}, ..., x_0]: $

其转化为无符号变量后的值为

\[B2U_w(\vec{x}) = \sum_{i=0}^{w-1}x_i2^i \]

就是基本的数学进制转化表示方法,比较简单。

2. 补码编码

2.1补码(two's complement)

我们不仅仅需要表示无符号的值,有些时候我们也要表示有符号的值,而计算机最常见的表示有符号数的方式为补码形式,补码的表示方式中,把最高位解释成负权,在第1节无符号数的编码时,可以看出向量\(\vec{x}\)的各位都是按照正权来解释的。

那么我们下面给出补码的定义:

对向量$\vec{x}=[x_{w-1}, x_{w-2}, ..., x_0]: $

其表示的有符号值为:

\[B2T_w(\vec{x}) = -x_{w-1}*2^{w-1} + \sum_{i = 0}^{w-2}x_i2^i \]

可以看出除了最高位是按照负权解释外,其余和无符号数的编码相同。

  • 对于一个字节的有符号数补码 0xFF = -1;

那么计算机为什么使用补码呢?下面介绍下源码和反码

2.2 原码和反码

原码(Sign-Magnitude):最高位是符号位,用来确定剩下的位是使用正权还是负权,其表示的值为:

\[B2S_w(\vec{x}) = -1^{x_{w-1}}*\sum_{i = 0}^{w-2}x_i2^i \]

  • 对于一个字节的有符号数补码 0xFF = -127;

反码(Ones' Complement):除最高有效位的权是$ -(2^{w-1}-1)$ 而不是\(-(2^{w-1})\)外,其他各位的解释方式和补码是一样的。

\[B2O_w(\vec{x}) = -x_{w-1}(2^{w-1}-1) + \sum_{i = 0}^{w-2}x_i2^i \]

从公式可以看出,相同的位向量\(\vec{x}\) 使用反码解释得到的值-1就得到了对应的使用补码解释得到的值。

从公式同样可以看出,当最高位是0时,反码和原码解释得到的值是一样的,

  • 0b0000 0000 ---> 原码表示0,反码也是表示0.

但当最高位是1时,

  • 0b1000 0000 ---> 原码表示0,反码表示-127

也就是说当最高位时1时,补码解释0b1000 0000 == 原码解释 0b1111 1111 (-127),此时可以看出原码解释到反码解释的转化,只需把位图 \(\vec{x}\) 最高位保持不变,其余各位取反。

细心的朋友可能会发现,对于0b1000 0000 用原码解释得到值0,对于0b0000 0000用原码解释得到的值也是0,这就是原码表示的缺点,就是对于零有两个位图 \(\vec{x}\) 来表示。

那么反码是否也有这个问题呢,答案是肯定的0b1111 1111 和0b0000 0000 在反码解释中都是0.

2.3 补码和反码的来源

补码的名字为two's complement,反码的名字为ones' complement. 注意这两名字的撇号的位置是不一样的。这归结与补码和反码的来源。

术语补码来源于这样一种情况,即对于非负数x,我们用 \(2^{w}-x\) 计算-x的w位表示。

术语反码来源于这样一种情况,即对于非负数x, 我们用[1111..1]-x来表示-x。

可见看原英文名更为贴切。

其实对于正数,用原码、反码或补码编码,其最终位向量 \(\vec{x}\) 是一样的。只有负数时,才有区别。而0是原码和反码都有两种表示方式,而补码对所有数都只有一种表示方式。

3.有符号无符号转化

注意一下所讲的有符号全部是用补码表示的。

有符号与无符号的相互转化其实也比较简单,就是位图 \(\vec{x}\) 保持不变,变化的只是对位图 \(\vec{x}\) 的解释。当用符号时,就用补码解释,当为无符号时,就用无符号数的编码来解释。虽然位图 \(\vec{x}\) 是不变的,但是解释出来的值却存在某种关系(此关系不是很重要,自己推一下就行了,本质还是位图 \(\vec{x}\) 不变)。

3.1 无符号-->有符号

对满足 $ 0<= u <= UMax_w$ 的 \(u\) 有:

\[U2T_w(u) = \begin{cases} u,\:\:\:\:\:\:\:\:\:\:\:\:\space u <= TMax_w \\ u-2^w, \:\:\:u > TMax_w\end{cases} \]

举例(以一个字节为例):

0b1111 1111 当是无符号表示时,它的值为255, 当是有符号值表示时,它的值是-1. (\(-1 = 255-2^8\))

推导过程如下:

一个位图 \(\vec{x}\) 用无符号表示时的值:

\[B2U_w(\vec{x}) = \sum_{i=0}^{w-1}x_i2^i \tag{1} \]

一个位图 \(\vec{x}\) 用有符号表示时的值:

\[B2T_w(\vec{x}) = -x_{w-1}*2^{w-1} + \sum_{i = 0}^{w-2}x_i2^i 1 \tag{2} \]

\((1) -(2)\)得到差值为\(x_{w-1}*2^{w-1} + x_{w-1}*2^{w-1} = x_{w-1}* 2^{w}\)

  • \(x_{w-1}\)为0时,差值为0,也就是当最高位为0时,无符号表示的值和有符号补码表示的值相等。
  • \(x_{w-1}\) 为1时,差值为 \(2^{w}\),所以,有符号值(2) = 无符号值(1) - \(2^{w}\)

3.2 有符号-->无符号

对满足 $ TMin_w<= x <= TMax_w$ 的 \(x\) 有:

\[T2U_w(u) = \begin{cases} x+2^w,\:\:\: x<0 \\ x, \:\:\:\:\:\:\:\:\:\:\:\:\:\: x > =0\end{cases} \]

举例(以一个字节为例):

0b1111 1111 当是有符号表示时,它的值为-1, 当是无符号值表示时,它的值是255. (\(255 = -1+2^8\))

推导过程与3.1中类似。

有符号和无符号转化时的坑:

当一个运算数是有符号的,另一个是无符号的,那么c语言会隐式的把有符号的数转为无符号的数,再进行计算,也就是说变成了两个非负数的运算。当是算术运算时,是没有影响的。但是对于逻辑运算就存在很大的坑。

printf("%d\n", -1 > 2U); // 1  
printf("%d\n", -1 > 0U); // 1   

输出的结果都是true,这显然不满足我们的预期。

4.字符扩展

所谓字符扩展就是,比如当一个1字节的数,转化为2字节的数时,在高位补充数据,那么高位应该填补0还是1呢?

我们先说结论:

  • 无符号数,0扩展,即高位补0
  • 有符号数,符号位扩展,即高位补扩展前高位的值。

我们举个例子:

#include<stdio.h>
void printByte(void* c, int n){
    for(int i = 0; i < n; ++i){
         printf("%x ", *((unsigned char*)c+i));
    }
    printf("\n");
}
int main(){
    unsigned char a = 0x12;            //0b00010010
    short b = (unsigned short)a;
    printf("%u\n", b);  		// 18, 16+2 = 18
    printByte(&b, sizeof(b));           // 12 00, (小端)也即使0x0012
    
    signed char a1 = 0x87; 		// 0b10000111
    short b1 = (short)a1;
    printf("%d\n", b1);    		// -121
    printByte(&b1, sizeof(b1));         // 87 ff, (小端)也即使0xff87
      
    //注意,当a1位unsigned char时,情况就发生了变化。
    unsigned char a2 = 0x87; 	        // 0b10000111
    short b2 = (short)a2;  	        // 在此时转化,推测是unsigned char ->unsigned short -> short
    printf("%d\n", b2);     	        // 135
    printByte(&b2, sizeof(b2));	        // 87 00, (小端)也即使0xff87
    
    return 0;
}

5.字符截断

与字符扩展相反,字符截断是,比如当一个2字节的数,转化为1字节的数时,应截断去掉高位数据,那么仅仅去掉高位的数据就行了么,不需要额外的其他操作了吗? 答案是肯定的,只需要把高位去掉,保留低位就行

  • 对于无符号数据,这是显然的。
  • 对于有符号数据,同样如此。
int main(){
    unsigned short a = 0x1288; 	        //0b0001 0010 1000 1000
    unsigned char b = (unsigned char)a;
    printf("%u\n", b);  		// 136, 16+2 = 18
    printByte(&b, sizeof(b));	        // 88, 也即使0x88
    
    short a1 = 0x1288; 			//0b0001 0010 1000 1000
    char b1 = (char)a1;
    printf("%d\n", b1);    		// -120
    printByte(&b1, sizeof(b1));         // 88, 也即使0x88
      
    unsigned short a2 = 0x1288;         //0b0001 0010 1000 1000
    char b2 = (char)a2;  		
    printf("%d\n", b2);     	        // -120
    printByte(&b2, sizeof(b2));         // 88, 也即使0x88
    
    return 0;
}

可以看出,字符截断,其实就是直接了当的,把高位的数据扔掉,只保留低位的数据就行了。


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM