C/C++에서는 지역 변수의 메모리에 범위(scope) 밖에서도 접근할 수 있을 가능성이 있습니다. 이번 글에서는 이러한 현상이 발생하는 예제와 그 원리, 그리고 숨겨진 위험성까지 자세히 살펴보겠습니다.
예제 코드
다음은 지역 변수의 주소를 함수 밖으로 반환하는 코드입니다.
#include<iostream>
using namespace std;
int* foo() {
int x = 3;
return &x; // 지역 변수의 주소를 반환
}
int main() {
int* address = foo();
cout << *address; // 값을 읽음
*address = 12; // 값을 씀
cout << *address;
}실행 결과
컴파일러나 환경에 따라 다르지만, 이 코드는 다음과 같은 출력을 낼 수도 있습니다.
3 12
왜 동작할 수 있을까?
앞서 강조했듯이, 이 코드는 '동작할 수도' 있는 것일 뿐입니다. 핵심 원리는 다음과 같습니다.
- 우리가 읽고 쓰는 대상은 예전에 변수 x였던 주소의 메모리일 뿐입니다.
main함수 입장에서는 이미foo의 범위를 벗어났으므로, 이 주소는 단순히 임의의 메모리 영역을 가리키는 포인터입니다.- 위 예제에서는 해당 메모리 영역이 실제로 존재하고, 당장 다른 곳에서 사용하지 않는 상태입니다.
따라서 이 메모리를 계속 사용해도 다른 프로세스의 메모리나 권한 없는 영역을 침범하는 것은 아니며, 아직 새로운 데이터가 덮어쓰이지 않았기 때문에 기존 값 3이 그대로 남아 있는 것입니다. 이런 이유로 마치 정상적으로 동작하는 것처럼 보일 수 있습니다.
실제 프로그램에서의 위험성
하지만 실제 프로그램에서는 이야기가 다릅니다. 스택 메모리는 재사용 빈도가 매우 높아서, 함수가 반환된 직후 해당 메모리가 거의 즉시 다른 용도로 재할당될 수 있습니다. 그 경우 이런 방식의 접근은 프로그램의 다른 부분을 손상시키게 됩니다.
foo 함수가 반환되는 순간, 프로그램은 운영체제(OS)에 "x의 메모리는 더 이상 사용되지 않으며 다른 용도로 재할당해도 된다"고 알립니다. 만약 운이 좋아 그 메모리가 아직 재할당되지 않았고, OS도 이 접근을 적발하지 않는다면 우연히 문제없이 통과할 수 있을 뿐입니다.
결론: 미정의 동작(Undefined Behavior)
이처럼 유효 기간이 끝난 변수의 메모리에 접근하는 행위를 C/C++ 표준에서는 미정의 동작(undefined behavior)이라고 부릅니다. 컴파일러 최적화, 실행 환경, 타이밍에 따라 결과가 달라질 수 있어 디버깅조차 매우 까다롭습니다. 흔히 말하는 댕글링 포인터(dangling pointer) 버그의 대표적인 사례이며, 이런 종류의 메모리 접근 오류는 추적하고 잡아내기가 극히 어렵습니다. 따라서 지역 변수의 주소를 함수 밖으로 반환하는 코드는 절대 작성해서는 안 됩니다.