(このコンテンツはメールマガジンの 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.. ///////////////////////////// まず空の文字配列を作成します。この文字配列は「文字列を受け取る」ためのものです。操作された文字列は、すべてこの文字配列へと送られてきます。 次に 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:文字配列に文字列がコピーされている。 さて、今回は文字列をコピーしましたが実際にはいろんな型の値をコピーできます。次回はその方法について見ていきましょう。 |
|
|
いろんな値を渡してみよう ( #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 )」など、ほとんどの組込型を渡せます。また、変数へのアドレスを渡すとそのアドレスをそのまま表示します。 こういった「全てをシームレスに操作できる」ことも、可読性を高めてくれるのです。 |
|
|
きれいに表示しよう ( #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 ///////////////////////////// ここで使用するマニピュレーターは 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. |