1.配置环境
1. 情境 (Situation)
最近我在学习音视频开发,尝试运行一个基于 Qt 和 FFmpeg 的开源播放器项目(HPlayer)。这个项目比较老,是基于 Qt 5.9 和 MSVC 编译器 开发的,而我的开发环境是 Qt 6 和 MinGW 编译器。"
2. 任务 (Task)
"我的目标是解决环境不兼容的问题,让这个项目在我的 MinGW 64-bit 环境下成功编译并运行。
3. 行动 (Action)
"我遇到了三个阶段的阻碍,我是这样一步步解决的:
-
第一步:版本迁移与预处理修复 。
首先我发现 Qt 6 删除了很多 Qt 5 的模块,导致大量报错。我决定降级安装 Qt 5.12 LTS 以保证兼容性。在编译初期,遇到了 sprintf 未定义的错误,我通过引入 <cstdio> 头文件解决了预处理阶段的问题。
-
第二步:解决编译器 ABI 不兼容(链接阶段) 。
这是最棘手的部分。项目自带的第三方库(FFmpeg 和 SDL2)是 MSVC 格式(.lib) 的,而我使用的是 MinGW 编译器,导致链接器报错 undefined reference 和 cannot find -lSDL2。
我意识到这是二进制接口(ABI)不兼容 导致的。我没有修改源码逻辑,而是去下载了 MinGW 版本的 FFmpeg 和 SDL2 开发包 ,替换了原来的库文件。
同时,我重写了 .pro 构建脚本(QMake),修正了库文件的搜索路径(-L)和链接顺序(MinGW 对库的链接顺序敏感)。
-
库也是不同编译器编译出来的,如果编译器换了,那么原本用的库也要换成不同版本的。
-
第三步:解决运行时依赖 。
编译成功后,程序启动即崩溃。通过分析,我判断这是动态链接库(DLL)缺失。静态链接阶段虽然通过了,但运行时操作系统找不到对应的 .dll。我手动将 FFmpeg 和 SDL2 的 DLL 文件复制到了可执行文件目录下,程序最终完美运行。"
4. 结果 (Result)
- "最终,我成功复活了这个老项目。通过这次经历,我不仅熟悉了 Qt 的构建系统,更深刻理解了 C++ 从预处理、编译到链接的全过程 ,特别是 静态库与动态库在 Windows 下不同编译器之间的差异。"
5.总结
c++运行的整个流程是预处理,编译,链接和运行。
1. 预处理 (Preprocessing) ------ "整理手稿"
这是流水线的第一步,编译器(cc1/cl)还没真正开始工作 ,主要是预处理器在干活。
-
输入:源代码文件 (.cpp) + 头文件 (.h)。
-
动作 :纯文本替换。它不检查语法,只认以 # 开头的指令。
-
#include (展开) :把头文件的内容原封不动地复制粘贴到 .cpp 文件里。
-
#define (替换):把代码里的宏(比如 #define PI 3.14)全部替换成具体的值。
-
#ifdef (过滤):根据条件决定哪些代码要保留,哪些要删掉(比如 Windows 下删掉 Linux 的代码)。
-
-
输出:一个巨大的、没有任何 # 指令的纯 C++ 文件(通常叫"翻译单元")。
-
为什么需要这一步?
-
为了代码复用。如果没有 #include,你每次想用 printf 都得把那几千行声明重新敲一遍。
-
为了跨平台。通过 #ifdef,一份代码可以在 Windows 和 Linux 上表现出不同的行为。
-
2. 编译 (Compilation) ------ "翻译成汇编"
这是最复杂、最"聪明"的一步。
-
输入:预处理完的纯 C++ 代码。
-
动作 :语法分析 & 翻译。
-
查错:检查你有没有少分号,变量类型对不对。如果有错,直接报错停止。
-
优化:编译器会分析你的逻辑,去掉没用的代码,或者把复杂的计算简化(比如把 a = 2 + 3 直接改成 a = 5)。
-
翻译 :把 C++(高级语言)翻译成 汇编语言(低级助记符)。
-
-
输出:汇编文件 (.s 或 .asm)。里面是类似 MOV EAX, 1 这种指令。
-
为什么需要这一步?
-
因为 C++ 太高级了,CPU 看不懂。
-
汇编语言是人类和机器之间的桥梁,它比二进制可读性强,方便调试和优化。
-
比喻:翻译官把中文手稿翻译成了英文(汇编)。同时修正了原来的语病(语法错误),并把啰嗦的句子改写得更简练(优化)。
3. 汇编 (Assembly) ------ "转成机器码"
这一步比较机械化,由 汇编器 (Assembler) 完成。
-
输入:汇编文件 (.s)。
-
动作 :查表翻译。
- 把汇编指令(如 ADD)一一对应地转换成 CPU 能直接执行的二进制指令(如 01001101)。
-
输出 :目标文件 (Object File),在 Windows 下是 .obj,Linux/MinGW 下是 .o。
-
为什么需要这一步?
-
CPU 是电路做的,它只认识高低电平(0 和 1)。这一步就是把人类的逻辑彻底变成机器的逻辑。
-
注意 :此时生成的 .o 文件是缺胳膊少腿的。它知道"我要调用 printf",但它不知道 printf 在内存的哪个地址,只是留了个空位。
-
比喻:排版员把英文手稿变成了莫斯密码(二进制)。现在你有了一堆散乱的、看不懂的密码纸,每一张纸对应一个 .cpp 文件。
4. 链接 (Linking) ------ "装订成书"
这是最后一步,也是最容易出"找不到文件"错误 的一步。由 链接器 (Linker) 完成。
2. 动态链接 (Dynamic Linking) ------ 引用
-
输入:
-
你自己生成的一堆 .o 文件。
-
第三方库文件(.lib / .a)。
-
-
动作 :地址回填 & 合并。
-
合并:把你所有的 .o 文件拼在一起。
-
找人:当 main.o 里说"我要调用 play()"时,链接器会去其他的 .o 或 .lib 库里找 play() 到底在哪。
-
回填:找到后,把 play() 在内存中的真实地址填回到 main.o 的那个空位上。
-
-
输出 :最终的 可执行文件 (.exe)。
-
为什么需要这一步?
-
模块化开发:我们不可能把几百万行代码写在一个文件里。链接器允许我们把代码拆分成很多个文件分别编译,最后再组装起来。
-
使用库:我们可以直接使用别人写好的功能(如 SDL2, FFmpeg),而不需要知道它们是怎么实现的,只要在最后一步把它们链接进来即可。
-
1. 静态链接 (Static Linking) ------ 抄书
-
做法 :在编译(链接)的时候,把百科全书里你用到的那几页内容,原封不动地抄写(复制)进你的文章里。
-
结果:
-
你的文章(.exe)会变厚(体积大)。
-
发布的时候,只需要给别人这一本书就够了,不需要附带百科全书。
-
文件名特征:Linux/MinGW 下通常叫 .a (Archive),Windows MSVC 下叫 .lib。
-
-
做法 :你不在文章里抄内容,而是写上一句:"此处详见《FFmpeg百科全书》第50页"。
-
结果:
-
你的文章(.exe)很薄(体积小)。
-
但是,发布的时候,你必须把《FFmpeg百科全书》(.dll)也一起打包给别人。如果只有文章没有书,读者读到这一行就会崩溃(Crash)。
-
文件名特征:Windows 下叫 .dll (Dynamic Link Library)。
-