您現在的位置是:首頁 > 垂釣

C程式從頭到尾的過程

  • 由 TTcome 發表于 垂釣
  • 2022-01-07
簡介另外,我還會用一段相對複雜的示例程式碼,讓你快速回顧 C 語言中最常見的那些語法及使用方式

zarchiver怎麼解壓分卷

大家好,我是TT。

這節來說說一個 C 程式從原始碼編寫到編譯,再到最後執行的完整過程。另外,我還會用一段相對複雜的示例程式碼,讓你快速回顧 C 語言中最常見的那些語法及使用方式。最後,我還會從語言本身的角度,來探討 C 語言與其他程式語言在程式設計正規化上的不同之處。

對於編譯工具,這裡會穿插使用運行於 x86-64 平臺的 GCC 11。2 或 Clang 13。0。0 版本編譯器。市面上有很多成熟的 C 編譯器可以選擇,但不同的編譯器可能存在著所支援平臺(類 Unix、Windows)以及 C 標準(C89、C99、C11、C17)上的差異,因此在選擇時需要特別注意這些問題。這裡使用的 GCC 和 Clang 都支援 C 語言的最新標準 C17,並且都可以執行在類 Unix 與 Windows 系統上。

當然,如果你在本地環境中沒有安裝上面這些編譯器,那麼也可以直接使用雲編譯器,比如 Godbolt。相較於本地編譯器,雲編譯器即開即用,而且可以隨時靈活切換不同的編譯器版本。

至於 IDE,那些常用的都可以,不過推薦你選擇 Visual Studio Code,因為它較為輕量,且目前提供的外掛能力也足夠進行 C 語言開發。

用一個程式快速回顧 C 核心語法

為了讓你比較完整地回顧 C 語言的核心語法,我設計了一個相對複雜的 C 語言程式作為例子。在這裡,你可以先試著閱讀這段程式碼,思考下 C 語言的使用方式。程式碼如下所示:

C程式從頭到尾的過程

C程式從頭到尾的過程

這段程式碼用到了橫跨 K&R C 到 C17 標準的許多語言特性,建立了多個基於自定義型別構建的物件,並在程式的最後將這些物件的相關資訊列印了出來。

下面,就來跟著我一起梳理這段程式碼中用到的 C 語法特性吧。我會按照程式程式碼的執行順序,來分別介紹每一個執行步驟中涉及到的關鍵語言知識點。其中,相關的語言結構和語法特性可以被分為下面這些類別。

入口函式

現在,讓我們來仔細觀察這個程式。首先,我們的目光來到第 31 行上名為 main 的函式。

所有的 C 程式都會使用 main 函式作為入口函式。入口函式,就是指程式開始執行時,程式碼中會被首先呼叫的那個函式。在 main 函式中,我們可以透過它接收到的實際引數,來選擇性地訪問程式在開始執行時,由使用者傳遞給程式的外部引數。

main 函式在執行結束時會返回一個整數,用於表示程式執行完畢時的狀態,通常返回數字 0 表示程式正常退出,返回其他數字則代表異常退出。為了保持程式碼的可讀性,這裡我們使用標準庫中定義的宏常量 EXIT_SUCCESS ,作為程式退出的返回值。顧名思義,這個宏常量對應的實際值就是數字 0。

陣列

接下來,我們來到第 33 行。可以看到,在 main 函式內部,我們使用了“括號列表(brace-enclosed lists)”的方式,完成了對陣列 conns 的初始化過程。

而在初始化列表中,我們還使用了指派初始化(為初始化列表中的項設定“指派符”)的方式,來明確指定這些項在陣列中的具體位置。比如這裡第一項對應的 “[2]” ,就表示將該項設定為陣列 conns 中的第 3 個元素(索引從 0 開始)。

陣列定義完畢後,第 44 到第 46 行的程式碼訪問了其內部存放的元素。這裡我們直接使用方括號加索引值的語法形式做到了這一點。

結構與聯合

陣列 conns 內部,存放有若干個型別為 CONN 的結構物件。在 C 語言中,結構和聯合(有時也被稱為結構體與聯合體)通常用來組織複雜型別的自定義資料。在結構中,所有定義欄位的對應資料按照記憶體連續的方向排列;而在聯合中,定義的欄位同一時間只會有一個“生效”。

觀察第 15 行到 24 行,可以看到:在我們對結構 CONN 的定義過程中,使用了來自 C99 標準的 _Bool 型別(這裡的宏 bool 會展開為該型別),以及來自 C11 標準的匿名聯合體。

第 34 到第 36 行,在我們對結構 CONN 物件的初始化過程中,也同樣使用了類似陣列的括號列表初始化,以及指派初始化。但和前面陣列初始化不同的是,這裡的指派是針對結構與聯合型別內部的成員欄位的,因此需要使用 “。” 符號來引用某個具體成員,而非陣列所使用的形式。

控制結構

在這段程式碼的第 39 行,我們使用了 for 語句以迴圈的形式遍歷了陣列 conns 中的內容。除此之外,C 語言中常用的控制結構還有 switch 語句、while 語句、以及 goto 語句等等。這些語句分別以選擇、迭代,及跳轉這三種不同方式控制著程式的實際執行邏輯。而程式本身也可以在這些控制語句的靈活組合下變得更加複雜。

指標

指標是 C 語言中最危險但也最強大的“武器”之一。藉助指標,我們能夠靈活地操控程式享有的記憶體資源。

在上面程式碼的第 45 行,我們將陣列 conns 中各個元素的地址傳遞給了函式 findAddr,而該函式則接收一個指向 CONN 型別物件的常量指標。所以,透過該指標,我們無法在函式內部修改指標所指向物件的值。而這在一定程度上保證了函式僅能夠擁有足夠完成其任務的最小許可權。

編譯器對 C 原始碼的處理過程分為幾個階段,其中,宏是最先被處理的一個部分。在這段程式碼的開頭處,我們透過宏指令 “#include” 引入了程式正常執行需要的一些外部依賴項,這些引入的內容會在程式編譯時得到替換。隨後,我們又透過 “#define” 指令定義了相應的宏常量與宏函式,而其中的宏函式 typename 則使用到了 C11 標準新引入的 _Generic 關鍵字,以用來實現基於宏的泛型。

斷言

在這段程式碼的第 32 行,我們使用了 C11 標準中提供的靜態斷言能力,來保證結構型別 CONN 的大小不會超過一定的閾值。而在程式碼的第 27 行,我們還使用了執行時斷言來保證傳遞給函式 findAddr 的 CONN 物件指標不為空。

在 C 程式碼中,我們通常會使用斷言,來對某種需要支援程式正常執行的假設性條件進行檢查。而當條件不滿足時,則在程式編譯或執行時終止,並向用戶丟擲相應的錯誤資訊。C 語言提供靜態與動態兩種型別的斷言,其中靜態斷言會在程式碼編譯時進行檢查;而動態斷言則會在程式執行過程中,執行到該斷言語句時再進行檢查。

函式內聯

在函式 findAddr 的定義程式碼中,我們為其添加了名為 inline 的關鍵字。透過使用該關鍵字,我們可以“建議”編譯器將該函式的內部邏輯直接替換到函式的呼叫位置處,以減少函式呼叫時產生的開銷。這種方式通常使用在那些函式體較小,且會被多次呼叫的函式上,以產生較為顯著的效能提升。

其他特性

除了上面提到的內容,這段程式碼中還涉及到了一些基本的 C 語言特性:

使用 const 定義只讀變數;

使用 typedef 定義新型別;

使用 static 宣告靜態函式;

使用各類運算子;

呼叫標準庫函式;

使用 enum 定義列舉型別;

……

這些語法特性本身比較常用,且概念較為簡單,這裡我就不再單獨介紹了。如果你對其中的一些特性感到陌生,可以選擇在 GeeksforGeeks網站上直接查詢特定主題並學習,或者查閱《C Primer Plus》這些入門書籍。

C程式從頭到尾的過程

C primer plus 第6版中文版 C語言程式設計從入門到精通自學C語言程式設計教材書計算機程式開發資料結構教程書籍C++primer

檢視

到這裡,我們就把 C 語言的核心語法大致捋了一遍。你可以看到,C 語言的語法並不複雜。C 語言在設計上就十分精簡,截止到 C17 標準,語言本身也僅有 44 個關鍵字。C 語言的強大並不是源於複雜的語法設計,相反,簡單的語法給了 C 開發者更高的自由度,讓我們可以更加靈活地設計程式的執行邏輯。

C 語言的程式設計正規化是怎樣的?

拋開語法細節,從總體上來看,C 語言是一種“命令式”程式語言,和它類似的還有 Java、C#、Go 等語言。

指令式程式設計(Imperative Programming)是這樣一種程式設計正規化:使用可以改變程式狀態的程式碼語句,描述程式應該如何執行。這種方式更關注計算機完成任務所需要執行的具體步驟。

下面我們來看一個例子。對於“從一個包含有指定數字的集合中,篩選出大於 7 的所有數字”這個需求,按照指令式程式設計的思路,我們需要透過程式語言來告訴計算機具體的執行步驟。

以 C 語言為例,解決這個需求的步驟可能會是這樣:

1。使用陣列,構造一塊可以存放這些數字的記憶體空間;

2。使用迴圈控制語句,依次檢查記憶體中的這些數字是否滿足要求(即大於 7);

3。對於滿足要求的數字,將它們複製到新的記憶體空間中,暫存為結果。

對應的程式碼可能如下所示:

#define ARR_LEN 5

int main(void) {

int arr[ARR_LEN] = { 1, 5, 10, 9, 0 };

for (int i = 0; i < ARR_LEN; ++i) {

if (arr[i] > 7) {

// save this element somewhere else。 }

} return 0;

}

相對於指令式程式設計語言,其他語言一般會被歸類為“宣告式”程式語言。宣告式程式設計(Declarative Programming)也是一種常見的程式設計正規化。不同的是,這種正規化更傾向於表達計算的邏輯,而非解決問題時計算機需要執行的具體步驟。

比如說,還是剛才那個需求,在使用宣告式程式語言時,對應的解決步驟可能是:

1。構建一個容器來存放資料;

2。按照條件對容器資料進行篩選,並將符合條件的資料作為結果返回。

如果以 JavaScript 為例,對應的程式碼可能如下所示:

let arr = [1, 5, 10, 9, 0]

let result = arr。filter(n => n > 7)

可以看到的是,相較於指令式程式設計,宣告式程式設計更傾向於表達在解決問題時應該做什麼(構建容器、篩選),而不是具體怎麼做(分配記憶體、遍歷、複製)。

通常來說,指令式程式設計語言和宣告式程式語言的差異,主要體現在兩者的語言特性相較於計算機指令集的抽象程度。其中,指令式程式設計語言的抽象程度更低,這意味著該類語言的語法結構可以直接由相應的機器指令來實現。而宣告式程式語言的抽象程度更高,這類語言更傾向於以敘事的方式來描述程式邏輯,開發者無需關心語言背後在機器指令層面的實現細節。兩種語言在使用上各有其適用場景,並無孰好孰壞之分。

C 程式的編譯和執行

編寫完一段 C 程式碼,接下來的步驟就是對這段程式碼進行編譯了。在執行編譯命令時,為了保證程式的健壯性,我們一般會同時附帶引數 “-Wall”,讓編譯器明確指出程式程式碼中存在的所有語法使用不恰當的地方。

如果將那段用來回顧核心語法的 C 程式碼存放在名為 “demo。c” 的檔案中,那我們可以使用下面這行命令來編譯並執行這個程式:

gcc demo。c -o demo -Wall && 。/demo

一般來說,C 程式碼的完整編譯過程可以分為如下四個階段:

C程式從頭到尾的過程

1。程式碼預處理:編譯器會首先移除原始碼中的所有註釋資訊,並處理所有宏指令。其中包括進行宏展開、宏替換,以及條件編譯等。

2。編譯最佳化:編譯器會分析和最佳化原始碼,並將其編譯成對應的彙編格式程式碼,這部分程式碼中含有使用匯編指令描述的原始 C 程式邏輯。

3。彙編:編譯器會將這些彙編程式碼編譯成具有一定格式,可以被作業系統使用的某種物件檔案格式。

4。連結:透過連結處理,編譯器會將所有程式目前需要的物件檔案進行整合、設定好程式中所有呼叫函式的正確地址,並生成對應的二進位制可執行檔案。編譯結束後,我們就得到了可以直接執行的二進位制檔案。在不同的作業系統上,你可以透過不同的方式來執行這個程式,比如雙擊或透過命令列。

總結

到這裡,這篇的內容也就基本結束了,最後我來給你總結一下。

我們透過一個例項,帶你快速回顧了 C 語言的一些重要語法特性。為了方便你複習,我把這些涉及到的核心語法特性總結成了一張表格:

C程式從頭到尾的過程

我還帶你回顧了一個 C 程式的完整生命週期:程式碼編寫、編譯、執行。其中,C 程式碼的完整編譯過程可以分為程式碼預處理、編譯最佳化、彙編、連結四個階段。程式的彙編、連結與執行,都會涉及與所在作業系統相關的一系列精細處理過程。

除此之外,我們還從語言本身的角度,探討了 C 語言與其他程式語言的不同之處。C 語言作為一種指令式程式設計語言,抽象程度更低,語法結構可以直接由相應的機器指令經過簡單的組合來實現。

Top