记录C++语言相关的基础知识
1 C++源码到可执行文件的四个阶段
预处理(.i)、编译(.s)、汇编(.obj)、链接。
1.1 预处理
预处理阶段,主要完成宏替换、文件展开、注释删除、条件编译展开、添加行号和文件名标识,输出.i/.ii预处理文件。
- 宏替换,对于使用#define定义的值直接进行文本替换
- 文件展开,对于#include h文件,直接对包含的文件进行展开
- 删除注释信息。保留#pragma 编译器指令。
- 根据条件编译参数进行条件编译的展开
- 添加行号和文件名标识
例如g++ -E compiler_test.cc -o compiler.i -DUSER_DEBUG=ON
, 可以指定只生成预处理后的文件。图中省略了#include<stdio.h>即对应的预处理后文件内容。
1.2 编译
对预处理文件进行词法、语法、语义分析和优化,产生对应的汇编代码,是构建过程中最复杂的部分。编译参数中-O选项就是在这一步生效的。
- 词法分析, 将源代码通过扫描器分析出源代码中的每一部分分别对应的类型是什么(tokens),例如数字、括号、运算符、赋值等。
- 语法分析,根据tokens进行语法分析,生成语法树(表达式组成的树,运算优先级会在此时确定, 源代码级别的优化也会在此时进行)。
- 语义分析,编译阶段进行的是静态语义分析,也就是检查语法树的类型是否匹配、进行类型转换、表达式是否有意义,语法树在这个阶段会被标记上语义。
- 中间语言生成, 在语法树上对代码直接进行优化比较困难, 源代码优化器往往是把整个语法树转化成中间代码(与运行环境无关)。中间代码的出现将整个编译过程分成了两部分,前端部分将源代码转化成机器无关的中间代码,后端部分将后端代码转化为目标机器代码。
- 目标代码生成与优化,通过代码生成器将中间语言转化为汇编代码,代码优化器针对汇编代码进行优化。
g++ -S compiler.i -o compiler.s
1.3 汇编
将汇编代码转化为机器可以执行的指令,生成目标文件(.o, .obj)。这个过程就是汇编器对汇编语言的翻译过程。
g++ -C compiler.s add.s -o compiler.o
1.4 链接
C++的每个源代码是独立编译的,会存在不同的目标文件相互引用的情况,这个时候目标文件中的符号(变量、函数等)还没有真实的地址(这个阶段的地址取的是0),经过链接将不同的目标文件进行组合。主要是进行地址和空间分配、地址绑定、重定位, 将目标文件转化为可执行文件。
2 模板为什么通常不是分开声明和实现
关键一点是模板的实例化是要在编译单元内看到了模板实例化定义,而编译单元是每个cpp文件,如此一来在模板的cpp文件中,由于没有使用到模板,所以在编译模板时根本不会实例化。在其他cc文件中引用时,在链接阶段就会报错。而在模板cc文件中实现模板定义
template <typename T> T Add(const T &left, const T &right) {return left + right;
}
template int Add(const int&, const int&);
通常是不合适的,首先是不知道用户到底会实例化什么类型,其次代码很繁琐,所以一般都是在h文件中声明和实现。
3 常用的编译工具
gcc/g++、 automake、cmake、bazel