標準入出力ストリーム | Programming Place Plus C++編【標準ライブラリ】 第27章

トップページC++編

C++編で扱っている C++ は 2003年に登場した C++03 という、とても古いバージョンのものです。C++ はその後、C++11 -> C++14 -> C++17 -> C++20 -> C++23 と更新されています。
なかでも C++11 での更新は非常に大きなものであり、これから C++ の学習を始めるのなら、C++11 よりも古いバージョンを対象にするべきではありません。特に事情がないなら、新しい C++ を学んでください。 当サイトでは、C++14 をベースにした新C++編を作成中です。

この章の概要 🔗

この章の概要です。


標準入出力ストリーム 🔗

標準ライブラリには、標準入力、標準出力、標準エラーという3つのストリームが定義されています。C言語においても stdinstdoutstderr という定義があり、同じものです。

これらの標準入出力ストリームはそれぞれ、標準ライブラリ内で定義されたクラス(正しくは、クラステンプレートに対する typedef)で表現されており、そのオブジェクトも定義済みになっています。

各ストリームで用いるクラスとオブジェクトは以下のようになります。クラスとオブジェクトが2つずつあるのは、char型の文字を扱うものと、wchar_t型の文字を扱うものとが分かれているためです。

種類

クラス

オブジェクト

標準入力

std::istream / std::wistream

std::cin / std::wcin

標準出力

std::ostream / std::wostream

std::cout / std::wcout

標準エラー

std::cerr / std::wcerr

標準エラー(ログ用途)

std::clog / std::wclog

標準エラーが2つあります。実装しているクラスは同一のものですが、別のオブジェクト (std::cerr/std::wcerr と std::clog/std::wclog) に分かれています。違いは、前者はバッファリングされず、後者はバッファリングされているという点です。

char型と wchar_t型の使い分けは、basic_string の発想と同じです(第2章)。つまり、クラステンプレートが定義されていて、そのテンプレート実引数を char型や wchar_t型にした typedef が用意されています。具体的には、以下のように定義されています。

namespace std {

    template <typename T, typename Traits = char_traits<T> >
    class basic_istream : public virtual basic_ios<T, Traits> {
    };

    template <typename T, typename Traits = char_traits<T> >
    class basic_ostream : public virtual basic_ios<T, Traits> {
    };


    typedef basic_istream<char>    istream;
    typedef basic_istream<wchar_t> wistream;

    typedef basic_ostream<char>    ostream;
    typedef basic_ostream<wchar_t> wostream;
}

basic_istreamクラステンプレートbasic_ostreamクラステンプレートはいずれも、basic_iosクラステンプレートを仮想継承【言語解説】第30章)しています。これは、「入力」のクラスと「出力」のクラスとを分けており、それらから「入出力」のクラスを定義する多重継承の形になっているためです。標準ストリーム周りのクラス構造は、この後の項で図示します。

標準入出力ストリームのクラス構造 🔗

標準入出力ストリーム関連のクラス構造を図示すると、以下のようになります。

標準入出力ストリームのクラス構造

上の方にあるクラスが基底クラスです。末尾に <> が付いているのは、クラステンプレートであることを示しています。また、basic_istream<>、basic_ostream<>、basic_iostream<> からは、薄い黄緑色の線が出ていますが、これは、標準入出力ストリーム以外の派生クラスがあることを表しています。これら派生クラスについては、第28章第29章で取り上げます。

標準で用意された cin、cout、cerr、clog のようなオブジェクトの型は、basic_istream<>、basic_ostream<> なので、basic_iostream<> は、あまり使う機会がありませんが、両者の機能を併せ持った入出力両用のクラステンプレートになっています。

【上級】ここで示したクラス以外に、basic_streambuf<> というクラステンプレートがあります。これは、各ストリームが使うバッファを管理するもので、ここからインスタンス化されたクラスが、実際に文字の読み書きを行っています。istream、ostream といった標準で用意されているオブジェクトを、標準的な使い方で使っているだけであれば、basic_streambuf<> の存在を意識することはありません。

標準入出力ストリームのヘッダ 🔗

標準入出力ストリームに関連する標準ヘッダは複数あります。

basic_istream や basic_iostream を使用する場合には、<istream> という標準ヘッダをインクルードします。また、basic_ostream を使用する場合には、<ostream> という標準ヘッダをインクルードします。

basic_iostream が定義されている標準ヘッダも <istream> です。分かりづらいことに、<iostream> という標準ヘッダもありますが、こちらは、cout、cin、cerr、clog といったオブジェクトを使用する場合に必要になるものです。

標準入出力ストリームのクラスの前方宣言のみが必要である場合(【言語解説】第25章)には、<iosfwd> という標準ヘッダをインクルードします。


演算子による出力 🔗

basic_ostream には、さまざまな型を扱える <<演算子が定義されており、これを使って、出力を行えます。

std::cout << x;         // x には多くの型が対応できる
std::cout << x1 << x2;  // 連続的につなげていくことができる

組み込み型や、いくつかの標準ライブラリの型については、上記のように簡単に出力可能です。一方、我々が自分で定義するクラス型は、そのままでは扱えませんが、<<演算子を非メンバとしてオーバーロード【言語解説】第35章)することで扱えるようになります。

#include <iostream>

class DataStore {
public:
    DataStore(int value) : mValue(value)
    {}

    inline int Get() const
    {
        return mValue;
    }

private:
    int    mValue;
};

std::ostream& operator<<(std::ostream& lhs, const DataStore& rhs)
{
    lhs << rhs.Get();
    return lhs;
}

int main()
{
    DataStore ds(100);

    std::cout << ds << std::endl;
}

実行結果:

100

左オペランドに ostream型を、右オペランドに独自のクラス(DataStore)型を受け取れる、非メンバの operator<< を定義しました。これがあれば、std::cout &lt;&lt; ds のように書いたとき、この operator<< が呼び出されるようにコンパイルされます。

戻り値の型を ostream の参照にし、第1引数をそのまま返すことで、<<演算子を連続的に使えるようにすることも重要です。

この実装の場合、wostream には対応されません。両方に対応するためには、operator<< を関数テンプレートとして定義すると、汎用的になります。

なお、上記のサンプルプログラムで、std::endl を使用していますが、これはマニピュレータと呼ばれる特別なオブジェクトです。マニピュレータは、ストリームに対して、何らかの操作を行うもので、標準ライブラリでいくつか定義されている他、独自で作ることも可能です。std::endl は、‘\n’ を出力し、バッファをフラッシュする効果があります。マニピュレータについての詳細は、第30章で取り上げます。

演算子による入力 🔗

basic_istream には、さまざまな型を扱える >>演算子が定義されており、これを使って、入力を受け取れます。

std::cin >> x;         // x には多くの型が対応できる
std::cin >> x1 >> x2;  // 連続的につなげていくことができる

読み取ったデータの先頭付近に空白文字がある場合、これは読み飛ばされます。読み飛ばしたくない場合には、std::noskipws というマニピュレータを使用して、動作を変更できます。マニピュレータについての詳細は、第30章で取り上げます。

入力処理では、バッファオーバーフローの危険性を考慮しなければなりません。文字列を受け取るときには、char型(basic_wistream なら wchar_t型)の配列ではなく、std::basic_string を使った方が安全です。std::basic_string は必要に応じて、追加のメモリ確保を行います。

もし、char型の配列を使うのであれば、std::setw というマニピュレータを使い、最大長の制限をかけるようにしてください。

std::string s1;
std::cin >> s1;    // 安全

char s2[80];
std::cin >> s2;    // 危険

char s3[80];
std::cin >> std::setw(sizeof(s3)) >> s3;  // 安全だが入力が途中で打ち切られる形になる

basic_ostream の <<演算子の場合と同様に、任意の型に対応させるためには、非メンバな演算子オーバーロードを行います。

#include <iostream>

class DataStore {
public:
    DataStore(int value) : mValue(value)
    {}

    inline void Set(int value)
    {
        mValue = value;
    }

    inline int Get() const
    {
        return mValue;
    }

private:
    int    mValue;
};

std::ostream& operator<<(std::ostream& lhs, const DataStore& rhs)
{
    lhs << rhs.Get();
    return lhs;
}

std::istream& operator>>(std::istream& lhs, DataStore& rhs)
{
    int value;
    lhs >> value;
    rhs.Set(value);
    return lhs;
}

int main()
{
    DataStore ds;

    std::cin >> ds;
    std::cout << ds << std::endl;
}

実行結果:

99
99

左オペランドに istream型を、右オペランドに独自のクラス(DataStore)型を受け取れる、非メンバの operator>> を定義しました。入力なので、第2引数は非const参照になります。

このサンプルプログラムでは、特に何も行っていませんが、入力の場合、入力データの不備について、多くのチェックが必要になることが多いでしょう。入力処理中のエラーなので、(このサンプルにおける DataStoreクラスのような)受け取り側ではなく、ストリーム自身がエラー状態を管理した方が良いです。ストリームが管理する「状態」に関しては、後の項で取り上げます


関数による出力 🔗

basic_ostream には、出力に使えるメンバ関数もあります。

basic_ostream<>putメンバ関数は、文字を出力し、 basic_ostream<>writeメンバ関数は、文字列を出力します。

#include <iostream>

int main()
{
    std::cout.put('x');
    std::cout.write("abcde", 3);
}

実行結果:

xabc

writeメンバ関数は、第1引数に文字列を指定し、第2引数に出力する文字数を指定します。文字数の指定が大きすぎる場合の動作は未定義なので、文字列長を越えないように注意してください。

putメンバ関数、writeメンバ関数のいずれも、ストリームへの参照(*this)を返します。

関数による入力 🔗

basic_istream には、入力に使えるメンバ関数もあります。これらのメンバ関数は、>>演算子による入力と異なり、先行する空白文字を特別扱いせず、そのまま読み取ります

basic_istream<>::getメンバ関数は、文字または文字列を読み取ります。この関数は、以下のようにオーバーロードされています。

int_type get();
basic_istream& get(T& c);
basic_istream& get(T* s, std::streamsize count);
basic_istream& get(T* s, std::streamsize count, T delim);

T という型は、basic_istreamクラステンプレートの第1テンプレート仮引数のことで、つまりは文字の型です。

int_type型は、Traits::int_type型のことですが、この「Traits」は、basic_istreamクラステンプレートの第2テンプレート仮引数のことです。

std::streamsize は、標準ヘッダ <ios> で定義された typedef名で、符号付き整数型です。

1つ目の形式は、入力ストリームから1文字読み取り、その文字を戻り値で返します。ストリームの終わりに達した場合は、それを表現する値(Traits::eofメンバ関数が返す値)が返されます。

2つ目の形式は、入力ストリームから1文字読み取り、引数c へ代入します。ストリームへの参照が返されるので、その「状態」を調べることで、読み取りの成否を判定できます。「状態」については、後の項で取り上げます

3つ目の形式は、入力ストリームから、第2引数で指定した文字数 - 1個の文字を、第1引数で指定したメモリアドレスへ読み取ります。ただし、改行文字が現れたら読み取りを行わず、終了します。また、読み取り処理を終えた後の第1引数が指す文字列には、終端文字が付加されています。ストリームへの参照が返されるので、その「状態」を調べることで、読み取りの成否を判定できます。なお、この形式では、バッファオーバーフローを起こさないように注意する必要があります。

4つ目の形式は、基本的には3つ目の形式と同じですが、第3引数に指定した文字が現れた場合も、その文字を読み取らずに終了します。

basic_istream<>::getlineメンバ関数は、1行分の文字列を読み取ります。この関数は、以下のようにオーバーロードされています。

basic_istream& getline(T* s, std::streamsize count);
basic_istream& getline(T* s, std::streamsize count, T delim);

改行文字あるいは、第3引数で指定した文字が現れたときに、その文字を読み取ってから、入力を終了するという点以外は、getメンバ関数の3つ目・4つ目の形式と同じです。なお、改行文字や第3引数で指定した文字は、引数s への格納は行われません

この関数はバッファオーバーフローの危険性があるため注意を払う必要があります。そもそも、文字列を扱うには std::basic_string を使った方がよく、そうするのであれば、std::getline関数を使った方が安全です(第2章)。

basic_istream<>::readメンバ関数は、シンプルに文字列を読み取ります。

basic_istream& read(T* s, std::streamsize count);

readメンバ関数は、第1引数で指定した位置へ、第2引数で指定した文字数分だけ読み取りを行います。終端文字は付加されません

また、ストリームの終わりに達した場合はエラーであるとみなされ、失敗を表す「状態」になります。 「状態」については、後の項で取り上げます

basic_istream<>::readsomeメンバ関数は、ストリームのバッファにある文字を、指定した文字数まで読み取ります。

std::streamsize readsome(T* s, std::streamsize count);

readメンバ関数と違い、呼出し時点で、ストリームのバッファに蓄えられているデータのみを読み取り対象とします。そのため、第2引数で指定した文字数よりも少ない文字数しか、s への格納が行われない可能性があります。実際に読み取られた文字数は、戻り値で返却されます。

ストリームの状態 🔗

ストリームは、エラー発生の有無や、末尾へ到達したかどうかといった情報を「状態」として管理しています。

状態は、ビットフラグの形で管理されており、以下の4つがあります。

フラグ 意味
goodbit 正常。すべてのビットが 0 である。
eofbit ファイルの終わりに到達した。
failbit 何らかのエラーが発生し、処理が正常に行えなかった。
badbit 致命的なエラーが発生した。

これらのフラグは、std::ios_baseクラスで定義されており、その型は std::ios_base::iostate型です。

goodbit に関しては、どのフラグも ON になっていない状態を表しており、つまりは「0」のことです。そのため、分かりづらいですが、goodbit というフラグが立つことはありません。もちろん、ストリームの初期状態は、この状態になっています。

これらのフラグは、入出力に関する処理を行ったとき、自動的にセットされます。一方で、自動的にクリアされることは無く、クリアに関しては明示的に行わなければなりません。クリアを行うには、ios_base::clearメンバ関数を使用します。

std::cout.clear();  // すべてのフラグがクリアされる(goodbit の状態になる)

failbit または badbit が立っている状態は、エラーが発生していることを示しており、このときに、入出力処理を行おうとしても何も起こりません。そのため、エラー状態から復帰させたければ、上記のように、フラグのクリアを明示的に行う必要があります。

現在、どのフラグが ON になっているかを調べるには、ios_base::rdstateメンバ関数を使って、フラグ全体を返してもらうか、直接的にある状態になっているかどうかを問い合わせる関数を呼び出します。後者の関数は、以下の4種類です。

メンバ関数 意味
good goodbit の状態かどうかを返す。
eof eofbit が ON になっているかどうかを返す。
fail failbit または badbit が ON になっているかどうかを返す。
bad badbit が ON になっているかどうかを返す。

いずれも、bool型の戻り値で結果を返す constメンバ関数です。

これらの関数のうち、failメンバ関数については、その意味に注意してください。この関数は、failbit だけでなく、badbit にも影響を受けます。failbit であろうと badbit であろうと、エラーであることに変わりはないので、この動作はむしろ好都合であることが多いと言えます。

また、以下の2つの変換演算子が定義されており、これを使ってエラーの有無を調べることも可能です。

変換演算子 意味
operator bool 「fail()」と同じ
operator void* 「!fail()」と同じ

たとえば、次のように書くと、入力を受け取り、エラーの有無を調べられます。

int num;
if (std::cin >> num) {
    // 正常
}
else {
    // エラー
}

エラーの方へ進んだ場合、failbit か badbit、あるいは両方が ON になっています。前述したとおり、フラグは自動的にクリアされることはなく、この状態のままでは、そのストリームは処理を行えません。ストリームを再度使用する場合は、状態のクリアを行ってください。


練習問題 🔗

問題① 標準入力から、複数行の文字列の入力を受け取るプログラムを作成してください。改行文字だけが入力されたときに終了するものとします。


解答ページはこちら

参考リンク 🔗


更新履歴 🔗

 std::getline関数の紹介を、第2章へ譲った。

 「グローバルな演算子オーバーロード」のような表現を、「非メンバの演算子オーバーロード」に修正。

 新規作成。



前の章へ (第26章 逆イテレータと挿入イテレータ)

次の章へ (第28章 ファイルストリーム)

C++編のトップページへ

Programming Place Plus のトップページへ



はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る