我们都知道cin可以读取输入的字符,cout可以向控制台输出一段字符,这没什么好讲的。看下面的程序
#include <iostream>
using namespace std;
int main() {
int a = 100;
cin >> a;
cout << a <<endl;
}
但是cin和cout作为c++的标准库,有一些其他的东西可以说一说。
一、十六进制输出和八进制输出
// 十六进制输出
cout << hex ;
cout << a << endl;
// 八进制输出
cout << oct ;
cout << a << endl;
二、cin的getline()和get()
cin.getline()可以直接读取一行的数据,但是,这一行的数据不包含最后的 \n
换行符,这个换行符会被读取然后丢弃
cin.get()也可以读取一行的数据,他们之间的区别是,get()遇到换行符就停下了,但是换行符还在输入队列中,如果下次还读取的话,直接会读取到换行符。
#include <iostream>
using namespace std;
int main() {
const int char_array_size = 100;
char *p1 = new char[char_array_size];
char *p2 = new char[char_array_size];
cin.getline(p1, char_array_size);
cin.getline(p2, char_array_size);
cout << p1 << endl;
cout << p2 << endl;
}
针对get()的特性,我们是这么写程序的
#include <iostream>
using namespace std;
int main() {
const int char_array_size = 100;
char *p1 = new char[char_array_size];
char *p2 = new char[char_array_size];
cin.get(p1, char_array_size);
cin.get();
cin.get(p2, char_array_size);
cout << p1 << endl;
cout << p2 << endl;
}
那为什么不使用getline()呢,明显的方便啊,主要原因是历史版本遗留问题,老版本中没有getline,还有检查的粒度问题,我们怎么知道是数组满了导致的读入停止,还是读到了换行符导致的停止呢,get可以,读到下一个字符是换行符的时候停止。
这在一种问题中还是蛮重要的,比如下面的程序
#include <iostream>
using namespace std;
int main() {
int a = 100;
char *b = new char[100];
cin >> a;
cin.getline(b,100);
cout << a << endl;
cout << b << endl;
}
我们是没有机会输入b的,为什么呢,我们输入a后留下的换行符被保存在了控制台,getline读取的时候直接读取到了换行符,他会以为这是一个空字符串,直接返回。我们可以在getline()前面加一个get()。
上面都是对于c风格的字符串,下面我们来讨论一下对于string类的字符串
对于string对象的读入有两种方式
char info[100];
string buffer;
cin >> buffer;
getline(cin,buffer);
两个版本的getline都有一个可选参数,用于指定使用哪个字符来确定输入边界
cin.getline(info,100,':');
getline(buffer,':')
我们可以发现读取c风格字符串是istream类方法,而string版本是独立函数。
string输入函数有两个版本,我们知道string可以自动调整大小使得字符串长度与输入匹配,但是也存在一些限制,第一个限制是string对象的最大允许长度,有常量string::npos指定,这通常是unsigned int值,因此对于普通文本交互不会有问题,但是如果您试图将整个文件内容读取到单个string对象中,这可能成为限制因素,第二个限制是内存容量的限制。
string版本的getline()函数从输入中读取字符,并将其存储到目标string中,直到发生下面三种情况
- 到达文件尾,这种情况下,输入流的eofbit将被设置,这意味着方法fail()和eof()都将返回true
- 遇到分界字符(默认为‘\n’),在这种情况下,将吧分界字符从输入流中删除,但不存储他
- 读取的字符达到最大允许值,(string::npos和可供分配内存字节数中较小的一个),将设置输入流的failbit,这意味着方法fail()将返回true
输入流对象有一个统计系统,用于跟踪流的错误状态。在这个系统中,检测到文件尾后将设置eofbit寄存器,检测到输入错误时将设置failbit寄存器,出现无法识别的故障(如硬盘故障)时将设置badbit寄存器,一切顺利时将设置goodbit寄存器。
string版本的operator>>()函数的行为与此类似,只是它不断读取,直到遇到空白字符并将其留在输入队列中,而不是不断读取,直到遇到分界字符并将其丢弃。空白字符指的是空格、换行符和制表符,更普遍地说,是任何将其作为参数来调用 isspace()时,该函数返回ture的字符。
由于用于string对象的输入函数使用输入流,能够识别文件尾,因此也可以使用他们从文件中读取输入
int main() {
std::ifstream fin;
fin.open("./test.txt");
if (!fin.is_open()) {
cout << "test can not be open" << endl;
exit(EXIT_FAILURE);
}
string item;
int count = 0;
getline(fin,item,':');
while(fin) {
++ count;
cout << count << ":" << item << endl;
getline(fin,item,':');
}
fin.close();
}
上面我们介绍了cin,cout这些基础用法,下面我们从C++的角度看一下流是怎么工作的。
三、流和缓冲区
C++程序把输入和输出看作字节流。输入时,程序从输入流中抽取字节,输出时,程序将字节插入到输出流中。对于面向文本的程序,每个字节代表一个字符,更通俗地说,字节可以构成字符或数值数据的二进制表示。输入流中的字节可能来自键盘,也可能来自存储设备(如硬盘)或其他程序。同样,输出流中的字节可以流向屏幕、打印机、存储设备或其他程序。流充当了程序和流源或流目标之间的桥梁。这使得C++程序可以以相同的方式对待来自键盘的输入和来自文件的输入。C++程序只是检查字节流,而不需要知道字节来自何方。同理,通过使用流,C++程序处理输出的方式将独立于其去向。因此管理输入包含两步:
- 将流与输入去向的程序关联起来
- 将流与文件链接起来
通常,通过使用缓冲区可以更高效地处理输入和输出。缓冲区是用作中介的内在块,它是将信息从设备传输到程序或从程序传输给设备的临时存储工具。通常,像磁盘驱动器这样的设备以512字节(或更多)的块为单位来传输信息,而程序通常每次只能处理一个字节的信息。缓冲区帮助匹配这两种不同的信息传输速率。例如,假设程序要计算记录在硬盘文件中的金额。程序可以从文件中读取一个字符,处理它,再从文件中读取下一个字符,再处理,依此类推。从磁盘文件中每次读取一个字符需要大量的硬件活动,速度非常慢。缓冲方法则从磁盘上读取大量信息,将这些信息存储在缓冲区中,然后每次从缓冲区里读取一个字节。因为从内存中读取单个字节的速度比从硬盘上读取快很多,所以这种方法更快,也更方便。当然,到达缓冲区尾部后,程序将从磁盘上读取另一块数据。这种原理与水库在暴风雨中收集几兆加仑流量的水,然后以比较文明的速度给您家里供水是一样的。输出时,程序首先填满缓冲区。
键盘输入每次提供一个字符,因此在这种情况下,程序无需缓冲区来帮助匹配不同的数据传输速率。然而,对键盘输入进行缓冲可以让用户在将输入传输给程序之前返回并更正,C++程序通常在用户按下回车键时刷新输入缓冲区。对于屏幕输出,C++程序通常在用户发送换行符时刷新输出缓冲区。程序也可能会在其他情况下刷新输入,例如输入即将到来时,这取决于实现。也就是说,当程序到达输入语句时,它将刷新输出缓冲区中当前所存的输出,与ANSI C一致的C++实现是这样工作的。
管理流和缓冲区的工作有点复杂,但iostream (以前为iostream.h)文件中包含一些专门设计用来实现、管理流和缓冲区,C++98定义了一些类模板以支持char和wchar_t数据;C++11添加了char16_t和char32_t具体化。
- streambuf类为缓冲区提供了内存,并提供了用于填充缓冲区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法;
- ios_base类表示流的一般特征,如是否可读取、是二进制流还是文本流等;ios类基于ios_base,其中包括了一个指向streambuf对象的指针成员;
- ostream类是从ios类派生而来的,提供了输出方法;
- istream类也是从ios类派生而来的,提供了输入方法;
- iostream类是基于istream和 ostream类的,因此继承了输入方法和输出方法。
要使用这些工具,必须使用适当的类对象,例如,使用ostreeam对象,来处理输出,创建这样的对象打开一个流,自动创建缓冲区,并将其与流关联起来,同时使得能够使用类成员函数。
C++的 iostream类库管理了很多细节。例如,在程序中包含iostream文件将自动创建8个流对象((4个用于窄字符流,4个用于宽字符流)。
- cin对象对应于标准输入流。在默认情况下,这个流被关联到标准输入设备(通常为键盘)。wcin对象与此类似,但处理的是wchar_t类型。
- cout 标准输出流
- cerr 标准错误流
- clog 标准错误流,但是输出为日志
3.1、重定向
标准输入和输出流通常连接着键盘和屏幕。但很多操作系统(包括UNIX、Linux和 Windows)都支持重定问,这个工具使得能够改变标准输入和标准输出。例如,假设有一个名为counter.exe 的、可执行的Windows命令提示符C程序,它能够计算输入中的字符数,并报告结果(在大多数 Windows 系统中,可以选择“开始”>“程序”,再单击“命令提示符”来打开命令提示符窗口)。
3.2、简单的文件IO
我们经常用的一个库是iostream,这个库的cin和cout可以从标准输入读取流,也可以向标准输出写入流,这两个实际上是两个操作系统操作,在linux操作系统中,标准输入,标准输出,标准错误分别对应0,1,2号文件,linux万物皆文件嘛。
要是我们想要从文件中读取和写入数据呢,这就要用到另一个标准库了,fstream,它定义了三个新的数据类型。
数据类型 | 描述 |
---|---|
ofstream | 该数据类型表示输出文件流,用于创建文件并向文件写入信息。 |
ifstream | 该数据类型表示输入文件流,用于从文件读取信息。 |
fstream | 该数据类型通常表示文件流,且同时具有 ofstream 和 ifstream 两种功能,这意味着它可以创建文件,向文件写入信息,从文件读取信息。 |
要在 C++ 中进行文件处理,必须在 C++ 源代码文件中包含头文件 和 。
3.2.1、打开文件
在从文件读取信息或者向文件写入信息之前,必须先打开文件。ofstream 和 fstream 对象都可以用来打开文件进行写操作,如果只需要打开文件进行读操作,则使用 ifstream 对象。
下面是 open() 函数的标准语法,open() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。
void open(const char *filename, ios::openmode mode);
在这里,open() 成员函数的第一参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式。
模式标志 | 描述 |
---|---|
ios::app | 追加模式。所有写入都追加到文件末尾。 |
ios::ate | 文件打开后定位到文件末尾。 |
ios::in | 打开文件用于读取。 |
ios::out | 打开文件用于写入。 |
ios::trunc | 如果该文件已经存在,其内容将在打开文件之前被截断,即把文件长度设为 0。 |
您可以把以上两种或两种以上的模式结合使用。例如,如果您想要以写入模式打开文件,并希望截断文件,以防文件已存在,那么您可以使用下面的语法:
ofstream outfile;
outfile.open("file.dat", ios::out | ios::trunc );
类似地,您如果想要打开一个文件用于读写,可以使用下面的语法:
ifstream afile;
afile.open("file.dat", ios::out | ios::in );
3.2.2、关闭文件
当 C++ 程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。但程序员应该养成一个好习惯,在程序终止前关闭所有打开的文件。
下面是 close() 函数的标准语法,close() 函数是 fstream、ifstream 和 ofstream 对象的一个成员。
void close();
3.2.3、写入文件
在 C++ 编程中,我们使用流插入运算符( << )向文件写入信息,就像使用该运算符输出信息到屏幕上一样。唯一不同的是,在这里您使用的是 ofstream 或 fstream 对象,而不是 cout 对象。
3.2.4、读取文件
在 C++ 编程中,我们使用流提取运算符( >> )从文件读取信息,就像使用该运算符从键盘输入信息一样。唯一不同的是,在这里您使用的是 ifstream 或 fstream 对象,而不是 cin 对象。
3.2.5、实例
#include <fstream>
#include <iostream>
using namespace std;
int main ()
{
char data[100];
// 以写模式打开文件
ofstream outfile;
outfile.open("afile.dat");
cout << "Writing to the file" << endl;
cout << "Enter your name: ";
cin.getline(data, 100);
// 向文件写入用户输入的数据
outfile << data << endl;
cout << "Enter your age: ";
cin >> data;
cin.ignore();
// 再次向文件写入用户输入的数据
outfile << data << endl;
// 关闭打开的文件
outfile.close();
// 以读模式打开文件
ifstream infile;
infile.open("afile.dat");
cout << "Reading from the file" << endl;
infile >> data;
// 在屏幕上写入数据
cout << data << endl;
// 再次从文件读取数据,并显示它
infile >> data;
cout << data << endl;
// 关闭打开的文件
infile.close();
return 0;
}
3.2.6、文件位置指针
istream 和 ostream 都提供了用于重新定位文件位置指针的成员函数。这些成员函数包括关于 istream 的 seekg(“seek get”)和关于 ostream 的 seekp(“seek put”)。
seekg 和 seekp 的参数通常是一个长整型。第二个参数可以用于指定查找方向。查找方向可以是 ios::beg(默认的,从流的开头开始定位),也可以是 ios::cur(从流的当前位置开始定位),也可以是 ios::end(从流的末尾开始定位)。
文件位置指针是一个整数值,指定了从文件的起始位置到指针所在位置的字节数。下面是关于定位 “get” 文件位置指针的实例:
// 定位到 fileObject 的第 n 个字节(假设是 ios::beg)
fileObject.seekg( n );
// 把文件的读指针从 fileObject 当前位置向后移 n 个字节
fileObject.seekg( n, ios::cur );
// 把文件的读指针从 fileObject 末尾往回移 n 个字节
fileObject.seekg( n, ios::end );
// 定位到 fileObject 的末尾
fileObject.seekg( 0, ios::end );
3.2.7、二进制文件
使用二进制文件模式时,程序将数据从内存传输给文件(反之亦然)时,将不会发生任何隐藏的转换,而默认的文本模式并非如此。例如,对于Windows文本文件,它们使用两个字符的组合(回车和换行)表示换行符;Macintosh文本文件使用回车来表示换行符;而 UNIX和Linux文件使用换行( linefeed )来表示换行符。C++是从UNIX系统上发展而来的,因此也使用换行( linefeed)来表示换行符。为增加可移植性,WindowsC++程序在写文本模式文件时,自动将C++换行符转换为回车和换行;Macintosh C++程序在写文件时,将换行符转换为回车。在读取文本文件时,这些程序将本地换行符转换为C++格式。对于二进制数据,文本格式会引起问题,因此 double值中间的字节可能与换行符的ASCII码有相同的位模式。另外,在文件尾的检测方式也有区别。因此以二进制格式保存数据时,应使用二进制文件模式(UNIX系统只有一种文件模式,因此对于它来说,二进制模式和文本模式是一样的)。