레퍼런스 변수와 포인터 변수

김 무무 ㅣ 2024. 4. 29. 18:51

1. 개요

혼자 이것저것 공부하던 중 다른 사람도 궁금해했을 법한 결과가 나와 글을 적는다.

 

래퍼런스 변수는 변수에 별명을 붙여주는 것이라고 배웠다.

그래서 함수의 매개변수에 주소값을 받는 것을 Pass by address, 레퍼런스 변수로 바로 참조하는걸 Call by reference로 구분한다고 배웠었다.

그런데 이번에 강의를 보다 레퍼런스 변수도 어셈블리 레벨에서는 포인터와 크게 다르지 않다는 것을 들었다.

 

그래서 이번에는 두 방식을 비교해 보며 정말 레퍼런스 변수가 포인터와 같은 방식으로 동작하는지 알아보려고 한다.

 

 

 

2. 어셈블리 코드 비교

레퍼런스 변수와 포인터 변수를 사용했을 때의 어셈블리 코드를 비교해 봤다.

 

코드 :

int main() {
	int a = 1;
	// 선언부
	int& ref = a;
	int* ptr = &a;
	// 주소 출력
	printf("%p\n", &ref);
	printf("%p\n", ptr);
	// 값 출력
	printf("%d\n", ref);
	printf("%d\n", *ptr);
}

 

어셈블리 코드 :

// 선언부
	int& ref = a;
00007FF7D40D5AF3  lea         rax,[a]  
00007FF7D40D5AF7  mov         qword ptr [ref],rax  
	int* ptr = &a;
00007FF7D40D5AFB  lea         rax,[a]  
00007FF7D40D5AFF  mov         qword ptr [ptr],rax  

// 주소 출력
	printf("%d\n", &ref);
00007FF7D40D5B03  mov         rdx,qword ptr [ref]  
00007FF7D40D5B07  lea         rcx,[string "%d" (07FF7D40D9C10h)]  
00007FF7D40D5B0E  call        printf (07FF7D40D1195h)  
	printf("%d\n", ptr);
00007FF7D40D5B13  mov         rdx,qword ptr [ptr]  
00007FF7D40D5B17  lea         rcx,[string "%d" (07FF7D40D9C10h)]  
00007FF7D40D5B1E  call        printf (07FF7D40D1195h)  

// 값 출력
	printf("%d\n", ref);
00007FF7D40D5B23  mov         rax,qword ptr [ref]  
00007FF7D40D5B27  mov         edx,dword ptr [rax]  
00007FF7D40D5B29  lea         rcx,[string "%d" (07FF7D40D9C10h)]  
00007FF7D40D5B30  call        printf (07FF7D40D1195h)  
	printf("%d\n", *ptr);
00007FF7D40D5B35  mov         rax,qword ptr [ptr]  
00007FF7D40D5B39  mov         edx,dword ptr [rax]  
00007FF7D40D5B3B  lea         rcx,[string "%d" (07FF7D40D9C10h)]  
00007FF7D40D5B42  call        printf (07FF7D40D1195h)

 

비교를 해보니 정말 코드가 완전히 똑같았다.

레퍼런스 변수를 사용할 때  포인터와 동일하게 동작했다.

 

다음은 함수를 비교해 보자.

 

코드 :

#include <stdio.h>
// 레퍼런스 변수를 매개변수로 갖는 함수
void refF(int& n) {
	printf("%p\n", &n);
}
// 포인터 변수를 매개변수로 갖는 함수
void ptrF(int* n) {
	printf("%p\n", n);
}

int main()
{
	int a = 1;

	refF(a);
	ptrF(&a);
}

 

어셈블리 코드 :

     1: #include <stdio.h>
     2: void refF(int& n) {
00007FF6E69717C0  mov         qword ptr [rsp+8],rcx  
00007FF6E69717C5  push        rbp  
00007FF6E69717C6  push        rdi  
00007FF6E69717C7  sub         rsp,0E8h  
00007FF6E69717CE  lea         rbp,[rsp+20h]  
00007FF6E69717D3  lea         rcx,[__BCD43FFC_Third@cpp (07FF6E6981008h)]  
00007FF6E69717DA  call        __CheckForDebuggerJustMyCode (07FF6E697136Bh)  
     3: 	printf("%p\n", &n);
00007FF6E69717DF  mov         rdx,qword ptr [n]  
00007FF6E69717E6  lea         rcx,[string "%p\n" (07FF6E6979CA4h)]  
00007FF6E69717ED  call        printf (07FF6E6971195h)  
     4: }
00007FF6E69717F2  lea         rsp,[rbp+0C8h]  
00007FF6E69717F9  pop         rdi  
00007FF6E69717FA  pop         rbp  
00007FF6E69717FB  ret  

     5: void ptrF(int* n) {
00007FF6E6971810  mov         qword ptr [rsp+8],rcx  
00007FF6E6971815  push        rbp  
00007FF6E6971816  push        rdi  
00007FF6E6971817  sub         rsp,0E8h  
00007FF6E697181E  lea         rbp,[rsp+20h]  
00007FF6E6971823  lea         rcx,[__BCD43FFC_Third@cpp (07FF6E6981008h)]  
00007FF6E697182A  call        __CheckForDebuggerJustMyCode (07FF6E697136Bh)  
     6: 	printf("%p\n", n);
00007FF6E697182F  mov         rdx,qword ptr [n]  
00007FF6E6971836  lea         rcx,[string "%p\n" (07FF6E6979CA4h)]  
00007FF6E697183D  call        printf (07FF6E6971195h)  
     7: }
00007FF6E6971842  lea         rsp,[rbp+0C8h]  
00007FF6E6971849  pop         rdi  
00007FF6E697184A  pop         rbp  
00007FF6E697184B  ret

 

함수를 사용해 매개변수로 받았을 때도 어셈블리 코드가 완전히 동일하다.

레퍼런스 변수는 포인터와 똑같이 동작하는 것이다.

 

 

 

3. 레퍼런스 변수의 메모리

그럼 여기서 의문이 든다.

레퍼런스 변수는 원본 변수에 별명을 붙이는 거라고 배웠는데, 사실 이때도 별명이라는 게 어디에 어떻게 붙는다는 건지 이해가 잘 안 갔었다.

그리고 그렇다는 건 레퍼런스 변수는 따로 메모리를 차지하지 않고 원본 변수의 메모리를 공유하는 건가 하는 의문도 생겼었다.

 

그런데 작동 방식을 확인해 보니 레퍼런스와 포인터는 작동방식이 같았다.

그렇다는 건 사실 레퍼런스도 포인터 변수와 똑같이 메모리를 차지하는 거 아닐까?

단지 컴파일러가 그 사실을 우리가 인식할 수 없도록 하는 게 아닐까?

 

실험해 보자.

 

코드 : 

int main() {
	int a = 1;
	int& ref = a;
	int* ptr = &a;
}

 

위 코드는 레퍼런스 변수를 선언하고 a를 대입한다.

그다음 동일하게 포인터 변수를 선언하고 a의 주소를 대입한다.

만약 내가 생각한 대로 레퍼런스 변수가 새로운 메모리를 차지한다면 코드를 실행할 때 a가 아닌 다른 메모리에 변화가 있을 것이다.

 

디버깅 :

 

우선 int a = 1까지만 실행한 후 a의 주소가 위치한 메모리를 확인했다.

그리고 레퍼런스 변수를 선언하는 코드를 실행하니 메모리의 새로운 위치에 a의 주소값이 들어갔다.

포인터 변수를 선언하는 코드를 실행했을 때도 마찬가지로 새로운 위치에 a의 주소값이 들어갔다.

 

레퍼런스 변수도 포인터와 동일하게 새로운 메모리에 변수의 주소가 할당된다.

결국 레퍼런스는 포인터와 완전히 똑같다.

구체적으로는 대입한 값을 변경할 수 없으니 const 포인터와 같다.

단지 컴파일러가 대입한 주소를 볼 수 없게 만들었을 뿐이다.

 

 

 

4. 마무리

글을 적고 나니 이걸 누가 궁금해할까 싶긴 하다.

그래도 직접 확인은 해보고 싶은데 어떻게 확인해야 될지 모르는 사람이 있을 수 있으니 글을 남긴다.

 

 

 

틀린 내용이 있으면 댓글로 알려주세요

'생각 정리' 카테고리의 다른 글

책, 강의 정리  (0) 2024.06.10
컴퓨터의 덧셈과 뺄셈  (0) 2024.05.30
재귀함수  (0) 2024.04.28
배열  (0) 2024.04.22
포인터 연산  (0) 2024.04.21