728x90
메모리 구조
프로그램에 사용되는 변수들을 선언하면, 이를 저장할 메모리가 필요하다.
프로그램이 사용하는 메모리는 세그먼트(Segment)라고 하는 여러 영역으로 나뉜다.
- 코드 세그먼트: 실행할 코드가 저장되는 영역
- 데이터 세그먼트: 전역 변수 및 정적 변수가 저장되는 영역
- 힙 세그먼트: 동적으로 할당된 변수가 저장되는 영역
- 스택 세그먼트: 함수 매개변수, 지역 변수 및 기타 함수 관련 정보가 저장되는 영역
스택
- 스택 세그먼트는 main() 함수부터 현재 실행 중인 시점까지 활성화된 함수를 추적하고, 모든 함수의 매개 변수와 지역 변수의 할당을 처리한다.
- 스택에 할당되는 메모리는 연속적이다. 즉, 스택에 메모리가 할당될 때는 연속적으로 한 단계씩 할당된다.
- 스택은 LIFO(Last In First Out)형 자료구조다. 가장 늦게 들어온 데이터가 가장 먼저 처리된다.
- 스택은 자동 메모리(Auto Memory)라고도 부른다. 메모리가 자동으로 관리되기 때문이다.
- 스택은 사용에 부담이 없고 처리 속도도 빠르지만, 메모리 크기가 고정되어 있다. 따라서 큰 데이터를 할당하기에는 적합하지 않다.
- Break Point를 사용해 콜 스택(호출 스택)을 확인하면서 디버깅을 할 수 있다.
스택 오버플로우(Stack Overflow)
- 스택을 사용할 때는 스택 오버플로우(Stack Overflow) 문제가 발생할 수 있어 주의해야 한다.
- 스택 세그먼트는 그 크기가 제한되어 있다. 따라서 스택의 크기를 벗어나는 정도로 사용하면 말 그대로 제한 범위를 넘어서는 일이 발생한다. 이런 상황이 스택 오버플로우다.
- 보통은 재귀 함수를 사용해 코드를 작성할 때 탈출 조건을 만나지 못해 일종의 무한 루프와 같이 동작하면, 스택 오버플로우 문제가 발생한다.
- 아래와 같이 스택에 매우 큰 공간을 할당하려고 하면 프로그램 상황에 따라서 스택 오버플로우가 발생할 것이다.
#include <iostream>
int main()
{
int Num[10000000000];
}
Visual Studio의 기본 스택 세그먼트 크기는 1MB 정도 제공된다.
힙
- 힙 세그먼트는 동적(실행 중에, Runtime)으로 할당되는 메모리를 관리하는 영역이다.
- C++에서 new 연산자를 사용해 메모리를 할당하면 힙 세그먼트에 할당된다.
- 힙에 할당되는 메모리는 연속적이지 않다. 그때 그때 필요한 만큼의 빈 공간을 찾아 할당해 준다.
- new와 delete는 항상 쌍으로 작성해야 한다. 동적으로 할당한 메모리는 항상 delete/delete[] 연산자를 사용해 해제해야 한다. 그렇지 않으면 메모리 누수(Memory Leak)가 발생한다.
#include <iostream>
int main()
{
// int 타입을 저장할 수 있는 포인터 변수 선언.
int* number = new int;
// 10개의 int 타입을 저장할 수 있는 배열 포인터 변수 언언.
int* array = new int[10];
// 동적 메모리 해제.
delete number;
delete[] array;
}
힙의 동작
- 힙은 스택에 비해 느리다. 동적으로 할당하고, 해제하는 작업은 속도가 느린 편이기 때문에 큰 데이터를 할당 및 해제할 때는 주의해서 사용해야 한다.
- 할당된 메모리는 명시적으로 해제하지 않으면 프로그램이 종료될 때까지 유지된다. 따라서 항상 메모리 누수에 주의해야 한다.
- 힙에 할당된 메모리는 메모리 주소를 저장하는 포인터를 통해 접근한다. 포인터가 가리키는 변수의 값을 읽어오거나 값을 저장할 때는 다음과 같은 과정을 거쳐야 한다.
- 포인터 변수에 저장된 메모리 주소로 이동한다.
- 해당 메모리 주소에 저장된 변수의 값을 읽거나 쓴다.
- 따라서 변수에 직접 접근하는 것보다 느릴 수밖에 없다.
- 하지만, 스택과 비교해 큰 데이터를 할당하는 데 문제가 없다. 큰 배열, 클래스 등을 할당해야 할 때는 힙을 사용해야 한다.
동적 할당
- C와 C++은 다른 개발언어와 달리 포인터를 사용하여 메모리를 직접 관리할 수 있다는 장점이 있다.
- 데이터 영역과 스택 영역에 할당되는 메모리의 크기는 컴파일 타임에 미리 결정된다.
- 하지만 힙 영역의 크기는 프로그램이 실행되는 도중인 런 타임에 사용자가 직접 결정한다.
- 이런 런 타임에 메모리를 할당받는 것을 메모리의 동적 할당이라고 한다. (Dynamic allocation)
동적 할당에는 C언어의 malloc() / free() 함수와 C++의 new / delete 연산자가 있다.
new는 malloc() 함수와 달리 메모리의 크기를 정하지 않고 동적 할당한다.
malloc() / free()
malloc()
- 할당할 메모리 크기를 건네준다.
- 메모리 할당 후 시작 주소를 가리키는 포인터를 반환해 준다. (메모리 부족의 경우 NULL 반환)
free()
- malloc (혹은 기타 calloc, realloc 등의 사촌? 함수들)을 통해 할당된 영역을 해제한다.
- 힙 관리자가 할당 / 미할당 여부를 구분해서 관리한다.
malloc으로 사용할 메모리 크기를 할당받고, 사용한 후 free로 할당받은 메모리 영역을 해제한다.
class Monster
{
public:
int _hp;
int _attack;
};
int main()
{
Monster* m1 = (Monster*)malloc(sizeof(Monster));
m1->_hp = 100;
m1->_attack = 20;
cout << sizeof(m1) << endl;
free(m1);
return 0;
}
/*실행 결과
8
*/
Monster 객체는 int 타입 2개가 있으므로 8Byte 메모리를 동적할당받는다.(출력은 Monster 객체의 포인터인 주소값을 출력하므로 8이 나온 것)
※ void* 이란?: 이 주소를 타고 가면 무슨 타입인지 모르니, 호출할 때 적당히 형변환해서 사용하라.
new / delete
- C++에 추가되었다.
- malloc / free는 함수(function)이고, new / delete는 연산자(operator)이다.
class Monster
{
public:
int _hp;
int _attack;
};
int main()
{
Monster* m1 = new Monster;
m1->_hp = 100;
m1->_attack = 20;
delete m1;
}
배열 형태의 new / delete
- 배열과 같이 N개의 데이터를 같이 할당하려면? new[ ]를 써야 한다.
- 하나의 포인터는 위와 같이 new / delete를 설정해 주면 되지만 배열 형태로 메모리를 설정해 준 것은 반드시 배열 형태로 메모리를 해제시켜줘야 한다.
class Monster
{
public:
int _hp;
int _attack;
};
int main()
{
Monster* m1 = new Monster[3];
m1->_hp = 100;
m1->_attack = 20;
Monster* m2 = m1 + 1;
m2->_hp = 50;
m2->_attack = 10;
delete[] m1;
delete[] m2;
}
malloc과 new의 차이점
- 사용 편의성 -> new / delete가 압도 (그냥 malloc보다 new가 편하다, 괜히 C++에서 추가된 게 아님)
- 경우에 따라 타입에 상관없이 특정한 크기의 메모리 영역을 할당받으려면? malloc을 사용해야 한다.
- 그런데 둘의 가장 근본적인 차이는 따로 있는데, 만약 둘의 차이가 뭐냐고 질문이 온다면
- malloc / free는 함수이고, new / delete는 연산자이다.
- new / delete는 (생성타입이 클래스일 경우) 생성자 / 소멸자를 호출해 준다.
class Monster
{
public:
Monster() { std::cout << "Monster() 생성자 호출" << "\n"; }
~Monster() { std::cout << "~Monster() 소멸자 호출" << "\n"; }
public:
int _hp;
int _attack;
};
int main()
{
// malloc / free 사용
Monster* m1 = (Monster*)malloc(sizeof(Monster));
m1->_hp = 50;
m1->_attack = 10;
free(m1);
std::cout << "\n";
// new / delete 사용
Monster* m2 = new Monster;
m2->_hp = 100;
m2->_attack = 20;
delete m2;
}
/*실행 결과
Monster() 생성자 호출
~Monster() 소멸자 호출
*/
2가지 방법으로 Monster 클래스를 동적 할당 했다.
malloc에서는 Monster 클래스의 생성자와 소멸자가 호출되지 않지만, new에서는 생성자와 소멸자가 호출된다.
주의할 점
Heap Overflow
- 유효한 힙 범위를 초과해서 사용하는 문제
- Player의 크기가 12byte, Player를 상속받은 Knight의 크기가 20byte일 때, Player로 동적할당을 받은 후에 Knight로 캐스팅하여 사용할 경우, 할당받은 메모리의 범위를 벗어나 사용할 수 있기 때문에 굉장히 위험하다.
free(delete)를 하지 않았을 경우
- malloc으로 할당받은 메모리는 free로 해제, new로 할당받은 메모리는 delete로 해제시켜줘야 한다.
- 또한, 메모리를 할당받았지만 제대로 할당된 영역을 해제시켜주지 않으면 메모리 누수 현상이 발생한다.
Double free(delete)
- 똑같은 free, delete를 두 번 입력했을 경우, 이건 대부분 그냥 크래시만 나고 끝난다.
Use - After - Free(delete)
- 이미 할당된 영역을 해제까지 다 했는데, 재사용을 해버린다면? 엉뚱한 메모리에 그대로 덮어씌져버린다.
- ex) 유저의 골드를 저장할 메모리를 할당받은 후 영역 해제까지 했는데, 재사용한다면 유저의 골드 = 유저의 체력이 될 수도 있고 난리 난다.
프로그래머 입장 : 오 마이갓! 진짜 망했다. 어디서 일어난지도 모를뿐더러, 못 찾으면 더 큰일 난다.
해커 입장 : 프로그램의 흐름을 제어하는 메모리가 덮어씌졌다면? 해킹이 가능하다.
예방 방법
- free, delete를 한 후에 사용했던 변수들을 nullptr로 다 초기화해 준다. (m1 = nullptr)
- 메모리 누수 확인하기 |참고|
- 그냥 꼼꼼히 체크하며 작업하기...
728x90
'📕Programming > 📝C/C++' 카테고리의 다른 글
| [C / C++] 함수 포인터, 함수 객체, 람다 표현식 (0) | 2023.10.24 |
|---|---|
| [C / C++] C++ 타입 변환 (캐스팅) 4종류 (0) | 2023.10.12 |
| [C / C++] 문자열 (0) | 2023.09.14 |
| [C / C++] 포인터 / 레퍼런스 (0) | 2023.09.12 |
| [C / C++] 배열 (array) (0) | 2023.09.11 |