最近接手了一个新的项目, 遇到一系列链接错误,折腾的头大。上周末终于完全解决了各个project之间的依赖及链接问题, 趁此机会,我仔细阅读了一些资料并在vs2019上做了一些实验,算是基本搞清楚了静态链接和动态链接的一些基本概念, 在这里记录一下,希望对自己也对其他人有所帮助。
下面通过一个实验来解释在vs2019环境下如何进行静态链接和动态链接。
1. 新建三个工程
首先我们需要新建一个名为Test的解决方案,并在Test中新建两个工程,Printer和Calc, Configuration均配置为Debug。在Calc工程中我们会实现加法函数,并调用Printer过程提供的打印函数打印计算结果。在Test工程中我们实现main函数,在main函数中调用Calc工程提供的加法函数。
因此这三个工程的依赖关系是Test工程依赖Calc工程,Calc工程依赖Printer工程。
2. 添加源文件
- Printer project
//Printer.h"
#pragma once
class Printer {
public:
static void PrintInt(int value);
};
//Printer.cpp
#include "Printer.h"
#include<iostream>
void Printer::PrintInt(int value)
{
std::cout << value << std::endl;
}
- Calc project
//Calc.h
#pragma once
class Calculator {
public:
int Add(int m, int n);
};
//Calc.cpp
#include "Calc.h"
#include "../Printer/Printer.h"
int Calculator::Add(int m, int n)
{
Printer::PrintInt(m + n);
return m + n;
}
- Test project
#include "../Calc/Calc.h"
int main()
{
int m = 3, n = 4;
Calculator c;
c.Add(m, n);
return 0;
}
好了,现在我们的三个工程如下图所示
3. 编译试试看
此时我们编译工程,会产生一大堆报错。
Severity Code Description Project File Line Suppression State
Error LNK2019 unresolved external symbol "public: int __thiscall Calculator::Add(int,int)" (?Add@Calculator@@QAEHHH@Z) referenced in function _main Test C:\Users\vincent.zheng\source\repos\Test\Test\main.obj 1
Error LNK2019 unresolved external symbol _main referenced in function "int __cdecl invoke_main(void)" (?invoke_main@@YAHXZ) Printer C:\Users\vincent.zheng\source\repos\Test\Printer\MSVCRTD.lib(exe_main.obj) 1
Error LNK1120 1 unresolved externals Test C:\Users\vincent.zheng\source\repos\Test\Debug\Test.exe 1
Error LNK1120 1 unresolved externals Printer C:\Users\vincent.zheng\source\repos\Test\Debug\Printer.exe 1
Error LNK1120 2 unresolved externals Calc C:\Users\vincent.zheng\source\repos\Test\Debug\Calc.exe 1
Error LNK2019 unresolved external symbol _main referenced in function "int __cdecl invoke_main(void)" (?invoke_main@@YAHXZ) Calc C:\Users\vincent.zheng\source\repos\Test\Calc\MSVCRTD.lib(exe_main.obj) 1
Error LNK2019 unresolved external symbol "public: static void __cdecl Printer::PrintInt(int)" (?PrintInt@Printer@@SAXH@Z) referenced in function "public: int __thiscall Calculator::Add(int,int)" (?Add@Calculator@@QAEHHH@Z) Calc C:\Users\vincent.zheng\source\repos\Test\Calc\Calc.obj 1
仔细看报错信息,你会发现Error LNK2019 unresolved external symbol xxx referenced in function xxx
出现的非常频繁。这就是说链接时没有找到外部符号, 具体原因也很简单,因为我们在test工程引用了Calc工程的函数(函数就是符号的一种), 在Calc中引用了Printer工程的函数,但是这两个工程并没有把它们的符号导出出来供其他工程使用。要想解决这个问题就要把这两个工程的符号导出来。导出符号一般有两种办法, 一是导出静态库,一是导出动态库,这两种方法各有利弊。
4. 静态链接
静态链接非常简单,我们把Printer 和Calc工程的Configuration Type修改为static library
即可(默认是Application)。
修改方法:
在工程名上右击, 选择Configuration Properties–> General --> Configuration Type, 下拉框选择static library
。
分别单独编译Printer和Calc工程,此时会在解决方案目录下的Debug文件夹分别生成Calc.lib
和Printer.lib
文件,这就是生成的静态链接库文件。
生成lib文件还不够,我们必须告诉链接器lib文件的路径,让链接器链接到这些库文件,有两种办法:
-
在工程上添加依赖,比如Calc工程依赖于Printer工程,Test工程依赖Calc工程,那么我们分别给相应的工程添加依赖。
-
手动添加lib文件路径
在Test工程上右击选择Properties–>Linker–>input–>Additional Dependencies.添加calc.lib和printer.lib文件路径
(此处相对路径的起点是Test工程文件所在的路径)
理论上来讲,我们添加好lib文件路径后,就可以编译链接成功, 但是实际上我们还可能遇到另外一个链接错误。
5. 解决编译顺序导致的链接错误
上述第二种添加文件路径的方式存在一点点副作用。第一种方法添加依赖之后,vs可以自动判断各个project的编译顺序。比如在我们这个例子当中,Test依赖于Calc, Calc依赖于Printer, 因此vs会首先编译Printer工程,然后编译Calc过程,最后编译Test工程。但是第二种方法并不能自动推导出编译顺序, 因此有可能Test工程先编译,Printer和Calc工程后编译,这时候链接器找不到lib文件,还是会报链接错误。比如在我们这个例子中,clean solution后重新编译,会报如下错误:
1>------ Build started: Project: Test, Configuration: Debug Win32 ------
2>------ Build started: Project: Calc, Configuration: Debug Win32 ------
3>------ Build started: Project: Printer, Configuration: Debug Win32 ------
2>Calc.cpp
3>Printer.cpp
1>main.cpp
2>Calc.vcxproj -> C:\Users\vincent.zheng\source\repos\Test\Debug\Calc.lib
1>LINK : fatal error LNK1104: cannot open file '..\Debug\Printer.lib'
1>Done building project "Test.vcxproj" -- FAILED.
3>Printer.vcxproj -> C:\Users\vincent.zheng\source\repos\Test\Debug\Printer.lib
========== Build: 2 succeeded, 1 failed, 0 up-to-date, 0 skipped ==========
可以看到,vs先编译了Test工程,后编译Calc,Printer工程,导致链接器报can not open ..\Debug\Printer.lib
错误。我们可以自定义编译顺序来解决这个问题。在solution 'Test’上右击,选择Project Dependencies,设置test依赖于Calc, Calc依赖于Printer。
1>------ Build started: Project: Printer, Configuration: Debug Win32 ------
1>Printer.cpp
1>Printer.vcxproj -> C:\Users\vincent.zheng\source\repos\Test\Debug\Printer.lib
2>------ Build started: Project: Calc, Configuration: Debug Win32 ------
2>Calc.cpp
2>Calc.vcxproj -> C:\Users\vincent.zheng\source\repos\Test\Debug\Calc.lib
3>------ Build started: Project: Test, Configuration: Debug Win32 ------
3>main.cpp
3>Test.vcxproj -> C:\Users\vincent.zheng\source\repos\Test\Debug\Test.exe
========== Build: 3 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
这一次,编译器按照我们想要的顺序编译成功,没有报错。
6. 静态链接存在的问题
静态链接相对来说还是非常简单的,生成lib文件,设置一下链接路径,我们可爱的代码就可以很嗨皮的跑起来。虽然使用起来很简单,但是静态链接还是存在一些其他的问题。
- 空间浪费。静态链接的可执行文件体积比较大,包含相同的公共代码。比如上面的例子当中最后生成的Test.exe文件Add函数,PrintInt函数代码。如果有其他的程序需要用到Add函数,PrintInt函数,它同样需要包含这两个函数的代码。如果这些程序同时运行起来,那么这些函数的代码就会被复制到每个运行进程的文本段中,造成极大的浪费。内存就像厨房里的垃圾桶,不管容量有多大,总是不够用的。
- 不方便维护,如果静态库做了修改,那么所有依赖改静态库的程序必须全部重新编译链接。
7. 动态链接
动态链接库就是致力于解决静态库缺陷的一个现代化产物。动态库是一个目标模块,在运行或加载时,可以加载到任意为内存地址,并和一个在内存中的程序链接起来。这个过程称为动态链接,是由一个叫做动态链接器的程序来执行的。
微软操作系统中就大量使用了动态库,它们称为DLL。
7.1 生成DLL
现在我们改用动态链接的方式来调用PrintInt和Add函数。将Calc和Printer工程的Configuration Type改为Dynamic Library。分别编译Printer, Calc, Test工程。
Project Printer build result:
1>------ Build started: Project: Printer, Configuration: Debug Win32 ------
1>Printer.cpp
1>Printer.vcxproj -> C:\Users\vincent.zheng\source\repos\Test\Debug\Printer.dll
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========
Project Calc build result:
Error LNK1120 1 unresolved externals Calc C:\Users\vincent.zheng\source\repos\Test\Debug\Calc.dll 1
Error LNK2019 unresolved external symbol "public: static void __cdecl Printer::PrintInt(int)" (?PrintInt@Printer@@SAXH@Z) referenced in function "public: int __thiscall Calculator::Add(int,int)" (?Add@Calculator@@QAEHHH@Z) Calc C:\Users\vincent.zheng\source\repos\Test\Calc\Calc.obj 1
Project Test build result:
Error LNK2019 unresolved external symbol "public: int __thiscall Calculator::Add(int,int)" (?Add@Calculator@@QAEHHH@Z) referenced in function _main Test C:\Users\vincent.zheng\source\repos\Test\Test\main.obj 1
Error LNK1120 1 unresolved externals Test C:\Users\vincent.zheng\source\repos\Test\Debug\Test.exe 1
Error LNK1120 1 unresolved externals Calc C:\Users\vincent.zheng\source\repos\Test\Debug\Calc.dll 1
Error LNK2019 unresolved external symbol "public: static void __cdecl Printer::PrintInt(int)" (?PrintInt@Printer@@SAXH@Z) referenced in function "public: int __thiscall Calculator::Add(int,int)" (?Add@Calculator@@QAEHHH@Z) Calc C:\Users\vincent.zheng\source\repos\Test\Calc\Calc.obj 1
不出所料,我们又得到了一大堆编译错误!不过不要着急,我们一个个来看。从上面的信息可以看出, Printer工程是编译成功了的,并且已经在Debug文件夹生成了DLL文件。而Calc和Test工程分别有一些外部符号找不到的报错。Test工程找不到Add函数不奇怪,因为Calc工程的dll文件文件还没有生成。但是Printer工程的dll文件已经生成了,Calc还是找不到PrintInt函数呢?
7.2 DLL导出符号
其实只有DLL文件还是不够的,我们还需要将DLL中的符号进行导出。导出的方法也很简单,在函数声明中加上__declspec(dllexport)
即可。在我们的工程中我们需要导出PrintInt函数和Add函数。因此我们修改源文件如下:
//Printer.h"
#pragma once
#define EXPORT __declspec(dllexport)
class EXPORT Printer {
public:
static void PrintInt(int value);
};
//Calc.h
#pragma once
#define EXPORT __declspec(dllexport)
class Calculator {
public:
int EXPORT Add(int m, int n);
};
这样我们导出了Printer类和Calculator类,再此编译就没有报错了(记得添加依赖)。在debug文件夹我们发现生成了Printer.lib, Printer.dll,Calc.lib, Calc.dll文件。