文字列からの読み取り
 
(このコンテンツはメールマガジンの STL & iostream 入門に手を加えたものです。「 STL と iostream が使えるかのチェック」等はメールマガジンの方のページをご覧ください)
 
 データの取り出し ( #07 )
 
 ここまではすべて、データを文字列に「書き込んで」いました。実は std::strstream は、文字列からデータを「取り出す」ということもできます。
 
////////////////////////////////////////////////////////////////
//  int 型の変数を取り出します。

#include <stdio.h>
#include <iostream>
#include <strstream>

void GetIntData()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128
            , std::ios::out | std::ios::in );    // 読み取りも。

    cStrStrm
        << "128"    // 整数値を「文字列」として書き込みます。
        << std::endl << std::ends;    // 閉じちゃいます。
    printf( "%s", ch );

    int i;    // この変数にデータを書き込みます。
    cStrStrm
        >> i;    // データを取り出します。
    printf( "i: %d\n", i );
}

// 結果
128
i: 128

/////////////////////////////
	
 
 まず注意して欲しいのは std::strstream 型変数(ここでは cStrStrm )を作るときに第3引数に渡しているフラグです。
 これまでずっと std::ios::out だけを渡していました。今回はこれに加えて std::ios::in も渡しています。この std::ios::in は「文字列からデータを取り出す」という意味になります。このふたつは、
 
std::ios::out : std::strstream から文字列に出力( output )する
std::ios::in : std::strstream が文字列から入力( input )する
 
と憶えるといいでしょう。これは iostream 全体や STL にも共通して言えることです。
 次に、あとで取り出すために "128" という文字列をコピーします。これを「整数」として取り出すための int 型変数も用意しておきます。
 用意したら、「 std::strstream 型変数」と「 >> 演算子」、そしてこの「 int 型変数」を組み合わせ、文字列をデータとして取り出してみます。といっても、することは「書き込んだときと演算子を逆向きにする」だけです。
 書き込んだときには「 << 」を使いました。それとは逆に、読み取る時には「 >> 」を使えばいいんです。それ以外は書き込んだときとまったく同じです。コードから伝わる雰囲気としても「整数値を取り出す」という意味が伝わってくると思います。
 
 まとめれば、読み取ることに必要なのは「 std::ios::in を加える」ことと「 >> を使う」ことだけです。書き込むことと読み取ること、その両方が std::strstream を通してできるということです。
 
透明
透明
■ C++ : フラグと enum ( #08 )
 プログラミングの世界では、一般に「状態を示すもの」を「フラグ」と言います。「旗」という意味ですね。ゲームで「証拠を掴んだ!」とか「ボスを倒した!」とかの場合に「フラグが立った」と表現します。フラグが立つことで、そのあとのストーリーが変わってくるのは、ストーリーを進めるプログラムがフラグが立っているかどうかを確認しているからです。
 ゲームじゃなく普通のプログラムならというと、 std::strstream のコンストラクタに渡す std::ios::in や std::ios::out 、std::setiosflags() に渡す std::ios::hex などがフラグに当たります。
 
 フラグは普通「ビットフラグ」と呼ばれるものを使用します。これは整数値で2の倍数ごとの値をフラグとするものです。例えば
 
    const int FLAG1 = 0x0001;
    const int FLAG2 = 0x0002;
    const int FLAG3 = 0x0004;
    const int FLAG4 = 0x0008;
	
 
 といった形式です。このようなとびとびの数字を使うと、ビット単位で0と1を変えることができて、32ビット整数値なら32個のフラグを同時に持つことができるからです。この辺は「ビット演算」について勉強してみてください。
 
 さて、この方式の問題点は「どんな整数値もフラグになってしまう」という点です。そのため、フラグとして用意されていない値なども渡せてしまい、バグの原因となってしまいます。
 それを防ぐため、 C++ では enum をフラグとして使用することが一般的になっています。
 
enum type_Flags
{
    F1 = 0x01, 
    F2 = 0x02, 
    F3 = 0x04, 
    F4 = 0x08
};
	
 
 enum をこのように使うと、 const な整数値を作ることができます。そして、この enum グループの名前を、関数の引数にします。
 
void Enumer( type_Flags p_iFlag )
{
    if( p_iFlag & F3 )
        printf( "Hit!\n" );
}
	
 
 すると、この関数の引数には同じ enum グループのものしか渡せません。つまり、単なる整数値は渡せないのです。
 
    Enumer( F3 );      // OK
//  Enumer( 0x01 );    // だめ!
	
 
 こうすることで、正しいフラグだけを受け付けることができるのです。
 さらに、 enum で作られた変数は、クラスのメンバとして持つことができます。そうして作られたのが、先ほどの std::ios::in や std::ios::hex なのです。
透明
透明
 
 今度は別のケースを考えてみましょう。今度は "12" と "8" というふたつの文字列を書き込んで、それを別々の整数値として取り出すとしたらどうすればいいのでしょう。 "128" と書き込んでしまったら、先ほどと同じように 128 として取り出されてしまいます。
 この問題を簡単に解決する方法は、 "12" と "8" を明確に分けて渡すことです。間に「スペース」を入れることで、ふたつを分けることができます。
 
////////////////////////////////////////////////////////////////
//  複数の int 型の変数を取り出します。

#include <stdio.h>
#include <iostream>
#include <strstream>

void GetManyData()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128, std::ios::out | std::ios::in );

    cStrStrm
        << "12 8"    // 整数値の間にスペースを入れておきます。
        << std::endl << std::ends;
    printf( "%s", ch );

    int i1, i2;    // このふたつの変数にデータを書き込みます。
    cStrStrm
        >> i1
        >> i2;    // データをふたつ取り出します。
    printf( "i1: %d, i2: %d\n", i1, i2 );
}

// 結果
12 8
i1: 12, i2: 8

/////////////////////////////
	
 
別々に読み取り  最初に書き込む文字列は "12 8" と間にスペースを入れておきます。そうすると、整数値として取り出すときに 12 としてまず取り出します。つまりスペースで読み取り用のポインタが止まるわけです。次にもう一度読み取ることで、その次の 8 を取り出します。このように、スペースを挟んでデータを書き込むことで、複数のデータを読み書きできるというわけです。
 ちなみに、「区切りになる文字」には、「スペース」の他に「改行文字( '\n' )」も使えます。
 
 書き込みと読み取りの違い ( #08 )
 
 前回は整数値だけ読み取りましたが、それ以外の型のデータももちろん取得できます。書き込めるデータは、読み取ることもできるというわけです。
 
/////////////////////////////
//  色々な型の変数を取り出します。

#include <stdio.h>
#include <iostream>
#include <strstream>
#include <iomanip>

void GetManyTypeData()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128, std::ios::out | std::ios::in );

    cStrStrm
        << "One 2 3.4"
        << std::endl << std::ends;
    printf( "%s", ch );

    //    取り出すデータを格納する変数。
    char chStr[130];
    int i;
    double d;

    // 読み取り幅を指定します。
    cStrStrm >> std::setw( 129 );

    //    データを取り出します。
    cStrStrm
        >> chStr >> i >> d;
    printf( "chStr: %s, i: %d, d: %f\n", chStr, i, d );
}

// 結果
One 2 3.4
chStr: One, i: 2, d: 3.400000

/////////////////////////////
	
 
 #02 (「いろんな値を渡してみよう」)のときと比べると、型が少ないなーと思われるかもしれません。ですが、bool やポインタは、文字列に格納したあとは整数値と変わらないため、整数値として取り出して出力する前に整形すれば問題ないでしょう。そのときまた std::strstream を使ってもいいでしょう。ちなみに「文字配列の最大幅」は #03 (「きれいに表示しよう」)で使用した std::setw() マニピュレーターで設定します。
 
 こういった「読み取りと書き込みとでの違い」の例をもうひとつ見てみましょう。
 #05 (「ポインタの移動 」)と #06 (「ポインタの位置」)で見た「書き込みポインタの位置」の「読み取り版」とでも言えるものが std::strstream には備わっています。「次はどこから書き込むかを指し示すポインタ」と「次はどこから読み取るかを指し示すポインタ」の「ふたつのポインタ」を std::strstream は持っているということです。
 書き込み時には、ポインタの移動には std::strstream::seekp() を使いました。読み取り時にはこれに似た std::strstream::seekg() を使います。「 p 」が「 g 」に変わっただけで、使い方は変わりません。
 
/////////////////////////////
//  読み取りポインタを移動します。

#include <stdio.h>
#include <iostream>
#include <strstream>

void MovePointerG()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128, std::ios::out | std::ios::in );
    cStrStrm
        << "ABCDEFG 100"
        << std::endl << std::ends;
    printf( "%s", ch );

    char chOutput[130];    // 出力用文字列。
    cStrStrm
        >> chOutput;    // 文字列を取り出します。
    printf( "%s\n", chOutput );

    cStrStrm.seekg( 2 );    // 先頭から3文字目へ移動。
    cStrStrm
        >> chOutput;    // 再び文字列を取り出します。
    printf( "%s\n", chOutput );
}

// 結果
ABCDEFG 100
ABCDEFG
CDEFG

/////////////////////////////
	
両ポインタの位置   std::strstream::seekg() を使うと、「読み取りポインタ」の先頭からの位置(先頭は 0 )を移動することができます。 std::strstream::seekp() の時と同じですね。同様に、第2引数を使った相対移動もできます。もちろん std::strstream::tellg() という「現在のポインタの位置を取得するメンバ関数」も用意されています。
 ちなみに上の例を見れば分かるように、文字列を取り出したときには自動的に「終端文字'\0' )」が追加されます。また、「改行文字( '\n' )」は「区切り文字」と見なされてしまうため、コピーされません。いちいち改行しているのはそのためです。こういった細かいクセも、「読み取り」と「書き込み」の違いと言えるでしょう。
 ちなみに「 p 」は「 put 」、「 g 」は「 get 」の略と憶えるといいでしょう。「読み取り= in = get 」、「書き込み= out = put 」です。
 
 読み取りエラーの対処法 ( #09 )
 
 「読み取りと書き込みの違い」の中でも最も大きな違いは「読み取りにはエラーの可能性がある」という点です。
 通常、書き込み時には実行時エラーは発生しません。ミョーな型を渡そうとしても、まずコンパイルエラーが発生します。オーバーランくらいしか実行時エラーが発生する機会はないでしょう。
 ところが、読み取り時にはあっさりとエラーが発生します。文字列として格納されているデータと、これから格納する変数との間で、型が一致するとは限らないからです。たとえば int 型変数へと値を格納しようとしているのに、文字列に "ABC" と書かれていたら読み取ることはできないでしょう。
 この例を実際に再現してみましょう。
 
/////////////////////////////
//  読み取りにわざと失敗してみます。

#include <stdio.h>
#include <iostream>
#include <strstream>

void FailBit()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128, std::ios::out | std::ios::in );

    cStrStrm
        << "ABC 100"
        << std::endl << std::ends;
    printf( "%s", ch );

    int i = 0;    // 格納用変数。
    // "ABC" を整数で受け取ろうとしてみます。
    cStrStrm
        >> i;    // 無理に読み取ります。
    printf( "%d\n", i );

    // 今度は 100 を受け取ります。
    cStrStrm
        >> i;    // 受け取れるはず……。
    printf( "%d\n", i );
}

// 結果
ABC 100
0
0

/////////////////////////////
	
 
 まず "ABC" というという文字列を「 int 型変数」に格納しようとしてみます。もちろんこれはできないことなので、失敗してしまい値は格納されません。
 次にもう一度「 int 型変数」に格納しようとしてみます。読み取りポインタは進んでいるはずなので 3 が入っていると思えますが、実はこれも失敗します。
 
 実は、1度目の失敗で「読み取りができない状態」になっているため、2度目以降の読み取りはできなくなっているのです。この「状態」は、例によって「 std::strstream 型変数」の中にフラグして存在しています。
 このフラグの状態を取得するのが std::strstream::fail() というメンバ関数です。
 
/////////////////////////////
//  読み取りに失敗したあと、復活します。

#include <stdio.h>
#include <iostream>
#include <strstream>

void FailBitClear()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128, std::ios::out | std::ios::in );

    cStrStrm
        << "ABC"
        << std::endl << std::ends;
    printf( "%s", ch );

    int i = 0;    // 格納用変数。
    // "ABC" を整数で受け取ろうとしてみます。
    cStrStrm
        >> i;    // 無理に読み取ります。
    if( cStrStrm.fail() )    // もし失敗していれば、
    {
        printf( "Failed. Clear now!\n" );
        cStrStrm.clear();    // フラグをリセットします。
    }
    // 今度は文字列として。
    char chOutput[130];    // 出力用文字列。
    cStrStrm    // ポインタは移動していません。
        >> chOutput;
    printf( "%s\n", chOutput );
}

// 結果
ABC
Failed. Clear now!
ABC

/////////////////////////////
	
 
 int 型変数に格納しようとして失敗したあと std::strstream::fail() を呼び出して「失敗しました状態」になっているか調べます。
 
0以外: 失敗
0    : 失敗していません
	
 
です。0以外が返ってきて「失敗しました」状態の場合、ほっとけばずっと「失敗しました」状態のままです。これではいつまで経っても読み取りを再開できないので、このフラグをリセットします。
 リセットは std::strstream::clear() というメンバ関数を呼び出します。このメンバ関数を呼び出すだけで、「失敗しました」状態をリセットすることができ、再び読み取りを開始することができます。
 読み取りに失敗した場合、「読み取りポインタ」はまったく移動しません。上の例なら、文字列の先頭のままです。ということで、もう一度先頭から文字列として読み取りなおして、晴れて成功、ということになります。
 通常のプログラムでは、この「エラーチェック」は必ずしてください。
 
 入力クラスと出力クラス ( #10 )
 
 std::strstream クラスは、入出力両用のクラスでした。ところが、実は「入力専用クラス」と「出力専用クラス」というものがあって、それがひとつに組合わさって std::strstream になっているのです。
 まず「出力専用クラス」について見てみましょう。 std::strstream の出力専用版は std::ostrstream です。
 
/////////////////////////////
//  std::ostrstream の使用例。

#include <stdio.h>
#include <iostream>
#include <strstream>

void Use_ostrstream()
{
    char ch[130];
    std::ostrstream cStrStrm( ch, 128 );

    cStrStrm
        << "ABC"
        << std::endl << std::ends;
    printf( "%s", ch );
    char chError[130];
//    cStrStrm
//        >> chError;    // は使えません。
}

// 結果
ABC

/////////////////////////////
	
 
 まず std::ostrstream 型変数 cStrStrm を作ります。このとき、これまで渡していた std::ios::out を渡す必要はありません。 std::ostrstream は出力専用クラスなので、わざわざ「出力」を示す std::ios::out を渡す必要はないというわけです。
 作ったら、あとは std::strstream の時と同じように書き込めます。
 ところが、 >> を使って取り出すことは、できません。 std::ostrstream は出力専用クラスなので、 >> で「入力」することはできないのです。
 
 次に「入力専用クラス」を見てみましょう。 std::istrstream クラスです。
 
/////////////////////////////
//  std::istrstream の使用例。

#include <stdio.h>
#include <iostream>
#include <strstream>

void Use_istrstream()
{
    char ch[] = "XYZ";
    std::istrstream cStrStrm( ch, 4 );

    char chData[130];
    cStrStrm
        >> chData;
    printf( "%s\n", chData );
//    cStrStrm
//        << "DEF";    // は使えません。
}

// 結果
XYZ

/////////////////////////////
	
 
 今度は逆に、>> を使ってデータを読み取ることはできますが、 << を使って書き込むことはできません。
 
ふたつがひとつに  そして、このふたつのクラスを組合わせたものが、 std::strstream なのです。 std::strstream が std::ostrstream と std::istrstream の両方の機能を持っているのはこのためです。
 このような仕様となっているのは、場合によっては「読み取りのみ」「書き込みのみ」必要な場合があるからです。常にどちらもできる場合には、操作ミスでうっかり書き込んでしまう、などの場合があるからです。
 ここで「書き込み」と「読み取り」をまとめてみましょう。
 
  クラス フラグ 演算子 ポインタ エラーチェック
書き込み std::ostrstream std::ios::out << seekp() 特に必要なし
読み取り std::istrstream std::ios::in >> seekg() 常に必要
 
 この「入力専用クラス」「出力専用クラス」「入出力両用クラス」の仕組みは、 std::strstream だけの仕組みじゃありません。 iostream 全体の仕組みです。次回からは、これを踏まえて「他の iostream クラス」について見ていきましょう。
(C)KAB-studio 2000 ALL RIGHTS RESERVED.