作者 謝恩銘,公眾號(hào)「程序員聯(lián)盟」(微信號(hào):coderhub)。
轉(zhuǎn)載請(qǐng)注明出處。
原文:http://www.itdecent.cn/p/2070cfd368ca
《C語言探索之旅》全系列
內(nèi)容簡介
- 前言
- 函數(shù)原型
- 頭文件
- 分開編譯
- 變量和函數(shù)的作用范圍
- 總結(jié)
- 第二部分第二課預(yù)告
1. 前言
上一課是 C語言探索之旅 | 第一部分練習(xí)題 。
話說上一課是第一部分最后一課,現(xiàn)在開始第二部分的探索之旅!
在這一部分中,我們會(huì)學(xué)習(xí) C語言的高級(jí)技術(shù)。這一部分內(nèi)容將是一座高峰,會(huì)挺難的,但是我們一起翻越。
俗語說得好:“一口是吃不成一個(gè)胖子的?!?/p>
但是一小口一小口,慢慢吃,還是能吃成胖子的嘛。所以要細(xì)水長流,肥油慢積,一路上有你(“油膩”)~
一旦你跟著我們的課程一直學(xué)到這一部分的結(jié)束,你將會(huì)掌握 C語言的核心技術(shù),也可以理解大部分 C語言寫的程序了。
到目前為止我們的程序都只是在一個(gè) main.c 文件里搗騰,因?yàn)槲覀兊某绦蜻€很短小,這也足夠了。
但如果之后你的程序有了十多個(gè)函數(shù),甚至上百個(gè)函數(shù),那么你就會(huì)感到全部放在一個(gè) main.c 文件里是多么擁擠和混亂。
正因?yàn)槿绱?,?jì)算機(jī)科學(xué)家才想出了模塊化編程。原則很簡單:與其把所有源代碼都放在一個(gè) main.c 當(dāng)中,我們將把它們合理地分割,放到不同的文件里面。
2. 函數(shù)原型
到目前為止,寫自定義函數(shù)的時(shí)候,我們都要求大家暫時(shí)把函數(shù)寫在 main 函數(shù)的前面。
這是為什么呢?
因?yàn)檫@里的順序是一個(gè)重要的問題。如果你將自己定義的函數(shù)放置在 main 函數(shù)之前,電腦會(huì)讀到它,就會(huì)“知道”這個(gè)函數(shù)。當(dāng)你在 main 函數(shù)中調(diào)用這個(gè)函數(shù)時(shí),電腦已經(jīng)知道這個(gè)函數(shù),也知道到哪里去執(zhí)行它。
但是假如你把這個(gè)函數(shù)寫在 main 函數(shù)后面,那你在 main 函數(shù)里調(diào)用這個(gè)函數(shù)的時(shí)候,電腦還不“認(rèn)識(shí)”它呢。你可以自己寫個(gè)程序測試一下。是的,很奇怪對(duì)吧?這絕對(duì)有點(diǎn)任性的。
那你會(huì)說:“C語言豈不是設(shè)計(jì)得不好么?”
我“完全”同意(可別讓 C語言之父 Dennis Ritchie 聽到了...)。但是請(qǐng)相信,這樣設(shè)計(jì)應(yīng)該也是有理由的。計(jì)算機(jī)先驅(qū)們?cè)缇拖氲搅?,也提出了解決之道。
下面我們就來學(xué)一個(gè)新的知識(shí)點(diǎn),借著這個(gè)技術(shù),你可以把你的自定義函數(shù)放在程序的任意位置。
用來聲明一個(gè)函數(shù)的“函數(shù)原型”
我們會(huì)聲明我們的函數(shù),需要用到一個(gè)專門的技術(shù):函數(shù)原型,英語是 function prototype。function 表示“函數(shù)”,prototype 表示“原型,樣本,模范”。
就好比你對(duì)電腦發(fā)出一個(gè)通知:“看,我的函數(shù)的原型在這里,你給我記住啦!”
我們來看一下上一課舉的一個(gè)函數(shù)的例子(計(jì)算矩形面積):
double rectangleArea(double length, double width)
{
return length * width;
}
怎么來聲明我們上面這個(gè)函數(shù)的原型呢?
- 復(fù)制,粘貼第一行。
- 在最后放上一個(gè)分號(hào)(
;)。 - 把這一整行放置在 main 函數(shù)前面。
很簡單吧?現(xiàn)在你就可以把你的函數(shù)的定義放在 main 函數(shù)后面啦,電腦也會(huì)認(rèn)識(shí)它,因?yàn)槟阍?main 函數(shù)前面已經(jīng)聲明過這個(gè)函數(shù)了。
你的程序會(huì)變成這樣:
#include <stdio.h>
#include <stdlib.h>
// 下面這一行是 rectangleArea 函數(shù)的函數(shù)原型
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;
}
// 現(xiàn)在我們的 rectangleArea 函數(shù)就可以放置在程序的任意位置了
double rectangleArea(double length, double width)
{
return length * width;
}
與原先的程序相比有什么改變呢?
其實(shí)就是在程序的開頭加了函數(shù)的原型而已(記得不要忘了那個(gè)分號(hào))。
函數(shù)的原型,其實(shí)是給電腦的一個(gè)提示或指示。比如上面的程序中,函數(shù)原型
double rectangleArea(double length, double width);
就是對(duì)電腦說:“老兄,存在一個(gè)函數(shù),它的輸入是哪幾個(gè)參數(shù),輸出是什么類型”,這樣就能讓電腦更好地管理。
多虧了這一行代碼,現(xiàn)在你的 rectangleArea 函數(shù)可以置于程序的任何位置了。
記得:最好養(yǎng)成習(xí)慣,對(duì)于 C語言程序,總是定義了函數(shù),再寫一下函數(shù)的原型。
那么不寫函數(shù)原型行不行呢?
也行。只要你把每個(gè)函數(shù)的定義都放在 main 函數(shù)之前。但是你的程序慢慢會(huì)越來越大,等你有幾十或者幾百個(gè)函數(shù)的時(shí)候,你還顧得過來么?
所以養(yǎng)成好習(xí)慣,不吃虧的。
你也許注意到了,main 函數(shù)沒有函數(shù)原型。因?yàn)椴恍枰?,main 函數(shù)是每個(gè) C程序必須的入口函數(shù)。人家 main 函數(shù)“有權(quán)任性”,跟編譯器關(guān)系好,編譯器對(duì) main 函數(shù)很熟悉,是經(jīng)常打交道的“哥們”,所以不需要函數(shù)原型來“介紹” main 函數(shù)。
還有一點(diǎn),在寫函數(shù)原型的時(shí)候,對(duì)于圓括號(hào)里的函數(shù)參數(shù),名字是不一定要寫的,可以只寫類型。
因?yàn)楹瘮?shù)原型只是給電腦做個(gè)介紹,所以電腦只需要知道輸入的參數(shù)是什么類型就夠了,不需要知道名字。所以我們以上的函數(shù)原型也可以簡寫如下:
double rectangleArea(double, double);
看到了嗎,我們可以省略 length 和 width 這兩個(gè)變量名,只保留 double(雙精度浮點(diǎn)型)這個(gè)類型名字。
千萬不要忘了函數(shù)原型末尾的分號(hào),因?yàn)檫@是編譯器區(qū)分函數(shù)原型和函數(shù)定義開頭的重要指標(biāo)。如果沒有分號(hào),編譯時(shí)會(huì)出現(xiàn)比較難理解的錯(cuò)誤提示。
3. 頭文件
頭文件在英語中是 header file。header 表示“數(shù)據(jù)頭,頁眉”,file 表示“文件”。
每次看到這個(gè)術(shù)語,我都想到已經(jīng)結(jié)婚的“我們的青春”:周杰倫 的《頭文字D》。
到目前為止,我們的程序只有一個(gè) .c 文件(被稱為“源文件”,在英語中是 source file。source 表示“源,源頭,水源”),比如我們之前把這個(gè) .c 文件命名為 main.c。當(dāng)然名字是無所謂的,起名為hello.c,haha.c 都行。
一個(gè)項(xiàng)目多個(gè)文件
在實(shí)際編寫程序的時(shí)候,你的項(xiàng)目一般肯定不會(huì)把代碼都寫在一個(gè) main.c 文件中。當(dāng)然,也不是不可以。
但是,試想一下,如果你把所有代碼都塞到這一個(gè) main.c 文件中,那如果代碼量達(dá)到 10000 行甚至更多,你要在里面找一個(gè)東西就太難了。也正是因?yàn)檫@樣,通常我們每一個(gè)項(xiàng)目都會(huì)創(chuàng)建多個(gè)文件。
那以上說到的項(xiàng)目是指什么呢?
之前我們用 CodeBlocks 這個(gè) IDE 創(chuàng)建第一個(gè) C語言項(xiàng)目的時(shí)候,其實(shí)已經(jīng)接觸過了。
一個(gè)項(xiàng)目(英語是 project),簡單來說是指你的程序的所有源代碼(還有一些其他的文件),項(xiàng)目里面的文件有多種類型。
目前我們的項(xiàng)目還只有一個(gè)源文件:main.c 。
看一下你的 IDE,一般來說項(xiàng)目是列在左邊。

如上圖,你可以看到,這個(gè)項(xiàng)目(在 Projects 一欄里)只有一個(gè)文件:main.c 。
現(xiàn)在我們?cè)賮碚故疽粋€(gè)包含好多個(gè)文件的項(xiàng)目:

上圖中,我們可以看到在這個(gè)項(xiàng)目里有好幾個(gè)文件。實(shí)際中的項(xiàng)目大多是這樣的。你看到那個(gè) main.c 文件了嗎?通常來說在我們的程序中,會(huì)把 main 函數(shù)只定義在 main.c 當(dāng)中。
當(dāng)然也不是非要這樣,每個(gè)人都有自己的編程風(fēng)格。不過希望跟著這個(gè)課程學(xué)習(xí)的讀者,可以和我們保持一致的風(fēng)格,方便理解。
那你又要問了:“為什么創(chuàng)建多個(gè)文件呢?我怎么知道為項(xiàng)目創(chuàng)建幾個(gè)文件合適呢?”
答案是:這是你的選擇。通常來說,我們把同一主題的函數(shù)放在一個(gè)文件里。
.h 文件和 .c 文件
在上圖中,我們可以看到有兩種類型的文件:一種是以 .h 結(jié)尾的,一種是以 .c 結(jié)尾的。
- .h 文件:header file,表示“頭文件”,這些文件包含了函數(shù)的原型。
- .c 文件:source file,表示“源文件”,包含了函數(shù)本身(定義)。
所以,通常來說我們不常把函數(shù)原型放在 .c 文件中,而是放在 .h 文件中,除非你的程序很小。
對(duì)每個(gè) .c 文件,都有同名的 .h 文件。上面的項(xiàng)目那個(gè)圖中,你可以看到 .h 和 .c 文件一一對(duì)應(yīng)。
- files.h 和 files.c
- editor.h 和 editor.c
- game.h 和 game.c
但我們的電腦怎么知道函數(shù)原型是在 .c 文件之外的另一種文件里呢?
需要用到我們之前介紹過的預(yù)處理指令 #include 來將其引入到 .c 文件中。
請(qǐng)做好準(zhǔn)備,下面將有一波密集的知識(shí)點(diǎn)“來襲”。
怎么引入一個(gè)頭文件呢?其實(shí)你已經(jīng)知道怎么做了,之前的課程我們已經(jīng)寫過了。
比如我們來看我們上面的 game.c 文件的開頭
#include <stdlib.h>
#include <stdio.h>
#include "game.h"
void player(SDL_Surface* screen)
{
// ...
}
看到了嗎,其實(shí)你早就熟悉了,要引入頭文件,只需要用 #include 這個(gè)預(yù)處理指令。
因此我們?cè)?game.c 源文件中一共引入了三個(gè)頭文件:stdlib.h, stdio.h,game.h。
注意到一個(gè)不同點(diǎn)了嗎?
在標(biāo)準(zhǔn)庫的頭文件(stdlib.h,stdio.h)和你自己定義的頭文件(game.h)的引入方式是有點(diǎn)區(qū)別的:
-
<>用于引入標(biāo)準(zhǔn)庫的頭文件。對(duì)于 IDE,這些頭文件一般位于 IDE 安裝目錄的 include 文件夾中;在 Linux 操作系統(tǒng)下,則一般位于系統(tǒng)的 include 文件夾里。 -
""用于引入自定義的頭文件。這些頭文件位于你自己的項(xiàng)目的目錄中。
我們?cè)賮砜匆幌聦?duì)應(yīng)的 game.h 這個(gè)頭文件的內(nèi)容:

看到了嗎,.h 文件中存放的是函數(shù)原型。
你已經(jīng)對(duì)一個(gè)項(xiàng)目有大致概念了。
那你又會(huì)問了:“為什么要這樣安排呢?把函數(shù)原型放在 .h 頭文件中,在 .c 源文件中用 #include 引入。為什么不把函數(shù)原型寫在 .c 文件中呢?”
答案是:方便管理,條理清晰,不容易出錯(cuò),省心。
因?yàn)槿缜八觯愕碾娔X在調(diào)用一個(gè)函數(shù)前必須先“知道”這個(gè)函數(shù),我們需要函數(shù)原型來讓使用這個(gè)函數(shù)的其他函數(shù)預(yù)先知道。
如果用了 .h 頭文件的管理方法,在每一個(gè) .c 文件開頭只要用 #include 這個(gè)指令來引入頭文件的所有內(nèi)容,那么頭文件中聲明的所有函數(shù)原型都被當(dāng)前 .c 文件所知道了,你就不用再操心那些函數(shù)的定義順序或者有沒有被其他函數(shù)知道
例如我的 main.c 函數(shù)要使用 functions.c 文件中的函數(shù),那我只要在 main.c 的開頭寫 #include "functions.h",之后我在 main.c 函數(shù)中就可以調(diào)用 function.c 中定義的函數(shù)了。
你可能又要問了:“那我怎么在項(xiàng)目中加入新的 .h 和 .c 文件呢?”
很簡單,在 CodeBlocks 里,鼠標(biāo)右鍵點(diǎn)擊項(xiàng)目列表的主菜單處,選擇 Add Files,或者在菜單欄上依次單擊 File -> New -> File... ,就可以選擇添加文件的類型了。

引入標(biāo)準(zhǔn)庫
你腦海里肯定出現(xiàn)一個(gè)問題:
如果我們用 #include 來引入 stdio.h 和 stdlib.h 這樣的標(biāo)準(zhǔn)庫的頭文件,而這些文件又不是我自己寫的,那么它們肯定存在于電腦里的某個(gè)地方,我們可以找到,對(duì)吧?
是的,完全正確!
如果你使用的是 IDE(集成開發(fā)環(huán)境),那么它們一般就在你的 IDE 的安裝目錄里。
如果是在純 Linux 環(huán)境下,那就要到系統(tǒng)文件夾里去找,這里不討論了,感興趣的讀者可以去網(wǎng)上搜索。
在我的情況,因?yàn)榘惭b的是 CodeBlocks 這個(gè) IDE,所以在 Windows下,我的頭文件們“隱藏”在這兩個(gè)路徑下:
C:\Program Files\CodeBlocks\MinGW\include

和
C:\Program Files\CodeBlocks\MinGW\x86_64-w64-mingw32\include

一般來說,都在一個(gè)叫做 include 的文件夾里。
在里面,你會(huì)找到很多文件,都是 .h 文件,也就是 C語言系統(tǒng)定義的標(biāo)準(zhǔn)頭文件,也就是系統(tǒng)庫的頭文件(對(duì) Windows,macOS,Linux 都是通用的,C語言本來就是可移植的嘛)。
在這眾多的頭文件當(dāng)中,你可以找到我們的老朋友:stdio.h 和 stdlib.h。
你可以雙擊打開這些文件或者選擇你喜歡的文本編輯器來打開,不過也許你會(huì)嚇一跳,因?yàn)檫@些文件里的內(nèi)容很多,而且好些是我們還沒學(xué)到的用法,比如除了 #include 以外的其他的預(yù)處理指令。
你可以看到這些頭文件中充滿了函數(shù)原型,比如你可以在 stdio.h 中找到 printf 函數(shù)的原型。
你要問了:“OK,現(xiàn)在我已經(jīng)知道標(biāo)準(zhǔn)庫的頭文件在哪里了,那與之對(duì)應(yīng)的標(biāo)準(zhǔn)庫的源文件(.c 文件)在哪里呢?”
不好意思,你見不到它們啦。因?yàn)?.c 文件已經(jīng)被事先編譯好,轉(zhuǎn)換成計(jì)算機(jī)能理解的二進(jìn)制碼了。
“伊人已去,年華不復(fù),吾將何去何從?”
既然見不到原先的它們了,至少讓我見一下“美圖秀秀”之后的它們吧…
可以,你在一個(gè)叫 lib 的文件夾下面就可以找到,在我的 Windows 下的路經(jīng)為:
C:\Program Files\CodeBlocks\MinGW\lib

和
C:\Program Files\CodeBlocks\MinGW\x86_64-w64-mingw32\lib

被編譯成二進(jìn)制碼的 .c 文件,有了一個(gè)新的后綴名:.a(在 CodeBlocks 的情況,它的編譯器是 MinGW。MinGW 簡單來說就是 GCC 編譯器的 Windows 版本)或者 .lib(在 Visual C++ 的情況),等。這是靜態(tài)鏈接庫的情況。
你在 Windows 中還能找到 .dll 結(jié)尾的動(dòng)態(tài)鏈接庫;你在 Linux 中能找到 .so 結(jié)尾的動(dòng)態(tài)鏈接庫。暫時(shí)我們不深究靜態(tài)鏈接庫和動(dòng)態(tài)鏈接庫,有興趣的讀者可以去網(wǎng)上自行搜索。
這些被編譯之后的文件被叫做庫文件或 Library 文件(library 表示“庫,圖書館,文庫”),不要試著去閱讀這些文件的內(nèi)容,因?yàn)槭强床欢膩y碼。
學(xué)到這里可能有點(diǎn)暈,不過繼續(xù)看下去就會(huì)漸漸明朗起來,下面的內(nèi)容會(huì)有示意圖幫助理解。
小結(jié)一下:
在我們的 .c 源文件中,我們可以用 #include 這個(gè)預(yù)處理指令來引入標(biāo)準(zhǔn)庫的 .h 頭文件或自己定義的頭文件。這樣我們就能使用標(biāo)準(zhǔn)庫所定義的 printf 這樣的函數(shù),電腦就認(rèn)識(shí)了這些函數(shù)(借著 .h 文件中的函數(shù)原型),就可以檢驗(yàn)?zāi)阏{(diào)用這些函數(shù)時(shí)有沒有用對(duì),比如函數(shù)的參數(shù)個(gè)數(shù),返回值類型,等。
4. 分開編譯
現(xiàn)在我們知道了一個(gè)項(xiàng)目是由若干文件組成的,那我們就可以來了解一下編譯器(compiler)的工作原理。
之前的課里面展示的編譯示例圖是比較簡化的,下圖是一幅編譯原理的略微詳細(xì)的圖,希望大家用心理解并記?。?/p>
上圖將編譯時(shí)所發(fā)生的事情基本詳細(xì)展示了,我們來仔細(xì)分析:
- 預(yù)處理器(preprocessor):顧名思義,預(yù)處理器為編譯做一些預(yù)備工作,所以預(yù)處理器是在編譯之前啟動(dòng)的。它的任務(wù)是執(zhí)行特殊的指令,這些指令是通過預(yù)處理命令給出的,預(yù)處理命令以 # 開頭,很容易辨認(rèn)。
預(yù)處理指令有好多種,目前我們學(xué)過的只有 #include,它使我們可以在一個(gè)文件中引入另一個(gè)文件的內(nèi)容。#include 這個(gè)預(yù)處理指令也是最常用的。
預(yù)處理器會(huì)把 #include 所在的那一句話替換為它所引入的頭文件的內(nèi)容,比如
#include <stdio.h>
預(yù)處理器在執(zhí)行時(shí)會(huì)把上面這句指令替換為 stdio.h 文件的內(nèi)容。所以到了編譯的時(shí)候,你的 .c 文件的內(nèi)容會(huì)變多,包含了所有引入的頭文件的內(nèi)容,顯得比較臃腫。
- 編譯(compilation):這是核心的步驟,以前的課我們說過,正是編譯把我們?nèi)祟悓懙拇a轉(zhuǎn)換成計(jì)算機(jī)能理解的二進(jìn)制碼(0 和 1 組成)。編譯器編譯一個(gè)個(gè) .c 文件。對(duì)于 CodeBlocks 這樣的 IDE 來說,就是你放在項(xiàng)目列表中的所有 .c 文件;如果你是用 gcc 這個(gè)編譯器來編譯,那么你要指定編譯哪幾個(gè) .c 文件。
編譯器會(huì)把 .c 文件先轉(zhuǎn)換成 .o 文件(有的編譯器會(huì)生成 .obj 文件),.o 文件一般叫做目標(biāo)文件(o 是 object 的首字母,表示“目標(biāo)”),是臨時(shí)的二進(jìn)制文件,會(huì)被用于之后生成最終的可執(zhí)行二進(jìn)制文件。
.o 文件一般會(huì)在編譯完成后被刪除(根據(jù)你的 IDE 的設(shè)置)。從某種程度上來說 .o 文件雖然是臨時(shí)中間文件,好像沒什么大用,但保留著不刪除也是有好處:假如項(xiàng)目有 10 個(gè) .c 文件,編譯后生成了 10 個(gè) .o 文件。之后你只修改了其中的一個(gè) .c 文件,如果重新編譯,那么編譯器不會(huì)為其他 9 個(gè) .c 文件重新生成 .o 文件,只會(huì)重新生成你更改的那個(gè)。這樣可以節(jié)省資源。
- 鏈接器(linker):顧名思義,鏈接器的作用是鏈接。鏈接什么呢?就是編譯器生成的 .o 文件。鏈接器把所有 .o 文件鏈接起來,“制作成”一個(gè)“大塊頭”:最終的可執(zhí)行文件(在 Windows下是 .exe 文件。在 Linux 下有不少種形式)。
現(xiàn)在你知道從代碼到生成一個(gè)可執(zhí)行程序的內(nèi)部原理了吧,下面我們要展示給大家的這張圖,很重要,希望大家理解并記住。
大部分的錯(cuò)誤都會(huì)在編譯階段被顯示,但也有一些是在鏈接的時(shí)候顯示,有可能是少了 .o 文件之類。
之前那幅圖其實(shí)還不夠完整,你可能想到了:我們用 .h 文件引入了標(biāo)準(zhǔn)庫的頭文件的內(nèi)容(里面主要是函數(shù)原型),函數(shù)的具體實(shí)現(xiàn)的代碼我們還沒引入呢,怎么辦呢?
對(duì)了,就是之前提到過的 .a 或 .lib 這樣的庫文件(由標(biāo)準(zhǔn)庫的 .c 源文件編譯而成)。
所以我們的鏈接器(linker)的活還沒完呢,它還需要負(fù)責(zé)鏈接標(biāo)準(zhǔn)庫文件,把你自己的 .c 文件編譯生成的 .o 目標(biāo)文件和標(biāo)準(zhǔn)庫文件整合在一起,然后鏈接成最終的可執(zhí)行文件。
如下圖所示:
這下我們的示意圖終于完整了。
這樣我們才有了一個(gè)完整的可執(zhí)行文件,里面有它需要的所有指令的定義,比如 printf 的定義。
5. 變量和函數(shù)的作用范圍
為了結(jié)束這一課,我們還得學(xué)習(xí)最后一個(gè)知識(shí)點(diǎn):變量和函數(shù)的作用范圍(有效范圍)。
我們將學(xué)習(xí)變量和函數(shù)什么時(shí)候是可以被調(diào)用的。
函數(shù)的私有變量(局部變量)
當(dāng)你在一個(gè)函數(shù)里定義了一個(gè)變量之后,這個(gè)變量會(huì)在函數(shù)結(jié)尾時(shí)從內(nèi)存中被刪除。
int multipleTwo(int number)
{
int result = 0; // 變量 result 在內(nèi)存中被創(chuàng)建
result = 2 * number;
return result;
} // 函數(shù)結(jié)束,變量 result 從內(nèi)存中被刪除
在一個(gè)函數(shù)里定義的變量,只在函數(shù)運(yùn)行期間存在。
這意味著什么呢?意味著你不能從另一個(gè)函數(shù)中調(diào)用它。
#include <stdio.h>
int multipleTwo(int number);
int main(int argc, char *argv[])
{
printf("15 的兩倍是 %d\n", multipleTwo(15));
printf("15 的兩倍是 %d", result); // 錯(cuò)誤!
return 0;
}
int multipleTwo(int number)
{
int result = 0;
result = 2 * number;
return result;
}
可以看到,在 main 函數(shù)中,我們?cè)囍{(diào)用 result 這個(gè)變量,但是因?yàn)檫@個(gè)變量是在 multipleTwo 函數(shù)中定義的,在 main 函數(shù)中就不能調(diào)用,編譯會(huì)出錯(cuò)。
記?。涸诤瘮?shù)里定義的變量只能在函數(shù)內(nèi)部使用,我們稱之為局部變量,英語是 local variable。local 表示“局部的,本地的”,variable 表示“變量”。
全局變量(請(qǐng)避免使用)
全局變量的英語是 global variable。global 表示“全局的,總體的”。
能被所有文件使用的全局變量
我們可以定義能被項(xiàng)目的所有文件的所有函數(shù)調(diào)用的變量。我們會(huì)展示怎么做,是為了說明這方法存在,但是一般來說,要避免使用能被所有文件使用的全局變量。
可能這樣做一開始會(huì)讓你的代碼簡單一些,但是不久你就會(huì)為之煩惱了。
為了創(chuàng)建能被所有函數(shù)調(diào)用的全局變量,我們須要在函數(shù)之外定義。通常我們把這樣的變量放在程序的開頭,#include 預(yù)處理指令的后面。
#include <stdio.h>
int result = 0; // 定義全局變量 result
void multipleTwo(int number); // 函數(shù)原型
int main(int argc, char *argv[])
{
multipleTwo(15); // 調(diào)用 multipleTwo 函數(shù),使全局變量 result 的值變?yōu)樵瓉淼膬杀?
printf("15 的兩倍是 %d\n", result); // 我們可以調(diào)用變量 result
return 0;
}
void multipleTwo(int number)
{
result = 2 * number;
}
上面的程序中,我們的函數(shù) multipleTwo 不再有返回值了,而是用于將 result 這個(gè)全局變量的值變成 2 倍。之后 main 函數(shù)可以再使用 result 這個(gè)變量。
由于這里的 result 變量是一個(gè)完全開放的全局變量,所以它可以被項(xiàng)目的所有文件調(diào)用,也就能被所有文件的任何函數(shù)調(diào)用。
注:這種類型的變量是很不推薦使用的,因?yàn)椴话踩R话阌煤瘮?shù)里的 return 語句來返回一個(gè)變量的值。
只能在一個(gè)文件里被訪問的全局變量
剛才我們學(xué)習(xí)的完全開放的全局變量可以被項(xiàng)目的所有文件訪問。我們也可以使一個(gè)全局變量只能被它所在的那個(gè)文件調(diào)用。
就是說它可以被自己所在的那個(gè)文件的所有函數(shù)調(diào)用,但不能被項(xiàng)目的其他文件的函數(shù)調(diào)用。
怎么做呢?
只需要在變量前面加上 static 這個(gè)關(guān)鍵字。如下所示:
static int result = 0;
static 表示“靜態(tài)的,靜止的”。
函數(shù)的 static(靜態(tài))變量
注意:
如果你在聲明一個(gè)函數(shù)內(nèi)部的變量時(shí),在前面加上 static 這個(gè)關(guān)鍵字,它的含義和上面我們演示的全局變量是不同的。
函數(shù)內(nèi)部的變量如果加了 static,那么在函數(shù)結(jié)束后,這個(gè)變量也不會(huì)銷毀,它的值會(huì)保持。下一次我們?cè)僬{(diào)用這個(gè)函數(shù)時(shí),此變量會(huì)延用上一次的值。
例如:
int multipleTwo(int number)
{
static int result = 0; // 靜態(tài)變量 result 在函數(shù)第一次被調(diào)用時(shí)創(chuàng)建
result = 2 * number;
return result;
} // 變量 result 在函數(shù)結(jié)束時(shí)不會(huì)被銷毀
這到底意味著什么呢?
就是說:result 這個(gè)變量的值,在下次我們調(diào)用這個(gè)函數(shù)時(shí),會(huì)延用上一次結(jié)束調(diào)用時(shí)的值。
有點(diǎn)暈是嗎?不要緊。來看一個(gè)小程序,以便加深理解:
#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;
}
上述程序中,在我們第一次調(diào)用 increment 函數(shù)時(shí),number 變量被創(chuàng)建,初始值為 0,然后對(duì)其做自增操作(++ 運(yùn)算符),所以 number 的值變?yōu)?1。
函數(shù)結(jié)束后,number 變量并沒有從內(nèi)存中被刪除,而是保存著 1 這個(gè)值。
之后,當(dāng)我們第二次調(diào)用 increment 函數(shù)時(shí),變量 number 的聲明語句(static int number = 0;)會(huì)被跳過不執(zhí)行(因?yàn)樽兞?number 還在內(nèi)存里呢。你想,一個(gè)皇帝還沒駕崩,太子怎么能繼位呢?)。
我們繼續(xù)使用上一次創(chuàng)建的 number 變量,這時(shí)候變量的值沿用第一次 increment 函數(shù)調(diào)用結(jié)束后的值:1,再對(duì)它做 ++ 操作(自加 1),number 的值就變?yōu)?2 了。
依此類推,第三次調(diào)用 increment 函數(shù)后 number 的值為 3。第四次 number 的值為 4。
所以程序的輸出如下:
1
2
3
4
一個(gè)文件中的局部函數(shù)(本地函數(shù)或靜態(tài)函數(shù))
我們用函數(shù)的作用域來結(jié)束我們關(guān)于變量和函數(shù)的作用域的學(xué)習(xí)。
正常來說,當(dāng)你在一個(gè) .c 源文件中創(chuàng)建了一個(gè)函數(shù),那它就是全局的,可以被項(xiàng)目中所有其他 .c 文件調(diào)用。
但是有時(shí)我們需要?jiǎng)?chuàng)建只能被本文件調(diào)用的函數(shù),怎么做呢?
聰明如你肯定想到了:對(duì)了,就是使用 static 關(guān)鍵字,與變量類似。
把它放在函數(shù)前面。如下:
static int multipleTwo(int number)
{
// 指令
}
現(xiàn)在,你的函數(shù)就只能被同一個(gè)文件中的其他函數(shù)調(diào)用了,項(xiàng)目中的其他文件中的函數(shù)就只“可遠(yuǎn)觀而不可褻玩焉”…
小結(jié)一下變量的所有可能的作用范圍
在函數(shù)體內(nèi)定義的變量,如果前面沒加 static 關(guān)鍵字,則是局部變量,在函數(shù)結(jié)束時(shí)被刪除,只能在本函數(shù)內(nèi)被使用。
在函數(shù)體內(nèi)定義,但是前面加了 static 關(guān)鍵字,則為靜態(tài)變量,在函數(shù)結(jié)束時(shí)不被刪除,其值也會(huì)保留。
在函數(shù)外面定義的變量被稱為全局變量,如果前面沒有static關(guān)鍵字,則其作用范圍是整個(gè)項(xiàng)目的所有文件,就是說它可以被項(xiàng)目的所有文件的函數(shù)調(diào)用。
函數(shù)外面定義的變量,如果前面加了 static 關(guān)鍵字,那就只能被本文件的所有函數(shù)調(diào)用,而不能被項(xiàng)目其他的文件的函數(shù)調(diào)用。
小結(jié)一下函數(shù)的所有可能的作用范圍
一個(gè)函數(shù)在默認(rèn)情況下是可以被項(xiàng)目的所有文件的函數(shù)調(diào)用的。
如果我們想要一個(gè)函數(shù)只能被本文件的函數(shù)所調(diào)用,只需要在函數(shù)前加上 static 關(guān)鍵字。
6. 總結(jié)
一個(gè)程序包含一個(gè)或多個(gè) .c 文件(一般稱為源文件,source file。當(dāng)然,我們一般也把所有的高級(jí)語言代碼叫做源代碼)。通常來說,每個(gè) .c 文件都有一個(gè)和它同名但不同擴(kuò)展名的 .h 文件。.c 文件里面包含了函數(shù)的實(shí)際定義,而 .h 文件里包含函數(shù)的原型聲明。
.h 文件的內(nèi)容被一個(gè)叫做預(yù)處理器(preprocessor)的程序引入到 .c 文件的開頭。
.c 文件被一個(gè)叫做編譯器(compiler)的程序轉(zhuǎn)換成 .o 的二進(jìn)制目標(biāo)文件。
.o 文件又被一個(gè)叫做鏈接器(linker)的程序鏈接成一個(gè)最終的可執(zhí)行文件(在 Windows 操作系統(tǒng)里可執(zhí)行程序的擴(kuò)展名是 .exe,因?yàn)?exe 是英語 executable 的前三個(gè)字母,表示“可執(zhí)行的”。在 Linux 系統(tǒng)里,可執(zhí)行程序有不少擴(kuò)展名(.elf,等),也可以沒有擴(kuò)展名)。
變量和函數(shù)都有“有效范圍”,某些時(shí)候是訪問不到的。
7. 第二部分第二課預(yù)告
今天的課就到這里,一起加油吧!
下一課:C語言探索之旅 | 第二部分第二課:進(jìn)擊的指針,C語言的王牌!
我是 謝恩銘,公眾號(hào)「程序員聯(lián)盟」(微信號(hào):coderhub)運(yùn)營者,慕課網(wǎng)精英講師 Oscar 老師,終生學(xué)習(xí)者。
熱愛生活,喜歡游泳,略懂烹飪。
人生格言:「向著標(biāo)桿直跑」