通过 <iostream>
和 <cstdio>
进行输入/输出
为什么我应该使用 <iostream>
而不是传统的 <cstdio>
?
提高类型安全性,减少错误,允许扩展,并提供可继承性。
printf()
可以说没有问题,而 scanf()
尽管容易出错,但可能尚可接受,然而两者都限制了 C++ I/O 的能力。与 C(使用 printf()
和 scanf()
)相比,C++ I/O(使用 <<
和 >>
)具有以下特点:
- 更类型安全: 使用
<iostream>
,被 I/O 的对象的类型由编译器静态确定。相比之下,<cstdio>
使用"%"
字段动态确定类型。 - 更不容易出错: 使用
<iostream>
,没有冗余的"%"
标记需要与实际被 I/O 的对象保持一致。消除冗余消除了错误的一类。 - 可扩展: C++
<iostream>
机制允许对新的用户定义类型进行 I/O,而不会破坏现有代码。想象一下,如果每个人都同时向printf()
和scanf()
添加新的不兼容的"%"
字段,那将是多么混乱?! - 可继承: C++
<iostream>
机制由真实的类构建,例如std::ostream
和std::istream
。与<cstdio>
的FILE*
不同,这些是真实的类,因此可以继承。这意味着您可以拥有其他用户定义的、看起来和行为像流的东西,但它们可以做您想要的任何奇特美妙的事情。您可以自动使用数以亿计由您甚至不认识的用户编写的 I/O 代码行,而且他们不需要了解您的“扩展流”类。
为什么当有人输入无效字符时,我的程序会进入无限循环?
例如,假设您有以下从 std::cin
读取整数的代码
#include <iostream>
int main()
{
std::cout << "Enter numbers separated by whitespace (use -1 to quit): ";
int i = 0;
while (i != -1) {
std::cin >> i; // BAD FORM — See comments below
std::cout << "You entered " << i << '\n';
}
// ...
}
这段代码的问题在于它缺乏任何检查以查看是否有人输入了无效字符。特别是,如果有人输入了不像整数的东西(例如“x”),流 std::cin
会进入“失败状态”,所有后续的输入尝试都会立即返回,什么都不做。换句话说,程序会进入无限循环;如果 42
是上次成功读取的数字,程序会一遍又一遍地打印消息 您输入了 42
。
检查无效输入的一种简单方法是将输入请求从 while
循环体移到 while
循环的控制表达式中。例如,
#include <iostream>
int main()
{
std::cout << "Enter a number, or -1 to quit: ";
int i = 0;
while (std::cin >> i) { // GOOD FORM
if (i == -1) break;
std::cout << "You entered " << i << '\n';
}
// ...
}
这将导致 while 循环在遇到文件末尾、输入错误的整数或输入 -1
时退出。
(当然,你可以通过将 while
循环表达式从 while (std::cin >> i)
更改为 while ((std::cin >> i) && (i != -1))
来消除 break
,但这不是本 FAQ 的重点,因为本 FAQ 涉及 iostreams 而不是通用的结构化编程指南。)
如何让 std::cin
跳过无效的输入字符?
使用 std::cin.clear()
和 std::cin.ignore()
。
#include <iostream>
#include <limits>
int main()
{
int age = 0;
while ((std::cout << "How old are you? ")
&& !(std::cin >> age)) {
std::cout << "That's not a number; ";
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
std::cout << "You are " << age << " years old\n";
// ...
}
当然,当输入超出范围时,您也可以打印错误消息。例如,如果您希望 age
介于 1 到 200 之间,您可以将 while
循环更改为
// ...
while ((std::cout << "How old are you? ")
&& (!(std::cin >> age) || age < 1 || age > 200)) {
std::cout << "That's not a number between 1 and 200; ";
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
// ...
这是一个示例运行
How old are you? foo
That's not a number between 1 and 200; How old are you? bar
That's not a number between 1 and 200; How old are you? -3
That's not a number between 1 and 200; How old are you? 0
That's not a number between 1 and 200; How old are you? 201
That's not a number between 1 and 200; How old are you? 2
You are 2 years old
那个奇怪的 while (std::cin >> foo)
语法是如何工作的?
有关“奇怪的 while (std::cin >> foo)
语法”的示例,请参阅上一个常见问题解答。
表达式 (std::cin >> foo)
调用适当的 operator>>
(例如,它调用左侧接受 std::istream
,如果 foo
是 int
类型,则右侧接受 int&
的 operator>>
)。std::istream
的 operator>>
函数通常返回其左参数,在这种情况下,它将返回 std::cin
。接下来,编译器注意到返回的 std::istream
位于布尔上下文中,因此它将该 std::istream
转换为布尔值。
为了将 std::istream
转换为布尔值,编译器会调用一个名为 std::istream::operator void*()
的成员函数。此函数返回一个 void*
指针,该指针又会转换为布尔值(NULL
变为 false
,任何其他指针变为 true
)。因此,在这种情况下,编译器会生成对 std::cin.operator void*()
的调用,就像您显式地进行类型转换一样,例如 (void*) std::cin
。
如果流处于良好状态,operator void*()
转换运算符将返回一个非 NULL
指针;如果流处于失败状态,则返回 NULL
。例如,如果您读取的次数过多(例如,如果已经到达文件末尾),或者如果输入流上的实际信息对于 foo
的类型无效(例如,如果 foo
是 int
而数据是字符‘x’),流将进入失败状态,并且转换运算符将返回 NULL
。
operator>>
不仅仅返回一个 bool
(或 void*
)来表示它是否成功或失败的原因是为了支持“级联”语法
std::cin >> foo >> bar;
operator>>
是左结合的,这意味着上面的代码被解析为
(std::cin >> foo) >> bar;
换句话说,如果我们将 operator>>
替换为一个普通的函数名,例如 readFrom()
,则这会变成表达式
readFrom( readFrom(std::cin, foo), bar);
和往常一样,我们从最里面的表达式开始求值。由于 operator>>
的左结合性,这恰好是最左边的表达式 std::cin >> foo
。这个表达式将 std::cin
(更准确地说,它返回其左手参数的引用)返回给下一个表达式。下一个表达式也返回(一个引用)std::cin
,但这个第二个引用被忽略了,因为它是这个“表达式语句”中最外层的表达式。
为什么我的输入似乎处理到文件末尾之后?
因为 EOF 状态可能直到尝试读取文件末尾之后才设置。也就是说,读取文件的最后一个字节可能不会设置 EOF 状态。例如,假设输入流映射到键盘——在这种情况下,C++ 库甚至在理论上都无法预测用户刚刚输入的字符是否是最后一个字符。
例如,以下代码可能在计数 i
上出现“差一”错误
int i = 0;
while (! std::cin.eof()) { // WRONG! (not reliable)
std::cin >> x;
++i;
// Work with x ...
}
你真正需要的是
int i = 0;
while (std::cin >> x) { // RIGHT! (reliable)
++i;
// Work with x ...
}
为什么我的程序在第一次迭代后会忽略我的输入请求?
因为数字提取器会在输入缓冲区中留下非数字字符。
如果你的代码看起来像这样
char name[1000];
int age;
for (;;) {
std::cout << "Name: ";
std::cin >> name;
std::cout << "Age: ";
std::cin >> age;
}
你真正想要的是
for (;;) {
std::cout << "Name: ";
std::cin >> name;
std::cout << "Age: ";
std::cin >> age;
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
}
当然,你可能想把 for (;;)
语句改成 while (std::cin)
,但是不要把它和通过 std::cin.ignore(...);
行在循环结束时跳过非数字字符混淆。
我应该用 std::endl
还是 '\n'
来结束我的输出行?
使用 std::endl
在发送 '\n'
后会刷新输出缓冲区,这意味着 std::endl
在性能上开销更大。显然,如果您需要在发送 '\n'
后刷新缓冲区,那么使用 std::endl
;但如果您不需要刷新缓冲区,使用 '\n'
会使代码运行更快。
这段代码只是输出一个 '\n'
void f()
{
std::cout << /*...stuff...*/ << '\n';
}
这段代码输出一个 '\n'
,然后刷新输出缓冲区
void g()
{
std::cout << /*...stuff...*/ << std::endl;
}
这段代码只是刷新输出缓冲区
void h()
{
std::cout << /*...stuff...*/ << std::flush;
}
注意:以上三个例子都需要 #include <iostream>
如何为我的 class
Fred
提供打印功能?
使用运算符重载
提供一个友元
左移运算符 operator<<
。
#include <iostream>
class Fred {
public:
friend std::ostream& operator<< (std::ostream& o, const Fred& fred);
// ...
private:
int i_; // Just for illustration
};
std::ostream& operator<< (std::ostream& o, const Fred& fred)
{
return o << fred.i_;
}
int main()
{
Fred f;
std::cout << "My Fred object: " << f << "\n";
// ...
}
我们使用非成员函数(在本例中为友元
),因为 Fred
对象是 <<
运算符的右操作数。如果 Fred
对象应该在 <<
的左侧(即 myFred << std::cout
而不是 std::cout << myFred
),我们可以使用名为 operator<<
的成员函数。
注意 operator<<
返回流。这是为了使输出操作能够级联。
但是,我难道不应该总是使用 printOn()
方法而不是友元函数吗?
不是。
人们希望总是使用 printOn()
方法而不是友元函数的常见原因是他们错误地认为友元违反了封装和/或友元是邪恶的。这些信念是天真而错误的:如果使用得当,友元实际上可以增强封装。
这并不是说 printOn()
方法方法从不有用。例如,它在为整个类层次结构提供打印时很有用。但是,如果使用 printOn()
方法,它通常应该是 protected
,而不是 public
。
为了完整起见,这里是“printOn()
方法方法”。其思想是有一个成员函数(通常称为 printOn()
)来执行实际的打印,然后让 operator<<
调用该 printOn()
方法。当操作错误时,printOn()
方法是 public
的,因此 operator<<
不需要是 friend
——它可以是一个简单的顶级函数,既不是 friend
也不是类的成员。以下是一些示例代码
#include <iostream>
class Fred {
public:
void printOn(std::ostream& o) const;
// ...
};
// operator<< can be declared as a non-friend [NOT recommended!]
std::ostream& operator<< (std::ostream& o, const Fred& fred);
// The actual printing is done inside the printOn() method [NOT recommended!]
void Fred::printOn(std::ostream& o) const
{
// ...
}
// operator<< calls printOn() [NOT recommended!]
std::ostream& operator<< (std::ostream& o, const Fred& fred)
{
fred.printOn(o);
return o;
}
人们错误地认为这会降低维护成本,“因为它避免了使用友元函数。”这是一个错误的假设,因为
- 由顶级函数调用的成员函数方法在维护成本方面零收益。 假设实际打印需要 N 行代码。对于
friend
函数,这 N 行代码将直接访问类的private
/protected
部分,这意味着无论何时有人更改类的private
/protected
部分,这 N 行代码都需要被扫描并可能被修改,这增加了维护成本。然而,使用printOn()
方法根本不会改变这一点:我们仍然有 N 行代码可以直接访问类的private
/protected
部分。因此,将代码从friend
函数移到成员函数中根本不会降低维护成本。零减少。维护成本没有好处。(如果说有什么不同,那就是使用printOn()
方法会稍微差一些,因为您现在需要维护更多行代码,因为您多了一个以前没有的函数。) - 通过顶级函数调用成员函数的方法使类更难使用,特别是对于不是类设计者的程序员。 该方法公开了一个程序员不应该调用的
public
方法。当程序员读取类的public
方法时,他们会看到两种做同一件事的方式。文档需要说明类似“这与那个完全相同,但不要使用这个;而是使用那个。”平均程序员会说,“嗯?如果我不应该使用它,为什么要将该方法设为public
?”实际上,printOn()
方法是public
的唯一原因是避免授予operator<<
友元状态,对于只想使用类的程序员来说,这个概念介于微妙和不可理解之间。
总之:通过顶级函数调用成员函数的方法有成本但没有好处。因此,总的来说,这是一个坏主意。
注意:如果 printOn()
方法是 protected
或 private
,则第二个异议不适用。在某些情况下,这种方法是合理的,例如在为整个类层次结构提供打印时。另请注意,当 printOn()
方法不是 public
时,operator<<
需要是 friend
。
如何为我的 class
Fred
提供输入?
使用运算符重载
提供一个友元
右移运算符 operator>>
。这与输出运算符类似,只是参数没有const
限定符:“Fred&
”而不是“const Fred&
”。
#include <iostream>
class Fred {
public:
friend std::istream& operator>> (std::istream& i, Fred& fred);
// ...
private:
int i_; // Just for illustration
};
std::istream& operator>> (std::istream& i, Fred& fred)
{
return i >> fred.i_;
}
int main()
{
Fred f;
std::cout << "Enter a Fred object: ";
std::cin >> f;
// ...
}
请注意,operator>>
返回流。这是为了使输入操作能够级联和/或在 while
循环或 if
语句中使用。
如何为整个类层次结构提供打印功能?
提供一个友元
operator<<
,它调用一个 protected
虚函数
class Base {
public:
friend std::ostream& operator<< (std::ostream& o, const Base& b);
// ...
protected:
virtual void printOn(std::ostream& o) const = 0; // Or plain virtual; see below
};
inline std::ostream& operator<< (std::ostream& o, const Base& b)
{
b.printOn(o);
return o;
}
class Derived : public Base {
public:
// ...
protected:
virtual void printOn(std::ostream& o) const;
};
void Derived::printOn(std::ostream& o) const
{
// ...
}
最终结果是 operator<<
表现得像是动态绑定的,尽管它是一个友元
函数。这被称为虚友元函数惯用法。
请注意,派生类会重写 printOn(std::ostream&)
const
。特别是,它们不会提供自己的 operator<<
。
至于 Base::printOn()
是普通虚函数还是纯虚函数,如果该函数的实现代码在两个或更多派生类中会重复,则考虑将其设为普通虚函数(不带“= 0
”)。但是,如果 Base
是一个抽象基类,成员数据很少或没有,您可能无法为 Base::printOn()
提供有意义的定义,并且您应该将其设为纯虚函数。如果您不确定,请将其设为纯虚函数,至少在您更好地掌握派生类之前。
如何以二进制模式打开流?
使用 std::ios::binary
。
一些操作系统区分文本和二进制模式。在文本模式下,行尾序列和其他内容可能会被转换;在二进制模式下,它们不会。例如,在 Windows 的文本模式下,"\r\n"
在输入时会被转换为 "\n"
,在输出时则相反。
要以二进制模式读取文件,请使用类似以下代码:
#include <string>
#include <iostream>
#include <fstream>
void readBinaryFile(const std::string& filename)
{
std::ifstream input(filename.c_str(), std::ios::in | std::ios::binary);
char c;
while (input.get(c)) {
// ...do something with c here...
}
}
注意:input >> c
会丢弃前导空格,因此在读取二进制文件时通常不会使用它。
如何“重新打开” std::cin
和 std::cout
为二进制模式?
这取决于具体的实现。请查阅您的编译器的文档。
例如,假设您想使用 std::cin
和 std::cout
进行二进制 I/O。
不幸的是,目前没有标准方法可以使 std::cin
、std::cout
和/或 std::cerr
以二进制模式打开。关闭流并尝试以二进制模式重新打开它们可能会产生意外或不良结果。
在有区别的系统上,实现可能会提供一种方法使它们成为二进制流,但您必须检查实现细节以找出答案。
如何将我的类的对象写入/从数据文件中读取?
阅读对象序列化部分。
如何将我的类的对象发送到另一台计算机(例如,通过套接字、TCP/IP、FTP、电子邮件、无线链接等)?
阅读对象序列化部分。
为什么我无法打开不同目录中的文件,例如 "..\test.dat"
?
因为 "\t"
是一个制表符。
在您的文件名中,即使在使用反斜杠的操作系统(DOS、Windows、OS/2 等)上,也应该使用正斜杠。例如
#include <iostream>
#include <fstream>
int main()
{
#if 1
std::ifstream file("../test.dat"); // RIGHT!
#else
std::ifstream file("..\test.dat"); // WRONG!
#endif
// ...
}
请记住,反斜杠("\"
)在字符串字面量中用于创建特殊字符:"\n"
是换行符,"\b"
是退格符,"\t"
是制表符,"\a"
是“警报”符,"\v"
是垂直制表符等。因此,文件名 "\version\next\alpha\beta\test.dat"
会被解释为一堆非常奇怪的字符。为了安全起见,即使在将 "\"
用作目录分隔符的系统上,也请使用 "/version/next/alpha/beta/test.dat"
。这是因为这些操作系统上的库例程可以互换处理 "/"
和 "\"
。
当然,你*可以*使用 "\\version\\next\\alpha\\beta\\test.dat"
,但这可能会对你造成伤害(你很有可能会忘记一个 "\"
,这是一个相当微妙的错误,因为大多数人不会注意到它),而且它对你没有帮助(使用 "\\"
比 "/"
没有优势)。此外,"/"
更具可移植性,因为它适用于所有 Unix、Plan 9、Inferno、所有 Windows、OS/2 等版本,而 "\\"
仅适用于其中一部分。所以 "\\"
会让你付出代价而一无所获:请改用 "/"
。
如何判断(如果按下了某个键,是哪个键)在用户按下 ENTER 键之前?
这不是 C++ 的标准功能——C++ 甚至不要求你的系统拥有键盘!。这意味着每个操作系统和供应商的做法都有所不同。
有关您特定安装的详细信息,请阅读编译器附带的文档。
(顺便说一句,在 UNIX 上,该过程通常分为两步:首先将终端设置为单字符模式,然后使用 select()
或 poll()
来测试是否有按键按下。您也许可以调整此代码。)
如何让用户按下的键不在屏幕上回显?
这不是 C++ 的标准功能——C++ 甚至不要求你的系统有键盘或屏幕。这意味着每个操作系统和供应商的做法都有所不同。
有关您特定安装的详细信息,请阅读编译器附带的文档。
如何在屏幕上移动光标?
这不是 C++ 的标准功能——C++ 甚至不要求你的系统有屏幕。这意味着每个操作系统和供应商的做法都有所不同。
有关您特定安装的详细信息,请阅读编译器附带的文档。
如何清除屏幕?是否有类似 clrscr()
的函数?
这不是 C++ 的标准功能——C++ 甚至不要求你的系统有屏幕。这意味着每个操作系统和供应商的做法都有所不同。
有关您特定安装的详细信息,请阅读编译器附带的文档。
如何更改屏幕颜色?
这不是 C++ 的标准功能——C++ 甚至不要求你的系统有屏幕。这意味着每个操作系统和供应商的做法都有所不同。
有关您特定安装的详细信息,请阅读编译器附带的文档。
如何将 char
打印为数字?如何打印 char*
以使输出显示指针的数值?
强制类型转换。
C++ 流在打印 char
时会像大多数程序员期望的那样工作。如果你打印一个字符,它会打印为实际的字符,而不是字符的数值
#include <iostream>
#include <string>
void f()
{
char c = 'x';
std::string s = "Now is";
const char* t = "the time";
std::cout << c; // Prints a character, in this case, x
std::cout << 'y'; // Prints a character, in this case, y
std::cout << s[2]; // Prints a character, in this case, w
std::cout << t[2]; // Prints a character, in this case, e
}
C++ 流在打印 char*
时也能做正确的事:它会打印字符串,该字符串必须以 '\0'
终止。
#include <iostream>
#include <string>
void f()
{
const char* s = "xyz";
std::cout << s; // Prints the string, in this case, xyz
std::cout << "pqr"; // Prints the string, in this case, pqr
}
这些看起来很明显,仅仅因为它们很直观,但实际上其中包含一些非常棒的功能。C++ 流根据您当前的语言环境将字符值解释为实际的人类可读符号,此外它们还知道,如果您给它们一个字符指针,您可能意图打印 C 风格的字符串。唯一的问题是当您不希望代码以这种方式运行时。
想象一下,您有一个结构,它将人的年龄存储为 unsigned char
。如果您想打印该结构,说一个人的年龄是 'A'
并没有多大意义。或者,如果出于某种原因,您想打印该年龄变量的地址,流将从该地址开始,并将每个后续字节(您的结构或类的字节,甚至是堆栈的字节!)解释为字符,直到最终遇到包含 '\0'
的第一个字节才停止。
// Variable 'age' stores the person's age
unsigned char age = 65;
// Our goal here is to print the person's age:
std::cout << age; // Whoops! Prints 'A', not the numeric age
// Our next goal is to print the age variable's location, that is, its address:
std::cout << &age; // Whoops! Prints garbage, and might crash
这不是所期望的。最简单且通常推荐的解决方案是将 char
或 char*
强制转换为您的编译器不解释为字符的类型,分别是 int
或 void*
// Variable 'age' stores the person's age
unsigned char age = 65;
// Our goal here is to print the person's age:
std::cout << static_cast<unsigned>(age); // Good: prints 65
// Our next goal is to print the age variable's location, that is, its address:
std::cout << static_cast<const void*>(&age); // Good: prints the variable's address
这对于显式指定的类型(如上面所示的 unsigned char
)非常有效。但是,如果您正在创建模板,其中上面提到的 unsigned char
类型仅被称为某种数字类型 T
,那么您不想假设正确的数字类型是 unsigned
或其他任何东西。在这种情况下,您希望将您的 T
对象转换为正确的数字类型,无论那是什么。
例如,您的类型 T
可能是从 char
到 int
到 long
或 long long
(如果您的编译器已经支持)的任何类型。或者您的类型 T
甚至可能是一个抽象数字类,它甚至不提供转换为任何内置整数的类型转换(例如,考虑 safe_integer
、ranged_integer
或 big_num
类)。
处理这个问题的一种方法是通过特性或模板特化,但有一个更简单的解决方案,适用于 char
类型而不会危及其他类型。只要类型 T
提供具有普通语义的一元 +
运算符[*脚注],所有内置数字类型都提供此运算符,一切都会正常工作
template <typename T>
void my_super_function(T x)
{
// ...
std::cout << +x << '\n'; // promotes x to a type printable as a number, regardless of type
// ...
}
简直是魔法。现在你最需要担心的是,这可能对其他开发者来说有点神秘。如果你正在想,“自己,我应该创建一个名为 promote_to_printable_integer_type()
的函数,让我的代码自文档化。”不幸的是,C++ 目前缺乏类型推断,所以编写这样的函数将需要非常复杂的代码,它可能会带来比你可能避免的(潜在)错误更多的错误。所以短期来看,最好的解决方案就是硬着头皮,使用 operator+
并给你的代码添加注释。
当您的组织可以使用 C++11 时,您就可以开始享受类型推断的便利了
template <typename T>
auto promote_to_printable_integer_type(T i) -> decltype(+i)
{
return +i;
}
恕不赘述,返回类型是“与 +i 类型相同的类型”。这可能看起来很奇怪,但就像大多数通用模板一样,重要的是使用的便捷性,而不是模板定义本身的美观。这是一个示例用法
void f()
{
unsigned char age = 65;
std::cout << promote_to_printable_integer_type(age); // Prints 65
}
template <typename T>
void g(T x)
{
// ...
std::cout << promote_to_printable_integer_type(x); // Works for any T that provides unary +
// ...
}
由于 C++11 类型推断,此答案将更新。请关注此空间以获取近期更新!!
[*脚注] 如果您正在定义一个表示数字的类,要提供具有规范语义的一元 +
运算符,请创建一个 operator+()
,它只是按值或按常量引用返回 *this
。