动态库和静态库
- 在Win下,动态库以.dll结尾,静态库以.lib结尾。
- 在Linux下,动态库文件以.so结尾,静态库以.a结尾。
- 在Mac下,动态库以.dylib结尾,静态库以.a结尾。
Linux
在编写代码的时候经常用到已有的接口,他们是以库的形式提供给我们使用的,而常见形式有两种,一种常以.a为后缀,为静态库;另一种以.so为后缀,为动态库
静态库:.a文件
动态库:.so文件
说明:本文主要说明Linux下的情况,windows不涉及。
目标文件
在解释静态库和动态库之前,需要简单了解一下什么是目标文件。目标文件常常按照特定格式来组织,在linux下,它是ELF格式(Executable Linkable Format,可执行可链接格式),而在windows下是PE(Portable Executable,可移植可执行)。
而通常目标文件有三种形式:
- 可执行目标文件(ELF文件)。即我们通常所认识的,可直接运行的二进制文件。
- 可重定位目标文件(.o)。包含了二进制的代码和数据,可以与其他可重定位目标文件合并,并创建一个可执行目标文件。
- 共享目标文件(.so)。它是一种在加载或者运行时进行链接的特殊可重定位目标文件。
举例
#include<stdio.h>
#include<math.h>
int main(int argc,char *argv[])
{
printf("hello 编程珠玑\n");
int b = 2;
double a = exp(b);
printf("%lf\n",a);
return 0;
}
代码计算e的2次方并打印结果。由于代码中用到了exp函数,它位于数学库libm.so或者libm.a中,因此编译时需要加上-lm。
生成可重定位目标文件starce_c.o:
通过上面的命令将main.c生成为可重定位目标文件。通过readelf命令也可以看出来:REL (Relocatable file)。
观察共享目标文件libm.so:
查看可执行目标文件starce_c:
这里必须要强调一点,如果使用到的函数没有在libc库中,那么你就需要指定要链接的库,本文中需要链接libm.so或libm.a。可以看到,最终生成的main类型是Executable file,即可执行目标文件。
静态库
前面所提到可重定位目标文件以一种特定的方式打包成一个单独的文件,并且在链接生成可执行文件时,从这个单独的文件中“拷贝”它自己需要的内容到最终的可执行文件中。这个单独的文件,称为静态库。linux中通常以.a(archive)为后缀
还是拿前面的例子来说,我们使用静态链接构建我们的可执行文件:
$ gcc -c strace_c.c
$ gcc -static -o strace strace_c.o -lm
在这个过程中,就会用到系统中的静态库libm.a。这个过程做了什么呢?首先第一条命令会将main.c编译成可重定位目标文件strace_c.o,第二条命令的static参数,告诉链接器应该使用静态链接,-lm参数表明链接libm.a这个库(类似的,如果要链接libxxx.a,使用-lxxx即可)。由于strace_c.c中使用了libm.a中的exp函数,因此链接时,会将libm.a中需要的代码“拷贝”到最终的可执行文件strace中。
特别注意,必须把-lm放在后面。放在最后时它是这样的一个解析过程:
- 链接器从左往右扫描可重定位目标文件和静态库
- 扫描main.o时,发现一个未解析的符号exp,记住这个未解析的符号
- 扫描libm.a,找到了前面未解析的符号,因此提取相关代码
- 最终没有任何未解析的符号,编译链接完成
那如果将-lm放在前面,又是怎样的情况呢?
- 链接器从左往右扫描可重定位目标文件和静态库
- 扫描libm.a,由于前面没有任何未解析的符号,因此不会提取任何代码
- 扫描main.o,发现未解析的符号exp
- 扫描结束,还有一个未解析的符号,因此编译链接报错
如果把-lm放在前面,编译结果如下:
静态库的大小是861K
由于最终生成的可执行文件中已经包含了exp相关的二进制代码,因此这个可执行文件在一个没有libm.a的linux系统中也能正常运行。
实例2:子函数分别实现加减和乘除的功能,主函数调用这些函数
- 先写一下实现加减功能的子函数。主函数要调用另一个c文件里面的函数,一般是通过共同包含同一个.h文件实现的。
#ifndef __ADD_MINUS_H__
#define __ADD_MINUS_H__
int add(int a, int b);
int minus(int a, int b);
#endif /*__ADD_MINUS_H__*/
#include"add_minus.h"
int add(int a, int b)
{
return a+b;
}
int minus(int a, int b)
{
return a-b;
}
- 主函数
#include<stdio.h>
#include"add_minus.h"
int main()
{
printf("hello\n");
printf("%d\n",add(1,2));
printf("%d\n",minus(1,2));
return 0;
}
- 编译静态库
假设我们需要把 add_minus.o 里面包含的 add 和 minus 编译为静态库,只需要对.o文件执行以下命令:
$ gcc -c add_minus.c #生成add_minus.o
$ ar rc libadd_minus.a add_minus.o #生成静态库libadd_minus.a
- 链接静态库
在上面得到了libadd_minus.a和main.o文件,这一步需要把这两个文件链接起来
$ gcc -o main2 main.o -L./ -ladd_minus
- **-L./ **表明库文件位置在当前文件夹
- **-ladd_minus **表示链接 libadd_minus.a 文件,使用“-l”参数时,前缀“lib”和后缀“.a”是需要省略的。
不加链接库,就会报错,未定义函数
动态库
动态库和静态库类似,但是它并不在链接时将需要的二进制代码都“拷贝”到可执行文件中,而是仅仅“拷贝”一些重定位和符号表信息,这些信息可以在程序运行时完成真正的链接过程。linux中通常以.so(shared object)作为后缀。
通常我们编译的程序默认就是实用动态链接:
$ gcc -o strace_c strace_c.c -lm #默认使用的是动态链接
我们来看最终生成的文件大小:17K
另外我们还可以通过ldd命令来观察可执行文件链接了哪些动态库:
正因为我们并没有把libm.so中的二进制代码“拷贝”可执行文件中,我们的程序在其他没有上面的动态库时,将无法正常运行。
实例2:
- 先写一下实现乘除功能的子函数,其余同上。
#ifndef __MULTI_DIV_H__
#define __MULTI_DIV_H__
int multi(int a, int b);
int div(int a, int b);
#endif /*__MULTI_DIV_H__*/
#include "multi_div.h"
int multi(int a, int b)
{
return a*b;
}
int div(int a, int b)
{
return a/b;
}
- 主函数
#include <stdio.h>
#include "add_minus.h"
#include "multi_div.h"
int main(void)
{
int rst;
printf("Hello Cacu!\n");
rst = add(3,2);
printf("3 + 2 = %d\n",rst);
rst = minus(3,2);
printf("3 - 2 = %d\n",rst);
rst = multi(3,2);
printf("3 * 2 = %d\n",rst);
rst = div(6,2);
printf("6 / 2 = %d\n",rst);
return 0;
}
- 编译动态库
假设我们需要把 multi_div.o 里面包含的 multi 和 div 编译为静态库,需要使用gcc:
$ gcc multi_div.c -fPIC -shared -o libmulti_div.so
$ gcc add_mins.c -fPIC -shared -o libadd_mins.so
- 链接动态库
在外部告诉程序,动态库在哪里
- **将 libmulti_div.so copy到/lib/ 或 /usr/lib/ 下 (**这个方法对很多软件都要使用的库比较友好)
- 在终端用export命令,但是退出终端就失效了,**在 LD_LIBRARY_PATH 变量中指定库文件路径 (**这个一般就是临时弄一下)
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/yourpath
- 编译链接
gcc -o main4 main.o -L./ -ladd_minus -lmulti_div
- 修改/.bashrc或/.bash_profile或/etc/profile
区别是前两个只对当前用户生效,/etc/profile对所有用户起效
修改~/.bashrc的例子
sudo gedit ~/.bashrc
# 添加一句
export LD_LIBRARY_PATH=/xxxx/lib:$LD_LIBRARY_PATH
# 保存退出
source ~/.bashrc
修改/etc/profile的例子
sudo gedit /etc/profile
# 在文件末尾添加
export PATH=/usr/local/xxx/bin${PATH:+:${PATH}} # PATH是可执行文件路径
export LD_LIBRARY_PATH=/usr/xxx/lib64{LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
# 保存退出
source /etc/profile
- /etc/ld.so.conf.d/路径下创建任意一个.conf文件,把lib文件的路径写在里面,比如
sudo gedit /etc/ld.so.conf.d/xxx.conf
# 写入
/xxxx/lib
/xxx/lib64 路径名
# 保存退出
# 更新缓存
sudo ldconfig
- Makefile编译
总结
静态库的生成:
$ gcc -c hello.c # 默认就是在编静态库,-c要求只预处理、编译,不链接。
$ ar -r libhello.a hello.o # 用ar命令将.o文件归档.a文件。
静态库的链接:
$ gcc main.c -static -L . -lhello -o main
# -static选项是告诉编译器,-L大写的L指明库所在的目录,-l小写的L是在指出需要的动态库,hello是静态库。
动态库的生成:
$ gcc hello.c -fPIC -shared -o libhello.so # 在使用GCC编译程序时,只需加上-shared选项,
动态库的链接:
$ gcc main.c -L./ -lhello -o main
# 不加-static选项,-l小写的L是在指出需要的动态库。
有什么区别
到这里我们大致了解了静态库和动态库的区别了,静态库被使用目标代码最终和可执行文件在一起(它只会有自己用到的),而动态库与它相反,它的目标代码在运行时或者加载时链接。正是由于这个区别,会导致下面所介绍的这些区别。
- 可执行文件大小不一样
- 占用磁盘大小不一样
- 如果有多个可执行文件,那么静态库中的同一个函数的代码就会被复制多份,而动态库只有一份,因此使用静态库占用的磁盘空间相对比动态库要大。
- 扩展性与兼容性不一样
- 如果静态库中某个函数的实现变了,那么可执行文件必须重新编译,而对于动态链接生成的可执行文件,只需要更新动态库本身即可,不需要重新编译可执行文件。正因如此,使用动态库的程序方便升级和部署。
- 依赖不一样
- 静态链接的可执行文件不需要依赖其他的内容即可运行,而动态链接的可执行文件必须依赖动态库的存在。所以如果你在安装一些软件的时候,提示某个动态库不存在的时候也就不奇怪了。
- 复杂性不一样
- 相对来讲,动态库的处理要比静态库要复杂,例如,如何在运行时确定地址?多个进程如何共享一个动态库?当然,作为调用者我们不需要关注。另外动态库版本的管理也是一项技术活。
- 加载速度不一样
- 由于静态库在链接时就和可执行文件在一块了,而动态库在加载或者运行时才链接,因此,对于同样的程序,静态链接的要比动态链接加载更快。所以选择静态库还是动态库是空间和时间的考量。但是通常来说,牺牲这点性能来换取程序在空间上的节省和部署的灵活性时值得的。再加上局部性原理,牺牲的性能并不多。
Windows
参考链接
Lib静态库介绍
静态链接库 .lib在链接时,编译器会将 .obj 文件和 .LIB 文件组织成一个 .exe 文件,程序运行时,将全部数据加载到内存。
如果程序体积较大,功能较为复杂,那么加载到内存中的时间就会比较长,最直接的一个例子就是双击打开一个软件,要很久才能看到界面。这是静态链接库的一个弊端。
动态库DLL介绍
动态链接库 DLL:(Dynamic Link Library)是一个被其他应用程序调用的程序模块,其中封装了可以被调用的资源或函数。DLL文件属于可执行文件,它符合Windows系统的PE文件格式,不过它是依附于EXE文件创建的的进程来执行的,不能单独运行。
动态链接库有两种加载方式:隐式加载和显示加载。
- 隐式加载又叫载入时加载,指在主程序载入内存时搜索DLL,并将DLL载入内存。隐式加载也会有静态链接库的问题,如果程序稍大,加载时间就会过长,用户不能接受。
- 显式加载又叫运行时加载,指主程序在运行过程中需要DLL中的函数时再加载。显式加载是将较大的程序分开加载的,程序运行时只需要将主程序载入内存,软件打开速度快,用户体验好。
动态链接库DLL的使用方法
- 创建DLL项目
选择->动态链接库(DLL)
- 创建自己的头文件
主要需要添加以下代码,dllexport,dllimport
// MathLibrary.h - Contains declarations of math functions
#pragma once
#ifdef MATHLIBRARY_EXPORTS
#define MATHLIBRARY_API __declspec(dllexport)
#else
#define MATHLIBRARY_API __declspec(dllimport)
#endif
// The Fibonacci recurrence relation describes a sequence F
// where F(n) is { n = 0, a
// { n = 1, b
// { n > 1, F(n-2) + F(n-1)
// for some initial integral values a and b.
// If the sequence is initialized F(0) = 1, F(1) = 1,
// then this relation produces the well-known Fibonacci
// sequence: 1, 1, 2, 3, 5, 8, 13, 21, 34, ...
// Initialize a Fibonacci relation sequence
// such that F(0) = a, F(1) = b.
// This function must be called before any other function.
extern "C" MATHLIBRARY_API void fibonacci_init(
const unsigned long long a, const unsigned long long b);
// Produce the next value in the sequence.
// Returns true on success and updates current value and index;
// false on overflow, leaves current value and index unchanged.
extern "C" MATHLIBRARY_API bool fibonacci_next();
// Get the current value in the sequence.
extern "C" MATHLIBRARY_API unsigned long long fibonacci_current();
// Get the position of the current value in the sequence.
extern "C" MATHLIBRARY_API unsigned fibonacci_index();
- 添加实现代码.cpp文件
// MathLibrary.cpp : Defines the exported functions for the DLL.
#include "pch.h" // use stdafx.h in Visual Studio 2017 and earlier
#include <utility>
#include <limits.h>
#include "MathLibrary.h"
// DLL internal state variables:
static unsigned long long previous_; // Previous value, if any
static unsigned long long current_; // Current sequence value
static unsigned index_; // Current seq. position
// Initialize a Fibonacci relation sequence
// such that F(0) = a, F(1) = b.
// This function must be called before any other function.
void fibonacci_init(
const unsigned long long a,
const unsigned long long b)
{
index_ = 0;
current_ = a;
previous_ = b; // see special case when initialized
}
// Produce the next value in the sequence.
// Returns true on success, false on overflow.
bool fibonacci_next()
{
// check to see if we'd overflow result or position
if ((ULLONG_MAX - previous_ < current_) ||
(UINT_MAX == index_))
{
return false;
}
// Special case when index == 0, just return b value
if (index_ > 0)
{
// otherwise, calculate next sequence value
previous_ += current_;
}
std::swap(current_, previous_);
++index_;
return true;
}
// Get the current value in the sequence.
unsigned long long fibonacci_current()
{
return current_;
}
// Get the current index position in the sequence.
unsigned fibonacci_index()
{
return index_;
}
- 正常编译
当前的Debug文件夹下会有编译好的lib和dll文件
- 创建可以使用DLL的客户端应用
- 添加头文件到当前项目中
方法一:添加路径
// MathClient.cpp : Client app for MathLibrary DLL.
// #include "pch.h" Uncomment for Visual Studio 2017 and earlier
#include <iostream>
#include "CameraDevice.h"
int main()
{
// Initialize a Fibonacci relation sequence.
fibonacci_init(1, 1);
// Write out the sequence values until overflow.
do {
std::cout << fibonacci_index() << ": "
<< fibonacci_current() << std::endl;
} while (fibonacci_next());
// Report count of values written before overflow.
std::cout << fibonacci_index() + 1 <<
" Fibonacci sequence values fit in an " <<
"unsigned 64-bit integer." << std::endl;
}
这时候运行是会报错的,因为你还没添加lib文件
- 添加lib文件到当前项目
这时候编译还是会报错,会说你还没缺失DLL文件
- 添加DLL文件到当前目录下
命令行里输入:xcopy /y /d “…\MathLibrary$(IntDir)MathLibrary.dll” “$(OutDir)”
更快捷的方法是直接将DLL文件复制到当下项目即可。同样lib文件也是如此
然后运行
成功!
dllexport, dllimport
和dllexport,dllimport存储类属性是特定于 C 和 C++ 语言的扩展。 可以使用它们从 DLL 中导出或向其中导入函数、数据和对象。
__declspec( dllimport ) declarator
__declspec( dllexport ) declarator
这些特性显式定义 DLL 到其客户端的接口,可以是可执行文件或另一个 DLL。 将函数声明为 dllexport 无需模块定义 (.def) 文件,至少与导出的函数规范有关。 该 dllexport 属性替换关键字 __export 。
如果标记 __declspec(dllexport)了类,则类层次结构中类模板的任何专用化都隐式标记为 __declspec(dllexport)。 这意味着显式实例化类模板,并且必须定义类的成员。
dllexport 的函数公开其修饰名称(有时称为“名称混乱”)的函数。 对于 C++ 函数,修饰的名称包括编码类型和参数信息的额外字符。 声明为 extern “C” 包含基于调用约定的平台特定修饰的 C 函数或函数。 不对使用**__cdecl**调用约定的导出的 C 函数或 C++ extern “C” 函数应用名称修饰。 有关 C/C++ 代码中的名称修饰的详细信息,请参阅 修饰名称。
声明dllexport时,必须使用dllimport扩展属性语法和__declspec关键字。
示例
// Example of the dllimport and dllexport class attributes
__declspec( dllimport ) int i;
__declspec( dllexport ) void func();
或者,若要提高代码的可读性,可以使用宏定义:
#define DllImport __declspec( dllimport )
#define DllExport __declspec( dllexport )
DllExport void func();
DllExport int i = 10;
DllImport int j;
DllExport int n;
__declspec关键字
用于指定存储类信息的扩展属性语法使用 __declspec 关键字,该关键字指定要将给定类型的实例与下面列出的特定于 Microsoft 的存储类属性一起存储。 其他存储类修饰符的示例包括 static 和 extern 关键字。 但是,这些关键字是 C 和 C++ 语言的 ANSI 规范的一部分,因此,扩展属性语法并未涵盖这些关键字。 扩展特性语法简化并标准化了 Microsoft 专用的 C 和 C ++ 语言扩展。
__declspec用户定义的类型声明开头指定的属性适用于该类型的变量。 例如:
__declspec(dllimport) class X {} varX;
在本例中,此特性应用于 varX。 __declspec放置在用户定义类型或struct关键字之后class的属性。 例如:
class __declspec(dllimport) X {};
以下代码声明了一个整数线程本地变量,并用一个值对其进行了初始化:
// Example of the __declspec keyword
__declspec( thread ) int tls_i = 1;
静态(隐式)加载
- 正常编译出dll文件
- 在要输出的函数头加上:
最好是在源码中引入 .lib 文件。
- 在 main.c 中除了用 **extern **关键字声明 add() 和 sub() 函数来自外部文件,还可以用 **_declspec(dllimport) **标识符声明函数来自动态链接库。
//main.cpp
#include<stdio.h>
#pragma comment(lib, "dllDemo.lib")
_declspec(dllimport) int add(int, int);
_declspec(dllimport) int sub(int, int);
int main(){
int a=10, b=5;
printf("a+b=%d\n", add(a, b));
printf("a-b=%d\n", sub(a, b));
return 0;
}
修改后的:
//dlltest.h
#ifndef _DLLDEMO_H
#define _DLLDEMO_H
#pragma comment(lib, "dllDemo.lib")
extern "C" _declspec(dllexport) 函数名
_declspec(dllexport) int add(int, int);
_declspec(dllexport) int sub(int, int);
#endif
//main.cpp
#include<stdio.h>
#include "dllDemo.h"
int main(){
int a=10, b=5;
printf("a+b=%d\n", add(a, b));
printf("a-b=%d\n", sub(a, b));
return 0;
}
- 将dll,lib,.h三个文件放在新的项目底下
- 配置新项目的环境。将dll,lib的路径添加到新项目的环境中。
测试使用
项目DLLTest
- 选择.exe生成
- 编译DLLTest.dll,DLLTest.lib,与DLLTest.exe同路径
//DLLTest.h
#pragma once
__declspec(dllexport) int add(int a, int b);
//DLLTest.cpp
#include "DLL_test.h"
int add(int a, int b) {
return a + b;
}
//main.cpp
#include<iostream>
#include "DLL_test.h"
using namespace std;
int main() {
cout << add(10, 3) << endl;
system("pause");
return 0;
}
创建新项目:getDLL
- 将DLLTest.h放入项目下
- 导入项目属性->链接器->输入->附加依赖项(导入DLLTest.lib路径)
- 将DLLTest.dll放入release目录下,与getDLL.exe同路径
//main.cpp
#include <iostream>
#include "DLL_test.h"
using namespace std;
int main() {
cout << add(10, 2) << endl;
system("pause");
return 0;
}
动态(显式)加载
应用程序必须进行函数调用,以在运行时显示加载DLL。为显示链接到DLL,应用程序必须:
-
调用 LoadLibrary(或相似的函数)以加载 DLL 和获取模块句柄。
-
调用 GetProcAddress,以获取指向应用程序要调用的每个导出函数的函数指针。由于应用程序是通过指针调用 DLL 的函数,编译器不生成外部引用,故无需与导入库链接。
-
使用完 DLL 后调用 FreeLibrary。
- 正常编译生成dll文件
- 将dll文件放在新项目文件夹下,不需要lib文件
- 需要用到LoadLibrary();
显式加载动态链接库时,需要用到 LoadLibrary() 函数,该函数的作用是将指定的可执行模块映射到调用进程的地址空间。
LoadLibrary() 函数的原型声明如下所示:
HMODULE LoadLibrary(LPCTSTR 1pFileName);
- LoadLibrary() 函数不仅能够加载DLL(.dll),还可以加载可执行模块(.exe)。
- LoadLibrary() 函数有一个字符串类型(LPCTSTR)的参数,该参数指定了可执行模块的名称,既可以是一个.dll文件,也可以是一个.exe文件。如果调用成功, LoadLibrary() 函数将返回所加载的那个模块的句柄。该函数的返回类型是HMODULE。 HMODULE类型和HINSTANCE类型可以通用。
当获取到动态链接库模块的句柄后,接下来就要想办法获取该动态链接库中导出函数的地址,这可以通过调用 **GetProcAddress() **函数来实现。该函数用来获取DLL导出函数的地址:
FARPROC GetProcAddress(HMODULE hModule, LPCSTR 1pProcName);
hModule:指定动态链接库模块的句柄DLL,即 LoadLibrary() 函数的返回值。
1pProcName:字符串指针,表示DLL中函数的名字。
#include <stdio.h>
#include <stdlib.h>
#include <windows.h> //必须包含 window.h
typedef int (*FUNADDR)(); //指向函数的指针
int main()
{
int a = 5,b = 10;
HINSTANCE dllDemo = LoadLibrary("dllDemo.dll");
FUNADDR add,sub;
if(dllDemo)
{
add = (FUNADDR)GetProcAddress(dllDemo,"add");
sub = (FUNADDR)GetProcAddress(dllDemo,"sub");
}
else
{
printf("Fail to load dll");
system("pause");
exit(1);
}
printf("a+b = %d\n",add(a,b));
printf("a - b = %d\n",sub(a,b));
system("pause");
return 0;
}
创建的 dllDemo 工程,将 debug 目录下的 dllDemo.dll 复制到当前工程目录下。注意,只需要 dllDemo.dll,不需要 dllDemo.lib。
总结
- 隐式加载和显式加载这两种加载DLL的方式各有优点,如果采用动态加载方式,那么可以在需要时才加载DLL,而隐式链接方式实现起来比较简单,在编写程序代码时就可以把链接工作做好,在程序中可以随时调用DLL导出的函数。但是,如果程序需要访问十多个DLL,如果都采用隐式链接方式加载它们的话, 那么在该程序启动时,这些DLL都需要被加载到内存中,并映射到调用进程的地址空间, 这样将加大程序的启动时间。
- 但是这时所有的DLL都已经被加载到内存中,资源浪费是比较严重的。在这种情况下,就可以采用显式加载的方式访问DLL,在需要时才加载所需的DLL,也就是说,在需要时DLL才会被加载到内存中,并被映射到调用进程的地址空间中。有一点需要说明的是,实际上, 采用隐式链接方式访问DLL时,在程序启动时也是通过调用LoadLibrary() 函数加载该进程需要的动态链接库的。
显示调用C++动态库注意点
对C++来说,情况稍微复杂。显式加载一个C++动态库的困难一部分是因为C++的name mangling;另一部分是因为没有提供一个合适的API来装载类,在C++中,您可能要用到库中的一个类,而这需要创建该类的一个实例,这不容易做到。
name mangling可以通过extern "C"解决。C++有个特定的关键字用来声明采用C binding的函数:extern “C” 。用 extern "C"声明的函数将使用函数名作符号名,就像C函数一样。因此,只有非成员函数才能被声明为extern “C”,并且不能被重载。尽管限制多多,extern "C"函数还是非常有用,因为它们可以象C函数一样被dlopen动态加载。冠以extern "C"限定符后,并不意味着函数中无法使用C++代码了,相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种类型的参数。
“显式”使用C++动态库中的Class是非常繁琐和危险的事情,因此能用“隐式”就不要用“显式”,能静态就不要用动态。