【C/C++】宏(macro)定义与使用

文章结构如下:

  • 宏的定义
  • 宏的替换与代码展开
  • 宏的替换产生的问题
  • 获取宏参数名称
  • 宏参数的结合
  • 宏的取消
  • 宏定义的换行连接
  • 标准预处理宏

宏(macro)是基于#define所实现的另一种预处理功能。
与基本的#define定义的是常量相比,宏(macro)允许多个参数化替换,参数中可以是固定的字符串,也被一些变量所替代。这个替换的操作将在预编译的时候完成。
宏(macro)的作用是用比较简单的方式表示复杂的函数调用,以提高程序的可读性。

宏的定义

宏的定义遵循以下格式:

#define macro_name(list_of_var) substitution_string

macro_name()括号中支持0个到多个参数,参数间用,分隔开;定义的最后一部分是宏的表达式,在实际的预处理中会将该表达式替换到代码正文中去,这里要特别注意的是替换这个词(坑)。
下面是一个宏的定义示例,用于求出两个数的乘积:

#define multi(x, y) x * y;

接收参数的宏看起来就像是函数。

宏的替换与代码展开

编译器在预编译时会把用到宏的代码替换成宏定义的表达式。
如上面定义的宏multi,可以如下代码中使用:
代码文件: macro.c

int multi = multi(2, 3);

在预编译时,替换操作的查看可以通过编译器命令查看效果,命令如下:

gcc -E macro.c > result.txt
// 或者 使用 -P 参数可以过滤到一些编译器链接标记符号信息
gcc -E -P macro.c > result.txt

打开result.txt则会看到宏的替换后代码展开的效果为:

int multi = 2 * 3;;   // 确实是两个";"  纯替换效果

最后根据表达式的运算,multi的值为6。

宏的 替换 产生的问题

如上所示,宏 替换 操作是纯表达式的原文替换,连末尾的“;”都不会落下。那么,这在某此情况下会产生一些不符合预期的结果。
继续以宏multi为例,如果改一下参数如下:

int multi = multi(2, 3 + 2);

以上的代码我们希望得到的结果是10,但代码输出却是8。我们使用预编译命令将预处理过宏的代码展开查看如下:

int multi = 2 * 3 + 2;;

可见,由于预处理时,对宏的操作只是替换,经替换后的代码由于运算符的优先级问题,结果可能与预期的是不一致的。在这个例子中,会先运行乘法得到6后再加上2结果为8。以上的这种问题还会发生在”++”、”- -“运算上。
所以在有参数的宏中,为了使用运算与预期的一致,需要对参数增加左右括号以保证运算结果,并且取消在宏定义处”;”的结尾。定义写法如下:

#define multi(x, y) (x)*(y)

获取宏参数名称

在宏的表达式中,配合”#”的使用可以获取宏参数的名称。
如下定义了一个打印字符串的例子:

#define PrintStr(var) printf("var=%s", var)

在上面的表达式中,只能固定的打印出参数名称var=,但在项目中参数var的名称和其值一样是不固定的,有时候需要能够知道该var的命名,则可以这么做:

#define PrintStr(var) printf(#var"=%s" , var)

则可以获取当前的字符串名称的命名。运用如下:

#define PrintStr(var) printf(#var"=%s" , var)
struct Student{
    char name[20];
    int age;
};
int main (int argc, char ** argv) {
    struct Student mine = {"jack.ma", 20};
    PrintStr(mine.name);
    printf("\n\n");
    PrintStr("break.line\n");
    return 0;
}

则打印输出如下:


PrintStr

再使用gcc命令把宏展开后的代码贴出来看一下:

struct Student{
    char name[20];
    int age;
};
int main (int argc, char ** argv) {
    struct Student mine = {"jack.ma", 20};
    printf("mine.name""=%s" , mine.name);
    printf("\n\n");
    printf("\"break.line\\n\"""=%s" , "break.line\n");
    return 0;
}

可以看出,在宏的表达式中#var的结果代码在预编译的时候会把参数的原文一字不动的替换到代码中。这样来获取参数的命名。

宏参数的结合

宏的表达式中还支持对宏参数进行拼接,定义如下:

#define macro_name(var1, var2, var3) var1##var2##var3

在上面的例子中,对宏的三个参数进行了拼接,拼接规则是在宏的表达式中,参数与参数之间用##连接起来,中间不能有空格。这个功能可以用于合成变量名称,或是从两个或多个宏参数中生成一个格式控制字符串。
用法示例:

#define PrintStr(var) printf(#var"=%s", var)
#define join(vA, vB, vC) vA##vB##vC
int main (int argc, char ** argv) {
    char * name_car_1 = "Fit";
    
    PrintStr(join(name, _car_, 1));
    printf("\n\n");
    return 0;
}

使用gcc命令对宏进行代码展开后的预处理代码为:

int main (int argc, char ** argv) {
    char * name_car_1 = "Fit";
    printf("join(name, _car_, 1)""=%s", name_car_1);
    printf("\n\n");
    return 0;
}

所以如上代码运行结果为:

join(name, _car_, 1)=Fit

宏的参数结合功能可以通过替换功能组合成一个新的参数或变量名称,但值得注意的是这些操作都是在编译的预处理过程中实现的,即代码运行之前就已经决定了新的参数或变量的最终形态,无法在代码的真实运行过程中再加工。

宏的取消

定义了一个宏后,要对宏进行取消,需要使用指令:

#undef macro_name

通常#undef会和#ifdef或#ifndef等指令配合使用。

宏定义的换行连接

有时候宏的定义可能会很长,为了方便阅读,需要把宏的表达式做换行处理,仍以上面定义的宏multi为例可以这么操作:

#define multi(x, y) \
        x * y

使用\进行换行连接,这样宏定义指令会在下一行的第一个非空白字符处继续。需要注意的是\必须是当前行的最后一个字符,其后必须是回车。

标准预处理宏

C/C++的编译器也支持了大量的标准预处理宏。这里只说下常用的几个,更多的详情资料请点击3.7. Predefined Macros

需要注意的是不同的编译器相同功能的宏名称可能命名不一样。

-   __FILE__
    把当前代码源文件的绝对路径表示为一个字符串常量。

-   __FUNCTION__
    也被命名为__func__,用于该宏所在函数体的函数名称。可以方便调试或异常判断。

-   __LINE__
    得到当前宏所在的代码的行数,是一个int型。一般和__FUNCTIN__中配合来标识源代码中的什么地方发生了某个事件。

-   __DATE__
    生成日期的字符串表达式,格式为Mmm dd yyyy,其中Mmm是月份如”Jan”、”Feb”等等。

-   __TIME__
    提供了包含时间值的字符串,格式是hh:mm:ss。再次强制宏只是在预处理的时间才进行的代码替换,所以这里的时间指的是编译器的运行时间点,而非程序运行时间。一般用这个宏来记录编译器的编译时间点。

代码示例:

int main (int argc, char ** argv) {
    printf("current file name=  %s\n", __FILE__);
    printf("current function name=  %s\n", __FUNCTION__);
    printf("current line number=      %d\n", __LINE__);
    printf("current date=    %s\n", __DATE__);
    printf("current time=       %s\n", __TIME__);
    return 0;
}

宏展开后的代码为:

int main (int argc, char ** argv) {
    printf("current file name=  %s\n", "macro.c");
    printf("current function name=  %s\n", __FUNCTION__);
    printf("current line number=      %d\n", 15);
    printf("current date=    %s\n", "Mar 15 2015");
    printf("current time=       %s\n", "12:31:35");
    return 0;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容