C 語言基礎,來嘍!
前言
C 語言是一門抽象的
、面向過程
的語言,C 語言廣泛應用於底層開發
,C 語言在電腦體系中佔據著不可替代的作用,可以說 C 語言是編程的基礎,也就是說,不管你學習任何語言,都應該把 C 語言放在首先要學
的位置上。下面這張圖更好的說明 C 語言的重要性
可以看到,C 語言是一種底層語言,是一種系統層級的語言,作業系統就是使用 C 語言來編寫的,比如 Windows、Linux、UNIX 。如果說其他語言是光鮮亮麗的外表,那麼 C 語言就是靈魂,永遠那麼樸實無華。
C 語言特性
那麼,既然 C 語言這麼重要,它有什麼值得我們去學的地方呢?我們不應該只因為它重要而去學,我們更在意的是學完我們能學會什麼,能讓我們獲得什麼。
C 語言的設計
C 語言是 1972 年,由貝爾實驗室的丹尼斯·里奇(Dennis Ritch)
和肯·湯普遜(Ken Thompson)
在開發 UNIX 作業系統時設計了C語言。C 語言是一門流行的語言,它把電腦科學理論和工程實踐理論完美的融合在一起,使用戶能夠完成模組化的編程和設計。
電腦科學理論:簡稱 CS、是系統性研究資訊與計算的理論基礎以及它們在電腦系統中如何實現與應用的實用技術的學科。
C 語言具有高效性
C 語言是一門高效性語言,它被設計用來充分發揮電腦的優勢,因此 C 語言程式運行速度很快,C 語言能夠合理了使用記憶體來獲得最大的運行速度
C 語言具有可移植性
C 語言是一門具有可移植性的語言,這就意味著,對於在一台電腦上編寫的 C 語言程式可以在另一台電腦上輕鬆地運行,從而極大的減少了程式移植的工作量。
C 語言特點
- C 語言是一門簡潔的語言,因為 C 語言設計更加靠近底層,因此不需要眾多 Java 、C# 等高級語言才有的特性,程式的編寫要求不是很嚴格。
- C 語言具有結構化控制語句,C 語言是一門結構化的語言,它提供的控制語句具有結構化特徵,如 for 循環、if⋯ else 判斷語句和 switch 語句等。
- C 語言具有豐富的數據類型,不僅包含有傳統的字元型、整型、浮點型、數組類型等數據類型,還具有其他程式語言所不具備的數據類型,比如指針。
- C 語言能夠直接對記憶體地址進行讀寫,因此可以實現彙編語言的主要功能,並可直接操作硬體。
- C 語言速度快,生成的目標程式碼執行效率高。
下面讓我們通過一個簡單的示例來說明一下 C 語言
入門級 C 語言程式
下面我們來看一個很簡單的 C 語言程式,我覺得工具無所謂大家用著順手就行。
第一個 C 語言程式
#include <stdio.h>
int main(int argc, const char * argv[]) {
printf("Hello, World!\n");
printf("my Name is cxuan \n")
printf("number = %d \n", number);
return 0;
}
你可能不知道這段程式碼是什麼意思,不過別著急,我們先運行一下看看結果。
這段程式輸出了 Hello,World!
和 My Name is cxuan
,下面我們解釋一下各行程式碼的含義。
首先,第一行的 #include <stdio.h>
, 這行程式碼包含另一個文件,這一行告訴編譯器把 stdio.h
的內容包含在當前程式中。 stdio.h
是 C 編譯器軟體包的標準部分,它能夠提供鍵盤輸入和顯示器輸出。
什麼是 C 標準軟體包?C 是由 Dennis M 在1972年開發的通用,過程性,命令式電腦程式語言。C標準庫是一組 C 語言內置函數,常量和頭文件,例如<stdio.h>,<stdlib.h>,<math.h>等。此庫將用作 C 程式設計師的參考手冊。
我們後面會介紹 stdio.h ,現在你知道它是什麼就好。
在 stdio.h 下面一行程式碼就是 main
函數。
C 程式能夠包含一個或多個函數,函數是 C 語言的根本,就和方法是 Java 的基本構成一樣。main()
表示一個函數名,int
表示的是 main 函數返回一個整數。void 表明 main() 不帶任何參數。這些我們後面也會詳細說明,只需要記住 int 和 void 是標準 ANSI C
定義 main() 的一部分(如果使用 ANSI C 之前的編譯器,請忽略 void)。
然後是 /*一個簡單的 C 語言程式*/
表示的是注釋,注釋使用 /**/
來表示,注釋的內容在兩個符號之間。這些符號能夠提高程式的可讀性。
注意:注釋只是為了幫助程式設計師理解程式碼的含義,編譯器會忽略注釋
下面就是 {
,這是左花括弧,它表示的是函數體的開始,而最後的右花括弧 }
表示函數體的結束。 { }
中間是書寫程式碼的地方,也叫做程式碼塊。
int number
表示的是將會使用一個名為 number 的變數,而且 number 是 int
整數類型。
number = 11
表示的是把值 11 賦值給 number 的變數。
printf(Hello,world!\n);
表示調用一個函數,這個語句使用 printf()
函數,在螢幕上顯示 Hello,world
, printf() 函數是 C 標準庫函數中的一種,它能夠把程式運行的結果輸出到顯示器上。而程式碼 \n
表示的是 換行
,也就是另起一行,把游標移到下一行。
然後接下來的一行 printf() 和上面一行是一樣的,我們就不多說了。最後一行 printf() 有點意思,你會發現有一個 %d
的語法,它的意思表示的是使用整形輸出字元串。
程式碼塊的最後一行是 return 0
,它可以看成是 main 函數的結束,最後一行是程式碼塊 }
,它表示的是程式的結束。
好了,我們現在寫完了第一個 C 語言程式,有沒有對 C 有了更深的認識呢?肯定沒有。。。這才哪到哪,繼續學習吧。
現在,我們可以歸納為 C 語言程式的幾個組成要素,如下圖所示
C 語言執行流程
C 語言程式成為高級語言的原因是它能夠讀取並理解人們的思想。然而,為了能夠在系統中運行 hello.c
程式,則各個 C 語句必須由其他程式轉換為一系列低級機器語言指令。這些指令被打包作為可執行對象程式
,存儲在二進位磁碟文件中。目標程式也稱為可執行目標文件。
在 UNIX 系統中,從源文件到對象文件的轉換是由編譯器
執行完成的。
gcc -o hello hello.c
gcc 編譯器驅動從源文件讀取 hello.c
,並把它翻譯成一個可執行文件 hello
。這個翻譯過程可用如下圖來表示
這就是一個完整的 hello world 程式執行過程,會涉及幾個核心組件:預處理器、編譯器、彙編器、連接器,下面我們逐個擊破。
-
預處理階段(Preprocessing phase)
,預處理器會根據開始的#
字元,修改源 C 程式。#include <stdio.h> 命令就會告訴預處理器去讀系統頭文件stdio.h
中的內容,並把它插入到程式作為文本。然後就得到了另外一個 C 程式hello.i
,這個程式通常是以.i
為結尾。 -
然後是
編譯階段(Compilation phase)
,編譯器會把文本文件hello.i
翻譯成文本hello.s
,它包括一段彙編語言程式(assembly-language program)
。 -
編譯完成之後是
彙編階段(Assembly phase)
,這一步,彙編器 as
會把 hello.s 翻譯成機器指令,把這些指令打包成可重定位的二進位程式(relocatable object program)
放在 hello.c 文件中。它包含的 17 個位元組是函數 main 的指令編碼,如果我們在文本編輯器中打開 hello.o 將會看到一堆亂碼。 -
最後一個是
鏈接階段(Linking phase)
,我們的 hello 程式會調用printf
函數,它是 C 編譯器提供的 C 標準庫中的一部分。printf 函數位於一個叫做printf.o
文件中,它是一個單獨的預編譯好的目標文件,而這個文件必須要和我們的 hello.o 進行鏈接,連接器(ld)
會處理這個合併操作。結果是,hello 文件,它是一個可執行的目標文件(或稱為可執行文件),已準備好載入到記憶體中並由系統執行。
你需要理解編譯系統做了什麼
對於上面這種簡單的 hello 程式來說,我們可以依賴編譯系統(compilation system)
來提供一個正確和有效的機器程式碼。然而,對於我們上面講的程式設計師來說,編譯器有幾大特徵你需要知道
優化程式性能(Optimizing program performance)
,現代編譯器是一種高效的用來生成良好程式碼的工具。對於程式設計師來說,你無需為了編寫高品質的程式碼而去理解編譯器內部做了什麼工作。然而,為了編寫出高效的 C 語言程式,我們需要了解一些基本的機器碼以及編譯器將不同的 C 語句轉化為機器程式碼的過程。理解鏈接時出現的錯誤(Understanding link-time errors)
,在我們的經驗中,一些非常複雜的錯誤大多是由鏈接階段引起的,特別是當你想要構建大型軟體項目時。避免安全漏洞(Avoiding security holes)
,近些年來,緩衝區溢出(buffer overflow vulnerabilities)
是造成網路和 Internet 服務的罪魁禍首,所以我們有必要去規避這種問題。
系統硬體組成
為了理解 hello 程式在運行時發生了什麼,我們需要首先對系統的硬體有一個認識。下面這是一張 Intel 系統產品的模型,我們來對其進行解釋
匯流排(Buses)
:在整個系統中運行的是稱為匯流排的電氣管道的集合,這些匯流排在組件之間來回傳輸位元組資訊。通常匯流排被設計成傳送定長的位元組塊,也就是字(word)
。字中的位元組數(字長)是一個基本的系統參數,各個系統中都不盡相同。現在大部分的字都是 4 個位元組(32 位)或者 8 個位元組(64 位)。
-
I/O 設備(I/O Devices)
:Input/Output 設備是系統和外部世界的連接。上圖中有四類 I/O 設備:用於用戶輸入的鍵盤和滑鼠,用於用戶輸出的顯示器,一個磁碟驅動用來長時間的保存數據和程式。剛開始的時候,可執行程式就保存在磁碟上。每個I/O 設備連接 I/O 匯流排都被稱為
控制器(controller)
或者是適配器(Adapter)
。控制器和適配器之間的主要區別在於封裝方式。控制器是 I/O 設備本身或者系統的主印製板電路(通常稱作主板)上的晶片組。而適配器則是一塊插在主板插槽上的卡。無論組織形式如何,它們的最終目的都是彼此交換資訊。 -
主存(Main Memory)
,主存是一個臨時存儲設備
,而不是永久性存儲,磁碟是永久性存儲
的設備。主存既保存程式,又保存處理器執行流程所處理的數據。從物理組成上說,主存是由一系列DRAM(dynamic random access memory)
動態隨機存儲構成的集合。邏輯上說,記憶體就是一個線性的位元組數組,有它唯一的地址編號,從 0 開始。一般來說,組成程式的每條機器指令都由不同數量的位元組構成,C 程式變數相對應的數據項的大小根據類型進行變化。比如,在 Linux 的 x86-64 機器上,short 類型的數據需要 2 個位元組,int 和 float 需要 4 個位元組,而 long 和 double 需要 8 個位元組。 -
處理器(Processor)
,CPU(central processing unit)
或者簡單的處理器,是解釋(並執行)存儲在主存儲器中的指令的引擎。處理器的核心大小為一個字的存儲設備(或暫存器),稱為程式計數器(PC)
。在任何時刻,PC 都指向主存中的某條機器語言指令(即含有該條指令的地址)。從系統通電開始,直到系統斷電,處理器一直在不斷地執行程式計數器指向的指令,再更新程式計數器,使其指向下一條指令。處理器根據其指令集體系結構定義的指令模型進行操作。在這個模型中,指令按照嚴格的順序執行,執行一條指令涉及執行一系列的步驟。處理器從程式計數器指向的記憶體中讀取指令,解釋指令中的位,執行該指令指示的一些簡單操作,然後更新程式計數器以指向下一條指令。指令與指令之間可能連續,可能不連續(比如 jmp 指令就不會順序讀取)
下面是 CPU 可能執行簡單操作的幾個步驟
-
載入(Load)
:從主存中拷貝一個位元組或者一個字到記憶體中,覆蓋暫存器先前的內容 -
存儲(Store)
:將暫存器中的位元組或字複製到主存儲器中的某個位置,從而覆蓋該位置的先前內容 -
操作(Operate)
:把兩個暫存器的內容複製到ALU(Arithmetic logic unit)
。把兩個字進行算術運算,並把結果存儲在暫存器中,重寫暫存器先前的內容。
算術邏輯單元(ALU)是對數字二進位數執行算術和按位運算的組合數字電子電路。
跳轉(jump)
:從指令中抽取一個字,把這個字複製到程式計數器(PC)
中,覆蓋原來的值
剖析 hello 程式的執行過程
前面我們簡單的介紹了一下電腦的硬體的組成和操作,現在我們正式介紹運行示常式序時發生了什麼,我們會從宏觀的角度進行描述,不會涉及到所有的技術細節
剛開始時,shell 程式執行它的指令,等待用戶鍵入一個命令。當我們在鍵盤上輸入了 ./hello
這幾個字元時,shell 程式將字元逐一讀入暫存器,再把它放到記憶體中,如下圖所示
當我們在鍵盤上敲擊回車鍵
的時候,shell 程式就知道我們已經結束了命令的輸入。然後 shell 執行一系列指令來載入可執行的 hello 文件,這些指令將目標文件中的程式碼和數據從磁碟複製到主存。
利用 DMA(Direct Memory Access)
技術可以直接將磁碟中的數據複製到記憶體中,如下
一旦目標文件中 hello 中的程式碼和數據被載入到主存,處理器就開始執行 hello 程式的 main 程式中的機器語言指令。這些指令將 hello,world\n
字元串中的位元組從主存複製到暫存器文件,再從暫存器中複製到顯示設備,最終顯示在螢幕上。如下所示
高速快取是關鍵
上面我們介紹完了一個 hello 程式的執行過程,系統花費了大量時間把資訊從一個地方搬運到另外一個地方。hello 程式的機器指令最初存儲在磁碟
上。當程式載入後,它們會拷貝
到主存中。當 CPU 開始運行時,指令又從記憶體複製到 CPU 中。同樣的,字元串數據 hello,world \n
最初也是在磁碟上,它被複制到記憶體中,然後再到顯示器設備輸出。從程式設計師的角度來看,這種複製大部分是開銷,這減慢了程式的工作效率。因此,對於系統設計來說,最主要的一個工作是讓程式運行的越來越快。
由於物理定律,較大的存儲設備要比較小的存儲設備慢。而由於暫存器和記憶體的處理效率在越來越大,所以針對這種差異,系統設計者採用了更小更快的存儲設備,稱為高速快取存儲器(cache memory, 簡稱為 cache 高速快取)
,作為暫時的集結區域,存放近期可能會需要的資訊。如下圖所示
圖中我們標出了高速快取的位置,位於高速快取中的 L1
高速快取容量可以達到數萬位元組,訪問速度幾乎和訪問暫存器文件一樣快。容量更大的 L2
高速快取通過一條特殊的匯流排鏈接 CPU,雖然 L2 快取比 L1 快取慢 5 倍,但是仍比記憶體要哦快 5 – 10 倍。L1 和 L2 是使用一種靜態隨機訪問存儲器(SRAM)
的硬體技術實現的。最新的、處理器更強大的系統甚至有三級快取:L1、L2 和 L3。系統可以獲得一個很大的存儲器,同時訪問速度也更快,原因是利用了高速快取的 局部性
原理。
Again:入門程式細節
現在,我們來探討一下入門級
程式的細節,由淺入深的來了解一下 C 語言的特性。
#include<stdio.h>
我們上面說到,#include<stdio.h>
是程式編譯之前要處理的內容,稱為編譯預處理
命令。
預處理命令是在編譯之前進行處理。預處理程式一般以 #
號開頭。
所有的 C 編譯器軟體包都提供 stdio.h
文件。該文件包含了給編譯器使用的輸入和輸出函數,比如 println() 資訊。該文件名的含義是標準輸入/輸出 頭文件。通常,在 C 程式頂部的資訊集合被稱為 頭文件(header)
。
C 的第一個標準是由 ANSI 發布的。雖然這份文檔後來被國際標準化組織(ISO)採納並且 ISO 發布的修訂版也被 ANSI 採納了,但名稱 ANSI C(而不是 ISO C) 仍被廣泛使用。一些軟體開發者使用ISO C,還有一些使用 Standard C。
C 標準庫
除了 <sdtio.h> 外,C 標準庫還包括下面這些頭文件
<assert.h>
提供了一個名為 assert
的關鍵字,它用於驗證程式作出的假設,並在假設為假輸出診斷消息。
<ctype.h>
C 標準庫的 ctype.h 頭文件提供了一些函數,可以用於測試和映射字元。
這些字元接受 int 作為參數,它的值必須是 EOF
或者是一個無符號字元
EOF是一個電腦術語,為 End Of File 的縮寫,在作業系統中表示資料源無更多的資料可讀取。資料源通常稱為檔案或串流。通常在文本的最後存在此字元表示資料結束。
<errno.h>
C 標準庫的 errno.h 頭文件定義了整數變數 errno,它是通過系統調用設置的,這些庫函數表明了什麼發生了錯誤。
<float.h>
C 標準庫的 float.h 頭文件包含了一組與浮點值相關的依賴於平台的常量。
<limits.h>
limits.h 頭文件決定了各種變數類型的各種屬性。定義在該頭文件中的宏限制了各種變數類型(比如 char、int 和 long)的值。
<locale.h>
locale.h 頭文件定義了特定地域的設置,比如日期格式和貨幣符號
<math.h>
math.h 頭文件定義了各種數學函數和一個宏。在這個庫中所有可用的功能都帶有一個 double 類型的參數,且都返回 double 類型的結果。
<setjmp.h>
setjmp.h 頭文件定義了宏 setjmp()、函數 longjmp() 和變數類型 jmp_buf,該變數類型會繞過正常的函數調用和返回規則。
<signal.h>
signal.h 頭文件定義了一個變數類型 sig_atomic_t、兩個函數調用和一些宏來處理程式執行期間報告的不同訊號。
<stdarg.h>
stdarg.h 頭文件定義了一個變數類型 va_list 和三個宏,這三個宏可用於在參數個數未知(即參數個數可變)時獲取函數中的參數。
<stddef.h>
stddef .h 頭文件定義了各種變數類型和宏。這些定義中的大部分也出現在其它頭文件中。
<stdlib.h>
stdlib .h 頭文件定義了四個變數類型、一些宏和各種通用工具函數。
<string.h>
string .h 頭文件定義了一個變數類型、一個宏和各種操作字元數組的函數。
<time.h>
time.h 頭文件定義了四個變數類型、兩個宏和各種操作日期和時間的函數。
main() 函數
main 函數聽起來像是調皮搗蛋的孩子故意給方法名起一個 主要的
方法,來告訴他人他才是這個世界的中心。但事實卻不是這樣,而 main()
方法確實是世界的中心。
C 語言程式一定從 main() 函數開始執行,除了 main() 函數外,你可以隨意命名其他函數。通常,main 後面的 ()
中表示一些傳入資訊,我們上面的那個例子中沒有傳遞資訊,因為圓括弧中的輸入是 void 。
除了上面那種寫法外,還有兩種 main 方法的表示方式,一種是 void main(){}
,一種是 int main(int argc, char* argv[]) {}
- void main() 聲明了一個帶有不確定參數的構造方法
- int main(int argc, char* argv[]) {} 其中的 argc 是一個非負值,表示從運行程式的環境傳遞到程式的參數數量。它是指向 argc + 1 指針數組的第一個元素的指針,其中最後一個為null,而前一個(如果有的話)指向表示從主機環境傳遞給程式的參數的字元串。 如果argv [0]不是空指針(或者等效地,如果argc> 0),則指向表示程式名稱的字元串,如果在主機環境中無法使用程式名稱,則該字元串為空。
注釋
在程式中,使用 /**/ 的表示注釋,注釋對於程式來說沒有什麼實際用處,但是對程式設計師來說卻非常有用,它能夠幫助我們理解程式,也能夠讓他人看懂你寫的程式,我們在開發工作中,都非常反感不寫注釋的人,由此可見注釋非常重要。
C 語言注釋的好處是,它可以放在任意地方,甚至程式碼在同一行也沒關係。較長的注釋可以多行表示,我們使用 /**/ 表示多行注釋,而 // 只表示的是單行注釋。下面是幾種注釋的表示形式
// 這是一個單行注釋
/* 多行注釋用一行表示 */
/*
多行注釋用多行表示
多行注釋用多行表示
多行注釋用多行表示
多行注釋用多行表示
*/
函數體
在頭文件、main 方法後面的就是函數體(注釋一般不算),函數體就是函數的執行體,是你編寫大量程式碼的地方。
變數聲明
在我們入門級的程式碼中,我們聲明了一個名為 number
的變數,它的類型是 int,這行程式碼叫做 聲明
,聲明是 C 語言最重要的特性之一。這個聲明完成了兩件事情:定義了一個名為 number 的變數,定義 number 的具體類型。
int 是 C 語言的一個 關鍵字(keyword)
,表示一種基本的 C 語言數據類型。關鍵字是用於語言定義的。不能使用關鍵字作為變數進行定義。
示例中的 number
是一個 標識符(identifier)
,也就是一個變數、函數或者其他實體的名稱。
變數賦值
在入門例子程式中,我們聲明了一個 number 變數,並為其賦值為 11,賦值是 C 語言的基本操作之一。這行程式碼的意思就是把值 1 賦給變數 number。在執行 int number 時,編譯器會在電腦記憶體中為變數 number 預留空間,然後在執行這行賦值表達式語句時,把值存儲在之前預留的位置。可以給 number 賦不同的值,這就是 number 之所以被稱為 變數(variable)
的原因。
printf 函數
在入門例子程式中,有三行 printf(),這是 C 語言的標準函數。圓括弧中的內容是從 main 函數傳遞給 printf 函數的。參數分為兩種:實際參數(actual argument)
和 形式參數(formal parameters)
。我們上面提到的 printf 函數括弧中的內容,都是實參。
return 語句
在入門例子程式中,return 語句是最後一條語句。int main(void)
中的 int 表明 main() 函數應返回一個整數。有返回值的 C 函數要有 return 語句,沒有返回值的程式也建議大家保留 return 關鍵字,這是一種好的習慣或者說統一的編碼風格。
分號
在 C 語言中,每一行的結尾都要用 ;
進行結束,它表示一個語句的結束,如果忘記或者會略分號會被編譯器提示錯誤。
關鍵字
下面是 C 語言中的關鍵字,C 語言的關鍵字一共有 32
個,根據其作用不同進行劃分
數據類型關鍵字
數據類型的關鍵字主要有 12 個,分別是
char
: 聲明字元型變數或函數double
: 聲明雙精度變數或函數float
: 聲明浮點型變數或函數int
: 聲明整型變數或函數long
: 聲明長整型變數或函數short
: 聲明短整型變數或函數signed
: 聲明有符號類型變數或函數_Bool
: 聲明布爾類型_Complex
:聲明複數_Imaginary
: 聲明虛數unsigned
: 聲明無符號類型變數或函數void
: 聲明函數無返回值或無參數,聲明無類型指針
控制語句關鍵字
控制語句循環的關鍵字也有 12 個,分別是
循環語句
for
: for 循環,使用的最多do
:循環語句的前提條件循環體while
:循環語句的循環條件break
: 跳出當前循環continue
:結束當前循環,開始下一輪循環
條件語句
if
:條件語句的判斷條件else
: 條件語句的否定分支,與 if 連用goto
: 無條件跳轉語句
開關語句
switch
: 用於開關語句case
:開關語句的另外一種分支default
: 開關語句中的其他分支
返回語句
retur
:子程式返回語句(可以帶參數,也看不帶參數)
存儲類型關鍵字
auto
: 聲明自動變數 一般不使用extern
: 聲明變數是在其他文件正聲明(也可以看做是引用變數)register
: 聲明暫存器變數static
: 聲明靜態變數
其他關鍵字
const
: 聲明只讀變數sizeof
: 計算數據類型長度typedef
: 用以給數據類型取別名volatile
: 說明變數在程式執行中可被隱含地改變
C 中的數據
我們在了解完上面的入門例子程式後,下面我們就要全面認識一下 C 語言程式了,首先我們先來認識一下 C 語言最基本的變數與常量。
變數和常量
變數和常量是程式處理的兩種基本對象。
有些數據類型在程式使用之前就已經被設定好了,在整個過程中沒有變化(這段話描述不準確,但是為了通俗易懂,暫且這麼描述),這種數據被稱為常量(constant)
。另外一種數據類型在程式執行期間可能會發生改變,這種數據類型被稱為 變數(variable)
。例如 int number
就是一個變數,而3.1415
就是一個常量,因為 int number 一旦聲明出來,你可以對其任意賦值,而 3.1415 一旦聲明出來,就不會再改變。
變數名
有必要在聊數據類型之前先說一說變數名的概念。變數名是由字母和數字組成的序列,第一個字元必須是字母。在變數名的命名過程中,下劃線 _
被看作字母,下劃線一般用於名稱較長的變數名,這樣能夠提高程式的可讀性。變數名通常不會以下劃線來開頭。在 C 中,大小寫是有區別的,也就是說,a 和 A 完全是兩個不同的變數。一般變數名使用小寫字母,符號常量(#define 定義的)全都使用大寫。選擇變數名的時候,盡量能夠從字面上描述出變數的用途,切忌起這種 abc 毫無意義的變數。
還需要注意一般局部變數都會使用較短的變數名,外部變數使用較長的名字。
數據類型
在了解數據類型之前,我們需要先了解一下這些概念 位、位元組和字。
位、位元組和字都是對電腦存儲單元的描述。在電腦世界中,最小的單元是
位(bit)
,一個位就表示一個 0 或 1,一般當你的小夥伴問你的電腦是 xxx 位,常見的有 32 位或者 64 位,這裡的位就指的是比特,比特就是 bit 的中文名稱,所以這裡的 32 位或者 64 位指的就是 32 bit 或者 64 bit。位元組是基本的存儲單元,基本存儲單元說的是在電腦中都是按照位元組來存儲的,一個位元組等於 8 位,即 1 byte = 8 bit。字是自然存儲單位,在現代電腦中,一個字等於 2 位元組。
C 語言的數據類型有很多,下面我們就來依次介紹一下。
整型
C 語言中的整型用 int
來表示,可以是正整數、負整數或零。在不同位數的電腦中其取值範圍也不同。不過在 32 位和 64 位電腦中,int 的取值範圍是都是 2^32 ,也就是 -2147483648 ~ +2147483647,無符號類型的取值範圍是 0 ~ 4294967295。
整型以二進位整數存儲,分為有符號數和無符號數兩種形態,有符號數可以存儲正整數、負整數和零;無符號只能存儲正整數和零。
可以使用 printf 列印出來 int 類型的值,如下程式碼所示。
#include <stdio.h>
int main(){
int a = -5;
printf("%d\n",a);
unsigned int b = 6;
printf("%d\n",b);
}
C 語言還提供 3 個附屬關鍵字修飾整數類型,即 short、long 和 unsigned。
- short int 類型(或者簡寫為 short)佔用的存儲空間
可能
比 int 類型少,適合用於數值較小的場景。 - long int 或者 long 佔用的存儲空間
可能
比 int 類型多,適合用於數值較大的場景。 - long long int 或者 long long(C99 加入)佔用的存儲空間比 long 多,適用於數值更大的場合,至少佔用 64 位,與 int 類似,long long 也是有符號類型。
- unsigned int 或 unsigned 只用於非負值的場景,這種類型的取值範圍有所不同,比如 16 位的 unsigned int 表示的範圍是 0 ~ 65535 ,而不是 -32768 ~ 32767。
- 在 C90 標準中,添加了 unsigned long int 或者 unsigned long 和 unsigned short int 或 unsigned short 類型,在 C99 中又添加了 unsigned long long int 或者 unsigned long long 。
- 在任何有符號類型前面加 signed ,可強調使用有符號類型的意圖。比如 short、short int、signed short、signed short int 都表示一種類型。
比如上面這些描述可以用下面這些程式碼來聲明:
long int lia;
long la;
long long lla;
short int sib;
short sb;
unsigned int uic;
unsigned uc;
unsigned long uld;
unsigned short usd;
這裡需要注意一點,unsigned 定義的變數,按照 printf 格式化輸出時,是能夠顯示負值的,為什麼呢?不是 unsigned 修飾的值不能是負值啊,那是因為 unsigned 修飾的變數,在計算時會有用,輸出沒什麼影響,這也是 cxuan 剛開始學習的時候踩的坑。
我們學過 Java 的同學剛開始都對這些定義覺得莫名其妙,為什麼一個 C 語言要對數據類型有這麼多定義?C 語言真麻煩,我不學了!
千萬不要有這種想法,如果有這種想法的同學,你一定是被 JVM 保護的像個孩子!我必須從現在開始糾正你的這個想法,因為 Java 有 JVM 的保護,很多特性都做了優化,而 C 就像個沒有傘的孩子,它必須自己和這個世界打交道!
上面在說 short int 和 long int 的時候,都加了一個可能,怎麼,難道 short int 和 long int 和 int 還不一樣嗎?
這裡就是 C 語言數據類型一個獨特的風格。
為什麼說可能,這是由於 C 語言為了適配不同的機器來設定的語法規則,在早起的電腦上,int 類型和 short 類型都占 16 位,long 類型占 32 位,在後來的電腦中,都採用了 16 位存儲 short 類型,32 位存儲 int 類型和 long 類型,現在,電腦普遍使用 64 位 CPU,為了存儲 64 位整數,才引入了 long long 類型。所以,一般現在個人電腦上常見的設置是 long long 佔用 64 位,long 佔用 32 位,short 佔用 16 位,int 佔用 16 位或者 32 位。
char 類型
char 類型一般用於存儲字元,表示方法如下
char a = 'x';
char b = 'y';
char 被稱為字元類型,只能用單引號 ” 來表示,而不能用雙引號 「」 來表示,這和字元串的表示形式相反。
char 雖然表示字元,但是 char 實際上存儲的是整數而不是字元,電腦一般使用 ASCII
來處理字元,標準 ASCII 碼的範圍是 0 – 127 ,只需 7 位二進位數表示即可。C 語言中規定 char 佔用 1 位元組。
其實整型和字元型是相通的,他們在記憶體中的存儲本質是相通的,編譯器發現 char ,就會自動轉換為整數存儲,相反的,如果給 int 類型賦值英文字元,也會轉換成整數存儲,如下程式碼
#include <stdio.h>
int main(){
char a = 'x';
int b;
b = 'y';
printf("%d\n%d\n",a,b);
}
輸出
120
121
所以,int 和 char 只是存儲的範圍不同,整型可以是 2 位元組,4 位元組,8 位元組,而字元型只佔 1 位元組。
有些 C 編譯器把 char 實現為有符號類型,這意味著 char 可表示的範圍是 -128 ~ 127,而有些編譯器把 char 實現為無符號類型,這種情況下 char 可表示的範圍是 0 – 255。signed char 表示的是有符號類型,unsigned char 表示的是無符號類型。
_Bool 類型
_Bool 類型是 C99 新增的數據類型,用於表示布爾值。也就是邏輯值 true 和 false。在 C99 之前,都是用 int 中的 1 和 0 來表示。所以 _Bool 在某種程度上也是一種數據類型。表示 0 和 1 的話,用 1 bit(位)表示就夠了。
float、double 和 long double
整型對於大多數軟體開發項目而言就已經夠用了。然而,在金融領域和數學領域還經常使用浮點數
。C 語言中的浮點數有 float、double 和 long double 類型。浮點數類型能夠表示包括小數在內更大範圍的數。浮點數能表示小數,而且表示範圍比較大。浮點數的表示類似於科學技術法。下面是一些科學記數法示例:
C 規定 float 類型必須至少能表示 6 位有效數字,而且取值範圍至少是 10^-37 ~ 10^+37。通常情況下,系統存儲一個浮點數要佔用 32 位。
C 提供的另一種浮點類型是 double(雙精度類型)。一般來說,double 佔用的是 64 位而不是 32 位。
C 提供的第三種類型是 long double ,用於滿足比 double 類型更高的精度要求。不過,C 只保證了 long double 類型至少與 double 類型相同。
浮點數的聲明方式和整型類似,下面是一些浮點數的聲明方式。
#include <stdio.h>
int main(){
float aboat = 2100.0;
double abet = 2.14e9;
long double dip = 5.32e-5;
printf("%f\n", aboat);
printf("%e\n", abet);
printf("%Lf\n", dip);
}
printf() 函數使用 %f 轉換說明列印十進位計數法的 float 和 double 類型浮點數,用 %e 列印指數記數法的浮點數。列印 long double 類型要使用 %Lf 轉換說明。
關於浮點數,還需要注意其上溢
和下溢
的問題。
上溢指的是是指由於數字過大,超過當前類型所能表示的範圍,如下所示
float toobig = 3.4E38 * 100.0f;
printf("%e\n",toobig);
輸出的內容是 inf
,這表示 toobig 的結果超過了其定義的範圍,C 語言就會給 toobig 賦一個表示無窮大的特定值,而且 printf 顯示值為 inf 或者 infinity 。
下溢:是指由於數值太小,低於當前類型所能表示的最小的值,電腦就只好把尾數位向右移,空出第一個二進位位,但是與此同時,卻損失了原來末尾有效位上面的數字,這種情況就叫做下溢。比如下面這段程式碼
float toosmall = 0.1234e-38/10;
printf("%e\n", toosmall);
複數和虛數類型
許多科學和工程計算都需要用到複數和虛數,C99 標準支援複數類型和虛數類型,C 語言中有 3 種複數類型:float _Complex、double _Complex 和 long double _Complex。
C 語言提供的 3 種虛數類型:float _Imaginary、 double _Imaginary 和 long double _Imaginary。
如果包含 complex.h 頭文件的話,便可使用 complex 替換 _Complex,用 imaginary 替代 _Imaginary。
其他類型
除了上述我們介紹過的類型之外,C 語言中還有其他類型,比如數組、指針、結構和聯合,雖然 C 語言沒有字元串類型,但是 C 語言卻能夠很好的處理字元串。
常量
在很多情況下我們需要常量,在整個程式的執行過程中,其值不會發生改變,比如一天有 24 個小時,最大緩衝區的大小,滑動窗口的最大值等。這些固定的值,即稱為常量,又可以叫做字面量。
常量也分為很多種,整型常量,浮點型常量,字元常量,字元串常量,下面我們分別來介紹
整數常量
整數常量可以表示為十進位、八進位或十六進位。前綴指定基數:0x 或 0X 表示十六進位,0 表示八進位,不帶前綴則默認表示十進位。整數常量也可以帶一個後綴,後綴是 U 和 L 的組合,U 表示無符號整數(unsigned),L 表示長整數(long)。
330 /* 合法的 */
315u /* 合法的 */
0xFeeL /* 合法的 */
048 /* 非法的:8 進位不能定義 8 */
浮點型常量
浮點型常量由整數部分、小數點、小數部分和指數部分組成。你可以使用小數形式或者指數形式來表示浮點常量。
當使用小數形式表示時,必須包含整數部分、小數部分,或同時包含兩者。當使用指數形式表示時, 必須包含小數點、指數,或同時包含兩者。帶符號的指數是用 e 或 E 引入的。
3.14159 /* 合法的 */
314159E-5L /* 合法的 */
510E /* 非法的:不完整的指數 */
210f /* 非法的:沒有小數或指數 */
字元常量
C 語言中的字元常量使用單引號(即撇號)括起來的一個字元。如『a』,『x』,’D’,『?』,『$』 等都是字元常量。注意,『a』 和 『A』 是不同的字元常量。
除了以上形式的字元常量外,C 還允許用一種特殊形式的字元常量,就是以一個 「\」 開頭的字元序列。例如,前面已經遇到過的,在 printf 函數中的『\n』,它代表一個換行符。這是一種控制字元,在螢幕上是不能顯示的。
常用的以 「\」 開頭的特殊字元有
表中列出的字元稱為「轉義字元」,意思是將反斜杠(\)後面的字元轉換成另外的意義。如 『\n』 中的 「n」 不代表字母 n 而作為「換行」符。
表中最後第 2 行是用ASCII碼(八進位數)表示一個字元,例如 『\101』 代表 ASCII 碼(十進位數)為 65 的字元 「A」。『\012』(十進位 ASCII 碼為 10)代表換行。
需要注意的是 『\0』 或 『\000』 代表 ASCII 碼為 0 的控制字元,它用在字元串中。
字元串常量
字元串常量通常用 “” 進行表示。字元串就是一系列字元的集合。一個字元串包含類似於字元常量的字元:普通的字元、轉義序列和通用的字元。
常量定義
C 語言中,有兩種定義常量的方式。
- 使用
#define
預處理器進行預處理 - 使用
const
關鍵字進行處理
下面是使用 #define 預處理器進行常量定義的程式碼。
#include <stdio.h>
#define LENGTH 5
#define WIDTH 10
int main(){
int area = LENGTH * WIDTH;
printf("area = %d\n", area);
}
同樣的,我們也可以使用 const 關鍵字來定義常量,如下程式碼所示
#include <stdio.h>
int main(){
const int LENGTH = 10;
const int WIDTH = 5;
int area;
area = LENGTH * WIDTH;
printf("area = %d\n", area);
}
那麼這兩種常量定義方式有什麼不同呢?
編譯器處理方式不同
使用 #define 預處理器是在預處理階段進行的,而 const 修飾的常量是在編譯階段進行。
類型定義和檢查不同
使用 #define 不用聲明數據類型,而且不用類型檢查,僅僅是定義;而使用 const 需要聲明具體的數據類型,在編譯階段會進行類型檢查。
文章參考:
//www.zhihu.com/question/19668080
我自己寫了四本 PDF ,非常硬核,鏈接如下
cxuan 嘔心瀝血肝了四本 PDF。