这题还是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之间的限制的原因。
启示
存在即合理
C语言中有大量对于底层的奇怪操作,尽管看起来奇怪,但是其在特定场合依然有很大作用
多看源码可以学到很多
##
宏运算符的例子就是在Arduino的源码中发现的,由此可见阅读高质量源码的意义之大阅读大牛的博客可以收获很多
##
宏运算符的例子就是在翁恺老师的博客中发现的……
MSC的语法题差不多说完了,本来计划把其他算法题也谈谈的,但是我最终放弃了,原因有二:
- 寒假太忙了……
- 在接触了许多其他题目后,我意识到决赛时碰到的题目,只是万千算法题中微不足道的几题。算法是一个极其庞大的课题,需要慢慢修炼,因此计划转为系统地整理一套算法笔记。
之后我遇到有意思的题目时,依然会分享在博客上的,敬请关注。