記錄一個面試被問到的問題。
extern 有什么用途?
除了多文件共享全局變量外還有呢?
extern "C" 的功能?
我想看完這篇文章就可以知道第三個問題了。
關於動態調用動態庫方法說明
一、 動態庫概述
1、 動態庫的概念
日常編程中,常有一些函數不需要進行編譯或者可以在多個文件中使用(如數據庫輸入/輸出操作或屏幕控制等標准任務函數)。可以事先對這些函數進行編譯,然后將它們放置在一些特殊的目標代碼文件中,這些目標代碼文件就稱為庫。庫文件中的函數可以通過連接程序與應用程序進行鏈接,這樣就不必在每次開發程序時都對這些通用的函數進行編譯了。
動態庫是一種在已經編譯完畢的程序開始啟動運行時,才被加載來調用其中函數的庫。其加載方式與靜態庫截然不同。
2、 動態庫的命名
Linux下,動態庫通常以.so(share object)結尾。(通常/lib和/usr/lib等目錄下存在大量系統提供的以.so結尾的動態庫文件)
Windows下,動態庫常以.dll結尾。(通常C:\windows\System32等目錄下存在大量系統提供的以.dll結尾的動態庫文件)
3、 動態庫與靜態庫之間的區別
靜態庫是指編譯連接時,把庫文件的代碼全部加入到可執行文件中,所以生成的文件較大,但運行時,就不再需要庫文件了。即,程序與靜態庫編譯鏈接后,即使刪除靜態庫文件,程序也可正常執行。
動態庫正好相反,在編譯鏈接時,沒有把庫文件的代碼加入到可執行文件中,所以生成的文件較小,但運行時,仍需要加載庫文件。即,程序只在執行啟動時才加載動態庫,如果刪除動態庫文件,程序將會因為無法讀取動態庫而產生異常。
二、 Linux下動態調用動態庫
備注:以下linux實例說明都是在RedHat 5.1系統+ gcc 版本 4.1.2 20080704 (Red Hat 4.1.2-46)上實現。
1、 .so動態庫的生成
可使用gcc或者g++編譯器生成動態庫文件(此處以g++編譯器為例)
g++ -shared -fPIC -c XXX.cpp
g++ -shared -fPIC -o XXX.so XXX.o
2、 .so動態庫的動態調用接口函數說明
動態庫的調用關系可以在需要調用動態庫的程序編譯時,通過g++的-L和-l命令來指定。例如:程序test啟動時需要加載目錄/root/src/lib中的libtest_so1.so動態庫,編譯命令可照如下編寫執行:
g++ -g -o test test.cpp –L/root/src/lib –ltest_so1
(此處,我們重點講解動態庫的動態調用的方法,關於靜態的通過g++編譯命令調用的方式不作詳細講解,具體相關內容可上網查詢)
Linux下,提供專門的一組API用於完成打開動態庫,查找符號,處理出錯,關閉動態庫等功能。
下面對這些接口函數逐一介紹(調用這些接口時,需引用頭文件#include <dlfcn.h>):
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之后,析構函數會被調用。
3、 普通函數的調用
此處以源碼實例說明。各源碼文件關系如下:
test_so1.h和test_so1.cpp生成test_so1.so動態庫。
test_so2.h和test_so2.cpp生成test_so2.so動態庫。
test_dl.cpp生成test_dl可執行程序,test_dl通過dlopen系列等API函數,並使用函數指針以到達動態調用不同so庫中test函數的目的。
////////////////////////////////test_so1.h//////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
extern "C" {
int test(void);
}
////////////////////////////////ttest_so1.cpp//////////////////////////////////////////////////////
#include "test_so1.h"
int test(void)
{
printf("USING TEST_SO1.SO NOW!\n");//注意此處與test_so2.cpp中的
//test函數的不同
return 1;
}
//////////////////////////////// test_so2.h //////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
extern "C" {
int test(void);
}
////////////////////////////////ttest_so2.cpp//////////////////////////////////////////////////////
#include "test_so2.h"
int test(void)
{
printf("USING TEST_SO2.SO NOW!\n");//注意此處與test_so1.cpp中的
//test函數的不同
return 1;
}
////////////////////////////////test_dl.cpp//////////////////////////////////////////////////////
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
int main(int argc, char **argv)
{
if(argc!=2)
{
printf("Argument Error! You must enter like this:\n");
printf("./test_dl test_so1.so\n");
exit(1);
}
void *handle;
char *error;
typedef void (*pf_t)(); //聲明函數指針類型
handle = dlopen (argv[1], RTLD_NOW); //打開argv[1]指定的動態庫
if (!handle)
{
fprintf (stderr, "%s\n", dlerror());
exit(1);
}
dlerror();
pf_t pf=(pf_t)dlsym(handle,"test" ); //指針pf指向test在當前內存中的地址
if ((error = dlerror()) != NULL)
{
fprintf (stderr, "%s\n", error);
exit(1);
}
pf(); //通過指針pf的調用來調用動態庫中的test函數
dlclose(handle); //關閉調用動態庫句柄
return 0;
}
////////////////////////////////makefile//////////////////////////////////////////////////////
.SUFFIXES: .c .cpp .o
CC=g++ -shared -fPIC
GCC=g++
all:test_so1.so test_so2.so test_dl clean
OBJ1=test_so1.o
OBJ2=test_so2.o
OBJ3=test_dl.o
test_so1.so:$(OBJ1)
$(CC) -o $@ $?
cp $@ /usr/lib
test_so2.so:$(OBJ2)
$(CC) -o $@ $?
cp $@ /usr/lib
test_dl:$(OBJ3)
$(GCC) -o $@ $? -ldl
.cpp.o:
$(CC) -c $*.cpp
.c.o:
$(CC) -c $*.c
clean:
rm -f *.o
上述源程序中,需重點注意兩個問題:
1、test_dl.cpp中,對於動態庫中的test函數調用是通過函數指針來完成的。
2、test_so1.h和test_so2.h中都使用了extern "C"。
在每個C++程序(或庫、目標文件)中,所有非靜態(non-static)函數在二進制文件中都是以“符號(symbol)”形式出現的。這些符號都是唯一的字符串,從而把各個函數在程序、庫、目標文件中區分開來。
在C中,符號名正是函數名:strcpy函數的符號名就是“strcpy”。這可能是因為兩個非靜態函數的名字一定各不相同的緣故。
而C++允許重載(不同的函數有相同的名字但不同的參數),並且有很多C所沒有的特性──比如類、成員函數、異常說明──幾乎不可能直接用函數名作符號名。為了解決這個問題,C++采用了所謂的name mangling。它把函數名和一些信息(如參數數量和大小)雜糅在一起,改造成奇形怪狀,只有編譯器才懂的符號名。例如,被mangle后的foo可能看起來像foo@4%6^,或者,符號名里頭甚至不包括“foo”。
其中一個問題是,C++標准(目前是[ISO14882])並沒有定義名字必須如何被mangle,所以每個編譯器都按自己的方式來進行name mangling。有些編譯器甚至在不同版本間更換mangling算法(尤其是g++ 2.x和3.x)。即使您搞清楚了您的編譯器到底怎么進行mangling的,從而可以用dlsym調用函數了,但可能僅僅限於您手頭的這個編譯器而已,而無法在下一版編譯器下工作。
用 extern "C"聲明的函數將使用函數名作符號名,就像C函數一樣。因此,只有非成員函數才能被聲明為extern "C",並且不能被重載。盡管限制多多,extern "C"函數還是非常有用,因為它們可以象C函數一樣被dlopen動態加載。冠以extern "C"限定符后,並不意味着函數中無法使用C++代碼了,相反,它仍然是一個完全的C++函數,可以使用任何C++特性和各種類型的參數。所以extern "C" 只是告訴編譯器編和鏈接的時候都用c的方式的函數名字,函數里的內容可以為c的代碼也可以為c++的。
執行makefile正常編譯后,可生成test_so1.so、test_so2.so動態庫以及test_dl執行程序。可執行test_dl,顯示結果如下:
[root@localhost so_src]# ./test_dl test_so1.so
USING TEST_SO1.SO NOW!
[root@localhost so_src]# ./test_dl test_so2.so
USING TEST_SO2.SO NOW!
[root@localhost so_src]# ./test_dl
Argument Error! You must enter like this:
./test_dl test_so1.so
備注:如果我們去掉test_so1.h和test_so2.h中的extern "C",重新編譯執行后將可能會出現什么情況?有興趣的朋友可以試下:
[root@localhost so_src]# ./test_dl test_so1.so
/usr/lib/test_so1.so: undefined symbol: test
[root@localhost so_src]# ./test_dl test_so2.so
/usr/lib/test_so2.so: undefined symbol: test
4、 類的調用
加載類有點困難,因為我們需要類的一個實例,而不僅僅是一個函數指針。我們無法通過new來創建類的實例,因為類是在動態庫中定義的而不是在可執行程序中定義的,況且有時候我們連動態庫中具體的類的名字都不知道。
解決方案是:利用多態性!我們在可執行文件中定義一個帶虛成員函數的接口基類,而在模塊中定義派生實現類。通常來說,接口類是抽象的(如果一個類含有虛函數,那它就是抽象的)。因為動態加載類往往用於實現插件,這意味着必須提供一個清晰定義的接口──我們將定義一個接口類和派生實現類。
接下來,在模塊中,我們會定義兩個附加的類工廠函數(class factory functions)(或稱對象工廠函數)。其中一個函數創建一個類實例,並返回其指針;另一個函數則用以銷毀該指針。這兩個函數都以extern "C"來限定修飾。
實例如下:
test_base.hpp中定義一個含有純虛函數virtual void display() const = 0的基類。
test_1.cpp中定義繼承類test1,並實現虛函數virtual void display() const的定義,並實現一個創建類函數和一個銷毀類指針函數。
test_2.cpp中定義繼承類test2,並實現虛函數virtual void display() const的定義,並實現一個創建類函數和一個銷毀類指針函數。
main.cpp中實現動態的調用不同庫中的display()方法。
////////////////////////////////test_base.hpp//////////////////////////////////////////////////////
#ifndef TEST_BASE_HPP
#define TEST_BASE_HPP
#include <iostream>
using namespace std;
class test_base {
public:
test_base(){}
virtual ~test_base() {}
void call_base() {
cout << "call base" << endl;
}
virtual void display() const = 0 ;
};
// the types of the class factories
typedef test_base* create_t();
typedef void destroy_t(test_base*);
#endif
////////////////////////////////test1.cpp//////////////////////////////////////////////////////
#include "test_base.hpp"
class test1 : public test_base {
public:
virtual void display() const {
cout << "Running in test1.so Now" << endl;
}
};
// the class factories
extern "C" test_base* create() {
return new test1;
}
extern "C" void destroy(test_base* p) {
delete p;
}
////////////////////////////////test1.cpp//////////////////////////////////////////////////////
#include "test_base.hpp"
class test2 : public test_base {
public:
virtual void display() const {
cout << "Running in test2.so Now" << endl;
}
};
// the class factories
extern "C" test_base* create() {
return new test2;
}
extern "C" void destroy(test_base* p) {
delete p;
}
////////////////////////////////main.cpp//////////////////////////////////////////////////////
#include "test_base.hpp"
#include <iostream>
#include <dlfcn.h>
int main(int argc , char** argv) {
// load the test library
if(argc!=2)
{
cout << "Argument Error! You must enter like this: " << '\n';
cout << "./a.out test_1.so " << '\n';
return 1;
}
void* test_index = dlopen(argv[1], RTLD_NOW);
if (!test_index) {
cerr << "Cannot load library: " << dlerror() << '\n';
return 1;
}
// reset errors
dlerror();
// load the symbols
create_t* create_test = (create_t*) dlsym(test_index, "create");
const char* dlsym_error = dlerror();
if (dlsym_error) {
cerr << "Cannot load symbol create: " << dlsym_error << '\n';
return 1;
}
destroy_t* destroy_test = (destroy_t*) dlsym(test_index, "destroy");
dlsym_error = dlerror();
if (dlsym_error) {
cerr << "Cannot load symbol destroy: " << dlsym_error << '\n';
return 1;
}
// create an instance of the class
test_base* c_test = create_test();
// use the class
c_test->display();
destroy_test(c_test);
// unload the test library
dlclose(test_index);
}
////////////////////////////////makefile//////////////////////////////////////////////////////
.SUFFIXES: .c .cpp .o
CC=g++ -g -shared -fPIC
GCC=g++ -g
all:clear test_1.so a.out test_2.so clean
OBJ1=test_1.o
OBJ2=main.o
OBJ3=test_2.o
clear:
rm -rf *.so a.out b.out
test_1.so:$(OBJ1)
$(CC) -o $@ $?
cp $@ /usr/lib
a.out:$(OBJ2)
$(GCC) -o $@ $? -ldl
test_2.so:$(OBJ3)
$(CC) -o $@ $?
cp $@ /usr/lib
.cpp.o:
$(CC) -c $*.cpp
.c.o:
$(CC) -c $*.c
clean:
rm -f *.o
執行makefile正常編譯后,可生成test_1.so、test_2.so動態庫以及a.out執行程序。可執行a.out,顯示結果如下:
[root@localhost c++_so_src]# ./a.out test_1.so
Running in test1.so Now
[root@localhost c++_so_src]# ./a.out test_2.so
Running in test2.so Now
[root@localhost c++_so_src]# ./a.out
Argument Error! You must enter like this:
./a.out test_1.so