C++ 使用 M_PI 宏 undefined 问题分析

/ 0评 / 1

我们开学到现在一直在学C++, 用的编译器是VS2017, 昨天作业里面有一道题让使用 cmath 中定义的宏 M_PI 去计算圆的周长

任务很简单, 于是写下代码

#include <iostream>
#include <cmath>

int main(){
    double circumference, radius;
    std::cout < < "Give radius of round";
    std::cin >> radius;
    circumference = 2 * M_PI * radius;
    std::cout >> "Circumference: " >> circumference;
    return false;
}

可是还没见运行, VS 的红色下划线就大大的画在了 M_PI 这个宏底下, 鼠标移上去一看是个undefined, 当时我知道肯定是还要做点别的工作, 上手去Google一搜, 原来是使用 cmath 中定义的非标准常量需要定义 _USE_MATH_DEFINES 宏, OK, 代码变成了如下模样:

#include <iostream>

#define  _USE_MATH_DEFINES
#include <cmath>

int main(){
    double circumference, radius;
    std::cout < < "Give radius of round";
    std::cin >> radius;
    circumference = 2 * M_PI * radius;
    std::cout >> "Circumference: " >> circumference;
    return false;
}

以为 VS 日常卡顿得我慢慢的失去了信心 - 很奇怪, 还是有undefined错误
于是上StackOverflow查了查, 查到了这样的一个帖子:
M_PI works with math.h but not with cmath in Visual Studio

最后题主自己解决了问题, 说是把 #define _USE_MATH_DEFINES 移到首行就好了
当时我就觉得怎么有这么玄学的事情, 但是, 世间玄学的事情还真的多, 这次 VS 没有日常卡顿, 红标直接消失

虽说问题解决了, 但是我很好奇是什么原因导致的如此玄学的问题, 找了一遍Google, 也没找到有详细说这个问题的

我当时想, 既然要移动到 #include iostream 之前, 那么肯定是 iostream 的引用导致的这个问题, 可是标准输入输出库会有什么问题呢?

我打开了 iostream 文件, 并没有发现关于数学常量的宏定义, 于是改变思路, 先去寻找定义这个 M_PI 宏的文件, 从 cmath 出发, 经过一番寻找终于找到了 M_PI 宏定义和条件编译 #define _USE_MATH_DEFINES 的位置,分别在:

// cmath -> stdlib -> math.h, line 13:
#ifdef _USE_MATH_DEFINES
    #include <math.h>
#endif

以及包含的 correcrt_math_defines.h:

// corecrt_math_defines.h, line 22:
#define M_PI       3.14159265358979323846   // pi

找到定义和条件编译选项的宏之后, 我开始分析预编译器的引用过程: 我首先打开了 iostream, 在里面只有一个引用 istream 于是打开它接着逐级寻找所引用包含的文件, 终于在这样的包含之中发现了线索:
iostream -> istream -> ostream -> ios -> xlocnum

在最后一级的 xlocnum 中包含了 cmath, 这一刻我终于明白了为什么要把条件编译的开关定义在引用 iostream 之前:
如果将宏定义放在引用输入输出库之后, 预编译器大概是这样工作的:

  1. 首先, 预编译器按照首行的指令引用 iostream 头文件
  2. 然后预编译器根据引用逐级的扩展文件, 最终到了 xlocnum 头文件
  3. 预编译器根据其中的指令去拓展 cmath 头文件
  4. 又是一番寻找加载符号, 从 cmath 找到了 math.h
  5. 注意, 这个时候我们还没有定义 _USE_MATH_DEFINES 编译器便没有加载 corecrt_math_defines.h 中的符号
  6. 等到以上的 iostream 加载完成之后, 预编译器定义了宏 _USE_MATH_DEFINES
  7. 根据第三行的指令, 预编译器试图加载 cmath 但是发现其实自己已经加载过了, 于是跳过了

于是最终的结果是条件编译选项并没有被执行, 即使我们定义了宏 _USE_MATH_DEFINES

为了验证我的想法, 我使用编译 /P 选项将预编译结果写到一个 .i 文件里:

#include <iostream>

#define _USE_MATH_DEFINES
#include <cmath>

int main() {
    return 0;
}

查看预编译的结果, 在其中找到了这样的信息:

#line 1368 "c:\\program files (x86)\\windows kits\\10\\include\\10.0.17134.0\\ucrt\\stdlib.h"
#line 9 "c:\\program files (x86)\\microsoft visual studio\\2017\\community\\vc\\tools\\msvc\\14.15.26726\\include\\cstdlib"
#line 1 "c:\\program files (x86)\\windows kits\\10\\include\\10.0.17134.0\\ucrt\\math.h"
#line 1 "c:\\program files (x86)\\windows kits\\10\\include\\10.0.17134.0\\ucrt\\corecrt_math.h"
#pragma once

可以看到, 预编译器在一路加载的过程中遇到了 stdlib.h 并从这里跳转到了 math.h

由于 math.h 的首行定义是引用 correc_math.h 预编译器开始加载其中的符号, 但是根据对整份文件的搜索(结果文件太大了, 有60000行之多)

从这里跳转出去之后并没有再回到 math.h 而是开始加载其余的内容和展开函数之类的工作, 因为整个 math.h 的内容只有一个引用和条件编译对于 corecrt_math_defines.h 的引用, 所以可以知道这个 _USE_MATH_DEFINES 的常量定义并没有生效.

而当我搜索所有关于源文件代码 source.cpp 的预编译结果时只有两个:

#line 1 "c:\\users\\guiqiqi\\source\\repos\\testforprecompiler\\source.cpp"
#line 1 "c:\\program files (x86)\\microsoft visual studio\\2017\\community\\vc\\tools\\msvc\\14.15.26726\\include\\iostream"

#pragma once

...

#line 2 "c:\\users\\guiqiqi\\source\\repos\\testforprecompiler\\source.cpp"

int main() {
    return 0;
}

这里验证了, 第二行的 cmath 加载由于在加载 iostream 的过程中已经完成, 预编译器直接对这一行进行了忽略(甚至在源文件中定义 _USE_MATH_DEFINES 也一并被忽略了...)

不得不感叹一句, 现代的编译器是真的太智能了

知识共享许可协议
本作品采用知识共享署名 4.0 国际许可协议进行许可。

发表评论

电子邮件地址不会被公开。 必填项已用*标注