简介:面向64位Windows平台进行C/C++开发、并希望保持与Unix/Linux工具链兼容的开发者,这套MinGW64 x86-64 15.2.0工具集提供了较完整的开源编译环境。它集成GCC/G++相关组件,支持SEH结构化异常处理与UCRT通用运行时,并包含面向Visual Studio 2019的RT v13修订,适合跨平台项目迁移、本地编译调试及需要较新运行时特性的工程。压缩包共2000个文件、约173.71MB,其中967个.h与243个.hpp头文件构成主要接口声明,761个.py脚本多用于构建或辅助处理,另有txt说明、sh脚本及少量c样例、md文档与前端配置文件,目录组织便于按模块检索。已有805人学习或下载,说明其在Windows开发环境搭建中有一定参考价值。对于需要离线获取工具链、核对头文件与运行时组成、减少GitHub下载等待的读者,可直接获得较完整的开发组件与兼容性支持,便于快速投入项目编译和问题排查。
1. 一个字母之差:win32 线程模型让 std::thread 编译不过
某开发者在旧笔记本上装 MinGW64,解压、配 Path、写 hello world,一路顺畅。第二天他要写个带后台线程的小工具,敲完std::thread t(worker);回车,编译器甩回一行红字:error: 'thread' in namespace 'std' does not name a type。他换了三个版本的包,重装了两遍系统变量,甚至怀疑自己 C++ 学错了。真正的原因藏在压缩包名里那三个字母:win32。GCC 的线程模型分 win32 与 posix 两套,前者直接调 Windows 原生线程 API,构建时 libstdc++ 不定义_GLIBCXX_HAS_GTHREADS,std::thread、std::mutex、std::future整套并发组件就地蒸发。这种环境太常见了:一个几百 MB 的 7z 包,解压即用,没有安装器,看着比装一整套包管理器省事;可一旦选错组合,它不会当场报错,而是等到你写第一行多线程代码时才爆。这篇记录的就是mingw64-x86-64-15.2.0-release-win32-seh-ucrt-rt-v13-rev0这一套从版本拆解、落盘、配 Path 到装坏救回来的全过程,给在 Windows 上写 C/C++、用 VS Code 或 CMake 的人照着走。
2. 拆开文件名再下载:15.2.0 的三段关键参数
这个文件名六十多个字符,每一段都对应编译器的某个开关,选错任何一段,失败方式都很隐蔽——有时是编译期一句语法级的报错,有时是运行期一个弹窗,有时是拿到别人机器上才炸。我自己的习惯是先把文件名拆成几段抄在纸上,确认要哪个组合,再去发布页找对应文件,别看到mingw64开头的就往下点。
2.1 从文件名读出版本、线程模型、异常模型和 C 运行库
| 片段 | 实际含义 | 选错的后果 |
|---|---|---|
| mingw64 | 目标为 64 位 Windows,三元组x86_64-w64-mingw32 | 拿成 32 位包,链接 64 位静态库时格式不匹配 |
| x86-64 | 目标 CPU 架构 | 同上,交叉链接直接失败 |
| 15.2.0 | GCC 主版本号,决定 C++23 特性的覆盖范围 | 版本太老编不了std::expected、std::print |
| release | 发布构建,带优化、无调试断言 | 拿成调试构建,产物体积大、跑得慢 |
| win32 | 线程模型,走 Windows 原生线程 API | std::thread、std::mutex、std::future全部不可用 |
| seh | 异常模型,结构化异常处理,仅 x86_64 可用 | 退到 sjlj 每次函数调用都有额外开销 |
| ucrt | C 运行库为 Universal CRT | 目标机低于 Win10 且没打 UCRT 补丁时起不来 |
| rt_v13 | mingw-w64 运行时版本 13 | 与 GCC 版本绑定,不能单独替换 |
| rev0 | 同一组合的第 0 次修订 | 修 bug 会出 rev1,有新版优先取新版 |
先说win32这一段,因为它是最容易吃亏的地方。win32 线程模型意味着 GCC 运行时直接调用CreateThread那一套系统 API,好处是不依赖libwinpthread-1.dll,发布时少一个外部文件,纯计算型代码的调用开销也更贴近系统;代价是 libstdc++ 在构建阶段就把 C++ 标准库的并发组件关掉了。注意报错形态:它给的是编译错误而不是链接错误,很多人第一反应是「头文件没包」或者「标准版本没开」,于是往-std=c++20上折腾,其实是包里根本没这个能力。
再看seh。这是 Windows 自己的结构化异常处理,x86_64 上栈展开效率高,C++ 异常、try/catch、析构函数在异常路径上的行为都正确;sjlj 是 setjmp/longjmp 的老方案,兼容性广但每次函数调用都要付一点额外代价。64 位包基本都是 seh,只有目标环境必须兼容很老的 32 位系统时才考虑 sjlj,正常开发不用纠结。
最后是ucrt。程序会依赖系统的ucrtbase.dll和一组api-ms-win-crt-*.dll,Windows 10 起系统自带,所以产物在 Win10/11 上不需要额外分发 CRT。换成msvcrt那一套,兼容性一路回溯到很老的系统,但 CRT 实现陈旧,宽字符、格式化输出、locale 的行为和现代实现有出入,写日志、处理中文时容易踩到不一致。选 ucrt 的代价只有一个:目标机不能太老。这三段组合起来——win32 + seh + ucrt——就是「产物干净、跑得快、但并发库缺席」的一套,用之前心里得有数。
2.2 系统要求和装前环境清单
装之前先把机器扫一遍,比装完再回滚省事得多。这套包对系统本身要求不高,真正的门槛在运行库和路径。
| 项目 | 要求 | 我一般这么配 |
|---|---|---|
| 操作系统 | Windows 10 1809 及以上、Windows 11 | Win10 22H2 以上,UCRT 系统自带 |
| 老系统补丁 | Win7/8.1 需先装 Universal CRT 更新 | 装完重启,再编译验证 |
| 磁盘 | 压缩包几百 MB,解压后 1 GB 上下 | 留 3 GB 余量,构建中间文件还要占 |
| 目录 | 无中文、无空格、非系统保护目录 | C:\dev\mingw64 |
| 权限 | 普通用户可写 | 不放 Program Files,避免每次要管理员 |
| 已有工具链 | 确认没有别的 gcc 抢 Path | where.exe gcc只留一行 |
老系统这条别省。低于 Win10 的机器如果没打 UCRT 补丁,编译一切正常,双击 exe 却弹「缺少 api-ms-win-crt-runtime-l1-1-0.dll」,很容易误判成包的问题,实际是系统缺运行库。补丁装完要重启,重启之后再谈别的。
再确认有没有旧工具链。机器上装过 Anaconda、某些 IDE 自带编译器、旧版 MinGW 的,Path里往往已经有一份gcc.exe。装新包之前先开一个干净的终端跑一句:
where.exe gcc输出为空最省心;输出多行说明已经有别的 gcc 在 Path 上,记下它们的路径,后面配 Path 时要么删掉,要么把新的排在前面。这一步做过,后面九成的「版本对不上」都不会发生。
下载完成后先验一遍完整性,压缩包几百 MB,网络抖一下就可能损坏,解压时只报一句笼统的「数据错误」,很难判断是包坏了还是 7-Zip 版本太老:
# 用 SHA256 核对发布页给出的摘要,不一致就重下,不要试着修复 Get-FileHash .\mingw64-x86-64-15.2.0-release-win32-seh-ucrt-rt-v13-rev0.7z -Algorithm SHA256Get-FileHash默认就是 SHA256,输出里那串十六进制和发布页贴的对比,逐字符看,别看头尾。哈希不一致没什么可修的,重新下一次,比在解压报错上猜半小时划算。
2.3 和 MSYS2、w64devkit 的取舍
同样是在 Windows 上要一个 gcc,路径不止一条。MSYS2 带 pacman,能装一大堆预编译库,生态最完整,代价是它会往Path里塞usr\bin,那里有自己的 gcc、自己的 make、自己的动态库,和这套独立包放在一起就是一场持久战——where gcc出三行、链接时找不到-lstdc++、CMake 挑错编译器,基本都是这么来的。w64devkit 更小更省事,一个压缩包解压完就带 make 和 busybox 风格的工具,版本节奏和这套不一样。
我自己的取舍是:做纯 Windows 原生开发、依赖不多的项目,用这套独立包,目录干净、可以同时放好几个版本、删掉不留痕迹;项目一旦要拉 Qt、OpenSSL、各种预编译二进制,再上 MSYS2,把两套分开装在不同盘符,Path 只留当前要用的那一条。最忌讳的是两套都挂在 Path 上,让系统去猜——它猜错了不会告诉你。
3. 解压落盘:路径、校验与目录结构
这套包没有安装器,双击一路下一步那套流程不存在,它就是个 7z 压缩包。好处是随时能删、随时能换版本;坏处是目录结构、Path、make 的名字全得自己定,定错了后面每一步都在还债。解压这一步看着最没技术含量,实际上决定了后面能不能顺利跑通。
3.1 目录规划:为什么我一律放 C:\dev\mingw64
三条硬性要求:路径短、无空格、无中文。空格会让一部分构建脚本的参数解析出错,中文在某些代码页下会变成乱码进而导致gcc: error: CreateProcess: No such file or directory——这个报错极其误导,你会以为是缺文件,其实是路径编码问题。再加一条:不要放C:\Program Files下面,那里的写权限受限,gcc生成临时文件、make写中间产物时会被拦,报错信息往往只有一句Permission denied。
我用的布局是这样,版本号只出现在父目录,当前在用的统一叫mingw64,切换版本时改 Path 或者改目录名都行:
C:\dev\ ├─ mingw64\ # 当前在用:15.2.0 win32-seh-ucrt ├─ mingw64-15.2.0-posix\ # 备选:需要 std::thread 时切过去 └─ src\ # 自己的代码,和工具链分开这么分的好处很实在:项目里的 CMake 缓存、VS Code 的compilerPath、脚本里写死的路径,都可以写死成C:\dev\mingw64,换版本时不用改一堆配置文件。备选目录那一份不用天天挂 Path 上,需要的时候临时切,用完切回来。注意别把代码目录塞进工具链目录里,将来删旧版本时容易连带删掉东西。
3.2 用 7-Zip 解压并核对关键文件
Windows 资源管理器不认.7z,得装 7-Zip。解压命令和参数如下:
# 1. 建父目录(已存在就跳过) New-Item -ItemType Directory -Force C:\dev | Out-Null # 2. 解压:x 表示按原路径展开,-o 指定输出目录,-y 表示全部覆盖不询问 & 'C:\Program Files\7-Zip\7z.exe' x ` .\mingw64-x86-64-15.2.0-release-win32-seh-ucrt-rt-v13-rev0.7z ` -oC:\dev -y # 3. 确认编译器落在预期位置 Test-Path C:\dev\mingw64\bin\gcc.exe三个参数里最容易写错的是-o:它和路径之间不能有空格,写成-o C:\dev会被当成两个参数,7-Zip 直接把内容解到当前目录,事后满硬盘找gcc.exe。x和e的区别也要留意,e会把所有文件平铺到一个目录里,整个目录结构就毁了,必须用x。
解压出来的压缩包一般自带一层mingw64目录,所以-oC:\dev之后正好得到C:\dev\mingw64。如果多套了一层,进目录看一眼再决定要不要挪。接着核对关键文件,这套包被清理过或者解压不完整时,bin下会缺东西:
| 路径 | 作用 | 能不能删 |
|---|---|---|
bin\gcc.exe | C 编译器驱动 | 不能 |
bin\g++.exe | C++ 编译器驱动 | 不能 |
bin\gdb.exe | 调试器 | 不调试可删,建议留着 |
bin\mingw32-make.exe | GNU Make 的 Windows 命名版 | 不能,构建要用 |
bin\libstdc++-6.dll | C++ 标准库动态库 | 不能删,发布时可静态链接绕开 |
bin\libgcc_s_seh-1.dll | GCC 底层运行时,SEH 版 | 同上 |
bin\libwinpthread-1.dll | 线程与同步封装 | 这套是 win32 模型,但 gcc 自身工具有依赖,别手工清 |
x86_64-w64-mingw32\include | 系统头文件 | 不能 |
x86_64-w64-mingw32\lib | 导入库与静态库 | 不能 |
这里有个反复出现的事故:有人为了「精简」,把bin下的 dll 全删了,只留 exe。结果gcc.exe一跑就报找不到libisl或者libmpc之类的库——编译器本身也是动态链接的,那些 dll 是它自己的口粮。要瘦身可以删share下的文档、lib\gcc下的多版本目录,但bin里的东西一个都别动。
3.3 一次性目录清理和 make 改名
解压完做两件小事,后面省很多麻烦。
第一件,把mingw32-make.exe复制一份改名成make.exe:
# 很多 Makefile、构建脚本、老教程里直接写 make,多一份副本免得改脚本 Copy-Item C:\dev\mingw64\bin\mingw32-make.exe C:\dev\mingw64\bin\make.exe为什么叫mingw32-make而不是make,是历史原因——当年为了不和系统里别的make撞名。但大量现成的 Makefile、build.bat、CI 脚本里写的就是make,报'make' 不是内部或外部命令时,新手会以为没装 make。复制一份改名最省事,两个名字都能用,不占多少空间。
第二件,留个后悔药。把原始压缩包单独存一份到别的盘,或者至少记下版本号和下载来源。将来需要换 posix 版本、换更新的修订号,或者同事要一套同样的环境,直接复制解压,不用重新找发布页。我自己吃过这个亏:为了省空间删了包,半年后要复现一个老项目的构建环境,翻了半天才对上版本号。
4. 配置环境变量:Path 的三条写法和一个截断坑
到这一步编译器已经在硬盘上了,但终端还不知道它在哪。配 Path 是整个流程里最容易留下隐患的一步,图形界面、setx、PowerShell 三种写法效果不一样,其中一种会静默毁掉你的用户变量。
4.1 图形界面和 setx 的差别:后者会截断
先说结论:用图形界面或者 PowerShell 的 API,别用setx。
setx有两个硬伤。一是它写的是用户变量,字符数超过 1024 会被直接截断,而且不报错,你以为写成功了,重启终端发现后半截路径全没了。二是很多人图省事这么写:
# 不建议:展开 %PATH% 会把系统 Path 和用户 Path 拼在一起,整串塞进用户变量 setx PATH "%PATH%;C:\dev\mingw64\bin"%PATH%展开时是「系统 Path + 用户 Path」的合并结果,这条命令把合并后的整串写进了用户变量。后果是系统 Path 被复制了一份进用户变量,以后系统层面装了新工具,用户变量里那份是旧的;删掉某个系统目录时,用户变量里还留着,Path 就开始玄学——同一个命令,在 cmd 里能跑,在 VS Code 的终端里找不到。
图形界面的路径是:Win + R输sysdm.cpl→ 高级 → 环境变量 → 在「用户变量」里选中Path→ 编辑 → 新建一行C:\dev\mingw64\bin。注意是新建一行,不是在原来的字符串末尾手工拼分号,拼错了会形成空路径项,某些老程序解析 Path 时会在这里卡住。
4.2 PowerShell 写法与当前会话生效
想脚本化、或者要装到多台机器上,用 PowerShell 调 .NET 的接口,它不会截断,也不会把系统变量混进来:
$bin = 'C:\dev\mingw64\bin' # 读用户级 Path(不是合并后的 $env:Path) $old = [Environment]::GetEnvironmentVariable('Path', 'User') if (($old -split ';') -notcontains $bin) { $new = if ([string]::IsNullOrWhiteSpace($old)) { $bin } else { $old.TrimEnd(';') + ';' + $bin } [Environment]::SetEnvironmentVariable('Path', $new, 'User') Write-Host "已追加: $bin" } else { Write-Host "已存在,跳过" }逐句说清楚。GetEnvironmentVariable('Path','User')的第二个参数指定作用域,取的是用户变量本身,不是$env:Path那个合并视图,这是和%PATH%写法最本质的区别。-split ';'拆成数组再判断,避免同一个路径被重复追加——重复项多了 Path 会超出长度限制。TrimEnd(';')处理末尾已经有分号的情况,否则拼出;;。写回时同样指定User,不动系统变量。
当前已经打开的终端不会自动刷新,要么关掉重开,要么临时改会话变量:
# 只在当前窗口生效,关掉就没了,适合临时验证 $env:Path = "C:\dev\mingw64\bin;" + $env:Path注意这里是把新路径拼在前面而不是后面。顺序在 Path 里是有意义的:靠前的先命中。机器上如果还有别的 gcc,把C:\dev\mingw64\bin放在最前面,等于强制优先用这一套,比挨个删旧路径稳妥。验证一下:
where.exe gcc期望只输出一行C:\dev\mingw64\bin\gcc.exe。输出多行,说明还有别的工具链在 Path 上,回头按 2.2 记下的路径去清理,或者把这一条往上挪。
4.3 用 gcc -v 核对三行
Path 通了之后别急着写代码,先跑一次版本查询,输出里三行信息必须和你下载的文件名对得上:
gcc -v正常情况下能看到类似这样的内容(不同修订号细节略有差异):
Target: x86_64-w64-mingw32 Thread model: win32 gcc version 15.2.0 (GCC)Target对应文件名里的mingw64和x86-64,必须是x86_64-w64-mingw32,出现i686就是拿错包了。Thread model对应win32,这一行是判断能不能用std::thread的唯一依据,别靠猜。gcc version对应15.2.0,如果显示的是别的版本号,说明命中的是另一套工具链,回 4.2 查 Path 顺序。
再补两条定位用的命令:
# 三元组,和 Target 一致 g++ -dumpmachine # 该版本实际使用的 C++ 标准库动态库位置,用于确认 bin 目录没被拆散 g++ -print-file-name=libstdc++-6.dll-print-file-name会输出一个绝对路径,指向C:\dev\mingw64\bin\libstdc++-6.dll。如果它只回显一个文件名,说明库没找到,通常是被「精简」过。
还有个流传很广的误区:网上很多讲交叉编译的文章会让你设C_INCLUDE_PATH、LIBRARY_PATH、CPLUS_INCLUDE_PATH。这套包不需要。gcc 会以自身可执行文件的位置为基准,去../x86_64-w64-mingw32/include和../x86_64-w64-mingw32/lib找头文件和库。手工设这些变量反而会打乱搜索顺序,把系统头文件顶掉,症状是编一个#include <stdio.h>都报找不到——我见过有人为此重装了三遍系统。
5. 避坑与排查:五条装完才暴露的错
装完之后能编译 hello world,不等于环境就对了。下面五条都是我自己或者身边的人真实遇到过的,按「现象 → 原因 → 解决」记,方便对照。
5.1 编译期:std::thread 那一行红字
现象:代码里写了std::thread、std::mutex或者std::async,编译报'thread' in namespace 'std' does not name a type,或者'mutex' has not been declared。同一份代码换台机器、换个编译器又能过。
原因:这套包的线程模型是win32,libstdc++ 在构建时没有开启_GLIBCXX_HAS_GTHREADS,C++ 标准库的并发组件整块不存在。注意它是编译期就失败,不是链接期,所以看链接命令、看-pthread参数都找不到问题。
解决:要么换线程模型为posix的同版本包(文件名里win32换成posix),两个目录并存,改 Path 顺序切换;要么在代码层面绕开标准库并发组件,直接用 Windows 线程 API,或者引入项目自带的线程封装库。判断项目要不要 posix 很简单:搜一遍源码里有没有std::thread、std::mutex、std::condition_variable、std::future,有就得换。纯计算、单线程的命令行工具用 win32 版没问题,产物还少一个 dll 依赖。
5.2 运行期:三个 DLL 和一堆方块
现象一:自己机器上双击 exe 正常,拷到同事机器或者测试服务器上,弹窗「无法启动此程序,因为计算机中丢失 libstdc++-6.dll」。换成libgcc_s_seh-1.dll、libwinpthread-1.dll也是同样的剧本。
原因:gcc 默认动态链接自己的运行库,这几个 dll 躺在C:\dev\mingw64\bin里。你的机器 Path 上有这个目录,加载器能找到;别人机器上没有,就找不着。很多人在开发机上测通了就以为没事,交付时才炸。
解决:编译时显式静态链接 GCC 运行时:
# 只静态链接 gcc 运行时,系统库仍然动态,产物增量小,兼容性够用 g++ -std=c++20 -O2 main.cpp -o app.exe -static-libgcc -static-libstdc++ # 全静态,连 libwinpthread 之类一起打进去,产物最大,最省心 g++ -std=c++20 -O2 main.cpp -o app-static.exe -static-static-libgcc和-static-libstdc++分别管 GCC 底层运行时和 C++ 标准库,这两个加上之后,普通的单文件程序基本不再依赖 MinGW 目录下的任何 dll。-static是更彻底的做法,把能静态的都静态掉,代价是产物变大、某些依赖动态加载的库(比如插件式加载的模块)要单独处理。验证方式在下一章。
现象二:printf("中文")在 cmd 里输出一堆方块或者问号;或者源文件里带中文注释,编译报stray '\357' in program。
原因:源文件存的是 UTF-8,GCC 默认输入字符集和执行字符集都是 UTF-8,而 cmd 默认代码页是 936(GBK)。两边对不上,要么是编译期解析注释出错,要么是运行期终端把 UTF-8 字节按 GBK 解释。
解决:两条路,选一条统一就行。一是让 exe 输出 GBK,编译时加-fexec-charset=GBK,终端不用动;二是把终端切成 UTF-8,chcp 65001,然后在 VS Code 的settings.json里把终端编码固定住。我一般选第二条,因为跨平台项目里源文件统一 UTF-8 更省事,切终端只是本地习惯。
5.3 环境层:Path 和多工具链打架
现象一:gcc -v显示的版本不是刚解压的那个,或者编译时报fatal error: bits/c++config.h: No such file or directory——这个头文件明明在包里躺着。
原因:机器上存在多套工具链,Path里靠前的那一套先被命中。头文件找不到是因为 gcc 驱动来自 A 版本,而搜索路径按 A 的位置算,实际的头文件属于 B 版本,两边错位。
解决:where.exe gcc列出全部命中项,逐个确认来源。把不用的从用户 Path 里删掉,或者把C:\dev\mingw64\bin上移到最前。改完必须重开终端,已经打开的窗口读的是旧环境。清理完再跑一次where.exe gcc,只留一行才算干净。
现象二:Path 莫名其妙少了一截,某些系统目录不见了,一部分命令在 cmd 里能跑、在 PowerShell 里报找不到。
原因:之前用setx PATH "%PATH%;..."写过,用户变量被 1024 字符截断,或者系统 Path 被复制进了用户变量。
解决:打开注册表编辑器,看HKEY_CURRENT_USER\Environment下Path的原始值,把被截断的后半段补回来;系统级的看HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment,那个不要随便动。补完之后按 4.2 的 PowerShell 写法维护,别再碰setx。这类事故恢复起来麻烦,最好的办法是改之前先导出一次用户变量做备份。
6. 装完怎么验证:三分钟跑通一个多文件 C++20 demo
单文件 hello world 只能证明gcc.exe能跑,证明不了头文件路径、库路径、链接器、C++ 标准库这几件事都对。我一般用一个最小的多文件工程做验收,三分钟能跑完,出问题也能立刻定位到是哪一层。
6.1 命令行三步自检
先准备两个源文件和一个头文件,故意跨文件调用,顺便用一个 C++20 特性:
// mathx.h #pragma once #include <vector> int sum_even(const std::vector<int>& v); // 跨文件调用,验证头文件和链接// mathx.cpp #include "mathx.h" int sum_even(const std::vector<int>& v) { int s = 0; for (int x : v) if (x % 2 == 0) s += x; return s; }// main.cpp #include <cstdio> #include <vector> #include <ranges> // C++20,用来确认标准版本真的开了 #include "mathx.h" int main() { std::vector<int> v{1, 2, 3, 4, 5, 6}; auto even = v | std::views::filter([](int x){ return x % 2 == 0; }); int s = 0; for (int x : even) s += x; std::printf("sum=%d, cross-file=%d\n", s, sum_even(v)); return s == sum_even(v) ? 0 : 1; }编译并运行,然后看依赖:
# 1) 编译:开 C++20、 <p> <a href="https://download.csdn.net/download/admans/91941352" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>