【C++】std::listとは?|特徴・使い方・vectorとの違いを初心者向けに解説

サムネイル画像 C++

今回は、C++でよく出てくる「std::listとは何か」について解説していきます。

「std::listって何?」
「std::vectorとどう違うの?」
「どんな場面で使えばいいの?」
こんな疑問はありませんか?

std::listは、C++の標準ライブラリに入っているコンテナのひとつです。配列のように要素をまとめて扱えますが、性質はかなり違います。この記事では、std::listの特徴・基本的な使い方・初心者がハマりやすいポイントを、できるだけわかりやすく整理していきます。

std::listとは?

std::listは、要素を順番に管理できるコンテナです。特に、途中への追加や削除がしやすいのが特徴です。

std::vectorは要素が連続した領域に並ぶのに対して、std::listは要素同士をつないで管理するタイプのコンテナです。そのため、途中に新しい要素を入れたり、特定の要素を消したりする処理が得意です。

ただし、その代わりに添字で直接アクセスするのは苦手です。たとえば list[0] のような書き方はできません。

なぜstd::listが必要?

配列やstd::vectorだけでも多くの処理は書けますが、要素の追加や削除が頻繁に起きる場面では、std::listのほうが考えやすいことがあります。

たとえば、敵の一覧、タスクの順番、イベント待ちのデータなど、「途中で消える」「途中に追加される」ことが多いデータでは使いやすいです。ループ処理の基本にまだ不安がある方は、for文とwhile文の違いの記事もあわせて読むと理解しやすいです。

std::listの基本的な使い方

まずは、要素の追加・表示・削除の基本を見てみます。

▼main.cpp

#include <iostream>
#include <list>

int main() {
    std::list<int> numbers;

    numbers.push_back(10);
    numbers.push_back(20);
    numbers.push_back(30);

    std::cout << "追加後: ";
    for (int value : numbers) {
        std::cout << value << ' ';
    }
    std::cout << '\n';

    numbers.pop_front();

    std::cout << "先頭削除後: ";
    for (int value : numbers) {
        std::cout << value << ' ';
    }
    std::cout << '\n';

    return 0;
}

このコードでは、push_backで末尾に要素を追加し、pop_frontで先頭の要素を削除しています。std::listはこのような前後の追加・削除が書きやすいです。

実行結果

追加後: 10 20 30 
先頭削除後: 20 30 

std::vectorとの違い

初心者のうちは、std::vectorとstd::listの違いで迷いやすいです。ざっくり言うと、添字で扱いやすいのがstd::vector、途中の追加や削除を考えやすいのがstd::listです。

  • std::vector:添字アクセスしやすい、扱う場面が多い
  • std::list:途中のinsertやeraseがしやすい
  • std::list:ランダムアクセスには向かない

そのため、何となくlistを選ぶというより、「途中削除をたくさんしたいかどうか」で判断するのがわかりやすいです。

実践でよく使うinsertとerase

std::listでは、iteratorを使って途中に要素を入れたり、要素を消したりできます。クラスでデータを管理したい場合は、classの記事とあわせて考えると整理しやすいです。

▼main.cpp

#include <iostream>
#include <list>

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

    auto it = numbers.begin();
    ++it;

    numbers.insert(it, 15);

    for (auto iter = numbers.begin(); iter != numbers.end(); ) {
        if (*iter == 20) {
            iter = numbers.erase(iter);
        } else {
            ++iter;
        }
    }

    std::cout << "結果: ";
    for (int value : numbers) {
        std::cout << value << ' ';
    }
    std::cout << '\n';

    return 0;
}

この例では、20の前に15を追加し、その後20だけ削除しています。eraseの戻り値を次のiteratorとして受け取っているのがポイントです。

実行結果

結果: 10 15 30 

【重要】私が実際にstd::listで困った体験談

私が個人開発で敵データの管理をしていたとき、削除が多いからという理由だけで最初からstd::listを選んだことがありました。たしかに要素を消す処理は書きやすかったのですが、あとから「何番目の敵か」を基準に処理を書きたくなって困りました。

std::vectorの感覚で添字アクセスしたくなるのですが、std::listはそこが得意ではありません。その結果、ループのたびにiteratorで進める必要があり、思ったよりコードが見づらくなりました。

最終的には、削除頻度が高い部分だけstd::list、添字で扱いたい部分はstd::vectorのように使い分けるようにしました。個人開発では「何となく便利そう」で選びがちですが、コンテナは用途で分けたほうがかなり管理しやすいです。

std::list使用時のよくある失敗例と対処法

1. 添字でアクセスしようとする

std::listでは list[0] のような書き方はできません。順番にたどる必要があるので、range-based forやiteratorを使いましょう。

2. eraseしたあとにiteratorをそのまま使う

eraseした要素のiteratorをそのまま使うと不具合の原因になります。eraseの戻り値を次のiteratorとして受け取るのが基本です。

3. vectorの代わりに何でもlistにしてしまう

途中削除が少ないなら、std::vectorのほうがシンプルなことも多いです。listは万能ではないので、用途を見て選ぶのが大事です。挙動がわかりにくいときは、Visual Studioのデバッグ記事も参考になります。

注意点

std::listは便利ですが、初心者向けとしてはまずstd::vectorのほうが扱いやすい場面も多いです。特に、番号でアクセスしたい、要素をまとめて見たい、という場合はvectorのほうが自然です。

そのうえで、途中への追加や削除が多い処理で困ったときにstd::listを選ぶ、という考え方にすると失敗しにくいです。

まとめ

  • std::listは途中の追加や削除がしやすいコンテナ
  • push_back、push_front、insert、eraseをよく使う
  • 添字アクセスはできないのでiteratorで扱う
  • vectorの代わりに何でもlistにするのはおすすめしにくい
  • 用途に応じてvectorとlistを使い分けるのが大事

関連記事