計算機中信息的表示與處理
信息的編碼方式
在計算機中信息都是以0、1兩種數據來(lái)表示的,大家都知道,但是就是這兩個(gè)簡(jiǎn)單的0、1如何實(shí)現了計算機的強大計算呢?這就涉及到了計算機中信息的表達和處理。在大學(xué)計算基礎課上,開(kāi)始就涉及了二進(jìn)制、八進(jìn)制、十進(jìn)制、十六進(jìn)制等進(jìn)制之間的轉換方式。其中二進(jìn)制是計算機的實(shí)現方式,其他的進(jìn)制都是出于一些目的定義的。在二進(jìn)制中有三個(gè)基礎概念:原碼、反碼、補碼是非常重要的,很多人也對三者之間的轉換方式非常清楚。
原碼:對于十進(jìn)制的數據,可以采用多個(gè)0、1構成的比特向量表示,其中最高位表示符號位,當為1時(shí),表示這個(gè)數為負數,當為0時(shí),表示這個(gè)數為正數。
反碼:是指原碼符號位除外的其他位進(jìn)行取反操作,但是取反操作只針對負數,也就是說(shuō)正數的原碼等于反碼。
補碼:也只是針對負數而言,對于負數補碼是指在反碼的基礎上加1即表示補碼(但是不能改變符號位數據)。對于正數而言,補碼等于反碼,等于原碼。
我們可以這樣認為,為了解決負數的問(wèn)題,在計算機中引入了反碼、補碼的概念,且補碼、反碼主要針對的對象就是負數,正數的補碼、原碼、反碼是相同的bit向量。
在實(shí)際運用中補碼相比另外兩種編碼方式更加的方便,因此對于有符號整形類(lèi)型,幾乎所有的編譯器中都是采用補碼方式進(jìn)行編碼,了解到編譯器是采用補碼的形式存儲計算機的整形數據信息是非常重要的。
一般而言,對于有符號的整數型數據類(lèi)型,即char、short、int這種類(lèi)數據類(lèi)型,都是采用補碼方式編碼的,但是對于無(wú)符號的整數型數據類(lèi)型,一般都是采用原碼的形式編碼的,也就是單純的比特向量,沒(méi)有符號位之說(shuō),由于常用的計算機系統都是32bit,這也是為什么說(shuō)計算機的地址空間是4G=2^32字節。char型數據類(lèi)型占1字節的空間,而short類(lèi)型一般占用2字節的空間,int型數據占用4個(gè)字節的空間。32個(gè)bit剛好占有4字節,因此我們可以認為int數據實(shí)際上就是一個(gè)32個(gè)bit向量。
由實(shí)際情況可知:
int型的范圍是-2^31---2^31-1,可以認為是非對稱(chēng)的空間。無(wú)符號的unsigned int的范圍則是0到2^32-1之間。
char型的范圍是-128到-127之間,unsigned char的范圍則是0到255之間,在很多的應用中可以充分運用char的范圍減小存儲量。
short的范圍是-2^15到2^15-1,unsigned short 的范圍是0-2^16-1。
需要了解的是有符號的整數型數據都是采用補碼方式進(jìn)行編碼的,而無(wú)符號的數據類(lèi)型一般采用原碼方式編碼(正數)。兩種編碼方式表示數據的范圍存在差別,實(shí)際上在C語(yǔ)言編程的過(guò)程中都會(huì )進(jìn)行隱式的強制類(lèi)型轉換,如果不清楚編碼方式的差別,就很難準確的把握計算的差別。在嵌入式編程中經(jīng)常會(huì )有一些簡(jiǎn)單的延遲操作,如果編寫(xiě)不恰當就會(huì )導致錯誤產(chǎn)生。如下所示:
void delay(int time)
{
unsigned char i = 0;
for(; time >= 0; -- time)
for(i = 0; i < 256; ++ i)
;
}
這個(gè)題乍一看沒(méi)什么問(wèn)題,但是仔細推敲就會(huì )發(fā)現存在問(wèn)題,因為存在死循環(huán),unsigned char的最大值是255,不可能大于等于256,因此一直滿(mǎn)足條件,也就是說(shuō)第二個(gè)循環(huán)會(huì )一直執行,這就是典型的不注意范圍問(wèn)題。
還有一個(gè)典型的排序問(wèn)題:
unsigned char array[1000][1000];
void sort(unsigned char (*a)[1000])
{}
這種屬于典型的大數排序問(wèn)題,只有選擇合適的排序策略才能減少排序的時(shí)間復雜度,那么如何實(shí)現呢?最簡(jiǎn)單的方式是采用計數排序,時(shí)間復雜度為O(n)。充分利用了unsigned char的數值范圍在0-255之間這個(gè)范圍。
左移右移處理
在整形數據類(lèi)型中有一個(gè)問(wèn)題就是典型的移位操作,在機器語(yǔ)言中也會(huì )有位操作,在C語(yǔ)言中也存在位操作,這為直接控制CPU提供了較好的實(shí)現方式。移位主要包含左移和右移操作,其中左移的實(shí)現是在當前數據的bit向量向左移動(dòng)n個(gè)bit,后續的bit補0。
而右移比左移復雜一些,右移存在兩種:邏輯右移和算術(shù)右移,邏輯右移主要是針對無(wú)符號型數據,邏輯右移是指將當前的bit向量向右移動(dòng),左邊bits補零。算術(shù)右移主要針對有符號型數據,將當前數據右移,左邊補上最高bit的值。這種實(shí)現方式能夠保證數據的符號不改變,也就是說(shuō)負數通過(guò)右移以后還是負數,不會(huì )變成正數。而邏輯右移則不行,負數通過(guò)邏輯右移以后就變成了正數,這是不合理的。因此可以總結如下:
對于左移操作,不管是有符號還是無(wú)符號類(lèi)型,都是右端直接補零。這時(shí)候可能導致數據發(fā)生較大的變化,正數通過(guò)左移變成了負數,出現這些的原因實(shí)際上是左移操作忽略了進(jìn)位操作,正負都是合理的。在整形數據中經(jīng)常采用左移操作實(shí)現數據的乘法操作,兩個(gè)正數相乘是有可能產(chǎn)生負數的。比如:
int x1 = 0x60000000;
printf("%d,%d,%d",x1,x1<<1,x1*2);
這段代碼就說(shuō)明了兩個(gè)正數相乘是可以產(chǎn)生一個(gè)負數的,但是如果我們采用無(wú)符號數據類(lèi)型進(jìn)行左移操作就不會(huì )產(chǎn)生兩個(gè)正數相乘產(chǎn)生負數的情況,這樣能夠避免很多不可思議的結果。
右移操作中的邏輯右移主要是針對無(wú)符號數據類(lèi)型,對有符號的數據類(lèi)型是無(wú)效的,對于有符號的數據類(lèi)型,右移操作對于大多數的編譯器都認為是算術(shù)右移,算術(shù)右移能夠保證數據的正負特性,不會(huì )發(fā)生左移中兩個(gè)正數相乘得到負值的情況。算術(shù)右移的實(shí)質(zhì)是在移位操作以后,數據的左邊采用符號位填充。通常在C語(yǔ)言中右移實(shí)現除法操作,比如8>>1,即實(shí)現了除以2的操作,對于無(wú)符號型數據可以采用右移操作實(shí)現除法操作,但是對于有符號數據類(lèi)型可能出現錯誤。下面說(shuō)一個(gè)典型的例子:
int x1 = -1;
printf("%d,%d,%d",x1,x1>>1,x1/2);
這個(gè)例子說(shuō)明了有符號數據類(lèi)型通過(guò)算術(shù)右移并不能完成除法操作,但是無(wú)符號的數據類(lèi)型基本上可以完成除法操作。搞清楚何時(shí)是算術(shù)右移何時(shí)是邏輯右移是非常有必要的。
對于無(wú)符號數據類(lèi)型,采用左移右移的方式提高乘除法的速度是可行的,但是對有符號的數據類(lèi)型最好不要采用這種實(shí)現方式,因為可能會(huì )出現意想不到的結果。雖然很多的程序書(shū)中建議少用unsigned的類(lèi)型,但是在移位操作這方面最好還是處理無(wú)符號型對象比較有保證。
強制類(lèi)型轉換處理
強制類(lèi)型轉換是C語(yǔ)言中比較重要的一個(gè)主題,其中程序員老手也會(huì )忽略這種問(wèn)題的發(fā)生,基本的實(shí)現方式是有符號向無(wú)符號轉變,小字節數據朝大字節數據轉換,當然也有高字節數據類(lèi)型往低字節數據轉變的問(wèn)題。
基本的轉換存在兩種:零擴展和符號擴展。
零擴展是針對無(wú)符號數據類(lèi)型,將一個(gè)小字節數據轉換為大字節數據時(shí),在高字節中填充零,實(shí)現數據的一致型。
符號擴展則針對有符號數據類(lèi)型,將一個(gè)小字節數據轉換為大字節數據時(shí),在高字節中填充小字節數據的符號位,填充符號位主要是為了實(shí)現補碼一致原則。零擴展實(shí)際上是符號擴展的子類(lèi)。
大字節數據向小字節數據轉換的過(guò)程是一個(gè)截取過(guò)程,這樣可能會(huì )導致數據正負的變化,也就是說(shuō)大到小的過(guò)程可能發(fā)現比較大的變化,截取數據的一部分作為小字節數據的bit向量。
強制類(lèi)型轉換實(shí)質(zhì)上包含了符號擴展,和零擴展,符號擴展保證了補碼的一致型,零擴展則保證了數值大小的一致性,符號擴展針對有符號數據類(lèi)型,而零擴展主要針對無(wú)符號數據類(lèi)型。
很多時(shí)候的強制類(lèi)型轉換是隱含進(jìn)行的,這是就需要我們準確的把握,有符號類(lèi)型到無(wú)符號類(lèi)型的轉變會(huì )導致很大的差別,因為無(wú)符號類(lèi)型數加上有符號類(lèi)型數將進(jìn)行有符號到無(wú)符號數據類(lèi)型的轉換,這時(shí)候就會(huì )產(chǎn)生一個(gè)無(wú)符號的數據,不會(huì )產(chǎn)生有符號類(lèi)型的數。
同時(shí)還有一個(gè)技巧,在C語(yǔ)言中如何判斷兩個(gè)數的和是否越界的問(wèn)題,由于很多編譯器對越界并不報錯,這時(shí)候可以通過(guò)判斷兩個(gè)數的和是否小于任何一個(gè)數即可,如果小于任何一個(gè),則說(shuō)明這個(gè)數就是越界,否則沒(méi)有發(fā)生越界問(wèn)題。
具體的實(shí)現可以采用下面的代碼測試:
int test()
{
char x2 = -20;
int x1 = x2;
printf("x2 = %d, x1 = %u",x2,x1);
x2 = 20;
x1 = x2;
printf("x2 = %d, x1 = %u",x2,x1);
}
浮點(diǎn)型數據類(lèi)型
浮點(diǎn)型數據類(lèi)型在前一章中已經(jīng)說(shuō)明了,浮點(diǎn)型數據類(lèi)型沒(méi)有無(wú)符號和有符號之說(shuō)。浮點(diǎn)型具有固定的編碼方式,本來(lái)就是存在正負之分,即沒(méi)有unsigned float之說(shuō)。需要注意的是浮點(diǎn)型是一個(gè)近似的值不能用來(lái)比較。還有對float類(lèi)型數據不能進(jìn)行移位操作,因為float是有固定的編碼方式的不像整形數據。
總結
了解計算機中的有符號整型數據一般按照補碼的方式存在,有符號數據類(lèi)型的擴展是按照符號擴展,而不是簡(jiǎn)單的零擴展,符號擴展是零擴展的延伸,主要是保證在延伸的過(guò)程中符號、數值保持不變。
在移位操作過(guò)程中,有符號、無(wú)符號數據類(lèi)型的左移都沒(méi)有問(wèn)題,但是可能會(huì )導致數據類(lèi)型的改變,特別是有符號數據類(lèi)型。對于對于右移操作需要注意,有符號數據類(lèi)型是算術(shù)右移,而無(wú)符號數據類(lèi)型是邏輯右移,右移的差別是在左端補齊的值的差別。
在用移位來(lái)模擬數據的乘除法時(shí)特別要注意,對無(wú)符號數據類(lèi)型是最有效的,左移右移的值也是我們認為正確的值,但是如果是對有符號數據類(lèi)型進(jìn)行右移模擬除法過(guò)程,左移模擬乘法過(guò)程時(shí)可能導致結果不是所想。因此建議只對無(wú)符號數據類(lèi)型采用移位來(lái)簡(jiǎn)化乘除操作。
評論