Reference(참조자)

레퍼런스의 도입

  • C++에서는 다른 변수나 상수를 가리키는 방법으로 또 다른 방식을 제공하는데, 이를 바로 레퍼런스(reference)라고 부른다.
  • 레퍼런스를 정하는 방법은 가리키고자 하는 타입 뒤에 & 를 붙이면 된다.
  • int 형 변수의 레퍼런스를 만들고 싶을 때에는 int& 를, double 의 레퍼런스를 만드려면 double& 로 하면 된다. int* 와 같은 포인터 타입의 레퍼런스를 만드려면 int*& 로 쓰면 된다.
  • 레퍼런스는 원래의 변수의 또 다른 이름을 설정하는 것과 같다.
💡 레퍼런스는 반드시 처음에 누구의 별명이 될 것인지 정해야 한다. **`int& another_a;`와 같은 선언은 불가능하다!!!

레퍼런스**는 한 번 별명이 되면 절대로 다른 이의 별명이 될 수 없다.
즉, 어떤 변수의 레퍼런스가 되버리면, 더 이상 다른 변수를 참조할 수 없게 된다.

  • 레퍼런스는 메모리 상에 존재하지 않을 수 도 있다.
    int a = 10;
    int* p = &a;
    • 위 포인터 변수 p는 메모리 상에서 8 바이트를 차지한다.(32비트의 경우 4바이트)

      int a = 10;
      int& another_a = a;
    • 레퍼런스의 경우 메모리 상에 존재하지 않게 된다. 물론 그렇다고 해서 항상 존재하지 않는 것은 아니고 아래 예와 같은 예외가 존재한다.

      #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;
      }
    • 위에서 중요한 부분은 int change_val(int &p) 함수 선언부다. 레퍼런스의 경우 반드시 처음에 누구의 별명이 될 것인지 정해줘야 한다는 규칙이 있어 이 같은 선언은 문제가 있을 것으로 보이지만 사실 p가 정의되는 순간은 change_val(number)로 호출할 때 이므로 사실상 int& p = number가 실행된다고 보면 된다. 그렇기 때문에 전혀 문제가 없는 선언이다.

  • 상수에 대한 레퍼런스
    #include <iostream>
    
    int main() {
    	int& ref = 4; // 에러가 발생하는 구문
    	std::cout << ref << std::endl;
    }
    • 위 코드는 에러가 발생하는데 그 이유는 상수값 자체는 리터럴이기 때문이다.

    • 리터럴은 그 차체로 변경할 수 없는 값인데 위 같은 코드가 실행된담녀 ref = 5;와 같이 리터럴의 값을 바꾸는 행위가 가능하게 된다.

    • 따라서 C++ 문법상 상수 리터럴을 일반적인 레퍼런스가 참조하는 것은 불가능하게 돼있다.

      const int& ref = 4;
    • 단 예외적으로 상수 레퍼런스로 선언하면 리터럴도 참조 할 수 있다.

  • 레퍼런스의 배열과 배열의 레퍼런스
    int a, b;
    int& arr[2] = {a, b};
    • 위 코드와 같은 레퍼런스 배열의 정의는 에러를 발생 시킨다.

    • C++의 문법 상 배열의 이름은 (arr)의 첫 번째 원소의 주소값으로 변환이 될 수 있어야 한다. 이 때문에 arr[1]과 같은 문장이 *(arr + 1)로 바뀌어서 처리될 수 있다.

    • 그런데 주소값이 존재한다는 의미는 해당 원소가 메모리상에 존재한다라는 의미와 같다.

    • 하지만 레퍼런스는 특별한 경우가 아닌 이상 메모리 상에서 공간을 차지 하지 않는다. 따라서 이러한 모순 때문에 레퍼런스들의 배열을 정의하는 것은 언어 차원에서 금지되어 있다.

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

      #include <iostream>
      
      int main() {
      	int arr[3] = {1, 2, 3}
      	int (&ref)[3] = arr;
      
      	ref[0] = 2;
      	ref[1] = 3;
      	ref[2] = 1;
      
      	std::cout << arr[0] << arr[1] << arr[2] << std::endl;
      	return 0;
      }
    • refarr 배열을 참조하도록 했다.

    • 따라서 ref[0]부터 ref[2]가 각각 arr[0] 부터 arr[2]의 레퍼런스가 된다.

    • 포인터와는 다르게 배열의 레퍼런스의 경우 참조하기 위해선 반드시 배열의 크기를 명시해야 한다.

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

  • 레퍼런스를 리턴하는 함수
    int function() {
    	int a = 2;
    	return a;
    }
    
    int main() {
    	int b = function();
    	return 0;
    }
    • 문제 없이 컴파일이 완료되면 b에 a의 값이 복사되어 저장된다. 즉, 값만 복사되고 a는 메모리에서 해제된다.

      int& fucntion() {
      	int a = 2;
      	return a;
      }
      
      int main() {
      	int b = function();
      	return 0;
      }
    • 이 코드는 런타임 에러를 발생시킨다.

    • function의 타입은 int& 이며 참조자를 리턴하게 된다.

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

    • 레퍼런스를 리턴하면서 원래 참조하고 있던 변수가 이미 사라져버렸으므로 오류가 발생하게 된다.

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

      💡 **따라서 레퍼런스를 리턴하는 함수에서 지역 변수의 레퍼런스를 리턴하지 않도록 조심해야 된다.**
    • 외부 변수의 레퍼런스를 리턴

      int& function(int& a) {
      	a = 5;
      	return a;
      }
      
      int main() {
      	int b = 2;
      	int c = function(b);
      	return 0;
      }
      • 아까와의 차이점은 인자로 받은 레퍼런스를 그래도 리턴하고 있다는 것이다.
      • function(b)를 실해한 시점에서 amainb 를 참조하고 있게 된다.
      • function 이 리턴한 참조자느 아직 살아있는 변수인 b 를 계속 참조 한다.
      • 결국 위 문장은 c에 현재의 b의 값인 5를 대입하는 것과 동일한 문장이 된다.
    • 레퍼런스를 리턴하는 경우의 장점

      • C언어에서 엄청나게 큰 구조체가 있을 때 해당 구조체 변수를 그냥 리턴하면 전체 복사가 발생해야 해서 시간이 오래걸리지만, 해당 구조체를 가리키는 포인터를 리턴하면 그냥 포인터 주소 한 번 복사로 매우 빠르게 끝난다.
      • 마찬가지로 레퍼런스를 리턴하게 되면 레퍼런스가 참조하는 타입의 크기와 상관 없이 딱 한 번의 주소값 복사로 전달이 끝나게 된다.

좋은 웹페이지 즐겨찾기