从 Visual Studio 2015 开始:2015、2017、2019、2022... 构建的应用与运行时库是二进制兼容的(注意是允许向前兼容,不允许向后兼容,即如:VS2019编译的库给VS2017去用可能会失败)
https://learn.microsoft.com/en-us/lifecycle/faq/visual-c-faq
然后还有个针对C/C++运行时一个重要的事实,无论是VS2015还是VS2017或更高版本的VC编译出的程序,它们均共享同一主版本运行时:vcruntime140.dll、ucrtbased.dll(其实还有msvcp140.dll)

上面这里是由VS2017编译的debug版你所看到的,根据查壳识别查出的编译器信息仅是MSVC14.0(事实上这个就是VS2015),而链接器信息是14.16这说明它是14.1x属于VS2017的范畴,而它依赖导入的是vcruntime都是140的版本(这个你就算在VS2026上编译也是一样的依赖)
当VC主项目被编译为release版后

好像把ucrtbased.dll拆分为了各个 api-ms-win-crt-... 等一堆补丁?
在 VS2013及 之前,每个编译器版本都有一套完整 CRT:
从 VS2015(VC140) 开始,微软做了三件大改:
更具体的完整C/C++体系分层应该是
其中vcruntime(目前几乎基础功能版硬编码写死的DLL名称是vcruntime140.dll,待会再说说后期版本的扩展方案)它负责:
总之就是一系列的C++运行时底层库(注意是C++程序运行的底层库)的代码 或是 换种说法是相关C++编译器给到应用程序运行时的内部通用实现(注意STL通常都是以模板代码为主故它大多数实现总是源码形式进行分发,不属于vcruntime的实现范畴,且也不是下面即将要说的msvcp)
msvcp(C++标准运行时库实现),虽然绝大多数标准库是模板源码实现,不存在于任何DLL中。但msvcp140.dll依然存在,那它实现的是一部分就是“非模板 或 需要统一”的运行时组件,下面列举一些C++标准库中非模板组件
这些必须提供稳定ABI、统一全局状态、跨模块共享、因此它们必须集中在单独可靠的DLL中,否则:EXE new/delete 与 DLL 不一致、iostream static 对象重复、locale 全局状态分裂
Universal CRT(UCRT,通用C运行时库,在debug版下基础版导入的DLL名称是ucrtbased.dll——结尾有个小d表示它是debug版)
它负责:
在release版下:
现在系统针对C运行时库提升为 Windows 组件,不再属于VC编译器的私有运行时(即对C运行时这部分的更新也更多由 Windows Update 管理,而不再完全跟着某个VS版本走),
参考:https://learn.microsoft.com/en-us/cpp/windows/universal-crt-deployment?view=msvc-170
并进行了对C库更细致的一系列按功能拆分

在说这个之前还要再补充一个背景知识:Windows API sets
参考:https://learn.microsoft.com/en-us/windows/win32/apiindex/windows-apisets
api-ms-win-xxx-* 这类名字属于 Windows API set,
微软官方明确说过:API set 本质是一个套壳DLL;加载器会根据系统里的 API set schema(API设置的模式映射),把这个契约名在运行时重定向到真正承载实现的 HostDLL。也就是说,它们更像“路由表/壳”,目的是让同一个二进制在不同 Windows 版本、不同设备上仍能按功能正确绑定到实现,即如果没那个功能的或对应功能的ABI不兼容,则直接报错该套壳DLL没有找到(感觉上更容易排错吧)。
总结一下微软在 UCRT 中做了三件事:
1. 把 CRT 从 VS 中剥离 → 成为 Windows 组件
2. 引入 API-Set 机制 → 提供外层稳定 ABI
3. 允许不同 Windows 版本自己对真实DLL内部的重新实现(容易即时发现C库漏洞,而直接打上系统级补丁的更新)
因此:对于应用程序依赖→ api-ms-win-crt-*.dll(外壳基本ABI稳定),系统决定→ 实际加载 ucrtbase.dll(当然也可能不加载它,或是其他实现了套壳需求的DLL——取决于系统自带的映射)
附有个三方网站记载了这些别名外壳所对应的系统真实DLL,https://apisets.info/apisetschemas/10.0.19041.1-amd64/?utm_source=chatgpt.com
然后对于非基础版功能(基础版是VS2015,而VS2017及之后的新增功能)它的命名采用了_1、_2等后缀作为版本迭代
注意是按功能进行选择式的迭代绑定,
也就是说在 VS2017 构建的程序中同时依赖 msvcp140.dll 与 msvcp140_1.dll 是完全正常且常见的情况(在VS2013以前每个VC版本库都是完整的CRT,直到VS2015开始做这种拆分的兼容策略)。
这并不是异常或冲突,而是 MSVC 为了保持旧版功能依然二进制兼容而设计的运行时扩展机制,
下面C++代码你在VS2017(需开启C++17标准)编译后就会看到DLL导入会同时存在msvcp140.dll 与 msvcp140_1.dll
#include <iostream>
#include <string>
#include <vector>
#include <memory_resource> // C++17: 触发 msvcp140_1.dll 的关键头文件
#include <new>
/**
* VS2015 开始,微软将 CRT 拆分为 vcruntime (底层), msvcp (STL), ucrt (C 规范).
* 当后期 VS2017/2019 需要增加新的 C++ 标准库非模板实现时,
* 为了不破坏原有 msvcp140.dll 的二进制兼容性(ABI),
* 采用了“附加版本DLL”策略,例如 msvcp140_1.dll, msvcp140_2.dll 等。
*/
int main()
{
// 1. 触发 msvcp140.dll:
// std::cout, std::string 的基础非模板导出符号基本都在 msvcp140.dll 中。
std::string base_msg = "Hello, I use msvcp140.dll";
std::cout << base_msg << std::endl;
// 2. 触发 msvcp140_1.dll:
// std::pmr (Polymorphic Memory Resources) 是在 VS2017 15.6 中正式引入的。
// 它的大部分实现(如 get_default_resource())位于 msvcp140_1.dll。
// 注意:项目需要开启 /std:c++17 编译选项。
std::pmr::memory_resource* res = std::pmr::get_default_resource();
std::pmr::vector<int> pmr_vec({ 10, 20, 30 }, res);
std::cout << "PMR Vector initial element: " << pmr_vec[0]
<< " (This triggered msvcp140_1.dll dependency)\n";
return 0;
}

对于VS2015及其之后的版本它们release版的VC静态库有如下拆分:
C runtime → libcmt.lib
C++ runtime → libcpmt.lib
compiler runtime → libvcruntime.lib
UCRT → libucrt.lib
我现在有个疑问是这样的:libucrt.lib这个是真的静态库吗(它不会只是系统级那个UCRT那个dll的导入库?),这个不应该只是win32层面的统一动态C运行时吗?那libcmt.lib这个是否包含所有C的静态运行时代码?

其实是这样的,微软为了拆分ISO标准的C语言部分则尽可能放到了libucrt.lib去实现,而libcmt.lib更多是VC自己底层的Win32程序初始化管理模型以及专属的Microsoft扩展,只有这两的结合才是等价于ucrtbase.dll。

