文章目录
- GCC简介
- 单个文件编译过程
- 预处理(Preprocessing)
- 编译(Compilation)
- 汇编(Assembly)
- 链接(Linking)
- 多文件编译过程
- 头文件搜索路径
- 三种不推荐的方法
- 两种推荐的方法
- 库文件
- 静态库文件
- 创建和使用静态库
- 链接顺序
- 动态库文件
- 创建和使用动态库
- Warning编译选项
- 调试信息(-g)
- 编译优化
- 总结
- 参考文献
GCC简介
gcc(GNU Compiler Collection,GNU编译器套件)是一个功能强大、跨平台的编译器套件,用于编译C、C++、Objective-C、Fortran、Ada、Go和D等多种编程语言的源代码。它是GNU项目的一部分,遵循GPL(GNU General Public License)许可证,因此可以自由使用和分发。gcc主要有如下特点:
- 1、跨平台:gcc可以在多种操作系统上运行,包括Linux、Windows(通过MinGW)等。
- 2、优化:gcc提供了多种优化选项,可以根据需要选择不同级别的优化来生成更高效率的代码。
- 3、可扩展性:gcc支持通过插件和扩展来增加新功能或支持新的编程语言。
- 4、开源。
- 5、多语言支持:支持C、C++、Objective-C、Fortran、Ada、Go和D等语言的编译。
- 6、丰富的选项:gcc提供了大量的编译选项,允许开发者精细地控制编译过程,包括预处理、编译、汇编和链接等各个阶段。
- 7、调试支持:gcc生成的代码可以与多种调试器(如gdb)配合使用,帮助开发者定位和解决程序中的问题。
- 8、文档和社区支持:gcc拥有详细的文档和广泛的社区支持。
本文主要是介绍gcc的入门使用,gcc的安装教程请参考其他文章。
检验gcc是否安装可以使用gcc --version
。如果内容是类似下面的格式,则说明已经安装了gcc。
zld@zld:~$ gcc --version
gcc (Ubuntu 13.2.0-4ubuntu3) 13.2.0
Copyright (C) 2023 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.
单个文件编译过程
对于经典的"Hello, World"程序:
zld@zld:~/Codes/tmp0926$ cat -n hello.c
1 #include <stdio.h>
2
3 int main()
4 {
5 printf("Hello, World\n");
6 return 0;
7 }
使用gcc编译单个文件非常简单,只需要使用命令gcc <源文件>
即可,比如用gcc编译上面的hello.c,则用命令gcc hello.c
就能编译成功,gcc会生成一个a.out的可执行文件(当然也可以通过参数-o
来指定生成的文件名)
zld@zld:~/Codes/tmp0926$ gcc hello.c
zld@zld:~/Codes/tmp0926$ ll
total 28
drwxrwxr-x 2 zld zld 4096 Sep 27 11:16 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rwxrwxr-x 1 zld zld 15952 Sep 27 11:16 a.out*
-rw-rw-r-- 1 zld zld 73 Sep 27 11:14 hello.c
zld@zld:~/Codes/tmp0926$ ./a.out
Hello, World
# 通过 -o 参数生成可执行文件
zld@zld:~/Codes/tmp0926$ gcc hello.c -o hello
zld@zld:~/Codes/tmp0926$ ll
total 44
drwxrwxr-x 2 zld zld 4096 Sep 27 11:22 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rwxrwxr-x 1 zld zld 15952 Sep 27 11:16 a.out*
-rwxrwxr-x 1 zld zld 15952 Sep 27 11:22 hello*
-rw-rw-r-- 1 zld zld 73 Sep 27 11:14 hello.c
zld@zld:~/Codes/tmp0926$ ./hello
Hello, World
上面的编译看似很简单,但其实这个过程可以分为4个阶段,分别是预处理(preprocessing)、编译(compilation)、汇编(assembly)和链接(linking)。
默认情况下,gcc编译不会保存中间结果,我们可以通过选项-save-temps
来保存中间结果。如下所示,当加了-save-temps
选项后,除了生成最终的可执行文件a.out
外,还有以下几个文件a-hello.i
、a-hello.s
、a-hello.o
这其实就是上面几个阶段生成的中间结果。其中,.i
文件是预处理之后产生的结果,.s
是编译后产生的结果,.o
是汇编产生的结果。
zld@zld:~/Codes/tmp0926$ gcc hello.c -save-temps
zld@zld:~/Codes/tmp0926$ ll
total 56
drwxrwxr-x 2 zld zld 4096 Sep 27 11:28 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 19640 Sep 27 11:28 a-hello.i
-rw-rw-r-- 1 zld zld 1496 Sep 27 11:28 a-hello.o
-rw-rw-r-- 1 zld zld 659 Sep 27 11:28 a-hello.s
-rwxrwxr-x 1 zld zld 15952 Sep 27 11:28 a.out*
-rw-rw-r-- 1 zld zld 73 Sep 27 11:14 hello.c
因此,对于上述hello.c
文件,用gcc编译的过程可以用下图说明:
下面将结合hello.c
源程序详细讲述这4个阶段的处理过程。
预处理(Preprocessing)
gcc编译过程的第一阶段就是预处理,预处理过程主要处理那些源代码文件中的以"#“开始的预编译指令。比如”#include"、"#define"等,主要处理规则如下:
- 将所有的"#define"删除,并且展开所有的宏定义。
- 处理所有条件预编译指令,比如"#if"、“#ifdef”、“#elif”、“#else”、“#endif”。
- 处理"#include"预编译指令,将被包含的文件插入到该预编译指令的位置。注意,这个过程是递归进行的,也就是说被包含的文件可能还包含其他文件。
- 删除所有的注释"//“和”/* */"。
- 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号。
- 保留所有的#pragma编译器指令,因为编译器需要使用它们。
经过预处理之后,生成的.i
文件不包含任何宏定义,因为所有的宏已经被展开,并且包含的文件也已经被插入到.i
文件中。
预处理阶段是用预处理器cpp程序处理的,可以用以下命令处理预处理过程。(-E表示只进行预处理)。
cpp hello.c > hello.i
或者
gcc -E hello.c -o hello.i
对上述的hello.c
程序进行预处理:
zld@zld:~/Codes/tmp0926$ ll
total 12
drwxrwxr-x 2 zld zld 4096 Sep 27 11:39 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 73 Sep 27 11:14 hello.c
zld@zld:~/Codes/tmp0926$ gcc -E hello.c -o hello1.i
zld@zld:~/Codes/tmp0926$ cpp hello.c > hello2.i
zld@zld:~/Codes/tmp0926$ ll
total 52
drwxrwxr-x 2 zld zld 4096 Sep 27 11:44 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 19640 Sep 27 11:43 hello1.i
-rw-rw-r-- 1 zld zld 19640 Sep 27 11:44 hello2.i
-rw-rw-r-- 1 zld zld 73 Sep 27 11:14 hello.c
预处理之后的结果如下:
zld@zld:~/Codes/tmp0926$ cat hello1.i
...
extern int __uflow (FILE *);
extern int __overflow (FILE *, int);
# 967 "/usr/include/stdio.h" 3 4
# 2 "hello.c" 2
# 3 "hello.c"
int main()
{
printf("Hello, World\n");
return 0;
}
对于上述的例子可能还不好理解,我们再举一个例子。
对于下面的一段程序test1.c
。
zld@zld:~/Codes/tmp0926$ cat -n test1.c
1 #define TEST "hello,world"
2
3 /* This is a comments */
4 const char str[] = TEST;
我们的预处理结果如下:
zld@zld:~/Codes/tmp0926$ cpp test1.c > test1.i
zld@zld:~/Codes/tmp0926$ ll
total 60
drwxrwxr-x 2 zld zld 4096 Sep 27 12:11 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 19640 Sep 27 11:43 hello1.i
-rw-rw-r-- 1 zld zld 19640 Sep 27 11:44 hello2.i
-rw-rw-r-- 1 zld zld 73 Sep 27 11:14 hello.c
-rw-rw-r-- 1 zld zld 78 Sep 27 12:09 test1.c
-rw-rw-r-- 1 zld zld 165 Sep 27 12:11 test1.i
zld@zld:~/Codes/tmp0926$ cat test1.i
# 0 "test1.c"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "test1.c"
const char str[] = "hello,world";
从上面的结果可以看到,源程序test1.c
的第1行的宏定义#define TEST "hello,world"
以及第3行的注释/* This is a comments */
已经被删除了,并且在源程序的第四行 const char str[] = TEST;
已经将该宏定义展开了。
编译(Compilation)
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生成相应的汇编代码文件(简单来说,编译就是将预处理之后的源代码翻译成汇编代码)。这是整个构建过程的核心部分。对于编译过程,gcc使用的是ccl程序来完成的。
编译过程相当于如下命令(-S表示执行编译后停止,不进行汇编和链接):
gcc -S hello.i -o hello.s
或者
gcc -S hello.c -o hello.s
对于hello.c
程序,执行结果如下:
zld@zld:~/Codes/tmp0926$ gcc -S hello.i -o hello.s
zld@zld:~/Codes/tmp0926$ ll
total 40
drwxrwxr-x 2 zld zld 4096 Sep 27 12:24 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 73 Sep 27 11:14 hello.c
-rw-rw-r-- 1 zld zld 19640 Sep 27 11:43 hello.i
-rw-rw-r-- 1 zld zld 659 Sep 27 12:24 hello.s
-rw-rw-r-- 1 zld zld 78 Sep 27 12:09 test1.c
如果想看详细过程,还可以加上选项-v
,它表示显示gcc执行时的详细过程。
从上面可以看到在编译过程实际是由ccl程序执行的,因此也可以直接用ccl程序执行:
zld@zld:~/Codes/tmp0926$ /usr/libexec/gcc/x86_64-linux-gnu/13/cc1 hello.i
main
Analyzing compilation unit
Performing interprocedural optimizations
<*free_lang_data> {heap 920k} <visibility> {heap 920k} <build_ssa_passes> {heap 920k} <opt_local_passes> {heap 1224k} <remove_symbols> {heap 1224k} <targetclone> {heap 1224k} <free-fnsummary> {heap 1224k}Streaming LTO
<whole-program> {heap 1224k} <fnsummary> {heap 1224k} <inline> {heap 1224k} <modref> {heap 1224k} <free-fnsummary> {heap 1224k} <single-use> {heap 1224k} <comdats> {heap 1224k}Assembling functions:
<simdclone> {heap 1224k} main
Time variable usr sys wall GGC
phase setup : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 33%) 1819k ( 80%)
phase parsing : 0.00 ( 0%) 0.01 (100%) 0.01 ( 33%) 403k ( 18%)
phase opt and generate : 0.01 (100%) 0.00 ( 0%) 0.01 ( 33%) 60k ( 3%)
callgraph optimization : 0.01 (100%) 0.00 ( 0%) 0.00 ( 0%) 0 ( 0%)
callgraph ipa passes : 0.01 (100%) 0.00 ( 0%) 0.00 ( 0%) 4880 ( 0%)
preprocessing : 0.00 ( 0%) 0.01 (100%) 0.00 ( 0%) 35k ( 2%)
parser (global) : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 33%) 351k ( 15%)
final : 0.00 ( 0%) 0.00 ( 0%) 0.01 ( 33%) 2120 ( 0%)
TOTAL : 0.01 0.01 0.03 2283k
汇编(Assembly)
汇编器就是将编译生成的汇编代码(.s)转变成机器可以执行的指令,在Linux系统上一般表现为ELF目标文件(OBJ文件)(简单来说,就是将汇编代码翻译成机器码),用到的汇编器工具为as。
汇编过程用到的命令如下:
gcc -c hello.s -o hello.o
或者
as hello.s -o hello.o
对于hello.c
程序,执行结果如下:
zld@zld:~/Codes/tmp0926$ gcc -c hello.s -o hello.o
zld@zld:~/Codes/tmp0926$ as hello.s -o hello2.o
zld@zld:~/Codes/tmp0926$ ll
total 52
drwxrwxr-x 2 zld zld 4096 Sep 27 12:51 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 1376 Sep 27 12:51 hello2.o
-rw-rw-r-- 1 zld zld 659 Sep 27 12:27 hello2.s
-rw-rw-r-- 1 zld zld 73 Sep 27 11:14 hello.c
-rw-rw-r-- 1 zld zld 19640 Sep 27 11:43 hello.i
-rw-rw-r-- 1 zld zld 1376 Sep 27 12:51 hello.o
-rw-rw-r-- 1 zld zld 474 Sep 27 12:40 hello.s
-rw-rw-r-- 1 zld zld 78 Sep 27 12:09 test1.c
详细执行过程:
链接(Linking)
链接就是将上步生成的OBJ文件和系统库的OBJ文件、库文件链接起来,最终生成了可以在特定平台运行的可执行文件,用到的工具为ld或collect2。
链接的命令如下:
zld@zld:~/Codes/tmp0926$ gcc hello.o -o hello
zld@zld:~/Codes/tmp0926$ ll
total 68
drwxrwxr-x 2 zld zld 4096 Sep 27 13:11 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rwxrwxr-x 1 zld zld 15880 Sep 27 13:11 hello*
-rw-rw-r-- 1 zld zld 1376 Sep 27 12:51 hello2.o
-rw-rw-r-- 1 zld zld 659 Sep 27 12:27 hello2.s
-rw-rw-r-- 1 zld zld 73 Sep 27 11:14 hello.c
-rw-rw-r-- 1 zld zld 19640 Sep 27 11:43 hello.i
-rw-rw-r-- 1 zld zld 1376 Sep 27 12:52 hello.o
-rw-rw-r-- 1 zld zld 474 Sep 27 12:40 hello.s
-rw-rw-r-- 1 zld zld 78 Sep 27 12:09 test1.c
其详细执行过程:
多文件编译过程
上一章节是讲的单个源文件的编译过程。在一个实际的项目中,一般情况下是包含多个源文件的。本章将介绍多个文件的编译过程。
我们首先将上一章的hello.c
程序改造一下:该目录下有三个文件分别是hello.c
、hello.h
、main.c
,其中,hello.c
中定义了一个函数hello(),其功能就是简单的打印字符串。hello.h
头文件中就是声明了hello()函数。在main.c
中则是定义了main()函数,在main函数中调用了hello.c
文件中的hello()函数。
zld@zld:~/Codes/tmp0926$ tree
.
├── hello.c
├── hello.h
└── main.c
zld@zld:~/Codes/tmp0926$ cat hello.c
#include <stdio.h>
#include "hello.h"
void hello(const char *string)
{
printf("%s\n", string);
}
zld@zld:~/Codes/tmp0926$ cat hello.h
void hello(const char *string);
zld@zld:~/Codes/tmp0926$ cat main.c
#include <stdio.h>
#include "hello.h"
int main()
{
hello("hello,world");
return 0;
}
对于多个源文件的编译,有两种方式,第1种就是将多个源程序一起编译生成可执行程序;第2种方法就是分别编译生成多个目标文件,然后再将这些目标文件再链接到一起生成可执行程序。
第1种方法:将多个源程序一起编译生成可执行程序。
zld@zld:~/Codes/tmp0926$ gcc hello.c main.c -o hello
zld@zld:~/Codes/tmp0926$ ll
total 36
drwxrwxr-x 2 zld zld 4096 Sep 27 13:48 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rwxrwxr-x 1 zld zld 16016 Sep 27 13:48 hello*
-rw-rw-r-- 1 zld zld 99 Sep 27 13:31 hello.c
-rw-rw-r-- 1 zld zld 32 Sep 27 13:31 hello.h
-rw-rw-r-- 1 zld zld 88 Sep 27 13:33 main.c
第2种方法:分别编译生成多个目标文件,然后再将这些目标文件再链接到一起生成可执行程序。
zld@zld:~/Codes/tmp0926$ gcc -c hello.c -o hello.o
zld@zld:~/Codes/tmp0926$ gcc -c main.c -o main.o
zld@zld:~/Codes/tmp0926$ ll
total 28
drwxrwxr-x 2 zld zld 4096 Sep 27 13:50 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 99 Sep 27 13:31 hello.c
-rw-rw-r-- 1 zld zld 32 Sep 27 13:31 hello.h
-rw-rw-r-- 1 zld zld 1360 Sep 27 13:49 hello.o
-rw-rw-r-- 1 zld zld 88 Sep 27 13:33 main.c
-rw-rw-r-- 1 zld zld 1488 Sep 27 13:50 main.o
zld@zld:~/Codes/tmp0926$ gcc hello.o main.o -o hello
zld@zld:~/Codes/tmp0926$ ll
total 44
drwxrwxr-x 2 zld zld 4096 Sep 27 13:50 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rwxrwxr-x 1 zld zld 16016 Sep 27 13:50 hello*
-rw-rw-r-- 1 zld zld 99 Sep 27 13:31 hello.c
-rw-rw-r-- 1 zld zld 32 Sep 27 13:31 hello.h
-rw-rw-r-- 1 zld zld 1360 Sep 27 13:49 hello.o
-rw-rw-r-- 1 zld zld 88 Sep 27 13:33 main.c
-rw-rw-r-- 1 zld zld 1488 Sep 27 13:50 main.o
zld@zld:~/Codes/tmp0926$ ./hello
hello,world
这里有以下几个问题值得讨论一下:
- 问题1、在上面的方法1或者方法2中,都没有用到头文件
hello.h
,为什么?
在编译过程中,不需要用到头文件,这是因为在预处理步骤中,预处理的程序会将"#include"中包含的文件插入该预处理指令的位置。
-
问题2、在预处理处理"#include"指令时,程序是在哪个目录查找到头文件的?
对于该问题,先卖个关子,在后面的例子中再讲。
-
问题3、在第2种方法中,当将多个目标文件链接再一起时,对目标文件的顺序有没有要求?
比如上面是
gcc hello.o main.o -o hello
,那能不能按照gcc main.o hello.o -o hello
的顺序呢?我们实践一下:
zld@zld:~/Codes/tmp0926$ gcc main.o hello.o -o hello2 zld@zld:~/Codes/tmp0926$ ll total 60 drwxrwxr-x 2 zld zld 4096 Sep 27 14:06 ./ drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../ -rwxrwxr-x 1 zld zld 16016 Sep 27 13:50 hello* -rwxrwxr-x 1 zld zld 16016 Sep 27 14:06 hello2* -rw-rw-r-- 1 zld zld 99 Sep 27 13:31 hello.c -rw-rw-r-- 1 zld zld 32 Sep 27 13:31 hello.h -rw-rw-r-- 1 zld zld 1360 Sep 27 13:49 hello.o -rw-rw-r-- 1 zld zld 88 Sep 27 13:33 main.c -rw-rw-r-- 1 zld zld 1488 Sep 27 13:50 main.o
实践证明,gcc对目标文件的链接顺序是没有要求。之所以讲这个,是因为有些老的链接器如果顺序不对,则有可能报“undefined references”错误,如果调整一下链接顺序,则该错误又没有了。但是现在的编译器和链接器一般是不需要考虑这个顺序的,GCC就不需要考虑。
但是,我们后面会看到,如果链接过程中有静态库文件和目标文件,则有顺序要求。
通常来说,编译所消耗的时间要比链接所消耗的时间要长,因此,如果把所有的程序都写到一个文件中,那只要有修改,则需要重新编译和链接,对于一个大型的程序来说,这是非常耗时的。但如果分成了多个文件,那我们可以只对修改的文件进行编译,然后再将编译后生成的目标文件和之前没有修改的文件的目标文件再一起链接即可,这样就能减少整个构建的时间。
头文件搜索路径
上面的例子是将3个文件(main.c、hello.c、hello.h)放到同一个目录中的。但有过实际项目的人都知道,一般不会将所有文件放到同一个目录中的,而是比如头文件放到注入include的目录中,库文件放到诸如lib的目录中,然后源文件也会基于各自的功能分别创建各自的目录的。
我们稍微改一下上面3个文件的目录。目录结构如下。将hello.c文件放到了hello目录中,将hello.h放到include目录中,而main.c和hello文件夹以及include文件夹在同一层中。
zld@zld:~/Codes/tmp0926$ tree
.
├── hello
│ └── hello.c
├── include
│ └── hello.h
└── main.c
3 directories, 3 files
此时,我们按照上面的方法1再编译一下(这里注意一下,我是在当前main.c文件所处的目录编译的,所以hello.c的相对位置就是hello/hello.c):
zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -o hello1
main.c:2:10: fatal error: hello.h: No such file or directory
2 | #include "hello.h"
| ^~~~~~~~~
compilation terminated.
hello/hello.c:2:10: fatal error: hello.h: No such file or directory
2 | #include "hello.h"
| ^~~~~~~~~
compilation terminated.
从上面可知,编译报错了,错误原因是找不到hello.h
头文件。为什么会找不到头文件呢?我们用-v
选项来看下详细信息:
zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -v -o hello1
...
#include "..." search starts here:
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/13/include
/usr/local/include
/usr/include/x86_64-linux-gnu
/usr/include
End of search list.
Compiler executable checksum: edbc28f9c9bb85637ee0b8e5b79ac141
main.c:2:10: fatal error: hello.h: No such file or directory
2 | #include "hello.h"
| ^~~~~~~~~
compilation terminated.
我只显示了关键的一些信息,从上面可以看到当搜索#include “…” 时是在当前目录下查找#include "..." search starts here:
。在搜索#include <...>
是在以下路径查找(下面的这些路径称为系统路径):
#include <...> search starts here:
/usr/lib/gcc/x86_64-linux-gnu/13/include
/usr/local/include
/usr/include/x86_64-linux-gnu
/usr/include
很显然,"hello.h"
既不在当前路径下,也不在系统路径上,所以找不到而报错。
既然知道报错的原因了,那就知道怎么解决问题了。
三种不推荐的方法
-
方法1:在
#include
中写出头文件hello.h
的绝对路径。如下所示:zld@zld:~/Codes/tmp0926/hello$ cat hello.c #include <stdio.h> // #include "hello.h" #include "/home/zld/Codes/tmp0926/include/hello.h" zld@zld:~/Codes/tmp0926$ cat main.c #include <stdio.h> // #include "hello.h" #include "/home/zld/Codes/tmp0926/include/hello.h"
修改完成后,再次编译,这次就编译成功了,而且运行正确:
zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -o hello2 zld@zld:~/Codes/tmp0926$ ll total 36 drwxrwxr-x 4 zld zld 4096 Sep 27 16:49 ./ drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../ drwxrwxr-x 2 zld zld 4096 Sep 27 16:47 hello/ -rwxrwxr-x 1 zld zld 16016 Sep 27 16:49 hello2* drwxrwxr-x 2 zld zld 4096 Sep 27 14:31 include/ -rw-rw-r-- 1 zld zld 142 Sep 27 16:46 main.c zld@zld:~/Codes/tmp0926$ ./hello2 hello,world
但这种方式非常不推荐。
- 1、可移植性差。
- 绝对路径是特定于一个具体的文件系统布局的。如果代码被移植到其他系统或环境,文件系统的结构可能不同,导致编译器无法找到头文件。
- 这使得代码在不同开发者之间、不同机器之间或不同项目之间的共享和复用变得困难。
- 2、维护困难。
- 如果头文件的位置发生变化,则所有包含绝对路径的源代码文件都需要更新。
- 这增加了维护负担,尤其是在大型项目中,头文件的位置可能会频繁变动。
- 1、可移植性差。
-
方法2:在
#include
中写出头文件hello.h
的相对路径。如下所示:zld@zld:~/Codes/tmp0926/hello$ cat hello.c #include <stdio.h> // #include "hello.h" #include "../include/hello.h" zld@zld:~/Codes/tmp0926$ cat main.c #include <stdio.h> // #include "hello.h" #include "include/hello.h"
运行结果:
zld@zld:~/Codes/tmp0926$ gcc main.c hello/hello.c -o hello3 zld@zld:~/Codes/tmp0926$ ll total 52 drwxrwxr-x 4 zld zld 4096 Sep 27 17:37 ./ drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../ drwxrwxr-x 2 zld zld 4096 Sep 27 17:36 hello/ -rwxrwxr-x 1 zld zld 16016 Sep 27 16:49 hello2* -rwxrwxr-x 1 zld zld 16016 Sep 27 17:37 hello3* drwxrwxr-x 2 zld zld 4096 Sep 27 14:31 include/ -rw-rw-r-- 1 zld zld 118 Sep 27 17:35 main.c zld@zld:~/Codes/tmp0926$ ./hello3 hello,world
这种方法比方法1要好一些,如果各个文件的相对位置不变,则也不用花费许多功夫维护(实际上,有些开源代码上有这么写)。但是如果相对位置一变,就需要修改对应的include。因此,这种方法本人也是不推荐的。
-
方法3:将头文件放到上面所说的系统路径中,然后在写
#include
语句时,就可以不想相对或绝对路径了:zld@zld:~/Codes/tmp0926/include$ cp hello.h /usr/local/include/ cp: cannot create regular file '/usr/local/include/hello.h': Permission denied zld@zld:~/Codes/tmp0926/include$ su Password: root@zld:/home/zld/Codes/tmp0926/include# cp hello.h /usr/local/include/ root@zld:/home/zld/Codes/tmp0926/hello# cat hello.c #include <stdio.h> #include "hello.h" root@zld:/home/zld/Codes/tmp0926# cat main.c #include <stdio.h> #include "hello.h"
运行结果:
root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -o hello4 root@zld:/home/zld/Codes/tmp0926# ./hello4 hello,world
但是这也有一些问题。比如,从上面可以知道,我将头文件拷贝到系统路径时,必须要root权限。此外,如果将所有的头文件都拷贝到系统路径,那这个系统路径下将包含各种各样的头文件,这样这个目录下将变得非常的杂乱不堪。因此,也是不推荐这种方法。
两种推荐的方法
上面的3种方法,我们都不太满意,那有什么更好的办法呢?我们想一下,如果我们告诉编译器去哪个路径去找头文件,那是不是事情就解决了?
有两种方法可以解决这个事情:
-
1、设置C头文件的搜索路径的环境变量。
export C_INCLUDE_PATH=/home/zld/zld/codes/0926:$C_INCLUDE_PATH
将上面的例子,用该方法编译一下,编译过程如下:
# 设置C语言头文件的环境变量 root@zld:/home/zld/Codes/tmp0926# export C_INCLUDE_PATH=$C_INCLUDE_PATH:/home/zld/Codes/tmp0926/include root@zld:/home/zld/Codes/tmp0926/include# env | grep C_INCLUDE C_INCLUDE_PATH=:/home/zld/Codes/tmp0926/include #重新编译与运行(注意:我已经将方法3中拷贝到系统路径下的hello.h文件已经删除了,防止影响) root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -o hello5 root@zld:/home/zld/Codes/tmp0926# ./hello5 hello,world
-
2、在gcc命令中加上
-Idir
选项,其中dir是头文件的路径,I是字母i的大小,并且I与dir之间没有空格。
# -I加上绝对路径
root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -I/home/zld/Codes/tmp0926/include -o hello6
root@zld:/home/zld/Codes/tmp0926# ./hello6
hello,world
# -I加上相对路径:点'.'表示当前路径
root@zld:/home/zld/Codes/tmp0926# gcc main.c hello/hello.c -I. -o hello7
上述两个方法比之前的三种方法,要方便得多。因此推荐这两种方法。在这两种方法中,在命令中加上-I选项更加常用。因为如果换到其他的环境中,那还得重新设置一下环境变量。但是命令行中加上-I选项一般可以在makefile文件中都已经写好了,因此更加方便。
对于添加环境变量的方法,对于C++来说,是用CPLUS_INCLUDE_PATH
,对于静态库文件来说是用LIBRARY_PATH
,对于动态库文件来说是LD_LIBRARY_PATH
。至于什么是静/动态库文件,本文后面会讲到。
这里做一下总结:
# C头文件搜索路径环境变量设置
C_INCLUDE_PATH=$C_INCLUDE_PATH:/xxx/yyy/zzz
export C_INCLUDE_PATH
# C++头文件搜索路径环境变量设置
CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/xxx/yyy/zzz
export CPLUS_INCLUDE_PATH
# 动态库搜索路径环境变量设置
LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/xxx/yyy/zzz/lib
export LD_LIBRARY_PATH
# 静态库搜索路径环境变量设置
LIBRARY_PATH=$LIBRARY_PATH:/xxx/yyy/zzz/lib
export LIBRARY_PATH
同样,对于gcc命令中,可以加-L
来添加库文件路径。
# 在gcc命令中添加 -Idir 选项来添加头文件搜索路径
# 在gcc命令中添加 -Ldir 选项来添加库文件搜索路径
库文件
请大家思考一个问题,在hello.c
程序中,我们调用了printf()
函数,但是我们实际上并不知道这个函数是怎么实现的,我们只是#include <stdio.h>
。
zld@zld:~/Codes/tmp0926$ cat -n hello.c
1 #include <stdio.h>
2
3 int main()
4 {
5 printf("Hello, World\n");
6 return 0;
7 }
假设还有另外一个场景:假设你是一个公司的老板,你们公司开发了一个非常牛B的模块,这时候有客户希望单独将该模块提供给他们使用。作为公司的老板,你自然是不希望将该模块的源代码提供给客户。
上面的两个场景都涉及到库(Library)文件。把库文件和头文件给到对方就可以达到提供功能又不暴露源码的目的了。
库文件可以分成静态库文件和动态库文件。在Linux下,静态库文件是以.a
后缀结束的文件。动态库文件是以.so
后缀结束的文件。
静态库文件
什么是静态库文件呢?我举一个例子。假设当前你有4个文件:add.c
、sub.c
、multi.c
、div.c
。从名字也可以看出来,它们分别实现了加法、减法、乘法、除法的功能。并且将这4个源文件都生成了对应的目标文件:add.o
、sub.o
、multi.o
、div.o
。我们前面说过,如果其他程序要引用这4个文件,那么可以在gcc命令生成可执行程序时,加上这4个目标文件即可。加上4个文件还好,但是如果有非常多的文件呢?这样一个个添加似乎不太合理,效率也不太高。另外一方面,加减乘除这四个文件,其实可以统称为对数字的运算嘛。能不能将这4个文件像压缩包一样压缩成一个文件呢?可以,静态库文件就是这样干的。
通俗的说,静态库(Static Library)文件就是一个打包了多个目标文件(.o文件)的归档(archive)文件。打个比方就是,目标文件相当于图书馆里面的书。而静态库文件就是图书馆(我想这就是为啥库文件英文是Library了)。
静态库文件有以下特点:
- 1.编译时链接:静态库在编译时被链接到最终的可执行文件中,这意味着库中的代码和数据会被直接复制到可执行文件中。
- 2.独立性:由于静态库在编译时就被整合到可执行文件中,因此生成的可执行文件是独立的,不依赖于任何外部的库文件。这使得静态库编译的程序具有更好的可移植性和部署简便性。
- 3.重复代码整合:如果多个程序使用相同的静态库,每个程序都会在自己的可执行文件中包含一份库代码的副本。这增加了可执行文件的大小,但同时确保了每个程序都有完整的代码副本,无需担心其他程序对库代码的修改或删除。
- 4.性能考虑:静态库在编译时就被整合到可执行文件中,因此没有运行时加载库的开销。这可能在某些性能敏感的应用中是一个优势。
- 5.更新和维护挑战:如果静态库中的代码需要更新或者修复,必须重新编译链接所有依赖于该库的程序。这可能会是一个繁琐的过程,特别是在大型项目或涉及多个依赖库的情况下。
创建和使用静态库
在Linux中,可以用ar命令来创建静态库:
ar cr libNAME.a file1.o file2.o ... filen.o
其中,其中cr是选项,c表示create,r表示replace。ar其实就是archiver的缩小。
通过该命令,最终创建出一个静态库文件libNAME.a
我们还可以使用以下命令查看一个静态库文件中包含的目标文件列表:
ar t libNAME.a
举例
假设有如下4个文件,func1.c
中定义了一个函数func1(),它的功能是返回两个整型数相加的值。func2.c
中定义了func2()函数,该函数的功能是打印给定的数字。在main.c
中分别调用了func1函数和func2函数。在mylib.h
中定义了两个函数的声明。我们试图将func1.c
和func2.c
的程序生成一个静态库文件,然后共main.c
使用。
zld@zld:~/Codes/tmp0926$ ll
total 24
drwxrwxr-x 2 zld zld 4096 Sep 27 22:07 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 63 Sep 27 22:04 func1.c
-rw-rw-r-- 1 zld zld 97 Sep 27 22:06 func2.c
-rw-rw-r-- 1 zld zld 101 Sep 27 22:07 main.c
-rw-rw-r-- 1 zld zld 44 Sep 27 22:05 mylib.h
zld@zld:~/Codes/tmp0926$ cat func1.c
#include "mylib.h"
int func1(int x, int y)
{
return (x+y);
}
zld@zld:~/Codes/tmp0926$ cat func2.c
#include <stdio.h>
#include "mylib.h"
void func2(int x)
{
printf("The result is %d\n", x);
}
zld@zld:~/Codes/tmp0926$ cat main.c
#include <stdio.h>
#include "mylib.h"
int main()
{
int i;
i = func1(1,2);
func2(i);
return 0;
}
zld@zld:~/Codes/tmp0926$ cat mylib.h
int func1(int x, int y);
void func2(int x);
首先,将func1.c
和func2.c
文件分别生成两个目标文件(.o):
# -Wall是告警选项,本文后面会讲到,如果你还不了解,可以去掉这个选项
zld@zld:~/Codes/tmp0926$ gcc -Wall func1.c -c
zld@zld:~/Codes/tmp0926$ gcc -Wall func2.c -c
zld@zld:~/Codes/tmp0926$ ll
total 32
drwxrwxr-x 2 zld zld 4096 Sep 27 22:14 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 63 Sep 27 22:04 func1.c
-rw-rw-r-- 1 zld zld 1224 Sep 27 22:13 func1.o
-rw-rw-r-- 1 zld zld 97 Sep 27 22:06 func2.c
-rw-rw-r-- 1 zld zld 1512 Sep 27 22:14 func2.o
-rw-rw-r-- 1 zld zld 101 Sep 27 22:07 main.c
-rw-rw-r-- 1 zld zld 44 Sep 27 22:05 mylib.h
然后,将func1.o
和func2.o
两个目标文件生成一个hello的静态库文件:
# 生成静态库文件
zld@zld:~/Codes/tmp0926$ ar cr libhello.a func1.o func2.o
zld@zld:~/Codes/tmp0926$ ll
total 36
drwxrwxr-x 2 zld zld 4096 Sep 27 22:16 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 63 Sep 27 22:04 func1.c
-rw-rw-r-- 1 zld zld 1224 Sep 27 22:13 func1.o
-rw-rw-r-- 1 zld zld 97 Sep 27 22:06 func2.c
-rw-rw-r-- 1 zld zld 1512 Sep 27 22:14 func2.o
-rw-rw-r-- 1 zld zld 2948 Sep 27 22:16 libhello.a
-rw-rw-r-- 1 zld zld 101 Sep 27 22:07 main.c
-rw-rw-r-- 1 zld zld 44 Sep 27 22:05 mylib.h
# 查看静态库文件libhello.a包含的目标文件
zld@zld:~/Codes/tmp0926$ ar t libhello.a
func1.o
func2.o
这样,我们就生成了静态库文件libhello.a
。
最后,生成最终的可执行文件并运行:
zld@zld:~/Codes/tmp0926$ gcc -Wall main.c libhello.a -o hello
zld@zld:~/Codes/tmp0926$ ./hello
The result is 3
# 也可以先生成main.o文件,然后再和静态库文件链接生成最终可执行文件
zld@zld:~/Codes/tmp0926$ gcc main.o libhello.a -o hello2
zld@zld:~/Codes/tmp0926$ ./hello2
The result is 3
上述命令我们是通过libNAME
的方式来链接的,我们还可以通过-lNAME
的方式来链接,因此,也可以用以下的命令实现:
zld@zld:~/Codes/tmp0926$ gcc -Wall main.o -lhello -o hello6
/usr/bin/ld: cannot find -lhello: No such file or directory
collect2: error: ld returned 1 exit status
我们发现,命令行竟然执行失败了,说找不到-lhello,也就是说,找不到libhello.a
。明明我们当前目录下有libhello.a,那为啥找不到呢?这是因为通过-lhello
的方式是在系统目录下去找,而系统目录下没有该静态库文件,因此找不到该hello库文件。那怎么办呢?前面我们说过有三种方法:
-
1、Command-line options ‘-I’ and ‘-L’, from left to right.在命令行中加上
-I
或者-L
选项,指明头文件或者库文件的目录。其中-I
是指明头文件的目录,-L
是指明库文件的目录。 -
2、设置环境变量。Directories specified by environment variables, such as C_INCLUDE_PATH and
# C头文件搜索路径环境变量设置 C_INCLUDE_PATH=$C_INCLUDE_PATH:/xxx/yyy/zzz export C_INCLUDE_PATH # C++头文件搜索路径环境变量设置 CPLUS_INCLUDE_PATH=$CPLUS_INCLUDE_PATH:/xxx/yyy/zzz export CPLUS_INCLUDE_PATH # 动态库搜索路径环境变量设置 LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/xxx/yyy/zzz/lib export LD_LIBRARY_PATH # 静态库搜索路径环境变量设置 LIBRARY_PATH=$LIBRARY_PATH:/xxx/yyy/zzz/lib export LIBRARY_PATH
-
3、将头文件或者库文件放到系统目录中。
前面我们在头文件的搜索过程说过不推荐使用方法3,并且在那里也有示例,因此这里我们就不再对方法3进行举例了。我们只对方法1和方法2进行举例。
我们使用第1种方法:
zld@zld:~/Codes/tmp0926$ gcc main.o -L. -lhello -o h6
zld@zld:~/Codes/tmp0926$ ll
total 120
drwxrwxr-x 2 zld zld 4096 Sep 27 23:09 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 63 Sep 27 22:04 func1.c
-rw-rw-r-- 1 zld zld 1224 Sep 27 22:13 func1.o
-rw-rw-r-- 1 zld zld 97 Sep 27 22:06 func2.c
-rw-rw-r-- 1 zld zld 1512 Sep 27 22:14 func2.o
-rwxrwxr-x 1 zld zld 16080 Sep 27 23:09 h6*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:20 hello2*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:26 hello3*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:27 hello4*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:27 hello5*
-rw-rw-r-- 1 zld zld 2948 Sep 27 22:16 libhello.a
-rw-rw-r-- 1 zld zld 101 Sep 27 22:07 main.c
-rw-rw-r-- 1 zld zld 1432 Sep 27 22:20 main.o
-rw-rw-r-- 1 zld zld 44 Sep 27 22:05 mylib.h
在上面的命令gcc main.o -L. -lhello -o h6
中,-L.
表示指定搜索库库文件目录为当前目录(所以用的是点号.
),然后指定外部库为-lhello
。
从上面的执行结果可以看到,成功生成了可执行文件h6。
-L
也可以用绝对路径:
zld@zld:~/Codes/tmp0926$ gcc main.o -L/home/zld//Codes/tmp0926 -lhello -o h7
zld@zld:~/Codes/tmp0926$ ll
total 136
drwxrwxr-x 2 zld zld 4096 Sep 27 23:11 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 63 Sep 27 22:04 func1.c
-rw-rw-r-- 1 zld zld 1224 Sep 27 22:13 func1.o
-rw-rw-r-- 1 zld zld 97 Sep 27 22:06 func2.c
-rw-rw-r-- 1 zld zld 1512 Sep 27 22:14 func2.o
-rwxrwxr-x 1 zld zld 16080 Sep 27 23:09 h6*
-rwxrwxr-x 1 zld zld 16080 Sep 27 23:11 h7*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:20 hello2*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:26 hello3*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:27 hello4*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:27 hello5*
-rw-rw-r-- 1 zld zld 2948 Sep 27 22:16 libhello.a
-rw-rw-r-- 1 zld zld 101 Sep 27 22:07 main.c
-rw-rw-r-- 1 zld zld 1432 Sep 27 22:20 main.o
-rw-rw-r-- 1 zld zld 44 Sep 27 22:05 mylib.h
下面使用第2种方法:
# 首先创建环境变量
zld@zld:~/Codes/tmp0926$ env | grep LIB
zld@zld:~/Codes/tmp0926$ pwd
/home/zld/Codes/tmp0926
zld@zld:~/Codes/tmp0926$ export LIBRARY_PATH=/home/zld/Codes/tmp0926:$LIBRARY_PATH
zld@zld:~/Codes/tmp0926$ env | grep LIB
LIBRARY_PATH=/home/zld/Codes/tmp0926:
# 执行命令
zld@zld:~/Codes/tmp0926$ gcc main.o -lhello -o h8
zld@zld:~/Codes/tmp0926$ ll
total 152
drwxrwxr-x 2 zld zld 4096 Sep 27 23:14 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 63 Sep 27 22:04 func1.c
-rw-rw-r-- 1 zld zld 1224 Sep 27 22:13 func1.o
-rw-rw-r-- 1 zld zld 97 Sep 27 22:06 func2.c
-rw-rw-r-- 1 zld zld 1512 Sep 27 22:14 func2.o
-rwxrwxr-x 1 zld zld 16080 Sep 27 23:09 h6*
-rwxrwxr-x 1 zld zld 16080 Sep 27 23:11 h7*
-rwxrwxr-x 1 zld zld 16080 Sep 27 23:14 h8*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:20 hello2*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:26 hello3*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:27 hello4*
-rwxrwxr-x 1 zld zld 16080 Sep 27 22:27 hello5*
-rw-rw-r-- 1 zld zld 2948 Sep 27 22:16 libhello.a
-rw-rw-r-- 1 zld zld 101 Sep 27 22:07 main.c
-rw-rw-r-- 1 zld zld 1432 Sep 27 22:20 main.o
-rw-rw-r-- 1 zld zld 44 Sep 27 22:05 mylib.h
链接顺序
我们前面说过,将几个目标文件进行链接时,各个目标文件的位置在哪是没有关系的,比如下面的几种链接顺序都是可以生成可执行程序的:
zld@zld:~/Codes/tmp0926$ gcc main.o func1.o func2.o -o hello3
zld@zld:~/Codes/tmp0926$ ./hello3
The result is 3
zld@zld:~/Codes/tmp0926$ gcc func1.o func2.o main.o -o hello4
zld@zld:~/Codes/tmp0926$ ./hello4
The result is 3
zld@zld:~/Codes/tmp0926$ gcc func2.o func1.o main.o -o hello5
zld@zld:~/Codes/tmp0926$ ./hello5
The result is 3
那我们之前的命令:
gcc -Wall main.c libhello.a -o hello6
或者
gcc main.o libhello.a -o hello7
可以将main.c(或者main.o)与libhello.a的顺序对调吗?也就是说,下面的命令会执行正确吗?
gcc -Wall libhello.a main.c -o hello
gcc libhello.a main.o -o hello7
我们运行一下,发现两条命令都报错了:
zld@zld:~/Codes/tmp0926$ gcc -Wall libhello.a main.c -o hello
/usr/bin/ld: /tmp/ccxCA1TA.o: in function `main':
main.c:(.text+0x17): undefined reference to `func1'
/usr/bin/ld: main.c:(.text+0x24): undefined reference to `func2'
collect2: error: ld returned 1 exit status
zld@zld:~/Codes/tmp0926$ gcc libhello.a main.o -o hello7
/usr/bin/ld: main.o: in function `main':
main.c:(.text+0x17): undefined reference to `func1'
/usr/bin/ld: main.c:(.text+0x24): undefined reference to `func2'
collect2: error: ld returned 1 exit status
链接器的主要任务之一就是解析符号引用,即将目标文件和库文件中的未解析符号(如函数和变量)与其他文件或库中的已定义符号进行匹配。链接器按照命令中指定的顺序依次处理每个目标文件和库文件。如果被依赖的目标文件(或库)在引用它的目标文件(或库)之前被处理,链接器将无法找到并解析这些符号引用,因为它们还未被加入到符号集合中。这有可能导致undefined reference
错误。
在上面的命令gcc libhello.a main.o -o hello7
中,gcc是按照从左到右的顺序依次处理的,这样被依赖的库libhello.a
先被处理,而引用它的目标文件main.o
后被处理,因此就无法解析func1
和func2
函数了。
动态库文件
和静态库对应的还有动态库。既然已经有了静态库,那为什么还要动态库呢?这是由于静态库的特点导致的。我们前面说过,如果多个程序使用相同的静态库,每个程序都会在自己的可执行文件中包含一份库代码的副本。这样会导致内存空间的浪费。并且如果静态库中的代码需要更新或者修复,必须重新编译链接所有依赖于该库的程序,这样会导致难以维护和更新。
正是由于静态库的这些缺点,引入了动态库。
动态库文件,也称为动态库链接(Dynamic Link Library,简称DLL)在Windows系统上,或在Linux和Unix系统上称为共享对象库(Shared Object Library,简称so),是一种包含可被多个程序同时使用的代码和数据的库文件。
使用动态库可以节省内存空间,因为多个程序可以共享同一个库文件中的代码,而不需要在每个程序的可执行文件中都包含一份副本。此外,它还有助于软件更新,因为只需要更新一个库文件,而不必重新编译所有依赖该库的应用程序。
动态库的主要特点包括:
- 1.共享性:多个应用程序可以同时使用同一个动态库中的函数和资源,这有助于减少系统的整体内存占用。
- 2.可更新性:如果动态库中的错误被修复或者功能得到增强,只需要替换掉旧版本的库文件,无需重新编译或重新安装依赖此库的所有应用程序。
- 3.模块化:动态库支持将大型应用程序分解为更小、更易于管理的部分,这有利于团队合作开发和维护。
下面总结一下动态库文件和静态库文件的区别:
- 1、链接时机:动态库文件在程序运行时被加载到内存中,而静态库文件在程序编译链接时被整合到可执行文件中。
- 2、文件大小与磁盘空间:动态库文件不会增加可执行文件的大小,多个程序可以共享一个动态库文件,节省磁盘空间。静态库文件会增加可执行文件的大小,因为静态库的代码被完全复制到了可执行文件中,如果多个程序使用同一个静态库,会造成存储资源的浪费。
- 3、运行时依赖:动态库文件编译的程序在运行时需要外部库文件的支持,如果动态库缺失或版本不匹配,程序可能无法正常运行。静态库文件编译的程序在运行时不需要外部库文件的支持,因为它们已经包含了所有必要的代码和数据。
- 4、更新与维护:动态库文件可以独立于程序进行更新,当需要更新时,只需要替换掉旧的动态库文件,无需重新编译链接依赖于它的所有程序。静态库文件更新时需要重新编译链接所有依赖该库的程序,以确保所有程序都使用更新后的代码。
- 5、性能与内存使用:动态库文件在程序运行时被加载,可能会增加一些加载时间,但多个程序可以共享同一个动态库文件,节省内存使用。静态库文件在程序运行时已经加载到内存中,无需额外的加载时间,但可能会导致内存使用量的增加,尤其是当多个程序使用同一个静态库时。
在实际中,动态库用得更多。
创建和使用动态库
在Linux上,动态链接库的名字形式为 libxxx.so,前缀是lib,后缀名为".so"。
可以通过gcc
编译器使用-fPIC
和-shared
选项来创建共享对象文件(.so文件)。其中-fPIC
创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享。
gcc -fpic -shared 源文件名... -o 动态库文件
或者
先使用 gcc -c 指令将指定源文件编译为目标文件,再由目标文件生成动态链接库
gcc -c -fPIC 源文件名... -o 目标文件
gcc -shared 目标文件... -o 动态库文件
我们对之前的4个文件,将func1.c
和func2.c
合并创建一个动态库文件。
zld@zld:~/Codes/tmp0926$ ll
total 24
drwxrwxr-x 2 zld zld 4096 Sep 27 23:59 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 63 Sep 27 22:04 func1.c
-rw-rw-r-- 1 zld zld 97 Sep 27 22:06 func2.c
-rw-rw-r-- 1 zld zld 101 Sep 27 22:07 main.c
-rw-rw-r-- 1 zld zld 44 Sep 27 22:05 mylib.h
zld@zld:~/Codes/tmp0926$ gcc -fpic -shared func1.c func2.c -o libhello.so
zld@zld:~/Codes/tmp0926$ ll
total 40
drwxrwxr-x 2 zld zld 4096 Sep 28 00:04 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 63 Sep 27 22:04 func1.c
-rw-rw-r-- 1 zld zld 97 Sep 27 22:06 func2.c
-rwxrwxr-x 1 zld zld 15600 Sep 28 00:04 libhello.so*
-rw-rw-r-- 1 zld zld 101 Sep 27 22:07 main.c
-rw-rw-r-- 1 zld zld 44 Sep 27 22:05 mylib.h
然后,像静态库链接一样,和main.c
一起生成可执行文件:
zld@zld:~/Codes/tmp0926$ gcc main.c libhello.so -o h1
zld@zld:~/Codes/tmp0926$ ll
total 56
drwxrwxr-x 2 zld zld 4096 Sep 28 00:10 ./
drwxrwxr-x 11 zld zld 4096 Sep 26 18:54 ../
-rw-rw-r-- 1 zld zld 63 Sep 27 22:04 func1.c
-rw-rw-r-- 1 zld zld 97 Sep 27 22:06 func2.c
-rwxrwxr-x 1 zld zld 15968 Sep 28 00:10 h1*
-rwxrwxr-x 1 zld zld 15600 Sep 28 00:04 libhello.so*
-rw-rw-r-- 1 zld zld 101 Sep 27 22:07 main.c
-rw-rw-r-- 1 zld zld 44 Sep 27 22:05 mylib.h
zld@zld:~/Codes/tmp0926$ ./h1
./h1: error while loading shared libraries: libhello.so: cannot open shared object file: No such file or directory
发现执行可执行程序h1时失败,说无法找到动态库文件libhello.so
运行由动态库生成的可执行文件时,必须确保程序在运行时可以找到这个动态库。和静态库文件一样,可以通过添加动态库的环境变量来解决,或者在执行上述gcc命令时添加动态库路径。
# 通过添加动态库环境变量解决
zld@zld:~/Codes/tmp0926$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/zld/Codes/tmp0926
zld@zld:~/Codes/tmp0926$ export LD_LIBRARY_PATH
zld@zld:~/Codes/tmp0926$ ./h1
The result is 3
# 在执行gcc命令时添加动态库链接
zld@zld:~/Codes/tmp0926$ gcc main.c -lhello -L. -o h2
zld@zld:~/Codes/tmp0926$ ./h2
The result is 3
zld@zld:~/Codes/tmp0926$ gcc main.c libhello.so -L. -o h3
zld@zld:~/Codes/tmp0926$ ./h3
The result is 3
同静态库文件一样,动态库文件在链接时也有顺序要求,如果将上述命令换一个顺序,则也会报错:
zld@zld:~/Codes/tmp0926$ gcc libhello.so main.c -L. -o h4
/usr/bin/ld: /tmp/ccLWWSlT.o: in function `main':
main.c:(.text+0x17): undefined reference to `func1'
/usr/bin/ld: main.c:(.text+0x24): undefined reference to `func2'
collect2: error: ld returned 1 exit status
Warning编译选项
我们在前面的一些例子中,增加了-Wall
选项,这个选项就是一个编译告警选项。事实上,在实际的项目中,一般总是会增加告警选项的。
比如下面的一段程序中,在打印printf("MAX + MIN = %f\n", MAX + MIN);
中,我们将格式化输出写成%f了(即浮点型),但是实际结果是一个整型。
[zld@localhost 0926]$ cat test.c
#include <math.h>
#include <stdio.h>
#define MAX 3
#define MIN 1
int main()
{
printf("MAX + MIN = %f\n", MAX + MIN);
return 0;
}
如果不加告警选项,则不会有告警信息,并且运行结果不符合预期:
[zld@localhost 0926]$ gcc test.c -o test
[zld@localhost 0926]$ ./test
MAX + MIN = 0.000000
因此,无论如何,都建议加上告警选项,提前识别出告警风险。
# 加上告警选项上,就有告警信息提示了
[zld@localhost 0926]$ gcc -Wall test.c -o test2
test.c: In function ‘main’:
test.c:7: warning: format ‘%f’ expects type ‘double’, but argument 2 has type ‘int’
# 根据提示,修改程序。然后告警消失
[zld@localhost 0926]$ cat test.c
#include <math.h>
#include <stdio.h>
#define MAX 3
#define MIN 1
int main()
{
printf("MAX + MIN = %d\n", MAX + MIN); #这里改成%d
return 0;
}
[zld@localhost 0926]$ gcc -Wall test.c -o test3
[zld@localhost 0926]$ ./test3
MAX + MIN = 4 # 运行结果正确
一些常用的告警选项:
-
1、-w(小写)禁止所有告警信息
-
2、以**-W**(大写)开头开启特定的告警。比如
-Wreturn-type(返回值告警), -Wsign-compare(有符号和无符号对比告警) -Wall (除extra外的所有告警) -Wextra (all外的其他告警)
-
3、以“-Wno-”开头关闭特定的警告;
例如:
-Wno-return-type (取消返回值告警)
-Wno-sign-compare(取消有符号和无符号对比告警)
- 4、将告警转变成错误。
- -Werror :所有告警当错误报
- -Werror= 将指定的警告转换为错误。
- 反过来:-Wno-error取消编译选项-Werror
GCC定义了非常多的告警信息,这里就不一一列出来了,可以直接参考gcc文档。
调试信息(-g)
一般来说,之前的gcc命令生成的可执行程序都不包含调试信息,如果程序崩溃了,那么则获取崩溃的文件名以及行号。
gcc提供了-g
调试选项,这样生成的可执行程序,倘若出现问题,便可以使用 gdb 找出问题具体出现的位置,便于问题的解决。
例如,对于下面一段有问题的程序:
[zld@localhost 0926]$ cat -n null.c
1 int a(int *p);
2
3 int main()
4 {
5 int *p = 0;
6 return a(p);
7 }
8
9 int a(int *p)
10 {
11 int y = *p;
12 return y;
13 }
在第5行,因为p赋值为0,也就是NULL。因此在调用函数a时,在第11行对指针进行解引用时,会出错。但是这是运行时错误,因此在编译时,编译器不会报错:
[zld@localhost 0926]$ gcc -Wall -g null.c -o null
[zld@localhost 0926]$
但是在运行时,就会报错:
[zld@localhost 0926]$ ./null
Segmentation fault (core dumped)
但是看当前目录,并没有产生core文件,这是因为很多操作系统默认是不产生core文件的。可以通过ulimit -c查看。如果是0,则说明不会产生core文件。
[zld@localhost 0926]$ ulimit -c
0
可以通过ulimit -c unlimited来修改可以产生core文件:
[zld@localhost 0926]$ ulimit -c unlimited
[zld@localhost 0926]$ ulimit -c
unlimited
再次运行,就产生了core文件了:
[zld@localhost 0926]$ ./null
Segmentation fault (core dumped)
[zld@localhost 0926]$ ll
-rw-------. 1 zld zld 188416 Sep 26 03:47 core.6484
然后就可以用gdb工具来定位了:
[zld@localhost 0926]$ gdb null core.6484
zld@zld:~/Codes/tmp0926$ gdb null core
GNU gdb (Ubuntu 14.0.50.20230907-0ubuntu1) 14.0.50.20230907-git
Copyright (C) 2023 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Type "show copying" and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<https://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from null...
[New LWP 2738]
This GDB supports auto-downloading debuginfo from the following URLs:
<https://debuginfod.ubuntu.com>
Enable debuginfod for this session? (y or [n]) n
Debuginfod has been disabled.
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Core was generated by `./null'.
Program terminated with signal SIGSEGV, Segmentation fault.
#0 0x0000625194ea415b in a (p=0x0) at null.c:13
13 int y = *p;
(gdb) backtrace
#0 0x0000625194ea415b in a (p=0x0) at null.c:13
#1 0x0000625194ea4149 in main () at null.c:7
由于本文是介绍gcc的使用,因此关于gdb如何使用,本文不做过多描述。大家可以在网上搜索如何使用gdb。
编译优化
gcc 提供了为了满足用户不同程度的的优化需要,提供了近百种优化选项,用来对{编译时间,目标文件长度,执行效率}这个三维模型进行不同的取舍和平衡。优化的方法不一而足,总体上将有以下几类:1)精简操作指令;2)尽量满足cpu的流水操作;3)通过对程序行为地猜测,重新调整代码的执行顺序;4)充分使用寄存器;5)对简单的调用进行展开等等。想全部了解这些编译选项,并在其中挑选适合的选项进行优化,无疑像个噩梦般的过程。
幸好gcc提供了从O0
到O3
这几种不同的优化级别供大家选择。
在编译时,如果没有指定上面的任何优化参数,则默认为 -O0,即没有优化。
参数 -O1
、-O2
、-O3
中,随着数字变大,代码的优化程度也越高,不过这在某种意义上来说,也是以牺牲程序的可调试性为代价的,因此优化和调试是一对矛盾体。
举例:
下面一段代码:
zld@zld:~/Codes/tmp0926$ cat test.c
#include <stdio.h>
double powern(double d, unsigned n)
{
double x = 1.0;
unsigned j;
for (j=1;j<=n;j++)
{
x *= d;
}
return x;
}
int main()
{
double sum = 0.0;
unsigned i;
// 循环20亿次
for (i=1;i <=2000000000;i++)
{
sum += powern(i, i%5);
}
printf("sum = %g\n", sum);
return 0;
}
用不同的优化级别看下运行的时间,从结果上看,随着优化级别的调高,相应的运行时间也逐渐减少。
zld@zld:~/Codes/tmp0926$ gcc -Wall -O0 test.c -o O0
zld@zld:~/Codes/tmp0926$ time ./O0
sum = 1.28e+45
real 0m12.983s
user 0m12.963s
sys 0m0.000s
zld@zld:~/Codes/tmp0926$ gcc -Wall -O1 test.c -o O1
zld@zld:~/Codes/tmp0926$ time ./O1
sum = 1.28e+45
real 0m3.522s
user 0m3.517s
sys 0m0.000s
zld@zld:~/Codes/tmp0926$ gcc -Wall -O2 test.c -o O2
zld@zld:~/Codes/tmp0926$ time ./O2
sum = 1.28e+45
real 0m3.245s
user 0m3.233s
sys 0m0.004s
zld@zld:~/Codes/tmp0926$
zld@zld:~/Codes/tmp0926$ gcc -Wall -O3 test.c -o O3
zld@zld:~/Codes/tmp0926$ time ./O3
sum = 1.28e+45
real 0m3.124s
user 0m3.118s
sys 0m0.000s
# 加上循环展开优化(-funroll-loop),又进一步优化了
zld@zld:~/Codes/tmp0926$ gcc -Wall -O4 -funroll-loops test.c -o O4
zld@zld:~/Codes/tmp0926$ time ./O4
sum = 1.28e+45
real 0m2.755s
user 0m2.748s
sys 0m0.000s
我们上面说过,编译优化和调试信息(-g)通常来说是一对矛盾体。我个人的建议是要优先保证调试信息,毕竟如果没有调试信息,当程序崩溃时,我们都无法定位,这在实际工作中是一件非常麻烦的事,得加班加点了。。。。。
总结
这个章节是对前面章节用到的一些gcc命令以及选项做一个总结。
参数选项 | 含义 |
---|---|
-E | 仅执行预处理,不进行编译、汇编和链接(生成后缀为 .i 的预编译文件) |
-S | 执行编译后停止,不进行汇编和链接(生成后缀为 .s 的预编译文件) |
-c | 编译程序,但不链接成为可执行文件(生成后缀为 .o 的文件) |
-o | 对输出文件命名 |
-O/-O1/-O2/-O3 | 优化代码,减少代码体积,提高代码效率,但是相应的会增加编译的时间 |
-lNAME | (这里是小写的L)指定程序要链接的库,NAME为库文件名称。可以是静态库文件,也可以是动态库文件 |
-Ldir | 指定-l(小写-L)所使用到的库文件所在路径。dir为具体的路径 |
-Idir | (这里是大写的i)增加 include 头文件路径。dir为具体的路径 |
-DNAME | 预定义宏,NAME为相应的宏名称 |
-shared | 生成共享文件,然后可以与其它文件链接生成可执行文件 |
-fpic | 生成适用于共享库的与地址无关的代码(PIC) |
-w | 不输出任何警告信息 |
-Wall | 开启编译器的批量告警选项 |
-Werror | 将所有的警告当成错误进行处理,在所有产生警告的地方停止编译 |
-g | 生成调试信息,方便gdb调试 |
-save-temps | 保存编译中间结果 |
-v | 显示gcc执行时的详细过程 |
–version | 显示gcc的版本信息 |
参考文献
1、GCC, the GNU Compiler Collection - GNU Project
2、https://blog.csdn.net/bandaoyu/article/details/115419255
3、https://blog.csdn.net/qq_31108501/article/details/51842166
4、https://www.runoob.com/w3cnote/gcc-parameter-detail.html
5、文心一言