简单地说,一个编译器就是一个程序,它可以阅读以某一种语言(源语言)编写的程序,并把该程序翻译成一个等价的、用另一种语言(目标语言)编写的程序。
C/C++编译系统将一个程序转化为可执行程序的过程包含:
- 预处理(preprocessing):根据已放置的文件中的预处理指令来修改源文件的内容。
- 编译(compilation):通过词法分析和语法分析,在确认所有指令都是符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
- 汇编(assembly):把汇编语言代码翻译成目标机器指令的过程。
- 链接(linking):找到所有用到的函数所在的目标文件,并把它们链接在一起合成为可执行文件(executable file)。
整个过程如下图所示:
预处理器是在程序源文件被编译之前根据预处理指令对程序源文件进行处理的程序。预处理器指令以#号开头标识,末尾不包含分号。预处理命令不是C/C++语言本身的组成部分,不能直接对它们进行编译和链接。C/C++语言的一个重要功能是可以使用预处理指令和具有预处理的功能。C/C++提供的预处理功能主要有文件包含、宏替换、条件编译等。
头文件是一种文本文件,使用文本编辑器将代码编写好之后,以扩展名.h(.hpp)保存就行了。头文件中一般放一些重复使用的代码,例如函数声明、变量声明、常数定义、宏的定义等等。当使用预处理指令#include
引用头文件时,相当于将头文件中所有内容,复制到include处。
那么编译器在哪里找到include的头文件呢?这就涉及到了 include 的搜索机制。
首先 include 有两种形式(What is the difference between #include and #include “filename”?):
#include <xxx.h> // 直接到系统目录中去找某些头文件。
#include "xxx.h" // 先到源文件所在文件夹去找,然后再到系统目录中去找某些头文件。
尖括号形式表示被包含的文件在某些系统目录中。双引号形式中可以指出文件路径和文件名,如果在双引号中没有给出绝对路径,则默认为用户当前目录中的文件,此时系统首先在用户当前目录中寻找要包含的文件,若找不到再在系统目录中查找。如果双引号中给出绝对路径,则按照路径查找。这里的系统目录,一般是:
/usr/include
/usr/local/include
/usr/lib/gcc-lib/i386-linux/2.95.2/include
最后一行是gcc程序的库文件地址,各个用户的系统上可能不一样。对于gcc来说,还可以用环境变量C_INCLUDE_PATH (对应C头文件),CPLUS_INCLUDE_PATH (对应C++头文件),CPATH(对应C,C++头文件)的值来指定头文件搜索目录。(How to add a default include path for gcc in linux?)
此外,使用 gcc 时,可以用 -I directory
来指定头文件搜索路径,如果指定路径有多个时,则按照指定路径的顺序搜索头文件。
对于用户自己编写的头文件,宜用双引号形式。对于系统提供的头文件,既可以用尖括号形式,也可以用双引号形式,它们都能找到被包含的文件,但显然用尖括号形式更直截了当,效率更高。
宏定义
:一般用一个短的名字代表一个长的代码序列。宏定义包括无参数宏定义和带参数宏定义两类。宏名和宏参数所代表的代码序列可以是任何意义的内容,如类型、常量、变量、操作符、表达式、语句、函数、代码块等。
宏定义在源文件中必须单独另起一行,换行符是宏定义的结束标志,因此宏定义以换行结束,不需要分号等符号作分隔符。如果一个宏定义中代码序列太长,一行不够时,可采用续行的方法。续行是在键入回车符之前先键入符号\,注意回车要紧接在符号\之后,中间不能插入其它符号,当然代码序列最后一行结束时不能有\。
预处理器在处理宏定义时,会对宏进行展开(即宏替换
)。宏替换首先将源文件中在宏定义随后所有出现的宏名均用其所代表的代码序列替换之,如果是带参数宏则接着将代码序列中的宏形参名替换为宏实参名。宏替换只作代码字符序列的替换工作,不作任何语法的检查,也不作任何的中间计算,一切其它操作都要在替换完后才能进行。如果宏定义不当,错误要到预处理之后的编译阶段才能发现。
一般情况下,在进行编译时对源程序中的每一行都要编译,但是有时希望程序中某一部分内容只在满足一定条件时才进行编译,如果不满足这个条件,就不编译这部分内容,这就是条件编译
。
条件编译主要是进行编译时进行有选择的挑选,注释掉一些指定的代码,以达到多个版本控制、防止对文件重复包含的功能。if, #ifndef, #ifdef, #else, #elif, #endif是比较常见条件编译预处理指令,可根据表达式的值或某个特定宏是否被定义来确定编译条件。
此外,还有 #pragma 指令,它的作用是设定编译器的状态或指示编译器完成一些特定的动作。
编译过程的第一个步骤称为词法分析(lexical analysis)或扫描(scanning),词法分析器读入组成源程序的字符流,并且将它们组织成有意义的词素的序列,对于每个词素,词法分析器产生一个词法单元(token),传给下一个步骤:语法分析。
语法分析(syntax analysis)或解析(parsing)是编译的第二个步骤,使用词法单元来创建树形的中间表示,该中间表示给出了词法分析产生的词法单元流的语法结构。一个常用的表示方法是语法树(syntax tree),树中每个内部结点表示一个运算,而该结点的子结点表示该运算的分量。
接下来是语义分析(semantic analyzer),使用语法树和符号表中的信息来检测源程序是否和语言定义的语义一致。
在源程序的语法分析和语义分析之后,生成一个明确的低级的或者类机器语言的中间表示。接下来一般会有一个机器无关的代码优化步骤,试图改进中间代码,以便生成更好的目标代码。
对于被翻译系统处理的每一个C/C++语言源程序,都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标机器语言代码。目标文件由段组成,通常一个目标文件中至少有两个段:代码段和数据段。
- 代码段:该段中所包含的主要是程序的指令。该段一般是可读和可执行的,但一般却不可写。
- 数据段:主要存放程序中要用到的各种全局变量或静态的数据。一般数据段都是可读,可写,可执行的。
链接程序的主要工作就是将有关的目标文件彼此相连接,也即将在一个文件中引用的符号同该符号在另外一个文件中的定义连接起来,使得所有的这些目标文件成为一个能够按操作系统装入执行的统一整体。主要有静态链接和动态链接两种方式:
静态链接
:在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中,程序运行的时候不再需要静态库文件。动态链接
:把调用的函数所在文件模块(DLL)和调用函数在文件中的位置等信息链接进目标程序,程序运行的时候再从DLL中寻找相应函数代码,因此需要相应DLL文件的支持。
这里的库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll),所谓静态、动态是指链接方式的不同。要注意静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
静态链接库与动态链接库都是共享代码的方式。静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:
- 静态库对函数库的链接是放在编译时期完成的,运行时不会再进行链接。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
- 静态库对程序的更新、部署和发布也会带来麻烦。如果静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户。
Linux创建静态库过程如下(大一点的项目会编写makefile文件来生成静态库,输入多个命令太麻烦了):
- 将代码文件编译成目标文件.o;
- 通过ar工具将目标文件打包成.a静态库文件;
整个过程如下图:
Linux下使用ar
命令将目标文件压缩到一起创建静态库,并且对其进行编号和索引,以便于查找和检索。静态库命名一般是"libxxx.a":lib为前缀,中间是静态库名,扩展名为.a
。此外,还可以使用ar查看其中的目标文件,如下:
$ ar -crv libadd.a binbuf.c.o base64.c.o add.o
$ ar -t libadd.a
add.o
base64.c.o
binbuf.c.o
静态库已经达到代码复用的目的,并且容易使用和理解,那为什么还需要动态库呢?因为静态库有着以下的缺点:
- 空间浪费。静态库在内存中可能存在多份拷贝,因为每一个使用了静态库的可执行文件都单独需要在内存中放一份静态库。
- 静态库对程序的更新、部署和发布会带来麻烦。如果静态库liba.q 更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
动态库在程序编译时并不会被链接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布带来的麻烦。用户只需要更新动态库即可,增量更新。
动态库特点总结:
- 动态库把对库函数的链接载入推迟到程序运行的时期。
- 可以实现进程之间的资源共享。(因此动态库也称为共享库)
- 将一些程序升级变得简单。
- 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
Linux 下动态链接库的名字形式为 libxxx.so.*
,前缀是lib,名字是xxx,后缀名为“.so.*”。与创建静态库不同的是,不需要打包工具(ar、lib.exe),直接使用编译器即可创建动态库。简单示例如下:
$ g++ -fPIC -shared -o libdynmath.so DynamicMath.cpp
Linux下可以用nm命令查看动态库包含的目标文件,此外还可以用 ldd 命令来查看一个可执行文件需要哪些动态库。
要想使用库文件,必须在链接时告诉编译器,库文件的名字是什么?在哪里找到库文件?以静态还是动态的方式链接库文件?
在生成静态库和动态库时提到过,库名字必须是libxxx.a或者libxxx.so.x的形式。当使用gcc编译链接时,必须用-lxxx
说明用到的动态库或静态库的名字。此外,还需要使用-Ldir
来指明库的查找路径。默认情况下使用动态方式链接,这要求路径中存在相应的.so动态库文件,如果不存在,则寻找相应的.a静态库文件。也可以在编译时向gcc传入-static
选项,指定使用静态方式链接。
对于共享库来说,除了在链接时指明库的路径,在运行前也必须知道动态库的位置。可以通过下面三种方法在运行前来告诉可执行文件搜索动态库的位置:
- 将动态库的路径添加到名为 LD_LIBRARY_PATH 的环境变量;
- 把库拷贝到 /usr/lib 和 /lib目录下;
- 修改
/etc/ld.so.conf
文件,把动态库所在的路径加到文件末尾,并执行sudo ldconfig
刷新。这样,加入的目录下的所有库文件对于可执行程序都是可见的;
下面是一个保存在文件 helloworld.cpp 中一个简单的 C++ 程序的代码:
/* helloworld.cpp */
#include <iostream>
int main(int argc,char *argv[])
{
std::cout << "hello, world" << std::endl;
return 0;
}
用下面命令编译:
$ g++ helloworld.cpp
编译器 g++ 通过检查命令行中指定的文件的后缀名可识别其为 C++ 源代码文件。编译器默认的动作:编译源代码文件生成对象文件(object file),链接对象文件和 libstd c++ 库中的函数得到可执行程序,然后删除对象文件。由于命令行中未指定可执行程序的文件名,编译器采用默认的 a.out。
选项 -E 使 g++ 将源代码用编译预处理器处理后不再执行其他动作。下面的命令预处理源码文件 helloworld.cpp,并将结果保存在 .ii 文件中:
➜ ~ g++ -E helloworld.cpp -o helloworld.ii
➜ ~ ls | grep helloworld
helloworld.cpp
helloworld.ii
➜ ~ wc -l helloworld.ii
38126 helloworld.ii
helloworld.cpp 的源代码,仅仅有六行,而且该程序除了显示一行文字外什么都不做,但是,预处理后的版本将超过3万行。这主要是因为头文件 iostream 被包含进来,而且它又包含了其他的头文件,除此之外,还有若干个处理输入和输出的类的定义。
选项 -S 指示编译器将程序编译成汇编代码,输出汇编语言代码而后结束。下面的命令将由 C++ 源码文件生成汇编语言文件 helloworld.s,生成的汇编语言依赖于编译器的目标平台。
g++ -S helloworld.cpp
选项 -c 用来告诉编译器将汇编代码(.s文件,或者直接对源代码)转换为目标文件,但不要执行链接。输出结果为对象文件,文件默认名与源码文件名相同,只是将其后缀变为 .o。
➜ ~ g++ -c helloworld.s
➜ ~ ls |grep helloworld.o
helloworld.o
加载相应的库,执行链接操作,将对象文件(.o,也可以直接将原文件)转化成可执行程序。
➜ ~ g++ helloworld.o -o helloworld.o
➜ ~ ./helloworld.o
hello, world
ldconfig命令的用途主要是在默认搜寻目录/lib
,/usr/lib
以及动态库配置文件/etc/ld.so.conf
内所列的目录下,搜索出可共享的动态链接库(格式如lib*.so*
),进而创建出动态装入程序(ld.so)所需的缓存文件。缓存文件默认为/etc/ld.so.cache
,此文件保存已排好序的动态链接库名字列表。
ldconfig通常在系统启动时运行,当用户安装了一个新的动态链接库时,就需要手工运行这个命令。
需要注意的地方:
- 往/lib和/usr/lib里面加东西,是不用修改/etc/ld.so.conf的,但是完了之后要用ldconfig重新生成cache,不然这个library会找不到。
- 往上面两个目录以外加东西的时候,一定要修改/etc/ld.so.conf,然后再调用ldconfig,不然也会找不到。
- 如果想在这两个目录以外放lib,但是又不想在/etc/ld.so.conf中加东西(或者是没有权限加东西),需要将路径添加到环境变量
LD_LIBRARY_PATH
中。一般来讲这只是一种临时的解决方案,在没有权限或临时需要的时候使用。 - ldconfig做的这些东西与运行程序时有关,跟编译一点关系都没有,编译的时候还是需要加-L。
Program Library HOWTO
详解C/C++预处理器
Compiling Cpp
C++静态库与动态库
高级语言的编译:链接及装载过程介绍
编译原理 (预处理>编译>汇编>链接)
帮 C/C++ 程序员彻底了解链接器
链接库以及编译过程
为什么不能在动态库里静态链接?
What is the correct syntax to add CFLAGS and LDFLAGS to “configure”?