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. 마무리
글을 적고 나니 이걸 누가 궁금해할까 싶긴 하다.
그래도 직접 확인은 해보고 싶은데 어떻게 확인해야 될지 모르는 사람이 있을 수 있으니 글을 남긴다.
틀린 내용이 있으면 댓글로 알려주세요