新闻  |   论坛  |   博客  |   在线研讨会
宏的妙用
patton | 2008-06-19 13:35:14    阅读:2756   发布文章

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);
上述代码看起来是完美的,但是有一些特殊还是不正确,这些特殊情况就是=用于赋值不正确的情况。

有两种常见问题:
  1. leftvar和rightvar分别是指针类型,但是其实想拷贝它们指向的缓冲区的内容(如字符串拷贝)。
  2. 为了控制显示精度,对话框控制变量是一个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

到底要实现哪些类型对,需要读者根据自己项目需要设计。

小结
宏的功能应该还有许多,但是我才疏学浅,只能想到这么一点,希望能对大家有所帮助。

*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。

参与讨论
登录后参与讨论
qiqiaixiah  2008-07-17 16:44:51 

原来宏还有这么多妙用哇

laochen  2008-06-20 00:40:57 

很好的东西

Avile  2008-06-19 18:19:13 

恩,很有用哦!

推荐文章
最近访客