⊙ 힙과 스택의 차이와 용도를 알아보자.
⊙ 싱글 변수, 배열, 싱글 객체, 객체 배열의 동적 메모리를 할당하는 방법을 알아보자.
⊙ 동적 할당된 변수나 객체를 사용하는 방법에 대해 알아보자.
⊙ 동적 메모리 해제의 필요성과 메모리 누수에 대해 알아보자.
⊙ 싱글 변수, 배열, 싱글 객체, 객체 배열의 동적 메모리 해제 방법을 알아보자.
동적 메모리 할당
동적 메모리 할당은 프로그램 실행 중에 필요한 메모리 크기를 결정하고 할당하는 방법이다. 이 방식은 프로그램의 유연성을 높이며, 데이터의 크기가 런타임에 결정될 때 유용하다. 예를 들어 사용자로부터 입력을 받아 그 크기만큼 배열을 생성하는 경우 동적 메모리 할당을 이용할 수 있다.
동적 메모리 할당은 힙(heap) 영역에 메모리를 할당된다는 뜻이다. 여기서 힙(heap) 영역이란 무엇일까?
힙(Heap)과 스택(Stack)
힙(Heap)
힙(heap)은 동적 메모리 할당을 통해 데이터를 저장한다. 프로그래머가 new를 통해 직접 메모리를 할당하고, delete로 직접 해제해 주어야 하기 때문에 자동 관리되지 않는다. 메모리가 크고, 프로그램 실행 중 유연하게 메모리를 사용할 수 있기 때문에 동적으로 크기를 변경하거나 다양한 데이터 구조를 구현하는데 적합하다는 장점이 있지만 메모리 할당과 해제가 잘 이루어지지 않으면 메모리 누수가 발생할 위험이 있다. 또한 속도가 느리기도 하다. 정리하면 아래와 같겠다.
- 동적 할당을 통해 데이터를 저장
- 프로그래머가 직접 할당(new)과 해제를 해야함(자동 아님)
- 큰 메모리 크기
- 메모리 누수 발생 위험
- 느린 속도
스택(Stack)
힙이 아닌 다른 메모리는 스택이 있다.
스택은 LIFO(Last In, First Out)의 방식으로 데이터를 저장하며 함수 호출, 지역 변수 등이 저장된다. 힙과 다르게 고정된 크기를 가지고 있으며 메모리 할당 및 해제가 매우 빠르다. 심지어 이 해제와 할당은 컴파일러에 의해 자동으로 관리된다. 즉, 사람의 실수로 메모리 누수가 발생하지 않는다는 뜻이다. 힙은 개발자가 delete를 하기 전까지는 메모리에 살아있지만 스택에 저장된 변수는 선언된 블록(함수나 제어문)이 끝나면 자동으로 해제된다. 하지만 힙에서는 장점이었던 것들이 스택에는 없다. 크기가 제한적이고 유연성이 부족하여 큰 데이터를 저장하거나 동적으로 변경되는 것에는 적합하지 않다.
- LIFO 방식으로 데이터를 저장
- 함수 호출, 지역 변수 등이 저장
- 고정된 메모리 크기
- 메모리 발생 위험 없음(컴파일러가 자동으로 처리)
- 빠른 속도
- 유연성이 부족함
둘을 비교하면 아래와 같다.
특징 | 스택(Stack) | 힙(Heap) |
메모리 할당 방식 | 컴파일러에 의해 자동 관리 | 프로그래머가 직접 관리 |
속도 | 빠름 | 느림 |
크기 | 상대적으로 작음 | 상대적으로 큼 |
수명 | 블록 범위를 벗어나면 자동 해제 | 명시적으로 해제할 때까지 유지 |
주요 용도 | 지역 변수, 함수 호출 정보 저장 | 동적 크기 데이터, 대규모 데이터 저장 |
메모루 누수 위험 | 없음 | 있음 |
동적 메모리 할당된 변수와 객체 선언
동적 메모리에 할당되는 변수나 객체를 선언하기 위해서는 new 라는 키워드를 사용해야 한다. new는 운영체제에 메모리를 요청하고, 할당된 메모리의 시작 주소를 반환한다. 객체를 선언할 때는 생성자를 호출하여 객체를 초기화한다. new는 할당된 메모리의 시작 주소를 포인터로 반환하므ㄹ, 반환값을 포인터 변수에 저장해야 한다.
동적 변수 선언
싱글 변수
포인터 변수를 선언하고 new를 이용해 동적 할당 후 할당된 메모리의 시작 주소를 포인터 변수에 저장한다.
int* p = new int;
int* p = new int(4); // 4로 초기화
배열 변수
마찬가지로 포인터 변수를 선언하고 new 뒤에 배열로 선언해주면 된다.
int* list = new int[10];
int* list = new int[size]; // 배열 크기에 변수 사용 가능
int* list = new int[4]{1, 2, 3, 4}; // 중괄호로 초기화
동적 객체 선언
싱글 객체
포인터 변수를 선언하고 new를 이용해 동적 할당된 메모리 주소값을 받아온다. 생성자를 호출하여 초기화 할 때 인수를 넣어주면 인수가 있는 생성자가 호출되고 아닌 경우 기본 생성자가 호출된다.
Circle* pCircle = new Circle(); // 기본 생성자
Circle* pCircle = new Circle; // 기본 생성자
Circle* pCircle = Circle(5); // 인수가 있는 생성자
Circle circle1;
Circle *pCircle = &circle1;
* String도 객체이기 때문에 이렇게 선언해야 한다.
객체 배열
아무것도 없이 배열을 선언한다면 기본 생성자로 초기화가 된다. 하지만 배열 선언 뒤에 중괄호를 이용하여 인수로 들어갈 값을 초기화 해주면 인수 있는 생성자를 호출할 수도 있다.
Circle* p = new Circle[3]; // 기본 생성자
Circle* p = new Circle[3]{1, 2, 3}; // 인수 있는 생성자
동적 할당된 변수와 객체 사용
그냥 정적 할당된 변수와 객체를 사용하는 것과 다르게 동적 할당된 변수와 객체들은 포인터 변수, 포인터 객체이다. 그렇기 때문에 저장된 값을 쓰기 위해서는 * 이렇게 생긴게 필요하다. 바로 역참조(*) 연산이다.
동적 변수 사용
싱글 변수
p가 가지고 있는 것은 주소이기 때문에 역참조(*) 연산을 이용해 그 값을 이용할 수 있다.
int* p = new int;
cout << *p << endl;
배열 변수
배열 자체가 원래 포인터다. 그래서 별로 달라질 건 없다. 원래 알던대로 배열을 사용하면 된다. 그래도 티나게 "*"를 붙여서 사용하는 표현 방법도 하나 보겠다.
int* list = new list[10];
cout << list << endl; // 배열의 시작
cout << *(list+6) << endl; // 6번째 값
cout << list[5] << endl; // 5번째 값
동적 객체 사용
싱글 객체
보통 정적 변수의 객체를 사용하기 위해서는 "." 라는 도트가 필요하다. 도트는 클래스의 멤버를 직접적으로 접근할 수 있도록 해주는 것이다. 동적 객체도 마찬가지로 역참조(*) 연산과 도트(.)를 이용해 멤버에 접근이 가능하다. 근데 괄호에 역참조에 도트까지 매번 이걸 쓰기는 귀찮으니 좀더 줄여서 표현해보자. "->"는 포인터를 통해 멤버를 간접적으로 접근이 가능하다.
Circle* pCircle = new Circle();
(*pCircle).getRadius();
pCircle->getRadius();
객체 배열
마찬가지로 화살표(->)를 사용하면 접근할 수 있다.
Circle* p = new Circle[3];
p[0]->getRadius();
동적 메모리 해제
스택과 다르게 동적으로 힙 메모리에 할당되는 경우 컴파일러가 자동으로 처리해주지 않는다. 개발자가 사용한 동적 할당된 변수나 객체는 알아서 스스로 정리할 줄 알아야 한다. 만약 이 것이 제대로 되지 않는다면 메모리 누수가 발생할 수 있다. 잠깐 메모리 누수가 뭔지 어떤 상황에서 일어나는지 알아보자.
메모리 누수
메모리 누수란 프로그램에서 동적으로 할당된 메모리를 제대로 해제하지 않아, 사용되지 않는 메모리가 회수되지 않고 게속 점유된 상태를 말한다. 아는 메모리 사용량을 점차 증가시키며, 결국에는 시스템 자원 고갈로 인해 프로그램이 느려지거나 종료되는 문제가 발생할 수 있다. 간단하게 두 가지 경우 메모리 누수가 발생할 수 있다.
"까먹고 해제를 안했어요."
말 그대로 할당은 해놓고 해제를 까먹은 경우이다. 반드시 할당하면 해제하도록 한다.
int* ptr = new int(32);
// delete ptr; => 메모리를 해제 안함.
"포인터를 잃어버렸어요."
다음은 안타깝게도 포인터를 잃어버린 경우이다. 위의 경우 잘 해제해주면 되지만 이렇게 되는 경우 잃어버린 포인터는 영영 찾을 수 없다. 32의 값으로 선언한 int 포인터 값이 있었는데 이 포인터가 43의 값을 가진 다른 값을 가리키게 되어 버린 것이다. 이런 경우 32의 값으로 선언하고 할당 받은 메모리는 영영 찾을 수 없어 해제할 수 없는 상황이 벌어진다.
int* ptr = new int(32);
ptr = new int(43); // 이전 메모리의 주소는 잃어버림
delete ptr; // 43은 해제되지만 32는 영영 찾을 수 없음
이처럼 메모리 누수는 꼭 신경써주어야 한다. 이제 실제로 delete를 어떻게 사용하는지 알아보도록 하자.
해제는 delete 키워드를 사용하여 한다. 단일 객체를 해제할 때는 delete를 사용하지만 동적으로 할당된 배열을 해제할 때는 delete[]를 사용한다는 점을 주의해야 한다.
동적 싱글 변수/객체 해제
싱글 변수/객체는 delete 키워드를 사용한다.
int* p = new int(3);
Circle* c = new Circle(4);
delete p; // 싱글 변수 메모리 해제
delete c; // 싱글 객체 메모리 해제
동적 변수/객체 배열 해제
배열을 해제할 때는 delete[]를 사용하여 해제한다. 잘못하여 delete를 사용하지 않도록 주의한다.
int* p = new p[5];
Circle *c = new Circle[6];
delete[] p; // 배열 변수 메모리 해제
delete[] c; // 배열 객체 메모리 해제
delete 이후의 상황도 신경써줘야 한다. delete를 하고나면 메모리 공간은 반환되지만 포인터는 계속 존재한다. 다시말해 delete p;를 하거 나서도 p는 있지도 않은 공간을 계속 가리키고 있는 것이다. 이 때 p를 허상 포인터라고 한다. 그래서 메모리 해제한 후 포인터를 nullptr로 초기화하면 잘못된 접근을 막을 수 있다.
int* arr = new int[5];
delete[] arr;
arr = nullptr; // 포인터 초기화
배운 내용 정리
- 동적 메모리 할당은 프로그램 실행 중에 필요한 메모리 크기를 결정하고 할당하는 방법이다.
- 힙은 스택과 달리 동적 할당을 통해 메모리에 저장된다.
- 동적 메모리 할당은 new 키워드로 할 수 있으며 할당된 메모리의 주소는 포인터 변수가 저장한다.
- 동적 메모리 할당되어 메모리가 저장된 포인터 변수는 역참조(*)를 이용해 사용할 수 있다.
- 동적 변수나 객체를 제대로 해제하지 않으면 메모리 누수가 발생할 수 있다.
- 동적 메모리 해제는 delete 키워드로 할 수 있으며 단일 값과 배열에서의 문법이 다르니 주의한다.
- 동적 메모리 해제 이후 포인터 변수도 nullptr로 초기화하면 잘못된 접근을 막을 수 있다.
'C++ > 게임 개발자를 위한 C++ 문법' 카테고리의 다른 글
다중 포함 방지 (0) | 2024.12.26 |
---|---|
포인터의 개념과 사용 (0) | 2024.12.26 |
함수 인자 전달 방식 (0) | 2024.12.23 |
연산 주의사항 (0) | 2024.12.23 |
Array와 Vector의 차이점과 사용법 (1) | 2024.12.23 |