水曜日, 2月 06, 2008

C++解説10(iterator)

イテレータとは汎用ポインタです。
っていうと間違っているような気もするのですが
そうやって覚えて問題ないと思います。

イテレータの簡単でかつ最も多様する方法に
STLが提供するコンテナの操作、参照といった事があります。

イテレータにはいくつかの種類が存在し
これを正確に理解する事で単純なバグを抑止する事ができると思います。

1.forward_iterator
前方イテレータや前進イテレータなどと呼ばれているもので
「++」というインクリメントのみの移動が可能なイテレータです。
イテレータの参照、イテレータへの更新といった操作が許されています。
参照ができるという事は比較演算子である「==」「!=」などが使用でき
更新が行えるという事は代入演算子である「=」が使用できるという事です。

2.input_iterator
forward_iteratorに対して、更新系の操作(要するに代入)を奪ったものが
input_iteratorと言います。
このイテレータに対して、代入演算子を使用するとコンパイルエラーとなります。

3.output_iterator
forward_iteratorに対して、参照系の操作を奪ったものが
output_iteratorと言います。
参照系の操作が出来ませんので、if文で使用したり、for、whileなどの終了条件に
使用する事もできません。

4.bidirectional_iterator
forward_iteratorに対して、デクリメントの機能を追加したものが
このbidirectional_iteratorと言い、双方向イテレータと呼ばれます。

5.revese_iterator
forward_iteratorに対して、インクリメントを行うと後方に進むのに対して
このrevese_iteratorはインクリメントすると前方に進みます。
この「reverse_iterator」と「forwaed_iterator」については
どちらのイテレータを使用しているかを認識していないと
進む方向が同じプログラムでも変っている事になります。

6.random_iterator
最後に、全ての機能を兼ね備えるのがrandom_iteratorと言われるものです。
bidirectonal_iteratorに何の機能が加わるかというと「[]」での参照機能です。
つまり「[]」という演算子でアクセスを行えるコンテナについては
使用されているイテレータがこのrandom_iteratorという事になります。


実際に「vector」でイテレータを使用してみます。

vector v;
のイテレータとして
vector::iterator i;
という宣言方法で「i」というイテレータを宣言します。

イテレータの型を特定するのが目的になります。
ポインタ宣言を行う場合に「char*」なのか「int*」なのか
といった事を宣言しています。

これはC言語の特徴の1つと言ってよいかもしれません。
理由は簡単で配列はポインタで表記されるのがC言語です。

int n[10];
char c[10];

というような2つの配列が存在した場合、
0番目の要素と1番目の要素の場所の間にはどのくらいの距離があるのでしょうか?
配列とは連続しているメモリー領域を約束されていますので
0番目の要素と1番目の要素が隣り合わせである事は間違いありません。
0番目にしても1番目にしても通常の「int」として扱う事ができますから
2つ間とは、配列の型のサイズである事が分かります。
つまり移動量を特定するためには、どの型であるかを知る必要があります。
そのため、「int*」「char*」と言った宣言が必要になります。
イテレータもこれと同じ理屈で型が必要という事になります。


前回あいまいのまま終わらせた中に「vector」で使用できる
アルゴリズムが他にも沢山ある事を書きました。
それを解説しながらイテレータというものを実際に使用してみます。


「vector」には「begin」「end」という
最初と最後のイテレータを返却するメソッドが存在します。

これで一番簡単なプログラムは「vector」の要素として管理されている
最初から最後までの全ての要素の値を出力するプログラムです。
それは以下の通り書くことができます。

vector v;
vector::iterator i;

for(i = v.begin(); i != v.end(); ++i) {
cout << *i << ","; } 「*i」などどいう記述はまさにポインタそのものです。 C++では様々なアルゴリズムが登場しますが ほとんどが 「i = v.begin()」~「 i != v.end()」 という考え方が基本となります 要するに「end」が指すのは、最後の位置ではなく 「最後+1」の位置という事になります。 次に「insert」です。 vector v1;
vector v2;

2つの「vector」の配列が存在し、

「v1」の後ろに「v2」を挿入する場合

v2.insert(v1.end(), v2.begin(), v2.end());

と記述する事ができ

「v1」の先頭に「v2」を挿入する場合

v2.insert(v1.begin(), v2.begin(), v2.end());

と記述する事ができます。


また途中に挿入する必要がある場合は

for(vector::iterator i = v1.begin(); i != v1.end(); ++i) {
if (*i == 挿入する配列の後にしたい要素) {
v1.insert(i, v2.begin(), v2.end());
}
}

と記述すれば良いという事になります。
これは本当に素晴らしい事です。
C言語からの進化、又はSTLで記述しないC++プログラム
と比べてもその素晴らしさは圧巻です。

「erase」もまったく同じ考え方で関数そのものは2つの引数で定義されています。

iterator erase(iterator pos);
iterator erase(iterator s_pos, iterator e_pos);
イテレータを指定する事でこのアルゴリズムも一瞬にして手の中に入っていますのです。

今回は「vector」を例に少しだけイテレータの世界に入りました。

0 件のコメント: