C++ (Call By Value, Call By Reference)
#include <iostream> int func1(int a) { a += 1; return 20; } int func2(int * a) { *a += 2; return 32; } int func3(int & a) { a += 3; return 100; } int main() { int t = 5; t += 3; printf("Before : %d\n", t); func1(t); printf("After func1() : %d\n", t); func2(&t); printf("After func2() : %d\n", t); func3(t); printf("After func3() : %d\n", t); }
Before : 8 After func1() : 8 After func2() : 10 After func3() : 13
보시면, main 함수에서는 t에 5를 넣어주고, 3을 더해줍니다.
그리고, func1() 함수를 호출해줍니다.
func1()을 보시면, 파라미터의 타입이 int 타입이고, a에 1을 더해줍니다.
func1()가 반환한 이후에 main()에서 t 값을 출력해보면, 값이 바뀌지 않고 8이 그대로 출력되는 것을 확인할 수 있습니다.
func1() 호출부의 아규먼트 값이 파라미터에 단순히 복사되었기 때문입니다.
다음으로, func2() 함수를 호출해줍니다.
func2()를 보시면, 파라미터의 타입이 int 포인터 형이고, 포인터 변수를 이용하여 값을 2 증가시켜주었습니다.
func1()과는 다르게, a가 t의 주소를 가리키고 있기 때문에, func2()가 반환했을 때, t의 값이 바뀝니다.
다음으로, func3() 함수를 호출해줍니다.
func3()의 파라미터는 int형의 참조자인데요, 참조자를 사용하면, t의 메모리 공간에 a라는 또 다른 이름을 붙인 것처럼 사용할 수 있습니다.
func3() 함수에서는 a에 3을 더해주고, func3()가 반환한 이후 t의 값은 변경되어서 13이 출력됩니다.
3: int func1(int a) { /* func1() 함수의 실행을 준비하는 prologue */ 4: a += 1; 00A51808 mov eax,dword ptr [ebp+8] // eax 레지스터에 8이 들어감 00A5180B add eax,1 00A5180E mov dword ptr [ebp+8],eax 5: return 20; 00A51811 mov eax,14h 6: } 8: int func2(int * a) { /* func2() 함수의 실행을 준비하는 prologue */ 9: *a += 2; // func3()와 동일 00A51868 mov eax,dword ptr [ebp+8] 00A5186B mov ecx,dword ptr [eax] 00A5186D add ecx,2 00A51870 mov edx,dword ptr [ebp+8] 00A51873 mov dword ptr [edx],ecx 10: return 32; 00A51875 mov eax,20h 11: } 13: int func3(int & a) { /* func3() 함수의 실행을 준비하는 prologue */ 14: a += 3; // // func2()와 동일 00A518D8 mov eax,dword ptr [ebp+8] 00A518DB mov ecx,dword ptr [eax] 00A518DD add ecx,3 00A518E0 mov edx,dword ptr [ebp+8] 00A518E3 mov dword ptr [edx],ecx 15: return 100; 00A518E5 mov eax,64h 16: } int main() { /* main() 함수의 실행을 준비하는 prologue */ 19: int t = 5; 00A51A48 mov dword ptr [ebp-8],5 20: t += 3; 00A51A4F mov eax,dword ptr [ebp-8] 00A51A52 add eax,3 00A51A55 mov dword ptr [ebp-8],eax 21: 22: printf("Before : %d\n", t); 00A51A58 mov eax,dword ptr [ebp-8] 00A51A5B push eax 00A51A5C push 0A57B30h 00A51A61 call 00A5104B 00A51A66 add esp,8 23: func1(t); 00A51A69 mov eax,dword ptr [ebp-8] // eax에 8이 들어감 00A51A6C push eax 00A51A6D call 00A51190 00A51A72 add esp,4 24: printf("After func1() : %d\n", t); 00A51A75 mov eax,dword ptr [ebp-8] 00A51A78 push eax 00A51A79 push 0A57B40h 00A51A7E call 00A5104B 00A51A83 add esp,8 25: func2(&t); 00A51A86 lea eax,[ebp-8] // eax에 t의 주소값이 들어감 00A51A89 push eax 00A51A8A call 00A510A0 00A51A8F add esp,4 26: printf("After func2() : %d\n", t); 00A51A92 mov eax,dword ptr [ebp-8] 00A51A95 push eax 00A51A96 push 0A57B58h 00A51A9B call 00A5104B 00A51AA0 add esp,8 27: func3(t); 00A51AA3 lea eax,[ebp-8] // eax에 t의 주소값이 들어감 00A51AA6 push eax 00A51AA7 call 00A511BD 00A51AAC add esp,4 28: printf("After func3() : %d\n", t); 00A51AAF mov eax,dword ptr [ebp-8] 00A51AB2 push eax 00A51AB3 push 0A57B70h 00A51AB8 call 00A5104B 00A51ABD add esp,8 29: }
위 어셈블리 코드를 보면,
func1()을 호출할 때, ebp-8 주소에 저장된 값을 eax 레지스터에 넣어주는데요, ebp-8에 저장된 값이 바로 t의 주소값입니다.
따라서 eax 레지스터에는 8이라는 값이 들어가고, eax 레지스터를 스택에 push 해줍니다.
그리고 func1()을 호출해줍니다. 그럼 func1()에서는 복사된 값을 사용하는 것이죠.
다음으로, func2()를 호출할 때에는, ebp-8에 저장된 값을 eax 레지스터에 저장하고, eax를 스택에 push 해줍니다.
그리고 func2()를 호출해줍니다. 즉, eax에는 t의 주소값이 들어가는 것이죠.
func2()에서는 a에 저장된 t의 주소값을 eax에 저장한 후, eax에 저장된 주소로부터 4바이트를 불러와서 ecx에 저장한 다음, 2를 더해주고, t의 주소에 저장해줍니다.
따라서, 2를 더한 값을 t의 주소에 저장해주었기 때문에 func2()가 반환한 이후에도 값이 변경된 것입니다.
그렇다면, 참조자는 어떻게 동작하는 걸까요?
func2()와 비교해보자면 func3()은 func2()와 완벽히 동일합니다.
호출부도 동일하고, 함수 내부의 동작도 동일합니다.
따라서, 포인터든 참조자든 호출 시 주소를 전달하고, 참조자는 컴파일 되었을 때, 포인터와 동일한 방식으로 사용되는 것을 확인할 수 있습니다.
정리하자면, 어셈블리어를 보니 C의 포인터와 C++의 참조자 모두 주소를 전달하는 방식이었습니다.
저의 경우 C/Java는 Call by Value, C++의 참조자는 Call by Reference라고 배웠는데요,
"C는 Call by Reference다", "아니다 주소 '값'을 전달하기 때문에 Call by Value다",
"자바의 Primitive 타입은 Call by Value고 Reference는 Call by Reference다", "아니다 일종의 포인터 역할을 하기 때문에 Call by Value다." 등 책마다, 교수님마다, 사람마다 서로 다른 의견을 갖고 있기 때문에, Call by Value, Call by Reference를 명확히 정의하는 것은 어렵겠습니다만,
제가 생각했을 때는, 주소의 전달 여부가 아닌, 함수 실행부에서 전달 받은 파라미터를 마치 아규먼트의 Aliasing처럼 사용할 수 있느냐로 Call by Reference냐 Call by Value냐를 구분할 수 있을 것 같습니다.
'C\C++' 카테고리의 다른 글
C언어 포인터는 왜 타입별로 구분되어 있는가? (0) | 2022.08.19 |
---|