의 차이는 무엇일까?
'참조자'는 성격상 포인터와 비유되기 쉽다. 그러나 참조자는 포인터를 모르는 사람도 이해할 수 있는 개념이다.
"변수란 할당된 메모리 공간에 붙여진 이름이다. 그리고 그 이름을 통해서 해당 메모리 공간에 접근이 가능하다."
그렇다면, 할당된 하나의 메모리 공간에 둘 이상의 이름을 부여할 수는 없을까?
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
가 될 것이다. 즉 참조자는 자신이 참조하는 변수를 대신할 수 있는 또 하나의 이름인 것이다!
int num1 = 1234;
int &num2 = num1;
int &num3 = num2;
int &num4 = num3;
와 같이 선언이 가능하고, 1234로 초기화된 메모리 공간에 num1,num2,num3,num4로 이름을 붙인 꼴이 된다.
int &ref = 20; // X!!
int &ref; // X!!
int &ref = NULL; // X!!
int &ref = num; // O :)
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라는 임시 별칭을 통해 메모리 공간에 직접 접근하게 된다.
참조자를 활용하여 함수 외부에 선언된 변수에 접근할 수 있음을 확인했다.
잠깐 다음의 코드를 보자.
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에 어떤 값을 저장하는 경우 컴파일 에러가 발생한다.
함수의 반환형에도 참조자가 사용될 수 있다.
#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은 소멸된다. 그러므로 위와 같은 일은 없어야 한다.