{ }codelper

学ぶ › C++

言語ごとの特徴 · 42

C++ の参照と STL

& 参照と const 参照、vector・map・set・unordered_map、ラムダを受け取るアルゴリズム、そしてコピー・無効化・自動挿入の落とし穴。

この単元は C++ 向けです。
C には参照もコンテナもないので、ポインタと自分で管理する配列を使います。
Python・Java・C#・JavaScript には独自のコレクション(リスト・辞書の単元)があり、リストは渡すといつも共有されるので & は要りません。

C++ では値を渡すとコピーされ、参照 & を使うと元のものをそのまま扱います。
下の例は参照で vector を2倍にし、並べ替え・検索・合計といった STL のアルゴリズムを使ってから、std::map で単語の数を数えます。

#include <algorithm>#include <iostream>#include <map>#include <numeric>#include <set>#include <string>#include <vector> void doubleAll(std::vector<int>& v) {          // & : 원본을 바꿔요    for (auto& x : v) x *= 2;} int total(const std::vector<int>& v) {         // const& : 복사 없이 읽기만    return std::accumulate(v.begin(), v.end(), 0);} void print(const std::vector<int>& v) {    for (size_t i = 0; i < v.size(); i++) std::cout << (i ? " " : "") << v[i];    std::cout << '\n';} int main() {    int n;    std::cin >> n;    std::vector<int> v(n);    for (auto& x : v) std::cin >> x;     for (auto x : v) x = 0;                     // 복사본만 바뀌어요    print(v);    doubleAll(v);    print(v);    std::cout << total(v) << '\n';     std::sort(v.begin(), v.end(), [](int a, int b) { return a > b; });    print(v);    auto it = std::find(v.begin(), v.end(), 8);    std::cout << (it - v.begin()) << ' ' << std::set<int>(v.begin(), v.end()).size() << '\n';     std::map<std::string, int> count;    std::string w;    while (std::cin >> w) count[w]++;    for (const auto& [word, c] : count) std::cout << word << ' ' << c << '\n';    std::cout << count.contains("kiwi") << ' ' << count.size() << '\n';    int k = count["kiwi"];                      // 없으면 0으로 새로 넣어요    std::cout << k << ' ' << count.size() << '\n';}
stdin
5
3 1 4 1 5
apple banana apple cherry banana apple
stdout
3 1 4 1 5
6 2 8 2 10
28
10 8 6 2 2
1 4
apple 3
banana 2
cherry 1
0 3
0 4

ポイント

  • 値で渡すと vector の要素がすべてコピーされるので、関数の中で変えても元はそのままです。
  • 参照 int&・std::vector<int>& は同じ変数の別名です。
    例の doubleAll(v) は元を2倍にします。
    参照は作るときに指す先を決める必要があり、あとで付け替えられず、ポインタと違って空(null)にはなれません。
  • 読むだけなら const std::vector<int>& で受け取ります。
    コピーの手間がなく、うっかり変えるとコンパイルエラーになります。
  • STL のコンテナ: std::vector(伸びる配列)、std::map(キー順に並んだ辞書、検索 O(log n))、std::set(並んだ重複なしの集まり)、std::unordered_map(ハッシュ辞書、平均 O(1)、順番なし)。
  • アルゴリズム(<algorithm>・<numeric>)はイテレータの範囲 v.begin(), v.end() を受け取ります。
    std::sort にラムダ [](int a, int b) { return a > b; } を渡すと大きい順、std::find は見つけた位置のイテレータ(なければ v.end())を返すので、it - v.begin() が番号です。
  • std::accumulate(v.begin(), v.end(), 0) は合計で、最後の 0 の型が合計の型になります(0 なら int、大きな合計は 0LL)。

言語ごとの書き方

C++小さな値(int・double)は値で、string・vector・map のような大きなものは const& で受け取るのが基本の習慣。
for (const auto& [word, c] : count) はキーと値をいっしょに取り出します(C++17)。
C++20 では std::ranges::sort(v) のように範囲を丸ごと渡すこともできます

よくある間違い

range-for がコピーを回る

for (auto x : v) x = 0;

→ x は要素のコピーなので、v はまったく変わりません(例の1行目 3 1 4 1 5)。clang は -Wall で "set but not used" と警告してくれます

変えるなら auto& x、コピーせずに読むだけなら const auto& x と書きます。

push_back のあとで古い参照を使った

int& first = v[0];
v.push_back(9);
std::cout << first;

→ サイズが容量を超えると vector は新しいメモリへ移り、それまでに取ったイテレータ・ポインタ・参照がすべて無効になります。使うと未定義動作です

push_back のあとは番号で取り出し直します。
range-for で回っている最中の同じ vector にも push_back しません。

map[key] であるかどうかを調べた

int k = count["kiwi"];

→ 読むだけに見えますが、キーがなければ 0 で新しく入れます。例では個数が 3 から 4 に増えます。だから const std::map& では [] がそもそも使えません

あるかどうかだけなら count.contains("kiwi")(C++20)・count.count(...)・count.find(...)、なければエラーにしたいなら count.at(...)(std::out_of_range 例外)を使います。

言語ごとの違い
for (auto x : v) はコピーを回します。
要素を変えるなら auto&、読むだけなら const auto& と書き、関数の引数も大きな値は const& で受け取りましょう。

練習問題