引言

本节涉及到预处理和编译的相关知识,我们先来回顾一下C/C++程序执行的过程(以g++编译器为例)。

  1. 预处理 的指令为cpp main.cpp > main.i

    它主要进行的是头文件的展开与宏的替换。预处理后得到的文件为main.i。

  2. 编译 的指令为g++ -S main.i

    编译后得到的文件为main.s。

  3. 汇编 的指令为as -o main.o main.s

    汇编得到的文件为main.o。.o文件时我们经常见到的中间文件(直接用IDE应该很少见到)。

  4. 链接 的指令为g++ -o main main.o

    Windows平台下生成的文件就是我们经常看到的.exe (可执行文件)。

如果读者还有疑惑或不熟悉,请移步这篇教程:

宏(Macro)

宏的使用

  1. 定义一个符号常量。这是我们经常使用的宏定义的形式,如#define PI 3.14 。它的好处是便于代码的维护。
  2. 宏函数替换常用的语句。如#define MAX(x,y) ((x)>(y)?(x):(y))实现求两个数的最大值 。这样我们在代码中使用MAX(1,2)的时候,就会在预处理阶段被预处理器自动替换成((1)>(2)?(1):(2)),最终的结果为2,没有问题。它的作用是减少代码量,偷懒伴侣

使用宏函数一定要注意打括号!少了括号就可能会出问题!

#define 我们常用的预处理指令。除此之外,常见的预处理指令还有#ifdef#ifndef#endif 等。它们在工程中用得很多,主要作用是防止头文件的重复包含以及函数的反复声明。

宏的实例

既然都讲宏了,那就顺便再讲讲注释#ifdef#endif夹带私货不过分吧~

我们在命令行输入cpp test.c > test.i,得到 .i文件,然后将.c文件和.i文件进行对比:

结论

预处理阶段,进行下面的操作:

  1. 宏的替换。MAX(x,y)在.i文件中被替换掉了。
  2. 注释的抹去。.i文件中没有注释了。
  3. 代码块的消除。#ifdef MIN(x,y)#endif之间的代码都被忽略了。

为什么预处理的指令是cpp开头而不是g++呢?因为预处理准确来说是由编译器套件中的预处理器来进行的。并不是由编译器完成。有时候说编译器对cpp文件进行预处理实际上是不准确的说法。

宏的优点

  • 使用宏定义符号常量可以方便我们的理解和对代码进行维护;
  • 使用宏函数可以避免执行一般的函数时所产生的开销,加快程序运行的速度;

宏的缺陷

对于宏函数,它的缺点有:

  • 不能调试,因为在预处理阶段进行替换。
  • 没有类型检查,不安全。
  • 写法复杂,容易出错。(下面通过例子讲解)

  • 先观察下面这个程序:
1
2
3
4
5
6
#include <iostream>
#define MUL(a,b) a*b
int main(){
std::cout<<MUL(1+2,3);
return 0;
}
结果与改进

最终输出7,与预想结果9不符。因为宏替换后变成了1+2*3,改变了运算顺序。

改进:在宏定义处加上括号,#define MUL(a,b) (a)*(b)

  • 再来看看这个程序:
1
2
3
4
5
6
7
#include <iostream>
#define abs(a) ((a>=0)?a:-a)
int main(){
int x=1;
std::cout<<abs(x)<<'\n'<<abs(++x);
return 0;
}
结果

它输出的结果为1和3!与目标输出1和2不符。因为宏替换后变成了((++x>=0)?++x:-++x),自增运算进行了两次。

而且,无法通过单纯地添加括号来避免!

总结:宏函数可通过添加括号避免大多数问题,但是有些情况却不能避免,是使用宏的缺陷!

那么,有没有既高效又能避免上述问题的方案呢?C++告诉我们要使用内联函数

内联函数

内联函数的定义

使用inline 修饰的函数叫做内联函数。

不同于宏在预处理阶段进行替换,编译中期 时编译器会在调用内联函数的地方将其展开。

所以内联函数没有函数压栈的开销,能提高程序运行的效率。同时相比于宏函数,内联函数可以调试,而且编译器能够对其进行优化,不会出现上面输出和预想不同的情形。

内联函数的使用

一般我们只将频繁调用,且函数体较小的函数定义为内联函数,下面对此进行说明:

  1. 如果函数的调用次数少,函数压栈所产生的开销对于程序而言是可以忽略的。这时使用内联函数意义不大。
  2. 如果函数体较大,编译中期就会将整个函数展开。且如果调用次数很多,那么会使得整个代码过长,编译器编译代码的时间还可能会增加。
  3. 如果函数频繁调用,且函数体较小,那么这时使用内联函数就很好。

我们使用inline函数来解决之前使用宏函数未解决问题:

1
2
3
4
5
6
7
8
9
#include <iostream>
inline int abs(int a){
return (a>=0)?a:-a;
}
int main(){
int x=1;
std::cout<<abs(x)<<'\n'<<abs(++x);
return 0;
}

这时正确地输出1和2。

使用内联函数的说明

  1. 内联函数内部不能有循环语句 ( 如for , while , switch )。一旦出现,即使函数前面有inline,编译器也会把它当作普通函数来处理。即编译器默认内联函数不能太复杂
  2. 使用内联函数实际上是一种以空间换时间 的做法。我们知道,在程序运行时,代码会以二进制指令的形式储存在代码区中。而使用inline将函数展开后,会使代码增长,代码区储存的指令增加,占用的空间增加。

内联函数总结

  1. 我们使用内联函数来代替宏函数,但是宏还有其独特的用法,比如定义符号常量。
  2. 要注意使用inline函数的情景。一般我们只将频繁调用,且函数体小的函数定义为内联函数。而且内联函数内部不能有循环语句。