《C++ Primer Plus》学习笔记 — 输入、输出和文件
一、概述
1、流和缓冲区
C++把输入和输出看做字节流。字节可以构成字符或数值数据的二进制表示。C++程序只是检查字节流,而不需要知道字节来自何方,去往何处。这就是说,我们可以独立地进行流输入、流处理以及流输出。下面列出了管理和操作流的步骤:
我们使用缓冲区进行高效地输入输出处理。通常,内存是以块来进行数据读取和写入的,而程序是以字节为单位进行数据处理的。缓冲区可以帮助我们匹配二者的速度。
2、iostream继承体系
C++的I/O解决方案不同于C,是基于类(iostream)所实现的。下面是其继承体系:
这些类提供了对输入输出字节流的操作,其中:
ios_base类提供了流的一般特征,如格式化,流状态,本地化等。
basic_ios类是一个模板类,模板参数为字符类及其特征类。该类通过包含一个指向basic_streambuf对象的指针提供了使用缓冲区处理流的功能。basic_streambuf的工作方式如下:
basic_ostream通过重载 << 运算符提供了输出不同类型数据的方法。
basic_istream通过重载 >> 运算符提供了输入不同类型数据的方法。
basic_iostream同时支持输入和输出操作。
其余类继承了上面所说的三个基类,是针对文件和字符串输入输出流的特化处理。
3、iostream流对象
上面我们说过使用字节流处理文件的步骤。C++的iostream类库为我们自动创建了8个流对象:
cin用于处理char类型的输入。默认情况下被关联到标准输入,通常为键盘。
cout用于处理char类型的输出。默认情况下被关联到标准输出,通常为显示器。
cerr用于处理char类型的错误显示。默认情况下被关联到标准输出,通常为显示器。需要注意的是这个流没有缓冲区,因此打印的错误信息将会直接被推送给屏幕。
clog同样用于处理char类型的错误显示。默认情况下被关联到标准输出,通常为显示器。这个流有缓冲区。
wcin、wcout、wcerr、wclog分别为上述对象针对于宽字节的流处理对象。
那么流对象与标准输入输出是怎样绑定的呢?
以gcc代码作为参考:
new (&buf_cout_sync) stdio_sync_filebuf<char>(stdout);
new (&buf_cin_sync) stdio_sync_filebuf<char>(stdin);
new (&buf_cerr_sync) stdio_sync_filebuf<char>(stderr);
// The standard streams are constructed once only and never
// destroyed.
new (&cout) ostream(&buf_cout_sync);
new (&cin) istream(&buf_cin_sync);
new (&cerr) ostream(&buf_cerr_sync);
new (&clog) ostream(&buf_cerr_sync);
cin.tie(&cout);
cerr.setf(ios_base::unitbuf);
// _GLIBCXX_RESOLVE_LIB_DEFECTS
// 455. cerr::tie() and wcerr::tie() are overspecified.
cerr.tie(&cout);
我们可以看出cout与标准输出的绑定主要是通过系统接口将缓冲区与标准输出绑定。
4、重定向
我们在启动程序时可以通过命令行参数重定向标准输入输出,但是这种重定向的输出并不影响cerr和clog:
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
int totalLength = 0;
string s;
while (cin >> s)
{
totalLength += s.length();
}
cout << totalLength << endl;
}
我们使用程序可执行文件名 > 输出文件 < 输入文件名可以将输入文件中的字符数输出到输出文件中。在此过程中,屏幕中将不会得到任何输出。
二、使用cout进行输出
与执行位运算的 << 操作符不同,ostream重载的 << 操作符被称为插入运算符。
1、用于输出数字类型的插入运算符重载
以uint为例:
basic_ostream& __CLR_OR_THIS_CALL operator<<(unsigned int _Val) {
// insert an unsigned int
ios_base::iostate _State = ios_base::goodbit;
const sentry _Ok(*this);
if (_Ok) {
// state okay, use facet to insert
const _Nput& _Nput_fac = _STD use_facet<_Nput>(this->getloc());
_TRY_IO_BEGIN
if (_Nput_fac.put(_Iter(_Myios::rdbuf()), *this, _Myios::fill(), static_cast<unsigned long>(_Val))
.failed()) {
_State |= ios_base::badbit;
}
_CATCH_IO_END
}
_Myios::setstate(_State);
return *this;
}
(1)sentry 和 tie
sentry是ostream类的一个公有内部类:
explicit __CLR_OR_THIS_CALL sentry(basic_ostream& _Ostr) : _Sentry_base(_Ostr) {
if (!_Ostr.good()) {
_Ok = false;
return;
}
const auto _Tied = _Ostr.tie();
if (!_Tied || _Tied == &_Ostr) {
_Ok = true;
return;
}
_Tied->flush();
_Ok = _Ostr.good(); // store test only after flushing tie
}
这个类的作用是刷新缓冲区。tie方法是用于给当前流对象(cout)设置或获取一个输出流对象(当二者的缓冲区管理的字符序列需要同步时,例如cin和cout),以保证当前流对象输入或输出之前,被管理的输出流对象缓冲区将会被刷新。我们以文件流举个例子(来源C++参考手册):
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
ofstream os("test.txt");
ifstream is("test.txt");
string value("0");
os << "Hello";
is >> value;
cout << "Result before tie(): \"" << value << "\"\n";
is.clear();
is.tie(&os);
is >> value;
cout << "Result after tie(): \"" << value << "\"\n";
}
我们简单解释下这段代码(这里test.txt文件一开始并不存在):首先我们使用文件输出流输出了hello。然而,文件输出流默认有缓冲区,只有遇到指定字符(默认为换行),才会刷新缓冲区。因此我们第一次从文件输入流中并不会得到任何数据。所以第一次我们打印的value为其初值。然后我们调用tie方法将文件输出流绑定到输入流上。这时我们再从文件输出流向value变量中输入字符时,根据我们前面的解释,会先调用绑定的输出流的flush方法。因此文件输出流的hello字符串就进入文件中了。所以我们第二次会将文件中的hello设置到value变量中。
(2)use_facet 和 locale
use_facet是一个模板方法,用于获取指定locale的某一组成部分:
template< class Facet >
const Facet& use_facet( const std::locale& loc )
{
...
const size_t _Id = _Facet::id;
const locale::facet* _Pf = _Loc._Getfacet(_Id);
...
return static_cast<const _Facet&>(*_Pf); // should be dynamic_cast
}
这里是由模板参数的成员变量id找到对应的部分。我们输出数字时获取到的facet就是num_put类。关于id的宏定义如下:
// xlocalinfo.h
#define _X_ALL LC_ALL // 0
#define _X_COLLATE LC_COLLATE // 1
#define _X_CTYPE LC_CTYPE // 2
#define _X_MONETARY LC_MONETARY // 3