跳轉到內容

ANSI C 與 Unix

25% developed
來自華夏公益教科書

Unix 和 C 的歷史

[編輯 | 編輯原始碼]

為什麼 C 很重要

[編輯 | 編輯原始碼]

C 的侷限性

[編輯 | 編輯原始碼]

目標受眾

[編輯 | 編輯原始碼]

75% 開發完成  截至 2007 年 1 月 10 日 01:36 (UTC) (2007 年 1 月 10 日 01:36 (UTC))

本書旨在用作大學二年級教科書。對主題內容的涵蓋確保讀者對高階語言有基本瞭解,並熟悉合理現代的程式開發環境。本書的目的是將那些已經熟悉程式設計的人介紹給更面向系統級的語言,這種語言為程式設計師提供了更大的自由度(因此也帶來了更大的責任)。

我們的本科計算機科學課程要求我們的學生學習至少三種高階語言。學位課程的前兩個學期使用 Java。本書所針對的課程緊隨其後,在二年級早期開設。本書假設學生已經成功完成了前兩門課程,因此本書不需要過多關注一般程式結構、流程控制或語法。在提及結構、語法和流程控制時,重點應放在 C 與 Java 的區別上。

開發本書的理由是為學生提供一種方式

  • 貢獻
  • 協作
  • 評論
  • 透過解釋和教學來學習 在暑假期間,我們作為一個團隊將能夠確定 a) 這本書本身是否有價值,因此應該更持久 b) 是否有一些值得保留的成分可以合併到現有的華夏公益教科書中,還是 c) 應該被丟棄,視為一個有趣但徒勞的實驗。:-)

    Bourne Shell 介面

    [編輯 | 編輯原始碼]
    [編輯 | 編輯原始碼]

    用佔位符代替。這些可能都是不同的頁面

  • 從常用發行版呼叫 shell
  • Unix/Linux 之間的差異
  • 目錄導航
  • 基本檔案管理
  • 許可權管理
  • 透過 emacs、vi/vim、joe 等進行基本文字編輯

    Unix 實用程式

    [編輯 | 編輯原始碼]

    指令碼概念

    [編輯 | 編輯原始碼]

    Make 和 Makefile

    [編輯 | 編輯原始碼]

    版本控制

    [編輯 | 編輯原始碼]

    使用 gcc 進行命令列編譯

    [編輯 | 編輯原始碼]

    編譯單個原始碼的“C”程式

    [編輯 | 編輯原始碼]

    最簡單的編譯情況是當您的所有原始碼都設定在一個檔案中時。這將消除任何不必要的同步多個檔案或過度思考的步驟。假設有一個名為 'single_main.c' 的檔案,我們要編譯它。我們將使用類似於以下的命令列來進行編譯

    cc single_main.c
    

    請注意,我們假設編譯器名為“cc”。如果你使用 GNU 編譯器,你需要寫 'gcc'。如果你使用 Solaris 系統,你可能需要使用 'acc',等等。每個編譯器可能以不同的方式顯示其訊息(錯誤、警告等),但在所有情況下,如果編譯成功完成,你都會得到一個名為 'a.out' 的檔案。注意,一些較舊的系統(例如 SunOs)帶有不理解 ANSI-C 的 C 編譯器,而是使用較舊的 'K&R' C 風格。在這種情況下,你需要使用 gcc(希望它已安裝),或者學習 ANSI-C 和 K&R C 之間的區別(如果你沒有必要,不建議這樣做),或者遷移到其他系統。

    你可能會抱怨 'a.out' 是一個過於通用的名稱(它究竟從哪裡來?- 嗯,這是一個歷史名稱,由於在較舊的 Unix 系統上編譯的程式使用了名為“a.out 格式”的東西)。假設你希望生成的程式名為“single_main”。在這種情況下,你可以使用以下命令列來編譯它

    cc single_main.c -o single_main
    </source
    
    Every compiler I've met so far (including the glorious gcc) recognized the '-o' flag as "name the resulting executable file 'single_main'".
    
    == Running The Resulting Program ==
    
    Once we created the program, we wish to run it. This is usually done by simply typing its name, as in:
    
    <syntaxhighlight lang=shell>
    single_main
    

    但是,這要求當前目錄位於我們的 PATH 中(這是一個告訴我們的 Unix shell 在哪裡查詢我們試圖執行的程式的變數)。在許多情況下,此目錄不會被放置在我們的 PATH 中。啊哈!- 我們說。然後讓我們向這臺計算機展示誰更聰明,因此我們嘗試

    ./single_main
    

    這一次我們明確地告訴我們的 Unix shell 我們想要從當前目錄執行程式。如果我們足夠幸運,這將足夠了。然而,還有一個障礙可能會阻礙我們的路徑——檔案許可權標誌。

    當系統中建立一個檔案時,它會立即被賦予一些訪問許可權標誌。這些標誌告訴系統誰應該被授予訪問檔案的許可權,以及將給予他們的哪種訪問許可權。傳統的 Unix 系統使用 3 種類型的實體來授予(或拒絕)訪問許可權:擁有該檔案的使用者,擁有該檔案的組,以及所有其他使用者。每個實體都可能被授予讀取檔案 ('r')、寫入檔案 ('w') 和執行檔案 ('x') 的許可權。現在,當編譯器為我們建立程式檔案時,我們成為該檔案的擁有者。通常,編譯器會確保我們獲得該檔案的所有許可權——讀取、寫入和執行。然而,可能發生了一些錯誤,許可權設定不同。在這種情況下,我們可以使用類似以下的命令來正確設定檔案的許可權(檔案的所有者通常可以更改檔案的許可權標誌)

    chmod u+rwx single_main
    

    這意味著“使用者 ('u') 應該被授予 ('+') 讀取 ('r')、寫入 ('w') 和執行 ('x') 對檔案 'single_main' 的許可權。現在我們肯定能夠執行我們的程式了。同樣,通常你不會在執行該檔案時遇到任何問題,但如果你將其複製到不同的目錄,或將其透過網路傳輸到不同的計算機,它可能會失去其原始許可權,因此你需要像上面所示那樣正確設定許可權。還要注意,你不能只將檔案移動到另一臺計算機並期望它執行——它必須是一臺具有匹配的作業系統(以理解可執行檔案格式)和匹配的 CPU 架構(以理解可執行檔案包含的機器語言程式碼)的計算機。

    最後,執行時環境必須匹配。例如,如果我們在具有一個版本的標準 C 庫的作業系統上編譯了程式,並且我們嘗試在具有不相容的標準 C 庫的版本上執行它,程式可能會崩潰,或者抱怨它找不到相關的 C 庫。對於快速發展的系統(例如,使用 libc5 的 Linux 與使用 libc6 的 Linux)來說尤其如此,因此請注意。

    建立除錯準備就緒的程式碼

    [edit | edit source]

    通常,當我們編寫程式時,我們希望能夠除錯它——也就是說,使用偵錯程式測試它,該偵錯程式允許我們逐步執行程式,在執行給定命令之前設定斷點,檢視程式執行期間變數的內容,等等。為了使偵錯程式能夠在可執行程式和原始原始碼之間建立關係,我們需要告訴編譯器將資訊插入到生成的可執行程式中,這些資訊將幫助偵錯程式。此資訊稱為“除錯資訊”。為了將它新增到我們的程式中,讓我們以不同的方式編譯它

    cc -g single_main.c -o single_main
    

    '-g' 標誌告訴編譯器使用除錯資訊,並且大多數編譯器都識別它。你會注意到,生成的程式檔案比沒有使用 '-g' 標誌建立的檔案大得多。尺寸差異是由於除錯資訊造成的。我們仍然可以使用 strip 命令刪除此除錯資訊,如下所示

    strip single_main
    

    你會注意到,現在檔案的大小甚至比我們最初沒有使用 '-g' 標誌時還要小。這是因為即使沒有使用 '-g' 標誌編譯的程式也包含一些符號資訊(例如函式名稱),strip 命令會刪除這些資訊。你可能需要閱讀 strip 的手冊頁(man strip)以瞭解有關此命令功能的更多資訊。

    建立最佳化程式碼

    [edit | edit source]

    在建立程式並正確除錯之後,我們通常希望將其編譯成高效的程式碼,並且生成的程式檔案儘可能小。編譯器可以透過最佳化程式碼來幫助我們,可以最佳化速度(更快地執行),也可以最佳化空間(佔用更小的空間),也可以兩者兼顧。建立最佳化程式的基本方法如下

    cc -O single_main.c -o single_main
    

    '-O' 標誌告訴編譯器最佳化程式碼。這也意味著編譯將花費更長時間,因為編譯器嘗試將各種最佳化演算法應用於程式碼。這種最佳化應該是保守的,因為它確保我們程式碼仍然能夠執行與沒有最佳化時相同的函式(嗯,除非我們的編譯器存在錯誤)。通常,我們可以透過在 '-O' 標誌後新增一個數字來定義最佳化級別。數字越大,生成的程式越最佳化,編譯器完成編譯的速度越慢。需要注意的是,由於最佳化以多種方式改變了程式碼,因此隨著我們提高程式碼的最佳化級別,非正常最佳化實際上改變我們程式碼的可能性會更高,因為其中一些最佳化往往是非保守的,或者僅僅是相當複雜的,並且包含錯誤。例如,長期以來,眾所周知,使用 gcc 時,使用高於 2 的編譯級別會導致可執行程式中的錯誤。在收到警告後,如果我們仍然想使用不同的最佳化級別(例如 4),我們可以這樣做

    cc -O4 single_compile.c -o single_compile
    

    我們已經完成了。如果你閱讀編譯器的手冊頁,你很快就會注意到它支援幾乎無限數量的與最佳化相關的命令列選項。要正確使用它們,需要透徹瞭解編譯理論和原始碼最佳化理論,否則你可能會破壞生成的程式碼。一個好的編譯理論課程(最好基於 Aho、Sethi 和 Ulman 撰寫的“龍書”)對你會有幫助。

    獲取額外的編譯器警告

    [edit | edit source]

    通常,編譯器只生成有關不符合 C 標準的錯誤程式碼的錯誤訊息,以及有關通常會導致執行時錯誤的事物的警告。但是,我們通常可以指示編譯器給我們更多警告,這有助於提高我們原始碼的質量,並發現將來會困擾我們的錯誤。對於 gcc,這可以透過使用 '-W' 標誌來完成。例如,要讓編譯器使用它熟悉的所有型別的警告,我們將使用類似以下的命令列

    cc -Wall single_source.c -o single_source
    

    這會首先讓我們感到厭煩——我們會收到各種似乎無關緊要的警告。但是,消除警告比消除使用此標誌要好。通常,此選項可以為我們節省更多時間,而不是浪費時間,並且如果始終如一地使用,我們將習慣於編寫正確的程式碼,而無需過多考慮。還需要注意的是,一些在某個體系結構上使用一個編譯器可以正常工作的程式碼,如果我們使用不同的編譯器或不同的系統來編譯程式碼,可能會中斷。在第一個系統上開發時,我們永遠不會看到這些錯誤,但在將程式碼遷移到其他平臺時,錯誤會突然出現。此外,在許多情況下,我們最終會希望將程式碼遷移到新系統,即使我們最初沒有這種意圖。

    請注意,有時 '-Wall' 會給你太多錯誤,然後你可以嘗試使用一些更簡潔的警告級別。閱讀編譯器的手冊以瞭解各種 '-W' 選項,並使用那些對你最有益的選項。最初它們聽起來可能太奇怪了,毫無意義,但如果你是一位(或將來會成為)更有經驗的程式設計師,你會了解哪些選項對你很有用。

    編譯單個源“C++”程式

    [edit | edit source]

    現在我們已經瞭解瞭如何編譯 C 程式,向 C++ 程式的過渡相當簡單。我們所要做的就是使用 C++ 編譯器,代替我們迄今為止使用的 C 編譯器。因此,如果我們的程式原始碼儲存在一個名為 'single_main.cc' 的檔案中('cc' 表示 C++ 程式碼。一些程式設計師更喜歡使用 'C' 字尾表示 C++ 程式碼),我們將使用類似以下的命令

    g++ single_main.cc -o single_main
    

    或者在某些系統上,你將使用“CC”而不是“g++”(例如,使用 Sun 的 Solaris 編譯器),或者使用“aCC”(HP 的編譯器),等等。你會注意到,對於 C++ 編譯器,命令列選項的統一性較差,部分原因是直到最近,該語言還在不斷發展,沒有達成一致的標準。但無論如何,至少對於 g++,你將使用“-g”表示程式碼中的除錯資訊,使用“-O”表示最佳化。

    編譯多個源“C”程式

    [編輯 | 編輯原始碼]

    所以你已經學會了如何正確地編譯單源程式(希望你現在已經玩過一些編譯器,並嘗試過一些你自己的例子)。然而,遲早你會發現,將所有原始碼放在一個檔案中是相當有限的,原因有幾個

    • 隨著檔案大小的增長,編譯時間也會隨之增長,而且對於每一個小改動,整個程式都需要重新編譯。
    • 以這種方式,幾個人一起在一個專案上工作是很難的,如果不是不可能的話。
    • 管理你的程式碼變得更加困難。撤銷錯誤的更改幾乎是不可能的。

    解決這個問題的方法是將原始碼分成多個檔案,每個檔案包含一組緊密相關的函式(或者,在C++中,包含單個類的所有原始碼)。

    編譯多源C程式有兩種可能的方法。第一種是使用單個命令列編譯所有檔案。假設我們有一個程式,它的原始碼位於檔案“main.c”,“a.c”和“b.c”中(位於本教程的“multi-source”目錄中)。我們可以這樣編譯它

    cc main.c a.c b.c -o hello_world
    

    這將導致編譯器分別編譯每個給定的檔案,然後將它們全部連結到一個名為“hello_world”的可執行檔案中。關於這個程式有兩個註釋

    1. 如果我們在一個檔案中定義一個函式(或一個變數),並試圖從第二個檔案中訪問它們,我們需要在第二個檔案中將它們宣告為外部符號。這是使用C的“extern”關鍵字完成的。
    2. 命令列中呈現原始檔的順序可以改變。編譯器(實際上是連結器)將知道如何從每個檔案中獲取相關程式碼到最終程式中,即使第一個原始檔試圖使用在第二個或第三個原始檔中定義的函式。

    這種編譯方式的問題是,即使我們只在一個原始檔中進行更改,當我們再次執行編譯器時,所有檔案都將被重新編譯。

    為了克服這個限制,我們可以將編譯過程分為兩個階段——編譯和連結。讓我們先看看這是如何完成的,然後解釋

    cc -c main.cc
    cc -c a.c
    cc -c b.c
    cc main.o a.o b.o -o hello_world
    

    前3個命令分別接受一個原始檔,並將其編譯成一個名為“目標檔案”的東西,具有相同的名稱,但帶有“.o”字尾。是“-c”標誌告訴編譯器只建立一個目標檔案,而不是現在就生成最終的可執行檔案。目標檔案包含原始檔的機器語言程式碼,但有一些未解析的符號。例如,“main.o”檔案引用了一個名為“func_a”的符號,該符號是檔案“a.c”中定義的函式。

    當然,我們不能像那樣執行程式碼。因此,在建立3個目標檔案之後,我們使用第4個命令將3個目標檔案連結到一個程式中。連結器(現在由編譯器呼叫)從3個目標檔案中獲取所有符號,並將它們連結在一起——它確保當從目標檔案“main.o”中的程式碼呼叫“func_a”時,目標檔案“a.o”中的函式程式碼被執行。此外,連結器還將標準C庫連結到程式中,在本例中,是為了正確解析“printf”符號。

    為了瞭解這種複雜性實際上對我們有幫助,我們應該注意到,通常連結階段比編譯階段快得多。當進行最佳化時,這一點尤其明顯,因為這一步是在連結之前完成的。現在,假設我們更改原始檔“a.c”,並且我們想要重新編譯該程式。我們現在只需要兩個命令

    cc -c a.c
    cc main.o a.o b.o -o hello_world
    

    在我們的小例子中,很難注意到速度的提升,但在有幾十個檔案的情況下,每個檔案包含幾百行原始碼,節省的時間是相當可觀的;更不用說更大的專案了。

    深入瞭解——編譯步驟

    [編輯 | 編輯原始碼]

    現在我們已經瞭解到編譯不僅僅是一個簡單的過程,讓我們試著看看為了編譯一個C程式,編譯器所採取的完整步驟列表。1. 驅動程式——我們呼叫“cc”的東西。這實際上是“引擎”,它驅動著編譯器所包含的整個工具集。我們呼叫它,它開始逐個呼叫其他工具,並將每個工具的輸出作為下一個工具的輸入。2. C預處理器——通常稱為“cpp”。它接受一個C原始檔,並處理所有預處理器定義(#include檔案,#define宏,使用#ifdef的條件原始碼包含等)。你可以單獨呼叫它對你的程式進行處理,通常使用類似的命令

    cc -E single_source.c
    

    嘗試一下,看看生成的程式碼是什麼樣的。3. C編譯器——通常稱為“cc1”。這是真正的編譯器,它將輸入檔案轉換成組合語言。如你所見,我們使用了“-c”標誌來呼叫它,以及C預處理器(以及可能還有最佳化器,請繼續閱讀),以及彙編器。4. 最佳化器——有時作為單獨的模組提供,有時作為編譯器模組的一部分。它處理對程式碼表示的最佳化,該程式碼表示是語言無關的。這樣,你就可以對不同程式語言的編譯器使用相同的最佳化器。5. 彙編器——有時稱為“as”。它接受編譯器生成的彙編程式碼,並將它轉換成儲存在目標檔案中的機器語言程式碼。使用gcc,你可以告訴驅動程式只生成彙編程式碼,使用類似的命令

    cc -S single_source.c
    

    6. 連結器-載入器——這個工具接受所有目標檔案(以及C庫),並將它們連結在一起,形成一個可執行檔案,該檔案採用作業系統支援的格式。目前常見的格式稱為“ELF”。在SunOs系統和其他舊系統中,使用了一種名為“a.out”的格式。這種格式定義了可執行檔案的內部結構——資料段的位置,原始碼段的位置,除錯資訊的位置等等。如你所見,編譯被分成許多不同的階段。並非所有編譯器都採用完全相同的階段,有時(例如,對於C++編譯器)情況甚至更復雜。但基本思路是相同的——將編譯器分成許多不同的部分,以便給程式設計師提供更大的靈活性,並允許編譯器開發者在不同的編譯器中儘可能多地重用模組,用於不同的語言(透過替換預處理器和編譯器模組),或用於不同的架構(透過替換匯編器和連結器-載入器部分)。

    C程式結構和一般語法

    [編輯 | 編輯原始碼]

    C預處理器

    [編輯 | 編輯原始碼]

    C程式結構

    [編輯 | 編輯原始碼]

    存根

    需要在這裡包含

  • 預處理器指令,特別是常量、標頭檔案 #ifdef 等。
  • 註釋
  • 帶有命令列引數的main函式
  • 函式原型
  • 常量和變數宣告
  • 自主程式碼塊
  • 函式

    基本資料型別和識別符號

    [編輯 | 編輯原始碼]

    C運算子

    [編輯 | 編輯原始碼]

    流程控制

    [編輯 | 編輯原始碼]

    陣列表示法

    [編輯 | 編輯原始碼]

    指標和定址

    [編輯 | 編輯原始碼]

    取地址運算子

    [編輯 | 編輯原始碼]

    指標表示法

    [編輯 | 編輯原始碼]

    指標運算

    [編輯 | 編輯原始碼]

    指標和陣列的關係

    [編輯 | 編輯原始碼]

    位操作

    [編輯 | 編輯原始碼]

    位運算子

    [編輯 | 編輯原始碼]

    移位運算子

    [編輯 | 編輯原始碼]

    I/O、流和流管理

    [編輯 | 編輯原始碼]

    結構、聯合和ADT

    [編輯 | 編輯原始碼]

    抽象資料型別(ADT)

    [編輯 | 編輯原始碼]

    動態資料結構

    [編輯 | 編輯原始碼]

    程序間通訊

    [編輯 | 編輯原始碼]

    資料庫連線

    [編輯 | 編輯原始碼]

    貢獻者

    [編輯 | 編輯原始碼]

    Petercooper 貢獻了這本書的初始結構。 Raquibul Islam (Rana) 貢獻了使用 gcc 的命令列編譯部分。

  • 華夏公益教科書