VS2019 VC++ 靜態庫的開發與使用

前言

這篇文章應該寫在這個系列裏面的:VS2017的C++開發心得(九)DLL動態鏈接——多項目開發

但當時沒寫,只寫了動態鏈接的相關內容,是因爲我認爲靜態鏈接相比較動態鏈接會簡單得多,所以乾脆就略過了。既然有人提問了,那我就簡單的開發流程和主要遇到的問題來講講靜態庫的開發。

首先總體來看,靜態庫和動態庫的區別在於:

1. 靜態庫只出現在項目的編譯生成的鏈接期,而動態庫則是程序運行時加載使用的;

2. 靜態庫只有一個.lib文件,而動態庫除了一個.dll文件以外一般還有個對應的.lib文件;

3. 靜態庫最後會內嵌到輸出的.exe或.dll中,而動態庫是外掛使用;

4. 靜態庫是沒有鏈接器的,因爲靜態庫不需要鏈接,所以你即便有些符號沒有找到實現也不會報錯。

VS2019創建靜態庫

1. 首先創建一個控制檯程序StaticLibTest,過程省略,我之前的文章有很多介紹。

2. 添加靜態庫項目,StaticLib1:

到這裏兩個項目創建起來了,但是兩個項目間是毫無聯繫的,下面用鏈接器讓StaticLibTest使用StaticLib1生成的靜態庫。

1. 在StaticLibTest中的鏈接器中添加StaticLib1的生成路徑:

這個路徑實際上就是整個解決方案的生成路徑,.exe也生成在這裏。我這裏就沒有做lib和bin分開的項目管理了。

2. 在輸入中加入StaticLib1.lib

3. 在StaticLibTest.cpp中加入fnStaticLib1的調用,這個函數是VS自動生成的,在StaticLib1.cpp下面,根據你的項目名不同,這個函數名也不一樣:

4. 添加項目依賴關係:

這裏是爲兩個項目在生成的時候綁定了依賴關係,StaticLibTest在生成前一定會檢查StaticLib1是不是最新的生成。很多人出現的問題大多數都是由於靜態庫沒有生成更新導致的,這是很愚蠢的問題。

5. 直接調試運行(先生成StaticLib1.lib再生成StaticLibTest.exe):

這樣就鏈接好了,非常簡單。

靜態庫的理解

爲了大家更好的理解靜態庫,我這裏做個等效測試。

1. 在控制檯項目中添加一個a.cpp文件,內容如下:

//////////////a.cpp//////////////
const char* filename = "a.cpp";
//const char* filename = __FILE__;
int codeline = 2;
//int codeline = __LINE__;

const char* GetFunctionName()
{
  return "GetFunctionName"; 
  //return __FUNCTION__;
}

註釋掉的代碼後面再解釋,這是編譯器爲我們提供的方便。

2. 在StaticLibTest.cpp中添加這三個變量函數的調試打印:

////////////StaticLibTest.cpp/////////////////
#include <iostream>
void fnStaticLib1();
extern const char* filename;
//const int codeline = 2;
extern int codeline;

const char* GetFunctionName();
int main()
{
    std::cout << "Hello World!\n";
    fnStaticLib1();
    std::cout << filename << std::endl;
    std::cout << codeline << std::endl;
    std::cout << GetFunctionName() << std::endl;
}

注意這裏我沒有用const int,因爲它不參與鏈接,只在當前文件可見。爲什麼const char*可以?讀者可以思考下。

3. 打印結果是:

4. 我把a.cpp直接添加到StaticLib1中,而在StaticLibTest中移出掉。即這個文件編譯在靜態庫中,不在控制檯項目中編譯,如下:

5. 拖過來以後會有這個錯誤提示,“PCH 警告: 找不到合適的頭停止點位置。未生成 IntelliSense PCH 文件”:

這個是由於VS默認爲靜態庫項目打開了PCH預編譯頭,即pch.h和pch.cpp。這裏不具體介紹預編譯頭用法,簡單說就是會提高編譯效率。但是,預編譯頭也要求每個項目下的.cpp都需要include"pch.h"。如果沒有這麼做,會有以下的編譯錯誤:

fatal error C1010: 在查找預編譯頭時遇到意外的文件結尾。是否忘記了向源中添加“#include "pch.h"”?

而且,這裏還有噁心的一點,必須是#include "pch.h".  #include "../pch.h"這個也是不行的。這個強制要求我感覺應該算VS的bug,暫時沒能理解爲啥VS這麼做。

所以a.cpp的文件要加上include "pch.h",然後在StaticLib1的項目屬性 C++/C的附加包含路徑下加入:"../StaticLib1":

或者你也可以簡單的去除pch,以便測試:

6. 再次調試運行,結果完全一樣:

以上的操作是爲了讓你理解,靜態鏈接庫的內容就像是把我這個項目內的某些文件到外面先編譯好,實際上在使用的時候,就像是在使用同一個項目下的cpp文件一樣。

這也是爲什麼我會說靜態庫比動態庫簡單得多。

靜態庫的問題

我這裏靜態庫的代碼都看得到,所以會覺得用起來很簡單。一般情況下,都是一些頭文件加上一個靜態庫的使用情景。

頭文件決定了靜態庫的鏈接符號名,鏈接器會去.lib裏找這些符號。但是,要是你自己脫離對方提供的頭文件來調用,或者頭文件寫的有問題,那往往容易出現無法解析的外部符號的鏈接錯誤。

比如,我去掉const char*的const修飾:

這時就會有以下的鏈接錯誤:

StaticLibTest.obj : error LNK2001: 無法解析的外部符號 "char * filename" (?filename@@3PEADEA)

遇到這種鏈接錯誤就根據這個符號名去對應.lib的符號,看看是否是完全一樣的。

比如這裏,StaticLibTest是調用端。調用端使用的符號是?filename@@3PEADEA。那需要到被調用端的StaticLib1.lib裏去查找下,是否有這樣的符號。如果沒有,那麼正確的符號名是什麼?

查找符號名也是使用DUMPBIN這個VS工具。

使用DUMPBIN查看符號表

這裏再說一次DUMPBIN的使用方式,因爲之前有人打開控制檯問我爲啥沒有。這個工具是在VS的環境下,不在系統環境,所以需要通過VS的命令行工具來打開。有兩種方式:

1. 在VS的菜單中打開:

2. 在開始菜單,VS2019的文件夾中打開:

打開以後cd到.lib的生成目錄:

1. 先點擊地址欄,複製文件夾路徑:

2. 然後就是 cd 切換文件夾的操作:

注意如果不在一個硬盤是不能用cd的. cd的路徑是同一個硬盤下才行。如果你的項目文件夾在D盤下,那麼先輸入D:,切換到D盤,再進行cd操作。

3. 輸入DUMPBIN /LINKERMEMBER StaticLib1.lib

這八個public symbols,就是你可以在你的調用項目裏通過鏈接符號而使用的變量或者函數。這裏看到被調用端的正確符號是:

?filename@@3PEBDEB,而調用端去掉const以後的符號名是?filename@@3PEADEA,是不一樣的。這時候你需要,仔細覈對了。你也可以自己根據符號名推理出是const char*,怎麼推理需要看下C++ name mangling的相關知識,比如:https://en.wikipedia.org/wiki/Name_mangling 

VS2019 dumpbin查看DLL的導出函數

最後補充兩個DUMPBIN的小竅門。如果是比較大的lib的話,有可能會把控制檯溢出,導致一部分內容被截斷,可以通過以下兩個方法來實現查找符號:

1. 寫入到一個txt文件來查找:

DUMPBIN /LINKERMEMBER StaticLib1.lib > staticlib1.txt

> *.txt這就是把左邊的控制檯輸出導出到右邊的這個.txt文件中,非常實用的命令。

2. 使用findstr來直接查找對應的符號名

DUMPBIN /LINKERMEMBER StaticLib1.lib | findstr filename

| findstr filename 這個就是對左邊的控制檯輸出進行findstr的篩選,對應Linux下的 |grep. 

調試技巧文件名,函數名,行號

看看上面的一段代碼:

const char* filename = __FILE__;
int codeline = __LINE__;
const char* GetFunctionName()
{
  return __FUNCTION__;
}

__FILE__,__LINE__,__FUNCTION__這三個保留字是編譯器保留的,可以在編譯的時候進行替換,分別對應文件名和行號,以及函數名。

所以一般會用它們來幫助我們調試輸出的時候帶上當前的代碼位置,比如:

這時候控制檯的輸出爲:

可以思考下,我爲啥不用LogLine的函數,而用一個宏定義函數。

這三個保留字還可以和宏定義中的#和##組合出非常有用的調試打印方法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章