有符號數和無符號數探討
這個問題,要是簡單的理解,是很容易的,不過要是考慮的深了,還真有些東西呢。
下面我就把這個東西儘可能的擴展一點,深刻一點和你們說說。
1、只有一個標準!
在彙編語言層面,聲明變量的時候,沒有 signed 和 unsignde 之分,彙編器通通,將你輸入的整數字面量看成有符號數處理成補碼存入到計算機中,只有這一個標準!彙編器不會區分有符號仍是無符號而後用兩個標準來處理,它通通看成有符號的!而且通通匯編成補碼!也就是說,db -20 彙編後爲:EC ,而 db 236 彙編後也爲 EC 。這裏有一個小問題,思考深刻的朋友會發現,db 是分配一個字節,那麼一個字節能表示的有符號整數範圍是:-128 ~ +127 ,那麼 db 236 超過了這一範圍,怎麼能夠?是的,+236 的補碼的確超出了一個字節的表示範圍,那麼拿兩個字節(固然更多的字節更好了)是能夠裝下的,應爲:00 EC,也就是說 +236的補碼應該是00 EC,一個字節裝不下,可是,別忘了「截斷」這個概念,就是說最後彙編的結果被截斷了,00 EC 是兩個字節,被截斷成 EC ,因此,這是個「美麗的錯誤」,爲何這麼說?由於,當你把 236 看成無符號數時,它彙編後的結果正好也是 EC ,這下皆大歡喜了,雖然彙編器只用一個標準來處理,可是借用了「截斷」這個美麗的錯誤後,獲得的結果是符合兩個標準的!也就是說,給你一個字節,你想輸入有符號的數,好比 -20 那麼彙編後的結果是符合有符號數的;若是你輸入 236 那麼你確定看成無符號數來處理了(由於236不在一個字節能表示的有符號數的範圍內啊),獲得的結果是符合無符號數的。因而給你們一個錯覺:彙編器有兩套標準,會區分有符號和無符號,而後分別彙編。其實,大家被騙了。:-)
2、存在兩套指令!
第一點說明彙編器只用一個方法把整數字面量彙編成真正的機器數。但並非說計算機不區分有符號數和無符號數,相反,計算機對有符號和無符號數區分的十分清晰,由於計算機進行某些一樣功能的處理時有兩套指令做爲後備,這就是分別爲有符號和無符號數準備的。可是,這裏要強調一點,一個數究竟是有符號數仍是無符號數,計算機並不知道,這是由你來決定的,當你認爲你要處理的數是有符號的,那麼你就用那一套處理有符號數的指令,當你認爲你要處理的數是無符號的,那就用處理無符號數的那一套指令。加減法只有一套指令,由於這一套指令同時適用於有符號和無符號。下面這些指令:mul div movzx … 是處理無符號數的,而這些:imul idiv movsx … 是處理有符號的。
舉例來講:
內存裏有 一個字節x 爲:0x EC ,一個字節 y 爲:0x 02 。當把x,y看成有符號數來看時,x = -20 ,y = +2 。看成無符號數看時,x = 236 ,y = 2 。下面進行加運算,用 add 指令,獲得的結果爲:0x EE ,那麼這個 0x EE 看成有符號數就是:-18 ,無符號數就是 238 。因此,add 一個指令能夠適用有符號和無符號兩種狀況。(呵呵,其實爲何要補碼啊,就是爲了這個唄,:-))
乘法運算就不行了,必須用兩套指令,有符號的狀況下用imul 獲得的結果是:0x FF D8 就是 -40 。無符號的狀況下用 mul ,獲得:0x 01 D8 就是 472 。(參看文後附錄2例程)
3、可愛又可怕的c語言。
爲何又扯到 c 了?由於大多數遇到有符號仍是無符號問題的朋友,都是c裏面的 signed 和 unsigned 聲明引發的,那爲何開頭是從彙編講起呢?由於咱們如今用的c編譯器,不管gcc 也好,vc6 的cl 也好,都是將c語言代碼編譯成彙編語言代碼,而後再用匯編器彙編成機器碼的。搞清楚了彙編,就至關於從根本上明白了c,並且,用機器的思惟去考慮問題,必須用匯編。(我通常遇到什麼奇怪的c語言的問題都是把它編譯成彙編來看。)
C 是可愛的,由於c符合kiss 原則,對機器的抽象程度剛恰好,讓咱們即提升了思惟層面(比彙編的機器層面人性化多了),又不至於離機器太遠(像c# ,java之類就太遠了)。當初K&R 版的c就是高級一點的彙編……:-)
C又是可怕的,由於它把機器層面的全部的東西都反應了出來,像這個有沒有符號的問題就是一例(java就不存在這個問題,由於它被設計成全部的整數都是有符號的)。爲了說明它的可怕特舉一例:
#include <stdio.h>
#include <string.h>
int main()
{
int x = 2;
char * str = "abcd";
int y = (x - strlen(str) ) / 2;
printf("%d\n",y);
}
結果應該是 -1 可是卻獲得:2147483647 。爲何?由於strlen的返回值,類型是size_t,也就是unsigned int ,與 int 混合計算時有符號類型被自動轉換成了無符號類型,結果天然出乎意料。。。
觀察編譯後的代碼,除法指令爲 div ,意味無符號除法。
解決辦法就是強制轉換,變成 int y = (int)(x - strlen(str) ) / 2; 強制向有符號方向轉換(編譯器默認正好相反),這樣一來,除法指令編譯成 idiv 了。
咱們知道,就是一樣狀態的兩個內存單位,用有符號處理指令 imul ,idiv 等獲得的結果,與用 無符號處理指令mul,div等獲得的結果,是大相徑庭的!因此牽扯到有符號無符號計算的問題,特別是存在討厭的自動轉換時,要倍加當心!(這裏自動轉換時,不管gcc仍是cl都不提示!!!)
爲了不這些錯誤,建議,凡是在運算的時候,確保你的變量都是 signed 的。
4、c的作法。
對於有符號和無符號的處理上,c語言層面作的更「人性化」一些。好比在聲明變量的時候,c 有signed 和 unsigned 前綴來區別,而彙編呢,沒有任何區別,把握全在你本身,好比:你想在一個字節中輸入一個有符號數,那麼這個數就別超過 -128 ~ +127 ,想輸入無符號數,要保證數值在 0~255 之間。若是你輸入了 236 ,你還要說你輸入的是有符號數,那麼你確定錯了,由於有符號數236至少要兩個字節來存放(爲00 EC),不要小看了那一個字節的00,在有符號乘法下,兩個字節的00 EC 與 一個字節的EC,在與一樣一個數相乘時,獲得的結果是大相徑庭的!!!
咱們來看下具體的列子(用vc6的cl編譯器生成):
C語言 編譯後生產的彙編語言
……
char x;
unsigned char y;
int z;
x = 3;
y = 236;
z = x*y;
…… ……
_x$ = -4
_y$ = -8
_z$ = -12
……
mov BYTE PTR _x$[ebp], 3
mov BYTE PTR _y$[ebp], 236
movsx eax, BYTE PTR _x$[ebp]
mov ecx, DWORD PTR _y$[ebp]
and ecx, 255
imul eax, ecx
mov DWORD PTR _z$[ebp], eax
……
咱們看到,在賦值的時候(綠色部分),彙編後與本文第一條論述相同,是否有符號把握全在本身,c比彙編作的更好這一點沒有獲得體現,這也能夠理解,由於c最終要被編譯成彙編,彙編沒有在變量聲明時區分有無符號這一功能,天然,c也沒有辦法。但既然c提供了signed和unsigned聲明,彙編後,確定有代碼體現這一點,表格裏的紅色部分就是。對有符號數x他進行了符號擴展,對無符號y進行了零擴展。這裏爲了舉例的方便,進行了有符號數和無符號數的混合運算,實際編程中要避免這種狀況。
(完)
附錄:
1.計算機對有符號整數的表示只採起一套編碼方式,不存在正數用原碼,負數用補碼這用兩套編碼之說,大多數計算機內部的有符號整數都是用補碼,就是說不管正負,這個計算機內部只用補碼來編碼!!!只不過正數和0的補碼跟他原碼在形式上相同,負數的補碼在形式上與其絕對值的原碼取反加一相同。
2. 兩套乘法指令結果例程:
;; 程序存儲爲 x.s
extern printf
global main
section .data
str1: db "%x",0x0d,0x0a,0
n: db 0x02
section .text
main:
xor eax,eax
mov al, 0xec
mul byte [n] ;有符號乘法指令爲: imul
push eax
push str1
call printf
add esp,byte 4
ret
編譯步驟:
1. nasm -felf x.s
2. gcc x.o
ubuntu7.04 下用nasm和gcc編譯經過。結果符合文章所述java