스탯을 정보를 나타내는 구조체를 반환하는 함수를 만들어보도록 합시다.
StatInfo ReturnStat() {
StatInfo stat;
stat.hp = 200;
stat.attack = 200;
stat.defence = 200;
return stat;
}
근데 이렇게 반환하려고 보니, 구조체 값을 그대로 반환하면 복사가 한 번 더 일어나 메모리를 낭비한다는 소리를 들은 것 같습니다.
그러니 값 말고 포인터를 반환해서 메모리를 아껴봅시다.
StatInfo* ReturnStat() {
StatInfo stat;
stat.hp = 200;
stat.attack = 200;
stat.defence = 200;
return &stat;
}
좋습니다. 이제 다른 함수를 이용해 작업을 하다가 스탯 정보를 출력한다고 가정해봅시다.
void ArrayTask() {
int aa[300] = {};
// 배열가지고 어떤 작업을 함
}
위에서 만든 함수들을 이용해서 main함수를 실행해봅시다.
int main()
{
StatInfo* ptr = ReturnStat();
ArrayTask();
cout << ptr->hp << endl;
}
딱히 Stat을 건드리지는 않았으니 처음에 넣은 값인 200이 나올 것입니다.
?
갑자기 근본도 없이 0이 나왔습니다.
다른 값도 찍어보죠.
??
왜 저러는 걸까요?
사실 저러는 이유를 알기 위해서는 스택 메모리의 구조를 알아봐야 합니다.
매개 변수, 지역 변수 등 함수를 사용할 때 필요한 여러가지 데이터들은 스택에서 관리됩니다.
이런 식으로 가장 먼저 main() 함수가 실행되고 후에 다른 함수가 실행되면 데이터가 메모리에 쌓이게 됩니다.
만약 함수가 종료되면 관련 메모리는 없어지게 됩니다.
그리고 이게 중요한데 이러한 스택 메모리의 특성상 메모리는 재활용이 가능합니다.
저희의 경우 ReturnStat()과 ArrayTask() 함수를 사용했습니다.
하지만 그림과는 다르게 ReturnStat()이 끝나고 ArrayTask()를 호출했습니다.
즉, 아래의 그림처럼 스택 메모리가 관리됩니다.
보시면 Main()의 위에 부분이 재활용되고 있는 것을 보실 수 있습니다.
근데 제가 ReturnStat에서 한 행동은 무었이었죠? StatInfo구조체를 만들고 반환하는건 좋았는데 메모리 좀 아끼겠다고 포인터를 반환해버렸습니다.
그리고 결과적으로 메모리를 아낀 건 맞습니다. 실제로 그냥 값을 반환했으면 값 복사가 한 번 더 일어나고 결과적으로 구조체의 크기만큼 메모리를 손해보게 됩니다.
문제는 그렇게 아낀 결과 포인터는 스택 메모리를 가르키고 있게 됩니다. 근데 그 다음에 어떤 일이 일어났죠?
void ArrayTask() {
int aa[300] = {};
// 배열가지고 어떤 작업을 함
}
배열 초기화를 했고 저건 main위쪽에 있는 스택 메모리를 사용할 겁니다. 예 제가 반환한 포인터가 가르키고 있는 그 메모리를 이용하는 것이죠.
말로만 말하면 이해하기 힘드니, 이러한 흐름을 비쥬얼 스튜디오에서 직접 확인해봅시다.
이렇게 브레이크 포인트를 걸고 디버그 모드로 실행하겠습니다.
그리고 디버그 -> 창 -> 메모리 -> 아무거나 하나 선택하시면 아래와 같은 창이 나옵니다.
되게 복잡해 보이는 창이 나오는데, 저희는 왼쪽 위에 있는 "주소"에만 집중하면 됩니다.
저 주소를 통하면 그 메모리에 있는 값에 접근할 수 있습니다.
여기서 포인터의 값을 이용하면 그 포인터가 가르키고 있는 메모리에 접근할 수 있습니다.
그 전에 보기가 불편하니 자세도 고쳐앉고, 보기 좋게 설정 좀 건드리겠습니다.
저기 오른쪽 끝에 보시면 열: 자동으로 되어있는 것을 확인하실 수 있는데 클릭 후 1열로 바꾸겠습니다.
1열로 변경하면 다음과 같은 화면이 됩니다.
이제 데이터를 보기 좋게 바꿔봅시다.
저희가 보고자 하는 int는 4바이트입니다. 그리고 저기 보이는 데이터는 1바이트라 그대로 보면 int변수 하나당 4줄을 읽어야 합니다.
그러기 싫으니 우클릭 후 4바이트로 변환합시다.
4바이트로 변환하면 아래와 같이 메모리 창이 변하게 되고 드디어 준비가 끝났습니다.
준비 작업은 거창했지만 사실 본 작업은 별 거 없습니다.
그저 ReturnStat() 함수에서 반환값을 받은 포인터가 어떻게 변하는지만 살펴볼 것입니다.
보시면 저는 포인터 이름을 ptr로 했는데 이름 그대로 주소창에 넣으면 됩니다.
ptr치고 엔터 누르면!!
아직은 ReturnStat() 반환값을 받지 않았기에 별 거 없는 창이 나옵니다.
이제 ReturnStat()을 진행시킨 후 ptr의 주소를 봐야 하는데요
그리고 그 전에 알아보기 쉽게 구조체의 값을 살짝 바꾸겠습니다.
StatInfo* ReturnStat() {
StatInfo stat;
stat.hp = 0x11111111;
stat.attack = 0x11111111;;
stat.defence = 0x11111111;;
return &stat;
}
메모리의 값은 16진수로 되어있기 때문에 보기 편하라고 16진수 기준 11111111이 나오도록 하겠습니다.
이 상태로 F10을 눌러 함수를 진행시킨 후 다시 주소창에 ptr을 입력해보겠습니다.
F10을 눌러서 진행 시 화살표 마크가 한 줄 내려온 것을 볼 수 있습니다.
그리고 다시 메모리 창에서 ptr을 입력하고 엔터를 누르면 설정한 대로 메모리가 잘 나올 것입니다.
보시면 값이 잘 밀려있는 것을 볼 수 있습니다. 참고로 위에서부터 stat 구조체의 hp, attack, defence 변수입니다.
이제 F10을 눌러 다음 함수인 ArrayTask()를 실행시켜 보겠습니다.
이렇게 실행시키고 다음 줄로 넘기면,
구조체의 값이 0이 되어 있습니다??
다시 한 번 ArrayTask()의 코드를 가져와 보죠.
void ArrayTask() {
int aa[300] = {};
}
보시면 지금 당장은 int형 배열을 만드는거 빼고 딱히 다른 일을 하고 있지는 않습니다.
그럼 문제가 뭘까요? 다시 한 번 스택 메모리의 그림을 가져와보겠습니다.
문제가 보이시나요? 보시면 ReturnStat()에서 사용하던 메모리는 함수가 끝나자 비워지고 그 부분을 ArrayTask()가 재활용하고 있습니다.
이때 ptr은 ReturnStat() 에서 사용 중인던 메모리를 가르키고 있습니다. 그러다가 메모리를 ArrayTask()가 사용하게 되자 싹 다 0으로 밀려버린 겁니다. 다 0으로 밀렸으니 hp가 0으로 출력되었고, 그 후에 값들이 이상하게 나온 건 아마 컴파일러에서 건드려서 그런 것 같습니다. 실제로 쓰지 않는 스택 메모리를 가끔 이상한 값들오 바꾸거든요.
이러한 상황은 마치 예전에 알고 지내던 친구의 집 주소를 받고 갔는데 친구는 이사가고 없고 다른 사람이 살고 있던 거죠.
아무튼, 이런 식으로 서브루틴에서 사용하던 메모리를 반환하는 것은 굉장히 위험합니다. 스택 메모리는 재사용되기 때문이죠.
만약 포인터를 이용하고 싶다면 외부에서 매개변수로 포인터를 전달하고 그것의 원본을 건드리는 것이 안전합니다.
cpp에서 메모리 주소를 직접적으로 건드릴 수 있기에 이런 부분을 다른 언어보다는 이런 식으로 고려해야 하는 부분이 좀 많다고 생각합니다.