linux之gcc
#################
生成可执行程序过程为成四个步骤:
- 由.c文件到.i文件,这个过程叫预处理。
- 由.i文件到.s文件,这个过程叫编译。
- 由.s文件到.o文件,这个过程叫汇编。
- 由.o文件到可执行文件,这个过程叫链接。
预处理阶段:gcc -E 主要处理#include和#define,它把#include包含进来的.h 文件插入到#include所在的位置,把源程序中使用到的用#define定义的宏用实际的字符串代替:
gcc –E hello.c –o hello.i
编译阶段:首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,Gcc把代码翻译成汇编语言
gcc –S hello.i –o hello.s
汇编阶段:把*.s文件翻译成二进制机器指令文件*.o,其中-c告诉gcc进行汇编处理。这步生成的文件是二进制文件,直接用文本工具打开看到的将是乱码,我们需要反汇编工具如GDB的帮助才能读懂它;这个阶段接收.c, .i, .s的文件都没有问题。
gcc -c hello.s -o hello.o
gcc -c hello.i -o hello.o
gcc -c hello.c -o hello.o
链接阶段:在成功编译之后,就进入了链接阶段。在这里涉及到一个重要的概念:函数库。在main.c中使用printf函数,但并没有定义”printf”的函数实现,且在预编译中包含进的<stdio.h>中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现”printf”函数的呢?
最后的答案是:系统把这些函数实现都被做到名为libc.so.6的库文件中去了,在没有特别指定时,gcc会到系统默认的搜索路径”/usr/lib”下进行查找,也就是链接到libc.so.6库函数中去,这样就能实现函数”printf”了,而这也就是链接的作用;
库的概念:
本质上来说库是一种可执行的二进制代码 (但不可以独立执行), 可以被操作系统载入内存执行. linux 下的库有两种: 静态库和共享库 (动态库).
- Windows下的库有两种: 静态库(.lib)和动态链接库(.dll);
- Linux下的库有两种: 静态库(.a)和共享库(.so);
usr是unix system resource缩写,不是user缩写;
/lib,/usr/lib,/usr/local/lib的区别:
- /lib是内核级的;
- /usr/lib是系统级的;
- /usr/local/lib是用户级的;
C/C++中可以通过#include <stdio.h>和#include "stdio.h"包含头文件,区别是:
- #include <stdio.h>,直接到系统指定目录去查找头文件;
- #include “stidio.h”,会首先到当前工程目录查找头文件,如果没找到再到系统指定目录查找;
一、编译时头文件的搜索路径:
- -I 参数是用来指定头文件目录,/usr/include 和/usr/local/include目录一般是不用指定的,gcc 知道去那里找;
- 但是如果头文件不在 /usr/include 里我们就要用 -I 参数指定了,比如头文件放在/myinclude 目录里,那编译命令行就要加上 -I/myinclude 参数了,如果不加你会得到一个 “xxxx.h: No such file or directory” 的错误;
- -I 参数可以用相对路径,比如头文件在当前目录,可以用 -I. 来指定。-I与后面的目录是否空格,空格也行不空格也行,比如-I.与-I . 等价;
- 先找gcc的参数“-I”指定的目录(要大写的i,即include的首字母);
- 再找gcc的环境变量:C_INCLUDE_PATH,CPLUS_INCLUDE_PATH,OBJC_INCLUDE_PATH;
- 再找gcc的默认头文件搜寻目录:/usr/include,/usr/local/include;(默认/usr/local/include为空)
- gcc自带的目录/usr/include/c++/11,/usr/lib/gcc/x86_64-redhat-linux/11/include等目录;
- 但是如果装gcc的时候,是有给定的prefix的话,那么就是/usr/include,prefix/include,prefix/xxx-xxx-xxx-gnulibc/include,prefix/lib/gcc-lib/xxxx-xxx-xxx-gnulibc/2.8.1/include;
说明:
- /usr/src/kernels/5.14.0-267.el9.x86_64/include/包含的是内核头文件,一般在编译模块时使用。/usr/src/kernels/5.14.0-267.el9.x86_64是内核源码路径,
- /usr/include路径主要是glibc的头文件,是用来编译用户态文件的。但是/usr/include其中也包含内核头文件所需的linux/、asm/文件夹,但是这两个文件夹主要是用来做一些兼容性用的,真正用到内核头文件还是要用/usr/src/linux/include。
export C_INCLUDE_PATH=path_name:$C_INCLUDE_PATH //为c语言程序设置include路径 export CPLUS_INCLUDE_PATH=path_name:$CPLUS_INCLUDE_PATH //为c++程序设置include路径 export LIBRARY_PATH=path_name:$LIBRARY_PATH //为静态库设置搜索路径 export LD_LIBRARY_PATH=path_name:$LD_LIBRARY_PATH //为动态库设置搜索路径
二、编译时动态库的搜索路径:
- 先找gcc的参数“-L”指定的目录;gcc main.c -L. -lxxx -lyyy -lzzz
- 再找gcc的环境变量:
LIBRARY_PATH;
再找内定目录:
/lib,/lib64
,/usr/lib ,/usr/lib64
,/usr/local/lib,/usr/local/lib64;
三、运行时动态库的搜索路径:
在编译目标代码时指定该程序的动态库搜索路径,还可以在编译目标代码时指定程序的动态库搜索路径。
- 编译目标代码时指定的动态库搜索路径,先找gcc的参数“-Wl,-rpath”指定的目录,当指定多个动态库搜索路径时,路径之间用冒号“:”分隔;
- 环境变量LD_LIBRARY_PATH指定动态库搜索路径,当通过该环境变量指定多个动态库搜索路径时,路径之间用冒号“:”分隔);
- 配置文件/etc/ld.so.conf中指定动态库搜索路径,该文件中每行为一个动态库搜索路径。每次编辑完该文件后,都必须运行命令ldconfig使修改后的配置生效,在这个文件内存放着可以被Linux共享的动态链接库所在目录的名字(系统默认的/lib, /usr/lib除外),多个目录之间可以使用空格,换行符进行隔开。;
- 默认的动态库搜索路径:/lib,/usr/lib
[root@2016d8c96b46 ~]# cat /etc/ld.so.conf include ld.so.conf.d/*.conf [root@2016d8c96b46 ~]#
[root@2016d8c96b46 ~]#cd /etc/ld.so.conf.d/ [root@2016d8c96b46 ld.so.conf.d]# ll total 8 -rw-r--r--. 1 root root 19 Apr 24 2022 dyninst-x86_64.conf -rw-r--r--. 1 root root 30 Mar 11 2022 pipewire-jack-x86_64.conf [root@2016d8c96b46 ld.so.conf.d]# cat ./* /usr/lib64/dyninst /usr/lib64/pipewire-0.3/jack/
(1)如何解决程序运行时找不到动态库的问题?
- gcc 加上“-Wl,-rpath ”参数,或者加上“-Wl,-R”参数,在gcc中使用ld链接选项时,需要在选项前面加上前缀-Wl(小写L), -R(或-rpath)指定程序运行时库的路径,它的缺点是只要更改了动态库, 那么就需要重新编译,不是一个好主意;
- 环境变量LD_LIBRARY_PATH指定动态库搜索路径,这是一个最佳方案;# export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/data/myso
- 修改配置文件/etc/ld.so.conf中指定动态库搜索路径,方案一般;echo "/data/myso" >> /etc/ld.so.conf;ldconfig /etc/ld.so.conf
- 将动态库添加到默认的动态库搜索路径/lib和/usr/lib中,最好是软连接,直接拷贝也行 # cp libtest.so /lib/
.
/etc/ld.so.conf 此文件记录了编译时使用的动态库的路径,也就是加载so库的路径 # 修改/etc/ld.so.conf的内容在最后添加库加载的新的路径
# 然后执行:ldconfig 使配置生效。
- /etc/profile中的系统环境变量对系统所有用户都生效,会在每个开机启动后进行加载;
- 添加到 ~/.bashrc,则会在shell启动时进加载;source ~/.bashrc 使其立刻生效;
GCC命令不仅能够编译,也能够链接程序,GCC链接程序是通过ld命令实现的,如何将GCC的命令行参数传递给ld命令呢,这就是通过“-Wl”来实现的:
命令:readelf -d
可以通过readelf -d来查看每个动态库的SONAME
-Wl选项告诉编译器将后面的参数传递给链接器。注意:-Wl, 后的逗号(,)必不可少,如果要传递多个参数,参数间用,分隔
静态库与动态库的区别:
执行依赖:
- 动态库在程序编译时并不会被链接到目标代码中,当你的程序执行到相关函数时才调用该函数库里的相应函数,故必须给程序的运行环境提供相应的库,在动态库中函数和变量的地址是相对地址而不是绝对地址,其真实地址在调用动态库的程序加载时形成的;
- 在编译时将库编译进可执行程序中,静态库的链接时将整个函数库的所有数据都整合进了目标代码,故程序的运行环境中不需要外部函数库;
名称区别:
- 动态库文件名:libxxx.so,库名前加lib,“xxx”为静态库名字,而Windows下的静态库名为:libxxx.dll;(动态库也叫共享库)
- 静态库文件名:libyyy.a, 库名前加lib,“yyy”为静态库名字,而Windows下的静态库名为:libyyy.lib;
文件大小:
- 可执行文件较小:动态库在程序编译时并不会被链接到目标代码中, 而是在程序运行时执行到相关函数时才调用该函数库里的相应函数,因此可执行文件体积较小。共享库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小;
- 可执行文件较大:静态库的代码在编译过程中已经被载入可执行程序,可执行程序运行时将不再需要动态库,移植方便,因此可执行文件体积较大。因为所有相关的对象文件与牵涉到库都被链接合成一个可执行文件,静态库的链接时将整个函数库的所有数据都整合进了目标代码;
库升级:
- 若动态库升级了, 只需要升级这个动态库文件, 而不需要去更换可执行文件;
- 若静态库升级了, 你的可执行程序必须重新编译;
4. 链接静态库和动态库的优先级
直接使用-l
查找路径的时候,gcc/g++默认优先链接动态库,找不到动态库后,才去找静态库
gcc xxx.c -o xxx -lopenssl // 链接openssl.so
要链接静态库就需要使用或者 -static 进行显示指定全都使用静态库
gcc xxx.c -o xxx -static -lopenssl // 链接libopenssl.a
也可以使用 -Wl,-Bstatic
, 这个选项使用后,后续的-l都是去找静态库,但可以通过Wl,-Bdynamic
再指定后面的都是动态库
//连接 libxx1.a libxx2.a libxx3.so
gcc xxx.c -o xxx -WL,-Bstatic -lxx1 -lxx2 -WL,-Bdynamic -lxx3
创建动态库.so文件:
动态链接库简介
- 动态库又叫动态链接库,是程序运行的时候加载的库,当动态链接库正确安装后,所有的程序都可以使用动态库来运行程序。动态库是目标文件的集合,目标文件在动态库中的组织方式是按特殊的方式组织形成的。在动态库中函数和变量的地址是相对地址而不是绝对地址,其真实地址在调用动态库的程序加载时形成的。
- 动态库的名字有别名(soname), 真名(realname)和链接名(linkername)。别名是由一个lib前缀,然后是库的名字,最后以“.so”结尾来构成。真名是动态链接库的真实名字,一般总是在别名的基础上添加一个版本号信息。除此之外还有一个链接名,他是在程序链接的时候使用的名字。
- 动态库安装的时候,总是复制库文件到某一个目录,然后使用一个软链接生成一个别名,在库文件更新的时候,仅仅更新软链接即可。
-fPIC
是编译选项,PIC
是 Position Independent Code 的缩写,表示要生成位置无关的代码,这是动态库需要的特性;-shared
是链接选项,告诉gcc生成动态库而不是可执行文件。为了让用户知道我们的动态库中有哪些接口可用,我们需要编写对应的头文件,比如可以写一个max.h
#ifndef __MAX_H__ #define __MAX_H__ int max(int a, int b, int c); #endif
如果希望把源码file1.c file2.c … fileN.c 做成库文件, 我们可以通过以下命令把他们制作成动态库:
gcc -shared -fPIC -o libxxx.so file1.c file2.c … fileN.c
举例说明:将hello.c,world.c直接制作成动态库libtest.so
gcc -fPIC -shared -o libtest.so hello.c world.c
也可分步制作:
#先将.c文件生成.o目标文件
gcc -c -fPIC hello.c world.c
# 再将.o目标文件生成.so动态文件
gcc -shared -o libtest.so hello.o world.o
使用动态库:
- -L指定动态库文件所在目录,指定为动态库所在位置的上上级目录都不行的,必须是动态库所在目录才行,静态库指定也一样的要求! -I(大写i)用来指定头文件目录则只要你指定的目录包含了你的头文件即可,并不需要头文件所在目录。
- 当你将动态库或静态库直接拷贝到系统库文件所在地方/lib或/lib64、/usr/lib或/usr/lib64、/usr/local/lib或/usr/local/lib64,那么可省去-L参数,但你拷贝到这些系统库目录的子目录时省去-L参数就不行,比如拷贝到/usr/local/lib/test/
- 放在/lib 和 /usr/lib 和 /usr/local/lib里的库直接用-l参数就能链接了,但如果库文件没放在这三个目录里,而是放在其他目录里,这时我们只用-l参数的话,链接还是会出错,出错信息大概是:“/usr/bin/ld: cannot find -lxxx”,也就是链接程序 ld 在那3个目录里找不到libxxx.so。
- 这时另外一个参数-L就派上用场了,比如常用的X11 的库,它在 /usr/X11R6/lib 目录下,我们编译时就要用 -L/usr/X11R6/lib -lX11 参数,-L 参数跟着的是库文件所在的目录名。再比如我们把libtest.so 放在/aaa/bbb/ccc 目录下,那链接参数就是 -L/aaa/bbb/ccc -ltest
比如:在当前目录下有main.c文件,需要用到/data/soa/share_lib/lib/libshare.so动态链接库和/data/soa/share_lib/include中的头文件
gcc main.c -I /data/soa/share_lib/include/ -L /data/soa/share_lib/lib/ -lmyshare
比如:
# gcc test.c -L. -lmax 来生成 a.out -L. 表示搜索要链接的库文件时包含当前路径 -lmax表示要链接 libmax.so 同一目录下同时存在同名的动态库和静态库,比如 libmax.so 和 libmax.a 都在当前路径下,则gcc会优先链接动态库 但是这样直接运行的话,会出现一个错误:
# ./a.out: error while loading shared libraries: libmax.so: cannot open shared object file: No such file or directory 由于 Linux 是通过/etc/ld.so.cache文件搜寻要链接的动态库的,而/etc/ld.so.cache是 ldconfig 程序读取/etc/ld.so.conf文件生成的,本次使用的动态库libmax.so并不在对应的目录下,就会导致程序无法找到对应的动态链接库,解决方法有: (1)如果仅仅是本地使用,可以在编译后指定一个环境变量:LD_LIBRARY_PATH=. ./a.out ,这样程序会在本地寻找 (2)如果需要在系统层面共享这个库,可以把 libmax.so 所在的路径添加到 /etc/ld.so.conf 中,再以 root 权限运行 ldconfig 程序,更新 /etc/ld.so.cache
(3)编译的时候,加上“-Wl,-rpath ”参数,或者加上“-Wl,-R”参数;
(4)将libmax.so拷贝到/usr/lib或/lib,或者在/usr/lib中建立软连接,连接到该libmax.so
具体采用的方法因使用场景而异,如果仅仅是测试用途的话,可以直接使用添加环境变量的方式解决。
查看文件:
[root@2016d8c96b46 lib]# file lishare.so
libshare.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=55d2af2eaf0be0797506d368d84eea76c89da4ce, not stripped
错误: error while loading shared libraries: libashared.so: cannot open shared object file: No such file or directory
这里为什么说链接器ld提示找不到库文件呢? 这是因为程序运行时没有找到动态链接库造成的. 这时一定会有人产生疑问, 我们在编译时不是使用’-L’ 指定动态库的路径了吗, 为什么运行时说我们找不到动态库呢?
程序编译时链接动态库和运行时使用动态链接库的概念是不同的,
在运行时, 程序链接的动态链接库需要在系统目录下才行. 系统目录包括( /lib、/lib64、/usr/lib以LD_LIBRARY_PATH环境变量指定的路径).
动态链接库管理命令:ldconfig,/etc/ld.so.cache
为了让新增加的动态链接库能够被系统所共享,我们需要设置运行动态链接库的管理命令ldconfig。
ldconfig命令的作用是在系统的默认搜索路径(/lib, /usr/lib, /usr/local/lib)以及动态链接库配置文件所列出的目录里搜索动态链接库,然后创建动态链接装入程序需要的链接和缓存文件。
搜索完毕后将结果写入到缓存文件“/etc/ld.so.cache”中, 文件中保存的是已经排好序的动态链接库名字列表,一般情况下里面的动态链接库很多,我们可以使用ldconfig -p命令来查看列表对应的动态库信息:
ldconfig -p
使用ldconfig命令默认情况下不输出扫描的结果信息,它的作用是更新系统默认搜索路径和配置文件中制定的搜索路径,然后将扫描结果缓存到“/etc/ld.so.cache”中,供运行程序快速访问调用。
我们也可以通过ldconfig命令来直接指定搜索路径:ldconfig 目录名。但这个是指临时制定,重新执行ldconfig则不会再包括制定的目录,除非在配置文件中添加上该目录。
动态库加载相关函数:
动态加载库和动态链接库不同的是, 一般的动态链接库需要在程序启动的时候就要寻找动态链接库,找到库函数。而动态加载库可以使用程序的方法控制什么时候 加载,动态加载库主要函数有: dlopen(), dlclose(), dlsym()和dlerror()。
函数dlopen()按照用户指定的方式打开动态链接库。 void *dlopen(const char *filename, int flags); # filename: 为动态链接库的文件名,当然可以包括路径部分 # flags: 打开方式,一般选择RTLD_LASY # 函数返回值为库指针 例如我们可以使用下面的栗子打开指定目录下的动态库libbhd_client.so: void *handle = dlopen("/tos/so/libbhd_client.so", RTLD_LASY);
获取函数指针dlsys()函数 我们使用动态链接库的最主要目的便是使用其中的函数接口(一个原因是模块间互相独立开发,另一个在于非开源保密)。 函数dlsys()可以获取指定函数名的函数指针,之后我们可以使用函数指针进行相关操作。 void *dlsym(void *handle, char *symbol) # handle : 为使用函数dlopen()获取到的动态链接库指针 # symbol : 函数的名称 # 返回值为函数指针
#include <stdio.h> #include <dlfcn.h> void main() { int (*add)(int x,int y); int (*sub)(int x,int y); void *libptr; libptr=dlopen("./libadd.so",RTLD_LAZY); //加载动态库 add=dlsym(libptr,"add"); //获取函数地址 sub=dlsym(libptr,"sub"); printf("add(5,4) is %d\n",add(5,4)); printf("sub(5,4) is %d\n",sub(5,4)); dlclose(libptr); }
创建与使用静态库.a文件:
创建静态库:ar -crs
如果希望把源码file1.c, file2.c …fileN.c 做成库文件, 我们可以通过下命令把他们制作成静态库:
gcc -c file1.c
gcc -c file2.c
…
gcc -c flieN.c
ar -crs libname.a file1.o file2.o … fileN.o
说明:‐c: create 的意思;‐r: replace 的意思
在进行编译之前介绍一下与库相关的 gcc 编译选项:
- -I(大写i) 指定include头文件的搜索路径;
- -L(大写L)指定链接所需库所在路径;比如:-L /data/soa/mystatic
- -l(小写L) 指定所需链接库的库名(比如链接libmystatic.a) -lmystatic;
- -static: 静态链接,禁止使用动态库, 让链接静态库后的程序彻底的独立起来, “完全静态”, 因此, 得到的二进制文件会非常大;
查看静态库中有哪些目标文件:ar -t
[root@igoodful src]ar -t libsum.a sum.o [root@igoodful src]
使用静态库:
比如在当前目录下有main.c文件,需要用到/data/soa/static_lib/lib/libmystatic.a静态链接库和/data/soa/static_lib/include中的头文件
gcc main.c -I /data/soa/static_lib/include/ -L /data/soa/static_lib/lib/ -lmystatic -static
分两步:
# gcc -c -I /home/xxxx/include test.c //假设test.c要使用对应的静态库 # gcc -o test -L /home/xxxxx/lib test.o libadd.a
一步到位:
gcc -c -I /home/xxxx/include -L /home/xxxxx/lib libadd.a test.c
查看可执行程序所依赖的库:ldd
[root@2016d8c96b46 ~]# ldd /bin/ls linux-vdso.so.1 (0x00007ffdd34bf000) libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f1028819000) libcap.so.2 => /lib64/libcap.so.2 (0x00007f102880f000) libc.so.6 => /lib64/libc.so.6 (0x00007f1028600000) libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007f1028564000) /lib64/ld-linux-x86-64.so.2 (0x00007f1028877000) [root@2016d8c96b46 ~]# ldd ./a.out linux-vdso.so.1 (0x00007ffd05d16000) libc.so.6 => /lib64/libc.so.6 (0x00007f5bcb200000) /lib64/ld-linux-x86-64.so.2 (0x00007f5bcb5d6000) [root@2016d8c96b46 ~]#
查看库中的所有符号:nm
nm工具可以打印出库中的涉及到的所有符号,下面是我们查看我们创建的动态库libadd.a:
[root@2016d8c96b46 soa]# nm libhello.so 0000000000004030 b completed.0 w __cxa_finalize@GLIBC_2.2.5 0000000000001060 t deregister_tm_clones 00000000000010d0 t __do_global_dtors_aux 0000000000003e10 d __do_global_dtors_aux_fini_array_entry 0000000000003e18 d __dso_handle 0000000000003e20 d _DYNAMIC 0000000000001134 t _fini 0000000000001110 t frame_dummy 0000000000003e08 d __frame_dummy_init_array_entry 0000000000002088 r __FRAME_END__ 0000000000004000 d _GLOBAL_OFFSET_TABLE_ w __gmon_start__ 0000000000002008 r __GNU_EH_FRAME_HDR 0000000000001119 T hello 0000000000001000 t _init w _ITM_deregisterTMCloneTable w _ITM_registerTMCloneTable U puts@GLIBC_2.2.5 0000000000001090 t register_tm_clones 0000000000004030 d __TMC_END__ U world [root@2016d8c96b46 soa]#
安装库:
(1)配置环境变量:/etc/profile
永久生效的环境变量设置,编辑/etc/profile即可。在/etc/profile文件里末尾加上对应的动态库的环境变量LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=/home/work/mylib/:$LD_LIBRARY_PATH
/home/work/mylib/指的是动态库文件夹所在位置。.so等文件在/home/work/mylib/下。
编辑完成,保存编辑并退出; 使配置即时生效:
# source /etc/profile
(2)拷贝到/lib或者/usr/lib下
如果直接安装在/lib或者/usr/lib下,那么ld默认能够找到,无需其他操作。
也可在/lib或者/usr/lib下创建软连接到实际的lib路径:
(3)./etc/ld.so.conf
编辑/etc/ld.so.conf文件,加入库文件所在目录的路径:
vim /etc/ld.so.conf
运行ldconfig,该命令会重建/etc/ld.so.cache文件:
# ldconfig
库迁移:
生成Makefile
./configure --host=arm-linux-gnueabihf --prefix=$PWD/temp_install
编译, 安装
make
make install
注意这个库的安装程序有BUG,不会自动创建发布的lib,include,man等,因此要手工创建,要不先把其它库做好,再安装这个库
mkdir -p /home/peng/jpeg-6b/temp_install/include
mkdir -p /home/peng/jpeg-6b/temp_install/lib
mkdir -p /home/peng/jpeg-6b/temp_install/man/man1
当我们在编写 C/C++ 程序的时候如何引入第三方的代码或者库呢?相信你跟我有类似的疑问,带着这个疑问我们一道去探究一下。经过编写测试代码和查阅网上的文章,我大概总结了下面这几种方式:
一、直接引入他人的源文件
二、分别引入头文件和源文件
三、引入头文件和静态库(打包好的二进制目标文件)
四、引入头文件和动态链接库(下文会具体解释什么是动态链接库)
五、在代码中通过 shell 调用他人的可执行命令程序
下面我们详细看一下流程是怎样的。
一、直接引入源文件
首先先新建一个项目文件夹 import-project 用来存放我们的代码。创建 main.c 文件,这里面是我们自己写的代码。然后在 import-project 目录下再创建一个子文件夹 thirdparty ,这个文件夹是用来存放我们需要引入的第三方的代码,目录结构大致如下:
~/import-project/ ├─ thirdparty/ │ └─ sum.c └─ main.c
大致思路是:main.c 中要调用一个外部函数 sum() 来实现两个数相加,而这个 sum() 是一个第三方的函数,需要引入才能使用,下面是这两个文件的代码:
// main.c 文件
#include <stdio.h> #include "thirdparty/sum.c" int main() { int a = 1; int b = 2; printf("%d + %d = %d\n", a, b, sum(a, b)); return 0; }
// thirdparty/sum.c 文件
int sum(int a, int b) { return a + b; }
在命令行中使用 gcc 编译我们的项目,然后执行生成的可执行程序。
~/import-project$ gcc main.c -o main ~/import-project$ ./main 1 + 2 = 3
可见我们成功引入了他人的代码,并编译出了可执行程序。这种引入方式还是比较常见的,Github 上有很多小工具库都可以用类似的方式引入,通常它们只是一些 .h、.hpp、.cpp、.cc 的单文件。下载下来直接 include 即可,如:Github:nlohmann/json。
二、分别引入头文件和源文件
当然绝大部分的 C/C++ 库是把头文件和源文件分开了的,这样分开主要有以下两个好处:
作为接口:当源文件编译为不可读的二进制文件时,其他开发者依然可以通过头文件去了解此二进制包含哪些函数和功能可供调用。
拆分编译:在大型项目中,源文件特别多,任意一个文件做了小小的修改,都需要全部重新编译生成可执行文件,这个过程是很耗时间的。于是开发者想到了拆分编译,只需单独编译修改过的源文件,再把上一次已经编译好的其他文件链接起来,就构成了最终的可执行程序。但 C 语言中变量和函数的使用必须先有定义,要想单独编译一个源文件,就必须先把它里边要用的外部变量和函数先声明好,这便是引入头文件的作用。
了解完头文件,我们再来看看这种头文件和源文件分开的第三方代码如何引入。代码还是我们之前的代码,不过我们要新增加一个 sum.h 头文件,再做一点小小的修改。
// main.c 文件
#include <stdio.h> #include "thirdparty/sum.h" int main() { int a = 1; int b = 2; printf("%d + %d = %d\n", a, b, sum(a, b)); return 0; }
// thirdparty/sum.h 文件
int sum(int a, int b); // thirdparty/sum.c 文件 int sum(int a, int b) { return a + b; }
在命令行中我们使用 gcc 编译我们的项目,然后执行生成的可执行程序。
~/import-project$ gcc main.c ./thirdparty/sum.c -o main
~/import-project$ ./main
1 + 2 = 3
main.c 中调用的 sum() 函数我们通过#include "thirdparty/sum.h" 引入了定义,因此编译不会出错。gcc 会分别编译 main.c 和 sum.c,最后再把他们链接起来构成最终的 main 程序。
三、引入头文件和静态库
先说明一下什么是目标文件?
目标文件(Object,.o 结尾)是由源文件(.c、.cpp)编译但还未链接得到的二进制文件,目标文件此时已完成为了编译流程(预处理 -> 编译 -> 组装 -> 链接)中的前三步。
那什么又是静态库呢,怎样获得静态库呢?
静态库是由多个目标文件打包到一起得到的二进制文件,命名约定俗成以 lib 开头,中间是库名,然后是 .a 结尾,形如:libNAME.a。(感谢酷友 小瑾她爸 的提示)
这里我们把第二点中编译的流程拆成三步来完成,代码保持不变,我们只更改一下编译流程。
步骤 1:首先我们用 gcc -c 参数只编译 sum.c 为二进制目标文件。注意这里生成的 sum.o 二进制文件,不能在命令行中直接执行,是因为:
gcc -c 只会编译出二进制,但并不会链接,因此生成的目标文件无法调用 C 的任何库。
sum.c 文件中并没有 main() 函数,这是程序执行的入口,没有是无法启动执行的。
~/import-project$ gcc -c ./thirdparty/sum.c -o sum.o
步骤 2:接着我们把 sum.o 打包到静态库中,这里需要用到一个命令 ar(archive 的缩写),简单介绍一下参数:
-r replace 如果静态库中目标文件已存在,则替换为最新的。
-c 如果静态库不存在,在创建的时候不用弹出警告提示。
~/import-project$ ar -rc libsum.a sum.o
如果我们有多个目标文件可以依次放在后面,最终会被一并打包进 libsum.a 静态库中。查看静态库中目标文件列表可以用 ar -t 参数:
~/import-project$ ar -t libsum.a
sum.o
步骤 3:把 main.c 和 上一步输出的静态库 libsum.a 合在一起编译出最终的可执行程序。
~/import-project$ gcc main.c libsum.a -o main
~/import-project$ ./main
1 + 2 = 3
通过上面的例子可以看出,如果我们只是修改了 main() 函数中的业务代码,我们并不需要重新编译生成 libsum.a,这大大节省了我们的编译时间。
四、引入头文件和动态链接库
在 Linux 系统中,你可以看到 /lib 和 /usr/lib 目录下有很多形如 lib___.so._ 的二进制文件,这些都是库文件。
Linux 下的可执行程序为了让打包出来的可执行程序尽可能小,同时也为了尽可能重用代码,把常用的一些函数功能都封装到了这些共享库(Shared Object,后缀 .so)中。
在编译程序时共享库的内容并不打包到最终的可执行程序中,而是在程序执行时动态链接调用,因此这些共享库通常也被称之为动态链接库,程序运行时会自动到 /lib 和 /usr/lib 等库目录去搜索,当然你也可以指定一个自己的库目录。
此外 Linux 中库文件的命名约定俗成以 lib 开头,中间是库名,然后是 .so 结尾,形如:libNAME.so。
下面我们来试试自己构建一个动态链接库,然后让可执行文件调用此库中的 sum() 函数。
使用的代码任然和第二点中的一致(希望你还没有删掉),我们使用这个命令来生成 sum.c 的动态链接库:
~/import-project$ gcc -shared ./thirdparty/sum.c -o libsum.so
接着去生成 main.c 的可执行程序,简单介绍一下参数:
-L. 指定编译时自定义的链接库目录,. 代表当前目录。
-lsum 指定要动态链接的库,只需写名字,系统会自动加前后缀,即:libsum.so
-Wl,-rpath=. 首先 -Wl,<option> 是传递选项给链接器,选项之间用 , 号分隔,-rpath=. 这个选项设置了程序运行时自定义库的目录为当前目录。
~/import-project$ gcc main.c -o main -L. -lsum -Wl,-rpath=.
~/import-project$ ./main
1 + 2 = 3
可见生成的 main 可执行程序比之前小了一点,但结果依旧符合预期。
Linux 中 ld 命令用来链接库,ldd 则可以打印程序链接库信息。我们用 ldd 来打印一下 main 程序的链接库信息:
~/import-project$ ldd ./main linux-vdso.so.1 (0x00007fffe1174000) libsum.so => ./libsum.so (0x00007ff8c9340000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff8c9140000) /lib64/ld-linux-x86-64.so.2 (0x00007ff8c935f000)
libsum.so 库确实被链接上了。
运行时链接动态库
除了上面那种在编译时添加动态库的链接外,一种更灵活的方式是在运行时进行动态库的链接,这样可以像插件一样自由载入需要使用的动态库。代码如下:
// main.c
#include <stdio.h> #include <dlfcn.h> int main() { void *handle = dlopen("./libsum.so", RTLD_NOW); if (handle == NULL) { printf("%s\n", dlerror()); return 1; } int (*sum)(int, int); // 定义一个函数指针 sum = dlsym(handle, "sum"); char *err = dlerror(); if (err != NULL) { printf("%s\n", err); return 1; } int a = 1; int b = 2; printf("%d + %d = %d\n", a, b, sum(a, b)); dlclose(handle); return 0; }
dlfcn.h 对应的库不在标准库中,而是作为动态库存在于系统,因此编译时需要使用 -ldl 来使用这个动态库。
~/import-project$ gcc main.c -o main -ldl
~/import-project$ ./main
1 + 2 = 3
可见,我们在运行时加载 libsum.so 库成功了。
五、调用可执行命令
介绍最后一种调用他人代码的方式:调用他人已编译好的可执行命令。这次我们需要调整一下我们的代码,首先删除 sum.h 头文件,让 sum.c 直接编译出一个可以执行的命令,代码修改后如下:
// main.c 文件
#include <stdio.h> #include <stdlib.h> #include <string.h> void exec(const char *cmd, char *res) { FILE *fp = NULL; if ((fp = popen(cmd, "r")) == NULL) { return; } fscanf(fp, "%s", res); fclose(fp); } int main() { int a = 1; int b = 2; char cmd[50], res[20]; sprintf(cmd, "./sum %d", a); strcat(cmd, " %d"); sprintf(cmd, cmd, b); exec(cmd, res); printf("%s = %s\n", cmd, res); return 0; }
// thirdparty/sum.c 文件
#include <stdio.h> #include <stdlib.h> int sum(int a, int b) { return a + b; } int main(int argc, char *argv[]) { int a = argc > 1 ? atoi(argv[1]) : 0; int b = argc > 2 ? atoi(argv[2]) : 0; printf("%d\n", sum(a, b)); return 0; }
先编译 sum.c 生成一个可执行的命令,测试一下运算 4 + 5 的结果:
~/import-project$ gcc ./thirdparty/sum.c -o sum
~/import-project$ ./sum 4 5
9
sum 命令编译测试好了,接着编译 main.c 生成主可执行程序,运行测试:
~/import-project$ gcc main.c -o main
~/import-project$ ./main
./sum 1 2 = 3
测试成功!在 main 主程序中通过 shell 的方式执行了 sum 命令完成了求和运算,最后将结果输出。
至此这五种 C/C++ 引入第三方的代码的方式就介绍完了~
参考列表:
################