预处理宏 VS 内联函数

1,117 阅读6分钟

原文地址

这篇文章会和大家介绍下宏和内联函数,以及二者的比较。

宏依赖于文本替换。预处理器宏只是在编译之前对代码进行简单的替换,不会进行类型检查。但是,在宏展开之后,编译器仍会对其进行类型检查。

先看一个例子:

#include <stdio.h>
#define SQUARE(x) x*x

int main() {
  int s = SQUARE(5); // This will be converted to: int s = 5*5;
  printf("%d\n", s);
  return 0;
}

因为代码中使用了#define SQUARE(x) x*x方式定义的宏,因此 SQUARE(x)会被替换为 x*x 。 因此,int s = SQUARE(5),将转换为int s = 5 * 5。

在看下面的例子:

int main() {
  printf("%d\n", SQUARE(3+2));
  printf("%lf\n", (double)1/SQUARE(5));
  return 0;
}

结果是:

11
1.000000

显然,这和我们的预期不符。那么我们尝试将将宏定义更改为下面的方式:

#define SQUARE(x) (x)*(x)

(double)1/SQUARE(5)会被替换为(double)1/(5)*(5),结果为:

25
1.000000

显然,对于第二个表达式的预期结果还是错误的。我们在尝试将宏定义改为下面的方式:

#define SQUARE(x) ((x)*(x))

结果如下:

25
0.040000

看起来,我们似乎解决了问题,但是,事实并非如此,我们稍微改动下代码:

int a = 5;
printf("%d\n", SQUARE(a++)); // 期望输出是 25
printf("%d\n", a);  // 期望输出是 6

实际上,结果为:

30
7

SQUARE(a++)会被替换为((a++)*(a++))。 因此,它们将为5 * 6,然后在此语句之后为a = 7。 第一个a = a + 1在5 *(...)之后立即运行。 同样,第二个a = a +1在5 * 6之后立即运行。 它的执行顺序类似于:

a = 5;
int result;
result = a;
a = a + 1;
result = result * a;
a = a + 1;
return result;

以下是使用GCC解决我们的问题的一种解决方案:

#define SQUARE(x) ({    \
  typeof (x) _x = (x);  \
  _x * _x;              \
})

typeof是非标准的GNU扩展,用于声明与另一个类型相同的变量。typeof(x) _x =(x),将变成int _x =(x++),由此,我们的问题会得到解决。

什么时候使用宏

常见的情况有:

  • 为了减轻函数调用的开销,而可能要花费较大的代码大小。
  • 定义类型以外的常规功能,如:
#define SUM(array, size) ({         \
 typeof(*array) total = 0;       \
 int i = 0;                      \
 for (i = 0 ; i < size ; i++) {  \
   total += array[i];            \
 }                               \
 total;                          \
})

需要注意什么

边缘效应

如果宏的定义不准确,则该行为将超出你的预期,而不会出现任何编译器警告或错误。如:

#include <stdio.h>
#define MAX(a,b) ((a < b) ? b : a)

int main() {
  int a = 5, b = 10;
  printf("%d\n", MAX(a++, b++)); // 期望值是 10, 实际值是 11
  return 0;
}

安全检查

宏在替换过程中,不会进行类型安全检查。

内联函数

内联函数是代码被插入到调用者代码处的函数。如同 #define 宏,内联函数通过避免被调用的开销来提高执行效率,尤其是它能够通过调用(“过程化集成”)被编译器优化。

内联函数由编译器解析,而宏由预处理器扩展。

什么时候应该使用内联函数代替宏

预处理器宏只是在编译之前对代码进行简单的替换,不会进行类型检查。在编译器没有任何调试提示的情况下,我们可能会无意识的为宏使用的错误的类型。

看下面的例子:

#include <stdio.h>
#include <stdbool.h> 

#define TURN_UP(audio)  ++audio.vol

struct Power {
  bool ac;  // true: Alternating current (AC), false: direct current(DC)
  int vol;  // Voltage
  int amp;  // Ampere
  int freq; // Frequency for AC. This must be zero for DC.
};

struct Audio {
  int freq; // Frequency
  int vol;  // Volume
  int dur;  // Duration
};

int main() {
  struct Power u = { .ac = 0, .vol = 5, .amp = 1, .freq = 0 };
  printf("Power: %s %dV %dA %dHz\n", (u.ac) ? "AC" : "DC", u.vol, u.amp, u.freq);

  struct Audio a = { .freq = 440, .vol = 10, .dur = 5 };
  printf("Sound: %dDB %dHz for %d Seconds \n", a.vol, a.freq, a.dur);

  TURN_UP(u); // Oops! Typo! It should be TURN_UP(a);
  printf("Sound: %dDB %dHz for %d Seconds \n", a.vol, a.freq, a.dur);

  return 0;
}

如果我们不自觉地使用TURN_UP(u)而不是TURN_UP(a),那么GCC不会给我们任何提示。

期望的结果是:

Power: DC 5V 1A 0Hz
Sound: 10DB 440Hz for 5 Seconds
Sound: 11DB 440Hz for 5 Seconds

实际结果是:

Power: DC 5V 1A 0Hz
Sound: 10DB 440Hz for 5 Seconds
Sound: 10DB 440Hz for 5 Seconds

当我们的代码库有成千上万的行时,这种意外的结果将是一个严重的问题。 没有任何线索,我们将不知道出了什么问题。最好有提示来找出原因。

我们将宏定义改为内联的方式,代码如下:

#include <stdio.h>
#include <stdbool.h>

struct Power {
  bool ac;  // true: Alternating current (AC), false: direct current(DC)
  int vol;  // Voltage
  int amp;  // Ampere
  int freq; // Frequency for AC. This must be zero for DC.
};

struct Audio {
  int freq; // Frequency
  int vol;  // Volume
  int dur;  // Duration
};

void turn_up(struct Audio*) __attribute__((always_inline));

void inline turn_up(struct Audio* a) {
  ++a->vol;
}

int main() {
  struct Power u = { .ac = 0, .vol = 5, .amp = 1, .freq = 0 };
  printf("Power: %s %dV %dA %dHz\n", (u.ac) ? "AC" : "DC", u.vol, u.amp, u.freq);

  struct Audio a = { .freq = 440, .vol = 10, .dur = 5 };
  printf("Sound: %dDB %dHz for %d Seconds \n", a.vol, a.freq, a.dur);

  turn_up(&u); // Oops! Typo! It should be turn_up(&a);
  printf("Sound: %dDB %dHz for %d Seconds \n", a.vol, a.freq, a.dur);

  return 0;
}

相反,如果我们使用内联函数来执行此操作,则gcc会提示此问题。

$ gcc test.c
test.c:34:11: warning: incompatible pointer types passing 'struct Power *' to parameter of type 'struct Audio *' [-Wincompatible-pointer-types]
  turn_up(&u);
          ^~
test.c:19:35: note: passing argument to parameter 'a' here
void inline turn_up(struct Audio* a) {
                                  ^
1 warning generated.

但是,它可能没有足够的帮助。 在开发大型软件时,通常会有很长的编译器日志。 很难追踪。

在这种情况下,给出错误胜于警告。 幸运的是,我们可以自己定义一些警告为错误。 这是调试的好选择。 我们可以使用-Werror标志将所有警告变为错误,或使用-Werror = 将指定的警告变为错误。

在例子中,我们可以将不兼容的指针类型定义为错误,如下:

$ gcc -Werror=incompatible-pointer-types [FILENAME]

结果会变为:

test.c:34:11: error: incompatible pointer types passing 'struct Power *' to parameter of type 'struct Audio *'
      [-Werror,-Wincompatible-pointer-types]
  turn_up(&u); // Oops! Typo! It should be turn_up(&a);
          ^~
test.c:19:35: note: passing argument to parameter 'a' here
void inline turn_up(struct Audio* a) {
                                  ^
1 error generated.

因此,我们在编译的时候,避免了此类问题的发生。

用内联函数替换宏的另一种情况是如何使用静态变量,例子如下:

#include <stdio.h>
#define COUNT ({        \
  static int a = 0;     \
  printf("%d\n", ++a);  \
})

void count() __attribute__((always_inline));

void inline count() {
  static int a = 0;
  printf("%d\n", ++a);
}

int main() {
  printf("--- macro ---\n");
  COUNT;
  COUNT;
  COUNT;

  printf("--- inline ---\n");
  count();
  count();
  count();

  return 0;
}

结果如下:

--- macro ---
1
1
1
--- inline ---
1
2
3

二者比较

  • 宏本身不是类型安全的。
  • 如果定义不正确,宏很可能会导致意外的结果。
  • 在调试时,不能单步执行 #define , 但可以单步执行内联函数。
  • 宏更加灵活,因为它不执行类型检查,并可以嵌入其它宏。
  • 内联函数并不总能保证内联。一些编译器需要进行额外的配置,或仅在发版中起作用。
  • 递归函数是大多数编译器忽略内联的实例。
  • 如果使用-O使GCC / G ++优化为最小大小,则内联函数可能会被忽略。
  • 如果将-O2或-O3用于最大速度,则GCC / G ++将尝试内联大多数可能的功能。
  • 内联函数可以提供变量的作用域。尽管预处理器宏可以使用代码块{...}来实现这一点,但是静态变量不会如你对宏的预期那样工作。
  • 宏不能访问私有的或保护的变量,而内联函数可以。
  • 有些情况下,无法使用内联。

扫一扫关注公众号,get更多技术好文