C語言中數據在內存如何存儲

數據類型

常見的數據類型

常見的數據類型 字節
char 字符數據類型 1
short 短整型 2
int 整形 4
long 長整型 4
long long 更長的整形 8
float 浮點型 4
double 雙精度浮點型 8

註意:

  • C語言裡面沒有字符串類型
  • 關於int和long的大小:C語言隻是規定瞭:sizeof(long)>=sizeof(int)
  • 佈爾類型(_Bool)(C99引入)專門用來表示真假,但是在C語言中不需要佈爾類型也可以表示真假
#include<stdbool.h>
int main()
{
    _Bool flag = false;
    _Bool flag2 = true;
    if(flag)
    {
        printf("haha\n");
	}
    if(flag2)
    {
        printf("hehe\n");
	}
return 0;
}//隻打印瞭hehe

類型的基本歸類

整形

char也屬於整形(每一個字符在儲存的時候是儲存他所對應的ascll值,ascll是整數)

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

有符號數和無符號數

有符號數
int main()
{
	int a = 10;
    int a = -10;
	return 0;
}//a是一個有符號數,它可以儲存正負整數

//int ===> signed int
//short ===> signed short
//long ===> signed long

無符號數

有一些變量隻有正數由意義,例如年齡,價格。定義這些變量的時候就可以用無符號數定義 ,無符號數隻能存儲正數。

int main()
{
    unsigned int a = 10;
    //無符號變量隻能儲存正數
    a = -10;
    //即使這裡輸入瞭一個負數,它也會把這個負數轉化成一個正數(不是簡單的去掉符號,這是關於二進制的計算)
    return 0;
}

是否char 等於signed char呢?

答案:取決於編譯器

  • 我們會發現這樣一件事:
    int 就是 signed int
    short 就是 signed short
    long 就是 signed long
  • char 等於signed char還是unsigned char 取決於編譯器,不同的編譯器可能是不同的結果,常見的編譯器下是等於signed char
對於有符號數字和無符號數字的打印

打印無符號數應該用%u

%u和%d打印的解讀方式不同:

  • 使用%d 時,會認為這是一個有符號數,打印的時候會認為二進制中第一位是符號位;
  • 使用%u時,會認為這是一個無符號數據,會認為整個二進制序列都是有效位。
#include<stdio.h>
int main()
{
    unsigned int a = 10;
    printf("%u",a);//正確的形式
    
    //如果存儲瞭一個-10進去會怎麼樣
    a = -10;
    printf("%u",a);
    //會打印4294967286,而這個數據不是隨機數
    return 0;
}

為什麼無符號整形儲存 -10 的時候會打印出來4284967286(並不是隨機數)?

%u在解讀的時候認為此時a仍然存儲的是正數,解讀瞭a的補碼。在本章後面介紹原反補嗎的時候在詳細解釋細節。

浮點型

浮點型 大小
float 4
double 8

構造類型(自定義類型)

構造類型
數組 數組名去掉後剩下的就是數組的類型
結構體 struct
枚舉類型 enum
聯合(聯合體)類型 union

指針類型

指針類型
char* pc
int * pi
float* pf
void* pv

空類型

void表示空類型(無類型)

通常應用於函數的返回類型,函數的參數,指針類型

整形在內存中的存儲

int a = 10;
int b = -10;

然後我們觀察a、b在內存中的儲存

數據在內存裡面是以二進制儲存的,但是編譯器是以十六進制展現給我們看的:

a在內存中的值是 :0a 00 00 00

b在內存中的值是: f6 ff ff ff

為什麼是這樣的值呢?下面介紹整數的原碼、反碼、補碼。

原碼,反碼,補碼

整數二進制有3種表示形式,而內存中存儲的是二進制的補碼

例如 1 的原碼:

00000000 00000000 00000000 00000001

正整數

正整數的原碼、反碼和補碼相同

負整數

  • 原碼:按照一個數的正負直接寫出來的二進制就是原碼
  • 反碼:符號位不變,其他位按位取反 (並不是按位取反)
  • 補碼:反碼的二進制序列加1

從原碼轉換成補碼:先取反再加一。

從補碼轉換成原碼:可以先減一再取反,也可以先取反再加一(和原碼轉換成補碼的過程相同)。

類型 數據:15 數據:-15
原碼 00000000 00000000 00000000 00001111 10000000 00000000 00000000 00001111
反碼 00000000 00000000 00000000 00001111 11111111 11111111 11111111 11110000
補碼 00000000 00000000 00000000 00001111 11111111 11111111 11111111 11110001

解釋為什麼%u打印-10;會出現4294967286

用上面的方法我們就可以計算出-10的補碼:

數據 補碼
10 11111111 11111111 11111111 11110110

回到最開始使用%u打印-10會打印成4294967286,是因為在使用%u的時候,不會解讀符號位,會將整個32位二進制都當作有效位,讀出一個數據,而這個數據就是4294967286。

同時回到剛才a,b的值

int main()
{
int a = 10;
int b = -10;
    //a: 00000000 00000000 00000000 00001010
    //b: 11111111 11111111 11111111 11110110
}
//四個二進制位轉換成一個十六進制位
//a: 00 00 00 0a
//b: ff ff ff f6

為什麼和在內存界面看到的不同呢?

內存界面和我們計算出來的順序是相反的:

數據 計算結果 內存
10 00 00 00 0a 0a 00 00 00
-10 ff ff ff f6 f6 ff ff ff

為什麼會倒著存進去?

這就和字節序有關,下面我們來瞭解字節序

大小端字節序

當儲存的內容超過一個字節的時候,儲存的時候就有順序

(一個char類型的數據是沒有字節序。char類型的數據隻有一個字節,沒有順序)

機器有兩種對字節的存儲順序:

  • 大端字節序存儲:
    低字節數據存放在高地址處,高字節數據存放在低地址處
  • 小端字節序存儲
    低字節數據存放在低地址處,高字節數據存放在高地址處

我們用下面這個例子來解釋:

int main()
{
    int a = 0x11223344;//0x開頭說明是十六進制數字
    //再內存界面看到:44 33 22 11
   
    return 0;
}

而我在觀察內存的時候發現我的機器是按照方式2進行存儲的,所以我的機器是采用的小端字節序。

那麼有什麼方法可以快速判斷自己當前使用的機器屬於哪一種字節序呢?

設計一個小程序判斷當前機器屬於哪種字節序

#include<stdio.h>
int main()
{
    int a = 1;
    //a的十六進制是 00 00 00 01
    //如果是大端那麼內存中為:00 00 00 01
    //如果是小端那麼內存中為:01 00 00 00
    //隻需要判斷第一個字節的內容是不是1
    char*pc = (char*)&a;
    //強制類型轉換截取瞭a的第一個字節
    if(*pc)//也可以是:if(*(char*)&a)
    {
        printf("小端");
	}
    else
    {
        printf("大端");
	}
        
	return 0;
}

儲存時數據發生截斷以及整型提升

例題1

int main()
{
	char a = -1;
	signed char b = -1;
	unsigned char c = -1;
	printf("%d %d %d ",a,b,c);//會打印出-1 -1 255
    return 0;
}

解釋:

  • 第一步:
    這裡我們將三個相同的整數 -1 分別存進瞭兩種類型的變量,(在我所使用的VS2019編譯器下char和signed char等價),而這兩種類型又都屬於char類型
  • 第二步:
    char 類型的變量隻能儲存一個字節(8個比特位)大小的數據,但是 -1 是整形(包含32個比特位),
    這裡就需要發生數據截斷: -1 的二進制補碼是11111111 11111111 11111111 11111111
    截斷:將補碼的最後八位賦值給變量a、b、c。
  • 第三步:
    這裡需要將char類型的數據以%d的方式打印,但是%d隻能解讀整數數據(整數有四個字節),而char類型的三個變量都隻有一個字節,所以這裡會發生整型提升:
    整形提升:對於有符號數據,高位補符號位,對於無符號數據:高位補0
數據(變量) 整形提升前 整形提升後(補碼) 原碼代表的數據
a/b 11111111 11111111 11111111 11111111 11111111 -1
c 11111111 00000000 00000000 00000000 11111111 255

%d打印的時候會認為這就是要打印數據的補碼,按照打印正常整數的形式打印這三個變量

例題2

#include<stdio.h>
int main()
{
    
    char a = -128;
    printf("%u",a);//會打印4294967168
    return 0;
}

解釋:

  • 第一步:
    觀察數據: -128 是一個整數,二進制有32位 : 但是接受這個數據的變量是一個char類型的變量(隻能接受8個比特位)
  • 第二步:
    數據的截斷:
數據 二進制(補碼) 截取(a儲存的部分)
-128 11111111 11111111 11111111 10000000 10000000

聯想:這裡如果是a = 128,那麼階段後的值仍然是10000000

  • 第三步:
    打印整數(四個字節),所以這裡需要發生整形提升(a是有符號char,高位補符號位)
數據(變量) 整形提升前 整形提升後(補碼)
a 10000000 11111111 11111111 11111111 10000000
  • 第四步:
    %u的形式打印這個變量,因為%u應該打印的是無符號整數,且打印的時候認為整個32位全為有效位,
    就會打印出4294967168

浮點型在內存中的存儲

常見的浮點數 例子
字面浮點數 3.14159
科學計數法的表示形式 1E10(1乘以10的10次方)

註意:

  • 在浮點型數據後面加瞭f是float類型,不加則默認是double類型
  • %f和%lf默認小數點後六位

浮點型和整形在內存中的儲存方式不同

我們通過下面這個例題來探究浮點型和整形在內存中的儲存方式有什麼不同

#include<stdio.h>
int main()
{

	int n = 9;
	float* pf = (float*)&n;
    //第一組
	printf("n = %d\n", n);    //打印出:9
	printf("*pf = %f\n", *pf);//打印出:0.000000
	//第二組
	*pf = 9.0;
	printf("n = %d\n", n);	  //打印出:1091567616
	printf("*pf = %f\n", *pf);//打印出:9.000000

	return 0;
}

為什麼會出現這樣的情況?

回答這個問題前,先來瞭解浮點型的二進制存儲形式:

國際標準電氣電子工程師學會(IEEE):任何一個二進制浮點數V都可以表示成下面的形式

  • 表示形式:(-1)^S * M * 2^E
  • (-1)^S表示符號位,當S為0 時表示正數,當S為1時,表示負數
  • M表示有效數字,大於等於1,小於等於2
  • 2^E表示指數位
(5.5)十進制 (5.5)二進制
5.5 101.1
5.5*(10^1) 1.011*(2^2)
十進制浮點數:5.5
轉化成二進制:101.1
可以寫成:(-1)^0 * 1.011 * (2^2)
S = 0
M = 1.011
E = 2
隻需要儲存SME三個值
  • IEEE浮點數標準定義瞭兩種基本的格式:以四個字節表示的的單精度格式和八個字節表示的雙精度格式

單精度浮點數儲存格式(32位)

第一位 接著8位 剩下23位
符號位 指數位 有效位

雙精度浮點數儲存格式(64位)

第一位 接著11位 剩下52位
符號位 指數位 有效位

IEEE754對於有效數字M和指數E有一些特別的規定:

  • 關於M:
    1.我們已經知道1<=M<2,也即是說M一定可以寫成這樣的形式:1.xxxxxx
    其中xxxxxx表示小數點後面的部分
    2.在計算機內部儲存M時,由於第一位總是1,所以把這個1省略,隻保存後面的小數部分,這樣可以節約一位有效數字,這樣的話儲存的數據精度就可以提升一位:例如在單精度浮點型存儲格式中,最後23位作為有效位,但是儲存在計算機的數據精度是24位
  • 關於E:
    計算機會認為這是一個無符號數,但是十幾行會存在很多E取負數的情況,所以IEEE754規定:存入內存時,E的真實值必須在加上一個中間數 ,對於單精度,這個中間數(也叫做偏移量)是127,對於雙精度,這個中間數是1023
    例如:對於十進制的數字0.5,它的二進制是0.1,E= -1;那麼我們就需要把-1在加上127得到126後,將126儲存在指數位

拿出這些儲存的數據(三種情亂)

情況一:E不全為0或不為全1

  • E: 指數位值減去127(1023)得到真實值
  • M: 有效位的數值前面加上1.

情況二:E全為0

  • E:
    真實的E是一個十分小的數字,接近0;
    這時候不用計算,E直接就等於1-127(1-1023)就是它的真實值
  • M:
    M不再在前面加上1,而是還原成0.XXXXXX的小數,這樣做是為瞭表示正負0,以及接近於0的很小的數字。

情況三:E為全1

  • 真實的E是一個十分大的數字,代表正負無窮大的數字

解釋上面的例子

  • 第一組為什麼以%f打印整數9,會打印出0.000000?

原因:

此時E為全0,是一個會被判定成一個十分小的數據,所以打印0.000000

  • 為什麼第二組中以%d的形式打印*pf時,會打印出1091567616?

以上就是本文的全部內容,希望對大傢的學習有所幫助,也希望大傢多多支持WalkonNet。

推薦閱讀: