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