环境:Ubuntu 18.04.6
一. GCC
简介:
GCC
是Linux
下的编译工具集,是GNU Compiler Collection
的缩写,包含gcc
、g++
等编译器,该工具及不仅包含编译器,还包含其他工具集,例如ar
、num
等。
GCC工具集不仅能编译C/C++语言,其它例如Objective-C、Pascal、FOrtan、Java、Ada等语言均能进行编译。GCC可以根据不同的硬件平台进行编译,可以进行交叉编译,在A平台上编译B平台上的程序,支持常见的X86、ARM、PowerPC、mips等,以及Linux、Windows等软件平台。
1. 安装
sudo apt update # 更新apt下载源,尽可能保证下载到的软件是最新的
sudo apt install gcc g++ # 同时下载gcc和g++
安装完毕后,查看版本:
gcc -v
g++ -v
2. 工作流程
GCC编译器的工作流程分为四步:
- 预处理:GCC调用预处理器主要完成三件事:
- 展开头文件
- 宏替换
- 去掉注释行
- 编译和优化:GCC调用编译器对文件进行编译,得到汇编文件。
- 汇编:GCC调用汇编器对汇编文件进行汇编,得到二进制文件。
- 链接:GCC调用链接器对程序需要的库进行链接,最终得到一个可执行的二进制文件。
具体如下:
处理前文件名后缀 | 处理 | gcc参数 | 处理后文件名后缀 |
---|---|---|---|
.c/.cpp | 预处理 | -E |
.i |
.i | 编译 | -S |
.s |
.s | 汇编 | -c | .o |
.o | 链接 | 无 | 无固定后缀 |
EG:
-
新建程序:test.c
// 假设程序对应的源文件名为 test.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> int main() { #ifdef HELLO printf("HELLO宏被定义"); #endif int arr[5] = {1,2,3,4,5}; for(int i=0; i<5; ++i) { printf("arr[%d] = %d\n", i, array[i]); } return 0; }
-
预编译:
gcc -E test.c -o test.i
得到文件形式如下:
// 前面是展开的头文件 int main() { int arr[5] = {1,2,3,4,5}; for(int i=0; i<5; ++i) { printf("arr[%d] = %d\n", i, array[i]); } return 0; }
可以看出:
- 头文件被展开
- 注释被消去
- 宏常量被替换。
-
编译:
gcc -S test.i -o test.s
得到的
test.s
文件内是汇编语言。 -
汇编:
gcc -c test.s -o test.o
得到二进制文件。
-
链接:
gcc test.o -o test
得到可执行文件
test
-
执行test文件,输出如下:
./test arr[0] = 1 arr[1] = 2 arr[2] = 3 arr[3] = 4 arr[4] = 5
3. gcc常用参数
以下表格列出gcc常用参数,这些参数在实际使用中并无顺序之分,只要指定即可。
参数 | 作用 |
---|---|
-E | 对源文件预编译,不进行编译 |
-S | 对源文件进行编译,不进行汇编。 |
-c | 对源文件进行汇编,不进行链接。 |
-o file | 将原文件编译为file |
-I |
指定include头文件的搜索路径 |
-D | 在程序编译时,定义一个宏 |
-w | 不生成任何警告信息。(不推荐,因为有时警告信息就是报错) |
-Wall | 生成所有警告信息。 |
-O[n] | 编译时优化代码。n代表优化的级别,0不优化,1为缺省值,3优化级别最高。 |
-l (小写的l) |
在程序编译时,指定使用的库的名字。要去掉前缀和后缀 |
-L | 指定编译的时候,搜索的库的路径。相对或绝对路径均可 |
-fpic/fPIC | 生成与位置无关的代码(通常动态链接库使用) |
-shared | 生成共享目标文件。通常在建立共享库时。 |
-std | 指定C/C++方言,如-std=c99,gcc默认的方亚是GNU C |
3.1 搜索头文件(-I)
注意是大写的i不是小写的l
编写文件如下:
-
头文件./include/head.h:
#ifndef _HEAD_H #define _HEAD_H // 加法 int add(int a, int b); // 减法 int sub(int a, int b); #endif
-
加法文件:./add.c
#include <stdio.h> #include "head.h" int add(int a, int b){ return a+b; }
-
减法文件:./sub.c
#include <stdio.h> #include "head.h" int sub(int a, int b) { return a-b; }
-
测试文件:./main.c:
#include <stdio.h> #include "head.h" int main() { int a = 10; int b = 5; printf("a+b=%d\n", add(a, b)); printf("a-b=%d\n", sub(a,b)); return 0; }
最终文件结构如下:
.
├── add.c
├── cal
├── include
│ └── head.h
├── main.c
└── sub.c
此时使用gcc进行编译,发现无法找到头文件head.h
。头文件寻找策略如下:
- 在c/c++中,头文件如果使用
<>
,那就是从默认库中寻找(linux默认在/usr/lib
) - 如果是
""
包裹,那就从当前目录下寻找。
而头文件在include
下,自然找不到,因此要在编译时指定头文件目录:
gcc *.c -o cal -I include/
编译成功。
3.2 定义宏(-D)
在编译程序时我们可以额外指定一个宏,这个宏会被写入文件中,从而对文件进行一定的控制。
EG:
-
定义程序如下:
#include <stdio.h> #define NUMBER 3 int main() { #ifdef DEBUG printf("这是一条调试信息\n"); #endif for (int i = 0; i < NUMBER; i++) { printf("%d\n", i); } return 0; }
-
不指定宏进行编译:此时不会输出调试语句
gcc test.c -o test.o ./test.o 0 1 2
-
指定宏进行编译:
gcc test.c -o test.o -D DEBUG ./test.o 这是一条调试信息 0 1 2
使用场景:
用于控制调试语句,将所有调试语句根据宏的存在与否判断是否编译,这样就可以在灵活控制程序中调试信息输出与否。
4. gcc与g++
二者之间的区别如下:
- 在编译阶段(即预编译之后的阶段):
- 后命名为
.c
的文件,gcc将其视作C程序,而g++将其视作C++程序。 - 后缀名为
.cpp
的文件,gcc和g++都会将其视作C++程序。 - 在该阶段g++会调用gcc对文件进行编译。也就是说编译阶段只会由gcc完成。
- 后命名为
- 在链接阶段(最后一个阶段):
- gcc和g++都能连接到标准C库。
- g++可以自动链接到C++标注库,而gcc想要连接到C++标准库需要参数
lstdc++
- 关于
_cplusplus
宏的定义- g++会自动定义该宏,但这并不影响其对于c文件的编译。
- gcc需要根据后缀名来确认是否需要定义该宏。
综上:
- gcc和g++都能编译C文件
- g++能直接编译C++文件,而gcc需要添加参数
-lstdc++
- gcc和g++都能定义宏
_cplusplus
EG:
# 编译c程序
gcc test.c -o test
g++ test.c -o test
# 编译c++程序
g++ test.cpp -o test
gcc test.cpp -o test -lstdc++
二.静态库和动态库
简介:
所谓库文件,其实就是经过编译的二进制源文件,可以分为静态库
和动态库
。在使用时需要搭配头文件。
在项目中使用库有两个目的:
- 使程序更加简洁,减少程序中的源文件数量。
- 避免源代码泄露。
1. 静态库
linux中静态库由ar
(gcc内自带的程序)命令生成,现在已经使用的很少,大多数情况都是使用动态库。
命名规则如下:
- Linux中,以
lib
为前缀,.a
为后缀,中间随意,也就是libxxx.a
的命名格式。 - Windows中,以
lib
为前缀,以.lib
为后缀,中间随意,也就是libxxx.lib
的命名格式。
1.1 生成静态链接库
将源文件经过预编译、编译、汇编得到的二进制文件,通过ar
工具打包即可得到静态库文件。
ar
工具参数如下:
-c
:创建一个库,不论库是否存在都将进行创建。-s
:创建目标文件索引,这样如果库较大时,能加快搜索速度。-r
:在库中插入模块。默认新成员是添加在库文件的结尾,但如果该模块名已经存在,那么就进行替换。
最后发布需要两个文件:
- 制作的
libxxx.a
库文件,里面包含了具体实现的源代码。 - 相应的头文件,相当于提供了源代码的接口。
1.2 实例
测试程序:
这里依旧使用一-->3-->3.1
中的简单的计算机程序,结构如下:
.
├── add.c
├── include
│ └── head.h
├── main.c
├── sub.c
其中:
- add.c和sub.c分别为加法和减法程序
- include/head.h为头文件
- main.c为测试文件
生成静态库:
-
将源文件进行汇编操作(前三步),得到二进制文件(注意指定头文件):
gcc add.c sub.c -c -I include/
得到二进制文件:
add.o sub.o
-
将生成的目标文件通过ar工具打包为静态库(注意命名):
ar -csr libcal.a add.o sub.o
得到静态库文件:
libcal.a
-
将头文件和静态库文件一起发给用户即可使用:
include/head.h libcal.a
1.3 静态库的使用
首先要得到静态库和头文件,随后开始使用,当前文件结构如下:
head.h
libcal.h
main.c
错误示范:
gcc main.c -o cal
/tmp/ccT4oiqj.o:在函数‘main’中:
main.c:(.text+0x21):对‘add’未定义的引用
main.c:(.text+0x43):对‘sub’未定义的引用
collect2: error: ld returned 1 exit status
发现编译报错了,这是因为main.c中引入了头文件head.h,但编译器未能找到head.h中函数的具体实现,也就是找不到库文件,这与库文件的检索有关(后面会讲),简而言之就是找不到库文件,因此我们只需要在编译时指定库文件的路径和名字即可:
-L
:指定库文件所在的目录,相对或绝对都可以。-l
(小写的l):指定库文件的名字(去掉前缀和后缀)。
正确示范:
gcc main.c -o test -L ./ -l cal
生成成功。得到可执行程序test。
该执行程序不依赖库文件和头文件即可运行,因为编译过程实际上是将库中的代码复制到了可执行程序中。
2. 动态库
简介:
与静态库不同,动态库是程序运行时才会加载的库,当动态链接成功部署后,多个程序可以使用同一个加载到内存中的动态库,因此在Linux中动态库也可以被称之为共享库。
动态链接库是目标文件的集合,目标文件在动态链接库中的组织方式是按照特殊形式形成的。库中函数和变量使用的地址是相对地址(静态库中使用的是绝对地址),其真实地址是在应用程序加载动态库时形成的。
命名规则如下:
- Linux中,以
lib
为前缀,以.so
为后缀,中间是库的名字。也就是libxxx.so
- Windows中,以
lib
为前缀,以.dll
为后缀,中间是库的名字。也就是libxxx.dll
。
2.1 生成动态链接库
具体步骤如下:
- 通过
-fpic
参数在汇编时生成与位置无关的代码。 - 通过
-shared
参数告知编译器生成一个动态链接库。 - 发布头文件和动态链接库。
2.2 实例
实例代码:
依旧以一-->3-->3.1
中的代码为例,其结构如下:
beasts777@ubuntu:~/coding/c++/cal$ tree
.
├── add.c
├── include
│ └── head.h
├── main.c
├── sub.c
生成动态链接库
-
使用
gcc
对源文件进行汇编(参数-c
)生成与位置无关的目标文件,需要指定参数-fpic
(注意指定头文件所在目录)gcc add.c sub.c -c -fpic -I include
得到目标文件:
add.o sub.o
-
使用
gcc
将二进制源文件打包成动态库,需要使用参数-shared
gcc add.o sub.o -shared -o libcalc.so
生成动态库文件:
libcalc.so
-
发布动态库文件和头文件
libcalc.so head.h
2.3 使用动态链接库
-
首先获取动态链接库和头文件:
. ├── head.h ├── libcalc.so └── main.c # 这是测试文件
-
编译测试文件(注意指定库文件的地址和名字):
gcc main.c -L ./ -l calc -o app
得到可执行文件
app
-
执行文件:
./app ./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
发现文件报错:找不到共享库
libcal.so
。这是为什么?明明在编译测试文件时制定了库文件的路径和名字,但实际运行时却记得名字却找不到目录。还有为什么静态库不会出现这一问题?答案见下一节?
2.4 解决动态库无法加载的问题
2.4.1 库的工作原理
静态库:
在程序编译的最后一个阶段,也就是链接阶段,提供的静态库会被打包进可执行程序中。也就是说:此时可执行程序内已经包含了静态库中的代码,当可执行程序执行时,其拷贝的静态库的代码也会加载到进程的代码区,因此也就不需要再去寻找静态库了。
动态库:
- 在链接阶段,虽然使用
gcc
命令的-L
和-l
指定了动态库的目录和名字,但此时:- 这一步只检查了动态库是否存在,并未将动态库中的代码拷贝到可执行程序中,因此运行时仍需要依赖动态库。
- 虽然链接时指定了动态库的目录和名字,但可执行程序中只保留了库名,而未保留库的路径,它寻找库实际上是通过程序链接器按照指定顺序在固定目录寻找的。
- 在可执行程序执行阶段:
- 程序执行时会先检测需要的动态库是否存在,加载不到就会报错,显示无法加载到动态库。
- 当动态库中的函数在程序中被调用了,这时动态库才会加载到内存中,不调用就不加载。
- 动态库的检测和内存加载操作都是通过动态链接器完成的。
2.4.2 动态链接器
简介:动态链接器是一个独立于应用程序的进程,其本身属于操作系统,搜索动态库的依照一定策略,优先级从高到低依次是:
- 可执行文件内部的
DT_RPATH
- 系统的环境变量:
LD_LIBRARY_PATH
- 系统动态库的缓存文件:
/etc/ld.so.cache
- 存储动态库、静态库的系统目录:
/lib/
,/usr/lib
按照以上顺序,依次搜索动态库是否存在,如果都搜索不到,那么动态链接器就会报错,提示无法找到动态库。
由此,便可以得到找不到动态库的解决方案。
2.4.3 解决方案
一共有三种方法:
-
方案一:将库的路径添加到环境变量
LD_LIBRARY_PATH
中。具体步骤如下:-
找到配置文件:
- 用户级别:
~/.bashrc
。该设置仅对当前用户有效。 - 全局级别:
/etc/profile
。该设置对所有用户都有效。
- 用户级别:
-
打开配置文件,添加一句话:
export LD_LIBRARY_PATH =$LD_LIBRARY_PATH :动态库的绝对路径 # eg: export LD_LIBRARY_PATH =$LD_LIBRARY_PATH :/home/beasts777/coding/c++/activeLib/libcalc.so
-
令配置的文件生效:
-
用户级别的修改:重启终端即可。(因为用户配置文件是在打开终端时加载的)
-
系统级别的修改:重启系统即可。(全局配置文件在开机时加载)
-
也可以用命令让操作系统重新加载配置文件,无需重启终端或系统:
# 用户级 source ~/.bashrc # 系统级 source /etc/profile
-
-
-
方案二:更新系统动态库的缓存文件:
/etc/ld.so.cache
(需要注意的是,我们无法直接更改缓存文件,应当更改:/etc/ld.so.conf
配置文件,随后再同步到缓存文件):-
打开
/etc/ld.so.conf
,将动态库的目录(注意这里时目录,不要添加库的名字)添加到最后一行,保存退出/home/beasts777/coding/c++/activeLib/
-
将
ld.so.conf
同步到ld.so.cache
中:sudo ldconfig
不需要进行其它操作即可生效。
-
-
方案三:将动态库文件拷贝到系统库目录
/lib/
或/usr/lib/
,或者在里面创建库的软连接(更推荐,因为这样如果后续库被修改了,就不用再拷贝一次了)# 拷贝库 sudo cp /home/beasts777/coding/c++/activeLib/libcalc.so /usr/lib/libcal.so # 创建软链接(推荐) sudo ln -s /home/beasts777/coding/c++/activeLib/libcalc.so /usr/lib/libcal.so
2.4.4 验证是否能够链接到动态库文件
语法:ldd
可执行程序名
EG:
ldd app
linux-vdso.so.1 (0x00007ffe5ffbb000)
libcalc.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f3a8b488000)
/lib64/ld-linux-x86-64.so.2 (0x00007f3a8ba7b000)
如果可以链接,那么会显示地址,否则会显示not found。
2.4.5 实操
通过在系统动态库内添加软连接实现。
当前文件结构如下:
.
├── app # 可执行文件
├── head.h # 头文件
├── libcalc.so # 动态库文件
-
在
/usr/lib
下创建动态库文件的软链接:sudo ln -s ~/coding/c++/cal/activeLib/libcalc.so /usr/lib/libcalc.so
-
查看可执行程序是否可以读取到动态库文件:
ldd app linux-vdso.so.1 (0x00007ffe3e3ee000) libcalc.so => /usr/lib/libcalc.so (0x00007f29c7c68000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f29c7877000) /lib64/ld-linux-x86-64.so.2 (0x00007f29c806c000)
读取成功。
-
直接运行程序即可。
3. 优缺点
3.1 静态库
优点:
- 静态库被直接打包到应用程序中,因此加载速度更快。
- 发布程序时无需发布静态库。
缺点:
- 相同的库文件可能在内存中被加载多份,浪费内存。
- 如果库文件更新,就需要对项目进行重新编译,将新的库文件代码打包到可执行程序中。
3.2 动态库
优点:
- 不同进程可使用同一动态库,实现不同进程间的资源共享,无需多次复制。
- 修改动态库时,只需替换库文件,无需重新编译应用程序。
- 因为动态库只有在使用库函数时才会被调用,因此程序员可以控制何时加载动态库。
缺点:
- 加载速度比静态库慢,但当今计算机基本可以忽略。
- 发布应用程序时需要发布以来的动态库。