2.GCC
2.1 GNU
要提到GCC就避免不了谈到GNU。
GNU(GNU is not UNIX),在UNIX诞生之后,很多利益和非利益集团都对其投入了研究和使用,从而导致了软件的利益纷争和版权问题,很多程序的源代码开始对用户不可见。
在当时软件闭源大势所趋的态势下,开源之父Richard. M. Stallman逆流而行,提出开放源码的概念(Open Source),提倡大家共享自己的程序,让很多人参与校验,并在不同的平台进行测试,从而能够完善程序本身。并与1984年创立了GNU项目与自由软件基金会,目标是创建一套完全自由的操作系统以及相关的环境组件。
GNU项目迄今为止开发了许多功能强大的软件,其中就包括GCC,还有Emacs等工具。
虽然GNU项目自身的内核hurd尚未成熟,而且目前看来,作为可以替代的GNU/Linux(即采用Linux内核,以及GNU软件—遵循GPL的软件)更为深入人心,这也大大的阻扰了hurd的发展,抛开这些不提,GNU的开源思想,现今依然影响着当今的软件行业,开源作为经营软件的主要方式之一,更为促进软件本身和整个行业的蓬勃发展。
言归正传,前面提到了GNU项目下催生了许多功能强大的软件,其中GCC就是GNU大家族中最为举足轻重的软件之一。
2.2 GCC
GCC(GNU Compiler Collection) 是GNU项目推出的功能强大且性能优越的跨平台编译器,可以在多种硬件平台上编译出可执行程序,平均效率极为优异。它并不是一款单一语言的编译器(尽管GCC一出生的时候是GNU C Collection的缩写,只能对C语言进行编译,后来经过扩大才成为一个编译套装),它包括了C、C++、Objective-C、Fortran、Java、Ada、Go语言等也包括了这些语言的库(如libstdc++、libgcj等等)。
GCC遵循GPL许可证,作为GNU项目的官方编译器,现已被大多数类Unix操作系统(如Linux、BSD、Mac OS X等)采纳为标准的编译器,GCC同样也可以在Windows上下载使用。
了解了GCC之后,来看看它对C++程序的编译过程。
2.3 g++以及其对C++程序的编译过程
g++是GCC中专门的C++编译器,尽管gcc和g++都能对C++进行编译(我们使用大写的GCC代表整个编译套件,小写的gcc作为GCC中C语言编译器),但是我们编译C++文件的最优选择还是更为专业的g++编译器,因为gcc可能会在处理C++标准库以及一些C++特性时出现一些意料之外的问题。所以我们这里主要介绍g++编译器。
源文件并非经过编译产生可执行程序,而是经过一个编译过程,我们使用GCC直接产生可执行程序,也可能通过一些命令来逐步呈现这个编译过程。
作为示例,我们在文件中有一个test.cpp文件,其中代码如下:
#include <iostream>
using namespace std;
#define MyWord "To be or not to be"
int main(int argc,char** argv)
{
cout<< MyWord<<endl;
return 0;
}
- 预编译:
预编译就是对原程序中的伪指令(以#开头,比如宏定义、条件编译和头文件包含等)和特殊符号进行处理的过程,这个过程中,会将宏定义(#define)删除并展开;处理所有的条件编译;处理引入指令,将被包含的文件插入该预编译指令的位置;删除所有注释同时添加行号;但是会保留#pragma编译器指令;
GCC的预编译指令是 -E,产生.i文件,我们在命令行里输入命令:
g++ -E test.cpp -o test.i
其中-o选项是指定要生成的结果文件,其后跟随的是结果文件名;
执行该命令后,在当前目录生成了一个test.i文件,可以打开该文件查看,变成了一个两万八千多行的文本,其中大部分内容是关于头文件引入和命名空间等,拉到最后可以看到我们的代码,并且可以看出宏命令被删除且定义被展开:
//... ... 省略前面两万多行 ... ...
# 2 "test.cpp" 2
# 2 "test.cpp"
using namespace std;
int main(int argc,char** argc)
{
cout<< "To be or not to be"<<endl;
return 0;
}
- 编译
编译过程就是把预处理完的文件进行词法-语法-语义层面的分析(熟悉编译原理的应该比较清楚)并且做出相应优化后生成汇编代码文件。和预处理一样,如果我们不做选项,直接使用GCC不会生成中间汇编文件,但是为了复现整个编译流程我们在命令行中输入:
g++ -S test.i -o test_i_s.s
编译之后生成test_i_s.s汇编代码,打开文件就能看到熟悉的汇编指令;另外我们之所以将输出汇编文件命名为test_i_s是表示它是由源 —— 预编译文件 —— 到汇编文件的;同样的,我们可以直接使用’-S’指令直接从源文件产生汇编代码:
g++ -E g++ -S test.cpp -o test.s
- 汇编
汇编就是将汇编代码转变成机器可以执行的二进制代码,其中每一条汇编指令都对应着一条机器指令。汇编的过程比编译过程简单许多,主要工作是根据汇编指令与机器指令的对照表一一翻译即可。
g++汇编选项是 -C 生成 .s文件:
g++ -C test_i_s.s -o test_i_s_o.o
-C选项生成的.O文件是二进制代码,我们可以用hexdump工具查看:
hexdump test_i_s_o.o
结果为:
0000000 457f 464c 0102 0001 0000 0000 0000 0000
0000010 0003 003e 0001 0000 07b0 0000 0000 0000
0000020 0040 0000 0000 0000 1b98 0000 0000 0000
... ... ... 一大堆 ... ... ...
另外,-C选项和-S也可以直接作用与源文件,这里不再赘述。
- 链接
汇编后,这一堆机器指令还不能直接执行,还需要一个链接阶段。链接主要是为了解决多个文件之间符号引用的问题。预处理器、编译器和汇编器主要处理的是单个文件,但是如果这个文件中需要引用到其他的文件中的内容(如函数或变量)的话,不经过链接是无法确定引用的内容的。只有链接器将所有的目标文件都生成后,g++在内部调用链接器来完成连接工作后,才能最终生成可执行程序。
指的注意的是,链接的过程,对于g++的用户来说是透明的,我们只需要将相互依赖的文件交给g++它会自动完成链接。
这里为了做示例,我们将前面的示例程序分开:
首先我们创建一个ntest.h
#include <iostream>
using namespace std;
#define MyWord "To be or not to be"
再来一个ntest.cpp
#include "ntest.h"
int main(int argc,char** argv)
{
cout<< MyWord<<endl;
return 0;
}
而后,我们直接使用g++并且传入这两个命令:
g++ ntest.cpp ntest.h -o test
g++自动调用链接器解决了符号引用问题。当然这里我们也可以将ntest.h和ntest.cpp一路 -E -> -S -> -C 到.o文件,再将两个.o文件传递给g++也能完成可执行程序的整个编译(当然也可以是两个.s文件或者是.i文件,甚至可以随意组合以上三种文件)。
2.4 g++其他常用选项
除了以上编译流程中的指令外,g++还有一些其他常用选项:
- I选项:如果源文件和其引入的头文件分离,可是使用"-I PATH"来指定头文件的搜索路径。
- include选项:我们习惯与在源文件中include引入头文件,实际上在使用g++编译时也可以使用include选项进行引入头文件,还是前面的例子,我们将ntest.cpp改写成:
int main(int argc,char** argv)
{
cout<< MyWord<<endl;
return 0;
}
~
我们在源代码中没有引入头文件,但是在相同目录下头文件ntest.h存在(否则要用-i指定路径),我们在命令行中使用:
g++ ntest.cpp -include ntest.h
依然能够完成编译。因此,有些代码中,虽然没有使用include语句引入头文件,但是其中仍然使用了头文件中的一些变量或者声明,那么就是在编译过程中使用include选项引入的。
-
g选项:选项g产生的是gdb可以调试的可执行文件,其中包含了gdb调试器进行调试的必要信息,gdb是C++开发过程中重要的调试器,后面会提到。
-
l选项:可以用来链接动态库(这个后面会专门写)。