본문 바로가기

프로그래밍/C++

[Study] C++ 기초(2) - reference

[ reference (참조자) ]

 

이번엔 참조자라는 C++에서 새로 도입된 개념을 다루도록 하겠다.

C언어에서 어떤 변수를 가리키려면 우리는 항상 포인터라는 개념을 사용하였지만,

C++에서는 새로운 방식이 도입된다. 바로 reference (참조자) 라는 것이다.

 

참조자란, '할당된 하나의 메모리 공간에 다른 이름을 붙이는 것'을 말하는데,

즉 참조할 변수의 새로운 하나의 이름을 만들어주는, 별명을 붙여주는 개념이다.

 

예전에 포인터를 공부하며 Call-by-valueCall-by-reference에 대해 배운 적이 있을 것이다.

Call-by-value 방식은 값을 인자로 전달하여 기존 변수에 접근하지 못하고,

Call-by-reference 방식은 주소를 인자로 전달하여 기존 변수의 변형이 가능하다.

(call-by-address / call-by-reference는 컴파일러 입장에서는 거의 동일)

여기서 나오는 reference가 우리가 알아갈 개념이다.

 

따라서, Call-by-reference 방식에서처럼 기존 변수의 변형이 가능하게 하려면,

새로 만든 변수도 같은 메모리 공간을 주소로 가져야 한다.

코드를 통해서 확인해보자.


#include <iostream>

int main()
{
    int d = 5;
    int& refer_d = d;
    
    refer_d = 10;
    
    // value
    std::cout << d << std::endl;
    std::cout << refer_d << std::endl;
    
    // address
    std::cout << &d << std::endl;
    std::cout << &refer_d << std::endl;
    return (0);
}

 

실행결과 )

10
10
0x7fffd47fc2dc
0x7fffd47fc2dc

 

우선 참조자를 만드는 방식부터 살펴보도록 하겠다.

위에서와 같이 가리키는 변수의 타입 뒤에 &를 붙이면 참조자가 만들어진다.

 

d라는 변수를 만들고, refer_d라는 참조자를 선언하면,

refer_d는 d의 또다른 이름임을 컴파일러에게 알려주는 것이 되며,

refer_d에 어떠한 작업을 하든 d에 작업을 하는 것과 똑같다.

 

따라서, refer_d의 값을 10으로 바꿨더니, d의 값을 확인하였을 때 10인 것을 확인할 수 있다.

또한, d와 refer_d의 주소도 출력해보면, 둘다 동일한 메모리 공간의 주소를 가리키고 있다.


#include <iostream>

int main()
{
    // 불가능한 cases
    
    // 1) 상수 참조
    int& d1 = 3;
    // 2) 참조하는 대상이 없을 때
    int& d2;
    // 3) NULL 값 참조
    int& d3 = NULL;
    
    // 상수 참조 예외
    const int& d4 = 5; // 가능
}

 

그렇다면, reference를 활용할 때, 사용이 불가능한 케이스들을 살펴보자.

 

우선 상수를 참조하는 경우이다. 상수의 별명을 짓는 것은 불가능하다.

(but, const int& 처럼 데이터 수정이 불가능한 const 상수 참조자로 선언한다면 상수 참조 가능 !)

두번째로, 참조하는 대상이 없을 때이다. 선언해두고 추후에 참조하는 것이 불가능하다. 선언과 동시에 참조해야 함.

마지막으로, NULL 참조 역시 불가능하다. 어떠한 하나의 대상을 참조해야만 한다.


reference는 그럼 메모리 상에 존재하게 되는 걸까?

포인터는 주소값을 저장하기 때문에 8바이트를 차지하게 되는데,

reference는 사실 상 기존 변수의 공간을 똑같이 사용하므로, 메모리 공간을 할당할 필요가 없다! (특수 케이스가 있을 수 있음)


- 함수 인자로 reference 사용하기

#include <iostream>

int change_value(int &d)
{
    d = 100;
    return (0);
}

int main()
{
    int n = 1;

    std::cout << n << std::endl;
    change_value(n);
    std::cout << n << std::endl;
    return (0);
}

 

실행결과 )

1
100

 

이번에는 함수 인자로 reference가 오는 케이스를 살펴보도록 하겠다.

int change_value(int &d) 를 보면, 인자로 reference가 왔다.

분명 위에서 참조하는 대상이 없으면 불가능한 케이스라고 했는데, 위 상황은 뭘까?

 

main문을 보면, change_value(n)이 있다.

따라서, n을 참조하는 reference가 d가 되는 것이고,

int &d = n; 이 실행된 상태와 마찬가지이므로 정상적인 참조 과정이다.

 

따라서 main문에서 n = 100; 을 실행한 것과 완벽히 일치하는 작업이 이뤄진 것이다.


- 참조자의 참조자?

#include <iostream>

int main()
{
    int a;
    int& b = a;
    int& c = b;
    
    a = 1;
    std::cout << a << " " << b << " " << c << std::endl;
    b = 2;
    std::cout << a << " " << b << " " << c << std::endl;
    c = 3;
    std::cout << a << " " << b << " " << c << std::endl;
    return (0);
}

 

실행결과 )

1 1 1
2 2 2
3 3 3

 

이번 경우는 reference로 만든 b를 다시 다른 reference인 c로 참조하는 경우인데,

a의 별명인 b, b의 별명이 c라고 생각하면, 결국 c도 a의 별명이라는 것을 알 수 있다.

결과적으로 b, c 모두 a의 reference가 된다.

 

참조자의 참조자라는 개념이 아니라,

어떤 reference를 참조하게 되면 그 reference가 참조 중인 대상을 똑같이 참조하는 것.


- reference의 배열 vs 배열의 reference

// 불가능한 케이스 : reference의 배열
int a, b;
int& ref[2] = {a, b};

// 가능한 케이스 : 배열의 reference
int arr[5] = {1, 2, 3, 4, 5};
int(&res)[5] = arr;
res[0] = 9;
std::cout << arr[0] << std::endl; // = 9

 

reference의 배열과 배열의 reference를 만들어보려고 한다.

 

우선, reference의 배열을 만들면, 부여된 배열의 순서로 주소가 나열되고,

주소가 존재하게 되면 메모리 상에 어떤 자리를 차지하게 된다.

이는 reference의 원칙인 기존 메모리 공간의 참조가 아니라, 새로운 메모리 공간을 가지게 되므로, 

C++ 언어 차원에서 존재할 수 없다고 규정해두었다. ( arrays of references are illegal )

 

반대로 배열의 reference는 위의 코드와 같이 만들었을 때 참조하는 배열의 각각의 원소가 참조가 된다.

하지만 반드시 배열의 크기를 명시하여야 하며, 동일한 배열 크기의 배열만 참조할 수 있다.


- reference 를 return 하는 함수?

#include <iostream>

int& function()
{
    int a = 10;
    return (a);
}

int main()
{
    int b = function();
    std::cout << b << std::endl;
    return (0);
}

 

reference 를 리턴하는 함수를 만들었다. 이를 main문에서 불러와서 쓰게 된다면 어떻게 될까?

우선 컴파일 시 경고가 나오며, 실행 시 segmentation fault (core dumped) 가 등장한다.

 

알다시피, 함수 내에서 쓰는 변수들은 지역변수로써 함수 밖을 벗어나면 사라지게 된다.

그럼 function에서 사용한 a는 사라질 것이고, return 되는 a는 값이 복사되어 main의 b에 저장될 것이다.

그런데, int&으로 반환하게 되면, 참조하고 있던 변수는 사라지고 별명만 남아 b에 담기는 것이다.

 

따라서 정상적인 동작이 불가능하게 되며,

이렇게 reference만 있고 참조하던 대상이 사라진 reference를 'Dangling reference' 라 한다.

 

#include <iostream>

int& function(int& a)
{
    a = 10;
    return (a);
}

int main()
{
    int x = 5;
    int b = function(x);
    std::cout << b << std::endl;
    return (0);
}

 

dangling reference와 같은 상황을 피하기 위해서는,

위의 함수처럼 인자로 받은 reference를 그대로 리턴해주면 된다.

main 문에 그대로 존재하고 있는 x라는 변수의 참조인 a 이므로, x의 값이 5에서 10으로 바뀐 후,

b라는 변수에 10이라는 값이 대입되어 저장될 것이다.

 

이런 식으로 reference 를 리턴하게 되면 주소값 복사 1번만으로 전달이 되므로,

메모리 전체를 복사하는 방식보다 훨씬 효율적이다.

'프로그래밍 > C++' 카테고리의 다른 글

[Study] C++ 기초(1) - namespace  (2) 2025.03.12
Recent Posts
Popular Posts
Recent Comments