news 2026/10/20 5:02:04

用VID/PID与物理序列号实现U盘身份识别:Windows C++工具详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用VID/PID与物理序列号实现U盘身份识别:Windows C++工具详解

简介:面向需要做U盘识别、移动存储设备管理的Windows开发者,这份VC6源码封装了获取设备VID、PID、盘符及物理序列号的关键逻辑。此前论坛资料多只给逻辑序列号,作者整合setupapi与cfgmgr调用后实现了对U盘、移动硬盘、手机卡、MP3/4的通用识别,运行结果形如盘符\VID&PID\物理序列号,三段信息非常清晰。资源包共31个文件,压缩后4.89MB,除.cpp与.h源码外,还含可直接运行的exe、setupapi.lib/cfgmgr32.lib链接库及pdb调试文件,在VC6下即可打开编译或二次开发。已有1746人学习下载,对希望绕过逻辑序列号限制、直接读取底层物理信息的开发者具有实用参考价值。

1. U盘身份识别:为什么需要VID/PID和物理序列号,而不是只依赖盘符

某天同事反馈,内部工具用U盘作为授权凭证,用户换电脑后插入同一把U盘,盘符从E:变成了G:,授权立刻失效。排查发现旧方案把盘符当设备标识,盘符只是Windows临时分配的字母,插口变化、重启、扩容分区都会让它变。真正稳定的是U盘硬件层的VID、PID和物理序列号。这套源码实现了一个完整的U盘身份识别工具,能枚举当前在线的USB存储设备,输出VID、PID、盘符和物理序列号。适合给软件做USB Key授权、给运维做外设合规审计,也可以作为设备指纹模块嵌入其他项目。代码用C++编写,不依赖第三方库,可直接编译运行。

2. 技术选型与目录结构:为什么选择SetupAPI加注册表,而不是纯WMI

2.1 先看WMI方案的短板

第一版原型用WMI实现,代码很短,PowerShell一条命令就能看到效果:

Get-CimInstance Win32_USBHub | Select-Object DeviceID, PNPDeviceID

这条命令能在绝大多数Windows上执行,输出的DeviceID形如USB\VID_1234&PID_5678,不带序列号。如果换用Win32_DiskDrive,PNPDeviceID偶尔能带出实例号,但实例号并不是稳定指纹。更关键的是WMI服务的初始化速度不稳定,有些精简版系统里WMI Repository损坏,查询直接抛异常;在性能要求高的U盘插拔监控场景,WMI的延迟不可控。

所以最终方案定为对接系统底层API:SetupAPI负责枚举设备节点,注册表USBSTOR负责提取物理序列号。SetupAPI是设备管理器同源接口,设备管理器能看到的设备它都能枚举;USBSTOR中保存了USB存储设备在系统中的持久化登记,最后一级子键名就是物理序列号。整个流程不依赖PowerShell运行时,也不依赖WMI服务,启动快、结果确定性高。

2.2 为什么需要交叉验证,而不是只读注册表

USBSTOR注册表长期累积所有历史设备数据,只读它无法区分“现在插着的U盘”和“昨天插过的U盘”。必须先用SetupAPI确认当前在线设备,拿到设备实例路径,再带着实例路径去注册表里匹配记录。这种数据源组合方式,本质上和做数据稽查一样:在线探测器提供实时状态,注册表提供持久化明细,两者互为参考。

举个实际例子:同一把U盘重新量产过,其固件序列号可能被量产工具改写,USBSTOR里会出现新旧两条记录。如果只用注册表,拿到的可能是旧序列号;如果只用SetupAPI,可能看到的是新实例路径但缺少历史上下文。交叉验证后,以当前设备实例路径的serial为主,USBSTOR的serial作为辅助记录写入日志,这类问题就能从容处理。

2.3 项目结构与数据流

工程目录如下,每个模块的边界非常清楚:

UDiskInfo/ ├── main.cpp ├── u_disk_enum.h ├── u_disk_enum.cpp ├── reg_query.h ├── reg_query.cpp ├── drive_mapper.h ├── drive_mapper.cpp └── README.md

模块划分说明:

  • u_disk_enum.cpp 只负责设备枚举,不关心盘符;
  • reg_query.cpp 只处理注册表,不关心设备管理器;
  • drive_mapper.cpp 把设备实例路径与逻辑盘符关联;
  • main.cpp 负责组装,把三个模块的结果合成一张表。

这种划分让二次开发容易得多:如果只想拿序列号,调用reg_query即可;如果只想监听新设备,调用u_disk_enum即可;盘符问题可以单独测试drive_mapper,不影响其他模块。

编译命令如下,依赖只有系统库:

cl /EHsc /std:c++17 main.cpp u_disk_enum.cpp reg_query.cpp drive_mapper.cpp setupapi.lib /Fe:udisk.exe

参数解释:/std:c++17要求C++17标准,因为代码里用到了std::vector的移动语义和统一的初始化列表;setupapi.lib链接的是SetupAPI的导入库,装Windows SDK就会带。如果编译过程中报“无法打开文件setupapi.lib”,先确认Windows SDK的Lib路径在LIB环境变量里,这个坑主要在安装了VS但没勾选“Windows SDK”组件的机器上出现。

3. 核心模块拆解:从USB设备枚举到盘符映射的完整链路

3.1 步骤一:用SetupAPI拿到当前在线USB设备列表

先定义贯穿项目的数据结构:

#include <string> #include <vector> struct UsbDeviceInfo { std::string vid; std::string pid; std::string serial; std::string instancePath; std::vector<char> driveLetters; };

字段含义:vid和pid是4位十六进制字符串,对应设备描述符中的idVendor和idProduct;serial是物理序列号,注意空值是完全合法的情况;instancePath是设备实例路径,格式USB\VID_1234&PID_5678\0011...;driveLetters保存此设备关联的盘符字符,例如{'E','G'}。

枚举函数主体如下:

#include <windows.h> #include <setupapi.h> #include <devguid.h> #include <stdio.h> #pragma comment(lib, "setupapi.lib") bool EnumerateUsbDevices(std::vector<UsbDeviceInfo>& outDevices) { HDEVINFO hDevInfo = SetupDiGetClassDevsA( nullptr, nullptr, nullptr, DIGCF_ALLCLASSES | DIGCF_PRESENT); if (hDevInfo == INVALID_HANDLE_VALUE) { return false; } SP_DEVINFO_DATA devInfoData; devInfoData.cbSize = sizeof(SP_DEVINFO_DATA); for (DWORD index = 0; SetupDiEnumDeviceInfo(hDevInfo, index, &devInfoData); ++index) { char instancePath[MAX_PATH] = {0}; if (!SetupDiGetDeviceInstanceIdA( hDevInfo, &devInfoData, instancePath, MAX_PATH, nullptr)) { continue; } std::string path(instancePath); if (path.find("VID_") == std::string::npos) continue; UsbDeviceInfo info; if (!ParseUsbInstancePath(path, info.vid, info.pid, info.serial)) { continue; } info.instancePath = path; outDevices.push_back(info); } SetupDiDestroyDeviceInfoList(hDevInfo); return true; }

代码逻辑说明:SetupDiGetClassDevs的第一个参数传nullptr表示所有设备类,结合DIGCF_ALLCLASSES和DIGCF_PRESENT,可以拿到当前系统全部在线设备。SetupDiEnumDeviceInfo是纯循环枚举,index从0开始,返回值变为FALSE表示枚举结束。SetupDiGetDeviceInstanceIdA输出实例路径后,只保留包含“VID_”的串,也就是USB总线上有厂商ID的设备。

参数说明:devInfoData.cbSize必须提前初始化为sizeof(SP_DEVINFO_DATA),这是SetupAPI的常规约束,漏掉会返回ERROR_INVALID_USER_BUFFER。DIGCF_PRESENT只枚举“在线的”,不写它会枚举出所有历史设备,U盘列表里会混入几个月前插过的记录。

解析函数把实例路径拆成三段:

bool ParseUsbInstancePath(const std::string& path, std::string& vid, std::string& pid, std::string& serial) { size_t vidPos = path.find("VID_"); if (vidPos == std::string::npos) return false; size_t pidPos = path.find("PID_", vidPos + 4); if (pidPos == std::string::npos) return false; vid = path.substr(vidPos + 4, 4); pid = path.substr(pidPos + 4, 4); size_t serialPos = path.find('\\', pidPos + 8); if (serialPos == std::string::npos) { serial.clear(); } else { serial = path.substr(serialPos + 1); } return true; }

解析说明:路径分隔符是反斜杠,第一层是总线名USB,第二层是VID_xxxx&PID_xxxx,第三层是实例ID。这里有个易错点:有的设备路径写成USB\VID_1234&PID_5678\6&2f...,反斜杠后面的是系统生成的实例ID而不是固件序列号。因此解析出的serial需要和USBSTOR注册表里的值比对,完全一致才算真正读到了物理序列号。

3.2 步骤二:用USBSTOR注册表提取物理序列号

USBSTOR注册表路径的特殊之处在于,它有两层子键。上一层是“型号标识串”,下一层才是“序列号”。读取时如果只枚举了一层,得到的是型号而不是序列号,这是最容易写错的点。

bool ReadUsbStorSerial(const std::string& targetInstancePath, std::string& outSerial) { HKEY hUsbStor = nullptr; LONG result = RegOpenKeyExA( HKEY_LOCAL_MACHINE, "SYSTEM\\CurrentControlSet\\Enum\\USBSTOR", 0, KEY_READ | KEY_WOW64_64KEY, &hUsbStor); if (result != ERROR_SUCCESS) return false; char modelKey[MAX_PATH] = {0}; DWORD modelKeyLen = MAX_PATH; DWORD modelIndex = 0; while (RegEnumKeyExA(hUsbStor, modelIndex++, modelKey, &modelKeyLen, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) { modelKeyLen = MAX_PATH; HKEY hModel = nullptr; if (RegOpenKeyExA(hUsbStor, modelKey, 0, KEY_READ | KEY_WOW64_64KEY, &hModel) != ERROR_SUCCESS) { continue; } char serialKey[MAX_PATH] = {0}; DWORD serialKeyLen = MAX_PATH; DWORD serialIndex = 0; while (RegEnumKeyExA(hModel, serialIndex++, serialKey, &serialKeyLen, nullptr, nullptr, nullptr, nullptr) == ERROR_SUCCESS) { serialKeyLen = MAX_PATH; if (serialKey[0] != 0) { outSerial = serialKey; RegCloseKey(hModel); RegCloseKey(hUsbStor); return true; } } RegCloseKey(hModel); modelKeyLen = MAX_PATH; } RegCloseKey(hUsbStor); return false; }

逻辑说明:外层循环枚举型号键,内层循环枚举序列号键。serialKey实际取到的是字符串,如20150101001。这段代码里的KEY_WOW64_64KEY专门给32位进程用,如果你确定只编译x64,不加也行;但如果要发布Win32版本,加上它才能访问64位注册表视图。

参数说明:RegEnumKeyExA的第四参数是局部变量,每次调用后会被修改为返回子键的实际长度,导致下次循环判断异常,所以每次循环重新赋值为MAX_PATH。这段是几乎所有注册表枚举代码里都会踩的坑,写起来不复杂,但不加就翻车。

拿到USBSTOR的serial后,与3.1中从实例路径解析的serial做一次合并:如果两者都非空且相等,这就是非常可靠的物理序列号;如果实例路径serial为空,可以用USBSTOR的值兜底;如果两者都为空,则标记为“无序列号设备”,业务上走弱指纹逻辑。注意:USBSTOR的枚举可能命中多条历史记录,所以不能无脑返回第一个,而是优先返回与targetInstancePath后半段相匹配的那一条。上面示例代码为了保持章节长度做了简化,完整工程中会对多个serial做匹配优先级排序。

3.3 步骤三:把设备节点关联到盘符

盘符关联是三个模块中最容易妥协的。一个物理U盘可能对应多个盘符,也可能一个盘符都没有。实现思路是先遍历所有逻辑盘符,再用卷设备路径去匹配USB设备。

std::vector<char> FindDriveLetters( const std::string& instancePath) { std::vector<char> result; char drives[256] = {0}; DWORD size = GetLogicalDriveStringsA(sizeof(drives), drives); if (size == 0) return result; char* p = drives; while (*p) { UINT type = GetDriveTypeA(p); if (type == DRIVE_REMOVABLE) { char volumeName[MAX_PATH] = {0}; if (GetVolumeNameForVolumeMountPointA( p, volumeName, MAX_PATH)) { // 通过卷名找到设备实例路径 if (MatchVolumeToUsbDevice(p, instancePath)) { result.push_back(p[0]); } } } p += strlen(p) + 1; } return result; }

代码逻辑:GetLogicalDriveStringsA把C:\、D:\等盘符写入缓冲区,返回后缓冲区里是多个以双空字符结尾的ANSI串,遍历时按strlen+1跳到下一个。GetVolumeNameForVolumeMountPointA把盘符转换成\\?\Volume{GUID}\形式的卷唯一标识,接下来用这个标识去匹配USB设备编号。

匹配函数MatchVolumeToUsbDevice内部可以调用QueryDosDeviceA获取盘符对应的设备对象名,比如\Device\HarddiskVolume3,再接一下IOCTL_VOLUME_GET_VOLUME_DISK_EXTENTS获取磁盘扩展信息,判断这个磁盘的成员是否和instancePath中的VID/PID一致。这里涉及存储栈,初学者容易上手就写死DRIVE_REMOVABLE判断,但U盘量产成“本地磁盘”时会退化成DRIVE_FIXED,所以代码里我保留了type == DRIVE_FIXED的注释分支,供有移动硬盘需求的读者扩展。

参数说明:MatchVolumeToUsbDevice的具体实现会根据系统版本做两种路径,Windows 10 1809以上推荐用SetupAPI2接口,旧系统用IOCTL。样例代码里保留的是兼容性更好的IOCTL版本,代价是代码量多二十几行。这里先给出接口,完整实现不做过度展开,读者对照工程注释即可看懂。

整个模块的调用顺序是:枚举设备 → 逐台解析VID/PID → 查询USBSTOR序列号 → 关联盘符 → 输出。其中任何一步都不要因为前面拿到空值就跳过后面的步骤,因为弱指纹业务可能要用到盘符和容量的组合。

4. 避坑指南:获取U盘信息时的五个常踩的坑

4.1 坑一:枚举结果混入大量非U盘设备

现象:程序输出上百条设备记录,键盘、鼠标、蓝牙适配器、USB集线器全部被当成目标设备。 原因:SetupAPI枚举的是USB总线上的全部设备,不只是大容量存储设备。仅凭“包含VID_”过滤远远不够。 解决:增加两层过滤。第一层检查设备类GUID是否为GUID_DEVCLASS_USB或GUID_DEVCLASS_DISKDRIVE,这样大部分人体学输入设备被过滤;第二层用SetupDiGetDeviceRegistryProperty读取SPDRP_DEVICEDESC文本,包含“Mass Storage”或“USB Storage”的优先保留。注意:少数读卡器描述文本不包含这两个单词,所以过滤只能是“优先保留”,不要把其他设备直接当成非法。

4.2 坑二:序列号为空或序列号全相同

现象:同一批U盘,有的能读到序列号,有的输出为空,更离谱的是序列号全是“00000000”或同一串字符。 原因:U盘主控固件并非全部写入唯一序列号。有些量产工具默认序列号字段为空,有的用固定模板写入,于是所有盘的序列号都一样。 解决:在代码里把“序列号为空”当成正常分支对待,而不是当成错误抛出。业务层定义弱指纹:VID+PID+容量,必要时把盘符的卷序列号也加入hash。同时打印日志时明确标记“weak fingerprint”,让后续审计能区分强指纹和弱指纹。这一条几乎每个设备识别项目都会遇到,属于忍耐度测试,过了才有进阶的资格。

4.3 坑三:盘符映射错乱,多分区和读卡器分不清

现象:双分区U盘只返回一个盘符;读卡器插了两张SD卡,返回的盘符却是错的。 原因:一个物理设备可以挂多个逻辑卷,代码找到第一个DRIVE_REMOVABLE就返回,自然丢信息。读卡器则因为同一个USB节点下挂多个磁盘对象,匹配时无法直接区分。 解决:把盘符映射改成收集模式,返回值用vector而非单个字符。读卡器场景必须额外加入磁盘索引和容量信息:先枚举磁盘对象,再判断每个磁盘对象是否对应当前读卡器,而不是用USB设备实例路径直接比。升级后的算法会多一次IOCTL查询,但对多卡槽读卡器的正确率提升明显。

4.4 坑四:32位编译在64位系统上读不到USBSTOR

现象:用Win32目标编译出的exe在x64系统上运行,设备枚举正常,但注册表查询结果为空。 原因:32位进程访问注册表时,系统把它重定向到WOW6432Node视图,HKEY_LOCAL_MACHINE路径下的Enum子键并不在可用视图中,所以RegOpenKeyExA打开失败或返回空。 解决:所有注册表访问统一加上KEY_WOW64_64KEY访问标志,强制读取64位注册表视图。这个坑不会报错,只会在结果上静默失败,属于最让人抓狂的类型。血泪经验是:构建机默认生成x64目标,发布过程中如果谁手动切换了Win32目标,务必跑一遍回归测试,不然用户拿到的程序就是半残状态。

4.5 坑五:U盘插入后看不到盘符,或卷被锁

现象:设备管理器能看到U盘设备,但资源管理器不显示盘符;有的盘符显示但打开提示“参数错误”或“拒绝访问”。 原因:U盘没有自动挂载卷,或者卷被BitLocker加密且当前未解锁。BitLocker锁定的卷在GetLogicalDrives里存在但无法正常打开。 解决:检测时需要区分“设备在线”和“卷可用”。程序层面先用SetupAPI枚举磁盘设备,发现设备在线但找不到盘符时,输出“设备在线未挂载”;如果盘符存在但打开失败,输出“卷被锁定,请解锁后重试”。对BitLocker卷解锁,业务允许的话可以调用WinAPI打开加密卷,但这属于另一个模块的事,不要在基础工具中默认实现,否则会把授权逻辑搞复杂。

5. 源码运行与验证:从编译到输出一条龙

5.1 环境要求与编译步骤

运行前确认三件事:系统是Windows 10/11 x64;装好Visual Studio 2019或2022,且勾选了“使用C++的桌面开发”;Windows SDK版本不低于10.0.19041.0。打开“Developer PowerShell for VS 2022”,进入源码目录执行:

cl /EHsc /std:c++17 main.cpp u_disk_enum.cpp reg_query.cpp drive_mapper.cpp setupapi.lib /Fe:udisk.exe

如果报“无法打开文件setupapi.lib”,说明SDK的Lib路径不在LIB环境变量里,手动追加:

set LIB=%LIB%;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\um\x64

环境变量解释:不同版本号会有不同目录,例如10.0.22621.0,按实际安装目录填。设置完成后同一命令行窗口重新执行编译命令即可。还有一种更简单的方式:打开“x64 Native Tools Command Prompt for VS 2022”,这个窗口预置了所有环境变量。

5.2 运行参数与输出解读

编译成功后,插入U盘,运行:

udisk.exe /list

输出示例(演示数据):

[1] VID=1234 PID=5678 Serial=001CC0F316F0F1A1 DriveLetter=E:\ BusType=USB [2] VID=8888 PID=9999 Serial=20150101001 DriveLetter=G:\, H:\ BusType=USB

字段说明:第一个设备serial为24位十六进制,说明固件序列号有效;第二个设备serial为11位数字,是白牌盘常见格式;第二个设备返回两个盘符,说明是双分区U盘。BusType字段来自磁盘总线类型,可用于排除非USB接口的移动硬盘。如果Serial为空,先看设备管理器里硬件ID是否本身不带实例号,如果是,则弱指纹分支生效,这是正常业务路径。

5.3 三层验证法

验证工具是否正确读到物理序列号,按三层来做。

第一层是设备管理器对比:右键U盘设备,选“属性-详细信息-硬件ID”,和程序输出的VID/PID对照。第二层是注册表对比:用regedit跳到USBSTOR路径,找对应型号子键,查看下一层子键名是否与输出的serial相同。第三层是行为验证:把U盘格式化一次,改卷标,换USB口插入,重新运行程序,此时VID/PID和serial必须保持不变,而盘符允许变化。如果serial跟着变了,说明代码实际读的是卷序列号或临时实例ID,需要回到设备实例路径和注册表的交叉逻辑重新排查。

三层验证里最有说服力的是第三层,因为它模拟了普通用户的实际使用场景。建议在虚拟机里也做一次USB直通测试,排除主机驱动的干扰。完成这套验证后,工具输出的设备指纹就可以作为授权依据了。

6. 二次开发改造技巧:把枚举逻辑变成U盘设备指纹库

拿到源码后,第一步如果要集成到现有系统,优先把它封装成DLL导出接口。定义一个对外结构体和导出函数:

struct UdExInfo { char vid[8]; char pid[8]; char serial[64]; char driveLetters[16]; unsigned long long diskSize; }; extern "C" __declspec(dllexport) int UdEx_GetDeviceList( UdExInfo* outArray, int maxCount);

接口设计说明:vid/pid固定长度,避免调用方和DLL之间出现指针生命周期问题;diskSize通过DeviceIoControl查磁盘几何信息得到,单位是字节。C#调用时加一行[DllImport("UDiskLib.dll")]即可;Python用ctypes定义相同结构体,约二三十行代码就能接入。

第二个改造点是加插拔监听。用RegisterDeviceNotification注册GUID_DEVCLASS_USB设备类,在WndProc里拦截WM_DEVICECHANGE,收到事件后重新调用枚举函数,和旧列表比对,就能在U盘插入的瞬间触发业务逻辑。注意监听的是GUID_DEVCLASS_USB,不是GUID_DEVCLASS_DISKDRIVE,否则读卡器插卡动作可能不触发。

第三个改造点是实现授权持久化。建议把U盘指纹记录在三张表:设备表(VID/PID/serial/首次登记时间)、授权表(设备ID/授权码/过期时间)、审计表(每次插拔历史)。授权校验时先查设备表,serial能精确匹配就走强指纹;serial为空时再按VID+PID+容量做弱指纹匹配;完全匹配不上才判定未授权。校验时不要忽视时间维度,审计表里记录最近活跃时间,可以识别“授权U盘被复制”这种反常行为。

我在实际项目里最早绑定盘符,结果用户换了一台电脑授权立刻失效,后来改成“VID+PID+序列号”复合指纹,问题才落地解决。从那以后,我接到设备识别需求都会强制走一遍“列数据源→交叉验证→定义降级策略”三个步骤,不再急着写代码。希望这篇笔记和源码能帮到你,少走一点USB识别上的弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 5:01:40

RAG效果评估全指南:从检索质量到生成质量的指标与实战

1. 先分清楚&#xff1a;RAG评估到底在评什么先说结论&#xff1a;RAG效果评估&#xff0c;核心考察的是两段流水线——检索段有没有捞到该捞的资料&#xff0c;生成段有没有把资料用好。这句话想明白了&#xff0c;你的评估体系就不会乱。我见过不少团队把RAG评估简单等价于“…

作者头像 李华
网站建设 2026/10/20 5:00:48

从ChatGPT到Claude:多模型提示工程的适配实践

做了三年多AI应用落地&#xff0c;我最明显的感受是&#xff1a;提示工程这个活儿&#xff0c;已经从一个“会写自然语言就行”的辅助技能&#xff0c;变成了一套需要成体系设计、跨模型适配的工程技术。两年前&#xff0c;团队里一个人死磕ChatGPT系产品的提示词就够用&#x…

作者头像 李华
网站建设 2026/10/20 5:00:46

人脸识别三阶段解耦设计:检测-对齐-编码的可审计实现

简介&#xff1a;这是一份面向Android开发者的人脸识别技术实战DEMO&#xff0c;聚焦移动端人脸检测、特征提取与匹配全流程实现&#xff0c;适用于安全门禁、身份核验等场景的快速原型开发与SDK集成学习。资源包含1758个文件&#xff0c;主体为Java源码&#xff08;42个&#…

作者头像 李华
网站建设 2026/10/20 5:00:18

鸿蒙+Flutter混合开发实战:从零构建跨平台汇率查询器

先聊点实在的。我这次要做的项目&#xff0c;是“鸿蒙 Flutter 跨平台开发——汇率查询器”。你可能会问&#xff1a;汇率查询这种小应用&#xff0c;用得着搬出“鸿蒙 Flutter”这一套组合吗&#xff1f;单写一个原生鸿蒙应用或者只写一个 Flutter 应用&#xff0c;不是更省…

作者头像 李华
网站建设 2026/10/20 5:00:18

C++实践排序函数模板项目的参考方法

前言 「排序函数模板」听起来像课堂练习&#xff1a;写一个 template<class T> void sort(T* data, int n)&#xff0c;里面套两层循环。真正放进项目后&#xff0c;问题会变成另一副样子&#xff1a;数据可能来自 std::vector、std::array、原生数组甚至自定义容器&…

作者头像 李华