这篇文章会和大家介绍下宏和内联函数,以及二者的比较。
宏
宏依赖于文本替换。预处理器宏只是在编译之前对代码进行简单的替换,不会进行类型检查。但是,在宏展开之后,编译器仍会对其进行类型检查。
先看一个例子:
#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更多技术好文
