1. 참조자 선언(==초기화)
1
2
3
4
5
6
// 사용 방법
자료형& 인스턴스명 = 원본;
// 사용 예시
int a = 5;
int& b = a;
사용 방법은 위처럼 간단하다.
하지만 주의해야 할 점이 있는데 참조형식 인스턴스에 할당할 원본 데이터는 상수(r-value)이면 안된다는 점이다.
포인터도 동일하게 상수(r-value)에 대해 초기화할 수 없다!!!
그래서 아래와 같이 상수(r-value)로 참조 형식 인스턴스를 초기화하려고 하면 컴파일 에러가 발생한다.
그러므로 초기화 방식에 대해 아래와 같이 정의할 수 있다.
참조자 형식으로 초기화하면 원본 데이터를 가리키는 또 다른 명칭(별명)이 되는 것이다.
그러므로, 원본 데이터가 존재해야(l-value) 참조자 형식으로 초기화 할 수 있다.
원본 데이터의 다른 명칭(별명)을 가지고 있는 것이기 때문에 포인터와 동일하게 작동한다.
아래 예제 코드를 살펴보자.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main()
{
int a = 5;
// b는 a의 또 다른 명칭이다.
int& b = a;
// b를 10으로 바꿈.
b = 10;
// b를 바꿈으로써 a도 영향을 받는 것을 확인할 수 있다.
std::cout << "a:"<< a <<" b:" << b << std::endl;
return 0;
}
// 출력 결과
a: 10 b: 10
참조 형식 변수인 b는 a로 초기화되어 있다.
즉, b는 a의 또 다른 이름이다.
그러므로, b = 10; 으로 b 값을 10으로 바꾸게 되면 a 와 b 둘 다 영향을 받는 것을 확인할 수 있다.
기능적으로는 포인터와 동일하다.
하지만 포인터처럼 다른 문법을 사용하지 않는다는 점이 큰 장점이지 않나 싶다.
2. const 참조자 형식
참조자 형식에 대해서 우리는 아래와 같이 정의했음을 확인했다.
참조자 형식으로 초기화하면 원본 데이터를 가리키는 또 다른 명칭(별명)이 되는 것이다.
그러므로, 원본 데이터가 존재해야(l-value) 참조자 형식으로 초기화 할 수 있다.
그럼 정말 상수(r-value)들에 대해서는 참조자 형식으로 초기화가 불가능한 것일까??
사실 const 참조자 형식을 사용하면 초기화가 가능하다!!
1
const int& a = 5;
const 참조자 형식으로 상수(r-value) 값을 초기화하면
참조자가 사라질 때까지 상수(r-value)의 수명이 연장되는 것으로 볼 수 있을 것 같다.
const 참조자 형식에 대해 특징점을 정리하면 아래와 같다.
- const 참조자 형식은 non-const, l-value, r-value 로 모두 초기화가 가능하다.
- non-const 데이터로 초기화하더라도 참조자는 const로 취급한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
int a = 5;
const int& b = a;
// a는 non-const이다. 그러므로 수정이 가능하다.
a = 6;
// b는 const이기 때문에 수정이 불가능하다.
b = 10; // complie error! - 식이 수정할 수 있는 l-value이여야 합니다.
return 0;
}
3. call-by-reference
1번 예제를 통해 기존에는 포인터로만 가능했던 call-by-reference를 참조자 형식을 통해서도 가능함을 알 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void FUNC(int& a)
{
a += 1;
}
int main()
{
int a = 5;
FUNC(a);
std::cout << "a:" << a << std::endl;
return 0;
}
// 출력 결과
a: 6
4. 주의할 점
4-1. 댕글링 레퍼런스(Dangling reference)
아래와 같이 코드가 있다고 가정하자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int& FUNC()
{
int a = 5;
return a;
}
int main()
{
int& num = FUNC();
std::cout << "num: " << num << std::endl;
return 0;
}
// 출력 결과
num: -858993460
실행해 보면 출력 결과가 5가 아닌 쓰레기 값이 된 것을 확인할 수 있다.
왜 그럴까? 다시 코드를 살펴보자
코드를 보면 FUNC() 함수는 int&
로 a를 반환한다. 그리고 지역 변수 a는 FUNC() 함수가 끝날 때 소멸된다.
그러므로, num은 소멸된 a를 참조하고 있기 때문에 쓰레기 값을 가지는 것이다.
이를 댕글링 레퍼런스(Dangling reference)라고 한다.
실제로 돌려보면 알겠지만 컴파일 에러가 아닌 경고를 띄우게 된다.
즉, 인지하지 못한다면 고치기 어려운 에러이기 때문에 주의하자!!
4-2. 댕글링 레퍼런스(Dangling reference) 피하기
그럼 참조자 반환은 할 수 없는 것일까??
참조자 반환을 하면서 댕글링 레퍼런스를 피하는 방법이 있다. 다음 예제 코드를 살펴보자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<iostream>
int& FUNC(int& a)
{
a +=1;
return a;
}
int main()
{
int num = 5;
int& refer = FUNC(num);
std::cout << "refer: " << refer << " num:" << num << std::endl;
return 0;
}
// 출력 결과
refer: 6 num:6
출력 결과를 보면 쓰레기 값이 아닌 정상적인 값이 출력된 것을 볼 수 있다.
이유는 FUNC(int& a) 함수에서 파라미터로 전달 받은 참조자 형식 a를 반환했기 때문이다.
그러므로, FUNC(int& a) 함수가 끝나도 변수 a는 소멸되지 않기 때문에 refer가 정상적인 값을 가짐을 알 수 있다.
여기서 우리는 가장 중요한 점을 하나 파악할 수 있는데
변수의 메모리가 언제 어떻게 해제되고 소멸되는지를 항상 생각하고 코드를 짜야 한다는 점이다
개인적으로 댕글링 레퍼런스(Dangling reference) 피하는 제일 좋은 방법은 참조 형식으로 반환하지 않는 것이라 생각한다.
파라미터를 통해 참조 형식으로 객체를 전달했다면 반환할 필요 없이 바로 사용할 수 있기 때문이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream>
void FUNC(int& a)
{
a +=1;
}
int main()
{
int num = 5;
FUNC(num);
std::cout << "num: " << num << std::endl;
return 0;
}
// 출력 결과
num: 6