VS静态链接和动态链接

最近接手了一个新的项目, 遇到一系列链接错误,折腾的头大。上周末终于完全解决了各个project之间的依赖及链接问题, 趁此机会,我仔细阅读了一些资料并在vs2019上做了一些实验,算是基本搞清楚了静态链接和动态链接的一些基本概念, 在这里记录一下,希望对自己也对其他人有所帮助。
下面通过一个实验来解释在vs2019环境下如何进行静态链接和动态链接。

1. 新建三个工程

首先我们需要新建一个名为Test的解决方案,并在Test中新建两个工程,Printer和Calc, Configuration均配置为Debug。在Calc工程中我们会实现加法函数,并调用Printer过程提供的打印函数打印计算结果。在Test工程中我们实现main函数,在main函数中调用Calc工程提供的加法函数。
因此这三个工程的依赖关系是Test工程依赖Calc工程,Calc工程依赖Printer工程。

2. 添加源文件

  1. 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;
}
  1. 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;
}
  1. 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.libPrinter.lib文件,这就是生成的静态链接库文件。

生成lib文件还不够,我们必须告诉链接器lib文件的路径,让链接器链接到这些库文件,有两种办法:

  1. 在工程上添加依赖,比如Calc工程依赖于Printer工程,Test工程依赖Calc工程,那么我们分别给相应的工程添加依赖。
    在这里插入图片描述

  2. 手动添加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文件,设置一下链接路径,我们可爱的代码就可以很嗨皮的跑起来。虽然使用起来很简单,但是静态链接还是存在一些其他的问题。

  1. 空间浪费。静态链接的可执行文件体积比较大,包含相同的公共代码。比如上面的例子当中最后生成的Test.exe文件Add函数,PrintInt函数代码。如果有其他的程序需要用到Add函数,PrintInt函数,它同样需要包含这两个函数的代码。如果这些程序同时运行起来,那么这些函数的代码就会被复制到每个运行进程的文本段中,造成极大的浪费。内存就像厨房里的垃圾桶,不管容量有多大,总是不够用的。
  2. 不方便维护,如果静态库做了修改,那么所有依赖改静态库的程序必须全部重新编译链接。

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文件。

猜你喜欢

转载自blog.csdn.net/ww1473345713/article/details/108569238