第1篇:C/C++ 有符號和無符號數(shù)字的迷途

本文主要詳細(xì)講述了無符號的各種負(fù)面特性。很多中文書籍或文章沒有專門詳細(xì)地解析清楚這方面的內(nèi)容,所以我這里專門開篇寫一文,在寫本文的時(shí)候,也從老外的相關(guān)資料做了不少的借鑒,并且利用bitset這個(gè)工具函數(shù),從內(nèi)存二進(jìn)制數(shù)據(jù)的角度來解析無符號和帶符號整數(shù)的相關(guān)特性。下面的各個(gè)示例已經(jīng)運(yùn)行過一片,做了自己充分的分析。本文可能還不太完善,我也會在以后不定時(shí)地更新。

常用整數(shù)的區(qū)間

有符號整數(shù)溢出

下面的示例代碼,是用來展示什么叫數(shù)字溢出(Integer Overflow)

int main(int argc, char const *argv[])
{
    short c = 33333;
    std::cout << c << std::endl;
    std::cout << std::bitset<sizeof(short) * 8>(c) << std::endl;
    std::cout << std::bitset<sizeof(short) * 8>(-33333) << std::endl;
    std::cout << std::bitset<17>(33333) << std::endl;
    return 0;
}

我們值整數(shù)33333(如果用auto關(guān)鍵字聲明,編譯器會為該值需要32位的int存儲該值,實(shí)際該值真正用到就17位,除了最高位0作為MSB判斷正/負(fù),其余15位是為了內(nèi)存對齊填充之用),但是我們故意強(qiáng)制存儲在16字節(jié)就會發(fā)生溢出,而溢出的部分是前16位的高位地址,因?yàn)槲覀兟暶鞯淖兞渴莝hort類型為2個(gè)字節(jié),CPU只會截取指定的16位中的二進(jìn)制數(shù)據(jù),如下圖


同時(shí)對于unsigned short的聲明類型,CPU截取的16位字節(jié)碼會被認(rèn)為是二進(jìn)制補(bǔ)碼形式,這里其實(shí)就是-32203的絕對值的補(bǔ)碼的后16位的字節(jié)碼,因此這種溢出的后果就輸出了-32203。

上面的示例代碼,如果我們聲明為 unsigned short的話,就不存二進(jìn)制的補(bǔ)碼形式,在無符號整數(shù)中也不存在所謂的MSB位。因此可以正確解讀為33333。

副作用2:無符號整數(shù)的環(huán)繞

int main()
{
    unsigned short x = 65535; 
    std::cout << "x是: " << x << '\n';
    std::cout << "65535 的二進(jìn)制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';

    x = 65536; 
    std::cout << "x是: " << x << '\n';
    std::cout << "65536 的二進(jìn)制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';

    x = 65537; 
    std::cout << "x 是: " << x << '\n';
    std::cout << "65537 的二進(jìn)制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';

    return 0;
}

其實(shí)無符號整數(shù)沒有溢出的說法,如果被賦值的整數(shù)超出該無符號整數(shù)可以表示的范圍, 任何大于該類型可表示的最大數(shù)字的數(shù)字都只是“環(huán)繞(wraps around)

65535在2字節(jié)整數(shù)范圍內(nèi),因此65535可以。
但是
65536超出了范圍,繞回了值0,二進(jìn)制形式:0000000000000000
65537超出了范圍,繞回了值1,二進(jìn)制形式:0000000000000001
65538超出了范圍,繞回了值2,二進(jìn)制形式:0000000000000010

如此類推....
當(dāng)我們向無符號類型的整數(shù)賦一個(gè)負(fù)數(shù),無符號整數(shù)的環(huán)繞會從該無符號整數(shù)的最大整數(shù)方向開始環(huán)繞

int main()
{
    unsigned short x = 0;

    std::cout << "x=0 時(shí),二進(jìn)制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';
    
    x = -1;
    std::cout << "x=-1時(shí),二進(jìn)制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';

    x = -2;
    std::cout << "x=-2 時(shí),二進(jìn)制形式: " << std::bitset<sizeof(unsigned short) * 8>(x) << '\n';

    return 0;
}

x=0 時(shí),二進(jìn)制形式: 0000000000000000,這個(gè)在取值區(qū)間內(nèi),是正常的
x=-1時(shí),二進(jìn)制形式: 1111111111111111,會被環(huán)繞成65535
x=-2 時(shí),二進(jìn)制形式: 1111111111111110,會被環(huán)繞成65534

副作用3:無符號整數(shù)的減法問題

許多老資格的C/C++程序員都推薦在日常項(xiàng)目盡量避免使用無符號整數(shù),上文已經(jīng)羅列了無符號整數(shù)的副作用了,這里再看看此處的代碼

int main()
{
    unsigned int x = 2;
    unsigned int y = 5;
    auto r = x - y;
    std::cout << "x-y=" << r << std::endl;
    return 0;
}

我們得知2-5是等于-3,但是-3是無法在無符號整數(shù)類型中正確表示的,那么為什么會輸出424967293,我們按照上面的例子提供思路,你推理得到,由于-3無法正常表示的,環(huán)繞到int類型最大整數(shù)范圍的頂部。

-1環(huán)繞到4,294,967,295
-2環(huán)繞到4,294,967,294
-3環(huán)繞到4,294,967,293

錯誤的輸出

我們再進(jìn)一步用
std::bitset<32>(-3)
std::bitset<32>(4294967293)
可以得知一個(gè)下圖的32位的二進(jìn)制碼,該字節(jié)碼能對于不同的數(shù)據(jù)類型能做不同解讀

  • 對于signed int能夠解讀為-3的二進(jìn)制補(bǔ)碼
  • 對于unsigned int 能夠解讀為4294967293


副作用4:無符號整數(shù)和帶符號整數(shù)混用

第二,混合有符號和無符號整數(shù)時(shí),可能會導(dǎo)致意外行為。例如我們將無符號的整數(shù)作為函數(shù)的參數(shù),C編譯器是無法檢測傳入的整數(shù)是否符合特定的程序邏輯的,尤其是無符號和帶符號的比較問題,可以參見如下例子,這個(gè)例子引用自stackflow網(wǎng)站,而且提問率也非常高。

例如執(zhí)行如下函數(shù),我們希望給函數(shù)傳遞一個(gè)無符號整數(shù),當(dāng)傳入?yún)?shù)大于10才執(zhí)行特定操作,否則就不執(zhí)行該操作。

void foo(size_t n){
      if(n>10){
            cout<<n<"我正在處理某項(xiàng)任務(wù)...."<<endl;
       }else{
            cout<<n<"條件沒達(dá)成,我沒有執(zhí)行任務(wù)...."<<endl;
      }
}

int main(void){
    foo(-10);
}

我們向foo中傳遞任意一個(gè)負(fù)數(shù),都能該程序不按我們的邏輯完全跑偏了,編譯器在編譯程序的時(shí)候也沒有警告。 因此,混淆帶符號和無符號對于程序設(shè)計(jì)來說通常是一個(gè)邏輯錯誤。那么問題的根源是什么呢?stackflow的答案我仍然不太滿意,同學(xué)們可以自行查找該問題,而問題的本質(zhì)仍然提到無符號整數(shù)的環(huán)繞行為。至于-10被環(huán)繞成size_t的某個(gè)無符號整數(shù)的最大值,請讀者自行思考,我就不重復(fù)熬述了。

程序沒有按我們的意愿去執(zhí)行

而解決該問題即非常簡單,只要我們將size_t類型的參數(shù)變換成帶符號的int/long等可以令程序正常運(yùn)行了。

直到寫本文用的是C++17,這個(gè)問題C ++程序員仍然無法回避,當(dāng)你翻開C++標(biāo)準(zhǔn)庫的頭文件,大量地使用了類似size_t作為參數(shù)的類型和函數(shù)返回值。我們必須勇敢面對無符號整數(shù)的各種副作用。

從那么多的副作用示例中,我們得到結(jié)論就是在我們的應(yīng)用代碼中盡量避免使用無符號類型的數(shù)字類型,但有時(shí)候我們必須從將標(biāo)準(zhǔn)符的API中返回size_t類型轉(zhuǎn)換為帶符號的數(shù)字類型版本。

正確面對無符號類型的整數(shù)

毫不奇怪,在任何類型的轉(zhuǎn)換期間都可能發(fā)生奇怪的事情,并且使用無符號到有符號整數(shù)也不例外。但是,這些類型的轉(zhuǎn)換特別棘手,因?yàn)樗鼈兛赡軐?dǎo)致并非總是引人注意的錯誤,并最終會影響您的C ++代碼,當(dāng)您具有可與STL庫互操作的C ++代碼時(shí),無符號整數(shù)很常見。實(shí)際上,例如您的程序是一個(gè)使用在std :: vector容器中獲取商品計(jì)數(shù),則vector的size方法將返回就是size_t的類型的值,這是一個(gè)無符號整數(shù)。

那么,從無符號到帶符號轉(zhuǎn)換的過程中,我們?nèi)绾潍@得singed類型變量的最大值?

轉(zhuǎn)換前檢查整數(shù)限制

從無符號整數(shù)到有符號整數(shù)的轉(zhuǎn)換。我們需要檢查輸入的無符號的字面量值是否超過當(dāng)前有符號整數(shù)類型的最大上限。 若超出在這種情況下,我們將繼續(xù)拋出C ++異常來,提醒用戶目前處理的整數(shù)超出了當(dāng)前帶符號整數(shù)的最大整數(shù)。

嗯~~,C ++標(biāo)準(zhǔn)庫中有一個(gè)名為std :: numeric_limits的標(biāo)準(zhǔn)組件。 這是一個(gè)類模板,可用于查詢算術(shù)類型(包括int)的各種屬性。 您將類型的名稱作“ numeric_limits <T> :: max()”將返回類型T的最大值。這里的例子以將最大值存儲在int類型的變量中,因此我們可以簡單地調(diào)用numeric_limits <int>::max() ,如下示例

template <typename T>
T convert_numberic(size_t n)
{
    if (n > static_cast<size_t>(std::numeric_limits<T>::max()))
    {
        throw std::overflow_error("參數(shù)n轉(zhuǎn)換錯誤");
    }
    return static_cast<T>(n);
};

除非你從事的是天文學(xué)類似的計(jì)算,我敢肯定從無符號的int /long到帶符號的int/long的轉(zhuǎn)換的有效數(shù)據(jù)區(qū)間
足以應(yīng)付日常開發(fā)的方方面面。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容