1. 程式人生 > 其它 >13.【C語言進階】資料的儲存

13.【C語言進階】資料的儲存

資料基本型別

為什麼會有不同的型別,這些型別有記憶體大小上的差異,那麼他們還有什麼差異呢?

  1. 在記憶體中開闢空間的大小
  2. 讀取記憶體空間方式的差異
	char //字元型別
    short //短整型
    int  //整型
    long //長整型
    long long //更長整型 
    float  // 單精度浮點型
    double  // 雙精度浮點型

除此以外,在整型中同一種類型的資料還分為有符號和無符號型.

整型中:

char
	signed char
	unsigned char
short
	signed short
	unsigned short
int
	signed int
	unsigned int
long
	signed long
	unsigned long

浮點數型別:

float
double

當然,還有指標型別,大致相同不再贅述。

整型在記憶體中的儲存

既然不同型別在記憶體中需要不同的空間,那麼它們如何在記憶體儲存的呢?

int main()
{
	int a = 10;
	int b = 5;
	printf("%d %d", a, b);
	return 0;
}

0x010FFC88是a在記憶體中的地址,在這裡我們可以看到:

0a 00 00 00 分別是四個位元組,而0a就是我們找到的那一個地址,即我們通常所說的a的首地址。

在32位機器上int整型的大小為32個bit,即四個位元組,這裡是使用的16進位制數字來表示的,每四個二進位制位可以轉換為一個十六進位制位(每四個二進位制位的權重與一個十六進位制位的權重相同,所以這裡可以使用8個十六進位制數字來表示a的值。

首先我們先要知道:

二進位制在計算機中都是以補碼的形式儲存的。

為什麼會存在補碼?

首先,如果單純的使用原碼進行計算,由於符號位的存在,那麼必定時會存在很多問題,那有沒有一種二進位制序列,可以不考慮符號位的存在直接進行運算的呢?

此外,原碼和補碼的相互轉化,都是取反加一併不需要額外的計算機硬體電路。

  • 正數的原碼和補碼相同。
  • 負數的補碼和原碼有著取反加一的關係。

比如 -1

原碼:10000000000000000000000000000001

反碼:11111111111111111111111111111110

補碼:11111111111111111111111111111111

既然32bit是四個位元組,而計算機中基本的記憶體單位也是一個位元組,那麼超過一個位元組的資料在記憶體中是按何種順序儲存的呢?

首先我們要知道有高位和低位的區別,權重高的位稱為高位,反之低位。

接下來我們就可以繼續了。

大端儲存:

存放低位的位元組在高地址處,高位的位元組在低地址處。

小端儲存:

存放低位的位元組在低地址處,高位的位元組在高地址處。

注意:大小端僅針對C語言中的內建資料型別,以巨集觀的角度看整個結構體,是不存在所謂的高位和低位的也就不存在位元組序的問題了,成員的儲存遵循大小端儲存模式。

也從另一個角度說明了對結構體的訪問是不存在巨集觀對於整個結構的單獨訪問,而是對它的成員的訪問和讀取。

再來看這張圖:

a == 10

十六進位制:0000000a

可我們卻看到好像資料在記憶體中不是這樣存放的,可以看到0a也就是資料的低位位元組是在低地址處的,它的高位位元組是在記憶體中的高地址處的。

為什麼會存在大小端儲存

這是因為我們在計算機系統中,我們是以位元組為單位的,每個地址單元都對應著一個位元組,一個位元組是8bit。

另外,對於位數大於8位的處理器,例如16位或者32位處理器,由於暫存器的寬度大於一個位元組,也就意味著在處理資料時如何安排多個位元組的問題。

想測測自己目前所使用機器時如何儲存資料的嗎?

從二者的差異入手,對於1

補碼:00000000000000000000000000000001

如果數大端儲存:

那麼在記憶體中應該是 00 00 00 01

而如果是小端儲存應該是 01 00 00 00

那麼我們就從此入手,看看在其首地址處的那個位元組裡儲存的是什麼,就可以解決相應問題。

#include<string.h>
int main()
{
	int a = 1;
	char* p = (char*)&a;
	if (*p == 1)
		printf("小端位元組序\n");
	else
		printf("大端位元組序\n");
	printf("%d ", *p);
	return 0;
}

浮點型儲存規則

int main()
{
	int n = 9;
	float* pa = (float*)&n;
	printf("%d\n", n);
	printf("%f\n", *pa);
	return 0;
}

同樣的資料同樣的位元組數,為什麼打印出來卻不同呢?

其實,這是浮點數和整型在記憶體中的儲存方式的差異導致的。

根據國際標準IEEE(電氣和電子工程協會) 754,任意一個二進位制浮點數V可以表示成下面的形式:

  • (-1)^S * M * 2^E
  • (-1)^S表示符號位,當S = 0時表示正數,當S= 1S時表示負數。
  • M表示有效數字 大於等於1小於2
  • 2^E表示指數位。

例如:十進位制的5.0,寫成二進位制是101.0,類比十進位制的科學計數法我們還可以寫成1.01*22.

按照上面規定的格式我們就可以得到:

S = 0,M = 1.01,E= 2。

同上:

十進位制的 -5.0,寫成二進位制為 -1.01 * 22 ,那麼只有正負號改變了,僅僅將上面的S改為1,其他的不變就可以了。

既然在表示一個浮點數需要在記憶體中儲存這三個資料,那麼這究竟在記憶體中是如何存放的呢?

​ IEEE 754規定:

對於32位的浮點數,最高位的是符號位S,緊接著的8個位是指數E,剩下的23位存放的是數字的有效位。

而對於64位的浮點數,最高位仍然是符號位,不過指數位和有效數字位都所有增加,分別是11位和52位。

IEEE 754還對M和E有一些特殊規定:

前面說過,類比十進位制的科學計數法,而我們都知道科學計數法的有效數字範圍為[ 1,10),同樣這裡的有效數字M也必須滿足 1 <= M <2,也就是說無論是什麼樣的數字,最終都可以寫成1.xxxxxxxxx * 2 n這樣的形式,因此為了提高我們的精度,在記憶體中,我們僅僅存放有效數字中小數點後面數字,在我們需要讀取時,再將其加上去,這樣就為可以儲存的有效位數就由23位變為了24位。

例如:1.01 * 2 2.

我們在儲存有效數字M的時候僅僅將 1.01 中的 01 存放,將1捨去。

64位浮點數同理。

至於指數E,情況稍微複雜

首先,E是一個無符號正數(unsigned int)

對於32 位浮點數來說,E有8位,也就意味著它的取值範圍是0~255.

但是我們知道,這裡的E是可以存放負數的,因此IEEE754規定,我們在記憶體中存入E時,必須將真實值加上一個中間數,對於8位的E,這個中間數是127,而對於64位的E這個中間值是1023.

也可以直接這樣理解:

在32位浮點數中,指數的範圍是 (0-127,255-127)。

例如 1.01 * 210的E是10,但我們想存到記憶體中時,必須先加上中間數127,也就是137,然後再將137的二進位制序列存入到記憶體中。

E的存放還可以分為三種:

  • E為不全為1或不全為0

這時是最一般的情況,我們先通過E在記憶體中的計算值減去127得到指數的真實值,然後再將有效數字M加上第一位的1。

例如:

浮點數 5.0 ->二進位制可以表示為 1.01 * 102

  1. 指數部分先加上 中間數127 ,得到 129.

  2. 129 的二進位制為 10000001.

  3. 有效數字為1.01,去掉正數部分為 01,補齊01到23位 01000000000000000000000

    則其二進位制序列可表示為:

    0 10000001 01000000000000000000000

  • E為全0

這是浮點數的指數的真實值就為 1-127或者1 - 1023.

而有效數字就不再加上首位的1,這樣就可以表示無限接近於 0。

  • E全為 1

    這時,如果有效數字M全為0,則表示無窮大。

int main()
{
	int n = 9;
	float* pa = (float*)&n;
	printf("%f", *pa);
	return 0;
}

那麼我們來解決這個問題。

先來看 整型 9 的二進位制序列:

00000000000000000000000000001001

我們取出它的地址後,以浮點數的儲存方式去看待它,那麼可以看成

0 00000000 0000000000000000001001

可以看到這裡和我們的的第三種情況 E全為0相同,浮點數可以寫成:

V=(-1)^0 × 0.00000000000000000001001×2(-126)=1.001×2(-146)

很顯然這是一個無限接近於0的數字,所以用十進位制數字表示就是0.000000.

問題解決了~