C++I/O流


流和基本文件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;
}

cincout 流是系统已经为你声明好的。要将流连接到文件,必须像声明其他任何变量那样声明流。“输入文件流”(input-file stream)变量的类型名称是 ifstream;“输出文件流”(output-file stream)变量的类型名称是 ofstream。所以,要将 inStream 声明为用于文件的输入流,将 outStream 声明为用于另一个文件的输出流,需要使用如下所示的语句:

ifstream inStream;
ofstream outStream;

ifstreamofstream 类型在头文件fstream的库中定义。所以,任何程序要想以这种方式声明流,都必须包含以下预编译指令(通常放在接近文件开头的位置):

#include <fstream>

使用 ifstreamofstream 类型时,程序还必须包含以下语句:

using namespace std;

流变量(例如前面声明的 inStreamoutStream)必须连接到一个文件。这称为打开文件,用 open 函数完成该操作。例如,假定希望输入流 inStream 连接到 infile.dat 文件,程序首先执行以下语句,然后才能从该文件读取输入:

inStream.open("infile.dat");

这里有两个细节。首先,流变量名称和一个圆点(也就是句点符号)要放在函数名称 open 之前,文件名则被指定为 open 函数的参数。其次,文件名要使用一对双引号。

一旦声明好输入流变量,并用 open 函数将其连接到文件,程序就可使用提取操作符 >> 从文件获取输入。例如,以下语句从连接到 inStream 的文件读取两个数,将它们分别放入变量 oneNumberanotherNumber 中:

int oneNumber, anotherNumber;
inStream >> oneNumber >> anotherNumber;

输出流采用和输入流相同的方式来打开(也就是连接到)文件。例如,以下语句声明输出流 outStream 并将其连接到 outfile.dat 文件:

ofstream outStream;
outStream.open("outfile.dat");

针对 ofstream 类型的一个流,成员函数 open 会新建一个尚不存在的输出文件。如输出文件存在,成员函数open会丢弃文件内容。所以在调用 open 函数之后,输出文件为空。

一旦调用 open 将文件连接到 outStream 流,程序就可使用插入操作符 << 将输出发送到那个文件。例如,以下语句将两个字符串和两个变量 oneNumberanotherNumber 的内容写入和 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。这里介绍两个新的操纵元:setwsetprecision。操纵元 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 的情况一样,设置会一直生效,直到把它重设为其他数字(通过再次调用 setprecisionprecision)。要使用 setwsetprecision 操纵元,必须在程序中包含以下预编译指令:

#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 成员函数

有时需要知道输入流中的下个字符。但在读取了下个字符之后,却发现自己不想处理该字符,所以想把它重新放回输入流。例如,假定希望程序一直读取一个输入流中的字符,直到(但不包括)第一个空格。所以,程序必须读取第一个空格,否则不知道什么时候应该停止读取。另一方面,既然已读取了该空格,它就不再包含在输入流中。与此同时,程序其他部分可能需要读取和处理这个空格。有许多方案都能解决这个问题,但最简单的方案就是使用成员函数 putbackputback 是每个输入流都有的成员函数。它获取一个 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)。不能打开或关闭 istreamostream 类型的对象。传给函数前打开这些对象,函数调用结束后关闭。

比如我们重写 newLine 函数:

// 使用iostream:
void newLine(istream& inStream)
{
	char symbol;
	do
	{
		inStream.get(symbol);
	} while (symbol != '\n');
}

eof 成员函数

每个输入文件流都有名为 eof 的成员函数,用于判断何时读完文件的全部内容,没有更多的输入。eofend 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');

这是因为 touppertolower 返回 int 类型的值,而不是 char 类型的值,因此需要将其赋给 char 类型的变量:

char c = toupper('a');
cout << c;
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值