[Linux] gcc/g++使用

GCC(英文全拼:GNU Compiler Collection)是 GNU 工具链的主要组成部分,是一套以 GPL 和 LGPL 许可证发布的程序语言编译器自由软件,由Richard Stallman 于 1985 年开始开发。

GCC 是Linux 下使用最广泛的 C/C++ 编译器,大多数的 Linux 发行版本都默认安装,不管是开发人员还是初学者,一般都将 GCC 作为 Linux 下首选的编译工具。

gccGUN C Compiler)是GCC中的 c 编译器,而g++GUN C++ Compiler)是GCC中的 c++ 编译器。

gccg++ 两者都可以编译ccpp文件,但存在差异。gcc在编译cpp时语法按照c来编译,但默认不能链接到c++的库(gcc默认链接c库,g++默认链接c++库)。
g++编译.c.cpp文件都统一按cpp的语法规则来编译。
所以一般编译c用gcc,编译c++g++

直接使用

gcc main.c # 在 gcc 命令后面紧跟源文件名 

不管源文件的名字是什么,GCC 生成的可执行文件的默认名字始终是a.out

不像 Windows,在 Linux 中不以文件后缀来区分可执行文件,Linux 下的可执行文件后缀理论上可以是任意的,这里的.out只是用来表明它是 gcc 的输出文件

  • -o选项 可以自定义文件名
    gcc main.c -o out/main
    

上面是通过gcc命令一次性完成编译和链接的整个过程,这样最方便,大家在使用的过程中一般都这么做。实际上,gcc命令也可以将编译和链接分开,每次只完成一项任务。

分步骤执行

注意,gcc对于语法错误的检查是在编译阶段进行的。

1. 预处理(Preprocessing)

-E选项 进行宏替换 将源文件执行预处理操作 也即生成*.i文件
gcc编译器将对以#开头的指令进行解析

gcc -E Demo.c -o Demo.i

.i文件可用记事本打开

预处理时会将所有注释都去掉,所以反编译回来的代码是没有注释的!

使用指令gcc -E Demo.c 而不使用-o指定输出的文件名时内容将会直接输出到Dos框中,而不会产生文件

2. 编译(Compiling)

-S选项 生成汇编代码 将 *.i 文件中源码转化为汇编代码 *.s 文件
用记事本打开会发现c源码已经被编译器转化为汇编代码

gcc -S Demo.i -o Demo.s

如果使用指令 gcc -S Demo.i 即不指定输出文件名,默认也将会在当前目录下产生文件Demo.s

也可以直接由 .c 文件生成 .s 汇编文件

gcc -S hello.c

3. 汇编(Assembling)

-c选项 生成机器可识别代码 将源文件编译成目标文件

gcc -c main.c

对于微软编译器(内嵌在 Visual C++ 或者 Visual Studio 中),目标文件的后缀为.obj;对于 GCC 编译器,目标文件的后缀为.o

如果希望自定义目标文件的名字,那么仍然可以使用-o选项

gcc -c main.c -o a.o

也可以由 .i.s 文件生成目标文件 .o

gcc -c hello.i -o hello.o gcc -c hello.s -o hello.o

4. 链接(Linking)

gcc命令后面紧跟目标文件的名字,就可以将目标文件链接成为可执行文件。

gcc命令后面紧跟源文件名字或者目标文件名字都是可以的,gcc命令能够自动识别到底是源文件还是目标文件:如果是源文件,那么要经过编译和链接两个步骤才能生成可执行文件;如果是目标文件,只需要链接就可以了。

gcc main.o #将 main.o 链接为 默认的 a.out

使用-o选项仍然能够自定义可执行文件的名字

gcc main.o -o main.out
  • -I :指定头文件的包含路径。
  • -L :指定链接库的包含路径。

链接库

库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。

库有两种:静态库.a.lib)和动态库.so.dll)。
所谓静态、动态是指链接方式

使用静态库

之所以称为“静态库”,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。

其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。

Linux下使用ar工具、Windows下vs使用lib.exe,将目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索。

静态库特点总结:

  • 静态库对函数库的链接是放在编译时期完成的。
  • 程序在运行时与函数库再无瓜葛,移植方便。
  • 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

将 .c 文件编译成静态库 .a 文件

  1. 生成 .o 文件
  2. 再由 .o 文件 生成 .a 文件
gcc -c foo.c # 生成 foo.o 目标文件
ar rcs libfoo.a foo.o # 生成 libfoo.a 静态库

-static :使用静态链接。

  • 编译 hello.c 并链接静态库 libfoo.a(通过加上 -static 选项)

    gcc hello.c -static libfoo.a -o hello
    
  • 使用 -L 指定库的搜索路径,并使用 -l 指定库名

    gcc hello.c -static -L. -l foo -o hello # -L . 代表搜索当前目录 -l foo指定库名为foo
    

使用共享库

为什么需要动态库,其实也是静态库的问题导致。空间浪费是静态库的一个问题。

另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。

动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入

不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新

-shared :创建共享库/动态库。
由于动态库可以被多个进程共享加载,所以需要使用 -fPIC 选项生成位置无关的代码

gcc foo.c -shared -fPIC -o libfoo.so

编译 hello.c 并链接共享库 libfoo.so

gcc hello.c libfoo.so -o hello

同样可以使用 -L-l 选项指定库的路径和名称

gcc hello.c -L . -l foo -o hello

但是此时直接运行 hello 程序仍然失败 原因是找不到 libfoo.so 共享库
这是因为 libfoo.so 并不在 Linux 系统的默认搜索目录中,解决办法是我们主动告诉系统,libfoo.so 共享库在哪里。

  1. 方式一:设置环境变量 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=$(pwd)
  1. 方式二:使用 rpath 将共享库位置嵌入到程序
gcc hello.c -L. -lfoo -Wl,-rpath=`pwd` -o hello

rpath 即 run path,是种可以将共享库位置嵌入程序中的方法,从而不用依赖于默认位置和环境变量。
这里在链接时使用 -Wl,-rpath=/path/to/yours 选项,-Wl会发送以逗号分隔的选项到链接器,注意逗号分隔符后面没有空格。

  1. 方式三:将 libfoo.so 共享库添加到系统路径
sudo cp libfoo.so /usr/lib/

通过命令ldd可以列出程序的动态依赖

其他选项

  • -D : 在程序编译的时候,指定一个宏
  • -B :将 <directory> 添加到编译器的搜索路径。
  • -save-temps :不用删除中间文件。
  • -save-temps=<arg> :不用删除指定的中间文件。
  • -time :为每个子流程的执行计时。
  • -Xassembler <arg> :将 <arg> 传递给汇编器(assembler)。
  • -Xpreprocessor <arg> :将 <arg> 传递给预处理器(preprocessor)。
  • -Xlinker <arg> :将 <arg> 传递给链接器(linker)。
  • -g : 生成调试信息。GNU 调试器可利用该信息, 否则不能使用GDB进行调试.
  • -O0/1/2/3 : 编译器的优化选项的4个级别,-O0表示没有优化,-O1为缺省值,-O3优化级别最高。
  • -Wall 生成所有警告信息。
  • -MM 输出依赖关系(不包含标准库文件)

gcc编译多个文件

编译这三个文件,可以一次编译:

gcc hello.c main.c -o main # 生成可执行文件main

也可以独立编译:

gcc -Wall -c main.c -o main.o
gcc -Wall -c hello.c -o hello.o
gcc -Wall main.o hello.o -o main

独立编译的好处是,当其中某个模块发送改变时,只需要编译该模块就行,不必重新编译所有文件,这样可以节省编译时间。