C++ Learning

第4回 std::vector を使う ★目玉

C で動的配列を書くには malloc → 要素追加のたびに realloc で 2 倍へ拡張 → 最後に free と、正しく書けるだけで中級者の仕事。std::vector はそれを 型の内部で自動でやってくれます。本回は push_back アニメで「容量が倍々に伸びていく瞬間」と「再確保でアドレスが変わる瞬間」を可視化して、vector がどう動いているかを体感します。

このページで押さえること
✅ 最低限ここだけ覚える
  • #include <vector> / std::vector<int> v;
  • v.push_back(x) で末尾追加(自動で伸びる)
  • 要素数は v.size()v[i] でアクセス
  • free() 不要(スコープ終了で自動解放)
⭐ 余裕があれば読む
  • size()capacity() の違い
  • 再確保(reallocation)と反復子の無効化
  • reserve() で再確保を避ける
  • 償却計算量 O(1) の意味

1. std::vector とは ― まず触ってみる

std::vector は、必要なだけ勝手に伸びる配列です。これ 1 行だけ先に覚えてください。

C の配列 int a[5]; は「サイズ 5」と決めたら増えも減りもしませんでした。std::vector はそれを「最初は空っぽ、使うほど勝手に大きくなる」箱に変えたものです。

📦 イメージ:箱の中に値を「放り込む」だけ。箱のサイズは気にしなくていい
v.size() = 0

まずは 4 行だけ

上のボタンでやっていることを、実際の C++ コードで書くとこうなります。これだけ書ければ、もう vector を「使う側」としては合格です。

first_vector.cppC++ 最小例
#include <vector> int main() { std::vector<int> v; // ← 空の「int が入る箱」を作る v.push_back(10); // ← 末尾に 10 を追加 v.push_back(20); // ← 末尾に 20 を追加 // v は {10, 20} }
ここまでで覚えること(3 つだけ):
  • std::vector<型> 名前; で空の vector を作る。<型> は中に入れたい型(int / double / std::string ...)
  • 名前.push_back(値) で末尾に追加
  • サイズは自動で伸びるmallocfree も不要

v.push_back(10). は? ― これも「メソッド呼び出し」

前回(第 3 回 std::string)で触れたとおり、変数名 . 関数名() という書き方は C++ のメソッド呼び出しです。「その変数にくっついている機能を呼ぶ」と読んでください。

v.push_back(10) は「v に対して push_back(末尾に追加)という機能を、引数 10 で実行してね」という意味です。std::vector「データ配列」と「それを操作する機能」をひとまとめにした型なので、要素を足すときは v 自身にお願いする形になります。

C のスタイル関数(対象, 値)
// 末尾追加は自作 or realloc を自分で if (size == cap) { cap *= 2; a = realloc(a, cap*sizeof(int)); } a[size++] = 10;
C++ のスタイル対象.機能(値)
// v 自身に「末尾に足して」と頼む v.push_back(10); // 他にも同じ書き方で: v.size(); // 要素数を聞く v.empty(); // 空か聞く
おさらい: . は「この変数の中の機能を呼ぶ」記号。このあと出てくる v.size() / v.at(i) / v.front() / v.reserve(n) なども全部同じ仕組み(= vector のメソッド)です。

よくある素朴な疑問

Q. 「std::vector<int>」の <int> って何?
→ 「この箱には int を入れます」という宣言です。std::vector<double> なら double の箱、std::vector<std::string> なら文字列の箱。C には無い書き方ですが、型を引数のように渡すと理解してください。(詳細は第 41 回「テンプレート」で扱います。今は「こう書くもの」で OK)

Q. 要素の取り出し方は?
→ 配列と同じ v[0], v[1], ... で取れます。個数は v.size()。詳しくは §3 で。

Q. サイズを自分で指定することもできる?
→ はい。std::vector<int> v(5); で「要素数 5(全部 0)」の vector が作れます。これも §3 で。

次のセクションから「C ではこう書く」との対比が出ます。 最初に vector の便利さを強調するために C の書き方と並べますが、C のコードの細部は読み飛ばしても大丈夫です。「C ではこんなに大変だったんだ」と雰囲気を掴めれば十分。

2. C の動的配列との比較

C で「可変長の int 配列に値を追加し続ける」を書くと、こうなります。

dyn_array.c C
#include <stdio.h> #include <stdlib.h> int main(void) { int* a = malloc(4 * sizeof(int)); int size = 0, cap = 4; for (int i = 0; i < 10; ++i) { if (size == cap) { // 満杯なら拡張 cap *= 2; a = realloc(a, cap * sizeof(int)); if (!a) return 1; // 失敗チェック } a[size++] = i; } free(a); // 忘れるとメモリリーク return 0; }
realloc / free / サイズ管理 / NULL チェックをすべて人が書く
dyn_array.cpp C++
#include <vector> int main() { std::vector<int> v; for (int i = 0; i < 10; ++i) v.push_back(i); // スコープ終了時に自動で解放される }
10 行 → 3 行。拡張もチェックも解放も vector が面倒を見る
本質的な違い: C は人間がメモリレイアウトを管理する言語。C++ は型がメモリ管理する責任を持つ(RAII)言語。vector はその最も基本的な例で、STL の設計思想を学ぶ入り口にもなります。

3. 基本操作(宣言・追加・アクセス)

ここからは std::vector で日常的に使う操作を、ひとつずつ丁寧に見ていきます。覚えるのは「作り方」「要素の取り出し方」「要素の追加・削除」の 3 つだけ。一度理解すれば、他のコンテナ(listdequestring など)も同じ感覚で使えます。

3-1. 宣言と初期化 ― 作り方 5 パターン

vector の作り方は 5 パターンあります。「最初から中身が決まっているか?」「サイズは分かっているか?」で選び分けます。まず 1 番と 4 番を覚えれば 9 割は足ります。

① 空で作る(一番よく使う)

① 空の vectorもっとも基本
std::vector<int> v; // 空っぽ。あとから push_back で足していく

「最終的にいくつ入れるか分からない」「これから集計していく」ような場面で使います。size() は 0。push_back するたびに自動で伸びます。迷ったらこれを選んで OK。

② 要素数だけ指定(0 で埋める)

② サイズ指定0 埋め
std::vector<int> v(5); // {0, 0, 0, 0, 0}

「サイズが最初から決まっている」場合。たとえば得点表を 5 人分用意する、など。int の場合は 0、std::string なら空文字列、double なら 0.0 で埋められます(型のデフォルト値)。丸カッコ () を使うのがポイント(中カッコ {} だと意味が変わります、後述)。

③ 要素数と初期値を指定

③ サイズ + 初期値好きな値で埋める
std::vector<int> v(5, -1); // {-1, -1, -1, -1, -1}

②と似ていますが、初期値を自分で指定できます。「未訪問を -1 で表す経路探索の配列」「全員 100 点スタート」などでよく使います。第 1 引数が要素数、第 2 引数が埋める値。

④ 中身を直接書く(初期化子リスト)

④ リスト初期化C++11~
std::vector<int> v = {10, 20, 30}; // {10, 20, 30}

中身が決まっているなら、{} で並べて書くのが一番読みやすい。②の (5) と ④の {5} はまったく違う動きになるので注意:

丸カッコと中カッコの使い分け: 「サイズを指定したい → ()」「中身を並べたい → {}」と覚えてください。初学者が最もハマる落とし穴の 1 つです。

⑤ 別の vector からコピー

⑤ コピー応用
std::vector<int> d = {1, 2, 3, 4, 5}; std::vector<int> e = d; // 全部コピー → {1,2,3,4,5} std::vector<int> f(d.begin()+1, d.begin()+4); // 一部コピー → {2,3,4}

= で別の vector を代入すると、中身が全部コピーされます(C の配列は = でコピーできませんでしたが、vector はできます)。一部だけコピーしたいときは begin() / end() の範囲指定で。

3-2. 要素アクセス ― 中身を取り出す 4 つの方法

vector の中身を取り出すには 4 つの方法があります。普段は v[i] で十分、入力が信用できないときは v.at(i)、特別な位置が欲しいときに front() / back() / data()

v[i] ― インデックスアクセス(最速)

[] 演算子日常の主役
std::vector<int> v = {10, 20, 30}; int x = v[0]; // 10 int y = v[2]; // 30 v[1] = 99; // 代入もできる → {10, 99, 30}

C の配列とまったく同じ感覚で書けます。インデックスは 0 始まり一番速い(チェックがないため)。ただし 範囲外アクセスはチェックされない ので、v[99] のように存在しない位置を読むと未定義動作(運が悪いとクラッシュ、運がもっと悪いと変な値が返って動き続ける)。

v.at(i) ― 範囲チェック付き

at() メソッド安全・少し遅い
std::vector<int> v = {10, 20, 30}; int x = v.at(1); // 20 (OK) int y = v.at(99); // 例外 std::out_of_range が投げられる

範囲外アクセスを例外として検出してくれる安全版。[] より少しだけ遅いですが、安全性が重要な場面(ユーザー入力が混ざるとき、競技プログラミングでデバッグ中、など)では便利。判断基準: 「確実に範囲内と分かっている → []」「外れる可能性がある → at()」。

v.front() / v.back() ― 先頭・末尾

先頭・末尾意図が明快
std::vector<int> v = {10, 20, 30}; int first = v.front(); // v[0] と同じ → 10 int last = v.back(); // v[v.size()-1] と同じ → 30

v[0] / v[v.size()-1] と書くのと結果は同じですが、「先頭が欲しい」「末尾が欲しい」という意図がコードから読めるのが利点。スタック風に使うとき(push_back + back)に相性が良い。注意:空の vector に対して呼ぶと未定義動作。必ず !v.empty() を確認してから。

v.data() ― 生ポインタ(C 連携用)

data()C API への橋渡し
std::vector<int> v = {1, 2, 3}; int* p = v.data(); // &v[0] と同じ。C 関数に渡せる // たとえば C の関数に渡す some_c_function(v.data(), v.size());

C ライブラリや OS API が int* + 要素数を要求するとき用。vector 本体が生きている間だけ有効なので、関数から返したポインタを後で使うと壊れます。

▶ インデックスアクセスを体験

v は要素数 5。0〜4 以外を入れると何が起きるか確かめてみましょう。

std::vector<int> v = {10, 20, 30, 40, 50};
インデックスを入力して「アクセス」を押してください。

3-3. 要素の追加 ― push_back / emplace_back / insert

要素を足すには基本 push_back を使います。他の 2 つは「性能をもう一段上げたい」「先頭や途中に挿入したい」という特殊な場面用。

push_back(値) ― 末尾に追加(一番よく使う)

push_back末尾追加の主役
std::vector<int> v; v.push_back(10); // v = {10} v.push_back(20); // v = {10, 20} v.push_back(30); // v = {10, 20, 30}

末尾への追加は速い(償却 O(1))。ほとんどの要素追加はこれで済みます。容量が足りなくなったら自動で再確保(§4 で詳しく)。

emplace_back(...) ― その場で構築(より速い)

emplace_backC++11~
std::vector<std::string> v; v.push_back(std::string("hello")); // 一時オブジェクト作ってコピー v.emplace_back("hello"); // vector 内で直接構築(より速い)

複雑な型(std::string、独自クラスなど)を末尾に追加するとき、push_back よりコピーやムーブを 1 段省略できることが多い。int のような単純な型では差はありません。迷ったら push_back で十分、性能が気になる箇所だけ emplace_back に置き換えるのが現代の作法。

insert(位置, 値) ― 途中に挿入(遅い)

insertO(n) ― 注意
std::vector<int> v = {10, 30, 40}; v.insert(v.begin() + 1, 20); // 1 番目に 20 を挿入 → {10, 20, 30, 40} v.insert(v.begin(), 5); // 先頭に 5 → {5, 10, 20, 30, 40}

指定位置に要素を割り込ませます。後ろの全要素がずれるので、O(n) で遅い。先頭や途中への挿入を頻繁にやるなら、vector ではなく std::deque(両端追加が速い)や std::list(どこでも O(1))を検討しましょう。

3-4. 要素の削除 ― pop_back / erase / clear / resize

削除も追加と同じで、末尾は速い/途中は遅いという原則があります。

pop_back() ― 末尾を取り除く(速い)

pop_backO(1)
std::vector<int> v = {10, 20, 30}; v.pop_back(); // 末尾 30 を削除 → v = {10, 20}

末尾 1 つを取り除きます。戻り値はない(値が欲しいなら先に back() で取得してから pop_back)。空の vector に呼ぶと未定義動作なので、!v.empty() の確認を。

erase(位置) ― 途中を削除(遅い)

eraseO(n)
std::vector<int> v = {10, 20, 30, 40}; v.erase(v.begin() + 1); // 1 番目を削除 → {10, 30, 40} // 20 が消え、30 と 40 が前に詰められる(O(n) のコスト)

任意の位置を削除できますが、後ろの要素が全部 1 つずつ前に詰められるため遅い。「条件に合うものを全部削除」のような大量削除は、後述の「erase-remove イディオム」を使うと効率的です(第 9 章で扱います)。

clear() ― 全部空にする

clear全消去
std::vector<int> v = {10, 20, 30}; v.clear(); // v = {} (size() は 0 に) // ただし確保済みのメモリ(capacity)はそのまま残る

要素を全部取り除きます。size() は 0 になりますが、capacity はそのまま。もう一度同じくらいの要素を詰めるなら再確保が発生しないので速い。完全にメモリも解放したいなら v.shrink_to_fit() を続けて呼ぶか、std::vector<int>().swap(v); の「空と入れ替えイディオム」を使います。

resize(n) ― サイズを変更

resizeサイズ調整
std::vector<int> v = {10, 20, 30}; v.resize(5); // サイズ 5 に → {10, 20, 30, 0, 0} (足りない分は 0 埋め) v.resize(5, -1); // 埋める値を指定 → {10, 20, 30, -1, -1} v.resize(2); // サイズ 2 に → {10, 20} (余った要素は削除)

指定サイズに伸ばすことも縮めることもできる。伸ばす場合は指定した値(省略時はデフォルト値)で埋められ、縮める場合は末尾から削除されます。

まとめ:どれを使えばいい?

迷ったときの判断フロー(これだけ覚えれば十分):
  • 要素を末尾に足したい → push_back
  • 中身を取り出したい → v[i](速い)/範囲外が怖いなら v.at(i)
  • 末尾を取り除きたい → pop_back
  • サイズが最初から決まっている → std::vector<int> v(5);{...}
  • 先頭・途中への挿入/削除が多い → vector じゃなく dequelist を検討

4. push_back と容量 2 倍の可視化

ここが本回の目玉。push_back のたびに要素数 (size)は 1 ずつ増えますが、確保済み容量 (capacity) は「満杯になった瞬間にだけ」2 倍へ拡張されます。ボタンを押して観察してください。

size
0
capacity
0
再確保
0
合計コピー
0
メモリレイアウト (青=要素あり/白抜き=確保済み空き)

観察ポイント:

なぜ 2 倍? 要素を n 個追加したときの総コピー回数n + n/2 + n/4 + ... ≒ 2n。つまり push_back 1 回あたりの平均コストは O(1)(償却計算量)。もし「毎回 +1 ずつ拡張」なら 1+2+3+...+n = O(n²) になって一気に遅くなります。
倍率は実装依存: GCC/Clang の libstdc++ / libc++ は 2 倍、MSVC は 1.5 倍を採用。どちらも定数倍なので漸近計算量は同じ O(1) 償却です。

成長曲線

上のデモを 0→20 まで動かしたときの成長の様子を、下のバーチャートでも表示します(赤=その回に再確保)。

横軸: push_back 回数 縦軸: capacity(赤=再確保が起きた回)

5. reserve で再確保を抑える

最終的な要素数がだいたい分かっているなら、reserve(n) で最初に領域を確保しておくと、途中の再確保が発生しません。大量データを詰め込むときの定石です。

遅い再確保 17 回
std::vector<int> v; for (int i = 0; i < 100000; ++i) v.push_back(i); // cap: 1,2,4,8,...,131072 // ≒ 17 回の realloc + コピー
速い推奨
std::vector<int> v; v.reserve(100000); // 最初に 1 回だけ確保 for (int i = 0; i < 100000; ++i) v.push_back(i); // realloc 0 回
size と capacity の違い:
  • size() = 今使っている要素数(ユーザーの観点)
  • capacity() = 確保してある領域の要素数(メモリの観点)
reserve は capacity だけを増やします。size は変わらない(要素は増えない)。

6. 反復子の無効化という罠

再確保が起きると、それ以前に取ったポインタや反復子は無効になります(指していた古いメモリはもう解放されているため)。次のようなコードは C++ での定番バグです。

NG未定義動作
std::vector<int> v = {1,2,3}; int* p = &v[0]; // 今の先頭を指す v.push_back(4); // ← 再確保が起きるかも v.push_back(5); v.push_back(6); std::cout << *p; // ← p は壊れている可能性
dangling pointer と呼ばれる定番バグ
OKインデックス保持
std::vector<int> v = {1,2,3}; size_t idx = 0; // 位置だけを記憶 v.push_back(4); // 再確保されても v.push_back(5); // idx は無効にならない v.push_back(6); std::cout << v[idx]; // 常に有効
ルール: vector を変更する(push_back, insert, resize など)可能性があるループの中では、ポインタ・反復子・参照を保持しない。どうしても必要ならインデックスsize_t)で持つ。
余裕があれば読む ― ここから先は応用
最低限はここまで。残りは多次元や性能比較なので、急ぐなら次章へ。

7. 2 次元配列としての vector<vector<int>>

行・列のサイズが実行時に決まる 2 次元配列は、vector をネストして作るのが最も簡単です。

grid.cppC++
#include <vector> int main() { int rows = 3, cols = 4; // 3x4 の 2次元配列を 0 で埋めて作る std::vector<std::vector<int>> grid( rows, std::vector<int>(cols, 0) ); // アクセス grid[1][2] = 7; }
grid.c (相当)C
#include <stdlib.h> int main(void) { int rows = 3, cols = 4; int** grid = malloc(rows * sizeof(int*)); for (int i = 0; i < rows; ++i) grid[i] = calloc(cols, sizeof(int)); grid[1][2] = 7; // 最後に全部 free for (int i = 0; i < rows; ++i) free(grid[i]); free(grid); }
パフォーマンス注意: vector<vector<T>> はメモリが連続でないので、行列演算のようにキャッシュ効率が重要な処理では1 次元 vector + 手動インデックス計算v[row * cols + col])のほうが速いです。大規模数値計算では std::vector<double> 1 本が定番。
広告スペース

確認クイズ

vector の理解度を 4 問で確認しましょう。

Q1. std::vector<int> v;push_back を 1 回目に呼んだ直後の capacity() は?(GCC/Clang の典型実装)

1
2
8
0(最後まで確保されない)
空の vector の capacity は 0。1 つ目の push_back で 1 要素分だけ確保されるのが典型です(2 回目で 2 に、3 回目で 4 に倍々へ伸びる)。ただし規格で具体値は保証されておらず、実装によっては最初から少し余裕を持たせることもあります。

Q2. size()capacity() の関係は?

常に等しい
size() ≦ capacity()
size() ≧ capacity()
無関係
capacity は「確保済みの領域の大きさ」、size は「実際に使っている要素数」。capacity の中に size が収まる形です。reserve(n) すると size は変わらず capacity だけ n 以上になります。

Q3. 次のコードの動作として正しい説明は?
int* p = &v[0];
v.push_back(999); // 再確保が起きたとする
std::cout << *p;

必ず 999 が出力される
必ず元の v[0] が出力される
p は無効になっており、未定義動作
コンパイルエラー
再確保が起きると古いメモリは解放され、vector は新しい領域を指します。そのとき取ったポインタ p はダングリングポインタになり、デリファレンスは未定義動作です。変更を含むループ内では反復子・参照・ポインタを保持しないのが鉄則。

Q4. 100 万個の要素を push_back することが最初から分かっている。最速の書き方は?

そのまま push_back を 100 万回
std::vector<int> v(1000000); してから v[i] = ... で書き込む
v.reserve(1000000); してから push_back を 100 万回
std::vector<int> v; v.capacity = 1000000;
reserve は「size を変えずに capacity だけ増やす」操作で、push_back 中の再確保を完全に避けられます。選択肢②の v(1000000) も正しく動きますが、100 万要素を 0 で初期化するコストが余分にかかります。用途に応じて使い分け。
この記事をシェア