前题
% 只要规则被制定了,就总有人试图凌驾于规则之上。
define
% 宏是一个很庞大的话题,其本质是字符串替换。
无参形式
#define
应该是最常见的宏定义了,它可以自定义一些标识符,来表示一些字符串,来在一定程度上简化代码。
例如#define LL long long
,后面就可以用LL
来代替long long
,这等价于typedef long long LL;
。这就是宏定义的无参形式。
带参形式
下面就是很多人认为的宏函数。
#define max(a,b) a>b?a:b
%
可是上面的代码并非总能得到我们预期的结果,例如!max(a,b)
会被简单地替换成!a>b?a:b
,而由优先级的关系,编译器将理解为是(!a)>b?a:b
。下面的写法避免了这种情况:
#define max(a,b) ((a)>(b)?(a):(b))
% 虽说这通常是正确的,仍然有风险。考虑下面的代码
#define max(a,b) ((a)>(b)?(a):(b))
int a=0,b=1;
printf("%d\n",max(a++,b++));
%
显然,预期输出应该为1,而事实上,这个程序的输出为2,宏将被展开为((a++)>(b++)?(a++):(b++))
。但只要了解了宏函数的原理,便可以自行展开代码判断是否符合我们的逻辑。
事实上,如果支持 GNU C 扩展,那这个问题可以用typeof
宏得到很好的解决,这里不再展开。
#
和##
#
:字符串化一个宏参数,即在参数名字前后加上"
,例如:
#define f(x) #x
const char * a=f(123);
%
展开后得到const char *a="123"
。利用这个特性,我们可以出函数f(x)
,使得调用f(x)
后,输出x=...
:
#define f(x) printf(#x"=%d",x)
%
除了#
,还有##
符号,它拼接宏参数和另一个符号,即连接两个符号生成一个新的符号。例如:
#define f(y) var_##y
int f(d);
%
会被解释成:int var_d;
变参宏
%
有的竞赛不能使用freopen
,而只能使用fopen
和fprintf
,而这样也导致了一定程度上的编程困难,能不能把printf(format,...)
变成fprintf(fp,format,...)
呢?好在系统提供了预定义宏__VA_ARGS__
,因此我们可以这样:#define printf(format,...) fprintf(fp,format,__VA_ARGS__)
唯一的缺点是printf必须至少有一个参数,同理,scanf也可以这么弄:#define scanf(format,...) fscanf(fp,format,__VA_ARGS__)
%
但是必须注意,当__VA_ARGS__
作为宏实参再次被传入另一个宏函数的时候,可能会被解释为一个参数(例如在VC中)
#define ATTR_1(arg) printf(arg);
#define ATTR_2(arg, ...) ATTR_1(arg) ATTR_1(__VA_ARGS__)
#define ATTR_3(arg, ...) ATTR_1(arg) ATTR_2(__VA_ARGS__)
int main(){
ATTR_3("123\n","456\n","789\n");
}
% 也有可能会被解释成:
int main(){
printf("123\n"); printf("456\n","789\n");
}
%
因为后面的"456\n","789\n"
被认为是同一个参数,那有什么解决方案吗?答案是借助辅助宏。
#define ATTR(args) args
#define ATTR_1(arg) printf(arg);
#define ATTR_2(arg, ...) ATTR_1(arg) ATTR(ATTR_1(__VA_ARGS__))
#define ATTR_3(arg, ...) ATTR_1(arg) ATTR(ATTR_2(__VA_ARGS__))
%
这样就可以了。但DEV_C++却并不会有这个问题,因此最好加上。
当然,如果允许使用C++11,更好的方法是使用可变参数模板,这里不再展开。
prescan
%
当一个宏参数被放进宏体时,这个宏参数会首先被全部展开。当展开后的宏参数被放进宏体时, 预处理器对新展开的宏体进行第二次扫描,并继续展开。例如:
#define put(x) printf("%d",x)
#define sign(x) INT_##x
int INT_1;
put(sign(1));
% 会被替换为
int INT_1;
printf("%d",INT_1);
但是有例外——当PARAM宏里对宏参数使用了#或##,那么宏参数不会被展开:
#define PARAM(x) #x
#define ADDPARAM(x) INT_##x
%
PARAM(ADDPARAM(1));
将被展开为"ADDPARAM(1)"
。使用这么一个规则,可以创建一个很有趣的技术:打印出一个宏被展开后的样子,这样可以方便你分析代码:
#define str(x) TO_STRING(x)
#define TO_STRING(x) #x
%
这样便可以用printf("%s",str(宏))
,来输出展开后的样子。
内置宏
系统提供了一些内置宏。
__func__
和__FUNCTION__
:该宏所在的函数名。__LINE__
:该宏所处的行数。__DATE__
:该宏所在函数的当前编译日期。__TIME__
:该宏所在函数的当前编译时间。__FILE__
:该宏所在的程序文件的名字。
纯定义
%
除了无参形式和带参形式,还有很多人都不知道的一个用法——定义标识符。
#define Sign
%
通常结合预处理指令#if
使用。
include
%
几乎每条程序都不可避免地使用到#include
,它的本质是文件展开。
#include<file>
%
这就是最常见的include
命令。它表示引用编译器的类库路径里面的头文件,例如#include<cstdio>
。
#include"file"
%
使用VC的同学可能比较习惯于使用它,与#include<file>
的区别在于,它引用的是程序目录的相对路径中的头文件。
%
比如,我想从网上下载了一个名为frac.h
的头文件,就可以用include"frac.h"
代表用在(处理后的)程序同一目录下的fun.h
的文件内容替换代码的该处的include
。但要注意,如果#include"cstdio"
没有找到cstdio
这个文件,那么它将去编译器的类库路径里面找。
%
事实上,它不仅仅可以包含.h
文件和C++的无后缀文件,它还可以包含任何类型的文件,甚至是.txt
。
%
如果被引用代码中没有某些特殊的编译开关,编译器将会把被引用文件的内容直接替换#include<file>
。
%
这便是制作头文件的理论基础。
//mains.cpp
int main(){
printf("Hello Include!");
}
//run.cpp
#include<cstdio>
#include"mains.cpp"
%
把两份代码按照命名放在相同目录下,然后编译run.cpp
其实等价于编译如下代码:
#include<cstdio>
int main(){
printf("Hello Include!");
}
强制转换
%
代码中不可避免地会遇到一些常量的定义,特别是一些常量的参数和返回值。按照规范,所有不会被修改的变量都应该用const
修饰。但如果严格按照标准行事,很多时候会遇到麻烦。比如下面的代码,其作用是在数组中顺序查找第一个元素所在的位置,并返回其指针。
const int *search(const int *a, int n, int val){
for(int i=1;i<=n;i++)
if(a[i]==val)
return a+i;
return NULL;
}
%
从规范上讲,这个代码不需要修改数组,因而数组需要常量修饰,由于参数是常量,返回值也不得不是一个常量指针,不论这个数组是不是不可被修改的。
这也导致了一个问题,如果我需要找到这个元素,并且修改这个元素呢?因为返回的是常量指针,因此我们不能直接修改到其数值,如果你试着将这个常量指针赋值给一个非常量指针,你将无法通过编译,可如果你仍然不甘心,尝试强制转换,你会发现你能通过编译了,而且输出正确。
int n,a[1010],x,y;
int main(void) {
scanf("%d%d%d",&n,&x,&y);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
int *p=(int*)search(a,n,x);
*p=y;
for(int i=1;i<=n;i++)
printf("%d",a[i]);
return 0;
}
%
当然,更好的写法是const_cast<int*>(search(a,n,x))
,这是一个泯灭良知的宏,它的意义是消除常量。那么至此,C++在内存上最后的一丝严谨也被彻底摧毁,只因下面的代码。
//不要让这种代码出现在任何正规学术讨论/比赛/工程项目中
int main(void) {
const int a=0;
int *x=const_cast<int*>(&a);
*x=10;
const int *p=&a;
printf("%d",*p);
return 0;
}
% 就这样,我们强行把一个常量当做一个变量来使用了。可惜,这世上比你更刨根问底的人多了去了,他们还发现了这样的代码不能输出正确的答案。
//不要让这种代码出现在任何正规学术讨论/比赛/工程项目中
int main(void) {
const int a=0;
int *x=const_cast<int*>(&a);
*x=10; printf("%d",a);
return 0;
}
%
为什么?如果你输出了x
和&a
,你会发现,他们所指向的地址完全一样,这证明我们确确实实修改了常量,可是对应到常量自身却没有变化,这是为何?
因为编译器太好心了,帮我们直接把a
理解成0了,并没有回到内存中读。于是,他们利用volatile
使得每次读取“常量”时都回到内存中读取,并且为他们获得的正确结果感到沾沾自喜。
//不要让这种代码出现在任何正规学术讨论/比赛/工程项目中
int main(void) {
volatile const int a=0;
int *x=const_cast<int*>(&a);
*x=10; printf("%d",a);
return 0;
}
%
IBM的C++指南称呼*x=7;
为未定义行为(Undefined Behavior)。换言之,这个语句在标准C++中没有明确的规定,由编译器来决定如何处理,即使编译器输出满屏的brz is a pig.
,这仍然是一个标准的C++编译器。
%
这种行为和a[i]=i++;
一样,你不清楚究竟是先自加还是先寻址,所以这也是一种未定义行为,我们要做的便是避免这种语句在代码中出现。可以想象,如果程序中存在一个能随意更改常量的指针,那么这个指针无疑会成为这个程序一个巨大的潜在漏洞。
因此,当别人问你,常量能否被修改时,权衡一下自己的能力,等到有能力把这种东西讲清楚了,再给出回复。