본문 바로가기
  • 쓸쓸한 개발자의 공부방
C++

[C++] reference 개념

by 심찬 2021. 8. 13.

 

c++ reference 개념

 

아주 기본 개념이지만 완벽하게 이해하지 않고 개발하려면 매우 혼란스럽다. reference 개념을 확실하게 이해해야 한다.

c에서는 pointer의 개념, 주소의 개념이 정말 중요했다면, 추가로 c++에서는 reference 개념을 정확히 이해해야 한다.

 

변수

  - 메모리의 특정 위치를 가리키는 이름이다.

  - 코드 안에서 해당 메모리에 접근하기 위해서 사용한다.

 

레퍼런스

  - 기존 변수(메모리)또 다른 이름(alias)를 부여하는 것

  - 아래 코드에서 n이 기존 변수 이며, r이 새로운 reference 변수 이름이다.

#include <iostream>

int main()
{
    int n = 10;
    
    n = 20;
    
    //int* p = &n;   // 포인터 변수에서 주소를 꺼내는 방법
    int& r = n;      // 레퍼런스 : 기존 변수의 또다른 alias (별명) 을 만든다.
    
    r = 30; // n = 30

    std::cout << n << std::endl; // 30
    std::cout << r << std::endl; // 30
    std::cout << &n << std::endl;
    std::cout << &r << std::endl; 
}

n의 주소값 (&n)과 r의 주소값(&r) 이 동일함을 확인 할 수 있다.

 

 

 

reference 개념 이해를 위한 예제

 

해당 예제에서 각 변수 a,b,c가 호출되었을 때 값들이 어떻게 저장되고 어떤 값이 증가(++) 수행되는지 과정을 살펴볼 수 있다.

#include <iostream>

void f1(int n)  { ++n;}
void f2(int* p) { ++(*p);}
void f3(int& r) { ++r;}   // int& r = c

int main()
{
    int a = 0, b = 0, c = 0;
    
    f1(a);
    f2(&b); // b가 변경될수 있다고 예측가능.
    f3(c);  // 
    
    std::cout << a << std::endl; // 0
    std::cout << b << std::endl; // 1
    std::cout << c << std::endl; // 
    
    int* p = &n;
    int& r = n;
}

 

 

 

1) a, b, c 세 개의 변수 공간이 생성됨

    int a = 0, b = 0, c = 0;

2) f1(a); 수행이 되면,

또 다른 n 이라는 메모리 공간(5000)이 만들어진다.

n 이라는 값을 새로운 메모리 공간(5000)의 n 이 {++n} 에 의해 증가 된다.

call by value 형태로 원본인 a 값은 수정되지 않는다. 

void f1(int n)  { ++n;}   // 인자를 받을 때 값으로 받는다!

int main()
{
    int a = 0, b = 0, c = 0; 
    f1(a);
}

 

 

3) f2(&b); 가 수행되면,

 포인터 변수가 만들어지면서 2000이라는 주소가 p의 메모리 공간(6000)에 저장된다. 

 *p 내가 가리키는 공간(2000)의 값을 ++ 시킨다.

 call by pointer 형태로 원본 값(b)이 수정된다.

void f1(int n)  { ++n;}
void f2(int* p) { ++(*p);}

int main()
{
    int a = 0, b = 0, c = 0;
    
    f1(a);
    f2(&b); // b가 변경될수 있다고 예측가능.
}

 

 

 

 

 

4) f3(c); 를 수행하면

 f3 안의 r 주소를 출력해 보면 c의 주소와 동일하다.

 r이 가리키는 c 의 값을 증가시킨다.

 reference는 포인터와 마찬가지로 원본을 수정할 때 사용한다.

 

#include <iostream>

void f1(int n)  { ++n;}
void f2(int* p) { ++(*p);}
void f3(int& r) { ++r;}   // int& r = c

int main()
{
    int a = 0, b = 0, c = 0;
    
    f1(a);
    f2(&b); // b가 변경될수 있다고 예측가능.  (오히려 reference보다 가독성이 좋은 면이 있다.)
    f3(c);  // c가 변경될 수 있다고 예측하기가 애매하다.
            // (reference 사용이 간편하다는 장점,
            // 하지만 직관적이지 않은 단점이 있다 call by value와 형태가 동일!.)
    
    std::cout << a << std::endl; // 0
    std::cout << b << std::endl; // 1
    std::cout << c << std::endl; // 1
    
    int* p = &n;   // 포인터 변수는 초기화 할때 오른쪽에 주소를 쓴다.
    int& r = n;    // 레퍼런스 변수는 초기화 할 때 변수를 그대로 쓴다.
}

 

 

 

 

Pointer  VS   Reference

 

포인터와 레퍼런스 변수 사용 방식이 다르고 그 차이를 정확하게 이해해야 한다.

 

이 코드 세 줄이 수행되면 아래와 같이 메모리 공간이 생성될 것이다.

    int n = 10;
    
    //  ============  포인터  ===================
    int* p1 = &n;  // 1. 변수 주소로 초기화,
                   // 2. *연산자 사용 *p1 = 10
                   // 3. NULL 가능,
                   // 4. 포인터 변수의 주소를 출력

    //  ============  레퍼런스  ===================
    int& r1 = n;   // 1. 변수 이름으로 초기화 (초기값 반드시 필요),
                   // 2. *연산자 필요없음
                   // 3. NULL 불가
                   // 4. 기존 변수와 동일 주소를 가짐

포인터 p2는 가리키는 곳은 없는 상태인 null 이 될 수 있다.

하지만 레퍼런스 r2는 null이 불가하며, 연결짓는 변수가 지정되어야 한다. 레퍼런스 변수는 별도의 공간이 생성되지 않고 연결만 되기 때문이다.

#include <iostream>

int main()
{
    int n = 10;
    
    int* p1 = &n;
    int& r1 = n;
        
    int* p2 = 0; // null pointer 가능.
    int& r2;     // error. : 초기값 없어서 에러 발생
    
    *p1 = 20;    // * 연산자 사용
    r1 = 20;     // * 연산자 필요없음  ==> 자동 * 연산되는 포인터
    
    if ( p1 != 0 ) {}   // pointer는 NULL이 가능하기 때문에 nullptr 체크 중요함!!!
    if ( r1 != 0 ) {}   // r1은 if 문으로 조사할 필요가 없다.
    
    std::cout << &p1 << std::endl;    // 포인터 변수의 주소값이 출력됨.
    std::cout << r1 << std::endl;     // 기존 변수와 동일
}

 

int& r2; 초기값 없어서 에러 발생함.

main.cpp:11:10: error: ‘r2’ declared as reference but not initialized

에러 발생 부분을 주석처리 후 실행하면 아래 결과를 볼 수 있다.

 

 

 

댓글