我们常见的可执行程序有不少都是不带参数直接执行的,特别对于界面交互类应用更是如此。但也有很多命令交互程序是带参数执行的,比如GCC编译指令不仅支持多参数运行,还有参数类型选项(gcc [options] file…),就如同下面的命令:
gcc -g main.cpp -o main
回想下我们常用main函数的参数int main(int argc, char* argv[]),其中argc表示参数个数,argv[]则是保存具体参数的字符串指针数组,默认执行程序名作为第一个参数。比如下面给出一段代码,原样输出所有的参数,按上面给出的gcc命令编译链接后输出可执行程序main,读者可试着加参数运行确认输出结果。
#include
int main(int argc, char *argv[])
{
for(int i = 0; i < argc; ++i){
printf("argv[%d]: %s\n", i, argv[i]);
}
return 0;
}
shell script也是可以加参数运行的,具体方法可参考博客:shell中脚本参数传递的两种方式
基于模块化程序设计的原则,一个复杂程序通常由多个模块相互链接而成,每个模块都可以是一个函数库,这便是扩展库。链接库主要有以下好处:
链接库根据链接方式不同,可分为静态链接库(.a, .lib)和动态链接库(.so, .dll),二者区别主要在链接阶段如何处理库:
一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:
Linux静态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.a。
// myadd.cpp
#include "mylib.h"
float add(float a, float b)
{
return a + b;
}
// mysub.cpp
#include "mylib.h"
float sub(float a, float b)
{
return a - b;
}
// mymul.cpp
#include "mylib.h"
float mul(float a, float b)
{
return a * b;
}
// mylib.h
#ifndef _TEST_H
#define _TEST_H
extern "C" float add(float a, float b);
extern "C" float sub(float a, float b);
extern "C" float mul(float a, float b);
#endif
g++ -c myadd.cpp mymul.cpp mysub.cpp # -c参数只编译汇编不链接
ar -crv libstaticmath.a myadd.o mymul.o mysub.o # -c创建一个库,-r在库中加入或替换成员文件, -v显示操作的附加信息
大一点的项目会编写Makefile文件(CMake等工程管理工具)来生成静态库,省去了输入太多命令的麻烦,本文最后也给出了完整的Makefile代码,想了解Makefile可以参考另一篇文章:VSCode+GCC+Makefile+GitHub项目管理。
// implicit.cpp 隐式调用测试代码
#include "./lib/mylib.h"
#include
#include
using namespace std;
int main(int argc, char *argv[])
{
float a = 3.7, b = 2.9;
for(int i = 0; i < argc; ++i){
printf("argv[%d]: %s\n", i, argv[i]);
if(i == 1)
a = atof(argv[1]);
if(i == 2)
b = atof(argv[2]);
}
cout << "a + b = " << add(a, b) << endl;
cout << "a - b = " << sub(a, b) << endl;
cout << "a * b = " << mul(a, b) << endl;
return 0;
}
g++ implicit.cpp -I./lib -L./lib -lstaticmath -static -o testa # -I指定头文件搜索路径,-L指定库文件搜索路径, -l指定库文件名, -static表示库文件不共享
动态链接库的出现,主要是为了解决静态链接库的一些问题,主要有以下两点:
代码虽然可复用,但没法共享,造成空间浪费。
静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
由上图可看出,动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。综上将动态链接库的特点总结如下:
动态库把对一些库函数的链接载入推迟到程序运行的时期。
可以实现进程之间的资源共享。(因此动态库也称为共享库)
将一些程序升级变得简单。(增量更新)
甚至可以真正做到链接载入完全由程序员在程序代码中控制(显式调用)。
Linux动态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.so。
g++ -fPIC -c myadd.cpp mymul.cpp mysub.cpp # -fPIC(Position Independent Code)编译为位置独立的代码
g++ -shared -o libdynamicmath.so myadd.o mymul.o mysub.o # -shared生成共享目标文件
g++ -fPIC -shared -o libdynamicmath.so myadd.cpp mymul.cpp mysub.cpp
测试代码也使用上面静态库时的示例代码:
g++ implicit.cpp -I./lib -L./lib -ldynamicmath -o testso
编译链接生成可执行文件正常,但在运行可执行文件时报错如下,经查询ld动态载入器的定位过程,发现ld默认能找到/lib或/usr/lib下的库文件,如需查找其他目录,还需要将库文件绝对路径添加到/etc/ls.so.conf文件中,并用ldconfig命令重建ld.so.cache文件。
paul@ubuntu:~/Desktop/MyCode$ ./testso
./testso: error while loading shared libraries: libdynamicmath.so: cannot open shared object file: No such file or directory
export LD_LIBRARY_PATH=`pwd` #将当前路径添加到动态库路径环境变量
上面介绍的动态库使用方法和静态库类似属于隐式调用,编译的时候指定相应的库和查找路径,可执行程序中也需要包含链接库头文件。其实,动态库还可以显式调用,不需要包含链接库的头文件。
Linux显式调用动态库,#include
下面给出示例代码,生成动态链接库(libdynamicmath.so)的代码跟前面一致,这里只列出测试代码explicit.cpp如下:
// explicit.cpp不再包含./lib/mylib.h头文件,通过程序内部命令显式加载和释放
#include
#include
#include
using namespace std;
int main(int argc, char *argv[])
{
if(argc < 2){
cout << "Argument error." << endl;
exit(1);
}
float a = 3.7, b = 2.9;
char *libname = nullptr;
char *err = nullptr;
for(int i = 0; i < argc; ++i){
printf("argv[%d]: %s\n", i, argv[i]);
if(i == 1)
libname = argv[1];
if(i == 2)
a = atof(argv[2]);
if(i == 3)
b = atof(argv[3]);
}
//open the lib
void *handle = dlopen(libname, RTLD_NOW);
if(!handle){
cout << "Load" << libname << "failed" << dlerror() << endl;
exit(1);
}
//clear error info
dlerror();
//get function pointer
typedef float (*pf_t)(float, float);
pf_t add = (pf_t)dlsym(handle, "add");
pf_t sub = (pf_t)dlsym(handle, "sub");
pf_t mul = (pf_t)dlsym(handle, "mul");
err = dlerror();
if(err){
cout << "Can't find symbol function" << err << endl;
exit(1);
}
//call library function
cout << "a + b = " << add(a, b) << endl;
cout << "a - b = " << sub(a, b) << endl;
cout << "a * b = " << mul(a, b) << endl;
//close the lib
dlclose(handle);
if(dlerror()){
cout << "Close" << libname << "failed" << dlerror() << endl;
exit(1);
}
return 0;
}
编译生成可执行文件时需要添加-ldl参数声明链接器链接了一个动态库,命令如下:
g++ explicit.cpp -ldl -o testexp # -ldl显式加载动态库的动态函数库
从上面的执行结果可以看出示例程序实现了把动态链接库作为参数传递给可执行程序的方式进行显式调用,如果多个动态库包含一个同名函数的不同实现,可以通过传参调用不同的动态库实现多态的效果。读者也可以稍加改动,把函数名也通过参数传递给可执行程序实现选择调用。可以通过nm -D libdynamicmath.so命令或objdump -T libdynamicmath.so查看符号表,从中找到库文件里面的函数名。
在包含库源文件和库头文件的目录下./lib新建一个Makefile文件,代码如下:
#自定义变量
MAKE = make
CC = g++
AR = ar
#静态库编译选项,-Wall生成所有警告、-O0不优化、-std=c++11采用c++11标准、-g输出调试信息、-c只编译汇编不链接
CAFLAGS = -Wall -O0 -std=c++11 -g -c
#动态库编译选项,-fPIC(Position Independent Code)、-shared生成共享目标文件
CSOFLAGS= -fPIC -shared -g
#打包选项,-c创建一个库,-r在库中加入或替换成员文件, -v显示操作的附加信息
ARFLAGS = -crv
#wildcard为Makefile模式匹配关键字,获取目标目录符合匹配模式的所有文件名
LIBSRCS = $(wildcard ./*.cpp)
#patsubst为Makefile模式替换关键字,查找字符串SRCS中按空格分开的单词,并将符合模式%.cpp的字符串全部替换成%.o
LIBOBJS = $(patsubst ./%.cpp, ./%.o, $(LIBSRCS))
LIBA = libstaticmath.a
LIBSO = libdynamicmath.so
RM = rm -f
#默认任务
default:
#默认任务要执行的命令,按上面的变量名替换为变量值后执行
$(MAKE) liba
$(MAKE) libso
#模式匹配,冒号前者为目标项,冒号后面为依赖项
liba: $(LIBOBJS)
$(AR) $(ARFLAGS) $(LIBA) $(LIBOBJS)
libso: $(LIBSRCS)
$(CC) $(CSOFLAGS) $(LIBSRCS) -o $(LIBSO)
# %模式自动匹配符
%.obj: %.cpp
# $<表示规则中的第一个依赖项、$@表示规则中的目标项
$(CC) $(CAFLAGS) $< -o $@
#伪目标,声明clean为伪目标或标签,为了避免该清理任务与文件名相同而被错识别
.PHONY: clean
clean:
#清理之前的目标文件,以便下次完整的重新编译
$(RM) $(LIBOBJS) $(LIBA) $(LIBSO)
在测试示例源码目录下新建Makefile文件,编写代码如下:
#自定义变量
CC = g++
MAKE = make
#静态链接选项,-g生成调试信息、-I指定头文件搜索路径,-L指定库文件搜索路径、-l静态库名、-static不共享
LDAFLAG = -g -I./lib -L./lib -lstaticmath -static
#动态编译选项,-g生成调试信息、-I指定头文件搜索路径,-L指定库文件搜索路径、-l动态库名
LDSOFLAG= -g -I./lib -L./lib -ldynamicmath
#显式链接选项,-ldl显式加载动态库的动态函数库
LDEXFLAG= -g -ldl
SRCIMP = implicit.cpp
SRCEXP = explicit.cpp
INCLUDE = ./lib
LIBA = $(INCLUDE)/libstaticmath.a
LIBSO = $(INCLUDE)/libdynamicmath.so
RM = rm -f
#默认任务
default:
#默认任务要执行的命令,按上面的变量名替换为变量值后执行
$(MAKE) testa
$(MAKE) testso
$(MAKE) testexp
#模式匹配,冒号前者为目标项,冒号后面为依赖项
testa: $(LIBA) $(SRCIMP)
$(CC) $(SRCIMP) $(LDAFLAG) -o $@
#如果执行报错,提示找不到动态库,需要添加环境变量export LD_LIBRARY_PATH=`pwd`/lib
testso: $(LIBSO) $(SRCIMP)
$(CC) $(SRCIMP) $(LDSOFLAG) -o $@
testexp: $(LIBSO) $(SRCEXP)
$(CC) $(SRCEXP) $(LDEXFLAG) -o $@
#make -C后跟目标目录,读取目标目录下的Makefile
$(LIBA): $(INCLUDE)
$(MAKE) -C $(INCLUDE) liba
$(LIBSO): $(INCLUDE)
$(MAKE) -C $(INCLUDE) libso
#伪目标,声明clean为伪目标或标签,为了避免该清理任务与文件名相同而被错识别
.PHONY: clean
clean:
#清理之前的目标文件,以便下次完整的重新编译
$(RM) testa testso testexp
$(MAKE) -C $(INCLUDE) clean
本文的源代码可以到GitHub下载:https://github.com/StreamAI/LinkLibrary(不熟悉GitHub使用的可以参考文章:GitHub社会化编程)。
如果想了解Windows环境动态库与静态库的创建和使用,可以查看我的另一个博文:Windows静态链接库与动态链接库的创建和显式与隐式调用
如果想深入了解动态库与静态库的链接与调用过程,推荐一本书《程序员的自我修养—链接、装载与库》。