ATLAS日本基礎ネットワーク C++トレーニングコース
ここでの目標
目次
文字列はC++でも非常に重要なプログラミング要素です。C言語との互換性のため、現在でも古い文字列の扱いが一般的に行われています。ANSIのC++ではそれに対して標準的な文字列処理ライブラリを用意しています。出来るだけ標準ライブラリを使うことがATLASでも推奨されているため、その両方を理解しておく必要があります。
C言語では文字列を文字型(char
)データの配列として扱ってきました。単純な配列では文字列の長さなどを記憶する場所がないので、文字列は「ヌル(ゼロ)で終端される文字型配列」と言えます。ですから、Cの文字型配列は、実際の文字列の長さにヌルの分を加えた記憶域が必要となります。
文字列はコンピュータプログラムでも非常に多用されるもので、そのための様々な関数が用意されてきました。/usr/include/string.h
で定義される、strcpy
などの関数です。
#include <string.h> char b[7]; strcpy( b, "Hello!" );
文字列リテラル"Hello!"
は6個の文字を含んでいますが、それを格納するためには7文字分の記憶域が必要です。
ANSIのC++では、文字列の扱いも標準化しようと、string
クラスを用意しています。このクラスはこれまでに見てきたコンテナクラスの一種と考えることが出来ます。例えば反復子を使ってアクセスすることも可能です。コンテナであるので、例えば境界のチェックなど、これまでの文字列で出来なくてバグの原因となってきたものを改善し、より安全なプログラムにすることが可能です。その点でもstring
を使うべきでしょう。
早速使ってみましょう。いつものように作業場所を用意しましょう。
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]
とすると今度は例外を投げません。これも確かめてみましょう。
ここまでのソースコードを整理すると次のようになります。
#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
先ほどの例ではコンストラクタで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"
を挿入します。全体の長さは伸びます。
i
nsert
メソッドは一般の標準コンテナとしても持っています。ただしこの場合は要素である文字単位で挿入することになります。下の表現で'Z'
は許されますが、"ABCDEF"
は許されません。hello.insert( hello.begin( ) + 5, 'Z' );
基本的にstring
クラスのメソッドは、文字の位置を先頭からのオフセット値(size_t
型)で表します。文字の位置を返す場合も反復子ではなくオフセット値を返しています。
ここまでの分をまとめてみましょう。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.
代入演算子が定義されているように比較演算子も定義されています。==
、!=
、<
、>
、<=
、>=
がそれぞれ使えます。
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; }
文字列の一部を置換したい場合、例えば
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( )
の動作と同じです。
これまで、暗黙にC言語文字列(ヌル文字で終端される文字型配列)を引数としては使用してきました。関数多重定義によりstring
が入るところはC言語文字列も入れることが出来ます。逆にC言語の文字列処理関数(string.h
で定義される)やC言語入出力ライブラリ(printf
等)を使うときにはstring
型からC言語文字列へ変換する必要があります。その場合、c_str()
メソッドを使います。
#include <stdio.h> ... printf( "%s\n", hello.c_str() );
ANSI-C++での入出力は入出力ストリームを通して行うことになります。入出力ストリームは、文字が次々に送り出されたり次々に流れ出てくる「くち」だと考えることが出来ます。
入出力を行うためには入出力ストリームオブジェクトが必要です。実装としては、いろいろなオブジェクトの入出力を想定して、basic_iostream
クラステンプレートが用意されています。入力ストリームのテンプレートとしてbasic_istream
、出力ストリームのテンプレートとしてbasic_ostream
があります。これらを文字型データで即値化したものがostream
とistream
です。通常はこれらのストリームクラスのオブジェクトに対して入出力を行います。
日本語など2バイト文字型のデータへの入出力も用意されています。2バイト文字型で
basic_iostream
を即値化したもので、wostream
とwistream
になります。ATLASソフトウエアとしては英語を標準語としているためこれらを使うことはありませんので説明は省略します。
ostream
クラスやistream
クラスのメソッドを用いて入出力の詳細を制御していくことになります。
C言語の標準入出力としてはstdin
という標準入力、stdout
という標準出力、それにstderr
という標準エラー出力の三つが用意されています。これらはプログラムが起動されたときから開設されているもので、通常のファイルのように開いたり閉じたりする必要がありません。また、これらの標準入出力はUNIXではコンソール入出力に割り当てられていてシェルレベルでリダイレクトすることが出来るものです。
C++の入出力ストリームもこれらの標準入出力に対応したストリームを用意しています。以下に述べるものはostream
やistream
のオブジェクトです。あらかじめこれらのオブジェクトは生成されstd
名前空間の中に作られています。
これらはiostream
で定義されています。
#include <iostream> std::cout << "Hello, World!" << std::endl;
でしたね。ここに出てくるcout
はstd
名前空間の中で作られたostream
クラスの一つのオブジェクトだと言うことを覚えておいてください。
以後、入出力について説明しますが、これらは基本的にistream
やostream
の機能について述べるものです。それが標準入出力に向けられたものであっても、後に述べるファイルや文字列に対するものであっても基本的に同じクラスですので同じ振る舞いをします。
出力ストリームの典型的な使い方が
std::cout << "Hello, World!" << std::endl;
というものです。これは分析しますと、std
名前空間のcout
というオブジェクトを左辺値とし、"Hello, World!"
という文字列リテラルを右辺値とする<<
演算です。<<
演算は左結合をしますので(同じ演算が繰り返された場合左から順番に繋ぐものです。)、この<<
演算の結果を今度は左辺値としてstd
で定義されているendl
オブジェクトとの間で再度<<
演算を行います。
( std::cout << "Hello, World!" ) << std::endl;
cout
はostream
のオブジェクトですので、ostream
クラスの定義の中で文字型へのポインターを右辺値とするoperator <<
を探します。すると演算子の戻り値としてostream
型への参照を取ることがiostream
ヘッダーファイルに書いてあることがわかります。
ostream & operator << ( const char * );
一つ上の例の括弧内はostream
オブジェクトへの参照ですから(実際、cout
ですが)、上の括弧内がstd::cout
となり、
std::cout << std::endl;
ということになります。
ostream
には組込型への<<
演算がすべて定義されています。整数や浮動小数点数などはそのままostream
に<<
することが出来ます。
一般的なクラスオブジェクトのostream
への出力は定義されていません。おのおの定義する必要があります。これについては後で述べます。
出力ストリームと同様に、ストリームから値を任意の組込型変数へ読み込むことが出来ます。例えば
#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
メソッドもあり、こちらは改行文字の除去を行いません。
ここまでの内容を実際にやってみましょう。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
abc
Input a string:
abc
20 def 10.5
Input an integer, a string and a floating number:
20 : def : 10.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;
このプログラムのscientific
をfixed
や0
に変えたり、precision
やwidth
の数字を変えていろいろと試してみてください。
これまでの例は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
も実はマニピュレータの一つです。現在の出力バッファに改行文字を追加してフラッシュ(強制書きだし)するというものです。
ここまでの整形を実際にやってみましょう。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
これまでの入出力ではostream
やistream
を対象としており、任意のストリームを使えます。これらのストリームをファイル対象に特化したクラスがofstream
やifstream
と呼ばれるものです。当然ostream
やistream
を継承していますのでこれまでのすべての説明が有効であるほか、ファイル固有の扱いもあります。
ファイルは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
クラスの中で定義されています。
ファイルの入出力を行う場合、その入出力がうまくいくかどうかいろいろと注意をする必要があります。例えばファイルを開こうとしても、指定されたファイルが存在しない場合など。
次をやってみましょう。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
関数に引数argc
とargv
をつけたのは、コマンド行に与えられた引数をプログラム内で使用するためです。argv
は文字列並びを指しています。最初の文字列がコマンド名a.out
を示しており、その次からが付加された引数を示します。argv++
は最初のコマンド名をスキップし、最初の引数をargv
が指すようにするためです。main
関数は元来整数型の値を返す関数です。return
を使って戻り値を与えてますので、整数型の値を返すことを明示しています。int main
というところです。
これを使って、
$ g++ fstat.cxx $ ./a.out test1.datFile open failure
$ ./a.out test.dat $
先ほどの例でtest.dat
は存在しますが、test1.dat
は存在しません。fail
メソッドによって、ファイル開設に失敗していることがわかります。
また、ファイルを読み込んでいくと、どこかで読み終わるはずです。それを検出することも必要です。
次を、先ほどの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
メソッドでファイル末尾にたどり着いたかどうか判断しています。
コンストラクタや入力動作メソッド、演算に例外を投げさせることも出来ます。
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; }
try
とcatch
で例外を受け取れるようにします。ifstream
オブジェクトのexceptions
メソッドで例外を投げる種類を指定しました。ファイル開設エラーを検出するために、ifstream
オブジェクトをファイルと結びつけない状態で生成し、後ほど、open
メソッドによってファイルを割り付けます。
今回の講習内容の最初にやったstring
オブジェクトをストリームに見立てて入出力を行うことが出来ます。この方法を使えば、組込型やクラスオブジェクトの出力機能を使って、上手に整形されたメッセージ文字列を作ることが出来ます。また、一旦文字列として読み込んだデータを、組込型やクラスの入力機能を使って変換してゆくという使い方も出来ます。
sstream
ヘッダーファイルで定義されるistringstream
やostringstream
を使用します。
#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; } }
コマンド行にいろいろ引数を与えて試してみてください。
自分で作成したクラスのオブジェクトをストリームに書き出すにはどうしたらよいでしょうか。組込型のデータについては本来のストリームが面倒を見てくれますが、ユーザのクラスは自動的にはやってくれません。それでは、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
は使わないようにするのがポリシーです。
ここまで説明した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
最後に、数値計算で必要になるいくつかの情報をまとめておきます。
第二講でも有効桁数について説明しました。そのときは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; }
int
をlong long
やshort
、double
をfloat
などに置き換えてもやってみましょう。
従来、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++講座を終了します。お疲れ様でした。