[CPP-01] 참조자(reference)와 포인터는 다르다

참조자와 포인터의 차이점, 그리고 댕글링 레퍼런스의 위험성과 그것을 해결하는 방법 중 하나인 Const 참조자에 대한 정리.

[CPP-Module01 / ex04: HI THIS IS BRAIN 과제]

  • Make a program in which you will create a string containing "HI THIS IS BRAIN", a pointer to it, and a reference to it.

  • You will then display it using the pointer, and finally display it using the reference. That’s all, no tricks.

고민한 지점

  1. CPP 참조자 사용법

  2. 참조자와 포인터의 차이점

1. 참조자(reference)는 포인터와 다르다

아래 예제와 같이 가리키고자 하는 자료형 뒤에 & 기호를 붙여 참조자를 정의할 수 있다.

std::string str = "HI THIS IS BRAIN";
std::string *ptr = &str;
std::string& ref = str;

출력되는 값을 봤을 때 *ptrret는 동일하게 취급된다. 실제로 참조형은 내부적으로 포인터를 사용하여 컴파일러에서 구현된다고 한다.

그러나 포인터와 참조자는 아예 다른 개념이라고 생각하는게 이해에 도움이 된다. 1. 참조형은 선언과 동시에 유효한 객체로 초기화 해야한다. 그리고 2. 한번 초기화되면 절대 다른 객체를 참조할 수 없으므로 포인터보다 안전하다. 포인터는 언제든지 다른 변수의 주소를 가리킬 수 있다.

  • 포인터 : 객체의 주소를 가리키는 변수

  • 참조자 : 객체의 을 담는 공간

그래서 포인터는 *->로 역참조를 해야 값을 가져올 수 있지만 3. 참조자는 &만 빼고 그대로 쓰면 값을 참조할 수 있다. 참조형은 참조된 객체의 또다른 이름(별명)이라고 생각해도 된다. 참조형 변수의 값을 연산하면 참조된 객체의 값도 변경된다.

2. 참조자 취급 주의: 댕글링 레퍼런스

  • l-value : 메모리 주소를 가진 객체

  • r-value : 메모리 주소가 없고, 표현식 범위에만 있는 임시 값. ex) 지역변수, 대입연산자 오른쪽에 오는 값

여기서 참조자의 특징이 나타난다. 참조자는 NULL, const, r-value 값으로 초기화할 수 없다. 없다! 이 경우 장점이 있다. 함수의 매개변수로 참조자를 사용하면, 매개변수로 NULL이 들어왔을 때를 예외처리하지 않아도 될 것이다.

하지만 문제도 있다. 아래 예제를 보자.

// 댕글링 레퍼런스 1

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

int main() {
  int b = function();
  b = 3;
  return 0;
}

function 함수는 a의 참조자를 리턴한다. 하지만 이 때 a는 함수가 끝나면 사라지는 r-value값이다. 따라서 a는 참조자가 될 수 없다. 이미 사라진 변수를 참조하려고 하니 에러가 날 수밖에 없는 것이다. 이런 참조자를 댕글링 레퍼런스 라고 부른다.

아래 예제도 마찬가지로 문제가 있다.

// 댕글링 레퍼런스 2

int function() {
  int a = 5;
  return a;
}

int main() {
  int& c = function();
  return 0;
}

변수 c의 참조자도 r-value로 초기화를 시도하고 있다. 영원히 짝궁을 만나지 못하고... 에러가 날 것이다.

댕글링 레퍼런스를 해결하는 방법이 두 가지 있다.

방법 1. 매개변수로 참조자를 받아 참조자를 그대로 리턴

// 댕글링 레퍼런스 1의 해결방법

int& function(int& a) { //매개변수로 참조자를 받아 전달된 인수를 수정할 수 있다.
  a = 5;
  return a;
}

int main() {
  int b = 2;
  int c = function(b);
  return 0;
}

a 자체가 이미 l-value인 b의 참조자이기 때문에 문제가 생기지 않을 것이다.

만약 b가 아니라 function(2)와 같이 직접 값을 넣었다면?

에러가 난다. non-const 참조자는 const 값으로 초기화 할 수 없기 때문이다.

방법 2. const 참조자 사용

위 예제에서는 전부 참조자가 non-const 참조자였다. CPP에서는 const 참조자를 지원한다. const 참조는 유용하다. non-const 값, const 값 및 r-value로 초기화 할 수 있기 때문이다. 참조하는 객체가 const가 아니더라도 const로 간주한다.

이 경우 어떤 장점이 있을까?

원래라면 r-value 값이 생성된 표현식 끝에서 소멸되겠지만, const로 간주된다면 상수형 변수의 특성처럼, 참조자가 사라질 때 까지 r-value 값의 수명이 연장된다. 위의 댕글링 레퍼런스2의 문제는 따라서 const 참조자를 사용하는 것으로 해결할 수 있다.

// 댕글링 레퍼런스 2의 해결방법

int function() {
  int a = 5;
  return a;
}

int main() {
  const int& c = function(); // const 참조자 사용
  return 0;
}

3. const 참조자를 매개변수로 받는 함수

CPP에서 const 지시자는 용도가 명확한 만큼 자주 사용된다. 이전 글에서 const 함수에 대해 다뤘던 것처럼, 만약 const 참조자를 매개변수로 받은 함수가 있다면, 함수 내부에서 참조된 변수의 값을 변경할 수 없다.

int& function(const int& a) {
  a = 5; //not allowed, a is const
  return a;
}

CPP에서 const를 활용하는 더 다양한 방법은 다음 과제에서 배울 수 있을 것 같다.

Last updated