新手问答
这个“新手区”是关于什么的?
这是一个随机排序的集合,包含新手可能会问的一些问题。
- 本节不声称有组织性。请将其视为随机的。事实上,请将其视为一个忙碌的人仓促进行的初步尝试。
- 本节不声称是完整的。请将其视为对少数人提供的一点帮助。它不会帮助所有人,也可能不会帮助你。
希望有一天我们能够改进本节,但目前,它是不完整且无组织的。如果这困扰你,我建议你点击浏览器窗口最右上角的那个小x
:-)
。
我从哪里开始?
阅读 FAQ,特别是学习 C++ 的章节,并阅读多本书籍。
但如果一切仍然显得太难,如果你感到被神秘的术语和概念轰炸,如果你想知道自己如何才能掌握任何东西,请这样做
- 从上面列出的任何来源输入一些 C++ 代码。
- 使其编译并运行。
- 重复。
就是这样。只需练习和尝试。希望这能让你站稳脚跟。
以下是一些可以获取“示例问题”的地方(按字母顺序排列)
如何从输入中读取字符串?
你可以像这样读取一个由空白符终止的单词
#include<iostream>
#include<string>
using namespace std;
int main()
{
cout << "Please enter a word:\n";
string s;
cin>>s;
cout << "You entered " << s << '\n';
}
请注意,这里没有显式的内存管理,也没有可能溢出的固定大小缓冲区。
如果你确实需要一整行(而不仅仅是一个单词),你可以这样做
#include<iostream>
#include<string>
using namespace std;
int main()
{
cout << "Please enter a line:\n";
string s;
getline(cin,s);
cout << "You entered " << s << '\n';
}
有关标准库设施(如 iostream
和 string
)的简要介绍,请参阅 TC++PL3 的第 3 章(在线可获取)。有关 C 和 C++ I/O 简单用法的详细比较,请参阅“将标准 C++ 作为新语言学习”,你可以从 Stroustrup 的出版物列表下载。
我如何编写这个非常简单的程序?
通常,尤其是在学期开始时,会有大量关于如何编写非常简单的程序的问题。通常,要解决的问题是读取一些数字,对它们进行操作,然后输出答案。这是一个执行此操作的示例程序
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<double> v;
double d;
while(cin>>d) v.push_back(d); // read elements
if (!cin.eof()) { // check if input failed
cerr << "format error\n";
return 1; // error return
}
cout << "read " << v.size() << " elements\n";
reverse(v.begin(),v.end());
cout << "elements in reverse order:\n";
for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n';
return 0; // success return
}
以下是关于这个程序的一些观察
- 这是一个使用标准库的标准 ISO C++ 程序。标准库设施在不带 .h 后缀的头文件中声明在 namespace std 中。
- 如果你想在 Windows 机器上编译它,你需要将其编译为“控制台应用程序”。记住给你的源文件加 .cpp 后缀,否则编译器可能会认为它是 C(而不是 C++)源文件。
- 是的,
main()
返回一个int
。 - 读取到标准
vector
中可以确保你不会溢出某个任意缓冲区。在不犯“愚蠢错误”的情况下读取到数组中超出了完全新手的能力——等你正确做到了,你就不再是完全新手了。如果你怀疑这个说法,请阅读 Stroustrup 的论文“将标准 C++ 作为新语言学习”,你可以在这里下载。 !cin.eof()
是对流格式的测试。具体来说,它测试循环是否因找到文件末尾而结束(如果不是,你没有得到预期类型/格式的输入)。更多信息,请查阅你的 C++ 教科书中的“流状态”。vector
知道它的大小,所以我不必计算元素。- 是的,你可以将
i
声明为vector<double>::size_type
而不是普通的int
,以消除一些过度怀疑的编译器的警告,但在这种情况下,我认为这过于迂腐和分散注意力。 - 这个程序不包含显式内存管理,并且不会泄漏内存。
vector
会跟踪它用于存储元素的内存。当vector
需要更多元素内存时,它会分配更多;当vector
超出作用域时,它会释放该内存。因此,用户无需关心vector
元素的内存分配和释放。 - 有关读取字符串的信息,请参阅如何从输入中读取字符串?。
- 程序在遇到“文件末尾”时停止读取输入。如果你在 Unix 机器上从键盘运行程序,“文件末尾”是 Ctrl-D。如果你在 Windows 机器上,由于一个 bug 不识别文件末尾字符,你可能更喜欢这个稍微复杂一些的版本,它以“end”这个词终止输入
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using namespace std;
int main()
{
vector<double> v;
double d;
while(cin>>d) v.push_back(d); // read elements
if (!cin.eof()) { // check if input failed
cin.clear(); // clear error state
string s;
cin >> s; // look for terminator string
if (s != "end") {
cerr << "format error\n";
return 1; // error return
}
}
cout << "read " << v.size() << " elements\n";
reverse(v.begin(),v.end());
cout << "elements in reverse order:\n";
for (int i = 0; i<v.size(); ++i) cout << v[i] << '\n';
return 0; // success return
}
有关如何使用标准库简单地完成简单事情的更多示例,请参阅C++ 之旅的第 3 部分和第 4 部分。
如何将整数转换为字符串?
调用 to_string
。这是 C++11 中的新功能,广泛可用,但截至本文撰写时,仍未被广泛注意到。:)
int i = 127;
string s = to_string(i);
如何将字符串转换为整数?
调用 stoi
string s = "127";
int i = stoi(s);
相关的函数 stol
和 strtoll
将分别将 string
转换为 long
或 long long
。
我应该使用 void main()
还是 int main()
?
int main()
main()
必须返回 int
。有些编译器接受 void main()
,但那是非标准的,不应该使用。应该使用 int main()
。至于具体的返回值,如果你不知道返回什么,就说 return 0;
定义
void main() { /* ... */ }
不是也从未是 C++,甚至也不是 C。请参阅 ISO C++ 标准 3.6.1[2] 或 ISO C 标准 5.1.2.2.1。符合标准的实现接受
int main() { /* ... */ }
和
int main(int argc, char* argv[]) { /* ... */ }
符合标准的实现可以提供更多版本的 main()
,但它们都必须具有返回类型 int
。main()
返回的 int
是程序向调用它的“系统”返回一个值的方式。在不提供这种设施的系统上,返回值会被忽略,但这并不能使 void main()
成为合法的 C++ 或合法的 C。即使你的编译器接受 void main()
,也要避免使用它,否则有被 C 和 C++ 程序员视为无知的风险。
在 C++ 中,main()
不需要包含显式的 return
语句。在这种情况下,返回的值是 0
,表示成功执行。例如
#include<iostream>
int main()
{
std::cout << "This program returns the integer value 0\n";
}
另请注意,ISO C++ 和 C99 都不允许你在声明中省略类型。也就是说,与 C89 和 ARM C++ 不同,在声明中缺少类型时,不会假定为 int
。因此
#include<iostream>
main() { /* ... */ }
是一个错误,因为 main()
的返回类型缺失。
我应该使用 f(void)
还是 f()
?
f()
C 程序员在声明一个不带参数的函数时经常使用 f(void)
,但在 C++ 中,这被认为是不好的风格。事实上,f(void)
风格被 C++ 的创建者 Bjarne Stroustrup、C 的共同创建者 Dennis Ritchie 和 Unix 诞生的研究部门负责人 Doug McIlroy 称为“一种憎恶”。
如果你正在编写 C++ 代码,你应该使用 f()
。f(void)
风格在 C++ 中是合法的,但只是为了更容易编译 C 代码。
这段 C++ 代码展示了声明一个不带参数的函数的最佳方式
void f(); // declares (not defines) a function that takes no parameters
这段 C++ 代码既声明又定义了一个不带参数的函数
void f() // declares and defines a function that takes no parameters
{
// ...
}
以下 C++ 代码也声明了一个不带参数的函数,但它使用了不太理想(有些人会说是“憎恶”)的风格,f(void)
void f(void); // undesirable style for C++; use void f() instead
实际上,这个 f()
就是你需要了解的关于 C++ 的全部。还有那些新奇的 //
注释。一旦你知道了这两件事,你就可以声称自己是 C++ 专家了。去吧:在你的简历上打上那些神奇的“++”标记。谁关心那些面向对象的东西——你为什么要费心改变你的思维方式?毕竟,真正重要的是思考;它是输入函数声明和注释。(叹气;我希望没有人真的那样想。)
选择 short
/ int
/ long
数据类型的标准是什么?
其他相关问题:如果 short int
在我的特定实现上与 int
大小相同,为什么选择其中一个?如果我开始考虑变量的实际字节大小,我的代码会不会变得不可移植(因为字节大小可能因实现而异)?或者我应该简单地选择比实际所需大得多的尺寸,作为一种安全缓冲?
答案:编写可移植到不同操作系统和/或编译器的代码通常是一个好主意。毕竟,如果你在工作中取得了成功,其他人可能希望在其他地方使用它。对于 int
和 short
等内置类型,这可能有点棘手,因为 C++ 不保证大小。然而,C++ 提供了两样可能对你有帮助的东西:保证的最小大小,这通常是你需要知道的全部,以及一个提供带大小整数类型定义的标准 C 头文件。
C++ 保证char
恰好是一个字节,至少有 8 位,short
至少有 16 位,int
至少有 16 位,long
至少有 32 位。它还保证这些类型的 unsigned
版本与原始类型大小相同,例如,sizeof(unsigned short) == sizeof(short)
。
在编写可移植代码时,你不应该对这些大小做额外的假设。例如,不要假设 int
有 32 位。如果你有一个需要至少 32 位的整数变量,请使用 long
或 unsigned long
,即使在你的特定实现上 sizeof(int) == 4
。另一方面,如果你有一个整数变量量总是适合 16 位并且你希望最小化数据内存的使用,请使用 short
或 unsigned short
,即使你知道在你的特定实现上 sizeof(int) == 2
。
另一个选择是使用以下标准 C 头文件(你的 C++ 编译器供应商可能提供也可能不提供)
#include <stdint.h> /* not part of the C++ standard */
该头文件定义了 int32_t
和 uint16_t
等类型的 typedef,它们分别是带符号的 32 位整数和无符号的 16 位整数。里面还有其他好东西。我的建议是,只在实际需要时使用这些“带大小”的整数类型。有些人崇尚一致性,他们非常想在所有地方使用这些带大小的整数,仅仅因为在某个地方需要它们。一致性是好的,但它不是最大的好,并且在所有地方使用这些 typedef 会导致一些麻烦,甚至可能导致性能问题。最好使用常识,这通常会让你在可能的情况下使用常规关键字,例如 int
、unsigned
等,而在必须的情况下使用显式带大小的整数类型,例如 int32_t
等。
请注意,这里有一些微妙的权衡。在某些情况下,你的计算机处理较小的东西可能比处理较大的东西更快,但在其他情况下则恰恰相反:在某些实现上,int
算术可能比 short
算术更快。另一个权衡是数据空间与代码空间:在某些实现上,int
算术可能比 short
算术生成更少的二进制代码。不要做简单的假设。仅仅因为一个特定变量可以声明为 short
并不一定意味着它应该,即使你试图节省空间。
请注意,C 标准不保证 <stdint.h>
专门为 n = 8, 16, 32 或 64 定义 int
n_t
和 uint
n_t
。但是,如果底层实现提供了任何这些大小的整数,则要求 <stdint.h>
包含相应的 typedef。此外,如果你的实现符合 POSIX 标准,你保证拥有 n = 8, 16 和 32 大小的 typedef。综合所有这些,可以公平地说,绝大多数实现,尽管不是所有实现,都将拥有这些典型大小的 typedef。
const
变量到底是什么?那不是一个矛盾的术语吗?
如果这困扰你,不如称之为“const
标识符”。
主要问题是弄清楚它是什么;我们稍后可以弄清楚如何称呼它。例如,考虑以下函数中的符号 max
void f()
{
const int max = 107;
// ...
float array[max];
// ...
}
无论你称 max
为 const
变量还是 const
标识符,这都不重要。重要的是你意识到它在某些方面像一个普通变量(例如,你可以获取它的地址或通过 const 引用传递它),但它不像一个普通变量,因为你不能改变它的值。
这是另一个更常见的例子
class Fred {
public:
// ...
private:
static const int max_ = 107;
// ...
};
在此示例中,你需要在恰好一个 .cpp 文件中添加行 int Fred::max_;
,通常在 Fred.cpp
中。
通常认为好的编程实践是给每个“魔术数字”(如 107)一个符号名称,并使用该名称而不是原始魔术数字。
我为什么会使用 const
变量 / const
标识符而不是 #define
?
const
标识符通常优于 #define
,因为
- 它们遵循语言的作用域规则
- 你可以在调试器中看到它们
- 如果你需要,你可以获取它们的地址
- 如果你需要,你可以通过
const
引用传递它们 - 它们不会在你的程序中创建新的“关键字”。
简而言之,const
标识符的行为就像它们是语言的一部分,因为它们就是语言的一部分。预处理器可以被认为是 C++ 之上的一层语言。你可以想象预处理器在你的代码中作为单独的遍运行,这意味着你的原始源代码只会被预处理器看到,而不是被 C++ 编译器本身看到。换句话说,你可以想象预处理器看到你的原始源代码并用它们的值替换所有 #define
符号,然后 C++ 编译器本身在原始符号被预处理器替换之后看到修改后的源代码。
在某些情况下需要 #define
,但通常在有选择时应避免使用它。你应该根据商业价值:时间、金钱、风险来评估是否使用 const
与 #define
。换句话说,一刀切不适用。大多数情况下你会为常量使用 const
而不是 #define
,但有时你会使用 #define
。但请记住事后洗手。
你是说预处理器是邪恶的吗?
是的,我正是这个意思:预处理器是邪恶的。
每个 #define
宏实际上在每个源文件和每个作用域中创建了一个新的“关键字”,直到该符号被 #undef
d。预处理器允许你创建一个 #define
符号,该符号总是被替换,而不管该符号出现在哪个 {...}
作用域中。
有时我们需要预处理器,例如每个头文件中的 #ifndef
/#define
包装器,但如果可以的话应该避免使用它。“邪恶”并不意味着“永不使用”。你有时会使用邪恶的东西,特别是当它们是“两害相权取其轻”时。但它们仍然是邪恶的 :-)
“标准库”是什么?它包含/排除了什么?
大多数(但不是所有)实现都有一个“标准包含”目录,有时是多个目录。如果你的实现是这样,标准库中的头文件很可能是这些目录中文件的一个子集。例如,iostream
和 string
是标准库的一部分,cstring
和 cstdio
也是。有一堆 .h 文件也是标准库的一部分,但并非这些目录中的每个 .h 文件都是标准库的一部分。例如,stdio.h
是,但 windows.h
不是。
你像这样包含标准库的头文件
#include <iostream>
int main()
{
std::cout << "Hello world!\n";
// ...
}
我应该如何布局我的代码?我的代码中何时应该使用空格、制表符和/或换行符?
简短的回答是:就像你团队的其他成员一样。换句话说,团队应该对空白符采用一致的方法,否则请不要浪费大量时间担心它。
以下是一些细节
在空白符方面没有普遍接受的编码标准。有一些流行的空白符标准,例如“一次性大括号”风格,但对于任何给定编码标准的某些方面存在很多争议。
大多数空白标准在一些点上达成一致,例如在 x * y
或 a - b
等中缀运算符周围放置空格。大多数(不是所有)空白标准不会在 a[i]
中的 [
或 ]
周围放置空格,f(x)
中的 (
和 )
也有类似的注释。然而,在垂直空白符方面存在很大争议,尤其是在 {
和 }
方面。例如,以下是布局 if (foo()) { bar(); baz(); }
的多种方式中的几种
if (foo()) {
bar();
baz();
}
if (foo())
{
bar();
baz();
}
if (foo())
{
bar();
baz();
}
if (foo())
{
bar();
baz();
}
if (foo()) {
bar();
baz();
}
……等等……
重要提示:请勿给我发电子邮件,告诉我您的空白处理方法比其他方法更好。我不在乎。而且我也不会相信您。在空白处理方面没有“更好”的客观标准,所以您的观点就是:您的观点。如果您不顾本段的劝告给我发电子邮件,我将认为您是一个无可救药的极客,专注于小细节。不要浪费时间担心空白:只要您的团队使用一致的空白样式,就继续您的生活,担心更重要的事情。
例如,您应该担心的事情包括设计问题,例如何时使用ABC,继承应该是实现还是规范技术,应该使用哪些测试和检查策略,接口是否应该为每个数据成员统一具有 get()
和/或 set()
成员函数,接口应该从外部设计还是从内部设计,错误应该通过 try
/catch
/throw
处理还是通过返回码处理等。阅读 FAQ 以获取对这些重要问题的一些看法,但请不要浪费时间争论空白。只要团队使用一致的空白策略,就放下它。
我的代码中出现大量数字可以吗?
可能不行。
在许多(并非所有)情况下,最好为你的数字命名,以便每个数字只在你的代码中出现一次。这样,当数字改变时,代码中只需要改变一个地方。
例如,假设你的程序正在处理运输箱。空箱的重量是 5.7
。表达式 5.7 + contentsWeight
可能表示箱子及其内容的重量,这意味着数字 5.7
可能在软件中出现多次。当(而不是如果)有人改变此应用程序中使用的箱子样式时,所有这些 5.7
的出现将难以查找和更改。解决方案是确保值 5.7
出现且只出现一次,通常作为 const
标识符的初始化器。通常这将是 const double crateWeight = 5.7;
。之后,5.7 + contentsWeight
将被替换为 crateWeight + contentsWeight
。
这是通用的经验法则。但不幸的是,有一些细则。
有些人认为永远不应该在代码中散布数字字面量。他们认为所有数字都应该像上面描述的那样命名。然而,这条规则,尽管意图高尚,但在实践中并不奏效。它对人们来说太繁琐了,最终公司为此付出的代价比节省的更多。记住:所有编程规则的目标是减少时间、成本和风险。如果一条规则实际上使事情变得更糟,那它就是一条糟糕的规则,没有商量余地。
一个更实用的规则是关注那些可能改变的值。例如,如果一个数字字面量可能改变,它应该只在软件中出现一次,通常作为 const
标识符的初始化器。这条规则允许不变的值,例如某些 0、1、-1 等的出现,直接编码在软件中,这样程序员就不必搜索 one
或 zero
的唯一正确定义。换句话说,如果一个程序员想要遍历 vector
的索引,他可以简单地写 for (int i = 0; i < v.size(); ++i)
。前面描述的“极端主义”规则会要求程序员四处寻找是否有人定义了一个初始化为 0 的 const
标识符,如果没有,则定义自己的 const int zero = 0;
,然后用 for (int i = zero; i < v.size(); ++i)
替换循环。这完全是浪费时间,因为循环将总是从 0 开始。它增加了成本,却没有增加任何价值来弥补这个成本。
显然,人们可能会争论哪些值“可能改变”,但这种判断能力就是你获得高薪的原因:做好你的工作并做出决定。有些人如此害怕做出错误的决定,以至于他们会采取一刀切的规则,例如“给每个数字命名”。但是,如果你采取这样的规则,你保证会做出错误的决定:这些规则给你的公司带来的成本比它们节省的还要多。它们是糟糕的规则。
选择很简单:使用灵活的规则,即使你可能会做出错误的决定,或者使用一刀切的规则,并且保证会做出错误的决定。
还有一点细则:const
标识符应该定义在哪里。有三种典型情况
- 如果
const
标识符仅在单个函数中使用,它可以是该函数的局部变量。 - 如果
const
标识符在整个类中使用而别无他处,它可以是该类的private
部分中的static
成员。 - 如果
const
标识符在多个类中使用,它可以是 最合适的类的public
部分中的static
成员,或者可能是该类的private
成员,带有public
static
访问方法。
最后,将其设为命名空间内的 static
或放入未命名命名空间。非常努力地避免使用 #define
,因为预处理器是邪恶的。如果你无论如何需要使用 #define
,请完成后洗手。并且请咨询一些朋友,看他们是否知道更好的替代方案。
(如 FAQ 全文所用,“邪恶”并不意味着“永不使用”。在某些情况下,你会使用“邪恶”的东西,因为在那些特定情况下,它将是两害相权取其轻。)
数字字面量上的 L
、U
和 f
后缀有什么作用?
当你需要强制编译器将数字字面量视为指定类型时,应使用这些后缀。例如,如果 x
的类型为 float
,则表达式 x + 5.7
的类型为 double
:它首先将 x
的值提升为 double
,然后使用双精度指令执行算术运算。如果这是你想要的,那很好;但如果你真的想让它使用单精度指令执行算术运算,你可以将代码更改为 x + 5.7f
。注意:“命名”你的数字字面量更好,尤其是那些可能改变的数字字面量。这将要求你写 x + crateWeight
,其中 crateWeight
是一个初始化为 5.7f
的 const
float
。
U
后缀也类似。对于总是 >= 0 的变量,使用无符号整数可能是一个好主意。例如,如果一个变量表示数组的索引,那么该变量通常会被声明为 unsigned
。主要原因是它需要更少的代码,至少如果你仔细检查范围的话。例如,检查一个变量是否同时 >= 0 和 < max,如果所有变量都是有符号的,则需要两次测试:if (n >= 0 && n < max)
,但如果所有变量都是无符号的,则只需一次比较:if (n < max)
。
如果你最终使用无符号变量,通常最好强制你的数字字面量也为无符号。这使得更容易看出编译器将生成“无符号算术”指令。例如:if (n < 256U)
或 if ((n & 255u) < 32u)
。在单个算术表达式中混合有符号和无符号值通常会使程序员感到困惑——编译器并不总是按你期望的方式行事。
L
后缀不那么常见,但偶尔也会出于与上述类似的原因使用:为了使编译器使用 long
算术变得明显。
底线是:强制所有数字操作数都是正确类型,而不是依赖于 C++ 的数字表达式提升/降级规则,这对于程序员来说是一个很好的规范。例如,如果 x
是 int
类型,y
是 unsigned
类型,那么将 x + y
修改一下是个好主意,这样下一个程序员就能知道你是想使用无符号算术(例如,unsigned(x) + y
)还是有符号算术(例如,x + int(y)
)。另一种可能性是长整数算术:long(x) + long(y)
。通过使用这些类型转换,代码更加明确,在这种情况下是好事,因为很多程序员不知道所有隐式类型提升的规则。
我能理解与 (&&
) 和或 (||
) 运算符,但非 (!
) 运算符的目的是什么?
有些人对 !
运算符感到困惑。例如,他们认为 !true
等同于 false
,或者!(a < b)
等同于 a >= b
,所以在两种情况下 !
运算符似乎都没有增加任何东西。
答案:!
运算符在布尔表达式中非常有用,例如在 if
或 while
语句中出现。例如,让我们假设 A 和 B 是布尔表达式,可能是返回 bool
的简单方法调用。有各种方法可以组合这两个表达式
if ( A && B) /*...*/ ;
if (!A && B) /*...*/ ;
if ( A && !B) /*...*/ ;
if (!A && !B) /*...*/ ;
if (!( A && B)) /*...*/ ;
if (!(!A && B)) /*...*/ ;
if (!( A && !B)) /*...*/ ;
if (!(!A && !B)) /*...*/ ;
以及使用 ||
运算符形成的类似组。
注意:布尔代数可用于将每个 &&
版本转换为等效的 ||
版本,因此从真值表的角度来看,只有 8 个逻辑上不同的 if
语句。然而,由于可读性在软件中非常重要,程序员应同时考虑 &&
版本和逻辑上等效的 ||
版本。例如,程序员应根据哪个对维护代码的人更明显来选择 !A && !B
和 !(A || B)
。从这个意义上讲,实际上有 16 种不同的选择。
所有这一切的重点很简单:!
运算符在布尔表达式中非常有用。有时它是为了可读性而使用,有时它是因为像 !(a < b)
这样的表达式实际上与 a >= b
不等价,尽管你的小学数学老师告诉你的并非如此。
!(a < b)
在逻辑上与 a >= b
相同吗?
不!
尽管你的小学数学老师教导你,这些等价关系在软件中并非总是有效,特别是对于浮点表达式或用户定义类型。
示例:如果 a
是一个浮点NaN,那么 a < b
和 a >= b
都将为假。这意味着 !(a < b)
将为真,而 a >= b
将为假。
示例:如果 a
是一个类 Foo
的对象,该类重载了 operator<
和 operator>=
,那么这些运算符是否具有相反的语义取决于类 Foo
的创建者。它们可能应该具有相反的语义,但这取决于编写类 Foo
的人。
这个 NaN 东西是什么?
NaN 表示“不是一个数字”,用于浮点运算。
有许多浮点运算没有意义,例如除以零,取零或负数的对数,取负数的平方根等。根据你的编译器,其中一些操作可能会产生特殊的浮点值,例如无穷大(正无穷大和负无穷大有不同的值)和不是一个数字的值 NaN。
如果你的编译器生成一个 NaN,它有一个不寻常的特性,即它不等于任何值,包括它自己。例如,如果 a
是 NaN,那么 a == a
为假。事实上,如果 a
是 NaN,那么 a
将既不小于、不等于、也不大于任何值,包括它自己。换句话说,无论 b
的值如何,a < b
、a <= b
、a > b
、a >= b
和 a == b
都将返回 false。
以下是如何检查一个值是否为 NaN
#include <cmath>
void funct(double x)
{
if (isnan(x)) { // Though see caveat below
// x is NaN
// ...
} else {
// x is a normal value
// ...
}
}
注意:尽管 isnan()
是最新 C 标准库的一部分,但你的 C++ 编译器供应商可能不提供它。例如,Microsoft Visual C++.NET 不提供 isnan()
(尽管它提供在 <float.h>
中定义的 _isnan()
)。如果你的供应商不提供任何 isnan()
变体,请定义此函数
inline bool my_isnan(double x)
{
return x != x;
}
无论如何,请勿给我写信,仅仅为了说明你的编译器是否支持 isnan()
。
为什么浮点数如此不准确?为什么它不打印 0.43?
#include <iostream>
int main()
{
float a = 1000.43;
float b = 1000.0;
std::cout << a - b << '\n';
// ...
}
(在一个 C++ 实现上,这打印 0.429993)
免责声明:对舍入/截断/近似的挫败感并非真正的 C++ 问题;它是一个计算机科学问题。然而,人们在 comp.lang.c++
上不断询问,因此以下是一个名义上的答案。
答案:浮点数是一种近似值。IEEE 32 位浮点标准支持 1 位符号、8 位指数和 23 位尾数。由于规范化的二进位尾数总是采用 1.xxxxx... 形式,所以前导的 1 被省略,实际上你得到了 24 位的尾数。数字 1000.43(以及许多其他数字,包括一些非常常见的数字,如 0.1)无法精确地表示为浮点或双精度格式。1000.43 实际上表示为以下位模式(“s
”表示符号位的位置,“e
”表示指数位的位置,“m
”表示尾数位的位置)
seeeeeeeemmmmmmmmmmmmmmmmmmmmmmm
01000100011110100001101110000101
移位的尾数是 1111101000.01101110000101 或 1000 + 7045/16384。小数部分是 0.429992675781。对于浮点数,24 位尾数只能提供大约 1/16M 的精度。double
类型提供更高的精度(53 位尾数)。
为什么我的浮点比较不起作用?
因为浮点算术与实数算术不同。
底线:永远不要使用 ==
比较两个浮点数。
这是一个简单的例子
double x = 1.0 / 10.0;
double y = x * 10.0;
if (y != 1.0)
std::cout << "surprise: " << y << " != 1\n";
上面的“惊喜”消息将出现在某些(但不是所有)编译器/机器上。但即使你的特定编译器/机器没有导致上述“惊喜”消息(如果你写信告诉我它是否出现,你将表明你错过了本 FAQ 的重点),浮点数总会在某个时候让你感到惊讶。所以阅读本 FAQ,你就会知道该怎么做。
浮点数让你感到惊讶的原因是 float
和 double
值通常使用有限精度的二进制格式表示。换句话说,浮点数不是实数。例如,在你的机器的浮点格式中,可能无法精确表示数字 0.1。类比来说,在十进制格式中(除非你使用无限位数),不可能精确表示数字三分之一。
深入一点,我们来研究一下十进制数 0.625 的含义。这个数字在“十分位”是 6,在“百分位”是 2,在“千分位”是 5。换句话说,我们每个 10 的幂都有一个数字。但在二进制中,我们可能,取决于你的机器浮点格式的细节,每个 2 的幂都有一个位。所以小数部分可能有一个“二分之一”位,一个“四分之一”位,一个“八分之一”位,“十六分之一”位等等,并且这些位置中的每一个都包含一个位。
假设你的机器使用上述方案表示浮点数的小数部分(通常比这更复杂,但如果你已经确切地知道浮点数是如何存储的,那么你可能一开始就不需要这篇 FAQ,所以把这看作一个很好的起点)。在那台假设的机器上,0.625 小数部分的位将是 101:二分之一位是 1,四分之一位是 0,八分之一位是 1。换句话说,0.625 是 ½ + ⅛。
但在这台假想的机器上,0.1 无法精确表示,因为它不能由有限个 2 的幂的和构成。你可以接近它,但无法精确表示。特别是,二分之一位是 0,四分之一位是 0,八分之一位是 0,最后十六分之一位是 1,留下 1/10 - 1/16 = 3/80 的余数。找出其他位留作练习(提示:寻找重复的位模式,类似于尝试在十进制格式中表示 1/3 或 1/7)。
信息是有些浮点数不能总是精确表示,因此比较并不总是能达到你想要的效果。换句话说,如果计算机实际将 10.0 乘以 1.0/10.0,它可能无法精确地得到 1.0。
这就是问题所在。现在是解决方案:在比较浮点数是否相等时(或在对浮点数进行其他操作时,例如,计算两个浮点数的平均值看起来很简单,但要正确操作需要至少三个情况的 if
/else
),要非常小心。
这是错误的做法
void dubious(double x, double y)
{
// ...
if (x == y) // Dubious!
foo();
// ...
}
如果你真正想要的是确保它们“非常接近”(例如,如果变量 a
包含值 1.0 / 10.0
,并且你想查看 if (10*a == 1)
),你可能需要做一些比上述更花哨的事情
void smarter(double x, double y)
{
// ...
if (isEqual(x, y)) // Smarter!
foo();
// ...
}
定义 isEqual()
函数的方法有很多种,包括
#include <cmath> /* for std::abs(double) */
inline bool isEqual(double x, double y)
{
const double epsilon = /* some small number such as 1e-5 */;
return std::abs(x - y) <= epsilon * std::abs(x);
// see Knuth section 4.2.2 pages 217-218
}
注意:上述解决方案并非完全对称,这意味着 isEqual(x,y)
!=
isEqual(y,x)
是可能的。从实际角度来看,当 x
和 y
的幅度显著大于 epsilon
时,这种情况通常不会发生,但你的情况可能会有所不同。
如需其他有用函数,请查阅以下内容(按字母顺序排列)
- 艾萨克森 (Isaacson, E.) 和凯勒 (Keller, H.),《数值方法分析》,多佛出版社。
- Kahan, W.,
http.cs.berkeley.edu/~wkahan/
。 - 克努特 (Knuth, Donald E.),《计算机程序设计艺术》,第二卷:半数值算法,艾迪生-韦斯利出版社,1969 年。
- LAPACK: 线性代数子程序库,
www.siam.org
- NETLIB:ACM Transactions on Mathematical Software 中收集的算法,这些算法都经过审阅,以及许多其他经过同行较不正式审查的算法,
www.netlib.org
- 数值食谱 (Numerical Recipes),由 Press 等人编写。但请注意一些负面评价,例如
amath.colorado.edu/computing/Fortran/numrec.html
- Ralston 和 Rabinowitz,《数值分析入门:第二版》,多佛出版社。
- Stoer, J. 和 Bulirsch, R.,《数值分析导论》,施普林格出版社,德语版。
仔细检查你的假设,包括“显而易见”的事情,例如如何计算平均值、如何求解二次方程等等。不要假设你在高中学到的公式适用于浮点数!
有关浮点计算的基本思想和问题的见解,请从 David Goldberg 的论文《每个计算机科学家都应该了解的浮点算术》或PDF 格式开始。你可能还想阅读 Doug Priest 的这份补充材料。合并后的论文 + 补充材料也可用。你可能还想点击此处获取其他浮点主题的链接。
为什么 cos(x) != cos(y)
,即使 x == y
?(或者正弦、正切、对数或几乎任何其他浮点计算)
我知道这很难接受,但浮点运算根本不像大多数人期望的那样工作。更糟的是,某些差异取决于你的特定计算机的浮点硬件细节和/或你在特定编译器上使用的优化设置。你可能不喜欢这样,但事实就是如此。唯一的“理解”方式是抛开你对事物应该如何表现的假设,并接受它们实际如何表现。
我们来举一个简单的例子。事实证明,在某些安装中,即使 x == y
,cos(x) != cos(y)
。这不是笔误;如果你不震惊,请再读一遍:某个数的余弦值可能不等于同一个数的余弦值。(或者正弦、正切、对数,或者几乎任何其他浮点计算。)
#include <iostream>
#include <cmath>
void foo(double x, double y)
{
if (std::cos(x) != std::cos(y)) {
std::cout << "Huh?!?\n"; // You might end up here when x == y!!
}
}
int main()
{
foo(1.0, 1.0);
return 0;
}
在许多(并非所有)计算机上,即使 x == y
,你最终也会进入 if
块。如果这没有让你震惊,你就是睡着了;再读一遍。如果你愿意,可以在你的特定计算机上试试。你们中的一些人会进入 if
块,一些人不会,而对于一些人来说,它将取决于你的特定编译器、选项、硬件或月相的细节。
为什么,你问,会发生这种事?好问题;感谢提问。答案如下(强调“通常”一词;行为取决于你的硬件、编译器等):浮点计算和比较通常由特殊硬件执行,这些硬件通常包含特殊寄存器,而这些寄存器通常比 double
拥有更多的位。这意味着中间浮点计算通常比 sizeof(double)
拥有更多的位,当浮点值写入 RAM 时,它通常会被截断,通常会损失一些精度位。
换句话说,中间计算通常比相同值存储到 RAM 时更精确(位数更多)。可以这样想:将浮点结果存储到 RAM 需要丢弃一些位,因此将 RAM 中(被截断的)值与浮点寄存器中(未被截断的)值进行比较可能不会得到你期望的结果。假设你的代码计算 cos(x)
,然后截断该结果并将其存储到一个临时变量,例如 tmp
。然后它可能会计算 cos(y)
,然后(请击鼓)将 cos(y)
的未截断结果与 tmp
,即 cos(x)
的截断结果进行比较。用一种假想的汇编语言表达,表达式 cos(x) != cos(y)
可能会被编译成这样
// Imaginary assembly language
fp_load x // load a floating-point register with the value of parameter x
call _cos // call cos(double), using the floating point register for param and result
fp_store tmp // truncate the floating-point result and store into temporary local var, tmp
fp_load y // load a floating-point register with the value of parameter y
call _cos // call cos(double), using the floating point register for param ans result
fp_cmp tmp // compare the untruncated result (in the register) with the truncated value in tmp
// ...
你明白了吗?你的特定安装可能会将其中一个 cos()
调用的结果存储到 RAM 中,在此过程中将其截断,然后稍后将该截断的值与第二次 cos()
调用的未截断结果进行比较。根据许多细节,这两个值可能不相等。
情况变得更糟了;最好坐下来。事实证明,行为可能取决于 cos()
调用和 !=
比较之间有多少指令。换句话说,如果你将 cos(x)
和 cos(y)
放入局部变量,然后稍后比较这些变量,比较结果可能取决于你的代码在将结果存储到局部变量并比较变量之后究竟做了什么(如果有的话)。吞咽。
void foo(double x, double y)
{
double cos_x = cos(x);
double cos_y = cos(y);
// the behavior might depend on what's in here
if (cos_x != cos_y) {
std::cout << "Huh?!?\n"; // You might end up here when x == y!!
}
}
现在你的嘴巴应该张大了。如果不是,你要么很快从上面学到了,要么还在睡觉。再读一遍。当 x == y
时,你仍然可能进入 if
块,这取决于,除此之外,...
行中有多少代码。哇。
原因:如果编译器能够证明你没有在 ...
行中干扰任何浮点寄存器,它可能实际上不会将 cos(y)
存储到 cos_y
中,而是将其留在寄存器中,并将未截断的寄存器与截断的变量 cos_x
进行比较。在这种情况下,你可能会进入 if
块。但是,如果你在两行之间调用了一个函数,例如打印一个或两个变量,或者你做了其他干扰浮点寄存器的事情,编译器将(可能)需要将 cos(y)
的结果存储到变量 cos_y
中,之后它将比较两个截断的值。在这种情况下,你将不会进入 if
块。
如果你没有听懂本次讨论中的其他任何内容,请记住这一点:浮点比较是棘手的、微妙的,并且充满危险。请小心。浮点实际工作的方式与大多数程序员倾向于认为它应该工作的方式不同。如果你打算使用浮点数,你需要学习它实际是如何工作的。
enum Color
这样的枚举类型是什么类型?它是 int
类型吗?
诸如 enum Color { red, white, blue };
这样的枚举是它自己的类型。它不是 int
类型。
当你创建一个枚举类型的对象时,例如 Color x;
,我们说对象 x
的类型是 Color
。对象 x
不是“枚举”类型,也不是 int
类型。
枚举类型的表达式可以被转换为一个临时的 int
。类比可能有所帮助。float
类型的表达式可以被转换成一个临时的 double
,但这并不意味着 float
是 double
的子类型。例如,在声明 float y;
之后,我们说 y
的类型是 float
,并且表达式 y
可以被转换成一个临时的 double
。当这种情况发生时,通过从 y
中复制一些东西来创建一个全新的临时 double
。同样,Color
对象(例如 x
)可以被转换成一个临时的 int
,在这种情况下,通过从 x
中复制一些东西来创建一个全新的临时 int
。(注意:本段中 float
/ double
类比的唯一目的是帮助解释枚举类型的表达式如何可以转换为临时的 int
;不要试图使用该类比来暗示任何其他行为!)
上述转换与子类型关系(例如派生类 Car
及其基类 Vehicle
之间的关系)非常不同。例如,Car
类的对象(例如 Car z;
)实际上是 Vehicle
类的对象,因此你可以将 Vehicle&
绑定到该对象,例如 Vehicle& v = z;
。与上一段不同,对象 z
没有被复制到临时对象;引用 v
绑定到 z
本身。所以我们说 Car
类的对象是一个 Vehicle
,但“Color”类的对象只是可以复制/转换为临时的 int
。区别很大。
最后一点,特别是对于 C 程序员:C++ 编译器不会自动将 int
表达式转换为临时的 Color
。由于这种转换是不安全的,它需要一个强制类型转换,例如 Color x = Color(2);
。但要确保你的整数是有效的枚举值。如果你提供一个非法值,你可能会得到意想不到的结果。编译器不会为你进行检查;你必须自己做。
如果枚举类型与任何其他类型都不同,那它有什么用?你能用它做什么?
我们来考虑这个枚举类型:enum Color { red, white, blue };
。
最好的看法(C 程序员:请坐稳了!!)是,这种类型的值是 red
、white
和 blue
,而不是仅仅将这些名称视为常量 int
值。C++ 编译器提供从 Color
到 int
的自动转换,在这种情况下,转换后的值将分别为 0、1 和 2。但你不应该将 blue
视为 2 的一个花哨名称。blue
的类型是 Color
,并且存在从 blue
到 2 的自动转换,但从 int
到 Color
的逆转换,C++ 编译器不会自动提供。
这是一个演示从 Color
到 int
转换的示例
enum Color { red, white, blue };
void f()
{
int n;
n = red; // change n to 0
n = white; // change n to 1
n = blue; // change n to 2
}
以下示例也演示了从 Color
到 int
的转换
void f()
{
Color x = red;
Color y = white;
Color z = blue;
int n;
n = x; // change n to 0
n = y; // change n to 1
n = z; // change n to 2
}
然而,从 int
到 Color
的逆转换,C++ 编译器不会自动提供
void f()
{
Color x;
x = blue; // change x to blue
x = 2; // compile-time error: can't convert int to Color
}
上面最后一行表明枚举类型不是伪装的 int
。如果你愿意,可以把它们看作 int
类型,但如果你这样做,你必须记住 C++ 编译器不会隐式地将 int
转换为 Color
。如果你真的需要那样做,可以使用强制类型转换
void f()
{
Color x;
x = red; // change x to red
x = Color(1); // change x to white
x = Color(2); // change x to blue
x = 2; // compile-time error: can't convert int to Color
}
枚举类型还有其他与 int
不同的地方。例如,枚举类型没有 ++
运算符
void f()
{
int n = red; // change n to 0
Color x = red; // change x to red
n++; // change n to 1
x++; // compile-time error: can't ++ an enumeration (though see caveat below)
}
最后一行警告:提供一个重载运算符使其合法是合法的,例如定义 operator++(Color& x)
。
还有哪些“新手”指南适合我?
一个很好的起点是本网站的入门页面。