C語言-宏的分類、作用與注意事項

宏 macro


來自QQ羣 Linux && 技術分享 311078264
如果有興趣的話可以加入進來一起學習,共同進步。
個體的精力總是有限的,分享經驗,互助互利纔是快速上升的捷徑。


宏廣泛用於C語言程序中,本文總結了宏的分類, 作用與使用注意事項
相信我,你對於宏的瞭解絕對沒你想象的那麼多。這篇文章是原來從網上找到的,原文出處已經找不到
了,文章思路清晰,包含的內容比較全,我經過整理,增加了些例子分享給大家。


宏定義分類:


(1)不帶參數的宏定義
形式: #define 宏名 [宏體]
功能:可以實現用宏體代替宏名
使用實例: #define TRUE 1
作用:程序中多次使用TRUE,如果需要對TRUE的值進行修改,只需改動一處就可以了
(2)帶參數的宏: #define 宏名 ( 參數表) [宏體]
#include<stdio.h>
#define isRightDay(y,m,d) \
{if(y<=0){printf("Year wrong!\n");return;}\
if(m<1||m>12) {printf("Month wrong!\n");return;}\
int month[]={31,0,31,30,31,30,31,31,30,31,30,31};\
if(y%4==0&&y%100!=0 || y%400==0)month[1]=29;else month[1]=28;\
if((d>=1)&&(d<=month[m-1]))printf("All right!\n");\
else printf("Day wrong!\n");} //所有定義在一行裏
void main()
{
int y,m,d;
printf("Input year month and day:\n");
scanf("%d %d %d",&y,&m,&d);
isRightDay(y,m,d);
}
定義一個宏使用#define,取消使用#undef。
如果一個像函數的宏在使用時沒有出現括號,那麼預處理器只是將這個宏作爲一般的符號處理(那就是不處
理,直接替換)。否則就需要進行參數展開,例外情況是,如果PARAM宏裏對宏參數使用了#或##,那麼宏
參數不會被展開。


宏定義作用:


(1)方便程序的修改
上面的#define TRUE 1就是一個實例
(2)提高程序的運行效率
宏定義的展開是在程序的預處理階段完成的,無需運行時分配內存,能夠部分實現函數的功能,卻沒有
函數調用的壓棧、彈棧開銷,效率較高
(3)增強可讀性
這點不言而喻,當我們看到類似PI這樣的宏定義時,自然可以想到它對應的是圓周率常量
(4)字符串拼接
例如:
#define CAT(a,b,c) a##b##c
2/10
main()
{
printf("%d\n" CAT(1,2,3));
printf("%s\n", CAT('a', 'b', 'c');
}
程序的輸出會是:
123
abc
(5)參數轉化成字符串
示例:
#defind CAT(n) "abc"#n
main()
{
printf("%s\n", CAT(15));
}
輸出的結果會是
abc15
注:這裏的n不能傳入變量,不起作用,會被直接文本替換掉。
(6)用於程序調試跟蹤
常見的用於調試的宏有,
_ L I N E _,_ F I L E _,_ D A T E _,_ T I M E _,_ S T D C _
(7)實現可變宏
舉例來說:
#define PR(...) printf(_ _VA_ARGS_ _)
__VA_ARGS__是系統預定義宏,被自動替換爲參數列表。
PR("Howdy");
PR("weight = %d, shipping = $%.2f\n", wt, sp);
等效於
printf("Howdy");
printf("weight = %d, shipping = $%.2f\n", wt, sp);
#define PR(X, ...) printf("Message" #X ": " _ _VA_ARGS_ _)
如果X=1,則有下面:
printf("Message " "1" ": " "x = %g\n", x);
四個字符串連接起來就是下面:
printf("Message 1: x = %g\n", x);
Don't forget, the ellipses have to be the last macro argument:
#define WRONG(X, ..., Y) #X #_ _VA_ARGS_ _ #y(這個是錯誤的例子。)
前文中講到了C語言中宏定義的作用。
使用宏定義中常見的注意事項有:
(1)不要爲宏定義加分號
宏定義在預處理階段只是進行字符串替換
例如: #define PI 3.14159
float square = PI*r*r;
3/10
如果此時將PI加上分號,顯然會編譯出錯
(2)爲了防止出現意想不到的替換結果,對於帶參數的宏,最好對每個參數和宏都加上配對的括號
例如: #define MUL(a,b) a*b
此時如果這樣來使用 a=MUL(2+3, 4+5);
替換後的結果是a=2+3*4+5。
結果變成了19,而不是我們期望的45
爲了解決這個問題,我們改進一下
#define MUL(a,b) (a)*(b)
上面的例子沒有問題了,但還有下面的情況處理不了
#define SUB(a,b) (a)-(b)
當我們調用 int x = 10*SUB(8,2)
展開後的結果是x=10*(8)-2=72,而不是我們所期望的60
因此比較好的寫法是
#define SUB(a,b) ((a)-(b))
(3)防止參數多次取值的錯誤
這個錯誤比較隱蔽一些,通過例子來說明
#define MIN(a,b) ((a) > (b) ? (a) : (b))
如果我們這樣來使用
int x=1;
int y=2;
int z=MIN(x++,y++);
int k=MIN(y++,x++);
z展開後是 int z=((x++) > (y++) ? (x++) : (y++));
k展開後是 int z=((y++) > (x++) ? (y++) : (x++));
MIN宏設計的初衷是希望得到x,y的較小值,z和k都應該返回1
但結果卻是z=2,k=3
同樣,如果宏的一個參數是一個函數,也會出現函數被多次執行的情況
比較直觀的想法是使用括號將代碼段括起來
#define MIN(a,b) \
{ \
typeof (a) _a = (a); \
typeof (b) _b = (b); \
(_a < _b) ? _a : _b; \
}
但這樣的定義,當我們按照常規這樣來使用的話
z=MIN(x++,y++);
展開後變成了z={typeof (x++) _x = (x++); typeof (y++) _y = (y++); (_a < _b) ? _a : _b;};
結尾的";"還是會導致編譯錯誤
解決這個問題的比較理想的做法是:
4/10
#define MIN(a,b) \
do { \
typeof (a) _a = (a); \
typeof (b) _b = (b); \
(_a < _b) ? _a : _b; \
} while (0)
這裏有三點需要注意:
(i)爲了防止參數的多次取值,我們利用臨時變量_a, _b來存儲了a,b的取值結果,這樣就避免了多次取

(ii)由於宏定義只執行字符替換,爲了爲臨時變量聲明一個合適的類型,因此使用了類型聲明, typeof
typeof的介紹參見文檔 http://gcc.gnu.org/onlinedocs/gcc/Typeof.html
(iii)爲了防止宏使用結尾的分號導致錯誤,儘量使用do while(0)來講代碼塊包含起來


看來,寫好一個宏真是不容易,後面還附了一些例子和分析,大家可以體會一下。


來自QQ羣 Linux && 技術分享 311078264
如果有興趣的話可以加入進來一起學習,共同進步。
個體的精力總是有限的,分享經驗,互助互利纔是快速上升的捷徑。

 


文本替換
下面是一個簡單示例:只進行簡單的文字替換。
#define P printf
#define M int main(void)
#define FUN do{int n;\
printf("Please input a number\n");\
scanf("%d", &n);\
}while(0)
//使用替換後的文本編寫代碼
M
{
FUN;
P("this is a test...\n");
return 0;
}


宏定義指針
一個經常出錯的例子:
#define ping int *
pin a, b;
本意是想讓a和b稱爲int型指針,但是實際上卻變成了int *a, b; a是int型指針,b是int型變量。
這個時候其應該用到typedef了,define滿足不了要求,應該改成typedef pin (int *) 。
5/10
數字交換SWAP
下面實現兩個數交換:
#include <stdio.h>
#define _SWAP(a,b) do{\
typeof(a) c;\
c = a;a = b;b = c;\
}while(0)
#define SWAP(a,b) _SWAP(a,b)
int main(void)
{
int a = 1,b = 2;
SWAP(a,b);
printf("a = %d,b = %d \n",a,b);
return 0;
}


數字平方POWER
一個數的平方:
#include <stdio.h>
/*把POWER(A) 寫爲POWER a*a 也可以,但是調用方式不一樣*/
#define POWER(a) a*a
int main(void)
{
int a = 4;
POWER(a);
printf("a*a = %d \n",a);
return 0;
}


宏傳參數
宏傳參數:
#include <stdio.h>
#define SQARE(n) (n) * (n)
int main(void)
{
printf("SQARE(%d) = %d\n", 1+2, SQARE(1+2));
return 0;
}
6/10


系統默認宏
系統定義的默認宏:
#include <stdio.h>
int main(void)
{
printf("%s\n", __FUNCTION__);
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%d\n",__STDC__);
return 0;
}


#,##,@#,\
一、#
名稱:字符串化操作符
其作:將宏定義中的傳入參數名轉換成用一對雙引號括起來參數名字符串。
使用條件:只能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前
舉例:
#符號把一個符號直接轉換爲字符串
#define STRING(x) #x
string str=STRING(abc); 將會展成:string str="abc";
注意:對空格的處理
a、忽略傳入參數名前面和後面的空格。
如:str=STRING( abc ); 將會被擴展成 str="abc";
b、當傳入參數名間存在空格時,編譯器將會自動連接各個子字符串,用每個子字符串中只以一個空格連
接,忽略其中多餘一個的空格。
如:str=STRING( abc def); 將會被擴展成 str="abc def";
二、##
名稱:符號連接操作符
作用:將宏定義的多個形參名連接成一個實際參數名
使用條件:只能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前
舉例:
#define exampleNum(n) num##n
int num=exampleNum(9); 將會擴展成 int num=num9;
注意:
1、當用##連接形參時,##前後的空格可有可無。
如:#define exampleNum(n) num ## n 相當於 #define exampleNum(n) num##n
2、連接後的實際參數名,必須爲實際存在的參數名或是編譯器已知的宏定義
三、@#
名稱:字符化操作符
作用:將傳入的單字符參數名轉換成字符,以一對單引用括起來。
使用條件:只能用於有傳入參數的宏定義中,且必須置於宏定義體中的參數名前。
舉例:
#define makechar(x) @#X
a = makechar(b); 展開後變成了:a= 'b';
7/10
四、\
名稱:行繼續操作符
作用:當定義的宏不能用一行表達完整時,可以用"\"表示下一行繼續此宏的定義。
注意:換行不能切斷單詞,只能在空格的地方進行。
=====
//# 以字符串的形式 ,//## 連接


宏#:
#include <stdio.h>
#define ERR(f) printf("%s is error\n", #f)
int func(int s)
{
if (s < 0) {
return -1;
}
return 0;
}
int main(void)
{
int var;
int ret;
while (1) {
printf("input:");
scanf("%d", &var);
ret = func(var);
if (ret < 0) {
ERR(func);
return -1;
}
}
return 0;
}


宏 ##:
#include <stdio.h>
#define CONNECT(fun) welcome##fun()
void welcomestudent(void)
{
printf("hello boy!!\n");
}
void welcometeacher(void)
{
printf("hello teacher...\n");
}
int main(void)
{
CONNECT(student);
CONNECT(teacher);
return 0;
}
8/10


帶參宏定義和函數
帶參的宏和帶參函數很相似,但有本質上的不同,把同一表達式用函數處理與用宏處理的結果有可能是不
同的。
【示例①】用函數計算平方值。
#include <stdio.h>
int SQ(int y){
return ((y)*(y));
}
int main(){
int i=1;
while(i<=5){
printf("%d^2 = %d\n", (i-1), SQ(i++));
}
return 0;
}
運行結果:
1^2 = 1
2^2 = 4
3^2 = 9
4^2 = 16
5^2 = 25
【示例②】用宏計算平方值。
#include <stdio.h>
#define SQ(y) ((y)*(y))
int main(){
int i=1;
while(i<=5){
printf("%d^2 = %d\n", i, SQ(i++));
}
return 0;
}
VC 6.0下運行結果:
1^2 = 1
3^2 = 9
5^2 = 25
分析如下:
在示例①中,函數調用是把實參 i 值傳給形參 y 後自增 1,然後輸出函數值,所以要循環5次,輸出
1~5的平方值。
而在示例②中宏調用時只作代換,SQ(i++) 被代換爲 ((i++)*(i++))。
第一次循環,i 的值爲1,(i++)*(i++)=1;
第二次循環 i 的值爲 3,(i++)*(i++)=9;
第三次循環 i 的值爲 5,(i++)*(i++)=25;
第四次循環,i 的值爲7,終止循環。
從以上分析可以看出函數調用和宏調用二者在形式上相似,在本質上是完全不同的。
注:這個錯誤其實在上面注意事項(3)中指出了原因,這裏列出是爲了增強理解。
9/10


宏調用自己
當一個宏自己調用自己時,會發生什麼?
例如:
#define TEST( x ) ( x + TEST( x ) )
TEST( 1 );
會發生什麼?
爲了防止無限制遞歸展開,語法規定,當一個宏遇到自己時,就停止展開,也就是
說,當對TEST( 1 )進行展開時,展開過程中又發現了一個TEST,那麼就將這個TEST當作一般的符號。
TEST(1) 最終被展開爲:1 + TEST( 1) 。


宏參數的prescan
宏參數的prescan(預掃描)
當一個宏參數被放進宏體時,這個宏參數會首先被全部展開(有例外,見下文)。當展開後的宏參數被
放進宏體時,預處理器對新展開的宏體進行第二次掃描,並繼續展開。例如:
#define PARAM( x ) x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) );
因爲ADDPARAM( 1 ) 是作爲PARAM的宏參數,所以先將ADDPARAM( 1 )展開爲INT_1,然後再將INT_1放
進PARAM。
例外情況是,如果PARAM宏裏對宏參數使用了#或##,那麼宏參數不會被展開:
#define PARAM( x ) #x
#define ADDPARAM( x ) INT_##x
PARAM( ADDPARAM( 1 ) ); 將被展開爲"ADDPARAM( 1 )"。
使用這麼一個規則,可以創建一個很有趣的技術:打印出一個宏被展開後的樣子,這樣可以方便你分
析代碼:
#define TO_STRING( x ) TO_STRING1( x )
#define TO_STRING1( x ) #x
TO_STRING首先會將x全部展開(如果x也是一個宏的話),然後再傳給TO_STRING1轉換爲字符串,現在
你可以這樣:const char *str = TO_STRING( PARAM( ADDPARAM( 1 ) ) ); 去一探PARAM展開後的樣子。

條件編譯
宏的另一個重要作用就是條件編譯
#ifdef
10/10
#else
#endif
#ifndef __INC_H
#define __INC_H
#endif
#ifdef __cplusplus
extern "C" {
#endif
//C的代碼
#ifdef __cplusplus
}
#endif
由於宏定義是在程序預處理階段起作用,這樣就可以條件編譯代碼,使不需要用到的代碼不被編譯。
例如下面這些應用非常廣泛固定的宏定義:
• 編譯器
• • GCC
#ifdef __GNUC__• #if __GNUC__ >= 3 // GCC3.0以上
• UNIX• UNIX
#ifdef __unix
or
#ifdef __unix__
• Linux
#ifdef __linux
or
#ifdef __linux__
• FreeBSD
#ifdef __FreeBSD__
• NetBSD
#ifdef __NetBSD__
• Windows• 32bit
#ifdef _WIN32(或者WIN32)
• 64bit
#ifdef _WIN64
• GUI App
#ifdef _WINDOWS
• CUI App
#ifdef _CONSOLE

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章