C語言支持多種整型數據類型來表示有限范圍的整數。每種類型都能用關鍵字來指定大小,這些關鍵字包括char、short、long,同時還可以指示被表示的數字是非負數(聲明為unsigned)、或者可能是負數(默認)。另外,為這些不同的大小分配的字節數根據程序編譯為32位還是64位而有所不同。
long的大小與int一樣大,屬于歷史遺留問題,歷史上的int在16位平臺時代是2個字節,long是4個字節,所以long要long,等到32位平臺將int提升到32位一個字長時,int和long的長度就一樣。
不同wordsize對應的最值的16進制編碼(其中的U表示Unsigned,T表示Two'scomlement(補碼)):
注意上面的16進制與二進制的對應關系:
7:0111
8:1000
F:1111
可以看出有以下數量關系:
1無符號數編碼假設有一個整數數據類型,有位(wordsizeorwidth)。將其寫成向量,將其視為二進制表示的無符號數,則每一位取值范圍為0或1。
用函數(BinarytoUnsigned的縮寫,長度為)表示為:
函數將一個長度為w的0、1串映射到非負整數。
例:
可以用長度為的指向右側箭頭的條表示每個位的位置i。每個位向量對應的數值就等于所有值為1的位對應的條的長度之和。
w位二進制所能表示的無符號整數的范圍:
2有符號整型數據的補碼編碼目前,最常見的計算機有符號整型數的計算機表示方式為補碼形式。在補碼中,將字的最高有效位解釋為負權(negativeweight)。這里通過函數B2T_w(BinarytoTwo's-complement的縮寫)來表示:
例:
最高有效位也稱為符號位,它的“權重”為,是無符號表示中權重的負數。符號位被設置為1時,表示值為負,而當設置為0時,值為非負。
補碼+它的負數=模;
補碼和它的負數的二進制位只有最低位是相同的,其它位都是相補:
補碼&&它的負數=1;
這也是經常有以下表述的來由:
對于負數,補碼等于反碼+1。
補碼使用最高位表示符號,但與無符號整數編碼表示的總體值域的個數是一致的:。
我們用向左指的條表示符號位具有負權重。于是,與一個位向量相關聯的數值是由可能的向左指的條和向右指的條加起來決定的。
最高位以外的其它位相對于最高位而言,是一個此漲彼消的狀態。
w位二進制所能表示的有符號整數的范圍:
負整數的范圍比正整數的范圍大1,多出來的數就是100…000(二進制序列,最小的負數),該數取反加1等于模,截斷后就是0了。
3有符號整數與無符號整數之間的轉換C語言允許在各種不同的數字數據類型之間做強制類型轉換。遵循底層的位表示不變,而按不同類型的編碼規則進行解釋的原則。
如果是相關類型的隱式轉換,C語言設置了一系列的轉換規則。
如果是不相關類型的強制轉換,C語言會在位級層面按編碼規則做重新解釋(解碼)。
例如,假設變量x聲明為int,u聲明為unsigned,表達式(unsigned)x會將x的值轉換成一個無符號數值,而(int)u將u的值轉換成一個有符號整數。將有符號數強制類型轉換成無符號數。另外,一個表達式中,如果同時存在無符號整數與有符號整數,計算時,有符號整數會隱式轉換為無符號整數。
對于有符號整數的正整數部分,與無符號整數是一致的。
對于對于有符號整數的負整數部分,與無符號整數只有最高位的解釋不同,其它位是一致的。
所以有以下數量關系:
負整數x→U=x+
u→負整數x=U-
由于C語言對同時包含有符號和無符號整型數表達式的隱式轉換,會出現一些奇特的行為。當執行一個運算時,如果它的一個運算數是有符號的而另一個是無符號的,那么C語言會隱式地將有符號整數強制類型轉換為無符號數,并假設這兩個數都是非負的,來執行這個運算。就像我們將要看到的,這種***對于標準的算術運算來說并無多大差異,但是對于像<和>這樣的關系運算符來說,它會導致非直觀的結果。
有符號數到無符號數的隱式強制類型轉換導致了某些非直觀的行為。而這些非直觀的特性經常導致程序錯誤,并且這種包含隱式強制類型轉換的細微差別的錯誤很難被發現。
4擴展一個整型數字的位表示一個常見的運算是在不同字長的整數之間轉換,同時又保持數值不變。
對于有符號整數和無符號整數,兩者在擴展時,高位的符號擴展有所區別:
shortsx=val;/*-12345*/unsignedshortusx=sx;/*53191*/intx=sx;/*-12345*/unsignedux=usx;/*53191*/在采用補碼表示的32位大端法機器上的內存布局:
有符號整型使用1來做符號擴展。
無符號整型使用0來做符號擴展。
有符號整數在做右移運算時,也會存在符號擴展的問題。
5截斷數字假設我們不用額外的位來擴展一個數值,而是減少表示一個數字的位數。例如下面代碼中這種情況:
intx=53191;shortsx=(short)x;/*-12345*/inty=sx;/*-12345*/當我們把x強制類型轉換為short時,我們就將32位的int截斷為了16位的shortint。
當將一個w位的整型數截斷為一個k位整型數字時,我們會丟棄高w-k位。
無符號整數取模:
有符號整數的補碼取模后要做轉換:
6整數運算與溢出C語言的整型數字是一個有限字長的表示,當在計算時存在溢出時,會做模運算處理。
當兩個w位的無符號整型相加時,其結果可能是一個w+1位的無符號數。
當兩個w位的無符號整型相加時其和等于或超過時,就會發生溢出,其結果為和與的模運算:
補碼加法存在正溢出與負溢出:
首先要明白,一個正數和一個負數相加,結果一定不會溢出(因為結果的絕對值一定小于兩個加數的絕對值,兩個加數都表達出來了,結果一定能表達出來。)
所以,發生溢出的情況一定是符號相同的兩個數相加。
分情況討論:
①正整數pa+正整數pb=r
符號位0,數位相加,如果結果的符號位變成1了,那一定是兩個加數的最高位相加進上來的。
假設將一個長度w=4的0、1串映射到有符號整數,其最大的正數只能是0111,也就是7。
0111+0001=1000//7+1=8-2^4=-8,向下溢出
0111+0011=1010//7+3=10-2^4=-6,向下溢出
發生向下溢出,判斷的方式之一是:r<pa||r<pb。
②負整數na+負整數nb=r
符號位都是1,所以符號位一定會進位。數位相加,如果最后符號位是0,說明結果變成正的了,那一定是發生溢出了(負+負!=正)。
假設將一個長度w=4的0、1串映射到有符號整數,其最小的負數只能是1000,也就是-8。
1000+1111=11000->0110//-8+(-1)=-9+2^4=7,向上溢出
1011+1011=10110->0110//-5+(-5)=-10+2^4=6,向上溢出
另外一種情況,沒有改變符號位的溢出,屬正常溢出,正如有符號位擴展、算術移位一樣:
1100+1100=10000->0000//-4+(-4)=-8,正常溢出
1111+1111=11110->1110//-1+(-1)=-2,正常溢出
發生向上溢出,判斷的方式之一是:r<pa||r<pb。
CPU的標志寄存器有一個溢出標志位,反映有符號數加減運算是否溢出。如果運算結果超過了8位或者16位有符號數的表示范圍,則OF置1,否則置0。
7整數的乘除①兩整數乘除的結果還是一個整數類型(類型一致),如果是除法,可能存在舍入(舍入到零,向上舍入,向下舍入)的情況;
②兩整數乘法要考慮溢出的情況。
③位運算也是一種特殊的整數乘除,乘數與除數是2的不同的整數冪(當一個整數乘除一個常數時,這個常數如果不是某個整數次冪,可以拆解成不同的冪次的加減)。
8代碼示例8.1帶符號數產生意外結果的例子。這個例子會造成無限循環,因為sizeof會返回unsignedint類型,由此帶來的結果是,i-sizeof(char)這個表達式的求值結果將會是unsignedint(隱式轉換!!),i會隱式轉換為unsigned,i從0減1后變成-1,其二進制編碼是0xFFFFFFFF,轉換成無符號數是2^32-1,從而產生無限循環,有時候你需要特別留心這種不經意的錯誤!
intn=10,i;for(i=n-1;i-sizeof(char)>=0;i--)printf("i:0x%x ",i);if(-1>0U)//-1的二進制編碼是0xFFFFFFFF,轉變成無符號數是2^32-1printf("YouSurprisedme! ");8.2以下是2002年的freeBSD內核的部分代碼,其中包含了漏洞,假設惡意人員將負值作為maxlen傳入這個函數,有發生什么情況?
以下size_t的類型是typedefunsignedintsize_t的類型定義:
#defineKSIZE1024charkbuf[KSIZE];/*Copyatmostmaxlenbytesfromkernelregiontouserbuffer*/intcopy_from_kernel(void*user_dest,intmaxlen){intlen=KSIZE<maxlen?KSIZE:maxlen;memcpy(user_dest,kbuf,len);returnlen;}/*Declarationoflibraryfunctionmemcpy*/void*memcpy(void*dest,void*src,size_tn);/*MaliciousUsage*/voidgetstuff(){charmybuf[MSIZE];copy_from_kernel(mybuf,-512);//-512轉變成無符號數后是2^31+512}8.3給定一個有序的整型數組,請編程實現二分查找算法。
高德納在《計算機程序設計的藝術》指出,雖然早在1946年就有人將二分查找的***公諸于世,但直到1962年才有人寫出沒有bug的二分查找程序,可見,寫一個安全的代碼并不容易,你是不是一不小心就寫出像下面這樣的二分查找代碼了?
intbinary_search(inta[],intlen,intkey){intlow=0;inthigh=len-1;while(low<=high){intmid=(low+high)/2;//提示:這里有溢出Bug!if(a[mid]==key){returnmid;}if(key<a[mid]){high=mid-1;}else{low=mid+1;}}return-1;}ref
RandalE.Bryant,DavidR.O’Hallaron《ComputerSystems:AProgrammersPerspective》
-End-