一、什么是库
C++编译链接的大体过程如下图所示,但是动态库/静态库、win/linux平台之间有一些差异需要注意。C++源码编译完成后最终会得到一些.o/.obj文件,这些文件进行连接器的处理最终才转换成目标机器上的可执行程序。
库的本质就是这些.o/.obj文件的集合,然后提供头文件,头文件的作用就是进行声明,当连接器进行连接时会自动去找到库的实现。
1.1 静态库
静态库其实就是函数名索引的机器码,将来拷贝到你的(编译后的)程序里面就好了;
使用静态库,需要把静态库拷贝一份到程序的内存中,这样会占用大量的空间。同时静态库对程序的更新、部署和发布也会带来麻烦,因为如果静态库更新了,那么所有依赖于该库的程序都需要重新编译。
1.2 动态库
动态库则有一个函数名列表、对应的机器码以及重定位信息,将来在你的程序运行时按照名字载入对应机器码就完了。
使用动态库时,编译完成后并不会把库载入程序中,只有程序运行时候才会。需要注意,虽然动态库只有一份,但是每个程序调用动态库产生的数据是存放在自己的进程空间中的,和其他调用库的进程不会互相影响。【库提供单例类,然后有一个成员变量A,程序X1调用函数SetA设置A的值,并不会影响程序X2调用GetA的结果。】
二、如何使用库
- windows下,使用vs的话就配置一下链接器的附加库目录、输入-附加依赖项就行了。
关于Linux下程序运行时连接动态库问题:
linux下,在编译的时候 -I /xx/xx/include指定头文件目录 -L /xx/xx/lib 指定库目录 -lxxx链接库,但是程序运行时候会出现一些问题,可能会找不到动态库在哪儿,linux查找动态库的顺序如下:
-
编译目标代码时指定的动态库搜索路径;
- -I先指定库的头文件所在目录,-L指定库所在的目录,-Wl,-rpath=xxxxx -lxxx
- 链接的时候还是用的-L路径下的库,但是运行的时候是-Wl,-rpath所指定的
- 通过-Wl,-rpath=/path/to/xxx,路径最好用绝对路径,并且把该动态库放到绝对路径下,这样移动exe到其他目录时,仍然能保证程序正常运行。
- -I先指定库的头文件所在目录,-L指定库所在的目录,-Wl,-rpath=xxxxx -lxxx
-
环境变量 LD_LIBRARY_PATH 指定的动态库搜索路径;
- export LIB_LIBRARY_PATH=/path/to/xxxx/:$LIB_LIBRARY_PATH,路径必须是绝对路径,并且只在当前会话生效。
-
配置文件 /etc/ld.so.conf.d/ 中指定的动态库搜索路径;
- 新建libxxxx.conf,写上库的路径,这个方法可能会导致库冲突(不同程序使用一个库的不同版本)。
-
默认搜索路径
/lib
、/usr/lib
三、如何编写一个库
一般可以选择将第三方库编译静态库,然后根据自己的功能需求再封装一层接口,做成dll,dll中只会抽取使用到的功能,这样就可以减小库的体积。
3.1 接口设计
a.接口导出问题
windows平台提供给别人使用的类或方法需要进行导出,linux平台不需要。一般写跨平台的时候使用这种宏配置来操作,最好给每一个要导出的类或方法就加上。
// export for shared library
#if defined(ET_COMPILER_IS_MSVC)
#define __et_export __declspec(dllexport)
#elif defined(ET_COMPILER_IS_GCC) && ((__GNUC__ >= 4) || (__GNUC__ == 3 && __GNUC_MINOR__ >= 3))
#define __et_export __attribute__((visibility("default")))
else
#define __et_export
#endif
class __et_export Test{
};
__et_export void func();
//只有导出了函数或者类,vs生成的dll才会有一个lib文件,然后连接器连接这个lib文件就能使用对应的类和函数了。
#ifndef __rectangle_h
#define __rectangle_h
class Rectangle {
int width, height;
public:
__attribute__((visibility("default"))) void set_values (int,int);
__attribute__((visibility("default"))) int area();
};
#endif
#include "rectangle.h"
int Rectangle::area() {return width*height;}
void Rectangle::set_values (int x, int y) {
width = x;
height = y;
}
g++ -Wall -c -fPIC -fvisibility=hidden rectangle.cpp
g++ -o librectangle.so --shared rectangle.o
-fvisibility=hidden
是会把所有的东西都藏起来的,而__attribute__((visibility("default")))
则是一个选择开关,暴露具体想暴露的接口。
b.接口设计问题
PIMP方法
这样暴露给用户的就是complex.h头文件,内部的具体细节就可以屏蔽起来。
//header complex.h
class Complex{
public:
Complex& operator+(const Complex& com );
Complex& operator-(const Complex& com );
Complex& operator*(const Complex& com );
Complex& operator/(const Complex& com );
private:
class ComplexImpl; //作为Complex的内嵌类,这样用户不可以直接定义
ComplexImpl* pimpl_;
};
这个是真正的实现。
//header compleximpl.h
#include "complex.h"
#include <vector>
class Complex::ComplexImpl{
public:
ComplexImpl& operator+(const ComplexImpl& com );
ComplexImpl& operator-(const ComplexImpl& com );
ComplexImpl& operator*(const ComplexImpl& com );
ComplexImpl& operator/(const ComplexImpl& com );
private:
std::vector<int> ids_;
double real_;
double imaginary_;
};
使用PIMP的好处:
- 假设使用常规的模式,当complex.h改动一下时,所有引用它的文件都要重新编译,使用PIMP可以减少编译依赖,降低编译时间
- 实现隐藏
Object-Interface 抽象基类法
通常写一个库,最好是使用这种纯虚函数的定义方式,这样子可能就只要一个头文件。所有具体的实现都继承这个纯虚基类,然后导出一些C方法即可。
class __declspec(dllexport) test_handle{
public:
virtual void open() = 0;
virtual void read() = 0;
virtual void write() = 0;
virtual ~test_handle(){}
};
extern "C" test_handle* get_handle(handle_type_e type);
3.2 如何编译
a.Linux
如果.so和.a同名,则在使用的时候,默认使用so,如果需要使用.a,则添加-static可指定。
静态库
1.linux静态库的命名规则:
linux静态库必须是 lib[库名].a
,lib为前缀,中间是静态库名,扩展名为.a
2.生成:
- g++ -c *.cpp //将所有代码生成.o
- ar cr libxxx.a *.o //将.o生成静态库
3.使用:
- g++ -static -std=c++11 -I ./include -L ./lib -lxxx -o main
- -I 指定头文件在哪
- -L 指定库文件在哪
- -l 连接库
动态库
1.生成.o文件:
g++ -std=c++11 -fpic -c *.cpp
2.生成.so文件:
g++ -shared -o libxxxx.so *.o
其实1、2可以合成一步完成:
g++ -fPIC -shared -o libxxx.so *.cpp
3.使用:
g++ -std=c++11 -I ./include -L ./lib -lxxx -o main
在程序运行前,需要执行命令 export LD_LIBRARY_PATH=库的绝对路径:$LD_LIBRARY_PATH
来设置临时的环境变量;告知系统这里有一个绝对路径的动态库路径,动态库不能够使用相对路径。 若是设置绝对路径的环境变量,则程序一定会跑失败。或者可以在/etc/ld.so.conf.d目录下面添加动态库的地址,然后指向ldconfig就ok了,这样就不用指定LD_LIBRARY_PATH。亦或者在编译时就指定动态库路径-Wl,-rpath=/path/to/xxx,需要注意的是-L
和-l
还是需要指定的,因为这两个是编译使用,-Wl,-rpath=是运行时使用。
b.Windows
使用visual studio就完事了.
四、C和C++兼容
4.1 C++调用C
方案1
在头文件中的每一个函数最前面添加extern “C”
//demo.h
extern "C"{
void func1(int arg1);
void fun2(int arg1, int arg2);
}
如果不确定当前编译环境是C还是C++,则
//demo.h
#ifdef __cplusplus
extern "C" {
#endif
void fun1(int arg1);
void fun2(int arg1, int arg2);
#ifdef __cplusplus
}
#endif
方案2
若是别人已经写好的头文件,无法修改,则重写一个专门被C++专用的头文件即可
extern "C"{
#include "demo.h"
}
4.2 C调用C++
C无法完全使用C++的一些功能,因为c本身就不支持重载、模板等,假设C++库使用了stl,那么C是无法直接调用的。但是部分C++封装的库还是能用C包一层的。
将c++封装成库,然后导出c接口,编译连接时,gcc需要添加-lstdc++表示链接c++库。
#ifndef TESTCLASS_H
#define TESTCLASS_H
#include<iostream>
#include<stdio.h>
class ValueClass
{
private:
int value;
int sum;
public:
ValueClass();
void Add(int i, int j);
};
#endif
#include "test_class.h"
ValueClass::ValueClass(){
printf("hello world \n");
}
void ValueClass::Add(int i, int j){
sum = i+j;
printf("sum : %d value : %d\n",sum,value);
}
g++ test_class.cpp -shared -o libtestclass.so -I./ -fPIC
将c++库封装一下
#ifndef _TEST_WRAPPER_H
#define _TEST_WRAPPER_H
#ifdef __cplusplus
extern "C" {
#endif
void myValueClass(int a, int b);
#ifdef __cplusplus
}
#endif
#endif
#include "TestWrapper.h"
#include "test_class.h"
void myValueClass(int a, int b){
ValueClass t;
t.Add(a,b);
}
4.3 extern"C"的解决之道
extern"C"的最根本的作用,就是为了让C的函数按照C的方式来编译链接,不会因为C++的重载机制,导致C的函数编译后函数签名变化,这样在C++代码里面调用C函数时能够正确的链接到。
void func(int a,int b);
c:编译出来就是__func
c++:编译出来是__func_int_int
在调用的时候,请求的是 __func,如果不用extern “C”,那g++就找不到 __func这个函数了…
4.4 C语言函数指针与C++成员函数问题
看一个c++集成http_parser解析的例子,需要把数据解析完了以后存到一个类的对象中去. 肯定不能使用全局对象来做临时解析存储对象,并发一高容易出问题。
http_context::http_context()
{
http_parser_init(&parser_, http_parser_type::HTTP_REQUEST);
parser_.data = this;
}
只能c语言的库中增加一些void* ctx字段,然后定义一堆static的函数,c++的对象构造函数的时候创建c库的对象,然后把this指针赋值过去。
五、so引用模型
libA.so使用了libB.so,就算libA.so被某个进程通过dlopen打开,libB.so也仍然会被libA.so所依赖。在编译的时候,libA.so最好使用绝对路径来引用libB.so,这样才不会被错误的加载。
有用的链接:
六、如何确认库是32位还是64位
对于动态库而言:file xxxx.so
对于静态库而言:objdump -a xxxx.a
七、问题集合
问:为什么静态库的体积比动态库大?
问:动态库如何进行热更新?
...