C++黑科技

前题

%   只要规则被制定了,就总有人试图凌驾于规则之上。

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,而只能使用fopenfprintf,而这样也导致了一定程度上的编程困难,能不能把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(宏)),来输出展开后的样子。


内置宏
系统提供了一些内置宏。

  1. __func____FUNCTION__:该宏所在的函数名。
  2. __LINE__:该宏所处的行数。
  3. __DATE__:该宏所在函数的当前编译日期。
  4. __TIME__:该宏所在函数的当前编译时间。
  5. __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++;一样,你不清楚究竟是先自加还是先寻址,所以这也是一种未定义行为,我们要做的便是避免这种语句在代码中出现。可以想象,如果程序中存在一个能随意更改常量的指针,那么这个指针无疑会成为这个程序一个巨大的潜在漏洞。
  因此,当别人问你,常量能否被修改时,权衡一下自己的能力,等到有能力把这种东西讲清楚了,再给出回复。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值