補碼編碼、有符號無符號轉化、字符擴展與截斷


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