C++에서 지역 변수(local variable)는 자신이 선언된 스코프(scope)를 벗어나면 더 이상 접근할 수 없습니다. 이것이 바로 '지역 변수'라는 이름이 붙은 이유입니다. 하지만 예외적으로, 스코프 밖에서 지역 변수의 메모리에 접근할 수 있을 것처럼 보이는 경우가 있습니다. 아래 예제를 통해 살펴보겠습니다.
예제 코드
#include<iostream>
using namespace std;
int* foo() {
int x = 3;
return &x;
}
int main() {
int* address = foo();
cout << *address << endl;
*address = 12;
cout << *address;
}실행 결과
컴파일러에 따라 다음과 같은 출력이 나올 수 있습니다.
3 12
왜 동작하는 것처럼 보일까?
앞서 말씀드렸듯이, 이 코드는 우연히 동작할 수 있을 뿐입니다. 여기서 우리는 단순히 예전에 x의 주소였던 메모리 공간을 읽고 쓰고 있을 뿐입니다.
main 함수에서는 이미 foo의 스코프를 벗어난 상태이므로, address는 사실상 임의의 메모리 영역을 가리키는 포인터일 뿐입니다. 위 예제에서 우연히 문제가 발생하지 않은 이유는 다음과 같습니다.
- 해당 메모리 영역이 실제로 존재하며, 당장 다른 용도로 사용되고 있지 않았습니다.
- 계속 사용해도 다른 프로세스의 메모리나 접근이 금지된 영역을 침범하지 않으므로 즉시 오류가 발생하지 않았습니다.
- 그 메모리가 아직 다른 데이터로 덮어쓰이지 않았기 때문에 값 3이 그대로 남아 있었습니다.
실제 프로그램에서의 위험성
실제 프로그램에서는 이야기가 다릅니다. foo 함수가 반환되면 프로그램은 운영체제(OS)에게 "x의 메모리는 더 이상 사용되지 않으니 다른 용도로 재할당해도 된다"고 알립니다. 따라서 해당 메모리는 거의 즉시 다른 데이터에 의해 재사용될 수 있으며, 그 순간 이전 값을 읽거나 쓰려는 시도는 프로그램 전체를 망가뜨릴 수 있습니다.
운이 좋아 메모리가 재할당되지 않고, OS가 잘못된 접근을 적발하지 않으면 일시적으로 문제없이 실행되는 것처럼 보일 수 있습니다. 하지만 이는 정상적인 동작이 아니라 미정의 동작(undefined behavior)입니다.
이렇게 유효 기간이 끝난 변수를 가리키는 포인터를 댕글링 포인터(dangling pointer)라고 부르며, 이로 인한 메모리 접근 버그는 재현이 어렵고 원인을 추적하기가 매우 까다롭기 때문에 반드시 피해야 합니다.
정리
지역 변수의 주소를 함수 외부로 반환하는 것은 표준적으로 허용되지 않는 위험한 패턴입니다. 함수 간에 데이터를 안전하게 전달하려면 값을 복사해 반환하거나, 동적 할당(std::make_unique 등) 또는 참조·스마트 포인터를 활용하는 것이 올바른 방법입니다.