摘要:數據的存儲前言數據類型匯總整型家族浮點型家族自定義類型指針類型。整型家族注在之后的標準規定,將類型數據劃分為整型家族,因為字符在內存中會將其轉化為碼值進行存儲。
我們在敲代碼的時候總是會定義各種變量,對各種數據進行存儲,比如int a = 10;就是將10這個數據存放進變量a中,而變量a,就是我們在內存中申請開辟的一塊空間。
在內存中如何開辟空間給變量的問題博主已經在函數棧幀里用反匯編的方式將其原理剖析了,具體可看圖解函數棧幀 - 函數棧幀的創建及銷毀。
本文將進一步剖析在已經開辟好存儲單元的情況下,各種數據是如何存儲的。
在了解數據如何存儲之前,應該先了解我們常見的數據類型。
在C99標準中,我們可將數據類型劃分為以下幾大類。
- 整型家族
- 浮點型家族(實型家族)
- 自定義類型(構造類型)
- 指針類型
- 空類型
下面一一介紹這五種類型的基本情況。
char unsigned char signed charshort unsigned short [int] signed short [int]int unsigned int signed intlong unsigned long [int] signed long [int]
注:在C99之后的標準規定,將char類型數據劃分為整型家族,因為字符在內存中會將其轉化為ASCII碼值進行存儲。
如上所示,所有的整型家族都被分為有符號整型和無符號整型,并且signed都是可以被省略的,換言之,signed int完全等價于int,其他以此類推,但其中有一個例外: char類型和signed char并不等價,只寫一個char ch = 0;我們將無法分辨這個ch變量到底是有符號字符型還是無符號字符型,他完全取決于編譯器,但經博主測試,大部分編譯器下char類型都被編譯器翻譯為有符號的char類型。
在C99中還引入了long long - 長長整型,用法和long類型一致,但C語言語法規定,sizeof(long)<= sizeof(long long),而long類型所占內存大小為4/8字節,所以long long類型所占內存空間大小一定為8個字節。
floatdouble
浮點型家族只有float和double這兩種類型,float類型所占空間大小為4byte,double類型所占空間大小為8byte。
他們之間的區別除了所占空間大小不同之外還有精度的區別,float稱為單精度浮點型,有效精度為小數點后6位,而double類型稱為雙精度浮點型,精確到小數點后15位,但其有效數字只有11位左右。
> 數組類型> 結構體類型 struct> 枚舉類型 enum> 聯合類型 union
這里可能會有很多人無法李姐為什么數組類型也被劃分為自定義類型,這里稍微做一些解釋。
我們知道數組類型的變量定義形式:數據類型+數組名+[數組大小];
如:
int arr[10] = { 0 };
這里可能會讓很多人產生誤區,認為arr數組的類型是int類型,也就把這條語句理解為是int類型的、數組名為arr的數組大小為10的數組,其實不然,這個數組的數組名確實是arr,但其數據類型是int [10],這里可能讓大部分人無法接受,
舉個簡單的例子即可解釋:
我們知道,sizeof操作符是用來計算所占內存空間大小的,其操作數既可以是變量名,也可以是變量類型。
#define _CRT_SECURE_NO_WARNINGS 1#include int main(){ int a = 10; printf("%d/n", sizeof(a)); printf("%d/n", sizeof(int)); return 0;}
這兩種寫法都正確,打印結果為:
而對于數組,操作數也同樣可以是數組名或者數組類型:
#define _CRT_SECURE_NO_WARNINGS 1#include int main(){ /*int a = 10; printf("%d/n", sizeof(a)); printf("%d/n", sizeof(int));*/ int arr[10] = { 0 }; printf("%d/n", sizeof(arr)); printf("%d/n", sizeof(int[10])); return 0;}
其打印結果為:
這么一來,就驗證了int [10]是數組類型。
知道了這點,解釋為什么數組類型是自定義類型就更清晰了,用上面解釋的結論就可以知道,int arr[10]和int arr[9]的數組類型不同,并不都是int類型的,數組大小是我們程序員人為規定的,所以可以把他劃分為自定義類型。
其他的自定義類型比較明顯,這里就不一一解釋。
指針類型很特殊。
我們常說的指針有兩個含義:
指針類型的定義方式為:
數據類型+*(用于標識指針類型)+指針變量名
常見的指針類型有:
int* pi;char* pc;float* pf;void* pv;
這里著重介紹一點,指針變量賦值大部分都是取出某變量地址存放進指針變量,如int pc = &c;
但有一個例外:
int main(){ char* pc = "hello world"; printf("%c/n", *pc); return 0;}
這里之間將一個字符串常量賦值給指針變量pc,我們知道,字符串常量時放在常量區的,他的值不可修改,并且這里的字符串加上隱藏的’/0’總共是12個字節,而我們的指針變量根據平臺的不同只能是4/8個字節,怎么都不可能放的下這個字符串常量,所以這么理解是錯誤的。
我們將其打印看看結果:
打印結果為單字母h,這么一來其實就解釋的通了,將整個常量字符串賦值給指針變量,其實并不會把整個字符串放進去,而是把整個字符串的首地址賦給指針變量,比較指針存放的就是地址,這和將字符數組名賦值給指針變量類似,存放的都是首元素地址。
void 用于表示空類型(無類型)
通常應用于函數的返回類型、函數的參數、指針類型。
下面舉幾空類型的例子幫助理解:
void test(int x){ printf("%d/n", x);}int main(){ int a = 10; test(a); return 0;}
這里test函數的返回類型就是void。
int test(void){ return 1;}int main(){ int ret = test(); printf("%d/n", ret); return 0;}
這個代碼就是將函數的參數置為空,表示不允許主調函數傳參,如果非要傳參,編譯器將給出警告。
int test(void){ return 1;}int main(){ int a = 10; int ret = test(a); printf("%d/n", ret); return 0;}
void* pc;
表示定義一個指針pc,但他什么都不指向,作為一個空指針存在。
我們知道不管是什么樣的數據,最終都會被編譯器編譯為二進制機器碼進行存儲,并且我們的內存是以字節為最小存儲單元劃分而進行存儲的,那么就存在了一個問題,數據以字節為單位進行存儲的時候,是以怎樣的順序進行存儲的呢?這就引出了大小端字節序的概念。
為什么會有大小端字節序模式之分呢?這是因為在計算機系統中,我們是以字節為單位的,每個地址單元都對應著一個字節,一個字節為8bit位。但是在C語言中除了8bit的char類型之外,還有16bit的short類型,32bit的long類型(要看具體的編譯器,64位平臺long類型為64位),另外,對于位數大于8位的處理器,例如16位或者32位的處理器,由于寄存器的寬度大于一個字節,那么必然存在著一個如何將多個字節安排的問題。因此就導致了大端存儲模式和小端存儲模式。
例如:一個16bit位的short類型變量x ,在內存中的地址為0x0010,變量x 的值為0x1122 ,那么0x11為高字節,0x22為低字節。對于大端模式,就將 0x11放在低地址中,即0x0010中,0x22 放在高地址中,即0x0011中。小端模式,剛好相反。我們常用的X86(32位平臺)結構是小端模式,而KEILC51則為大端模式。很多的ARM,DSP都為小端模式。有些ARM處理器還可以由硬件來選擇是大端模式還是小端模式。
字節序,即字節順序,又稱端序或尾序,在計算機科學領域中,指「存儲器」中或者「數字通信鏈路」中,組成多字節的字節排列順序 。在幾乎所有的機器上,多字節對象都被存儲為連續的字節序列 。例如在C語言中,一個 int類型的變量x地址為0x100,那么其對應的地址表達式&x的值為0x100 且 x 的4個字節將被存儲在存儲器的0x100, 0x101, 0x102, 0x103位置。字節的排列方式有2個通用規則。
- 順序排列 - 大端字節序
- 逆序排列 - 小端字節序
上面的文字描述也許過于抽象,接下來用較為容易理解的方式分別簡單的介紹大端字節序和小端字節序的概念。
所謂大小端字節序,就是將多字節數據中的高低字節位按不同順序存放在內存中的高低地址處,相當于順(逆)序存放。接下來博主將把上述抽象概念劃分逐一介紹:
我們知道一個數據根據大小不同被劃分為不同的數據類型,各數據類型所占字節數不同,我們也就據此根據數據字節大小來將其存放于不同的數據類型中。
比如字符類型 - 其擴展之后的ASCII碼值為0~255,我們知道一個字節是8位,按照無符號字符型的理解也就是從00000000 ~ 11111111,剛好是0 ~ 255,所以字符類型被稱為單字符類型數據。
而十六進制數,如:0x11223344則為多字節數據,其中有4個字節,分別是0x11、0x22、0x33、0x44,像這樣的數據則被稱為多字節數據。
在一個二進制序列中,
如:01010110101001011010100101101001
我們把前方高亮部分的0101稱為高字節位,把后端加刪除線的1001 部分稱為低字節位,以此區分。
其實很好理解,因為最后一個1的的權重為20,也就是2的0次方,而第一個0的權重為231,也就是2的31次方,以此來區分高低字節位也是很不錯的選擇。
接下來介紹大小端字節序的存儲方式:
大端字節序
所謂大端字節序,就是將處于高字節位的數據存放在內存的低地址處,將處于低字節位的數據存放在內存的高地址處
如今給一數據:0x11223344
在內存中的存放形式為:
以這樣的形式存放的模式,就稱為大端存儲模式,這樣的存放順序,也就被稱為大端字節序。
小端字節序
所謂小端字節序,就是將處于高字節位的數據存放在內存的高地址處,將處于低字節位的數據存放在內存的低地址處
今給一數據:0x11223344
在內存中的存放形式為:
以這樣的形式存放的模式,就稱為小端存儲模式,這樣的存放順序,也就被稱為小端字節序。
在博主使用的VS2019編譯器上,采用的就是小端字節序:
例:
int main(){ int a = 0x0000ff40; return 0;}
調試 - 內存窗口(&a):
0x001DFEFC就是該代碼中a變量的地址,存放情況為40 ff 00 00。
也就是小端存儲模式。
百度2015年系統工程師筆試題:
請簡述大端字節序和小端字節序的概念,設計一個小程序來判斷當前機器的字節序。(10分)
該題前半部分在上文其實已經解決了,這里博主將分析問題,并實現代碼。
要判斷編譯系統到底是大端存儲還是小端存儲,其實并不復雜。
如0x11223344
如果是在大端存儲模式下:
存儲方式為:11 22 33 44
如果是在小端存儲模式下:
存儲方式為:44 33 22 11
所以其實只需要知道第一個字節的內容到底是11還是44就可以判斷了。
但這樣的數據太過于復雜,不如換簡單一點的數字,比如1。
1的高字節位就是00,低字節位就是01,比較好判斷。
int check_sys(int x){ return *(char*)&x;}int main(){ int a = 1; //約定: //如果是大端,返回0 //如果是小端,返回1 int ret = check_sys(a); if (ret) { printf("是小端存儲模式/n"); } else { printf("是大端存儲模式/n"); } return 0;}
運行結果:
之前也分析了,我的編譯器VS2019是小端存儲模式,所以代碼的結果正確,下面分析代碼。
想要在4個字節中拿到第一個字節,只需要在取地址時將整型強制類型轉換為字符型即可,拿到存放第一個字節的地址后對其解引用便可拿到第一個字節數據。
如果拿到的是01,說明存儲方式是01 00 00 00,也就是小端存儲模式,反之則為大端存儲模式。
這里如果有沒有講清楚的地方,歡迎評論區留言或者私信博主解決嗷。
數據在內存中的存儲遵循一定的法則,而整型數據和浮點型數據在內存中所遵循的法則是不同的,這里我們先介紹整型數據在內存中是如何存儲的。
介紹整型數據的存儲需要先引進一個概念:原反補碼。
計算機中的有符號數有三種表示方法,即原碼、反碼和補碼。三種表示方法均有符號位和數值位(或稱有效位)兩部分,符號位都是用0表示“正”,用1表示“負”,而數值位,三種表示方法各不相同。在計算機系統中,數值一律用補碼來表示和存儲。原因在于:使用補碼,可以將符號位和數值域統一處理;同時,加法和減法也可以統一處理。
而補碼其實是針對負數存儲設定的,對于無符號數來說,其反碼和補碼都和原碼相等。
原碼:
所謂原碼,就是將數據直接翻譯為二進制序列。
拿32位平臺舉例,最高位作為符號位,正數的符號位為0,負數的符號位為1,后面的31位稱為有效位,以不同的權重計算出不同的數字,最低位的權重為20,其次為21,以此類推。
如:
13的原碼為:00000000000000000000000000001101-3的原碼為:10000000000000000000000000000011
反碼:
反碼,顧名思義,就是將原碼的二進制序列按位取反,但這里需要注意,并不是將所有的二進制位都按位取反,符號位是特殊獨立出來的,他表示一個數的正負,隨意取反可能會遭遇意想不到的結果。
所以反碼應該通過原碼除符號位,其他位按位取反獲得。
(注:正數的反碼和原碼相等。)
如:
13的反碼為:00000000000000000000000000001101-3的反碼為:11111111111111111111111111111100
補碼:
整數在內存中的存儲存的都是補碼,所以要通過上面的反碼求出補碼,補碼的獲取規則是原碼按位取反(除符號位)再加一。
(注:正數的補碼和原碼相等。)
如:
13的補碼為:00000000000000000000000000001101-3的補碼為:11111111111111111111111111111101
因為整數在內存中的存儲形式是補碼,所以引出原反補的意義就是求出補碼,而補碼的計算公式為:補碼 = 原碼按位取反(除符號位)再加一
這里我們通過VS2019編譯器進行驗證內存中存儲的是數據的補碼:
int main(){ int a = 13; //原碼:00000000 00000000 00000000 00001101 //反碼:01111111 11111111 11111111 11110010 //補碼:01111111 11111111 11111111 11110011 int b = -3; //原碼:10000000 00000000 00000000 00000011 //反碼:11111111 11111111 11111111 11111100 //補碼:11111111 11111111 11111111 11111101 return 0;}
編譯器下調試 - 內存 - &a:
內存中存儲的是:0d 00 00 00
為小端存儲模式,00001101轉換為十六進制就是0d。
編譯器下調試 - 內存 - &b:
內存中存儲的是:fd ff ff ff
為小端存儲模式,1111 1111轉換為十六進制就是ff,1111 1101轉換為十六進制就是fd。
如此說來,在內存中真的存放的就是補碼,所以為了弄清楚整型數據在內存中的存儲,必須牢牢掌握原反補的概念。
我們知道int類型的變量所占空間大小是4個字節32個bit位(32位平臺下),而char類型的變量所占空間大小是1個字節8個bit位,那我要怎么將一個整型的數據存放在一個char類型的變量里呢?這里教大家一個很有用的辦法,那就是沒辦法,32個比特位是不可能放進8個小格子里的,所以就會發生所謂的截斷。
我們知道,一個char類型只能存放8個比特位,那如果我要將char類型的數據以%d的形式打印,也就是看做32位數據將其打印,那有要怎么做呢?再教大家一個辦法,那依然是沒辦法,所以編譯器只能對char類型的數據進行整型提升。
接下來簡單講解截斷和整型提升的原理。
截斷
假設我有一個32位二進制序列:
01010011001000110001000100100011
這是一個非常大的數字:
有一個char類型的空間:
在把32位數字往里放的時候會發現放不下,便會發生截斷,只保留低八位的數字,其他24位數字直接舍棄,
最終存放的結果為:
這就是截斷的過程。
整型提升
當我要將char類型的數據以%d的形式打印時,我們知道,%d是打印有符號整型,打印的是32位0/1序列的最終結果,但我們的char類型里只存放了8位,這個時候就會發生整型提升。
整型提升規則:
如:
今有一8位無符號數。
unsigned char a = 148;
首先我們寫出該數的二進制序列。
10010100 - 148
由于變量a是無符號類型的,所以不管該二進制序列首元素是0還是1,都將全部補0
獲得:
00000000000000000000000010010100
最終打印的結果就是148
對以下代碼分析輸出結果:
1.//輸出什么?int main(){ char a = -1; signed char b = -1; unsigned char c = -1; printf("a=%d b=%d c=%d/n", a, b, c); return 0;}
首先VS2019編譯器對char類型的處理為默認認為是有符號的char,所以變量a和變量b屬于同一類型。
先計算出-1的補碼。
int main(){ //-1 //原碼:10000000000000000000000000000001 //反碼:11111111111111111111111111111110 //補碼:11111111111111111111111111111111 char a = -1; signed char b = -1; unsigned char c = -1; printf("a=%d b=%d c=%d/n", a, b, c); return 0;}
三個變量都是char類型,所以存儲時都將發生截斷。
int main(){ //-1 //原碼:10000000000000000000000000000001 //反碼:11111111111111111111111111111110 //補碼:11111111111111111111111111111111 char a = -1; //存儲的補碼:11111111 signed char b = -1; //存儲的補碼:11111111 unsigned char c = -1; //存儲的補碼:11111111 printf("a=%d b=%d c=%d/n", a, b, c); return 0;}
現在要將三個變量以%d形式打印,則會發生整型提升。
變量a和變量b整型提升后的結果為:
11111111111111111111111111111111
變量c整型提升后的結果為:
00000000000000000000000011111111
因為提升后的c符號位是0,所以原反補碼均相等。
而按%d形式打印需要將補碼轉化為原碼后轉化為十進制進行打印,
所以:
int main(){ //-1 //原碼:10000000000000000000000000000001 //反碼:11111111111111111111111111111110 //補碼:11111111111111111111111111111111 char a = -1; //存儲的補碼:11111111 //提升后的補碼:11111111111111111111111111111111 //提升后的反碼:10000000000000000000000000000000 //提升后的原碼:10000000000000000000000000000001 signed char b = -1; //存儲的補碼:11111111 //提升后的補碼:11111111111111111111111111111111 //提升后的反碼:10000000000000000000000000000000 //提升后的原碼:10000000000000000000000000000001 unsigned char c = -1; //存儲的補碼:11111111 //提升后的補碼:00000000000000000000000011111111 //提升后的反碼:00000000000000000000000011111111 //提升后的原碼:00000000000000000000000011111111 printf("a=%d b=%d c=%d/n", a, b, c); return 0;}
這么一來,打印的結果就應該是-1 -1 255
打印結果:
2.int main(){ char a = -128; printf("%u/n", a); return 0;}
這道題的變量a是有符號的char類型的。
首先計算出-128的原反補碼。
int main(){ char a = -128; //-128 //原碼:10000000000000000000000010000000 //反碼:11111111111111111111111101111111 //補碼:11111111111111111111111110000000 printf("%u/n", a); return 0;}
將01111111111111111111111110000000這樣一個二進制序列存放進a中將會發生截斷。
截斷之后a中存放的結果為:10000000
這時以%u的形式打印,也就是以無符號整型的形式打印,要進行整型提升,而變量a是一個有符號的char類型,第一個元素是1,所以整型提升24個1。
int main(){ char a = -128; //-128 //原碼:10000000000000000000000010000000 //反碼:11111111111111111111111101111111 //補碼:11111111111111111111111110000000 //截斷的結果:10000000 //整型提升后的結果:11111111111111111111111110000000 printf("%u/n", a); return 0;}
這時要將提升之后的補碼轉換為原碼后以十進制的形式進行打印。
而%u的形式將把補碼中的符號位看做是有效位,所以其原反補都是一樣的。
int main(){ char a = -128; //-128 //原碼:10000000000000000000000010000000 //反碼:11111111111111111111111101111111 //補碼:11111111111111111111111110000000 //截斷的結果:10000000 //整型提升后的結果:11111111111111111111111110000000 //補碼:11111111111111111111111110000000 //反碼:11111111111111111111111110000000 //原碼:11111111111111111111111110000000 printf("%u/n", a); return 0;}
而11111111111111111111111110000000的值應該是4,294,967,168
所以輸出結果:
3.int main(){ char a = 128; printf("%u/n", a); return 0;}
還是一樣,先求出128的補碼,由于128是正數,所以其原反補都是相同的為:
00000000000000000000000010000000
存放進變量a中將發生整型截斷:
10000000
而變量a為有符號的char類型,所以整型提升為
11111111111111111111111110000000
變量a以%u形式打印,則把符號位看成有效位,則此時原碼反碼補碼相同,直接進行計算,11111111111111111111111110000000的十進制形式為4,294,967,168
所以打印結果為:
4.int mian(){ int i = -20; unsigned int j = 10; //按照補碼的形式進行運算,最后格式化成為有符號整數 printf("%d/n", i + j); return 0;}
還是先把-20和10的補碼計算出來,但是這里的i和j都是整型變量,所以不會發生截斷和整型提升。
int mian(){ int i = -20; //-20 //原碼:10000000000000000000000000010100 //反碼:11111111111111111111111111101011 //補碼:11111111111111111111111111101100 unsigned int j = 10; //10 //補碼:00000000000000000000000000001010 //按照補碼的形式進行運算,最后格式化成為有符號整數 printf("%d/n", i + j); return 0;}
數據的計算是按照二進制補碼的形式進行計算的,最后的結果再根據打印要求或者存儲要求進行調整更改。
計算的結果:
int mian(){ int i = -20; //-20 //原碼:10000000000000000000000000010100 //反碼:11111111111111111111111111101011 //補碼:11111111111111111111111111101100 unsigned int j = 10; //10 //補碼:00000000000000000000000000001010 //計算: //11111111111111111111111111101100 //00000000000000000000000000001010 //11111111111111111111111111110110 - 補碼相加的結果 //按照補碼的形式進行運算,最后格式化成為有符號整數 printf("%d/n", i + j); return 0;}
要求按%d的形式打印,則將計算的結果轉化為原碼以有符號十進制數打印。
補碼:11111111111111111111111111110110反碼:10000000000000000000000000001001原碼:10000000000000000000000000001010
計算結果為-10
int main(){ unsigned int i; for (i = 9; i >= 0; i--) { printf("%u/n", i); } return 0;}
程序分析:
變量i從9開始自減到0時,都可以正常進入程序打印的值就是
9 8 7 6 5 4 3 2 1 0
在打印完0之后,變量i再自減1,變成-1,按道理來說應該跳出循環,但我們注意,這里的變量i為無符號整型,而-1的補碼為11111111111111111111111111111111,所以會被解析為一個特別大的正整數:4294967295。
那么他也符合循環控制條件(i >= 0),所以循環會繼續4294967295次,而一直自減到0的時候,再次自減又變成-1,有被解析為4294967295,所以該程序將無限循環下去。
這里博主隨便截兩張打印結果的圖供大家參考。
6.#include <
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/122193.html
摘要:在符號位中,表示正,表示負。我們知道對于整型來說,內存中存放的是該數的補碼。在計算機系統中,數值一律用補碼來表示和存儲。表示有效數字,。規定對于位的浮點數,最高的位是 ...
摘要:的理解和區別代表有符號,整數在內存中存儲的二進制位的最高位為符號位,表示負數,表示正數。那接下來我們來學習數據在所開辟的內存空間時如何存儲的。請看下面例子為什么內存中存儲的是補碼對于整數來說數據存放內存中其實存放的是補碼。 ...
摘要:講解從三個部分展開短視頻應用場景阿里云短視頻解決方案阿里云對短視頻用戶體驗的相關優化。同時,為了面對業務的突發流量,阿里云提供了超過的帶寬儲備,為持續增長的業務保駕護航。二播放卡頓是指在播放過程中的不流暢情況,會嚴重影響用戶體驗。 深圳云棲大會已經圓滿落幕,在3月29日飛天技術匯-彈性計算、網絡和CDN專場中,阿里云CDN高級技術專家周哲為我們帶來了《海量短視頻極速分發》的主題分享,帶...
摘要:接上篇合約升級模式介紹筆者改寫了一個可用于實踐生產的升級框架,需要自取。在介紹合約升級模式中提到了一個可以解決這個問題的方法。深度理解注意為中的低階方法下文中出現的方法,是我在智能合約中寫的一個方法名稱,不要混淆。 接上篇:合約升級模式介紹筆者改寫了一個可用于實踐生產的升級框架,需要自取。https://github.com/hammewang/... 同時歡迎討論,微信xiuxiu1...
閱讀 775·2023-04-25 16:55
閱讀 2804·2021-10-11 10:59
閱讀 2070·2021-09-09 11:38
閱讀 1782·2021-09-03 10:40
閱讀 1485·2019-08-30 15:52
閱讀 1125·2019-08-30 15:52
閱讀 953·2019-08-29 15:33
閱讀 3494·2019-08-29 11:26