什麼時候應該使用宏定義?

宏定義相比函數調用,除了執行效率上的考量,還有其他的優勢么? 什麼時候應當使用宏定義,什麼時候應當定義函數完成操作呢?


以C++來說,用宏的目的不是性能問題(一般可用內聯函數及模板元編程),而是為了減少一些重複代碼(如利用#和##),以及條件編譯(是否編譯某些模塊、使用不同平台/編譯器的非C++語言標準功能)。


宏定義用來做函數什麼的弱爆了,如果不能創造更優美的表達,那你用宏就是給自己找麻煩。


在這裡我舉一個例子

這是我在

這是我在 https://github.com/vczh/tinymoe 用的一個超簡單的單元測試框架。使用的時候是這樣子的,隨便找一個cpp空白的地方寫:

TEST_CASE(ThisIsATestCase)
{
TEST_ASSERT(1+1==2);
}

所有的testcase就會在main函數之前自動運行。最後在main函數裡面把所有全局變數刪除掉,用VC++自帶的內存泄漏工具一搞,就完美了。test case不僅寫起來方便,而且保證沒有內存泄漏


同意 @vczh大神的用法,另外再補充一些。

1.在嵌入式編程(C語言)中,用宏定義有時候會更加方便一些。

比如:

#define ADC_RESET(x) {if(x0x01) P1OUT |= BIT0; else P1OUT = ~BIT0;}

這行代碼就是將一個寄存器 (MSP430系列單片機為例) 的第0位的高低電平控制寫成了一個函數的形式,這樣做有什麼好處呢?首先就是直觀易懂,你後面在寫程序的時候,直接寫ADC_RESET(1),就可以將控制的ADC晶元RESET引腳拉高,別人讀起來也比較容易懂,這樣就可以避免每次都寫P1OUT |= BIT0, 也避免了一次一次查看連線原理圖(接線圖看多了容易瞎,真的TT)。

更重要的一點是,嵌入式編程通常要考慮程序的可移植性,比如上面這兩行代碼是給MSP430單片機寫的晶元驅動程序。要是換了個平台,如果不用宏定義而是直接寫寄存器名稱操作的話,程序移植起來就比較蛋疼,因為各個晶元寄存器名稱可能都不太一樣。或者,更常見的情況,你把驅動程序寫好了交給用戶使用,用戶根據自己需要也許會把P1埠換到P2埠,這時用戶只需將宏定義稍作修改即可,而不必對程序主體進行修改。

2.雖然宏定義能帶來以上諸多方便,但是!

Google的C++編程規範是不推薦使用宏定義的[1]。為什麼呢?Google的解釋是:

Macros mean that the code you see is not the same as the code the compiler sees. This can introduce unexpected behavior, especially since macros have global scope.

也就是說,會引入不可預知的風險。 @vczh 所說的給自己找麻煩,可能也是這個意思吧。按Google編程規範[1]里的說法,宏定義在一些情況下可以用內聯函數(inline function),const型變數,枚舉(enums)等方法代替。而在第一點中所說的嵌入式編程環境中,首先工程一般都不會太龐大,一般在人肉可控範圍,其次在某些情況下不用宏定義反而會造成不便,例如對於某個型號的單片機,使用const聲明的變數在編譯的時候是默認存儲在Flash裡面,而非和其他變數存儲在一起。

3. 例外,在頭文件里:

#ifndef __AD9850_H__
#define __AD9850_H__
//bla bla
#endif

這種防止多重包含的宏定義Google是推薦使用的。

總結:在工程規模較小,不是很複雜,與硬體結合緊密,要求移植性的時候,可採用宏定義簡化編程,增強程序可讀性;在PC平台上,工程規模較大時,除上述第三點所述情況及自己非常有把握的情況下,盡量少用宏定義。

[1] http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml?showone=Preprocessor_Macros#Preprocessor_Macros
以上。


以 Lua 代碼為例,一般用在:

  1. 展開前後代碼量相差不大。比如一般是去掉一些不常用的參數,用 default value 代替。
  2. 展開後的代碼可能受一些條件編譯的影響。比如 api_checknelems。

宏,就其本身來說,沒有太大問題。它最大的問題是容易愚弄工具。IDE 語法提示,定義跳轉,Debugger 單步都會受到宏的愚弄。第一條就主要是為了 debugger 單步考慮。第二條是因為,不用宏來處理條件編譯固然也可以做到(比如 Linux 著名的 arch 目錄),但是要寫的額外處理比較多,對小量代碼不合適。


宏定義我喜歡,我來答。
就C語言來說,宏定義太重要了,在沒有他兒子和孫子輩語言那些高級功能的時候,宏定義就靠你了!下面說的均是在C語言/類C語言環境中。

1 取代Magic Number
其他知友也提到了,這個作用在嵌入式開發時太重要了。一個Soc 50多頁的datasheet,上百個寄存器的位操作,如果沒有很好的語義宏定義,調程序查手冊絕對是崩潰的節奏!

2 防止重複定義
#ifndef XXXX
#define XXXX
#endif

3 快速控制代碼編譯

#define SIMULATION_DEBUG 1
#if SIMULATION_DEBUG
use simulated data
#else
real data
#endif
我會告訴你我經常用#if 0嗎?

4 編譯系統控制鏈
在某些系統下,可以把宏定義傳遞給編譯器,從而通過編譯腳本(makefile)來控制編譯選項
例如,配合上面代碼,就可以在Makefile里定義:
CDEFINES=$(CDEFINES) /SIMULATION_DEBUG

說道編譯,自然還要提一下 ANSI C中預定義的幾個非常有用的編譯宏:

  • _ L I N E _
  • _ F I L E _
  • _ D A T E _
  • _ T I M E _
  • _ S T D C _

例如
#define DEBUGMSG(msg,date) printf(msg);printf(「%d%d%d」,date,_LINE_,_FILE_)

5 使用typedef
C語言下typedef也是屬於宏定義,我會告訴你C語言下struct+typedef+函數指針可以做到面向對象的繼承,重載和多態嗎?(有興趣可以看一下MFC的源碼實現,把傳統的C風格的Windows API各種包裝修飾和裝逼。)
typedef int (*PFFunc1)(int);

6 簡化操作
得到一個字的高位和低位位元組

#define WORD_LO(xxx) ((byte) ((word)(xxx) 255))

#define WORD_HI(xxx) ((byte) ((word)(xxx) &>&> 8))


迄今為止,宏定義主要是用在,
(1)精簡代碼;
(2)做一下簡單運算,
(3)定義一些簡單直觀的常量的方面了。

向上面的(2)和(3)在 C++ 中都有可以替代品,也就是說,宏在這些場合中顯得沒那麼重要了,但是依然作為使用習慣,被繼續沿用著。

對於(1),宏可以被多處引用,
可以減輕一些程序員的 記憶負擔,書寫負擔,提高可維護性。
(我們知道,對可維護性損傷很大的一個來源就是,代碼中相互關聯而又分布零散混亂的 hard code 。)

簡單的例子,比如說 windowsx.h 中的
ComboBox_SetCurSel 一類的,可以避免程序員每次使用時,查閱相關消息中 wParam,lParam 的含義。

再比如說,你有一個內容固定的規模不大的數組,需要在項目中很多處進行臨時聲明,則它就適合定義成宏。

當然,還有遠比這顯得更巧妙的提高可維護性的例子,這裡就不細說了。

--
總之,宏並不是那種「十惡不赦」絕對不能碰的東西,相反,它是一個程序員的工具,並且要求你理解它的運作方式,以及正確,合理的運用,則它就會成為程序員的輔助。

因為宏有一些副作用,所以宏被用於出一些題目,以及有的人不提倡使用。


在 C/C++ 盡量避免使用,盡量自己寫腳本生成 .c .h 。


從宏的幾種用法來說說宏的具體應用範圍:

1、編譯器參數和條件編譯

2、定義常數

3、宏函數

一個一個說:

1、編譯器參數和條件編譯

這個沒別的選擇,絕大多數C編譯器都是通過define某些宏(或者宏的值)來告知代碼編譯平台/硬體平台的。

同時,Makefile或者其它編譯腳本希望傳遞某些編譯參數給代碼的幾乎唯一方式就是宏,所以這是宏最重主要的用途。頭文件里大量使用的#ifdef _XXXX_H_ 也是類似用途。

如果說普通宏函數什麼的可以被替代的話,條件編譯這些是很難被別的方法取代的。

2、定義常數

肯定有人會說,用const更好,但const是強類型的,單獨佔用一個內存的,在某些場景下const的效果不如用宏好。

例子1:

#define MIN_VALUE -1
const unsigned int x = -1;
unsigned long long y;

y = MIN_VALUE;
y = x;

可能有人說我耍賴,數據類型不一致,但實際工程中,如果大量用到const,那麼使用者很難一一確認const的類型是什麼。

例子2:

#define STRING "Hello"
const char * p = "Hello";
char * n;
n = p;
n = STRING;

這個例子中,const賦值給非const變數,會有風險,編譯器有警告,而用宏定義是沒有問題的。如果const賦值給多個變數,各個變數共享同一段內存空間,如果是宏常數,通過修改編譯器開關,可以保證各個變數使用不同的。

3、宏函數

宏函數可以分為兩類,一類是「普通」的宏函數,就是一小段跟普通函數差不多的代碼,這種宏函數是完全可以被普通函數或者inline替代的。

一般來說,如果是「普通」但又複雜(代碼較長)的宏函數,多數商業代碼里是不推薦使用的,因為宏函數有展開風險。(比較著名的max宏傳++參數的代碼,可以自己在網上找到)

對於「普通」且較小的宏函數,在嵌入式行業特別常見,因為早期的時候編譯器優化做的還不夠好,嵌入式代碼有些對性能有要求,所以有大量使用宏函數的地方,但現在正在被慢慢取代。

宏的另一類函數就是帶「#」和「##」的宏,就比如這個東西:

#define ERRMSG(type,fmt,arg)
do {
if ((type##Debug) DEBUG_LEVEL)
printf("ERROR: "#type" - " fmt, arg)
} while (FALSE)

C語言里沒有模板,可以大概理解為#和##類似於C++里的模板。這也是宏函數不可替代的部分。

---------總結---------

宏是有很多問題,但大多數宏的問題都表現在宏函數展開風險上,在我列舉的其它應用中,宏的意義很重要,並且不可取代。如果是普通宏函數,根據自己的需要,盡量使用普通函數替代。


這些答案其實都在說一個問題,把宏當作meta-programming的手段來用。

最好的例子請參考boost.preprocessor,以及其作者的一個增強版本chaos-pp,還有Jens Gustedt的P99,http://gustedt.wordpress.com/。

如果這個方式還不如直接編碼,那就不要用,這方面比較不好例子就是MFC。


講真。。。除了那種死東西
比如說定義個PI啊什麼的,最好少用。。。難維護的一逼
90%的宏我都要琢磨半天,(當然是因為我水平有限,可惜大部分維護你程序的人估計連我這個水平都沒有)要是裡面有bug。。。那調試都不好調
mfc難學的一個原因就是它那些光怪陸離的宏
寫個函數就那麼難么?


在inline函數、模板(對於C++)不能完成你想乾的事情的時候,就試試宏。這種事情通常都是類似於代碼生成。

如果宏也不好用,那麼這時候你很可能需要外部的代碼生成器,比如把業務邏輯或者重複內容寫成tab分割的文本表格,然後用自己寫的腳本按照內容生成代碼。


直接回答:當函數無法實現某個功能的時候才用宏

  • 不要為了效率而使用宏,函數展開這種優化,是編譯器的事情;
  • 用宏代替部分函數,或許真的會提升一丁點兒的效率,但是為了這點效率,付出的代價很多,不划算(具體付出了哪些代價,各種書籍上已經說的很多了);
  • 函數求值通常實現為應用序,宏有點類似正則序,而且可以拼接symbol,所以宏可以做一些函數不能做的事情,只有這個時候用宏,其他時候用函數;

註:上面提及的宏指的是C/C++這類語言中的宏,scheme這類語言中的宏另當別論。


我可以給出一個使用錯誤的栗子。
比如這樣(重度中二病的時候乾的事情):

namespace __cmp{
namespace value{
template &
struct VTrue{
bool operator() (const T x, const T y) const{
return true;
}
};
template &
struct VFalse{
bool operator() (const T x, const T y) const{
return false;
}
};
};
template &,class FVal = value::VFalse& &>
struct _if{
bool operator() (const T x, const T y) const{
return (Func()(x,y))?(TVal()(x,y)):(FVal()(x,y));
}
};

template &
struct _less{
bool operator() (const T x, const T y) const{
return (LVal()(x)&
struct _greater{
bool operator() (const T x, const T y) const{
return (LVal()(x)&>RVal()(y));
}
};

template &
struct _equ{
bool operator() (const T x, const T y) const{
return (LVal()(x)==RVal()(y));
}
};

template &
struct _value{
typedef RVal T::*q;
template &
struct instance{
RVal operator() (const T x) const{
return x.*func;
}
};
};

template &
struct _ref{
//typedef RVal q;
template &
struct instance{
RVal operator() (const T x) const{
return func;
}
};
};

template &
struct _static
{
template &
struct instance{
RVal operator() (const T x) const{
return data;
}
};
};

template &
struct _func{
typedef RVal (T::*func)();
template &
//template &
struct instance{
RVal operator() (const T x) const{
return ((const_cast&(x))-&>*ptr)();
}
};
};
};

#define _cmp(T , Name , Arg)
namespace __cmp{
namespace X{
struct Name##_Entry{
typedef T Type;
typedef __cmp::value::VTrue& True;
typedef __cmp::value::VFalse& False;
typedef Arg invoke;
};
};
};
typedef __cmp::X::Name##_Entry::invoke Name;

#define _if(condition,Vt,Vf) __cmp::_if&
#define _val(S) __cmp::_value&::instance&
#define _ref(S) __cmp::_ref&

::instance&
#define _static(S) __cmp::_static&

::instance&
#define _less(v1,v2) __cmp::_less&< v1, v2 , Type&>
#define _greater(v1,v2) __cmp::_greater&< v1, v2 , Type&>
#define _equ(v1,v2) __cmp::_equ&< v1, v2 , Type&>

// using namespace __cmp;
// typedef _if&

&> pair_less;
struct pos{
int x,y;
int GetX(){
return x + 1;
}
};
struct Cmp{
bool operator () (const pos x,const pos y) const{
return (x.x &< y.x) || (x.x==y.x x.y&::instance&()(A));
//typedef int (pos::*func)();
//func F = pos::GetX;
//printf("%d
",);
//char const[] = ;
//(A);
//printf("%d
",pair_less()(A,B));
}


老問題了還是來回答一發。
宏可以用來節省很多重複代碼。一些代碼可以用模板來省,但是另外一些卻只能用宏。

舉個例子

/// 代碼里很多函數都是這種形狀的

uint32_t session = theNetwork.request_guid();
theApp.invoke([=] {
auto src = theSourceMgr.get_source(source_id);
if (src == nullptr) {
respond_failure(m);
} else {
/// 不同的邏輯處理
}
});

/// 還有些這樣的函數,只是getter不同

uint32_t session = theNetwork.request_guid();
theApp.invoke([=] {
auto src = theSourceMgr.get_source_camera(source_id); /// getter 不同
if (src == nullptr) {
respond_failure(m);
} else {
/// 不同的邏輯處理
}
});

/// 把共同部分定義成宏

#define query_begin()
uint32_t session = theNetwork.request_guid();
theApp.invoke([=] {

#define query_end() });

#define query_source_begin_base(m, getter)
query_begin()
auto src = theSourceMgr.##getter(source_id);
if (src == nullptr) {
respond_failure(m);
} else {

/// 細化getter

#define query_source_begin(m) query_source_begin_base(m, get_source)

#define query_source_end()
}
query_end()

#define query_camera_begin(m) query_source_begin_base(m, get_source_camera)

#define query_camera_end() query_source_end()

/// 使用了宏之後,大量的函數就變成了這樣的形狀

query_camera_begin(source_camera_get_config_sc)
auto rr = src-&>get_config();
respond_success(source_camera_get_config_sc, rr);
query_camera_end()

/// 在一對begin和end的內部放置真正不同的邏輯處理,代碼更整潔清楚了


創造一些新事物的時候用,比如想用C++的引用或模板,想用跨平台的條件編譯,想創造類型無關的函數,想裝逼的時候。
如果可以,還是少用,除非萬不得已,沒有任何辦法,那你就用吧。
不過linux內核在這方面用得還是很優雅的,比如它在對容器的設計時使用宏是為了定義變數或語法糖時提高可讀性,再比如它為了把一些底層位運算變得更好理解改裝成宏,也是為了可讀性,這在嵌入式中經常會使用,因為代碼終究是給人看的,比如對硬體的IO口操作往往只是像變數賦值一樣操作。這TM誰讀得懂,所以後來的代碼設計全用宏封裝成可讀性函數了2333。


我也說下我所知道的使用宏定義的兩個場景,為了表述好我的意思,語言描述不一定很嚴謹。
1. 第一個場景:你寫了一個.c的源代碼,裡面用到一個整型常量,比如「5」這個數字吧。你的源代碼里有100處用到了「5」。有一天,你突然發現要把「5」改成「6」,你懵逼了,使用ctrl+f來搜索5,結果搜了遠遠超過100個地方的「5」,你仰天長哭。此時使用宏定義,比如:#define number 5 然後在你代碼中那100個用到「5」的地方,全部改用number,這樣下次你改6也好,改7也好,只需改#define number 5就行;
2. 第二個場景:你又寫了一個程序,而且是在32位機上寫的。程序中用到了「int」這個類型,有1000個地方用到了,在32位機上,int佔用了4個位元組。有一天你老闆發神經,說公司發財了,要買128位機(假設有),要把你的程序移到128位機上,但是在128位機上,int佔用8個位元組(假設佔8個位元組),從4位元組到8位元組,你又懵逼了,程序里好多處理邏輯都是按照4位元組來處理的!此時使用宏定義,比如:#define int INT4,然後在你代碼那1000個地方用到「int"的地方,全部改為用INT4,這樣你還到128位機,假設128位機short類型佔4位元組,你只要改#define short INT4就可以了。


// Happy debugging, suckers
# define true (rand() &> 10)


能省代碼的時候
能隱藏糟糕的實現和不可讀的代碼時
和以上各答案。


宏定義是C語言一項十分重要的功能。宏定義的一部分功能和語言本身的功能是相同的。這是我們往往會糾結於是應該使用宏定義去實現呢,還是去用語言實現呢。 這是我們就需要非常明確的知道宏定義和功能相當的宏定義之間的細微區別。這是來幫助我們做出正確的決定,

舉個例子: 帶參數的宏定義和函數之間的細微區別,

地址,宏定義在編譯時是展看稱特定的語句,而函數是變異成稱一段獨立的代碼,我們可以獲取一個函數的地址,區無法獲取宏定義的地址(宏定義沒有地址),如果我們需要獲取函數的地址作為參數傳遞給另外的函數,這是我們就只能使用函數而不能使宏定義。

大小,效率:由於宏定義編譯時展看,會在使用的地方展看,這樣會比使用函數生成的目標代碼大,但另一方面,由於去掉了函數調用所帶來的開銷,和編譯器的優化可以產生更為高效的代碼。

宏定義和語言結構之間的細微差別之處還有很多,更多精彩內容請關注我的個人微信公眾號:操作系統探究。


enum ValueTy {

#define HANDLE_VALUE(Name) Name##Val,

#include "llvm/IR/Value.def"

// Markers:

#define HANDLE_CONSTANT_MARKER(Marker, Constant) Marker = Constant##Val,

#include "llvm/IR/Value.def"

};
有空可以去看下llvm的代碼這些是如何做到的。


推薦閱讀:

用 Linux 真的能學到很多平台無關的東西嗎?
國內人寫代碼的水平跟美國的差距在哪?
為什麼說讀代碼比寫代碼難?
是什麼阻礙了代碼的重用?問題是否應該只解決一次即可?

TAG:編程 | C(編程語言) |