2. C++ 참조자(reference) 의 도입

참조자의 도입

  • 기존 C 언어에서는 함수의 인자로 주소값을 매개변수로 전달해 함수 내에서 주소를 참조해 값을 변경하였다.

#include <iostream>

int change_val(int *p) {
	*p = 3; 
	
	return 0;
}

int main() {
	int number = 5; 
	
	std::cout << number << std::endl;
	change_val(&number);
	std::cout << number << std::endl;
}
Untitled
  • C 언어에서는 어떠한 변수를 가리키고 싶을 때는 반드시 포인터를 사용해야만 했다. 하지만 C++ 에서는 다른 변수나 상수를 가리키는 방법으로 또 다른 방법으로 참조자(reference) 라는 방식을 제공한다.

  • 아래 코드에서 int& (reference) 타입으로 another_a 를 선언하였고, 이 말인 즉슨 “another_aa 의 참조자다!” 라는 의미이다.

  • a 의 참조자 라는 것은 a의 또 다른 별칭이 생겨났다는 것을 컴파일러에게 전달해 주는 것이다.

  • 때문에 아래 코드에서 another_aa 를 대입했지만, 출력했을 때 5가 출력되는 것을 확인할 수 있다.

Untitled

포인터와 참조자의 차이1 - 레퍼런스는 반드시 초기화 되어야 한다.

  • 레퍼런스는 정의 시 반드시 누구의 별칭인지 명시 해야 한다. → 포인터의 경우 명시하지 않아도 된다.

  • 초기화 하지 않은 코드는 사용이 불가하다.

포인터와 참조자의 차이2 - 레퍼런스가 한번 초기화 되면 다른 값으로 초기화 될 수 없다.

  • 한번 정의 된 레퍼런스는 다른 변수의 레퍼런스가 될 수 없다.

    • int& another_a = a; 코드는 another_a 보고 a 를 가리키라는 의미이다.

    • another_a = b; 코드의 의미는 a=b; 와 동일한 의미다. → 우리가 생각하는 의도대로 흘러가지 않는다.

  • 반면의 포인터는 아래 코드와 같이 자유롭게 변경될 수 있다.

포인터와 참조자의 차이3 - 레퍼런스는 메모리 상에 존재하지 않을 수 도 있다.

  • 포인터를 아래와 같이 정의한다면, p 는 당당히 메모리 상에서 8바이트를 차지하게 된다. → 물론 32비트 시스템에서는 4바이트가 될 것이다.

  • 하지만 레퍼런스의 경우, 내가 컴파일러라면 another_a 를 위해서 메모리 상에 공간을 할당할 필요가 있을까? -> 아니다!

  • 왜냐면, another_a 가 쓰이는 자리는 모두 a 로 바꿔치기 하면 되기 때문이다. → 이 경우 레퍼런스는 메모리 상에 존재하지 않게 된다.

  • 물론 항상 존재하지 않는 것은 아니다! -> 예외는 존재한다.

함수의 인자로 레퍼런스 받기

  • 함수의 매개변수로 int& p 를 받게 하였다. → p 가 정의되는 순간은 change_val(number) 가 호출될 때 이므로, 사실상 int& p = number 로 보아야한다. → 이는 p 에게 너는 앞으로 number 의 참조자 라고 알려주게 된다.

  • 이후, p = 3; 코드를 통해 number = 3; 을 수행하는 것도 정확히 일치하게 동작한다.

Untitled

상수에 대한 참조자

  • ref 가 가리키고 있는 값이 리터럴이기 때문에, 만일 아래 코드가 정상이라면, 리터를 값을 바꾸는 말도 안되는 일이 발생한다..

  • 때문에, C++ 문법 상 상수 리터럴을 일반적인 레퍼런스가 참조하는 것은 불가능하게 되어 있다.

  • 물론 그 대신에, const int& ref = 4; 처럼 상수 참조자로 선언한다면 리터럴도 참조할 수 있다. → 따라서 int a = refa = 4; 와 동일하게 처리된다.

Untitled

레퍼런스의 배열과 배열의 레퍼런스

  • 레퍼런스 배열은 가능할까? 불가능하다.. → C++ 규정 자체에서 불가능하다고 못박았다.

Untitled
  • 때문에, 아래 코드는 당연하게 컴파일 오류가 발생한다.

Untitled
  • C++ 상에서 배열이 어떤 식으로 처리될까?

    • 문법 상 배열의 이름은 첫 번째 원소의 주소값으로 변환이 될 수 있다.

    • 때문에, arr[1] 과 같은 문장이 *(arr+1) 로 바뀌어서 처리될 수 있다.

  • 그런데 주소값이 존재한다라는 의미는 “해당 원소가 메모리 상에서 존재한다.” 라는 의미와 같다. → 하지만 레퍼런스는 특별한 경우가 아닌 이상 메모리 상에서 공간을 차지 하지 않는다!! → 이런 모순된 상황 때문에, 레퍼런스의 배열을 정의하는 것은 언어 차원에서 금지가 되어 있다!!

  • 그렇다고 그와 반대인 배열들의 레퍼런스가 불가능한 것은 아니다.

  • 아래 코드에서 ref[0] 부터 ref[2] 가 각각 arr[0] 부터 arr[2] 의 레퍼런스가 됩니다. 포인터와는 다르게 배열의 레퍼런스의 경우 참조하기 위해선 반드시 배열의 크기를 명시해야 합니다.

  • 따라서 int (&ref)[3] 이라면 반드시 크기가 3 인 int 배열의 별명이 되어야 하고 int (&ref)[5] 라면 크기가 5 인 int 배열의 별명이 되어야 합니다.

Untitled

레퍼런스를 리턴하는 함수

  • int b = function(); 에서 어떤 일이 일어날까?

  • function 안에 정의된 a 라는 변수의 값이 b복사 되었다! → 여기서 주목할 점은 복사 되었다는 점이다!

  • function 이 종료되고 나면 a 는 메모리에서 사라지게 된다. → 따라서 main 안에서 a 를 만날 길이 없다.

지역변수의 레퍼런스를 리턴

  • 실제로 실행해보면 아래와 같은 컴파일 경고가 발생하고,

Untitled
  • 다음과 같은 런타임 에러가 발생한다.

Untitled
  • 무엇이 문제일까?

    • function 의 리턴 타입은 int& 이다. 따라서 참조자를 리턴하게 되는데,

    • 문제는 리턴하는 function 안에 정의되어 있는 a 는 함수의 리턴과 함께 사라진다는 점이다.

  • 이와 같이 레퍼런스는 있는데 원래 참조 하던 것이 사라진 레퍼런스를 댕글링 레퍼런스(Dangling reference) 라고 부른다.

외부 변수의 레퍼런스를 리턴

  • 아래의 코드는 위 코드와 다르게 매개변수로 받은 레퍼런스를 그대로 리턴하고 있다!

  • 때문에, function(b) 를 실행하는 시점에서 amainb 를 참조하고 있게 된다. 따라서 function 이 리턴한 참조자는 아직 살아있는 변수인 b 를 계속 참조한다.

  • 결국 아래 코드는 다음과 같은 코드이다. int c = function(b);

  • 그렇다면 참조자를 리턴하는 경우의 장점은 무엇일까?

    • C 언어에서 엄청나게 큰 구조체가 있을 때 해당 구조체 변수를 그냥 리턴하면 전체 복사가 발생해서 시간이 오래 걸린다..

    • 하지만, 해당 구조체를 가리키는 포인터를 리턴한다면 그냥 포인터 주소 한번의 복사로 매우 빠르게 끝이 난다.

    • 마찬가지로 레퍼런스를 리턴하게 되면 레퍼런스가 참조하는 타입의 크기와 상관 없이 딱 한번의 주소값 복사로 전달이 끝나게 된다. → 매우 효율적이다.

참조자가 아닌 값을 리턴하는 함수를 참조자로 받기

  • 해당 코드는 아래와 같이 컴파일 오류가 발생하는데, 내용은 상수가 아닌 레퍼런스가 function 함수의 리턴값을 참조할 수 없다는 의미가 된다.

  • 왜냐면, 함수의 리턴값은 해당 문장이 끝난 후 바로 사라지는 값이기 때문에, 참조자를 만들게 되면 바로 다음에 댕글링 레퍼런스가 되기 때문이다.

Untitled
  • 하지만 C++ 에서 중요한 예외 규칙이 있다.

  • 아래 코드는 function() 의 리턴값을 const 참조자로 받았다. 이 코드는 문제 없이 컴파일 되었다. → 심지어 제대로 출력도 된다.

  • 원칙상 함수의 리턴값은 해당 문장이 끝나면 소멸되는 것이 정상이지만,

  • 하지만 예외적으로 상수 레퍼런스로 리턴값을 받게 되면 해당 리턴값의 생명이 연장된다! → 이 연장기간은 레퍼런스가 사라질 때까지이다..

Untitled

Last updated