GCC是什么?

GCC官网:http://gcc.gnu.org/

GCC 编译器是 Linux 系统下最常用的 C/C++ 编译器,大部分 Linux 发行版中都会默认安装。

对于 GCC 的认知,很多读者还仅停留在“GCC 是一个C语言编译器”的层面,是很片面的。

谈到 GCC,就不得不提 GNU 计划。GNU 全称 GNU’s Not UNIX,又被称为“革奴计划”,由理查德·斯托曼于 1983 年发起。GNU 计划的最终目标是打造出一套完全自由(即自由使用、自由更改、自由发布)、开源的操作系统,并初步将其命名为 GNU 操作系统(其 logo 如图 1 所示)。

GNU 计划的实施可谓一波三折,最重要的一点是,虽然该计划为 GNU 操作系统量身定做了名为 Thr Hurd 的系统内核,但由于其性能比不上同时期诞生的 Linux 内核,最终 GNU 计划放弃 The Hurd 而选用 Linux 作为 GNU 操作系统的内核。在 Linux 内核的基础上,GNU 计划开发了很多系统部件,GCC 就是其中之一(除此之外,还有 Emacs 等非常实用的软件)。

由此看来,GNU 计划最终实现了“打造一套自由、开源的操作系统”的初衷,但该操作系统并非完全产自 GNU 计划,因此其被称为 GNU/Linux 操作系统(人们更习惯称为 Linux 操作系统)。注意,开源、自由并不等于免费。

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

所谓编译器,可以简单地将其理解为“翻译器”。要知道,计算机只认识二进制指令(仅有 0 和 1 组成的指令),我们日常编写的 C 语言代码、C++ 代码、Go 代码等,计算机根本无法识别,只有将程序中的每条语句翻译成对应的二进制指令,计算机才能执行。

GCC 编译器从而停止过改进。截止到2020 年 5 月,GCC 已经从最初的 1.0 版本发展到了 10.1 版本,期间历经了上百个版本的迭代。作为一款最受欢迎的编译器,GCC 被移植到数以千计的硬件/软件平台上,几乎所有的 Linux 发行版也都默认安装有 GCC 编译器。

表1:GCC 支持的硬件平台(部分):

硬件 操作系统
Alpha Red Hat Linux 7.1
HPPA HPUX 11.0
Intel x86 Debian Linux 2.2、Red Hat Linux 6.2 和 FreeBSD 4.5
MIPS IRIX 6.5
PowerPC AIX 4.3.3
Sparc Solaris 2.7

值得一提的是,原汁原味的 GCC 编译器没有我们熟悉的界面窗口,要想使用它,必须编写对应的 gcc 命令。所谓原汁原味,指的是纯 GCC 编译器和集成了 GCC 编译器的开发软件(IDE),和前者相比,后者在集成 GCC 编译器功能的同时,还向用户提供了友好的界面窗口,使得用户即便记不住 gcc 命令,也能从事开发工作,这极大地降低了用户的学习成本。

我们知道,操作系统大致分为 2 大阵营,分别是 Windows 阵营和类 Unix 阵营(包括 Unix、Linux、Mac OS、安卓等)。通常情况下,Windows 系统下用户更习惯使用现有的 IDE 来编译程序;而类 Unix 系统下,用户更喜欢直接编写相应的 gcc 命令来编译程序。

在了解什么是 GCC 编译器的基础上,这里以在 CentOS 操作系统(Linux 发行版之一)上使用 gcc 命令运行 C 语言程序为例,让读者更直观的感受一下 GCC 编译器的功能和使用方法。

如下是我们在 vim 编辑器中编写的一段完整的 C 语言程序,其功能是输出一个 “Hello, World!”:

对于此程序,我们可以使用如下的 gcc 命令:

1
gcc helloworld.c -o helloworld

有关 gcc 命令各部分的含义,后续章节会做详细讲解,这里不必深究。

下图演示了如何使用 gcc 命令将 C 语言代码编译成一个可执行文件:

如图 2 所示,通过编写对应的 gcc 命令并执行,就可以轻松将我们编写的程序编译成一个二进制可执行文件。

那么,gcc 命令该如何编写呢?别急,下面我将分章节给大家做详细地讲解。

Linux GCC编译器下载与安装

由于 Linux 操作系统的自由、开源,在其基础上衍生出了很多不同的 Linux 操作系统,如 CentOS、Ubuntu、Debian 等。这些 Linux 发行版中,大多数都默认装有 GCC 编译器(版本通常都较低)。

如果读者不清楚当前使用的 Linux 发行版是否已经装有 GCC 编译器,或者忘记了已安装 GCC 的版本号,可以打开命令行窗口(Terminal)并执行如下指令:

1
2
3
4
5
lautung@PC:~$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

以上信息表明已经安装了GCC。

快速安装GCC编译器

要知道,每个 Linux 发行版都有自己的软件包管理工具,比如 CentOS 系统的 yum 包管理器、Ubuntu 系统的 apt 包管理器等等,并且大多数 Linux 发行版都提供有 GCC 编译器的二进制软件包。因此,我们可以直接“傻瓜式”地安装 GCC 编译器(以 yum 为例):

1
2
yum -y install gcc
yum -y install gcc-c++

通过执行这 2 条指令,就可以在 CentOS 系统中利用 gcc 命令来执行 C 语言程序,利用 g++ 命令来执行 C++ 程序。

注意,读者切勿认为 gcc 只能用来编译 C 语言程序,g++ 只能用于编译 C++ 程序,这是不对的。有关 gcc 和 g++ 命令,我们会在后续章节给大家做详细讲解。

这意味着,如果读者使用此方式安装 GCC 编译器,需要查看 GCC 编译器的版本(通过gcc –version指令)是否符合自己的需求。举个例子,如果读者想编译 C++ 11 标准下的 C++ 程序,则至少要安装 4.8 版本的 GCC 编译器,低版本的 GCC 编译器是不支持 C++11 标准的。

总的来说,如果读者对 GCC 编译器的版本没有要求,则推荐使用此安装方式;反之,如果读者需要安装指定版本的 GCC 编译器,则需要使用接下来介绍的安装方法。

手动安装GCC编译器

华中的镜像:http://mirror.hust.edu.cn/gnu/gcc/ ,和使用 yum 自动安装 GCC 编译器不同,手动安装 GCC 编译器需要提前到 GCC 官网下载指定版本的 GCC 源码安装包,我们在镜像中下载。

值得一提的是,每个版本中都包含 2 种格式的压缩包,分别为 tar.gz 和 tar.xz,只是压缩格式不同,本节以 tar.gz 压缩包教大家安装 GCC 编译器。

暂略

Windows上使用GCC编译器

要知道,GCC 官网提供的 GCC 编译器是无法直接安装到 Windows 平台上的,如果我们想在 Windows 平台使用 GCC 编译器,可以安装 GCC 的移植版本。

目前适用于 Windows 平台、受欢迎的 GCC 移植版主要有 2 种,分别为 MinGW 和 Cygwin。其中,MinGW 侧重于服务 Windows 用户可以使用 GCC 编译环境,直接生成可运行 Windows 平台上的可执行程序,相比后者体积更小,使用更方便;而 Cygwin 则可以提供一个完整的 Linux 环境,借助它不仅可以在 Windows 平台上使用 GCC 编译器,理论上可以运行 Linux 平台上所有的程序。

如果读者仅需要在 Windows 平台上使用 GCC,可以使用 MinGW 或者 Cygwin;除此之外,如果读者还有更高的需求(例如运行 POSIX 应用程序),就只能选择安装 Cygwin。

本节要重点给大家介绍的是 MinGw 的安装和使用。MinGw 全称 Minimalist GNU for Windows,应用于 Windows 平台,可以为我们提供一个功能有限的 Linux 系统环境以使用一些 GNU 工具,比如 GCC 编译器、gawk、bison 等等。

MinGW的安装

相比在 Linux 平台上安装 GCC 编译环境,在 Windows 平台上安装 MinGW 是比较简单的,只需经历以下几个过程。

①打开 MinGW 官网(点击即可进入官网),下载 MinGW 安装包。

②暂略

Sublime Text运行C和C++程序

暂略

VS Code运行C和C++程序

暂略

GCC的组成部分以及使用到的软件

GCC 是由许多组件组成的。表 1 列出了 GCC 的各个部分,但它们也并不总是出现 的。有些部分是和语言相关的,所以如果没有安装某种特定语言,系统:中就不会出现相关的文件。

表1:GCC 安装的各个部分

部分 描述
c++ gcc的一个版木,默认语言设置为C++,而且在连接的时候自动包含标准C++库。这和 g++一样
ccl 实际的C编译程序
cclplus 实际的C++编泽程序
collect2 在不使用GNU连接程序的系统上,有必要运行collect2 来产生特定的全局初始化代码(例如C++的构造函数和析构函数)
configure GCC源代码树根目录中的一个脚木。用于设置配置值和创建GCC编译程序必需的make 程序的描述文件
crt0.o 这个初始化和结束代码是为每个系统定制的,而且也被编译进该文件,该文件然后会被连接到每个可执行文件中来执行必要的启动和终止程序
cygwin1.dll Windows的共享库提供的API,模拟UNIX系统调用
f77 该驱动程序可用于编译Fortran
f771 实际的Fortran编译程序
g++ gcc的一个版木,默认语言设置为C++,而且在连接的时候自动包含标准C++库。这和c++ 一样
gcc 该驱动程序等同于执行编译程序和连接程序以产生需要的输出
gcj 该驱动程序用于编译Java
gnat1 实际的Ada编译程序
gnatbind 一种工具,用于执行Ada语言绑定
gnatlink 一种工具,用于执行Ada语言连接
jc1 实际的Java编译程序
libgcc 该库包含的例程被作为编泽程序的一部分,是因为它们可被连接到实际的可执行程序中。它们是特殊的例程,连接到可执行程序,来执行基木的任务,例如浮点运算。这些库中的例程通常都是平台相关的
libgcj 运行时库包含所有的核心Java类
libobjc 对所有Objective-C程序都必须的运行时库
libstdc++ 运行时库,包括定义为标准语言一部分的所有的C++类和函数

表 2 列出的软件和 GCC 协同工作,目的是实现编译过程。有些是很基本的(例如 as 和 Id),而其他一些则是非常有用但不是严格需要的。尽管这些工具中的很多都是各种 UNIX 系统的本地工具,但还是能够通过 GNU 包 binutils 得到大多数工具。

表2:GCC 使用的软件工具

工具 描述
addr2line 给出一个可执行文件的内部地址,addr2line使用文件中的调试信息将地址翻泽成源代码文件名和行号。该程序是binutils包的一部分
ar 这是一个程序,可通过从文档中增加、删除和析取文件来维护库文件。通常使用该工具是为了创建和管理连接程序使用的目标库文档。该程序是binutils 包的一部分
as GNU汇编器。实际上它是一族汇编器,因为它可以被编泽或能够在各种不同平台上工作。该程序是binutils 包的一部分
autoconf 产生的shell 脚木自动配置源代码包去编泽某个特定版木的UNIX
c++filt 程序接受被C++编泽程序转换过的名字(不是被重载的),而且将该名字翻泽成初始形式。该程序是binutils 包的一部分
f2c 是Fortran到C的翻译程序。不是GCC的一部分
gcov gprof使用的配置工具,用来确定程序运行的时候哪一部分耗时最大
gdb GNU调试器,可用于检查程序运行时的值和行为
GNATS GNU的调试跟踪系统(GNU Bug Tracking System)。一个跟踪GCC和其他GNU软件问题的在线系统
gprof 该程序会监督编泽程序的执行过程,并报告程序中各个函数的运行时间,可以根据所提供的配置文件来优化程序。该程序是binutils 包的一部分
ld GNU连接程序。该程序将目标文件的集合组合成可执行程序。该程序是binutils 包的一部
libtool 一个基本库,支持make程序的描述文件使用的简化共享库用法的脚木
make 一个工具程序,它会读makefile脚木来确定程序中的哪个部分需要编泽和连接,然后发布必要的命令。它读出的脚木(叫做makefile或Makefile)定义了文件关系和依赖关系
nlmconv 将可重定位的目标文件转换成NetWare可加载模块(NetWare Loadable Module,NLM)。该程序是binutils 的一部分
nm 列出目标文件中定义的符号。该程序是binutils 包的一部分
objcopy 将目标文件从一种二进制格式复制和翻译到另外一种。该程序是binutils 包的一部分
objdump 显示一个或多个目标文件中保存的多种不同信息。该程序是binutils 包的一部分
ranlib 创建和添加到ar文档的索引。该索引被Id使用来定位库中的模块。该程序是binutils 包的一部分
ratfor Ratfor预处理程序可由GCC激活,但不是标准GCC发布版的一部分
readelf 从ELF格式的目标文件显示信息。该程序是binutils 包的一部分
size 列出目标文件中每个部分的名字和尺寸。该程序是binutils 包的一部分
strings 浏览所有类型的文件,析取出用于显示的字符串。该程序是binutils 包的一部分
strip 从目标文件或文档库中去掉符号表,以及其他调试所需的信息。该程序是 binutils 包的一部
vcg Ratfor 浏览器从文木文件中读取信息,并以图表形式显示它们。而vcg工具并不是GCC发布中的一部分,但-dv选项可被用来产生vcg可以理解的优化数据的格式
windres Window资源文件编泽程序。该程序是binutils 包的一部分

GCC 与 G++

发展至今(2021年4月),GCC编译器版本已经至10,其功能也由最初仅能编译 C 语言,扩增至可以编译多种编程语言,其中就包括 C++ 。

除此之外,当下的 GCC 编译器还支持编译 Go、Objective-C,Objective-C++ ,Fortran,Ada,D 和 BRIG(HSAIL)等程序,甚至于 GCC 6 以及之前的版本还支持编译 Java 程序。但本教程主要讲解如何使用 GCC 编译器编译运行 C 和 C++ 程序,因此有关其它编程语言如何使用 GCC 编译器编译,将不再做具体讲解。

那么,在已编辑好 C 语言或者 C++ 代码的前提下,如何才能调用 GCC 编译器为我们编译程序呢?很简单,GCC 编译器已经为我们提供了调用它的接口,对于 C 语言或者 C++ 程序,可以通过执行 gcc 或者 g++ 指令来调用 GCC 编译器。

值得一提的是,实际使用中我们更习惯使用 gcc 指令编译 C 语言程序,用 g++ 指令编译 C++ 代码。需要强调的一点是,这并不是 gcc 和 g++ 的区别,gcc 指令也可以用来编译 C++ 程序,同样 g++ 指令也可以用于编译 C 语言程序。

gcc 和 g++ 的区别是什么呢?

实际上,只要是 GCC 支持编译的程序代码,都可以使用 gcc 命令完成编译。可以这样理解,gcc 是 GCC 编译器的通用编译指令,因为根据程序文件的后缀名,gcc 指令可以自行判断出当前程序所用编程语言的类别,比如:

①xxx.c:默认以编译 C 语言程序的方式编译此文件;

②xxx.cpp:默认以编译 C++ 程序的方式编译此文件。

③xxx.m:默认以编译 Objective-C 程序的方式编译此文件;

④xxx.go:默认以编译 Go 语言程序的方式编译此文件;

当然,gcc 指令也为用户提供了“手动指定代表编译方式”的接口,即使用 -x 选项。例如,gcc -xc xxx 表示以编译 C 语言代码的方式编译 xxx 文件;而 gcc -xc++ xxx 则表示以编译 C++ 代码的方式编译 xxx 文件。有关 -x 选项的用法,后续会给出具体样例。

但如果使用 g++ 指令,则无论目标文件的后缀名是什么,该指令都一律按照编译 C++ 代码的方式编译该文件。也就是说,对于 .c 文件来说,gcc 指令以 C 语言代码对待,而 g++ 指令会以 C++ 代码对待。但对于 .cpp 文件来说,gcc 和 g++ 都会以 C++ 代码的方式编译。

有读者可能会认为,C++ 兼容 C 语言,因此对于 C 语言程序来说,使用 gcc 编译还是使用 g++ 编译,应该没有什么区别,事实并非如此。严格来说,C++ 标准和 C 语言标准的语法要求是有区别的。

除此之外对于编译执行 C++ 程序,使用 gcc 和 g++ 也是有区别的。要知道,很多 C++ 程序都会调用某些标准库中现有的函数或者类对象,而单纯的 gcc 命令是无法自动链接这些标准库文件的。比如  和 。

如果想使用 gcc 指令来编译执行 C++ 程序,需要在使用 gcc 指令时,手动为其添加 -lstdc++ -shared-libgcc 选项,表示 gcc 在编译 C++ 程序时可以链接必要的 C++ 标准库。就是gcc -xc++ -lstdc++ -shared-libgcc filename.cpp

也这样认为,g++ 指令就等同于gcc -xc++ -lstdc++ -shared-libgcc指令。显然后者书写是非常麻烦的,大多数人会更喜欢前者。

总结一下就是,使用 gcc 指令编译C程序,而编译 C++ 程序则推荐使用 g++ 指令。

GCC自动识别的文件扩展名(一览表)

通过前面的学习我们知道,对于执行 C 或者 C++ 程序,需要借助 gcc(g++)指令来调用 GCC 编译器。并且对于以 .c 为扩展名的文件,GCC 会自动将其视为 C 源代码文件;而对于以 .cpp 为扩展名的文件,GCC 会自动将其视为 C++ 源代码文件。

除此之外,GCC 编译器还可以自动识别多种扩展名(如表 1 所示),即根据不同的扩展名确定该文件该怎样编译。

文件名称+扩展名 GCC编译器识别的文件类型
file.c 尚未经过预处理操作的C源程序文件。
file.i 经过预处理操作、但尚未进行编译、汇编和连接的C源代码文件。
file.cpp
file.cp
file.cc
file.cxx
file.CPP
file.c++
file.C 尚未经过预处理操作的C++源代码文件。
file.ii 已经预处理操作,但尚未进行编译、汇编和连接的C++源代码文件。
file.s 经过编译生成的汇编代码文件。
file.h C、C++或者Objective-C++语言头文件。
file.hh
file.H
file.hp
file.hxx
file.hpp
file.HPP
file.h++
file.tcc C++头文件。

注意,表 1 仅罗列了 GCC 编译器可识别的与 C 和 C++ 语言相关的文件后缀名。除此之外,GCC 编译器还支持 Go、Objective-C,Objective-C ++,Fortran,Ada,D 和 BRIG(HSAIL)等编程语言的编译,关于这些编程语言可被识别的文件扩展名,感兴趣的读者可前往GCC官网查看。

有读者可能会问,如果当前文件的扩展名和表 1 不符,还能使用 GCC 编译器吗?答案是肯定的。只需要借助 -x 选项(小写)指明当前文件的类型即可。

举个例子,如下是一个 C 语言程序,存储在 demo 文件中:

1
2
3
4
5
6
//存储在 demo 文件中
#include <stdio.h>
int main(){
puts("hello world!");
return 0;
}

显然,这是一段完整的 C 语言程序,但由于其存储在无扩展名的 demo 文件中,如果直接使用 gcc 指令调用 GCC 编译器,则执行会报错:

1
2
3
[root@PCdemo]# gcc demo
demo: file not recognized: File format not recognized
collect2: ld returned 1 exit status

可以看到,GCC 编译器无法识别 demo 这个文件。这种情况下,就必须使用 -x 选项手动为其指定文件的类型,例如:

1
2
3
4
5
[root@PC demo]# gcc -xc demo
[root@PC demo]# ls
a.out demo
[root@PC demo]# ./a.out
GCC教程:hello world!

可以看到,通过为 gcc 指令添加 -xc 选项,表明当前 demo 为 C 语言程序文件,由此 GCC 编译器即可成功将其编译为 a.out 可执行文件。

除了用 c 表明 C 语言程序文件外,-x 指令还是后跟 c-header(C语言头文件)、c++(C源文件)、c-header(C++程序头文件)等选项,感兴趣的读者同样可直接前往 GCC 官网查看,这里不再做详细介绍。

GCC -std编译标准一览表

要知道,任何一门编程语言都有相关的组织和团体在不停的维护和更新。原因很简单,时代在发展,编程语言如果停滞不前,最终就会被淘汰。

以 C 语言为例,发展至今该编程语言已经迭代了诸多个版本,例如 C89(偶尔又称为 C90)、C94(C89 的修订版)、C99、C11、C17,以及当下正在开发的 C2X 新标准。甚至于在这些标准的基础上,GCC 编译器本身还对 C 语言的语法进行了扩展,先后产生了 GNU90、GNU99、GNU11 以及 GNU17 这 4 个版本。

有趣的是,GCC 编译器对 C 语言的很多扩展,往往会被 C 语言标准委员会所采纳,并添加到新的 C 语言标准中。例如,GNU90 中对 C 语言的一些扩展,就融入到了新的 C99 标准中;GNU90、GNU99 中对 C 语言的一些扩展,被融入到了新的 C11 标准中。

C++ 语言的发展也历经了很多个版本,包括 C++ 98、C++ 03(C++ 98 的修订版)、C++ 11(有时又称为 C++ 0x)、C++ 14、C++ 17,以及即将要发布的 C++ 20 新标准。和 C 语言类似,GCC 编译器本身也对不同的 C++ 标准做了相应的扩展,比如 GNU++ 98、GNU++ 11、GNU++ 14、GNU++ 17。

读者可能会问,这么多标准,GCC 编译器使用的到底是哪一套呢?不同版本的 GCC 编译器,默认使用的标准版本也不尽相同。以当前最新的  GCC 10.1.0 版本为例,默认情况下 GCC 编译器会以 GNU11 标准(C11 标准的扩展版)编译 C 语言程序,以 GNU++ 14 标准(C++ 14 标准的扩展版)编译 C++ 程序。

关于不同版本的 GCC 编译器,默认使用的编译标准,感兴趣的读者可自行到 GCC 手册中查找。

那么,我们可以手动控制 GCC 编译器使用哪个编译标准吗?答案是肯定的,对于编译 C、C++ 程序来说,借助 -std 选项即可手动控制 GCC 编译程序时所使用的编译标准。也就是说,当使用 gcc 指令编译 C 语言程序时,我们可以借助 -std 选项指定要使用的编译标准;同样,当使用 g++ 指令编译 C++ 程序时,也可以借助 -std 选项指定要使用的编译标准。

-std 选项的使用方式很简单,其基本格式如下:

1
gcc/g++ -std=编译标准

注意,不同版本的 GCC 编译器,所支持使用的 C/C++ 编译标准也是不同的。表 1 罗列了常用的 GCC 版本对 C 语言编译标准的支持程度。

GCC版本 C语言常用标准
C89/C90 C99
10.1~8.4 c89/c90
7.5~5.5 c89/c90
4.9.4~4.8.5 c89/c90
4.7.4 c89/c90
4.6.4 c89/c90
4.5.4 c89/c90

注意,表头表示的是各个编译标准的名称,而表格内部的则为 -std 可用的值,例如 -std=c89、-std=c11、-std=gnu90 等(表 2 也是如此)。

表 2 罗列了常用的 GCC 版本对 C++ 程序编译标准的支持程度。

GCC版本 C++常用标准
C++98/03 C++11
10.1~8.4 c++98/c++03
7.5~5.5 c++98/c++03
4.9.4~4.8.5 c++98/c++03
4.7.4 c++98
4.6.4 c++98
4.5.4 c++98

表 1、2 中,有些版本对应的同一编译标准有 2 种表示方式,例如对于 8.4~10.1 版本的 GCC 编译器来说,-std=c89 和 -std=c90 是一样的,使用的都是 C89/C90 标准。另外,GCC 编译器还有其他版本,读者可查阅GCC文档获得相关信息。

举个例子,如下是一个 C 语言源程序:

1
2
3
4
5
6
7
8
9
[root@bogon demo]# ls
main.c
[root@bogon demo]# cat main.c
#include <stdio.h>
int main(){
for(int i=0;i<10;i++){
printf("i=%d ",i);
}
}

如果我们想以 c99 的标准编译它,在确认当前所有 GCC 编译器版本支持 C99 标准的前提下,通过执行如下指令,即可完成编译:

1
2
3
[root@bogon demo]# gcc -std=c99 main.c -o main.exe
[root@bogon demo]# ./main.exe
i=0 i=1 i=2 i=3 i=4 i=5 i=6 i=7 i=8 i=9

但是,对于在 for 循环中声明变量 i 的做法,是违反 C89 标准的。也就是说,如果我们以 C89 的编译标准编译 main.c,GCC 编译器会报错:

1
2
3
4
[root@bogon demo]# gcc -std=c89 main.c -o main.exe
main.c: In function ‘main’:
main.c:3: error: ‘for’ loop initial declarations are only allowed in C99 mode
main.c:3: note: use option -std=c99 or -std=gnu99 to compile your code

这也就意味着,在编写程序前必须明确要使用的编译标准,并清楚得知道该标准下什么可用,什么不可用。

GCC编译C/C++程序(一步完成)

通过前面章节的学习我们知道,GCC 编译器并未提供给用户可用鼠标点击的界面窗口,要想调用 GCC 编译器编译 C 或者 C++ 程序,只能通过执行相应的 gcc 或者 g++ 指令。本节将重点给大家讲解如何编写 gcc 或者 g++ 指令来编译 C、C++ 程序。

注意,在前面的讲解中我们一直提到“编译”C、C++ 程序,其本意指的是将 C、C++ 代码转变为可执行程序(等同于 Windows 系统中以 .exe 为后缀的可执行文件)。但实际上,C 或者 C++ 程序从源代码生成可执行程序的过程,需经历 4 个过程,分别是预处理、编译、汇编和链接。

同样,使用 GCC 编译器编译 C 或者 C++ 程序,也必须要经历这 4 个过程。但考虑在实际使用中,用户可能并不关心程序的执行结果,只想快速得到最终的可执行程序,因此 gcc 和 g++ 都对此需求做了支持。

首先以运行 C 语言程序为例,给大家演示如何使用 gcc 快速获得对应的可执行程序。如下就是一段 C 语言程序:

//存储在 demo.c 文件中

#include <stdio.h>

int main(){

puts(“GCC教程:http://c.biancheng.net/gcc/“);

return 0;

}

如上所示,这是一个很简单的输出“Hello,World!”字符串的 C 语言程序,接下来打开命令行窗口(Terminal),编写如下 gcc 指令:

[root@bogon ~]# gcc demo.c

按下 Enter 回车键,由此 GCC 编译器就帮我们在当前目录下生成了对应的可执行文件,该文件的名称为 a.out,可以通过 ls 指令查看该文件是否存在:

[root@bogon ~]# ls

a.out    demo.c

#或许还有其他文件,这里不再一一列出

在此基础上,我们可以执行该文件,查看其执行结果,继续编写如下指令:

[root@bogon ~]# ./a.out

GCC教程:http://c.biancheng.net/gcc/

通过前面的学习我们知道,执行 C++ 程序和执行 C 语言程序不同的是,要么使用 g++ 指令,要么使用 gcc -xc++ -lstdc++ -shared-libgcc 指令。比如下面为一段简单的 C++ 程序:

//位于 demo.cpp 文件中

1
2
3
4
5
6
#include <iostream>
using namespace std;
int main(){
cout << "hello world!" << endl;
return 0;
}

运行此程序,可以编写如下指令并执行:

1
[root@bogon ~]# g++ demo.cpp  #或者 gcc -xc++ -lstdc++ -shared-libgcc demo.cpp

同样,GCC 编译器会在当前目录下生成一个名为 a.out 的可执行文件(如果之前有同名文件,旧文件会被覆盖)。通过如下指令即可运行该文件:

[root@bogon ~]# ./a.out

GCC教程:hello world!

注意,gcc 或者 g++ 指令还支持用户手动指定最终生成的可执行文件的文件名,例如修改前面执行 C、C++ 程序的 gcc 和 g++ 指令:

[root@bogon ~]# gcc demo.c -o demo.exe

[root@bogon ~]# g++ demo.cpp -o democpp.exe   # 或者 gcc -xc++ -lstdc++ -shared-libgcc demo.cpp -o democpp.exe

其中 -o 选项用于指定要生成的文件名,例如 -o demo.exe 即表示将生成的文件名设为 demo.exe。

可以看到,GCC 编译器支持使用 gcc(g++)指令“一步编译”指定的 C(C++)程序。

注意,虽然我们仅编写了一条 gcc 或者 g++ 指令,但其底层依据是按照预处理、编译、汇编、链接的过程将 C 、C++ 程序转变为可执行程序的。而本应在预处理阶段、编译阶段、汇编阶段生成的中间文件,此执行方式默认是不会生成的,只会生成最终的 a.out 可执行文件(除非为 gcc 或者 g++ 额外添加 -save-temps 选项)。

对于初学者来说,可能需要深入探究 C、C++ 程序转变为可执行程序的整个过程,查看该过程中产生的中间文件。如此,上面介绍的执行方式将不再使用,而要采用分步编译的方式。

所谓“分步编译”,即由用户手动调用 GCC 编译器完成对 C、C++源代码的预处理、编译、汇编以及链接过程,每个阶段都会生成对源代码加工后的文件。

那么,到底如何分步编译 C、C++ 程序呢?事实上,GCC 编译器除了提供 gcc 和 g++ 这 2 个指令之外,还提供有大量的指令选项,方便用户根据自己的需求自定义编译方式。在前面的学习过程中,我们已经使用了一些指令选项,比如编译 C++ 程序时 gcc 指令后跟的 -xc++、-lstdc++、-shared-libgcc,再比如手动指定可执行文件名称的 -o 选项。

表 1 罗列出了实际使用 gcc 或者 g++ 指令编译 C/C++ 程序时,常用的一些指令选项:

gcc/g++指令选项 功能
-E(大写) 预处理指定的源文件,不进行编译。
-S(大写) 编译指定的源文件,但是不进行汇编。
-c 编译、汇编指定的源文件,但是不进行链接。
-o 指定生成文件的文件名。
-llibrary(-I library) 其中library表示要搜索的库文件的名称。该选项用于手动指定链接环节中程序可以调用的库文件。建议-l和库文件名之间不使用空格,比如-lstdc++。
-ansi 对于C语言程序来说,其等价于-std=c90;对于C++程序来说,其等价于-std=c++98。
-std= 手动指令编程语言所遵循的标准,例如c89、c90、c++98、c++11等。

注意,表 1 中仅列出了初学者常用的一些指令选项,事实上这仅是冰山一角,GCC 编译器提供有大量的指令选项,可满足我们在大部分场景下的编译需求。有关更多的编译指令,感兴趣的读者可自行查看GCC 手册

在表 1 的基础上,接下来将分章节对如何实现分步编译做详细的讲解,即如何将一个源代码程序经历预处理、编译、汇编以及链接这 4 个过程,最终生成对应的可执行程序。

GCC -E选项:对源程序做预处理操作

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

默认情况下 gcc -E 指令只会将预处理操作的结果输出到屏幕上,并不会自动保存到某个文件。因此该指令往往会和 -o 选项连用,将结果导入到指令的文件中。比如:

1
2
3
[root@bogon demo]# gcc -E demo.c -o demo.i
[root@bogon demo]# ls
demo.c demo.i

Linux 系统中通常用 “.i” 作为 C 语言程序预处理后所得文件的后缀名。由此,就完成了 demo.c 文件的预处理操作,并将其结果导入到了 demo.i 文件中。

读者可以通过执行cat demo.i指令查看该文件中的内容,但通常没有足够 C 语言功底的读者是看不懂的。为此,我们可以为 gcc 指令再添加一个 -C 选项,阻止 GCC 删除源文件和头文件中的注释:

1
[root@bogon demo]# gcc -E -C demo.c -o demo.i

注意,这里是大写的 -C,不是小写的 -c。小写的 -c 另作他用,后续章节会做详细讲解。

gcc -E支持的常用选项

除了 -C、-o 以外,根据实际场景的需要,gcc -E 后面还可以添加其它的选项,例如:

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

其中,对于指定 #include 搜索路径的几个选项,作用的先后顺序如下:

  • 对于用 #include “” 引号形式引入的头文件,首先搜索当前程序文件所在的目录,其次再前往 -iquote 选项指定的目录中查找;
  • 前往 -I 选项指定的目录中搜索;
  • 前往 -isystem 选项指定的目录中搜索;
  • 前往默认的系统路径下搜索;
  • 前往 -idirafter 选项指定的目录中搜索。

除表 1 罗列的几个选项之外,预处理过程可以使用的选项还有很多,比如 -imacros、-undef、-M 等,感兴趣的读者可前往官网GCC10.1.0 预处理过程选项查看。

GCC -S选项:编译非汇编文件

gcc -S指令

编译是整个程序构建的核心部分,也是最复杂的部分之一。所谓编译,简单理解就是将预处理得到的程序代码,经过一系列的词法分析、语法分析、语义分析以及优化,加工为当前机器支持的汇编代码。

通过给 gcc 指令添加 -S(注意是大写)选项,即可令 GCC 编译器仅将指定文件加工至编译阶段,并生成对应的汇编代码文件。例如:

1
2
3
[root@bogon demo]# gcc -S demo.i
[root@bogon demo]# ls
demo.c demo.i demo.s

可以看到,经过执行 gcc -S 指令,其生成了一个名为 demo.s 的文件,这就是经过编译的汇编代码文件。也就是说默认情况下,编译操作会自行新建一个文件名和指定文件相同、后缀名为 .s 的文件,并将编译的结果保存在该文件中。

当然如果需要的话,我们还可以为 gcc -S 指令添加 -o 选项,令 GCC 编译器将编译结果保存在我们指定的文件中。例如:

1
2
3
[root@bogon demo]# gcc -S demo.i -o test.i
[root@bogon demo]# ls
demo.c demo.i demo.s test.i

需要注意的是,gcc -S 指令操作的文件并非必须是经过预处理后得到的 .i 文件,-S 选项的功能是令 GCC 编译器将指定文件处理至编译阶段结束。这也就意味着,gcc -S 指令可以操作预处理后的 .i 文件,也可以操作源代码文件:

  • 如果操作对象为 .i 文件,则 GCC 编译器只需编译此文件;
  • 如果操作对象为 .c 或者 .cpp 源代码文件,则 GCC 编译器会对其进行预处理和编译这 2 步操作。

因此,如果我们想直接得到 demo.c 文件对应的汇编文件,就可以借助 gcc -S 指令:

1
2
3
[root@bogon demo]# gcc -S demo.c -o demo.s
[root@bogon demo]# ls
demo.c demo.s

由此,我们就可以直接获得 demo.c 对应的 demo.s 汇编文件。

对于最终生成的 .s 汇编文件,感兴趣的读者可执行 cat demo.s 指令查看文件中的内容。在此基础上,如果想提高文件内汇编代码的可读性,可以借助 -fverbose-asm 选项,GCC 编译器会自行为汇编代码添加必要的注释,例如:

1
[root@bogon demo]# gcc -S demo.c -fverbose-asm

GCC -c选项:生成目标文件

简单地理解,汇编其实就是将汇编代码转换成可以执行的机器指令。大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令。相对于编译操作,汇编过程会简单很多,它并没有复杂的语法,也没有语义,也不需要做指令优化,只需要根据汇编语句和机器指令的对照表一一翻译即可。

通过为 gcc 指令添加 -c 选项(注意是小写字母 c),即可让 GCC 编译器将指定文件加工至汇编阶段,并生成相应的目标文件。例如:

1
2
3
[root@bogon demo]# gcc -c demo.s
[root@bogon demo]# ls
demo.c demo.i demo.o demo.s

可以看到,该指令生成了和 demo.s 同名但后缀名为 .o 的文件,这就是经过汇编操作得到的目标文件。

当然如果必要的话,还可以为 gcc -c 指令在添加一个 -o 选项,用于将汇编操作的结果输入到指定文件中,例如:

1
2
3
[root@bogon demo]# gcc -c demo.s -o test.o
[root@bogon demo]# ls
demo.c demo.i demo.o demo.s test.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 步操作。

注意,如果指定文件已经经过汇编,或者 GCC 编译器无法识别,则 gcc -c 指令不做任何操作。

这里以 demo.c、demo.i、demo.s 为例,演示 gcc -c 指令的作用:

1
2
3
4
5
6
7
8
9
10
11
[root@bogon demo]# ls
demo.c demo.i demo.s
[root@bogon demo]# gcc -c demo.c -o democ.o
[root@bogon demo]# ls
demo.c demo.i democ.o demo.s
[root@bogon demo]# gcc -c demo.i -o demoi.o
[root@bogon demo]# ls
demo.c demo.i demoi.o democ.o demo.s
[root@bogon demo]# gcc -c demo.s -o demos.o
[root@bogon demo]# ls
demo.c demo.i demoi.o democ.o demo.s demos.o

以上操作分别生成的 democ.o、demoi.o 以及 demos.o,其包含的二进制内容是完全一样的,只是文件名不同而已。

gcc执行链接操作

得到生成目标文件之后,接下来就可以直接使用 gcc 指令继续执行链接操作,例如:

1
2
3
4
5
6
7
8
9
[root@bogon demo]# gcc democ.o -o democ.exe
[root@bogon demo]# ./democ.exe
Hello,World!
[root@bogon demo]# gcc demoi.o -o demoi.exe
[root@bogon demo]# ./demoi.exe
Hello,World!
[root@bogon demo]# gcc demos.o -o demos.exe
[root@bogon demo]# ./demos.exe
Hello,World!

gcc 会根据所给文件的后缀名 .o,自行判断出此类文件为目标文件,仅需要进行链接操作,所以这里的 gcc 指令只会对 democ.o、demoi.o、demos.i 执行链接操作,并分别生成 democ.exe、demoi.exe 以及 demos.exe 这 3 个可执行文件。

通过分别执行这 3 个可执行文件,其执行结果完全相同,从侧面验证了 democ.o、demo.i.o、demos.o 包含二进制的内容相同。

gcc -o选项用来指定输出文件,如果不使用-o选项,那么将采用默认的输出文件。例如默认情况下,生成的可执行文件的名字默认为 a.out。

如下是 gcc -o 指令的使用语法格式:

1
[root@bogon demo]# gcc [-E|-S|-c] [infile] [-o outfile]

其中,用方括号 [] 括起来的部分可以忽略。

[infile] 表示输入文件(也即要处理的文件),它可以是源文件、汇编文件或者目标文件;[outfile] 表示输出文件(也即处理的结果),可以是预处理文件、目标文件、可执行文件等。

值得一提的是,通常情况下 [infile] 处放置一个文件,但根据实际需要也可以放置多个文件,表示有多个输入文件(后续会给出实例)。

GCC -o选项使用举例

  1. 将源文件作为输入文件,将可执行文件作为输出文件,也即完整地编译整个程序:
1
$ gcc main.c func.c -o app.out

将 main.c 和 func.c 两个源文件编译成一个可执行文件,其名字为 app.out。如果不使用 -o 选项,那么将生成名字为 a.out 的可执行文件。

  1. 将源文件作为输入文件,将目标文件作为输出文件,也即只编译不链接:
1
$ gcc -c main.c -o a.o

将源文件 main.c 编译为目标文件 a.o。如果不使用 -o 选项,那么将生成名为 main.o 的目标文件。

  1. 将源文件作为输入文件,将预处理文件作为输出文件,也即只进行预处理操作:
1
$ gcc -E main.c -o demo.i

对源文件 main.c 进行预处理操作,并将结果放在 demo.i 文件中。如果不使用 -o 选项,那么将生成名为 main.i 的预处理文件。

  1. 将目标文件作为输入文件,将可执行文件作为输出文件:
1
2
$ gcc -c func.c main.c
$ gcc func.o main.o -o app.out

第一条命令只编译不链接,将生成 func.o 和 main.o 两个目标文件。第二条命令将生成的两个目标文件生成最终的可执行文件 app.out。如果不使用 -o 选项,那么将生成名字为 a.out 的可执行文件。

GCC -l选项:手动添加链接库

链接器把多个二进制的目标文件(object file)链接成一个单独的可执行文件。在链接过程中,它必须把符号(变量名、函数名等一些列标识符)用对应的数据的内存地址(变量地址、函数地址等)替代,以完成程序中多个模块的外部引用。

而且,链接器也必须将程序中所用到的所有C标准库函数加入其中。对于链接器而言,链接库不过是一个具有许多目标文件的集合,它们在一个文件中以方便处理。

当把程序链接到一个链接库时,只会链接程序所用到的函数的目标文件。在已编译的目标文件之外,如果创建自己的链接库,可以使用 ar 命令。

标准库的大部分函数通常放在文件 libc.a 中(文件名后缀.a代表“achieve”,译为“获取”),或者放在用于共享的动态链接文件 libc.so 中(文件名后缀.so代表“share object”,译为“共享对象”)。这些链接库一般位于 /lib/ 或 /usr/lib/,或者位于 GCC 默认搜索的其他目录。

当使用 GCC 编译和链接程序时,GCC 默认会链接 libc.a 或者 libc.so,但是对于其他的库(例如非标准库、第三方库等),就需要手动添加。

令人惊讶的是,标准头文件 <math.h> 对应的数学库默认也不会被链接,如果没有手动将它添加进来,就会发生函数未定义错误。

GCC 的-l选项(小写的 L)可以让我们手动添加链接库。下面我们编写一个数学程序 main.c,并使用到了 cos() 函数,它位于 <math.h> 头文件。

1
2
3
4
5
6
7
8
9
10
11
12
//main.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;
}

如果我们不使用 -l 选项:

1
2
3
4
[root@bogon demo]# gcc main.c
/tmp/ccYfkZJk.o: In function `main':
main.c:(.text+0x34): undefined reference to `cos'
collect2: ld returned 1 exit status

显然,GCC 编译器无法找到 cos() 这个函数。为了编译这个 main.c,必须使用-l选项,以链接数学库:

1
[root@bogon demo]# gcc main.c -o main.out -lm

数学库的文件名是 libm.a。前缀lib和后缀.a是标准的,m是基本名称,GCC 会在-l选项后紧跟着的基本名称的基础上自动添加这些前缀、后缀,本例中,基本名称为 m。

在支持动态链接的系统上,GCC 自动使用在 Darwin 上的共享链接库 libm.so 或 libm.dylib。

链接其它目录中的库

链接其它目录中的库

通常,GCC 会自动在标准库目录中搜索文件,例如 /usr/lib,如果想链接其它目录中的库,就得特别指明。有三种方式可以链接在 GCC 搜索路径以外的链接库,下面我们分别讲解。

①把链接库作为一般的目标文件,为 GCC 指定该链接库的完整路径与文件名。

例如,如果链接库名为 libm.a,并且位于 /usr/lib 目录,那么下面的命令会让 GCC 编译 main.c,然后将 libm.a 链接到 main.o:

1
[root@bogon demo]# gcc main.c -o main.out /usr/lib/libm.a

②使用-L选项,为 GCC 增加另一个搜索链接库的目录:

1
[root@bogon demo]# gcc main.c -o main.out -L/usr/lib -lm

可以使用多个-L选项,或者在一个-L选项内使用冒号分割的路径列表。

③把包括所需链接库的目录加到环境变量 LIBRARYPATH 中。

GCC分步编译 C++ 源程序(汇总)

1
2
3
4
5
6
7
//位于 demo.cpp 文件中
#include <iostream>
using namespace std;
int main(){
cout << "helloworld!" << endl;
return 0;
}

①预处理

通过给 g++ 指令添加 -E 选项,即可轻松实现令 GCC 编译器只对目标源程序进行预处理操作。比如:

1
2
3
[root@bogon demo]# g++ -E demo.cpp -o demo.i
[root@bogon demo]# ls
demo.cpp demo.i

注意,由于编译阶段需要用到预处理的结果,因此这里必须使用 -o 选项将该结果输出到指定的 demo.i 文件中(Linux 系统中,通常用 “.i” 或者 “.ii” 作为 C++ 程序预处理后所得文件的后缀名)。

感兴趣的读者可自行运行 cat demo.i 指令查看 demo.i 文件中的内容。

②编译

值得一提的是,编译阶段针对的将不再是 demo.cpp 源文件,而是 demo.i 预处理文件。对预处理文件进行编译操作,实际上就是对 demo.i 文件做进一步的语法分析,并生成对应的汇编代码文件(Linux 发行版通常以 “.s” 作为其后缀名)。

通过给 g++ 指令添加 -S 选项,即可令 GCC 编译器仅对指定预处理文件做编译操作。例如:

1
2
3
[root@bogon demo]# g++ -S demo.i
[root@bogon demo]# ls
demo.cpp demo.i demo.s

和预处理阶段不同,即便这里不使用 -o 选项,编译结果也会输出到和预处理文件同名(后缀名改为 .s)的新建文件中。

③汇编

汇编阶段就是将之前生成的汇编代码文件(demo.s)做进一步转换,生成对应的机器指令。通过给 g++ 指令添加 -c 选项,即可令 GCC 编译器仅对指定的汇编代码文件做汇编操作。

例如:

1
2
3
[root@bogon demo]# g++ -c demo.s
[root@bogon demo]# ls
demo.cpp demo.i demo.o demo.s

显然,默认情况下汇编操作会自动生成一个和汇编代码文件名称相同、后缀名为 .o 的二进制文件(又称为目标文件)。

④链接

目标文件已经是二进制文件,与可执行文件的组织形式类似,只是有些函数和全局变量的地址还未找到,因此还无法执行。链接的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件。

完成链接操作,并不需要给 g++ 添加任何选项,只要将汇编阶段得到的 demo.o 作为参数传递给它,g++就会在其基础上完成链接操作。例如:

1
2
3
[root@bogon demo]# g++ demo.o
[root@bogon demo]# ls
a.out demo.cpp demo.i demo.o demo.s

在链接阶段,如果不使用 -o 选项将执行结果输出到指定文件,则 g++ 会默认创建一个名为 a.out 的可执行文件,并将执行结果输出到该文件中。

经过以上 4 步,最终生成了 a.out 可执行文件,我们可以尝试运行该文件,查看其结果是否正确:

1
2
[root@bogon demo]# ./a.out
GCC教程:http://c.biancheng.net/gcc/

显然,该结果和我们的预期相符。

除此之外,如果读者不想执行这么多条指令,但想获得预处理、编译、汇编以及链接这 4 个过程产生的中间文件,可以执行如下指令:

1
2
3
[root@bogon demo]# g++ demo.cpp -save-temps
[root@bogon demo]# ls
a.out demo.c demo.cpp demo.ii demo.o demo.s

可以看到,通过给 g++ 添加 -save-temps 选项,可以使 GCC 编译器保留编译源文件过程中产生的所有中间文件。

gcc指令一次处理多个文件

共用一条 gcc 指令:

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

GCC编译多文件项目

在一个 C(或者 C++)项目中,往往在存储多个源文件,如果仍按照之前“先单独编译各个源文件,再将它们链接起来”的方法编译该项目,需要编写大量的编译指令,事倍功半。事实上,利用 gcc 指令可以同时处理多个文件的特性,可以大大提高我们的工作效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
举个例子,如下是一个拥有 2 个源文件的 C 语言项目:
[root@bogon demo]# ls
main.c myfun.c
[root@bogon demo]# cat main.c
#include <stdio.h>
int main(){
display();
return 0;
}
[root@bogon demo]# cat myfun.c
#include <stdio.h>
void display(){
printf("helloworld");
}
[root@bogon demo]#

可以看到,该项目中仅包含 2 个源文件,其中 myfun.c 文件用于存储一些功能函数,以方便直接在 main.c 文件中调用。

对于此项目,我们可以这样编译:

[root@bogon demo]# ls

main.c  myfun.c

[root@bogon demo]# gcc -c myfun.c main.c

[root@bogon demo]# ls

main.c  main.o  myfun.c  myfun.o

[root@bogon demo]# gcc myfun.o main.o -o main.exe

[root@bogon demo]# ls

main.c  main.exe  main.o  myfun.c  myfun.o

[root@bogon demo]# ./main.exe

helloworld

也可以直接编译并链接它们:

1
2
3
4
5
[root@bogon demo]# gcc myfun.c main.c -o main.exe
[root@bogon demo]# ls
main.c main.exe myfun.c
[root@bogon demo]# ./main.exe
helloworld

以上 2 种方式已然可以满足大部分场景的需要。但值得一提的是,如果一个项目中有十几个甚至几十个源文件,即便共用一条 gcc 指令编译(并链接),编写各个文件的名称也是一件麻烦事。

为了解决这个问题,我们可以进入该项目目录,用 *.c 表示所有的源文件,即执行如下指令:

1
2
3
4
5
6
7
[root@bogon demo]# ls
main.c myfun.c
[root@bogon demo]# gcc *.c -o main.exe
[root@bogon demo]# ls
main.c main.exe myfun.c
[root@bogon demo]# ./main.exe
GCC:http://c.biancheng.net/gcc/

由此,大大节省了手动输入各源文件名称的时间。

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

我们知道,C、C++程序从源文件到生成可执行文件需经历 4 个阶段,分别为预处理、编译、汇编和链接,本节将重点围绕链接阶段,对静态链接库和动态链接库做详细的讲解。

总的来说链接阶段要完成的工作,就是将同一项目中各源文件生成的目标文件以及程序中用到的库文件整合为一个可执行文件。

通过前面对 gcc(g++)指令的系统学习,我们已经学会了如何使用 gcc -c 指令将源文件生成对应的目标文件。那么,什么是库文件呢?

所谓库文件,读者可以将其等价为压缩包文件,该文件内部通常包含不止一个目标文件(也就是二进制文件)。值得一提的是,库文件中每个目标文件存储的代码,并非完整的程序,而是一个个实用的功能模块。例如,C 语言库文件提供有大量的函数(如 scanf()、printf()、strlen() 等),C++ 库文件不仅提供有使用的函数,还有大量事先设计好的类(如 string 字符串类)。

库文件的产生,极大的提高了程序员的开发效率,因为很多功能根本不需要从 0 开发,直接调取包含该功能的库文件即可。并且,库文件的调用方法也很简单,以 C 语言中的 printf() 输出函数为例,程序中只需引入 <stdio.h> 头文件,即可调用 printf() 函数。

有读者可能会问,调用库文件为什么还要牵扯到头文件呢?首先,头文件和库文件并不是一码事,它们最大的区别在于:头文件只存储变量、函数或者类等这些功能模块的声明部分,库文件才负责存储各模块具体的实现部分。读者可以这样理解:所有的库文件都提供有相应的头文件作为调用它的接口。也就是说,库文件是无法直接使用的,只能通过头文件间接调用。

头文件和库文件相结合的访问机制,最大的好处在于,有时候我们只想让别人使用自己实现的功能,并不想公开实现功能的源码,就可以将其制作为库文件,这样用户获取到的是二进制文件,而头文件又只包含声明部分,这样就实现了“将源码隐藏起来”的目的,且不会影响用户使用。关于如何制作库文件,后续章节会做详细讲解。

事实上,库文件只是一个统称,代指的是一类压缩包,它们都包含有功能实用的目标文件。要知道,虽然库文件用于程序的链接阶段,但编译器提供有 2 种实现链接的方式,分别称为静态链接方式和动态链接方式,其中采用静态链接方式实现链接操作的库文件,称为静态链接库;采用动态链接方式实现链接操作的库文件,称为动态链接库

那么,静态链接库和动态链接库到底有什么不同呢?

静态链接库

静态链接库实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,GCC 编译器就会将该模板代码直接复制到程序文件的适当位置,最终生成可执行文件。

使用静态库文件实现程序的链接操作,既有优势也有劣势:

  • 优势是,生成的可执行文件不再需要任何静态库文件的支持就可以独立运行(可移植性强);
  • 劣势是,如果程序文件中多次调用库中的同一功能模块,则该模块代码势必就会被复制多次,生成的可执行文件中会包含多段完全相同的代码,造成代码的冗余。

和使用动态链接库生成的可执行文件相比,静态链接库生成的可执行文件的体积更大。

另外值得一提的是,在 Linux 发行版系统中,静态链接库文件的后缀名通常用 .a 表示;在 Windows 系统中,静态链接库文件的后缀名为 .lib。

动态链接库

动态链接库,又称为共享链接库。和静态链接库不同,采用动态链接库实现链接操作时,程序文件中哪里需要库文件的功能模块,GCC 编译器不会直接将该功能模块的代码拷贝到文件中,而是将功能模块的位置信息记录到文件中,直接生成可执行文件。

显然,这样生成的可执行文件是无法独立运行的。采用动态链接库生成的可执行文件运行时,GCC 编译器会将对应的动态链接库一同加载在内存中,由于可执行文件中事先记录了所需功能模块的位置信息,所以在现有动态链接库的支持下,也可以成功运行。

采用动态链接库实现程序的连接操作,其优势和劣势恰好和静态链接库相反:

  • 优势是,由于可执行文件中记录的是功能模块的地址,真正的实现代码会在程序运行时被载入内存,这意味着,即便功能模块被调用多次,使用的都是同一份实现代码(这也是将动态链接库称为共享链接库的原因)。
  • 劣势是,此方式生成的可执行文件无法独立运行,必须借助相应的库文件(可移植性差)。

和使用静态链接库生成的可执行文件相比,动态链接库生成的可执行文件的体积更小,因为其内部不会被复制一堆冗余的代码。

在 Linux 发行版系统中,动态链接库的后缀名通常用 .so 表示;在 Windows 系统中,动态链接库的后缀名为 .dll。

值得一提的是,GCC 编译器生成可执行文件时,默认情况下会优先使用动态链接库实现链接操作,除非当前系统环境中没有程序文件所需要的动态链接库,GCC 编译器才会选择相应的静态链接库。如果两种都没有(或者 GCC 编译器未找到),则链接失败。

在 Linux 发行版中,静态链接库和动态链接库通常存放在 /usr/bin 或者 /bin 目录下。

实际开发中,我们还可以亲自动手,自定义一个静态链接库或者动态链接库。有关链接库文件的具体创建以及使用方法,我会在后续章节中做详细讲解。

用GCC制作静态链接库

假设当前有一个 C 语言项目,其目录结构如下所示:

1
2
3
4
5
6
7
8
demo项目
├─ headers
│ └─ test.h
└─ sources
├─ add.c
├─ sub.c
├─ div.c
└─ main.c

其中,headers 用于表示该项目拥有的所有头文件;sources 表示该项目拥有的所用源文件,读者可按照此目录结构构建 demo 项目,也可以将所有文件统一放置在 demo 项目下,本节选择的是后者,即将所有文件统一放置在 demo 目录下。

可以看到,该项目中包含 1 个头文件( .h ),4 个源文件( .c ),它们各自包含的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[root@bogon demo]# ls                       <- demo 目录结构
add.c div.c main.c sub.c test.h
[root@bogon demo]# cat test.h <- test.h 文件内容
#ifndef __TEST_H_
#define __TEST_H_

int add(int a,int b);
int sub(int a,int b);
int div(int a,int b);

#endif
[root@bogon demo]# cat add.c <- add.c 文件内容
#include “test.h”
int add(int a,int b)
{
return a + b;
}
[root@bogon demo]# cat sub.c <- sub.c 文件内容
#include “test.h”
int sub(int a,int b)
{
return a - b;
}
[root@bogon demo]# cat div.c <- div.c 文件内容
#include “test.h”
int div(int a,int b)
{
return a / b;
}
[root@bogon demo]# cat main.c <- main.c 文件内容
#include <stdio.h>
#include "test.h" //必须引入头文件
int main(void)
{
int m, n;
printf("Input two numbers: ");
scanf("%d %d", &m, &n);
printf("%d+%d=%d\n", m, n, add(m, n));
printf("%d-%d=%d\n", m, n, sub(m, n));
printf("%d÷%d=%d\n", m, n, div(m, n));
return 0;
}
[root@bogon demo]#

整个项目的逻辑很简单,其中 add.c、sub.c 和 div.c 这 3 个文件中各包含一个函数,分别实现将两个整数做相加、相减和除法操作,而 test.h 仅包含这 3 个函数的声明部分,main.c 是主程序文件,其通过引入 test.h 头文件调用了 3 个函数,从而分别完成了对用户输入的 2 个整数做相加、相减以及除法操作。

对于编译、运行 demo 项目,我们可以直接使用 gcc 命令完成:

1
2
3
4
5
6
7
8
9
[root@bogon demo]# gcc main.c add.c sub.c div.c -o main.exe
[root@bogon demo]# ls
add.c div.c main.c main.exe sub.c test.h
[root@bogon demo]# ./main.exe
Input two numbers: 10 2
10+2=12
10-2=8
10÷2=5
[root@bogon demo]#

注意,由于在程序预处理阶段,GCC 编译器会自行处理各个 .c 文件内部引入的 .h 头文件(将 .h 文件中的代码直接拷贝到当前 .c 源文件中),因此编译运行 demo 项目时,我们只需要提供所有的源文件即可,不需要处理头文件。

注意,add.c、sub.c 和 div.c 这 3 个文件,其包含的都是一些功能模块(实现具体功能的函数),对于这样的源文件,只要我们愿意共享,每个人都可以直接用到自己的项目中。这就产生一个问题,如果仅希望别人使用我们实现的功能,但又不想它看到具体实现的源码,该怎么办呢?很简单,就是将它们加工成一个静态链接库。

静态链接库的创建

通过前面的学习我们知道,静态链接库其实就相当于压缩包,其内部可以包含多个源文件。但需要注意的是,并非任何一个源文件都可以被加工成静态链接库,其至少需要满足以下 2 个条件:

  • 源文件中只提供可以重复使用的代码,例如函数、设计好的类等,不能包含 main 主函数;
  • 源文件在实现具备模块功能的同时,还要提供访问它的接口,也就是包含各个功能模块声明部分的头文件。

显然对于 demo 项目中的 add.c、sub.c 以及 div.c 这 3 个源文件来说,以上 2 个条件都符合,因此都可以被加工成静态链接库。并且根据实际需要,我们可以将它们集体压缩到一个静态链接库中,也可以各自压缩成一个静态链接库。

将源文件打包为静态链接库的过程很简单,只需经历以下 2 个步骤:

①将所有指定的源文件,都编译成相应的目标文件:

1
2
3
[root@bogon demo]# gcc -c sub.c add.c div.c
[root@bogon demo]# ls
add.c add.o div.c div.o main.c sub.c sub.o test.h

②然后使用 ar 压缩指令,将生成的目标文件打包成静态链接库,其基本格式如下:

1
ar rcs 静态链接库名称 目标文件1 目标文件2 ...

有关 ar 打包压缩指令,以及 rcs 各选项的含义和功能,感兴趣的读者可自行查找相关资料了解。这里需要重点说明的是,静态链接库的不能随意起名,需遵循如下的命名规则:

libxxx.a

Linux 系统下,静态链接库的后缀名为 .a;Windows 系统下,静态链接库的后缀名为 .lib。

其中,xxx 代指我们为该库起的名字,比如 Linux 系统自带的一些静态链接库名称为 libc.a、libgcc.a、libm.a,它们的名称分别为 c、gcc 和 m。

下面,我们尝试将 add.o、sub.o 和 div.o 打包到一个静态链接库中:

1
2
3
[root@bogon demo]# ar rcs libmymath.a add.o sub.o div.o
[root@bogon demo]# ls
add.c add.o div.c div.o libmymath.a main.c sub.c sub.o test.h

其中,libmymath.a 就是 add.o、sub.o 和 div.o 一起打包生成的静态链接库,mymath 是我们自定义的库名。

通过以上 2 步操作,我们就成功创建出了 libmymath.a 静态链接库。那么,该如何使用它呢?

静态链接库的使用

静态链接库的使用很简单,就是在程序的链接阶段,将静态链接库和其他目标文件一起执行链接操作,从而生成可执行文件。

以 demo 项目为例,首先我们将 main.c 文件编译为目标文件:

1
2
3
4
[root@bogon demo]# gcc -c main.c
[root@bogon demo]# ls
add.c div.c libmymath.a main.o sub.c
test.h add.o div.o main.c sub.o

在此基础上,我们可以直接执行如下命令,即可完成链接操作:

1
2
3
4
[root@bogon demo]# gcc -static main.o libmymath.a
[root@bogon demo]# ls
add.c a.out div.o main.c sub.c test.h
add.o div.c libmymath.a main.o sub.o

其中,-static 选项强制 GCC 编译器使用静态链接库。

注意,如果 GCC 编译器提示无法找到 libmymath.a,还可以使用如下方式完成链接操作:

1
2
3
4
[root@bogon demo]# gcc main.o -static -L /root/demo/ -lmymath
[root@bogon demo]# ls
add.c a.out div.o main.c sub.c test.h
add.o div.c libmymath.a main.o sub.o

其中,-L(大写的 L)选项用于向 GCC 编译器指明静态链接库的存储位置(可以借助 pwd 指令查看具体的存储位置); -l(小写的 L)选项用于指明所需静态链接库的名称,注意这里的名称指的是 xxx 部分,且建议将 -l 和 xxx 直接连用(即 -lxxx),中间不需有空格。

由此,就生成了 a.out 可执行文件:

1
2
3
4
5
[root@bogon demo]# ./a.out
Input two numbers: 10 2
10+2=12
10-2=8
10÷2=5

用GCC制作动态链接库

假设当前有一个 C 语言项目,其目录结构如下所示:

1
2
3
4
5
6
7
8
demo项目
├─ headers
│ └─ test.h
└─ sources
├─ add.c
├─ sub.c
├─ div.c
└─ main.c

其中,headers 用于表示该项目拥有的所有头文件;sources 表示该项目拥有的所用源文件,读者可按照此目录结构构建 demo 项目,也可以将所有文件统一放置在 demo 项目下,本节选择的是后者,即将所有文件统一放置在 demo 目录下。

如下是该目录的具体构成和相关源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
[root@bogon demo]# ls                       <- demo 目录结构
add.c div.c main.c sub.c test.h
[root@bogon demo]# cat test.h <- test.h 文件内容
#ifndef __TEST_H_
#define __TEST_H_

int add(int a,int b);
int sub(int a,int b);
int div(int a,int b);

#endif
[root@bogon demo]# cat add.c <- add.c 文件内容
#include “test.h”
int add(int a,int b)
{
return a + b;
}
[root@bogon demo]# cat sub.c <- sub.c 文件内容
#include “test.h”
int sub(int a,int b)
{
return a - b;
}
[root@bogon demo]# cat div.c <- div.c 文件内容
#include “test.h”
int div(int a,int b)
{
return a / b;
}
[root@bogon demo]# cat main.c <- main.c 文件内容
#include <stdio.h>
#include "test.h" //必须引入头文件
int main(void)
{
int m, n;
printf("Input two numbers: ");
scanf("%d %d", &m, &n);
printf("%d+%d=%d\n", m, n, add(m, n));
printf("%d-%d=%d\n", m, n, sub(m, n));
printf("%d÷%d=%d\n", m, n, div(m, n));
return 0;
}
[root@bogon demo]#

动态链接库的创建

总的来说,动态链接库的创建方式有 2 种。

① 直接使用源文件创建动态链接库,采用 gcc 命令实现的基本格式如下:

1
gcc -fpic -shared 源文件名... -o 动态链接库名

其中,-shared 选项用于生成动态链接库;-fpic(还可写成 -fPIC)选项的功能是,令 GCC 编译器生成动态链接库(多个目标文件的压缩包)时,表示各目标文件中函数、类等功能模块的地址使用相对地址,而非绝对地址。这样,无论将来链接库被加载到内存的什么位置,都可以正常使用。

例如,由 demo 项目中的 add.c、sub.c 和 div.c 这 3 个源文件生成一个动态链接库,执行命令为:

1
2
3
4
5
[root@bogon demo]# ls
add.c div.c main.c sub.c test.h
[root@bogon demo]# gcc -fpic -shared add.c sub.c div.c -o libmymath.so
[root@bogon demo]# ls
add.c div.c libmymath.so main.c sub.c test.h

注意,动态链接库的命令规则和静态链接库完全相同,只不过在 Linux 发行版系统中,其后缀名用 .so 表示;Windows 系统中,后缀名为 .dll。

②先使用 gcc -c 指令将指定源文件编译为目标文件。仍以 demo 项目中的 add.c、sub.c 和 div.c 为例,先执行如下命令:

1
2
3
4
5
[root@bogon demo]# ls
add.c div.c main.c sub.c test.h
[root@bogon demo]# gcc -c -fpic add.c sub.c div.c
[root@bogon demo]# ls
add.c add.o div.c div.o main.c sub.c sub.o test.h

注意,为了后续生成动态链接库并能正常使用,将源文件编译为目标文件时,也需要使用 -fpic 选项。

在此基础上,接下来利用上一步生成的目标文件,生成动态链接库:

1
2
3
[root@bogon demo]# gcc -shared add.o sub.o div.o -o libmymath.so
[root@bogon demo]# ls
add.c add.o div.c div.o libmymath.so main.c sub.c sub.o test.h

以上 2 种操作,生成的动态链接库是完全一样的,读者任选一种即可。

动态链接库的使用

通过前面章节的学习我们知道,动态链接库的使用场景就是和项目中其它源文件或目标文件一起参与链接。以 demo 项目为例,前面我们将 add.c、sub.c 和 div.c 打包到了 libmymath. so 动态链接库中,此时该项目中仅剩 main.c 源程序文件,因此执行 demo 项目也就演变成了将 main.c 和 libmymath. so 进行链接,进而生成可执行文件。

注意,test.h 头文件并不直接参与编译,因为在程序的预处理阶段,已经对项目中需要用到的头文件做了处理。

执行如下指令,即可借助动态链接库成功生成可执行文件:

1
2
3
[root@bogon demo]# gcc main.c  libmymath.so -o main.exe
[root@bogon demo]# ls
add.c div.c libmymath.so main.c main.exe sub.c test.h

注意,生成的 main.exe 通常无法直接执行,例如:

1
2
[root@bogon demo]# ./main.exe
./a.out: error while loading shared libraries: libd.so: cannot open shared object file: No such file or directory

可以看到,执行过程中无法找到 libmymath.so 动态链接库。通过执行ldd main.exe指令,可以查看当前文件在执行时需要用到的所有动态链接库,以及各个库文件的存储位置:

1
2
3
4
5
[root@bogon demo]# ldd main.exe
linux-vdso.so.1 => (0x00007fff423ff000)
libmymath.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00000037e2c00000)
/lib64/ld-linux-x86-64.so.2 (0x00000037e2800000)

可以看到,main.exe 文件的执行需要 4 个动态链接库的支持,其中就包括 libmymath.so,但该文件无法找到,因此 main.exe 执行会失败。

运行由动态链接库生成的可执行文件时,必须确保程序在运行时可以找到这个动态链接库。常用的解决方案有如下几种:

  • 将链接库文件移动到标准库目录下(例如 /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指令(此方式仅对当前登陆用户有效)。

本操作系统(CentOS 6.5 64 位)中,只需要将 libmymath.so 库文件移动 /usr/lib64 或者 /lib64 目录下,即可使 main.exe 成功执行:

1
2
3
4
5
6
7
8
9
10
[root@bogon demo]# ldd main.exe
linux-vdso.so.1 => (0x00007fff06fb3000)
libmymath.so => /lib64/libmymath.so (0x00007f65b2a62000)
libc.so.6 => /lib64/libc.so.6 (0x00000037e2c00000)
/lib64/ld-linux-x86-64.so.2 (0x00000037e2800000)
[root@bogon demo]# ./main.exe
Input two numbers: 10 2
10+2=12
10-2=8
10÷2=5

显示调用C/C++动态链接库

《动态链接库的创建和使用》一节给读者详细介绍了如何创建一个动态链接库,以及如何使用它完成程序的链接和运行。但需要指明的是,这只是动态链接库的其中一种用法,本节将讲解它的另外一种用法。

总的来讲,动态链接库的调用方式有 2 种,分别是:

  • 隐式调用(静态调用):将动态链接库和其它源程序文件(或者目标文件)一起参与链接;
  • 显式调用(动态调用):手动调用动态链接库中包含的资源,同时用完后要手动将资源释放。

显式调用动态链接库的过程,类似于使用 malloc() 和 free()(C++ 中使用 new 和 delete)管理动态内存空间,需要时就申请,不需要时就将占用的资源释放。由此可见,显式调用动态链接库对内存的使用更加合理。

显式调用动态链接库,更常应用于一些大型项目中。

那么,C/C++ 程序中如何实现显式地调用动态链接库呢?接下来就给大家做详细地讲解。

和隐式调用动态链接库不同,在 C/C++ 程序中显示调用动态链接库时,无需引入和动态链接库相关的头文件。但与此同时,程序中需要引入另一个头文件,即 <dlfcn.h> 头文件,因为要显式调用动态链接库,需要使用该头文件提供的一些函数。

#include <dlfcn.h>

该头文件中,以下几个函数是显式调用动态链接库时常用的:

  1. 类似于读写文件前必须先打开文件,要想显示调用某个动态链接库提供的资源,首先要做的就是打开该库文件。

打开库文件,其本质就是将库文件装载到内存中,为后续使用做准备。

打开动态库文件,需要借助 dlopen() 函数,其语法格式为:

void _dlopen (const char _filename, int flag);

其中,filename 参数用于表明目标库文件的存储位置和库名;flag 参数的值有 2 种:

RTLD_NOW:将库文件中所有的资源都载入内存;

RTLD_LAZY:暂时不降库文件中的资源载入内存,使用时才载入。

值得一提的是,对于 filename 参数,如果用户提供的是以 / 开头,即以绝对路径表示的文件名,则函数会前往该路径下查找库文件;反之,如果用户仅提供文件名,则该函数会依次前往 LD_LIBRARY_PATH 环境变量指定的目录、/etc/ld.so.cache 文件中指定的目录、/usr/lib、/usr/lib64、/lib、/lib64 等默认搜索路径中查找。

  1. 借助 dlsym() 函数可以获得指定函数在内存中的位置,其语法格式为:

void _dlsym(void _handle, char *symbol);

其中,hanle 参数表示指向已打开库文件的指针;symbol 参数用于指定目标函数的函数名。

如果 dlsym() 函数成功找到指定函数,会返回一个指向该函数的指针;反之如果查找失败,函数会返回 NULL。

3.
和 dlopen() 相对地,借助 dlclose() 函数可以关闭已打开的动态链接库。该函数的语法格式如下:

int dlclose (void *handle);

其中,handle 表示已打开的库文件指针。当函数返回 0 时,表示函数操作成功;反之,函数执行失败。

注意,调用 dlclose() 函数并不一定会将目标库彻底释放,它只会是目标库的引用计数减 1,当引用计数减为 0 时,库文件所占用的资源才会被彻底释放。

4.
借助 dlerror() 函数,我们可以获得最近一次 dlopen()、dlsym() 或者 dlclose() 函数操作失败的错误信息。该函数的语法格式如下:

const char *dlerror(void);

可以看到,该函数不需要传递任何参数。同时,如果函数返回 NULL,则表明最近一次操作执行成功。

下面通过一个 C 语言项目实例,给大家演示显式调用动态链接库的具体实现过程。这里仍引用前面章节中创建的 demo 项目,其项目结构如下:

demo项目

├─ headers

│     └─ test.h

└─ sources

├─ add.c

├─ sub.c

├─ div.c

└─ main.c

项目中各个文件包含的代码如下:

[root@bogon demo]# cat test.h

#ifndef _TEST_H

#define _TEST_H

int add(int a,int b);

int sub(int a,int b);

int div(int a,int b);

#endif

[root@bogon demo]# cat add.c

#include “test.h”

int add(int a,int b)

{

return a + b;

}

[root@bogon demo]# cat sub.c

#include “test.h”

int sub(int a,int b)

{

return a - b;

}

[root@bogon demo]# cat div.c

#include “test.h”

int div(int a,int b)

{

return a / b;

}

以上这些文件中的代码,和之前一样,没有任何变化。对于 add.c、sub.c 和 div.c 这 3 个源文件,我们可以将它们打包生成一个动态链接库:

[root@bogon demo]# gcc -fpic -shared add.c sub.c div.c -o libmymath.so

[root@bogon demo]# ls

add.c  div.c  libmymath.so  main.c  sub.c  test.h

接下来重点分析 main.c 主程序文件的代码:

[root@bogon demo]# cat main.c

#include <stdio.h>

#include <dlfcn.h>

int main()

{

int m,n;

//打开库文件

void* handler = dlopen(“libmymath.so”,RTLD_LAZY);

if(dlerror() != NULL){

printf(“%s”,dlerror());

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//获取库文件中的 add() 函数
int(*add)(int,int)=dlsym(handler,"add");
if(dlerror()!=NULL){
printf("%s",dlerror());
}

//获取库文件中的 sub() 函数
int(*sub)(int,int)=dlsym(handler,"sub");
if(dlerror()!=NULL){
printf("%s",dlerror());
}

//获取库文件中的 div() 函数
int(*div)(int,int)=dlsym(handler,"div");
if(dlerror()!=NULL){
printf("%s",dlerror());
}
//使用库文件中的函数实现相关功能
printf("Input two numbers: ");
scanf("%d %d", &m, &n);
printf("%d+%d=%d\n", m, n, add(m, n));
printf("%d-%d=%d\n", m, n, sub(m, n));
printf("%d÷%d=%d\n", m, n, div(m, n));
//关闭库文件
dlclose(handler);
return 0;

}

首先,该程序中并没有引入 test.h 头文件,因为对于显式调用动态链接库来说,并不需要引入它。与此同时,在使用库文件中的相关函数之前,我们需要先调用 dlopen() 函数打开库文件,然后才能通过 dlsym() 函数找到相关的函数。另外,最后不要忘记调用 dlclose() 函数关闭库文件。

通过执行如下指令,即可生成相应的可执行文件:

[root@bogon demo]# gcc main.c -ldl -o main.exe

[root@bogon demo]# ls

add.c  div.c  libmymath.so  main.c  main.exe  sub.c  test.h

注意,这里需要添加 -ldl 选项(该可执行程序需要 libdl.so 动态库的支持)。运行 main.exe,其执行结果为:

[root@bogon demo]# ./main.exe

Input two numbers: 10 2

10+2=12

10-2=8

10÷2=5

GCC找不到库文件怎么办?

我们已经了解了如何创建静态链接库和动态链接库,并学会了如何使用它们。但即便是相同的操作,由于所用操作系统的环境不同,很多读者在实操中会碰到各种各样的问题,其中 GCC 编译器提示“找不到库文件(No such file or directory)”,就是最常见的问题之一。

关于 GCC 提示找不到指定库文件的这个问题,通常出现在以下 2 个场景中:

  • 利用静态库或者动态库文件实现链接操作(生成可执行文件)时,GCC 可能会提示“xxx:No such file or directory”(其中 xxx 表示查找失败的静态库或者动态库);
  • 执行借助动态库生成的可执行文件时,GCC 可能会提示“./main.exe: error while loading shared libraries: xxx.so: cannot open shared object file: No such file or directory”(其中 xxx 表示动态库的文件名)。

本节将就以上这 2 种情况,给读者分析 GCC 编译器查找库文件失败的原因,同时会给出相应的解决方案。

GCC生成可执行文件时找不到库文件

要想彻底解决这个问题,读者就必须先了解在生成可执行文件时,GCC 编译器默认的查找库文件的路径。

通过前面的学习我们知道,程序链接阶段指明所用库文件的方式有 2 种。假设当前 mian.c 文件需要借助 libmymath.a 才能完成链接,则完成链接操作的 gcc 指令有以下 2 种写法:

1
2
[root@bogon demo]# gcc -static main.c libmymath.a -o main.exe
[root@bogon demo]# gcc -static main.c -lmymath -o main.exe

当以第一种写法完成链接操作时,GCC 编译器只会在当前目录中(这里为 demo 目录)查找 libmymath.a 静态链接库;反之,如果使用 -l(小写的 L)选项指明了要查找的静态库的文件名,则 GCC 编译器会按照如下顺序,依次到指定目录中查找所需库文件:

  1. 如果 gcc 指令使用 -L 选项指定了查找路径,则 GCC 编译器会优先选择去该路径下查找所需要的库文件;
  2. 再到 Linux 系统中 LIBRARY_PATH 环境变量指定的路径中搜索需要的库文件;
  3. 最后到 GCC 编译器默认的搜索路径(比如 /lib、/lib64、/usr/lib、/usr/lib64、/usr/local/lib、/usr/local/lib64 等,不同系统环境略有差异)中查找。

如果读者使用第一种方法完成链接操作,但 GCC 编译器提示找不到所需库文件,表明所用库文件并未存储在当前路径下,解决方案就是手动找到库文件并将其移至当前路径,然后重新执行链接操作。

反之,如果读者使用的是第二种方法,也遇到了 GCC 编译器提示未找到所需库文件,表明库文件的存储路径不对,解决方案有以下 3 种:

  • 手动找到该库文件,并在 gcc 指令中用 -L 选项明确指明其存储路径。比如 libmymath.a 静态库文件存储在 /usr 目录下,则完成链接操作的 gcc 指令应为gcc -static main.c -L/usr -lmymath -o main.exe;
  • 将库文件的存储路径添加到 LIBRARY_PATH 环境变量中。仍以库文件存储在 /usr 目录下,则通过执行export LIBRARY_PATH=$LIBRARY_PATH:/usr指令,即可将 /usr 目录添加到该环境变量中(此方式仅在当前命令行窗口中有效);
  • 将库文件移动到 GCC 编译器默认的搜索路径中。

GCC运行可执行文件时找不到动态库文件

执行已生成的可执行文件时,如果 GCC 编译器提示找不到所需的库文件,这意味着 GCC 编译器无法找到支持可执行文件运行的某些动态库文件。

事实上,当 GCC 编译器运行可执行文件时,会按照如下的路径顺序搜索所需的动态库文件:

  1. 如果在生成可执行文件时,用户使用了-Wl,-rpath=dir(其中 dir 表示要查找的具体路径,如果查找路径有多个,中间用 : 冒号分隔)选项指定动态库的搜索路径,则运行该文件时 GCC 会首先到指定的路径中查找所需的库文件;
  2. GCC 编译器会前往 LD_LIBRARY_PATH 环境变量指明的路径中查找所需的动态库文件;
  3. GCC 编译器会前往 /ect/ld.so.conf 文件中指定的搜索路径查找动态库文件;
  4. GCC 编译器会前往默认的搜索路径中(例如 /lib、/lib64、/usr/lib、/usr/lib64 等)中查找所需的动态库文件。

注意,可执行文件的当前存储路径,并不在默认的搜索路径范围内,因此即便将动态库文件和可执行文件放在同一目录下,GCC 编译器也可能提示“找不到动态库”。

因此,对于 GCC 运行可执行文件时提示找不到动态库文件的问题,常用的解决方法是:

  • 将动态库文件的存储路径,添加到 LD_LIBRARY_PATH 环境变量中。假设动态库文件存储在 /usr 目录中,通知执行export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr指令,即可实现此目的(此方式仅在当前命令行窗口中有效);
  • 修改动态库文件的存储路径,即将其移动至 GCC 编译器默认的搜索路径中。
  • 修改~/.bashrc 或 ~/.bash_profile 文件,即在文件最后一行添加export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx(xxx 为动态库文件的绝对存储路径)。保存之后,执行 source .bashrc 指令(此方式仅对当前登陆用户有效)。

值得一提的是,GCC 编译器提供有 ldd 指令,借助该指令,我们可以明确知道某个可执行文件需要哪些动态库文件做支撑、这些动态库文件是否已经找到、各个动态库文件的具体存储路径等信息。

以《动态链接库的创建和使用》一节中生成的 main.exe 可执行文件为例,执行如下 ldd 指令:

1
2
3
4
5
[root@bogon demo]# ldd main.exe
linux-vdso.so.1 => (0x00007fff06fb3000)
libmymath.so => /lib64/libmymath.so (0x00007f65b2a62000)
libc.so.6 => /lib64/libc.so.6 (0x00000037e2c00000)
/lib64/ld-linux-x86-64.so.2 (0x00000037e2800000)

注意,如果某个动态库文件未找到,则 => 后面会显示 not found,表明 GCC 编译器无法找到该动态库,此时该可执行文件将无法执行。

GCC -Wall

当GCC在编译过程中检查出错误的话,它就会中止编译;但检测到警告时却能继续编译生成可执行程序,因为警告只是针对程序结构的诊断信息,它不能说明程序一定有错误,而是存在风险,或者可能存在错误。虽然GCC提供了非常丰富的警告,但前提是你已经启用了它们,否则它不会报告这些检测到的警告。

在众多的警告选项之中,最常用的就是-Wall选项。该选项能发现程序中一系列的常见错误警告,该选项用法举例如下:

1
$ gcc -Wall test.c -o test

该选项相当于同时使用了下列所有的选项:

◆unused-function:遇到仅声明过但尚未定义的静态函数时发出警告。

◆unused-label:遇到声明过但不使用的标号的警告。

◆unused-parameter:从未用过的函数参数的警告。

◆unused-variable:在本地声明但从未用过的变量的警告。

◆unused-value:仅计算但从未用过的值得警告。

◆Format:检查对printf和scanf等函数的调用,确认各个参数类型和格式串中的一致。

◆implicit-int:警告没有规定类型的声明。

◆implicit-function-:在函数在未经声明就使用时给予警告。

◆char-subscripts:警告把char类型作为数组下标。这是常见错误,程序员经常忘记在某些机器上char有符号。

◆missing-braces:聚合初始化两边缺少大括号。

◆Parentheses:在某些情况下如果忽略了括号,编译器就发出警告。

◆return-type:如果函数定义了返回类型,而默认类型是int型,编译器就发出警告。同时警告那些不带返回值的 return语句,如果他们所属的函数并非void类型。

◆sequence-point:出现可疑的代码元素时,发出报警。

◆Switch:如果某条switch语句的参数属于枚举类型,但是没有对应的case语句使用枚举元素,编译器就发出警告(在switch语句中使用default分支能够防止这个警告)。超出枚举范围的case语句同样会导致这个警告。

◆strict-aliasing:对变量别名进行最严格的检查。

◆unknown-pragmas:使用了不允许的#pragma。

◆Uninitialized:在初始化之前就使用自动变量。

需要注意的是,各警告选项既然能使之生效,当然也能使之关闭。比如假设我们想要使用-Wall来启用个选项,同时又要关闭unused警告,利益通过下面的命令来达到目的:

$ gcc -Wall -Wno-unused test.c -o test

下面是使用-Wall选项的时候没有生效的一些警告项:

◆cast-align:一旦某个指针类型强制转换时,会导致目标所需的地址对齐边界扩展,编译器就发出警告。例如,某些机器上只能在2或4字节边界上访问整数,如果在这种机型上把char _强制转换成int _类型, 编译器就发出警告。

◆sign-compare:将有符号类型和无符号类型数据进行比较时发出警告。

◆missing-prototypes :如果没有预先声明函数原形就定义了全局函数,编译器就发出警告。即使函数定义自身提供了函数原形也会产生这个警告。这样做的目的是检查没有在头文件中声明的全局函数。

◆Packed:当结构体带有packed属性但实际并没有出现紧缩式给出警告。

◆Padded:如果结构体通过充填进行对齐则给出警告。

◆unreachable-code:如果发现从未执行的代码时给出警告。

◆Inline:如果某函数不能内嵌(inline),无论是声明为inline或者是指定了-finline-functions 选项,编译器都将发出警告。

◆disabled-optimization:当需要太长时间或过多资源而导致不能完成某项优化时给出警告。

上面是使用-Wall选项时没有生效,但又比较常用的一些警告选项。本文中要介绍的最后一个常用警告选项是-Werror。使用该选项后,GCC发现可疑之处时不会简单的发出警告就算完事,而是将警告作为一个错误而中断编译过程。该选项在希望得到高质量代码时非常有用。

冗余编译文件 GGC -v

将编译过程打印出来,方便排错。

-v verbose冗余

GCC -I/usr/

链接顺序

重编译重链接

Warning选项

Debug

gprof和gcov

C语言注释最好使用预处理条件编译#if 0 … #endif …

参考

  1. http://gcc.gnu.org/
  2. http://www.boobooke.com/
  3. c.biancheng.net
  4. 《An Introduction to GCC》