参考资料:VisualStudio2022构建调试C++_夏曹俊_bilibili
我这边不再提供相关资源。
目录
编译调试 C++入门篇
vs2022软件安装
安装
- 若需要用vs2022打开vs2019工程文件,则需另外勾选
MSVCv142-VS2019 C++ x64/x86生成工具
,以此类推。 - 有些应用开发依赖特定的Windows SDK(Software Development Kit),需要额外勾选
Windows 10 SKD
等。
- 取消
安装后保留下载缓存
,其用于下一台新电脑可用缓存来进行vs安装,没必要。
登录
创建C++控制台应用
编写代码
编辑器使用
插入代码片段
右键 - 片段 - 插入片段 (Ctrl K+X)
- Ctrl+K 是 Visual Studio 中的一个“前缀键”(Prefix Key),用于触发一系列与代码相关的操作。按下 Ctrl+K 后,再按其他键可以执行不同的功能。
X
取自extend
,有扩展之义。
波浪线错误自动修正
鼠标悬浮或选中,Alt + Enter
创建类和添加源码文件
- 菜单 - 项目 - 添加类
- 解决方案资源管理器 - 头文件/源文件 - 添加 - 新建项
在
解决方案资源管理器
中,选中源文件,F2
,可进行重命名。
查找和替换
快速查找
菜单 - 编辑 - 查找和替换 - 快速查找 Ctrl + F
区分大小写、全字匹配、使用正则表达式、搜索范围:
查找全部:
在文件中查找
菜单 - 编辑 - 查找和替换 - 在文件中查找 Ctrl + Shift + F
快速替换
菜单 - 编辑 - 查找和替换 - 快速替换 Ctrl + H
保留大小写:
在文件中替换
菜单 - 编辑 - 查找和替换 - 在文件中替换 Ctrl + Shift + H
代码视图
类视图
菜单 - 视图 - 类视图
导航C++代码
查找所有引用
右键类名/函数名 - 查找所有引用 Ctrl + 左键类名/函数名
转到定义/声明
右键类名/函数名 - 转到定义/声明 Ctrl + 左键类名/函数名
查看调用层次结构
右键类名/函数名 - 查看调用层次结构
项目和解决方案
为解决方案添加新项目
右键解决方案 - 添加 - 新建项目
文件 | |
---|---|
.sln | 解决方案文件(配置多项目、项目启动项等) |
.vcxproj | 项目配置文件(平台[x64/x86]、配置[Debug/Release]相关) |
.vcxproj.filters | 项目目录 / 项目资源管理器 |
.vcxproj.user | 用户配置信息(主要为调试配置信息) |
//.sln
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.13.35806.99
MinimumVisualStudioVersion = 10.0.40219.1
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "vs_1", "vs_1.vcxproj", "{DA762D7E-3598-427A-8228-BB997E10C303}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "vs_2", "..\vs_2\vs_2.vcxproj", "{9C3ECFC4-899C-4E00-ADA7-DE61A26D179E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{DA762D7E-3598-427A-8228-BB997E10C303}.Debug|x64.ActiveCfg = Debug|x64
{DA762D7E-3598-427A-8228-BB997E10C303}.Debug|x64.Build.0 = Debug|x64
{DA762D7E-3598-427A-8228-BB997E10C303}.Debug|x86.ActiveCfg = Debug|Win32
{DA762D7E-3598-427A-8228-BB997E10C303}.Debug|x86.Build.0 = Debug|Win32
{DA762D7E-3598-427A-8228-BB997E10C303}.Release|x64.ActiveCfg = Release|x64
{DA762D7E-3598-427A-8228-BB997E10C303}.Release|x64.Build.0 = Release|x64
{DA762D7E-3598-427A-8228-BB997E10C303}.Release|x86.ActiveCfg = Release|Win32
{DA762D7E-3598-427A-8228-BB997E10C303}.Release|x86.Build.0 = Release|Win32
{9C3ECFC4-899C-4E00-ADA7-DE61A26D179E}.Debug|x64.ActiveCfg = Debug|x64
{9C3ECFC4-899C-4E00-ADA7-DE61A26D179E}.Debug|x64.Build.0 = Debug|x64
{9C3ECFC4-899C-4E00-ADA7-DE61A26D179E}.Debug|x86.ActiveCfg = Debug|Win32
{9C3ECFC4-899C-4E00-ADA7-DE61A26D179E}.Debug|x86.Build.0 = Debug|Win32
{9C3ECFC4-899C-4E00-ADA7-DE61A26D179E}.Release|x64.ActiveCfg = Release|x64
{9C3ECFC4-899C-4E00-ADA7-DE61A26D179E}.Release|x64.Build.0 = Release|x64
{9C3ECFC4-899C-4E00-ADA7-DE61A26D179E}.Release|x86.ActiveCfg = Release|Win32
{9C3ECFC4-899C-4E00-ADA7-DE61A26D179E}.Release|x86.Build.0 = Release|Win32
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {0AB6B4D1-C08A-4F7F-83D1-AD67F335DD3C}
EndGlobalSection
EndGlobal
一个
.sln
文件可对应多个项目,每个项目都有各自的.vcxproj
、.vcxproj.filters
、.vcxproj.user
文件。
项目资源管理
右键项目 - 添加 - 资源
可用于为项目添加可执行文件的图标、动态库的版本号等,可在
视图 - 其他窗口 - 资源视图
中查看加载的资源。
项目和解决方案基本设置
项目设置
右键项目 - 属性 - 配置属性 - 常规
输出目录和目标文件名
配置中,$(SolutionDir)
即是.sln
文件所在目录,$(ProjectDir)
即是相应.vcxproj
文件所在目录。当是对项目进行配置时,当前目录即为$(ProjectDir)
;当是对解决方案进行配置时,当前目录即为$(SolutionDir)
。
右键项目 - 属性 - 配置属性 - 链接器 - 常规
链接器中也可设置输出目录和目标文件名,二者区别在于:
常规输出目录是编译器生成的所有文件(包括中间文件和最终输出文件)的存放路径,它是一个全局设置,通常用于指定整个项目的输出文件夹。
链接器负责将编译后的对象文件(.obj)和其他库文件链接成最终的可执行文件(.exe)、动态链接库(.dll)或静态库(.lib)。链接器输出文件指定了最终生成的目标文件的具体名称和路径。
解决方案设置
设置启动项
右键解决方案 - 属性 - 通用属性 - 配置启动项目
当前选择(执行鼠标选中的项目)、单启动项目、多启动项目(可设置项目间启动次序、是否调试)
设置项目间依赖
右键解决方案 - 属性 - 通用属性 - 项目依赖项
批量生成
菜单 - 生成 - 批生成
清理解决方案
右键解决方案 - 清理解决方案
当选择“清理解决方案”时,Visual Studio 会执行以下操作:
- 删除所有由编译器生成的中间文件(如
.obj
文件)。 - 删除链接器生成的最终输出文件(如
.exe
、.dll
或.lib
文件)。 - 删除调试符号文件(如
.pdb
文件)。 - 删除其他与构建过程相关的临时文件。
清理解决方案的主要目的是:
- 确保干净的构建环境: 当代码或项目配置发生变化时,清理可以避免旧的编译结果干扰新构建的结果。
- 解决构建错误: 如果项目出现奇怪的编译错误或链接错误,清理解决方案可以帮助排除因缓存或旧文件导致的问题。
- 节省磁盘空间: 删除不必要的中间文件和输出文件,释放磁盘空间。
- 测试项目完整性: 清理后重新构建项目,可以验证项目是否能够正确地从头开始构建。
若清理解决方案后仍是出现了无法解释的编译错误或链接错误,可手动删除.vs
文件夹和平台文件夹。
.vs
文件夹是 Visual Studio 自动生成的隐藏文件夹,包含与解决方案相关的配置和缓存数据,例如扩展缓存、SQLite 数据库、预编译头文件缓存、设计时构建文件以及全局设置等。它的主要作用是提高开发效率和用户体验。如果遇到问题,可以安全地删除该文件夹,Visual Studio 会自动重新生成所需内容。
调试快速入门
设置工作目录和命令参数
右键项目 - 属性 - 配置属性 - 调试
int main(int argc, char* argv[])
{
std::cout << argc << std::endl; // 2
std::stringstream ss1;
ss1 << argv[0];
std::cout << ss1.str() << std::endl; // C:\Users\94517\Desktop\vs2022构建调试C++\Lcode\vs_1\x64\Debug\vs_1.exe
std::stringstream ss2;
if (argc > 1) {
ss2 << argv[1];
std::cout << ss2.str() << std::endl; // abc1d
}
}
工作目录与输出目录一致可简化资源加载路径,避免调试时的路径问题。如找不到外部资源文件(如配置文件、图片、数据文件等)、调试器无法正确加载符号文件(如 .pdb
文件)、程序运行时找不到依赖的动态链接库(.dll
文件)等。
设置断点并启动调试器
设置断点
鼠标左击代码左侧 F9
开始调试
工具栏 - 本地Windows调试器
F5 ,运行到断点处至程序结束- F10 、F11 单步进入程序第一行代码
调试中
- F5 继续运行到下一个断点,至程序结束。
- F10 逐过程,不进入函数。
- F11 逐语句,遇到有调试源码的函数则进入。
查看变量值
- 鼠标悬停在对象上显示其值
菜单 - 调试 - 窗口 - 自动窗口/局部变量
:分别查看当前行的变量和当前范围的局部变量。菜单 - 调试 - 窗口 - 监视
:设置监视,指定要关注的变量和表达式,当超出变量范围时变量会变灰。
查看函数被调用的顺序
菜单 - 调试 - 窗口 - 调用堆栈
提升开发效率
C++程序生成流程
x64 Native Tools Command Prompt for VS 2022
:用于在x64平台上编译x64平台程序。x86 Native Tools Command Prompt for VS 2022
:用于在x86平台上编译x32平台程序。x64_x86 Cross Tools Command Prompt for VS 2022
:用于在x64平台上交叉编译x32平台程序。
cl main.cpp //编译
cl .exe选项 | |
---|---|
/E | 将源代码通过预处理器处理后的内容输出到标准输出(即控制台),带行号。 |
/P | 与 /E 类似,但它会将预处理后的代码写入到一个文件中,而不是直接输出到屏幕,带行号。(.ii ) |
/EP | 将源代码通过预处理器处理后的内容输出到标准输出,不带行号。 |
> | 与/E 组合,重定向 |
/Fa | 保留汇编中间文件(.asm ) |
/c | 编译,不链接(.obj ) |
cl main.cpp /EP >main.ii #预处理,不带行号
cl main.cpp /Fa /c
- 头文件仅参与到预处理阶段,不参与后续的编译和汇编。
重构代码
重命名
#include <iostream>
static int gcount = 0;
int main()
{
gcount++;
std::cout << "gcount=" << gcount << std::endl;
std::cout << "test refactor" << std::endl;
return 0;
}
选中变量,右击重命名,或者两次 Ctrl + R
:
设置新名称,确定搜索范围,预览:
只替换变量,不替换字符串,点击应用。按住Ctrl+Z
可撤销此次操作。
创建修改声明/定义
从声明到定义
- 右键声明 - 快速操作和重构 - 创建定义
- 光标处于声明行 - Alt+Enter - 创建定义
从定义到声明
右键定义 - 快速操作和重构 - 创建声明
同时修改声明和定义的参数
右键声明 /定义 - 快速操作和重构 - 更改签名
提取函数
选中代码 - 右键 - 快速操作和重构 - 提取函数
移动定义位置
右键函数声明/定义 - 快速操作和重构 - 移动定义位置。
用于函数内联与非内联的转换。
自动实现所有纯虚函数
//.h
class Base
{
public:
virtual void start()=0;
virtual void end() = 0;
};
class Refactor:public Base
{
public:
Refactor(int a, double b);
};
右键派生类声明 - 快速操作和重构 - 实现所有纯虚方法:
//.cpp
#include "refactor.h"
Refactor::Refactor(int a, double b)
{
}
void Refactor::start()
{
}
void Refactor::end()
{
}
VS项目详细配置
常规配置
中间目录
中间目录是项目设置中的一个重要属性,它指定了编译过程中生成的中间文件存放的位置。这些中间文件包括但不限于对象文件(.obj)、预编译头文件(.pch)等。
平台工具集
平台工具集由C++编译器(cl.exe)、链接器(link.exe)以及C/C+标准库组成,与visual studio版本相关。
当用高版本VS打开由低版本VS创建的项目时,一般都是要改成对应版本的平台工具集。
所在路径:C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC
Windows SDK版本
Windows SDK是为开发适用于Windows操作系统的应用程序而设计的工具包。其主要用途是:
-
访问Windows API:
-
提供必要的头文件和库文件,使开发者能够调用Windows操作系统提供的各种API。
-
支持最新的Windows功能和特性,如文件系统操作、网络通信、图形绘制、多媒体处理等。
-
-
开发Windows应用程序
- 提供对Windows操作系统底层API的访问,使开发者能够利用系统功能,如文件系统操作、网络通信、图形绘制、多媒体处理等。
- 包含最新的Windows API和特性,帮助开发者利用最新的操作系统功能。
-
调试与诊断
- 提供各种调试工具和诊断工具,如性能分析器、内存泄漏检测器、事件查看器等,帮助开发者优化和调试他们的应用程序。
-
UI设计与开发
- 提供丰富的用户界面控件和设计工具,支持现代UI设计模式,如响应式布局、触摸交互等。
- 包含XAML和DirectX等技术,用于创建复杂的图形界面和高性能的游戏。
-
打包与分发
- 提供工具来打包和签名应用程序,以便在Microsoft Store或其他渠道进行分发。
- 支持应用程序的自动更新和版本管理。
所在路径:C:\Program Files (x86)\Windows Kits\10
C++语言标准
默认支持C++14,目前最高支持到C++20
编译配置
常规
头文件的查找路径
- 包含目录用于指定项目自身依赖的头文件路径,通常是项目内部的头文件或第三方库的头文件路径。
- 外部包含目录用于指定工具链或系统级依赖的头文件路径,例如 Windows SDK、平台工具集(如 MSVC)的路径。
多处理器编译
若有多个cpp文件需要编译,多处理器编译开启时,多个cpp会同时进行编译,提高编译速度。
预处理器
预处理器定义
#include <iostream>
#include<Windows.h>
int main()
{
#ifdef USE_DLL
std::cout << "haha" << std::endl;
#endif
std::cout << "Hello World!\n";
}
运行库
Visual Studio中的“C运行库”即“C运行时库”(英文全称为C Runtime Library,简称CRT),这是微软Visual Studio中提供的一个核心库,用于支持C和C++程序的基本运行时功能。
MT:C运行库静态链接,MTd为debug版本;MD:C运行库动态链接,MDd为debug版本。
查看依赖:dumpbin /dependents test_config.exe
Microsoft ® COFF/PE Dumper Version 14.43.34808.0
Copyright © Microsoft Corporation. All rights reserved.Dump of file test_config.exe
File Type: EXECUTABLE IMAGE
Image has the following dependencies:
MSVCP140D.dll
VCRUNTIME140_1D.dll
VCRUNTIME140D.dll
ucrtbased.dll
KERNEL32.dllSummary
1000 .00cfg
1000 .data
2000 .idata
1000 .msvcjmc
3000 .pdata
3000 .rdata
1000 .reloc
1000 .rsrc
9000 .text
10000 .textbss
修改为MTd后只依赖了一个系统内核:
Microsoft ® COFF/PE Dumper Version 14.43.34808.0
Copyright © Microsoft Corporation. All rights reserved.Dump of file test_config.exe
File Type: EXECUTABLE IMAGE
Image has the following dependencies:
KERNEL32.dll
Summary
1000 .00cfg
6000 .data
2000 .idata
1000 .msvcjmc
16000 .pdata
50000 .rdata
4000 .reloc
1000 .rsrc1D0000 .text
DC000 .textbss
链接器
常规
输出文件
输出文件的路径
输出文件:$(OutDir)$(TargetName)$(TargetExt)
常规-输出目录 + 常规-目标文件名 + 扩展名(exe/lib/dll)
附加库目录
查找lib库的路径
输入
附加依赖项
指定具体的lib文件
例如设置ws2_32.lib,链接器会先去附加库目录下寻找,然后再到VC++库目录下去寻找。
调试
- 不管是debug配置还是release配置,都是可以生成调试信息的,调试信息存储在pdb中。pdb核心是将源码与二进制机器码做了个对应关系。
- release相对于debug在编译时会有更多的优化。
- linux中调试信息是直接存储在可执行程序当中的。
系统
控制台,以main函数作为入口函数,默认调用main函数:
窗口,以WinMain函数作为入口函数,默认调用WinMain函数:
WINAPI
宏保证了WinMain
函数使用正确的调用约定 (__stdcall
)。这种调用约定是 Windows API 标准的一部分,确保了函数可以被操作系统正确调用。使用WINAPI
可以避免由于调用约定不匹配导致的堆栈问题和其他潜在错误。
构建静态库和动态库
静态库
创建
解决方案 - 添加 - 新建项目
//slib.h
#pragma once
class SLib
{
public:
SLib();
~SLib();
};
//slib.cpp
#include "slib.h"
#include<iostream>
SLib::SLib()
{
std::cout << "Create SLib" << std::endl;
}
SLib::~SLib()
{
std::cout << "Drop SLib" << std::endl;
}
配置
文档管理程序中的设定与链接器的设定效果是一致的。
输出目录
/* 目录结构
Project1
src
slib
slib.vcxproj
lib
Project1.sln
Project1.vcxproj
*/
调用
-
项目结构
/* Project1 src slib slib.vcxproj main.cpp lib Project1.sln Project1.vcxproj */
-
解决方案配置启动项目
-
解决方案配置项目依赖项
-
指定头文件查找路径
-
链接库
-
启动项目
//main.cpp #include<iostream> #include"slib.h" int main() { { SLib slib; } std::cout << "test SLib" << std::endl; } /* Create SLib Drop SLib test SLib */
静态库和调用程序的运行时库不一致时可能会产生冲突,报RuntimeLibrary
不匹配错误。
动态库
好处
- 动态链接可节省内存,多进程共享 DLL。
- 维护、安全修复和升级会更容易。如更新 DLL 时,不需要重新编译或重新链接使用它们的应用程序。
- 显式链接在运行时发现和加载 DLL。如无需重新生成或重新部署就可将新功能添加到你的应用的应用程序扩展。
- 对于用不同编程语言编写的应用程序。如python 调用 c++
创建
//dlib.h
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 DLIB_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// DLIB_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef DLIB_EXPORTS
#define DLIB_API __declspec(dllexport)
#else
#define DLIB_API __declspec(dllimport)
#endif
// 此类是从 dll 导出的
class DLIB_API DLib {
public:
DLib();
~DLib();
};
extern DLIB_API int ndlib;
DLIB_API int fndlib(void);
//dlib.cpp
// dlib.cpp : 定义 DLL 的导出函数。
#include "dlib.h"
#include<iostream>
// 这是导出变量的一个示例
DLIB_API int ndlib=0;
// 这是导出函数的一个示例。
DLIB_API int fndlib(void)
{
return 0;
}
// 这是已导出类的构造函数。
DLib::DLib()
{
std::cout << "Create DLib" << std::endl;
return;
}
DLib::~DLib()
{
std::cout << "Drop DLib" << std::endl;
return;
}
删除framework.h
和dllmain.cpp
,dllmain.cpp
里面定义了入口程序,考虑跨平台,linux没有这个东西,索性删除,我们通过其他方式进行入口控制。
配置
输出目录
动态链接库输出*.dll
、*.lib
、*.pdb
三个文件。
*.dll
、*.pdb
输出目录:配置属性 - 常规 - 输出目录
和启动项的输出目录一致。
*.lib
输出目录:链接器 - 高级 - 导入库
一般放在lib库中供开发者使用。
静态库中lib放的是函数代码,动态库中lib放的是dll中函数的地址。
调用
-
项目结构
/* Project1 src slib slib.vcxproj dlib dlib.vcxproj main.cpp lib slib.lib dlib.lib bin dlib.dll Project1.exe Project1.sln Project1.vcxproj */
-
解决方案配置启动项目
-
解决方案配置项目依赖项
-
指定头文件查找路径
-
链接库(dll的lib)
-
启动项目
//main.cpp #include<iostream> #include"slib.h" #include"dlib.h" int main() { { SLib slib; } std::cout << "test SLib" << std::endl; { DLib dlib; } std::cout << "test DLib" << std::endl; } /* Create SLib Drop SLib test SLib Create DLib Drop DLib test DLib */
符号说明
//dlib.h
#ifdef DLIB_EXPORTS
#define DLIB_API __declspec(dllexport)
#else
#define DLIB_API __declspec(dllimport)
#endif
// 此类是从 dll 导出的
class DLIB_API DLib {
public:
DLib();
~DLib();
};
extern DLIB_API int ndlib; //定义变量,仿写
DLIB_API int fndlib(void); //定义函数,仿写
dllexport
-
只有加了
__declspec(dllexport)
才会导出dll对应的lib文件(供开发者直接使用dll中的函数),不加则只生成dll文件(只能通过函数签名去访问dll中的函数)。 -
dlib.h
头文件被多次调用:动态库自己调用(需要做dllexport导出lib文件);主项目调用(不需要做dllexport导出lib文件)。如何避免主项目中导出lib,可使用宏DLIB_EXPORTS
。若定义DLIB_EXPORTS
则dllexport,否则dllimport.//dlib.h #ifdef DLIB_EXPORTS #define DLIB_API __declspec(dllexport) #else #define DLIB_API __declspec(dllimport) #endif class DLIB_API DLib { public: DLib(); ~DLib(); }; extern DLIB_API int ndlib; DLIB_API int fndlib(void);
dllimport
__declspec(dllimport)
是可选的,但如果你使用了编译器会生成更高效的代码。
//dlib.h
#ifdef DLIB_EXPORTS
#define DLIB_API __declspec(dllexport)
#else
#define DLIB_API __declspec(dllimport)
#endif
extern "C" DLIB_API void DFunc();
//dlib.cpp
#include "dlib.h"
#include<iostream>
void DFunc()
{
std::cout << "in" << std::endl;
}
//main.cpp
#include <iostream>
#include <dlib.h>
extern "C" void DFunc();
int main()
{
DFunc();
//反编译
//00007FF621D2177B call DFunc (07FF621D211CCh) 呼叫,找不到,跳转跳转
//00007FF621D211CC jmp DFunc (07FF621D21797h)
//00007FF621D21797 jmp qword ptr[__imp_DFunc(07FF621D301F8h)] 跳转到dll了
return 0;
}
//main.cpp
#include <iostream>
#include <dlib.h>
extern "C" __declspec(dllimport) void DFunc();
int main()
{
DFunc();
//反编译
//00007FF79061177B call qword ptr [__imp_DFunc (07FF7906201F8h)] 已知在dll中,直接去
return 0;
}
调试代码
略
补充
快捷键 | |
---|---|
Ctrl K+O | 头文件源文件切换 |
Ctrl K+C | 注释 |
Ctrl K+U | 取消注释 |
Windows平台文件名大小写不敏感,Linux平台文件名大小写敏感,未避免冲突,规范c++文件名全小写。
头文件的引用和名称空间的using尽量放在.cpp
文件中,放在.h
文件中可能会不可控,.h
文件可能被任意的文件所引用。
总结:附加包含目录、附加依赖项、VC++ 目录(包含目录、库目录)
- 附加包含目录
- 位置:项目属性 -> 配置属性 -> C/C++ -> 常规 -> 附加包含目录
- 作用:指定编译器在编译 C/C++ 源代码时查找头文件的位置。这里添加的路径告诉编译器去哪些目录找 .h 文件。
- 设置方式:可以添加相对路径(相对于项目目录)或绝对路径。
- 附加依赖项
- 位置:项目属性 -> 配置属性 -> 链接器 -> 输入 -> 附加依赖项
- 作用:指定链接器在链接阶段需要链接的库文件(例如 .lib 文件)。这里添加的库文件告诉链接器在链接生成可执行文件或库时需要包含哪些外部库。
- 设置方式:输入库文件名,可以是相对路径或库文件的名称。如果有多个文件,用分号(;)分隔。
- VC++ 目录
- 位置:项目属性 -> 配置属性 -> VC++ 目录
- 作用:定义 Visual Studio 用于查找包括头文件、库文件和其他相关文件的目录。
- 包含目录:与“附加包含目录”类似,指定编译器查找头文件的位置。
- 库目录:指定链接器查找库文件的位置。这里的路径告诉链接器去哪些目录找 .lib 文件。
- 设置方式:可以设置不同的配置(如调试、发布)的目录路径。
- 区别与应用
- 附加包含目录 和 VC++ 目录中的包含目录都是用于告诉编译器头文件的搜索路径。这两个设置通常功能相似,但在一些项目设置中,VC++ 目录 是全局性的,适用于所有项目,而“附加包含目录”是项目级的设置,特定于当前项目。
- 附加依赖项是在链接阶段用来指定库文件的。链接器需要知道哪些库文件应该被链接到最终的可执行文件或库中。这与编译阶段的头文件查找无关。
总结:VS中的MD与MT运行库
对比
特性 | MD (Multi-threaded DLL) | MT (Multi-threaded Static) |
---|---|---|
链接方式 | 动态链接到外部的CRT DLL(如msvcrXXX.dll ) | 静态链接,CRT代码直接嵌入可执行文件 |
文件大小 | 可执行文件较小 | 可执行文件较大(包含完整的CRT代码) |
部署依赖 | 需目标系统安装对应版本的CRT DLL(通过安装包或系统更新) | 无需额外依赖,单文件即可运行 |
共享性 | 多个应用共享同一份CRT DLL(需版本一致) | 无共享,每个应用自带CRT副本 |
适用场景 | 减少磁盘占用,适用于多模块协作项目 | 简化部署,适用于独立工具或环境受限的场景 |
运行时库的核心作用
CRT(C/C++ Runtime Library)为程序提供底层支持,包括:
- 标准库函数:如
printf
、malloc
、文件操作等。 - 启动/终止代码:初始化全局变量、调用构造函数等。
- 异常处理与RTTI:支持C++异常和类型识别(需一致启用)。
- 线程本地存储(TLS):管理多线程环境下的局部存储。
混合使用MD和MT的风险与注意事项
-
内存管理冲突
-
若主程序(MT)与DLL(MD)使用不同CRT实例,内存分配与释放必须严格隔离。例如:
// 危险操作:DLL用MD的`new`分配,主程序用MT的`delete`释放 void* p = DLL_alloc_memory(); // 在DLL中通过MD的堆分配 delete p; // 在主程序MT的堆释放 → 可能崩溃
-
解决方案:跨模块接口统一内存管理(如DLL提供
Create()
/Destroy()
函数)。
-
-
全局状态不共享
-
不同CRT实例的文件句柄、环境变量、静态变量相互独立。例如:
// DLL(MD)中设置环境变量 _putenv("KEY=Value"); // 主程序(MT)无法读取此变量 const char* value = getenv("KEY"); // 返回nullptr
-
-
C++对象与异常传递
- 避免跨模块传递C++对象:不同CRT可能导致对象布局或异常处理机制不一致。
- 纯C接口更安全:使用
extern "C"
定义函数,避免名称修饰(Name Mangling)问题。
-
版本一致性
- 所有模块需使用相同VS版本编译:不同VS版本的CRT二进制不兼容(如VS2019的
MD
与VS2022的MD
可能冲突)。
- 所有模块需使用相同VS版本编译:不同VS版本的CRT二进制不兼容(如VS2019的
最佳实践
- 统一编译设置:所有项目模块(主程序、DLL、Lib)使用相同的运行时库设置(MD或MT)。
- 隔离资源管理:若必须混合使用,确保内存、文件句柄等资源在同一模块内分配和释放。
- 谨慎设计接口:跨模块接口优先使用纯C风格函数,并通过
Handle
或Context
封装对象操作。 - 部署验证:若使用MD,需在安装包中捆绑对应版本的VC Redistributable。
MD与MT的选择需权衡部署便利性与性能需求。混合使用二者虽技术上可行,但需严格遵循资源隔离原则,否则可能导致难以调试的崩溃或数据损坏。强烈建议全项目统一MD/MT设置,这是确保稳定性的最简路径。
DLL中的extern "C"
//.h
#ifdef DLIB_EXPORTS
#define DLIB_API __declspec(dllexport)
#else
#define DLIB_API __declspec(dllimport)
#endif
class DLIB_API DLib {
public:
DLib();
~DLib();
};
// 变量声明:必须加 extern
extern DLIB_API int ndlib;
// 函数声明:不需要加 extern
DLIB_API int fndlib(void);
extern "C" DLIB_API int fndlib(void); //extern "C++"是默认行为
其是 C++ 中一种常见的用法,用于控制函数在编译时的符号名(symbol name)。
作用
使用extern "C"
的目的是禁用 C++ 的名称改编(Name Mangling),在 C++ 中,为了支持函数重载等功能,编译器会对函数名进行“改编”(mangling),比如:
int add(int a, int b); // 可能变成 _Z3addii
float add(float a, float b); // 可能变成 _Z3addff
而 C 语言没有这些特性,函数名就是它本身。使用 extern "C"
后,告诉编译器:“把这个函数当作 C 函数来处理”,即不进行名称改编。
使用场景
常见于 DLL 或共享库中导出函数,供 C 或其他语言调用。
这样做的好处是:
- 便于从 C 程序中调用
- 方便通过 LoadLibrary / GetProcAddress 动态加载调用
- 防止 C++ 编译器对函数名做 mangling,导致链接失败
注意事项
extern "C"
不能用于重载函数
extern "C" void foo(int); // OK
extern "C" void foo(float); // ❌ 错误!C 不支持重载
extern "C"
通常只包裹函数声明,而不是类或变量。
虽然可以对变量也使用 extern "C"
,但不太常见,主要用于全局变量导出:
extern "C" DLIB_API int globalVar;
但这并不影响变量本身的访问方式,主要用于 DLL 导入/导出。
类成员函数不能直接使用 extern "C"
因为类成员函数隐含了 this 指针和命名空间等信息,不能被当作 C 函数处理。
总结
场景 | 是否加 extern "C" | 说明 |
---|---|---|
导出函数供 C 调用 | ✅ 必须加 | 防止 C++ 名称改编 |
导出函数供 C++ 调用 | ❌ 一般不加 | 支持重载、类方法等 C++ 特性 |
导出变量 | 可选 | 如果要保证变量名不变,可加 |
成员函数 | ❌ 不可用 | 不符合 C 的语法规范 |
生成了dll后是否还需要头文件吗?
✅ 简短回答:
是的,生成 DLL 后,在使用这个 DLL 的项目中,仍然需要头文件(.h 文件)。
🧠 为什么需要头文件?
DLL(动态链接库)只是包含了函数和变量的实现(定义),而头文件则提供了这些函数和变量的声明(接口)。
C++ 编译器在编译一个程序时,必须知道它调用的函数长什么样、参数是什么、返回值是什么。这些信息都来自头文件。
📌 具体说明
假设你有一个 DLL 项目,里面导出了一个函数:
// dllmain.cpp
#include "MyDll.h"
DLIB_API int fndlib(void) {
return 42;
}
对应的头文件:
// MyDll.h
#pragma once
#ifdef MYDLL_EXPORTS
#define DLIB_API __declspec(dllexport)
#else
#define DLIB_API __declspec(dllimport)
#endif
DLIB_API int fndlib(void);
当你在另一个项目(比如控制台应用)中使用这个 DLL 时:
// main.cpp
#include "MyDll.h" // 必须包含头文件才能知道 fndlib 是什么样子
int main() {
int result = fndlib(); // 调用 DLL 中的函数
return 0;
}
即使你已经把 MyDll.dll
拷贝到了可执行文件目录,并且配置好了 .lib
导入库,你也必须包含头文件,否则编译器不知道怎么调用这个函数。
🧱 DLL + 头文件 + Lib 文件的关系
文件类型 | 作用 |
---|---|
.dll | 包含函数/变量的实际代码,运行时加载 |
.lib (导入库) | 静态链接时告诉链接器函数在哪个 DLL 里 |
.h (头文件) | 告诉编译器函数/变量的声明(接口),以便正确调用 |
✅ 使用 DLL 的完整步骤
-
编写 DLL 项目
- 实现函数
- 导出符号(使用
__declspec(dllexport)
) - 提供
.h
头文件
-
编译生成:
- 得到:
MyDll.dll
和MyDll.lib
- 得到:
-
在客户端项目中使用:
- 引入
.h
文件(让编译器知道接口) - 引入
.lib
文件(让链接器知道去哪找实现) - 把
.dll
放在可执行文件路径下(运行时加载)
- 引入
🚫 如果不提供头文件会怎样?
- 编译器无法识别你调用的函数原型。
- 你必须手动声明所有函数,例如:
extern "C" __declspec(dllimport) int fndlib(void);
int main() {
int result = fndlib();
return 0;
}
虽然也能工作,但非常不方便,也不安全。
💡 小贴士:发布 DLL 时应提供哪些内容?
如果你是一个 DLL 的开发者,建议提供以下内容:
MyDll.dll
(动态库)MyDll.lib
(导入库,用于静态链接)MyDll.h
(头文件,包含函数/变量声明)- 可选:文档说明、版本信息等
✅ 总结
问题 | 回答 |
---|---|
生成 DLL 后还需要头文件吗? | ✅ 是的,必须要有头文件才能正确使用 DLL |
头文件的作用是什么? | 提供函数和变量的声明(接口) |
没有头文件可以使用 DLL 吗? | ❌ 不推荐,除非你手动声明每个函数 |
客户端使用 DLL 需要什么? | .dll + .lib + .h |
如你是做插件系统、跨模块通信、DLL 注入、Python 调用 C++ 等高级用途,也可以进一步讨论具体方案 😄