首頁 > 軟體

C語言零基礎徹底掌握預處理下篇

2022-08-09 14:00:14

1、條件編譯

1.1 條件編譯如何使用

C語言提供的條件編譯的功能可以讓我們按照不同的條件去編譯不同的程式部分,從而產生不同目的碼檔案。

第一種形式:

#ifdef 識別符號

程式段1

#else

程式段2

#endif

它的功能是,如果識別符號已經被 #define 定義了,則只會對程式段1進行編譯,不會對程式段2進行編譯,如果沒有被定義則反之,如果我們不需要程式段2,也可以省去 #else 和他對應的程式段。

第二種形式:

#ifndef 識別符號

程式段1

#else

程式段2

#endif

第二種形式與第一種形式的區別是將 ifdef 改為 ifndef,它的功能是,如果識別符號沒有被 #dfine 定義,則對程式段1進行編譯,不會對程式段2進行編譯,如果被定義了則反之,如果我們不需要程式段2,也可以省去 #else 和他對應的程式段。

第三種形式:

#if 常數表示式

程式段1

#else

程式段2

#endif

第三種形式的功能是:如果常數表示式的值為真(非0),則對程式段1進行編譯,否則對程式段2進行編譯,因此可以使程式在不同條件下,完成不同的功能。

至於裡面還可以新增 #elif 命令,意義與 else if 相同,形成一個 if else 階梯狀語句,可進行多種編譯選擇。

注意:如果定義空宏則會報錯,因為 #if 後面必須要更常數表示式!

1.2 用 #if 模擬 #ifdef

此程式碼的意思是,如果 PRINT 宏被定義了,則執行第一個列印函數,否則執行第二個列印函數,同時我們也可以模擬 #ifndef,只需前面加個邏輯非就可以 ' ! ',例如:#if (!defined(PRINT))

就這樣完了嗎?其實並沒有,在更復雜的專案中,往往會出現兩個或多個宏需要同時定義才能滿足需求,我舉一個很簡單的例子,如果我定義了 C 宏和 CPP 宏,我才可以編譯所對應的程式碼:

如上程式碼就需要兩個宏都被定義才能編譯下面的程式段,相信學習過邏輯與的小夥伴應該很容易理解吧,那麼我們如果需要兩個都未定義才能編譯下面的程式段呢?如何寫?

兩個都未定義才編譯: #if (!defined(C) && !defined(CPP))前面分別加邏輯非就可以 ' ! '

或者:#if (!(defined(C) || defined(CPP)))本程式碼中邏輯或只要有一個被定義,就為真,然後執行邏輯非,這樣也能保證兩個都未定義才進行編譯!

至於最後用不用大括號給括起來,我的建議是括起來,這樣我們閱讀程式碼會更直觀!

既然出現了邏輯與,是不是也可以出現邏輯或呢?當然上面已經有例子了,但是這裡我就不一一演示了,感興趣的可以下來自己去嘗試一下。

條件編譯支援巢狀:

這裡其實和我們平常用的 if 巢狀式是似的,也很容易理解,這裡我們就不細說,有一點要注意的就是,條件編譯每個 #if 都需要有對應的 #endif 來結束

1.3 為何要有條件編譯

我們先對我們上面2小節的內容做一個總結:條件編譯本質上是讓編譯器對程式碼進行裁剪!

本質認識:條件編譯,其實就是編譯器根據實際情況,對程式碼進行裁剪,而這裡 “實際情況” ,取決於程式碼平臺,程式碼本身的業務邏輯。

  • 可以只保留當前最需要的程式碼邏輯,其他去掉,可以減少生成程式碼的大小
  • 可以寫出跨平臺的程式碼,讓一個具體業務,在不同平臺編譯的時候,可以有同樣的表現

條件編譯都用在哪些地方呢?

張三有個公司,公司有個專案,專案對應的軟體又有專業版,免費版,精簡版等等...

難道每個版本都對應著不同的程式碼嗎?不是的,這樣維護起來太麻煩了,其實所謂不同的版本,本質就是功能上的有和無,所以在技術層面上,為了更好的維護,當然可以使用條件編譯,需要哪個版本,就是用條件編譯裁剪就行。

著名的 Linux 核心,功能上,其實也是用條件編譯進行功能裁剪的,用來滿足不同平臺的軟體。

2、檔案包含

2.1 #include 究竟幹了什麼

我相信 #include 對於每個程式設計小夥伴來說都不陌生,很多人寫 C 語言第一件事就是寫上 #include <stdio.h> 可能老師會告訴你們這是包含標準輸入輸出標頭檔案,至於如何包含的,可能不會跟你講。那今天我們就來通過預處理來看一看到底是如何包含的:

我們來寫上一小段程式碼:

前面說過,預處理會將標頭檔案展開,去註釋,宏替換,條件編譯等等

在 Linux 環境下我們可以執行命令:gcc -E test.c -o test.i保留預處理之後的檔案並命名為 test.i

為了更好的對比,我們執行 vim 命令模式下的 vs 指令:vs/sur/include/tdio.h 也就是開啟標準輸入輸出的標頭檔案:

看到預處理的結果之後,發現檔案大小比我們實際程式碼要大得多!

結論:#include 本質是把標頭檔案相關內容,拷貝到原始檔中。

2.2 防止標頭檔案重複包含的條件編譯是如何做到的

既然我們會包含標頭檔案,那有沒有可能存在標頭檔案重複被包含的可能性呢?導致我們標頭檔案被重複拷貝?

這裡可能會有很多老師也教過,同學們啊,我們寫標頭檔案的時候一定要寫如下程式碼啊,這是防止標頭檔案重複包含的啊:

#ifndef _TEST_H_
#define _TEST_H_
#include <stdio.h>
#define MAX 999
int g_val = 10;
extern void Print();
...
#endif

如上程式碼很多小夥伴都知道在#ifndef _TEST_H_ 和 #endif 之間寫的標頭檔案包含,宏定義,全域性變數,函數宣告,都不會被重複拷貝,為什麼呢?他是如何做到的?我們實驗證明 (如下兩張圖最右邊是預處理之後的結果) :

如下程式碼是沒有帶上條件編譯防止標頭檔案重複包含,但在原始檔已經重複包含的例子:

我們加上#ifndef _TEST_H_ 和 #endif在來看重複包含的效果:

已經沒有重複拷貝的情況了,看來確實有防止標頭檔案重複包含的效果!

那麼這條語句是如何做到的呢?

我們前面學過 #ifndef 如果沒有定義這個宏,則執行後續語句,當第一次我們標頭檔案展開的時候,確實沒有定義_TEST_H_ 這個宏,所以會執行後續的語句,但是在第一次展開的時候我們立馬定義了_TEST_H_ 宏,所以我們重複包含標頭檔案第二次展開的時候,這個宏已經被定義了,所以也就不會去執行#ifndef後續語句了!

結論:所有標頭檔案都得帶上條件編譯,防止標頭檔案重複包含!當然也可以直接 #pragma once

重複包含的一定會報錯嗎?顯然是不會的,但是會引起多次拷貝,會影響編譯效率。

3、選學內容

3.1 #error 預處理

#error 預處理指令的作用是:編譯程式時,只要遇到 #error 就會生成一個編譯錯誤提示訊息,並停止編譯:

3.2 #line 預處理

#line 的作用時改變當前行數和檔名稱,他們是在編譯程式中預先定義的識別符號。這裡我就不給你們看執行結果了,感興趣的可以複製程式碼下去自行了解下哦:

int main()
{
	printf("%s, %dn", __FILE__, __LINE__); //C預定義符號,代表當前檔名和程式碼行號
#line 60 "hehe.h" //客製化化完成
	printf("%s, %dn", __FILE__, __LINE__);
	return 0;
}

本質其實是可以客製化化你的檔名稱和程式碼行號,很少使用!

3.3 #pragma 預處理

3.3.1 #pragma message

message 引數他能在編譯資訊輸出視窗中輸出相應的資訊,這對於原始碼資訊的控制是非常重要的。

#define TEST
int main()
{
#ifdef TEST
#pragma message("TEST macor activated!")
#endif
    return 0;
}

當我們定義了 TEST 這個宏後,應用程式在編譯時就會在編譯輸出視窗裡顯示TEST macor activated! 因此我們就不會因為不記得自己定義的一些宏而著急了!

3.3.2 #pragma once

這個還是比較常用的,只要在標頭檔案的最開始加入這條指令就能夠保證標頭檔案被編譯一次,但是考慮到相容性的問題,並沒有太多的使用。

3.3.3 #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 號警告資訊作為一個錯誤

當使用 windows vs 環境的小夥伴們,在使用庫函數的時候比如 scanf 會說這個函數不安全,推薦你使用 scanf_s,那我們要保證程式碼可以移植性如何辦呢?通過檢視報錯發現是 4996 報錯,那我們則可以:

#pragma warning(disable : 4996) //這樣就解決問題了!

3.3.4 #pragma pack

設定結構體記憶體對齊,我們還沒更新到結構體,加上用的並不算多,所以感興趣的可以先去自行研究哦。

3.4 # 和 ##

假設說我們今天定義了一個列印宏:

#define PRINT(x) printf("hello x is %d.n", ((x)*(x)))

呼叫宏 PRINT(8); 則會輸出:hello x is 64.

如果你希望字串中包含宏引數,那我們就可以使用 "#",它可以把語言符號轉換成字串:

#define PRINT(x) printf("hello "#x" is %d.n", ((x)*(x)))

這樣呼叫PRINT(8); 則會輸出:hello 8is 64.

## 使用起來也很簡單,就是將兩個相連的符號,連線成為一個符號:

#define XNAME(n) x##n

如果這樣使用宏: XNAME(8)則會被展開成為:x8

在 "#" 或 "##" 預處理操作符相關的計算次序,如果未被指定則會產生問題,為了避免該問題,在單一的宏定義中只能使用其中一種操作符。除非是必須使用,否則儘量不適用這兩個預處理操作符!

到此這篇關於C語言零基礎徹底掌握預處理下篇的文章就介紹到這了,更多相關C語言預處理內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com