1. 無符號數的編碼
無符號數的編碼其實就是我們平時認知的二進制表示形式,比如\(0b0000 1101 = 1*2^{3}+1*2^{2}+1*2^{0} = 13\)
其一般定義為:
對於向量 $\vec{x}=[x_{w-1}, x_{w-2}, ..., x_0]: $
其轉化為無符號變量后的值為
就是基本的數學進制轉化表示方法,比較簡單。
2. 補碼編碼
2.1補碼(two's complement)
我們不僅僅需要表示無符號的值,有些時候我們也要表示有符號的值,而計算機最常見的表示有符號數的方式為補碼形式,補碼的表示方式中,把最高位解釋成負權,在第1節無符號數的編碼時,可以看出向量\(\vec{x}\)的各位都是按照正權來解釋的。
那么我們下面給出補碼的定義:
對向量$\vec{x}=[x_{w-1}, x_{w-2}, ..., x_0]: $
其表示的有符號值為:
可以看出除了最高位是按照負權解釋外,其余和無符號數的編碼相同。
- 對於一個字節的有符號數補碼 0xFF = -1;
那么計算機為什么使用補碼呢?下面介紹下源碼和反碼
2.2 原碼和反碼
原碼(Sign-Magnitude):最高位是符號位,用來確定剩下的位是使用正權還是負權,其表示的值為:
- 對於一個字節的有符號數補碼 0xFF = -127;
反碼(Ones' Complement):除最高有效位的權是$ -(2^{w-1}-1)$ 而不是\(-(2^{w-1})\)外,其他各位的解釋方式和補碼是一樣的。
從公式可以看出,相同的位向量\(\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\) 有:
舉例(以一個字節為例):
0b1111 1111 當是無符號表示時,它的值為255, 當是有符號值表示時,它的值是-1. (\(-1 = 255-2^8\))
推導過程如下:
一個位圖 \(\vec{x}\) 用無符號表示時的值:
一個位圖 \(\vec{x}\) 用有符號表示時的值:
用\((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\) 有:
舉例(以一個字節為例):
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;
}
可以看出,字符截斷,其實就是直接了當的,把高位的數據扔掉,只保留低位的數據就行了。