TIL
참조자
C언어에서는 어떠한 변수를 가리키고 싶을 때는 반드시 포인터를 사용해야 했음.
ㅡ> C++에서는 다른 변수나 상수를 가리키는 다른 방식이 존재. 이를 참조자(reference)라고 부름.
#include <iostream>
using namespace std;
int main(){
int num=3;
int& refNum=num;
refNum=5;
cout<<"num : "<<num<<endl;
cout<<"refNum : "<<refNum<<endl;
return 0;
}
실행결과를 보면 알 수 있듯이 refNum은 num의 참조자이기 때문에 refNum은 num의 또다른 이름(별명)이라고 컴파일러에게 인식되어 refNum에 작업을 수행하는 것들은 사실상 num에 작업하는 것과 마찬가지인 것입니다. 따라서 실행결과가 num, refNum 모두 3이었던 값이 5로 바뀐 것을 확인할 수 있습니다.
*유의사항 : 참조자는 정의 시에 반드시 누구의 별명인지 명시 해줘야 합니다. 아래와 같은 코드는 불가능!
int& refNum;
반면 포인터는 아래와 같이 정의해도 전혀 문제가 없다.
int* refNum;
더불어 참조자는 한 번 별명이 되면 절대로 다른 것의 별명이 되는 것은 불가능하다. (재정의 불가)
int num=10;
int &refNum=num;
int num2=5;
refNum=num2;
마지막 코드를 살펴보면 알 수 있는데 refNum=num2는 refNum이 num2의 별명이 되는 것이 아니라 refNum이 가리키는 num에 num2를 대입하라는 num=num2와 같은 의미가 되버리기 때문에 다른 것의 별명이 될 수 없다.
다시 한번 포인터의 경우로 보면
int num=10;
int* ptr=# // p->num
int num2=5;
p=&num2; // p->num2
전혀 문제 없이 가리키는 대상을 바꾸게 된다.
*참조자는 메모리 상에 존재하지 않을 수 있다.
포인터의 경우는 선언되는 순간 메모리를 차지하게 됩니다.
int num=10;
int &refNum=num;
위 코드에서 참조자는 메모리 상에 공간을 할당해 줄 필요가 없다. refNum이 언급되는 부분을 모두 num으로 바꿔주면 되는 문제이기 때문이다. 따라서 이러한 경우에는 참조자는 메모리 상에 존재하지 않게 된다.
#include <iostream>
using namespace std;
int changeVal(int &ref){
ref=3;
return 0;
}
int main(){
int num=5;
cout<<num<<endl;
changeVal(num);
cout<<num<<endl;
return 0;
}
하지만 위와 같이 함수의 인자를 참조자로 받는 경우에는 ref가 main함수의 num을 가리키는 별명이 되어 아래와 같은 결과가 출력된다.
상수에 대한 참조자
int &ref=4;
위와 같은 코드는 오류를 발생시킵니다. 그 이유는 상수 값 자체는 소스코드 상에서 고정된 값인 리터럴(literal)이기 때문에 "ref=5;"와 같은 코드로 리터럴의 값을 바꾸는 행위가 가능하게 되기 때문에 C++문법 상 상수 리터럴을 일반적인 참조자가 참조하는 것을 불가능합니다.
대신 아래와 같이 상수 참조자로 선언하면 리터럴도 참조가 가능합니다.
const int &ref=4;
int a=ref;
따라서 위 코드가 "a=4;"와 같이 취급됩니다.
참조자 배열은 불가능하다
int a, b;
int& arr[2]={a,b};
위 코드는 오류를 발생시킵니다. 언어차원에서 불가능하다고 정해 놓은 것입니다. 생각해 보면 배열의 이름은 arr의 첫 번째 원소의 주소값으로 변환이 가능해야 합니다. 이를 통해 arr[1]이 *(arr+1)로 바뀌어 처리가 가능하기 때문입니다. 이때, '주소값이 존재한다는 의미는 해당 원소가 메모리 상에서 존재한다 ' 와 같습니다. 하지만 위에서 언급했듯이 참조자는 특별한 경우가 아니면 메모리 상에서 공간을 차지하지 않고, 이러한 모순 때문에 언어 차원에서 참조자들의 배열을 정의하는 것이 금지되어 있습니다.
하지만 그 반대로 배열들의 레퍼런스는 불가능하지 않습니다.
#include <iostream>
using namespace std;
int main() {
int arr[3] = {1, 2, 3};
int(&ref)[3] = arr;
ref[0] = 2;
ref[1] = 3;
ref[2] = 1;
cout << arr[0] << arr[1] << arr[2] << std::endl;
return 0;
}
결과를 보면 알 수 있듯이 레퍼런스가 arr의 별명이 되어 레퍼런스를 통해 수정한 값들이 arr에서도 바뀐 것을 알 수 있습니다.
*유의사항 : 반드시 같은 크기로 설정해 줘야 합니다.
지역변수의 레퍼런스를 리턴하는 경우
#include <iostream>
using namespace std;
int& function() {
int a = 2;
return a;
}
int main() {
int b = function();
b = 3;
return 0;
}
위와 같은 경우에 컴파이을 하게 되면 아래와 같은 경고가 나옵니다.
문제가 되는 부분은 function()에서 반환하는 값이 참조자이기 때문입니다. return a;의 경우 값을 반환하고 지역변수인 a는 사라지기 때문에 참조하고 있던 지역변수가 이미 사라졌으니 오류가 발생하는 것입니다. 이와 같이 참조자는 있는데 참조하던 것이 사리진 참조자를 Dangling reference라고 합니다.
참조자가 아닌 값을 반환하는 함수를 참조자로 받기
int function() {
int a = 5;
return a;
}
int main() {
int& c = function();
return 0;
}
위와 같은 경우에도 Dangling reference가 되면서 오류가 발생합니다. 함수가 반환하는 값인 'a'가 반환 이후 바로 사라지는 값이기 때문에 만들어진 참조자가 바로 Dangling reference가 되기 때문입니다.
외부 변수의 참조자
int& function(int& a) {
a = 5;
return a;
}
int main() {
int b = 2;
int c = function(b);
return 0;
}
위와 같이 코드를 작성하게 되면 정상 컴파일됩니다. main에서 변수 'b'를 참조하여 매개변수로 전달해 줬고 이를 다시 반환해 줬으니 참조자가 가리키던 변수 'b' 가 그대로 존재하여 문제가 없기 때문입니다.
참조자를 반환하는 경우의 장점
C언어에서 엄청나게 큰 구조체 변수를 그냥 반환하면 전체 복사가 이루어져야 해서 시간이 오래걸립니다. 하지만, 해당 구조체를 가리키는 포인터를 반환한다면 주소 한 번만 복사하면 끝나기에 비교적 매우 빠릅니다.
마찬가지로 참조자를 반환하게 된다면 참조자가 참조하는 타입의 크기와 상관 없이 딱 한 번의 주소값 복사로 전달이 끝나게 되어 매우 효율적입니다.
참고자료
씹어먹는 C++ - <2. C++ 참조자(레퍼런스)의 도입>
모두의 코드 씹어먹는 C++ - <2. C++ 참조자(레퍼런스)의 도입> 작성일 : 2012-01-01 이 글은 128942 번 읽혔습니다. 안녕하세요 여러분! 오랜만에 찾아온 Psi 입니다. 사실 이전 강좌에서 부터 강조해왔지
modoocode.com
'C++' 카테고리의 다른 글
[C++] STL 요약 정리 #1 (0) | 2025.02.17 |
---|---|
[C++] STL(Standard Template Library) (0) | 2025.01.15 |
[C++] 메모리 누수 점검 (0) | 2025.01.02 |
[C++] 오버로딩(overloading), 오버라이딩(overriding), 템플릿(Template) (1) | 2024.12.31 |
[C++] 포인터(Pointer) (0) | 2024.12.24 |