linux上靜態庫和動態庫的編譯和使用(附外部符號錯誤淺談)


主要參考博客gcc創建和使用靜態庫和動態庫

對於熟悉windows的同學,linux上的靜態庫.a相當於win的.lib,動態庫.so相當於win的.dll.

首先簡要地解釋下這兩種函數庫的區別,參考《Linux程序設計》

1. 靜態庫也被稱為歸檔文件(archive,因此創建命令是ar),編譯器和鏈接器負責將程序代碼和靜態庫結合在一起組成單獨的可執行文件;

但是缺點是許多應用程序同時運行並使用來自同一個靜態庫的函數時,內存中就會有一個函數的多份副本,而且程序文件自身也有多份同樣的副本,這將消耗大量的內存和磁盤空間。

2. 動態庫,也稱共享庫(因此創建命令包含share)。可執行文件不會包含動態庫的函數代碼,而是引用運行時可訪問的共享代碼,函數引用被解析並產生對動態庫的調用時,動態庫才會被加載到內存中。因此系統可以只保留動態庫的一份副本,並且當函數功能需要改變時,只需要重新編譯生成動態庫,而不用重新編譯整個源程序。

現在直接進入重點,貼代碼。首先我創建了三個文件hello.h hello.c main.c,其中前兩個是函數hello()的頭文件和源文件,main.c則是調用hello()函數。代碼如下

/*************************************************************************
    > File Name: hello.h
 ************************************************************************/
#ifndef _HELLO_H_
#define _HELLO_H_

void hello();

#endif
/*************************************************************************
    > File Name: hello.c
 ************************************************************************/

#include <stdio.h>
#include "hello.h"

void hello()
{
	printf("hello world!\n");
}
/*************************************************************************
    > File Name: main.c
 ************************************************************************/
#include "hello.h"

int main(int argc, char** argv)
{
    hello();
    return 0;
}

目錄組織如下(這里使用了tree,我是Ubuntu系統,命令apt-get install tree即可安裝)

兩個sh文件分別是使用動態庫和靜態庫的shell文件,把命令行整合到一起,代碼如下

#########################################################################
# File Name: static-compile.sh
#########################################################################
#!/bin/bash
cd ./lib
gcc -c -I../include hello.c  # 生成hello.o
ar rc libhello.a hello.o  # 生成libhello.a
cd ../src
gcc main.c -I../include -L../lib -lhello -o main  # 生成main
cd ../  # 回到根目錄

首先進入lib目錄把自定義函數庫的源文件hello.c進行編譯生成目標文件hello.o。然后用ar rc(ar是歸檔,rc分別代表replace和create,即若已存在則替換、創建新文件)生成靜態庫libhello.a。

然后進入src目錄,-I后面緊接着(沒有空格)頭文件目錄路徑(I代表include),-L后面緊接着(沒有空格)庫文件目錄路徑(L代表lib)

#########################################################################
# File Name: shared-compile.sh
#########################################################################
#!/bin/bash
cd ./lib
# 生成動態庫libhello.so
gcc hello.c -I../include -fpic -shared -o libhello.so 
# 生成可執行文件
cd ../src
mv ../lib/libhello.so ./
cp ./libhello.so /lib  # 將動態庫復制到/lib文件夾
gcc main.c -I../include -L../lib -lhello -o main
cd ../  # 回到根目錄

對於動態庫來說,生成命令多了-fpic和-shared。PIC代表Position-Independent Code,與位置無關,也就是使用的都是相對路徑和絕對路徑。shared代表共享。

而在使用動態庫之前,需要把.so復制到/lib文件夾(或者設置環境變量,見我文章開頭引用的博客)。

使用動態庫的命令除了-I和-L外,還有個-l(小寫的L),后面接着的是hello。

——這是因為我的動態庫命名為libhello.so,使用-l的話會忽略前面的lib和后面的后綴名。

 

分別在linux下運行static-compile.sh和shared-compile.sh,效果如下

由於動態庫沒有把函數庫的代碼加入到可執行文件中,所以可以看出使用動態庫鏈接出的程序大小偏小(8592<8664)

當然,生成程序之后,.a、.so(非/lib目錄下的)文件都可以刪掉,程序一樣能運行。

 

最后談下這兩者的實際應用,就以Visual C++編程來談吧。

很多時候會出現unsolved external symbol(未解決的外部符號)錯誤,如果是用其他人的庫,很有可能就是忘記在菜單設置-鏈接器->輸入->附加依賴項中加入需要的.lib文件(也就是靜態庫)。(比如使用winsock2.h的一些庫函數時,沒有#pragma comment(lib, "Ws2_32.lib"))把Ws2_32.lib靜態庫加載進去的話,函數就只有頭文件中的聲明,而缺少了庫文件中的定義。

而如果是忘記把.dll路徑(往往是bin文件夾)添加到環境變量中(PS:我的dynamic-compile.sh對應windows相當於是把dll放到了system32目錄下),在編譯的時候不會出錯,而是Debug運行的時候會報錯。

 

這就是靜態庫和動態庫的顯著區別,靜態庫是編譯期間由鏈接器通過include目錄找到並鏈接到到可執行文件中,而動態庫則是運行期間動態調用,只有運行時找不到對應動態庫才會報錯。

說回外部符號錯誤,如果是自己寫的,很有可能就是函數只編譯了,沒有定義。比如f.h中寫的是void f(); 結果對應的f.cpp寫的是void f2() {}看起來不可能出現這種錯誤,實際上由於C++重載某種意義上是改了函數名(而且還改得很長),這樣的錯誤對新手來說很常見。

還有個典型的例子,就是C++調用C函數,報錯如下

main.obj : error LNK2019: unresolved external symbol "void __cdecl f(void)" (?f@@YAXXZ) referenced in function _main
// hello.h
#pragma once

void f();
// hello.c
#include "hello.h"

void f() { }
// main.cpp
#include "hello.h"

int main()
{
	f();
	return 0;
}

因為C++眼中,void f(void);其實是void f@@YAXXZ(void);(f后面的取決於編譯器),而C眼中,f就是f。一般需要使用extern "C"來使用,或者直接把hello.c改后綴為hello.cpp。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM