4.动态库和静态库
如果编写了一个很有价值的算法,想让别人使用,但又不想公开代码。该怎么办?
在C/C++中,使用库的技术,将编译好的代码提供给他人使用。库分为动态库和静态库,在windows系统下,动态库扩展名为dll,静态库扩展名为lib;在linux系统下,动态库扩展名为so,静态库扩展名为a。
4.1动态库
4.1.1动态库的生成
Linux下动态库的规范命名:libxxx.so。前缀lib,库名称xxx,扩展名so。
在/home/lzb/linux_projects/project4/lib目录下,有other.cpp、other.h,内容如下:
//other.cpp
#include <stdio.h>
#include "other.h"
void fun(void)
{
printf("hello linux!\n");
}
//other.h
void fun(void);
方法1:命令行编译
cd /home/lzb/linux_projects/project4/lib
编译:加-fPIC选项
g++ -c -fPIC other.cpp -o other.o
链接:加-shared选项
g++ -shared other.o -o libother.so
方法2:使用Makefile
在/home/lzb/linux_projects/project4目录下,创建文件Makefile,内容如下:
#Makefile:shared library
EXE=libother.so
SUBDIR=lib
CXX_SOURCES = $(foreach dir, $(SUBDIR), $(wildcard $(dir)/*.cpp))
CXX_OBJECTS = $(patsubst %.cpp, %.o, $(CXX_SOURCES))
DEP_FILES = $(patsubst %.o, %.d, $(CXX_OBJECTS))
$(EXE):$(CXX_OBJECTS)
g++ -shared $(CXX_OBJECTS) -o $(EXE)
%.o:%.cpp
g++ -c -fPIC -MMD $< -o $@
-include $(DEP_FILES)
clean:
rm -rf $(CXX_OBJECTS) $(DEP_FILES) $(EXE)
提交:other.h、libother.so、使用平台。
4.1.2动态库的使用—自动加载
在/home/lzb/linux_projects/project5/lib目录下,有libother.so;在/home/lzb/linux_projects/project5/include目录下,有other.h;在/home/lzb/linux_projects/project5目录下,有hello.cpp,其内容如下:
#include "./include/other.h"
int main()
{
fun();
return 0;
}
方法1:命令行编译
cd /home/lzb/linux_projects/project5
(1)编译:
g++ -c hello.cpp -o hello.o
(2)链接:-L指定库文件的位置,-lother指定使用libother.so库文件
g++ hello.o -o hello -L./lib -lother
(3)运行:
./hello
./hello: error while loading shared libraries: libother.so: cannot open shared object file: No such file or directory
为什么操作系统找不到libother.so?
操作系统寻找库文件时,会从标准位置开始寻找。对于Linux系统,寻找目录为/lib、/usr/lib、/usr/local/lib。若在这些目录没有找到对应的库文件,则从LD_LIBRARY_PATH环境变量里寻找。
在Linux中,库文件要么放在标准位置,要么放在LD_LIBRARY_PATH指定的位置。常采用第二种方法。
(4)设置环境变量LD_LIBRARY_PATH:
export LD_LIBRARY_PATH=./lib
(5)查看环境变量LD_LIBRARY_PATH:
echo $LD_LIBRARY_PATH
./lib
(6)运行:
./hello
hello linux!
(7)查看hello依赖的库:
readelf -d hello
基本库:libc.so标准C库、libstdc++.so标准C++库,这2个库不需要在命令行中指定,g++会默认链接。
方法2:Makefile
通常使用的第三库目录结构,一般是libxxx/lib存放库文件、libxxx/include存放头文件。因此在/home/lzb/linux_projects/project5/lib目录下,有libother.so;在/home/lzb/linux_projects/project5/include目录下,有other.h;在/home/lzb/linux_projects/project5/src目录下,有hello.cpp,在/home/lzb/linux_projects/project5目录下,有Makefile。
#include “other.h”表示在当前目录下搜索头文件other.h,若不存在则去系统的头文件目录下查找。Linux系统头文件目录为:/usr/include、 /usr/local/include。
#include <other.h>表示在系统的头文件目录下查看other.h,g++编译器可以使用-l选项来指定自定义的头文件查找目录。
//hello.cpp
#include <other.h>
int main()
{
fun();
return 0;
}
#Makefile
EXE=hello
SUBDIR=src
#CXXFLAGS:编译选项
CXXFLAGS += -I/home/lzb/linux_projects/project5/include
#LDFLAGS:链接选项
LDFLAGS += -L/home/lzb/linux_projects/project5/lib -lother
#LDFLAGS += -L/home/lzb/linux_projects/project6/lib -lother
CXX_SOURCES = $(foreach dir, $(SUBDIR), $(wildcard $(dir)/*.cpp))
CXX_OBJECTS = $(patsubst %.cpp, %.o, $(CXX_SOURCES))
DEP_FILES = $(patsubst %.o, %.d, $(CXX_OBJECTS))
$(EXE):$(CXX_OBJECTS)
g++ $(CXX_OBJECTS) -o $(EXE) $(LDFLAGS)
%.o:%.cpp
g++ -c $(CXXFLAGS) -MMD $< -o $@
-include $(DEP_FILES)
clean:
rm -rf $(CXX_OBJECTS) $(DEP_FILES) $(EXE)
命令行编译运行:
cd /home/lzb/linux_projects/project5
make
export LD_LIBRARY_PATH=./lib
./hello
上面Makefile主要是使用命令行:
g++ -c -I/home/lzb/linux_projects/project5/include -MMD src/hello.cpp -o src/hello.o
g++ src/hello.o -o hello -L/home/lzb/linux_projects/project5/lib -lother
上述的-L和-I中,可以使用绝对路径,也可以使用相对路径,如:
g++ -c -I./include -MMD src/hello.cpp -o src/hello.o
g++ src/hello.o -o hello -L./lib -lother
注:-I/home/lzb/linux_projects/project5/include中的-I是include的首字符i的大写;-L/home/lzb/linux_projects/project5/lib -lother中的-l是libxxx中的l,也是L的小写。
4.1.3动态库的使用—手工加载
在windows平台下,动态库分为隐式调用和显式调用。同理,在Linux平台下,动态库分为自动加载和手工加载。在前面4.1.2中讲的是动态库的自动记载,在4.1.3会讲解动态库的手工加载。
在windows平台下,在应用程序中使用函数来调用。(1)LoadLibrary函数是加载DLL且返回句柄;(2) GetProcAddress是根据句柄找到动态库中的函数,并返回此函数的指针;(3)使用此函数指针来调用动态库中的某个函数;(4)FreeLibrary释放动态库。
在Linux平台下,也是在应用程序中使用函数来调用。(1)dlopen函数用来加载动态库;(2)dlsym根据句柄找到动态库中的函数,并返回此函数的指针;(3)使用此函数指针来调用动态库中的某个函数;(4)dlclose卸载动态库。注:手工加载的方式只适合于调用C库。
使用这3个函数,需要添加头文件#include <unistd.h>、#include <dlfcn.h>;在链接时需要添加链接选项-ldl;库文件libother.so是使用other.c和other.h来编译;库文件libother.so是使用other.cpp和other.h来编译,但other.h的函数声明前加extern “C”。
//hello.cpp
#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
int main()
{
//步骤1:加载库,返回一个句柄(handle),指向加载到内存里的库
void * libother = dlopen("lib/libother.so", RTLD_NOW);
if(!libother)
{
printf("Load Library Failed...\n");
return -1;
}
//步骤2:找到函数符号
typedef void (*SO_FUNCTION) (void);
SO_FUNCTION fun = (SO_FUNCTION) dlsym(libother, "fun");
if(!fun)
{
printf("Can Not Find Symbol...\n");
return -1;
}
//步骤3:调用函数
fun(1, 2);
//步骤4:卸载动态库
dlclose(libother);
return 0;
}
g++ hello.cpp -o hello -ldl
export LD_LIBRARY_PATH=./lib
./hello
注意:
(1)手工加载不需要提供头文件,如other.h。
(2)手工加载实际上是搜索动态库中的函数符号(未修饰,即函数名)。若是C++库,其函数会在编译动态库时进行名称修饰;C库函数名称不变。解决方法:在进行C++库编译时,在头文件的函数声明前,加上extern “C”,表示此函数按照C语言的方式来编译。
4.2静态库
4.2.1静态库的生成和使用
静态库的命名方式:libxxx.a。
(1)编译:得到*.o文件;
(2)打包:将多个*.o文件打包,并不是链接
ar -rcs libxxx.a file1.o file2.o ... fileN.o
如:
g++ -c other.cpp -o other.o
ar -rcs libother.a other.o
提交静态库:other.h、libother.a、使用平台。
在/home/lzb/linux_projects/project6/lib目录下,有libother.a;在/home/lzb/linux_projects/project6/include目录下,有other.h;在/home/lzb/linux_projects/project6/src目录下,有hello.cpp。
方法1:由于静态库本质上是*.o文件的打包,因此可以像*.o一样使用libxxx.a。
cd /home/lzb/linux_projects/project6/src
g++ -I../include hello.cpp ../lib/libother.a -o hello
./hello
其中,-I../include中的-I是include首字符的大写;执行hello不需要再设置环境变量LD_LIBRARY_PATH,因为静态库编译到可执行文件中去了,在执行hello时不需要静态库的支持。
方法2:和动态库使用方法一样
g++ -I../include hello.cpp -o hello -L../lib -lother
./hello
其中,-L../lib -lother中的-l是L的小写。
通过-L和-l的方法来指定静态库,和使用动态库的方法一样。
4.2.2动态库和静态库的区别
使用静态库,生成可执行程序hello。hello会包含静态库libother.a的代码,因此在执行时不需要静态库的支持,但也导致可执行程序过大。
使用动态库,生成的可执行程序中不会包含libother.so的代码,因此在执行时需要动态库的支持,但可执行程序小。
问题1:在前面执行链接过程时,通过-L../lib -lother来指定库,但是这个库可以是libother.a,也可以是libother.so。当lib目录下同时有libother.a和libother.so时,它到底会使用哪个库?
答案:优先选择动态库libother.so。
问题2:若要指定使用静态库libother.a,该怎么办?
由于静态库可以像*.o一样使用,因此使用全路径的方式来使用静态库,如:
g++ -I../include hello.cpp ../lib/libother.a -o hello
4.3使用C库
在C语言中,用函数名来标识一个函数,因此函数不可重载。
在C++语言中,用函数名+参数类型+名称修饰来标识一个函数,因此可以函数重载。
在windows平台下,生成动态库时,其头文件中的函数声明需要加上extern “C”。extern “C”标识编译器将这个函数按照C语言的方式来编译,因此动态库无法使用函数重载。由于编译器的不同,导致C++函数的名称修饰规则不同,因此若动态库中的函数按照C++方式来编译,其他编译器的代码会无法调用此动态库。
而在Linux平台下,其编译器统一使用GCC,导致其名称修饰规则一样,因此C++方式编译的动态库,可以在任何Linux平台下使用。总结:C++代码可以调用C++库。
问题:Linux平台下,C++代码如何调用C库呢?
C++代码调用C++库,通过修饰过的函数符号来寻找库中对应的函数;若调用C库,通过修饰过的函数符号肯定找不到对应的函数,因此C库中的函数符号就是函数名,没有修饰过。
解决方法:修改C库的头文件,在函数声明前加上extern “C”。表示这个库按照C语言方式来编译;并且调用C库时,按照函数名的方式来搜索对应函数。
extern “C”的3种使用方式:
(1)声明一个函数:
extern "C" void fun(void);
(2)声明多个函数:
extern "C"
{
void fun1(void);
void fun2(void);
}
(3)声明整个头文件
extern "C"
{
#include “other.h”
}
一般情况下,C库的提供者会提供一个兼容C/C++的头文件:
//other.h
#ifdef __cplusplus
extern "C" {
#endif
void fun(void);
#ifdef __cplusplus
}
#endif