1. 项目概述:为什么C++项目需要一个“版本管家”?
干了这么多年C++开发,尤其是在Visual Studio这个老伙计的陪伴下,我经手过不少从零到一,再到持续迭代好几年的项目。不知道你有没有遇到过这种场景:测试同事跑过来说,“你昨天给我的那个Debug版本,和今天这个,功能上好像有点不一样?” 或者,线上突然报了个诡异的Bug,你翻遍SVN或Git的提交记录,想定位到底是哪个版本引入的问题,结果发现,所有的可执行文件都叫MyApp.exe,唯一的区别可能就是文件日期——这简直是大海捞针。
这就是我们今天要聊的核心:给Visual Studio下的C++项目,系统化地增加和管理版本号。这听起来像是个“锦上添花”的小功能,但在实际的团队协作、持续集成、问题排查和发布管理中,它扮演着“定海神针”的角色。一个清晰、自动、可追溯的版本号,就像是给每个构建产物打上了一个独一无二的身份证,上面写着:我是谁(产品名),我处于生命周期的哪个阶段(主版本.次版本),我是第几次构建(修订号),甚至是在哪个时间点、基于哪次代码提交诞生的(构建号/提交哈希)。
更关键的是“模块化管理”。现代软件,尤其是C++项目,动辄几十上百个模块(DLL、静态库),每个模块都有自己的迭代节奏。如果还用手动修改rc文件或者头文件里的#define,不仅容易出错,版本同步更是噩梦。我们需要的是一个中心化的、可配置的、能自动渗透到每个相关模块的版本管理方案。
所以,这个“项目”的目标很明确:在Visual Studio环境下,建立一套自动化、可配置、支持模块化同步的C++项目版本号管理机制。接下来,我会把我趟过的路、踩过的坑,以及最终稳定运行的方案,毫无保留地分享给你。
2. 版本号设计哲学与标准选择
在动手写第一行代码或改第一个配置文件之前,我们必须先统一“语言”,也就是版本号的格式和含义。没有规矩,不成方圆。
2.1 常见版本号规范解析
市面上主流的版本号规范有好几种,我们需要根据项目性质来选择:
语义化版本(SemVer):格式为
主版本号.次版本号.修订号,例如2.1.15。这是目前开源世界和商业软件最推崇的规范。- 主版本号:做了不兼容的 API 修改。
- 次版本号:向下兼容的功能性新增。
- 修订号:向下兼容的问题修正。
- 优点:含义清晰,能直接传达版本兼容性信息,对依赖管理极其友好。
- 适用场景:提供公共API的库(如OpenSSL、Boost)、有明确版本发布周期的应用程序。
微软风格版本:通常表现为四段式,如
文件版本:2.1.15.1024,产品版本:2.1.15。在Windows资源文件(.rc)和文件属性中常见。- 前三位(
2.1.15)通常对应语义化版本。 - 第四位(
1024)是构建号(Build Number),由构建系统自动生成,常用于区分每日构建或持续集成产生的不同版本。 - 优点:与Windows生态系统无缝集成,能清晰区分“产品版本”和“文件版本”。
- 前三位(
日期版本号:如
2024.0510.1,表示2024年5月10日的第1次构建。- 优点:一眼就能看出构建时间,对需要频繁每日构建(Nightly Build)或内部测试的项目非常直观。
- 缺点:无法体现兼容性和功能变化。
对于大多数Visual Studio C++项目,我强烈推荐采用“语义化版本 + 自动构建号”的组合策略。即:
- 产品版本(Product Version):
主版本.次版本.修订号(如1.2.0),用于对外发布和标识功能阶段,手动或根据Git Tag更新。 - 文件版本(File Version):
主版本.次版本.修订号.构建号(如1.2.0.1024),用于唯一标识每一个具体的构建产出,构建号由CI/CD系统自动递增。
2.2 版本信息注入Windows资源的必要性
在Windows平台上,版本信息需要写入到可执行文件(EXE)或动态库(DLL)的资源段中。这样,在文件管理器里右键点击文件 -> “属性” -> “详细信息”选项卡,就能看到规整的版本信息。这不仅是为了好看,更是为了:
- 安装程序识别:许多安装工具(如MSI)依赖文件版本来决定是否覆盖。
- 调试与支持:用户报告问题时,可以轻松获取完整的版本信息。
- 系统管理:管理员可以通过PowerShell等工具批量查询软件版本。
在Visual Studio C++项目中,这部分信息通常定义在一个.rc(资源脚本)文件里,其中包含一个VS_VERSION_INFO资源块。我们的自动化方案,最终就是要动态生成或更新这个资源块中的相关字段。
注意:版本资源中的
FILEVERSION和PRODUCTVERSION是4个由逗号分隔的整数,例如1,2,0,1024。而在字符串表中显示的“文件版本”和“产品版本”,通常是点号分隔的字符串,如1.2.0.1024。两者需要保持一致,但格式不同,这是新手常踩的坑。
3. 核心方案选型:手动、半自动与全自动
根据项目规模和团队流程,版本号管理可以分为几个层次。
3.1 方案一:手动维护(不推荐,但需了解)
这是最原始的方式,直接编辑项目的resource.h和.rc文件。
resource.h中定义宏:#define VER_FILEVERSION 1,2,0,1024 #define VER_FILEVERSION_STR "1.2.0.1024\0" #define VER_PRODUCTVERSION 1,2,0,0 #define VER_PRODUCTVERSION_STR "1.2.0\0".rc文件中引用这些宏:#include "resource.h" // ... VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION PRODUCTVERSION VER_PRODUCTVERSION // ... VALUE "FileVersion", VER_FILEVERSION_STR VALUE "ProductVersion", VER_PRODUCTVERSION_STR- 缺点:极易遗忘,多人协作时冲突频繁,无法自动递增构建号,与源码版本管理脱节。
3.2 方案二:预生成头文件与构建后事件(推荐中小项目)
这是性价比很高的半自动化方案,利用Visual Studio的“生成前事件”和“生成后事件”。
- 创建一个版本模板文件,如
version_template.h.in,内容类似resource.h,但版本号部分使用占位符:#pragma once #define VER_MAJOR @PROJECT_VERSION_MAJOR@ #define VER_MINOR @PROJECT_VERSION_MINOR@ #define VER_PATCH @PROJECT_VERSION_PATCH@ #define VER_BUILD @BUILD_NUMBER@ #define VER_FILEVERSION VER_MAJOR, VER_MINOR, VER_PATCH, VER_BUILD #define VER_FILEVERSION_STR _T("@PROJECT_VERSION_MAJOR@.@PROJECT_VERSION_MINOR@.@PROJECT_VERSION_PATCH@.@BUILD_NUMBER@") - 编写一个简单的脚本(Python/PowerShell/Batch),在“生成前事件”中执行。这个脚本做三件事:
- 从某个地方(如一个简单的
version.txt文件、Git标签、环境变量)读取主版本号、次版本号。 - 自动生成或读取一个递增的构建号(可以基于日期时间戳,或一个持久化的计数器文件)。
- 用读取到的值替换模板中的占位符,生成最终的
version.h文件。
- 从某个地方(如一个简单的
- 在项目的
rc文件和所有需要版本号的源码中,包含这个自动生成的version.h。 - 在“生成后事件”中,可以可选地将本次构建的版本信息写入日志或上传到服务器。
- 优点:实现简单,无需额外工具,能与Visual Studio项目属性紧密集成。
- 缺点:构建号的生成逻辑相对简单,跨项目同步版本号需要额外设计(比如让所有项目都读取同一个中央
version.txt)。
3.3 方案三:使用CMake等构建系统(推荐大型/跨平台项目)
如果你的项目已经开始使用或考虑使用CMake,那么管理版本号会变得非常优雅。CMake本身提供了一个project()命令,可以声明版本号,并且能自动生成包含版本信息的头文件。
- 在顶层
CMakeLists.txt中:cmake_minimum_required(VERSION 3.10) project(MyAwesomeProject VERSION 1.2.0 LANGUAGES CXX) # 你可以通过 ${PROJECT_VERSION_MAJOR} 等变量访问版本各部分 - 使用
configure_file()命令处理模板文件,原理与方案二类似,但由CMake在配置阶段完成,更集成化。# 假设有个 version.h.in 模板 configure_file(version.h.in generated/version.h @ONLY) include_directories(${CMAKE_CURRENT_BINARY_DIR}/generated) - 对于构建号,CMake没有内置机制,但可以通过自定义脚本、读取Git提交信息(如
git describe --tags --always)或环境变量来获取,并传递给configure_file。 - 在Windows上,CMake可以生成包含正确版本资源的
.rc文件。对于更复杂的需求,可以使用FindRC模块或直接编写.rc.in模板进行配置。
- 优点:与构建系统深度集成,跨平台支持好,是现代C++项目的趋势。
- 缺点:需要引入或迁移到CMake,有一定学习成本。
3.4 方案四:集成到CI/CD流水线(企业级最佳实践)
这是最彻底、最自动化的方案。版本号的“所有权”完全交给持续集成/持续部署系统(如Jenkins, GitLab CI, Azure DevOps, GitHub Actions)。
- 版本号来源:主版本号、次版本号可以固化在CI配置文件中,或由Git标签(Tag)驱动。例如,打上
v2.1.0的标签即触发发布构建,CI系统自动解析此标签作为产品版本。 - 构建号生成:CI系统通常提供唯一且递增的构建ID(如
BUILD_ID、RUN_NUMBER),完美用作构建号。 - 注入过程:CI流水线在编译前,通过脚本将版本号写入项目目录下的一个配置文件(如
version.props属性表),或直接作为编译预定义宏(/D命令行参数)传递给MSBuild。 - 统一输出:所有解决方案(Solution)中的项目,都通过导入统一的
version.props属性表来获取版本号,实现模块间的绝对同步。
- 优点:全自动,可追溯性强(版本号与CI构建记录绑定),非常适合敏捷开发和频繁发布。
- 缺点:依赖于CI/CD基础设施,本地开发构建时可能需要一个后备的版本号生成机制。
实操心得:对于大多数团队,我建议从方案二开始,它简单有效,能立刻解决手动维护的痛点。当项目逐渐复杂,特别是需要管理多个相互依赖的模块时,可以平滑过渡到方案四,将版本控制作为DevOps流程的一环。方案三则是技术栈升级时的自然选择。
4. 实战:基于属性表与预生成事件的模块化管理方案
下面,我将详细拆解一个结合了方案二和方案四思想的、在Visual Studio 2019/2022中经过实战检验的方案。这个方案的核心思想是:“一次定义,处处使用”,通过Visual Studio的“属性表”实现版本号的集中管理和模块化共享。
4.1 创建中心化的版本属性表
- 新建属性表:在解决方案资源管理器中,右键点击你的解决方案 -> 添加 -> 新建项目 -> 选择“属性表”,命名为
Version.props。我习惯把它放在解决方案根目录下的Build或Properties文件夹里,方便管理。 - 编辑属性表:双击打开
Version.props。我们需要在“通用属性”->“用户宏”中定义版本变量。- 点击“添加宏”。
- 名称:
VersionMajor,值:1 - 名称:
VersionMinor,值:2 - 名称:
VersionPatch,值:0 - 名称:
VersionBuild,值:$([System.DateTime]::Now.ToString("yyyyMMdd"))(这是一个使用MSBuild内联任务的示例,生成20240510格式的日期作为构建号。对于更复杂的逻辑,建议使用外部脚本)
- 定义预处理器宏:在“C/C++” -> “预处理器” -> “预处理器定义”中,添加基于用户宏的预处理器定义。这里很关键,因为
.rc文件编译时也需要这些宏。VERSION_MAJOR=$(VersionMajor) VERSION_MINOR=$(VersionMinor) VERSION_PATCH=$(VersionPatch) VERSION_BUILD=$(VersionBuild) FILE_VERSION=\"$(VersionMajor).$(VersionMinor).$(VersionPatch).$(VersionBuild)\" PRODUCT_VERSION=\"$(VersionMajor).$(VersionMinor).$(VersionPatch)\"注意:预处理器定义中的字符串需要用反斜杠转义引号,即
\"。
4.2 改造资源文件(.rc)以使用动态宏
现在,我们需要修改项目的.rc文件,使其使用我们定义的预处理器宏,而不是硬编码的数字。
- 在
.rc文件开头,移除或注释掉对固定resource.h中版本宏的引用。改为直接使用预处理器宏。 - 修改
VS_VERSION_INFO块:
关键点:// 旧版本(硬编码) // FILEVERSION 1,0,0,1 // PRODUCTVERSION 1,0,0,1 // ... // VALUE "FileVersion", "1.0.0.1" // VALUE "ProductVersion", "1.0.0" // 新版本(使用宏) FILEVERSION VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, VERSION_BUILD PRODUCTVERSION VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH, 0 // 产品版本通常不需要构建号 // ... BEGIN BLOCK "StringFileInfo" BEGIN BLOCK "040904b0" // 英语(美国)代码页 BEGIN VALUE "FileVersion", FILE_VERSION VALUE "ProductVersion", PRODUCT_VERSION VALUE "InternalName", "YourApp.exe" VALUE "OriginalFilename", "YourApp.exe" // ... 其他字符串信息 END END // ... VarFileInfo 块 ENDFILEVERSION和PRODUCTVERSION后面跟的是由逗号分隔的4个数字,所以我们传入了VERSION_MAJOR等宏。而字符串表中的FileVersion和ProductVersion值,我们直接使用了FILE_VERSION和PRODUCT_VERSION这两个字符串宏。
4.3 为项目关联属性表并实现模块同步
- 关联属性表:打开每个需要统一版本号的C++项目属性页。在“通用属性” -> “框架和引用”下,点击“添加新引用”,可以引用其他项目。但这里我们需要的是属性表。在“通用属性” -> “属性管理器”视图中(如果没看到,在“视图”菜单中打开),为每个项目的每个配置(Debug, Release等)右键 -> “添加现有属性表”,选择我们创建的
Version.props。 - 模块化同步的奥秘:至此,所有引用了同一个
Version.props文件的项目,在编译时都会使用其中定义的VersionMajor等用户宏。当你需要更新版本号时,只需修改这一个Version.props文件,然后重新生成解决方案,所有项目的版本号都会自动同步更新。这完美解决了多模块项目的版本一致性问题。 - 在代码中访问版本号:有时我们需要在程序运行时读取自身的版本号。可以创建一个公共头文件,如
app_version.h,同样利用属性表中的预处理器宏:
在任何源文件中包含此头文件,即可使用// app_version.h #pragma once #include <string> #include <sstream> namespace AppVersion { constexpr int MAJOR = VERSION_MAJOR; constexpr int MINOR = VERSION_MINOR; constexpr int PATCH = VERSION_PATCH; constexpr int BUILD = VERSION_BUILD; inline std::string GetVersionString() { std::ostringstream oss; oss << MAJOR << "." << MINOR << "." << PATCH << "." << BUILD; return oss.str(); } inline std::string GetProductVersionString() { std::ostringstream oss; oss << MAJOR << "." << MINOR << "." << PATCH; return oss.str(); } }AppVersion::GetVersionString()获取版本字符串。
4.4 集成自动化构建号生成脚本
上面的例子用内联任务生成了日期构建号。但对于需要严格递增的构建号,我们需要一个外部脚本。这里以Python脚本为例,结合“生成前事件”:
- 编写版本脚本
update_build_number.py:#!/usr/bin/env python3 import os import sys import re def read_version_props(filepath): major = minor = patch = 0 with open(filepath, 'r') as f: content = f.read() # 简单解析XML,查找 VersionMajor 等值。实际可使用xml.etree.ElementTree major_match = re.search(r'<VersionMajor>(\d+)</VersionMajor>', content) minor_match = re.search(r'<VersionMinor>(\d+)</VersionMinor>', content) patch_match = re.search(r'<VersionPatch>(\d+)</VersionPatch>', content) if major_match: major = int(major_match.group(1)) if minor_match: minor = int(minor_match.group(1)) if patch_match: patch = int(patch_match.group(1)) return major, minor, patch def update_build_number(props_file, build_num_file='build_number.txt'): # 读取或初始化构建号 if os.path.exists(build_num_file): with open(build_num_file, 'r') as f: build_num = int(f.read().strip()) + 1 else: build_num = 1 # 或从CI环境变量获取 # 更新构建号文件 with open(build_num_file, 'w') as f: f.write(str(build_num)) # 读取属性表模板,替换构建号占位符,生成最终属性表 # 这里假设有一个 Version.props.in 模板,里面有 @BUILD_NUMBER@ 占位符 with open('Version.props.in', 'r') as f: template = f.read() final_content = template.replace('@BUILD_NUMBER@', str(build_num)) with open(props_file, 'w') as f: f.write(final_content) print(f"Updated build number to: {build_num}") return build_num if __name__ == '__main__': update_build_number('Version.props') - 配置生成前事件:在
Version.props属性表(或主项目的属性页)中,进入“生成事件” -> “预生成事件”。- 命令行输入:
python $(SolutionDir)scripts\update_build_number.py "$(ProjectDir)Version.props"(请根据你的脚本实际路径调整) - 这样,每次编译前,脚本都会自动递增构建号并更新
Version.props。
- 命令行输入:
重要提示:将
build_number.txt文件加入.gitignore,避免构建号被提交到代码库引起冲突。构建号应该只在构建机器上持久化。
5. 高级主题:与CI/CD和安装项目集成
当你的项目需要走向自动化部署时,版本号管理需要与更广阔的流程对接。
5.1 在Azure DevOps / GitHub Actions中驱动版本
在CI流水线中,版本号通常由管道变量或Git标签决定。
- 使用Git标签作为产品版本:在CI脚本中,可以运行
git describe --tags --always --match "v[0-9]*"来获取最近的标签,并解析出主、次、修订号。如果没有标签,则使用默认值或提交哈希。 - 使用流水线运行号作为构建号:Azure DevOps中有
$(Build.BuildId),GitHub Actions中有${{ github.run_number }},这些都是天然递增、全局唯一的完美构建号。 - 注入到MSBuild:在CI的构建任务中,将这些版本号作为MSBuild参数传递:
msbuild MySolution.sln /p:Configuration=Release /p:Platform=x64 /p:VersionMajor=2 /p:VersionMinor=1 /p:VersionPatch=0 /p:VersionBuild=$(Build.BuildId) - 修改属性表以接受外部参数:我们需要改造
Version.props,使其优先使用外部传入的参数。这可以通过在属性表中使用条件判断来实现。但更简单的方式是,在CI流水线中,用一个脚本根据环境变量动态生成最终的Version.props文件,替换掉项目中的模板。
5.2 让安装项目(如Setup Project、WiX)也使用统一版本
如果你的解决方案里包含Visual Studio安装项目(Visual Studio Installer Projects)或使用WiX工具集,确保安装包版本与主程序一致至关重要。
- 对于VS安装项目:安装项目的版本属性是独立的。你可以在安装项目的“属性”窗口中设置版本。为了实现同步,一个办法是写一个“生成后事件”,从主程序输出的EXE/DLL文件中读取版本信息(例如使用
System.Diagnostics.FileVersionInfo.GetVersionInfo写一个小工具),然后去修改安装项目文件(.vdproj)中的版本号字段。这个过程比较繁琐,也是很多人放弃VS安装项目的原因之一。 - 对于WiX(Windows Installer XML):WiX是高度可编程的。你可以在WiX项目(
.wixproj)中,通过MSBuild任务读取主项目的版本号,并传递给WiX的预处理器变量(PreprocessorVariable)或通过Harvest(Heat)工具自动获取文件的版本。- 在
.wixproj文件中,可以添加一个Target,在Build之前执行,调用一个脚本获取版本号,并设置为MSBuild属性。 - 在
.wxs文件中,使用<?define ProductVersion="$(var.Version)"?>来引用这个变量。 - 这样,整个解决方案的版本号源头依然是那个
Version.props或CI变量,通过MSBuild的依赖关系自动传递到WiX,实现全局统一。
- 在
5.3 版本号与调试符号(PDB)文件
确保版本信息也嵌入到PDB文件中,对于崩溃转储(Dump)分析非常有用。幸运的是,当你在资源文件中正确设置了版本信息,并且使用/DEBUG选项编译时,Visual Studio的链接器会自动将版本信息关联到生成的PDB中。使用SymChk或调试器查看PDB详情时,就能看到对应的版本。
6. 常见问题与排查技巧实录
即使方案设计得再完美,实操中总会遇到各种“坑”。下面是我总结的一些典型问题及其解决方法。
6.1 资源编译错误:RC2104: undefined keyword or key name
- 问题描述:编译时,资源编译器(rc.exe)报错,提示预处理器宏未定义。
- 根本原因:资源编译器在解析
.rc文件时,没有获得你在项目属性中定义的预处理器宏。项目属性中C/C++的预处理器定义,默认只传递给C++编译器(cl.exe),不自动传递给资源编译器。 - 解决方案:
- 打开项目属性 -> “资源” -> “常规”。
- 在“附加包含目录”中,确保包含了定义宏的头文件所在目录(如果宏定义在头文件里)。
- 最关键的一步:在“资源” -> “命令行”的“附加选项”中,手动添加定义宏的参数:
这样,资源编译器就能接收到和C++编译器一样的宏定义了。/D "VERSION_MAJOR=$(VersionMajor)" /D "VERSION_MINOR=$(VersionMinor)" /D "VERSION_PATCH=$(VersionPatch)" /D "VERSION_BUILD=$(VersionBuild)"
6.2 文件属性中版本信息显示为“0.0.0.0”或空白
- 问题描述:程序编译成功,但右键查看EXE属性时,版本信息页是空的或全是0。
- 排查步骤:
- 检查
.rc文件是否被编译:在解决方案资源管理器中,确保.rc文件存在于项目中,并且其“项类型”为“资源编译器”。有时文件被意外排除在生成之外。 - 检查宏展开是否正确:使用Visual Studio的“预处理器”视图查看
.rc文件展开后的结果。右键点击.rc文件 -> “属性” -> “常规” -> “从生成中排除”选择“否”,然后编译。在输出目录找到中间文件(通常在项目名.dir\Debug\Rxxxxxxx文件夹里的.res文件对应的.rc展开文件),打开查看FILEVERSION等关键字后面是不是具体的数字。 - 检查数字格式:确保
FILEVERSION后面是4个由逗号分隔的整数,如1,2,0,1024,而不是1.2.0.1024。字符串表里的VALUE "FileVersion", "1.2.0.1024"才是点号分隔。 - 检查字符集:如果你的项目使用Unicode字符集,确保字符串常量前加了
_T()或L前缀,如_T("1.2.0.1024"),否则可能导致乱码或显示不全。
- 检查
6.3 多项目解决方案中,版本号更新后部分项目未生效
- 问题描述:修改了
Version.props后,重新生成解决方案,但有些DLL的版本号还是旧的。 - 原因与解决:
- 项目未引用属性表:在“属性管理器”中仔细检查,确保所有项目在所有配置(Debug|x64, Release|x86等)下都添加了
Version.props。 - 属性表未被重新加载:Visual Studio有时会缓存属性表。尝试“清理解决方案”,然后“重新生成解决方案”。
- 依赖项生成顺序:如果A项目依赖B项目,且B项目生成了lib/dll被A项目链接,确保B项目先于A项目重新编译。检查解决方案的“项目依赖项”设置。
- 中间文件缓存:手动删除所有项目的中间输出目录(通常是
Debug、Release、x64等文件夹)和解决方案的*.suo、*.vcxproj.user文件,然后重新打开解决方案并生成。
- 项目未引用属性表:在“属性管理器”中仔细检查,确保所有项目在所有配置(Debug|x64, Release|x86等)下都添加了
6.4 构建号在每次本地编译时都递增,导致版本号“污染”仓库
- 问题描述:按照上述预生成事件脚本,每次按F5调试,构建号都会+1,导致可执行文件版本不停变化,且构建号文件可能被误提交。
- 解决方案:
- 区分本地构建与CI构建:在预生成脚本中,检查是否存在CI环境变量(如
TF_BUILDfor Azure DevOps,CIfor GitHub Actions)。如果存在,则使用CI提供的构建号;如果不存在,则可以使用一个固定的本地开发构建号(如9999),或者基于日期时间生成一个不会提交的构建号。 - 使用
.gitignore:务必把存储自动生成构建号的文件(如build_number.txt、自动生成的version.h)加入到.gitignore中。 - 将版本生成移至CI阶段:最干净的做法是,本地开发时版本号固定或使用占位符,只在CI服务器上执行版本号替换和递增的步骤。这需要将版本模板文件(如
version.h.in)纳入版本控制,而将生成最终文件的步骤作为CI流水线的一部分。
- 区分本地构建与CI构建:在预生成脚本中,检查是否存在CI环境变量(如
6.5 如何从程序中读取自身版本号
除了在代码中直接使用预处理器宏,有时需要在运行时动态读取。可以使用Windows APIGetFileVersionInfo和VerQueryValue。
#include <windows.h> #include <string> std::string GetModuleVersionStr(HMODULE hModule = nullptr) { std::string version; char modulePath[MAX_PATH]; GetModuleFileNameA(hModule, modulePath, MAX_PATH); DWORD dummyHandle; DWORD infoSize = GetFileVersionInfoSizeA(modulePath, &dummyHandle); if (infoSize) { std::vector<BYTE> buffer(infoSize); if (GetFileVersionInfoA(modulePath, 0, infoSize, buffer.data())) { VS_FIXEDFILEINFO* pFileInfo = nullptr; UINT len = 0; if (VerQueryValue(buffer.data(), "\\", (LPVOID*)&pFileInfo, &len)) { version = std::to_string(HIWORD(pFileInfo->dwFileVersionMS)) + "." + std::to_string(LOWORD(pFileInfo->dwFileVersionMS)) + "." + std::to_string(HIWORD(pFileInfo->dwFileVersionLS)) + "." + std::to_string(LOWORD(pFileInfo->dwFileVersionLS)); } } } return version; }这个函数可以读取指定模块(EXE或DLL)的文件版本,非常适合用于日志输出或关于对话框。