1、概述
C++中出了const關鍵字以後,宏定義常量的功能已經不在被推薦使用。這使 得宏似乎沒有了用武之地。實際上,宏還可以做很多事情,筆者也難以全部列舉 。這裡,僅僅列舉幾個典型的用法,希望大家能夠從中獲益。
2、實現多環境兼容
常見的情況是,我們實現了一個函數,希望它只在某種編譯條件滿足是被編譯和使用。例如,我希望在源碼中插入調試語句,以便以Debug方式運行時能夠 通過調試信息觀察程序運行情況。但是,在產品發售給用戶時,我又希望這些調 試信息不要輸出,以降低代碼尺寸,提高運行性能。 這一問題的解決方法就是 使用宏。根據條件編譯指令,對於不同的編譯條件,提供不同的實現。例如:我們希望在特定的位置向日志中寫入當前行號和文件名,以判斷對應代碼是否被執 行到,可以使用下面的宏:
#ifdef _DEBUG
#define TRACE_FILE_LINE_INFO() do{\
CString str;\
str.Format(_T("file=%s,line=%u\r\n",__FILE__,__LINE__);\
CFile file("logfile.txt");\
file.Write(str,str.GetLength());\
}while(0)
#else
#define TRACE_FILE_LINE_INFO()
#endif
上面這段代碼通過#ifdef #else #endif三個條件編譯 指令,根據_DEBUG定義情況(該宏用於區分DEBUG版本和Release版本),決定了 具體的TRACE_FILE_LINE_INFO宏函數的實現。使用者可以用如下方法使用
TRACE_FILE_LINE_INFO();//這裡顯示行號和文本信息
當然 ,采用其他方式也可以實現這一功能,但是使用宏有以下特殊好處: 只有需要 的代碼才會被編譯,減少了符號表的尺寸,也減少了代碼尺寸 宏在編譯時被展 開,因此用於表示代碼位置的__FILE__,__LINE__宏可以起作用,如果用函數實 現,這兩個宏則不能起作用。
3、用新函數替換原有函數
對於一個設計好的函數,假設它已經在一個很大的工程中到處使用,突然發 現它的一個不足,想修改它的功能。也許這個新增加的功能需要一個額外的參數 ,但是又不想修改使用這些函數的地方。 假設有兩個函數必須成對使用,一個 占用資源並使用,另外一個則釋放資源以供其他模塊使用。典型的例子是,函數 一(假設為Lock)獲得一個全局的鎖,這個鎖用於保護在多線程情況下多個線程 對一個公共資源如一個全局變量的訪問。問題是,這個Lock函數獲得鎖以後,其 他線程將不能再獲得這個鎖,直到當前線程釋放這個鎖。編制Lock函數的程序員 同時提供了一個 Unlock函數用於釋放鎖,並要求使用Lock的人必須對應的使用 Unlock。調試程序時,發現線程被死鎖,懷疑有人使用完Lock後忘記調用 Unlock,但是Lock和Unlock在這個大工程中都被廣泛的使用,因此設計者希望 Lock和Unlock都增加兩個額外的參數file和line,以說明這兩個函數在哪裡被調 用了,哪些地方被死鎖以及哪些地方調用了Lock但是沒有調用Unlock。 假設這 兩個函數的原型為:
void Lock();
void Unlock();
新設計的函數的原型是:
void Lock(LPCTSTR szFileName,UINT uLineNo);
void Unlock(LPCTSTR szFileName,UINT uLineNo);
設計完新的函數後,項目經理希望所有模塊統一使用這兩個函數並提供文件 名和行號信息作為參數。這樣將是一個非常浩大且煩瑣的工作,意味著重復性的 勞動、數小時無聊的加班和工期的延誤,這是誰都不願意遇到的。 使用宏可以 非常輕松的解決這一切。首先,應該把新設計的函數換個名字,不妨叫它們 NewLock和NewUnlock,也就是他們的原型為:
void NewLock(LPCTSTR szFileName,UINT uLineNo);
void NewUnlock(LPCTSTR szFileName,UINT uLineNo);
這個函數原型應該放在一個頭文件中,避免在多個地方重復的聲明。需要用 到這兩個函數的cpp文件,只要包含他們原型所在的頭文件即可。為了不改動使 用Lock/Unlock函數的模塊,在頭文件中增加如下兩行:
#define Lock() NewLock(__FILE__,__LINE__)
#define Unlock() NewUnlock(__FILE,__LINE__)
這樣,當不同模塊使用這個函數時,宏替換功能在編譯時起作用,自動使用 了__FILE__和__LINE__為參數,調用了新設計的函數。調試的時候就可以根據日 志來判斷什麼地方遺漏了調用Unlock。
4、給一個函數捆綁其他功能
上述方法修改了原來函數的設計。實際上,這兩個函數本身沒有問題,只是 使用者使用上出了問題。你可能只需要在調試版本中測試到底誰遺漏了這些重要 信息。對於一些嚴謹的公司,一旦軟件被修改,推出銷售前就需要進行嚴格的測 試。因此項目經理可能不會允許修改原有函數的設計,要求直接捆綁一個測試代 碼。產品發售時,刪除捆綁代碼即可。 使用宏也可以捆綁代碼,這需要首先了 解一個宏的特點:如果你的代碼中出現了一個字符串,編譯器會首先匹配宏,並 試圖用宏展開。這樣,即使你有同名的函數,它也不會被當作函數處理。但是, 如果一個宏展開時發現,展開式是一個嵌套的宏展開,展開式就試圖在進入下一 次嵌套展開之前,試圖用函數匹配來終止這種無限循環。 為此,定義如下兩個 宏:
#define Lock() Lock();\
TRACE("Lock called in file = %s at line =%u\n",__FILE__,__LINE__)
#define Unlock() Unlock();\
TRACE("Unlock called in file = %s at line =%u\n",__FILE__,__LINE__)
編譯器在編譯過程中,發現如下代碼
//here the Lock function is called
Lock();
它首先把這個Lock理解成宏函數,展開成:
//here the Lock function is called
Lock();
TRACE("Lock called in file = %s at line = %u\n",__FILE__,__LINE__);
上述代碼中,__FILE__和__LINE__應該 同時被展開,由於與論題無關,所以還是原樣給出。展開以後,Lock還是一個和 宏匹配的式子,但是編譯器發現如果這樣下去,它將是一個無休止的迭代,因此 它停止展開過程,訊中同名的函數,因此上面的代碼已經是最終展開式。 這樣 ,我們成功的不改變Lock函數的原型和設計,捆綁了一條調試信息上去。由於 TRACE語句在Release版本中不會出現,這樣也避免了不得不進行額外的測試過程 。
5、實現一些自動化過程
程序中需要輸入一組參數,為此設計了一個對話框來輸入。問題是:每次顯 示對話框時,都希望能按照上次輸入的值顯示。設計當然沒有問題,在文檔中保 存輸入的參數,在顯示對話框前在把保存的值賦值給對話框對應控制變量。下面 是常見的代碼:
CMyDoc * pDoc = GetDocument();
ASSERT_VALID(pDoc);
CParameterDlg dlg;
//設置對話框初值
dlg.m_nValue1 = pDoc->m_nValue1;
dlg.m_szValue2 = pDoc->m_szValue2;
......
dlg.m_lValuen = pDoc->m_lValuen;
//顯示對話框
if(dlg.DoModal() == IDOK)
{
//點擊OK按鈕後保存設置
pDoc->m_nValue1 = dlg.m_nValue1;
pDoc->m_szValue2 = dlg.m_szValue2;
......
pDoc->m_lValuen = dlg.m_lValuen;
}
如果整個程序只有一兩個這樣的代碼段,並且每個代碼段涉及 的變量個數都很少,當然沒有問題,但是當你程序中有成百上千個這樣的參數對 話框,每個對話框又對應數十個這樣的參數,工作量就非常可觀了(而且是沒有 任何成就感的工作量)。我想,用VC做界面的朋友們大多遇到過這樣的問題。可 以注意到,上述代碼在DoModal前後都是一組賦值過程,但是賦值的方向不是很 一致,因此每個變量對都需要寫兩個賦值語句。那麼是否可以做一個函數,前後 各調用一次,根據一個參數決定方向。而且函數中也只需要對每個變量寫一次?
下面這個函數就是一個實現:
void DataExchange(CMyDoc * pMyDoc,CParameterDlg * pDlg,BOOL flag )
{
BEGIN_EXCHANGE(pMyDoc,CMyDoc,pDlg,CParameterDlg,flag)
EXCHANGE(m_nValue1);
EXCHANGE(m_szValue2);
....
EXCHANGE(m_lValue2);
END_EXCHANGE()
}
為了使上述語義能起作用,定義上面三個宏如下:
#define BEGIN_EXCHANGE(left,lefttype,right,righttype,flag)\
{\
CSmartPtr<lefttype> pLeft = left;\
CSmartPtr<righttype> pRight = right
#define END_EXCHANGE() }
#define EXCHANGE(varible)\
if(flag)\
{\
pLeft->varible = pRight->varible ;\
}else{\
pRight->varible = pLeft->varible;\
|
這裡為了避免每次都輸入varible所屬對象的指針,使用了 一個智能指針來提供一個左指針pLeft和一個右指針pRight語義,這個智能指針 只需要實現取下標功能即可,因此可以簡單實現如下(為了通用,必須為模板類) :
template <typename TYPE>
class CSmartPointer
{
protected:
TYPE * m_pPointer;
public:
CSmartPointer(TYPE * pPointer):m_pPointer(pPointer){};
TYPE* operator->() {return m_pPointer;}
};
這樣,原來的代碼就可以修改成這樣:
CMyDoc * pDoc = GetDocument();
ASSERT_VALID(pDoc);
CParameterDlg dlg;
//設置對話框初值
DataExchange(pDoc,&dlg,FALSE);
//顯示對話框
if(dlg.DoModal() == IDOK)
{
//點擊OK按鈕後保存設置
DataExchange(pDoc,&dlg,TRUE);
}
上述代碼要求左右指針對應變量名必須相同,如果變量名不同 ,就不能這樣使用,需要設計成這樣的EXCHANGE2宏:
#define EXCHANGE2(leftvar,rightvar)\
if(flag)\
{\
pLeft->leftvar,pRight->rightvar;\
}else{\
pRight->rightvar = pLeft->leftvar;\
}
這樣,對應的EXCHANGE子句需要修改成
EXCHANGE2(m_lValue1,m_dwValue2);
上述代碼看起來是完 美的,但是有一些特殊還是不正確,這些特殊情況就是=用於賦值不正確的情況 。
有兩種常見問題:
leftvar和rightvar分別是指針類型,但是其實想拷貝它們指向的緩沖區的內 容(如字符串拷貝)。
為了控制顯示精度,對話框控制變量是一個CString對象,它是文檔對象中對 應變量的格式化後的信息。最常見的是, leftvar是一個浮點數,需要以幾個小 數位格式輸出,因此rightvar是一個CString對象。
為了實現上面的目的,就不能使用=來直接賦值,而應該用一個函數Assign( 函數名當然可以任意取啦)來做這件事。為此,修改上述的EXCHANGE和EXCHANGE2 宏如下:
#define EXCHANGE(var)\
if(flag)\
{\
Assign(pLeft->var,pRight->var);\
}else{\
Assign(pRight->var,pLeft->var);\
}
#define EXCHANGE2(leftvar,rightvar)\
if(flag)\
{\
Assign(pLeft->leftvar,pRight->rightvar);\
}else{\
Assign(pRight->rightvar,pLeft->leftvar);\
}
這樣只要針對每個類型對實現一次Assign即可。由於C++允 許重載,這顯得很容易。需要實現的函數一般有:
函數 功能 void Assign(CString & left,CString & right) 直接賦值CString類型 void Assign(CString & left, float & fValue) 格式化float數值到left void Assign(float & fValue,CString & right) 從字符串中讀取出float void Assign(CString & left, double& dValue) 格式化double數值到left void Assign(double& dValue,CString & right) 從字符串中讀取出double void Assign(CString & left, int & iValue) 格式化int數值到left void Assign(int & iValue,CString & right) 從字符串中讀取出int void Assign(CString & left, short& sValue) 格式化short數值到left void Assign(short & sValue,CString & right) 從字符串中讀取出short void Assign(CString & left, long & lValue) 格式化long數值到left void Assign(long & lValue,CString & right) 從字符串中讀取出long void Assign(CString & left, CTime & time) 格式化CTime數值到left void Assign(CTime & time,CString & right) 從字符串中讀取出CTime
到底要實現哪些類型對,需要讀者根據自己項目需要設計。
小結
宏的功能應該還有許多,但是我才疏學淺,只能想到這麼一點,希望能對大 家有所幫助。