【C语言进阶】从入门到入土(数据的存储)
前言:
這一篇我們來深度剖析數據在內存中的存儲,讓我們走進數據在內存中到底是任何進行存儲的,不同的數據類型有何差異。
進階之數據的存儲:
- 一、數據類型詳細介紹及歸類
- 1.數據類型介紹
- 2.類型的基本歸類
- ① 整形家族
- ② 浮點數家族
- ③ 構造類型
- ④ 指針類型
- ⑤ 空類型
- 二、整形在內存中的存儲
- 1.關于整形的大小范圍
- 2.變量創建時的內存空間
- 3.原碼、反碼、補碼
- 4.補碼的表示與存儲
- 三、大小端字節序介紹及判斷
- 1.大小端介紹
- 四、浮點型在內存中的存儲
- 1.常見的浮點數及范圍
- 2.整形與浮點型存儲方式
- 3.浮點型在內存中的存儲
- 4.IEEE754的特別規定
- 5.重新觀察代碼
一、數據類型詳細介紹及歸類
1.數據類型介紹
基本內置類型(即為C語言本身具有的類型):
我們先前學到的這幾類基本類型:
| char | 字符數據類型 |
| short | 短整型 |
| int | 整形 |
| long | 長整型 |
| long long | 更長的整形 |
| float | 單精度浮點數 |
| double | 雙精度浮點數 |
ps:一些較為老的編譯器中沒有long long類型。
類型的兩個重要意義:
對于第一個意義,我們可以拆開來看待:
1.這么多種類型又如何分類?
我們可以歸類一下,因為char字符在底層存儲的時候是ASCII碼值,ASCII碼值為整數,所有也算屬于整形。所以上面的char到long long 都屬于整形家族,float和double為浮點數家族。
2.而為什么要有整形浮點型的劃分呢?
因為我們生活中到處都是整數和小數。
3.為什么要有如短整型長整形,單精度雙精度之分呢?
這是因為我們在創建類型的內存大小直接影響我們所能存儲的范圍大小,簡單來說就是能使用適當的空間去存儲適當的值,不至于太大的空間浪費或越界溢出。
而第二個意義中,就是類似于同一種事務以不同的看法去看待它和對待它。
比如下面代碼:
int main() {int a = 10;float b = 10.0f;return 0; }在這里,a和b所創建的類型都是4個字節,而不同的是類型,當你用int類型創建的時候,a的創建,存取都是按照整形來操作;同理在創建b的時候,就是完全按照float類型來了。
具體有什么不同,我們接著往下看就知道了。
2.類型的基本歸類
① 整形家族
ps:[ ]內可省略,如 signed short int 可寫為signed short,或者short int 也可以寫為short。
| char | unsigned char signed char |
| short | unsigned short [int] signed short [int] |
| int | unsigned int signed int |
| long | unsigned long [int] signed long [int] |
我們可以看到在整形家族中,每個類型分為有無符號兩種,而一般我們在編譯過程中,定義一個整形的時候并沒有在前面寫有無符號,這時一般默認為有符號。如:int a = 10;定義的a就是有符號的,short和long同樣適用。
但char 在定義的時候,默認符號取決于編譯器,大多數編譯器中char是有符號的char,即為signed char。
即:
#include <stdio.h>int main() {//有無符號?char a;//取決于編譯器short b;//等于 signed shortint c;//等于 signed intlong d;//等于 signed longreturn 0; }那么有符號和無符號有什么區別呢?
我們知道,整數有原反補三種編碼方式,原碼為顯示數值的形式,補碼為在內存中存儲的形式。
有符號的原碼中,最高位為符號位,即0代表正號,1代表符號,其他位是有效(數值)位。而無符號的原碼中,全部為有效(數值)位。
我們來看這一段代碼:
int main() {unsigned char c1 = 255;signed char c2 = 255;char c3 = 255;printf("%d\n", c1);printf("%d\n", c2);printf("%d\n", c3);//三個輸出分別是多少?return 0; }答案是: 255 ,-1 , -1 。
還不清楚原碼反碼補碼的可以到 【C語言】從入門到入土(操作符篇)中的移位操作符處學習一下。也可以先看下面也有介紹。
當我們將255儲存的時候,其原碼為11111111,因為正數原反補碼相同,所有在內存中存儲的時候也是11111111。不清楚我們可以打開內存中查看。在vs2019中 F10運行起來,然后點擊調試——窗口——內存——內存1,然后輸入地址就可以看見了。
這里是十六進制顯示,一個f就是15,也就是二進制中1111,兩個f所以就是11111111,證明我們的原碼是正確的。那這里為什么有255和-1兩個結果呢?答案就在符號位那里:
int main() {unsigned char c1 = 255;signed char c2 = 255;char c3 = 255;//存進去時,都是 11111111printf("%d\n", c1);//取c1的時候,c1無符號,都是有效位,值為255printf("%d\n", c2);//取c2的時候,c2有符號,第一位符號位,是負數,需要算出原碼再得值//補碼11111111 反碼11111110 原碼10000001 所以c2= -1 printf("%d\n", c3);//與c2同理得-1return 0; }當有符號和無符號存進去的時候都是255,但取出來的時候就在判斷有無符號的時候產生了差異,這里既然同一個數存儲取出的時候有不同,那有無符號的范圍也是有所不同的。
有符號char因為有負數域,所以范圍是 -128~127。
無符號char因為無符號,所以范圍是0~255,沒有負數。
而從char中,我們也可以延申計算出short,int,long等有無符號的范圍,這里就不一一計算了,有興趣的可以自己算一下。
② 浮點數家族
float
double
浮點型家族中有單精度浮點型和雙精度浮點型,什么時候用單精度什么時候用雙精度取決于你需要精度多高,你需要更高精度用double,不需要就用float,在float足夠用時用double的話會浪費內存空間。
③ 構造類型
構造類型就是自己能夠創造的類型,自定義類型。
數組類型
結構體類型 struct
枚舉類型 enum
聯合類型 union
1.數組也是有類型的。
int main() {int a = 0;//整形類型int arr[10] = { 0 };//數組,arr為數組名//去掉數組名就是數組類型,在這里數組類型是intreturn 0; }我們可以驗證一下,當我們創建類型后需要計算類型大小的時候,我們可以用變量名來進行計算,也可以用類型來計算。
int main() {int a = 0;int arr[10] = { 0 };printf("%d\n", sizeof(a));//4printf("%d\n", sizeof(int));//4printf("%d\n", sizeof(arr));//40printf("%d\n", sizeof(int [10]));//40//答案是一樣的,說明數組除去數組名就是數組類型return 0; }2.關于結構體
關于結構體可以查看操作符篇中的詳解。
【C語言】從入門到入土(操作符篇)
3.枚舉類型
枚舉類型定義的一般形式為:
enum 枚舉名{ 枚舉值表 };
在枚舉值表中應羅列出所有可用值。這些值也稱為枚舉元素。
4.聯合類型
在后面的博客中再做詳解。
④ 指針類型
int *pi; char *pc; float* pf; void* pv;指針也是有類型的。
當我們需要將一個變量的地址保存起來的時候,我們就需要用到指針,例如下面這段代碼中,p就把num的地址儲存了起來,p就是指針變量:
int num = 10; p = #而指針的定義方式是: type + * ,如char * p;
其實: char* 類型的指針是為了存放 char 類型變量的地址。 short* 類型的指針是為了存放 short 類型變量的地址。 int* 類型的指針是為了存放int 類型變量的地址。
而指針類型的意義就在于確定了該指針的類型,就決定了指針向前或者向后走一步有多大(距離)。
⑤ 空類型
void 表示空類型(無類型)
通常應用于函數的返回類型、函數的參數、指針類型。
我們看下面兩段代碼:
void fun() {printf("hello!"); } int main() {fun();fun(10086);//正常運行,輸出hello!hello!return 0; } void fun(void) {printf("hello!"); } int main() {fun();fun(10086);//能運行,但錯誤列表中提示錯誤,輸出hello!hello!return 0; }當我們調用自定義函數時,我們不設傳參,但在主函數中有值傳過去,結果也是不影響的,因為fun函數根本沒有接收這個傳參,我們也可以直接在函數后寫上void拒絕接收。
二、整形在內存中的存儲
1.關于整形的大小范圍
我們可以直接在vs2019中去輸入下列代碼,然后點擊INT_MAX,轉到定義,就可以查看int的最大值和最小值了,同時還可以看到其他整形的最大最小值。
#include <limits.h> //整形限制大小頭文件 INT_MAX;2.變量創建時的內存空間
當一個變量創建的時候是要在內存中開辟空間的??臻g的大小是根據不同的類型而決定的。
那數據在空間中是任何開辟的呢?
比如int a = -1,創建這個整形變量之后,內存中會如何變化,我們來觀察觀察。
這里我們創建了一個變量a,F10代碼走起來之后,我們點擊查看內存中的變化,地址寫&a,我們會發現當執行完int a = -1后,&a處地址的值變成了ff ff ff ff,我們知道一個f就是4個1(十六進制轉二進制),那-1為什么會儲存中是8個f呢,這里就引進數據在儲存是有原反補三種編碼方式了。
3.原碼、反碼、補碼
計算機中的有符號數有三種表示方法,即原碼、反碼和補碼。三種表示方法均有符號位和數值位兩部分,符號位都是用0表示“正”,用1表示“負”,而數值位三種表示方法各不相同。正數的原、反、補碼都相同,負數則按下面規則進行運算。
原碼:
直接將二進制按照正負數的形式翻譯成二進制就可以。
反碼:
將原碼的符號位不變,其他位依次按位取反就可以得到了。
補碼:
反碼+1就得到補碼。
比如:
int main() {int a = -1;//原碼:1 0000000 00000000 00000000 00000001//反碼:1 1111111 11111111 11111111 11111110//補碼:1 1111111 11111111 11111111 11111111//原碼:顯示值//補碼:數據存放內存中其實存放的是補碼return 0; }4.補碼的表示與存儲
在計算機系統中,數值一律用補碼來表示和存儲。
原因在于,使用補碼,可以將符號位和數值域統一處理; 同時,加法和減法也可以統一處理(CPU只有加法器)此外,補碼與原碼相互轉換,其運算過程是相同的,不需要額外的硬件電路。
單純理解可能有點困難,我們可以舉例理解:
int main() {int a = 1;int b = -1;int c = a - b;//c是怎么計算的呢return 0;}由于CPU只有加法器,所以表示為1+(-1)。
假設是用原碼計算(二進制運算):
a:00000000 00000000 00000000 00000001
b:10000000 00000000 00000000 00000001
—————————————————————
c:10000000 00000000 00000000 00000010
結果為-2,但是1-1應該等于0呀。
用補碼計算:
a:00000000 00000000 00000000 00000001
b:11111111 11111111 11111111 11111111
—————————————————————
c:100000000 00000000 00000000 00000000
最前面的1會舍棄,保留后面的0得:
c:00000000 00000000 00000000 00000000
結果為0,這才是正確的答案。
所以說使用補碼,可以將符號位和數值域統一處理。
而最后一句,補碼與原碼相互轉換,其運算過程是相同的,不需要額外的硬件電路。就是說上面原碼轉反碼,反碼轉補碼的過程,還有倒回去也是成立的,不需要其他算法。
之前我們認識的原反補轉換:
而這句話中的原反補轉換:
也就是怎么從原碼轉換補碼,也可以以同樣的方式按順序從補碼轉換為原碼。
我們再看看,數據存儲中有什么地方值得我們關注的,內存中有什么秘密,我們看一下這張圖:
圖中我們創建了兩個變量,一正一負,然后我們算出a和b在存儲中的十六進制表現形式,發現和內存中倒了過來,我們明明得出a是00 00 00 ff,而在內存中顯示為ff 00 00 00,這就涉及到大小段的概念了。
三、大小端字節序介紹及判斷
1.大小端介紹
大端(存儲)模式,是指數據的低位保存在內存的高地址中,而數據的高位,保存在內存的低地址中;
小端(存儲)模式,是指數據的低位保存在內存的低地址中,而數據的高位,,保存在內存的高地址中。
為什么會有大小端:
因為在計算機系統中,我們是以字節為單位的,每個地址單元都對應著一個字節,一個字節為8bit。但是在C語言中除了8bit的char之外,還有16bit的short型,32bit的long型(要看具體的編譯器),另外,對于位數大于8位的處理器,例如16位或者32位的處理器,由于寄存器寬度大于一個字節,那么必然存在著一個如果將多個字節安排的問題。因此就導致了大端存儲模式和小端存儲模式。
例如一個 16bit 的 short 型 x ,在內存中的地址為 0x0010 , x 的值為 0x1122 ,那么 0x11 為高字節, 0x22為低字節。對于大端模式,就將 0x11 放在低地址中,即 0x0010 中, 0x22 放在高地址中,即 0x0011 中。小端模式,剛好相反。我們常用的 X86 結構是小端模式,而 KEIL C51 則為大端模式。很多的ARM,DSP都為小端模式。有些ARM處理器還可以由硬件來選擇是大端模式還是小端模式。
我們來看一個更詳細的例子:
0x表示以十六進制表示值,這樣子我們就很直觀的看見。我們所存儲的變量,在內存中恰恰反著存儲了過來。
其實,數據在內存中存儲時,我們可以選擇千奇百怪的存儲方式,我們可以存進去時為11 33 22 44 ,也可以是22 33 11 44,但是為了規整統一,存取方便,我們就把其他的都除去了,留下了大小端這兩種存儲方式。
大小端圖示:
其實大小端儲存模式就是牽扯到數據存儲到內存時字節順序的一個問題,而我們當前的vs2019就是小段字節序。
四、浮點型在內存中的存儲
1.常見的浮點數及范圍
3.14159 ,1E10 (即為1.0乘以10的10次方),浮點數家族包括: float、double、long double 類型。
① 查看浮點數的大小范圍與整形的查看類似,float.h頭文件就是浮點數限制大小發文件。寫出下列代碼,然后點擊FLT_MAX,轉到定義,就可以查看了。
#include <float.h> FLT_MAX;②也可以在電腦中直接搜索float.h文件,然后把他拖到vs編譯器里面,也可以查看,直接用此電腦查看估計比較緩慢,這里推薦一個查找器everything(基于名稱快速定位文件和文件夾。)。
Everything 下載鏈接
快速查找定位文件和文件夾:
2.整形與浮點型存儲方式
整形與浮點型存儲方式是不是一樣的呢?
我們可以用一個代碼來試驗一下就知道了,我們存一個整形以浮點型的形式取出,存浮點型以整形的形式取出,看看數值是否正確便知。
int main() {int n = 9;float* pFloat = (float*)&n;printf("n的值為:%d\n", n);printf("*pFloat的值為:%f\n", *pFloat);*pFloat = 9.0;printf("num的值為:%d\n", n);printf("*pFloat的值為:%f\n", *pFloat);return 0; }結果是不一樣的!以整形存儲進去浮點型拿出的9得到的是0.00000,而浮點型存進去整形拿出的9.0竟然成了1091567616。
所以我們要重新理解浮點型是如何存儲的。
3.浮點型在內存中的存儲
根據國際標準IEEE(電氣和電子工程協會) 754,任意一個二進制浮點數V可以表示成下面的形式:
- (-1)^S * M * 2^E
- (-1)^s表示符號位,當s=0,V為正數;當s=1,V為負數。
- M表示有效數字,大于等于1,小于2。
- 2^E表示指數位。
舉例來說:
在這里,十進制小數點前面的,均按照二進制換算進行,而小數點后面的,則不是直接換算,而是小數點后面每一位,對應著2的負n次方的值,比如上面的5.5轉換后是101.1,小數點后面第一位表示的就是2的負一次方,即為0.5,小數點后面第二位就是0.25。但不要糾結如3.3的數字,這類數是比較難以保存的。
同理,當浮點數為負數的時候,s就是1,其他的不變。比如十進制的-5.0,寫成二進制是 -101.0 ,相當于 -1.01×2^2 。那么,s=1,M=1.01,E=2。
所以根據上述IEEE754規定,浮點數存儲的時候只需要存儲S,M,E三個數就可以了。
而存儲時空間的分配:
對于32位的浮點數,最高的1位是符號位s,接著的8位是指數E,剩下的23位為有效數字M。
對于64位的浮點數,最高的1位是符號位S,接著的11位是指數E,剩下的52位為有效數字M。
4.IEEE754的特別規定
IEEE 754對有效數字M和指數E,還有一些特別規定!
①有效數字M:
前面說過, 1≤M<2 ,也就是說,M可以寫成 1.xxxxxx 的形式,其中xxxxxx表示小數部分。
IEEE 754規定,在計算機內部保存M時,默認這個數的第一位總是1,因此可以被舍去,只保存后面的xxxxxx部分。比如保存1.01的時候,只保存01,等到讀取的時候,再把第一位的1加上去。這樣做的目的,是節省1位有效數字。以32位浮點數為例,留給M只有23位,將第一位的1舍去以后,等于可以保存24位有效數字。
也就是說,所有的浮點數都可以化為(-1)^S * M * 2^E這樣的形式,小數也不例外,比如0.5,化為0.1,也就是(-1)^0*1.0*2^(-1)。
②指數E:
1.首先,E為一個無符號整數(unsigned int)
這意味著,如果E為8位,它的取值范圍為0~255;如果E為11位,它的取值范圍為0~2047。但是我們上面的0.5轉換時就已經發現,E是有可能為負數的。所以IEEE754又規定了:
存入內存時E的真實值必須再加上一個中間數,對于8位的E,這個中間數是127;對于11位的E,這個中間數是1023。比如,2^10的E是10,所以保存成32位浮點數時,必須保存成10+127=137,即10001001;而2^(-1)存進去時,保存的便是-1+127=126,011111110。
2.那存進去的形式有了,取出的時候如何呢?
指數E從內存中取出還可以再分成三種情況:
這時,浮點數就采用下面的規則表示,即指數E的計算值減去127(或1023),得到真實值,再將有效數字M前加上第一位的1,即為怎么存的就怎么取。
當E全為0時,浮點數的指數E等于1-127(或者1-1023)即為真實值,是一個非常小的數!所以有效數字M不再加上第一位的1,而是還原為0.xxxxxx的小數。這樣做是為了表示±0,以及接近于0的很小的數字。
與E為全0一樣,當E為全1的時候,是一個非常非常大的數字,再加上正負號,就類似接進于正負無窮大。所以這時,如果有效數字M全為0,表示±無窮大(正負取決于符號位s)。
5.重新觀察代碼
知道了浮點數的存取,我們重新來打量一下這段代碼:
int main() {int n = 9;//整形理解:00000000 00000000 00000000 00001001//浮點型理解:0 00000000 0000000000000000001001float* pFloat = (float*)&n;printf("n的值為:%d\n", n);//9printf("*pFloat的值為:%f\n", *pFloat);//0//所以按照上面的浮點型理解,E還要減去127,得出為//(-1)^0 * 1.001*2^(-126) 接近為 0,所以顯示為0.000000//默認打印小數點后六位*pFloat = 9.0;printf("num的值為:%d\n", n);printf("*pFloat的值為:%f\n", *pFloat);return 0; }而下面那一段也是同樣的道理,按照浮點型存儲,9.0應該存儲為1001.0
所以為(-1)^0 * 1.001*2^3 ,而且E要加上127,然后轉化為二進制數列就是
浮點型理解:0 10000010 00100000000000000000000
整形理解:01000001000100000000000000000000
我們可以用計算機算一下:
的確和程序運行起來一樣的數字,所以證明上面的說法都是正確的。
好啦,本篇的內容就到這里,小白制作不易,有錯的地方還請xdm指正,互相關注,共同進步。
還有一件事:
總結
以上是生活随笔為你收集整理的【C语言进阶】从入门到入土(数据的存储)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: visual c++ 下载地址
- 下一篇: Latex工具(texlive+texs