イテレータ

イテレータとは

コンテナ型の要素へのアクセスにはイテレータ(反復子)というものがたびたび登場します。
イテレータを一言で言い表すならば「ポインタのようなもの」です。
(内部的にはポインタを利用して実装されています)

配列やarrayクラス、vectorクラスでは、メモリ上に配置されるデータは「先頭の要素から順番に、最後まで隙間なく並べられる」ことが保障されています。
このようなデータへのアクセスはポインタだけで十分足ります。
(ただし、余りに古いコンパイラでは保障されていないかもしれません)


//メモリ上へは連続して配置される
int arr[]{ 1, 2, 3, 4, 5 };
int *pointer = arr;

//メモリ上では、arr[0]の次は確実に
//arr[1]が存在することが保障されている
//なお、以下のふたつは全く同じ意味
std::cout << pointer[1] << std::endl;
std::cout << *(pointer + 1) << std::endl;

添字演算子[]は、ポインタによるアクセスを簡単な記述で行えるように用意された構文です。
(シンタックスシュガー、糖衣構文)
配列名をそのまま書くと配列の先頭要素へのポインタを返します。
つまり、「arr[1]」は「*(arr + 1)」と同じことです。

しかし、他のコンテナ型ではメモリのあちこちにデータが散らばって配置されていることがあります。
「pointer[1]」はあくまでも「'pointer'が示すメモリ位置の次の位置の値」でしかなく、データの連続性が保障されていない場合はそこにどんなデータがあるかはわかりません。
そのため、ポインタを用いて前後の要素にアクセスする方法は使えません。
それを、ポインタのように比較的簡単に扱えるようにしたものがイテレータです。

イテレータは次のように使用を宣言します。


std::vector<int> vec{ 1, 2, 3, 4, 5 };

//becの先頭要素を示すイテレータ
std::vector<int>::iterator itr1;
itr1 = vec.begin();

//宣言と同時に初期化するなら以下でも良い
std::vector<int>::iterator itr2 = vec.begin();

//vecの最後の要素を示すイテレータ
std::vector<int>::iterator itr3 = vec.end();

先頭要素を指すイテレータを取得するにはbegin関数を使用します。
最後の要素はend関数で取得できます。

現在の要素、次の要素、前の要素を示すには以下のようにします。


std::vector<int> vec{ 1, 2, 3, 4, 5 };
std::vector<int>::iterator itr = vec.begin();

//itrが指す要素を表示
//1
std::cout << *itr << std::endl;

//次の要素に移動
itr++;

//2
std::cout << *itr << std::endl;

//前の要素に移動
itr--;

//1
std::cout << *itr << std::endl;

//3つ後ろの要素に移動
itr += 3;

//4
std::cout << *itr << std::endl;

イテレータに「*」記号を付加すると、イテレータが指す要素にアクセスできます。
ポインタとほとんど同じ感覚で扱えることが分かると思います。

イテレータの注意点

イテレータはポインタとは異なる点もあります。

移動方法の制限

コンテナ型によっては、後ろにしか移動できないイテレータ、前にしか移動できないイテレータが存在します。
後ろにしか移動できないタイプでは「itr--」といった方法で前に戻ることはできません。

また、ランダムアクセスできないイテレータも存在します。
このタイプは「itr + 3」といった方法で移動することはできません。
つまり「itr++」「itr--」で要素を順番にたどって行く移動方法しか許されていません。

これらの違いは、それぞれのコンテナ型の特徴に起因します。
不便に思えるような実装も、特定の用途では配列やvectorなどよりも便利に扱える場合があります。

いままで紹介したarrayクラス、vectorクラス、そしてstringクラスは全ての方法での移動が可能です。
今後新しいコンテナクラスを紹介する際、使えない移動方法があればその都度説明します。

stringクラスを「文字データの集合を扱うクラス」と考えれば、コンテナ型の一種という事ができます。
そのためコンテナ型に共通の関数や、イテレータによる操作も提供されています。

しかし、文字列を扱うという性質上、他のコンテナ型とは分けて考えることが多いです。

イテレータ同士の互換性はない

例え扱うデータ型が同じでも、コンテナクラスが異なる場合はそのイテレータ同士には互換性はありません。
int型arrayクラスのイテレータとint型vectorクラスのイテレータは、それぞれ別々のデータ型です。

データが連続していないクラスの検証コード

以下はメモリ上にデータが連続して配置されないコンテナクラスの検証コードです。
listクラスはまだ説明していませんが、メモリ上のデータに連続性がないクラスです。


#include <iostream>
#include <vector>
#include <list>

int main()
{
    std::vector<int> vec{ 55, 3, 97, -21, 6 };
    std::list<int> lst{ 55, 3, 97, -21, 6 };

    std::vector<int>::iterator itrVec = vec.begin();
    std::list<int>::iterator itrLst = lst.begin();

    //vectorの各要素のアドレスを表示
    std::cout << "vector" << std::endl;
    while (itrVec != vec.end())
    {
        std::cout << &(*itrVec) << std::endl;
        itrVec++;
    }

    //listの各要素のアドレスを表示
    std::cout << "\nlist" << std::endl;
    while (itrLst != lst.end())
    {
        std::cout << &(*itrLst) << std::endl;
        itrLst++;
    }

    std::cin.get();
}
vecotr
00EDB618
00EDB61C
00EDB620
00EDB624
00EDB628

list
00EE1048
00EE1080
00EE1068
00EE10B8
00EE1058

※実行結果は実行毎に異なります。

vectorクラスはアドレスが4ずつ(int型ひとつ分)増えていることが確認できますが、listクラスはそういった規則性はありません。

コンテナ型使用時の注意点

arrayクラスを除き、コンテナ型は値の削除や追加が自由に行えます。
これは便利な反面、気を付けないとバグのあるコードを書いてしまいます。

vectorクラスでは、途中の要素を削除した場合は後ろのデータは前にずらされます。

この時、その「後ろのデータ」や、「今削除した要素」を指しているイテレータは使用できなくなります。
データの移動により、正しいアドレスではなくなっているためです。


//間違いコード

std::vector<int> vec{ 33, 16, 21, 91, 8 };
std::vector<int>::iterator itr = vec.begin();

++itr;
vec.erase(itr);

//ここでエラー
std::cout << *itr << std::endl;

反対に、insert関数やpush_back関数などで要素を増やした場合、確保していたメモリ領域が足りなくなると別の領域にメモリを確保し、そこを新しい領域とします。
つまりアドレスが変わるので、イテレータも無効になります。

insert関数やerase関数など、要素のアドレスが変わるような関数はイテレータを戻り値にするものがあります。
その戻り値を新しいイテレータとして使用すればエラーを防ぐことができます。


std::vector<int> vec{ 33, 16, 21, 91, 8 };
std::vector<int>::iterator itr = vec.begin();

++itr;
//戻り値をイテレータに代入
itr = vec.erase(itr);

//正常に表示可能
std::cout << *itr << std::endl;

ループ文を利用して、条件に合う要素を削除する場合は以下のようにします。


using std::vector;

vector<int> vec1{ 33, 16, 21, 91, 8 };
vector<int>::iterator itr = vec1.begin();

//値が偶数の要素を削除
while (itr != vec1.end())
{
	if (*itr % 2 == 0)
		itr = vec1.erase(itr);
	else
		++itr;
}

//for文で書く場合
vector<int> vec2{ 33, 16, 21, 91, 8 };
//再初期化式(括弧内のみっつ目の式)で
//イテレータをインクリメントしないこと
for (vector<int>::iterator itr = vec2.begin(); itr != vec2.end();)
{
	if (*itr % 2 == 0)
		itr = vec2.erase(itr);
	else
		++itr;
}

他のコンテナクラスでもデータの追加/削除を行うと、それまで保存していたイテレータが無効になることが多いので注意しましょう。