關於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 語言程序,我是 mac 電腦,所以我使用的是xcode進行開發,我覺得工具無所謂大家用着順手就行。


 

你可能不知道這段代碼是什么意思,不過別着急,我們先運行一下看看結果。


 

這段程序輸出了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 編譯器驅動從源文件讀取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 的縮寫,在操作系統中表示資料源無更多的資料可讀取。資料源通常稱為檔案或串流。通常在文本的最后存在此字符表示資料結束。

C 標准庫的errno.h頭文件定義了整數變量errno,它是通過系統調用設置的,這些庫函數表明了什么發生了錯誤。

C 標准庫的float.h頭文件包含了一組與浮點值相關的依賴於平台的常量。

✿ limits.h頭文件決定了各種變量類型的各種屬性。定義在該頭文件中的宏限制了各種變量類型(比如 char、int 和 long)的值。

✿ locale.h頭文件定義了特定地域的設置,比如日期格式和貨幣符號

✿ math.h頭文件定義了各種數學函數和一個宏。在這個庫中所有可用的功能都帶有一個double類型的參數,且都返回double類型的結果。

✿ setjmp.h頭文件定義了宏setjmp()、函數longjmp()和變量類型jmp_buf,該變量類型會繞過正常的函數調用和返回規則。

✿ signal.h頭文件定義了一個變量類型sig_atomic_t、兩個函數調用和一些宏來處理程序執行期間報告的不同信號。

✿ stdarg.h頭文件定義了一個變量類型va_list和三個宏,這三個宏可用於在參數個數未知(即參數個數可變)時獲取函數中的參數。

✿ stddef .h頭文件定義了各種變量類型和宏。這些定義中的大部分也出現在其它頭文件中。

✿ stdlib .h頭文件定義了四個變量類型、一些宏和各種通用工具函數。

✿ string .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 語言的重要性;

之后我們以一道 C 語言的入門程序講起,我們講了 C 語言的基本構成要素,C 語言在硬件上是如何運行的,C 語言的編譯過程和執行過程等;

在這之后我們又加深講解了一下入門例子程序的組成特征。


 

如果你想更深入的了解——編程俱樂部【下圖進入】!

涉及到:C語言、C++、windows編程、網絡編程、QT界面開發、Linux編程、游戲編程、黑客等等......


 

程序員編程入門資料:


 

程序員​推薦學習書籍:


 

帶你一個活躍、高逼格、高層次的程序員編程學習殿堂;編程入門只是順帶,思維的提高才有價值!

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM