大家好,我是良許,一個深耕嵌入式 12 年的老工程師,前世界 500 強高工。
我花了 3 個月時間,寫了一個 C 語言電子書,以非常通俗的語言跟大家講解 C 語言,把複雜的技術講得連小學生都能聽得懂,絕不是 AI 生成那種晦澀難懂的電子垃圾。
點擊此處免費領取 C 語言電子書
C 語言電子書目錄如下:
2.1 C語言數據類型概述
在我們的日常生活中,我們會遇到各種各樣的信息:數字、文字、圖片、聲音等等。比如你的年齡是一個數字,你的姓名是一段文字,你的照片是圖像信息。不同類型的信息需要用不同的方式來處理和存儲。
同樣地,在計算機程序中,我們也需要處理各種不同類型的數據。有時候我們需要存儲一個人的年齡,有時候需要存儲一個人的身高,有時候需要存儲一個人的姓名。這些不同種類的數據就需要用不同的數據類型來表示。
數據類型就像是給數據貼上的"標籤",告訴計算機這個數據是什麼類型的,應該如何處理。就像超市裏的商品都有標籤一樣,食品類商品有食品標籤,電子產品有電子產品標籤,不同的標籤決定了商品的處理方式。
2.1.1 數據類型分類
在C語言中,數據類型可以看作是一個大家族,這個家族有很多分支。讓我們用一個家族族譜的方式來理解C語言的數據類型分類。
整個C語言數據類型家族可以分為兩大主要分支:基本數據類型和構造數據類型。這就像一個大家族分為"原生家庭成員"和"通過結合組成的新家庭"一樣。
1. 基本數據類型詳解
基本數據類型是C語言中最基礎、最原始的數據類型,就像化學中的原子一樣,它們是構成其他複雜數據類型的基礎。基本數據類型又可以細分為幾個小類:
整型數據類型
整型數據類型專門用來存儲整數,就像我們數學中學習的整數一樣:...,-3,-2,-1,0,1,2,3,...
在整型家族中,有好幾個成員,它們的區別主要在於能夠存儲的數值範圍不同:
int:這是最常用的整型,就像家族中的"長子",是整型家族的代表。它通常可以存儲-2147483648到2147483647之間的整數。為什麼是這個範圍呢?這與計算機的內部存儲方式有關,我們後面會詳細解釋。short:這是"小弟弟",能存儲的數值範圍比int小,通常是-32768到32767。雖然範圍小,但佔用的內存空間也更少,在內存珍貴的嵌入式系統中很有用。long:這是"大哥哥",能存儲的數值範圍比int大。在不同的系統中,long的大小可能不同,但它至少和int一樣大。long long:這是"超級大哥",能存儲非常大的整數,範圍通常從-9223372036854775808到9223372036854775807。
每種整型還可以加上unsigned修飾符,表示"無符號",也就是隻能存儲非負數(0和正數)。這就像把負數的存儲空間也用來存儲正數,所以無符號類型能存儲的正數範圍會翻倍。
浮點型數據類型
浮點型用來存儲小數,比如3.14,2.718,0.5等等。為什麼叫"浮點"呢?這是因為小數點的位置是"浮動"的,可以在數字中的任何位置。
float:單精度浮點數,就像用普通的尺子測量長度,精度有限但夠用。它通常能提供大約6-7位有效數字的精度。double:雙精度浮點數,就像用精密的遊標卡尺測量,精度更高。它通常能提供大約15-16位有效數字的精度。大多數情況下,我們使用double來處理小數。long double:擴展精度浮點數,精度最高,但在不同系統中的具體實現可能不同。
字符型數據類型
char類型用來存儲單個字符,比如字母'A',數字'5',標點符號'!'等等。需要注意的是,字符要用單引號括起來,比如'A',而不是"A"。
有趣的是,在計算機內部,字符實際上是以數字的形式存儲的。每個字符都對應一個數字編碼,比如字母'A'對應數字65,字母'B'對應數字66。這套編碼標準叫做ASCII碼。這就像每個漢字都有一個拼音編碼一樣,計算機用數字來編碼字符。
2. 構造數據類型詳解
構造數據類型是由基本數據類型組合而成的更復雜的數據類型,就像用磚塊建造房子一樣,用基本數據類型構造更復雜的數據結構。
數組類型
數組就像是一排儲物櫃,每個櫃子裏可以放同樣類型的東西。比如,一個整型數組可以存儲一系列整數,就像一排櫃子裏都放着數字。
數組有一維數組、二維數組、多維數組等。一維數組像是一排櫃子,二維數組像是一個櫃子矩陣(行和列),三維數組像是一個立體的櫃子組合。
指針類型
指針是C語言中一個非常重要但也比較難理解的概念。指針就像是地址標籤,它不直接存儲數據,而是存儲數據的地址。
想象一下,你要告訴朋友你家在哪裏,你不會把整個房子搬過去給他看,而是告訴他你家的地址。指針就是這樣,它存儲的是數據在內存中的"地址"。
結構體類型
結構體允許我們把不同類型的數據組合在一起,就像填寫一張學生信息表一樣,可以包含姓名(字符串)、年齡(整數)、身高(浮點數)等不同類型的信息。
聯合體類型
聯合體比較特殊,它允許不同類型的數據共享同一塊內存空間。這就像一個多功能房間,有時候當卧室使用,有時候當客廳使用,但同一時間只能有一種用途。
枚舉類型
枚舉類型用來表示一組有限的選擇,比如一週的七天、一年的十二個月、交通燈的三種顏色等。這讓程序更容易理解和維護。
3. 自定義數據類型
除了C語言提供的基本數據類型,我們還可以使用typedef關鍵字來定義自己的數據類型。這就像給數據類型起別名一樣,讓程序更容易理解。
比如,我們可以定義:
typedef int StudentAge; // 定義學生年齡類型
typedef float StudentHeight; // 定義學生身高類型
這樣在程序中使用StudentAge和StudentHeight就更容易理解這些變量的用途。
2.1.2 數據在內存中的存儲
1. 內存的基本概念
要理解數據在內存中的存儲,我們首先要了解什麼是內存。計算機的內存就像一個巨大的儲物櫃,有無數個小格子,每個格子都有一個唯一的編號(地址),可以存儲一個字節的數據。
想象一下一個巨大的郵局,有無數個郵箱,每個郵箱都有一個唯一的編號。當你要寄信時,需要知道收信人的郵箱編號;當你要取信時,也需要知道自己的郵箱編號。計算機內存的工作原理就是這樣,每個數據都存儲在特定編號的"郵箱"裏。
2. 字節和位的概念
在深入瞭解數據存儲之前,我們需要理解兩個基本概念:位(bit)和字節(byte)。
位(bit)是計算機中最小的數據單位,它只能存儲0或1這兩個值。這就像一個開關,只有"開"和"關"兩種狀態。位的英文"bit"實際上是"binary digit"(二進制數字)的縮寫。
字節(byte)由8個位組成,是計算機中基本的存儲單位。一個字節可以存儲256種不同的值(從00000000到11111111,也就是十進制的0到255)。為什麼是8個位呢?這是歷史上形成的標準,8個位恰好可以表示一個英文字符。
3. 不同數據類型的存儲空間
不同的數據類型在內存中佔用的空間是不同的,這就像不同大小的物品需要不同大小的盒子來裝一樣。
字符型(char)
char類型通常佔用1個字節的空間。一個字節的8個位可以表示256種不同的值,這足夠表示所有的ASCII字符(包括大小寫字母、數字、標點符號等)。
想象一下,我們用一個小盒子來裝一個字符,這個盒子剛好夠放下一個字符,不多不少。
整型
不同的整型佔用不同的內存空間:
short通常佔用2個字節(16位),可以表示65536種不同的值。如果是有符號的,範圍是-32768到32767;如果是無符號的,範圍是0到65535。int在現代系統中通常佔用4個字節(32位),可以表示約42億種不同的值。long的大小取決於系統,在32位系統中通常是4個字節,在64位系統中通常是8個字節。long long通常佔用8個字節(64位),可以表示非常大的數值範圍。
這就像我們有不同大小的盒子:小盒子裝小物品,大盒子裝大物品。如果我們知道要裝的物品不大,就不需要浪費空間使用大盒子。
浮點型
float通常佔用4個字節,按照IEEE 754標準的單精度格式存儲。double通常佔用8個字節,按照IEEE 754標準的雙精度格式存儲。
浮點數的存儲比整數複雜得多,它分為三個部分:符號位、指數位和尾數位。這就像科學計數法一樣,比如3.14×10²,其中3.14是尾數,2是指數,符號是正號。
4. 數據的二進制表示
計算機內部所有數據都是以二進制形式存儲的,也就是隻用0和1來表示。這就像用莫爾斯電碼來傳遞信息一樣,只用"滴"和"嗒"兩種符號就能表示所有的文字。
整數的二進制表示
正整數的二進制表示比較直觀,就是將十進制數轉換為二進制數。比如:
- 十進制的5在二進制中是101
- 十進制的10在二進制中是1010
負整數的表示稍微複雜一些,大多數系統使用"二進制補碼"的方式。這種方式的好處是可以用同樣的電路來處理正數和負數的加法運算。
字符的二進制表示
字符是通過ASCII碼來轉換為數字,然後再轉換為二進制的。比如:
- 字符'A'的ASCII碼是65,二進制是01000001
- 字符'a'的ASCII碼是97,二進制是01100001
- 字符'0'的ASCII碼是48,二進制是00110000
注意,字符'0'和數字0是不同的。字符'0'是一個顯示符號,它的ASCII碼是48;而數字0的二進制表示就是00000000。
5. 內存對齊的概念
在實際的內存存儲中,還有一個重要的概念叫做"內存對齊"。這是為了提高內存訪問效率而採用的策略。
想象一下,如果你要從書架上取書,整齊擺放的書比雜亂擺放的書更容易找到和取出。內存對齊就是這樣,它讓數據在內存中按照一定的規則整齊擺放。
<img src="https://lxlinux.superbed.verylink.top/item/68465eb958cb8da5c83bd7cd.png" style="zoom: 33%;" />
比如,一個int類型的變量通常要求存儲在4的倍數的地址上。如果有一個char變量佔用了地址1,那麼下一個int變量不會從地址2開始,而是從地址4開始,中間的地址2和3會被空出來。
這樣做雖然可能浪費一些內存空間,但可以大大提高數據訪問的速度。在結構體中,編譯器會自動進行內存對齊,有時候結構體的實際大小會比各個成員大小的總和要大。
6. 棧區和堆區的存儲
程序中的變量根據定義方式的不同,會被存儲在內存的不同區域:
棧區存儲
局部變量(在函數內部定義的變量)通常存儲在棧區。棧區就像一摞盤子,後放的盤子在上面,先拿走的也是上面的盤子,這叫做"後進先出"。
當函數被調用時,函數的局部變量會被"壓入"棧中;當函數結束時,這些變量會被自動"彈出"棧,內存空間會被自動回收。
堆區存儲
動態分配的內存(使用malloc等函數分配的內存)存儲在堆區。堆區的管理比棧區複雜,程序員需要手動申請和釋放內存。
全局區存儲
全局變量和靜態變量存儲在全局區,這些變量在程序運行期間一直存在。
2.1.3 字節序概念
1. 什麼是字節序?
字節序(Byte Order)是一個聽起來很技術化的概念,但實際上可以用一個很簡單的例子來理解。
想象一下,你要在紙上寫下數字"1234"。你會從左到右寫,先寫1,再寫2,然後3,最後4。但是,如果有些人習慣從右到左寫字,他們可能會先寫4,再寫3,然後2,最後1,最終在紙上呈現的可能是"4321"。
在計算機世界中,也存在類似的情況。當一個數據需要多個字節來存儲時,這些字節在內存中的排列順序就是字節序的問題。
2. 大端序與小端序
計算機世界中主要有兩種字節序:大端序(Big Endian)和小端序(Little Endian)。
大端序(Big Endian)
大端序的排列方式是高位字節存儲在低地址,低位字節存儲在高地址。這就像我們平常寫數字的習慣一樣,高位在前,低位在後。
舉個例子,十六進制數0x12345678在大端序的32位系統中會這樣存儲:
- 地址1000: 0x12(最高位字節)
- 地址1001: 0x34
- 地址1002: 0x56
- 地址1003: 0x78(最低位字節)
大端序的命名來源於《格列佛遊記》中的故事,在那個故事裏,有些人習慣從大頭(Big End)開始吃雞蛋。
小端序(Little Endian)
小端序的排列方式正好相反,低位字節存儲在低地址,高位字節存儲在高地址。這就像倒着寫數字一樣。
同樣的十六進制數0x12345678在小端序的32位系統中會這樣存儲:
- 地址1000: 0x78(最低位字節)
- 地址1001: 0x56
- 地址1002: 0x34
- 地址1003: 0x12(最高位字節)
小端序的命名也來源於《格列佛遊戲》,對應從小頭(Little End)開始吃雞蛋的人。
4. 為什麼會有不同的字節序?
你可能會想,為什麼要有兩種不同的字節序呢?直接統一成一種不是更好嗎?這其實有歷史原因和技術原因。
歷史原因
不同的計算機廠商在設計處理器時,基於不同的考慮選擇了不同的字節序。比如,Intel的x86系列處理器採用小端序,而Motorola的68000系列處理器採用大端序。隨着時間的推移,這些不同的選擇就固化下來了。
技術考慮
兩種字節序各有優勢:
大端序的優勢是比較直觀,符合人類的閲讀習慣。在網絡傳輸中,大端序被廣泛採用,所以也被稱為"網絡字節序"。
小端序的優勢是在進行某些數學運算時效率更高。比如,在進行類型轉換時,小端序系統可以直接使用低地址的數據,不需要重新計算地址。
5. 不同系統的字節序
常見系統的字節序
- Intel x86/x64系列:小端序
- ARM處理器:可配置,但通常使用小端序
- PowerPC:大端序
- SPARC:大端序
- MIPS:可配置,可以是大端序或小端序
網絡字節序
在網絡通信中,為了保證不同系統之間能夠正確交換數據,規定統一使用大端序,這被稱為"網絡字節序"。當數據在網絡中傳輸時,發送方需要將數據轉換為網絡字節序,接收方再將數據轉換為本地字節序。
6. 字節序的影響
對程序員的影響
在大多數情況下,程序員不需要關心字節序問題,因為:
- 在同一台機器上運行的程序,字節序是一致的
- C語言的編譯器會自動處理大部分字節序問題
- 高級語言通常會屏蔽這些底層細節
但在某些情況下,字節序就變得很重要:
文件存儲
如果一個程序在小端序系統上創建了一個二進制文件,然後這個文件被傳輸到大端序系統上讀取,就可能出現數據錯誤。
比如,數字1234在小端序文件中可能存儲為D2 04(十六進制),但在大端序系統讀取時可能被解釋為1234(十六進制),這完全是錯誤的值。
網絡編程
在網絡編程中,經常需要在本地字節序和網絡字節序之間轉換。C語言提供了專門的函數來處理這種轉換:
htons():主機字節序轉網絡字節序(短整型)htonl():主機字節序轉網絡字節序(長整型)ntohs():網絡字節序轉主機字節序(短整型)ntohl():網絡字節序轉主機字節序(長整型)
嵌入式系統
在嵌入式系統開發中,特別是當需要與其他系統通信或處理特定格式的數據時,字節序問題就變得很重要。程序員需要明確知道數據的字節序,並進行正確的處理。
7. 檢測系統字節序
我們可以用一個簡單的C程序來檢測當前系統的字節序:
#include <stdio.h>
int main() {
int test = 1;
char *p = (char*)&test;
if (*p == 1) {
printf("當前系統是小端序\n");
} else {
printf("當前系統是大端序\n");
}
return 0;
}
這個程序的工作原理是:整數1在內存中,如果是小端序,最低字節(值為1)會存儲在最低地址;如果是大端序,最低字節會存儲在最高地址。通過檢查最低地址的值,就可以判斷字節序。
8. 字節序轉換的實現
雖然系統提供了字節序轉換函數,但瞭解其實現原理也很有意義。以16位數據的字節序轉換為例:
unsigned short swap16(unsigned short value) {
return ((value & 0xFF00) >> 8) | ((value & 0x00FF) << 8);
}
這個函數通過位運算來交換高低字節的位置,從而實現字節序轉換。