详解C/C++常用预处理器及相关应用

文章目录

1.预处理是啥

2.#include

3.#define #undef

| 3.1 基本用法

| 3.2 宏

| 3.3 #undef

4.#if #else #elif #endif

5.#ifndef #ifdef

6.#error

7.总结


1.预处理是啥

在了解预处理之前让我们先看一下C/C++从代码到运行的过程

返回错误信息
源代码
预处理
编译
错误信息
优化
汇编过程
链接
可执行代码

简单介绍一下这几个部分:

  1. 预处理:就是我们的主题,它干的事情简单来说就是在被编译器看到之前做手脚,以达到我们的目的
  2. 编译:它干的事情就是把我们写的代码变成汇编语言,可以直接使用指令gcc和g++来编译,它可以返回错误信息
  3. 优化:其实优化这件事情对于我们来说也很重要,它几乎可以把一段被编译好的代码变成最优的,可以看看这篇文章:C++编译器到底能帮我们把代码优化到什么程度?
  4. 汇编过程:就是把汇编代码直接变成机器语言代码
  5. 链接:简单理解就是把每个文件都拼起来,使得我们可以使用其他文件的函数、类等等
  6. 得到可执行代码
    所以回归正题,预处理器就是对程序干偷鸡摸狗见不得人的事的东西,它在编译器看到代码之前就已经完成了它的工作,这也暗示了它的一个很严重的缺陷,那就是:编译器看到的不是我们看到的,返回的错误信息就也不是我们想要的,这一点在#define指令上显得尤为突出

2.#include

这肯定是在座的各位写过的第一条C/C++语句,所以肯定都知道咋用,不知道的可以重新回去学了,这里就简单介绍一下,用法很简单,后面接上文件名就可以,自己的文件用""来括起来,不是的用<>括起来,其实这个预处理器工作原理极其简单粗暴,就是把两个文件的东西拼起来,比如这条示例:

//a.h
class a
{
  int val;
public: 
  a(const int & v):val(v){} 
  int & value(){return val;}
};

//a.cpp
#include
#include"a.h"
using namespace std;
int main()
{
   a n(90);
   cin>>n.value();
   cout<<n.value();
}

而经过预处理器#include做过手脚以后,编译器看到的应该是这样的(由于iostream太长了,我就直接写#include了,大家意会一下就好):

#include
class a
{
  int val;
public: 
  a(const int & v):val(v){}
  int & value(){return val;}
};
using namespace std;
int main()
{
  a n(90);
  cin>>n.value();
  cout<<n.value();
}

就是这么简单,但是记住,不要同时包含多个同样的头文件,这个规则很重要,比如下面这个例子就会出错:

//b.h
#include
class b
{
  double dv;
public: 
  b(const double & v):dv(v){} 
  virtual void report() {std::cout<<"I'm class b\n";}};
//c.h
#include
#include
#include"b.h"
class c:public b
{
 char ch;
public: 
 c(const double & d, const char & c):ch(c), b(d){} 
 virtual void report() {std::cout<<"I'm class c\n";}}
//2.cpp
#include
#include"b.h"
#include"c.h"
int main()
{
   b classB(90.29); 
   c classC(90.39, 'd'); 
   b.report(); 
   c.report();
}

其实这个例子也是帮大家温习一下继承,当然这里的逻辑很简单,但是编译时会出现问题,因为c.h里包含了b.h,而在2.cpp里却同时引用了b.h和c.h,这就会导致b.h里的东西会出现两次,导致重复声明,从而产生错误,但是为什么头文件iostream尽管被包含了三次但是仍然没有报错,这是为什么呢,答案会在讲ifndef和ifdef时讲到

3.#define

3.1 基本用法

熟悉C的朋友们绝对对它很熟悉,它的作用就是替换代码里的内容,它的格式如下:

#define (此处换成标识符) (替换的内容)

它的作用和原理其实就是替换,比如我们在C里声明一个常量,我们可以这样写:

#define NUM 100

static int NUM=100;

对于第一种方法其实原理非常简单粗暴,比如我们写出了以下代码:

#define NUM 100
int a[NUM];

在#define预处理完之后编译器看到的代码是这样的:

int a[100];

由此可见,#define把标识符NUM换成了替换内容即100,就是这么简单粗暴,而且它还可以替换其他的,比如类型:

#define UINT unsigned int

各种大杂烩

#define WINMAIN_FUNC int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)

这个其实就是把标识符WINMAIN_FUNC变成了WinMain函数的函数头,虽然使用了标识符简化了代码,但是却降低了代码的可读性,所以不建议这么做,这里只是为了演示方便,当然其实也可以不写替换的内容:

#define _HELLO_WORLD_

这种用法其实就是声明了一个标识符,并且把它替换为空,那我们为什么要这么干一个看起来像没事闲的多写一条语句的行为呢,答案也将在讲ifndef与ifdef时讲到,当然这里还是不推荐使用#define来声明常量或类型别名,应该使用staticconsttypedef,因为#define的工作原理就是替换,所以当我们写出这样的代码时

#define n 5
int main()
{
  int n=10;
}

而编译器看到的代码是:

int main()
{
  int 5=10;
}

所以报错信息就会指出不能把数字当变量名,而你会觉得自己根本没有写,所以就会很难排错,这就是它的缺点,但是加入换成使用const

const int n=5;
int main()
{
  int n=5;
}

这时编译器就会指出不能重复声明,然后就可以轻而易举的排错,这就是使用编译器而不是预处理器的好处

3.2 宏

宏其实就是类似于一个函数的一个东西,声明格式如下:

#define (此处换成标识符)(参数1, 参数2, ...) (替换内容)

比如假如我们写一个可以算出一个数的立方的宏:

#define cube(X) (X*X*X)

算平均数的宏

#define aver(a, b) ((double)a*b/2.0)

简单写一个调用它们的小例子:

#include
#define cube(X) (X*X*X)
#define aver(a, b) ((double)a*b/2.0)
int main()
{
  using namespace std;
  int a; cout<<"输入a:"; 
  cin>>a; 
  cout<<"a的立方为"<<cube(a)<<endl; 
  int x, y; 
  cout<<"输入x和y:"; 
  cin>>x>>y; 
  cout<<"x和y的平均数为"<<aver(x, y);
}

就是这样,但是它也只是替换,还是不容易排错,而且有一个很严重的问题,那就是不能写出这样的语句:

cube(23+29)

因为它只是在替换,所以这样写会出现问题,C++也给出了一种解决方案:内联函数,它可以使用inline关键字来声明,比如对于cube的宏,内联函数方法如下:

template<typename T>
inline T cube(const T & x)
{
  return x*x*x;
}

它也达到了同样的效果,但是明显麻烦了许多,这是因为对于#define只是替换,所以不用声明类型,但是对于内联函数就不一样了,必须声明类型,但是它做到了真正的参数传递,而且编译器可以看到,这就是它们分别的优缺点,在哪适合用哪个就是了。

3.3 #undef

#undef可以撤销定义,比如我们可以写出这样的代码:

#define _UNDEF_
...
#undef _UNDEF_

4.#if #else #elif #endif

看字面意思就知道它与我们的if else语句差不多,然而呢,确实如此,不好解释,先上示例:

#include
using namespace std;
#define N 11
int main()
{
  #if N>10//判断条件
    cout<<"N比10大"; 
  #elif N==10//elif是else if的简写,有点像python是不是  
    cout<<"N等于10小"; 
  #else//跟普通if else语句一样,一个意思,当然也可以不写  
    cout<<"N比10小"; #endif//每一个if语句后面都有一个#endif来表示结尾
}

差不多就是这样,运行结果如下:

N比10大

就是这样,在#if后面的条件语句若为真就编译,若为假就删除这段代码,编译器就看不到它了,它有时被用来写注释,下面演示了它的优势和工作原理:

#if 0
我是注释内容,编译器看不见
因为上面的#if的判断条件为0
所以这段文字永远不会被执行
这样写比起/**/有什么好处呢
对于/**/这种注释不能嵌套
比如我们有一段完整的代码:
/*---------------
function:hmean
uses:算出调和平均数
---------------*/
double hmean(const double & a,const double & b)
{/*以下代码包含了调和平均数的公式*/ 
  return 2.0/(double)a*b;
/*函数尾*/}
但是假如我们因为种种原因想把它删了,但是觉得以后还有可能用,所以决定把他注释掉,假如使用/**/这种注释方法就会变成这样:
/*
/*---------------
function:hmean
uses:算出调和平均数
---------------*/
double hmean(const double & a,const double & b)
{/*以下代码包含了调和平均数的公式*/ 
  return 2.0/(double)a*b;/*函数尾*/
}
*/
由此可见,对于/**/这种注释会以下一个*/来做结尾,导致不能嵌套,但是使用
#if 0 
... 
#endif
就不会有这样的问题,因为外面有#if 0 #endif,所以里面的就直接被删除,所以对于这段代码应该这么注释:
#if 0
/*---------------
function:hmean
uses:算出调和平均数
---------------*/
double hmean(const double & a,const double & b)
{/*以下代码包含了调和平均数的公式*/
  return 2.0/(double)a*b;
/*函数尾*/}
#endif
所以对于注释大量代码,还是用#if 0 #endif吧
#endif

5.#ifndef #ifdef

在头文件里,我们经常可以看到类似这样的代码(其中的_THIS_HEADER_FILE_可以替换成别的):

#ifndef _THIS_HEADER_FILE_
#define _THIS_HEADER_FILE_
//头文件内容...
#endif//_THIS_HEADER_FILE_

这里的#ifndef的意思就是对于它后面的标识符是否被定义过,最后也是需要跟上一个#endif,比如我们可以写出以下代码:

#include 
using namespace std;
#define _IFNDEF_//后面为空,其实可以输入任何字符串,代表要把它单纯的定义一下,内容是啥不重要
int main()
{
  #ifndef _IFNDEF_ 
    cout<<"_IFNDEF_未被定义";
  #else  
    cout<<"_IFNDEF_已被定义";
  #endif
}

程序运行:

_IFNDEF_已被定义

#ifdef的意思刚好相反,代表它后面的标识符是否被定义,为了实现同样效果,我们可以把这个程序主函数部分改成:

  #ifdef _IFNDEF_
    cout<<"_IFNDEF_已被定义";
  #else  
    cout<<"_IFNDEF_未被定义";
  #endif

就是这样,但是为什么头文件里需要这个语句呢,那先让我们看一看这个例子:

//d.h
#ifndef _MY_CLASS_D_//假如_MY_CLASS_D_没有被声明
#define _MY_CLASS_D_//那就声明它
#include//包含头文件
class d
{
  int val;
public:
  d(const int & v):val(v){} 
  virtual void report() 
  {std::cout<<"I'm class d and have value "<<val<<endl;}
};
#endif//结尾

//d.cpp
#include"d.h"
#include//注意,这里以及d.h里都包含了它,但是仍然合法,这是因为它也使用了这种技巧
#include"d.h"//再次包含,仍然合法
#include"d.h"//多次包含,仍然合法
int main()
{
  d a(90);
  a.report();
}

让我们展开一下这个#include(至于iostream我就不展开了):

#ifndef _MY_CLASS_D_//第一次包含,还没有声明,会执行
#define _MY_CLASS_D_//假如还没有声明,所以第一次包含过后就得声明,以确保下一次这些代码不会执行,从而避免出现重复定义
#include
class d
{
  int val;
public: 
  d(const int & v):val(v){} 
  virtual void report() 
  {std::cout<<"I'm class d and have value "<<val<<endl;}
};
#endif
#include
#ifndef _MY_CLASS_D_//非第一次包含,_MY_CLASS_D_已被声明,这段代码相当于直接跳过
#define _MY_CLASS_D_
#include
class d
{
  int val;
public: 
  d(const int & v):val(v){} 
  virtual void report() 
  {std::cout<<"I'm class d and have value "<<val<<endl;}
};
#endif
#ifndef _MY_CLASS_D_//同样非第一次包含,_MY_CLASS_D_已被声明,这段代码相当于直接跳过
#define _MY_CLASS_D_
#include
class d
{
  int val;
public:
  d(const int & v):val(v){} 
  virtual void report() 
  {std::cout<<"I'm class d and have value "<<val<<endl;}
};
#endif
int main()
{
  d a(90); 
  d.report();
}

这就是它的原理了,第一次包含过后,后面每一次都会被忽略,导致可以多次包含而不产生任何重复定义,这就是一般它的应用了。

6.#error

#error用法极其简单,就是阻止程序运行,让编译器返回与#error后面字符串相同的错误信息,一般与#if等配合使用,不说了,上示例:

//这个程序检测编译器标准是否为C++11及以上
#if __cplusplus < 201103//__cplusplus代表c++版本,201103为版本号
#error C++版本不到C++11
#else 
int main(){}
#endif

运行一下就知道你的版本够不够了~~

7.总结

原创不易,感谢支持

你可能感兴趣的:(笔记,c++)