2013-12-13 13:58:20 start000722 阅读数 8416
  • Spark初级入门(9):Scala 隐式转换

    Spark底层语言实现视频教程,快学Scala隐式转换教程。Spark是当前流行的开源大数据内存计算框架,采用Scala语言实现,由UC 伯克利大学AMPLab实验室开发(2009)并于2010年开源,在2014年成为Apache基金会的顶级项目。

    5859 人正在学习 去看看 CSDN讲师

Linux下静态、动态库(隐式、显式调用)的创建和使用及区别


一、静态链接库的创建与使用:

1、编写add.c 及main.c代码:

/**************************************************************************/
/*add.c*/
int add(int x, int y)
{
return x + y;

return 0;
}
/*************************************************************************/
然后add.h代码为:
/*add.h*/
#ifndef _ADD_H_
#define _ADD_H_

int add(int, int);

#endif
/***************************************************************************/
main函数代码:
/*main.c*/
#include <stdio.h>

int main(void)
{
printf("2+3= %d\n", add(2,3));
return 0;
}
/**********************************************************************************/

2、现在首先要明确我们目的是将add.c做成静态链接库,然后main.c调用生成的静态链接库中的add()

(1)将add.c做成静态链接库(创建静态库):
首先将add.c编译成目标文件(add.o文件),如下:
#gcc -c add.c             //生成add.o
然后将生成的目标文件(add.o)生成静态库libadd.a:
   #ar crv libadd.a add.o    //生成libadd.a
(2)静态库做好了,就可以在编译main.c时将静态库链接进去了,接下来就编译生成可执行文件(静态库的使用):
#gcc -o exe main.c -I. -L. -ladd  
//或者 #gcc -o exe main.c ./libadd.a
//再或者#gcc -o exe main.c -L. libadd.a

(注:这里的-I/路径, -L/路径, 是通过-I和-L指定对应的库文件名和库文件的路径,这里就是当前目录,
  libadd.a就是要用的静态库,这样对应的静态库已经编译到对应的可执行程序中。执行对应的可执行文件
  便可以得到对应函数调用的结果。在main.c中不需要包含导出文件的头文件。
  上面的(2)分开就是:
  1)编译生成对应的目标文件: 
#gcc -c -I/home/hcj/xxxxxxxx main.c  
  2)生成可执行文件: 
#gcc -o exe -L/home/hcj/xxxxxxxx main.o libstr.a  
还有若主函数是C++程序(即.cpp),则需要在main.cpp中用extern "C"{}包含被调用函数(add.c)的
头文件,编译时用g++编译或者还用gcc编译但需加上一个链接c++库的参数(-lstdc++)
)

(3)最后执行可执行程序:
#./exe

二、动态链接库的创建与使用:

1、把add.c编译生成动态库(创建动态库):

#gcc -fPIC -c add.c    //生成add.o
#gcc -shared -o libadd.so add.o     /* 或者 #ar crv libadd.so add.o */
(上面两行可以整合成一行:#gcc -fPIC -shared -o libadd.so add.c)
注:-fpic 使输出的对象模块是按照可重定位地址方式生成的(即与位置无关)。 
-shared指定把对应的源文件生成对应的动态链接库文件libstr.so文件


2、动态库的使用(动态链接库分:隐式调用和显式调用2种):

(1)隐式调用:

动态链接库(隐式调用)在代码上与写静态链接库没什么区别,主要是在编译时。
代码编写与静态库一样,不需要包含导出函数的头文件,若主函数是C++程序(即.cpp),则需要在main.cpp中用
extern "C"{}包含被调用函数(add.c)的头文件(这里需要包含头文件是与.cpp和.c混合编译有关,同静态\动态库
无关),编译时用g++编译或者还用gcc编译但需加上一个链接c++库的参数(-lstdc++)
1)代码编写: 与静态库一样
2)编译main.c生成可执行程序(动态库隐式调用的使用):
 #gcc -o exe main.c ./libadd.so
  (或者 #gcc -o exe main.c -L. libadd.so
   再或者将libadd.so copy到目录 /usr/lib或/lib中,然后执行:
   #gcc -o exe main.c libadd.so //此时不需要指定搜索路径
  )
   注: 最直接最简单的方法就是把libadd.so拉到/usr/lib或/lib中去。 、
   还有一种方法 export LD_LIBRARY_PATH=$(pwd) 
   另外还可以在/etc/ld.so.conf文件里加入我们生成的库的目录,然后执行#/sbin/ldconfig。
    /etc/ld.so.conf是非常重要的一个目录,里面存放的是链接器和加载器搜索共享库时要检查的目录,
    默认是从/usr/lib /lib中读取的,所以想要顺利运行,我们也可以把我们库的目录加入到这个文件中
    并执行/sbin/ldconfig 。另外还有个文件需要了解/etc/ld.so.cache,里面保存了常用的动态函数
    库,且会先把他们加载到内存中,因为内存的访问速度远远大于硬盘的访问速度,这样可以提高软件加载
    动态函数库的速度了。 

3)#./exe

(2)显式调用:

显式调用的动态库的创建与隐式调用相同。(隐式调用与静态库的使用方法一样,不需要包含导出函数的头文件(显式调用
也不用包含头文件),只需要在编译可执行程序时指定库文件的路径)
显式调用和隐式调用的区别在于:编译可执行程序时需要指定库文件的搜索路径,而显式调用编译可执行程序时不用加上
动态库的搜索路径(因为已经在主函数中包含了库文件的路径),但是需要增加几个系统调用:
(#include <dlfcn.h>,   //头文件 
1)dlopen()
第一个参数:指定共享库的名称,将会在下面位置查找指定的共享库。 
-环境变量LD_LIBRARY_PATH列出的用分号间隔的所有目录。 
-文件/etc/ld.so.cache中找到的库的列表,用ldconfig维护。 
-目录usr/lib。 
-目录/lib。 
-当前目录。 
第二个参数:指定如何打开共享库。 
-RTLD_NOW:将共享库中的所有函数加载到内存 
-RTLD_LAZY:会推后共享库中的函数的加载操作,直到调用dlsym()时方加载某函数 
返回值:返回动态库的句柄
2)dlsym()
调用dlsym时,利用dlopen()返回的共享库的句柄以及函数名称作为参数,返回要加载函数的入口地址。
3)dlclose()
关闭动态链接库
4)dlerror() 
该函数用于检查调用共享库的相关函数出现的错误。 
如果dlerror返回值不为空,则dlsym执行出错
)

1、编写add.c 及main.c代码:
/**************************************************************************/
/*add.c*/
int add(int x, int y)
{
return x + y;

return 0;
}
/*************************************************************************/
然后add.h代码为:
/*add.h*/
#ifndef _ADD_H_
#define _ADD_H_

int add(int, int);

#endif
/***************************************************************************/
main函数代码:
/*main.c*/
#include <stdio.h>
#include <dlfcn.h>   //显式加载需要用到的头文件

#define LIB  "./libadd.so"   //指定动态库路径

int main(void)
{
void *dl;
char *error;
int (*func)();

dl = dlopen(LIB, RTLD_LAZY); /*打开动态链接库*/
if(dl == NULL)
{
printf("Failed load libary\n");
}
error = dlerror(); /*检测错误*/
if(error != NULL)
{
printf("%s\n", error);
return -1;
}

func = dlsym(dl, "test"); /*获取函数的地址*/
error = dlerror(); /*检测错误*/
if(error != NULL)
{
printf("%s\n", error);
return -1;
}

func(); /*调用动态库中函数*/

dlclose(dl);  /*关闭共享库*/
error = dlerror(); /*检测错误*/
if(error != NULL)
{
printf("%s\n", error);
return -1;
}

return 0;
}
/**********************************************************************************/
2、编译main.c生成可执行程序,动态库的创建已经在上面讲了(动态库显式调用):
1)#gcc -ldl -o exe mian.c
注意要添加-ldl选项,以使用显式调用相关的函数调用。
可以看到,显式调用的代码看上去要复杂很多,但是却比隐式调用要灵活,我们不必在编译时就确定要加载哪个
动态链接库,可以在运行时再确定,甚至重新加载。

2)#./exe






2015-05-13 11:20:39 u010154760 阅读数 1447
  • Spark初级入门(9):Scala 隐式转换

    Spark底层语言实现视频教程,快学Scala隐式转换教程。Spark是当前流行的开源大数据内存计算框架,采用Scala语言实现,由UC 伯克利大学AMPLab实验室开发(2009)并于2010年开源,在2014年成为Apache基金会的顶级项目。

    5859 人正在学习 去看看 CSDN讲师

进入主题前,先看看两点预备知识。

一、显式调用和隐式调用的区别

        我们知道,动态库相比静态库的区别是:静态库是编译时就加载到可执行文件中的,而动态库是在程序运行时完成加载的,所以使用动态库的程序的体积要比使用静态库程序的体积小,并且使用动态库的程序在运行时必须依赖所使用的动态库文件(.so文件),而使用静态库的程序一旦编译好,就不再需要依赖的静态库文件了(.a文件)。

        动态库的调用又分为显示和隐式两种方式,区别如下:

        1、 隐式调用需要调用者写的代码量少,调用起来和使用当前项目下的函数一样直接;而显式调用则要求程序员在调用时,指明要加载的动态库的名称和要调用的函数名称。

        2、隐式调用由系统加载完成,对程序员透明;显式调用由程序员在需要使用时自己加载,不再使用时,自己负责卸载。

        3、由于显式调用由程序员负责加载和卸载,好比动态申请内存空间,需要时就申请,不用时立即释放,因此显式调用对内存的使用更加合理, 大型项目中应使用显示调用。

        4、当动态链接库中只提供函数接口,而该函数没有封装到类里面时,如果使用显式调用的方式,调用方甚至不许要包含动态链接库的头文件(需要调用的函数名是通过dlsym函数的参数指明的),而使用隐式调用时,则调用方不可避免要加上动态库中的头文件,g++编译时还需要要用参数-I指明包含的头文件的位置。需要注意的是,当动态链接库中的接口函数是作为成员函数封装在类里面时,即使使用显式调用的方式,调用方也必须包含动态库中的相应头文件(详见五、显示调用动态链接中的类成员函数)。

        5、显式调用更加灵活,可以模拟多态效果(具体见后文)。


二、extern "C"的作用

        C++程序(或库、目标文件)中,所有非静态(non-static)函数在二进制文件中都是以“符号(symbol)”形式出现的。这些符号都是唯一的字符串,从而把各个函数在程序、库、目标文件中区分开来。在C中,符号名正是函数名,两者完全一样。而C++允许重载(不同的函数有相同的名字但不同的参数,甚至const重载),并且有很多C所没有的特性──比如类、成员函数、异常说明──几乎不可能直接用函数名作符号名。为了解决这个问题,C++采用了所谓的name mangling。它把函数名和一些信息(如参数数量和大小)杂糅在一起,改造成奇形怪状,只有编译器才懂的符号名。例如,被mangle后的foo可能看起来像foo@4%6^,或者,符号名里头甚至不包括“foo”。

        其中一个问题是,C++标准并没有定义名字必须如何被mangle,所以每个编译器都按自己的方式来进行name mangling。有些编译器甚至在不同版本间更换mangling算法(尤其是g++ 2.x和3.x)。前文说过,在显示调用动态库中的函数时,需要指明调用的函数名,即使您搞清楚了您的编译器到底怎么进行mangling的,从而知道调用的函数名被C++编译器转换为了什么形式,,但可能仅仅限于您手头的这个编译器而已,而无法在下一版编译器下工作。

extern "C"即可以解决这个问题。用 extern "C"声明的函数将使用函数名作符号名,就像C函数一样。因此,只有非成员函数才能被声明为extern "C",并且不能被重载。尽管限制多多,extern "C"函数还是非常有用,因为它们可以象C函数一样被dlopen动态加载。冠以extern "C"限定符后,并不意味着函数中无法使用C++代码了,相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种类型的参数。所以extern "C" 只是告诉编译器编和链接的时候都用c的方式的函数名字,函数里的内容可以为c的代码也可以为c++的。


       有了上面两个预备知识后,下面以实际例子来演示两种不同的动态库调用方式。例子的结构组织为如下:

    so1.h和so1.cc是第一个动态库中的文件,会编译链接为libso1.so;so2.h和so2.cc是第一个动态库中的文件,会编译链接为libso2.so;test.cc是调用两个动态库的程序。


三、显式调用

so1.h:

  1. extern "C" void fcn();  
so1.cc:

  1. #include <iostream>  
  2. #include "so1.h"  
  3.   
  4. void fcn() {  
  5.     std::cout << "this is fcn in so1" << std::endl;  
  6. }  

so1的makefile:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. libso1.so:so1.o  
  2.     g++ so1.o -shared -o libso1.so  
  3. so1.o:so1.cc so1.h  
  4.     g++ -c so1.cc -fPIC -o so1.o  
  5.   
  6. .PHONY:clean  
  7. clean:  
  8.     rm so1.o libso1.so  
make之后,将生成的libso1.so拷贝到test.cc所在目录下。


so2.h:

  1. extern "C" void fcn();  
so2.cc:
  1. #include <iostream>  
  2. #include "so2.h"  
  3.   
  4. void fcn() {  
  5.     std::cout << "this is fcn in so2" << std::endl;  
  6. }  
so2的makefile:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. libso2.so:so2.o  
  2.     g++ so2.o -shared -o libso2.so  
  3. so2.o:so2.cc so2.h  
  4.     g++ -c so2.cc -fPIC -o so2.o  
  5.   
  6. .PHONY:clean  
  7. clean:  
  8.     rm so2.o libso2.so  
make之后,将生成的libso2.so拷贝到test.cc所在目录下。


test.cc:

  1. #include <iostream>  
  2. #include <cstdlib>  
  3. #include <dlfcn.h>  
  4.   
  5. using namespace std;  
  6.   
  7. int main(int argc, char **argv) {  
  8.     if(argc != 2) {  
  9.         cout << "argument error!" << endl;  
  10.         exit(1);  
  11.     }  
  12.   
  13.     //pointer to function  
  14.     typedef void (*pf_t)();  
  15.       
  16.     char *err = NULL;  
  17.     //open the lib  
  18.     void *handle = dlopen(argv[1], RTLD_NOW);  
  19.       
  20.     if(!handle) {  
  21.         cout << "load " << argv[1] << "failed! " << dlerror() << endl;  
  22.         exit(1);  
  23.     }  
  24.   
  25.     //clear error info  
  26.     dlerror();  
  27.   
  28.     pf_t pf  = (pf_t)dlsym(handle, "fcn");  
  29.     err = dlerror();  
  30.     if(err) {  
  31.         cout << "can't find symbol fcn! " << err << endl;  
  32.         exit(1);  
  33.     }  
  34.   
  35.     //call function by pointer  
  36.     pf();  
  37.   
  38.     dlclose(handle);  
  39.   
  40.     return 0;  
  41. }  
test的makefile:

[plain] view plaincopy在CODE上查看代码片派生到我的代码片
  1. test:test.o  
  2.     g++ test.o -lso1 -L. -lso2 -L. -ldl -Wl,-rpath=. -o test  
  3. test.o:test.cc   
  4.     g++ -c test.cc -o test.o  

make之后,终端运行结果如下:

可以看到这里,通过输入不同的参数,调用了不同的共享库中的fcn函数,是一种多态的表现,许多软件的不同插件就是这样实现的。

需要注意的是,要使用显式调用的方式,必须加入头文件dlfcn.h,makefile中的链接命令中要加入参数-ldl,否则报错。

dlfcn.h中提供的API说明如下:

1)        dlopen

函数原型:void *dlopen(const char *libname,int flag);

功能描述:dlopen必须在dlerror,dlsym和dlclose之前调用,表示要将库装载到内存,准备使用。如果要装载的库依赖于其它库,必须首先装载依赖库。如果dlopen操作失败,返回NULL值;如果库已经被装载过,则dlopen会返回同样的句柄。

参数中的libname一般是库的全路径,这样dlopen会直接装载该文件;如果只是指定了库名称,在dlopen会按照下面的机制去搜寻:

a.根据环境变量LD_LIBRARY_PATH查找

b.根据/etc/ld.so.cache查找

c.查找依次在/lib和/usr/lib目录查找。

flag参数表示处理未定义函数的方式,可以使用RTLD_LAZY或RTLD_NOW。RTLD_LAZY表示暂时不去处理未定义函数,先把库装载到内存,等用到没定义的函数再说;RTLD_NOW表示马上检查是否存在未定义的函数,若存在,则dlopen以失败告终。

2)        dlerror

函数原型:char *dlerror(void);

功能描述:dlerror可以获得最近一次dlopen,dlsym或dlclose操作的错误信息,返回NULL表示无错误。dlerror在返回错误信息的同时,也会清除错误信息。

3)        dlsym

函数原型:void *dlsym(void *handle,const char *symbol);

功能描述:在dlopen之后,库被装载到内存。dlsym可以获得指定函数(symbol)在内存中的位置(指针)。如果找不到指定函数,则dlsym会返回NULL值。但判断函数是否存在最好的方法是使用dlerror函数,

4)        dlclose

函数原型:int dlclose(void *);

功能描述:将已经装载的库句柄减一,如果句柄减至零,则该库会被卸载。如果存在析构函数,则在dlclose之后,析构函数会被调用。


四、隐式调用

隐式调用不需要包含头文件dlfcn.h,只需要包含动态链接库中的头文件,使用动态库中的函数也不需要像显示调用那么复杂。


五、显式调用动态链接中的类成员函数

显示调用动态链接库的类成员函数,有单独的写法,但比较少用。推荐的写法是为每个要被外部调用的类成员函数设计一个普通的借口函数,在接口函数内部使用类的成员函数。当然这就需要将类设计为单例模式,因为不可能在每个接口函数中都构造一个类的对象。



来源:http://blog.csdn.net/lc_910927/article/details/42393121

2016-10-20 14:43:38 Andy_93 阅读数 1515
  • Spark初级入门(9):Scala 隐式转换

    Spark底层语言实现视频教程,快学Scala隐式转换教程。Spark是当前流行的开源大数据内存计算框架,采用Scala语言实现,由UC 伯克利大学AMPLab实验室开发(2009)并于2010年开源,在2014年成为Apache基金会的顶级项目。

    5859 人正在学习 去看看 CSDN讲师

        这几天在写程序中时候,用到了一个第三方库。在下载完他的源码编译后,不知道怎么使用,后来在网上找了找了一些资料查看了一些文档。终于解决了问题!

        动态库和静态库的介绍在在前的博客:http://blog.csdn.net/andy_93/article/details/52708678 已经介绍过。       现在具体讲讲Windows下和Liux下的使用

首先是windows下的使用:
准备库:
我们在编译完库的时候或者是使用库的时候需要有三样东西:XXX.lib、XXX.dll、include文件夹包括库的头文件

在这里需要说明的是,有的库的dll和lib是区分Debug和Release版本的。所以根据不同的版本对应不同的库。

修改pro文件:

<pre name="code" class="cpp">	我们假设我们的lib、include、bin三个文件夹在同一级目录
	lib:库文件夹, 存放XXX.dll 和XXX.lib, 如果我们要区分Debug和Release版本的库,建议lib文件夹下建             立Debug和Release文件夹放各自的库文件
	include: 库头文件文件夹 
	bin:     可执行文件夹
win32{
	INCLUDEPATH += ./../include                    //包含库头文件
	Release:QMAKE_LIBDIR = ./../lib/Release    //库路径
	Release:LIBS +=  -lXXX\			  //使用到的库
			     -lXXX
	Debug:QMAKE_LIBDIR = ./../lib/Debug		
	Debug:LIBS  += 	-lXXX\
			    -lXXX	
}

以上的XXX是库的名字,去掉后缀,如QMyDialog.dll 直接-lQMyDialog就行了。注意:我们在LIBS +=的时候之所以没有用-L来指明库的路径 是因为我们的QMAKE_LIBDIR已经指明了路径。使用库: 
     在需要使用库的文件中添加头文件#include“XXX.h” 然后调用库的方法即可使用。执行:      在执行可执行文件之前,将用到的库XXX.dll拷贝到bin目录下,与可执行文件放到同一级目录即可运行。Linux下的使用:

Linux下的使用和Windows下的使用大同小异,只是在准备库的时候Linux下生成的是libXXX.so库文件,不需要XXX.lib然后其他的就和Windows下一样了。

修改pro文件:
unix{
    QMAKE_LIBDIR = ./../lib
     LIBS += -lXXX \
                    -lXXX
}

使用和执行和windows下一样的使用。都是添加头文件,然后调用库的东西。

2019-03-11 23:26:22 m0_37621078 阅读数 401
  • Spark初级入门(9):Scala 隐式转换

    Spark底层语言实现视频教程,快学Scala隐式转换教程。Spark是当前流行的开源大数据内存计算框架,采用Scala语言实现,由UC 伯克利大学AMPLab实验室开发(2009)并于2010年开源,在2014年成为Apache基金会的顶级项目。

    5859 人正在学习 去看看 CSDN讲师

一、可执行程序的参数传递

我们常见的可执行程序有不少都是不带参数直接执行的,特别对于界面交互类应用更是如此。但也有很多命令交互程序是带参数执行的,比如GCC编译指令不仅支持多参数运行,还有参数类型选项(gcc [options] file…),就如同下面的命令:

gcc -g main.cpp -o main

回想下我们常用main函数的参数int main(int argc, char* argv[]),其中argc表示参数个数,argv[]则是保存具体参数的字符串指针数组,默认执行程序名作为第一个参数。比如下面给出一段代码,原样输出所有的参数,按上面给出的gcc命令编译链接后输出可执行程序main,读者可试着加参数运行确认输出结果。

#include <stdio.h>

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),二者区别主要在链接阶段如何处理库:

  • 静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库,因此体积较大。
  • 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行时才被载入,因此在程序运行时还需要动态库存在,因此代码体积较小。
  • 库文件链接过程

三、Linux静态链接库的创建和使用

一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:

  • 静态库对函数库的链接是放在编译时期完成的。
  • 程序在运行时与函数库再无瓜葛,移植方便。
  • 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。

3.1 Linux静态库命名规则

Linux静态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.a。

3.2 Linux静态库创建

  • 首先,编写一些代码文件,用作库文件的函数
// 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
  • 将代码文件编译成目标文件.o
g++ -c myadd.cpp mymul.cpp mysub.cpp		# -c参数只编译汇编不链接
  • 然后,通过ar工具将目标文件打包成.a静态库文件
ar -crv libstaticmath.a myadd.o mymul.o mysub.o		# -c创建一个库,-r在库中加入或替换成员文件, -v显示操作的附加信息

大一点的项目会编写Makefile文件(CMake等工程管理工具)来生成静态库,省去了输入太多命令的麻烦,本文最后也给出了完整的Makefile代码,想了解Makefile可以参考另一篇文章:VSCode+GCC+Makefile+GitHub项目管理

3.3 Linux静态链接库的使用

  • 先给出一段调用上面库函数的测试代码:
// implicit.cpp	隐式调用测试代码

#include "./lib/mylib.h"
#include <cstdlib>
#include <iostream>

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表示库文件不共享
  • 运行程序结果如下:

静态库调用结果

四、Linux动态链接库的创建和使用

动态链接库的出现,主要是为了解决静态链接库的一些问题,主要有以下两点:

  • 代码虽然可复用,但没法共享,造成空间浪费。

  • 静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
    动态库共享代码
    由上图可看出,动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。综上将动态链接库的特点总结如下:

  • 动态库把对一些库函数的链接载入推迟到程序运行的时期。

  • 可以实现进程之间的资源共享。(因此动态库也称为共享库)

  • 将一些程序升级变得简单。(增量更新)

  • 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显式调用)。

4.1 Linux动态库的命名规则

Linux动态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.so。

4.2 Linux动态库的创建

  • 依然使用上面静态库的示例代码,首先生成目标文件,此时要加编译器选项-fPIC
g++ -fPIC -c myadd.cpp mymul.cpp mysub.cpp		# -fPIC(Position Independent Code)编译为位置独立的代码
  • 生成动态库,此时要加链接器选项-shared
g++ -shared -o libdynamicmath.so myadd.o mymul.o mysub.o		# -shared生成共享目标文件
  • 由于都是g++工具,上面两步可合并为下面一条命令
g++ -fPIC -shared -o libdynamicmath.so myadd.cpp mymul.cpp mysub.cpp

4.3 Linux动态库的使用

测试代码也使用上面静态库时的示例代码:

  • 动态库链接命令与静态库完全一致,命令如下
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`			#将当前路径添加到动态库路径环境变量
  • 程序执行结果如下(执行程序如果找不到动态库会报错,推荐一个程序依赖库查询命令:ldd libdynamicmath.so):

动态库隐式调用结果

五、Linux动态链接库的显式调用

上面介绍的动态库使用方法和静态库类似属于隐式调用,编译的时候指定相应的库和查找路径,可执行程序中也需要包含链接库头文件。其实,动态库还可以显式调用,不需要包含链接库的头文件。

Linux显式调用动态库,#include <dlfcn.h>头文件中提供了下面几个接口:

  • void * dlopen( const char * pathname, int mode ):函数以指定模式打开指定的动态连接库文件,并返回一个句柄给调用进程。
  • void* dlsym(void* handle,const char* symbol):dlsym根据动态链接库操作句柄(pHandle)与符号(symbol),返回符号对应的地址。使用这个函数不但可以获取函数地址,也可以获取变量地址。
  • int dlclose (void *handle):dlclose用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。
  • const char *dlerror(void):当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。

下面给出示例代码,生成动态链接库(libdynamicmath.so)的代码跟前面一致,这里只列出测试代码explicit.cpp如下:

// explicit.cpp不再包含./lib/mylib.h头文件,通过程序内部命令显式加载和释放

#include <cstdlib>
#include <iostream>
#include <dlfcn.h>

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查看符号表,从中找到库文件里面的函数名。
nm或objdump查看动态库符号表

六、Makefile文件编写

6.1 Makefile生成静态库与动态库

在包含库源文件和库头文件的目录下./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)

make命令执行结果如下:
makefile生成库文件

6.2 Makefile链接生成可执行文件

在测试示例源码目录下新建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

make命令执行结果如下:
makefile链接库生成可执行文件

本文的源代码可以到GitHub下载:https://github.com/StreamAI/LinkLibrary(不熟悉GitHub使用的可以参考文章:GitHub社会化编程)。

如果想了解Windows环境动态库与静态库的创建和使用,可以查看我的另一个博文:Windows静态链接库与动态链接库的创建和显式与隐式调用

如果想深入了解动态库与静态库的链接与调用过程,推荐一本书《程序员的自我修养—链接、装载与库》。

2018-02-01 16:38:37 xmmdbk 阅读数 555
  • Spark初级入门(9):Scala 隐式转换

    Spark底层语言实现视频教程,快学Scala隐式转换教程。Spark是当前流行的开源大数据内存计算框架,采用Scala语言实现,由UC 伯克利大学AMPLab实验室开发(2009)并于2010年开源,在2014年成为Apache基金会的顶级项目。

    5859 人正在学习 去看看 CSDN讲师

什么是库:一种二进制的复用,也就是,将函数或方法,变成二进制文件。

库的种类:静态库和动态库

在windows平台下静态库后缀为.lib ,在linux 下后缀名.a。

静态库在被调用时,要将头文件和.lib文件拷贝到你的工程下,静态库工作时,会把整个库到考入你的目标文件中,所以在生成可执行文件后,静态库不存在了,也没关系。在linux下也一样,在linux下,你要将你的源文件和库一起编译。

动态库是在运行是被调用;所以在编译阶段只需要库信息*.lib文件和头文件,就可以进行生成目标文件和链接,但是在执行可执行文件时,如果没有库,将无法执行。所以在执行.exe文件时,必须要有相应的库。

动态库的隐式调用:隐式调用需要,.dll库+lib信息文件+头文件,同时在调用程序中,需要,引用库#pragma comment(lib,"*.lib"); 隐式调用偏简单。

动态库的显示调用:显式调用,可以不用lib信息文件和头文件,只需要库文件,也不需要引用库,

 typedef int(*fc)();
HMODULE hdll=LoadLibrary(TEXT("testdll.dll"));
if (!hdll)
{
cout << "查找失败" << endl;
}
else
{
fc f = (fc)GetProcAddress(hdll, "fntestdll");
if (f == NULL)
{
cout << "查找失败!" << endl;
}
else
cout << f << endl;
}
FreeLibrary(hdll);

需要头文件windows.h,在显式调用时,如果遇到,找不到函数时,可能是,dll库中.pp文件,没有对方法进行extren "C"定义,导致,方法按照c++重载的方式,将函数名更改了,导致你找不到函数。

静态库的调用和动态库的隐式调用基本相同。


静态库和动态库的对比:静态库编译之后可以不用再带着静态库,但是,可执行文件会比较大,浪费空间,动态库在使用必须带着,但是动态库可以被共享调用,节省空间,并且,修改,更容易实现。可以只替换动态库。必要时,要替换头文件。

动态库隐式调用和显式调用的对比:隐式调用简单,但是,一旦,某个库缺失或出错,会导致整个应用程序的不能使用。显示调用稍,麻烦,但是,在某个库缺失时,可以进行判断,不妨碍其他函数的运行,可以实现局部更新升级,有更好的使用性。



没有更多推荐了,返回首页