这题还是MSC决赛的一题,原题题干已经交代得很清楚了(我懒……),直接搬过来好了

宏&宏函数简介

宏是 C 语言中极为强大,也极具魅力的一个功能。宏为极其贴近机器语言的 C 提供了一定程度上的元编程能力。程序员们能够利用宏来减少代码中的重复,或是解决一些条件编译之类的问题。

宏、或者说 C 预处理器,实际上干的事情就是进行字符串拼接和处理,其中比较简单的就是普通的字符串替换:

#define abc efg
#define one 1
#define INT int
#define DENGYU
INT abc DENGYU one; // -> int efg = 1;

为了防止替换的过程无法结束,通过这种方式定义的宏在它的(和它的递归)展开内容中是不会再对其本身进行替换

#define abc abc efg
#define efg abc hij
#define hij efg abc
	abc // (所有东西都可以被展开)
->  abc efg // (abc 不会再被展开)
->  abc abc hij // (abc efg 不会再被展开)
->  abc abc efg abc

另一种则是宏函数,规则稍微复杂一些。宏函数首先会对它的参数进行宏展开,如果参数中存在 # 或者 ## (具体作用后面会提及), 那么不再对这个参数进行递归展开,然后再按照宏的定义将参数进行拼接,得到这一次展开的结果;之后对结果进行下一次展开。这次展开和之前说的展开规则类似,不再会对生成的结果中相同的宏函数进行展开。

#define foo(a, b) a b
foo(1+, 2) // -> 1 + 2
#define foo2(a, b) foo(a) foo(b)
foo2(1+, 2) // -> foo(1+) foo(2) -> 1+ 1+ 2 2

C 预处理器提供了一些方法来生成特殊的字符串,例如 ###.

## 能将它两边的内容直接连接起来,去除中间的空格,可以用来拼接新的宏或者标识符等

#define CONCAT(X, Y) X ## Y
int a = CONCAT(x, 1); // -> int a = x1;
#define ADD_PREFIX_AND_SUFFIX(X) pre ## X ## suf
int b = ADD_PREFIX_AND_SUFFIX(X); // int a = preXsuf;

#define IF(COND, PASS, FAIL) IF_ ## COND (PASS, FAIL)
#define IF_0(PASS, FAIL) FAIL
#define IF_1(PASS, FAIL) PASS

IF(0, A, B) // -> B
IF(1, A, B) // -> A

# 能作用于它右边的宏参数,把它转化为一个 C 语言字符串字面量。而 C 语言也提供了“自动拼接相邻两个字符串字面量”的语法糖,使得我们可以在 编译期完成拼接字符串字面量的操作

#define TO_C_STR(X) #X

#define ADD_C_STR_SUFFIX(X, Y) X TO_C_STR(Y)

const char* c_str = ADD_C_STR_SUFFIX("abc", efg); // -> const char* c_str = "abc" "efg"; === const char* c_str = "abcefg";

宏也提供了可变参数的宏函数。简单的说,配合 __VA_ARGS__ 它可以将传入的参数按原样替换到内容里。 相比与 C 语言的可变参数函数,宏函数不需要"至少有一个固定参数"。 在宏函数的替换内容中,可以使用 __VA_ARGS__ 来代表它获得的可变参数. 可以使用 , ##__VA_ARGS__来实现对 0 个可变参数的适配。可变参数 ... 只能在参数列表中出现一次。 下面是一些例子:

#define PRINT(...) printf(__VA_ARGS__)
#define PRINTF(FMT, ...) printf(FMT, ##__VA_ARGS__)

PRINT("123"); // -> printf("123");
PRINT("%d", 1); // -> printf("%d", 1);
PRINTF("123"); // -> printf("123");
PRINTF("%d", 1); // -> printf("%d", 1);

宏函数有什么用?

偶读翁恺老师博客上的文章,看到这样一篇

在讲到编译预处理指令的宏运算符##时,同学们都不太理解这东西有什么用。今天恰好在Arduino的Ethernet库的头文件里看到了活的例子。在Arduino的Ethernet库的w5100.cpp里有这样的函数调用:

writeTMSR(0x55);

但是遍寻整个.cpp和对应的w5100.h也找不到这个writeTMSR()函数,即使把所有的源代码目录拿来搜索一遍都没有。但是,编译显然是通过了的,那么,这个函数在哪里呢?

w5100.h,我们发现了这样的代码:

#define __GP_REGISTER8(name, address)             \
  static inline void write##name(uint8_t _data) { \
    write(address, _data);                        \
  }                                               \
  static inline uint8_t read##name() {            \
    return read(address);                         \
  }

这个宏定义是说,如果你提供一个name和一个address,那么宏__GP_REGISTER8就会被展开为两个函数,一个的名字是write后面跟上name,另一个是read后面跟上name。

于是,在w5100.h里接下去的代码:

__GP_REGISTER8 (TMSR,   0x001B);    // Transmit memory size

在编译预处理后,就会被展开成为:

  static inline void writeTMSR(uint8_t _data) { 
    write(0x001B, _data);                        
  }                                               
  static inline uint8_t readTMSR() {            
    return read(0x001B);                         
  }

看,这就是活生生的宏运算符的例子。

宏函数的技巧性使用

题目-MAX()

题目描述

实现一个可以接受可变参数的宏函数 MAX, 它返回其中的最大值。

保证传入的参数为合法的 C 表达式,且类型为 int,个数在 1 ~ 5 之间,中间用逗号分隔

保证传入的表达式无副作用

函数接口定义

虽然并不是函数

#define MAX(...)

裁判测试程序样例

#include <stdio.h>

// your code here

int data[10];

int main() {
  int n;
  scanf("%d", &n);
  int x, y, z, w, u;
  switch(n) {
    case 1: scanf("%d", &x); printf("%d\n", MAX(x)); break;
    case 2: scanf("%d %d", &x, &y); printf("%d\n", MAX(x, y)); break;
    case 3: scanf("%d %d %d", &x, &y, &z); printf("%d\n", MAX(x, y, z)); break;
    case 4: scanf("%d %d %d %d", &x, &y, &z, &w); printf("%d\n", MAX(x, y, z, w)); break;
    case 5: scanf("%d %d %d %d %d", &x, &y, &z, &w, &u); printf("%d\n", MAX(x, y, z, w, u)); break;
    default: break;
  }
  return 0;
}

样例仅供参考

样例1

输入

4 1 3 2 1

输出

3

样例2

输入

1 0

输出

0

提示

  • 具体的规则最好自己实验一下, 使用 gcc -E test.c 可以打印出 test.c 经过预处理后的代码
  • 宏函数的参数使用时在不影响语义的情况下尽量在两边加上 () ,一个常见的错误例子就是:
#define MUL(A, B) A * B
MUL(1 + 2, 3 + 4) // -> 1 + 2 * 3 + 4 
  • 在预处理器进行处理的过程中, 可以产生一些临时参数,但它们很快就在下一次替换过程消失,而不会导致生成非法的表达式。
  • 你可能需要一个形式类似于 #define FOO(_USELESS_PARAMETER, ...) BLAHBLAH 的宏

思路&题解

我曾经想过将宏函数里的参数全部传到另一个变参函数中(上一篇文章讲的就是变参函数),但是MAX()的实现需要知道参数的个数,而这一点又是变参函数无法实现的(原因见上一篇文章),因此此思路失败。

但是请注意,MAX()的实现需要知道参数的个数,这句话其实提醒了我们,此题的突破口其实就是如何让程序知道参数的个数

看到这里你要是还没有思路的话,其实太正常了——因为这题技巧性很强,解法也很漂亮。

直接放代码好了,看了代码一定会有豁然开朗的感觉。

#define MAX1(A) (A)
#define MAX2(A,B) (A)>(B)?(A):(B)
#define MAX3(A,B,C) MAX2(MAX2(A,B),(C))
#define MAX4(A,B,C,D) MAX2(MAX3(A,B,C),(D))
#define MAX5(A,B,C,D,E) MAX2(MAX4(A,B,C,D),(E))
#define GETMAX(A,B,C,D,E,MAXNAME,...) MAXNAME
#define MAX(...) GETMAX(__VA_ARGS__,MAX5,MAX4,MAX3,MAX2,MAX1)(__VA_ARGS__)

这里其实就是利用参数个数的不同,使固定位置上获取的参数不同,从而实现基于参数个数的宏函数重载

这解法太骚了不过其局限性在于依然只能处理一定数量以内的参数,这也是此题给出个数在1~5之间的限制的原因。

启示

  1. 存在即合理

    C语言中有大量对于底层的奇怪操作,尽管看起来奇怪,但是其在特定场合依然有很大作用

  2. 多看源码可以学到很多

    ##宏运算符的例子就是在Arduino的源码中发现的,由此可见阅读高质量源码的意义之大

  3. 阅读大牛的博客可以收获很多

    ##宏运算符的例子就是在翁恺老师的博客中发现的……

MSC的语法题差不多说完了,本来计划把其他算法题也谈谈的,但是我最终放弃了,原因有二:

  1. 寒假太忙了……
  2. 在接触了许多其他题目后,我意识到决赛时碰到的题目,只是万千算法题中微不足道的几题。算法是一个极其庞大的课题,需要慢慢修炼,因此计划转为系统地整理一套算法笔记。

之后我遇到有意思的题目时,依然会分享在博客上的,敬请关注。