C 和 C++ 宏 详解
From:https://www.cnblogs.com/njczy2010/p/5773061.html
C中的預編譯宏詳解:http://www.cppblog.com/bellgrade/archive/2010/03/18/110030.html
C語言的宏總結:http://blog.csdn.net/pirlck/article/details/51254590
C 語言中的 宏定義:http://www.360doc.com/content/13/0125/13/10906019_262310086.shtml
gcc 宏 :https://gcc.gnu.org/onlinedocs/cpp/Macros.html
C/C++ 中宏與預處理使用方法大全 (VC):http://www.oschina.net/question/234345_54797#
------------------------------------------------------------------------------------------------------------------------
?
?
?
C 和 C++ 宏 詳解
?
?
宏 替換 發生的時機
?
? ? 為了能夠真正理解#define的作用,需要了解下C語言源程序的處理過程。當在一個集成的開發環境如Turbo C中將編寫好的源程序進行編譯時,實際經過了預處理、編譯、匯編和連接幾個過程。其中預處理器產生編譯器的輸出,它實現以下的功能:
可以把源程序中的#include 擴展為文件正文,即把包含的.h文件找到并展開到#include 所在處。
預處理器根據#if和#ifdef等編譯命令及其后的條件,將源程序中的某部分包含進來或排除在外,通常把排除在外的語句轉換成空行。
預處理器將源程序文件中出現的對宏的引用展開成相應的宏 定義,即本文所說的#define的功能,由預處理器來完成。經過預處理器處理的源程序與之前的源程序有所有不同,在這個階段所進行的工作只是純粹的替換與展開,沒有任何計算功能,所以在學習#define命令時只要能真正理解這一點,這樣才不會對此命令引起誤解并誤用。
?
?
1. #define的基本用法
?
? #define 是 C語言中提供的宏定義命令,其主要目的是為程序員在編程時提供一定的方便,并能在一定程度上提高程序的運行效率,但學生在學習時往往不能 理解該命令的本質,總是在此處產生一些困惑,在編程時誤用該命令,使得程序的運行與預期的目的不一致,或者在讀別人寫的程序時,把運行結果理解錯誤,這對 C語言的學習很不利。
?
1.1 #define命令剖析
?
? ? ? ? #define命令是C語言中的一個宏定義命令,它用來將一個標識符定義為一個字符串,該標識符被稱為宏名,被定義的字符串稱為替換文本。該命令有兩種格式:一種是簡單的宏定義,另一種是帶參數的宏定義。
? ? 一個標識符被宏定義后,該標識符便是一個宏名。這時,在程序中出現的是宏名,在該程序被編譯前,先將宏名用被定義的字符串替換,這稱為宏替換,替換后才進行編譯,宏替換只是簡單的替換,即 簡單的純文本替換,C預處理器不對宏體做任何語法檢查,像缺個括號、少個分號什么的預處理器是不管的。
?
- 宏體換行需要在行末加反斜杠 \
- 宏名之后帶括號的宏?被認為是?宏函數。用法和普通函數一樣,只不過在預處理階段,宏函數會被展開。優點是沒有普通函數保存寄存器和參數傳遞的開銷,展開后的代碼有利于CPU cache的利用和指令預測,速度快。缺點是可執行代碼體積大。
#define min(X, Y) ((X) < (Y) ? (X) : (Y))
y = min(1, 2);會被擴展成y = ((1) < (2) ? (1) : (2)); - 分號吞噬 問題: #define MAX(x,y) \ { \return (x) > (y) ? (x):(y); \ }if(1)MAX(20, 10); //這個分號導致這一部分代碼塊結束,致使else找不到對應的if分支而報錯 else;;上面 宏 展開后 if else 代碼如下if(1){ return (20) > (10) ? (20):(10); };//后面多了一個分號,導致 if 代碼塊結束,致使else找不到對應的if分支而報錯 else;;
示例代碼(test.c):
#include <stdio.h> #include <stdlib.h> #include <unistd.h>#define MAX(x,y) \ { \ return (x) > (y) ? (x):(y); \ } void main() { if(1) MAX(20, 10); //這個分號導致這一部分代碼塊結束,致使else找不到對應的if分支而報錯 else ;; }gcc -E test.c -o test.e? 會生成 test.e 的預處理文件
gcc -E test.c 會直接把預處理后內容輸出到屏幕上。直接輸出到屏幕上截圖:
?
可以看到后面多了一個分號。現在執行編譯
可以看到 else 分支缺少 對應的 if 。
?
預處理并不分析整個源代碼文件, 它只是將源代碼分割成一些標記(token), 識別語句中哪些是C語句, 哪些是預處理語句. 預處理器能夠識別C標記, 文件名, 空白符, 文件結尾標志.
預處理語句格式:????#command name(...) token(s)
1, command預處理命令的名稱, 它之前以#開頭, #之后緊隨預處理命令, 標準C允許#兩邊可以有空白符, 但比較老的編譯器可能不允許這樣. 若某行中只包含#(以及空白符), 那么在標準C中該行被理解為空白. 整個預處理語句之后只能有空白符或者注釋, 不能有其它內容.
2, name代表宏名稱, 它可帶參數. 參數可以是可變參數列表(C99).
3, 語句中可以利用"\"來換行.
e.g.
# define ONE 1 /* ONE == 1 */
等價于:?#define ONE 1
#define err(flag, msg) if(flag) \
?? printf(msg)
等價于:?#define err(flag, msg) if(flag) printf(msg)
?
簡單的宏定義
#define <宏名> <字符串> 例: #define PI 3.1415926#define FALSE 0?
帶參數的宏定義
#define <宏名>(<形式參數表>) <宏體> 例: #define A(x) x#define MAX(a,b) ( (a) > (b) ) ? (a) : (b)?
取消宏定義:#undef 宏名
?
可變參數 的 宏
C/C++宏定義的可變參數詳細解析_C 語言:https://yq.aliyun.com/ziliao/134584
#define LOG( format, ... ) printf( format, __VA_ARGS__ ) LOG( "%s %d", str, count );__VA_ARGS__是系統預定義宏,被自動替換為參數列表。
#define debug(format, args...) fprintf (stderr, format, args)
#define debug(format, ...) fprintf (stderr, format, __VA_ARGS__)
或者
#define debug(format, ...) fprintf (stderr, format, ## __VA_ARGS__)
前兩者存在多余逗號問題,第三個宏使用##去掉可能多余的逗號。
即可變參數被忽略或為空,’##’操作將使預處理器(preprocessor)去除掉它前面的那個逗號
?
當一個宏自己調用自己時,會發生什么?
例如:#define TEST( x ) ( x + TEST( x ) )
TEST( 1 ); 會發生什么?為了防止無限制遞歸展開,語法規定,當一個宏遇到自己時,就停止展開,也就是說,當對TEST( 1 )進行展開時,展開過程中又發現了一個TEST,那么就將這個TEST當作一般的符號。TEST(1) 最終被展開為:1 + TEST( 1) 。
?
宏參數的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 TO_STRING( x ) TO_STRING1( x ) #define TO_STRING1( x ) #xTO_STRING首先會將x全部展開(如果x也是一個宏的話),然后再傳給TO_STRING1轉換為字符串。
現在你可以這樣:
? ? ? ? const char *str = TO_STRING( PARAM( ADDPARAM( 1 ) ) );
去一探PARAM展開后的樣子。
?
一個很重要的補充:
? ? 如果一個像函數的宏在使用時沒有出現括號,那么預處理器只是將這個宏作為一般的符號處理(即 就是不處理)。
? ??函數宏對參數類型是不敏感的, 你不必考慮將何種數據類型傳遞給宏. 那么, 如何構建對參數類型敏感的宏呢? 參考關于"##"的介紹.
?
對象宏
不帶參數的宏被稱為"對象宏(objectlike macro)"
#define 經常用來定義常量, 此時的宏名稱一般為大寫的字符串. 這樣利于修改這些常量.
e.g.
#define MAX 100
int a[MAX];
#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif
#define __FILE_H__ 中的宏就不帶任何參數, 也不擴展為任何標記. 這經常用于包含頭文件.
要調用該宏, 只需在代碼中指定宏名稱, 該宏將被替代為它被定義的內容.
?
函數宏
帶參數的宏也被稱為"函數宏". 利用宏可以提高代碼的運行效率: 子程序的調用需要壓棧出棧, 這一過程如果過于頻繁會耗費掉大量的CPU運算資源. 所以一些代碼量小但運行頻繁的代碼如果采用帶參數宏來實現會提高代碼的運行效率.
函數宏的參數是固定的情況
函數宏的定義采用這樣的方式: #define name( args ) tokens
其中的args和tokens都是可選的. 它和對象宏定義上的區別在于宏名稱之后不帶括號.
注意, name之后的左括號(必須緊跟name, 之間不能有空格, 否則這就定義了一個對象宏, 它將被替換為 以(開始的字符串. 但在調用函數宏時, name與(之間可以有空格.
e.g.
#define mul(x,y) ((x)*(y))
注意, 函數宏之后的參數要用括號括起來, 看看這個例子:
e.g.
#define mul(x,y) x*y
"mul(1, 2+2);" 將被擴展為: 1*2 + 2
同樣, 整個標記串也應該用括號引用起來:
e.g.
#define mul(x,y) (x)*(y)
sizeof mul(1,2.0) 將被擴展為 sizeof 1 * 2.0
調用函數宏時候, 傳遞給它的參數可以是函數的返回值, 也可以是任何有意義的語句:
e.g.
mul (f(a,b), g(c,d));
e.g.
#define insert(stmt) stmt
insert ( a=1; b=2;) 相當于在代碼中加入 a=1; b=2 .
insert ( a=1, b=2;) 就有問題了: 預處理器會提示出錯: 函數宏的參數個數不匹配. 預處理器把","視為參數間的分隔符.?
insert ((a=1, b=2;)) 可解決上述問題.
在定義和調用函數宏時候, 要注意一些問題:
1, 我們經常用{}來引用函數宏被定義的內容, 這就要注意調用這個函數宏時的";"問題.
example_3.7:
#define swap(x,y) { unsigned long _temp=x; x=y; y=_tmp}
如果這樣調用它: "swap(1,2);" 將被擴展為: { unsigned long _temp=1; 1=2; 2=_tmp};?
明顯后面的;是多余的, 我們應該這樣調用: swap(1,2)
雖然這樣的調用是正確的, 但它和C語法相悖, 可采用下面的方法來處理被{}括起來的內容:
#define swap(x,y) \
?? do { unsigned long _temp=x; x=y; y=_tmp} while (0)
swap(1,2); 將被替換為:
do { unsigned long _temp=1; 1=2; 2=_tmp} while (0);
在Linux內核源代碼中對這種do-while(0)語句有這廣泛的應用.
2, 有的函數宏是無法用do-while(0)來實現的, 所以在調用時不能帶上";", 最好在調用后添加注釋說明.
eg_3.8:
#define incr(v, low, high) \
?? for ((v) = (low),; (v) <= (high); (v)++)
只能以這樣的形式被調用: incr(a, 1, 10) /* increase a form 1 to 10 */
函數宏中的參數包括可變參數列表的情況
C99標準中新增了可變參數列表的內容. 不光是函數, 函數宏中也可以使用可變參數列表.
#define name(args, ...) tokens
#define name(...) tokens
"..."代表可變參數列表, 如果它不是僅有的參數, 那么它只能出現在參數列表的最后. 調用這樣的函數宏時, 傳遞給它的參數個數要不少于參數列表中參數的個數(多余的參數被丟棄).?
通過__VA_ARGS__來替換函數宏中的可變參數列表. 注意__VA_ARGS__只能用于函數宏中參數中包含有"..."的情況.
e.g.
#ifdef DEBUG
#define my_printf(...) fprintf(stderr, __VA_ARGS__)
#else
#define my_printf(...) printf(__VA_ARGS__)
#endif
tokens中的__VA_ARGS__被替換為函數宏定義中的"..."可變參數列表.?
注意在使用#define時候的一些常見錯誤:
#define MAX = 100
#define MAX 100;
=, ; 的使用要值得注意. 再就是調用函數宏是要注意, 不要多給出";".
?
?
1. 關于定義宏的另外一些問題
?
(1)宏可以被多次定義, 前提是這些定義必須是相同的。
這里的"相同"要求先后定義中空白符出現的位置相同, 但具體的空白符類型或數量可不同, 比如原先的空格可替換為多個其他類型的空白符: 可為tab, 注釋...
e.g.
#define NULL 0
#define NULL /* null pointer */???? 0
上面的重定義是相同的, 但下面的重定義不同:
#define fun(x) x+1
#define fun(x) x + 1 或: #define fun(y) y+1
如果多次定義時, 再次定義的宏內容是不同的, gcc會給出"NAME redefined"警告信息.
應該避免重新定義函數宏, 不管是在預處理命令中還是C語句中, 最好對某個對象只有單一的定義. 在gcc中, 若宏出現了重定義, gcc會給出警告.
?
(2) 在gcc中, 可在命令行中指定對象宏的定義:
e.g.
$?gcc -Wall -DMAX=100 -o tmp tmp.c
相當于在tmp.c中添加" #define MAX 100".
那么, 如果原先tmp.c中含有MAX宏的定義, 那么再在gcc調用命令中使用-DMAX, 會出現什么情況呢?
---若-DMAX=1, 則正確編譯.
---若-DMAX的值被指定為不為1的值, 那么gcc會給出MAX宏被重定義的警告, MAX的值仍為1.
注意: 若在調用gcc的命令行中不顯示地給出對象宏的值, 那么gcc賦予該宏默認值(1), 如: -DVAL == -DVAL=1
?
(3) #define所定義的宏的作用域
宏在定義之后才生效, 若宏定義被#undef取消, 則#undef之后該宏無效. 并且字符串中的宏不會被識別
e.g.
#define ONE 1
sum = ONE + TWO??? /* sum = 1 + TWO */
#define TWO 2
sum = ONE + TWO??? /* sum = 1 + 2??? */?
#undef ONE
sum = ONE + TWO??? /* sum = ONE + 2 */
char c[] = "TWO"?? /* c[] = "TWO", NOT "2"! */
?
(4) 宏的替換可以是遞歸的, 所以可以嵌套定義宏.
e.g.
# define ONE NUMBER_1
# define NUMBER_1 1
int a = ONE /* a = 1 */
?
2. #undef
#undef用來取消宏定義, 它與#define對立:
#undef name
如夠被取消的宏實際上沒有被#define所定義, 針對它的#undef并不會產生錯誤.
當一個宏定義被取消后, 可以再度定義它.?
?
3. #if, #elif, #else, #endif
#if, #elif, #else, #endif用于條件編譯:
#if 常量表達式1
?? 語句...
#elif 常量表達式2
?? 語句...
#elif 常量表達式3
?? 語句...
...
#else
?? 語句...
#endif
#if和#else分別相當于C語句中的if, else. 它們根據常量表達式的值來判別是否執行后面的語句. #elif相當于C中的else-if. 使用這些條件編譯命令可以方便地實現對源代碼內容的控制.
else之后不帶常量表達式, 但若包含了常量表達式, gcc只是給出警告信息.
使用它們可以提升代碼的可移植性---針對不同的平臺使用執行不同的語句. 也經常用于大段代碼注釋.
e.g.
#if 0
{
?? 一大段代碼;
}
#endif
?
常量表達式可以是包含宏, 算術運算, 邏輯運算等等的合法C常量表達式, 如果常量表達式為一個未定義的宏, 那么它的值被視為0.
#if MACRO_NON_DEFINED?== #if 0
在判斷某個宏是否被定義時, 應當避免使用#if, 因為該宏的值可能就是被定義為0. 而應當使用下面介紹的#ifdef或#ifndef.
注意: #if, #elif, #else之后的宏只能是對象宏. 如果name為名的宏未定義, 或者該宏是函數宏. 那么在gcc中使用"-Wundef"選項會顯示宏未定義的警告信息.
?
4. #ifdef, #ifndef, defined.
#ifdef, #ifndef, defined用來測試某個宏是否被定義
#ifdef name 或 #ifndef name
它們經常用于避免頭文件的重復引用:
#ifndef __FILE_H__
#define __FILE_H__
#include "file.h"
#endif
defined(name): 若宏被定義,則返回1, 否則返回0.
它與#if, #elif, #else結合使用來判斷宏是否被定義, 乍一看好像它顯得多余, 因為已經有了#ifdef和#ifndef. defined用于在一條判斷語句中聲明多個判別條件:
#if defined(VAX) && defined(UNIX) && !defined(DEBUG)?
和#if, #elif, #else不同, #indef, #ifndef, defined測試的宏可以是對象宏, 也可以是函數宏. 在gcc中使用"-Wundef"選項不會顯示宏未定義的警告信息.
?
5. #include , #include_next
#include用于文件包含. 在#include 命令所在的行不能含有除注釋和空白符之外的其他任何內容.
#include "headfile"
#include <headfile>
#include 預處理標記
前面兩種形式大家都很熟悉, "#include 預處理標記"中, 預處理標記會被預處理器進行替換, 替換的結果必須符合前兩種形式中的某一種.
實際上, 真正被添加的頭文件并不一定就是#include中所指定的文件. #include"headfile"包含的頭文件當然是同一個文件, 但#include <headfile>包包含的"系統頭文件"可能是另外的文件. 但這不值得被注意. 感興趣的話可以查看宏擴展后到底引入了哪些系統頭文件.
關于#include "headfile"和#include <headfile>的區別以及如何在gcc中包含頭文件的詳細信息, 參考本blog的GCC筆記.
相對于#include, 我們對#include_next不太熟悉. #include_next僅用于特殊的場合. 它被用于頭文件中(#include既可用于頭文件中, 又可用于.c文件中)來包含其他的頭文件. 而且包含頭文件的路徑比較特殊: 從當前頭文件所在目錄之后的目錄來搜索頭文件.
比如: 頭文件的搜索路徑一次為A,B,C,D,E. #include_next所在的當前頭文件位于B目錄, 那么#include_next使得預處理器從C,D,E目錄來搜索#include_next所指定的頭文件.
?
可參考cpp手冊進一步了解#include_next
?
6. 預定義 的 宏
標準C中定義了一些對象宏, 這些宏的名稱以"__"開頭和結尾, 并且都是大寫字符. 這些預定義宏可以被#undef, 也可以被重定義.
下面列出一些標準C中常見的預定義對象宏(其中也包含gcc自己定義的一些預定義宏:
ANSI標準說明了五個預定義的宏名。它們是:
_LINE_ /* (兩個下劃線),對應%d*/ _FILE_ /* 對應%s */ __FUNCTION__ /* 對應%s */ _DATE_ /* 對應%s */ _TIME_ /* 對應%s */?
gcc定義的預定義宏:
__OPTMIZE__ 如果編譯過程中使用了優化, 那么該宏被定義為1. __OPTMIZE_SIZE__ 同上, 但僅在優化是針對代碼大小而非速度時才被定義為1. __VERSION__ 顯示所用gcc的版本號.可參考"GCC the complete reference".
要想看到gcc所定義的所有預定義宏, 可以運行: $ cpp -dM /dev/null
?
7. #line
#line用來修改__LINE__和__FILE__.?
e.g.
printf("line: %d, file: %s\n", __LINE__, __FILE__);
#line 100 "haha"
printf("line: %d, file: %s\n", __LINE__, __FILE__);
printf("line: %d, file: %s\n", __LINE__, __FILE__);
顯示:
line: 34, file: 1.c
line: 100, file: haha
line: 101, file: haha?
?
8. #pragma 和 _Pragma
#pragma用編譯器用來添加新的預處理功能或者顯示一些編譯信息. #pragma的格式是各編譯器特定的, gcc的如下:
#pragma GCC name token(s)
#pragma之后有兩個部分: GCC和特定的pragma name. 下面分別介紹gcc中常用的.
?
(1) #pragma GCC dependency
dependency測試當前文件(既該語句所在的程序代碼)與指定文件(既#pragma語句最后列出的文件)的時間戳. 如果指定文件比當前文件新, 則給出警告信息.?
e.g.
在demo.c中給出這樣一句:
#pragma GCC dependency "temp-file"
然后在demo.c所在的目錄新建一個更新的文件: $?touch temp-file, 編譯: $?gcc demo.c?會給出這樣的警告信息:?warning: current file is older than temp-file
如果當前文件比指定的文件新, 則不給出任何警告信息.
還可以在在#pragma中給添加自定義的警告信息.
e.g.
#pragma GCC dependency "temp-file" "demo.c needs to be updated!"
1.c:27:38: warning: extra tokens at end of #pragma directive
1.c:27:38: warning: current file is older than temp-file
注意: 后面新增的警告信息要用""引用起來, 否則gcc將給出警告信息.
?
(2) #pragma GCC poison token(s)
若源代碼中出現了#pragma中給出的token(s), 則編譯時顯示警告信息. 它一般用于在調用你不想使用的函數時候給出出錯信息.
e.g.
#pragma GCC poison scanf
scanf("%d", &a);?
warning: extra tokens at end of #pragma directive
error: attempt to use poisoned "scanf"
注意, 如果調用了poison中給出的標記, 那么編譯器會給出的是出錯信息. 關于第一條警告, 我還不知道怎么避免, 用""將token(s)引用起來也不行.
?
(3) #pragma GCC system_header
從#pragma GCC system_header直到文件結束之間的代碼會被編譯器視為系統頭文件之中的代碼. 系統頭文件中的代碼往往不能完全遵循C標準, 所以頭文件之中的警告信息往往不顯示. (除非用 #warning顯式指明).?
(這條#pragma語句還沒發現用什么大的用處)
?
由于#pragma不能用于宏擴展, 所以gcc還提供了_Pragma:
e.g.
#define PRAGMA_DEP #pragma GCC dependency "temp-file"
由于預處理之進行一次宏擴展, 采用上面的方法會在編譯時引發錯誤, 要將#pragma語句定義成一個宏擴展, 應該使用下面的_Pragma語句:
#define PRAGMA_DEP _Pragma("GCC dependency \"temp-file\"")
注意, ()中包含的""引用之前引該加上\轉義字符.
?
9. #warning, #error
#warning, #error分別用于在編譯時顯示警告和錯誤信息, 格式如下:
#warning tokens
#error tokens
e.g.
#warning "some warning"
注意, #error 和 #warning 后的 token 要用""引用起來!
(在gcc中, 如果給出了warning, 編譯繼續進行, 但若給出了error, 則編譯停止. 若在命令行中指定了 -Werror, 即使只有警告信息, 也不編譯.
?
10. 常用的預處理命令
預處理命令由#(hash字符)開頭, 它獨占一行, #之前只能是空白符. 以#開頭的語句就是預處理命令,不以#開頭的語句為C中的代碼行。
常用的預處理命令如下:
#define 定義一個預處理宏 #undef 取消宏的定義#include 包含文件命令 #include_next 與#include相似, 但它有著特殊的用途#if 編譯預處理中的條件命令, 相當于C語法中的if語句 #ifdef 判斷某個宏是否被定義, 若已定義, 執行隨后的語句 #ifndef 與#ifdef相反, 判斷某個宏是否未被定義 #elif 若#if, #ifdef, #ifndef或前面的#elif條件不滿足, 則執行#elif之后的語句, 相當于C語法中的else-if #else 與#if, #ifdef, #ifndef對應, 若這些條件不滿足, 則執行#else之后的語句, 相當于C語法中的else #endif #if, #ifdef, #ifndef這些條件命令的結束標志. defined 與#if, #elif配合使用, 判斷某個宏是否被定義#line 標志該語句所在的行號 # 將宏參數替代為以參數值為內容的字符竄常量 ## 將兩個相鄰的標記(token)連接為一個單獨的標記 #pragma 說明編譯器信息#warning 顯示編譯警告信息 #error 顯示編譯錯誤信息?
?
?
2. #define使用中的常見問題解析
?
2.1 簡單宏定義使用中出現的問題
?
?在簡單宏定義的使用中,當替換文本所表示的字符串為一個表達式時,容易引起誤解和誤用。如下例:
#define N 2+2 void main() {int a=N*N;printf(“%d”,a); }?
出現問題:
? ? 在此程序中存在著宏定義命令,宏N代表的字符串是2+2,在程序中有對宏N的使用,一般同學在讀該程序時,容易產生的問題是先求解N為2+2=4,然后在程序中計算a時使用乘法,即N*N=4*4=16,其實該題的結果為8,為什么結果有這么大的偏差?
?
問題解析:
宏展開是在預處理階段完成的,這個階段把替換文本只是看作一個字符串,并不會有任何的計算發生,在展開時是在宏N出現的地方 只是簡單地使用串2+2來代替N,并不會增添任何的符號,所以對該程序展開后的結果是a=2+2*2+2,計算后=8,這就是宏替換的實質,如何寫程序才能完成結果為16的運算呢?
?
解決辦法:
/*將宏定義寫成如下形式*/ #define N (2+2) /*這樣就可替換成(2+2)*(2+2)=16*/總結:把 宏體 和 所有的宏變量 都用 括號括起來
?
?
2.2? 帶參數的宏定義出現的問題
?
? ? 在帶參數的宏定義的使用中,極易引起誤解。例如我們需要做個宏替換能求任何數的平方,這就需要使用參數,以便在程序中用實際參數來替換宏定義中的參數。一般容易寫成如下形式:
#define area(x) x*x /*這在使用中是很容易出現問題的,看如下的程序*/ void main() {int y = area(2+2); printf(“%d”,y); }? ? 按理說給的參數是2+2,所得的結果應該為4*4=16,但是錯了,因為該程序的實際結果為8,仍然是沒能遵循純粹的簡單替換的規則,又是先計算再替換 了,在這道程序里,2+2即為area宏中的參數,應該由它來替換宏定義中的x,即替換成2+2*2+2=8了。那如果遵循(1)中的解決辦法,把2+2 括起來,即把宏體中的x括起來,是否可以呢?#define?area(x) (x)*(x),對于area(2+2),替換為(2+2)*(2+2)=16,可以解決,但是對于area(2+2)/area(2+2)又會怎么樣呢,有的學生一看到這道題馬上給出結果,因為分子分母一樣,又錯了,還是忘了遵循先替換再計算的規則了,這道題替換后會變為 (2+2)*(2+2)/(2+2)*(2+2)即4*4/4*4按照乘除運算規則,結果為16/4*4=4*4=16,那應該怎么呢?解決方法是在整個宏體上再加一個括號,即#define?? area(x) ((x)*(x)),不要覺得這沒必要,沒有它,是不行的。
??? 要想能夠真正使用好宏定義,那么在讀別人的程序時,一定要記住先將程序中對宏的使用全部替換成它所代表的字符串,不要自作主張地添加任何其他符號,完全展開后再進行相應的計算,就不會寫錯運行結果。
????如果是自己編程使用宏替換,則在使用簡單宏定義時,當字符串中不只一個符號時,加上括號表現出優先級,如果是帶參數的宏定義,則要給宏體中的每個參數加上括號,并在整個宏體上再加一個括號。看到這里,不禁要問,用宏定義這么麻煩,這么容易出錯,可不可以摒棄它, 那讓我們來看一下在C語言中用宏定義的好處吧。
如下代碼:
#include <iostream.h> #define product(x) x*x int main() {int i=3;int j,k;j = product(i++);cout<<"j="<<j<<endl;cout<<"i="<<i<<endl;k = product(++i);cout<<"k="<<k<<endl;cout<<"i="<<i<<endl;return 0; }依次輸出結果:
j=9;i=5;k=49;i=7
?
?
?
3. 宏定義的優點
?
(1)?? 方便程序的修改
??? 使用簡單宏定義可用宏代替一個在程序中經常使用的常量,這樣在將該常量改變時,不用對整個程序進行修改,只修改宏定義的字符串即可,而且當常量比較長時, 我們可以用較短的有意義的標識符來寫程序,這樣更方便一些。我們所說的常量改變不是在程序運行期間改變,而是在編程期間的修改,舉一個大家比較熟悉的例子,圓周率π是在數學上常用的一個值,有時我們會用3.14來表示,有時也會用3.1415926等,這要看計算所需要的精度,如果我們編制的一個程序中 要多次使用它,那么需要確定一個數值,在本次運行中不改變,但也許后來發現程序所表現的精度有變化,需要改變它的值, 這就需要修改程序中所有的相關數值,這會給我們帶來一定的不便,但如果使用宏定義,使用一個標識符來代替,則在修改時只修改宏定義即可,還可以減少輸入 3.1415926這樣長的數值多次的情況,我們可以如此定義 #define?? pi?? 3.1415926,既減少了輸入又便于修改,何樂而不為呢?
?
(2) 提高程序的運行效率
??? 使用帶參數的宏定義可完成函數調用的功能,又能減少系統開銷,提高運行效率。正如C語言中所講,函數的使用可以使程序更加模塊化,便于組織,而且可重復利用,但在發生函數調用時,需要保留調用函數的現場,以便子 函數執行結束后能返回繼續執行,同樣在子函數執行完后要恢復調用函數的現場,這都需要一定的時間,如果子函數執行的操作比較多,這種轉換時間開銷可以忽 略,但如果子函數完成的功能比較少,甚至于只完成一點操作,如一個乘法語句的操作,則這部分轉換開銷就相對較大了,但使用帶參數的宏定義就不會出現這個問 題,因為它是在預處理階段即進行了宏展開,在執行時不需要轉換,即在當地執行。宏定義可完成簡單的操作,但復雜的操作還是要由函數調用來完成,而且宏定義所占用的目標代碼空間相對較大。所以在使用時要依據具體情況來決定是否使用宏定義。
?
?
?
2. define中的三個特殊符號:#,##,#@ 和 do while
?
?
(1)?x##y?表示什么?表示x連接y。
? ? ##符號會連接兩個符號,從而產生新的符號(詞法層次),即?“##”是一種分隔連接方式,它的作用是先分隔,然后進行強制連接。 例如:
? ? #define SIGN( x ) INT_##x
? ? int SIGN( 1 ); 宏被展開后將成為:int INT_1;
? ? 舉例說:
? ? ? ? int n = Conn(123,456);? ? ? ? ? ? ? ?/*? 結果就是n=123456;? ? ? ? */
? ? ? ? char* str = Conn("asdf", "adf");? ?/* 結果就是 str = "asdfadf"; */
? ? ? ? #define TYPE1(type,name) type name_##type##_type
? ? ? ? #define TYPE2(type,name) type name##_##type##_type
? ? ? ? TYPE1(int, c); 轉換為:int name_int_type ; (因為##號將后面分為 name_ 、type 、 _type三組,替換后強制連接)
? ? ? ? TYPE2(int, d);轉換為: int d_int_type ; (因為##號將后面分為 name、_、type 、_type四組,替換后強制連接)
?
(2)再來看#@x,其實就是給x加上單引號,結果返回是一個const char。舉例說:
? ? ? ? char a = ToChar(1);結果就是a='1';
? ? ? ??做個越界試驗char a = ToChar(123);結果就錯了;
? ? ? ??但是如果你的參數超過四個字符,編譯器就給給你報錯了!
? ? ? ??error C2015: too many characters in constant?? :P
?
(3)最后看看#x,估計你也明白了,他是給x加雙引號。即 #符號把一個符號直接轉換為字符串。
? ? ? ? 也就是?#是“字符串化”的意思,出現在宏定義中的#是把跟在后面的參數轉換成一個字符串
? ? ? ??char* str = ToString(123132);就成了str="123132";
? ? ? ??#define ERROR_LOG(module) fprintf(stderr,"error: "#module"\n")
? ? ? ??ERROR_LOG("add"); 轉換為 fprintf(stderr,"error: "add"\n");
? ? ? ? ERROR_LOG(devied =0); 轉換為 fprintf(stderr,"error: devied=0\n");
?
(4)?宏定義用 do{ }while(0)
復雜宏定義及do{}while(0)的使用
#define foo() do{}while(0)
采用這種方式是為了防范在使用宏過程中出現錯誤,主要有如下幾點:
(1)空的宏定義避免warning:
#define foo() do{}while(0)
(2)存在一個獨立的block,可以用來進行變量定義,進行比較復雜的實現。
(3)如果出現在判斷語句過后的宏,這樣可以保證作為一個整體來是實現:
?#define foo(x) \
action1(); \
? action2();
在以下情況下:
if(NULL == pPointer)
? ?foo();
就會出現action1和action2不會同時被執行的情況,而這顯然不是程序設計的目的。
(4)以上的第3種情況用單獨的{}也可以實現,但是為什么一定要一個do{}while(0)呢,看以下代碼:
#define switch(x,y) {int tmp; tmp="x";x=y;y=tmp;}
if(x>y)
switch(x,y);
else? ?? ? //error, parse error before else
otheraction();
在把宏引入代碼中,會多出一個分號,從而會報錯。使用do{….}while(0) 把它包裹起來,成為一個獨立的語法單元,從而不會與上下文發生混淆。同時因為絕大多數的編譯器都能夠識別do{…}while(0)這種無用的循環并進行優化,所以使用這種方法也不會導致程序的性能降低。
為了看起來更清晰,這里用一個簡單點的宏來演示:
#define SAFE_DELETE(p) do{ delete p; p = NULL} while(0)假設這里去掉do...while(0),
#define SAFE_DELETE(p) delete p; p = NULL;那么以下代碼:
if(NULL != p) SAFE_DELETE(p) else ...do sth...就有兩個問題:
- 1) 因為if分支后有兩個語句,else分支沒有對應的if,編譯失敗
- 2) 假設沒有else, SAFE_DELETE中的第二個語句無論if測試是否通過,會永遠執行。
你可能發現,為了避免這兩個問題,我不一定要用這個令人費解的do...while, 我直接用{}括起來就可以了
#define SAFE_DELETE(p) { delete p; p = NULL;}的確,這樣的話上面的問題是不存在了,但是我想對于C++程序員來講,在每個語句后面加分號是一種約定俗成的習慣,這樣的話,以下代碼:
if(NULL != p) SAFE_DELETE(p); else ...do sth...其else分支就無法通過編譯了(原因同上),所以采用do...while(0)是做好的選擇了。也許你會說,我們代碼的習慣是在每個判斷后面加上{}, 就不會有這種問題了,也就不需要do...while了,如:
if(...)?
{
}
else
{
}
現有一個例子:#define PROJECT_LOG(level,arg) \ dosomething();\ if (level <= PROJECT_LOG_get_level()) \ PROJECT_LOG_wrapper_##level(arg);
現在假設有以下應用,現有一個例子:
#define PROJECT_LOG(level,arg) \dosomething();\if (level <= PROJECT_LOG_get_level()) \PROJECT_LOG_wrapper_##level(arg);現在假設有以下應用:if(L==1)PROJECT_LOG(L,"AAA");宏轉開為:if(L==1)dosomething();if (1 <= PROJECT_LOG_get_level())PROJECT_LOG_wrapper_1("AAA"); ;顯然if(L==1)只管到dosomething();而后面的if (1 <= PROJECT_LOG_get_level())PROJECT_LOG_wrapper_1("AAA"); ;則成了獨立的語句。假如使用do{}while(0)語句塊,進行宏定義:#define PROJECT_LOG(level,arg)do{ \dosomething();\if (level <= PROJECT_LOG_get_level()) \PROJECT_LOG_wrapper_##level(arg); \ }while(0)上述應用轉開后為: if(L==1) do{dosomething();if (1<= PROJECT_LOG_get_level())PROJECT_LOG_wrapper_1("AAA"); }while(0);這樣避免了意外的麻煩。OK現在明白了很多C程序中奇怪的do{}while(0)宏定義了吧使用示例代碼:
#include <stdio.h>#define PRINT1(a,b) \{ \printf("print a\n"); \printf("print b\n"); \}#define PRINT2(a, b) \do{ \printf("print a\n"); \printf("print b\n"); \}while(0) #define PRINT(a) \do{\printf("%s: %d\n",#a,a);\printf("%d: %d\n",a,a);\}while(0)#define TYPE1(type,name) type name_##type##_type #define TYPE2(type,name) type name##_##type##_type#define ERROR_LOG(module) fprintf(stderr,"error: "#module"\n")int main() {int a = 20;int b = 19;TYPE1(int, c);ERROR_LOG("add");name_int_type = a;TYPE2(int, d);d_int_type = a;PRINT(a);if(a > b){PRINT1(a, b);}else{PRINT2(a, b);}return 0; }?
?
?
3. 常用的一些宏定義
?
1 防止一個頭文件被重復包含
#ifndef BODYDEF_H #define BODYDEF_H //頭文件內容 #endif2 得到指定地址上的一個字節或字
#define MEM_B( x ) ( *( (byte *) (x) ) ) #define MEM_W( x ) ( *( (word *) (x) ) )用法如下:
#include <iostream> #include <windows.h> #define MEM_B(x) (*((byte*)(x))) #define MEM_W(x) (*((WORD*)(x))) int main() {int bTest = 0x123456;byte m = MEM_B((&bTest));/*m=0x56*/int n = MEM_W((&bTest));/*n=0x3456*/return 0; }3 得到一個field在結構體(struct)中的偏移量
#define OFFSETOF( type, field ) ( (size_t) &(( type *) 0)-> field )??請參考文章:詳解寫宏定義:得到一個field在結構體(struct type)中的偏移量。
?
4 得到一個結構體中field所占用的字節數
#define FSIZ( type, field ) sizeof( ((type *) 0)->field )5 得到一個變量的地址(word寬度)
#define B_PTR( var ) ( (byte *) (void *) &(var) ) #define W_PTR( var ) ( (word *) (void *) &(var) )6 將一個字母轉換為大寫
#define UPCASE( c ) ( ((c) >= ''a'' && (c) <= ''z'') ? ((c) - 0x20) : (c) )7 判斷字符是不是10進值的數字
#define DECCHK( c ) ((c) >= ''0'' && (c) <= ''9'')8 判斷字符是不是16進值的數字
#define HEXCHK( c ) ( ((c) >= ''0'' && (c) <= ''9'') ||((c) >= ''A'' && (c) <= ''F'') ||((c) >= ''a'' && (c) <= ''f'') )9 防止溢出的一個方法
#define INC_SAT( val ) (val = ((val)+1 > (val)) ? (val)+1 : (val))10 返回數組元素的個數
#define ARR_SIZE( a ) ( sizeof( (a) ) / sizeof( (a[0]) ) )?
?
?
4. 宏的使用場景
?
1. 打印錯誤信息
如果程序的執行必須要求某個宏被定義,在檢查到宏沒有被定義是可以使用#error,#warning打印錯誤(警告)信息,如:
#ifndef __unix__ #error "This section will only work on UNIX systems" #endif只有__unix__宏被定義,程序才能被正常編譯。
?
2. 方便調試
__FILE, __LINE, __FUNCTION是由編譯器預定義的宏,其分別代表當前代碼所在的文件名,行號,以及函數名。可以在代碼中加入如下語句來跟蹤代碼的執行情況:
if(err) {printf("%s(%d)-%s\n",__FILE__,__LINE__,__FUNCTION__); }?
3. C/C++的混合編程
? ? ? ? 函數int foo(int a, int b);
? ? ? ? 在C語言的該函數在編譯器編譯后在庫中的名字為_foo,而C++中該函數被編譯后在庫中的名字為_foo_int_int(為實現函數重載所做的改變)。如果C++中需要使用C編譯后的庫函數,則會提示找不到函數,因為符號名不匹配。C++中使用extern “C”解決該問題,說明要引用的函數是由C編譯的,應該按照C的命名方式去查找符號。
? ? ? ? 如果foo是C編譯的庫,如果要在C++中使用foo,需要加如下聲明,其中__cplusplus是c++編譯器預定義的宏,說明該文件是被C++編譯器編譯,此時引用C的庫函數,就需要加extern “C”。
4. 使用宏打印 Log 使用示例
#include <stdio.h>typedef enum {ERROR_ONE, // 0ERROR_TWO,ERROR_THREE,ERROR_END }E_ERROR_CODE;unsigned long g_error_statistics[ERROR_END] = {0};/* LOG 打印, # 直接常亮字符串替換 */ #define LOG_PRINT(ERROR_CODE) \ do { \g_error_statistics[ERROR_CODE]++; \printf("[%s : %d], error is %s\n", __FILE__, __LINE__, #ERROR_CODE); \ } while (0)/* ERROR 公共前綴,傳參時省略的寫法, ## 直接展開拼接 */ #define LOG_PRINT_2(CODE) \ do { \g_error_statistics[ERROR_ ## CODE]++; \printf("[%s : %d], error is %s\n", __FILE__, __LINE__, "ERROR_" #CODE); \ } while (0)int main() {LOG_PRINT(ERROR_TWO);LOG_PRINT_2(ONE);for (unsigned int i = 0; i < ERROR_END; ++i) {printf("error %u statistics is %lu \n", i, g_error_statistics[i]);}return 0; }寫文件記錄log
#include <stdio.h> #include <stdarg.h> #include <time.h> int write_log (FILE* pFile, const char *format, ...) { va_list arg; int done; va_start (arg, format); //done = vfprintf (stdout, format, arg); time_t time_log = time(NULL); struct tm* tm_log = localtime(&time_log); fprintf(pFile, "%04d-%02d-%02d %02d:%02d:%02d ", tm_log->tm_year + 1900, tm_log->tm_mon + 1, tm_log->tm_mday, tm_log->tm_hour, tm_log->tm_min, tm_log->tm_sec); done = vfprintf (pFile, format, arg); va_end (arg); fflush(pFile); return done; } int main() { FILE* pFile = fopen("123.txt", "a"); write_log(pFile, "%s %d %f\n", "is running", 10, 55.55); fclose(pFile); return 0; } /* 編譯運行: gcc log.c -o log ./log返回結果:cat 123.txt 2016-12-13 13:10:02 is running 10 55.550000 2016-12-13 13:10:04 is running 10 55.550000 2016-12-13 13:10:04 is running 10 55.550000 */使用示例代碼:
#include<stdio.h> #include <time.h> #include <windows.h> #include <string.h> #include <stdarg.h>#define DBG_WRITE(fmt,args) DBG_Write_Log(strrchr(__FILE__, '\\')+1, __LINE__, fmt,##args);void DBG_Write_Log(char* filename, int line, char* fmt, ...) { FILE* fp;va_list argp;char* para;char logbuf[512];char timeStr[20];time_t tt;struct tm *local;tt = time(NULL);local = localtime(&tt);strftime(timeStr, 20, "%Y-%m-%d %H:%M:%S", local);sprintf(logbuf, "[%s] %s[%d]", timeStr, filename, line);va_start(argp, fmt);vsprintf(logbuf+strlen(logbuf), fmt, argp);va_end(argp);fprintf(fp, logbuf);fclose(fp);printf(logbuf); }void main() {DBG_WRITE("test log [%d]system[%s][%d]\n", 1234,"add by test", 5);DBG_Write_Log(strrchr(__FILE__,'\\')+1, __LINE__, "%s %d\n", "add by test", 5); }幾種 log 打印 printf 函數 的 宏定義 示例代碼
#include <stdio.h>#define lU_DEBUG_PREFIX "##########"#define LU_DEBUG_CMD 0x01 #define LU_DEBUG_DATA 0x02 #define LU_DEBUG_ERROR 0x04#define LU_PRINTF_cmd(msg...) do{if(g_lu_debugs_level & LU_DEBUG_CMD)printf(lU_DEBUG_PREFIX msg);}while(0) #define LU_PRINTF_data(msg...) do{if(g_lu_debugs_level & LU_DEBUG_DATA)printf(lU_DEBUG_PREFIX msg);}while(0) #define LU_PRINTF_error(msg...) do{if(g_lu_debugs_level & LU_DEBUG_ERROR)printf(lU_DEBUG_PREFIX msg);}while(0)#define lu_printf(level, msg...) LU_PRINTF_##level(msg) #define lu_printf2(...) printf(__VA_ARGS__) #define lu_printf3(...) lu_printf(__VA_ARGS__) static int lu_printf4_format(int prio, const char *fmt, ...); #define lu_printf4(prio, fmt...) lu_printf4_format(prio, fmt)int g_lu_debugs_level; //控制打印等級的全局開關 //lu_printf 類似內核的分等級打印宏,根據g_lu_debugs_level和輸入的第一個標號名來決定該句打印是否輸出。 //lu_printf3 等同于 lu_printf //lu_printf2 等同于 printf //lu_printf4 等同于 lu_printf4_format,作用是把輸入的第一個整型參數用<val>的格式打印出來 int main(int argc, char *argv[]) {g_lu_debugs_level |= LU_DEBUG_CMD | LU_DEBUG_DATA | LU_DEBUG_ERROR;printf("g_lu_debugs_level = %p\n", g_lu_debugs_level);lu_printf(cmd,"this is cmd\n");lu_printf(data,"this is data\n");lu_printf(error,"this is error\n");g_lu_debugs_level &= ~(LU_DEBUG_CMD | LU_DEBUG_DATA);printf("g_lu_debugs_level = %p\n", g_lu_debugs_level);lu_printf(cmd,"this is cmd\n");lu_printf(data,"this is data\n");lu_printf(error,"this is error\n");lu_printf2("aa%d,%s,%dbbbbb\n", 20, "eeeeeee", 100);g_lu_debugs_level |= LU_DEBUG_CMD | LU_DEBUG_DATA | LU_DEBUG_ERROR;printf("g_lu_debugs_level = %p\n", g_lu_debugs_level);lu_printf3(cmd,"this is cmd \n");lu_printf3(data,"this is data\n");lu_printf3(error,"this is error\n");lu_printf4(0,"luther %s ,%d ,%d\n", "gliethttp", 1, 2);return 0; }#include <stdarg.h> static int lu_printf4_format(int prio, const char *fmt, ...) { #define LOG_BUF_SIZE (4096)va_list ap;char buf[LOG_BUF_SIZE];va_start(ap, fmt);vsnprintf(buf, LOG_BUF_SIZE, fmt, ap);va_end(ap);printf("<%d>: %s", prio, buf);printf("------------------------\n");printf(buf); }#define ENTER() LOGD("enter into %s", __FUNCTION__)#define LOGD(...) ((void)LOG(LOG_DEBUG, LOG_TAG, __VA_ARGS__))#define LOG(priority, tag, ...) \LOG_PRI(ANDROID_##priority, tag, __VA_ARGS__)#define LOG_PRI(priority, tag, ...) \android_printLog(priority, tag, __VA_ARGS__)#define android_printLog(prio, tag, fmt...) \__android_log_print(prio, tag, fmt)使用示例代碼:
#include <stdio.h>/* Define Log print macro */ #define MyLog(DebugLevel, format, ...) \do{ \switch (DebugLevel) \{ \case 1: \printf(format, ##__VA_ARGS__); \break; \case 2: \printf("Function: "__FUNCTION__", Line: %d, ---> "format"", __LINE__, ##__VA_ARGS__); \break; \case 3: \printf("File: "__FILE__", Function: "__FUNCTION__", Line: %d, ---> "format"", __LINE__, ##__VA_ARGS__); \break; \default: \break; \} \}while(0)int main(void) {MyLog(1, "Simple Log print!\r\n");MyLog(2, "Satndard Log display!\r\n");MyLog(3, "Detail Log view!\r\n");MyLog(1, "If debug level is not equal 1,2 or 3 that log is invisible, such as next line :\r\n");MyLog(6, "I am invisible log!\r\n");MyLog(1, "Now, I think you have understood how to use MyLog macro.\r\n");return 0; }使用示例代碼:
#include <stdio.h> #define LOG_DEBUG "DEBUG" #define LOG_TRACE "TRACE" #define LOG_ERROR "ERROR" #define LOG_INFO "INFOR" #define LOG_CRIT "CRTCL" #define LOG(level, format, ...) \ do { \ fprintf(stderr, "[%s|%s@%s,%d] " format "\n", \ level, __func__, __FILE__, __LINE__, ##__VA_ARGS__ ); \ } while (0) int main() { LOG(LOG_DEBUG, "a=%d", 10); return 0; } // 或者 /* #define DBG(format, ...) fprintf(stderr, "[%s|%s@%s,%d] " format "\n", APP_NAME, __FUNCTION__, __FILE__, __LINE__, ##__VA_ARGS__ ); */使用示例代碼:
#define?LOG(level,?format,?...)?\ do?{?\ fprintf(stderr,?"[%s|%s@%s,%d]?"?format?"/n",?\level,?__func__,?__FILE__,?__LINE__,?##__VA_ARGS__?);?\ }?while?(0)使用示例代碼:
#include <stdio.h> #include <stdlib.h> #include <unistd.h>#define LOG(fmt, ...) do \ { \printf("[%s][%s][%s:%d] %s:"fmt"\n", __DATE__, __TIME__, __FILE__, __LINE__, __FUNCTION__, ##__VA_ARGS__); \ }while(0)int main(void) {char* str = "this is test string";int num = 100;LOG("test string : %s . test num: %d", str, num);return 0; }?
有關宏定義的經驗與技巧-簡化代碼-增強Log:http://blog.csdn.net/zh_2608/article/details/46646385
C語言日志處理:https://www.cnblogs.com/274914765qq/p/4589929.html
?
------------------------------------------------------------------------------------------------------------------------
?
?
?
總結
以上是生活随笔為你收集整理的C 和 C++ 宏 详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安卓逆向_3 --- 篡改apk名称和图
- 下一篇: C 和 C++ 文件操作详解