본문 바로가기

공부기록/C, C++

[C++] 포인터 대신 참조자(reference)를 써야하는 경우는 ?

 

코딩테스트 준비를 C++로 하겠다고 마음먹고 문법 공부에 뛰어들자마자 참조자를 만났다.

생각보다 헷갈리는 부분이 많아서 나름대로 정리를 해봤다.

 

목차
1. 함수를 사용함에 있어서의 참조자
2. 어떤 경우에 포인터를 쓰는 것 보다 참조자를 쓰는 것이 적합할까?
3. 참조자가 반드시 메모리상에 존재해야하는 경우와 메모리상에 존재할 필요가 없는 경우는?

1. 함수를 사용함에 있어서의 참조자

  • 지역변수의 참조자를 리턴
int& function() {
  int a = 2;
  return a;
}

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

결과 : warning: reference to local variable ‘a’ returned

지역변수 a 는 함수의 리턴과 함께 소멸되기 때문에, b에서 계속 참조할 수가 없다. (Dangling reference)

  • 외부변수의 참조자를 리턴
int& function(int& a) {
  a = 5;
  return a;
}

int main() {
  int b = 2;
  int c = function(b);
  
  std::cout << "c = " << c << std::endl;	// c = 5
  return 0;
}

여기서 a는 main의 b를 참조하고 있으며, 함수가 반환되어도 b는 살아있기 때문에, 결국 c에는 b의 값이 대입되는 셈이다.

이와 같은 참조자 리턴의 장점은?
예를 들어 아주 큰 구조체가 있을 때, 구조체 변수 자체를 리턴하면 그 값을 모두 복사하느라 오랜 시간이 걸리지만, 참조자로 리턴하면 빠르게 전달을 끝낼 수 있다. 이는 포인터를 사용할 때의 장점과 동일하다.

  • 참조자가 아닌 값을 리턴하는 함수를 참조자로 받기
int function() {
  int a = 5;
  return a;
}

int main() {
  int& c = function();        //컴파일 에러
  const int& c = function(); //상수 참조자로 받으면 컴파일 가능
  return 0;
}

결과: error: cannot bind non-const lvalue reference of type ‘int&’ to an rvalue of type ‘int’

a 는 function()함수가 끝나면 소멸되기 때문에, 컴파일 에러가 발생하지만, const reference로 받으면 리턴값의 생명이 연장되어 정상적인 실행이 가능하고, 해당 참조자가 소멸할 때 까지 유효하다.

 


2. 어떤 경우에 포인터를 쓰는 것 보다 참조자를 쓰는 것이 적합할까?

Stackoverflow에서 찾아보니 "가능하면 참조자를 쓰고, 어쩔 수 없다면 포인터를 써라" 라고 말하고 있었다.

그럼 포인터를 쓸 수 밖에 없는 경우는 무엇인가하니,

  • 매개변수에 NULL 포인터를 넘겨주는 것 or 리턴값으로 NULL 포인터를 반환하는 것이 허용될 경우, 포인터를 사용해야 한다.
    참조자는 선언과 동시에 초기화 되어야해서 NULL이 허용되지 않기 때문이다. 

 

+) 참조자를 쓰는게 더 좋은 이유

  • & 나 * 와 같은 연산자를 쓰지 않아 코드가 깔끔해진다
  • 포인터만 쓸 수 있는 C언어와 달리 C++ 에서는 포인터와 참조자를 모두 쓸 수 있다. 그런데 포인터와 참조자를 무분별하게 섞어 쓰면 코드를 읽기가 더 어려워진다. 이런 이유로, C++에선 최대한 참조자를 쓰고 불가피한 경우에만 포인터를 사용하는 것이 좋다.
  • 포인터를 이용하다가 엉뚱한 메모리를 수정하는 불상사를 미연에 방지할 수 있다.

아래 코드는 반드시 참조자를 쓰는게 좋은 경우라는데, 이 부분은 아직 이해가 안간다. c++ 문법을 더 공부하고 다시 살펴봐야할 것 같다.

// 이런 경우에는 반드시 참조자를 쓰는 것이 좋습니다.
 
vector<int> v(10);    // 크기 10의 int 벡터를 만듭니다.
 
v[5] = 10;            // 이 대입 연산의 대상은 operator[]의
                    // 반환값입니다.
 
// 만일 operator[]가 포인터를 반환하면,
// 다음과 같이 조금 어색한(포인터의 벡터처럼 보이는)
// 형태가 되어야 할 것입니다.
 
*v[5] = 10;

3. 참조자가 반드시 메모리상에 존재해야하는 경우와 메모리상에 존재할 필요가 없는 경우는?

이 질문에 대한 해답은 생각보다 단순한 것 같다.

#include <iostream>

int func1(int &a){ 
    a++;
    return a; 
}
  
int &func2(int &a){ 
    a++;
    return a;
}
  
int main(){
    int x = 10;
    int y = 20;
    int& j = x;
    
    std::cout << fn1(x) << std::endl;
    std::cout << fn2(y) << std::endl;
}

컴파일러는 참조자를 메모리에 할당하지 않고, 참조자가 참조하는 변수의 alias처럼 생각한다.

예를 들어 main 함수의 셋째줄 선언의 경우, 컴파일러는 j가 나오면 그냥 다 x로 생각하는 것이다.

 

메모리에 존재하는 경우는, 참조자와 참조자가 참조하는 변수가 같은 함수 내에 있는 경우라고 볼 수 있다.
main 함수의 셋째줄과 같은 경우이다.

 

메모리에 존재하지 않는 경우란, 해당 함수가 끝나고 runtime 이 다음 영역으로 갔을때이다.

위의 예제에서 func1의 반환값은 참조자가 아니기 때문에 반환과 동시에 참조자 a는 메모리 상에도 존재하지 않으며 x와 아무 관계가 없다. 
func2의 경우 반환값이 참조자이기 때문에 a는 사라졌을지언정 f2(x)가 x의 참조자로서 남아있다.

 

즉, 참조자 자체가 메모리 공간을 필요로 하는 것이 아니라, 참조자가 참조할 것이 있을 때 존재한다고 보면 될 것 같다. 

강의 페이지 댓글을 보니 생각해보기 문제 자체를 모호하게 낸 것 같기도....

 

참조

https://modoocode.com/141#page-heading-9

 

씹어먹는 C++ - <2. C++ 참조자(레퍼런스)의 도입>

 

modoocode.com

https://stackoverflow.com/questions/7058339/when-to-use-references-vs-pointers

 

When to use references vs. pointers

I understand the syntax and general semantics of pointers versus references, but how should I decide when it is more-or-less appropriate to use references or pointers in an API? Naturally some

stackoverflow.com

http://ajwmain.iptime.org/programming/book_summary/%5B01%5Dmore_effective_cpp/more_effective_cpp.html#I01

 

More Effective C++ 정리

 

ajwmain.iptime.org

 

와 영어 읽느라 토할뻔했다....