메모리 구조
우리가 C++을 다루면서 메모리 영역에 대해 많이 다뤄봤다. 특히 데이터 영역과 스택 영역에 대해 정말 많이 그리고 중요하게 다뤘다.
실행할 코드가 저장되는 영역이 코드 영역이고
전역(global)/정적(static) 변수가 데이터 영역에 할당된다.
또 지역변수/ 매개변수는 스택 영역에 할당된다.
그렇다면 마지막 힙 영역은 누가 차지 하느냐 바로 동적 할당한 데이터가 힙 영역을 차지한다.
예를 들어보자.
MMOPRG는 동접자가 1명~5만명 정도 되고, 몬스터는 1마리~500만 마리 있을 것이다.
지금까지 배운 내용으로 이걸 구현하기 위해선 몬스터 배열을 500만을 잡아야한다.
Moster monster[500 * 10000];을 할것이다.
다만 이렇게하면 스택 오버플로우가 날것이다. 즉 메모리가 부족하다는 소리이다.
스택 영역에 대해 우리가 아쉬운 부분이 있는데 함수가 끝나면 같이 정리되는 불 안정한 메모리이다.
잠시 함수에 매개변수를 넘기거나 하는 용도로는 괜찮다.
또 메모리 영역은 프로그램이 실행되는 도중에는 무조건 사용되는 영역이다.
이걸 보고 우리는 희망사항이 들것이다. 필요할 때만 사용하고, 필요없으면 반납할 수 있는 공간!
그러면서도 우리가 생성/ 소멸 시점을 관리할 수 있는 영역이 필요할 것이다.
그것이 바로 Heap 영역이다.
다양한 키워드가 있다. 미리 보자면
- malloc
- free
- new
- delete
- new[]
- delete[]
알고 가야하는 점은 OS에는 유저 영역과 커널 영역이 존재한다. 유저 영역은 메모장이나 곰플레이어 등의 프로그램을 구동하는 영역이고 커널 영역은 windows 등의 핵심 코드등이 구동되고 있다. 우리가 동적 할당을 하기 위해서는 유저 영역에서 운영체제에서 제공하는 api를 호출한다. 그럼 커널 영역에서 계산을 해서 메모리를 할당해 건네주게된다. 그제서야 유저 영역에서 힙 영역을 사용할 수 있게 된다.
C++에서는 기본적으로 CTR(C런타임 라이브러리)의 힙 관리자를 통해 힙 영역을 사용한다.
단, 정말 원한다면 우리가 직접 api를 호출해 힙을 생성하고 관리할 수 있다. 이런 경우는 MMORPG 대규모 서버에서 정말 효율적으로 메모리 풀링 기법을 사용할 때나 적용한다.
malloc
mlloac은 함수인데 인자가 size_t 타입이다. size_t는 unsigned int 형을 의미한다. 그리고 반환형은 void * 이다.
void* ptr = malloc(1000);
다음과 같은 코드가 있을 때, 1000 byte의 메모리 영역을 확보해 그 시작주소를 ptr에 넣겠다는 의미이다.
즉 할당할 메모리 크기를 건네주면 메모리를 할당 후 시작 주소를 가리키는 포인터를 반환해준다. 만약 메모리가 부족할 경우 실패할 수도 있다.
그렇다면 void 형 포인터는 무엇일까? 포인터니까 주소를 밤는 바구니이다.
그러면 포인터를 타고 가면 void? 아무것도 없다는 의미인가? 아니다 타고 가면 뭐가(어떤 타입)이 있는 모르겠으니까 너가 적당히 변환해서 사용해라 라는 의미이다.
#include <iostream>
using namespace std;
class Monster
{
public:
int hp;
int attack;
};
// 전역 연산자 함수
int main()
{
Monster* m1;
void* p = malloc(sizeof(Monster));
m1 = (Monster*)p;
m1->hp = 100;
return 0;
}
free
malloc 혹은 기타 calloc, realloc, 등 을 통해 할당된 영역을 해제하는 역할을 한다.
힙 관리자가 할당/ 미할당 여부를 구분해서 관리
#include <iostream>
using namespace std;
class Monster
{
public:
int hp;
int attack;
};
// 전역 연산자 함수
int main()
{
Monster* m1;
void* p = malloc(sizeof(Monster));
m1 = (Monster*)p;
m1->hp = 100;
free(p);
// 주의 free 했기 때문에 그 영역을 사용하지 않는데
// 그 메모리의 값을 수정하면 큰일남.
//m1->hp = 12;
return 0;
}
free를 호출 할 때 우리는 메모리의 사이즈를 입력하지 않았다. 그렇다면 어떻게 해제할 메모리 영역의 크기가 얼마인지 어떻게 알 수 있을까?? 힙 관리자가 따로 그 윗부분의 메모리의 사이즈를 입력을 해주기 때문이다.
이런 메모리를 관리할 때에는 유효환 힙 범위를 초과해서 사용하는 문제를 조심해야한다.(heap overflow), 요청한 힙의 범위를 넘어서서 포인터를 사용해 데이터를 수정하거나 접근하는 경우이다.
또 free를 하지 않으면 메모리 누수가 발생한다. 지금은 코드가 짧고 데이터가 적기 때문에 괜찮지만 라이브 서비스 같은 경우 계속 누적되다보면 힙 영역이 부족해져 더이상 진행할 수 없다.
그리고 free를 두번하는 경우(Double free) 여러번 해제를 하는경우 크래시가 나고 끝난다.
마지막으로 위의 코드에도 있지만 Use-After-Free를 조심해야 한다. 지금 당장에서 에러가 나지 않을 수 있지만 나중에 그게 나비효과가 되어서 큰 오류를 초래할 수 있다.
new / delete
그전에 malloc,free 는 C언어 부터 사용하던 고전적인 방식이다. C++에 추가된 new/delete는 연산자이다.
Monster* m2 = new Monster;
m2->hp = 100;
delete m2;
단순히 문법만 달라졌지 malloc과 free에 적용되는 내용을 그대로 따라간다.
new[] / delete[]
배열을 할당할 때 사용한다.
Monster* m3 = new Monster[5];
m3[1].hp = 100;
delete[] m3;
malloc vs new
사용 편의성에서는 메모리의 크기를 할당해 줄 필요 없이 자동으로 되는 new가 더 유리하다.
하지만 타입에 상관 없이 특정한 크기의 메모리를 할당 받기위해선 malloc을 사용해야한다.
그런데 둘의 가장 근본적인 중요한 차이점은 따로있다.
면접에 자주등장한다.
뭐 malloc은 함수고 new는 연산자다 라는 답변도 틀린말은 아니지만 중요한 것은 new/delete는 생성 타입이 클래스 일 경우 생성자/소멸자를 호출해 준다는 점이다!
'C++ > 기초' 카테고리의 다른 글
[C++] 타입 변환 - 2 (1) | 2023.11.28 |
---|---|
[C++] 타입 변환 - 1 (0) | 2023.11.28 |
[C++] 객체 지향 - 3 (0) | 2023.11.27 |
[C++] 객체 지향 - 2 (1) | 2023.11.27 |
[C++] 객체 지향 - 1 (3) | 2023.11.27 |