ATLAS日本基礎ネットワーク C++トレーニングコース

文字列、入出力と数値計算

ここでの目標

目次

1.文字列

文字列はC++でも非常に重要なプログラミング要素です。C言語との互換性のため、現在でも古い文字列の扱いが一般的に行われています。ANSIのC++ではそれに対して標準的な文字列処理ライブラリを用意しています。出来るだけ標準ライブラリを使うことがATLASでも推奨されているため、その両方を理解しておく必要があります。

1.1.C言語の文字列

C言語では文字列を文字型(char)データの配列として扱ってきました。単純な配列では文字列の長さなどを記憶する場所がないので、文字列は「ヌル(ゼロ)で終端される文字型配列」と言えます。ですから、Cの文字型配列は、実際の文字列の長さにヌルの分を加えた記憶域が必要となります。

文字列はコンピュータプログラムでも非常に多用されるもので、そのための様々な関数が用意されてきました。/usr/include/string.hで定義される、strcpyなどの関数です。

#include <string.h>

char b[7];
strcpy( b, "Hello!" );

文字列リテラル"Hello!"は6個の文字を含んでいますが、それを格納するためには7文字分の記憶域が必要です。

1.2.stringクラス

ANSIのC++では、文字列の扱いも標準化しようと、stringクラスを用意しています。このクラスはこれまでに見てきたコンテナクラスの一種と考えることが出来ます。例えば反復子を使ってアクセスすることも可能です。コンテナであるので、例えば境界のチェックなど、これまでの文字列で出来なくてバグの原因となってきたものを改善し、より安全なプログラムにすることが可能です。その点でもstringを使うべきでしょう。

実習1

早速使ってみましょう。いつものように作業場所を用意しましょう。

cd ~/tutorial/cplusplus
mkdir l9
cd l9

string.cxxを書きます。

#include <string>
#include <iostream>

main( ) {
std::string hello( "Hello, World!" );

    std::cout << hello << std::endl;

}

実行してみましょう。

g++ string.cxx
./a.out

Hello, World!

単なるHello, World!プログラムですが、stringのオブジェクトが標準出力へ書き出されていることがわかります。コンテナであることを見るために最後の}の直前に次を書き足してみましょう。

std::string::iterator p = hello.begin( );

    while( p != hello.end( ) )
         std::cout << * p ++ << std::endl;

今度は一行に一文字ずつ縦に出力されるはずです。さらに、

    std::cout << "size = " << hello.size( ) << std::endl;

空白も数えて、13文字あることがわかります。

先頭に

#include <stdexcept>

を加えておいて、先ほどの例に続けて

    try {
        std::cout << "12th element is " << hello.at( 12 ) << std::endl;
    }
    catch( std::out_of_range ) {
        std::cout << "out_of_range caught" << std::endl;
    }

この例では12(1から数えると13)番目の文字はまだ境界内のため例外は投げられません。上記の例の12を13に変えてみてください。

標準コンテナと同じ実装なので、hello.at(13)hello[13]とすると今度は例外を投げません。これも確かめてみましょう。

実習2

ここまでのソースコードを整理すると次のようになります。

#include <string>
#include <iostream>
#include <stdexcept>
main( ) { std::string hello( "Hello, World!" ); std::cout << hello << std::endl; std::string::iterator p = hello.begin( ); while( p != hello.end( ) ) std::cout << * p ++ << std::endl; std::cout << "size = " << hello.size( ) << std::endl; try { std::cout << "12th element is " << hello.at( 12 ) << std::endl; } catch( std::out_of_range ) { std::cout << "out_of_range caught" << std::endl; } try { std::cout << "13th element is " << hello.at( 13 ) << std::endl; } catch( std::out_of_range ) { std::cout << "out_of_range caught" << std::endl; } }

実行してみましょう。

g++ string.cxx
./a.out

Hello, World!
H
e
l
l
o
,

W
o
r
l
d
!
size = 13
12th element is !
out_of_range caught

1.3.文字列の代入

先ほどの例ではコンストラクタでhelloに初期値を与えていました。それ以外に代入演算を行うことも出来ます。string.cxxの最後の}の前に次を追加しましょう。

    hello = "How are you doing today?";
    std::cout << hello << std::endl;

当然ですが、helloの内容が置き換えられたのでHow are you doing today?と表示されるはず。この代入では、文字列リテラルが使われています。stringの代入演算の右辺項には文字型だけでなく、stringオブジェクトを与えることももちろん出来ます。

    hello = std::string( "How are you doing today?" );

とすることで、定数オブジェクトを作って代入しています。

    std::string s( "How are you doing today?" ); 
    hello = s;

これも同じ効果ですが、変数オブジェクトsを作って、それを代入しています。

文字列の追加や連結も同様に演算子を用いて行うことが出来ます。Cの文字列では出来ませんでした。演算子多重定義のおかげです。

    hello += " Fukunaga-san.";
    std::cout << hello << std::endl;

+=演算子を用いましたが、+演算子で複数の文字列オブジェクトや文字列リテラルを結合することも出来ます。

途中に文字列を挿入することも出来ます。

    hello.insert( 5, "ABCDEF" );

先頭から5文字のオフセット後に文字列"ABCDEF"を挿入します。全体の長さは伸びます。

insertメソッドは一般の標準コンテナとしても持っています。ただしこの場合は要素である文字単位で挿入することになります。下の表現で'Z'は許されますが、"ABCDEF"は許されません。

    hello.insert( hello.begin( ) + 5, 'Z' );

基本的にstringクラスのメソッドは、文字の位置を先頭からのオフセット値(size_t型)で表します。文字の位置を返す場合も反復子ではなくオフセット値を返しています。

実習3

ここまでの分をまとめてみましょう。string2.cxxを作ります。

#include <iostream>
#include <string>
main( ) { std::string hello( "Hello, World!" ); std::cout << hello << std::endl; hello = "How are you doing today?"; std::cout << hello << std::endl; hello += " Fukunaga-san."; std::cout << hello << std::endl; hello.insert( 5, "ABCDEF" ); std::cout << hello << std::endl; }

 

これを実行しましょう。

g++ string2.cxx
./a.out

Hello, World!
How are you doing today?
How are you doing today? Fukunaga-san.
How aABCDEFre you doing today? Fukunaga-san.

1.4.文字列の比較と検索

代入演算子が定義されているように比較演算子も定義されています。==!=<><=>=がそれぞれ使えます。

    std::string name( "hoge" );
    if( name == "hoge" )
         std::cout << "Name matched." << std::endl;

また、文字や部分文字列を検索するメソッドも用意されています。次を最後に加えて走らせてみましょう。

    std::cout << "offset to 'doing' is " << hello.find( "doing" ) << std::endl;

文字単位でも

    std::cout << "offset to 't' is " << hello.find( 't' ) << std::endl;

後から探すrfindや、最初の出現find_first_of、最後の出現find_last_of、最初のそれ以外の文字の出現find_first_not_of、最後のそれ以外の文字の出現find_last_not_ofなどいろいろなメソッドが用意されています。これらはすべて、発見された文字の位置を先頭からのオフセット値で返します。もし見つからなければstd::string::nposという値を返します。

    int ioffset = hello.find( "hogehoge" );    //もともとそんな文字列は含まれていない。
    if( ioffset == std::string::npos ) {
         std::cout << "Substring hogehoge not found" << std::endl;
    }

1.5.文字列の置換と削除

文字列の一部を置換したい場合、例えば

    hello.replace( 5, 2, "XYZ" );

この場合は5文字先から2文字分を"XYZ"の3文字で置き換えることになります。

削除の場合

    hello.erase( 5, 3 );

この場合は5文字先から3文字を削除します。次の場合は全文字を削除することになります。

    hello.erase( 0, std::string::npos );

実はこれは次と一緒です。

    hello.erase( );

この動作は標準コンテナとしてはhello.clear( )の動作と同じです。

1.6.C言語文字列への変換

これまで、暗黙にC言語文字列(ヌル文字で終端される文字型配列)を引数としては使用してきました。関数多重定義によりstringが入るところはC言語文字列も入れることが出来ます。逆にC言語の文字列処理関数(string.hで定義される)やC言語入出力ライブラリ(printf等)を使うときにはstring型からC言語文字列へ変換する必要があります。その場合、c_str()メソッドを使います。

#include <stdio.h>
...
    printf( "%s\n", hello.c_str() );

2.入出力

ANSI-C++での入出力は入出力ストリームを通して行うことになります。入出力ストリームは、文字が次々に送り出されたり次々に流れ出てくる「くち」だと考えることが出来ます。

2.1.入出力ストリーム

入出力を行うためには入出力ストリームオブジェクトが必要です。実装としては、いろいろなオブジェクトの入出力を想定して、basic_iostreamクラステンプレートが用意されています。入力ストリームのテンプレートとしてbasic_istream、出力ストリームのテンプレートとしてbasic_ostreamがあります。これらを文字型データで即値化したものがostreamistreamです。通常はこれらのストリームクラスのオブジェクトに対して入出力を行います。

日本語など2バイト文字型のデータへの入出力も用意されています。2バイト文字型でbasic_iostreamを即値化したもので、wostreamwistreamになります。ATLASソフトウエアとしては英語を標準語としているためこれらを使うことはありませんので説明は省略します。

ostreamクラスやistreamクラスのメソッドを用いて入出力の詳細を制御していくことになります。

2.2.標準入出力

C言語の標準入出力としてはstdinという標準入力、stdoutという標準出力、それにstderrという標準エラー出力の三つが用意されています。これらはプログラムが起動されたときから開設されているもので、通常のファイルのように開いたり閉じたりする必要がありません。また、これらの標準入出力はUNIXではコンソール入出力に割り当てられていてシェルレベルでリダイレクトすることが出来るものです。

C++の入出力ストリームもこれらの標準入出力に対応したストリームを用意しています。以下に述べるものはostreamistreamのオブジェクトです。あらかじめこれらのオブジェクトは生成されstd名前空間の中に作られています。

cin
標準入力で、コンソール入力に対応します。istreamのオブジェクトです。
cout
標準出力で、通常のコンソール出力に対応します。ostreamのオブジェクトです。
cerr
標準エラー出力で、バッファされないコンソール出力です。coutが一旦バッファにためられ、改行文字などでまとめて出力されるのに対し、cerrへの出力はそのまま直ちに表示されます。そのため、coutの文字列を追い越して先に表示されることがあります。ostreamのオブジェクトです。
clog
エラー出力用ですが、cerrと違い、バッファされています。ostreamのオブジェクトです。

これらはiostreamで定義されています。

#include <iostream>

    std::cout << "Hello, World!" << std::endl;
 

でしたね。ここに出てくるcoutstd名前空間の中で作られたostreamクラスの一つのオブジェクトだと言うことを覚えておいてください。

以後、入出力について説明しますが、これらは基本的にistreamostreamの機能について述べるものです。それが標準入出力に向けられたものであっても、後に述べるファイルや文字列に対するものであっても基本的に同じクラスですので同じ振る舞いをします。

2.3.出力ストリーム

出力ストリームの典型的な使い方が

    std::cout << "Hello, World!" << std::endl;

というものです。これは分析しますと、std名前空間のcoutというオブジェクトを左辺値とし、"Hello, World!"という文字列リテラルを右辺値とする<<演算です。<<演算は左結合をしますので(同じ演算が繰り返された場合左から順番に繋ぐものです。)、この<<演算の結果を今度は左辺値としてstdで定義されているendlオブジェクトとの間で再度<<演算を行います。

    ( std::cout << "Hello, World!" ) << std::endl;

coutostreamのオブジェクトですので、ostreamクラスの定義の中で文字型へのポインターを右辺値とするoperator <<を探します。すると演算子の戻り値としてostream型への参照を取ることがiostreamヘッダーファイルに書いてあることがわかります。

ostream & operator << ( const char * );

一つ上の例の括弧内はostreamオブジェクトへの参照ですから(実際、coutですが)、上の括弧内がstd::coutとなり、

    std::cout << std::endl;

ということになります。

ostreamには組込型への<<演算がすべて定義されています。整数や浮動小数点数などはそのままostream<<することが出来ます。

一般的なクラスオブジェクトのostreamへの出力は定義されていません。おのおの定義する必要があります。これについては後で述べます。

2.4.入力ストリーム

出力ストリームと同様に、ストリームから値を任意の組込型変数へ読み込むことが出来ます。例えば

#include <iostream>

main( ) {
int    l;
    std::cin >> l;
    std::cout << l << std::endl;
}

超簡単なプログラムですが、コンソールから読み込んだ文字列を数値と解釈し、変換して整数型変数lに代入します。組込型の変数については>>演算が定義されています。また、std::stringに関しても

#include <iostream>
#include <string>

main( ) {
std::string s;
    std::cin >> s;
    std::cout << s << std::endl;
}

一般的なクラスへのistreamからの入力はそれぞれ定義が必要です。

istreamからの>>演算も、istreamオブジェクトへの参照(この例ではstd::cin)を返します。それ故、>>演算を繰り返し記述することが出来ます。

int    l;
double f;
std::string s;

    std::cin >> l >> f >> s;

この場合、入力文字列は空白などの区切り文字で区切って(トークンとして)読み込まれます。

この例では文字列も空白で区切って読まれるため、空白を含んだ文字列を読み込むことが出来ません。ストリームから一行をそのまま読み込むには他の方法を使います。ただし、この方法は古いC++の頃から用意されているもので、C言語文字列を対象にしています。そのため、

char buf[256];

    std::cin.getline( buf, 256 );

という感じになります。getlineメソッドは行末まで読み込んだ後、行末の改行文字を除去します。getメソッドもあり、こちらは改行文字の除去を行いません。

実習4

ここまでの内容を実際にやってみましょう。consoleio.cxxを書きます。

#include <iostream>
#include <string>
main( ) { int l; std::cout << "Input a number: "; std::cin >> l; std::cout << l << std::endl; std::string s; std::cout << "Input a string: "; std::cin >> s; std::cout << s << std::endl; double d; std::cout << "Input an integer, a string and a floating number: "; std::cin >> l >> s >> d; std::cout << l << " : " << s << " : " << d << std::endl; }

実行しましょう。

g++ consoleio.cxx
./a.out

Input a number: 10
10
Input a string:
abc
abc
Input an integer, a string and a floating number:
20 def 10.5
20 : def : 10.5

2.5.ストリームの整形

特に出力の場合、表示桁数や表示方式(固定小数点350.0、科学計算3.5e2など、あるいは10進法、8進法、16進法)などを制御したくなります。これらの制御に使われる方法は二つあります。

メソッドを使って整形する

まず、メソッドを使って整形します。出力幅を指定します。widthメソッドを使います。

    std::cout.width( 10 );
    std::cout << 12345 << std::endl;

この例を実行すると、出力に幅10文字分が取られます。12345という数値は5文字分を使うので、先頭に5文字分の空白が追加されます。ただし、この設定はその直後の出力でのみ有効です。毎回出力毎に設定する必要があります。

8進法で出力するには

    std::cout.setf( std::ios_base::oct, std::ios_base::basefield );
    std::cout << 12345 << std::endl;

こちらは出力制御フィールドの一つbasefieldをセットしますので、これ以降の出力に対して常に有効です。16進法では

    std::cout.setf( std::ios_base::hex, std::ios_base::basefield );

同様に10進法では

    std::cout.setf( std::ios_base::dec, std::ios_base::basefield );

これらのフィールドや値はstd名前空間のios_baseクラス(入出力のための基底クラス)で定義されています。そのためにこのように長ったらしい記述が必要です。もちろんスコープ内でusing namespace std;とすることでstd::の入力をサプレス出来ますが。

浮動小数点数の表示方式を変えるには同様にフィールドをセットします。科学計算表示(x.yyyyEzz)にするには

    std::cout.setf( std::ios_base::scientific, std::ios_base::floatfield );

固定表示(xxxx.yyyyyy)にするには

    std::cout.setf( std::ios_base::fixed, std::ios_base::floatfield );

通常は与えられたスペースでもっとも精度良く表示するように、これらを混ぜて使うのがデフォルトです。デフォルトに戻すには

 std::cout.setf( 0, std::ios_base::floatfield );

とします。これらの表示精度を指定するためにはprecisionメソッドを使います。有効桁数を指定します。次をやってみましょう。

    double f = 12345.6789;
    std::cout.setf( std::ios_base::scientific, std::ios_base::floatfield );
    std::cout.precision( 8 );
    std::cout.width( 12 );
    std::cout << f << std::endl;

このプログラムのscientificfixed0に変えたり、precisionwidthの数字を変えていろいろと試してみてください。

マニピュレータを使って整形する

これまでの例はostreamクラスのメソッドを使って整形しました。毎回、メソッドを呼び出す記述をしなければならないので煩雑な感じがします。出力ストリームへの書き出し<<演算の中に必要な制御を書き込めないでしょうか。そのための方法がマニピュレータと呼ばれるもので、文法的に言えば制御する関数へのポインターを<<演算に渡すことで実現します。マニピュレータの一部はiomanipヘッダーファイルで定義されています。次の一行をファイルの先頭に追加してください。

#include <iomanip>

幅を指定したいときは

    std::cout << std::setw( 8 ) << 12345 << std::endl;

と書けます。同様に進法を変更したいときも

    std::cout << std::oct << i << std::hex << i << std::dec << i << std::endl;

浮動小数点数の場合も

    std::cout << std::scientific << std::setprecision( 8 ) << f << std::endl;

等とすることで出力文の一行の中に書き込むことが出来ます。

ここまで書くとお気づきかもしれませんが、Hello, World!以来のおつきあいであるstd::endlも実はマニピュレータの一つです。現在の出力バッファに改行文字を追加してフラッシュ(強制書きだし)するというものです。

実習5

ここまでの整形を実際にやってみましょう。format.cxxを書きます。

#include <iostream>
#include <iomanip>

main( ) {

int i = 12345;
double f = 12345.6789;

    std::cout.width( 10 );
    std::cout << i << std::endl;

    std::cout.setf( std::ios_base::oct, std::ios_base::basefield );
    std::cout << i << std::endl;

    std::cout.setf( std::ios_base::hex, std::ios_base::basefield );
    std::cout << i << std::endl;

    std::cout.setf( std::ios_base::dec, std::ios_base::basefield );

    std::cout.setf( std::ios_base::scientific, std::ios_base::floatfield );
    std::cout << f << std::endl;

    std::cout.setf( std::ios_base::fixed, std::ios_base::floatfield );
    std::cout << f << std::endl;

    std::cout.setf( std::ios_base::scientific, std::ios_base::floatfield );
    std::cout.precision( 8 );
    std::cout.width( 12 );
    std::cout << f << std::endl;

    std::cout << std::setw( 8 ) << i << std::endl;
    std::cout << "oct: " << std::oct << i
              << " hex: " << std::hex << i
              << " dec: " << std::dec << i << std::endl;
    std::cout << std::scientific << std::setprecision( 8 ) << f << std::endl;



}

実行しましょう。

g++ format.cxx
./a.out

12345
30071
3039
1.234568e+04
12345.678900
1.23456789e+04
12345
oct: 30071 hex: 3039 dec: 12345
1.23456789e+04

2.6.ファイルストリーム

これまでの入出力ではostreamistreamを対象としており、任意のストリームを使えます。これらのストリームをファイル対象に特化したクラスがofstreamifstreamと呼ばれるものです。当然ostreamistreamを継承していますのでこれまでのすべての説明が有効であるほか、ファイル固有の扱いもあります。

ファイルストリームの生成

ファイルはUNIXではディスク上のファイルとプログラム上の入出力とを結びつける操作を行うことでプログラムの中からファイル入出力が出来るようになります。オペレーティングシステムレベルではopen操作と呼びます。これは、ファイルストリームではコンストラクタの中で行われます。

ファイルの開設にはいくつかの情報が必要です。ファイル名はもちろんですが、それ以外に、そのファイルが既に存在するのかどうか、ファイルを開いたときに書き込み位置や読み込み位置がどこにあるべきか、読み込み専用か、書き込み専用か、読み書きの両方を行うかなどです。これらの情報をコンストラクタに与えてファイルを開くことになります。

次をやってみましょう。出力ファイルストリームクラスofstreamを使います。

#include <fstream>

main( ) {
    std::ofstream ofs( "test.dat" );
    for( int i = 0; i < 10; i ++ )
        ofs << i << std::endl;
}

これをコンパイルして実行してみてください。test.datというファイルが出来ていますね。中身をcatすると0から9までの数字が縦に並んでいることでしょう。a.outを繰り返し実行してもtest.datの中身は変わりません。デフォルトでofstreamはトランケートモード(ファイルの中身を空にしてから書き出す)でファイルを開きます。ofsを生成した行を次のように修正してみてください。

    std::ofstream ofs( "test.dat", std::ios_base::app );

今度はアペンドモード(ファイルの最後まで進んでから書き出す)になります。

モードと指定できるものには次のものがあります。std名前空間のios_baseクラスの中で定義されています。

in
入力として開きます。ifstreamの場合のデフォルトです。
out
出力として開きます。ofstreamの場合のデフォルトです。
app
オープン後に末尾に追記していきます。
trunc
オープン後、ファイルサイズを0にしてから書き込みます。先頭から上書きされます。
binary
バイナリーファイルとして開きます。行の概念が当てはまりません。

ファイルの状態

ファイルの入出力を行う場合、その入出力がうまくいくかどうかいろいろと注意をする必要があります。例えばファイルを開こうとしても、指定されたファイルが存在しない場合など。

実習6

次をやってみましょう。fstat.cxxとします。

#include <iostream>
#include <fstream>
int main( int argc, char ** argv ) 
{
    argv ++;
    std::ifstream ifs( * argv );
    if( ifs.fail( ) ) {
        std::cerr << "File open failuer." << std::endl;
        return 1;
    }
}

main関数に引数argcargvをつけたのは、コマンド行に与えられた引数をプログラム内で使用するためです。argvは文字列並びを指しています。最初の文字列がコマンド名a.outを示しており、その次からが付加された引数を示します。argv++は最初のコマンド名をスキップし、最初の引数をargvが指すようにするためです。main関数は元来整数型の値を返す関数です。returnを使って戻り値を与えてますので、整数型の値を返すことを明示しています。int mainというところです。

これを使って、

$ g++ fstat.cxx
$ ./a.out test1.dat
File open failure
$ ./a.out test.dat
$

先ほどの例でtest.datは存在しますが、test1.datは存在しません。failメソッドによって、ファイル開設に失敗していることがわかります。

また、ファイルを読み込んでいくと、どこかで読み終わるはずです。それを検出することも必要です。

実習7

次を、先ほどのfstat.cxxの最後の}の前に追加してください。

    while( ! ifs.eof( ) ) {
        char buf[ 256 ];
        ifs.getline( buf, 256 );
        std::cout << buf << std::endl;
    }

これを使って例えば

$ g++ fstat.cxx
$ ./a.out fstat.cxx
#include <iostream>
...
$

コマンドラインで指定されたファイルを一行ずつ読み込み、coutに出力します。eofメソッドでファイル末尾にたどり着いたかどうか判断しています。

コンストラクタや入力動作メソッド、演算に例外を投げさせることも出来ます。

実習8

fexcep.cxxとして次をやってみましょう。

#include <iostream>
#include <fstream>

int main( int argc, char ** argv ) {
    std::ifstream ifs;        //ファイル名指定無しでストリームを生成
    ifs.exceptions( std::ios_base::failbit | std::ios_base::eofbit );    //例外を許可
    try {
        argv ++;
        ifs.open( * argv );        //改めてファイルを開設
    }
    catch( std::ios_base::failure ) {    //例外の受け取り。ファイル開設エラー
        std::cout << "File open failure = " << * argv << std::endl;
        return 1;
    }

    try {
        while( 1 ) {
            char buf[ 256 ];
            ifs.getline( buf, 256 );
            std::cout << buf << std::endl;
        }
    }
    catch( std::ios_base::failure ) {    //例外の受け取り。EOF検出
        return 0;        //正常終了
    }
    return 1;
}        

trycatchで例外を受け取れるようにします。ifstreamオブジェクトのexceptionsメソッドで例外を投げる種類を指定しました。ファイル開設エラーを検出するために、ifstreamオブジェクトをファイルと結びつけない状態で生成し、後ほど、openメソッドによってファイルを割り付けます。

2.7.文字列ストリーム

今回の講習内容の最初にやったstringオブジェクトをストリームに見立てて入出力を行うことが出来ます。この方法を使えば、組込型やクラスオブジェクトの出力機能を使って、上手に整形されたメッセージ文字列を作ることが出来ます。また、一旦文字列として読み込んだデータを、組込型やクラスの入力機能を使って変換してゆくという使い方も出来ます。

sstreamヘッダーファイルで定義されるistringstreamostringstreamを使用します。

#include <sstream>
#include <iostream>

main( int argc, char ** argv ) {
    std::ostringstream oss;
    oss << "The command line has " << argc - 1 << " arguments:";
    for( int i = 1; i < argc; i ++ ) {
        argv ++;
        oss << " " << * argv;
    }
    std::istringstream iss( oss.str( ) );
    while( !iss.eof( ) ) {
        std::string s;
        iss >> s;
        std::cout << s << std::endl;
    }
}

コマンド行にいろいろ引数を与えて試してみてください。

2.8.クラスオブジェクトの入出力

自分で作成したクラスのオブジェクトをストリームに書き出すにはどうしたらよいでしょうか。組込型のデータについては本来のストリームが面倒を見てくれますが、ユーザのクラスは自動的にはやってくれません。それでは、ostreamクラスを継承して自分のクラスも出力が出来るようにして、それを使うのでしょうか。これは全く好ましくありません。誰もそんなストリームを使ってくれません。無関係なクラスの出力をどんどん付け加えていったとんでもない化け物のようなostreamが出来てしまいます。

<<演算を、ostreamクラスに属する演算と考えるのではなくて、第一引数にostreamの参照を取り、第二引数にあなたのクラスのオブジェクト(の参照)を取る、そして、ostreamへの参照を戻り値として持つ大域的な演算子と考えることとします。大域的な演算子<<にどんどんオーバーロードしていくことになります。具体例を考えましょう。まずPhoneBookクラスを考えます。PhoneBook.h

#ifndef __PHONEBOOK_H__
#define __PHONEBOOK_H__
#include <string>
#include <iostream>

class PhoneBook {
public:
    PhoneBook( const char * pname, int number );
    std::string & getName( ) { return m_name; }
    int getNumber( ) { return m_number; }
protected:
    std::string m_name;
    int m_number;
};
#endif

メソッドの実装はお任せします(PhoneBook.cxx)。さて、次のようなことをしたいわけです。

    PhoneBook myphonebook[ 10 ];
    //ここでmyphonebookの中身を埋める。
    for( int i=0; i < 10; i ++ )
        std::cout << myphonebook[ i ] << std::endl;

これが出来るようにするには次のような宣言が必要です。これをPhoneBook.hの末尾、#endifの直前に追加してください。

std::ostream & operator << ( std::ostream & os, PhoneBook & pb );

この関数の実装は次のようになるでしょう。PhoneBook.cxxに次を追加します。

std::ostream & operator << ( std::ostream & os, PhoneBook & pb ) {
    os << pb.getName( ) << ": " << pb.getNumber( );
    return os;
} 

この演算子<<は大域的演算子であるので、PhoneBookの私的メンバーにアクセスすることが出来ません。そのため、この例ではゲッターメソッドと呼ばれるgetNameとかgetNumberとかのメンバーを用意してpublicメンバーを使って出力コードを書いています。

場合によってはPhoneBookクラス定義の中で

friend std::ostream & operator << ( std::ostream & os, PhoneBook & pb );

friend宣言することによって直接プライベートメンバーを見せてしまうことも出来ます。friendは使わないようにするのがポリシーです。

実習9

ここまで説明したPhoneBookクラスの例をまとめておきましょう。

PhoneBook.hでクラスPhoneBookを定義します。この中で大域オペレータoperator << をオーバーロードします。

#ifndef __PHONEBOOK_H__
#define __PHONEBOOK_H__
#include <string>
#include <iostream> class PhoneBook { public: PhoneBook( const char * pname, int number ) : m_name( pname ), m_number( number ) {}; std::string & getName( ) { return m_name; } int getNumber( ) { return m_number; } protected: std::string m_name; int m_number; }; std::ostream & operator << ( std::ostream & os, PhoneBook & pb ); #endif

PhoneBook.cxxですが、大域オペレータのみ定義しています。

#include <iostream>
#include "PhoneBook.h"

std::ostream & operator << ( std::ostream & os, PhoneBook & pb ) {
os << pb.getName( ) << ": " << pb.getNumber( );
return os;
}

main.cxxを記述します。

#include <iostream>
#include "PhoneBook.h"
main( ) { PhoneBook * pb[ 4 ]; pb[ 0 ] = new PhoneBook( "sakamoto", 1234 ); pb[ 1 ] = new PhoneBook( "nakamura", 2345 ); pb[ 2 ] = new PhoneBook( "kishimoto", 4567 ); pb[ 3 ] = new PhoneBook( "maeda", 5678 ); for( int i = 0; i < 4; i ++ ) std::cout << * pb[ i ] << std::endl; }

 

実行しましょう。

g++ -c PhoneBook.cxx
g++ -c main.cxx
g++ main.o PhoneBook.o
./a.out

sakamoto: 1234
nakamura: 2345
kishimoto: 4567
maeda: 5678

3.数値計算

最後に、数値計算で必要になるいくつかの情報をまとめておきます。

3.1.限界値

第二講でも有効桁数について説明しました。そのときはC言語のマクロを使いましたが、本来マクロは使うべきではありません。その代わりに限界値を与えるクラステンプレートnumeric_limitsが用意されています。これはこのオブジェクトを作るのではなくて、静的メンバーを呼ぶことで表現の限界値を得ることが出来るようになっています。limits.cxxを作ってみましょう。

#include <limits>
#include <iostream>

main( ) {
    std::cout << "Min. of int: " << std::numeric_limits<int>::min( ) << std::endl;
    std::cout << "Max. of int: " << std::numeric_limits<int>::max( ) << std::endl;

    std::cout << "Min. of double: " << std::numeric_limits<double>::min( ) << std::endl;
    std::cout << "Max. of double: " << std::numeric_limits<double>::max( ) << std::endl;
    std::cout << "Epsilon of double: " << std::numeric_limits<double>::epsilon( ) << std::endl;
}

intlong longshortdoublefloatなどに置き換えてもやってみましょう。

3.2.数学関数

従来、C言語では数学関数を使用する際はmath.hヘッダーファイルを読み込んでいました。ANSI-C++ではcmathもしくはmath.hを読み込む規格になっています。しかし実際には匿名名前空間、およびstd名前空間の両方で定義されているようです。

#include <iostream>
#include <cmath>

main( ) {
    double f = 2.0;
    std::cout << "sqrt of " << f << " is " << ::sqrt( f ) << std::endl;
    std::cout << "sqrt of " << f << " is " << std::sqrt( f ) << std::endl;
}

最初の例は匿名空間に置かれた::sqrtを使っています。これを次の例のようにstd::sqrtとしても問題なくコンパイルできます。cmathではsqrt(d), pow(d,e), sin(d), cos(d), tan(d), asin(d), acos(d), atan(d), atan2(d,e), sinh(d), cosh(d), tanh(d), exp(d), log(d), log10(d), ceil(d), floor(d), abs(d), fabs(d)などが利用出来ます。

また、C言語では数学関数ライブラリを指定してリンクしなければなりませんでしたが、g++の3.2以降ではその必要がなくなっているようです。

乱数もプログラミングでは重要な役割を果たします。計算機で使われるのは再現性が保証されている疑似乱数です。シード(種)の値を変えることにより異なったシーケンスの乱数を発生できます。同じシードを使うと同じシーケンスになります。

#include <cstdlib>

main( ) { long int myseed = 123456; srand48( myseed ); //乱数の種を設定する。 double d = drand48( ); //0.0から1.0の範囲の疑似乱数を発生 }

 

48という数字がつけられているのはこのシリーズの疑似乱数の生成に48ビット整数が使われているからです。疑似乱数はやがて最初と同じ値に戻ってくる周期性を持っていますが、それを十分大きなインターバルにするために工夫されています。man srand48とかやってみてください。

これでC++講座を終了します。お疲れ様でした。


前へ 上へ


2017年7月23日