ストリームとは、C 言語での入出力用関数 printf、scanf などの代替という意味もあるのですが、もし cout、cin、cerr しか使っていないのであれば、それはかなり損をしています。何故ストリームなのでしょうか。
まずは、簡単なところから始めましょう。
cout、cin、cerr はそれぞれ、標準出力、標準入力、標準エラー出力のストリームの入出力に関連するクラスオブジェクトで、コンソールアプリケーションでは、main 関数に実行が移ったときに既にシステムによって構築され、使用できる状態になっています。これらは、次のように使います。
int main() { cout << "Hello maniac" << endl; // Hello maniac と表示する。 int iValue = 0; cin >> iValue; // iValue を入力する。 cerr << "iValue = " << iValue << endl; // iValue を表示する。 return 0; }
cout、cin、cerr のサンプル CinCoutCerr1.cpp
上記サンプルを実行して、Hello world と表示された後に、キーボードから、例えば 23[ENTER] と入力すると、実行結果は、次のようになります。
Hello maniac 23 iValue = 23
上記のサンプルで、cout には、演算子 << で、文字列や endl(改行)などを挿入しているという事になります。逆に cin の場合には、演算子 >> で int 型の変数 iValue に値を取り出しているということになります。endl は、cout に改行を挿入するマニピュレータと呼ばれるもので、その実体は、関数の名前です。
上記のように、cout、cin などは、ほぼ C 言語での printf、scanf の代わりとして、使用できるようになっています。
しかし、C++ でも、依然として、printf、scanf などの関数は使用可能で、変数の値をうまく整形して出力するという意味では cout よりも printf の方がよほど簡単に使えるように感じられるし、scanf も捨てたものではありません。とすると、ストリームが cout、cin だけを対象としているのであれば、不便なだけで、ほとんど使う意味はないということになります。
注意:C++ のライブラリで用意されているストリームには STL を使わない古い版と STL として用意されている新しい版の二種類があります。ヘッダを使い分ければ今でも両方使えるようですが、両方のヘッダを同時にインクルードするとかなり競合が起こるようです。私は STL 版しか使っていません。このサイトでは STL 版のストリームしか扱いません。
では、ストリームを使うと何が良いのかということになりますが、それは、ストリームオブジェクトを入れ替えることによって、入出力先をうまく切り替えられるという一点に尽きるといえます。
printf、scanf は、標準入出力に対して使用する関数ですが、これに対して、fprintf、fscanf はファイルに対して、sprintf、vsprintf、sscanf はメモリに対して、など、これらの関数を使用する場合、入出力の対象によって関数を使い分ける必要があります。ストリームという少しややこしいものを持ち込むことによって、この方法よりももっとスマートな入出力先の切り替えが実現するわけです。
それでは、ストリームを使って、出力先を切り替えるサンプルを見て見ましょう。Hello maniac! という文字列をストリームに出力する関数を作成してみます。この関数は、出力先がストリーム ostream であるということはコーディングされていますが、その出力が、標準出力であるとか、ファイルであるとかいうことは、決まっていないことに注目してください。
void Output(ostream& ros) { ros << "Hello maniac!" << endl; }
ostream& ros が第一引数になっていますね。これが出力ストリームです。ostream は出力ストリームの基本クラスで、このクラスには、文字列など(その他 int、float など色々)をストリームに挿入するための演算子 << が定義されています。そのため << を使って、文字列 Hello maniac! を挿入出来るわけです。
ros としては、例えば cout をその一例として渡すことが出来ます。cout だけでなく、例えばファイルに出力を行うストリームオブジェクトを作成すれば、それを渡すことが出来ます。早速、やってみましょう。
int main() { Output(cout); // システムが用意した標準出力 cout に出力する。 ofstream ofs("D:\\TestOutput.txt"); // ファイルへの出力ストリームオブジェクト ofs を作成する。 Output(ofs); // ofs を渡して、ファイルに出力する。 return 0; }
ストリームによる出力切り替えのサンプル Ostream1.cpp
上記サンプルを実行すると、コンソールには、次のように表示され、
Hello maniac!
ファイル D:\TestOutput.txt にも、同様の内容が書き込まれます。
このように、出力ストリーム ostream から派生したクラスのオブジェクトを Output に渡すことによって、出力先を切り替えることが出来るわけです。
上記は、標準出力とファイルへの出力の例でしたが、各出力先に対応するストリームオブジェクトが作成できれば、関数 Output を修正することなく、あらゆる出力先に書き込むことが可能となるわけです。printf、fprintf などでは、このようなことは出来ませんね。
ここで、ストリームの動作について、少し確認しておきます。次のようなコード例で考えてみます。
int main() { ofstream ofs("C:\\hello.txt"); ofs << "hello" << "maniac" << endl; return 0; }
何の変哲もないコードですが、この式の左辺 ofs の型は、ofstream ですが、次の "hello" まで実行した後の型、つまり、式 (ofs << "hello") の型は、ostream::operator<< の返す型である ostream& になります。もはや ofstrem ではありません。
これが何を意味するのかというと、一番左の ofs には ofstream の演算子や関数が使えますが、ostream の挿入演算子 << が一つ噛むともはや ofstream の関数や演算子は使えず、ostream のものしか使えなくなるということです。これは、ストリームの挿入、抽出演算子の右側では、派生クラスとしてではなく基本クラスである ostream や istream として動作するということを示しています。これが、入出力先を切り替えてもまったく同様に動作する理由であるし、逆に ostream や istream の派生クラスに自前で定義する関数で意味のあるのは、初期化に関するものだけであるということも示しています。
入出力用ストリームとして標準ライブラリでは、標準入出力、ファイル、メモリ用に次のようなオブジェクトやクラスが用意されています。
これらは、ostream や istream として、そのまま使えばよいだけなので、ここでは、上記以外の入出力先を対象とするストリームオブジェクトの作成方法について解説します。
ストリームオブジェクトの作成には、次の三つの要素(基本クラス)がそれぞれ関連しています。
自作のストリームを作るには、基本的には、上記の基本クラスから派生する最低二つのクラスを作成する必要があります。これらのクラスの役割ですが、 istream、ostream は抽出演算子 >>、挿入演算子 << などに代表される演算子、関数により入出力の形式を支配しており、streambuf は入出力先を支配しています。クラスの関係からいえば、istream、ostream が streambuf を使って、入出力を行うようになっています。
この項では、話を、出力に限定して、streambuf と ostream から派生するクラスを作成してみます。実用性も少し考えて、統合環境のデバグ窓に文字列を出力するクラスを作成してみましょう。具体的には、OutputDebugString() という関数を使用すると、文字列をデバグ窓に出力できるので、これを streambuf で使用できるようにします。
最も簡単に済ませるために、出力のバッファリングは行わずに streambuf のメンバ関数 overflow だけをオーバーライドします。この関数で、与えられた文字を出力に書き込むようにします。本来は、文字列を出力する関数 xsputn もオーバーライドするとより効率は良くなりますが、解説用のサンプルなので、なるべくコードが短くなるように、これは省略します。デフォルトの xsputn は、何もしない(常に失敗する)ので、streambuf は仕方なく、overflow を使って全ての文字を出力してくれます。非効率ではありますが、これでも、完全に正しく動作します。では、サンプルコードを見てください。まず streambuf から派生したクラス CDstreambuf を作成します。D は、デバグ窓に出力するという意味です。
class CDstreambuf : public streambuf { char m_acBuffer[2]; // 出力用の一時バッファ public: CDstreambuf() { ZeroMemory(m_acBuffer, sizeof m_acBuffer); } // バッファをクリアしておく。 virtual int_type overflow(int_type iChar = EOF) { if (iChar != EOF) { m_acBuffer[0] = iChar; // 文字をバッファに書き込んで、 OutputDebugString(m_acBuffer); // デバグ窓に出力する。 } return iChar; // 出力が成功したときには EOF 以外を返す。 } };
上記で、本質的なところは、関数 overflow をオーバーライドして、文字を OutputDebugString で出力しているということだけです。
次に、ostream から派生したクラス CDout を作成します。これの役割は、上記で定義した出力用バッファを ostream のコンストラクタに渡すことです。
class CDout : public ostream { CDstreambuf* m_pdstreambuf; public: ~CDout() { delete m_pdstreambuf; } CDout() : ostream(m_pdstreambuf = new CDstreambuf) {} };
これで、自作の出力ストリーム CDout を使うことができます。つまり、次のように呼び出せます。
int main() { float fValue = 1.23f; CDout() << "hello maniac! fValue = " << fValue << endl; return 0; }
streambuf のサンプル Streambuf1.cpp
左辺の CDout() は、クラス CDout のコンストラクタを呼び出して、CDout オブジェクトを作成しています。オブジェクトが作成できたら、<< 演算子で、文字列や整数などを挿入することが出来るわけです。
上記を、実行すると、意図どおり、デバグ窓に、次のように表示されます。ちょっと、便利ではないですか?
hello maniac! fValue = 1.23
非統合環境では、デバグ窓が無いので、上記の動作を直接的に確認することは出来ませんが、DebugView などのフリーソフトを使えば、確認が可能です。
ストリームを使用していると、その入出力の形式を少し変更したいと思うことが良くあります。例えば、CSV 形式のファイル(Comma Separated Value = コンマで要素が区切られた表)を読み込もうとする場合、抽出演算子 >> で、次のコンマまでの要素を読み込んで、コンマ自体は読み飛ばしてくれれば便利です。この項では、この課題について検討します。例として次のような入力ファイルを考えます。
123, 456, 789 222, 333 444, 555, 666
これを、コンマの自動処理次を前提とした、次のようなコーディングで読み取れれば便利です。
int main() { ifstream ifs("C:\\data.csv"); // データファイルを開く。 CistreamCsv istreamcsv(ifs); int aiTable[3][3]; {for (int iRow = 0; iRow < 3; ++iRow) { {for (int iCol = 0; iCol < 3; ++iCol) { istreamcsv >> aiTable[iRow][iCol]; // 要素を一つ読み込む。コンマは自動処理される。 }} istreamcsv >> endl; // 次の行に移る。endl で改行が処理される。 }} return 0; }
実質的な読み込みを行う CistreamCsv は istream オブジェクトをもらって、それを使って入力を行います。これは istream や ostream でいわば規格化された入出力ストリームを使って入出力を行い、演算子やマニピュレータは自分で定義するということですので、CSV を自動処理しようという、この企画は、少ないコーディングで超スマートに、とはいかないようです。
では、そのようなクラス CistreamCsv を作成しましょう。入力の切り替えのための istream オブジェクトは、コンストラクタに与えることにし、その参照をメンバ変数として持つことにします。抽出演算子 >> と改行を吸収するマニピュレータ endl も定義します。空白の処理などが入るため、ちょっと長くなってしまいましたが、じっくり読んでみてください。
class CistreamCsv { // マニピュレータ endl の定義 friend CistreamCsv& endl(CistreamCsv& riscsv) { int iChar = 0; while ((iChar = riscsv.m_ris.get()) != '\n' && iChar != EOF) {} // 行末まで読み飛ばす。 return riscsv; } private: istream& m_ris; // 入力に使う istream の参照 public: CistreamCsv(istream& ris) : m_ris(ris) {} CistreamCsv& operator >>(int& ri) { ri = 0; int iChar = 0; // 空白を読み飛ばす。 while (isspace(iChar = m_ris.get()) && iChar != '\n') {} if (iChar == EOF ) { return *this; } string strValue; if (iChar == '\n') { m_ris.putback(iChar); iChar = 0; } else { strValue += iChar; } // コンマまで読む。 while ((iChar = m_ris.get()) != ',' && iChar != EOF && iChar != '\n') { strValue += iChar; } ri = atoi(strValue.c_str()); if (iChar == EOF ) { return *this; } if (iChar == '\n') { m_ris.putback(iChar); } return *this; } // マニピュレータを受け入れるための挿入演算子 CistreamCsv& operator >>(CistreamCsv& (*pmanipulatorfunction)(CistreamCsv&)) { return (*pmanipulatorfunction)(*this); } };
CistreamCsv のサンプル StreamCsv1.cpp
長いサンプルコードは、疲れますね。
ナオ : これって、一行読み込んでから、コンマで分けたほうが早いんじゃないですか?
店主 : 普通はそうするだろうね。しかし、どこで分けるにしてもある程度複雑になるのは避けられないと思うよ。
ナオ : フム...、それなら、いっそのこと入力ストリームで分けちゃえってことか。確かに、後で使う人は楽ネ。