跳转到内容
新建笔记

C++ 编译、链接与加载:按阶段定位错误

先定位失败发生在哪一层

跳转到“先定位失败发生在哪一层”

从源代码到运行中的进程,要经过语言处理、目标文件生成、链接和加载。IDE 的补全/检查系统还可能单独解析一次代码。错误出现在同一张“错误列表”里,并不代表它们由同一个工具产生。

阶段输入与工作典型故障
预处理与头文件查找处理包含、宏和条件编译文件不存在、包含目录不正确、宏选中了错误分支
编译解析语言、检查类型并生成目标代码所需表示名称未声明、语法或类型错误
汇编将汇编表示变为可重定位目标文件目标架构或汇编指令不匹配
链接组合目标文件和库,处理符号与重定位未定义引用、重复定义、库或架构不匹配
加载与初始化读取可执行文件、装入依赖、执行启动逻辑找不到 DLL/SO、符号版本或入口点不兼容
运行与结束执行业务、管理资源并退出逻辑错误、越界、异常或外部故障

GCC 的 -E、-S、-c 可以停在不同阶段。实际工具链可能通过内存或管道传递中间结果;没有看到 .i/.s 文件不代表没有这些处理。GCC 通常把预处理后的 C 写成 .i、C++ 写成 .ii;C 和 C++ 的前端也不是同一个程序。GCC 总体选项

一个可以逐阶段检查的 C++17 项目

跳转到“一个可以逐阶段检查的 C++17 项目”

创建 include/demo/build_choice.h:

#ifndef DEMO_BUILD_CHOICE_H
#define DEMO_BUILD_CHOICE_H
#if defined(DEMO_BONUS)
inline constexpr int selected_base = 40;
#else
inline constexpr int selected_base = 1;
#endif
int adjustment() noexcept;
#endif

adjustment.cpp 提供声明所对应的定义:

#include "demo/build_choice.h"
int adjustment() noexcept
{
return 3;
}

main.cpp 使用该接口:

#include "demo/build_choice.h"
#include <cassert>
#include <cstdio>
int main()
{
const int value = selected_base + adjustment();
#if defined(DEMO_BONUS)
assert(value == 43);
#else
assert(value == 4);
#endif
std::printf("value=%d\n", value);
}

在项目目录执行下列 GCC 命令。它们显式保留本次检查需要的中间文件:

g++ -std=c++17 -DDEMO_BONUS -I include -E main.cpp -o main.ii
g++ -std=c++17 -S main.ii -o main.s
g++ -c main.s -o main.o
g++ -std=c++17 -DDEMO_BONUS -I include -c adjustment.cpp -o adjustment.o
g++ main.o adjustment.o -o demo

Windows MinGW 生成 demo.exe,Linux 下上述输出为 demo;分别运行 ./demo.exe 或 ./demo,应得到 value=43。不要用 gcc 代替最后一行的 C++ 驱动,期待它自动补齐所有 C++ 标准库依赖。

这组例子还能区分几个问题:

  • 从预处理命令去掉 -I include,头文件查找失败,后面的链接根本还没有开始。
  • 只链接 main.o,adjustment() 缺少定义,属于链接失败;重复增加头文件包含不能生成这个定义。
  • 去掉所有翻译单元的 -DDEMO_BONUS 后重新生成中间文件,结果变为 4。
  • 使用 -DDEMO_BONUS=0,defined(DEMO_BONUS) 仍然成立,结果仍为 43。检测“存在”和检测宏的数值不是同一种条件。

各翻译单元必须采用一致的接口配置,尤其不要让同一头文件中的定义因不一致的宏而违反单一定义规则。修改宏或头文件后,要重新生成受影响的目标文件,再链接。

保留原 OpenCV 条件编译场景

跳转到“保留原 OpenCV 条件编译场景”

原文使用 BATCHED_NMS 选择 OpenCV DNN 的 NMS API:定义该宏时调用 cv::dnn::NMSBoxesBatched,否则调用 cv::dnn::NMSBoxes。这是编译时选择,未选中的源分支不会进入后续 C++ 编译。

两个接口都可以处理多个候选框。NMSBoxesBatched 的关键额外输入是 class_ids,用于按类别处理抑制;不能把它概括成“多个框”,再把 NMSBoxes 概括成“单个框”。原片段还依赖 bboxes/scores/labels、阈值、输出索引和匹配版本的 OpenCV 模块,因此不是独立完整程序。本页验证的是上面的构建阶段例子,未声称运行了该 OpenCV 工程。OpenCV NMS API

E1696 与“启用 Windows 运行时扩展”

跳转到“E1696 与“启用 Windows 运行时扩展””

另一条原记录只给出了 Visual Studio 中“使用 Windows 运行时扩展→是”的做法,没有提供缺失的头文件和项目类型。这个设置对应 /ZW,用于 C++/CX Windows Runtime 扩展,不是普通 C++ 项目通用的头文件修复开关。只有工程确实采用相应语言/平台模型时,才应按它的构建要求配置。MSVC /ZW

看到 E1696“无法打开源文件”时,先区分 IntelliSense 与真正构建输出,再检查:

  1. 错误中具体缺失的文件是否存在,名称、大小写和路径是否正确。
  2. 当前配置/平台的编译命令是否具有对应包含目录,所需 SDK 或生成头文件是否已就绪。
  3. 若命令行构建成功,IDE 的解析配置是否与该命令一致;Makefile 项目和 CMake 项目应检查各自的配置来源。

库目录和链接输入解决不了预处理器找不到头文件的问题;反过来,增加包含目录也不能修复缺失库实现。重扫 IDE 数据库适合配置已正确而缓存仍旧的情形,不能替代补齐实际依赖。Visual Studio IntelliSense 配置

入口点、main 与资源清理

跳转到“入口点、main 与资源清理”

操作系统开始执行的是二进制规定的入口点。普通托管 C/C++ 应用通常先经过运行库启动和初始化,再调用 main;不能把加载器的入口点直接等同于 main。嵌入式或独立环境还有自己的启动约定。

正常从 main 返回会经历相应的 C++ 正常终止流程,但异常终止、强制结束进程等路径有不同语义。操作系统回收进程内存也不代表应用已正确保存文件、完成网络协议或释放所有外部资源。编译器、链接器、加载器各解决一部分问题;构建工具组织这些步骤,调试器和版本控制则负责其他工作。