将你的C++库发布到NuGet全攻略

在C++领域,目前最流行的库管理工具是vcpkg。但是,Visual Studio 对vcpkg并不原生支持,需要进行繁琐的文件和命令行配置,不同项目甚至还依赖不同vcpkg版本,远不如NuGet简明易懂。但是,NuGet主要是面向.NET的库管理工具。尽管支持C++,但提供的文档非常稀少晦涩。本文会为你提供完整新手入门攻略。实事求是地说,将C++库发布到NuGet未必比vcpkg简便;它提供的方便主要是面向库用户而非库开发者。如果你愿意为了用户的方便宁可自己麻烦一些,那NuGet就是适合你的选择!

建立NuGet目录结构

发布到NuGet的库必须遵守特定的目录结构。此处用"\"表示库的根目录。根目录下应包含以下内容:

\库名.nuspec

nuspec文件的内容格式官网有详细文档,此处不赘述,只提供一个示例:

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2011/10/nuspec.xsd">
	<metadata>
		<id>MATLAB.MexTools</id>
		<version>7.1.1</version>
		<description>
			使用本工具快速生成 MATLAB C++ MEX 数据API文件函数.mexw64。你只需要在 Visual Studio 中新建一个C++动态链接库项目,添加此NuGet程序包,定义初始化、执行、清理三个函数,就可以生成出合法的MEX文件函数。此外,本工具还额外提供强大的异常处理、C++/MATLAB数据交换和对象生命周期管理功能。
			支持的平台:x64。支持的平台工具集:v143 v145。依赖 Visual C++ 可再发行程序包。
		</description>
		<authors>埃博拉酱</authors>
		<projectUrl>https://github.com/Ebola-Chan-bot/MexTools</projectUrl>
		<tags>native Static v143 v145 x64</tags>
		<icon>图标.png</icon>
		<license type="expression">MIT</license>
		<summary>使用本工具快速生成 MATLAB C++ MEX 数据API文件函数</summary>
		<releaseNotes>
			修复WindowsErrorMessage并不使用输入的错误码的问题
		</releaseNotes>
		<readme>README.md</readme>
		<dependencies>
			<dependency id="native.magic_enum" version="0.9.5" />
		</dependencies>
	</metadata>
	<!-- 不能指定files,否则只会包含files中的文件 -->
</package>

id就是你的库名,应该与nuspec文件名匹配。对于tags,约定俗成地,对于C++库应该至少添加一个native标签,但并非强制。icon和readme路径可以使用相对路径,相对于库根目录。

\图标.png

这个图标文件可以有任意名称,只要在 nuspec icon 中指定即可。这个图标将在用户在NuGet上浏览到你的库时显示。

\README.md

跟图标类似,可以有任意名称,只要在 nuspec readme 中指定即可。这个文件将在用户使用 Visual Studio 安装了你的库之后自动打开。但是,默认不会渲染MarkDown格式,所以也可以指定为txt纯文本。

\build\native

此目录下包含你库的主体内容,无论是头文件、静态库、动态库还是其它数据。这些文件的组织没有强制要求,你可以按照你的喜好任意放置。下面的targets文件将指导MSBuild该如何将你的文件加入编译过程。

\build\native\库名.targets

该文件将被用户的vcxproj项目文件引用,并在编译时指导编译器如何将你的库加入编译。示例:

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
	<ItemDefinitionGroup>
		<ClCompile>
			<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
			<PreprocessorDefinitions>MARIADB_STATIC_LINK;%(PreprocessorDefinitions)</PreprocessorDefinitions>
		</ClCompile>
		<ResourceCompile>
			<AdditionalIncludeDirectories>$(MSBuildThisFileDirectory);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
			<PreprocessorDefinitions>MARIADB_STATIC_LINK;%(PreprocessorDefinitions)</PreprocessorDefinitions>
		</ResourceCompile>
	</ItemDefinitionGroup>
	<ItemDefinitionGroup>
		<Link>
			<AdditionalDependencies>Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
		</Link>
	</ItemDefinitionGroup>
	<Target Name="设置链接输入" BeforeTargets="Link">
		<PropertyGroup>
			<!--ItemGroup不能直接捕获ClCompile的属性,因此需要在这里传递-->
			<RuntimeLibrary>%(ClCompile.RuntimeLibrary)</RuntimeLibrary>
		</PropertyGroup>
		<ItemGroup>
			<Link Update="@(Link)">
				<AdditionalDependencies>$(MSBuildThisFileDirectory)MariaDB_Connector.$(RuntimeLibrary).$(PlatformToolsetVersion).lib;%(Link.AdditionalDependencies)</AdditionalDependencies>
			</Link>
		</ItemGroup>
	</Target>
</Project>

稍后会在讲解MSBuild流程时解释此文件的内容和意义。

二进制兼容性问题

如果你的库以纯源码形式提供,可以跳过本节。如果你的库是预先编译的,由于C++没有强制规定二进制接口,不同编译器和不同版本之间可能会产生不兼容问题。对于MSVC来说,你需要考虑如下兼容性问题:

运行库

此设置影响你的目标用户是否需要额外安装Microsoft Visual C++ 可再发行程序包,以及是否能以调试模式链接你的库。在项目配置属性\C/C++\代码生成\运行库中提供4种依赖方式:

  • 多线程(/MT),使用此方式意味着用户无需安装可再发行程序包,所有必要的运行库代码一并编译到文件中,这会导致生成的文件较大,但性能是最高的。此选项不支持调试。
  • 多线程调试(/MTd),类似于MT,但支持调试,但生成的代码性能较低。
  • 多线程DLL(/MD),用户必须安装可再发行程序包,因为运行库代码未编译到文件中,需要运行时从可再发行程序包查找载入。这样生成的文件较小,但存在动态载入库的开销。此选项不支持调试。
  • 多线程调试DLL(/MDd),类似于MD,但支持调试,生成的代码性能最低。

为了支持尽可能多样的用户需求,你可能需要为所有4种方式各编译一份二进制文件。

使用调试库

此设置影响你的库是否支持调试。在项目配置属性\高级\使用调试库中设置。此设置必须与运行库匹配:如果支持调试,只能选MTd或MDd;否则只能选MT或MD。

平台工具集

在项目配置属性\常规\平台工具集中设置。如果用户需要安装可再发行程序包,此设置将影响用户需要安装的版本。平台工具集和可再发行程序包版本的对应关系没有明确的文档列出或不会及时更新,因此最好能测试一下。即使不需要安装,用户的项目也必须使用和你相同的平台工具集。

全程序优化

在项目配置属性\高级\全程序优化中设置。如果使用全程序优化,可以将性能优化到极限,但用户必须使用和你的编译器精确相同的版本,包括小版本号。因此通常不会使用此设置。

库体积膨胀的权衡

使用你的库的用户只能使用和你的二进制文件相同的设置,因此你可能需要为所有可能的设置各提供一个二进制文件,这样才能最大化允许用户自由设置他自己的项目。当然,这会导致你的库体积膨胀数倍。具体来说,运行库有4种选项,因此你至少要提供4个版本;如果你还需要支持更多平台工具集,还要倍乘。如果你不希望库体积太大,可以考虑如下权衡方案:

  • 直接发布源码。此方法的问题主要在于会增加编译时间。
  • 不支持某些配置。此方法可能导致潜在的用户流失,不是所有用户都会愿意为了你的库去修改他全局的编译设置。
  • 设计平凡的C风格二进制接口。C++的非平凡对象跨模块传递(包括函数调用和异常抛出)都可能造成内存管理问题。一种较为复杂的做法是将二进制接口全部设置为平凡的C风格,非平凡的、C++风格的接口在头文件中提供转换功能,这样可以提高单个lib的兼容性,而无需提供多个版本。但是,这种做法将提高设计复杂性,并产生接口转换开销。

MSBuild与targets

之前略过了targets文件内容的详解。targets文件不会影响你自己的生成流程,而是会嵌入到用户的MSBuild流程中。通常来说,你需要在targets文件中处理以下问题:

外部包含目录

用户的MSBuild需要知道你的头文件放在哪里,这样用户代码中才能使用简短的相对路径包含你的头文件。在targets文件中,你需要在ClCompile和ResourceCompile项中添加AdditionalIncludeDirectories,通常需要这样写:

XML 复制代码
<ItemDefinitionGroup>
	<ClCompile>
		<AdditionalIncludeDirectories>你的包含目录;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
	</ClCompile>
	<ResourceCompile>
		<AdditionalIncludeDirectories>你的包含目录;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
	</ResourceCompile>
</ItemDefinitionGroup>

如果你的库中没有需要编译的资源,也可以不写ResourceCompile节点。如果有多个包含目录,用分号分隔。如果你希望使用相对路径,可以使用$(MSBuildThisFileDirectory)宏,将会被替换为你的targets文件所在的目录。此处指定的目录并不限制必须是你的库内,也可以指定任何绝对路径。例如:

XML 复制代码
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
	<PropertyGroup>
		
		<MexTools_MatlabExtern>$([System.IO.Directory]::GetDirectories("$(ProgramFiles)\MATLAB").GetValue($([MSBuild]::Subtract($([System.IO.Directory]::GetDirectories("$(ProgramFiles)\MATLAB").Length), 1))))\extern\</MexTools_MatlabExtern>
		<!--这句使用了.GetValue()而不是方括号索引,因为方括号索引中不能调用属性函数-->
		
		<MexTools_MatlabLib>$(MexTools_MatlabExtern)lib\win64\microsoft\</MexTools_MatlabLib>
		<!--必须创建新属性,不能在使用后覆盖MatlabExtern。因为XML不保证运行顺序。-->
		
		<TargetExt>.mexw64</TargetExt>
		<MexTools_Configuration>Release</MexTools_Configuration>
		<MexTools_Configuration Condition="$(UseDebugLibraries)">Debug</MexTools_Configuration>
	</PropertyGroup>
	<ItemDefinitionGroup>
		<ClCompile>
			<AdditionalIncludeDirectories>$(MexTools_MatlabExtern)include;$(MSBuildThisFileDirectory);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
			<LanguageStandard>stdcpplatest</LanguageStandard>
			<SDLCheck>false</SDLCheck>
		</ClCompile>
		<ResourceCompile>
			<AdditionalIncludeDirectories>$(MexTools_MatlabExtern)include;$(MSBuildThisFileDirectory);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
			<LanguageStandard>stdcpplatest</LanguageStandard>
			<SDLCheck>false</SDLCheck>
		</ResourceCompile>
	</ItemDefinitionGroup>
</Project>

如上例,你可以指定某个不在你库内的已知目录加入外部包含,甚至可以调用一些.NET函数辅助处理。MSBuild支持的语法有官方文档详解,此处不再赘述。

附加依赖项

如果你将要发布的是静态lib库,用户的MSBuild需要知道那个文件的路径。类似于外部包含目录,你需要将你的路径附加Link到项的AdditionalDependencies。

对于不存在二进制兼容性问题的lib

可以简单写成:

XML 复制代码
<ItemDefinitionGroup>
	<Link>
		<AdditionalDependencies>Shlwapi.lib;%(AdditionalDependencies)</AdditionalDependencies>
	</Link>
</ItemDefinitionGroup>

上述代码附加了一个名为Shlwapi.lib的静态库。类似于包含目录,这里附加的静态库不一定是你所发布的库,也可以是任何用户机上已知应当存在的库,例如本例Shlwapi.lib就是 Windows SDK 的库。

如果存在二进制兼容性问题

你还需要根据用户的编译配置路由到正确的lib文件,最简单的方法就是直接将编译配置参数文本嵌入文件名。这其实包含两个部分的配置:首先你需要在你自己的vcxproj中根据当前编译配置生成相应的文件名,然后你需要在targets中根据用户的编译配置将对应文件名添加到AdditionalDependencies。

在vcxproj中

作为示例,可以在Project节点下添加如下内容:

XML 复制代码
<Target Name="设置程序数据库" BeforeTargets="ClCompile">
	<PropertyGroup>
		<TargetName>$(ProjectName).%(ClCompile.RuntimeLibrary).$(PlatformToolsetVersion)</TargetName>
		<TargetPath>$(OutDir)$(TargetName)$(TargetExt)</TargetPath>
	</PropertyGroup>
	<ItemGroup>
		<ClCompile Update="@(ClCompile)">
			<ProgramDataBaseFileName>$(OutDir)$(TargetName).pdb</ProgramDataBaseFileName>
		</ClCompile>
		<!-- 此时Lib项尚未创建,无法更新,所以必须在后面DoLibOutputFilesMatch目标前设置 -->
	</ItemGroup>
</Target>
<Target Name="设置库输出" BeforeTargets="DoLibOutputFilesMatch">
	<ItemGroup>
		<!-- 此时程序数据库文件已经创建,再修改已经晚了,所以必须在前面ClCompile目标前设置 -->
		<Lib Update="@(Lib)">
			<OutputFile>$(TargetPath)</OutputFile>
		</Lib>
	</ItemGroup>
</Target>

这样做是基于MSBuild的一个基本运行机制:以Target为单元执行任务。执行生成命令时,MSBuild会收集vcxproj、targets以及内置的一系列XML,按照一定的顺序执行其中定义的Target。如上例中定义了两个Target:

一个是"设置程序数据库",并要求它在ClCompile之前执行------ClCompile是一个内置的Target名称。注意之前targets文件和此处ItemGroup节点中也提到了ClCompile,但在ItemGroup和ItemDefinitionGroup节点内定义的是Item类型,和作为内置Target的ClCompile仅仅是名称恰好相同,本质上不是一类东西。内置ClCompile作为Target被执行时,会生成IDB和PDB,因此必须在该Target之前设置正确的输出路径,也就是:

  • 在ItemGroup中,将作为Item的ClCompile的ProgramDataBaseFileName设为$(OutDir)$(TargetName).pdb,IDB的路径会自动修改扩展名无需另外设置。Update="@(ClCompile)"意思是更新所有ClCompile项(实际上,vcxproj中会为每个cpp源文件创建一个ClCompile项,所以需要"更新所有")其中:
  • OutDir就是配置属性\常规\输出目录
  • TargetName则在前面PropertyGroup中设为了$(ProjectName).%(ClCompile.RuntimeLibrary).$(PlatformToolsetVersion)。其中:
  • ProjectName顾名思义就是整个的项目名称
  • ClCompile.RuntimeLibrary就是配置属性\C/C++\代码生成\运行库。注意它不是全局属性宏,而是ClCompile项的属性,所以引用语法也不一样,前缀是%而不是$。中英文对应关系是:
    • 多线程:MultiThreaded
    • 多线程调试:MultiThreadedDebug
    • 多线程DLL:MultiThreadedDLL
    • 多线程调试DLL:MultiThreadedDebugDLL
  • PlatformToolsetVersion就是配置属性\常规\平台工具集
  • 这样一来TargetName就成了三个字段的拼接,例如MariaDB_Connector.MultiThreaded.145,然后用它进一步拼接成TargetPath:$(OutDir)$(TargetName)$(TargetExt)
  • OutDir前已述,TargetExt就是配置属性\高级\目标文件扩展名,对静态库默认是lib。

下一个Target"设置库输出"将会用到TargetPath。该Target要求在DoLibOutputFilesMatch之前执行------这也是一个内置Target名称。在DoLibOutputFilesMatch中,MSBuild将检查TargetPath、TargetName和Lib项的OutputFile属性是否匹配,因此必须在此之前正确设置Lib项属性。如注释中所述,在"设置程序数据库"阶段,Lib项尚未创建------它是在ClCompile之后、DoLibOutputFilesMatch之前创建的(由内置XML脚本创建,在vcxproj中不可见),因此不能在ClCompile之前设置该Lib项属性。语法与ClCompile类似,将所有Lib项的OutputFile更新为TargetPath。

以上设置可以令你的vcxproj输出的lib文件名始终包含必要的配置信息,主要就是ClCompile.RuntimeLibrary和PlatformToolsetVersion。

在targets中

始终记得,targets将在用户的而非你的MSBuild中执行。作为示例,同样在Project节点内添加:

XML 复制代码
<Target Name="设置链接输入" BeforeTargets="Link">
	<PropertyGroup>
		<!--ItemGroup不能直接捕获ClCompile的属性,因此需要在这里传递-->
		<RuntimeLibrary>%(ClCompile.RuntimeLibrary)</RuntimeLibrary>
	</PropertyGroup>
	<ItemGroup>
		<Link Update="@(Link)">
			<AdditionalDependencies>$(MSBuildThisFileDirectory)MariaDB_Connector.$(RuntimeLibrary).$(PlatformToolsetVersion).lib;%(Link.AdditionalDependencies)</AdditionalDependencies>
		</Link>
	</ItemGroup>
</Target>

需要确保在 Link Target,也就是执行链接步骤之前设置附加依赖项。这里有一个之前未详述的语法限制,就是在Update项属性时,不能直接引用其它项的属性。而事实上,我们恰恰需要在Link项属性中引用%(ClCompile.RuntimeLibrary),这无法直接做到,因此需要先在PropertyGroup中设置一个全局属性宏,可以直接命名为RuntimeLibrary。那之后,可以在更新Link项的AdditionalDependencies时引用它。

你可能还记得在不存在二进制兼容问题的情形中,我们是在ItemDefinitionGroup节点中设置的Link属性。这里为什么不能这样写呢?这其实也涉及MSBuild执行顺序问题。ItemDefinitionGroup是在任何项创建之前执行的,也就是说定义ItemDefinitionGroup时,我们需要引用的ClCompile项尚未创建,当然也就无法获取其RuntimeLibrary属性。而等到该项属性被设置后,MSBuild也是不会再回头去重新查看ItemDefinitionGroup的。因此这里需要的是在项创建阶段修改Link项属性,只能在Target内部使用 ItemGroup Update 语法。

其它项目配置

除了上述两项常见的,你还可以在targets中对用户的MSBuild流程进行任何自定义配置。更多高级用法请参阅MSBuild相关文档教程,本文不胜枚举,仅提供一些启发性示例:

XML 复制代码
<ItemDefinitionGroup>
	<ClCompile>
		<AdditionalIncludeDirectories>$(MexTools_MatlabExtern)include;$(MSBuildThisFileDirectory);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
		<LanguageStandard>stdcpplatest</LanguageStandard>
		<SDLCheck>false</SDLCheck>
	</ClCompile>
</ItemDefinitionGroup>

本示例将用户的语言标准设置为最新,并且关闭SDL检查。是的,一个NuGet包是有权限通过targets直接修改用户的全局生成配置的。因此理论上,可能存在精心设计的恶意targets利用MSBuild安全漏洞对用户进行攻击。在使用任何NuGet包生成前检查其targets有无可疑内容,是一个良好的安全习惯。

发布前的调试

作为一个负责任的库开发者,建议你在发布NuGet之前进行充分的调试测试,因为NuGet不支持删除已发布的库的任何版本,如果你发布了一个丑陋的充满bug的无法使用的版本,它将永远无法从你的发布记录中移除,你只能添加文本说明将其标记为弃用。如果你希望尽可能减少这种不优雅的事故,应当在发布之前确认你的NuGet包能够被正确引用并成功生成。

首先你当然需要自己设计一个测试项目,引用库中的任何待测功能。但是,由于该库尚未发布到NuGet,你当然不能用NuGet的机制自动引用该库,因此需要手动编辑vcxproj文件,如下示例:

XML 复制代码
<ImportGroup Label="ExtensionTargets">
	<Import Project="..\packages\MariaDB_Connector.1.1.7\build\native\MariaDB_Connector.targets" Condition="Exists('..\packages\MariaDB_Connector.1.1.7\build\native\MariaDB_Connector.targets') And '$(Configuration)' != '调试MariaDB'" />
</ImportGroup>
<ImportGroup Label="调试">
	<Import Project="$(USERPROFILE)\source\repos\mariadb-connector-cpp\NuGet\build\native\MariaDB_Connector.targets" Condition="'$(Configuration)'=='调试MariaDB'" />
</ImportGroup>

示例中是两个ImportGroup节点,分别具有Label名曰ExtensionTargets和"调试"。其中,ExtensionTargets表示该节点是 Visual Studio 自动创建的,实际上就是NuGet自动化流程的结果。如果你的项目中安装了任何NuGet包,都会在这个节点中有记录。如果你的项目未引用NuGet包,可以忽略这个节点。但如果引用了,特别是如果引用了你将要发布的库的旧版本(而你想要测试新版),你必须在这里将其暂时屏蔽。最优雅的方法就是创建一个专门的调试用Configuration(在项目属性\配置管理器中),然后为旧库的Import节点新增一个And '$(Configuration)' != '调试MariaDB'附加条件,这样在激活调试配置时,该Import就会被屏蔽。

无论如何,你始终需要Import新库。如示例中的做法,新建一个ImportGroup,然后在其中设置新库的targets文件的位置,并设置其条件为仅在调试配置中使用。这个路径可以使用Windows自带的环境变量,如示例中的USERPROFILE,也可以设置任何绝对路径。这样一来,你就可以用你的测试项目模拟用户环境,Import你将要发布的库,尝试生成,测试各项功能是否正常。

打包和发布

打包需要下载一个nuget.exe,它是一个独立于 Visual Studio 的命令行工具,不会随VS被安装,需要手动单独下载。那之后,你需要将你放置该exe的位置添加到用户环境变量Path以便在命令行中调用。最后,执行nuget pack 根目录,根目录就是你的nuspec所在的目录,然后就会在当前工作目录生成一个nupkg文件,这就是你将要上传到NuGet的包。它实际上是一个zip压缩包,你可以用大多数压缩文件管理器打开它,查看其中内容是否正确。一般来说会比你打包的根目录下多出一些额外的元数据文件,这是正常的。另外,如果有非ASCII字符可能会被URL编码,这通常也无需担心,用户安装后会正常解码使用。

有时你可能会希望将一些额外的、不在根目录下的文件也包进去,例如存储库最外层的README.md,因为GitHub只能显示最外层的README,要想把它包入nupkg中就只能临时拷贝到包根目录下。以下是一个实用的PowerShell脚本:

PowerShell 复制代码
Copy-Item .\README.md .\NuGet
Copy-Item .\include\* .\NuGet\build\native -Force -Recurse
nuget pack NuGet

上述脚本假定当前工作目录就是存储库最外层,也是包根目录(假定命名为NuGet)的直接上级。因此首先将README复制到包根目录中,然后将include目录内所有项目(包括文件和目录)全部复制到build\native下,强制覆盖且遍历所有目录层级。最后执行打包。当然,这些命令是高度自定义的,你需要根据你存储库的实际布局调整。

最后一步,登录NuGet网站,将你的nupkg上传。如果一切设置正确,你通常不需要进行任何额外配置,一路下一步、提交等等就行了。发布后,你需要等待一段时间才能在NuGet库浏览器中找到它------通常不会超过1小时。

下载使用

使用 C++ NuGet 库非常简单,只需要在项目的"管理NuGet包"页面搜索安装即可,库开发者精心设计的targets文件自动帮你完成包含目录和静态库的导入(注意检查targets是否安全!),无需手动设置。

相关推荐
百锦再3 小时前
第6章 结构体与方法
android·java·c++·python·rust·go
北冥湖畔的燕雀3 小时前
C++STL之vector
开发语言·c++
apocelipes3 小时前
C++23的out_ptr和inout_ptr
c++
敲上瘾4 小时前
Elasticsearch从入门到实践:核心概念到Kibana测试与C++客户端封装
大数据·linux·c++·elasticsearch·搜索引擎·全文检索
第七序章4 小时前
【C + +】C + + 11(中)——Lambda 表达式 + 可变参数模板
c语言·c++·算法·1024程序员节
学涯乐码堂主13 小时前
GESP C++ 四级第一章:再谈函数(上)
c++·青少年编程·gesp·四级·学漄乐码青少年编程培训
微露清风14 小时前
系统性学习C++-第九讲-list类
c++·学习·list
大佬,救命!!!14 小时前
C++多线程同步与互斥
开发语言·c++·学习笔记·多线程·互斥锁·同步与互斥·死锁和避免策略
散峰而望14 小时前
C++入门(一)(算法竞赛)
c语言·开发语言·c++·编辑器·github