創(chuàng)作不易,來個三連唄!
一、預定義符號
C語?設置了?些預定義符號,可以直接使?,預定義符號也是在預處理期間處理的。
__FILE__ //進?編譯的源?件
__LINE__ //?件當前的?號
__DATE__ //?件被編譯的?期
__TIME__ //?件被編譯的時間
__STDC__ //如果編譯器遵循ANSI C,其值為1,否則未定義
VS不支持ANSI C,但是gcc是支持的。
二、#define定義常量
基本語法:
#define name stuff //name代表符號名,stuff代表內容
本質:將內容在符號名處原原本本地替換。
使用舉例:
1、定義一個常量的標識符。
#define MAX 1000
2、給較長的關鍵字(比如register)創(chuàng)建一個簡短的名字
#define reg register//為register這個關鍵字創(chuàng)建一個簡短的名字
3、用更形象的符號來替換一種實現(xiàn)。
如果在我們書寫程序時想寫一個無限循環(huán),我們可以這樣寫
int main()
{
for ( ; ; ) //for循環(huán)什么判斷都不寫的時候表示恒成立
;
return 0;
}
而我們可以#define定義一個符號來方便我們完成這種實現(xiàn)
#define do_forever for(;;)
程序就可以這樣寫:
#define do_forever for(;;)
int main()
{
do_forever;
return 0;
}
4、在寫case語句時自動把break寫上
我們知道在使用switch時,如果步驟特別繁瑣,那么每次都得加個break,很麻煩,所以我們想了一種方式。
#define CASE break;case
利用這個#define定義的符號,我們可以這樣使用。
5、如果定義的stuff過長,可以分成幾行寫,除了最后一行外,每行的后面都要加一個反斜杠(續(xù)航符)
#define DEBUG_PRINT printf("file:%s\tline:%d\t \
date:%s\ttime:%s\n" ,\
__FILE__,__LINE__ , \
__DATE__,__TIME__ )
注意:define定義標識符的時候盡量不要往后加? ?;?這樣容易導致問題
三、#define定義宏
#define 機制包括了?個規(guī)定,允許把參數(shù)替換到?本中,這種實現(xiàn)通常稱為宏(macro)或定義宏 (define macro)。
計算機科學里的宏是一種抽象(Abstraction),它根據(jù)一系列預定義的規(guī)則替換一定的文本模式。解釋器或編譯器在遇到宏時會自動進行這一模式替換。對于編譯語言,宏展開在編譯時發(fā)生,進行宏展開的工具常被稱為宏展開器。
下?是宏的申明?式:
#define name( parament-list ) stuff//parement-list 即參數(shù)表
注意:參數(shù)列表的左括號必須與name緊貼,如果兩者之間有任何空白存在,參數(shù)列表就會唄解釋為stuff的一部分。
使用舉例:
1、利用#define定義宏求一個數(shù)的平方
#define SQUARE( x ) x * x
這個宏接收?個參數(shù) x .如果在上述聲明之后,你把 SQUARE( 5 ); 置于程序中,預處理器就會? 下?這個表達式替換上?的表達式: 5 * 5
觀察第54行的語句,關于SQUARE(a+1),按道理應該打印36,為什么打印的時11??
我們發(fā)現(xiàn)替換之后,參數(shù)x被替換成了a+1,所以這條語句實際上變成了
printf ("%d\n",a + 1 * a + 1 );
這就說明,通過替換產生的表達式并沒有按照我們希望的次序去執(zhí)行
要怎么解決呢??? 加括號就可以解決??!通過括號來保證計算順序
#define SQUARE(x) (x) * (x)
這樣該行的語句被替換為
printf ("%d\n",(a + 1) * (a + 1) );
2、利用#define定義宏求一個數(shù)的兩倍
吸取上次的經驗,我們給宏定義的參數(shù)加上括號,因此我們會這樣寫
#define DOUBLE(x) (x) + (x)
這時又出現(xiàn)了問題,第62行代碼按道理應該輸出100,但是卻輸出了55。
我們發(fā)現(xiàn)替換之后:
printf("%d\n", 10*(5)+(5));
說明此時乘法運算優(yōu)先于宏定義的加法,導致了計算不達預期。
要怎么解決呢??? 再外部再加一個大括號,來保證宏定義的加法在乘法運算之前!
#define DOUBLE( x) ( ( x ) + ( x ) )
此時語句被替換為
printf("%d\n", 10*((5)+(5)));
總結:
1、#define定義宏并不具備計算能力,他只負責將文本內容原原本本地替換!!
2、?于對數(shù)值表達式進?求值的宏定義都應該?這種?式加上括號,避免在使?宏時由于參數(shù)中的 操作符或鄰近操作符之間不可預料的相互作?。
四、帶有副作用的宏參數(shù)
當宏參數(shù)在宏的定義中出現(xiàn)超過?次的時候,如果參數(shù)帶有副作?,那么你在使?這個宏的時候就可 能出現(xiàn)危險,導致不可預測的后果。副作?就是表達式求值的時候出現(xiàn)的永久性效果。
例如:
x+1;//不帶副作?
x++;//帶有副作?
通過下面代碼來證明具有副作用參數(shù)所引起的問題。
我們發(fā)現(xiàn)第70行代碼經過預處理后是這樣的
int z = ( (x++) > (y++) ? (x++) : (y++));
參數(shù)帶有副作用會導致參數(shù)本身也被改變!
我們發(fā)現(xiàn)最后x加了1,y加了2,如果我交換原先x和y的值
發(fā)現(xiàn)x加了2,y加了1。這說明我們傳入的參數(shù)產生了無法預料的結果!
結論:因為參數(shù)是完全不加替換帶進去的,所以如果傳入帶有副作用的參數(shù),可能會存在一些潛在的風險,無法預期后果,所以我們平時要盡量避免使用帶有副作用的宏參數(shù)。
五、宏替換的規(guī)則
在程序中擴展#define定義符號和宏時,需要涉及?個步驟。
1. 在調?宏時,首先先對參數(shù)進行檢查,看看是否包含任何由#define定義的符號。如果是,它們?先被替換。
2. 替換?本隨后被插?到程序中原來?本的位置。對于宏,參數(shù)名被他們的值所替換。
3. 最后,再次對結果?件進?掃描,看看它是否包含任何由#define定義的符號。如果是,就重復上 述處理過程
注意:
1. 宏參數(shù)和#define 定義中可以出現(xiàn)其他#define定義的符號。但是對于宏,不能出現(xiàn)遞歸。
#define M 10
#define MAX(M,3+5)
2. 當預處理器搜索#define定義的符號的時候,字符串常量的內容并不被搜索。
#define M 10
printf("M");//M在字符串內部,不會被搜索
六、宏和函數(shù)的區(qū)別
宏通常被應?于執(zhí)?簡單的運算。
?如在兩個數(shù)中找出較?的?個時,寫成下?的宏,更有優(yōu)勢?些。
#define MAX(a, b) ((a)>(b)?(a):(b))
那為什么不?函數(shù)來完成這個任務?
6.1 宏的優(yōu)勢
1. ?于調?函數(shù)和從函數(shù)返回的代碼可能?實際執(zhí)?這個?型計算?作所需要的時間更多。所以宏? 函數(shù)在程序的規(guī)模和速度方面更勝?籌。
我們發(fā)現(xiàn)這兩種方法達到了一致的效果,但是我們可以觀察一下反匯編就可以知道效率。
這是定義宏的方法計算a+b需要的步驟
這是函數(shù)的方法計算a+b需要的步驟
函數(shù)調用時還需要給函數(shù)創(chuàng)建函數(shù)棧幀,所以相比宏效率更低點。
2. 更為重要的是函數(shù)的參數(shù)必須聲明為特定的類型。所以函數(shù)只能在類型合適的表達式上使?。反之 這個宏怎可以適?于整形、?整型、浮點型等可以?于 > 來?較的類型。宏是類型?關的。
6.2 宏的劣勢
1. 每次使?宏的時候,?份宏定義的代碼將插?到程序中。除?宏?較短,否則可能?幅度增加程序 的?度。
2. 宏是沒法調試的。
3. 宏由于類型?關,也就不夠嚴謹。
4. 宏可能會帶來運算符優(yōu)先級的問題,導致程容易出現(xiàn)錯。
6.3 宏有時可以做到函數(shù)做不到的事情
宏的參數(shù)可以出現(xiàn)類型,但是函數(shù)做不到!!
假設我們需要頻繁使用malloc,但是malloc書寫較為繁瑣,我們可以這樣:
#define MALLOC(num, type)\
(type*)malloc(num*sizeof(type))
...
//使?
MALLOC(10, int);//類型作為參數(shù)
//預處理器替換之后:
(int*)malloc(10*sizeof(int));
6.4 宏和函數(shù)的全面對比
七、#define和typedef的區(qū)別
#define與typedef大體功能都是使用時給一個對象取一個別名,增強程序的可讀性,但它們在使用時有以下幾點區(qū)別:
1、原理不同
#define是C語言中定義的語法,是預處理指令,在預處理時進行簡單而機械的字符串替換,不作正確性檢查,只有在編譯已被展開的源程序時才會發(fā)現(xiàn)可能的錯誤并報錯。
typedef是關鍵字,在編譯時處理,有類型檢查功能。它在自己的作用域內給一個已經存在的類型一個別名,但不能在一個函數(shù)定義里面使用typedef。用typedef定義數(shù)組、指針、結構等類型會帶來很大的方便,不僅使程序書寫簡單,也使意義明確,增強可讀性。
2、功能不同
typedef用來定義類型的別名,起到類型易于記憶的功能。另一個功能是定義機器無關的類型。如定義一個REAL的浮點類型,在目標機器上它可以獲得最高的精度:typedef long double REAL, 在不支持long double的機器上,看起來是這樣的,typedef double REAL,在不支持double的機器上,是這樣的,typedef float REAL
#define不只是可以為類型取別名,還可以定義常量、變量、編譯開關等。
3、作用域不同
#define沒有作用域的限制,只要是之前預定義過的宏,在以后的程序中都可以使用,而typedef有自己的作用域。
4、對指針的操作不同
#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1 p1, p2; //int *p1,p2
INTPTR2 p3, p4; //int*p3,p4
含義分別為:
聲明一個指針變量p1和一個整型變量p2
聲明兩個指針變量p3、p4
#define INTPTR1 int*
typedef int* INTPTR2;
int a = 1;
int b = 2;
int c = 3;
const INTPTR1 p1 = &a;//const int*p1=&a
const INTPTR2 p2 = &b;//int*const p2=&b 因為int*這個類型是一個整體不能分開
INTPTR2 const p3 = &c;//int*const p3=&c 因為int*這個類型是一個整體不能分開
上述代碼中,
const INTPTR1 p1是一個常量指針,即不可以通過p1去修改p1指向的內容,但是p1可以指向其他內容。
const INTPTR2 p2是一個指針常量,不可使p2再指向其他內容。因為INTPTR2表示一個指針類型,因此用const限定,表示封鎖了這個指針類型。
INTPTR2 const p3是一個指針常量
八、#和##
8.1 #
#運算符將宏的?個參數(shù)轉換為字符串字?量。它僅允許出現(xiàn)在帶參數(shù)的宏的替換列表中。 #運算符所執(zhí)?的操作可以理解為”字符串化“。
當我們有?個變量 int a = 10; 的時候,我們想打印出: the value of a is 10 .
我們可以這樣:
當我們把n和format替換到宏體內時,就會出現(xiàn)#n和#format,他的意義就是將n和format分別轉換成“n”和“format”。
8.2 ##
## 可以把位于它兩邊的符號合成?個符號,它允許宏定義從分離的文本?段創(chuàng)建標識符。 ## 被稱 為記號粘合
這樣的連接必須產??個合法的標識符。否則其結果就是未定義的。
這?我們想想,寫?個函數(shù)求2個數(shù)的較?值的時候,不同的數(shù)據(jù)類型就得寫不同的函數(shù)。
比如:
int int_max(int x, int y)
{
return x>y?x:y;
}
float float_max(float x, float y)
{
return x>yx:y;
}
但是這樣寫起來確實很繁瑣,所以我們可以使用宏,去定義一個通用的定義函數(shù)模板
#define GENERIC_MAX(type)\
type type##_max(type x, type y)\
{ \
return (x>y?x:y); \
}
//GENERIC泛型
使用這個宏去定義不同的函數(shù)并使用
GENERIC_MAX(int) //替換到宏體內后int##_max ?成了新的符號 int_max做函數(shù)名
GENERIC_MAX(float) //替換到宏體內后float##_max ?成了新的符號 float_max做函數(shù)名
int main()
{
//調?函數(shù)
int m = int_max(2, 3);
printf("%d\n", m);
float fm = float_max(3.5f, 4.5f);
printf("%f\n", fm);
return 0;
}
運行結果:3? ? ?4.500000
在實際開發(fā)過程中##使?的很少
九、命名約定
?般來講函數(shù)的宏的使?語法很相似。
所以語?本?沒法幫我們區(qū)分?者。
那我們平時的?個習慣是:
把宏名全部大寫
函數(shù)名不要全部大寫
十、#undef
這條指令?于移除?個宏定義。
#undef NAME
//如果現(xiàn)存的?個名字需要被重新定義,那么它的舊名字?先要被移除。
十一、命令行定義
許多C 的編譯器提供了?種能?,允許在命令?中定義符號。?于啟動編譯過程。 例如:當我們根據(jù)同?個源?件要編譯出?個程序的不同版本的時候,這個特性有點?處。(假定某 個程序中聲明了?個某個?度的數(shù)組,如果機器內存有限,我們需要?個很?的數(shù)組,但是另外?個 機器內存?些,我們需要?個數(shù)組能夠?些。)
#include <stdio.h>
int main()
{
int array [ARRAY_SIZE];
int i = 0;
for(i = 0; i< ARRAY_SIZE; i ++)
{
array[i] = i;
}
for(i = 0; i< ARRAY_SIZE; i ++)
{
printf("%d " ,array[i]);
}
printf("\n" );
return 0;
}
編譯指令:
//linux 環(huán)境演?
gcc -D ARRAY_SIZE=10 programe.c
十二、條件編譯
在編譯?個程序的時候我們如果要將?條語句(?組語句)編譯或者放棄是很?便的。因為我們有條 件編譯指令。
比如說:
調試性的代碼,刪除可惜,保留?礙事,所以我們可以選擇性的編譯。
常見的條件編譯指令:
1.
#if 常量表達式 //為真編譯,為假不編譯
//...
#endif
//常量表達式由預處理器求值。
如:
#define __DEBUG__ 1
#if __DEBUG__
//..
#endif
2.多個分?的條件編譯 //只編譯滿足條件的一條
#if 常量表達式
//...
#elif 常量表達式
//...
#else
//...
#endif
3.判斷是否被定義
#if defined(symbol) //定義過編譯,沒定義過不編譯
#ifdef symbol
#if !defined(symbol) //沒定義過編譯,定義過不編譯
#ifndef symbol
4.嵌套指令 //嵌套指令下,一個條件是否編譯可能需要判斷2次以上
#if defined(OS_UNIX)
#ifdef OPTION1
unix_version_option1();
#endif
#ifdef OPTION2
unix_version_option2();
#endif
#elif defined(OS_MSDOS)
#ifdef OPTION2
msdos_version_option2();
#endif
#endif
舉例:
#include <stdio.h>
#define __DEBUG__
int main()
{
int i = 0;
int arr[10] = {0};
for(i=0; i<10; i++)
{
arr[i] = i;
#ifdef __DEBUG__
printf("%d\n", arr[i]);//為了觀察數(shù)組是否賦值成功。
#endif //__DEBUG__
}
return 0;
}
易錯點:
a==10,明明是真的,為什么還是不編譯??
因為條件編譯的語句在預處理的時候就已經做出判斷了!而參數(shù)a是在執(zhí)行程序的過程中才出現(xiàn)的!所以對于條件編譯來說,他并不認識a!
結論:使用條件編譯時,給的條件一定不要用參數(shù),最好使用常量
十三、頭文件的包含
13.1 頭文件的包含方式
13.1.1 本地文件包含
1 #include "filename"
查找策略:先在源文件所在?錄下查找,如果該頭文件未找到,編譯器就像查找?guī)旌瘮?shù)頭?件?樣在 標準位置查找頭文件。 如果找不到就提示編譯錯誤。
Linux環(huán)境的標準頭?件的路徑:
/usr/include
VS2022環(huán)境的標準頭?件的路徑:?
C:\Program Files (x86)\Windows Kits\10\Include\10.0.22621.0\ucrt
//vs2022默認路徑
13.1.2 庫文件包含
#include <filename.h>
?查找頭文件直接去庫文件標準路徑下去查找,如果找不到就提示編譯錯誤。
這樣是不是可以說,對于庫?件也可以使? “? ” 的形式包含?
答案是可以的,但是這樣做會有兩個問題:
1、對于庫文件來說,用< >可以直接到庫文件路徑去尋找,但是如果改成“ ”,會先在源文件所在目錄下查找,然后才去庫文件路徑查找,但我們知道庫文件在源文件目錄是不可能找得到的,所以這樣是沒有意義的,還會導致查找效率降低。
2、在未來書寫大量代碼時,我們經常需要寫多個頭文件,如果不加以區(qū)分,就難以很快地判斷出哪些文件是庫文件哪些文件是本地文件。
13.2 嵌套文件包含
我們已經知道, #include 指令可以使另外?個?件被編譯。就像它實際出現(xiàn)于 #include 指令的地??樣。
這種替換的?式很簡單:預處理器先刪除這條指令,并?包含?件的內容替換。
?個頭?件被包含10次,那就實際被編譯10次,如果重復包含,對編譯的壓?就?較?。
test.c
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
#include "test.h"
int main()
{
return 0;
}
test.h
void test();
struct Stu
{
int id;
char name[20];
};
? ? ? 如果直接這樣寫,test.c?件中將test.h包含5次,那么test.h?件的內容將會被拷?5份在test.c中。 如果test.h ?件?較?,這樣預處理后代碼量會劇增。如果?程?較?,有公共使?的頭?件,被?家 都能使?,?不做任何的處理,那么后果真的不堪設想。
未來當我們的代碼量增大時,重復包含的情況很容易就發(fā)生,所以我們就得采取措施。
方法就是條件編譯。
在每個頭文件的開頭這樣寫
#ifndef __TEST_H__
#define __TEST_H__
//頭?件的內容
#endif //__TEST_H__
//解析:第一次檢索的時候,該頭文件還沒定義,所以條件判斷為假,輸出了頭文件的內容
//第二次檢索同類型文件的時候,因為頭文件已經定義了,所以條件判斷永遠真!
//因此該方法可以保證頭文件只被包含一次。
或者
#pragma once
//保證頭文件只被編譯一次
就可以避免頭?件的重復引入。?
13.3 頭文件的本質作用
頭文件的本質作用就是:當其他源文件包含該頭文件時,在預處理時就會先刪除這條指令,然后用包含文件的內容替換。這種方法可以使得不同源文件之間的函數(shù)和聲明可以互相使用。
如果你想在一個源文件中使用該工程下另一個源文件的函數(shù),那么有兩種方法:
1、包含一個頭文件,這個頭文件有函數(shù)的聲明。
add.h
#include<stdo.h>
int add(int x,int y);
add.c
int add(int x, int y)
{
return x+y;
}
test.c
#include"add.h"
int main()
{
int a=10;
int b=10;
printf("%d",add(a,b));
}
2、使用extern聲明外部函數(shù)。
add.c
int add(int x, int y)
{
return x+y;
}
test.c
#include<stdio.h>
extern int add(int x,int y);//外部聲明函數(shù)
int main()
{
int a=10;
int b=10;
printf("%d",add(a,b));
}
13.4 兩道經典筆試題
出自《?質量C/C++編程指南》
1. 頭?件中的 ifndef/define/endif是?什?
答:防止頭文件被重復包含
2. #include <filename.h>和 #include"filename.h"有什么區(qū)別?
答:< >是針對標準庫文件的包含,查找策略是直接去標準庫所在路徑下查找,而“ ”是針對自定義頭文件的包含,查找策略是先去當前工程的源目錄底下查找,找不到再去標準庫文件所在的路徑查找。一般我們寫代碼時習慣用< >包含庫文件,“ ”包含自定義的本地頭文件,這樣方便我們區(qū)分文件類型。
十四、其他預處理指令
#error //當預處理器預處理遇到#error命令時停止編譯并輸出用戶自定義的錯誤消息
#pragma//用于指示編譯器完成一些特定的動作
//(1) #pragma message 用于自定義編譯信息
//(2)#pragma once 用于保證頭文件只被編譯一次
//(3)#pragama pack用于指定內存對齊(一般用在結構體)struct占用內存大小
#line// 指令指示預處理器將編譯器的行號和文件名報告值設置為給定行號和文件名。
參考書籍:《C語言深度解剖》文章來源:http://www.zghlxwxcb.cn/news/detail-805342.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-805342.html
到了這里,關于C語言:預處理詳解的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!