C++预处理
预处理指令
一,预处理
1.1.定义
在编译器正式编译源代码之前,会先进行预处理操作。
在C++中,所有的预处理指令都由“#”开头。预处理指令结尾不写分号。
1.2.预处理了什么
把注释去掉,把include的头文件引用进来,define的内容进行替换。
1.3.有哪些预处理指令?
#include
#define
#if
#else
#endif
等
二,include
用法1:#include<文件>
用法2:#include“文件”
用法1用于包含标准库所在目录的头文件,用法2表示包含当前目录下的头文件。
说白了,include就是把另外一个文件的所有内容不管三七二十一放进来就是了。
问题:如果想要引用其他文件夹的头文件?
回答:加上目录不就行了。例如bits/stdc++.h,表示在标准库目录下bits目录下的stdc++.h文件。
问题:引用的文件有类型的要求吗?(必须是.h?)
答案:没有要求,只要内容是文本就行(例如txt)。#include<iostream>,iostream就是一个没有扩展名的文件。
三,define指令
define指令用于定义宏。
3.1.无参数宏
define在这类场合下,可以起到常量定义的效果。
#include<bits/stdc++.h> #define PI 3.14159 using namespace std; int main(){ cout<<PI<<endl; }
在运行第5行的时候,预处理器会把PI替换为3.14159。
3.2.带参数宏
格式:#define 宏名(参数列表) 内容
我们知道,直接定义函数,函数由于要有调用——返回的过程,执行速度相对较慢。如果使用宏,速度会有提高。
示例:swap函数。
#include<bits/stdc++.h> #define Swap(a,b) {int temp=a;a=b;b=temp;} using namespace std; int main(){ int m=2,n=3; Swap(m,n); cout<<m<<n; }
程序会把swap(m,n)按照define替换为:int temp=m;m=n;n=temp;
3.3.参数宏的局限性
例如:计算平方。
#include<bits/stdc++.h> #define func(a) a*a using namespace std; int main(){ cout<<func(2+3); }
输出:11
我们观察一下,func(2+3)替换成了:
2+3*2+3,2+3没有打上括号,结果等于2+6+3=11。
因此,我们需要进行修改。
#include<bits/stdc++.h> #define func(a) ((a)*(a)) using namespace std; int main(){ cout<<func(2+3); }
这样就解决了。但是还有问题,这是避免不了的。
#include<bits/stdc++.h> #define func(a) ((a)*(a)) using namespace std; int main(){ int n=4; cout<<func(++n); }
输出:36
这是因为,++n执行了两次。这是由于宏的局限性所致的。对于这类的函数,我们不能用define,必须使用普通函数或者Inline函数。
三,条件编译
3.1.条件编译
有几个指令可以用来有选择地对部分程序源代码进行编译。这个过程被称为条件编译。条件预处理的结构与if 选择结构很像。请看下面这段的代码:
#ifdef DEBUG cout<<a<<endl; #endif
这句话说明,如果定义了debug宏,那么执行下面的语句。在调试程序的时候,可以这样编译,不调试的时候只要取消定义debug即可。
3.2.示例:重复引用头文件问题
比较常见的例子是重复引用头文件的问题。例如,程序中有:
#include<a.h>
#include<b.h>
不巧的是,b.h的开头也有个include<a.h>,这样把a.h引用两遍。如果变量或者函数定义两遍会出错,因此,头文件一般都有一个避免重复引用的机制,如下:
#ifndef _a_h
#define _a_h 1
头文件内容
#endif
第一次引用时候,没有定义a_h,因此把头文件引用进来。第二次,已经定义了a_h,就跳过整个头文件。这样的设计就可以保护头文件不被重复引用。自己编写自己的头文件时,也必须要注意这一点。
四,#和##运算符
4.1.token
C++的token包括关键字,标识符,括号,符号等。
我们所有的define替换都是对token进行替换的(并非按字符替换)。例如:#define f 1
那么:cout<<ff<<endl;
不会把ff替换为11。(因为ff是一个token)
4.2.#运算符
#运算符用于把一个token转换为字符串。
举例:assert模拟。assert(x)的作用是检测x是否为0。
例如,assert(a+2),如果a=-2,那么a+2等于0,输出:“assertion a+2 failed”
assert(x==3),如果x不等于3,输出“assertion x==3 failed”
分析:如果直接用普通函数assert(x),只能判断x是否为0,无法输出表达式。(因为输出的内容有assertion 表达式 failed,用普通变量只能输出assertion 0 failed)
因此,使用#运算符先转换为字符串,再输出。
程序:
#define Assert(expression) Assert_Sub(#expression,expression) void Assert_Sub(char *s,int n){ if(n==0)cout<<“assertion”<<s<<“failed”; }
#expression表示把expression这个token转换为字符串。因此,我们可以完美解决这个问题。
4.3.##运算符
##运算符用于把两个token连接为一个token。
举例:
#include<bits/stdc++.h> #define cat(a,b) a##b using namespace std; int main(){ int ab=3; cout<<cat(a,b); }
a和b连接起来就是标识符ab,也就是变量。