C++을 공부하다면 C언어도 있었던 포인터(Pointer)와 유사한 개념인 참조자(Reference)가 나타나게 된다.
언뜻 보면 둘을 사용하는 용도가 비슷한데, 해당 포스팅에서는 다음과 같이 5개를 알아볼 것이다
1. 참조자(Reference)란?
2. 포인터(Pointer)와 참조자(Reference)의 어떤 차이점이 있는가?
3. 내부적으로 어떻게 달리 동작하는가?
4. 왜 참조자(Reference)가 등장하게 되었는가?
5. 그렇다면 언제 포인터(Pointer)와 참조자(Reference)를 구분지어서 사용해야 할까?
에 대해서 알아보자.
1. 참조자(Reference)란?
이미 선언한 변수를 대신하는 또 하나의 변수입니다. 이해하기 쉽게 말하면 우리가 친구들을 이름으로 부르지 않고 '별명'으로 부르는 것과 같습니다.
위 코드를 보면 참조자는 & 키워드를 이용하여 선언합니다.
이미 선언한 변수의 앞(num1)에 해당 연산자(&)가 오게되면 변수가 가지고 있는 주소 값을 반환하라는 뜻이지만, 위 코드에서는 새로 선언된 변수(num2)에 사용하면 참조자의 선언을 뜻하게 됩니다.
즉 위 코드에서 num2를 풀어서 설명하면 int &num2 = num1;과 같은 선언입니다. 또한 참조자는 한 개만 사용할 수 있는 것이 아니라, 여러개도 참조가 가능합니다.
int main(void)
{
int number1 = 10;
int &number2 = number1;
int &number3 = number1;
int &number4 = number1;
return 0;
}
2. 포인터(Pointer)와 참조자(Reference)의 어떤 차이점이 있는가?
1. 포인터(Pointer)는 선언 시 주소를 할당할 필요가 없으며, 나중에 다른 주소를 할당 할 수 있지만, 참조자(Reference)는 반드시 처음에 변수를 할당하고, 이는 나중에 변경할 수 없다 이 말은 즉슨 포인터(Pointer)는 NULL의 값을 가질 수 있으나(선언 시 주소를 할당할 필요가 없으므로), 참조자(Reference)는 NULL 값을 가질 수 없습니다. (반드시 처음에 변수를 할당해야 하기 때문에)
2. 포인터(Pointer)는 만약 함수를 이용하면 새로운 포인터에 포인터 주소를 복사하기 때문에 추가적인 메모리가 필요하지만, 참조자(Reference)의 경우 이름만 빌리어 사용하기 때문에 추가적인 메모리가 필요하지 않다. 이 말은 즉슨 포인터(Pointer)는 주소 값을 입력 받고, 참조자(Reference)는 변수를 입력 받는다. 라고 이해하시면 되겠습니다.
3. 포인터(Pointer)는 변수를 바꾸기 위해 *연산자를 사용하지만, 참조자(Reference)는 이름만 변경되었기 때문에 그대로 사용할 수 있다. 이 말은 즉슨 포인터(Pointer)는 *연산자를 이용 해 지정한 객체를 변경할 수 있으며, 참조자(Reference)는 이름(변수)으로 사용하기 때문에 지정한 객체를 변경할 수 없다. 라고 이해하시면 되겠습니다.
3. 내부적으로 어떻게 달리 동작하는가?
VS2019의 디스어셈블 기능을 이용해 아래와 같이 테스트 코드를 작성하고 함수를 호출 할 때 인자를 전달하는 과정에서 두 방법이 내부적(어셈블리)으로 각각 어떤 차이가 있는지를 확인해보자.
8번 라인의 함수 호출 단계를 디버깅 기능을 이용해 변환하면 아래와 같다.
위 코드를 모두 이해할 필요는 없다. 첫 네줄만 보면된다.
lea eax, [num]은 num의 주소값을 eax 레지스터에 저장한다는 의미이다. push eax는 eax 레지스터에 저장된 값을 스택에 저장한다는 의미이다.
위 코드의 대략적인 의미는 변수 num을 주소 또는 레퍼런스로 전달하기 위해, 주소를 스택에 저장하는 명령이다. 그러면 함수 내에서 스택에 올라온 주소를 사용할 수 있다. 참고로 함수의 인자들은 우측에서 좌측으로 읽힌다. 따라서 첫줄은 2번 째 인자에 관한 코드이다.
2번째 인자를 레퍼런스로 넘기기 위해 준비되는 과정은
00A61D39 lea eax, [num]
00A61D3C push eax
이고
1번째 인자 주소를 넘기는 과정도
00A61D3D lea ecx, [num]
00A61D40 push ecx
이다.
포인터(Pointer)와 참조자(Reference)를 이용해 전달한 두 과정을 까보니 과정이 동일하다. (eax와 ecx는 저장되는 레지스터 종류 차이기 때문에 지금 중점에선 상관없는 문제다.)
어쨌든 주소(=포인터(Pointer)를 전달하나 참조자(Reference)를 전달하나 내부적으로 동일한 2단계를 거친다는 소리고 전달 방식이 겉으론 달라보여도 내부적으론 똑같다는 소리다.
4. 그렇다면 참조자(Reference)가 등장하게 되었는가?
위와 같은 결과로 포인터와 레퍼런스 모두 동일시하게 처리되는데 참조자(Reference)가 등장한 이유가 무엇일까? 먼저 포인터(Pointer)의 위험성을 살펴보자
첫번째. 기본적으로 차이점에서 말한 것 처럼 포인터(Pointer)는 주소를 가리킬 수 있다. 그러나 아래의 코드와 같이 널(nullptr)을 가리킬 수도 있다.
int* ptr = nullptr;
만약 nullptr를 가리키는 포인터(Pointer)를 역참조하면 어떻게 될까? nullptr(널포인터)는 유효하지 않는 객체라는 NULL(value 타입)의 객체를 가리키는 상수이다. 유효하지 않는 공간을 가리키게 되면 결국 당연히 에러를 발생시키며 이는 프로그램에 문제를 줄 수 있다.
두번째.
인자에 주소를 담아 콜 바이 레퍼런스(Call by Reference)로 함수를 호출했다고 가정해보자. 호출된 함수에선 포인터(Pointer)를 통해 해당 변수의 주소가 가리키는 값에 접근할 수 있다. 그런데 포인터가 강력한 이유는 전달 받은 주소에 덧셈/뺄셈하면서 다음과 같이 다른 주소에도 자유롭게 접근이 가능하다는 것이다.
void func(int* ptr)
{
*(ptr) = 3;
*(ptr+1) = 4;
*(ptr+2) = 5;
}
int arr[10]{};
func(arr);
이 말은 반대로 생각하면 포인터를 통해 할당되지 않은 메모리 공간 또는 다른 용도로 사용되고 있는 메모리 공간에 임의로 접근할 수 있다는 의미이다. 이는 반대로 A라는 주소값을 가지는 공간에 절대 없어지면 안되는 값을 저장해놨는데 포인터를 통해 실수로 A가 존재하는 주소의 값을 건드릴 수 있다는 얘기이다. 그래서 이러한 위험은 없애고 포인터는 그대로 사용할 수 있도록 만들어진 것이 레퍼런스이다.
5. 그렇다면 언제 포인터(Pointer)와 참조자(Reference)를 구분지어서 사용해야 할까?
이 부분은 상황에 따라 다르지만, 대게 아래와 같은 상황서 구분지어서 사용한다.
포인터(Pointer) : 가리키는 대상을 바꿀 수 있어야 한다 (예 : iterator, list 등)
참조자(Reference) : 가리키는 대상이 절대로 변하지 않는다. (예 : 함수에 결과 값을 저장하는 변수를 저장한다 등)
아울러 포인터(Pointer)와 참조자(Reference)는 문법적으로 쓰는 모양새가 다르기 때문에 따라서 참조자(Reference)를 통해 호출하는 문법으로만 원하는 구현을 할 수 있을 때가 있습니다. 이는 반환타입으로써 사용 될 때 이다.
만약 반환하고자 하는 값 자체가 원본에 대한 참조를 전달하고 싶을때 포인터(Pointer)는 전달받은 입장이므로 다시 한번 *을 리턴값 앞에 붙여주어야 하지만, 참조자(Reference)는 특성 상 *를 붙여서 전달 할 필요가 없다. 만약, 이러한 형태를 구현을 하고자 할 때에는 참조자(Reference) 밖에 답이 없을 수가 있다.
출처 : https://bloodstrawberry.tistory.com/271
출처 : https://k96-ozon.tistory.com/27
출처 : https://woo-dev.tistory.com/43
'공부 > C++' 카테고리의 다른 글
가상 함수(Virtual Function)와 가상 함수 테이블(Virtual Function Table) 개념 및 차이 (0) | 2022.12.22 |
---|---|
Vector resize, reserve의 차이점 (0) | 2022.12.21 |
Vector push_back과 emplace_back의 차이 (0) | 2022.12.16 |
구조체(Struct)와 클래스(Class)의 개념과 차이 (2) | 2022.12.14 |
열거형(enum)과 열거형 클래스(enum class)의 개념, 사용법, 차이점 (0) | 2022.12.14 |