宏的妙用

[TOC]

变长数组

​ 严格说来,变长数组的实现在c++中并不是一件麻烦的事情。Stl中的vector本身就是一个变长数组,并且有自动管理内存的能力。但是在c中,实现变长数组就稍显麻烦。用C实现,必然需要一个结构,结构当中应当有一个指针,指针分配一段内存空间,空间大小根据需要而定,而且必须有另外一个字段记录究竟开辟了多大多长的空间。

大致描述如下:

struct MutableLenArray
{
  Int count;
  Char *p;
}
P = new Char[Count];

没什么问题,但是C语言的使用者有个最大的自豪就在于对于效率、空间使用的掌控。他们会有这样的疑问,如果count=0,那么p就没必要了,白白占了4(64位系统为8)个字节的空间,简直浪费。 那有没有更好的方式能实现上面的需求,又保证空间合理呢?答案是有的,用0长度。如下:

Struct MutableLenArray 
{ 
    Int count; 
    Char p[0]; 
};

和上面的结构使用方法一致,但是我们可以用sizeof尝试读取其大小,发现竟然只有count字段的长度4字节,p没有被分配空间。完美!

宏的妙用

1. do{...}while(0)

在很多的C程序中,你可能会看到许多看起来不是那么直接的较特殊的宏定义。下面就是一个例子:

#define __set_task_state(tsk,state_value)    \
    do{(tsk)->state = (state_value); }while(0)

在Linux内核和其它一些著名的C库中有许多使用do{...}while(0)的宏定义。这种宏的用途是什么?有什么好处?Google的Robert Love(先前从事Linux内核开发)给我们解答如下:

do{...}while(0)是C中唯一的构造程序,让你定义的宏总是以相同的方式工作,不管怎样使用宏(尤其是没有使用大括号包围调用宏的语句),宏后面的分号也是相同的效果。

这句话听起来可能有些拗口,其实用一句话概括就是:使用do{...}while(0)构造后的宏定义不会受到大括号、分号等的影响,总是会按你期望的方式调用运行。同时因为绝大多数的编译器能够识别do{...}while(0) 这种无用的循环并进行优化,所以使用这个方法并不会导致程序的性能降低。

例如:

#define foo(x) bar(x); baz(x)

然后你可能这样调用:

foo(wolf);

这将被宏扩展为:

bar(wolf); baz(wolf);

这的确是我们期望的正确输出。下面看看如果我们这样调用:

if (!feral)
    foo(wolf);

那么扩展后可能就不是你所期望的结果。上面语句将扩展为:

if (!feral)
    bar(wolf);
baz(wolf);

显而易见,这是错误的,也是大家经常易犯的错误之一。

使用do{...}while(0)消除goto语句

​ 通常,如果在一个函数中开始要分配一些资源,然后在中途执行过程中如果遇到错误则退出函数,当然,退出前先释放资源,我们的代码可能是这样:

bool Execute()
{
   // 分配资源
   int *p = new int;
   bool bOk(true);

   // 执行并进行错误处理
   bOk = func1();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }

   bOk = func2();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }

   bOk = func3();
   if(!bOk) 
   {
      delete p;   
      p = NULL;
      return false;
   }
   // ..........
   // 执行成功,释放资源并返回
    delete p;   
    p = NULL;
    return true;
}

这里一个最大的问题就是代码的冗余,而且我每增加一个操作,就需要做相应的错误处理,非常不灵活。于是我们想到了goto:

bool Execute()
{
   // 分配资源
   int *p = new int;
   bool bOk(true);

   // 执行并进行错误处理
   bOk = func1();
   if(!bOk) goto errorhandle;

   bOk = func2();
   if(!bOk) goto errorhandle;

   bOk = func3();
   if(!bOk) goto errorhandle;
   // ..........
   // 执行成功,释放资源并返回
    delete p;   
    p = NULL;
    return true;

errorhandle:
    delete p;   
    p = NULL;
    return false;
}

代码冗余是消除了,但是我们引入了C++中身份比较微妙的goto语句,虽然正确的使用goto可以大大提高程序的灵活性与简洁性,但太灵活的东西往往是很危险的,它会让我们的程序捉摸不定,那么怎么才能避免使用goto语句,又能消除代码冗余呢,请看do...while(0)循环:

bool Execute()
{
   // 分配资源
   int *p = new int;
   bool bOk(true);
   do
   {
      // 执行并进行错误处理
      bOk = func1();
      if(!bOk) break;

      bOk = func2();
      if(!bOk) break;

      bOk = func3();
      if(!bOk) break;
      // ..........
   }while(0);

    // 释放资源
    delete p;   
    p = NULL;
    return bOk;
   
}

2. “#”符号

"#"符号用来将一个符号直接转换为为字符串,例如

#define TO_STRING(x) #x
const char * str=TO_STRING(test);

str指向的内容就是"test",也就是说宏定义中#会把其后的符号直接加上双引号。这个特性为C++的反射的实现提供了极大的方便。

3. "##"符号

用来连接两个符号,从而产出新的词法(词法层次),例如:

#define SIGN(x) INT_##x
int SIGN(1);

宏被展开后将会成为:int INT_1;
可以把##看成是连字符,连字符为新符号的产生提供了方便。google的Gtest框架就巧妙地运用了连字符来生成新的测试案例。

4. 变参宏

#define LOG(format,...) printf(format,__VA_ARGS__)
LOG("%s %d",str,count);
#define LogE(format, ...) usb_logWrite('e', __FILE__, __LINE__, format, ##__VA_ARGS__)
//此处使用__VA_ARGS__或者是 ##__VA_ARGS__均可
#define LogD(format, ...) usb_logWrite('d', __FILE__, __LINE__, format, ##__VA_ARGS__)

#define LogI(format, ...) usb_logWrite('i', __FILE__, __LINE__, format, ##__VA_ARGS__)

#define LogW(format, ...) usb_logWrite('w', __FILE__, __LINE__, format, ##__VA_ARGS__)

#define LogO(format, ...) usb_logWrite('o', __FILE__, __LINE__, format, ##__VA_ARGS__)

extern int usb_logWrite(char level,char *fileName, int lineNum, const char * format, ...);
int usb_logWrite(char level,char *fileName, int lineNum, const char * format, ...)
{
    int n, m;
    char logWriteBuf[BUF_SIZE];

    _init_fd();

    if(fileName != NULL) {
        char *rf = strrchr(fileName, '/');
        if(rf != NULL) fileName = rf+1;
    }
    n = snprintf(logWriteBuf,BUF_SIZE,"%%%%<L=%c;PN=%d;P=%s;F=%s;N=%d;>",\
                                    level,Id,name,fileName,lineNum);

    va_list argList;
    va_start(argList,format);
    m = vsnprintf(logWriteBuf+n,BUF_SIZE-n,format,argList);
    va_end(argList);

    if(m <= 0) {
        m = snprintf(logWriteBuf+n,BUF_SIZE-n, "format error\n");
    }
    n += m;
    if(n > BUF_SIZE-2) n = BUF_SIZE-2;
    logWriteBuf[n++] = '#';
    logWriteBuf[n++] = '#';

    write(log_fd, logWriteBuf, n);
    return n;
}

_VA_ARGS_是系统预定义的宏,被自动的替换为参数列表,经常需要用到格式化输出,重定义时,可以使用以上技巧。

C中一些特殊的宏还有__FUNCTION__、__DATE__、__FILE__、__LINE__、__STDC__、__TIME__、__TIMESTAMP__

5. 宏参数的prescan

​ prescan的定义为:当一个宏参数被放进宏体当中时,这个宏参数会首先被全部展开,当展开以后的宏参数放入宏体时,预处理器对新展开的宏体进行二次扫描,并继续展开。

#define PARAM(x) x
#define ADDPARAM(x) INT_##x
PARAM(ADDPARAM(1))

因为ADDPARAM(1)是作为PARAM的宏参数,所以先将ADDPARAGM(1)展开为INT_1,然后再将INT_1放进PARAM,例外的情况是如果PARAM宏里面对宏参数使用了"#"或者是"##",那么宏参数不会被展开。

#define PARAM( x ) #x 
#define ADDPARAM( x ) INT_##x

PARAM( ADDPARAM( 1 ) ); 将被展开为ADDPARAM( 1 )。 所以此时要得到"INT_1"的结果,必须要加入一个中间宏:

#define PARAM(x) PARAM1(x)
#define PARAM1( x ) #x

PARAM( ADDPARAM( 1 ) );此时的结果将会是“INT_1”。根据prescan原则,当ADDPARAM(1)传入,会展开得到INT_1,然后将INT_1带入PARAM1宏,最终得到“INT_1”的结果。

6. C++中的接口宏

C++的目标之一就是把类的声明和定义分离开来,这对于项目的开发极其有利——这可以使开发人员不用看到类的实现就能知晓类的功能。但是,C++实现类的声明与类定义的分离的方法会导致一些额外的工作——每个非内联函数的表示都需要写两次,一次在类声明中,一次在类定义中。 代码如下:

// .h File 
class Element 
{ 
void Tick (); 
};
 
// .cpp File 
void Element ::Tick () 
{ 
// todo 
}

由于Tick的标识在两个地方都出现了,因此如果我们需要改变这个方法的参数的时候(改变函数名、返回类型或者加const),我们需要改变两个地方。
当然通常这没有什么工作量,但是有些情况下这个特性会带来不少麻烦。
举个例子,如果我们有一个叫做BaseClass的基类,有三个从BaseClass继承而来的子类——D1、D2和D3.其中BaseClass声明了一个虚函数Foo()并且有一个缺省实现,并且D1、D2、D3中重载了Foo()函数。
现在,如果说我们给BaseClass::Foo()添加一个参数,但是忘了给D3中做相应的修改。麻烦来了——编译可以通过,编译器会把BaseClass::Foo(…)和D3::Foo()当成两个完全不同的函数。当我们想通过虚函数机制来调用D3的Foo的时候,这就容易出一些问题。
UE4中光继承自AActor类的类就有上千个,如果需要对AActor类做一个修改,那么如果使用传统方法,我们还要针对上千个派生类进行修改,而且万一有一个派生类没有修改,编译器也不会报错!
这么看来,理想的情况是我们希望一个函数的表示只在一个地方存在,如果说只声明BaseClass::Foo()一次,然后再它的派生类中不用再额外声明Foo就好了。
而且在效率方面来说,在C++中使用继承的时候我们经常会使用很多浅层次的类继承关系,一个父类往往有一堆子类。很多时候我们只需要把很多互不相关的功能集成到一个单独的类继承家族里面。
对于浅继承来说,我们只是把开始的父类声明为一个接口——也就是说它声明了一些虚函数(大部分是纯虚函数)。在大多数情况下,我们会在这个类家族里面有一个基类以及其余的派生类。
如果说我们的基类有10个函数,我们从这个基类派生了20个类,那么我们就需要额外做200个函数声明。但是这些声明的目的往往只是为了Implement基类中的那些方法而已,这就或多或少的容易使得头文件不好维护。
传统方法的实现
如果说我们有一个Animal的类,这个类被视为基类,我们希望从这个基类派生出不同的子类。在Animal中有3个纯需函数,如下所示:

class Animal 
{ 
public: 
virtual std :: string GetName () const = 0 ; 
virtual Vector3f GetPosition () const = 0; 
virtual Vector3f GetVelocity () const = 0; 
};

同时,这个基类拥有三个派生类——Monkey,Tiger,Lion。
那么我们三个方法的每一个都会在7个地方存在:Animal中一次,Monkey、Lion、Tiget的声明和定义各一次。
然后假设我们做一个小改动——我们想将GetPosition和GetVelocity的返回类型改为Vector4f以适应Transform变换,那么我们就要在7个地方进行修改:Animal的.h文件,Lion、Tiger和Monkey的.h文件和.cpp文件。
使用宏的实现
有一种很妙的处理方法就是将这些方法进行包装,改成所谓接口宏的形式。我们可以试试看:

#define INTERFACE_ANIMAL(terminal)             \
public:                           \
  virtual std::string GetName() const ##terminal     \
  virtual IntVector GetPosition() const ##terminal    \
  virtual IntVector GetVelocity() const ##terminal    
 
#define BASE_ANIMAL   INTERFACE_ANIMAL(=0;)
#define DERIVED_ANIMAL INTERFACE_ANIMAL(;)

值得一提的是,##符号代表的是连接,\符号代表的是把下一行的连起来。
通过这些宏,我们就可以大大简化Animal的声明,还有所有从它派生的类的声明了:

// Animal.h
class Animal
{
  BASE_ANIMAL ;
};
 
 
 
// Monkey.h
class Monkey : public Animal
{
  DERIVED_ANIMAL ;
};
 
 
// Lion.h
class Lion : public Animal
{
  DERIVED_ANIMAL ;
};
 
 
// Tiger.h
class Tiger : public Animal
{
  DERIVED_ANIMAL ;
};
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,332评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,508评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,812评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,607评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,728评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,919评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,071评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,802评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,256评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,576评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,712评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,389评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,032评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,026评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,473评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,606评论 2 350

推荐阅读更多精彩内容