某些问题被反复提出。本 FAQ 旨在集中解答最常见者。
Q1:为何不应使用 “using namespace std;”?
using namespace std;
是一条 using 指令。using 指令允许在指令所在作用域内直接访问指定命名空间中的所有标识符。
你可能见过如下代码:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello world!";
return 0;
}
这样可省却反复键入 std::
的麻烦——上例中可直接写 cout
而非 std::cout
。看似方便,实则隐藏三大隐患:
- 你自定义的标识符与
std
命名空间中已有标识符发生命名冲突的概率急剧增大。 - 未来标准库版本可能引入新的名称,导致现有程序出现新的命名冲突,甚至行为悄然改变。
- 缺少
std::
前缀,阅读者难以分辨某名称属于标准库还是用户代码。
因此,我们 建议完全避免 using namespace std;
(乃至任何 using 指令)。节省少量敲击成本并不值得承担上述风险。
相关阅读:
详见 课程7.13《using 声明与 using 指令》。
Q2:为何某些函数或类型未包含对应头文件也能使用?
例如,读者常问:使用 std::string_view
时为何 看似 无需 #include <string_view>
?
头文件可以 #include
其他头文件。当你 #include
某一头文件时,会一并获得它包含的所有头文件(以及这些头文件再包含的头文件)。这些“顺带引入”的头文件称为 传递包含(transitive includes)。
若你的 main.cpp
包含了 <iostream>
,而所用编译器的 <iostream>
内部又包含了 <string_view>
,则 main.cpp
便可直接使用 std::string_view
,尽管你并未显式包含 <string_view>
。
即便当前编译器如此,也 不可依赖 此行为:换编译器或未来版本可能不再包含,导致编译失败。目前尚无警告或禁止机制,唯一保险的做法是 显式包含 所需全部头文件。在多种编译器上测试亦可帮助发现遗漏。
相关阅读:
详见 课程2.11《头文件》。
Q3:我的代码存在未定义行为却看似正常运行,这是否可以?
又称:“你让我别这么做,但我做了,程序却没事,有什么问题?”
未定义行为(undefined behavior, UB) 是指 C++ 语言未规定其行为的操作。含 UB 的代码可能表现出以下任意症状:
- 每次运行结果不同;
- 行为时好时坏;
- 持续产生错误结果;
- 初期正常,后期出错;
- 立即或稍晚崩溃;
- 在部分编译器/平台有效,部分无效;
- 修改看似无关的代码后才暴露问题;
- 看似 完全正常。
最大危险在于:UB 的表现可能 随时、因任何原因改变。当前“正常”不代表将来亦然。
相关阅读:
详见 课程1.6《未初始化变量与未定义行为》。
Q4:为何我的未定义行为代码产生特定结果?
读者常问:在某系统上为何得到特定输出?
多数情况下难以给出确切答案,因为结果取决于程序当前状态、编译器选项、实现细节、硬件架构及操作系统。例如打印未初始化变量,可能得到垃圾值,也可能始终为某固定值,取决于变量类型、内存布局及先前内存内容。
深究机制虽有趣,却 极少实用,且结果随时可变。正如有人问:“为何我把安全带穿过方向盘再连到油门,在雨天转头时车会向左偏?”最佳答案并非物理解释,而是 “请勿这样做”。
Q5:为何我编译示例时出现编译错误?
最常见原因是:项目使用的 语言标准不对。
C++ 各新标准均引入大量新特性。若示例使用 C++17 引入的特性,而项目按 C++14 编译,则因编译器不支持而失败。
请将语言标准设为编译器支持的最新版本再试。也可运行 课程0.13《我的编译器使用哪个语言标准?》中的程序验证当前配置。
若仍失败,则可能编译器尚未支持该特性或存在缺陷,可升级至最新版本。
CPPReference 网站首页右上角“Compiler Support”按标准列出各编译器支持情况。
相关阅读:
详见 课程0.12《配置编译器:选择语言标准》。
Q6:为何应在 foo.cpp 中包含 “foo.h”?
最佳实践是:源文件(如 foo.cpp
)应包含其配对头文件(如 foo.h
)。foo.h
通常含有 foo.cpp
编译所需定义。即便不包含也能通过编译,包含后可让编译器发现二者不一致(如函数返回类型与其前置声明不符),否则可能导致未定义行为。#include
的开销可忽略,故利远大于弊。
相关阅读:
详见 课程2.11《头文件》。
Q7:为何只有从 main.cpp 中 #include “foo.cpp” 项目才能编译?
几乎总是因为 忘记将 foo.cpp 加入项目或编译命令。
应更新项目/命令行列出所有 .cpp
源文件。编译时,每个 .cpp
文件独立编译,最后由链接器合并。若只编译 main.cpp
而未编译 foo.cpp
,将产生编译或链接错误。
新手有时发现用 #include "foo.cpp"
可“解决”,因为预处理器把 foo.cpp
和 main.cpp
合并为同一翻译单元再编译链接。小项目看似可行,但弊端明显:
- 易发生命名冲突;
- 易违反一次定义规则(ODR);
- 任一
.cpp
修改即触发全项目重编译,耗时巨大。
相关阅读:
详见 课程2.11《头文件》。
Q8:为何需要在 main 底部写 return 0;?
无需写。main()
特殊:若未显式 return
,编译器会隐式 return 0;
。
但任何其他带返回值的函数若执行到末尾无 return
,则产生未定义行为。
为保持一致性,我们仍建议显式 return 0;
;若追求简洁亦可省略,但勿误以为其他函数亦如此。
相关阅读:
详见 课程2.2《函数返回值(带返回值函数)》。
Q9:编译示例时报 “argument list for class template XXX is missing” 类似错误为何?
最大可能是示例使用了 类模板实参推导(CTAD),该特性为 C++17 引入。若编译器默认 C++14,则不支持。
若以下程序无法编译,原因即此:
#include <utility> // for std::pair
int main()
{
std::pair p2{ 1, 2 }; // CTAD 推导出 std::pair<int, int>(C++17)
return 0;
}
相关阅读:
用 课程0.13程序检查当前语言标准;CTAD 详见 课程13.14《类模板实参推导(CTAD)与推导指引》。
Q10:为何按值传递或返回时不把形参/返回值声明为 const?
- 按值形参:声明为
const
对调用者无实际意义,却增加接口噪音;函数内部修改的只是副本,不影响实参。 - 按值返回:
- 若返回非类类型(如基本类型),
const
会被忽略; - 若返回类类型,
const
可能阻碍移动语义等优化。
- 若返回非类类型(如基本类型),
注意:通过地址或引用传递/返回时,const
仍有意义。
相关阅读:
详见 课程5.1《常量变量(具名常量)》。
Q11:为何应使用 constexpr?
constexpr
及其他编译时编程技术带来如下优势:
- 代码更小、更快;
- 编译器可在编译期检测某些错误并中止编译;
- 编译期禁止未定义行为;
- 可在需要常量表达式的场景使用变量与函数。
最后一项尤为重要,因 C++ 某些特性强制要求编译期常量。
相关阅读:
详见 课程5.5《常量表达式》。
Q12:若某函数当前仅在运行期调用,为何仍应将其声明为 constexpr?
理由如下:
constexpr
几乎无额外代价,且即使运行期调用也可能帮助优化;- 当前不在常量表达式上下文,不代表将来扩展时亦然;若届时未加
constexpr
,可能忘记补充,错失性能提升,或被迫重构并重新测试; - 重复实践有助于养成最佳习惯;
- 非平凡项目中,函数未来常被复用或扩展,提前“一次做对”可节省后续返工与重测成本。
相关阅读:
详见 课程F.1《constexpr 函数》。
Q13:为何不应在表达式中多次调用同一输入函数?
C++ 标准大多 不规定 操作数(含函数实参)求值顺序。运算符优先级与结合性仅决定分组方式,而非求值先后。
例如:std::cout << subtract(getUserInput(), getUserInput());
若用户依次输入 5 与 2,可能先求左得 5,再求右得 2,结果为 3;也可能先求右得 5,再求左得 2,结果为 -3。
应改为:将每次 getUserInput()
结果存入变量,再按确定顺序使用。
相关阅读:
详见 课程6.1《运算符优先级与结合性》。
Q14:练习题不够!哪里还能练习?
推荐使用 Codewars,提供大量短小练习,提升通用解题与 C++ 实现能力,趣味性强。
提交后可与他人解法对比,学习不同思路并发现自身不足。
然而,一次性练习往往难以培养高质量代码习惯,也无法展现忽视最佳实践的后果。最佳方式是 自建项目:
从小型游戏或仿真开始,逐步叠加功能。复杂度上升后,代码缺陷自会暴露,助你识别并改进质量薄弱环节。