流和基本文件I/O
流(stream) 是由字符(或其他类型的数据)构成的"流"(flow)。流向程序,称为 输入流(input stream)。流出程序,则称为 输出流(output stream)。如输入流来自键盘,表明程序要从键盘获取输入。如输入流来自文件,表明程序要从文件获取输入。类似地,输出流可发送给屏幕或文件。
之前我们已经使用过 cin
作为连接到键盘的输入流,cout
作为连接到屏幕的输出流。下面主要讨论如何使用流对象来进行文件 I/O。
// 从文件infile.dat读取3个数,求和,将结果写入文件outfile.dat
#include <fstream>
using namespace std;
int main()
{
ifstream inStream;
ofstream outStream;
inStream.open("infile.dat");
outStream.open("outfile.dat");
int first, second, third;
inStream >> first >> second >> third;
outStream << "The sum of the first 3\n"
<< "numbers in infile.dat\n"
<< "is " << (first + second + third)
<< endl;
inStream.close();
outStream.close();
return 0;
}
cin
和 cout
流是系统已经为你声明好的。要将流连接到文件,必须像声明其他任何变量那样声明流。“输入文件流”(input-file stream)变量的类型名称是 ifstream
;“输出文件流”(output-file stream)变量的类型名称是 ofstream
。所以,要将 inStream
声明为用于文件的输入流,将 outStream
声明为用于另一个文件的输出流,需要使用如下所示的语句:
ifstream inStream;
ofstream outStream;
ifstream
和 ofstream
类型在头文件fstream
的库中定义。所以,任何程序要想以这种方式声明流,都必须包含以下预编译指令(通常放在接近文件开头的位置):
#include <fstream>
使用 ifstream
和 ofstream
类型时,程序还必须包含以下语句:
using namespace std;
流变量(例如前面声明的 inStream
和 outStream
)必须连接到一个文件。这称为打开文件,用 open
函数完成该操作。例如,假定希望输入流 inStream
连接到 infile.dat
文件,程序首先执行以下语句,然后才能从该文件读取输入:
inStream.open("infile.dat");
这里有两个细节。首先,流变量名称和一个圆点(也就是句点符号)要放在函数名称 open
之前,文件名则被指定为 open
函数的参数。其次,文件名要使用一对双引号。
一旦声明好输入流变量,并用 open
函数将其连接到文件,程序就可使用提取操作符 >>
从文件获取输入。例如,以下语句从连接到 inStream
的文件读取两个数,将它们分别放入变量 oneNumber
和 anotherNumber
中:
int oneNumber, anotherNumber;
inStream >> oneNumber >> anotherNumber;
输出流采用和输入流相同的方式来打开(也就是连接到)文件。例如,以下语句声明输出流 outStream
并将其连接到 outfile.dat
文件:
ofstream outStream;
outStream.open("outfile.dat");
针对 ofstream
类型的一个流,成员函数 open
会新建一个尚不存在的输出文件。如输出文件存在,成员函数open
会丢弃文件内容。所以在调用 open
函数之后,输出文件为空。
一旦调用 open
将文件连接到 outStream
流,程序就可使用插入操作符 <<
将输出发送到那个文件。例如,以下语句将两个字符串和两个变量 oneNumber
与 anotherNumber
的内容写入和 outStream
流连接的文件:
outStream << "oneNumber = " << oneNumber << " anotherNumber = " << anotherNumber;
程序使用的每个输入和输出文件都有两个名称。外部文件名是文件真实名称,但只在 open
函数调用中使用一次,该函数将文件连接到一个流。一旦调用了 open
,就必须将流名称作为文件名使用。
程序完成从文件输入或者向文件输出之后,应该将文件关闭。关闭文件导致流与文件断开。调用 close
函数关闭文件。
inStream.close();
outStream.close();
注意 close
函数不获取任何参数。如程序正常终止,但没有关闭文件,系统会自动为你关闭。但最好养成主动关闭文件的好习惯,原因有二。其一,只有在程序正常终止的前提下,系统才会为你关闭文件。假如程序因为错误而异常终止,文件就不会关闭,并可能损坏。程序在结束文件处理后立即关闭文件,文件损坏的概率就大大降低。其二,你可能希望程序将输出发送给一个文件,以后又将那些输出读回程序。为此,程序应该在完成向文件的写入之后立即关闭文件,再用 open
函数将文件连接到一个输入流(也可以打开一个文件,并同时进行输入和输出,但方式稍有区别)。
open
调用可能因许多原因而失败。例如,打开不存在的输入文件,open
调用就会失败。在这种情况下,可能不会报告错误信息,你的程序将在你不知情的情况下执行非预期的操作。因此,我们可以使用成员函数 fail
测试一个流操作是否成功。其返回一个 bool
值:
inStream.open("stuff.dat");
if (inStream.fail())
{
cout << "Input file opening failed.\n";
exit(1);
}
上面提到,如果用输出文件流打开文件,会使得文件清空,但有时我们需要在原有文件的基础上追加一些东西,那么我们可以使用下面这种方式打开文件:
ofstream fout;
fout.open("data.txt", ios::app);
流 I/O 工具
用流函数格式化输出
对程序输出的布局进行调整称为对输出进行格式化。在 C++ 中,可以用一些命令来控制格式,且可为任何输出流使用这些格式化命令。
其中一种使用 setf
成员函数来设置格式,其有以下标志:
标志 | 含义 | 默认 |
---|---|---|
ios::fixed | 设置这个标志,就不用 e 记数法写浮点数(设置该标志会自动取消设置 ios::scientific 标志) | 未设置 |
ios::scientific | 设置这个标志,会用 e 记数法写浮点数(设置该标志会自动取消设置 ios::fixed 标志) 如果没有设置 ios::fixed 也没有设置 ios::scientific,就由系统决定如何输出每个数字 | 未设置 |
ios::showpoint | 如果设置这个标志,就始终为浮点数显示小数点和尾随的0。如果没有设置这个标志,而且一个数字在小数点之后全是0,那么当这个数字输出时,就可能不会显示小数点和尾随的 0 | 未设置 |
ios::showpos | 如果设置这个标志,正整数之前会输出一个正号 | 未设置 |
ios::right | 如果设置这个标志,同时调用成员函数 width 指定了域宽,输出的下一项会对齐指定域的右侧(右对齐)。也就是说,在输出的项之前,会根据需要添加空格(设置该标志会自动取消设置ios::left标志) | 已设置 |
ios::left | 如果设置这个标志,同时调用成员函数 width 指定域宽,输出的下一项会对齐指定域的左侧(左对齐)。也就是说,在输出的项之后,根据需要添加填充空格(设置该标志会自动取消设置ios::right标志) | 未设置 |
比如设置格式为 定点记数法(fixed-point notation),且始终显示小数点和尾随的 0 :
outStream.setf(ios::fixed);
outStream.setf(ios::showpoint);
要决定浮点型小数点后的位数,可以使用 precision
成员函数:
outStream.precision(3);
也可以 width
成员函数设置域宽:
cout << "Start Now";
cout.width(4);
cout << 7 << endl;
这段代码导致屏幕上显示以下输出:
Start Now 7
在输出中,字母 w
与数字 7
之间有 3 个空格。 width
函数告诉流一个输出项需要占多少个字符位置(即域宽)。本例中,输出项(也就是数字 7
)只占一个字符位置,而 width
要求使用 4 个字符位置,所以其他 3 个位置用空格填充。如果输出所需的字符位置数目超过了在 width
函数调用中指定的数目,就自动补足缺少的字符位置。总之,输出项始终都会完整输出,不会被截短,无论为 width
指定的参数是什么。
需要注意的是,对 width
函数的调用只适用于下一个要输出的项。要输出 12 个数字,而且每个都占 4 个字符位置,就必须调用 12 次 width
。
设置的任何标志都可用 unsetf
函数取消。例如,以下语句导致程序停止为输出到 cout
流的正整数显示正号:
cout.unsetf(ios::showpos);
操纵元
操纵元(manipulator) 是以非传统方式调用的函数。调用操纵元后,它本身又会调用一个成员函数。操纵员位于插入操作符 <<
之后,好似它本身就是下一个要输出的项。
之前已经见过一个操纵元,即 endl
。这里介绍两个新的操纵元:setw
和 setprecision
。操纵元 setw
和成员函数 width
所做的事情完全一样。要调用 setw
操纵元,在插入操作符 <<
之后写上 setw
即可。这如同将该操纵元发送到输出流,后者随即调用成员函数 width
例如,以下语句用不同域宽输出数字 10,20 和 30:
cout << "Start" << setw(4) << 10
<< setw(4) << 20 << setw(6) << 30;
流操纵元 setprecision
所做的事情和成员函数 precision
完全一样。但 setprecision
调用要放在插入操作符 <<
之后,这和调用 setw
操纵元是类似的。例如,以下语句为指定的数字输出小数部分,小数位数由 setprecision
调用来指定:
cout.setf(ios::fixed);
cout.setf(ios::showpoint);
cout << "$" << setprecision(2) << 10.3 << endl
<< "$" << 20.5 << endl;
使用 setprecision
操纵元设置小数位数时,和成员函数 precision
的情况一样,设置会一直生效,直到把它重设为其他数字(通过再次调用 setprecision
或 precision
)。要使用 setw
和 setprecision
操纵元,必须在程序中包含以下预编译指令:
#include <iomanip>
流作为函数实参
流可作为函数实参使用。唯一限制就是形参必须传引用。流参数不能传值。下面为一实例:
// 演示输出格式化命令
// 读取 rawdata.dat 文件中的所有数字,然后采用美观的格式
// 将数字写到屏幕,同时写到 neat.dat 文件
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <iomanip>
using namespace std;
void makeNeat(ifstream& messyFile, ofstream& neatFile,
int numberAfterDecimalpoint, int fieldWidth);
// 前条件:messyFile 和 neatFile 这两个流已用 open 函数连接到文件
// 后条件:与 messyFile流连接的那个文件中的数字写到屏幕,
// 同时写到与neatFile流连接的那个文件
// 每个数字单独占一行,并采用定点记数法(换言之,不采用e记数法),
// 而且在小数点之后保留 numberAfterDecimalpoint 位小数;
// 每个数字的前面要么添加一个正号,要么添加一个负号,
// 而且每个数字都占用宽带为 fieldWidth 的一个域(该函数不关闭文件)
int main()
{
ifstream fin;
ofstream fout;
fin.open("rawdata.dat");
if (fin.fail())
{
cout << "Input file opening failed.\n";
exit(1);
}
fout.open("neat.dat");
if (fout.fail())
{
cout << "Output file opening failed.\n";
exit(1);
}
makeNeat(fin, fout, 5, 12);
fin.close();
fout.close();
cout << "End of program.\n";
return 0;
}
// 使用iostream,fstream 和 iomanip:
void makeNeat(ifstream& messyFile, ofstream& neatFile,
int numberAfterDecimalpoint, int fieldWidth)
{
neatFile.setf(ios::fixed);
neatFile.setf(ios::showpoint);
neatFile.setf(ios::showpos);
neatFile.precision(numberAfterDecimalpoint);
cout.setf(ios::fixed);
cout.setf(ios::showpoint);
cout.setf(ios::showpos);
cout.precision(numberAfterDecimalpoint);
double next;
while (messyFile >> next)
{
cout << setw(fieldWidth) << next << endl;
neatFile << setw(fieldWidth) << next << endl;
}
}
字符 I/O
get 和 put 成员函数
每个输入流都有名为 get
的成员函数,它读取一个输入字符。和提取操作符不同,无论下个输入字符是什么,get
都会读取。具体地说,无论下个输入字符时空白字符(空格、制表符等),还是换行符,get
都会读取它。get
函数获取 char
类型的变量作为参数。调用 get
时,会读取下一个输入字符,并把实参变量设为该字符:
char nextSymbol;
cin.get(nextSymbol);
每个输出流都有名为 put
的成员函数,它获取一个 char
类型的参数。调用成员函数 put
后,它的参数的值被输出到输出流:
cout.put(nextSymbol);
cout.put('a');
要使用 get
或者 put
成员函数,需要在程序中包含以下预编译指令:
#include <fstream>
putback 成员函数
有时需要知道输入流中的下个字符。但在读取了下个字符之后,却发现自己不想处理该字符,所以想把它重新放回输入流。例如,假定希望程序一直读取一个输入流中的字符,直到(但不包括)第一个空格。所以,程序必须读取第一个空格,否则不知道什么时候应该停止读取。另一方面,既然已读取了该空格,它就不再包含在输入流中。与此同时,程序其他部分可能需要读取和处理这个空格。有许多方案都能解决这个问题,但最简单的方案就是使用成员函数 putback
。putback
是每个输入流都有的成员函数。它获取一个 char
参数,并将该参数的值放回输入流。该参数可以是能求值为 char
值的任何表达式。
例如,以下代码从与输入流 fin
连接的文件读取字符,将它们写入与输出流 fout
连接的另一个文件。代码一直读取字符,直到(但不包括)第一个空格:
fin.get(next);
while (next != ' ')
{
fout.put(next);
fin.get(next);
}
fin.putback(next);
注意,执行完这段代码之后,已被读取的空格仍然包含在输入流 fin
中,因为代码在读取了这个空格之后,又把它放回原来的输入流中。还要注意,putback
是将一个字符放回输入流中,而 put
是将一个字符放到输出流中。被成员函数 putback
放回输入流的字符不一定是上次读取的字符,它可以是任意字符。putback
是将字符放回输入”流“而不是放回输入”文件“,原始输入文件的内容不发生任何改变。
万用型流参数
下面是一个检查输入的例子:
// 演示输出格式化命令
// 读取 rawdata.dat 文件中的所有数字,然后采用美观的格式
// 将数字写到屏幕,同时写到 neat.dat 文件
#include <iostream>
#include <fstream>
#include <cstdlib>
#include <iomanip>
using namespace std;
void makeNeat(ifstream& messyFile, ofstream& neatFile,
int numberAfterDecimalpoint, int fieldWidth);
// 前条件:messyFile 和 neatFile 这两个流已用 open 函数连接到文件
// 后条件:与 messyFile流连接的那个文件中的数字写到屏幕,
// 同时写到与neatFile流连接的那个文件
// 每个数字单独占一行,并采用定点记数法(换言之,不采用e记数法),
// 而且在小数点之后保留 numberAfterDecimalpoint 位小数;
// 每个数字的前面要么添加一个正号,要么添加一个负号,
// 而且每个数字都占用宽带为 fieldWidth 的一个域(该函数不关闭文件)
int main()
{
ifstream fin;
ofstream fout;
fin.open("rawdata.dat");
if (fin.fail())
{
cout << "Input file opening failed.\n";
exit(1);
}
fout.open("neat.dat");
if (fout.fail())
{
cout << "Output file opening failed.\n";
exit(1);
}
makeNeat(fin, fout, 5, 12);
fin.close();
fout.close();
cout << "End of program.\n";
return 0;
}
// 使用iostream,fstream 和 iomanip:
void makeNeat(ifstream& messyFile, ofstream& neatFile,
int numberAfterDecimalpoint, int fieldWidth)
{
neatFile.setf(ios::fixed);
neatFile.setf(ios::showpoint);
neatFile.setf(ios::showpos);
neatFile.precision(numberAfterDecimalpoint);
cout.setf(ios::fixed);
cout.setf(ios::showpoint);
cout.setf(ios::showpos);
cout.precision(numberAfterDecimalpoint);
double next;
while (messyFile >> next)
{
cout << setw(fieldWidth) << next << endl;
neatFile << setw(fieldWidth) << next << endl;
}
}
如果函数要获取输入流实参,而且实参有时是 cin
,有时是输入文件流,那么可以使用 istream
(没有f)类型的形参。但即使作为 istream
类型的实参提供给函数,输入文件流也必须声明为 ifstream
类型(有 f)。
类似,如果函数要获取输出流实参,而且实参有时是 cout
,有时是输出文件流,那么可以使用 ostream
(没有 f)类型的形参。但即使作为 ostream
类型的实参提供给函数,输出文件流也必须声明为 ofstream
类型(有 f)。不能打开或关闭 istream
或 ostream
类型的对象。传给函数前打开这些对象,函数调用结束后关闭。
比如我们重写 newLine
函数:
// 使用iostream:
void newLine(istream& inStream)
{
char symbol;
do
{
inStream.get(symbol);
} while (symbol != '\n');
}
eof 成员函数
每个输入文件流都有名为 eof
的成员函数,用于判断何时读完文件的全部内容,没有更多的输入。eof
是 end of file 的缩写,表示文件尾。函数无参,对于名为 fin
的输入流,可像下面这样写 eof
函数调用:
fin.eof()
文件末尾有一个特殊的“文件尾”标记。成员函数 eof
只有在读取了这个文件尾标记之后,才会从 false
变为 true
,因此可以用于判断何时读完整个输入文件。
预定义字符函数
下面对 cctype
库常用的函数进行总结:
函数 | 说明 |
---|---|
toupper(Char_Exp) | 返回 Char_Exp 的大写形式 |
tolower(Char_Exp) | 返回 Char_Exp 的小写形式 |
isupper(Char_Exp) | 如果 Char_Exp 是大写字母,就返回true,否则返回false |
islower(Char_Exp) | 如果 Char_Exp 是小写字母,就返回true,否则返回false |
isalpha(Char_Exp) | 如果 Char_Exp 是字母表中的字母,就返回 true;否则返回false |
isdigit(Char_Exp) | 如果 Char_Exp 是’0’到’9’的数字,就返回true,否则返回false |
isspace(Char_Exp) | 如果 Char_Exp 是空白字符(比如空格或换行符),就返回true;否则返回false |
需要注意的是以下语句不会输出字母 ‘A’,而是输出为字母’A’分配的数字:
cout << toupper('a');
这是因为 toupper
和 tolower
返回 int
类型的值,而不是 char
类型的值,因此需要将其赋给 char
类型的变量:
char c = toupper('a');
cout << c;