第17章:输入、输出和文件


流的特点:一维、单方向
Extractors: ****>>

  • read a value from the stream
  • overload the >> operator

Inserters: <<

  • Insert a value into a stream
  • overload the << operator

manipulators: (操纵器)

  • change the stream state;

others

  • some functions

Kinds of streams

1. text streams
(输入) 解析 / (输出)格式化

  • deal in ASCII text
    2. Binary streams(不以人的阅读为目的的文件)
  • 不经过解析和格式化的过程

Predefined streams

  • cin
    standard input
  • cout
    standard output
  • cerr
    unbuffered error output
  • clog
    buffered error outpit

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jyn3bC4C-1657814432263)(2022-07-07-20-53-09.png)]

Defining a stream extractor

istream operatror>>(istream & is, T & obj) {
    ...
    return is;
} 

流、缓冲区和iostream文件

C++程序把输入和输出看做字节流。输入时,程序从输入流中抽取字节,输出时,程序将字节插入到输出流中。
流充当了程序和流源(文件或键盘等输入设备)或流目标(文件或显示器)之间的桥梁。C++只是检查字节流,使得程序处理输入输出与来源和方向相互独立,不需要关心不同的设备去向具体的底层实现的差别。

缓冲区作为设备和程序之间的媒介,可以起到平衡输入输出的处理速度差异的作用,提高效率。

流和缓冲区的管理工作交由一些专门设计的类来完成。

  • ios_base类表示流的一般特征,如是否可读取,是二进制流还是文本流
  • ios类基于ios_base,其中包括了一个指向streambuf类对象的指针成员
  • streambuf类为缓冲区提供了内存,并提供了用于填充缓冲区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法。
  • istream和ostream类都是从ios类派生而来的,提供了输入和输出方法
  • iostream是基于istream和ostream类,多重继承了这两个类的输入和输出方法。

创建这样的对象将打开一个流,自动创建缓冲区,并将其与流关联起来,同时使得能够使用类成员函数。

C++的iostream类库管理了很多细节,在程序中包含iostream文件将自动创建8个流对象(4个用于窄字符流,4个用于宽字符流)
cin,cout,cerr,clog \wcin,wcout,wcerr,wclog

使用cout进行输出

ostream类的最重要的任务之一数据编码格式的转换:即将数值类型(如int、float等计算机内部的数据表示格式)转换成文本形式表示的字符流(如ASCII码,显示器能直接显示的、给人看的字符数据格式)

ostream的类方法

  1. ostream重载了<< 运算符 (插入运算符) 使之能够识别C++中所有的基本类型。
  2. ostream类还为下面的指针类型定义了插入运算符
  • const signed char*
  • const unsigned char*
  • const char*
  • void* //其他类型的指针C++都将其对应于void*,并打印地址的数值表示

C++使用指向字符串存储位置的指针来表示字符串。指针的形式可以是char数组名、显式的char指针、或用括号引起的字符串
因此,下面的所有cout语句都显示字符串

char name[20] = "hello world";
char *pn = "dejifeede";


cout << "hello";  //括号引起的字符串
cout <<name;      //char数组名
cout << pn;       //显式的char指针

方法使用字符串中的终止空字符(“\0”)来确定何时停止显示字符

重载 << 运算符

ostream类中的<<运算符重载的写法:

ostream & operator<<(T & obj)
{
    ...
    return *this;
}

但是在ostream类中进行<<运算符重载是危险的,所以一般要在自定义的类中进行<<运算符的重载。使用友元来解决参数顺序的问题。

Creating a stream inserter

class T
{
    friend ostream & operator<<(ostream & os, const T & obj);
}
ostream & operator<<(ostream & os, const T & obj){
    ...
    return os;
}

put()方法和write方法

除了各种operator<<()方法外,ostream类还提供了put()方法和write方法,前者用于显示字符,后者用于显示字符串。

put方法:显示字符

//返回对象的引用,支持拼接输出
ostream & put(char);

cout.put('w');
cout.put('I'),put('t');

cout.put(65); //displaty the A character
cout.put(66.3);// 66.3 → 66 —→ B 

wiite()方法:显示字符串的前n个字符
需要注意的是,write()方法并不会在遇到空字符时自动停止打印字符,而只是打印指定数目的字符

//函数模板原型
//使用cout调用write()时,将调用char具体化,因此返回类型为ostream&, 也支持拼接输出
basic_ostream<charT,traits>& write(const char_Type* s, streamsize n);

//使用演示
#include<iostream>
int main()
{
	using std::cout;
	using std::endl;

	const char* str1 = "0123456789";
	const char* str2 = "**********";
	const char* str3 = "hello world";
	const char* str4 = "niubi";

	for (int i = 1; i <= std::strlen(str1); i++) //递增循环输出
	{
		cout.write(str1, i);
		cout << endl;
	}

	cout << "-----------------"<<endl;
	for (int i = std::strlen(str2); i >= 1; i--)//递减循环输出
	{
		cout.write(str2, i);
		cout << endl;
	}
	cout << "-----------------" << endl;
	cout.write(str3, std::strlen(str3) - 5).write(str4, std::strlen(str4))<<endl; //输出可以进行拼接
	
	//将wirte()方法用于数值数据
	long val = 560031841; //long数据占用4个字节传输,
	cout.write((char*)&val, sizeof(long));
	//将数字的地址强转为char*,传递给write()函数
	//每个字节被翻译成ASCII码进行解释,因此在屏幕上,数值560031841将被显示为4个字符的组合
}

刷新输出缓冲区

由于ostream类对cout对象处理的输出进行缓冲,所以输出不会立即发送到目标地址,而是被存储在缓冲区中,知道缓冲区填满。然后,程序将刷新缓冲区,把内容发送出去,并清空缓冲区,以存储新的数据。通常,缓冲区为字节及其整数倍。

  • 当标准输出连接的是硬盘上的文件时,缓冲可以节省大量的时间。将大量字符一次性写入硬盘的效率会更高。
  • 对于屏幕来说,程序不必等到缓冲区被填满才进行刷新,例如,将换行符发送到缓冲区后,将刷新缓冲区。
    另外,多数C+会在输入即将发生时刷新缓冲区。
cout << "dewfewf";
float num;
cin >> num;

因为下面有cin,程序期待输入这一事实,所以即使输出字符串中没有换行符,也会刷新缓冲区,立刻输出。

有两个控制符可以对缓冲区进行强行刷新

  • 控制符flush刷新缓冲区
  • 控制符endl刷新缓冲区,并插入一个换行符。
cout << "efgewfgw" << flush;
cout << "ferwerewf"<< endl;

用cout进行格式化

1. 修改计数系统 cout << hex;

使用控制符控制显示证整数时使用的计数系统,以十进制、十六进制、还是八进制可以显示可以使用dec,hex,oct控制符。
使用方式如下:

//控制符不是成员函数,不必通过对象来调用
hex(cout);
dec(cout);
oct(cout);

//或 ostream类重载了<<运算符,也可以这么使用控制符
cout << hex;
cout << dec;
cout << oct;

2.调整字段宽度 cout.width(5);

  • 可以使用width成员函数将长度不同的数字放到宽度相同的字段中。
  • width()方法只影响下一个项目,然后字段宽度将恢复为默认值
  • C++总会增长字段,以容纳数据,因此这种值适用于所有的数据。
  • 右对齐是默认的

使用示例:

int width();  //用来返回当前字段宽度的设置
int width(int i);  //用来设置字段段杜,并返回之前的字段宽度

#include <iostream>

int main()
{
    using std::cout;
    int old = cout.width(); //默认宽度为0
    cout << "default field width = " << old << ":\n";

    cout.width(5);
    cout << "N" << ':';
    cout.width(8);
    cout << "N * N" << ":\n";

    for (long i = 1; i <= 100; i *= 10)
    {
        cout.width(5);
        cout << i << ':';
        cout.width(8);
        cout << i * i << ":\n";
    }
    // std::cin.get();
    return 0;
}

3. 填充字符 cout.fill(‘*’);

  • cout.fill(‘*’);将使用 * 来填充字段中未使用的部分
  • 默认情况下使用空格填充
  • 注意:与字段宽度不同的是,新的填充字符将一直有效,直到更改它为止。

4. 设置浮点数的显式精度 cout.precision(2);

浮点数精度的含义取决于输出模式

  • 在默认模式下,指的是显式的总位数
  • 在定点模式和科学模式下,指的是小数点后面的位数
  1. cout.precision(2);设置精度为2
  2. 与fill()类似,新的精度设置将一直有效,直到被重新设置。
  3. C++的默认精度为6位,但末尾的0将不显示

setf()方法

ios_base类提供了一个setf()函数,能够控制多种格式化特性。这个类还提供了多个常量,可用作该函数的参数。
setf()成员函数有两个原型:

//fmtflags是bitmask类型的typedef名。bitmask是一种用来存储各个位值的类型,每一位都是可以单独访问的,都有自己的含义。iostream软件包使用bitmask来存储状态信息。
fmtflags setf(fmtflags);

fmtflags setf(fmtflags, fmtflags);

要点:

  • ios_base类位于std名称空间中
  • 这些格式常量都是在ios_base类中定义的,使用时需要加上作用域解析符
  • 修改将一直有效,直到被覆盖位置
  • setf()是ios_base类的一个成员函数,由于这个类是ostream的基类,因此可以用cout对象来调用该函数
setf()一个参数设置格式,并返回以前的设置

一个参数setf()使用的格式常量:

  • ios_base::boolalpha 输入和输出bool值
  • ios_base::showbase 输出使用基数前缀0,,0x
  • ios_base::showpoint 显示末尾的小数点
  • ios_base::uppercase 对于16进制输出,使用大写字母,E表示法
  • ios_base::showpos 在正数前面加+号显示

使用示例:

#include <iostream>

int main()
{
    using std::cout;
    using std::endl;
    using std::ios_base;//预编译指令

    int temperature = 63;

    cout.setf(ios_base::showpos);    // 加+号
    cout << temperature << endl;

   
    cout << std::hex << temperature << endl; // 使用hex控制符

    cout.setf(ios_base::uppercase);  // 16进制大写
    cout.setf(ios_base::showbase);   // 使用基数前缀
    cout << "or\n";
    cout << temperature << endl;
   
    cout << true ; //输出1
    cout.setf(ios_base::boolalpha); //输出bool值true
    cout << true << "!\n";

    return 0;
}
setf()原型接受两个参数,并返回以前的设置

第一位指出要设置哪些位,第二位指出要清除哪些位。

第2个参数第一个参数含义
ios_base::basefieldios_base::dec使用基数10
ios_base::oct使用基数8
ios_base::hex使用基数16
ios_base::floatfieldios_base::fixed使用定点计数法
ios_base::scientific使用科学计数法
ios_base::adjustfieldios_base:left使用左对齐
ios_base::right使用右对齐
ios_base::internal符号或基数前缀左对齐,值右对齐

定点法和科学计数法都有下面两个特征:

  • 精度是值小数位数,而不是总位数
  • 显示末尾的0

使用示例:

//调整左对齐
ios_base::fmtflags old = cout.setf(ios_base::left,ios_base::adjustfield);
//恢复以前的设置
cout.setf(old,ios::base::adjustfield);
// setf2.cpp -- using setf() with 2 arguments to control formatting
#include <iostream>
#include <cmath>

int main()
{
    using namespace std;
   //左对齐、加+号、显示小数点、精度为3位
    cout.setf(ios_base::left, ios_base::adjustfield);
    cout.setf(ios_base::showpos);
    cout.setf(ios_base::showpoint);
    cout.precision(3);
    //保存之前的设置(根据第二个参数,保存当前的浮点数默认输出模式)并使用科学计数法
    ios_base::fmtflags old = cout.setf(ios_base::scientific,
        ios_base::floatfield);
    cout << "Left Justification:\n";
    long n;
    for (n = 1; n <= 41; n += 10)
    {
        cout.width(4);   //在循环输出中调整字段宽度,因为width()用完一次之后就恢复默认值了
        cout << n << "|";
        cout.width(12);
        cout << sqrt(double(n)) << "|\n";
    }

    // 使用internal对齐方式
    cout.setf(ios_base::internal, ios_base::adjustfield);
    // 恢复默认的浮点数计数法(总共显示三位)
    cout.setf(old, ios_base::floatfield);

    cout << "Internal Justification:\n";
    for (n = 1; n <= 41; n += 10)
    {
        cout.width(4);
        cout << n << "|";
        cout.width(12);
        cout << sqrt(double(n)) << "|\n";
    }

    // 右对齐、定点计数法(显示小数点后3位)
    cout.setf(ios_base::right, ios_base::adjustfield);
    cout.setf(ios_base::fixed, ios_base::floatfield);
    cout << "Right Justification:\n";
    for (n = 1; n <= 41; n += 10)
    {
        cout.width(4);
        cout << n << "|";
        cout.width(12);
        cout << sqrt(double(n)) << "|\n";
    }
    // std::cin.get();
    return 0;
}

7. 标准控制符

C++提供了多个控制符,能够调用setf()函数,并自动提供正确的参数,使用起来更简单。
使用方式如下:

cout << left << fiexd; //使用左对齐、定点计数法

一些标准控制符如下:
boolalpha / noboolalpha 使用bool值
showbase / noshowbase 使用基数前缀0,,0X
showpoint/ noshowpoint 显示末尾的小数点
showpos / noshowpos 整数加+号
uppercase/ nouppercase 16进制大写E表示法
left / right / internal 左对齐、右对齐、内部表示法
dec / oct / hex 10进制、8进制、16进制
fixed / scientific 定点计数法、科学计数法

8. 头文件 iomanip

iomanip提供了几个常用的控制符,如setprecision(int i),setfill(char c),setw(int i),它们分别用来设置精度、填充字符和字段宽度。相比较cout.precision()、cout.fill()、cout.width(),它们在某些情况下使用起来更方便,可以直接插入到输出序列中使用,如cout<<setw(10) << setfill() << n; 它们都是控制符,可以直接用cout语句链接起来。

#include <iostream>
#include <)iomanip>
#include <cmath>

int main()
{
    using namespace std;
    // 使用标准控制符 右对齐、定点计数法(小数点后位数、显示末尾的0)
    cout << fixed << right;

    // 使用 iomanip 控制符
    cout << setw(6) << "N" << setw(14) << "square root"
        << setw(15) << "fourth root\n";

    double root;
    for (int n = 10; n <= 100; n += 10)
    {
        root = sqrt(double(n));
        cout << setw(6) << setfill('.') << n             //第1列
            << setfill(' ') 
            << setw(10) << setprecision(3) << root       //第2列
            << setw(14) << setprecision(5) << sqrt(root) //第3列
            << endl;
    }
    return 0;
}

使用cin进行输入

键盘 → 字符流 → 缓冲区 —→(读入字节) cin(>>进行数据类型的转换,get、getline不转换) → 内存单元

>> 抽取运算符 进行单词的输入(涉及到基本数据类型的转换)

istream类重载了抽取运算符 >>,基本数据类型都能识别
eg: istream & operator>>(int &);
参数都使用了引用,因为这样可以直接修改该参数变量的值,而不是修改其拷贝。
返回值使用引用是可以连续输入。
通常可以这样使用cin
cin >> value_holder

istream类还为下列字符指针类型重载了>>抽取运算符:

  • char*
  • signed char*
  • unsigned char*

对于这种类型的参数,抽取运算符将读取输出流中的下一个单词(连续的字符流,中间没有空格),将它放置到指定的地址上,并加上一个空值字符(\0),使之成为一个字符串。

每个抽取运算符都返回调用对象的引用,能够实现拼接输入。
使用 >> 进行连续输入 不同类型的数据 输入到时候使用空格分隔或换行符分隔均可。

 using namespace std;

    char name[20];
    float fee;
    int group;
    //使用 >> 进行连续输入 不同类型的数据 输入到时候使用空格分隔或换行符分隔均可
    cin >> name >> fee >> group;  
    cout << left<< fixed;
    cout.precision(3);
    cout << setw(10) << name << setw(10) << fee << setw(10) << group << endl;
    return 0;

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TvNtFeNR-1657814432266)(2022-07-12-00-54-30.png)]

cin>>如何检查输入

当cin>>从缓冲区中读取数据时,若缓冲区第一个字符是空格、tab或换行符这些分隔符时,cin>>会将其忽略并删除,继续读取下一个字符.若缓冲区为空,则继续等待。但如果读取成功,字符后面的分隔符是会残留在缓冲区中的,cin>>不做处理。也就是说,它读取从非空白字符开始,到第一个与目标类型不匹配的字符结束这两者之间的全部内容

int elevation;
cin >> elevation;
  1. 键入 -123z时,运算符将读取 - 1 2 3 等4个int型的有效字符,z将留在缓冲区中,下一个cin语句将从这里开始读取。

  2. 但是假设输入的是acaer,而不是-123z。在这种情况下,抽取运算符不会修改elevation的值,返回0.(如果istream对象的错误状态被设置,if或while语句将判定该对象为false,返回值false让程序能够检查输入是否满足要求

条件状态

使用cin读取键盘输入时,难免发生错误,一旦出错,cin将设置条件状态(condition state).条件状态有:

1 | goodbit(0x0):无错误
2 | eofbit (0x1):已到达文件尾
3 | failbit(0x2):非致命的输入/输出错误,可挽回
4 | badbit (0x4):致命的输入/输出错误,无法挽回

与这些条件对应的就是设置、读取、判断条件状态的流对象的成员函数。它们有:

1 | s.eof():若流s的eofbit置位,则返回true
2 | s.fail():若流s的failbit置位,则返回true
3 | s.bad():若流s的badbit置位,则返回true
4 | s.good():若流s的goodbit置位,则返回true
5 | s.clear(flags):清空状态,然后把状态设置为flags,返回void
6 | s.setstate(flags):不清空状态,设置给定的状态为flags,返回void
7 | s.rdstate():返回流s的当前状态,返回值类型为ios_base::iostate
8 | s.eof():若流s的eofbit置位,则返回true

其他istream类方法

  1. 方法get(char&)和get(void)提供不跳过空白字符的单字符输入功能
  2. 函数get(char*,int ,char)和getline(char*, int ,char)在默认情况下读取整行而不是一个单词.

他们被称为非格式化输入函数,因为他们只读取字符输入,而不会跳过空白,也不进行数据转换。

单字符输入

get()方法读取下一个输入的字符,即使该字符是空格、制表符、或换行符。get(char&)版本将输入字符赋给其参数,get(void)版本将输入字符转换成整型(通常是int,ASCII码)并将其返回。

成员函数get(char &)
int ct = 0;
	char ch;
	cin.get(ch);
	while (ch != '\n'){ //直到程序将回车键作为换行符处理,终止循环。
		cout << ch;
		ct++;
		cin.get(ch);
	}
	cout << ct << endl;

输入: hello world !
输出: hello world !15

这里的重点是,通过get(char&),代码读取、显示并考虑空格和可打印字符。
假设程序使用 >> 抽取运算符,将自动跳过空格、制表符、或换行符。

int ct = 0;
	char ch;
	//cin.get(ch);
    cin >> ch;
	while (ch != '\n'){ 
		cout << ch;
		ct++;
		//cin.get(ch);
        cin >> ch;
	}
	cout << ct << endl;

输入: hello world !
输出:
helloworld!
_(光标)

1. 代码会跳过空格,将输入压缩
==2. 更糟糕的是,>> 会自动跳过换行符,不会将换行符赋值给ch,所以while()循环将不会终止。

istream & get(char &)返回引用,所以可以实现拼接输出。
cin.get(c1).get(c2);

读取文件时:

char ch;
while(cin.get(ch))  
{
    //process input;
}

只要存在有效输入,cin.get(ch)的返回值都将是cin,此时结果判定为true,因此循环将继续。到达文件尾时,返回值判定为false,循环终止。

成员函数get(void)

get(void)也读取空白,但是使用返回值的方式将输入传递给程序。
int get(void)

char ch;
ch = cin.get();
while(ch != '\n')
{
    cout << ch;
    ct++;
    ch = cin.get();
}
cout << st << endl;

读取文件时:
到达文件尾时,cin,get(void)都将返回值EOF——头文件iostream提供的一个符号常量。

int ch;
while ((ch = cin.get()) != EOF )
{
    //process input
}

这里应将ch的类型声明为int,而不是char,因为值EOF可能无法使用char类型来表示。

特征cin.get(ch)ch= cin.get()
输入字符的方法赋给参数ch将函数返回值赋给ch
字符输入时函数的返回值指向istream对象的引用字符编码(int值)
到达文件尾时函数的返回值转换为false返回EOF
使用哪种字符?

希望输入跳过空格、制表符、或换行符时,选择>>抽取运算符
希望检查所有字符时,使用get()方法,一般来说,get(char&)方法更佳,而get(void)方法与C语言的兼容性更好。

字符串输入:getline()、get()和ignore() (整行读取,包括空格,遇到换行符结束)

getline()和get()字符串读取版本都读取字符串,它们的函数特征标相同。
它们在默认情况下读取整行,而不是一个单词。

istream & get(char*,int , char);
istream & get(char*,int);

istream & get(char*, int, char);
istream & getline(char*, int);
  • 参数一:放置输入字符串的内存单元的地址
  • 参数二:存放的字节数:读取的字符数 + 1(额外的一个字符用于存储结尾的空字符,以便将输入存储为一个字符数)
  • 参数三: 指定用作分界符的字符,只有两个参数的版本 默认将换行符用作分界符
    上述函数都在读取最大数目的字符或遇到换行符后为止。

get()和getline()的主要区别在于get()将换行符留在输入流中,这样接下来的操作首先看到的将是换行符,而getline()抽取并丢弃输入流中的换行符。
第三个参数用于制定分界符,get()将分界符留在输入流中,而getline()不保留(抽取并丢弃)

ignore()成员函数:从输入流抽取并丢弃一定数量的字符

istream & ignore(int i, int  = EOF);
//默认参数EOF导致ignore()读取指定数目的字符或读取到文件尾。
  • 参数一:要读取的最大字符数
  • 参数二:字符参数: 输入分解符

eg: cin.ignore(255,‘\n’);读取并丢弃接下来的255个字符或直到达到第一个换行符。

读取字符串getline()、get()的使用

// get_fun.cpp -- using get() and getline()
#include <iostream>
const int Limit = 255;

int main()
{
    using std::cout;
    using std::cin;
    using std::endl;

    char input[Limit];

    cout << "Enter a string for getline() processing:\n";
    cin.getline(input, Limit, '#');
    //使用#结束,不会跳过换行符
    //getline()抽取并丢弃了分界符(结束符)#
    cout << "Here is your input:\n";
    cout << input << "\nDone with phase 1\n";

    char ch;
    cin.get(ch);
    cout << "The next input character is " << ch << endl;

    if (ch != '\n')
        cin.ignore(Limit, '\n');    // discard rest of line

    cout << "Enter a string for get() processing:\n";
    cin.get(input, Limit, '#');
    //get()并未抽取分界符 #
    cout << "Here is your input:\n";
    cout << input << "\nDone with phase 2\n";

    cin.get(ch);//所以这里能够读取到分界符
    cout << "The next input character is " << ch << endl;
    /* keeping output window open */
    /*
        cin.clear();
        while (cin.get() != '\n')
            continue;
        cin.get();
    */
    return 0;
}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6mvE1pgi-1657814432269)(2022-07-11-21-54-00.png)]

意外字符串输入

待扩充,先暂时跳过…

其他istream方法

  • istream & read(char*, int)

从输入流中读取int个字符,并将它们插入到char*位置处。与get()和getline()的区别是不会在输入后加上空字符,不能将输入转换为字符串。用途主要是与ostream write()结合起来,用于文件的输入和输出。

  • char ch = cin.peek() ,ch = ?

函数返回输入流中的下一个字符,但并不抽取输入流中的字符。用于查看下一个输入的字符,但并不影响输入流,可以用来做循环的判断条件。

  • gcount(),统计非格式化输入(不是>>,没有数据类型转换)的字符数。没有strlen()直接统计目的数组来得快。
  • istream & putback(char ch) :将ch插回到输入流中。
    peek()的效果相当于先get()再putback()。

文件的输入与输出

#inclue < fstream >
C++ IO类软件包处理文件的输入输出与标准输入输出非常相似。创建一个流对象,使一端连接文件一端连接程序即可。ifstream和ofstream继续自istream和ostream类,所以标准输入输出的方法,这些文件类也可以正常使用。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xw5i8cv0-1657814432271)(2022-07-12-15-41-19.png)]

简单的文件 I/O

写入文件:

  1. 创建ofstream对象(如fout)来管理输出流
  2. 将该对象与特定的文件关联起来(流对象的一端(流目的端)与文件相连)/ 或者叫打开文件
  3. 使用cout的方式使用该对象,唯一的区别是输出将进入文件而不是屏幕。

close()方法只是显式的断开流对象与文件的链接,并不会删除输入输出流的缓冲区。也不会删除流对象(需要析构函数来销毁),关闭之后仍可以重新打开流对象进行连接其他文件。

输出时,没有文件就创建文件,有文件默认以截短方式打开文件(清空文件,输出到一个空文件中)

读文件:

  1. 创建一个ifstream对象管理输入流
  2. 将流对象的流源与特定的文件关联起来 /(打开文件)
  3. 以使用cin的方式使用该对象

需要进行文件打开成功检测,不能创建文件,没有文件会打开失败。

  • 创建流对象
ofstream fout;  //创建输出流(程序输出到文件)
ifstream fin;  // 创建输入流(文件输入到程序)
  • 打开文件(打开流与文件链接): 使用open()方法或构造函数完成流与文件的关联
方式一:

fstream fs; //创建一个未绑定的流
fs.open(s,mode);//创建时没有绑定,使用open打开
s : 可以是string或c风格的字符串
mode: 默认的模式依赖于fstream的类型
***请养成检测open是否成功的习惯*******

方式二:

fstream fs(s); 
//创建一个流,并打开名为s的文件,s可以是string或C风格字符串
使用构造函数将这两步(创建流对象和关联文件)合并成一句。

方式三:

fstream fs(s,mode);
//同上,并以mode模式打开

读写操作总结

fout写:

  • 使用ostream方法如 << 或 put(char) , write(const char*,int)

fin读:

  • 使用istream方法如
    抽取运算符>> ,读入时跳过空白字符
    读入字符串:
    get(char* ,int,‘#’),不抽取分界符
    getline(char*, int size ,‘#’),抽取并丢弃分界符

    两参数版本默认以换行符作为分解符,每次只能读一行
    get(char* ,int)
    getline(char*, int size)

每次读取整行:

  while(getline(fin,str))//一行一行读
  {
     cout<<str<<endl;
  }while(!fin.eof())//一行一行读
  {
     fin.getline(buffer,n,delim);//读n-1个字符到buffer,直到碰到换行或EOF或delim
  }

读入单字符:
get(ch&) 更好 false
int get(void)

while(fin.get(ch)) //到结尾退出 fin → false
{
    逐个字节读入
}
  • 关闭文件

流状态检查和is_open()

对于文件流来说,检查文件是否打开,使用is_open()方法是比较好的。(可以检测到文件模式配置不合理时打开失败的请情形)

if ( !is_open() )
{
    ...
}

打开多个文件


for () //循环打开多个文件
{
   fin.open(...);    //1. 打开文件
   if ( !is_open() )// 2. 打开成功检测
   {

   }
   while(fin.get(ch))//3. 读取文件
   {

   }
   fin.clear(); //重置条件状态,不一定需要这句话
   fin.close(); //4. 关闭文件
}

命令行处理技术

// count.cpp -- counting characters in a list of files
#include <iostream>
#include <fstream>
#include <cstdlib>          // or stdlib.h
int main(int argc,     //argc是命令行中的参数个数,包括命令名本身
        char* argv[])  //argv[0]是命令行中的第一个字符串,以此类推。
    //命令行中的参数可以传入程序

{
    using namespace std;
    if (argc == 1)          //没有参数时退出
    {
        cerr << "Usage: " << argv[0] << " filename[s]\n";
        exit(EXIT_FAILURE);
    }

    ifstream fin;              // open stream
    long count;
    long total = 0;
    char ch;

    for (int file = 1; file < argc; file++)  //按命令行中的参数个数进行循环 (减掉了命令名本身)
    {
        fin.open(argv[file]);  // 逐个打开命令行中的文件参数
        if (!fin.is_open())    //检查是否打开
        {
            cerr << "Could not open " << argv[file] << endl; //使用cerr表示错误消息
            fin.clear();   //重置条件状态
            continue;      //跳出本次循环,重新检测
        }
        count = 0;
        while (fin.get(ch))
            count++;
        cout << count << " characters in " << argv[file] << endl;
        total += count;
        fin.clear();           //重置条件状态
        fin.close();           //关闭文件
    }
    cout << total << " characters in all files\n";

    return 0;
}

文件模式

使用ios_base类中定义的常量来指定模式,这个模式也是bitmask类型,需要使用 | (按位或)来叠加。

常量含义
ios_base::in打开文件,读
ios_base::out打开文件,写
ios_base::ate打开文件,并移到文件尾
ios_base::app追加到文件尾
ios_base::trunc如果文件存在,则截短文件
ios_base::binary二进制文件

默认模式:
ifstream 默认 读
ofstream 默认 截短并写
位运算符OR( | )用于将两个位置合并成一个可用于设置两个位的值。
fstream类不提供默认模式值,因此必须显式的提供模式。

app和ate的区别在于,app只允许将数据添加到文件尾,而ate模式将指针放到文件尾。

二进制文件

对于字符来说,二进制和文本的表示是一样的,都是字符的ASCII码表示。区别主要在数字上,二进制文件的整数浮点数等表示方式,而文本文件是数字的ASCII码,可能存在转换精度问题,而且占用内存也更多。对于结构体和其他非基本的数据结构,也可以使用二进制文件比较方便。

以二进制的方式对文件进行读写操作
打开文件要指定为ios::binary

写文件:

二进制写文件主要利用流对象调成员函数write
函数原型: ostream & write(const char buffer, int len)*;
参数解释:字符串指针buffer指向内存中一段存储空间,len是读写的字节数。

读文件:

二进制读文件主要利用的是流对象调用成员函数read
函数原型:istream & read(char * buffer, int len);
参数解释:字符串指针buffer指向内存中一段存储空间,len是读写的字节数

注意:

  • 第一个参数可能需要把其他数据类型的地址(指针)强转成write和read要求的类型
  • 第二个参数使用sizeof()运算符来计算读取或写入的字节数。

eg:

const int  LIM = 20;
struct planet
[
    char name[LIM];
    double population;
    double g;
]

planet p1;

//以文本文件格式保存
ofstream fout("planet.dat", ios_base::in | ios_base::app);
fout << p1.name << " "<<p1.population<<" "<<p1.g<<"\n";
必须使用成员运算符显式的提供每个结构成员,还必须将相邻的数据分隔开,以便区分。

//以二进制文件格式保存
ofstream fout("planet.dat", ios_base::app | ios_base::binary);
fout.write((const char*)&p1, sizeof(p1));
上述代码将使用计算机的内部数据表示,将整个结构作为一个整体保存。
与文本文件相比,信息的保存更为紧凑、精确。

同样的方法也适用于不使用虚函数的类(隐藏指针也会参与简单复制,保存这种地址而不是数据没有意义。),,在这种情况下,只有数据成员会保存,而方法不会保存。(类成员有指针会就出问题,因为读和写操作是简单复制,不是深度复制

二进制文件读写示例:

// binary.cpp -- binary file I/O
#include <iostream> // not required by most systems
#include <fstream>
#include <iomanip>
#include <cstdlib>  // (or stdlib.h) for exit()

inline void eatline() { while (std::cin.get() != '\n') continue; }
struct planet
{
    char name[20];      // name of planet
    double population;  // its population
    double g;           // its acceleration of gravity
};

const char* file = "planets.dat";

int main()
{
    using namespace std;
    planet pl;
    cout << fixed << right;

    // show initial contents
    ifstream fin;
    fin.open(file, ios_base::in | ios_base::binary);  // binary file
    //NOTE: some systems don't accept the ios_base::binary mode
    if (fin.is_open())
    {
        cout << "Here are the current contents of the "
            << file << " file:\n";
        while (fin.read((char*)&pl, sizeof pl))
        {
            cout << setw(20) << pl.name << ": "
                << setprecision(0) << setw(12) << pl.population
                << setprecision(2) << setw(6) << pl.g << endl;
        }
        fin.close();
    }

    // add new data
    ofstream fout(file,
        ios_base::out | ios_base::app | ios_base::binary);
    //NOTE: some systems don't accept the ios::binary mode
    if (!fout.is_open())
    {
        cerr << "Can't open " << file << " file for output:\n";
        exit(EXIT_FAILURE);
    }

    cout << "Enter planet name (enter a blank line to quit):\n";
    cin.get(pl.name, 20);
    while (pl.name[0] != '\0')
    {
        eatline();
        cout << "Enter planetary population: ";
        cin >> pl.population;
        cout << "Enter planet's acceleration of gravity: ";
        eatline();  //吃掉一次换行符
        //因为之前的get()将换行符留在输入流中,cin >> pl.g;结束时输入流中会输入一个换行符,吃掉这个换行符
        // 接下来用换行符退出时,才能准确识别出来这个退出的换行符
        //eatline()读取并丢弃输入流中换行符之前的内容(包括换行符),这样接下来的get看到的第一个字符就不是换行符
        fout.write((char*)&pl, sizeof pl);
        cout << "Enter planet name (enter a blank line "
            "to quit):\n";
        cin.get(pl.name, 20);
        //如果读取的第一个字符是换行符,将把换行符留在输入流中,返回0个字符,并在pl.name[0]后面加一个 '\0'
        //这样就将退出循环
        //而换行符被吃掉一次之后,get()可以顺利读取pl.name
    }
    fout.close();

    // show revised file
    fin.clear();    // not required for some implementations, but won't hurt
    fin.open(file, ios_base::in | ios_base::binary);
    if (fin.is_open())
    {
        cout << "Here are the new contents of the "
            << file << " file:\n";
        while (fin.read((char*)&pl, sizeof pl))
        {
            cout << setw(20) << pl.name << ": "
                << setprecision(0) << setw(12) << pl.population
                << setprecision(2) << setw(6) << pl.g << endl;
        }
        fin.close();
    }
    cout << "Done.\n";
    // keeping output window open
        // cin.clear();
        // eatline();
        // cin.get();
    return 0;
}

这个程序不能使用string对象(应该使用固定容量的字符数组形式),因为string的实现实际上包含了一个字符指针,但是读和写操作只是简单复制,并不能进行深度复制,会出现乱码和程序崩溃

随机读取

随机存取常被用于数据库文件,程序维护一个独立的索引文件,该文件指出数据在主数据文件的位置。这样程序便可以直接跳转到这个位置,读取或修改其中的数据。

读写流

fstream finout;

读写流对象继承了两个缓冲区,一个用于输入,一个用于输出
并能同步这两个缓冲区的处理。也就是说,当程序读写文件时,它能协调地移动输入缓冲区中的输入指针和输出缓冲区的输出指针。

fstream类是从iostream类派生而来的,而iostream类是基于istream类和ostream类的。因此fstream类能够使用它们的所有方法

文件模式的设置

finout.open(file,ios_base::in | ios_base::out | ios_base::binary)
  1. 文件记录对应于程序结构或类,因此用二进制格式(.dat)存储起来更方便。
  2. 使用out模式可以修改原始数据,而使用app追加模式,不能修改原始数据,指正追加新数据,所以这里选择使用out模式。

文件中的移动方式:读指针seekg()和写指针seekp()

由于fstream类使用缓冲区来存储中间数据,因此指针指向的是缓冲区中的位置,并不是实际的文件。

fstream继承了两个方法:
seekg()用于istream对象
seekp()用于ostream对象

读指针移动函数seekg()原型如下:它们都是模板

basic_istream<charT,traits>& seekg(off_type,ios_bas::seekdir);
basic_istream<charT,traits>& seekg(pos_typ);

本章使用的是char类型的模板具体化。

istream & seekg(streamoff,ios_base::seekdir); //文件相对位置
istream & seekg(streampos); //文件绝对位置

原型一:读指针定位到距离第二个参数指定的文件位置…字节的位置。

  • streamoff值被用于度量相对于文件特定位置(第二个参数提供)的偏移量(单位为字节,整型)。
  • seekdir参数也是ios_base类中定义的另一种整型,有三个预定义的常量共选择:
    ios_base::beg 文件开头
    ios_base::cur 当前位置
    ios_base::end 文件结尾

使用示例:

fin.seekg(30,ios_base::beg); //将读指针定位到相对于文件开头处30个字节处。
fin.seekg(-1,ios_base::cur);//当前读指针向前移动一个字节
fin.seeg(0,ios_base::end);//将读指针定位到文件尾

原型二:读指针定位到里开头特定距离(…字节)的位置。

  • streampos的值表示文件中的绝对位置,(文件开始处的第一个字节编号为0),即相对于开头的第多少个字节处。

使用示例:

fin。seekg(112; 将读指针指向文件的第113个字节(从0计数开始算第一个字节)

:早期streamoff和streampos类型实际上是一些标准整形(如long)的typedef,后来随着类型升级,它们作为off_type和pos_type的char具体化继续存在。

检查文件指针的当前位置:读指针 tellg() ;写指针tellp()

tellg()和tellp()都返回一个表示当前绝对位置的streampos值(以字节为单位的整型,从文件开始处的第一个字节称为0字节处)。

  • 创建fstream对象时,输入指针和输出指针将一前一后的移动,因此tellg和tellp返回的值相同。
  • 如果分别创建istream对象和ostream对象,两指针将相互独立的移动,返回不同的值。

重置流对象条件状态

if (finout.eof())
    finout.clear();  //clear eof flag
else
{
    cerr <<"Error in reading"<<file<<".\n";
    exit(EXIT_FAILURE);
}

上述代码解决的问题是:程序读取并显示整个文件后,将设置eofbit元素。这使程序相信,它已经处理完文件,并禁止对文件做进一步的操作。使用clear方法重置流状态,并打开eofbit后,程序便可以再次访问该文件。

eatline()和get()配合使用?

随机修改二进制文件程序示例

// random.cpp -- random access to a binary file
#include <iostream>     // not required by most systems
#include <fstream>
#include <iomanip>
#include <cstdlib>      // (or stdlib.h) for exit()
const int LIM = 20;
struct planet
{
    char name[LIM];      // name of planet
    double population;  // its population
    double g;           // its acceleration of gravity
};

const char* file = "planets.dat";  // ASSUMED TO EXIST (binary.cpp example)
inline void eatline() { while (std::cin.get() != '\n') continue; }

int main()
{
    using namespace std;
    planet pl;
    cout << fixed;

    // show initial contents
    fstream finout;     // 创建读写流
    finout.open(file,
        ios_base::in | ios_base::out | ios_base::binary); //读、写、二进制
    //NOTE: Some Unix systems require omitting | ios::binary
    int ct = 0;
    //文件成功打开
    if (finout.is_open())
    {
        finout.seekg(0);    // 读指针 跳转到文件开始
        cout << "Here are the current contents of the "
            << file << " file:\n";
        while (finout.read((char*)&pl, sizeof pl))
        {
            cout << ct++ << ": " << setw(LIM) << pl.name << ": "
                << setprecision(0) << setw(12) << pl.population
                << setprecision(2) << setw(6) << pl.g << endl;
        }
        //程序读取显示整个文件之后,将设置eofbit元素
        //这使程序相信,它已经处理完文件,并禁止对文件进一步的读写。
        //使用clear方法重置条件状态,并打开eofbit之后,程序便可以再次访问该文件。

        if (finout.eof())
            finout.clear(); // clear eof flag
        //else部分处理程序程序因到达文件尾之外的其他原因(如硬件故障)而停止读取的情况
        else
        {
            cerr << "Error in reading " << file << ".\n";
            exit(EXIT_FAILURE);
        }
    }
    //文件打开失败
    else
    {
        cerr << file << " could not be opened -- bye.\n";
        exit(EXIT_FAILURE);
    }

    // 修改记录
    cout << "Enter the record number you wish to change: ";
    long rec;
    cin >> rec;
    eatline();              // get rid of newline
    if (rec < 0 || rec >= ct)
    {
        cerr << "Invalid record number -- bye\n";
        exit(EXIT_FAILURE);
    }
    streampos place = rec * sizeof pl;  // convert to streampos type
    finout.seekg(place);    // 读指针 根据输入 进行随机跳转
    
    if (finout.fail())  //跳转失败
    {
        cerr << "Error on attempted seek\n";
        exit(EXIT_FAILURE);
    }
    //跳转成功 开始读文件,并显示所选部分文件
    finout.read((char*)&pl, sizeof pl);
    cout << "Your selection:\n";
    cout << rec << ": " << setw(LIM) << pl.name << ": "
        << setprecision(0) << setw(12) << pl.population
        << setprecision(2) << setw(6) << pl.g << endl;
    if (finout.eof())
        finout.clear();     //读取完毕 , 重置条件状态

    cout << "Enter planet name: ";
    cin.get(pl.name, LIM);  //重新输入名称
    eatline();
    cout << "Enter planetary population: ";
    cin >> pl.population;  //重新输入,进行了数据类型的转换。这里使用<<抽取运算符,所以后面不需要加eatline()吃掉换行符
    cout << "Enter planet's acceleration of gravity: ";
    cin >> pl.g;           //重新输入,进行了数据类型的转换。这里使用<<抽取运算符,所以后面不需要加eatline()吃掉换行符
   
    //将修改写入文件
    finout.seekp(place);    // 写指针位置 跳到所更改记录处
    finout.write((char*)&pl, sizeof pl) << flush;//主动刷新输出缓冲区
   
    if (finout.fail()) //输出成功检验
    {
        cerr << "Error on attempted write\n";
        exit(EXIT_FAILURE);
    }

    // 显示更改后的文件
    ct = 0;
    finout.seekg(0);            //读指针 跳转到文件开头
    cout << "Here are the new contents of the " << file
        << " file:\n";
    while (finout.read((char*)&pl, sizeof pl)) //循环读取
    {
        cout << ct++ << ": " << setw(LIM) << pl.name << ": "
            << setprecision(0) << setw(12) << pl.population
            << setprecision(2) << setw(6) << pl.g << endl;
    }
    finout.close();  //关闭读写流
    cout << "Done.\n";
    // keeping output window open
        // cin.clear();
        // eatline();
        // cin.get();
    return 0;
}

内核格式化

读取string对象中的格式化信息或者将格式化信息写入string对象被称为内核格式化(incore formatting)

头文件 < sstream >定义了三个类:istringstream,ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。根据派生关系,这些类能够使用istream和ostream方法来抽取字符串中的信息,并对要放到字符串中的信息进行格式化。(主要用来进行数据类型转换的。)

参考文献:

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值