C++——流和输入输出

写在前面

应该知道的一些知识

掩码(mask)

定点计数法

科学计数法  //应该都知道

基本认识 

C++把程序的输入和输出看作一个字节流

流的概念:流是一个抽象、动态的概念,是一连串连续动态的数据集合。 对于输入流而言,数据源就像水箱,流 (stream)就像水管中流动着的水流,程序就是我们最终的用户。 我们通过流 (A Stream)将数据源 (Source)中的数据 (information)输送到程序 (Program)中,或者从程序中接受。

其实流就是一堆数据,这些数以依次发往要去的地方,就像水流一样。

输入时,程序从输入流中抽取字节,输出时,程序将字节添加到输出流里。

在进行输入输出时,数据不会立刻流动到目标(例如从键盘输入),而是被放在一个名为缓冲区的内存块中。


 输入流

例如下面的例子。

#include <iostream>

using namespace std;

int main()
{
    cout << "please input words";
    char ch;
    int total = 0;
    while(cin >>ch&&ch!='~')
    {
        if(ch>='0'&&ch<='9')
            total++;
        cout << "now total is " << total << endl;
    }
    cout << total;
    return 0;
}

运行结果

在回车的那一刻,从缓冲区中发送给程序。

输出时,程序首先填满缓冲区,再把数据块以块的形式传递,并清空缓冲区,以便供下一波输出使用(缓冲区刷新)


输出流

输出流时逐字符输出的,即只会发送字符,例如一个-3.14,不是发送-3.14这个数据块,而是经过ostream类处理,逐字符输出字符  -  3  .  1  4。对于宽字符类型,像汉字,ostream类中也有相应的字符类型(wchar_t)。

从键盘输入每次只能键入一个字符,为什么要有缓冲区呢

这样做为了提高容错,在没有按下回车前可对要发送到输入流中的数据修改。在键入回车的一刻,缓冲区将数据流发送到目标(屏幕或其他。


抽象的解释

源 -> 流  ->  目标


流的作用

无论是进行输入还是输出,都需要将流链接两个目标对于,程序而言,可通过流与文件,显示器,键盘相连。

因此,流充当的类似桥梁的作用。

缓冲区的作用

应该都知道硬盘和内存的关系

流和文件也是对应的这种关系

通常,通过使用缓冲区可以更高效的处理输入和输出。他是一个数据的临时存储工具。磁盘通常以块传输数据(例如512字节),而程序只能一次处理一个字符,二者直连很不合理,所以就放在缓冲区中。以便以合理的方式处理数据。


一些类

管理流和缓冲区的工作C++已经为我们实现好了

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

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

ios类 基于ios_base 其中包括了一个指向streambuf对象的指针成员

ostream类 从ios类派生而来,提供了输出方法(类方法)

istream类  从ios类派生而来,提供了输入方法(类方法)

iostream类 基于istream ostream类,因此继承了输入和输出方法

从上面得知,这么平常的输入输出竟然包含了这么多发杂的逻辑!

而cout和cin本质上是两个重载了<<(左移运算符)和>>(右移运算符)的两个iostream类的实例化对象!(在名称空间std中)


cout

<<重载过后用于输出,不再叫左移运算符,而是叫插入运算符。

最简单的输出

int a = 10;
cout << "a is" << a << endl;

 因为<<返回一个ostream对象的引用,可进行递归输出。


它能够识别一些基本数据类型

unsigned char
signed char
char
short
unsigned short
int
unsigned int
long
unsigned long
long long //C++ 11
unsigned long long //C++ 11
float
double
long double

 假如我们要显示一个整形的地址

int main()
{
    int a = 10;
    int *pa = &a;
    cout << pa;
    return 0;
}

非常简单


那一个字符串地址呢

    char str[] ="hello world";
    cout << str;

运行结果

为什么不是地址呢

是因为C++为下列指针也重载了<<

const signed char *
const unsigned char *
const char *
void *

传入字符串的指针具体化的模板(重载的<<)就会输出字符串,要想输出地址强制转换即可

    cout << (void*)str;

 运行结果


其他的ostream类方法

put(char_type __c);
write(const char_type *__s, streamsize __n);

前者用于显示字符,后者用于显示字符串,二者返回 ostream &,因此均可链式调用

    cout.put('A').put('b');
    cout << endl;
    cout.write("hello",5).write("world",10);

运行结果

合适的情况下也可将int用于put(),自动转换为ASCII码对应的字符


缓冲区的刷新

如果说在等待缓冲区满再发送消息,那为什么程序还会即时发送消息呢?
这与一些机制有关:在屏幕输出时,程序不会等到缓冲区满再发送消息,例如在换行符发送到缓冲区后,程序就会清空并且发送消息,或者在输入即将发生时。即不是先输出在输入,而是将要输入的时候输出并且输入。

控制符flush,也是控制函数,可直接传入cout。

cout << flush;
//or
flush(cout);

使用cout进行格式调整

在C中可用%说明符来控制打印的格式。例如,输出小数点后的两位。

double val = 3.14159;
printf("%.2f",val);

在C++中也有这样的格式控制

首先,在默认情况下

对于char,如果是一个可打印字符,则被用作字符打印在宽度为1的字段中

对于整形,显示十进制,字段宽度刚好容纳数字

对于字符,串显示字段宽度正好为字符串长度

对于浮点,显示6位,并且小数点后的0不显示,使用正常显示还是科学计数法取决于具体的值,指数大于等于6或者小于等于5即显示科学计数法(不同编译器可能不同),显示字段宽度恰好能容纳符号和数字。

特别的:老式的浮点显示方法为,显示小数点后6位,0不显示


可以用cout的类方法,或者函数更改格式或者

再说一下继承关系

ostream继承ios ios继承ios_base

因此,ios_base的方法在ostream的对象中也能使用


1.显示 十进制 十六进制 或者 八进制

使用控制符

dec(cout); //十
hex(cout); //十六
oct(cout); //八

 使用重载了的<<等价调用上面的形式

    cout << dec 
         << hex
         << oct;

示例

    int a = 11;
    cout << dec << a << endl;
    cout << hex << a << endl;
    cout << oct << a << endl;

 运行结果

 注意:这样上述的两种方式都是永久有效(如果不再进行格式调整),而不是临时的(只用于下一项的输出,下一项和下一次是不同的概念)。


2.设置字段宽度

可用于对齐(默认右对齐)

注意:字段宽度的设置,暂时有效,只适用于下一项,不是下一条输出语句,也不是永久

//返回当前设置的宽度,这里的返回指的return value
cout.width();
//设置输出字段宽度并恢复之前的设置,并且返回一个之前设置的宽度值
cout.width(int len);
//比如
#include <iostream>

using namespace std;


int main()
{
    char ch = 'a';
    cout.width(5);
    //下面的语句输出10个 字宽,因为被重设置了,但将会输出5
    cout << cout.width(10) << endl;
    cout << ch << endl;
    //正常输出
    cout << ch << endl;
    //set 10
    //len is 10
    cout.width(10);
    int len = cout.width();
    //下面的输出字宽,并且返回默认值0 即return 0,并且输出一项后字宽重新 被设置为0
    cout << "len is " << len << endl;\
    //***len*is* 10 个字宽
    //<< len 正常输出
    cout << ch;
    return 0;
}

运行结果


 3.填充字符

cout.fill(char);

注意:这个设置永久有效

    cout.fill('*');
    cout.width(10);
    int a =10;
    cout << a << endl << a;

运行结果


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

cout.precision(int);

示例

    double val = 3.14159;
    cout.precision(4);
    cout << val;
    cout << endl << val;

注意:新的精度设置是永久有效(如果不人为改变),在进行截断时会进行四舍五入 

运行结果


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

显示末尾小数点,即使位数够了,也显示 .

所以,可能会有这样的显示 : 20 . 

这个类方法是ios_base中提供的,并且接受一个参数

ios_base::showpoint 是ios_base中定义的一个静态常量,可用于修改设置

同时ios_base还提供了多个类似常量,因此使用常量时用作用域解析符号 : :

cout.setf(ios_base::showpoint);
    double a = 2.00;
    cout << a << endl;
    cout.setf(ios_base::showpoint);
    cout << a;

运行结果

  

可以看一看showpoint的具体值

cout << "ios_base::showpoint value is " << ios_base::showpoint;

运行结果

 但不可直接传入1024作为参数


6.setf函数

应该知道的一些东西

ios_base类有一个保护的数据成员,以位为单位,分别控制着格式的各个方面(把他称作标记),参数是一个fmtflags值,指出要设置哪一位。返回值是类型为fmtflags的数字,指出所有标记以前的设置。如果打算以后恢复格式,则可以保存这个值。

函数原型其一

fmtflags setf(fmtflags);

其中,fmtflags是bitmask(比特位掩码)的一个别名,通过这个命名就可以隐约的感觉出来实现原理——基于掩码

ps:掩码是位运算中的重要知识点

如何传入参数?

如果要设置11位为1,则传入一个bit位11位为1的数字。

这样的传参太令人头疼,但C++已经帮我们实现了

上面的1024转换为2进制数字是什么样子的呢

 10000000000  //二进制表示

当作掩码改变第一位 


一些其他的格式常量

注意:都是永久设置

    ios_base::boolalpha;    //输出bool值,即true和false
    ios_base::showbase;     //对于输出使用C++基数前缀 (0, 0x)
    ios_base::showpoint;    //显示小数点和小数点后面的位数
    ios_base::uppercase;    //对于16进制输出,使用大写字母,用E法表示
    ios_base::showpos;      //在正数前面加上+

想要恢复设置可用调用函数unsetf()函数,恢复其中的某一个设置 


演示

#include <iostream>
using namespace std;

int main()
{

    bool p = false;
    const int a = 114514;

    cout.setf(ios_base::boolalpha);
    cout.setf(ios_base::showbase);
    cout << "p(bool) is " << p << endl;

    cout << hex << a << endl;

    double b = 30.45;
    cout.precision(2);
    cout.setf(ios_base::showpoint);

    cout << b << endl;

    cout.setf(ios_base::uppercase);
    cout << hex << a << endl;

    cout.setf(ios_base::showpos);
    cout << dec << a << endl;
    return 0;
}

运行结果


 第二个setf()接受两个参数,并返回以前设置的值。

//函数原型
fmtflags setf(fmtflags,mask/*fmtflags*/);

第一个参数的作用与上面的版本相同,第二个参数是一个掩码,表示要清除哪些位。

可以这样使用

#include <iostream>
using namespace std;
int main()
{
    ios_base::fmtflags temp;
    bool p = false;
    const int a = 114514;

    cout.setf(ios_base::boolalpha);
    cout.setf(ios_base::showbase);
    cout << "p(bool) is " << p << endl;

    cout << hex << a << endl;

    double b = 30.45;
    cout.precision(2);
    //关闭上面开启的码位
    temp = cout.setf(ios_base::showpoint);

    cout << b << endl;

    cout.setf(ios_base::uppercase,temp);
    cout << hex << a << endl;

    cout.setf(ios_base::showpos);
    cout << dec << a << endl;
    return 0;
}

运行结果


或者也可以恢复到之前的设置

cout.setf(temp,ios_base::adjustfidld);

调用函数unsetf()恢复设置

#include <iostream>
using namespace std;
int main()
{
    bool p = false;
    cout.setf(ios_base::boolalpha);
    cout << "before p is " << p << endl;
    cout.unsetf(ios_base::boolalpha);
    cout << "after p is " << p;
}

 运行结果


                                  C++提供了一些参数

setf()重载版本的一些参数
第二个参数第一个参数含   义
ios_base::dec十进制
ios_base::basefieldios_base::oct八进制
ios_base::hex十六进制
ios_base::fixed定点计数
ios_base::floatfieldios_base::scientific科学计数
ios_base::left左对齐
ios_base::adjustfieldios_base::right右对齐
ios_base::internal

符号/基数前缀左对齐

值又对齐


8.头文件iomanip

提供了一些控制符

例如

setprecision(int);
setfill(char);
setw(int);

见名知义了,不解释。

可以和cout连用

演示

#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
    double a =20.4055;
    cout << setprecision(4) << setfill('#') << setw(10);
    cout << a;
}

运行结果

注意: setprecision(int)函数在位数够的情况下不会显示小数点,在小数点后全是0的情况即使规定也不会去做

double a =20.0045;
cout << setprecision(4) << a;

运行结果


想要显示全用fixed 

int main()
{
    double a =20.00000;
    cout << setprecision(4) << a << endl;
    cout << fixed << a;
}

 运行结果



9.小数点后几位的表示

浮点是数的含义取决于输出模式。在默认的情况下,它指的是显示的总位数。在定点模式和科学计数模式下,精度指的是小数点后面的位数

原理:使用定点计数法再限制浮点数显示精度即可


法一

    double a =20.4055;
    cout.precision(4);
    cout << fixed  << a;

法二

    cout << fixed << setprecision(4) << a;

还有很多种方法,可以遵循上述原理排列组合。


cin

cin将对象标准输入表示位字节流。通常情况下,通过键盘来生成这种字符流。cin 根据对象接收值的变量的类型,使用其类方法将字符序列转换为所需的类型。

简单的来说,从键盘输入的全看作字符流,也可以理解为字符串,聪明的cin 用他的类方法来判断要把这些数据流转换为什么类型。

int a;
cin >> a;

 例如,我们从键盘输入114514,我们以为输入了一个整形,实际上只是把键盘的输入信息添加到输入流中而已,这种流的本质是字符流。


C++重载了>>(更名为抽取运算符),使之能够识别下面的基本类型

unsigned char &
signed char &
char &
short &
unsigned short &
int &
unsigned int &
long &
unsigned long &
long long  & //C++ 11
unsigned long long & //C++ 11
float &
double &
long double &

比如重载了int的原型

istream & operator >>(int &);

这样处理的不是数据的副本,而是本身,并可以修改其值,正对应可以用cin修改一个变量。

返回istream & 能使cin 链式调用

int a,b;
cin >> a >> b;

可以将hex,oct,dec控制符与cin一起使用,来指定以几进制读取数据

using namespace std;
int main()
{
    int a;
    cin >> hex >> a;
    cout << a;
}

运行结果


 cin与指针

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

signed char *
char *
unsigned char *

使得cin能用字符指针读取字符串


演示

int main()
{
    char str[10];
    cin >> str;
    cout << str;
}

运行结果


cin如何检查输入

cin查看输入流的方式是:逃过空白(空格,回车,制表符等),直到遇到非空白字符,再从输入流中抽取。

>>运算符将读取一个指定类型的数据。也就是说,他读取从非空白字符开始,到与目标类型不匹配的第一个字符之间的全部内容。

如果输入类型不匹配,会返回0,于是可以有这种语法

while(cin >> a);

读取到不符合a数据类型的数据将跳出循环。


流状态,对输入流的进一步探索

cin或者cout对象包含一个描述流状态的数据成员(就是一个变量)(从ios_base继承而来),流状态由3个ios_base元素组成:其中每个元素都是一位

eofbit

cin读取到文件结尾的时候,将设置eofbit

badbit

在一些无法诊断的失败破坏流时,设置badbit

failbit

未能读取到预期字符时,或者I/O失败,设置failbit

上述3个位全部为0的时候,表示输入顺利。

可以看一看他们对应的掩码

    cout << ios_base::badbit << ios_base::eofbit << ios_base::failbit;

运行结果

分别对应的二进制

ios_base::badbit         mask: 001

ios_base::eofbit          mask: 010

ios_base::failbit           mask: 100


可以检查流状态,并利用这些信息进行一些程序处理

成员描述
eofbit如果到达文件结尾,设置为1
badbit如果流被破坏,设置为1.例如文件损坏
failbit如果输入未能读取预期的字符或者输出操着没有写入预期的字符,设置为1
goodbit另一种表示流的方法
good()如果流可以使用( 000 ),则返回true
eof()如果eofbit被设置,返回true
bad()如果badbit被设置,返回true
fail()如果failbit被设置,返回true
rdstate()返回流状态
exceptions()返回一个位掩码,指出哪些标记导致异常
exceptions(iostate ex)

设置哪些状态将导致clear()引发异常;例如,如果ex是eofbit,并且eofbit已经被设置

则将会引发异常

clear(iostate s)流状态设置为s,default = goodbit,并将其他位清除
setstate(iostate s)调用( clear(rdstate|s) )。单独设置s对应的位,其余位不变

1.设置状态

下面的代码将会使流状态重新设置

cin.clear(); //default argument is goodbit

或者用上面介绍的方法设置某一个位

为什么需要重新设置流状态呢?
最常见的理由是,输入不匹配或者达到文件结尾时,重新打开输入流,因为这时候一些位已经被设置,会影响流的正常读取。当然,具体的情况具体分析。


演示

int main()
{
    int a;
    int n = 5;
    int sum = 0;
    while(n--)
    {
        cin >> a;
        if(!cin.good())
        {
            cout << "输入流错误\n";
            //break;
        }else
            sum += a;
        cout << "sum is " << sum << endl;
    }
    cin >> a;
}

运行结果

可以看见,因为输入流错误,不能再进行输入,只能等待循环结束,并且,循环外的输入语句也没有执行。可用clear()重新设置输入流,但还是需要一些处理,下面会说到。


I/O与异常

假设某个输入函数设置了eofbit,这是否会引发程序异常呢?

在默认情况下,这是不行的,因为exceptions(iostate ex)其中的ex默认位goodbit

如果更改了设置,使之引发了异常,则将引发ios_base::failure,从srd::expction类派生而来,因此有一个what()方法。

int main()
{
    int sum = 0;
    int a;
    cin.exceptions(ios_base::failbit);
    try{
        while(cin >> a)
        {
            sum += a;
        }
    } catch (ios_base::failure & bf) {
        cout << bf.what() << endl;
    }
    cout << "sum is " << sum;
}

运行结果


流状态的影响

 对于读入数据类型错误的情况,输入流被设置了failbit,理应用clear()重置。

using namespace std;
int main()
{
    int a;
    int n = 5;
    int sum = 0;
    while(n--)
    {
        cin >> a;
        if(!cin.good())
        {
            cout << "输入流错误,已经重新设置\n";
            cin.clear();
/*            while(!isspace(cin.get()))
            {
                continue;
            }*/
        }else
            sum += a;
        cout << "sum is " << sum << endl;
    }
    cin >> a;
}

运行结果


这是为什么呢?
原因是e被一直留在了输入流中

执行第一次的显示错误消息后重新进行cin>>a,此时读取到的还是e,因为类型不同,e一直没被读取,就一直进行上述循环。

将代码中的注释打开,读取到空白字符,即可重新输入。

运行结果

  


其他的istream类方法

1.单字符输入

get(char &)             //返回一个istream & ,因此可以递归使用

get(void)                 //读取字符并且返回其对应的整形,不能递归使用

不跳过空白的字符读入

 顺便一说,下面的代码将永远不能跳出循环,因为cin忽略输入流中的空白字符

    char ch;
    cin >> ch;
    while(ch != '\n')
    {
        //do something
    }

但下面的代码可以,cin.get(ch)不跳过空白字符

    char ch;
    while(cin.get(ch)&&ch!='\n')
    {
        //do something
    }

可用这样的形式读取

cin.get(ch).get();

将会丢弃第二个输入字符


演示

using namespace std;
int main()
{
    string str;
    char ch;
    int i=0;
    while(cin.get(ch).get()&&ch!='\n')
    {
        str += ch;
    }
    cout << str;
}

运行结果 


cin.get(void)达到文件结尾反回EOF,因此可以这样使用

    while(ch = cin.get() != EOF)
    {
        //do something
    }

2.读入单个字符的选择

如果要读入菜单选项等,使用跳过空白符号的cin>> 更加合适

如果希望检查输入流中的每一个字符,应该使用get()函数的两个版本

其中get(ch)的接口更佳,get(void)的优点是方便C程序到C++程序的转换。


3.字符串读入

四个函数

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

istream & get(char *, int);

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

istream & getline(char *, int);

第一个参数是用于存放字符串的地址

第二个参数是表示读取的长度+1(存放 '\0' )

第三个参数用于表示分界符

上述函数均在读取最大数目的字符或遇到换行符后为止

 get()和getline()的主要区别是,前者将换行符留在输入流中,而后者从输入流中读取换行并且舍弃

ignoer()成员函数

该函数接受两个参数:一个是数字,指定要读取的最大字符数,另一个参数是字符,用作分界符号。

函数功能,丢且接下来输入的字符长度为第一个参数,直到遇到分界符或者读满。

 演示


using namespace std;
int main()
{
    string str;
    char str1[12];
    char ch;
    //cin.get(str,12) //error str isn't a char
    cout << "use cin.get input str1" << endl;
    cin.get(str1,12);
    cout << "now str1 is " << str1 << endl;
    ch = cin.get();
    if(ch == '\n')
        cout << "next char is \\n" << endl;
    cout << "use function ignore(10,'\\n')" << endl;
    cin.ignore(10,'\n');
    cout << "use cin.getline input str1" << endl;
    cin.getline(str1,12);
    cout << "now str1 is " << str1 << endl;
    cin.get(ch);
    if(ch == '\n')
        cout << "next char is\\n";
    else
        cout << "next char not is \\n";
    return 0;
}

一些运行结果


use cin.get input str1
hello world
now str1 is hello world
next char is \n
use function ignore(10,'\n')
1234567890
use cin.getline input str1
now str1 is /*\n*/

next char is \n

 上面的空行是str1本身是回车输出回车,我们又输入了一个回车的结果


字符串输入错误

getline()和get()与其他输入函数一样,遇到文件结尾时将设置eofbit,流被破坏设置badbit。

无输入或者输入超限的情况下将设置failbit

其中,如果不能从流中抽取字符则把空字符放在输入字符串中,并使用setstate设置failbit。

不能从流中抽取字符的情况下有两种

第一种是到达了文件尾

对于get(char *, int)来说(不是getline()),另一种可能是读入了空行

 对上面的补充

getline()读入了空行也不会设置failbit,而是读入换行符并将其舍弃


演示

    char str1[12];
    cin.getline(str1,5);
    cout << str1 << endl;
    cin >> str1;

 运行结果

 可以看见,不能再进行读取了


    char ch;
    char str1[12];
    cin.get(str1,10);
    cout << "str1 is " << str1 << endl;
    cin.get(ch);
    cout << "ch is " << ch << endl;

运行结果

 可以看见str1没有读入换行,ch也是乱码


如果想让getline()读入空行结束,可以这样书写

    char str1[12];
    while(cin.getline(str1,12)&&str1[0]!='\0')
    {
        cout << str1 << endl;
    }

运行结果

 注意输入时不要超过getline中的参数限制

其他的istrream类方法


1.read()

读取指定数目的字节,不会再末尾加上空字符'\0',不是设计专门用来键盘读入的,常与ostream wrtie组合使用来读写文件,反回istream &,可链式调用


2.peek()

反回输入流中的下一个字符,但不抽取

演示

    char ch;
    char str[10];
    int i= 0;
    while((ch = cin.peek()) != '.' && ch!='\n')
        str[i++] = ch = cin.get();
    cout << "str is " << str << endl;
    cin.get(ch);
    cout << "next char is " << ch;

运行结果


3.gcount()

返回最后一个非格式化抽取方法(不是cin>>)读取的字符数。

演示

    char ch;
    char str[10];
    cin >> ch;
    while(ch!='\n')
    {
        cin.get(ch);
    }
    cin.getline(str,10);
    cout << cin.gcount();

 运行结果

为什么是7个呢

因为换行符也是一个字符


4.putback()

将一个字符头差插到输入流中 ,反回istream &

演示

    char str[10];
    cin.putback('1');
    cin >> str;
    cout << str;

运行结果

  • 11
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值