CodeGym /コース /C++ SELF /std::vector:

std::vector: push_back, size, capacity

C++ SELF
レベル 12, レッスン 1
使用可能

1. 配列は時に扱いづらい

もしこれまで std::array<int, 10> を使っていたなら、配列の安定性は馴染みがあるはずです: 配列は常に 10 個の要素を持ち、要素が「移動」することはなく、添字も分かりやすい。しかし現実には、データの個数が事前に分からない場面がよくあります。例えば: ユーザーが 0 を入力するまで評価を入力し続ける、または入力の終わりまで数を読み取る、あるいはユーザーが「もういい」と言うまで買い物リストを集める、といった場合です。

固定長配列にはここで二つの典型的な不便さが出ます。第一に、最大サイズを事前に選ぶ必要があることです(「じゃあ 1000 にしておこう…足りるかな?」)。これは「足りないリスク」か「余計なメモリを使う」ことになります。第二に、大きな配列を選んでも「実際に何個書き込まれたか」を別途保持する必要があります。今日扱うのは、この「実際に書き込まれた数」をコンテナの組み込みの一部にする方法です。

2. std::vector<T> を知る

std::vector<T> は、要素型 T の要素を連続したメモリに格納する(配列のように)標準ライブラリのコンテナですが、実行時に要素数を変更できる点が特徴です。つまり「伸縮する配列」のようなもので、内部は標準ライブラリが管理します。

インクルードは他のヘッダと同様に行います:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;
    std::cout << "ok\n"; // ok
}

今日のモデル観点では、vector には重要な数値が二つあります。

頭に入れておくと便利な表:

用語 取得方法 意味
size()
v.size()
実際に存在する要素の数
capacity()
v.capacity()
メモリを拡張せずに格納できる要素数

「メモリ」については低レベルの詳細は扱わず大まかに説明しますが、capacity() が存在するという事実とその「意味」の議論はコンテナの標準インターフェースの一部です。

3. ベクターの作り方

std::vector を最初に作るときは、「変数として存在する」と「内部に要素が入っている」という二つの考えを区別する習慣をつけることが重要です。ベクターは存在していて中身が空という状態は正常であり、エラーではありません。

空のベクター

空のベクターとは size() が == 0 の状態です。

#include <iostream>
#include <vector>

int main() {
    std::vector<int> numbers;
    std::cout << numbers.size() << '\n'; // 0
}

ここでの典型的な初心者ミスは、ベクターが存在するからといって numbers[0] にアクセスしようとすることです。ベクター自体は存在しても、インデックス 0 の要素は存在しない場合があります。空の冷蔵庫に行って「番号0のケーキはどこ?」と聞くようなものです。

初期化リスト {...} による初期化

初期データが分かっているなら、ベクターを最初から値で満たして作成できます:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v{10, 20, 30};
    std::cout << v.size() << '\n'; // 3
    std::cout << v[0] << '\n';     // 10
}

4. 追加とアクセス: push_back と size()

ベクターを「伸ばす」最も基本的な方法は push_back(value) メソッドです。これは末尾に要素を追加し、その後 size() は 1 増えます。この操作は頻繁に使われるため、C++ では「リストにもう一つ要素を入れる」という行為の等価物と考えていいでしょう。

7行のミニ例: 二つの数を追加してサイズを確認します。

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;
    v.push_back(5);
    v.push_back(8);
    std::cout << v.size() << '\n'; // 2
}

重要な点: push_back は「末尾に追加する」ことに注意してください。つまり要素の順序は保持され、先に追加したものほど小さいインデックスになります。

size():存在する要素数と許容されるインデックス

ベクターが成長する際、実用的な習慣はロジックを常に size() を基準に組み立てることです。「多分10個いるだろう」とか「何回か追加したから」といった勘ではなく、コンテナ自身が保持する実際の数に依存します。

もし size() が == n なら、正しいインデックスは 0 から n - 1 です。これは配列と同じルールで、ただし n は実行時に変化します。

簡単な例で確認します:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;
    v.push_back(100);
    v.push_back(200);

    std::cout << "size=" << v.size() << '\n'; // size=2
    std::cout << v[0] << ' ' << v[1] << '\n'; // 100 200
}

ここでの重要なルール: ループを書くとき、条件はほとんど常に次のようになります:

for (std::size_t i = 0; i < v.size(); ++i) {
    // ...
}

決して <= にしないこと。<= や「最大を 10 にする」などは最後に境界を越える可能性があります。常に厳密に i < v.size() とするのが安全です。これはシートベルトのようなものです: 退屈に思えても突然役に立ちます。

5. capacity():なぜ「場所があるが要素はまだない」のか

capacity はフィットネス用語のように聞こえますが、要するに「収容力」です。std::vector における capacity() は、現時点でベクターが内部メモリを拡張せずに保持できる要素数を示します。

なぜこれが存在するのかというと、各 push_back ごとにメモリを1つずつ割り当てるのは非常にコストが高いため、ベクターは通常「余裕をもって」メモリを確保します。例えば要素が 3 個のとき、capacity() は 4 や 8 になっていることがあり、次の数回の push_back が高速に行えるようにしているのです。

重要な点: capacity() はほとんど常に >= size() です。これは基本的な不変条件の一つです。

実践的な例で見てみましょう — 診断用のサンプル:

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;

    v.push_back(1);
    std::cout << v.size() << ' ' << v.capacity() << '\n'; // 例えば: 1 1 (または 1 2)

    v.push_back(2);
    std::cout << v.size() << ' ' << v.capacity() << '\n'; // 例えば: 2 2 (または 2 4)

    v.push_back(3);
    std::cout << v.size() << ' ' << v.capacity() << '\n'; // 例えば: 3 4
}

capacity() の値はコンパイラやプラットフォームによって異なることがあり、それは正常です。成長の「式」を予想するのではなく、意味を理解することが重要です: size は実際の要素数、capacity は予備の容量です。

6. std::vector の不変条件: 常に成り立つこと

「不変条件」はファンタジーの古代のドラゴンの名前のように聞こえますが、実際にはコンテナ自身が守る約束事のリストです。形式的な数学に深入りする必要はありません。プログラム中で「驚きのあるコード」を書かないために覚えておくべきルールがいくつかあります。

第一の不変条件 — 0 <= size() <= capacity()。つまり要素数が負になることはなく、ベクターは現在確保されている容量以上の要素を持つことはできません。

第二の不変条件 — 要素は連続して格納される(配列のように)。これが vector の人気の理由の一つで、インデックスアクセスが高速でキャッシュに親和性が高いという利点があります。時に「vector は contiguous コンテナ(連続メモリのコンテナ)」と表現されます。イテレータの話はまだしませんが、この性質は覚えておくと役に立ちます。

第三の不変条件 — 要素の順序は保持される。もし 10、次に 20、次に 30 を追加すれば、v[0] == 10、v[1] == 20、v[2] == 30 になります。これが vector を日常的な意味での「リスト」的に使いやすくしている点です。

最後に capacity() に関する実用的な帰結: 要素を追加するときに内部ストレージが拡張されることがあり、その瞬間にベクターは「より広い部屋に引っ越す」ような挙動をします。今のところ「どのように」引っ越すかを知る必要はありませんが、push_back の際に capacity() が変化する可能性があることは覚えておいてください。だから具体的な数値に依存したロジックを組むのは避けます。

簡単なモデル図(低レベルの詳細は省略、考え方のイメージ):

flowchart LR
    A["vector: size=3"] -- "push_back" --> B{"空きはあるか? capacity > size"}
    B -- "はい" --> C["要素を追加 size=4 capacityは変わらない"]
    B -- "いいえ" --> D["拡張が必要 capacityが増えてから size=4"]

7. ミニアプリ: std::vector<int> を使った「支出の貯金箱」

std::vector を単なる理論に終わらせないために、小さなアプリケーションを始めましょう。今後の講義でこのアプリを発展させていきますが、今日は基本部分を作ります: push_back で数値を集め、統計を出力して size/capacity を観察します。

シナリオは単純です: ユーザーが支出(整数)を入力し、0 が「終了」を意味します。支出は事前に個数が分からないため、std::vector<int> に格納します。

ひな形: 挨拶と空のリスト表示

#include <iostream>
#include <vector>

int main() {
    std::vector<int> expenses;

    std::cout << "Enter expenses (0 to stop)\n"; // Enter expenses (0 to stop)
    std::cout << "size=" << expenses.size() << '\n'; // size=0
}

ここではリストが存在しているが空であることを視覚的に確認できます。

数値の読み取りと push_back

ループを追加します。ここではまだ「不正な入力への対処」は行いません — それは別の重要なトピックです。本日の目的はコンテナそのものの動きに集中することです。

#include <iostream>
#include <vector>

int main() {
    std::vector<int> expenses;
    int x = 0;

    std::cout << "Enter expenses (0 to stop)\n"; // Enter expenses (0 to stop)

    std::cin >> x;
    while (x != 0) {
        expenses.push_back(x);
        std::cin >> x;
    }

    std::cout << "count=" << expenses.size() << '\n'; // 例えば: count=3
}

もし 10 25 7 0 と入力した場合、size() は 3 になります。つまり size() は「実際に入力された個数」を表します。

通常の for ループで合計を計算する

まだ STL のアルゴリズムは使わないので(後の講義で学びます)、単純な for で合計を計算します。ここでも size() が役に立ち、ループは要素数に依存して正しく動作します。

#include <cstddef>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> expenses{10, 25, 7};

    int sum = 0;
    for (std::size_t i = 0; i < expenses.size(); ++i) {
        sum += expenses[i];
    }

    std::cout << "sum=" << sum << '\n'; // sum=42
}

「メモリ診断」を追加して capacity() を表示する

これはユーザー向けでは必須ではありませんが、ベクターの成長の仕方を体感するために非常に役立ちます。

#include <iostream>
#include <vector>

int main() {
    std::vector<int> v;

    for (int i = 1; i <= 8; ++i) {
        v.push_back(i);
        std::cout << "size=" << v.size()
                  << " cap=" << v.capacity() << '\n';
    }
}

実行して目で見てください: size は毎回 1 ずつ増えますが、cap は飛び飛びに増えます。これは正常な挙動で、capacity() は予備の容量を表すからです。

この講義の現時点でのアプリ全体をまとめる

これが今回の講義の最終版です: 既に学んだ push_back, size, capacity, 添字、while ループと一つの for ループだけを使っています。

#include <cstddef>
#include <iostream>
#include <vector>

int main() {
    std::vector<int> expenses;

    std::cout << "Enter expenses (0 to stop)\n"; // Enter expenses (0 to stop)

    int x = 0;
    std::cin >> x;

    while (x != 0) {
        expenses.push_back(x);

        // 診断出力 (ユーザ向けではありません):
        std::cout << "[debug] size=" << expenses.size()
                  << " cap=" << expenses.capacity() << '\n';

        std::cin >> x;
    }

    int sum = 0;
    for (std::size_t i = 0; i < expenses.size(); ++i) {
        sum += expenses[i];
    }

    std::cout << "count=" << expenses.size() << '\n'; // 例えば: count=3
    std::cout << "sum=" << sum << '\n';              // 例えば: sum=42
}

確かに今は「荒削りなプロトタイプ」ですが、主要なことはできています: 事前にサイズを決めずに不定個の値を保持でき、従来の配列のように「手動の記録用カウンタ」を必要としません。

8. 初期段階での std::vector 利用時の典型的なミス

ミスその1: 「ベクターが存在する」と「ベクターに要素がある」を混同すること。
std::vector<int> v; の後ではベクターは作られていますが、v.size() はゼロです。すぐに v[0] にアクセスすると存在しない要素に触れることになります。解決法は size() を基に十分な要素数があるか確認する習慣をつけることです。

ミスその2: 「最後のインデックスは size() だ」と思い込むこと。
サイズが n のベクターの最後のインデックスは n - 1 です(n > 0 の場合)。特にループでは、i <= v.size() は最後のステップで境界を越えてしまうことがほとんどです。安全なパターンは常に i < v.size() とすることです。

ミスその3: capacity() を「要素数」として使うこと。
capacity() はメモリの余裕を表すもので、実際の要素数ではありません。診断目的で表示するのは有用ですが、ループやインデックスチェックなどのロジックは常に size() に基づいて構築してください。

ミスその4: capacity() が「規則的に増える」と期待してそれに依存すること。
「capacity が倍になるなら…」といった仮定でコードを書かないでください。capacity() の増え方は実装依存であり様々です。重要なのは、capacity() は要素の追加の際に変わる可能性があるという点だけです。

ミスその5: インデックスを int にして v.size() と無造作に比較すること。
v.size() は符号なしサイズ型を返します。int と size_t を混ぜて比較すると(特に負の値が絡むと)奇妙な挙動を招くことがあります。インデックスには習慣として std::size_t を使うことを推奨します(ループ例の通り)。

1
タスク
C++ SELF, レベル 12, レッスン 1
ロック未解除
最後の注文
最後の注文
1
タスク
C++ SELF, レベル 12, レッスン 1
ロック未解除
コンテナのパスポート
コンテナのパスポート
1
タスク
C++ SELF, レベル 12, レッスン 1
ロック未解除
最高の結果
最高の結果
1
タスク
C++ SELF, レベル 12, レッスン 1
ロック未解除
拡張の記録
拡張の記録
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION