简介:MinGW-w64 v12.0.0 是 GNU GCC 编译器在 Windows 平台的精简移植版,面向需要在 Windows 下编写 C/C++ 程序、又习惯 Linux 开发工具链的开发者。相比 Cygwin,它不依赖第三方 C 运行时库,体积更小,安装后即可将源码直接编译为 Windows 可执行程序,同时支持部分 GNU 工具,适合本地编译调试。压缩包共 2000 个文件,其中 1676 个头文件和 276 个 C 源码文件构成核心代码库,另有 HTML、TXT、Shell 脚本、CSS 等辅助文档与配置内容,整体仅 16.75MB。资源详细展示了从基础类型定义到线程、互斥锁、条件变量、动态库加载等底层实现,对理解 Windows 下 GCC 编译原理和 MinGW 内部机制有直接帮助。已有 565 人学习下载,适合 C 语言学习者、跨平台开发者及需在 Windows 平台复现 Linux 编译环境的技术人员。
1. 为什么还在折腾 MinGW-w64:GCC 的 Windows 版到底解决什么问题
mingw-w64 v12.0.0 不是一个小插件,它是把 GCC 编译器完整搬到 Windows 的那套工具链。要在 Windows 上写 C 语言、又希望编译出的 .exe 不依赖庞大的虚拟机或模拟层,这就是最直接的一条路:源代码还是那份源代码,gcc、g++、gdb、mingw32-make 全部变成 Windows 原生程序,链接时调用 Win32 API,最终产物就是一个普通 Windows 进程。适合三类人:在 Windows 上做 C 开发的新手、需要把 Linux 下 C 代码交付给 Windows 用户的从业者、以及用 Matlab 跑 mex 但系统里缺 C 编译器的工程师。它省掉的不只是安装时间,而是整个"在 Windows 上编译 C 代码"这件事的复杂度。
2. 从 zip 到可执行文件:装对 mingw-w64 的路径与环境变量
2.1 先认版本号:v12.0.0 对应什么,压缩包该选哪种
mingw-w64 本身是源码项目,v12.0.0 指的是 mingw-w64 项目这一次发布的大版本,内部配套的 GCC 主版本、binutils、运行时头文件是打通的。平时下载到的内容其实分两类:一类是源码包,拿到手要自己用 configure 和 make 编译,适合做二次开发或交叉编译研究;另一类是别人预先编好的二进制发行版,解压就能用。绝大多数场景直接选二进制包就好。
选二进制包时要盯三个参数,它们决定你后面会不会翻车:
| 参数 | 常见选项 | 我的选择依据 |
|---|---|---|
| 位数 | x86_64 / i686 | 现代的 Windows 几乎都是 64 位,直接选 x86_64;只有要交付给老 32 位系统时才用 i686 |
| 线程模型 | posix / win32 | 选 posix,否则 std::thread 和 pthread 相关代码会编译失败 |
| 异常模型 | seh / dwarf / sjlj | x86_64 选 seh;i686 选 dwarf 更常见 |
这里最容易忽视的是线程模型。很多下载站默认给的是 win32 版本,理由是"Windows 上用 Windows 线程模型更原生"。这个说法对纯 C 项目影响不大,但只要你的代码里出现 pthread 或者 C++ 标准库里的 std::thread,win32 版本就会在编译期报出各种未定义符号。我的习惯是无论做什么项目都选 posix,哪怕暂时用不到线程,也不给自己留隐患。
2.2 解压和环境变量:让 gcc 在任意目录都能被调用
假设解压到 C:\mingw64。压缩包内部的目录结构是 bin、include、lib 分层,其中最关键的是把 C:\mingw64\bin 加进 PATH。命令行里验证:
setx PATH "%PATH%;C:\mingw64\bin" gcc --versionsetx 有两个明显的坑:第一,它会截断超过 1024 字符的 PATH,如果 PATH 本来就长,加完反而把后面的内容弄丢了;第二,setx 只写环境变量,当前已经打开的终端不会生效,必须重新开一个 cmd 才能读到。所以更稳妥的做法是打开系统属性里的环境变量编辑框,手动追加 C:\mingw64\bin,然后重新开终端验证。
如果你用的是 Git Bash 或 MSYS2,又不想动系统 PATH,可以在当前会话里临时追加。这样只对当前终端有效,适合同时测试多个版本的工具链:
export PATH=/c/mingw64/bin:$PATH which gcc这条命令把 mingw64 的 bin 放在 PATH 最前面,which gcc确认找到的是哪个 gcc。如果你装了多套工具链,这一步能立刻看出当前终端实际用的是哪一个。接着写一个 hello.c 验证整个编译链路:
#include <stdio.h> int main(void) { printf("mingw-w64 %s\n", __VERSION__); return 0; }gcc hello.c -o hello.exe hello.exegcc hello.c -o hello.exe的含义是:对 hello.c 做预处理、编译、汇编、链接,-o指定输出文件名;不写-o默认生成 a.exe。如果输出里出现 GCC 版本号,说明 gcc 能找到头文件,链接器也正常。这一关过了,后面碰到的报错基本都是路径或版本混搭问题,不再是工具链本身的问题。
2.3 解压目录里谁在起作用:bin、lib、include 的分工
bin 下面是可以直接在命令行调用的程序:gcc.exe 是 C 编译器前端,g++.exe 是 C++ 前端,mingw32-make.exe 是 make 工具的 Windows 版本,gdb.exe 是调试器,还有一批 DLL。include 里放的是头文件,C 标准库和 Win32 API 的头都在这里。lib 里是导入库和静态库,Windows 的 DLL 导入对应的是 .a 文件。x86_64-w64-mingw32 这个子目录是目标平台相关的运行时配置,交叉编译时这个概念尤其关键。
我想让你养成一个习惯:装完立刻跑一次gcc -v,看输出里 Target 那一行。如果是 x86_64-w64-mingw32,说明是 64 位编译器;i686-w64-mingw32 则是 32 位。后面遇到任何"架构不匹配"的报错,回来看这一行就能定位。还有一个值得开的目录是 bin 里的 DLL 清单。libgcc_s_seh-1.dll、libstdc++-6.dll、libwinpthread-1.dll 这三个文件决定了你编出的 exe 是否能在别人电脑上直接运行,它们和动态链接模型有关,下一章专门讲。
3. 链接期与运行期:把 DLL 依赖和静态链接一次说透
3.1 为什么你的 exe 离不开那几个 DLL
用 objdump 查看 exe 的导入表,可以看到这个程序依赖哪些 DLL:
objdump -p hello.exe | grep "DLL Name"MinGW-w64 默认采用动态链接方式。hello.exe 的导入表里会出现 libgcc_s_seh-1.dll、libwinpthread-1.dll 等名字。libgcc_s_seh-1.dll 提供异常处理和编译器内部辅助函数,libstdc++-6.dll 是 C++ 标准库,libwinpthread-1.dll 是 POSIX 线程实现。你把 exe 拷贝到一台没有安装 MinGW-w64 的机器上,双击就会弹出"找不到 libwinpthread-1.dll"。
这个问题的根源在于 Windows 不像 Linux 那样系统级统一管理 C 运行库。Linux 上 glibc 是所有程序共享的,而 Windows 上每个构建工具链有自己的运行库分发方式。VC 系列编译器把运行库拆成 vcruntime 和 universal CRT,而 MinGW 则把这些库拆成上面几个 DLL。你编译时需要决定:动态链接,运行时依赖外部 DLL;静态链接,把库代码直接并进 exe。
3.2 -static 和 -static-libgcc/-static-libstdc++ 怎么选
常用命令是这两条:
gcc -static hello.c -o hello.exe gcc -static-libgcc -static-libstdc++ hello.c -o hello.exe第一条把 libgcc、libstdc++、libwinpthread 全部静态链进 exe,交付时只需要带一个文件。第二条只把 libgcc 和 libstdc++ 静态链入,libwinpthread 仍然动态链接。参数区别在于:-static是大包干,副作用是如果项目里还有第三方静态库,可能引入重定义或地址重定位问题;-static-libgcc -static-libstdc++更精细,适合主要依赖 C++ 标准库、但第三方库需要保持动态加载的工程。
我一般对内部小工具直接用-static,省得分发时带一堆 DLL;对要配合插件体系交付的程序,用-static-libgcc -static-libstdc++并单独拷贝 libwinpthread-1.dll。还有第三种做法就是不去静态链,直接把 bin 目录里的三个 DLL 复制到 exe 同目录,一起分发。这个方案最省心,DLL 版本升级时只需替换文件,不需要重新编译。
3.3 pthread 层:winpthreads 是怎么把 POSIX 线程搬过来的
MinGW-w64 源码包里有一批测试文件:thread.c、cond.c、mutex.c、rwlock.c、framebased.c、benchtest2.c。它们都是 winpthreads 的测试用例,验证 POSIX 线程 API 在 Windows 上是否正常工作。winpthreads 是 POSIX 线程在 Windows 上的实现层,当你编译时加-pthread,GCC 会链接到 libwinpthread。
这层封装把 pthread_mutex_lock、pthread_cond_wait、pthread_rwlock_rdlock 这些 API 映射成 Windows 的原生机制:mutex 对应 CRITICAL_SECTION,条件变量对应 Windows 的 CONDITION_VARIABLE,rwlock 用 SRWLock 实现,framebased.c 则验证基于这些原语实现的框架行为。用户代码不需要感知底层是 Windows 原生机制还是 POSIX 模拟。
线程模型选 posix 的意义就在这里:GCC 的头文件、libstdc++ 的线程实现、你自己写的 pthread 代码,全部统一走这层封装。如果选了 win32 线程模型,C++ 标准库的 std::thread 会退化成不可用状态,因为 libstdc++ 在 Windows 上依赖 winpthread 提供 pthread 语义。这也是我一直强调选 posix 的原因,它不是性能问题,是能不能编译过的问题。
4. 避坑清单:mingw-w64 最常见的五个翻车现场
4.1 gcc 不是内部命令,但明明装了
现象:在 cmd 里输入 gcc 提示"'gcc' 不是内部或外部命令";VSCode 里 task.json 配好了,按 F5 依然报同样的错。原因:bin 目录没有加进 PATH,或者 setx 写入后没有重新打开终端。环境变量只对之后启动的进程生效,已经打开的 cmd 和 VSCode 读到的都是旧值。解决:重新打开终端,先运行echo %PATH%看有没有 C:\mingw64\bin;如果有,再确认 VSCode 是彻底退出后重启的。如果 PATH 里同时存在多个版本的 mingw,前面的那个会拦截后面的。我的习惯是在 VSCode 的 settings.json 里显式指定编译器的绝对路径,不让 IDE 依赖系统 PATH,这样即使 PATH 出现问题,编译任务也能照常跑。
4.2 编译通过,双击 exe 提示缺少 libwinpthread-1.dll
现象:开发机上运行正常,换一台干净的 Windows 机器双击就报找不到 DLL。原因:编译时用了动态链接,而开发机上 mingw64 的 bin 目录在 PATH 里,运行 exe 时系统能找到 libwinpthread-1.dll;目标机器没有这个目录。解决:三个方案任选。编译加-static把所有运行库打进去,或者把 bin 目录下 libgcc_s_seh-1.dll、libstdc++-6.dll、libwinpthread-1.dll 三个文件复制到 exe 同目录,也可以用-static-libgcc -static-libstdc++只保留 libwinpthread-1.dll 的动态依赖。我交付前必跑一次objdump -p app.exe | grep "DLL Name",确认导入表里剩下的 DLL 都是目标机器上一定会有的系统 DLL,而不是某个工具链专属文件。
4.3 32 位与 64 位混装导致的链接器架构不匹配
现象:编译时提示 "skipping incompatible ... 文件格式无法识别",随后跟着一堆 undefined reference。原因:下载了 i686 版本的压缩包,却在编译命令行里混入了 x86_64 的 lib 路径;或者两个版本解压到同一个目录,include 和 lib 互相覆盖。这类问题在安装过多个版本、后来又手动改过 PATH 的环境里特别常见。解决:确认当前 gcc 的 target 字段,64 位输出 x86_64-w64-mingw32,32 位输出 i686-w64-mingw32,然后只保留一个版本。如果你确实需要同时使用两个位数,用一个单独的目录放 32 位工具链,编译时用完整路径调用对应 gcc,不要依赖 PATH 去猜。还有一点容易被忽略:从源码包自己编译第三方库时,configure 的 --host 参数必须和工具链位数一致,拿 64 位 gcc 编 32 位库,编出来的静态库链接时会报一堆莫名其妙的重定义。
4.4 把 Cygwin 的 include 混进 MinGW 的编译命令
现象:编译自己项目时 undefined reference 出现一大堆,甚至"找不到 stdio.h",但明明 include 目录里就有这个头文件。原因:PATH 里同时有 C:\Cygwin\bin 和 C:\mingw64\bin,前面那套工具的 gcc 被优先找到。Cygwin 的 gcc 是 Cygwin 环境下的移植版,编译出的程序依赖 cygwin1.dll;它的头文件路径和 lib 路径与 MinGW-w64 完全不兼容,两者混用就是灾难。解决:构建时不要让两个 bin 同时出现在 PATH 里。命令行做项目构建时,我在脚本开头把 PATH 固定成只有 C:\mingw64\bin;IDE 里则在 tasks.json 里写死编译器绝对路径。验证方法很简单:gcc -dumpmachine,MinGW 输出 x86_64-w64-mingw32,Cygwin 输出 x86_64-pc-cygwin,一眼就能分辨。
4.5 Matlab 一直报"未找到受支持的编译器"
现象:mex -setup 提示 "No supported compiler was found",装了 mingw-w64 仍然识别不到。原因:Matlab 对编译器的检测很死板,它只在特定版本范围内去找 MinGW-w64,而且要求对应的 bin 目录在 PATH 里。R2021b 前后要求 GCC 主版本在特定区间,装最新的 v12.0.0 反而不一定被认。解决:先查清楚当前 Matlab 版本对应的受支持 MinGW-w64 版本区间,装对应版本;确认 bin 已加入 PATH;然后运行mex -setup C。如果还识别不到,在 Matlab 命令行里设置环境变量setenv('MW_MINGW64_LOC', 'C:\mingw64'),让编译器定位逻辑能直接找到工具链。这段不想展开太多,但我想提醒一点:不要迷信"最新版本",Matlab 的编译器支持矩阵是固定的,装一个支持范围内的版本比装最新版省事得多。
5. 把源码包里那堆 C 文件利用起来:winpthreads 的验证与使用边界
5.1 这批 .c 文件是哪来的,怎么快速用起来
mingw-w64 源码包里会出现一批熟悉的名字:thread.c、mutex.c、cond.c、rwlock.c、framebased.c、benchtest2.c,还有 m_ms.c、m_token.c、dll_math.c 这类配套验证文件。thread.c、mutex.c、cond.c、rwlock.c 分别对应 POSIX 线程里线程创建、互斥锁、条件变量、读写锁的单项测试;benchtest2.c 是压力测试;m_ms.c 和 m_token.c 是消息队列和词法解析类的小工具,验证线程间通信和字符串处理在 Windows 上的行为,dll_math.c 则测试 DLL 导出函数的链接正确性。
我不建议逐行读这些文件,它们的最佳用途是当回归测试集。你装完一个新的 mingw-w64 发行版之后,把 cond.c、mutex.c、thread.c 编一遍跑一遍,退出码为 0 就说明这一层线程库没装歪。它同时也是行为参照:想知道 pthread_cond_wait 在这个实现里是否会 spurious wakeup,直接看 cond.c 的测试写法比查文档更快。
5.2 写一个 m_ms 风格的消费者模型
m_ms.c 这类消息队列测试的核心套路是 mutex 加 cond,我重写一个贴近实际工程的版本:三个生产者线程往环形队列塞整数,消费者线程拿到就打印。编译命令统一-pthread:
#include <stdio.h> #include <string.h> #include <pthread.h> #define MSG_NUM 6 typedef struct { int buf[MSG_NUM]; int head, tail, count; pthread_mutex_t lock; pthread_cond_t not_empty; } msg_queue_t; static msg_queue_t q; void queue_init(msg_queue_t *q2) { memset(q2, 0, sizeof(*q2)); pthread_mutex_init(&q2->lock, NULL); pthread_cond_init(&q2->not_empty, NULL); } void *producer(void *arg) { int base = *(int *)arg; for (int i = 0; i < MSG_NUM; i++) { pthread_mutex_lock(&q.lock); q.buf[q.tail] = base + i; q.tail = (q.tail + 1) % MSG_NUM; q.count++; pthread_cond_signal(&q.not_empty); pthread_mutex_unlock(&q.lock); } return NULL; } void *consumer(void *arg) { (void)arg; for (int i = 0; i < MSG_NUM * 2; i++) { pthread_mutex_lock(&q.lock); while (q.count == 0) pthread_cond_wait(&q.not_empty, &q.lock); int v = q.buf[q.head]; q.head = (q.head + 1) % MSG_NUM; q.count--; printf("recv: %d\n", v); pthread_mutex_unlock(&q.lock); } return NULL; }这段逻辑里,count 表示队列中待消费的消息数,tail 是写入位置,head 是读取位置,环形缓冲避免数组越界。消费者进入 wait 前必须持有 mutex,而 pthread_cond_wait 内部会原子地释放锁并进入睡眠,被唤醒后再重新获取锁。注意我用的是while (q.count == 0)而不是if,这是条件变量的标准写法——避免 spurious wakeup 导致的空读。
编译参数说明:-pthread让 GCC 定义 _REENTRANT 宏并链接 libwinpthread;手写-lwinpthread也能链上,但-pthread同时负责编译期宏定义,所以用-pthread更稳。如果要把线程模型相关的宏调出来,可以在代码里加#ifdef _REENTRANT确认这个宏是否被定义。
5.3 用自己的程序跑回归:把测试文件当验证工具
调用方式是把测试文件单独编译运行:
gcc -O2 -pthread cond.c -o cond_test.exe && cond_test.exe gcc -O2 -pthread benchtest2.c -o bench2.exe && bench2.exe观察点:cond_test 正常退出且退出码为 0;bench2 打印吞吐结论信息,没有 assertion failed。如果失败,多半是 _WIN32_WINNT 版本宏不对。winpthreads 的部分异步原语会按这个宏的取值决定调用哪些 Windows API,版本给低了条件变量退化成自旋循环,给高了在老系统上无法运行。解决办法是显式指定:
gcc -D_WIN32_WINNT=0x0601 -pthread cond.c -o cond_test.exe0x0601 对应 Windows 7,这是兼容性和功能之间的平衡点。如果目标环境是 Windows 10,可以直接用 0x0A00。我给一个工程补这个宏时踩过坑:同样的代码在一台 Windows 10 上跑得飞快,换到 Windows 7 测试机上直接卡死,最后对比线程库实现才发现是 API 版本宏的问题。从那以后,凡是涉及 winpthreads 的工程,编译命令里都显式带上 _WIN32_WINNT,不依赖系统默认值。
6. 把自带的测试套件全量编一遍:确认工具链没有装歪
全量编译脚本可以这样写,把 winpthreads 源码目录下的测试文件循环编译:
for f in thread.c mutex.c cond.c rwlock.c framebased.c benchtest2.c; do if gcc -O2 -pthread "$f" -o "${f%.c}_test.exe" 2>err.log; then echo "PASS $f" else echo "FAIL $f" cat err.log fi done脚本逻辑:逐个编译测试源文件,失败时打印错误日志。${f%.c}_test.exe是 bash 的变量替换,把 thread.c 替换成 thread_test.exe,避免生成同名文件互相覆盖。如果全部 PASS,说明 gcc 前端、ld 链接器、libwinpthread、C 运行库这一整条链都正常。跑完编译测试后,我会再抽查一个 exe 的导入表,确认没有异常依赖。
这个五分钟的验证流程,比后面编译期和运行期反复折腾划算太多。我养成的习惯是装完任何 mingw-w64 发行版之后,先把gcc --version、gcc -dumpmachine、线程测试套件这三样全过一遍,都绿了才开始写业务代码。这个习惯来自一次接手别人项目的经历——对方说"环境配好了,你直接编",结果他的 gcc 实际是 Cygwin 版,编出来的程序和项目 CMake 脚本完全不兼容,我花了几乎一天才定位到问题根源是编译器镜像类型不对。从那以后,我每次装完工具链都强制走一遍全量编译测试,确认目标架构、线程模型、运行库三个维度全部正确才算安装有效。希望这次的经验能帮你在 Windows 上把 C 工具链一次装对。
本文还有配套的精品资源,点击获取