定义
预处理令牌
预处理令牌(Token)是指预处理器在处理源代码时识别的基本单元。预处理令牌用于执行宏替换、条件编译和文件包含等操作。预处理令牌的主要类型包括:标识符(变量名、函数名)、关键字(#define、#include)、常量、运算符、分隔符。
逻辑代码行
代码中涉及实际业务逻辑的行,一个逻辑行中的所有Token应被视为一个单元来解析和处理。
空白字符
包括空格、制表符(Tab)、垂直制表符(Vertical Tab)、换行符、回车符、换页符。
预处理指示
一条预处理指示由一个逻辑代码行组成。以#开头,后面跟若干Token,在预处理指示的开头、结尾以及各Token之间可以包含任意数量的空格和制表符,但不允许出现其他空白字符。
C编译器在进行语法解析之前的预处理步骤
- 连续的代码行通过续行符"\"合并成一个逻辑代码行。
- 注释替换成空格。
- 将逻辑代码行划分成不同的Token。
- 在字符常量或字符串字面值中,处理转义序列,用相应的字节替换。
- 在Token中识别出宏定义和预处理指示,进行宏展开,预处理。
- 去除空白字符。
编译并查看预处理结果
g++ -E example.cpp -o example.i
生成一个名为example.i的文件,里面包含宏展开的代码。
生成汇编代码
g++ -S example.cpp -o example.s
生成的example.s文件会包含编译器生成的汇编代码。
编译代码并生成目标文件,指定优化级别
g++ -O0 -c example.cpp -o example_0.o # 无优化
g++ -O1 -c example.cpp -o example_1.o # 基本优化
g++ -O2 -c example.cpp -o example_2.o # 更高级优化
生成不同优化级别的目标文件,如example_0.o,example_1.o,example_2.o。
生成反汇编代码
objdump -d example.o > example.asm
把example.o的反汇编代码输出到example.asm文件中。
变量式宏定义
宏定义名可以像变量一样在代码中使用。
#define N 20
#define STR "hello, world"
函数式宏定义
宏定义名可以像函数一样在代码中使用。
// test.cpp
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int main() {
int i = 5;
int j = 10;
int k = MAX(i & 0x0f, j & 0x0f);
return 0;
}
函数式宏定义的参数没有类型,预处理器只负责进行形式上的替换,而不做参数类型检查。
对于真正的函数,编译器会生成一个函数体,调用时有传参指令和call指令。这种方式在编译时生成了额外的调用开销,但在多个地方调用同一个函数时,函数体只会出现一次。对于函数式宏定义,在预处理阶段被展开为具体的代码。预处理器在每次使用宏的地方展开宏定义,生成的代码直接包含宏体,可能导致目标文件增大。
定义这种宏最好不要省略括号,可能出现运算优先级错误。
宏定义在处理有副作用的表达式时可能会导致一些意外的结果,这主要是因为宏在预处理时只是简单地展开代码,而不是进行真正的函数调用。
eg. 可能导致的错误
#include <iostream>
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int max(int a, int b) {
return (a > b) ? a : b;
}
int main() {
int a = 1;
int b = 2;
int result = MAX(++a, b);
std::cout << "Result: " << result << std::endl;
std::cout << "a: " << a << std::endl;
a = 1;
b = 2;
result = max(++a, b);
std::cout << "Result: " << result << std::endl;
std::cout << "a: " << a << std::endl;
return 0;
}
a的值可能会被递增两次,因编译器而异,这就导致了不可预期的行为。这是宏的一个潜在陷阱,尤其是当宏的参数有副作用时。
eg. 可能导致效率降低
#define MAX(a, b) ((a)>(b)?(a):(b))
#include <iostream>
int a[] = { 9, 3, 5, 2, 1, 0, 8, 7, 6, 4 };
int max(int n){
return n == 0 ? a[0] : MAX(a[n], max(n-1));
}
int main(){
std::cout<<max(9)<<std::endl;
return 0;
}
a[n]被计算了两次,尤其是当a[n]是复杂的表达式时。
eg. 函数式宏的常见形式
#include <stdio.h>
#define device_can_wakeup(dev) ((dev)->can_wakeup)
#define device_set_wakeup_enable(dev, val) ((dev)->wakeup_enabled = (val))
typedef struct {
int can_wakeup;
int wakeup_enabled;
} device_t;
#define device_init_wakeup(dev, val) \
do { \
device_can_wakeup(dev) = !!(val); \
device_set_wakeup_enable(dev, val); \
} while(0)
int main() {
device_t my_device = {0, 0}; // 初始化设备
device_init_wakeup(&my_device, 1); // 使用宏设置设备唤醒功能
printf("Device can wake up: %d\n", my_device.can_wakeup);
printf("Device wakeup enabled: %d\n", my_device.wakeup_enabled);
return 0;
}
do{...} while(0);是一种常见的技巧,用于确保宏在使用时总是作为一个单独的语句出现。使得宏在if语句等控制流结构中更安全,因为它在被展开时不会引入意外的代码块。
内联函数
#include <iostream>
inline int MAX(int a, int b){
return a > b ? a : b;
}
int a[] = { 9, 3, 5, 2, 1, 0, 8, 7, 6, 4 };
int max(int n){
return n == 0 ? a[0] : MAX(a[n], max(n-1));
}
int main(){
std::cout<<max(9)<<std::endl;
return 0;
}
生成的文件
# test5_0.0 elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z3maxi>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 83 7d fc 00 cmpl $0x0,-0x4(%rbp)
f: 75 08 jne 19 <_Z3maxi+0x19>
11: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 17 <_Z3maxi+0x17>
17: eb 2f jmp 48 <_Z3maxi+0x48>
19: 8b 45 fc mov -0x4(%rbp),%eax
1c: 83 e8 01 sub $0x1,%eax
1f: 89 c7 mov %eax,%edi
21: e8 00 00 00 00 callq 26 <_Z3maxi+0x26>
26: 89 c1 mov %eax,%ecx
28: 8b 45 fc mov -0x4(%rbp),%eax
2b: 48 98 cltq
2d: 48 8d 14 85 00 00 00 lea 0x0(,%rax,4),%rdx
34: 00
35: 48 8d 05 00 00 00 00 lea 0x0(%rip),%rax # 3c <_Z3maxi+0x3c>
3c: 8b 04 02 mov (%rdx,%rax,1),%eax
3f: 89 ce mov %ecx,%esi
41: 89 c7 mov %eax,%edi
43: e8 00 00 00 00 callq 48 <_Z3maxi+0x48>
48: c9 leaveq
49: c3 retq
000000000000004a <main>:
4a: 55 push %rbp
4b: 48 89 e5 mov %rsp,%rbp
4e: bf 09 00 00 00 mov $0x9,%edi
53: e8 00 00 00 00 callq 58 <main+0xe>
58: 89 c6 mov %eax,%esi
5a: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 61 <main+0x17>
61: e8 00 00 00 00 callq 66 <main+0x1c>
66: 48 89 c2 mov %rax,%rdx
69: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # 70 <main+0x26>
70: 48 89 c6 mov %rax,%rsi
73: 48 89 d7 mov %rdx,%rdi
76: e8 00 00 00 00 callq 7b <main+0x31>
7b: b8 00 00 00 00 mov $0x0,%eax
80: 5d pop %rbp
81: c3 retq
0000000000000082 <_Z41__static_initialization_and_destruction_0ii>:
82: 55 push %rbp
83: 48 89 e5 mov %rsp,%rbp
86: 48 83 ec 10 sub $0x10,%rsp
8a: 89 7d fc mov %edi,-0x4(%rbp)
8d: 89 75 f8 mov %esi,-0x8(%rbp)
90: 83 7d fc 01 cmpl $0x1,-0x4(%rbp)
94: 75 32 jne c8 <_Z41__static_initialization_and_destruction_0ii+0x46>
96: 81 7d f8 ff ff 00 00 cmpl $0xffff,-0x8(%rbp)
9d: 75 29 jne c8 <_Z41__static_initialization_and_destruction_0ii+0x46>
9f: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # a6 <_Z41__static_initialization_and_destruction_0ii+0x24>
a6: e8 00 00 00 00 callq ab <_Z41__static_initialization_and_destruction_0ii+0x29>
ab: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx # b2 <_Z41__static_initialization_and_destruction_0ii+0x30>
b2: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # b9 <_Z41__static_initialization_and_destruction_0ii+0x37>
b9: 48 8b 05 00 00 00 00 mov 0x0(%rip),%rax # c0 <_Z41__static_initialization_and_destruction_0ii+0x3e>
c0: 48 89 c7 mov %rax,%rdi
c3: e8 00 00 00 00 callq c8 <_Z41__static_initialization_and_destruction_0ii+0x46>
c8: 90 nop
c9: c9 leaveq
ca: c3 retq
00000000000000cb <_GLOBAL__sub_I_a>:
cb: 55 push %rbp
cc: 48 89 e5 mov %rsp,%rbp
cf: be ff ff 00 00 mov $0xffff,%esi
d4: bf 01 00 00 00 mov $0x1,%edi
d9: e8 a4 ff ff ff callq 82 <_Z41__static_initialization_and_destruction_0ii>
de: 5d pop %rbp
df: c3 retq
Disassembly of section .text._Z3MAXii:
0000000000000000 <_Z3MAXii>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 89 7d fc mov %edi,-0x4(%rbp)
7: 89 75 f8 mov %esi,-0x8(%rbp)
a: 8b 45 fc mov -0x4(%rbp),%eax
d: 3b 45 f8 cmp -0x8(%rbp),%eax
10: 7e 05 jle 17 <_Z3MAXii+0x17>
12: 8b 45 fc mov -0x4(%rbp),%eax
15: eb 03 jmp 1a <_Z3MAXii+0x1a>
17: 8b 45 f8 mov -0x8(%rbp),%eax
1a: 5d pop %rbp
1b: c3 retq
# test5_1.o elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z3maxi>:
0: 85 ff test %edi,%edi
2: 75 07 jne b <_Z3maxi+0xb>
4: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # a <_Z3maxi+0xa>
a: c3 retq
b: 53 push %rbx
c: 89 fb mov %edi,%ebx
e: 8d 7f ff lea -0x1(%rdi),%edi
11: e8 00 00 00 00 callq 16 <_Z3maxi+0x16>
16: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx # 1d <_Z3maxi+0x1d>
1d: 48 63 fb movslq %ebx,%rdi
20: 39 04 ba cmp %eax,(%rdx,%rdi,4)
23: 0f 4d 04 ba cmovge (%rdx,%rdi,4),%eax
27: 5b pop %rbx
28: c3 retq
0000000000000029 <main>:
29: 48 83 ec 08 sub $0x8,%rsp
2d: bf 09 00 00 00 mov $0x9,%edi
32: e8 00 00 00 00 callq 37 <main+0xe>
37: 89 c6 mov %eax,%esi
39: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 40 <main+0x17>
40: e8 00 00 00 00 callq 45 <main+0x1c>
45: 48 89 c7 mov %rax,%rdi
48: e8 00 00 00 00 callq 4d <main+0x24>
4d: b8 00 00 00 00 mov $0x0,%eax
52: 48 83 c4 08 add $0x8,%rsp
56: c3 retq
0000000000000057 <_GLOBAL__sub_I_a>:
57: 48 83 ec 08 sub $0x8,%rsp
5b: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 62 <_GLOBAL__sub_I_a+0xb>
62: e8 00 00 00 00 callq 67 <_GLOBAL__sub_I_a+0x10>
67: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx # 6e <_GLOBAL__sub_I_a+0x17>
6e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 75 <_GLOBAL__sub_I_a+0x1e>
75: 48 8b 3d 00 00 00 00 mov 0x0(%rip),%rdi # 7c <_GLOBAL__sub_I_a+0x25>
7c: e8 00 00 00 00 callq 81 <_GLOBAL__sub_I_a+0x2a>
81: 48 83 c4 08 add $0x8,%rsp
85: c3 retq
# test5_2.o elf64-x86-64
Disassembly of section .text:
0000000000000000 <_Z3maxi>:
0: 85 ff test %edi,%edi
2: 75 0c jne 10 <_Z3maxi+0x10>
4: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # a <_Z3maxi+0xa>
a: c3 retq
b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
10: 53 push %rbx
11: 89 fb mov %edi,%ebx
13: 8d 7f ff lea -0x1(%rdi),%edi
16: e8 00 00 00 00 callq 1b <_Z3maxi+0x1b>
1b: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx # 22 <_Z3maxi+0x22>
22: 48 63 fb movslq %ebx,%rdi
25: 5b pop %rbx
26: 39 04 ba cmp %eax,(%rdx,%rdi,4)
29: 0f 4d 04 ba cmovge (%rdx,%rdi,4),%eax
2d: c3 retq
Disassembly of section .text.startup:
0000000000000000 <main>:
0: 48 83 ec 08 sub $0x8,%rsp
4: bf 08 00 00 00 mov $0x8,%edi
9: e8 00 00 00 00 callq e <main+0xe>
e: 39 05 00 00 00 00 cmp %eax,0x0(%rip) # 14 <main+0x14>
14: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 1b <main+0x1b>
1b: 0f 4d 05 00 00 00 00 cmovge 0x0(%rip),%eax # 22 <main+0x22>
22: 89 c6 mov %eax,%esi
24: e8 00 00 00 00 callq 29 <main+0x29>
29: 48 89 c7 mov %rax,%rdi
2c: e8 00 00 00 00 callq 31 <main+0x31>
31: 31 c0 xor %eax,%eax
33: 48 83 c4 08 add $0x8,%rsp
37: c3 retq
38: 0f 1f 84 00 00 00 00 nopl 0x0(%rax,%rax,1)
3f: 00
0000000000000040 <_GLOBAL__sub_I_a>:
40: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 47 <_GLOBAL__sub_I_a+0x7>
47: 48 83 ec 08 sub $0x8,%rsp
4b: e8 00 00 00 00 callq 50 <_GLOBAL__sub_I_a+0x10>
50: 48 8b 3d 00 00 00 00 mov 0x0(%rip),%rdi # 57 <_GLOBAL__sub_I_a+0x17>
57: 48 8d 15 00 00 00 00 lea 0x0(%rip),%rdx # 5e <_GLOBAL__sub_I_a+0x1e>
5e: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi # 65 <_GLOBAL__sub_I_a+0x25>
65: 48 83 c4 08 add $0x8,%rsp
69: e9 00 00 00 00 jmpq 6e <_GLOBAL__sub_I_a+0x2e>
内联函数的目的是将函数的代码直接嵌入到调用它的地方,从而避免函数调用的开销。MAX函数是内联的,在优化后的汇编代码中看不到对MAX函数的call指令,因为函数体已经被直接嵌入到使用它的地方。
内联函数和宏定义相比,有类型安全、调试友好、代码易维护、副作用控制、代码优化等优点。
预处理运算符
# 运算符
在函数式宏定义中,#用于将宏参数转换为字符串字面值,预处理器用引号把实参括起来成为一个字符串字面值,并且实参中的连续多个空白字符被替换成一个空格。
#include <iostream>
#include <cstdio> // 包含 FILE 和 fputs 的头文件
// 定义一个宏,将宏参数转化为字符串
#define STR(s) #s
int main() {
const char* str = STR(hello world);
std::cout << str << std::endl;
// 将标准输出流stdout赋值给FILE*类型的指针s, s指向标准输出流
FILE* s = stdout;
const char* output1 = STR(strncmp("ab\"c\0d", "abc", '\4"') == 0);
const char* output2 = STR(: @\n);
// 将output字符串的内容写入到由s指向的流中
fputs(output1, s);
fputs(output2, s);
return 0;
}
## 运算符
#include <iostream>
#define CONCAT(a, b) a##b
void concat() {
std::cout << "concat function called!" << std::endl;
}
int main() {
// 调用 concat 函数
CONCAT(con, cat)();
return 0;
}
可变参数
宏定义中可变参数的部分用__VA_ARGS__表示,在宏展开时和...对应的几个实参可以看成一个实参来替换掉__VA_ARGS__。
#include <stdio.h>
#define showlist(...) printf(__VA_ARGS__)
// report 宏,根据条件打印
#define report(test, ...) ((test) ? printf(#test "\n") : printf(__VA_ARGS__))
void concat() {
printf("concat function called!");
}
int main() {
showlist("The first, second, and third items.\n");
int x = 10, y = 5;
report(x > y, "x is %d but y is %d\n", x, y);
return 0;
}
##__VA_ARGS__
##运算符的作用是,如果可变参数是空的,它会删除多余的逗号。
#include <iostream>
#include <cstdarg> // va_list
#define DEBUGP(format, ...) printk(format, ##__VA_ARGS__)
// 假设这是日志打印函数
void printk(const char* format, ...) {
va_list args;
va_start(args, format); // 初始化 va_list
vprintf(format, args); // 使用 vprintf 输出格式化的日志
va_end(args); // 结束 va_list 的使用
}
int main() {
int errorCode = 404;
// 使用 DEBUGP 宏打印有参数的日志
DEBUGP("error code: %d\n", errorCode);
// 使用 DEBUGP 宏打印没有参数的日志
DEBUGP("operation complete\n");
return 0;
}
内核函数printk类似于printf,也带有格式化字符串和可变参数,由于内核不能调用Iibc的库函数,所以另外实现了这样一个打印函数。