{ }codelper

배우기 › C++

언어마다 다른 것 · 42

C++ 참조와 STL

& 참조와 const 참조, vector·map·set·unordered_map, 람다를 받는 알고리즘, 그리고 복사·무효화·자동 삽입 함정.

이 단원은 C++에서 배워요.
C에는 참조도 컨테이너도 없어서 포인터와 직접 관리하는 배열을 써요.
Python·Java·C#·JavaScript는 자기 컬렉션(목록·사전 단원)이 있고, 목록은 넘기면 늘 함께 쓰여서 &가 필요 없어요.

C++에서 값을 넘기면 복사되고, 참조 &를 쓰면 원본을 그대로 다뤄요.
아래 예제는 참조로 vector를 두 배로 바꾸고, 정렬·찾기·합 같은 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)는 원본을 두 배로 바꿔요.
    참조는 만들 때 가리킬 것을 정해야 하고, 나중에 바꿀 수 없고, 포인터와 달리 비어 있을(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는 하나도 안 바뀌어요(예제 첫 줄 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&로 받으세요.

연습 문제