C++のイテレータの使い方について

AI実装検定のご案内

C++のイテレータ(iterator)とは、vectorlistmapなどのコンテナに格納されている要素を順番に参照したり、操作したりするための仕組みです。

イテレータはポインタに似た操作方法を持っており、現在指している要素を取得したり、次の要素へ移動したりできます。

例えば、std::vectorの要素を先頭から順番に表示する場合は、次のように記述します。

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {10, 20, 30, 40, 50};

    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << std::endl;
    }

    return 0;
}

実行結果は次のとおりです。

10
20
30
40
50

イテレータを理解するうえでは、まずbegin()end()*it++itという4つの基本操作を押さえることが重要です。

目次

C++のイテレータの基本的な使い方

begin()で先頭要素を取得する

begin()は、コンテナの先頭要素を指すイテレータを返します。

std::vector<int> numbers = {10, 20, 30};

auto it = numbers.begin();

std::cout << *it << std::endl;

実行結果は次のようになります。

10

itには先頭要素を指すイテレータが格納されています。

イテレータが指している実際の要素を取得するときは、*演算子を使用します。

*it

この操作は「参照外し」と呼ばれます。

end()は最後の要素を指すわけではない

end()は、コンテナの最後の要素を指すイテレータではありません。

最後の要素の直後にある「終端位置」を表すイテレータを返します。

例えば、次のようなvectorがあるとします。

std::vector<int> numbers = {10, 20, 30};

概念的には、次のようになります。

begin()
  ↓
[10][20][30]
            ↑
        最後の要素

                ↑
              end()

end()が指す位置には実際の要素が存在しません。

そのため、次のようにend()を参照外ししてはいけません。

*numbers.end()

このような操作は未定義動作につながります。

通常、end()はループの終了条件として使用します。

it != numbers.end()

++で次の要素へ移動する

イテレータは++演算子を使うことで、次の要素へ移動できます。

std::vector<int> numbers = {10, 20, 30};

auto it = numbers.begin();

std::cout << *it << std::endl;

++it;

std::cout << *it << std::endl;

実行結果は次のとおりです。

10
20

このように、イテレータを1つずつ進めながらコンテナ内を走査できます。

autoを使うとイテレータを簡潔に記述できる

C++11以降では、イテレータの型を明示的に記述する代わりにautoを使うことが一般的です。

例えば、次のコードは、

std::vector<int>::iterator it = numbers.begin();

次のように簡潔に記述できます。

auto it = numbers.begin();

特にstd::mapなどでは、イテレータの型が長くなるためautoが便利です。

std::map<std::string, int>::iterator it = scores.begin();

これを次のように書けます。

auto it = scores.begin();

型が明確に推論できる場面では、autoを使用することでコードの可読性を高められます。

イテレータを使って要素を書き換える方法

通常のイテレータでは、現在指している要素の値を変更できます。

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {1, 2, 3};

    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        *it *= 10;
    }

    for (auto value : numbers) {
        std::cout << value << std::endl;
    }
}

実行結果は次のようになります。

10
20
30

*itは現在の要素を参照しているため、

*it *= 10;

とすることでコンテナ内の値そのものを変更できます。

const_iteratorで読み取り専用にする

const_iteratorとは

コンテナ内の要素を読み取るだけで、変更したくない場合はconst_iteratorを利用できます。

std::vector<int> numbers = {10, 20, 30};

std::vector<int>::const_iterator it;

for (it = numbers.cbegin(); it != numbers.cend(); ++it) {
    std::cout << *it << std::endl;
}

const_iteratorを使用している場合、次のような変更はできません。

*it = 100;

cbegin()とcend()を使う

C++11以降では、cbegin()cend()を使うことで、読み取り専用のイテレータを明示的に取得できます。

auto it = numbers.cbegin();

要素を変更する必要がない処理では、cbegin()cend()を使用すると処理の意図が分かりやすくなります。

reverse_iteratorで逆順に処理する

コンテナの要素を後ろから順番に処理したい場合は、reverse_iteratorを利用できます。

std::vector<int> numbers = {10, 20, 30};

for (auto it = numbers.rbegin(); it != numbers.rend(); ++it) {
    std::cout << *it << std::endl;
}

実行結果は次のとおりです。

30
20
10

rbegin()から走査を開始すると、最後の要素から逆方向に要素を参照できます。

rend()は逆方向の走査における終端を表します。

読み取り専用の逆イテレータとして、crbegin()crend()も利用できます。

vectorでイテレータを使う方法

vectorではランダムアクセスができる

std::vectorのイテレータは、任意の位置へ移動できるランダムアクセスの機能を持っています。

std::vector<int> numbers = {10, 20, 30, 40, 50};

auto it = numbers.begin();

std::cout << *it << std::endl;
std::cout << *(it + 2) << std::endl;

実行結果は次のとおりです。

10
30

vectorのイテレータでは、次のような操作も可能です。

it + 2
it - 1
it += 3
it -= 2
it[3]

また、C++20以降の考え方では、std::vectorのイテレータは要素がメモリ上で連続していることを表現できる連続イテレータとしての性質も持っています。

listでイテレータを使う方法

listではit + 2のような操作はできない

std::listでも基本的なイテレータの使い方は同じです。

#include <iostream>
#include <list>

int main() {
    std::list<int> numbers = {10, 20, 30};

    for (auto it = numbers.begin(); it != numbers.end(); ++it) {
        std::cout << *it << std::endl;
    }
}

ただし、std::liststd::vectorとは異なり、次のような操作はできません。

it + 2

std::listは連結リストであり、基本的には要素を1つずつたどる必要があるためです。

std::advance()で複数個先へ進める

複数個先へイテレータを進めたい場合は、std::advance()を利用できます。

#include <iterator>

auto it = numbers.begin();

std::advance(it, 2);

std::cout << *it << std::endl;

std::advance()は、指定した数だけイテレータ自体を移動させます。

ただし、listでは移動距離に応じて要素を順番にたどる必要があるため、vectorのランダムアクセスと同じ速度で移動できるわけではありません。

mapでイテレータを使う方法

firstでキー、secondで値を取得する

std::mapでは、1つの要素がキーと値の組み合わせになっています。

#include <iostream>
#include <map>
#include <string>

int main() {
    std::map<std::string, int> scores = {
        {"Alice", 80},
        {"Bob", 90},
        {"Carol", 70}
    };

    for (auto it = scores.begin(); it != scores.end(); ++it) {
        std::cout << it->first
                  << ": "
                  << it->second
                  << std::endl;
    }
}

mapのイテレータでは、

it->first

でキーを取得し、

it->second

で値を取得できます。

mapの値は変更できる

値については、次のように変更できます。

it->second = 100;

一方、std::mapの要素ではキー部分がconst Keyとして扱われるため、イテレータ経由でキーを直接変更することはできません。

it->first = "NewKey";

このようなコードは使用できません。

イテレータを使って要素を削除する方法

erase()で要素を削除する

イテレータは、コンテナ内の特定の要素を削除するときにも利用されます。

例えば、vectorの2番目の要素を削除する場合は次のように記述できます。

std::vector<int> numbers = {10, 20, 30, 40};

auto it = numbers.begin() + 1;

numbers.erase(it);

削除後の内容は次のようになります。

10
30
40

ループ中に削除するときは戻り値を利用する

ループ中に要素を削除する場合は、イテレータの扱いに注意が必要です。

例えば、偶数だけを削除する場合は次のように書けます。

std::vector<int> numbers = {1, 2, 3, 4, 5, 6};

for (auto it = numbers.begin(); it != numbers.end();) {
    if (*it % 2 == 0) {
        it = numbers.erase(it);
    } else {
        ++it;
    }
}

erase()は、削除した要素の次の要素を指すイテレータを返します。

そのため、

it = numbers.erase(it);

とすることで、安全に次の要素から処理を続けられます。

vectorではイテレータの無効化に注意する

erase()すると削除位置以降のイテレータが無効になる

std::vectorerase()を実行すると、削除された位置以降を指していたイテレータや参照は無効になります。

例えば、次のようなコードがあります。

std::vector<int> numbers = {10, 20, 30, 40};

auto it1 = numbers.begin();
auto it2 = numbers.begin() + 2;

numbers.erase(numbers.begin() + 1);

この場合、削除位置より前を指しているit1はそのまま使用できます。

一方、削除位置以降を指していたit2は無効になります。

そのため、erase()後は戻り値として返された新しいイテレータを使うのが重要です。

push_back()でもイテレータが無効になることがある

std::vectorでは、push_back()によってメモリの再配置が発生することがあります。

std::vector<int> numbers = {1, 2, 3};

auto it = numbers.begin();

numbers.push_back(4);

push_back()によって再配置が発生した場合、それまで取得していたすべてのイテレータや参照は無効になります。

その状態で、

std::cout << *it;

とするのは危険です。

再配置が発生しなかった場合、既存要素を指すイテレータや参照は基本的に維持されます。

ただし、それまで取得していたend()イテレータは無効になります。

vectorに要素を追加・削除しながらイテレータを利用する場合は、無効化のルールを意識する必要があります。

std::next()で数個先のイテレータを取得する

現在のイテレータそのものを変更せずに、数個先のイテレータを取得したい場合はstd::next()を使用します。

#include <iterator>

auto it = numbers.begin();

auto nextIt = std::next(it, 2);

この場合、nextItは2要素先を指しますが、元のitは変更されません。

一方、

std::advance(it, 2);

では、itそのものが2要素先へ移動します。

この違いを理解しておくと、イテレータを扱いやすくなります。

std::prev()で前の要素を取得する

現在位置より前のイテレータを取得したい場合はstd::prev()を利用できます。

auto it = numbers.end();

auto previous = std::prev(it);

std::cout << *previous << std::endl;

end()自体は要素を指していませんが、

std::prev(numbers.end())

とすることで、最後の要素を指すイテレータを取得できます。

ただし、std::prev()を使用するには、後方へ移動できるイテレータである必要があります。

C++のイテレータには種類がある

イテレータは対応する操作によって分類される

C++のイテレータは、どのような操作に対応しているかによって複数の種類に分類されます。

代表的なものは次のとおりです。

種類主な特徴
Input Iterator前方向に読み取りながら進む
Output Iterator前方向に書き込みながら進む
Forward Iterator前方向に繰り返し走査できる
Bidirectional Iterator前後に移動できる
Random Access Iterator任意の位置へ高速に移動できる
Contiguous Iterator要素がメモリ上で連続している

例えば、std::vectorではランダムアクセスができます。

it += 3;

一方、std::listでは、

++it;
--it;

のように前後へ移動できますが、

it += 3;

のようなランダムアクセスはできません。

なお、C++20以降ではイテレータに関するConceptsも導入されており、従来のイテレータカテゴリと合わせてより厳密な性質を表現できるようになっています。

範囲for文とイテレータの違い

単純な繰り返し処理なら範囲for文が便利

コンテナ内のすべての要素を単純に処理するだけであれば、現代のC++では範囲for文を使うと簡潔に記述できます。

std::vector<int> numbers = {10, 20, 30};

for (int value : numbers) {
    std::cout << value << std::endl;
}

要素を書き換える場合は参照を使用します。

for (int& value : numbers) {
    value *= 10;
}

読み取りだけを行い、不要なコピーも避けたい場合は次のような書き方もできます。

for (const auto& value : numbers) {
    std::cout << value << std::endl;
}

イテレータが必要になる場面も多い

単純な全要素の走査であれば範囲for文が便利ですが、イテレータが不要になるわけではありません。

例えば、

container.erase(it);

のように現在位置の要素を削除したり、標準アルゴリズムを使用したりするときにはイテレータが重要になります。

標準アルゴリズムとイテレータを組み合わせる

std::find()で要素を検索する

C++標準ライブラリでは、イテレータとアルゴリズムを組み合わせて使用する場面が多くあります。

例えば、std::find()を使うと特定の値を検索できます。

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {10, 20, 30, 40};

    auto it = std::find(numbers.begin(), numbers.end(), 30);

    if (it != numbers.end()) {
        std::cout << "見つかりました: " << *it << std::endl;
    }
}

std::find()は、指定した範囲内に値が見つかった場合、その要素を指すイテレータを返します。

見つからなかった場合はend()を返します。

そのため、

if (it != numbers.end())

と確認してから参照する必要があります。

[begin, end)という範囲を理解する

C++標準ライブラリでは、次のような範囲指定が頻繁に使われます。

[begin, end)

これは、beginが指す要素を含み、endが指す位置を含まない半開区間を意味します。

例えば、

std::find(numbers.begin(), numbers.end(), 30);

では、先頭要素から最後の要素までが検索対象となり、end()自体は検索対象にはなりません。

C++20以降ではRangesも利用できる

C++20以降では、Rangesライブラリを利用することで、コンテナ全体を直接アルゴリズムに渡せる場合があります。

#include <algorithm>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers = {10, 20, 30};

    auto it = std::ranges::find(numbers, 20);

    if (it != numbers.end()) {
        std::cout << *it << std::endl;
    }
}

従来は、

std::find(numbers.begin(), numbers.end(), 20);

と書いていました。

Rangesを使用すると、

std::ranges::find(numbers, 20);

のようにコンテナやRangeを直接指定できるため、コードを簡潔にできる場合があります。

ただし、Rangesを使う場合でも、内部ではイテレータや範囲という考え方が重要です。

そのため、C++を学習するうえでは基本的なイテレータの仕組みを理解しておく必要があります。

C++のイテレータとポインタの違い

イテレータはポインタそのものではない

イテレータとポインタは操作方法がよく似ています。

例えば、どちらも次のような操作が可能です。

*it
++it

そのため、イテレータは「コンテナを走査するために一般化されたポインタのようなもの」と考えると理解しやすいでしょう。

ただし、イテレータとポインタは必ずしも同じ型ではありません。

例えば、

int* p;

はポインタ型ですが、

std::vector<int>::iterator it;

std::vectorが提供するイテレータ型です。

したがって、

イテレータ = ポインタ

と考えるのではなく、

イテレータはポインタに似た操作ができる仕組み

と理解するのが適切です。

C++のイテレータを覚えるポイント

C++のイテレータを初めて学ぶ場合は、まず次の4つを覚えると理解しやすくなります。

先頭要素を指すイテレータを取得するには、

container.begin()

を使用します。

終端位置を取得するには、

container.end()

を使用します。

現在の要素を取得するには、

*it

とします。

次の要素へ移動するには、

++it

を使用します。

基本的なループは次の形です。

for (auto it = container.begin();
     it != container.end();
     ++it) {
    // *itを処理する
}

この基本形を理解しておけば、vectorlistsetmapなど、さまざまな標準コンテナに応用できます。

現代のC++では範囲for文やRangesを使うことで、イテレータを直接記述しない場面も増えています。

しかし、要素の検索や削除、標準アルゴリズムの利用、コンテナごとの動作を理解するうえでは、イテレータの知識が欠かせません。

C++のコンテナや標準ライブラリを本格的に使いこなすためにも、begin()end()を中心とした基本的な仕組みから理解しておくことが大切です。

以上、C++のイテレータの使い方についてでした。

最後までお読みいただき、ありがとうございました。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次