[Holo_wo]-c++库工程总结

一、什么是库

​ C++编译链接的大体过程如下图所示,但是动态库/静态库、win/linux平台之间有一些差异需要注意。C++源码编译完成后最终会得到一些.o/.obj文件,这些文件进行连接器的处理最终才转换成目标机器上的可执行程序。

​ 库的本质就是这些.o/.obj文件的集合,然后提供头文件,头文件的作用就是进行声明,当连接器进行连接时会自动去找到库的实现。

1.1 静态库

使用静态库,需要把静态库拷贝一份到程序的内存中,这样会占用大量的空间。同时静态库对程序的更新、部署和发布也会带来麻烦,因为如果静态库更新了,那么所有依赖于该库的程序都需要重新编译。

1.2 动态库

使用动态库时,编译完成后并不会把库载入程序中,只有程序运行时候才会。需要注意,虽然动态库只有一份,但是每个程序调用动态库产生的数据是存放在自己的进程空间中的,和其他调用库的进程不会互相影响。【库提供单例类,然后有一个成员变量A,程序X1调用函数SetA设置A的值,并不会影响程序X2调用GetA的结果。】

二、如何使用库

  • windows下,使用vs的话就配置一下链接器的附加库目录、输入-附加依赖项就行了。

关于Linux下程序运行时连接动态库问题

  • 编译目标代码时指定的动态库搜索路径;
    • 通过-Wl,-rpath=/path/to/xxx,路径最好用绝对路径,这样移动exe到其他目录时,仍然能保证程序正常运行。
  • 环境变量 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

image-20220726205139415

-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的好处:

  1. 假设使用常规的模式,当complex.h改动一下时,所有引用它的文件都要重新编译,使用PIMP可以减少编译依赖,降低编译时间
  2. 实现隐藏
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
静态库

1.linux静态库的命名规则:

​ linux静态库必须是 lib[库名].a,lib为前缀,中间是静态库名,扩展名为.a

2.生成:

  • g++ -c *.cpp //将所有代码生成.o
  • ar cr libxxx.a *.o //将.o生成静态库

3.使用:

  • g++ -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这个函数了…

五、问题集合

问:为什么静态库的体积比动态库大?

问:动态库如何进行热更新?

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值