C++ primer Plus 第十七章——输入、输出和文件

17.1.1流和缓冲区

C++程序把输入和输出看作字节流(若干字节组成的字符序列)。输入时,程序从输入流中抽取字节;输出时,程序将字节插入到输出流中。

输入流中的字节可能来自键盘以及存储设备或其他程序

输出流中的字节可以流向屏幕、打印机、存储设备或其他程序

充当了程序和流源或流目标之间的桥梁,打个比方说,水滴就是字节,水滴汇成的水流就是流,我们可以从水流中取水,也可以往水流中加水,也就是从流中提取输出的内容,插入输入的内容。

缓冲区:用作中介的内存块,它是将信息从设备传输到程序或从程序传输到设备的临时存储工具,可以提高数据的读取速率

具体原理:内存是放进程运行时的程序和数据的,CPU访问内存比访问硬盘速度快,每次从磁盘文件中读取一个字符,要消耗大量的时间,缓冲方法是从硬盘上读取大量信息,将信息存在缓冲区里,因为CPU访问内存所用时间比访问硬盘少,所以这种方法更快。

输出时,程序首先填满缓冲区,然后把整块的数据传输给硬盘,并清空缓冲区,这被称为刷新缓冲区

程序在什么时候刷新缓冲区?

    (1) 缓冲区被填满;
    (2) 换行符被发送到缓冲区;
    (3) 即将发生输入;
    (4) 控制符 flush 强制刷新缓冲区,如 cout<<"hello, this is ...."<<flush;
    (5) flush() 方法(函数)强制刷新缓冲区,如 flush(cout);
    (6) 控制符endl强制刷新缓冲区,不过控制符 endl 比 flush 多一个功能,即 endl 还将插入一个换行符;
    (7)endl() 方法(函数)强制刷新缓冲区,功能同 (6) 一样,具体代码为:endl(cout);

17.1.2流、缓冲区和iostream文件

为了管理流,C++提供了一系列头文件,它们的关系如下

streambuf类为缓冲区提供了内存,并提供了填充缓冲区、访问缓冲区、刷新缓冲区和管理缓冲区内存的方法

ios_base类表示流的一般特征,如是否可读取,是二进制流还是文本流

ostream类提供了输出方法

istream类提供了输入方法

iostream类提供了输入和输出方法

C++为了能够处理需要16位国际字符集或更宽的字符类型,在传统的8位char(“窄”)类型的基础上添加了wchar_t(“宽”)类型,这也意味着有专门的输入输出对象用于处理宽字符如wcin wcout(这个地方有兴趣的可以区查一下两种类型的区别)

iostream头文件会自动生成八个流对象(四个是宽类型的,这里只列出窄类型的)

cin

对应于标准输入流,平时就是用cin输入各种数据类型,也可以通过重载使其输入用户自定义的类

cout

对应于标准输出流,就是用cout输出各种数据类型,也可以通过重载使其输出用户自定义的类

cerr

对应着标准错误流,可用于显式错误信息,默认情况下,这个流被关联到标准输出设备(显示器),这个流没有被缓冲,这意味着信息将直接发送给屏幕

clog

对应着标准错误流,可用于显式错误信息,默认情况下,这个流被关联到标准输出设备(显示器),与cerr不同的是,clog有缓冲区

 

17.2用cout进行输出

C++将输出看作字符流,这说明如果要在屏幕上显示数字-2.34,需要五个字符,-、2、.、3、4。ostream类的主要任务就是将数值类型转换为文本形式表示的字符流。

C++通过重载<<运算符使之能够识别c++所有的基本类型

就是所cout可以用于输出C++的各种基本类型

1.输出和指针

C++用指向字符串存储位置的指针来表示字符串,所以下列cout语句都可显示字符串

char *p = "123";
char name[20] = "1234";
cout<<"hello";
cout<<name;
cout<<p;

如何输出指针地址呢

可以用void*将char*转化为其他类型来输出其地址

如cout<<(void *)p;

这样将会输出p指向的地址

2.拼接输出

可以通过cout<<a<<b<<c;这种方式输出

原因是 cout<<a返回一个cout的对象,可以继续输出

17.2.2其他ostream方法

cout.put()输出一个字符,相当于putchar()

cout.write(char*,x)输出指定字符串的指定数量的字符,注意如果x超出了指定字符串的长度,它仍然会输出,即输出垃圾值

举个例子

cout.put('a');
//输出字符a
cout.put("1234",2);
//输出12

17.2.3刷新输出缓冲区

每次等缓冲区满了再输出太麻烦了,我们可以使用endl或者flush来刷新缓冲区

具体语法如下

cout<<"132"<<endl;

或者

cout<<"132"<<flush;(<<重载,会将其替换为flush(cout))

17.2.4用cout进行格式化

ostream插入运算符将值转换为文本格式。在默认情况下,格式化值的方式如下。

    *  对于char值,如果它代表的是可打印字符,则将被作为一个字符显示在宽度为一个字符的字段中。

    *  对于数值整型,将以十进制方式显示在一个刚好容纳该数字及负号(如果有的话)的字段中;

    *  字符串被显示在宽度等于该字符串长度的字段中。

  浮点数的默认行为有变化。下面详细说明了老式实现和新式实现之间的区别。

    *新式:浮点类型被显示为6位,末尾的0不显示(注意,显示的数字位数与数字被存储时精度设置没有任何关系)。数字以定点表示法显示还是科学计数法表示,取决于它的值。具体来说,当指数大于等于6或小于等于-5时,将使用科学计数法表示。另外,字段宽度恰好容纳数字和负号(如果有的话)。默认的行为对应于带%g说明符的标准C库函数fprintf()。

    *老式:浮点类型显示为带6位小数,末尾的0不显示(注意,显示的数字位数与数字被存储时的精度没有任何关系)。数字以定点表示法显示还是以科学计数法表示,取决于他的值。另外,字段宽度恰好容纳数字和负号(如果有的话)。

1.输出十进制 八进制 十六进制

要控制整数以十进制、十六进制还是八进制显示,可以使用dec、hex和oct控制符。

例如,下面的函数调用将cout对象的计数系统格式状态设置为十六进制:

hex(cout);设置输出为十六进制

oct(cout);设置输出为八进制

dec(cout);设置输出为十进制

由于stream类重载了<<,所以可以直接使用cout<<hex;来设置输出为十六进制

#include <iostream>
using namespace std;

int main()
{
    int a = 20;
    cout<<hex<<a<<endl;
    cout<<oct<<a<<endl;
    cout<<dec<<a<<endl;
    return 0;
}

/*输出为
14
24
20
*/

注意设置完输出16进制之后,要想输出10进制需要设置回来。

2.调整字段宽度

可以使用width成员函数将长度不同的数字放到宽度相同的字段中,该方法的原型为:

    int width();

    int width(int i);

  第一种格式返回字段宽度的当前设置;第二种格式将字段宽度设置为i个空格,并返回以前的字段宽度值。这使得能够保存以前的值,以便以后恢复宽度值时使用。

  width()方法之影响显示的下一个项目,然后字段宽度将恢复为默认值。由于width()是成员函数,因此必须使用对象来调用它。

#include <iostream>
using namespace std;

int main()
{
    int a = 20;
    cout.width(5);
    cout<<a<<a<<endl;
    return 0;
}
/*输出
   20
*/

3.填充字符

 在默认情况下,cout使用空格填充字段中未被使用的部分,可以使用fill()成员函数来改变填充字符。例如,下面的函数调用将填充字符改为星号:
    cout.fill('*');

#include <iostream>
using namespace std;

int main()
{
    int a = 20;
    cout.width(5);
    cout.fill('*');
    cout<<a<<a<<endl;
    return 0;
}
/*输出
***20
*/

fill填充字符会一直有效,直到修改它为止

4.设置浮点数的显示精度

 浮点数精度的含义取决于输出模式。在默认情况下,它指的是显示的总位数。在定点模式和科学模式下,精度指的是小数点后面的位数。已经知道,C++的默认精度为6位(但末尾的0将不显示)。precision成员函数使得能够选择其他值。例如,下面的函数调用将cout的精度设置为2:

    cout.precision(2);

  和width()的情况不同,但与fill()相似,新的精度设置将一直有效,直到被重新设置。下面的程序说明了这一点:

#include <iostream>
using namespace std;

int main()
{
    cout.precision(2);
    double a = 1.28;
    double b = 0.222;
    double c = 11.2;
    cout<<a<<endl;
    cout<<b<<endl;
    cout<<c<<endl;
    return 0;
}
/*输出结果
1.3
0.23
11
*/

这说明precision会进行四舍五入

5.打印末尾的0和小数点

对于有些输出,希望保留末尾的0,iostream系列类没有提供专门用于完成这项任务的函数,但ios_base类提供了一个setf()函数(用于set标记),能够控制多种格式化特性。这个类还定义了多个常量,可以作为函数的参数。例如,下面的函数调用使cout显示末尾的小数点:

    cout.setf(ios_base::showpoint);

  使用默认的浮点格式时,上述语句还将导致末尾的0被显示出来。

注意cout.setf(ios_base::showpoint);要结合precision使用

  showpoint是ios_base类声明中定义的类级静态常量。类级意味着如果在成员函数定义的外面使用它,则必须在常量名前加上作用域运算符(::)。因此,ios_base::showpoint指的是在ios_base类中定义的一个常量。

#include <iostream>
using namespace std;

int main()
{
    cout.setf(ios_base::showpoint);
    double a = 1.28;
    double b = 0.225;
    double c = 11.2;
    cout<<a<<endl;
    cout<<b<<endl;
    cout<<c<<endl;
    return 0;
}
/*输出

1.28000
0.225000
11.2000
*/

头文件iomanip

iomanip提供了很多函数来设置cout,如

setprecision()设置精度

setfill()填充字段

setw()设置字段宽度

例子:

#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
    double a = 1.222222;
    cout<<setw(10)<<a<<endl;
    cout<<setfill('*')<<a<<endl;
    cout<<setw(10)<<setfill('*')<<a<<endl;
    cout<<setprecision(2)<<a<<endl;
    return 0;
}
/*输出结果
   1.22222
1.22222
***1.22222
1.2
*/

17.3使用cin进行输入

cin支持C++的各种基本类型 int char double 等等

cin可以连续输入数据 如cin>>a>>b>>c

cin可以标注输入的数字是什么进制  如 cin>>hex>>a 输入的数字是一个十六进制

17.3.1cin>>如何检查输入

cin将跳过空白(空格、换行符和制表符),直到遇到非空白字符

int main()
{
    char a[10];
    cin>>a;
    cout<<a;
    return 0;
}

例如,如果是字符串

输入     123

输出为123

cin遇到换行会停止读入,并把换行符留在缓冲区

int main()
{
    char a[10];
    cin>>a;
    char c = cin.get();
    cout<<(int)c;
    return 0;
}

/*输入123

输出为
10   10是换行的ascii码值

cin将读取从非空白字符开始,到与目标类型不匹配的第一个字符之间的全部内容,并把剩余没有读入的留在缓冲区

int main()
{
    int a;
    cin>>a;
    char c = cin.get();
    cout<<a<<endl;
    cout<<c;
    return 0;
}

/*输入123z


输出
123
z
*/

运算符将读取1、2、3,因为它们是整数的部分,但是不会读取z,所以z会留在缓冲区里,下一个cin从这里开始读,与此同时,运算符将字符序列123转换为一个整数值 赋给a

17.3.2流状态

1设置状态

cin或cout对象包含一个描述流状态的数据成员,流状态由三个ios_base元素组成:eofbit,failbit,badbit。每一个都有0和1两个值

1是表明发生了这个问题,都将停止cin的输入,并且只有在clear()方法来回调之后,才可以继续使用cin输入数据

eofbit = 1 说明到达文件末尾

failbit = 1 说明cin操作未能读取到预期的字符或者 I/O失败(试图读取不可访问的文件或试图写入受保护的磁盘)

badbit = 1 发生了其他错误

clear(eofbit)将会使eofbit为1

2.I/O异常

3.流状态的影响

只有在流状态良好的情况(即使eofbit failbit badbit均为0)cin才能进行输入

int main()
{
    int a;
    while(cin>>a)
    {
        ;
    }
    if(cin.eof())
        cout<<"到达文件末尾"<<endl;
    return 0;
}

使用ctril+z来告诉程序到达了文件末尾,从而使eofbit = 0

如果发生的是fail错误,不仅要使用clear()来使failbit =0,还要清空缓存区

#include <iostream>
using namespace std;

int main()
{
    int a,c;
    while(cin>>a)
    {
        ;
    }
    cin.clear();
    while(!isspace(c = cin.get()))读取缓存区里的字符,一直到读取到空字符
        cout<<(char)c<<endl;
    /*
     while((ch = cin.get())!='\n')读取缓存区里的字符,一直到读取到换行符
        cout<<(char)c<<endl;
    */
    cin>>a;
    cout<<a;
    return 0;
}
/*输入
123z
123

输出
z
123
*/

17.3.3其他istream类方法

具体见cin cin.get() cin.getline() getline()的区别

17.4文件的输入和输出

输入输出的基本语法以及规范见我的另一篇博客

一些细节:

1.在创建ofstream对象时,将会为输出缓冲区分配空间,如果创建了两个ofstream对象,则会创建两个缓冲区,每个对象各一个

由于磁盘驱动器被设计成以大块的方式传输数据,而不是逐字节地进行传输,因此通过缓冲可以大大提高从程序到文件传输的速度。

2.使用close()方法只会关闭流与文件的连接,并不会删除流,因此该对象的缓冲区仍存在,使用该对象 将数据写入其他文件的时候,要记得清空缓存区。

17.4.2流状态检查和is_open()

有时候可见可能打开失败,可以使用is_open()方法来判断一下 是否成功打开文件。

如果打开了将返回1,反之将返回0

ifstream fin("a.txt");
if(!fin.is_open())
{
cout<<"文件打开失败"<<endl;
}
else
{
执行相应的写入操作
}

 

17.4.3打开多个文件

如果创建了多个输出对象,程序会创建多个缓冲区,这样会花费大量的计算机资源。

我们可以重复的使用一个输出对象,只需要每次将其与不同的文件连接,并且在切断连接后,清楚错误状态,清空流。

clear()方法 清除错误状态

sync()方法 清空流(实际上就是清空缓冲区)

#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main()
{
    int i,n;
    int object =1;
    string filename[10];//存十个文件名
    ifstream fin;
    cin>>n;
    for(i=0;i<n;i++)
    {
        cin>>filename[i];//输出文件名
    }
    for(i=0; i<n; i++)
    {
        fin.open(filename[i].c_str());//因为is_open方法是接受一个char类型的字符串
        if(!fin.is_open())//所以要将string类型的字符串转为char类型
        {
            cerr<<"打开文件失败"<<endl;
            continue;
        }
        else
        {
            fin>>object;
            fin.close();//切断fin与a.txt的联系
            fin.clear();//清除错误状态
            fin.sync();//清空流
        }
    }

}

 

17.4.4命令行处理技术

文件处理程序通常使用命令行参数来指定文件。命令行参数是用户在输入命令时,在命令行中输入的参数。例如在linux系统中计算文件包含的字数,可以在命令行提示符下输入下面的命令:

wc report1 report2 report3

其中wc是程序名,report1 report2 report3是作为命令行参数传递给程序的文件名

C++中可以访问命令行参数,可以使用下面的方法

int main(int argc, char* argv[])

argc为命令参数的个数,argv[]为一个字符串数组,(终于明白java主方法为啥有个字符串了)

通过这种方法可以 在命令行里指定程序将操作哪一个文件,比较方便。

17.4.5文件模式

open()方法有俩参数,一个是目标文件名,另一个是打开模式。

不指定打开模式时

如果是ofstream对象,默认是只写方式,而且每次写入会将清空之前的内容

如果是ifstream对象,默认是只读方式

如果要自定义模式,需要使用相应的格式常量

执行二进制输入需要使用 ios_base::in|ios_base::binary(中间以| 分隔)

执行以追加模式输出时需要使用ios_base::out|ios_base::app

同时使用ios_base::in|ios_base::out

二进制文件

将数据存储在文件中,可以将其存储为文本格式或二进制格式

文本格式:所有内容都存储为文本

优点:便于读取,可以使用编辑器或字处理器来读取和编辑文本文件,可以很方便的将文本文件从一个计算机系统传输到另一个计算机系统。

二进制格式:计算机的内部存储值的格式

优点:不会有转化误差或舍入误差。以二进制格式保存数据速度更快,因此不需要转换,并可大块地存储数据。

用文本方式存储信息不但浪费空间,而且不便于检索。例如,一个学籍管理程序需要记录所有学生的学号、姓名、年龄信息,并且能够按照姓名查找学生的信息。程序中可以用一个类来表示学生:

要以二进制格式存储数据,可以使用write()成员函数,这个方法只会逐字节的赋值数据,而不进行任何转换

class CStudent
{
    char szName[20];  //假设学生姓名不超过19个字符,以 '\0' 结尾
    char szId[l0];  //假设学号为9位,以 '\0' 结尾
    int age;  //年龄
};

如果用文本文件存储学生的信息,文件可能是如下样子:
Micheal Jackson 110923412 17
Tom Hanks 110923413 18

这种存储方式不但浪费空间,而且查找效率低下。因为每个学生的信息所占用的字节数不同,所以即使文件中的学生信息是按姓名排好序的,要用程序根据名字进行查找仍然没有什么好办法,只能在文件中从头到尾搜索。

可以用二进制的方式来存储学生信息,即把 CStudent 对象直接写入文件。在该文件中,每个学生的信息都占用 sizeof(CStudent) 个字节。对象写入文件后一般称作“记录”。本例中,每个学生都对应于一条记录

读写二进制文件不能使用前面提到的类似于 cin、cout 从流中读写数据的方法。这时可以调用 ifstream 类和 fstream 类的 read 成员函数从文件中读取数据,调用 ofstream 和 fstream 的 write 成员函数向文件中写入数据。

write函数的原型如下

ostream & write(char* buffer, int count);

该成员函数将内存中 buffer 所指向的 count 个字节的内容写入文件,返回值是对函数所作用的对象的引用,如 obj.write(...) 的返回值就是对 obj 的引用。

write 成员函数向文件中写入若干字节,可是调用 write 函数时并没有指定这若干字节要写入文件中的什么位置。那么,write 函数在执行过程中到底把这若干字节写到哪里呢?答案是从文件写指针指向的位置开始写入。

文件写指针是 ofstream 或 fstream 对象内部维护的一个变量。文件刚打开时,文件写指针指向文件的开头(如果以 ios::app 方式打开,则指向文件末尾),用 write 函数写入 n 个字节,写指针指向的位置就向后移动 n 个字节。

具体代码如下:

#include <iostream>
#include <fstream>
using namespace std;
class CStudent
{
public:
    char szName[20];
    int age;
};
int main()
{
    CStudent s;
    ofstream outFile("students.dat", ios::out | ios::binary);
    while (cin >> s.szName >> s.age)
        outFile.write((char*)&s, sizeof(s));
    outFile.close();
    return 0;
}

注意我们在使用write函数时要先将CStudent类型的地址转换为char*类型地址。实际上这里并不是char*类型,具体可见C++二进制文件读写(read和write)详解

read函数原型如下

istream & read(char* buffer, int count);

read 成员函数从文件读指针指向的位置开始读取若干字节。文件读指针是 ifstream 或 fstream 对象内部维护的一个变量。文件刚打开时,文件读指针指向文件的开头(如果以ios::app 方式打开,则指向文件末尾),用 read 函数读取 n 个字节,读指针指向的位置就向后移动 n 个字节。因此,打开一个文件后连续调用 read 函数,就能将整个文件的内容读取出来。

具体代码如下:

#include <iostream>
#include <fstream>
using namespace std;
class CStudent
{
    public:
        char szName[20];
        int age;
};
int main()
{
    CStudent s;       
    ifstream inFile("students.dat",ios::in|ios::binary); //二进制读方式打开
    if(!inFile) {
        cout << "error" <<endl;
        return 0;
    }
    while(inFile.read((char *)&s, sizeof(s))) { //一直读到文件结束
        int readedBytes = inFile.gcount(); //看刚才读了多少字节
        cout << s.szName << " " << s.age << endl;   
    }
    inFile.close();
    return 0;
}
发布了37 篇原创文章 · 获赞 3 · 访问量 2365

猜你喜欢

转载自blog.csdn.net/Stillboring/article/details/105513631