C++ Learning

第13回 イテレータと const の基本

std::sort(v.begin(), v.end())begin()end() ― それがイテレータです。ポインタを一般化した「コンテナの要素を指す印」で、STL アルゴリズムすべての基礎。ここが腑に落ちると、std::findstd::transformstd::accumulate が全部同じパターンで書けることが見えてきます。可視化アニメで「歩く印」の動きを体感しましょう。

このページで押さえること
✅ 最低限ここだけ覚える
  • イテレータ = コンテナの要素を指す印(ポインタのようなもの)
  • v.begin() は最初の要素、v.end()最後の要素の次
  • *it で値、++it で次へ
  • 読むだけなら const_iteratorv.cbegin()
⭐ 余裕があれば読む
  • 半開区間 [begin, end) の美しさ
  • イテレータの 5 カテゴリ
  • std::find / std::distance
  • std::advance / std::prev / std::next

1. まず触ってみる ― 「歩く印」を動かす

イテレータは「コンテナのどこか 1 か所を指す印」です。C のポインタをイメージしてください。++ で次の要素に進み、* で指している値を取り出します。下のデモで実際に動かしてみましょう。

まずは 4 行だけ

first_iter.cppC++ 最小例
std::vector<int> v = {10, 20, 30}; auto it = v.begin(); // ← 先頭要素を指す std::cout << *it; // 10 (値の取り出し) ++it; // 次の要素へ std::cout << *it; // 20
ここまでで覚えること(3 つ):
  • イテレータはコンテナの位置を指す印(ポインタと同じ感覚)
  • *it値の読み書き++it
  • v.begin() が先頭、v.end()末尾の次(要注意)

よくある素朴な疑問

Q. ポインタとどう違うの?
→ vector の場合、実際ほぼポインタと同じです(内部でポインタで実装されている)。ただ、std::mapstd::list のようにメモリが連続していないコンテナでも統一的に使えるのがイテレータの強み。「歩き方を抽象化したポインタ」と思ってください。

Q. なぜ end() は「末尾の次」なの?
→ ループを while (it != end) で書きやすくするため。半開区間 [begin, end) は数学的にも実務的にも扱いやすい設計です。§2 で詳しく。

Q. 範囲 for(for (auto x : v))があればイテレータ要らない?
→ 日常の読み書きループは範囲 for で OK。でも STL アルゴリズムstd::sortstd::find など)を使うときは必ずイテレータを渡します。覚えておけば応用が利く機能です。

2. begin/end と半開区間

イテレータの最大の特徴は「範囲は [begin, end) の半開区間」で表すこと。begin含むend含まない

v = {10, 20, 30, 40, 50} の場合
[
10
20
30
40
50
)
↑ v.begin() v.end() ↑

end() が「実在しない 1 つ先」を指すおかげで、以下の便利さが生まれます:

重要:*v.end() は NG。 end が指す先には要素がないので、デリファレンス(*)すると未定義動作。「end の 1 つ前」が最後の有効要素です:*(v.end() - 1) または v.back()

C の配列と比較すると腑に落ちる

C でも「配列の末尾」を示すのに、arr + n(最後の要素の 1 つ先のアドレス)を使う慣習がありました:

C の感覚C
int arr[5] = {10, 20, 30, 40, 50}; int* begin = arr; int* end = arr + 5; // 1 つ先 for (int* p = begin; p != end; ++p) printf("%d ", *p);
C++ のイテレータC++
std::vector<int> v = {10, 20, 30, 40, 50}; auto begin = v.begin(); auto end = v.end(); for (auto it = begin; it != end; ++it) std::cout << *it << ' ';

書き方はほぼ同じ。 イテレータは「ポインタの考え方を、map や list にも使えるよう抽象化したもの」と理解するのが一番分かりやすい。

3. イテレータの基本操作

覚えるべき操作は 5 つだけ。順に見ていきます。

*it ― 値の読み書き

デリファレンス*
auto it = v.begin(); std::cout << *it; // 読む *it = 99; // 書き換えもできる

++it / it++ ― 次の要素へ

インクリメント++
auto it = v.begin(); ++it; // 次へ(前置のほうが速いので推奨) it++; // 結果は同じだが一時オブジェクトを作る場合あり

前置 ++it を使うのがモダン C++ の流儀。ポインタでは差がないですが、重いイテレータ(map など)では前置のほうが速い場合があります。

it == end / it != end ― 範囲チェック

比較==, !=
for (auto it = v.begin(); it != v.end(); ++it) { std::cout << *it << ' '; }

it + n / it - n(ランダムアクセス限定)

加算vector/array で OK
auto it = v.begin() + 3; // 3 つ先を指す auto last = v.end() - 1; // 末尾要素(有効)

vector / string / array では +n / -n ができます(ランダムアクセスイテレータ)。list / map ではできないので、std::next(it, n) を使います(§4)。

it->member ― メンバアクセス

メンバアクセス->
std::vector<std::pair<int, int>> v = {{1,2}, {3,4}}; auto it = v.begin(); std::cout << it->first; // (*it).first と同じ

C のポインタの p->x と同じです。

4. const_iterator と const の基本

読むだけで書き換えない」ことを保証したいとき、const_iterator を使います。書き換えようとするとコンパイルエラーになるので、間違いを未然に防げます。

iterator書き換え可
std::vector<int> v = {1,2,3}; std::vector<int>::iterator it = v.begin(); *it = 99; // OK // auto でも同じ(通常のイテレータが推論される) auto it2 = v.begin();
const_iterator読み取り専用
std::vector<int> v = {1,2,3}; std::vector<int>::const_iterator it = v.begin(); // *it = 99; ← コンパイルエラー // 新しい書き方: cbegin() / cend() auto cit = v.cbegin();

いつ const_iterator を使う?

const の考え方: C++ の const は「この変数経由では変更しない」という約束です。コンパイラが守ってくれるので、チームで書く大きなコードで「うっかり書き換え」を防げます。関数引数、メンバ関数、ポインタ/参照…と、いろんな場所で使います。詳しくは STEP 4(第 19 回「メンバ関数と const メンバ」)で深掘り。
ここまでで日常は OK
残りは「イテレータで動く便利関数」と「5 カテゴリの一覧」。STL アルゴリズム編(第 48 回)に進む前に眺めておくと楽になります。

5. find / distance / advance

イテレータで動く標準関数の代表 3 つを紹介します。

std::find ― 値を探して位置を返す

find<algorithm>
std::vector<int> v = {10, 20, 30, 40}; auto it = std::find(v.begin(), v.end(), 30); if (it != v.end()) { std::cout << "found at " << (it - v.begin()); } else { std::cout << "not found"; }

見つかった位置を返し、見つからないときは end() を返すのがイテレータ流。string::find(npos を返す)とは違うので注意。

std::distance ― イテレータ間の距離

distance<iterator>
auto n = std::distance(v.begin(), it); // vector なら it - v.begin() と同じ // list や map でも動く(こちらは O(n))

std::next / std::prev / std::advance ― n だけ進める

advance 系<iterator>
auto it2 = std::next(v.begin(), 3); // 3 個先 auto it3 = std::prev(v.end(), 1); // 1 個手前(= 末尾要素) auto it4 = v.begin(); std::advance(it4, 2); // it4 を破壊的に +2

vector / string / array では it + n で済みますが、list / map+ が使えないので、これらの関数が汎用的です。

6. 5 つのカテゴリ(概要)

イテレータには「できる操作」のレベルに応じて5 段階のカテゴリがあります。深く理解しなくて OK ですが、「このコンテナは何ができるか」の目安になります。

カテゴリ代表例*it 読*it 書++it--itit+nit[n]
Inputistream_iterator ××××
Outputostream_iterator ××××
Forwardforward_list ×××
Bidirectionallist / map / set ××
Random Accessvector / string / array
日常のまとめ: vector / string / array は Random Access(ポインタと同じ強さ)。map / set / list は Bidirectional で +n ができない。ストリームは Input / Output で前方にしか進めない。普段 vector ばかり使っているなら、他のコンテナで「あれ、it + n ができない」と気付いたときにこの表を思い出してください。
イテレータ無効化に注意: vector で push_back / insert すると、再確保が起きたタイミングで既存のイテレータはすべて無効になります(ダングリングポインタと同じ問題)。ループ中でコンテナを変更するときは要注意。第 4 回「vector」の罠として扱いました。
広告スペース

確認クイズ

イテレータの理解度を 4 問で確認。

Q1. std::vector<int> v = {10,20,30}; のとき、v.end() が指す場所は?

30(末尾の値)
10(先頭の値)
末尾の要素の 1 つ先(そこには要素はない)
nullptr
end()「末尾の要素の 1 つ先」を指します。*v.end() はデリファレンスしてはいけません(未定義動作)。「末尾の要素」が欲しければ v.back()*(v.end() - 1)。この半開区間 [begin, end) の設計で、空コンテナでも自然に扱える・ループ終了条件が統一できる、などの利点が生まれます。

Q2. 次のコードを実行したあとの it が指すのは?
std::vector<int> v = {10,20,30,40};
auto it = v.begin();
++it; ++it;

10
20
30
40
v.begin() は 10 を指す。++it で 20、もう一度 ++it で 30。*it は 30 になります。

Q3. std::find(v.begin(), v.end(), 999) で 999 が見つからなかったとき、戻り値は?

-1
v.begin()
nullptr
v.end()
「見つからなかった」ことを示すために end()(存在しない末尾の次)を返すのがイテレータの慣例。if (it != v.end()) でチェックするのが定型。std::string::findnpos を返すのとは別の流儀なので注意。

Q4. std::list<int> のイテレータで使えない操作は?

*it(デリファレンス)
++it / --it
it + 3(n だけ進める)
it != end
std::list双方向イテレータ(Bidirectional)で、+n が使えません(要素が連続していないので)。代わりに std::next(it, 3)std::advance(it, 3) を使います。vector / string / array はランダムアクセスなので +n が可能。
この記事をシェア