C語言探索之旅 | 第二部分第五課:預處理

做者 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)。
轉載請註明出處。
原文: https://www.jianshu.com/p/cb8...

《C語言探索之旅》全系列程序員

內容簡介


  1. 前言
  2. include 指令
  3. define 命令
  4. 條件編譯
  5. 總結
  6. 第二部分第六課預告

1. 前言


上一課 C語言探索之旅 | 第二部分第四課:字符串 ,咱們結束了關於字符串的旅程。編程

你們在一塊兒經歷了前三課:指針、數組和字符串的「疲勞轟炸」以後,這一課迴歸輕鬆。數組

就像剛在沙漠裏行走了很多天,忽然看到一片綠洲,還有準備好的躺椅,清澈的小湖,冷飲,西瓜,一臺頂配電腦(又暴露了程序員的本質...)... 腦補一下這個畫面仍是挺開心的。微信

前面三課咱們一會兒學了很多新知識點,雖然我沒有那麼"善良",但也不至於不給你們小憩的機會啊。函數

這一課咱們來聊聊「預處理器」,這個程序就在編譯以前運行。學習

固然了,雖然這一課不難,能夠做爲中場休息,但不要認爲這一課的內容不重要。相反,這一課的內容很是有用(讀者:「你就說哪一課的內容不是很是有用吧...」)。測試

2. include 指令


在這個系列教程最初的某一課裏,咱們已經向你們解釋過:在源代碼裏面總有那麼幾行代碼是很特別的,稱之爲預處理命令網站

這些命令的特別之處就在於它們老是以 # 開頭,因此很容易辨認。spa

預處理命令有好幾種,咱們如今只接觸了一種:以 #include 開始的預處理命令。指針

#include 命令能夠把一個文件的內容包含到另外一個文件中。

在以前的課程裏咱們已經學習瞭如何用 #include 命令來包含頭文件(以 .h 結尾的)。

頭文件有兩種,一種是 C語言的標準庫定義的頭文件(stdio.h,stdlib.h,等),另外一種是用戶自定義的頭文件。

  • 若是要導入 C語言標準庫的頭文件(位於你安裝的 IDE(集成開發環境)的文件夾或者編譯器的文件夾裏),須要用到尖括號 <>。以下所示:
#include <stdio.h>
  • 若是要導入用戶本身項目中定義的頭文件(位於你本身項目的文件夾裏),須要用到雙引號。以下所示:
#include "file.h"

事實上,預處理器(preprocessor)在編輯以前運行,它會遍歷你的源文件,尋找每個以 # 開頭的預處理命令。

例如,當它遇到 #include 開頭的預處理命令,就會把後面跟的頭文件的內容插入到此命令處,做爲替換。

假設我有一個 C 文件(就是 .c 文件),包含個人函數的實現代碼;還有一個 H 文件(就是 .h 文件),包含函數的原型。

咱們能夠用下圖來描繪預處理的時候發生的狀況:

如上圖所示,H 文件的全部內容都將替換 C 文件的那一行預處理命令(#include "file.h")。

假設咱們的 C 文件內容以下所示:

#include "file.h"

int myFunction(int thing, double stuff)
{
/* 函數體 */
}

void anotherFunction(int value)
{
/* 函數體 */
}

咱們的 H 文件內容以下所示:

int myFunction(int thing, double stuff);
void anotherFunction(int value);

編輯以前,預處理器就會用 H 文件的內容替換那一行 #include "file.h"

通過替換以後,C 文件內容以下:

int myFunction(int thing, double stuff);
void anotherFunction(int value);

int myFunction(int thing, double stuff)
{
/* 函數體 */
}

void anotherFunction(int value)
{
/* 函數體 */
}

3. define 命令


如今咱們一塊兒來學習一個新的預處理命令,就是 #define 命令。

define 表示「定義」。

這個命令使咱們能夠定義預處理常量,也就是把一個值綁定到一個名稱。例如:

#define LIFE_NUMBER 7

咱們必須按照如下順序來寫:

  • #define
  • 要綁定數值的那個名稱
  • 數值
注意:雖說這裏的名稱是大寫字母(由於習慣如此,你也能夠小寫),可是這與咱們以前學過的 const 變量仍是很不同。

const 變量的定義是像這樣的:

const int LIFE_NUMBER = 7;

上面的 const 變量在內存中是佔用空間的,雖然其不能改變,可是它確確實實儲存在內存的某個地方。可是預處理常量卻不是這樣。

那預處理常量是怎樣運做的呢?

事實上,預處理器會把由 #define 定義的全部的名稱替換成對應的值。

若是你們使用過微軟的軟件 Word,那應該對「查找並替換」的功能比較熟悉。咱們的 #define 就有點相似這個功能,它會查找當前文件的全部 #define 定義的常量名稱,將其替換爲對應的數值。

你也許要問:「用預處理常量意義何在呢?有什麼好處?」

問得好。

  • 第一,由於預處理常量不用儲存在內存裏。就如咱們以前所說,在編譯以前,預處理常量都被替換爲代碼中的數值了。
  • 第二,預處理常量的替換會發生在全部引入 #define 語句的文件裏。若是咱們在一個函數裏定義一個 const 變量,那麼它會在內存裏儲存,可是若是前面不加 static 關鍵字(關於 static,請參看以前的課程)的話,它只在當前函數有效,函數執行完就被銷燬了。然而預處理常量卻不是這樣,它能夠做用於全部函數,只要函數裏有那個名稱,都會替換爲對應的數值。這樣的機制在有些時候是很是有用的。特別對於嵌入式開發,內存比較有限,常常能看到預處理常量的使用。

「可否給出一個實際使用 #define 的例子呢?」

固然能夠。例如,當你用 C語言來建立一個窗口時,你須要定義窗口的寬度和高度,這時候就可使用 #define 了:

#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600

看到使用預處理常量的好處了麼?以後若是你要修改窗口的寬度和高度,沒必要到代碼裏去改每個值,只須要在定義處修改就行了,很是節省時間。

注意:一般來講, #define 語句放在 .h 頭文件中,和函數原型那些傢伙在一塊兒。
若是有興趣,你們能夠去看一下標準庫的 .h 文件,例如 stdio.h,你能夠看到有很多 #define 語句。

用於數組大小(維度)的 #define


咱們在 C語言編程中也可使用預處理常量(#define 語句定義)來定義數組的大小。例如:

#define MAX_DIMENSION 2000

int main(int argc, char *argv[])
{
    char string1[MAX_DIMENSION], string2[MAX_DIMENSION];
    // ...
}

你也許會問:「可是,不是說咱們不能在數組的中括號中放變量,甚至是 const 變量也不能夠嗎?」

對,可是 MAX_DIMENSION 並非一個變量,也不是一個 const 變量啊!就如以前說的,預處理器會在編譯以前把以上代碼替換爲以下:

int main(int argc, char *argv[])
{
    char string1[2000], string2[2000];
    // ...
}

這樣有一個好處,就如以前所說,若是未來你以爲你的數組大小要修改,能夠直接修改 MAX_DIMENSION 的數值,很是便捷。

在 #define 中的計算


咱們還能夠在定義預處理常量時(#define 語句中)作一些計算。

例如,如下代碼首先定義了兩個預處理常量 WINDOW_WIDTH(表示「窗口寬度」)和 WINDOW_HEIGHT(表示「窗口高度」),接着咱們能夠利用這兩個預處理常量來定義第三個預處理常量:PIXEL_NUMBER(意思是「像素數目」,等於「窗口寬度」 x 「窗口高度」),以下:

#define WINDOW_WIDTH 800
#define WINDOW_HEIGHT 600
#define PIXEL_NUMBER (WINDOW_WIDTH * WINDOW_HEIGHT)

在編譯以前,PIXEL_NUMBER 會被替換爲 800 x 600 = 480000 。

固然預處理常量對於基本的運算:+-*/% 都是支持的。

注意:用 #define 定義預處理常量時要儘可能多用括號括起來,否則會出現意想不到的結果,由於預處理常量只是簡單的替換。

系統預先定義好的預處理常量


咱們本身能夠定義不少預處理常量,C語言系統也爲咱們預先定義了幾個有用的預處理常量。

這些 C語言預約義的預處理常量通常都以兩個下劃線開始,兩個下劃線結束,例如:

  • __LINE__ :當前行號。
  • __FILE__ :當前文件名。
  • __DATE__ :編譯時的日期。
  • __TIME__ :編譯時的時刻。

這些預處理常量對於標明出錯的地方和調試是頗有用的,用法以下:

printf("錯誤在文件 %s 的第 %d 行\n", __FILE__, __LINE__);
printf("此文件在 %s %s 被編譯\n", __DATE__, __TIME__);

輸出以下:

錯誤在文件 main.c 的第 10 行
此文件在 Jun 8 2020 09:11:01 被編譯

不帶數值的 #define


咱們也能夠像以下這樣定義預處理常量:

#define CONSTANT

很奇怪吧,後面居然沒有對應的數值。

以上語句用於告訴預處理器:CONSTANT 這個預處理常量已經定義了,僅此而已。雖然它沒有對應的數值,可是它「存在」。

你也許要問:「這樣有什麼意義呢?」

這樣作的用處暫時還不明顯,可是咱們在這一課裏立刻會學到,請繼續讀下去。

4. 宏


咱們如今知道用 #define 語句能夠把一個數值綁定到一個名稱上,而後在預處理階段(編譯以前)預處理器就能夠在代碼裏用數值替換全部的預處理常量了,很是方便。例如:

#define NUMBER 10

意味着接下來你的代碼裏全部的 NUMBER 都會被替換爲 10。是簡單的「查找-替換」。

可是 #define 預處理命令還能夠作更厲害的事。

#define 還能夠用來替換… 一整個代碼體。當咱們用 #define 來定義一個預處理常量,這個預處理常量的值是一段代碼的時候,咱們說咱們建立了一個「宏」。

「宏」,英語是 macro。一開始可能不太好理解。這是一個編程術語,臺灣通常翻成「巨集」。能夠說是一種抽象,但在 C語言裏就只用於簡單的「查找-替換」。

趣事:以前某網站出現一個詞:「王力巨集」,原來這個網站在作簡體中文到繁體中文轉換時,把明星「王力宏」的名字中的「宏」替換爲了「巨集」,咱們的力宏就這麼「躺槍」了…

沒有參數的宏


下面給出一個很簡單的宏的定義:

#define HELLO() printf("Hello\n");

能夠看到,與以前的預處理常量不太同樣的是:名稱後多了一對括號,咱們立刻就來看這有什麼用處。

咱們用一段代碼來測試一下:

#include <stdio.h>

#define HELLO() printf("Hello\n");

int main(int argc, char *argv[])
{
    HELLO()

    return 0;
}

運行輸出:

Hello

是否是有點意思?不過暫時還不是那麼新穎。

須要理解的是:宏不過是在編譯以前的一些代碼的簡單替換。

上面的代碼在編譯前會被替換爲以下:

int main(int argc, char *argv[])
{
    printf("Hello\n");

    return 0;
}

若是你理解了這個,那對於宏的基本概念也差很少理解了。

你也許會問:「那咱們每個宏只能寫在一行上麼?」

不是的,只須要在每一行的結尾寫上一個 (反斜槓),就能夠開始寫新的一行了,而預處理器會把這些行當作一行。能夠說 起到了連接的做用。例如:

#include <stdio.h>

#define PRESENT_YOURSELF() printf("您好, 我叫 Oscar\n"); \
                           printf("我住在浙江杭州\n"); \
                           printf("我喜歡游泳\n");

int main(int argc, char *argv[])
{
    PRESENT_YOURSELF()

    return 0;
}

運行輸出:

您好,我叫 Oscar
我住在浙江杭州
我喜歡游泳

咱們注意到了,調用宏的時候,在末尾是沒有分號的。事實上,由於

PRESENT_YOURSELF()

這一行是給預處理器來處理的,因此不必以分號結尾。

有參數的宏


咱們剛學習了無參的宏,也就是括號裏沒有帶參數的宏。這樣的宏有一個好處就是可使代碼裏常常出現的較長的代碼段變得短一些,看起來簡潔。

可是,宏帶了參數才真正變得有趣起來。

#include <stdio.h>

#define MATURE(age) if (age >= 18) \
                    printf("你成年了\n");

int main(int argc, char *argv[])
{
    MATURE(25)

    return 0;
}

運行輸出:

你成年了

這樣是否是就有點像函數了?就是這麼酷炫。

上面的宏是怎麼運做的呢?

age 這個參數在實際調用宏的時候,會被替換爲括號裏的數值,這裏是 25。因此,整個宏就替換爲了:

if (25 >= 18)
    printf("你成年了\n");

不就是咱們熟悉的老朋友:if 語句麼。

上面的宏定義中,咱們也能夠用一個 else 來處理「你還未成年」的條件,本身動手試一下吧,不難。

固然咱們也能夠建立帶多個參數的宏,例如:

#include <stdio.h>

#define MATURE(age, name) if (age >= 18) \
                          printf("你已經成年了,%s\n", name);

int main(int argc, char *argv[])
{
    MATURE(32, "Oscar")

    return 0;
}

運行輸出:

你已經成年了,Oscar

好了,對於宏咱們須要瞭解的也差很少介紹完了。若是使用得當,宏是至關有用的。

可是有些時候,濫用宏也會產生不少難以調試的錯誤,因此宏是 C語言的一把雙刃劍。

一般咱們在 C語言的編程中是不須要常用宏的,由於宏有一個缺點:

宏只是簡單的替換,根本不檢查變量和參數類型,因此用得很差會出問題。

不過,不少複雜的庫,例如擅長圖形界面編程的 wxWidgets 和 Qt,就大量使用了宏。

因此對於宏,咱們須要理解。

5. 條件編譯


預處理命令除了有以上三個做用之外,還能夠實現「條件編譯」。聽起來有點玄乎,可是隻要語文沒有還給小學體育老師,那應該不難理解。

開個玩笑。咱們仍是一塊兒來看看以下的例子:

#if 條件1
/* 若是條件1爲真,將會被編譯的代碼 */
#elif 條件2
/* 若是條件2爲真,將會被編譯的代碼 */
#endif

是否是有點相似以前學過的 if 語句?

能夠看到:

  • 關鍵字 #if 是一個條件編譯塊的起始,在後面能夠插入一個條件。
  • 關鍵字 #elif(elif 是 else if 的縮寫)的後面能夠插入另外一個條件。
  • 關鍵字 #endif(end 表示「結束」)是一個條件編譯塊的結束。

與 if 語句不一樣的是,條件編譯沒有大括號。

你會發現「條件編譯」是至關有用的,它使咱們能夠按照不一樣的條件來選擇編譯哪些代碼。

與 if 語句相似,條件編譯塊必須有且只能有一個 #if,能夠沒有或有多個 #elif,必須有且只能有一個 #endif

若是條件爲真,那麼後面跟着的代碼會被編譯,若是條件爲假,後面的代碼就會在編譯時被忽略。

ifdef 和 #ifndef


如今咱們就來看看以前介紹的「沒有數值的 #define」的用處。

還記得嗎?

#define CONSTANT

咱們能夠

  • #ifdef 來表述:「若是此名稱已經被定義」。由於 ifdef 是 if defined 的縮寫,表示「若是已被定義」。
  • #ifndef 來表述:「若是此名稱沒有被定義」。由於 ifndef 是 if not defined 的縮寫,表示「若是沒有被定義」。

不得不重提英語對於編程進階的重要性,能夠參看我以前寫的文章:對於程序員, 爲何英語比數學更重要? 如何學習

例如咱們有以下代碼:

#define WINDOWS

#ifdef WINDOWS
/* 當 WINDOWS 已經被定義的時候要編譯的代碼 */
#endif

#ifdef LINUX
/* 當 LINUX 已經被定義的時候要編譯的代碼 */
#endif

#ifdef MAC
/* 當 MAC 已經被定義的時候要編譯的代碼 */
#endif

能夠看到,用這樣的方法,能夠很方便地應對不一樣平臺的編譯,使咱們的代碼實現跨平臺。

好比,我要編譯針對 Windows 系統的代碼,那就在開始處寫:

#define WINDOWS

我要編譯針對 Linux 系統的代碼,那就改爲:

#define LINUX

若是是 macOS 系統,那就改爲:

#define MAC

固然了,每次修改代碼以後都要從新編譯(畢竟沒有那麼神奇)。

使用 #ifndef 來避免「重複包含」


#ifndef 是很是有用的,常常用於 .h 頭文件中,以免「重複包含」。

什麼是「重複包含」呢?

其實不難理解,設想如下狀況:

我有兩個頭文件,分別命名爲 A.h 和 B.h 。在 A.h 中我寫了

#include "B.h"

而不巧在 B.h 中我寫了

#include "A.h"

要知道,在代碼複雜度提升之後,這樣的狀況不是不可能發生的。不少時候,咱們一個文件裏要 inculde 好多個頭文件,很容易暈。

這樣一來,A.h 文件須要 B.h 來運行,而 B.h 文件須要 A.h 來運行。

若是咱們稍加思索,就不難想到會發生什麼:

  • 電腦讀入 A.h 文件,發現須要包含 B.h。
  • 電腦在 A.h 中包含進 B.h 文件的內容,但是在 B.h 文件的內容裏又發現須要包含 A.h。
  • 如此循環往復,何時是個頭啊…

你確定認爲這會永不止息…

事實上,碰到這種狀況,預處理器會中止,而且拋出「我受不了這麼多包含啦!」的錯誤,你的程序就不能經過編譯。

那如何來避免這樣的悲劇呢?

下面就是解方。而且從今之後,我強烈建議你們在每個 .h 頭文件中都這樣作!讓我任性一回吧...

#ifndef DEF_FILENAME  // 若是此預處理常量還未被定義,便是說這個文件未被包含過
#define DEF_FILENAME  // 定義此預處理常量,以免重複包含

/* file.h 文件的內容 (其餘的 #include,函數原型,#define,等...) */

#endif

如上所示,在 #ifndef#endif 之間咱們放置 .h 文件的內容(其餘的 #include,函數原型,#define,等)。

咱們來理解一下到底這段代碼是怎麼起做用的?(我本身第一次碰到這個技術時也有點不太理解):

假使咱們的 file.h 文件是第一次被包含(被 include),預處理器讀到開頭的那句話:

#ifndef DEF_FILENAME

意思是「DEF_FILENAME 這個預處理常量尚未被定義」,這個條件是真的。因此預處理器就進入 #if 語句內部啦(和普通的 if 語句相似的機制)。

接着預處理器就讀到第二句命令:

#define DEF_FILENAME

這句的意思是「定義 DEF_FILENAME 這個預處理常量」。因此預處理器乖乖地執行,定義 DEF_FILENAME。

接着它就將 file.h 頭文件的主體內容都包含進調用 #include "file.h" 或者 #include <file.h> 的那個文件。

這樣的話。下一次這個 file.h 頭文件再被其餘文件包含時,`

#ifndef DEF_FILENAME

這個條件就不爲真了,預處理器就不會執行條件編譯內部的語句了,天然就不會再把頭文件的主體內容包含了。

這樣就能巧妙地避免「重複包含」。

固然,那個預處理常量的名稱不必定要和個人同樣,也不必定要大寫,可是最好大寫,是約定俗成的用法。

可是每個頭文件的所用常量名稱必須不一樣,不然,只有第一個頭文件會被包含。

很是建議你們有空去看一下標準庫的頭文件,如 stdio.h,stdlib.h,等。你會發現它們都是以這樣的方式寫的(開頭 #ifndef,結尾 #endif)。

6. 總結


  1. 預處理器是這樣一個程序,它在編譯以前執行,它先分析源代碼,而後作出必定修改。
  2. 預處理命令有好幾種。#include 命令用於在一個文件中插入另外一個文件的內容。
  3. #define 命令定義一個預處理常量。以後預處理器就會把代碼裏全部 #define 定義的常量名稱替換成對應的值。
  4. 宏是一些代碼塊,定義也要藉助 #define。宏能夠接受參數。
  5. 咱們也能夠用預處理器的語言來寫一些預處理條件,以實現條件編譯。通常咱們使用關鍵字:#if#elif#endif 等。
  6. 爲了防止一個頭文件被屢次包含,咱們會用條件編譯和預處理常量的組合來「保護」它。以後咱們寫的 .h 頭文件都會採用這種方式,也很建議採用。

7. 第二部分第六課預告


今天的課就到這裏,一塊兒加油吧!

下一課:C語言探索之旅 | 第二部分第六課:建立你本身的變量類型


我是 謝恩銘,公衆號「程序員聯盟」(微信號:coderhub)運營者,慕課網精英講師 Oscar 老師,終生學習者。 熱愛生活,喜歡游泳,略懂烹飪。 人生格言:「向着標杆直跑」
相關文章
相關標籤/搜索