文字配列への書き込み
 
(このコンテンツはメールマガジンの STL & iostream 入門に手を加えたものです。「 STL と iostream が使えるかのチェック」等はメールマガジンの方のページをご覧ください)
 
 文字列操作 ( #01 )
 
 C 言語には「文字列型」が存在しません。「文字列」を格納する場合、通常「 char 型の配列」を使用します。配列の要素ひとつひとつが、文字ひとつひとつに当たるとみなして操作します。
 ですがこの文字配列は、文字列を操作するのには不便すぎます。たとえば「文字配列に文字列をコピーする」ということを直感的に行おうとすると、次のようになります。  
void BadCharCopy()
{
    char ch[128];
    ch = "Nya";	// 文字列をコピー……できません。
}
	
 
 このコードはコンパイルエラーが発生するでしょう。これは「ポインタどうしの処理」と見なされてしまい、文字列とはまったく関係がないからです。
 そこで登場するのが iostream です。
 
 文字列操作には iostream に含まれる std::strstream というクラスを使用します。 std::strstream は文字配列に文字列をコピーするためのクラスです。使い方は簡単。 std::strstream 型の変数を作って、そのときに文字配列へのポインタを渡してしまえばいいだけです。
 
/////////////////////////////
//  std::strstream の使用例。
#include <stdio.h>
#include <iostream>
#include <strstream>    // をインクルードしてください。

void CharCopy()
{
    char ch[130];     // 実際には定数値には const int を使いましょう。
    std::strstream cStrStrm
        ( ch         // 文字配列へのポインタ。
        , 128        // 書き込める範囲。
        , std::ios::out );    // 書き込み専用。

    cStrStrm
        << "Written.."
        << std::endl
        << std::ends;
    printf( "%s", ch );    // 文字配列を渡していることに注意。
}

// 結果
Written..

/////////////////////////////
	
 
文字配列と cStrStrm  まず空の文字配列を作成します。この文字配列は「文字列を受け取る」ためのものです。操作された文字列は、すべてこの文字配列へと送られてきます。  
 次に std::strstream 型変数(ここでは cStrStrm )を作成します。
 cStrStrm を作成するときに、同時に引数を渡す必要があります。 std::strstream は「特定の文字配列と読み書きする」というクラスなので、cStrStrm を操作する前に「相手の文字配列へのポインタ」を渡しておく必要があります。 cStrStrm を作成しているときに第1引数に渡しているものが、文字配列へのポインタです。このポインタを通じて、文字列を書き込むことになります。
 また、文字配列を超えて書き込まないよう、文字配列のサイズを渡します。 cStrStrm を通じてデータを送っても、決してこの値を超えて文字配列へと書き込むことはありません。
 初期化の最後、「 std::ios::out 」は「書き込み専用」という意味です。これから文字列を書き込むので、このフラグを渡します。
 
 さて、 cStrStrm の準備ができました。では、 cStrStrm を通じて文字配列へと文字列をコピーします。
 まず cStrStrm へ文字列を渡します。渡す時には << という演算子を使います。 cStrStrm に向けて << を、その後ろに文字列を置きます。こうすると、 cStrStrm は文字列を受け取り、文字配列へとコピーしてくれます。
 文字列をコピーしたあと、文字配列を閉じておきましょう。ダブルクォーテーションで閉じた文字列は「最後に終端文字( '\0' )が入っている」と見なされますが、 std::strstream は、この「終端文字」はコピーしません。代わりにちゃんと加えておく必要があります。
 まず「 std::endl 」を渡します。これは「改行文字( '\n' )」を追加します。次に「 std::ends 」を渡します。これが「終端文字( '\0' )」を加えます。これを加えないと、文字配列が閉じられません。
 
 さて、以上で「文字列のコピー」は終了です。文字列は文字配列へとコピーされました。つまり std::strstream は単なる仲介役です。 cStrStrm には文字列はありません。文字配列の方に文字列があります。なので、文字配列の方に文字列が入っているか、最後に確認します。
 
 以上をまとめると、次のようになります。
 
1:文字配列を用意する。
2:「仲介役」 std::strstream 型変数を作る。
3:このとき「出力先」文字配列へのポインタを渡す。
4: << を使って文字列を std::strstream 型変数に渡す。
5: std::ends を渡して文字配列を閉じる。
6:文字配列に文字列がコピーされている。
 
 さて、今回は文字列をコピーしましたが実際にはいろんな型の値をコピーできます。次回はその方法について見ていきましょう。
 
透明
透明
■ C++ : クラス ( #03 )
 std::strstream は「クラス」と呼ばれるものです。今回はこのクラスというものについて見てみましょう。
 
 まず、クラスは「プログラマーが作る」です。型ですから、その変数を作ることで初めて使うことができるようになります。また、初めからは組み込まれていないので、自分か誰かが作る必要があります。クラスライブラリは「誰かが作ったクラス集」ってことですね。
 では、最も標準的なクラスについて見てみましょう。
 
//	クラス。これが「型」になります。
class CClassData
{
private:
    // メンバ変数。
    int m_i;
public:
    // メンバ関数。
    int GetData()
    {
        return m_i;
    }

    void SetData( int p_i )
    {
        m_i = p_i;
    }
};
	
 
 これが標準的なクラスです。
 まず「メンバ変数」と書かれた部分を見てください。クラスは、中に変数を持つことができます。これを「メンバ変数」と言います。この例だと m_i がメンバ変数に当たります。
 この変数は「クラス型変数の状態を持つ」ためのものです。この例では、何に使うかは分かりませんがとにかく整数値を持つようですね。
 メンバ変数の前に private: と付いてますね。これは「アクセス指定子」のひとつで、これが付いていると「クラスの外からアクセスできない」ことになります。これによって、先ほどのメンバ変数 m_i は、クラスの外からはどんなにがんばっても値を代入したり読み取ったりすることができなくなります。
 そこで、このメンバ変数にアクセスするための「メンバ関数」を用意します。
 クラスは変数だけじゃなく関数も持つことができます。これを「メンバ関数」といいます。この例では GetData() と SetData() がそうです。関数ですから、もちろん呼び出すことができます。
 また、メンバ関数の前に public: というものが付いているでしょう。これもアクセス指定子のひとつで、これが付いていると「クラスの外からアクセスできる」という意味になります。これが付いているおかげで、このふたつのメンバ関数は、クラスの外から呼び出すことができます。
 「クラスの外」の反対となる「クラスの中」は、メンバ関数の中を指します。 GetData() と SetData() の中身は「 CClassData クラスの中」ということになります。クラスの中はアクセス指定子と関係がないので、メンバ変数にアクセスすることができます。
 まとめると、「外から触れられない private なメンバ変数を操作するために、外から触れられる public なメンバ関数を作り、そのメンバ関数内でメンバ変数を操作する」ということです。
 このクラスの使用例を見てみましょう。
 
////////////////////////////////////////////////////////////////
//  std::strstream の使用例。

void Use_CClassData()
{
    CClassData cClsData;
    cClsData.SetData( 100 );
    printf( "%d\n", cClsData.GetData() );
}

// 結果
100

/////////////////////////////
	
 
 まず CClassData 型の変数 cClsData を作ります。これをしなければ、クラスは使えません。
 次に CClassData::SetData() を呼び出して、メンバ変数 m_i に 100 という値をセットします。外から直接 m_i に触れることはできないので、代わりに CClassData::SetData() を呼び出しているというわけです。 最後に CClassData::GetData() を呼び出して、 m_i の値を取得します。もちろん 100 が入っています。このメンバ関数を呼び出すのも、 m_i に直接触れられないからです。
 
 別に、メンバ変数は private 、メンバ関数は public と決まっているわけではありません。これらは自由に決めることができます。でも、これが標準的な仕組みなんだということは憶えておいてください。
透明
透明
 
 
 いろんな値を渡してみよう ( #02 )
 
 ここで、先ほど std::strstream を使ったときに出てきた iostream のクラスや関数を見てみましょう。
 
std::strstream :文字列をコピーしてくれるクラス。
std::ios::out :「書き込み用」という意味のフラグ。
std::endl :「改行文字( '\n' )」を追加する関数。
std::ends :「終端文字( '\0' )」を追加する関数。
 
  std::endl と std::ends は、実は「関数」です。ですから、次のように「関数として呼び出す」こともできます。
 
////////////////////////////////////////////////////////////////
//  std::endl() を呼び出します。

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

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

    cStrStrm << "Before \\n" << std::endl;
    std::endl( cStrStrm );    // std::endl() を呼び出します。
    cStrStrm << "After \\n" << std::endl << std::ends;

    printf( "%s", ch );
}

// 結果
Before \n

After \n

/////////////////////////////
	
 
 でもこのコードにはほとんど意味はありません。 std::endl は「 std::strstream に << を通じて渡す」ことにこそ意味があります。そういう使い方をしてこそ、解りやすく読みやすいプログラムを書くことができるのです。
 これは STL の解説でも言いましたね。 STL と iostream を含めた「標準C++ ライブラリ」のポリシーのひとつが「可読性を高める」ということだということです。
 
 さて、今度は std::strstream を使って、文字列以外の色々なデータを追加してみましょう。
 C 言語ではデータの格納形式が大きく違う「文字」と「数字」ですが、 std::strstream を使うとまったく気にせずに済みます。つまり、「文字列」をコピーしたときと同じ方法で「数字」を「文字」としてコピーできる、というわけです。
 
////////////////////////////////////////////////////////////////
//  いろんな型のデータを追加していきます。

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

void AppendAllType()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128, std::ios::out );
    const int i = 6;    // この数字は出力されません。

    cStrStrm
        << false << std::endl    // 論理値も、
        << "One" << std::endl    // 文字列も、
        << '2'     << std::endl  // 文字も、
        << 3    << std::endl     // 数字( int 型)も、
        << 4.5    << std::endl   // 実数も、
        << &i    << std::endl    // 変数のアドレスも、
        << std::ends;            // 追加できます。
    printf( "%s", ch );
}

// 結果
0
One
2
3
4.5
006CFC2C

/////////////////////////////
	
 
 このように、std::strstream へは「文字列( const char *s )」「文字( char )」「整数( short, int, long )」「論理値( bool )」「実数( double, fload )」など、ほとんどの組込型を渡せます。また、変数へのアドレスを渡すとそのアドレスをそのまま表示します。
 こういった「全てをシームレスに操作できる」ことも、可読性を高めてくれるのです。
 
透明
透明
■ C++ : 演算子のオーバーロード ( #04 )
 こんな例を考えてみましょう。
 
template< class type_Parameter >
void OneHundred( type_Parameter &p_rRet )
{
    p_rRet = 100;
}
	
 
 この関数テンプレートは、参照として渡された引数に 100 を代入するというものです。この関数に引数として渡せるのは、どんな型でしょう。
 int や double などの組込型は、当然渡すことができます。でも、次のようなクラスは渡すことができません。
 
class CNoOperator
{
public:
    int m_i;
};
	
 
 普通のクラスは、 = のような演算子に対応していないので、そういった型の変数は先ほどの OneHundred() のような関数テンプレートには渡せません。コンパイルエラーになってしまいます。これは std::fill() のような一般的な STL アルゴリズムにも渡せないということです。
 ですが、次のようなクラスは渡すことができます。
 
class CWithOperator
{
public:
    int m_i;

    void operator =( int p_i )
    {
        m_i = p_i;
    }
};
	
 
 operator =() というメンバ関数を作ると、 = 演算子が使われたときに自動的に呼び出されます。つまり、
 
    CWithOperator cWithOp;
    cWithOp = 100;
	
 
がちゃんと機能するということです。そして、これによって CWithOperator 型の変数は OneHundred() や std::fill() に渡すことができるようになります。
 このように、クラス型変数に演算子が使えるようにすることを「演算子のオーバーロード(多重定義)」と言います。
 iostream の解説の中で std::strstream 型変数に << で値を渡せるのも、 operator <<() という演算子のオーバーロード関数がちゃんと備わっているからなのです。
 
 もうひとつ、面白い例を見てみましょう。
 
class CWithParentheses
{
public:
    int m_i;

    void operator ()( int p_i )
    {
        m_i = p_i;
    }
};
	
 
 このクラスは「 () 」演算子をオーバーロードしてますね。 = や << は普通の組込型でも使うものですが、 () は普段使いません。この () をオーバーロードすると、次のように使用することができます。
 
	CWithParentheses cWithPa;
	cWithPa( 100 );
	printf( "%d\n", cWithPa.m_i );
	
 
 あらびっくり、クラス型変数の cWithPa が、まるで関数のように振る舞っています。これは () をオーバーロードしているからこそできることなのです。
 これはトリッキーなものではなく、 STL の「関数オブジェクト」で一般的に使われているものなので、今のうちに慣れておきましょう。
透明
透明
 
 
 きれいに表示しよう ( #03 )
 
 「改行文字」を出力する std::endl や 「終端文字」を出力する std::ends は「マニピュレーター」と呼ばれています。マニピュレーターは iostream が持つ関数で、「特別なこと」をしたいときに std::strstream に渡します。
 マニピュレーターの中には「整数値を 16 進数として出力する」機能を持つものがあります。これを使えば、 2 進数のデータを 16 進数として処理します。
 
////////////////////////////////////////////////////////////////
//  16 進の数字を 16 進数として出力します。

#include <stdio.h>
#include <iostream>
#include <strstream>
#include <iomanip>    // をインクルードしてください。

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

    const int i
        = 0x1B;    // 10 進数では 27 。
    cStrStrm
        << std::resetiosflags( std::ios::dec )    // リセットします。
        << std::setiosflags( std::ios::hex )      // セットします。
        << i                                      // i を文字列としてコピー。
        << std::endl << std::ends;
    printf( "%s", ch );
}

// 結果
1b

/////////////////////////////
	
 
マニピュレーターと cStrStrm  ここで使用するマニピュレーターは std::resetiosflags() と std::setiosflags() のふたつです。 cStrStrm の中には「どういう形で出力するか」という設定が入っています。 std::resetiosflags() を使うと設定をリセットし、逆に std::setiosflags() を使うと設定をセットします。
 
 何をセット/リセットするかは、マニピュレーターに渡す引数によって変わってきます。
 引数として渡せるものは、「 10 進モード」の std::ios::dec 、「 16 進モード」の std::ios::hex 、それと「 8 進モード」の std::ios::oct の3つがあります。この設定を std::setiosflags() と std::resetiosflags() に渡し、これらの関数(の戻り値)を、他の変数と同じように << を使って cStrStrm に渡すことで、モードを変更することができます。デフォルトは「 10 進モード」なので、上の例では最初に「 10 進モード」をリセットして、そのあと「 16 進モード」をセットしています。
 
 マニピュレーターや std::setiosflags() に渡す設定は他にもあります。これらも使って、もっと 16 進数っぽく表示してみましょう。
 
////////////////////////////////////////////////////////////////
//  16 進数をきれいに表示します。

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

void BeautifulHex()
{
    char ch[130];
    std::strstream cStrStrm( ch, 128, std::ios::out );
    const int GC_CANCEL = 0x0F00;                // この値を出力します。

    cStrStrm
        << std::resetiosflags( std::ios::dec )   // 10 進数をリセット。
        << std::setiosflags( std::ios::hex       // 16 進数にセット。
            | std::ios::uppercase )              // 大文字表示に。
        << std::resetiosflags( std::ios::right ) // 右詰をリセット。
        << std::setiosflags( std::ios::left )    // 左詰にセット。
        << std::setw( 10 )                       // 10 桁表示。
        << std::setfill( ' ' )                   // 間を空白で埋めます。
        << "GC_CANCEL"

        // setw() は無効になりますが、左揃えはそのまま。
        << "0x"

        << std::resetiosflags( std::ios::left )   // 左詰をリセット。
        << std::setiosflags( std::ios::right )    // 右詰にセット。
        << std::setw( 4 )                         // 4 桁表示。
        << std::setfill( '0' )                    // 間を 0 で埋めます。

        << GC_CANCEL                              // 最後に値の書き込み。
        << std::endl << std::ends;
    printf( "%s", ch );
}

// 結果
GC_CANCEL 0x0F00

/////////////////////////////
	
 
 まず std::setiosflags() にセットしているのは std::ios::uppercase です。これをセットすると16進数が大文字として表示されます。ちなみにこれをリセットすると小文字に戻ります。
 マニピュレーターもふたつ使ってます。 std::setw() と std::setfill() です。
 std::setw() は「最低文字数」をセットするマニピュレーターです。たとえばこれを10にセットすると、最低でも10文字分のスペースが確保されます。 "GC_CANCEL" は9文字なので、だからその右側が1文字空いているんです。
 その「空いた部分」に何の文字を詰めるかをセットするのが std::setfill() です。たとえばこれを '0' にセットすれば、空いた部分に '0' を埋めてくれます。
 さらに std::setiosflags() 用の設定 std::ios::right と std::ios::left を使うと「右詰」「左詰」を決められます。「空いた部分」ができてしまうような場合には、 std::ios::right で「右側に文字を、左側に空白部分を」と変えることができます。 std::ios::left を使えばこれと逆のことができます。
 こんなふうに、マニピュレーターを渡すことで出力結果を思いのままにすることができるというわけです。
 
 C 言語との違い ( #04 )
 
 iostream が含まれている標準 C++ ライブラリの元になった「標準 C ライブラリ( C ランタイムライブラリ)」には、 sprintf() という「数字や文字列を文字配列にコピーする」関数がありました。 std::strstream はこれを C++ 用に作り直したものです。
 sprintf() には「文字列の体裁を整える」機能が備わっていました。それらも、各マニピュレーターへと置き換わりました。
 現状では標準 C ライブラリについての解説書の方が多いので、この変換表を使ってマニピュレーターの使い方を調べてみてください。ちなみに解説書では printf() という関数の項に書いてあると思います。
 
iostream             C 標準            機能

[ std::resetiosflags() と std::setiosflags() に渡す設定 ]
dec                  型指定に d        10進表記
oct                  型指定に o        8進表記
hex                  型指定に x        16進数(小文字)
hex | uppercase      型指定に X        16進数(大文字)
left                 フラグに -        左詰
right                該当ナシ          右詰
internal             該当ナシ          中詰
showpos              フラグに +        正の時 + を書き加える
showbase             フラグに #        0x を書き加える
boolalpha            該当ナシ          true または false と表示
fixed                型指定に f        小数点表記
scientific           型指定に e        指数表記
showpoint            フラグに #        小数点を書き加える

[ マニピュレーター ]
setw()               幅指定            最小幅の指定
setprecision()       精度指定          実数の精度の指定
setfill()            フラグに 0(注)  空白部分を埋める文字の指定

注: setfill() はどんな文字でも埋められるので、実際には違います。
	
 
 この sprintf() を使って、前回の「きれいに16進数表示」を実現してみると、次のようになります。
 
////////////////////////////////////////////////////////////////
//  16進数を sprintf を使ってきれいに表示します。

#include <stdio.h>

void BeautifulHex_sprintf()
{
    char ch[130];
    const int GC_CANCEL = 0x0F00;

    sprintf( ch, "%-10s0x%04X\n", "GC_CANCEL", GC_CANCEL );

    printf( "%s", ch );
}

// 結果
GC_CANCEL 0x0F00

/////////////////////////////
	
 
 sprintf() に %-10s0x%04X\n という文字列が使われてますね。これが、std::strstream に渡したマニピュレーターと同じ意味を持っています。
 この sprintf() を使った場合と std::strstream を使ったときとを比べてみると、
 
・ sprintf() は短くて簡素だが読みにくく、型チェックがされない。
・ std::strstram は読みやすく型チェックがされるが、冗長で無駄がある。
 
と比較できます。
 「可読性」には個人差があるのでどちらがいいとは明言できない場合もありますが、型チェックに関しては明らかに std::strstream の方が勝っています。
  sprintf() は「可変数引数」という、引数の数を変更できる方法を取っています。この方法を採用することで、いくつもの変数を sprintf() を一度呼ぶだけで表示することができます。その代わり、渡された引数の型をチェックすることができません。そのため、表示する引数とは別に「引数がなんの型なのか」を示す必要があります。
 この方法は危険な面があります。この型指定と、実際の引数の型が異なる場合には実行時にエラーが発生する可能性があります。 sprintf() を使用する場合には、この点について常に気を付ける必要があります。
 対して std::strstream には、そんな必要はありません。 << を通して書き込む値を渡せば、正確な型を判別してくれるからです。また、本来渡せない値を渡した場合には、コンパイル時にエラーが発生します。
 
 さらに付け加えるのなら、 std::setiosflags() に渡すフラグに関してもこのことが言えます。 sprintf() で表記指定をする場合には、まったく関係のない指定文字を渡しても、実行するまでエラーが発生しません。逆に、std::ios にないものを std::setiosflags() に渡そうとした場合にはコンパイルエラーが発生します。
 
 標準 C++ ライブラリ全体の傾向として、テンプレートと演算子のオーバーロードを使って、できる限りコンパイル時にエラーが発生するような構造になっているということが言えます。こういった考え方に慣れることが、 STL や iostream を理解する手助けになることでしょう。
(C)KAB-studio 2000 ALL RIGHTS RESERVED.