[C++] 참조자(reference)

김형태·2021년 5월 3일
0

C++

목록 보기
2/13
post-custom-banner

0. 들어가기 전

  • const int num = 10;
  • const int * ptr1 = &val1;
  • int * const ptr2 = &val2;
  • const int * const ptr3 = &val3;

의 차이는 무엇일까?

  • const int num = 10;
    : 변수 num을 상수화
  • const int * ptr1 = &val1;
    : 포인터 ptr1을 이용해서 val1 값을 변경할 수 없음
  • int * const ptr2 = &val2;
    : 포인터 ptr2가 상수화 됨
  • const int * const ptr3 = &val3;
    : 포인터 ptr3가 상수화 되었으며, ptr3을 이용해서 val3의 값을 변경할 수 없음

1. 참조자란?

'참조자'는 성격상 포인터와 비유되기 쉽다. 그러나 참조자는 포인터를 모르는 사람도 이해할 수 있는 개념이다.

1.1. 변수란 무엇일까?

"변수란 할당된 메모리 공간에 붙여진 이름이다. 그리고 그 이름을 통해서 해당 메모리 공간에 접근이 가능하다."

그렇다면, 할당된 하나의 메모리 공간에 둘 이상의 이름을 부여할 수는 없을까?

int num1 = 2021;

위의 변수 선언을 통해 2010으로 초기화 된 메모리 공간에 num1이라는 이름이 붙게 된다. 이 때,

int &num2 = num1;

과 같이 선언하면 같은 메모리 공간에 이름이 하나 더 붙게 되는 것이다.

& 연산자는 이미 선언된 변수의 앞에 오면 주소값을 반환하지만, 새로 선언되는 변수의 이름 앞에 등장하면 참조자의 선언을 뜻하게 된다.

int *ptr = &num1; // ptr에 변수 num1의 주소값을 저장
int &num2 = num1; // 변수 num1에 대한 참조자 num2

2021로 초기화 된 공간에 num1과 num2라는 두 이름이 생겼다. 아래의 코드를 실행하면 어떻게 될까?

int num1 = 2021;
int &num2 = num1;

num2 = 4042;

std::cout << num1 << std::endl;
std::cout << num2 << std::endl;

결과는 모두 4042가 될 것이다. 즉 참조자는 자신이 참조하는 변수를 대신할 수 있는 또 하나의 이름인 것이다!

1.2. 참조자의 특징

1.2.1. 참조자의 수에는 제한이 없으며, 참조자를 대상으로도 참조자를 선언할 수 있다. 즉,

int num1 = 1234;
int &num2 = num1;
int &num3 = num2;
int &num4 = num3;

와 같이 선언이 가능하고, 1234로 초기화된 메모리 공간에 num1,num2,num3,num4로 이름을 붙인 꼴이 된다.

1.2.2. 참조자는 변수에 대해서만 선언이 가능하다.

int &ref = 20; // X!!

1.2.3. 참조자는 선언과 동시에 누군가를 참조해야만 한다.

int &ref; // X!!
int &ref = NULL; // X!!
int &ref = num; // O :)

2. 참조자와 함수

2.1. call-by-value & call-by-reference

  • call-by-value: 값을 인자로 전달하는 함수의 호출방식
  • call-by-reference: 주소 값을 인자로 전달하는 함수의 호출 방식

call-by-value의 형태로 정의된 함수의 내부에서는, 함수 외부에 선언된 변수에 접근이 불가능하다. swap함수를 떠올려 보자.

void	swapByValue(int num1, int num2)
{
	int temp = num1;
    num1 = num2;
    num2 = temp;
    return ;
} // call-by-value

위 함수의 호출하면 어떻게 될까? 그리고 아래 함수를 호출하면 어떻게 될까?

void	swapByReference(int *ptr1, int *ptr2)
{
	int temp = *ptr1;
    *ptr1 = *ptr2;
    *ptr2 = temp;
} // call-by-reference

call-by-value와 call-by-value를 구분하는 기준은 주소 값이 참조의 도구로 사용되었냐이다.

C++에서는 함수 외부에 선언도니 변수의 접근방법으로 포인터가 아닌 다른 방법이 존재하는데, 바로 그것이 참조자다. 다음의 코드를 작성하고 실행해보자.

#include <iostream>

void	swapByReference2(int &ref1, int &ref2)
{
	int temp = ref1;
    ref1 = ref2;
    ref2 = temp;
} // call-by-reference

int main(void)
{
	int val1 = 10;
    int val2 = 20;
    
    swapByReference2(val1, val2);
    std::cout << "val1: " << val1 << std::endl;
    std::cout << "val2: " << val2 << std::endl;
}
	return (0);

val1 : 20;
val2 : 10;

위 함수에서 swapByReference2를 호출할 때, val1과 val2를 인자로 넘겨주면 val1과 val2에게 ref1과 ref2라는 임시 별칭이 생기고, ref1과 ref2라는 임시 별칭을 통해 메모리 공간에 직접 접근하게 된다.

2.2. const 참조자

참조자를 활용하여 함수 외부에 선언된 변수에 접근할 수 있음을 확인했다.

잠깐 다음의 코드를 보자.

int num = 30;
simpleFunc(num);
std::cout << num << std::endl;

과연 위 코드의 출력 결과는 어떻게 될까? C언어 관점에서는 주소값을 넘겨주지 않았으니 당연히 30이 출력되어야 한다. 하지만 C++에서는 얼마가 출력될지 알 수 없다! 함수가 다음과 같이 정의되어 있다면 바뀌지 않겠지만,

void simpleFunc(int num) {...}

만약 다음과 같이 정의되어 있다면,

void simpleFunc(int &ref) { ... }

함수 안에서 ref에 접근해서 값을 바꿀 수도 있기 때문이다!

즉, C언어에서는 함수의 호출문장만 보고도 값이 변경되지 않음을 알 수 있지만, C++에서는 최소한 함수 원형은 확인해야 하는 것이다.

그러나.. 이러한 단점을 극복할 수 있는 방법이 있다. 바로 다음과 같이 const 선언을 하는 것이다.

void simpleFunc(const int & ref) { ... }

이는 다음과 같은 의미를 지닌다.

"함수 simpleFunc 내에서 참조자 ref를 이용한 값의 변경은 하지 않겠다!"

위와 같이 선언하면 ref에 어떤 값을 저장하는 경우 컴파일 에러가 발생한다.


2.3. 반환형이 참조자

함수의 반환형에도 참조자가 사용될 수 있다.

#include <iostream>

int&	returnReference(int &ref)
{
	ref++;
    return (ref);
}

int		main(void)
{
	int num1 = 1;
    int &num2 = returnReference(num1);
    
    num1++;
    num2++;
    std::cout << "num1: " << num1 << std::endl;
    std::cout << "num2: " << num2 << std::endl;
}

num1: 4
num2: 4

위 코드를 실행하면 다음과 동일한 상황이 발생한다.

int num1 = 1;
int &ref = num1;
int &num2 = ref

하지만 ref는 함수 호출 시 임시로 생긴 참조자이므로 반환 시에 삭제된다.

만약 returnReference의 결과를 다음과 같이 참조자가 아닌 정수형 변수에 저장하면 어떻게 될까?

#include <iostream>

int&	returnReference(int &ref)
{
	ref++;
    return (ref);
}

int		main(void)
{
	int num1 = 1;
    int num2 = returnReference(num1);
    
    num1++;
    num2++;
    std::cout << "num1: " << num1 << std::endl;
    std::cout << "num2: " << num2 << std::endl;
}

num1: 3
num2: 3

num2는 참조자가 아니므로, num2에는 ref가 참조하는 값만 저장이 된다.

그럼 반대로, 반환형이 int인 함수를 참조자 선언에 사용할 수 있을까?

int returnReference2(int &ref)
{
	ref++;
    return (ref);
}

를 다음과 같이 반환할 수 있을까?

int &num2 = returnReference2(num1);

답은 '안 된다'이다. 반환 값이 상수나 다름 없기 때문에 참조자 선언 시에 사용할 수가 없는 것이다.

반환형이 참조자인 함수가 함수 내에 선언된 지역변수를 반환할 수도 있을까?

int	returnRef(int n)
{
	int num = 20;
    num += n;
    return (num);
}
int &ref = returnRef(10);

이러면 지역변수 num에 ref라는 별칭이 생기게 되는 꼴인데, 함수가 반환되면 함수 안에 선언된 지역변수 num은 소멸된다. 그러므로 위와 같은 일은 없어야 한다.

profile
steady
post-custom-banner

0개의 댓글