C語言入門之C 預處理器

gcc/cc xxx.c 可以編譯鏈接C源程序生成一個可執行文件 a.out

整個過程中可以劃分為以下的4步流程:

(1)預處理/預編譯: 主要用於包含頭文件的擴展,以及執行宏替換等 //加上 -E

(2)編譯:主要用於將高級語言程序翻譯成彙編語言,得到彙編語言 //加上 -S

(3)彙編:主要用於將彙編語言翻譯成機器指令,得到目標文件 //加上 -c

(4)鏈接:主要用於將目標文件和標準庫鏈接,得到可執行文件 //加上 -o

-E 實現預處理的執行,默認將處理結果輸出到控制台,可以通過-o選項指定輸出到xxx.i文件中,預處理文件中包含了很多頭文件,類型的別名,以及各種函數的聲明等等

-S 實現編譯的處理,得到一個.s為後綴的彙編文件

Advertisements

-c 實現彙編的處理,得到一個.o為後綴的目標文件

gcc/cc xxx.o 實現鏈接的處理,默認生成可執行文件a.out,可以通過選項-o來指定輸出文件名

參看:C語言再學習 -- GCC編譯過程

根據上面的描述我們可以知道預處理的所在時期,編譯程序之前,先由預處理器檢查程序,根據程序中使用的預處理指令,預處理器用符號縮略語所代表的內容替換程序中的縮略語。下面詳細介紹各個預處理指令:

一、#define的用法

#define 是一個預處理指令,這個預處理執行可以定義宏。與所有預處理指令一樣,預處理指令#define用#符號作為行的開頭。預處理指令從#開始,到其後第一個換行符為止。也就是說,指令的長度限於一行代碼。如果想把指令擴展到幾個物理行,可使用反斜線后緊跟換行符的方法實現,該出的換行符代表按下回車鍵在源代碼文件中新起一行所產生的字元,而不是符號 \n 代表的字元。在預處理開始錢,系統會刪除反斜線和換行符的組合,從而達到把指令擴展到幾個物理行的效果。可以使用標準C註釋方法在#define行中進行註釋。

Advertisements

//使用反斜線+回車

我建立了一個C語言學習交流群,在群里有很多新手教程和大神交流q群,怎麼找到先搜索178在加上923最後還有056,這樣你就可以找到組織一起學習,群里人很多都是新手在大神的幫助下,已經成長為高薪工程師,你不來對我們沒有任何損失,但是只要你來了基本可以有很多的收穫。

每一個#define行由三部分組成:

第一部分,指令#deine自身。

第二部分,所選擇的縮略語,這些縮略語稱為宏(分為對象宏和函數宏)。宏的名字中不允許有空格,而且必須遵循C變數命名規則:只能使用字母、數字和下劃線(_),第一個字元不能為數字。習慣上宏名用大寫字母表示,以便於與變數區別。但也允許用小寫字母。

第三部分,(#define行的其餘部分)稱為替換列表或主體。

注意,結尾沒有分號

下面來看一個例子:

#include <stdio.h>

相同定義意味著主體具有相同順序的語言符號。因此,下面兩個定義相同:

#define OW 2 * 2

#define OW 2 * 2

兩者都有三個相同的語言符號,而且額外的空格不是主體的一部分。下面的定義則被認為是不同的:

#define OW 2*2

上式只有一個(而非三個)語言符號,因此與前面兩個定義不同。可以使用#undef指令重新定義宏。

宏所代表的數字可以在編譯命令中指定(使用-D選項)

/*

函數宏:

通過使用參數,可以創建外形和作用都與函數相似的類函數宏。宏的參數也用圓括弧括起來。類函數宏的定義中,用圓括弧括起來一個或多個參數,隨後這些參數出現在替換部分。

#include <stdio.h>

SQUARE(x+2) 輸出結果是14,而不是想要的6*6 = 36。這是因為預處理器不進行計算,而只進行字元串替換。在出現x的地方,預處理都用字元串 x+2進行替換。x*x 變為 x+2*x+2 根據運算符優先順序,則結果為 14

100/SQUARE(2)輸出結果是 100,而不是想要的 25。因為,根據優先順序規則,表達式是從左到右求值的。

100/2*2 = 100

要處理前面兩個示例中的情況,需要如下定義:

#define SQUARE(x) ((x) * (x))

從中得到的經驗是使用必須的足夠多的圓括弧來保證以正確的順序進行運行和結合。

SQUARE(++x) 根據編譯器的不同會出現兩種不同的結果。解決這個問題的最簡單的方法是避免在宏的參數中使用++x。一般來說,在宏中不要使用增量或減量運算符。

參看:C 語言再學習 -- 運算符與表達式

利用宏參數創建字元串:#運算符

在類函數宏的替換部分中,#符號用作一個預處理運算符,它可以把語言符號轉化為字元串。

例如:如果x是一個宏參量,那麼#x可以把參數名轉化為相應的字元串。該過程稱為字元串化。

#include <stdio.h>
#include <stdio.h>

預處理器的粘合劑:##運算符

和#運算符一樣,##運算符可以用於類函數宏的替換部分。另外,##還可用於類對象宏的替換部分。這個運算符把兩個語言符號組合成單個語言符號。

#include <stdio.h>

宏用於簡單函數:

#include <stdio.h>

下面是需要注意的幾點:

1、宏的名字中不能有空格,但是在替代字元串中可以使用空格。ANSI C 允許在參數列表中使用空格。

2、用圓括弧括住每個參數,並括住宏的整體定義。

3、用大寫字母表示宏函數名,便於與變數區分。

4、有些編譯器限制宏只能定義一行。即使你的編譯器沒有這個限制,也應遵守這個限制。

5、宏的一個優點是它不檢查其中的變數類型,這是因為宏處理字元型字元串,而不是實際值。

面試:用預處理指令#define 聲明一個常數,用以表明1年中有多少秒(忽略閏年問題)

#define SEC (60*60*24*365)UL

考察內容:

1、懂得預處理器將為你計算常量表達式的值,因此,可直接寫出你是如何計算一年中有多少秒而不是計算出實際的值,這樣更清晰而沒有代價。

2、意識到這個表達式將使一個16 位機的整形數溢出,因此要用到長整形符號 L ,告訴編譯器這個常數是長整形數。

3、如果你在你的表達式中用到UL(表示無符號長整型),那麼你有了一個好的起點。

面試:寫一個「標準」宏MIN ,這個宏輸入兩個參數並返回較小的一個

#define MIN(A,B) ((A) <= (B) ? (A) : (B))

我建立了一個C語言學習交流群,在群里有很多新手教程和大神交流q群,怎麼找到先搜索178在加上923最後還有056,這樣你就可以找到組織一起學習,群里人很多都是新手在大神的幫助下,已經成長為高薪工程師,你不來對我們沒有任何損失,但是只要你來了基本可以有很多的收穫。

考察內容:

1、三目表達式的使用

2、使用必須的足夠多的圓括弧來保證以正確的順序進行運行和結合

3、進一步討論,在宏中不要使用增量或減量運算符

參看:宏名必須用大寫字母嗎?

研究:C語言中用宏定義(define)表示數據類型和用typedef定義數據類型有什麼區別?

宏定義只是簡單的字元串代換,是在預處理完成的,而typedef是在編譯時處理的,它不是作簡單的代換,而是對類型說明符重新命名。被命名的標識符具有類型定義說明的功能。

請看下面的例子:

#define P1 int *

typedef (int *) P2

從形式上看這兩者相似,但在實際使用中卻不相同。

下面用P1、P2說明變數時就可以看出它們的區別:

P1 a, b; 在宏代換后變成: int *a, b; 表示 a 是指向整型的指針變數,而 b 是整型變數。

P2 a, b; 表示a,b都是指向整型的指針變數。因為PIN2是一個類型說明符。

由這個例子可見,宏定義雖然也可表示數據類型, 但畢竟是作字元代換。在使用時要分外小心,以避出錯。

總結,typedef和#define的不同之處:

1、與#define不同,typedef 給出的符號名稱僅限於對類型,而不是對值。

2、typedef 的解釋由編譯器,而不是是處理器執行。

3、雖然它的範圍有限,但在其受限範圍內,typedef 比 #define 更靈活。

用於定義字元串,尤其是路徑

A),#define ENG_PATH_1 E:\English\listen_to_this\listen_to_this_3

B),#define ENG_PATH_2 「 E:\English\listen_to_this\listen_to_this_3」

A 為 定義路徑, B 為定義字元串

C), #define ENG_PATH_3 E:\English\listen_to_this\listen\

_to_this_3

還沒發現問題?這裡用了 4 個反斜杠,到底哪個是接續符?回去看看接續符反斜杠。反斜杠作為接續符時,

在本行其後面不能再有任何字元,空格都不行。所以,只有最後一那給 ENG_PATH_1 加上雙引號不就成了:「ENG_PATH_1」。但是請注意:有的系統里規定路徑的要用雙反斜杠「 \\」 ,比如:

#define ENG_PATH_4 E:\\English\\listen_to_this\\listen_to_this_3

二、#undef 指令

取消定義一個給定的 #define。

例如有如下宏定義:

#define LIMIT 40

則指令

#undef LIMIT

會取消該定義。

現在就可以重新定義LIMIT,以使它有一個新的值。即使開始沒有定義LIMIT,取消LIMIT的定義也是合法的。如果想使用一個特定名字,但又不能確定前面是否已經使用了該名字,為安全起見,就可以取消該名字的定義。

注意:#define 宏的作用域從文件中的定義點開始,直到用 #undef 指令取消宏為止,或直到文件尾為止(由二者中最先滿足的那個結束宏的作用域)。還應注意,如果用頭文件引入宏,那麼,#define 在文件中的位置依賴 #define 指令的位置。

#include <stdio.h>
#include <stdio.h>

三、文件包含:#include

預處理器發現#include指令后,就會尋找後跟的文件名並把這個文件的內容包含但當前文件中。被包含文件中的文件將替換源代碼文件中的#include指令,就像你把被包含文件中的全部內容鍵入到源文件中的這個特定位置一樣。

#include 指令有兩種使用形式:

1) #include <filename.h> 文件名放在尖括弧中

在UNIX系統中,尖括弧告訴預處理器在一個或多個標準系統目錄中尋找文件。

如: #include <stdio.h>

查看:

ls /usr/include

ls kernel/include

2) #include "filename.h" 文件名放在雙引號中

在UNIX系統中,雙引號告訴預處理器現在當前目錄(或文件名中指定的其他目錄)中尋找文件,然後在標準位置尋找文件。

如: #include "hot.h" #include "/usr/buffer/p.h"

習慣上使用後綴 .h 表示頭文件,這類文件包含置於程序頭部的信息。頭文件經常包含預處理語句。有些頭文件由系統提供。但也可以自由創建自己的頭文件。

擴展:C語言再學習 -- 常用頭文件和函數(轉)

Lniux的文件系統中有一個大分組,它包含了文件系統中所有文件,這個大的分組用一個專門的目錄表示,這個目錄叫做根目錄,根目錄可以使用「/」表示。

路徑可以用來表示文件或者文件夾所在的位置,路徑是從一個文件夾開始走到另一個文件夾或者文件位置中間的這條路。把這條路經過的所有文件夾名稱按順序書寫出來的結果就可以表示這條路。

路徑分為絕對路徑和相對路徑

絕對路徑:起點必須是根目錄,如 /abc/def 所有絕對路徑一定是以「/」作為開頭的

相對路徑:可以把任何一個目錄作為起點,如../../abc/def 相對路徑編寫時不應該包含起點位置

相對目錄中「..」表示上層目錄

相對路徑中用「.」表示當前

終端窗口裡的當前目錄是所有相對路徑的起點,當前目錄的位置是可以修改的。

pwd 命令可以用來查看當前目錄的位置

cd 命令可以用來修改當前目錄位置

ls 命令可以用來查看一個目錄的內容

四、條件編譯

參看:條件編譯#ifdef的妙用詳解_透徹

#if:表示如果...

#ifdef:表示如果定義...

#ifndef:表示如果沒有定義...

#else:表示否則...與#ifdef/#ifndef搭配使用 //筆試題 注意,沒有#elseif

#elif:表示否則如果...與#if/#ifdef/#ifndef搭配使用

#endif:表示結束判斷,與#if/#ifdef/#ifndef搭配使用

注意:#if 和 if 區別

#if=>主要用於編譯期間的檢查和判斷

if =>主要用於程序運行期間的檢查和判斷Z

最常見的形式:

#ifdef 標識符

作用:當標識符已經被定義過(一般用#define命令定義),則對程序段1進行編譯,否則編譯程序段2。其中#else部分也可以沒有,即:

#ifdef 標識符

這裡的「程序段」可以是語句組,也可以是命令行。這種條件編譯可以提高C源程序的通用性。如果一個C源程序在不同計算機系統上運行,而不同的計算機又有一定的差異。例如,我們有一個數據類型,在Windows平台中,應該使用long類型表示,而在其他平台應該使用float表示,這樣往往需要對源程序做必要的修改,這就降低了程序的通用性。可以用以下的條件編譯:

#ifdef WINDOWS

如果在Windows上編譯程序,則可以在程序的開始加上

#define WINDOWS

這樣則編譯下面的命令行:

#define MYTYPE long

如果在這組條件編譯命令之前曾出現以下命令行:

#define WINDOWS 0

則預編譯后程序中的MYTYPE都用float代替。這樣,源程序可以不必任何修改就可以用於不同類型的計算機系統。當然以上介紹的只是一種簡單的情況,可以根據此思路設計出其他的條件編譯。

例如,在調試程序時,常常希望輸出一些所需的信息,而在調試完成後不再輸出這些信息。可以在源程序中插入以下的條件編譯段:

#ifdef DEBUG

如果在它的前面有以下命令行:

#define DEBUG

則在程序運行時輸出file指針的值,以便調試分析。調試完成後只需將這個define命令行刪除即可。有人可能覺得不用條件編譯也可以達到此目的,即在調試時加一批printf語句,調試后一一將prntf語句刪除。的確,這是可以的。但是,當調試時加的printf語句比較多時,修改的工作量是很大的。用條件編譯,則不必一一刪除printf語句。只需刪除前面的一條#define DEBUG 命令即可,這時所有的用DEBUG 作標識符的條件編譯段都使其中的printf語句不起作用,起到統一控制的作用,如同一個「開關」一樣。

有時也採用下面的形式:

#ifndef 標識符

只是第一行與第一種形式不同:將「#ifdef」改為「#ifndef」。它的作用是,若標識符未被定義則編譯程序段1,否則編譯程序段2。這種形式與第一種形式的作用相反。

一般地,當某文件包含幾個頭文件,而且每個頭文件都可能定義了相同的宏,使用#ifndef可以防止該宏重複定義。

#ifndef 指令通常用於防止多次包含同一文件,也就是說,頭文件可採用類似下面幾行的設置:

//頭文件衛士

還有一種形式,就是#if 後面跟一個表達式,而不是一個簡單的標識符:

#if 表達式

它的作用是:當指定的表達式為真(非零)時就編譯程序段1,否則編譯程序段2.可以事先給定一定條件,使程序在不同的條件下執行不同的功能。例如:

#include <stdio.h>

這種形式也可以用作註釋用:#if 1 和 #if 0

#include <stdio.h>

最後一種形式

#if 標識符

#if...#elif(任意多次)...#else...#endif,以上結構可以從任意邏輯表達式選擇一組編譯,這種結構可以根據任意邏輯表達式進行選擇。

/*

這裡,define是一個預處理運算符。如果 define 的參數已用#define定義過,那麼define返回1,否則返回 0 。這種方法的優點在於它可以和#elif一起使用。

應用示例:

我們主要使用以下幾種方法,假設我們已在程序首部定義:

#define DEBUG

#define TEST

1、利用#ifdef / #endif 將程序功能模塊包括進去,以向某用戶提供該功能.

在程序首部定義#define HNLD:

如果不許向別的用戶提供該功能,則在編譯之前將首部的HNLD加下劃線即可。

2、在每一個子程序前加上標記,以便追蹤程序的運行。

#ifdef DEBUG

3、避開硬體的限制。有時一些具體應用環境的硬體不一樣,但限於條件,本地缺乏這種設備,於是繞過硬體,直接寫出預期結果。具體做法是:

#ifndef TEST

有一個問題,如何確保使用的標識符在其他任何地方都沒有定義過?

通常編譯器提供商採用下述方法解決這個問題:用文件名做標識符,並在文件名中使用大寫字母、用下劃線代替文件名中的句點字元、用下劃線(可能使用兩條下劃線)做前綴和後綴。例如,檢查頭文件read.h,可以發現許多類似的語句:

#ifndef __READ_H__ //作為開頭的預處理指令則當它後面的宏名稱被定義過則編譯后一組否則編譯前一組

參看:C語言再學習 -- 標識符

擴展:extern "C"

通過 extern "C" 可以要求 C++ 編譯器按照 C方式處理函數介面,即不做換名,當然也就無法重載。

1) C 調 C++,在 C++ 的頭文件如下設置:

extern "C" int add (int x, int y);
//示例 add.h

2)C++ 調 C,在C++ 的主函數如下設置:

extern "C" {
//示例 main.cpp

五、預定義宏

__DATE__進行預處理的日期(「Mmm dd yyyy」形式的字元串文字)

__FILE__代表當前源代碼文件名的字元串文字

__BASE_FILE__獲取正在編譯的源文件名

__LINE__代表當前源代碼文件中的行號的整數常量

__TIME__源文件編譯時間,格式為「hh: mm: ss」

__STDC__設置為 1時,表示該實現遵循 C標準

__STDC_HOSTED__為本機環境設置為 1,否則設為 0

__STDC_VERSION__為C99時設置為199901L

__FUNCTION__或者 __func__ 獲取所在的函數名(預定義標識符,而非預定義宏)

#include <stdio.h>

六、常用的新指令

#line 整數n =>表示修改代碼的行數/指定行號 插入到程序中表示從行號n開始執行,修改下一行的行號為n

#error 字元串 => 表示產生一個錯誤信息

#warning 字元串 => 表示產生一個警告信息

//#line 預處理指令的使用
//#error和#warning的使用

七、#pragma

#pragma GCC dependency 文件名

表示當前文件依賴於指定的文件,如果當前文件的最後一次,修改的時間早於依賴的文件,則產生警告信息

#include <stdio.h>

#pragma GCC poison 標示符

表示將後面的標示符設置成毒藥,一旦使用標示符,則產生錯誤或警告信息

//毒藥的設置

#pragma pack (整數n)

表示按照整數n倍進行補齊和對齊

//設置結構體的對齊和補齊方式

#pragma message

message 參數: message 參數是我最喜歡的一個參數,它能夠在編譯信息輸出窗,口中輸出相應的信息,這對於源代碼信息的控制是非常重要的。其使用方法為:

我建立了一個C語言學習交流群,在群里有很多新手教程和大神交流q群,怎麼找到先搜索178在加上923最後還有056,這樣你就可以找到組織一起學習,群里人很多都是新手在大神的幫助下,已經成長為高薪工程師,你不來對我們沒有任何損失,但是只要你來了基本可以有很多的收穫。

#pragma message(「消息文本」)

當編譯器遇到這條指令時就在編譯輸出窗口中將消息文本列印出來。當我們在程序中定義了許多宏來控制源代碼版本的時候,我們自己有可能都會忘記有沒有正確的設置這些宏,此時我們可以用這條指令在編譯的時候就進行檢查。假設我們希望判斷自己有沒有在源代碼的什麼地方定義了_X86 這個宏可以用下面的方法.

#define _X86

當我們定義了_X86 這個宏以後,應用程序在編譯時就會在編譯輸出窗口裡顯示「_X86 macro activated!」。我們就不會因為不記得自己定義的一些特定的宏而抓耳撓腮了.

#pragma code_seg

另一個使用得比較多的 pragma 參數是 code_seg。格式如:

#pragma code_seg( ["section-name"[,"section-class"] ] )

它能夠設置程序中函數代碼存放的代碼段,當我們開發驅動程序的時候就會使用到它。

#pragma once

#pragma once (比較常用)

只要在頭文件的最開始加入這條指令就能夠保證頭文件被編譯一次,這條指令實際上在Visual C++6.0 中就已經有了,但是考慮到兼容性並沒有太多的使用它。

#pragma hdrstop

#pragma hdrstop 表示預編譯頭文件到此為止,後面的頭文件不進行預編譯。 BCB 可以預編譯頭文件以加快鏈接的速度,但如果所有頭文件都進行預編譯又可能占太多磁碟空間,所以使用這個選項排除一些頭文件。有時單元之間有依賴關係,比如單元 A 依賴單元 B,所以單元 B 要先於單元 A 編譯。你可以用#pragma startup 指定編譯優先順序,如果使用了#pragma package(smart_init) , BCB就會根據優先順序的大小先後編譯。

#pragma resource

#pragma resource "*.dfm"表示把*.dfm 文件中的資源加入工程。 *.dfm 中包括窗體外觀的定義。

#pragma warning

#pragma warning( disable : 4507 34; once : 4385; error : 164 )

等價於:

#pragma warning(disable:4507 34) // 不顯示 4507 和 34 號警告信息

#pragma warning(once:4385) // 4385 號警告信息僅報告一次

#pragma warning(error:164) // 把 164 號警告信息作為一個錯誤。

同時這個 pragma warning 也支持如下格式:

#pragma warning( push [ ,n ] )

#pragma warning( pop )

這裡 n 代表一個警告等級(1---4)。

#pragma warning( push )保存所有警告信息的現有的警告狀態。

#pragma warning( push, n)保存所有警告信息的現有的警告狀態,並且把全局警告等級設定為 n。

#pragma warning( pop )向棧中彈出最後一個警告信息,在入棧和出棧之間所作的一切改動取消。例如:

#pragma warning( push )

#pragma warning( disable : 4705 )

#pragma warning( disable : 4706 )

#pragma warning( disable : 4707 )

//.......

#pragma warning( pop )

在這段代碼的最後,重新保存所有的警告信息(包括 4705, 4706 和 4707)。

#pragma comment

#pragma comment(...)

該指令將一個註釋記錄放入一個對象文件或可執行文件中。常用的 lib 關鍵字,可以幫我們連入一個庫文件。 比如:

#pragma comment(lib, "user32.lib")

該指令用來將 user32.lib 庫文件加入到本工程中。linker:將一個鏈接選項放入目標文件中,你可以使用這個指令來代替由命令行傳入的或者在開發環境中設置的鏈接選項,你可以指定/include 選項來強制包含某個對象,例如:

#pragma comment(linker, "/include:__mySymbol")

Advertisements

你可能會喜歡