{{ label!='' ? 'Label : ' : (q!='' ? '검색 : ' : '전체 게시글') }} {{ label }} {{ q }} {{ ('('+(pubs|date:'yyyy-MM')+')') }}

[C++] The C++ Programming Language: 7. Pointers, Arrays, and References (2) - Lvalue References

 7.7 References

  포인터는 우리가 잠재적으로 많은 양의 데이터를 적은 비용으로 다룰 수 있도록 해줍니다. 데이터를 복사하는 것 대신, 단순히 포인터의 값으로 주소를 전달하면 되기 때문이죠. 포인터의 타입은 포인터를 통해 데이터에게 무엇을 할 수 있는지를 결정합니다. 객체의 이름 대신 포인터를 사용하는 데에는 몇 가지 차이가 있습니다:

  • 다른 문법을 사용합니다. obj 대신 *p를, obj.m 대신 p->m을 사용합니다.
  • 서로 다른 시기에 서로 다른 객체를 참조하는 포인터를 만들 수 있습니다.
  • 객체를 직접 사용할 때 보다 포인터를 활용할 때 더욱 주의를 기울여야 합니다: 포인터는 nullptr일 수 있으며 예상치 못한 객체를 가리키고 있을 수 있습니다.

  이러한 차이는 우리를 짜증나게 하죠. 예를 들어, 몇몇 프로그래머는 f(x) 대신 보기 좋지 않은 f(&x)를 볼 수 있습니다. 더욱이, 값이 변하는 포인터 변수를 관리하고 nullptr의 가능성으로부터 보호하는 것은 큰 부담입니다. 마지막으로, 우리가 연산자를 오버로딩하고 싶을 때도 있습니다. &x+&y대신 x+y를 쓰고 싶은 것이죠. 이러한 문제들을 해결하는 언어 메커니즘을 참조(reference)라고 합니다. 포인터와 같이, 참조는 객체의 별명(alias)입니다. 객체의 머신 주소를 이용하는데 구현되죠. 포인터에 비해 추가적인 성능의 오버헤드가 있는 것도 아닙니다. 하지만 포인터와 다른 점이 있죠:

  • 여러분들은 객체의 이름과 똑같은 문법으로 참조를 접근할 수 있습니다.
  • 참조는 항상 초기화될 때 얻은 객체를 참조합니다.
  • "null 참조"라는 것은 없으며, 특정 객체를 가리키는 참조임을 보장할 수 있습니다.

  참조는 객체의 대체 이름입니다. 참조의 주된 사용은 일반적인 함수, 특히 연산자를 오버로딩하는 함수의 인자와 반환 값을 명세하는데 사용됩니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
template<class T>

class vector {
    T∗ elem;
    // ...
public:
    T& operator[](int i) { return elem[i]; } // return reference to element
    const T& operator[](int i) const { return elem[i]; } // return reference to const element

    void push_back(const T& a); // pass element to be added by reference
    // ...
};

void f(const vector<double>& v)
{
    double d1 = v[1]; // copy the value of the double referred to by v.operator[](1) into d1
    v[2] = 7; // place 7 in the double referred to by the result of v.operator[](2)

    v.push_back(d1); // give push_back() a reference to d1 to work with
}
cs

  함수 인자를 참조로서 넘기는 생각은 고수준 프로그래밍들의 역사들만큼 오래되었습니다.

  lvalue/rvalue와 const/non-const의 차이를 반영하기 위해, 세 종류의 참조가 존재합니다:

  • lvalue references: 값을 변경할 수 있는 객체를 참조합니다.
  • const references: 값을 변경하고 싶지 않은 객체를 참조합니다.
  • rvalue references: 우리가 사용하고 난 뒤에 유지할 필요가 없는 값을 참조합니다.

  이들을 종합해 우리는 참조라고 말합니다. 앞의 두 개를 lvalue references라고 말합니다.


7.7.1 Lvalue References

  타입 이름에서 X&의 표기는 "X의 참조"를 의미합니다. lvalue를 참조하는 데 사용되며, 이를 lvalue reference라고 말합니다:

1
2
3
4
5
6
7
8
void f()
{
    int var = 1;
    int& r {var}; // r and var now refer to the same int
    int x = r; // x becomes 1

    r = 2; // var becomes 2
}
cs

  참조가 무언가의 이름임을 보장하기 위해서, 우리는 참조를 반드시 초기화해야 합니다:

1
2
3
4
int var = 1;
int& r1 {var}; // OK: r1 initialized
int& r2; // error : initializer missing
extern int& r3; // OK: r3 initialized elsewhere
cs

  참조의 초기화는 할당과는 약간 다릅니다. 보기와는 달리, 참조에는 어떤 연산자도 연산되지 않습니다:

1
2
3
4
5
6
7
void g()
{
    int var = 0;
    int& rr {var};
    ++rr; // var is incremented to 1
    int∗ pp = &rr; // pp points to var
}
cs

  ++rr은 참조 rr을 증가시키는 것이 아닙니다. rr이 참조하는 var의 정수형 값을 증가시키죠. 결론적으로, 참조의 값은 초기화 이후 변할 수 없습니다. 만약 rr이 나타내는 객체의 포인터를 얻고 싶다면, &rr을 쓰면 됩니다. 따라서, 참조의 포인터를 얻을 순 없습니다. 더 나아가, 참조의 배열을 정의할 수 없습니다. 그러한 점에서, 참조는 객체가 아닙니다.

  명확한 참조의 구현은 그것이 사용될 때마다 역참조 되는 (상수)포인터입니다. 참조가 포인터처럼 생성되지 않는다는 것만 기억한다면, 앞서 설명한 대로 생각하여도 상관 없습니다.



  아주 가끔, 컴파일러가 최적화를 위해 참조를 날려버리고, 해당 참조가 런타임에 아무 객체도 가리키지 않는 경우가 생길 수 있습니다.

  참조의 초기화는 초기자가 lvalue인 경우 간단합니다. "plain" T&에 대한 초기자는 T 타입의 lvalue여야 합니다. 

  const T&의 경우, lvalue이거나 T 타입일 필요도 없습니다. 이러한 경우에:

  1. 필요에 따라 T로의 묵시적 타입 변환이 적용될 것입니다.
  2. 결과 값이 T 타입 임시 변수에 저장될 것입니다.
  3. 임시 변수의 값이 초기자의 값으로 사용될 것입니다.

가령:

1
2
double& dr = 1; // error : lvalue needed
const double& cdr {1}; // OK
cs

  2번 줄의 경우 다음과 같이 변경된다고 생각할 수 있습니다:

1
2
double temp = double{1}; // first create a temporar y with the right value
const double& cdr {temp}; // then use the temporar y as the initializer for cdr
cs

  임시로 생성된 값은 참조의 스코프 끝까지 유지될 것입니다.

  변수의 참조와 상수의 참조는 다릅니다. 변수에 대해서 임시로 무언가를 생성하는 것은 굉장히 error-prone한 일입니다. 변수에 곧 사라질 임시 변수를 할당하는 것이죠. 상수에 대한 참조는 이러한 문제를 일으키지 않습니다. 또한 함수 인자에서 중요한 역할을 합니다.

  참조는 함수 인자를 명세하는 데 사용될 수 있으며 함수는 전달된 객체의 값을 변경시킬 수 있게 됩니다:

1
2
3
4
5
6
7
8
9
10
void increment(int& aa)
{
    ++aa;
}

void f()
{
    int x = 1;
    increment(x); // x=2
}
cs

  인자 전달은 곧 해당 변수를 초기화해 정의하는 것이며, increment의 인자 aa는 x의 다른 이름이 됩니다. 프로그램을 가독성 있게 만들기 위해서, 이렇게 인자를 직접 변경하는 함수의 형식은 피하는 것이 좋습니다. 대신, 명시적으로 값을 반환해 사용할 수 있죠:

1
2
3
4
5
6
7
8
int next(int p) { return p+1; }

void g()
{
int x = 1;
increment(x); // x=2
= next(x); // x=3
}

cs

  incremente(x)의 표기는 x의 값이 변경된다는 근거를 제시하지는 않습니다. 하지만 x=next(x)는 x 값이 변경될 것임을 명확히 알 수 있죠. 결론적으로, "plain" 참조 인자는 반드시 함수의 이름으로부터 해당 인자가 변경될 수 있음을 강력하게 알 수 있는 근거를 마련해주어야 합니다.

  참조는 반환 값으로도 줄 수 있습니다. 이는 left-hand, right-hand 측의 양쪽에서 대입할 수 있는 함수를 정의할 때 가장 많이 사용됩니다:

1
2
3
4
5
6
7
8
9
10
template<class K, class V>
class Map { // a simple map class
public:
    V& operator[](const K& v); // return the value corresponding to the key v

    pair<K,V>∗ begin() { return &elem[0]; }
    pair<K,V>∗ end() { return &elem[0]+elem.size(); }
private:
    vector<pair<K,V>> elem; // {key,value} pairs
};

cs

  표준 라이브러리 map은 전형적인 레드-블랙 트리로 구현됩니다. 하지만 잡다한 구현의 세부사항을 피하기 위해서, 선형 탐색에 기반한 키 매칭 구현을 보여드리려고 합니다.

1
2
3
4
5
6
7
8
9
10
template<class K, class V>
V& Map<K,V>::operator[](const K& k)
{
    for (auto& x : elem)
        if (k == x.first)
            return x.second;

    elem.push_back({k,V{}}); // add pair at end (§4.4.2)
    return elem.back().second; // return the (default) value of the new element
}
cs

  키 인자로 k를 전달합니다. 이는 복사하기엔 비용이 클 수 있으므로, 참조로 받습니다. 비슷하게, 반환 값 역시 참조입니다. 저는 k의 const 참조를 활용했습니다. 왜냐하면 해당 인자를 변경하고 싶지 않으며, 리터럴 혹은 임시 객체로서 활용하고 싶었기 때문입니다. 반환의 경우 non-const 참조입니다. 사용자는 찾은 값을 이용해서 변경을 하고 싶을 수 있기 때문이죠:

1
2
3
4
5
6
7
8
9
int main() // count the number of occurrences of each word on input
{
    Map<string,int> buf;

    for (string s; cin>>s;) ++buf[s];

    for (const auto& x : buf)
        cout << x.first << ": " << x.second << '\n';
}
cs

  해당 코드는 표준 입력 스트림인 cin을 이용해서 s에 문자열을 입력 받습니다. buf에는 입력된 스트링이 몇 번 입력되었는지를 저장합니다. 마지막으로, buf에 저장된 각각의 단어가 입력된 숫자를 출력하게 됩니다. 예를 들어 "aa bb bb aa aa bb aa aa"의 경우, aa: 5, bb: 3이라는 출력이 나타나게 될 것입니다.

  for문이 동작하는 이유는 Map의 begin()과 end() 덕분입니다. 표준 라이브러리 map에서ㅏ도 이를 찾아볼 수 있죠.

댓글

이 블로그의 인기 게시물

[코딩의탑] 4층: 툰 쉐이딩

[코딩의탑] 3층: 바다 렌더링

[코딩의탑] 5층: 포탈(Portal), 더 나아가기