GCC基础

GCC基础

GCC简介

  • 早期 GCC 的全拼为 GNU C Compiler,即 GUN 计划诞生的 C 语言编译器,显然最初 GCC 的定位确实只用于编译 C 语言。但经过这些年不断的迭代,GCC 的功能得到了很大的扩展,它不仅可以用来编译 C 语言程序,还可以处理 C++、Go、Objective -C 等多种编译语言编写的程序。与此同时,由于之前的 GNU C Compiler 已经无法完美诠释 GCC 的含义,所以其英文全称被重新定义为 GNU Compiler Collection,即 GNU 编译器套件。

GCC在Linux的安装

查看当前GCC的版本

  • 命令:gcc --version或者gcc -v

快速安装GCC

  • 此处以ubuntu系统的为例
  • 命令:sudo apt-get install gcc
    • 因为已经安装了所以没有安装的过程
  • 注:采用此方式安装的 GCC 编译器,版本通常较低。

编译安装GCC

  • 此方式需要耗费的时间较长(几个小时),但支持安装指定版本的 GCC 编译器,并适用于大多数 Linux 发行版;同时,如果想对已安装的 GCC 编译器进行版本升级,也可以使用此方式。
  • 编译安装 GCC 编译器需要提前到 GCC 官网下载指定版本的 GCC 源码安装包,读者可直接点击GCC源码包进行下载。
  • 安装步骤:
    1. 找到下载好的 gcc-10.2.0.tar.gz 安装包(2020年7月23日更新),将其解压至 /usr/local/ 目录下,命令为:sudo tar -xf gcc-10.1.0.tar.gz -C /usr/local
    2. 紧接着执行如下指令,下载安装 GCC 所需要的依赖包(如 gmp、mpfr、mpc 等):进入/usr/local/gcc-10.2.0目录下 cd /usr/local/gcc-10.2.0,之后执行安装依赖包的命令 ./contrib/download_prerequisites
    3. 在/usr/local目录下手动创建一个目录,用于存放编译 GCC 源码包生成的文件。执行如下命令:创建目录sudo mkdir gcc-build-10.2.0 之后进入gcc-10.2.0目录下cd gcc-10.2.0
    4. 通过执行如下指令,可以配置 GCC 支持编译 C 和 C++ 语言:../gcc-10.2.0/configure --enable-checking=release --enable-languages=c,c++ --disable-multilib
    5. 在第 4 步创建好 makefile 文件之后,接下来就可以使用 make 命令来编译 GCC 源程序:sudo make(此过程十分耗时,本机使用vmware虚拟机,运行内存4GB,用了8个线程make -j8花了35分钟)
    6. 最后在漫长的等待后不要忘了执行如下命令安装 gcc:sudo make install(如果此时直接执行 gcc --version,则 GCC 版本仍会显示之前安装的版本。操作系统重启之后,GCC 版本就会自行更正过来。)
    7. 注意:因为/usr/local所在目录本身需要sudo权限才能访问,所以最好在每个命令前加上sudo;如果某个命令执行不成功也可加上sudo再试一下。

gcc和g++命令的区别

  • 实际使用中我们更习惯使用 gcc 指令编译 C 语言程序,用 g++ 指令编译 C++ 代码。需要强调的一点是,这并不是 gcc 和 g++ 的区别,gcc 指令也可以用来编译 C++ 程序,同样 g++ 指令也可以用于编译 C 语言程序。
  • 只要是 GCC 支持编译的程序代码,都可以使用 gcc 命令完成编译。可以这样理解,gcc 是 GCC 编译器的通用编译指令,因为根据程序文件的后缀名,gcc 指令可以自行判断出当前程序所用编程语言的类别。(gcc 指令也为用户提供了“手动指定代表编译方式”的接口,即使用 -x 选项。例如,gcc -xc xxx 表示以编译 C 语言代码的方式编译 xxx 文件;而 gcc -xc++ xxx 则表示以编译 C++ 代码的方式编译 xxx 文件。)
  • 使用 g++ 指令,则无论目标文件的后缀名是什么,该指令都**一律按照编译 C++**代码的方式编译该文件。
  • 如果想使用 gcc 指令来编译执行 C++ 程序,需要在使用 gcc 指令时,手动为其添加-lstdc++ -shared-libgcc选项,表示 gcc 在编译 C++ 程序时可以链接必要的 C++ 标准库。即g++ 指令就等同于gcc -xc++ -lstdc++ -shared-libgcc指令

指定编译标准

  • 命令:gcc/g++ -std=标准 ...
  • GCC支持的标准一览:

GCC一步编译c/c++程序

  • 编译命令:gcc xx.c -o <指定生成的文件名>
    • 若编译c++ 程序换为g++即可
    • -o 选项后面接上想要GCC在当前目录下生成的可执行文件的文件名,若去掉-o选项则默认在当前目录下生成一个名为a.out的可执行文件
  • 执行命令:在当前目录下./a.out(文件名),即只要在终端输入可执行的文件的全路径(绝对或相对均可)就可运行相应可执行文件。

GCC分布编译c/c++程序

  • 无论是 C 还是 C++ 程序,其从源代码转变为可执行代码的过程,具体可分为 4 个过程,分别为预处理(Preprocessing)、编译(Compilation)、汇编(Assembly)和链接(Linking)。默认情况下,gcc 指令会一气呵成,直接将源代码历经这 4 个过程转变为可执行代码,且不会保留各个阶段产生的中间文件。
  • 如果想查看这 4 个阶段各自产生的中间文件,最简单直接的方式就是对源代码进行“分步编译”,即控制 GCC 编译器逐步对源代码进行预处理、编译、汇编以及链接操作。

预处理

  • 所谓预处理操作,主要是处理那些源文件和头文件中以 # 开头的命令(比如 #include、#define、#ifdef 等),并删除程序中所有的注释 // 和 /* … */。

  • 为 gcc 指令添加 -E 选项,即可控制 GCC 编译器仅对源代码做预处理操作。

    • 默认情况下 gcc -E 指令只会将预处理操作的结果输出到屏幕上,并不会自动保存到某个文件。因此该指令往往会和 -o 选项连用,将结果导入到指令的文件中。
    • Linux 系统中通常用 “.i” 作为 C 语言程序预处理后所得文件的后缀名。
    • 为 gcc 指令再添加一个 -C (大写的C)选项,阻止 GCC 删除源文件和头文件中的注释。
  • gcc -E 常用选项

选 项 功 能
-D name[=definition] 在处理源文件之前,先定义宏 name。宏 name 必须是在源文件和头文件中都没有被定义过的。将该选项搭配源代码中的#ifdef name命令使用,可以实现条件式编译。如果没有指定一个替换的值(即省略 =definition),该宏被定义为值 1。
-U name 如果在命令行或 GCC 默认设置中定义过宏 name,则“取消”name 的定义。-D 和 -U 选项会依据在命令行中出现的先后顺序进行处理。
-include file 如同在源代码中添加 #include “file” 一样。
-iquote directory 对于以引号(#include “”)导入的头文件中,-iquote 指令可以指定该头文件的搜索路径。当 GCC 在源程序所在目录下找不到此头文件时,就会去 -iquote 指令指定的目录中查找。
-isystem dir -idirafter dir 都用于指定搜索头文件的目录,适用于以引号 “” 和 <> 导入的头文件。
  • 注意:其中,对于指定 #include 搜索路径的几个选项,作用的先后顺序如下:
    • 对于用 #include “” 引号形式引入的头文件,首先搜索当前程序文件所在的目录,其次再前往 -iquote 选项指定的目录中查找;
    • 前往 -I 选项指定的目录中搜索;
    • 前往 -isystem 选项指定的目录中搜索;
    • 前往默认的系统路径下搜索;
    • 前往 -idirafter 选项指定的目录中搜索。

编译

  • 编译是整个程序构建的核心部分,也是最复杂的部分之一。所谓编译,简单理解就是将预处理得到的程序代码,经过一系列的词法分析、语法分析、语义分析以及优化,加工为当前机器支持的汇编代码。
  • 通过给 gcc 指令添加 -S(注意是大写)选项,即可令 GCC 编译器仅将指定文件加工至编译阶段,并生成对应的汇编代码文件。
  • 编译操作会自行新建一个文件名和指定文件相同、后缀名为 .s 的文件,并将编译的结果保存在该文件中。
  • 如果需要的话,我们还可以为 gcc -S 指令添加 -o 选项,令 GCC 编译器将编译结果保存在我们指定的文件中。
  • gcc -S 指令操作的文件并非必须是经过预处理后得到的 .i 文件,-S 选项的功能是令 GCC 编译器将指定文件处理至编译阶段结束。这也就意味着,gcc -S 指令可以操作预处理后的 .i 文件,也可以操作源代码文件:
    • 如果操作对象为 .i 文件,则 GCC 编译器只需编译此文件;
    • 如果操作对象为 .c 或者 .cpp 源代码文件,则 GCC 编译器会对其进行预处理和编译这 2 步操作。

汇编

  • 汇编其实就是将汇编代码转换成可以执行的机器指令。大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令。相对于编译操作,汇编过程会简单很多,它并没有复杂的语法,也没有语义,也不需要做指令优化,只需要根据汇编语句和机器指令的对照表一一翻译即可。
  • 通过为 gcc 指令添加 -c 选项(注意是小写字母 c),即可让 GCC 编译器将指定文件加工至汇编阶段,并生成相应的目标文件(文件名与指定的文件相同,只是改成了.o后缀)。
  • 和 gcc -S 类似,gcc -c 选项并非只能用于加工 .s 文件。事实上,-c 选项只是令 GCC 编译器将指定文件加工至汇编阶段,但不执行链接操作。这也就意味着:
    • 如果指定文件为源程序文件(例如 demo.c),则 gcc -c 指令会对 demo.c 文件执行预处理、编译以及汇编这 3 步操作;
    • 如果指定文件为刚刚经过预处理后的文件(例如 demo.i),则 gcc -c 指令对 demo.i 文件执行编译和汇编这 2 步操作;
    • 如果指定文件为刚刚经过编译后的文件(例如 demo.s),则 gcc -c 指令只对 demo.s 文件执行汇编这 1 步操作。
    • 如果文件已经是.o文件,则gcc -c指令不做任何操作。

链接

执行默认链接操作

  • gcc 会根据所给文件的后缀名 .o,自行判断出此类文件为目标文件,仅需要进行链接操作。
  • 命令:gcc xx.o -o xx,若不指定文件名,则默认输出为a.out

手动添加链接库

添加标准链接库目录中的库
  • 链接器把多个二进制的目标文件(object file)链接成一个单独的可执行文件。在链接过程中,它必须把符号(变量名、函数名等一系列标识符)用对应的数据的内存地址(变量地址、函数地址等)替代,以完成程序中多个模块的外部引用。而且,链接器也必须将程序中所用到的所有C标准库函数加入其中。对于链接器而言,链接库不过是一个具有许多目标文件的集合,它们在一个文件中以方便处理。
  • 标准库的大部分函数通常放在文件 libc.a 中(文件名后缀.a代表“achieve”,译为“获取”),或者放在用于共享的动态链接文件 libc.so 中(文件名后缀.so代表“share object”,译为“共享对象”)。这些链接库一般位于 /lib/ 或 /usr/lib/。当使用 GCC 编译和链接程序时,GCC 默认会链接 libc.a 或者 libc.so,但是对于其他的库(例如非标准库、第三方库等),就需要手动添加。
  • GCC 的-l选项(小写的 L)可以让我们手动添加链接库。基本命令格式:gcc xx.c -lxx,前缀lib和后缀.a是标准的,xx是基本名称,GCC 会在-l选项后紧跟着的基本名称的基础上自动添加这些前缀、后缀。
    • 标准头文件 <math.h> 对应的数学库默认也不会被链接,如果没有手动将它添加进来,就会发生函数未定义错误。
    • 数学库的文件名是 libm.a。前缀lib和后缀.a是标准的,m是基本名称,GCC 会在-l选项后紧跟着的基本名称的基础上自动添加这些前缀、后缀,本例中,基本名称为 m。math.c代码如下:
    #include <stdio.h>      /* printf */
    #include <math.h>       /* cos */
    #define PI 3.14159265
    int main ()
    {
      double param, result;
      param = 60.0;
      result = cos ( param * PI / 180.0 );
      printf ("The cosine of %f degrees is %f.\n", param, result );
      return 0;
    }
    • 又例如Linux的多线程程序:因为pthread的库不是linux系统的库,所以在进行编译的时候要加上-lpthread,否则编译不过。
添加其它目录中的库
  • 通常,GCC 会自动在标准库目录中搜索文件,例如 /usr/lib,如果想链接其它目录中的库,就得特别指明。
  1. 法一:把链接库作为一般的目标文件,为 GCC 指定该链接库的完整路径与文件名。
    • 例如,如果链接库名为 libm.a,并且位于 /usr/lib 目录,那么下面的命令会让 GCC 编译 main.c,然后将 libm.a 链接到 main.o:gcc main.c -o main.out /usr/lib/libm.a
  2. 法二:使用-L选项,为 GCC 增加另一个搜索链接库的目录
    • gcc main.c -o main.out -L/usr/lib -lm -L后接目录,但还是要有-l选项。另外:可以使用多个-L选项,或者在一个-L选项内使用冒号分割的路径列表。
  3. 法三:把包括所需链接库的目录加到环境变量 LIBRARYPATH 中。

GCC使用静态链接库和动态链接库

  • 库文件只是一个统称,代指的是一类压缩包,它们都包含有功能实用的目标文件。虽然库文件用于程序的链接阶段,但编译器提供有 2 种实现链接的方式,分别称为静态链接方式和动态链接方式,其中采用静态链接方式实现链接操作的库文件,称为静态链接库;采用动态链接方式实现链接操作的库文件,称为动态链接库。
  • GCC 编译器生成可执行文件时,默认情况下会优先使用动态链接库实现链接操作,除非当前系统环境中没有程序文件所需要的动态链接库,GCC 编译器才会选择相应的静态链接库。如果两种都没有(或者 GCC 编译器未找到),则链接失败。
  • 在 Linux 发行版中,静态链接库和动态链接库通常存放在 /usr/bin 或者 /bin 目录下。
静态链接库
  • 静态链接库实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,GCC 编译器就会将该模板代码直接复制到程序文件的适当位置,最终生成可执行文件。
  • 静态链接库的优劣:
    • 优势是,生成的可执行文件不再需要任何静态库文件的支持就可以独立运行(可移植性强)。
    • 劣势是,如果程序文件中多次调用库中的同一功能模块,则该模块代码势必就会被复制多次,生成的可执行文件中会包含多段完全相同的代码,造成代码的冗余。和使用动态链接库生成的可执行文件相比,静态链接库生成的可执行文件的体积更大。
  • 静态链接库的后缀名:在 Linux 发行版系统中,静态链接库文件的后缀名通常用 .a 表示;在 Windows 系统中,静态链接库文件的后缀名为 .lib。
动态链接库
  • 动态链接库,又称为共享链接库。和静态链接库不同,采用动态链接库实现链接操作时,程序文件中哪里需要库文件的功能模块,GCC 编译器不会直接将该功能模块的代码拷贝到文件中,而是将功能模块的位置信息记录到文件中,直接生成可执行文件。显然,这样生成的可执行文件是无法独立运行的。采用动态链接库生成的可执行文件运行时,GCC 编译器会将对应的动态链接库一同加载在内存中,由于可执行文件中事先记录了所需功能模块的位置信息,所以在现有动态链接库的支持下,也可以成功运行。
  • 动态链接库的优劣:
    • 优势是,由于可执行文件中记录的是功能模块的地址,真正的实现代码会在程序运行时被载入内存,这意味着,即便功能模块被调用多次,使用的都是同一份实现代码(这也是将动态链接库称为共享链接库的原因)。和使用静态链接库生成的可执行文件相比,动态链接库生成的可执行文件的体积更小,因为其内部不会被复制一堆冗余的代码。
    • 劣势是,此方式生成的可执行文件无法独立运行,必须借助相应的库文件(可移植性差)。
  • 动态链接库的后缀名:在 Linux 发行版系统中,动态链接库的后缀名通常用 .so 表示;在 Windows 系统中,动态链接库的后缀名为 .dll(经常看见.dll文件,搞了半天原来是你)。

GCC创建链接库

  • 仅希望别人使用我们实现的功能,但又不想它看到具体实现的源码,就可以使用创建链接库的办法。
GCC创建静态链接库
  • 并非任何一个源文件都可以被加工成静态链接库,其至少需要满足以下 2 个条件:
    • 源文件中只提供可以重复使用的代码,例如函数、设计好的类等,不能包含 main 主函数
    • 源文件在实现具备模块功能的同时,还要提供访问它的接口,也就是包含各个功能模块声明部分的头文件
  • 将源文件打包为静态链接库的过程很简单,只需经历以下 2 个步骤:
    1. 将所有指定的源文件,都编译成相应的目标文件;
    2. 然后使用 ar 压缩指令,将生成的目标文件打包成静态链接库,其基本格式如下:
      ar rcs 静态链接库名称 目标文件1 目标文件2 ...
  • 静态链接库命名规则:libxxx.a
    • Linux 系统下,静态链接库的后缀名为 .a;Windows 系统下,静态链接库的后缀名为 .lib。
    • 其中,xxx 代指我们为该库起的名字,比如 Linux 系统自带的一些静态链接库名称为 libc.a、libgcc.a、libm.a,它们的名称分别为 c、gcc 和 m。
  • 静态链接库的使用很简单,就是在程序的链接阶段,将静态链接库和其他目标文件一起执行链接操作,从而生成可执行文件。gcc -static 目标文件 链接库名,其中,-static 选项强制 GCC 编译器使用静态链接库。
GCC创建动态链接库
  • 法一:

    • 直接使用源文件创建动态链接库,采用 gcc 命令实现的基本格式如下:gcc -fpic -shared 源文件名... -o 动态链接库名,其中,-shared 选项用于生成动态链接库;-fpic(还可写成 -fPIC)选项的功能是,令 GCC 编译器生成动态链接库(多个目标文件的压缩包)时,表示各目标文件中函数、类等功能模块的地址使用相对地址,而非绝对地址。这样,无论将来链接库被加载到内存的什么位置,都可以正常使用。
  • 法二:

    • 先使用 gcc -c 指令将指定源文件编译为目标文件。
    • 注意,为了后续生成动态链接库并能正常使用,将源文件编译为目标文件时,也需要使用 -fpic 选项。
    • 在此基础上,接下来利用上一步生成的目标文件,生成动态链接库:gcc -shared 目标文件名1 目标文件名2 ... 动态链接库名
  • 动态链接库的使用场景就是和项目中其它源文件或目标文件一起参与链接。

    • 命令:gcc 源文件名 动态链接库名
    • 运行由动态链接库生成的可执行文件时,必须确保程序在运行时可以找到这个动态链接库。常用的解决方案有如下几种:
    • 将链接库文件移动到标准库目录下(例如 /usr/lib、/usr/lib64、/lib、/lib64);
    • 在终端输入export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx,其中 xxx 为动态链接库文件的绝对存储路径(此方式仅在当前终端有效,关闭终端后无效);
    • 修改~/.bashrc 或~/.bash_profile 文件,即在文件最后一行添加export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx(xxx 为动态库文件的绝对存储路径)。保存之后,执行source .bashrc指令(此方式仅对当前登陆用户有效)。

gcc指令一次处理多个文件

  • 以下这些操作都可以共用一条 gcc 指令:

    • 将多个 C(C++)源文件加工为汇编文件或者目标文件;
    • 将多个 C(C++)源文件或者预处理文件加工为汇编文件或者目标文件;
    • 将多个 C(C++)源文件、预处理文件或者汇编文件加工为目标文件;
    • 同一项目中,不同的源文件、预处理文件、汇编文件以及目标文件,可以使用一条 gcc 指令,最终生成一个可执行文件。
  • 处理多个文件的三种方法:

    • 法一:先单独编译各个源文件,再将它们链接起来。
    • 法二:同时编译多个源文件,再将它们链接起来;
    • 法三:同时直接一步编译多个源文件生成一个可执行文件。
  • 进入该项目目录,用 *.c 表示所有的源文件,即执行如下指令 gcc *.c -o a.out可同时编译该目录下所有源文件。