int& Rf()
{
int a1 = 3;
int& A1 = a1;
printf("%d", A1);
return A1;
}
int main()
{
int& a=Rf();
a = 3;
printf("%d", a);
}
위의 코드와 같이 Rf함수에서 지역변수의 참조인 A1을 리턴하여 main함수에서의 참조자a에 저장하였다. 원래대로라면 지역변수 A1이 가리키는 a1은 함수 Rf의 스코프가 종료될때 해제되고 접근이 불가능해야 정상일것이다.
하지만 위의 코드는 잘 동작한다. a를 바꾸는것은 물론이고 main과 Rf에서의 변수의 주소값이 동일하게 나온다.
왜 이것이 가능한지 나는 궁금했고 결과적으로 말하면 정확한 원인은 밝혀내지 못했다.
하지만 조사했던 부분을 한번 포스팅해보고자 한다.
int& Rf()
{
int a1 = 3;
int& A1 = a1;
return A1;
}
int& Rf2()
{
int a2 = 1;
int& A2 = a2;
return A2;
}
int main()
{
int& a=Rf();
a = 3;
printf("%d", a);
Rf2();
printf("%d", a);
}
위의 코드에서 출력된 a의 값은 뭐가 나올까? 신기하게도 3과1이 나온다. 이걸 보고 나는 대략적으로 main함수에서 a가 참조하는값이 Rf의 특정 리턴값이 아니라 리턴값을 저장하는 레지스터의 값을 참조하는게 아닐까란 생각이 들었다.
vs코드의 어셈블리코드를 살펴보았다. 위에서 보듯 Rf가 리턴한뒤 mov dword ptr [eax],3를 통해 a의 주소가 가리키는 곳에 eax를 저장한다. 또한 Rf2가 리턴한 뒤 mov eax,dword ptr [a]를 하는데 해당 ptr[a]에 Rf2가 리턴한 1의 값이 담긴걸 알수 있었다.
다음은 Rf2를 참조자가 아닌 일반변수값을 리턴한 경우와 비교해 보았다.
int& Rf()
{
int a1 = 3;
int& A1 = a1;
return A1;
}
int Rf2()
{
int a2 = 1;
return a2;
}
int main()
{
int& a=Rf();
a = 3;
printf("%d", a);
Rf2();
printf("%d", a);
}
위의 Rf2가 참조값을 리턴했을때와 달리 이 코드의 결과는 3, -어쩌구하는 쓰레기값이 리턴되었다. 즉 리턴값이 참조냐 아니냐에 따라 리턴값을 eax 레지스터에 저장하는 과정이 틀려짐을 알수 있었다.
참조값을 리턴하는 Rf의 어셈블리코드와 참조자가 아닌 값을 리턴하는 Rf2의 어셈블리코드를 비교해 보았다. 해당 코드의 차이점은 참조자를 리턴했을때는 스코프가 끝난직 후 eax를 스택에 push하고 다시 eax를 pop하는 반면에 Rf2는 eax를 push하지도 pop하지도 않는다.
즉 main함수의 a가 Rf의 리턴값을 저장할때 스택에 쌓여있는 eax가 push된 위치의 값을 저장하지 않을까? 하는 추측을 해보았다.
결론을 내리자면 일단 위와 같은 상황을 vs상에서 막아놓지 않았으므로 최대한 피하는 것이 좋다. 왜냐하면 리턴된 값을 참조하는 참조자의 값이 이후에 호출하는 함수들이 스택에 push하는 eax의 리턴값들에 따라 달라지기 때문이다.
Rust라는 언어에서는 이러한 상황을 스코프 단위로 분석해 막아준다고 한다. 아무튼 앞으로 c++을 쓸때 조심해야 겠다.
'언어 > C++' 카테고리의 다른 글
(C++)함수 호출 규약 (0) | 2020.07.13 |
---|---|
(C++)함수 포인터와 콜백함수 (0) | 2020.07.13 |
(C++)타입변환 연산자 오버로딩 (0) | 2020.07.02 |
(C++)연산자 오버로딩의 활용 (0) | 2020.06.29 |
(C++)함수에서의 const 사용 (0) | 2020.06.29 |