有些问题往往会被反复问到。本常见问题解答将尝试回答最常见的问题。
Q1: 为什么我们不应该使用“using namespace std”?
语句 using namespace std;
是一个**using 指令**。using 指令允许在 using 指令语句的作用域内访问给定命名空间中的所有标识符。
您可能见过这样的代码:
#include <iostream>
using namespace std;
int main()
{
cout << "Hello world!";
return 0;
}
这允许我们使用 std
命名空间中的名称,而无需一遍又一遍地显式键入 std::
。在上面的程序中,我们只需键入 cout
而不是 std::cout
。听起来很棒,对吗?
然而,当编译器遇到 using namespace std
时,它将使 std
命名空间中的每个标识符都可以在全局作用域中访问(因为 using 指令被放在那里)。这引入了 3 个关键挑战:
- 您选择的名称与
std
命名空间中已有的名称发生命名冲突的可能性大大增加。 - 标准库的新版本可能会破坏您当前正常工作的程序。这些未来的版本可能会引入导致新命名冲突的名称,或者在最坏的情况下,您的程序的行为可能会悄悄地、意外地改变!
- 缺少
std::
前缀使得读者更难理解什么是std
库名称,什么是用户定义的名称。
由于这些原因,我们建议完全避免使用 using namespace std
(或任何其他 using 指令)。节省的少量输入不值得额外的麻烦和未来的风险。
相关内容
有关更多详细信息和示例,请参阅课程 7.13 -- using 声明和 using 指令。
Q2: 为什么我可以在不包含声明该函数或类型的头文件的情况下使用某些函数或类型?
例如,许多读者会问,为什么他们使用 std::string_view
的程序需要 #include <string_view>
,而没有 #include
似乎也能正常工作。
头文件可以 #include
其他头文件。当您 #include
一个头文件时,您也会得到它包含的所有额外头文件(以及这些头文件所包含的所有头文件)。所有这些您没有显式包含的额外头文件被称为“传递性包含”。
在您的 *main.cpp* 文件中,您可能 #include <iostream>
。在您的编译器上,如果您的 <iostream> 头文件(为了它自己的使用)包含了 <string_view> 头文件,那么当您 #include <iostream>
时,您将获得 <string_view> 头文件的内容(以及 <iostream> 包含的任何其他头文件)。这意味着您的 *main.cpp* 将能够在没有显式包含 <string_view> 头文件的情况下使用 std::string_view
类型。
即使这在您的编译器上可以编译,您也不应依赖它。现在对您编译的代码可能无法在另一个编译器上编译,甚至无法在您的编译器的未来版本上编译。
当这种情况发生时,没有办法警告或阻止它。您能做的最好的事情就是小心地显式包含您使用的所有内容的正确头文件。在不同的编译器上编译您的程序可能有助于识别在其他编译器上被传递性包含的头文件。
相关内容
在课程 2.11 -- 头文件 中有介绍。
又名:“我做了你告诉我不要做的事情,它奏效了。那问题出在哪里?”
当您执行其行为未由 C++ 语言定义的操作时,就会发生未定义行为。实现未定义行为的代码可能会表现出以下**任何**症状:
- 您的程序每次运行时都会产生不同的结果。
- 您的程序行为不一致(有时产生期望的结果,有时不)。
- 您的程序始终产生相同的错误结果。
- 您的程序最初看起来工作正常,但在程序的后期会产生一些不正确的结果。
- 您的程序崩溃,立即或稍后。
- 您的程序在某些编译器或平台上可以工作,但在其他编译器或平台上不行。
- 您的程序工作正常,直到您更改了一些其他看似无关的代码。
- 您的程序似乎无论如何都能产生期望的结果。
未定义行为最大的问题之一是,程序表现出的行为可能随时出于任何原因而改变。因此,尽管此类代码现在看起来可能有效,但不能保证它在稍后再次运行时也会有效。
相关内容
未定义行为在课程 1.6 -- 未初始化变量和未定义行为 中有介绍。
读者经常询问在他们的系统上产生特定结果的原因。在大多数情况下,这很难说,因为产生的结果可能取决于当前的程序状态、您的编译器设置、编译器如何实现某个功能、计算机的架构和/或操作系统。例如,如果您打印一个未初始化变量的值,您可能会得到垃圾值,或者您可能总是得到一个特定值。这取决于变量的类型、编译器如何在内存中布局变量,以及该内存中预先有什么(这可能会受到操作系统或程序在该点之前的状态的影响)。
虽然这样的答案在机械上可能很有趣,但它很少在总体上有所帮助(并且如果任何其他东西发生变化,很可能会改变)。这就像问:“当我把安全带穿过方向盘并连接到加速器时,为什么在下雨天我转头时汽车会向左拉?”最好的答案不是对正在发生的事情的物理解释,而是“不要那样做”。
Q5: 为什么我尝试编译一个看起来应该能工作的示例时会收到编译错误?
最常见的原因是您的项目正在使用错误的语言标准进行编译。
C++ 在每个新的语言标准中都引入了许多新功能。如果我们的示例使用了 C++17 中引入的功能,但您的程序是使用 C++14 语言标准编译的,那么它可能无法编译,因为我们使用的功能不受我们选择的语言标准支持。
尝试将您的语言标准设置为您的编译器支持的最新版本,看看是否能解决问题。您还可以通过运行此处的程序来检查您的编译器是否正确配置为使用您期望的语言标准:0.13 -- 我的编译器正在使用哪个语言标准?。
相关内容
在课程 0.12 -- 配置您的编译器:选择语言标准 中有介绍。
也可能您的编译器尚不支持特定功能,或者存在导致某些情况下无法使用的错误。在这种情况下,请尝试将您的编译器更新到可用的最新版本。
CPPReference 网站跟踪每个语言标准中每个功能的编译器支持。您可以在其主页右上角的“编译器支持”(按语言标准)下找到这些支持表。例如,您可以在此处查看支持哪些 C++23 功能。
Q6: 为什么我应该从 foo.cpp 中 #include “foo.h”?
源文件(例如 foo.cpp)包含其配对头文件(例如 foo.h)是一种最佳实践。在许多情况下,foo.h 将包含 foo.cpp 正确编译所需的定义。
然而,即使 foo.cpp 在没有 foo.h 的情况下也能编译文件,包含配对头文件允许编译器发现两个文件之间某些类型的不一致(例如,当函数的返回类型与其前向声明的返回类型不匹配时)。如果没有包含,这可能会导致未定义行为。
#include 的成本可以忽略不计,因此包含头文件几乎没有缺点。
相关内容
在课程 2.11 -- 头文件 中有介绍。
Q7: 为什么我的项目只有在从“main.cpp”中 #include “foo.cpp”时才能编译?
这几乎总是由于忘记将 foo.cpp 添加到您的项目和/或编译命令行中。更新您的项目和/或命令行以包含每个源 (.cpp) 文件。当您编译项目时,您应该会看到每个源文件都被编译。
通常在一个有多个文件的项目中,编译器会单独编译每个源 (.cpp) 文件。所有源文件编译完成后,链接器会将它们链接在一起并创建最终的输出文件(例如可执行文件)。然而,如果您将代码拆分到两个或更多文件中(例如 main.cpp 和 foo.cpp),然后只编译 main.cpp,您可能会得到编译错误或链接器错误,因为项目所需的部分代码没有被编译。
新程序员有时会发现,通过在 main.cpp 中添加 #include "foo.cpp"
而不是将 foo.cpp 添加到项目或编译命令行中,他们可以让程序工作。这样做之后,当 main.cpp 被编译时,预处理器将创建一个由 foo.cpp 和 main.cpp 的代码组成的翻译单元,然后将被编译和链接。在较小的项目中,这可能有效。那么为什么不这样做呢?
有几个原因:
- 它可能导致文件之间命名冲突。
- 很难避免 ODR 违规。
- 对任何 .cpp 文件的任何更改都将导致整个项目重新编译。这可能需要很长时间。
相关内容
在课程 2.11 -- 头文件 中有介绍。
Q8: 为什么我需要在 main 的末尾 return 0
?
您不需要。main()
函数很特殊,如果您不提供 return 语句,它将隐式返回 0。
然而,任何其他值返回函数如果在没有遇到 return 语句的情况下到达其函数体的末尾,都将产生未定义行为。
为了一致性,我们建议从 main()
显式返回 0。但如果您为了简洁起见更喜欢在 main()
中省略 return 语句,您可以这样做。只是不要错误地认为其他值返回函数也以类似方式工作。
相关内容
在课程 2.2 -- 函数返回值(值返回函数) 中有介绍。
Q9: 当我编译本网站上的示例时,收到类似“class template XXX 的参数列表缺失”的错误。为什么?
最可能的原因是示例使用了名为类模板参数推导 (CTAD) 的功能,这是一个 C++17 功能。许多编译器默认使用 C++14,它不支持此功能。
如果以下程序无法编译,那就是原因:
#include <utility> // for std::pair
int main()
{
std::pair p2{ 1, 2 }; // CTAD used to deduce std::pair<int, int> from the initializers (C++17)
return 0;
}
相关内容
您可以使用课程 0.13 -- 我的编译器正在使用哪个语言标准? 中的程序检查您的编译器配置的语言标准。
我们在课程 13.14 -- 类模板参数推导 (CTAD) 和推导指南 中介绍了 CTAD。
Q10: 为什么我们不将按值传递或返回的函数参数或返回类型设为 const?
我们通常不会将按值函数参数设为 const
,因为:
- 将此类参数设为
const
对函数的调用者没有实际意义,但会增加函数接口的混乱。 - 我们通常不关心函数是否修改这些参数,因为它们是副本,无论如何都将在函数结束时销毁。
我们不将按值返回类型设为 const
,因为:
- 如果返回类型是非类类型(例如基本类型),则
const
将被忽略。 - 如果返回类型是类类型,
const
可能会抑制某些类型的优化(例如移动语义)。
注意:const
在按地址或按引用传递/返回时相关。
相关内容
在课程 5.1 -- 常量变量(命名常量) 中有介绍。
Constexpr 和其他编译时编程技术提供了许多好处,包括:
- 更小、更快的代码。
- 我们可以让编译器检测某些类型的错误,并在发生时中止编译。
- 编译时不允许未定义行为。
- 能够在需要常量表达式的地方使用变量和函数。
最后一点也许是最不可避免的,因为某些 C++ 功能需要编译时已知的值。
相关内容
在课程 5.5 -- 常量表达式 中有介绍。
Q12: 当我知道我的程序中的某个符合条件的函数只会在运行时求值时,为什么我还要将其 constexpr 化?
您可能这样做有几个原因:
- 使用 constexpr 几乎没有缺点,它甚至可以在不进行编译时求值的上下文中帮助编译器优化。
- 仅仅因为您目前没有在编译时可求值的上下文中调用该函数,并不意味着您在修改或扩展程序时不会在这样的上下文中调用它。如果您还没有 constexpr 该函数,那么当您开始在这样的上下文中调用它时,您可能不会想到这样做,然后您将错过性能优势。或者您可能被迫在以后需要将返回值用于某个需要常量表达式的上下文中时,再将其 constexpr 化。
- 重复有助于巩固最佳实践。
在一个非平凡的项目中,以函数将来可能被重用(或扩展)的心态来实现您的函数是一个好主意。每次修改现有函数时,您都面临着破坏它的风险,这意味着它需要重新测试,这需要时间和精力。通常值得多花一两分钟“第一次就把它做好”,这样您就不必以后再重做(和重新测试)它了。
相关内容
在课程 F.1 -- Constexpr 函数 中有介绍。
Q13: 为什么我不应该在一个表达式中多次调用同一个输入函数?
在大多数情况下,C++ 标准并未指定操作数(包括函数参数)的求值顺序。运算符的优先级和结合性仅用于确定操作数如何与运算符分组,以及值计算的顺序。
例如,给定语句 std::cout << subtract(getUserInput(), getUserInput())
,对 subtract()
函数调用的左参数或右参数都可以先求值。假设用户输入值 5
和 2
。如果左参数先求值,则左参数将求值为 5
,右参数将求值为 2
。5 - 2
是 3
。如果右参数先求值,则右参数将求值为 5
,左参数将求值为 2
。2 - 5
是 -3
。因此,此语句可能会打印 3
或 -3
。
这可以通过将每次对 getUserInput()
的函数调用都作为单独的语句(以便顺序是确定的),并将返回值存储在一个变量中直到需要时再使用来消除歧义。
相关内容
在课程 6.1 -- 运算符优先级和结合性 中有介绍。
我们推荐 https://www.codewars.com/,它有很多小练习可以帮助您提高一般问题解决和 C++ 实现技能。它也很有趣!
一旦您有了解决方案,您可以将其与他人的答案进行比较,以查看其他方法,或了解您自己的代码可以在哪些方面改进。
然而,由于一次性练习有一次性答案,解决此类测验并不能真正鼓励编写高质量的代码,也不能说明不遵循最佳实践会发生什么。为此,没有什么比创建自己的项目更好的了。
从小处着手——一个简单的游戏或模拟。然后随着时间的推移增加功能。随着项目复杂性的增加,您将开始看到代码中的各种缺陷。这将帮助您确定代码的哪些区域需要改进质量。