換個角度帶你學C語言的基本資料型別

華為雲開發者聯盟發表於2022-06-01
摘要: C語言的基本資料型別,大家從學生時代就開始學習了,但是又有多少人會試圖從底層的角度去學習呢?這篇文章會用一問一答的形式,慢慢解析相關的內容和困惑。

本文分享自華為雲社群《從深入理解底層的角度學習C語言之基本資料型別》,作者: breakDawn 。

C語言的基本資料型別,大家從學生時代就開始學習了,但是又有多少人會試圖從底層的角度去學習呢?這篇文章會用一問一答的形式,慢慢解析相關的內容和困惑。

  1. 資料型別位數和符號
  2. 資料型別轉換
  3. 浮點數

資料型別位數和符號

Q: C裡的signed 和unsigned型別的區別是什麼?

A:拿unsigned char無符號char 和 signed char有符號char舉例(因為他們都是1位元組,比較好舉例子)

假設某個區域性變數a,記憶體裡存的都是0xff(即二進位制11111111)

執行printf("%d",a)時, 輸出的是255,還是-1呢?

如果a是無符號,那就是255。

如果a是有符號,那就是-1。

Q:為什麼有符號的0xff輸出的是-1?
A:這個就是補碼的概念。

正數的補碼就是其本身
負數的補碼是在其原碼的基礎上, 符號位不變, 其餘各位取反, 最後+1. (即在反碼的基礎上+1)

  • 補碼的計算方式:如果是-1,則負號就是首位的“1”, 而“-1”裡的1作為二進位制是0000001,取反+1,得到1111111, 和首位1拼接,變成了11111111.
  • 進行printf列印時,C語言通過變數型別,確認11111111的首位是符號位,於是通過補碼的反向計算,得到實際真值為-1。
  • 如果是無符號,則C語言通過變數型別,確認11111111的首位不是符號位,不需要反向計算,於是直接輸出255。

原碼、反碼、補碼對於+1和-1的表示如下
[+1] = [00000001]原 = [00000001]反 = [00000001]補
[-1] = [10000001]原 = [11111110]反 = [11111111]補

Q: 已知正負數預設都是補碼的形式,為什麼不能用原碼錶示數字呢?即只用第一個識別符號號位,後面7位就是代表真實絕對值
A:計算機CPU做計算時,無法區別符號位,只會死板的將8位數字進行加法計算。
假設做減法,就和下面那樣
1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2
可以看到符號位的資訊會誤導減法的計算。

Q: 那為什麼不用能反碼呢
A:因為反碼對於0的表示有兩種情況,11111111可以代表-0,而00000000代表+0,相當於浪費了。
而補碼不存在這個情況。11111111代表-1,00000000代表0。

Q: 為什麼要有補碼?補碼有什麼好處?
A:當計算機執行1 - 1時, 希望都是用加法的動作來做,且不希望做if-else判斷,根據符號位去判斷正負再做加減,對計算機的消耗是很大的。

使用補碼的機制,則可以將1-1轉成變成1+(-1)
那麼-1就是補碼0xff,和0x01相加,變成了0,即不需要做真正的減法即可

Q: 剛才提到CPU希望都是位加法,不肯做減法,為什麼?
A:因為CPU的減、乘、除都是基於加法、移位等操作實現的。
加法過程依賴CPU的ALU累加器,累加器背後的電路是數位電路異或門和與門的組合。

換個角度帶你學C語言的基本資料型別

Q: 為什麼補碼錶示的情況下,範圍是-128到127?為什麼補碼會比原碼和反碼多一位?
A:就是上面提到的0的問題。原碼的10000000、00000000都表示0,補碼的11111111和00000000都表示0,而補碼只有1個0的表示

同時補碼有一個100000000, 把後7位取反+1,等同於-128。

原碼、反碼、補碼知識詳細講解(此作者是我找到的講的最細最明白的一個)

Q: 計算機在CPU做計算時,怎麼識別是無符號還是有符號?
A:CPU 所處理的暫存器、記憶體中的數本身無符號資訊。CPU 做加減法時會一起做無符號數的進位/有符號數的溢位標誌,並不專門對待有符號數和無符號數。

有無符號的區別是隻屬於(中)高階語言的概念,反映到機器語言上,是跟運算及與其結果相關的指令上的區別,而不會反映到 CPU 所處理的數本身。

即CPU處理時,統一用加法處理,但是否要做求補等操作,取決於提供的運算指令。

Q: C語言的char是signed char還是unsigned char?
A: 當你定義為char時, 可能是signed char,也可能是unsigned char。

這個取決於你編譯器的實現。
-funsigned-char : 設定為 unsigned char
-fno-signed-char : 設定為 非 signed char
-fsigned-char : 設定為 signed char
-fno-unsigned-char : 設定為 非 unsigned char

Q: int有可能像char一樣,即可能是signed int也可能是unsigned int嗎?
A:int一定是有符號int。不會因為編譯器不同而不同。

Q: 為什麼char可以區分有符號或者無符號,但是int只能預設為signed int ?
A:個人理解和應用場景有關,char不一定會參與計算,而int大部分情況下都是有符號計算,因此預設為signed int比較好。

Q: ILP32、LP64、LLP64分別是什麼?
A:指的是這個作業系統中,有哪些型別分別是多少位的意思。
I指int
L指long
LL指long long
P指point指標
32和64就是分別指32位和64位。

  • 32位系統一定是ILP32模型
  • 64位系統中,unix一般是LP64,而windows則是LLP64
    即linux中,long是64位, 而在windows中,long是32位,而只有long long是64位

Q: 為什麼windos要用LLP64這麼奇怪的模型?這個模型裡, long是32位,long long 才是64位。
A:來自知乎陳碩大佬的回答:

我猜,是因為 Windows API 從 16-bit 升級到 32-bit 發生得太晚了——大約是隨 1995 年釋出的 Windows 95 而普及 。

雖然之前有 Windows NT 3.x 和 Win32s,但似乎比較小眾。

而 Unix 從 16-bit 升級到 32-bit 發生在 1980 年前後,當時執行在 VAX 上的 Unix/32V 和 3BSD 都是 32-bit 的。

造成的結果是,兩邊的程式對 short/int/long 的長度形成了不同的習慣認知:

Unix 程式習慣了 int 是 32-bit,而 long 不一定只有 32-bit。Windows/DOS 習慣了 long 是 32-bit,而 int 有可能是 16-bit 或 32-bit,因為剛剛從 16-bit 升級上來嘛。

當往 64-bit 升級的時候,如果把 Windows 的 long 升級到 64-bit,會破壞原來很多程式的假設,只好用個新的型別來表示 64-bit 整數了。反正 LONGLONG 在 32-bit 程式中也是 64-bit 整數,乾脆用它好了。

詳細的資料型別展示:

換個角度帶你學C語言的基本資料型別

PS: 從上面可以看到java虛擬機器的一個優勢,就是對開發者而言,遮蔽了各不同系統情況下的資料位數。

Q: 那麼又有個問題,java虛擬機器如何實現不同平臺可以跑相同的java程式碼,不用擔心底層資料型別的?
A:如圖所示,class位元組碼都是同一份,但是不同的系統,會有不同的虛擬機器直譯器實現,在直譯器實現裡處理了不同的資料型別位數情況。

資料型別轉換

Q: C裡的隱式型別轉換有什麼規律?
A:

  • 佔用位元組數少的型別,向佔用位元組數多的型別轉換;
    int->long
  • 佔用位元組數相同情況下,有符號向無符號轉換;
    int->unsigned int
  • 整數型別向浮點型別轉換;
    int -> double
  • 單精度向雙精度轉換;
    float->double
換個角度帶你學C語言的基本資料型別

Q: 下面這個例子輸出多少,為什麼?
A:

void Test()
{
    int a = -1;
    unsigned b = 10;

    if (a > b)
    {
        printf("a is greater than b.\n");
    }
    else
    {
        printf("a is less than or equal b.\n");
    }
}

輸出a>b即a is greater than

因為a=-1,儲存的二進位制是11111111, 強轉成unsigned時,二進位制沒有變,但是對編譯器而言表示的大小變成了255了。

浮點數

float、double、long double的位元數、有效位數、數值範圍如下:

換個角度帶你學C語言的基本資料型別

Q: 下面這個程式碼輸出什麼?

#include <stdio.h> 
int main(void)
{    
    float a = 9.87654321;  
    float b = 9.87654322;
    if(a > b)
    { 
        printf("a > b\n");    
    }
    else if(a == b)
    {
        printf("a == b\n");
    }
    else
    {
        printf("a < b\n");
     }
     return 0;
}

A:輸出"a=b", 因為float最多7位有效小數點位數。

Q: 32位float,1bit為符號位,23bit為位數,8bit為指數, 這3個劃分是如何得到float的有效位數以及數值範圍的?
A:IEEE754標準理解。

【計算機組成原理】IEEE754標準

有人問為什麼要學習這個?

對於高精度場景下的浮點計算,掌握IEEE754的標準很重要,否則無法理解高精度場景時計算過程出現的各種問題, 特別是一些金融場景,對於小數點後面的數字會特別敏感。

Q:java的BigDecimal類可以表示任意精度,原理是啥?

A:BigDecimal的原理很簡單,就是將小數擴大N倍,轉成整數後再進行計算,同時結合指數,得出沒有精度損失的結果。

以long型的intCompact和scale來儲存精確的值。

 

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章