【C語言】 —— 預處理詳解(下)
- 前言
- 七、# 和 \##
- 7.1 # 運算符
- 7.2 ## 運算符
- 八、命名約定
- 九、# u n d e f undef undef
- 十、命令行定義
- 十一、條件編譯
- 11.1、單分支的條件編譯
- 11.2、多分支的條件編譯
- 11.3、判斷是否被定義
- 11.4、嵌套指令
- 十二、頭文件的包含
- 12.1 頭文件被包含的方式
- (1) 本地文件被包含的方式
- (2)庫文件包含
- 12.2 嵌套文件包含
- 十三、 其他預處理指令
前言
??在上期【C語言】 —— 預處理詳解(下)的學習中,我們詳細介紹了預處理中宏的相關知識,相信大家都收獲不少。別急還有,本期讓我們繼續學習預處理方面的其他知識吧。
七、# 和 ##
7.1 # 運算符
# 運算符
將宏的一個參數轉換成字符串字面量。它進允許出現在帶參數的宏的替換列表中# 運算符
所執行的操作可理解為“字符串化”
??什么意思呢?
??我們先來做一個鋪墊:
int mian()
{printf("hello" "world\n");printf("helloworld\n");return 0;
}
??上述兩句代碼有什么區別呢?我們一起來看看:
??可以看到,兩個字符串和一個字符串的效果是一樣的。C語言會把兩個字符串天然連成一個字符串
,中間加空格也沒用。
??現在有這么一個場景:
int main()
{int a = 1;printf("The value of a is %d\n", a);int b = 20;printf("The value of b is %d\n", b);float f = 8.5f;printf("The value of f is %f\n", f);return 0;
}
??我們發現三句代碼的邏輯都是非常相像
的,但又有些許不同
。
??
??那我們想既然他們這么相像,能不能把他們封裝成一個函數
,以方便使用呢?
??但是函數是做不到的這個功能的
??那怎么辦呢?
??我們可以嘗試用宏來解決呢
#define Print(n, format) printf("The value of n is " format "\n", n)int main()
{int a = 1;Print(a, "%d");//printf("The value of a is %d\n", a);int b = 20;Print(b, "%d");//printf("The value of b is %d\n", b);float f = 8.5f;Print(f, "%f");//printf("The value of f is %f\n", f);return 0;
}
??
運行結果:
??我們發現 n n n 一直沒變,那應該怎么修改呢?
??這時就應該用到我們的 # 運算符了:# 將宏的一個參數轉換成字符串字面量,即 n n n 變成 “ n ” “n” “n”
??這時,我們再運用拼接大法就成了
#define Print(n, format) printf("The value of " #n " is " format "\n", n)
??
??不懂?看下面的解釋就懂啦
??
7.2 ## 運算符
??##
可以把位于他兩邊的符號合成一個符號,它允許宏定義從分離的文本片段創建標識符
。##
被稱為記號粘合。這樣的連接必須產生一個合法
的表示符,否則其結果就是未定義的。
??
??前面我們曾說,寫一個求兩個數的較大值的函數,對于不同的數據類型就需要寫不同的函數。
int int_max(int x, int y)
{return x > y ? x : y;
}
float float_max(float x, float y)
{return x > y ? x : y;
}
??
??這樣難免太過繁瑣,而且兩函數之間有很多相似的地方。
??那有沒有辦法能快速地創建出這樣的函數呢?就像一個函數的模具一樣,套一下一個函數就出來了
??
??我們可以這樣來寫一個宏
:
#define GENERIC_MAX(type) \type type##_max(type x, type y)\{\return x > y ? x : y;\}
??
#define GENERIC_MAX(type) \type type##_max(type x, type y)\{\return x > y ? x : y;\}GENERIC_MAX(int); //相當于定義了一個函數int_max
GENERIC_MAX(float); //相當于定義了一個函數float_maxint main()
{int r1 = int_max(3, 5);printf("%d\n", r1);float r2 = float_max(2.3f, 7.6f);printf("%f\n", r2);return 0;
}
??
運行結果:
??
??我們也可以在 g c c gcc gcc 環境下觀察預處理后的 .i
文件,有個更直觀地了解
??當然,這樣生成的函數也是不方便調試的
??那這里 ##
起到什么作用呢?
??加了 ##
,編譯器才會認為他們是一個符號
??讓我們來看看不加 ##
的效果:
??
八、命名約定
??一般來講,函數和宏的使用語法很相似,所以語言本身沒法幫我們區分二者
??
我們平時的一個習慣是:
- 把宏名全部大寫
- 函數名不要全部大寫
??當然,這些命名規則并不是絕對的
??比如 o f f s e t offset offset 這個宏就寫成了全小寫
??
注: o f f s e t offset offset 是用來計算結構體成員相對于結構體起始位置的偏移量的
??
九、# u n d e f undef undef
??# u n d e f undef undef 指令用來移出一個宏定義
??上述代碼,在 169 行使用 # u n d e f undef undef 移除了宏 MAX。在移除之前的 168 行調用時沒問題的,但移除之后的 170 行調用就會報錯
??
十、命令行定義
??許多 C 的編譯器(不包括VS)提供了一種能力,允許在命令行中定義符號。用于啟動編譯過程
??
??例如:當我們根據同一個源文件要編譯出一個程序的不同版本的時候,這個特性就有用處。(假定某個程序中聲明了一個某長度的數組,如果機器內存有限,我們需要一個很小的數組,但是另外一個機器內存大些,我們需要的數組能夠大些)
??命令行定義式在預處理階段處理的,在預處理階段時,上述代碼中 s z sz sz 的值已經確定了
??
十一、條件編譯
??在編譯一個程序的時候如果將一條(一組語句)編譯或者放棄是很方便
的。因為我們可以用條件編譯指令
??
??條件編譯指令就是這段代碼我想讓你編譯就編譯,不想讓你編譯你就不要編譯了
。我們可以給他設定一個條件,條件為真,這段代碼就參與編譯,條件為假,這段代碼就不要編譯了。
??
比如說:
??一些調試性的代碼,刪除可惜,保留又礙事,所以我們可以選擇性編譯
??
??
常用的條件編譯指令:
11.1、單分支的條件編譯
#if 常量表達式//···
#endif
??
11.2、多分支的條件編譯
#if 常量表達式//···
#elif 常量表達式//···
#else//···
#endif
??哪條語句為真,就執行哪條語句
#define M 1
int main()
{
#if M == 0printf("hello\n");
#elif M == 1printf("world\n");
#elif M == 2printf("csdn\n");
#endifprintf("886\n");return 0;
}
??
11.3、判斷是否被定義
#if defined(symbol)
#ifdef symbol//上面兩個的反面
if !defined(symbol)
#ifndef symbol
??
11.4、嵌套指令
#if defined(OS_UNIX)#ifdef OPTION1unix_version_option1();#endif#ifdef OPTION2unix_version_option2();#endif#elif defined(OS_MSDOS)#ifdef OPTION2msdos_version_option2();#endif#endif
??
十二、頭文件的包含
12.1 頭文件被包含的方式
(1) 本地文件被包含的方式
# include "filename"
??查找策略:先在源文件所在的工程目錄下查找,如果頭文件未找到,編譯器就像查找庫函數頭文件一樣在標準位置查找頭文件
??如果再找不到就編譯錯誤
??
L i n u x Linux Linux 環境的標準頭文件路徑(頭文件放在哪):
/usr/include
VS 環境的標準頭文件路徑:
C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include
//這是VS2013的默認路徑
??
(2)庫文件包含
#include <filename.h>
??查找頭文件直接去標準路徑下去查找,如果找不到就提示編譯錯誤。
??
??這樣是不是可以說,對于庫文件也可以使用 “ ”
的形式包含
??答案是肯定的,但是這樣做查找的效率比較低,當然這樣也不容易區分是庫文件還是本地文件
??
12.2 嵌套文件包含
??學習了前面的(編譯和鏈接),我們知道頭文件的包含在預處理階段就是直接將該文件的代碼拷貝到包含頭文件的地方
??
??如果一個頭文件被包含了 10 次,那就實際被編譯了 10 次,如果重復包含,對編譯的壓力就比較大
t e s t . c test.c test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
t e s t . h test.h test.h
void test();
struct Stu
{
int id;
char name[20];
};
??但在一個工程中,一個文件難免被包含多次,那么如何解決這個問題呢?
??答案:條件編譯
#ifndef __TEST_H__
#define __TEST_H__//頭文件的內容
#endif
??
怎么理解呢?
- 當第一次包含頭文件時,要不要編譯呢?先進行判斷
- __TEST_H__這個符號并沒有被定義,要進行編譯
- 緊接著定義__TEST_H__符號
- 之后再次包含該頭文件,發現__TEST_H__已被定義,不再對之后包含的該頭文件進行編譯
不過上面這種寫法比較麻煩,還有另外一種寫法:
#pragma once
??效果與上面的方式是一樣的
??這樣就可以避免頭文件的重復引入
??
十三、 其他預處理指令
#error
#pragma
#line
···
#pragma pack()//在結構體部分介紹
有興趣的小伙伴可以閱讀 《C語言深度解剖》
??
??
??
??
??
??好啦,本期關于預處理的知識就介紹到這里啦,希望本期博客能對你有所幫助。同時,如果有錯誤的地方請多多指正,讓我們在C語言的學習路上一起進步!