言語ごとの特徴 · 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';}5 3 1 4 1 5 apple banana apple cherry banana apple
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& で受け取りましょう。