做者 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)。
轉載請註明出處。
原文: https://www.jianshu.com/p/207...《C語言探索之旅》全系列程序員
上一課是 C語言探索之旅 | 第一部分練習題 。編程
話說上一課是第一部分最後一課,如今開始第二部分的探索之旅!小程序
在這一部分中,咱們會學習 C語言的高級技術。這一部份內容將是一座高峯,會挺難的,可是咱們一塊兒翻越。安全
俗語說得好:「一口是吃不成一個胖子的。」微信
可是一小口一小口,慢慢吃,仍是能吃成胖子的嘛。因此要細水長流,肥油慢積,一路上有你(「油膩」)~編輯器
一旦你跟着咱們的課程一直學到這一部分的結束,你將會掌握 C語言的核心技術,也能夠理解大部分 C語言寫的程序了。模塊化
到目前爲止咱們的程序都只是在一個 main.c 文件裏搗騰,由於咱們的程序還很短小,這也足夠了。函數
但若是以後你的程序有了十多個函數,甚至上百個函數,那麼你就會感到所有放在一個 main.c 文件裏是多麼擁擠和混亂。學習
正由於如此,計算機科學家纔想出了模塊化編程。原則很簡單:與其把全部源代碼都放在一個 main.c 當中,咱們將把它們合理地分割,放到不一樣的文件裏面。測試
到目前爲止,寫自定義函數的時候,咱們都要求你們暫時把函數寫在 main 函數的前面。
這是爲何呢?
由於這裏的順序是一個重要的問題。若是你將本身定義的函數放置在 main 函數以前,電腦會讀到它,就會「知道」這個函數。當你在 main 函數中調用這個函數時,電腦已經知道這個函數,也知道到哪裏去執行它。
可是假如你把這個函數寫在 main 函數後面,那你在 main 函數裏調用這個函數的時候,電腦還不「認識」它呢。你能夠本身寫個程序測試一下。是的,很奇怪對吧?這絕對有點任性的。
那你會說:「C語言豈不是設計得很差麼?」
我「徹底」贊成(可別讓 C語言之父 Dennis Ritchie 聽到了...)。可是請相信,這樣設計應該也是有理由的。計算機先驅們早就想到了,也提出瞭解決之道。
下面咱們就來學一個新的知識點,藉着這個技術,你能夠把你的自定義函數放在程序的任意位置。
咱們會聲明咱們的函數,須要用到一個專門的技術:函數原型,英語是 function prototype。function 表示「函數」,prototype 表示「原型,樣本,模範」。
就比如你對電腦發出一個通知:「看,個人函數的原型在這裏,你給我記住啦!」
咱們來看一下上一課舉的一個函數的例子(計算矩形面積):
double rectangleArea(double length, double width) { return length * width; }
怎麼來聲明咱們上面這個函數的原型呢?
;
)。很簡單吧?如今你就能夠把你的函數的定義放在 main 函數後面啦,電腦也會認識它,由於你在 main 函數前面已經聲明過這個函數了。
你的程序會變成這樣:
#include <stdio.h> #include <stdlib.h> // 下面這一行是 rectangleArea 函數的函數原型 double rectangleArea(double length, double width); int main(int argc, char *argv[]) { printf("長爲 10,寬爲 5 的矩形面積 = %f\n", rectangleArea(10, 5)); printf("長爲 3.5,寬爲 2.5 的矩形面積 = %f\n", rectangleArea(3.5, 2.5)); printf("長爲 9.7,寬爲 4.2 的矩形面積 = %f\n", rectangleArea(9.7, 4.2)); return 0; } // 如今咱們的 rectangleArea 函數就能夠放置在程序的任意位置了 double rectangleArea(double length, double width) { return length * width; }
與原先的程序相比有什麼改變呢?
其實就是在程序的開頭加了函數的原型而已(記得不要忘了那個分號)。
函數的原型,實際上是給電腦的一個提示或指示。好比上面的程序中,函數原型
double rectangleArea(double length, double width);
就是對電腦說:「老兄,存在一個函數,它的輸入是哪幾個參數,輸出是什麼類型」,這樣就能讓電腦更好地管理。
多虧了這一行代碼,如今你的 rectangleArea 函數能夠置於程序的任何位置了。
記得:最好養成習慣,對於 C語言程序,老是定義了函數,再寫一下函數的原型。
那麼不寫函數原型行不行呢?
也行。只要你把每一個函數的定義都放在 main 函數以前。可是你的程序慢慢會愈來愈大,等你有幾十或者幾百個函數的時候,你還顧得過來麼?
因此養成好習慣,不吃虧的。
你也許注意到了,main 函數沒有函數原型。由於不須要,main 函數是每一個 C程序必須的入口函數。人家 main 函數「有權任性」,跟編譯器關係好,編譯器對 main 函數很熟悉,是常常打交道的「哥們」,因此不須要函數原型來「介紹」 main 函數。
還有一點,在寫函數原型的時候,對於圓括號裏的函數參數,名字是不必定要寫的,能夠只寫類型。
由於函數原型只是給電腦作個介紹,因此電腦只須要知道輸入的參數是什麼類型就夠了,不須要知道名字。因此咱們以上的函數原型也能夠簡寫以下:
double rectangleArea(double, double);
看到了嗎,咱們能夠省略 length 和 width 這兩個變量名,只保留 double(雙精度浮點型)這個類型名字。
千萬不要忘了函數原型末尾的分號,由於這是編譯器區分函數原型和函數定義開頭的重要指標。若是沒有分號,編譯時會出現比較難理解的錯誤提示。
頭文件在英語中是 header file。header 表示「數據頭,頁眉」,file 表示「文件」。
每次看到這個術語,我都想到已經結婚的「咱們的青春」:周杰倫 的《頭文字D》。
到目前爲止,咱們的程序只有一個 .c 文件(被稱爲「源文件」,在英語中是 source file。source 表示「源,源頭,水源」),好比咱們以前把這個 .c 文件命名爲 main.c。固然名字是無所謂的,起名爲hello.c,haha.c 都行。
在實際編寫程序的時候,你的項目通常確定不會把代碼都寫在一個 main.c 文件中。固然,也不是不能夠。
可是,試想一下,若是你把全部代碼都塞到這一個 main.c 文件中,那若是代碼量達到 10000 行甚至更多,你要在裏面找一個東西就太難了。也正是由於這樣,一般咱們每個項目都會建立多個文件。
那以上說到的項目是指什麼呢?
以前咱們用 CodeBlocks 這個 IDE 建立第一個 C語言項目的時候,其實已經接觸過了。
一個項目(英語是 project),簡單來講是指你的程序的全部源代碼(還有一些其餘的文件),項目裏面的文件有多種類型。
目前咱們的項目還只有一個源文件:main.c 。
看一下你的 IDE,通常來講項目是列在左邊。
如上圖,你能夠看到,這個項目(在 Projects 一欄裏)只有一個文件:main.c 。
如今咱們再來展現一個包含好多個文件的項目:
上圖中,咱們能夠看到在這個項目裏有好幾個文件。實際中的項目大可能是這樣的。你看到那個 main.c 文件了嗎?一般來講在咱們的程序中,會把 main 函數只定義在 main.c 當中。
固然也不是非要這樣,每一個人都有本身的編程風格。不過但願跟着這個課程學習的讀者,能夠和咱們保持一致的風格,方便理解。
那你又要問了:「爲何建立多個文件呢?我怎麼知道爲項目建立幾個文件合適呢?」
答案是:這是你的選擇。一般來講,咱們把同一主題的函數放在一個文件裏。
在上圖中,咱們能夠看到有兩種類型的文件:一種是以 .h 結尾的,一種是以 .c 結尾的。
因此,一般來講咱們不常把函數原型放在 .c 文件中,而是放在 .h 文件中,除非你的程序很小。
對每一個 .c 文件,都有同名的 .h 文件。上面的項目那個圖中,你能夠看到 .h 和 .c 文件一一對應。
但咱們的電腦怎麼知道函數原型是在 .c 文件以外的另外一種文件裏呢?
須要用到咱們以前介紹過的預處理指令 #include
來將其引入到 .c 文件中。
請作好準備,下面將有一波密集的知識點「來襲」。
怎麼引入一個頭文件呢?其實你已經知道怎麼作了,以前的課程咱們已經寫過了。
好比咱們來看咱們上面的 game.c 文件的開頭
#include <stdlib.h> #include <stdio.h> #include "game.h" void player(SDL_Surface* screen) { // ... }
看到了嗎,其實你早就熟悉了,要引入頭文件,只須要用 #include 這個預處理指令。
所以咱們在 game.c 源文件中一共引入了三個頭文件:stdlib.h, stdio.h,game.h。
注意到一個不一樣點了嗎?
在標準庫的頭文件(stdlib.h,stdio.h)和你本身定義的頭文件(game.h)的引入方式是有點區別的:
<>
用於引入標準庫的頭文件。對於 IDE,這些頭文件通常位於 IDE 安裝目錄的 include 文件夾中;在 Linux 操做系統下,則通常位於系統的 include 文件夾裏。""
用於引入自定義的頭文件。這些頭文件位於你本身的項目的目錄中。咱們再來看一下對應的 game.h 這個頭文件的內容:
看到了嗎,.h 文件中存放的是函數原型。
你已經對一個項目有大體概念了。
那你又會問了:「爲何要這樣安排呢?把函數原型放在 .h 頭文件中,在 .c 源文件中用 #include 引入。爲何不把函數原型寫在 .c 文件中呢?」
答案是:方便管理,條理清晰,不容易出錯,省心。
由於如前所述,你的電腦在調用一個函數前必須先「知道」這個函數,咱們須要函數原型來讓使用這個函數的其餘函數預先知道。
若是用了 .h 頭文件的管理方法,在每個 .c 文件開頭只要用 #include 這個指令來引入頭文件的全部內容,那麼頭文件中聲明的全部函數原型都被當前 .c 文件所知道了,你就不用再操心那些函數的定義順序或者有沒有被其餘函數知道
例如個人 main.c 函數要使用 functions.c 文件中的函數,那我只要在 main.c 的開頭寫 #include "functions.h"
,以後我在 main.c 函數中就能夠調用 function.c 中定義的函數了。
你可能又要問了:「那我怎麼在項目中加入新的 .h 和 .c 文件呢?」
很簡單,在 CodeBlocks 裏,鼠標右鍵點擊項目列表的主菜單處,選擇 Add Files,或者在菜單欄上依次單擊 File -> New -> File... ,就能夠選擇添加文件的類型了。
你腦海裏確定出現一個問題:
若是咱們用 #include 來引入 stdio.h 和 stdlib.h 這樣的標準庫的頭文件,而這些文件又不是我本身寫的,那麼它們確定存在於電腦裏的某個地方,咱們能夠找到,對吧?
是的,徹底正確!
若是你使用的是 IDE(集成開發環境),那麼它們通常就在你的 IDE 的安裝目錄裏。
若是是在純 Linux 環境下,那就要到系統文件夾裏去找,這裏不討論了,感興趣的讀者能夠去網上搜索。
在個人狀況,由於安裝的是 CodeBlocks 這個 IDE,因此在 Windows下,個人頭文件們「隱藏」在這兩個路徑下:
C:\Program Files\CodeBlocks\MinGW\include
和
C:\Program Files\CodeBlocks\MinGW\x86_64-w64-mingw32\include
通常來講,都在一個叫作 include 的文件夾裏。
在裏面,你會找到不少文件,都是 .h 文件,也就是 C語言系統定義的標準頭文件,也就是系統庫的頭文件(對 Windows,macOS,Linux 都是通用的,C語言原本就是可移植的嘛)。
在這衆多的頭文件當中,你能夠找到咱們的老朋友:stdio.h 和 stdlib.h。
你能夠雙擊打開這些文件或者選擇你喜歡的文本編輯器來打開,不過也許你會嚇一跳,由於這些文件裏的內容不少,並且好些是咱們還沒學到的用法,好比除了 #include 之外的其餘的預處理指令。
你能夠看到這些頭文件中充滿了函數原型,好比你能夠在 stdio.h 中找到 printf 函數的原型。
你要問了:「OK,如今我已經知道標準庫的頭文件在哪裏了,那與之對應的標準庫的源文件(.c 文件)在哪裏呢?」
很差意思,你見不到它們啦。由於 .c 文件已經被事先編譯好,轉換成計算機能理解的二進制碼了。
「伊人已去,年華不復,吾將何去何從?」
既然見不到原先的它們了,至少讓我見一下「美圖秀秀」以後的它們吧…
能夠,你在一個叫 lib 的文件夾下面就能夠找到,在個人 Windows 下的路經爲:
C:\Program Files\CodeBlocks\MinGW\lib
和
C:\Program Files\CodeBlocks\MinGW\x86_64-w64-mingw32\lib
被編譯成二進制碼的 .c 文件,有了一個新的後綴名:.a(在 CodeBlocks 的狀況,它的編譯器是 MinGW。MinGW 簡單來講就是 GCC 編譯器的 Windows 版本)或者 .lib(在 Visual C++ 的狀況),等。這是靜態連接庫的狀況。
你在 Windows 中還能找到 .dll 結尾的動態連接庫;你在 Linux 中能找到 .so 結尾的動態連接庫。暫時咱們不深究靜態連接庫和動態連接庫,有興趣的讀者能夠去網上自行搜索。
這些被編譯以後的文件被叫作庫文件或 Library 文件(library 表示「庫,圖書館,文庫」),不要試着去閱讀這些文件的內容,由於是看不懂的亂碼。
學到這裏可能有點暈,不過繼續看下去就會漸漸明朗起來,下面的內容會有示意圖幫助理解。
小結一下:
在咱們的 .c 源文件中,咱們能夠用 #include 這個預處理指令來引入標準庫的 .h 頭文件或本身定義的頭文件。這樣咱們就能使用標準庫所定義的 printf 這樣的函數,電腦就認識了這些函數(藉着 .h 文件中的函數原型),就能夠檢驗你調用這些函數時有沒有用對,好比函數的參數個數,返回值類型,等。
如今咱們知道了一個項目是由若干文件組成的,那咱們就能夠來了解一下編譯器(compiler)的工做原理。
以前的課裏面展現的編譯示例圖是比較簡化的,下圖是一幅編譯原理的略微詳細的圖,但願你們用心理解並記住:
上圖將編譯時所發生的事情基本詳細展現了,咱們來仔細分析:
預處理指令有好多種,目前咱們學過的只有 #include
,它使咱們能夠在一個文件中引入另外一個文件的內容。#include 這個預處理指令也是最經常使用的。
預處理器會把 #include 所在的那一句話替換爲它所引入的頭文件的內容,好比
#include <stdio.h>
預處理器在執行時會把上面這句指令替換爲 stdio.h 文件的內容。因此到了編譯的時候,你的 .c 文件的內容會變多,包含了全部引入的頭文件的內容,顯得比較臃腫。
編譯器會把 .c 文件先轉換成 .o 文件(有的編譯器會生成 .obj 文件),.o 文件通常叫作目標文件(o 是 object 的首字母,表示「目標」),是臨時的二進制文件,會被用於以後生成最終的可執行二進制文件。
.o 文件通常會在編譯完成後被刪除(根據你的 IDE 的設置)。從某種程度上來講 .o 文件雖然是臨時中間文件,好像沒什麼大用,但保留着不刪除也是有好處:假如項目有 10 個 .c 文件,編譯後生成了 10 個 .o 文件。以後你只修改了其中的一個 .c 文件,若是從新編譯,那麼編譯器不會爲其餘 9 個 .c 文件從新生成 .o 文件,只會從新生成你更改的那個。這樣能夠節省資源。
如今你知道從代碼到生成一個可執行程序的內部原理了吧,下面咱們要展現給你們的這張圖,很重要,但願你們理解並記住。
大部分的錯誤都會在編譯階段被顯示,但也有一些是在連接的時候顯示,有多是少了 .o 文件之類。
以前那幅圖其實還不夠完整,你可能想到了:咱們用 .h 文件引入了標準庫的頭文件的內容(裏面主要是函數原型),函數的具體實現的代碼咱們還沒引入呢,怎麼辦呢?
對了,就是以前提到過的 .a 或 .lib 這樣的庫文件(由標準庫的 .c 源文件編譯而成)。
因此咱們的連接器(linker)的活還沒完呢,它還須要負責連接標準庫文件,把你本身的 .c 文件編譯生成的 .o 目標文件和標準庫文件整合在一塊兒,而後連接成最終的可執行文件。
以下圖所示:
這下咱們的示意圖終於完整了。
這樣咱們纔有了一個完整的可執行文件,裏面有它須要的全部指令的定義,好比 printf 的定義。
爲告終束這一課,咱們還得學習最後一個知識點:變量和函數的做用範圍(有效範圍)。
咱們將學習變量和函數何時是能夠被調用的。
當你在一個函數裏定義了一個變量以後,這個變量會在函數結尾時從內存中被刪除。
int multipleTwo(int number) { int result = 0; // 變量 result 在內存中被建立 result = 2 * number; return result; } // 函數結束,變量 result 從內存中被刪除
在一個函數裏定義的變量,只在函數運行期間存在。
這意味着什麼呢?意味着你不能從另外一個函數中調用它。
#include <stdio.h> int multipleTwo(int number); int main(int argc, char *argv[]) { printf("15 的兩倍是 %d\n", multipleTwo(15)); printf("15 的兩倍是 %d", result); // 錯誤! return 0; } int multipleTwo(int number) { int result = 0; result = 2 * number; return result; }
能夠看到,在 main 函數中,咱們試着調用 result 這個變量,可是由於這個變量是在 multipleTwo 函數中定義的,在 main 函數中就不能調用,編譯會出錯。
記住:在函數裏定義的變量只能在函數內部使用,咱們稱之爲 局部變量,英語是 local variable。local 表示「局部的,本地的」,variable 表示「變量」。
全局變量的英語是 global variable。global 表示「全局的,整體的」。
咱們能夠定義能被項目的全部文件的全部函數調用的變量。咱們會展現怎麼作,是爲了說明這方法存在,可是通常來講,要避免使用能被全部文件使用的全局變量。
可能這樣作一開始會讓你的代碼簡單一些,可是不久你就會爲之煩惱了。
爲了建立能被全部函數調用的全局變量,咱們需要在函數以外定義。一般咱們把這樣的變量放在程序的開頭,#include 預處理指令的後面。
#include <stdio.h> int result = 0; // 定義全局變量 result void multipleTwo(int number); // 函數原型 int main(int argc, char *argv[]) { multipleTwo(15); // 調用 multipleTwo 函數,使全局變量 result 的值變爲原來的兩倍 printf("15 的兩倍是 %d\n", result); // 咱們能夠調用變量 result return 0; } void multipleTwo(int number) { result = 2 * number; }
上面的程序中,咱們的函數 multipleTwo 再也不有返回值了,而是用於將 result 這個全局變量的值變成 2 倍。以後 main 函數能夠再使用 result 這個變量。
因爲這裏的 result 變量是一個徹底開放的全局變量,因此它能夠被項目的全部文件調用,也就能被全部文件的任何函數調用。
注:這種類型的變量是很不推薦使用的,由於不安全。通常用函數裏的 return 語句來返回一個變量的值。
剛纔咱們學習的徹底開放的全局變量能夠被項目的全部文件訪問。咱們也可使一個全局變量只能被它所在的那個文件調用。
就是說它能夠被本身所在的那個文件的全部函數調用,但不能被項目的其餘文件的函數調用。
怎麼作呢?
只須要在變量前面加上 static 這個關鍵字。以下所示:
static int result = 0;
static 表示「靜態的,靜止的」。
注意:
若是你在聲明一個函數內部的變量時,在前面加上 static 這個關鍵字,它的含義和上面咱們演示的全局變量是不一樣的。
函數內部的變量若是加了 static,那麼在函數結束後,這個變量也不會銷燬,它的值會保持。下一次咱們再調用這個函數時,此變量會延用上一次的值。
例如:
int multipleTwo(int number) { static int result = 0; // 靜態變量 result 在函數第一次被調用時建立 result = 2 * number; return result; } // 變量 result 在函數結束時不會被銷燬
這到底意味着什麼呢?
就是說:result 這個變量的值,在下次咱們調用這個函數時,會延用上一次結束調用時的值。
有點暈是嗎?沒關係。來看一個小程序,以便加深理解:
#include <stdio.h> int increment(); int main(int argc, char *argv[]) { printf("%d\n", increment()); printf("%d\n", increment()); printf("%d\n", increment()); printf("%d\n", increment()); return 0; } int increment() { static int number = 0; number++; return number; }
上述程序中,在咱們第一次調用 increment 函數時,number 變量被建立,初始值爲 0,而後對其作自增操做(++ 運算符),因此 number 的值變爲 1。
函數結束後,number 變量並無從內存中被刪除,而是保存着 1 這個值。
以後,當咱們第二次調用 increment 函數時,變量 number 的聲明語句(static int number = 0;
)會被跳過不執行(由於變量 number 還在內存裏呢。你想,一個皇帝還沒駕崩,太子怎麼能繼位呢?)。
咱們繼續使用上一次建立的 number 變量,這時候變量的值沿用第一次 increment 函數調用結束後的值:1,再對它作 ++ 操做(自加 1),number 的值就變爲 2 了。
依此類推,第三次調用 increment 函數後 number 的值爲 3。第四次 number 的值爲 4。
因此程序的輸出以下:
1 2 3 4
咱們用函數的做用域來結束咱們關於變量和函數的做用域的學習。
正常來講,當你在一個 .c 源文件中建立了一個函數,那它就是全局的,能夠被項目中全部其餘 .c 文件調用。
可是有時咱們須要建立只能被本文件調用的函數,怎麼作呢?
聰明如你確定想到了:對了,就是使用 static 關鍵字,與變量相似。
把它放在函數前面。以下:
static int multipleTwo(int number) { // 指令 }
如今,你的函數就只能被同一個文件中的其餘函數調用了,項目中的其餘文件中的函數就只「可遠觀而不可褻玩焉」…
今天的課就到這裏,一塊兒加油吧!
下一課:C語言探索之旅 | 第二部分第二課:進擊的指針,C語言的王牌!
我是 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)運營者,慕課網精英講師 Oscar 老師,終生學習者。 熱愛生活,喜歡游泳,略懂烹飪。 人生格言:「向着標杆直跑」