동적 메모리 할당
지난 장에서 포인터의 기본 개념을 학습하며 포인터가 메모리 주소를 저장하고, 그 주소를 통해 값에 접근할 수 있는 강력한 도구임을 이해했습니다.
이제 이 포인터의 가장 중요하고 강력한 활용 중 하나인 동적 메모리 할당(Dynamic Memory Allocation) 에 대해 알아보겠습니다.
지금까지 우리가 사용한 변수들은 대부분 프로그램이 시작될 때 미리 크기가 결정되고 메모리에 할당되는 정적 메모리(Static Memory) 나, 함수 호출 시 스택에 할당되는 지역 메모리(Stack Memory) 였습니다.
하지만 때로는 프로그램 실행 중에 필요에 따라 유동적으로 메모리 크기를 결정하고 할당해야 할 필요가 있습니다.
예를 들어 사용자 입력에 따라 배열의 크기를 정하거나, 데이터를 저장할 공간이 얼마나 필요할지 미리 알 수 없는 경우 등입니다.
이때 동적 메모리 할당이 사용됩니다.
동적 메모리 할당의 필요성
- 가변적인 크기: 프로그램 실행 시점에 필요한 메모리 크기를 결정하고 싶을 때. (예: 사용자가 배열의 크기를 입력)
- 긴 생명주기: 함수가 종료된 후에도 데이터가 유지되어야 할 때. 지역 변수는 함수 종료 시 자동으로 소멸됩니다.
- 큰 데이터 저장: 스택 메모리는 크기가 제한적입니다. 큰 배열이나 객체를 저장할 때는 힙(Heap) 영역의 동적 메모리를 사용해야 합니다.
동적 메모리는 프로그램의 힙(Heap) 이라는 영역에서 할당됩니다. 힙은 스택과 달리 크기 제한이 비교적 자유로우며, 프로그래머가 직접 메모리를 할당하고 해제해야 합니다.
new
연산자를 이용한 메모리 할당
C++에서 동적으로 메모리를 할당할 때는 new
연산자를 사용합니다.
new
연산자는 지정된 타입의 크기만큼 힙 메모리 공간을 할당하고 할당된 메모리 블록의 시작 주소를 반환합니다.
이 주소는 포인터 변수에 저장되어 사용됩니다.
포인터타입* 포인터변수 = new 데이터타입; // 단일 변수 할당
포인터타입* 포인터변수 = new 데이터타입[크기]; // 배열 할당
#include <iostream>
int main() {
int* pInt = nullptr; // int형 포인터 선언 및 초기화
// int형 변수 하나를 힙에 동적 할당하고 주소를 pInt에 저장
pInt = new int;
// 할당된 메모리에 값 저장
*pInt = 100;
// 할당된 메모리의 값 출력
std::cout << "pInt가 가리키는 값: " << *pInt << std::endl; // 출력: 100
std::cout << "pInt가 저장하는 주소: " << pInt << std::endl; // 할당된 메모리의 주소
// 메모리 해제는 나중에!
return 0;
}
사용자에게 배열의 크기를 입력받아 동적으로 할당하는 경우입니다.
#include <iostream>
int main() {
int size;
std::cout << "몇 개의 정수를 저장하시겠습니까? ";
std::cin >> size;
// int형 배열을 힙에 동적 할당하고 주소를 pArr에 저장
// 배열의 크기는 변수로 지정 가능!
int* pArr = new int[size];
// 할당된 배열에 값 저장 (배열처럼 인덱스 사용)
for (int i = 0; i < size; ++i) {
pArr[i] = (i + 1) * 10;
}
// 할당된 배열의 값 출력
std::cout << "동적 할당된 배열의 요소: ";
for (int i = 0; i < size; ++i) {
std::cout << pArr[i] << " ";
}
std::cout << std::endl;
// 메모리 해제는 나중에!
return 0;
}
여기서 pArr
은 힙에 할당된 배열의 첫 번째 요소의 주소를 가리키는 포인터입니다.
배열 이름처럼 pArr[i]
문법을 사용하여 각 요소에 접근할 수 있습니다.
delete
연산자를 이용한 메모리 해제
동적으로 할당된 메모리는 프로그램이 종료될 때까지 자동으로 해제되지 않습니다.
따라서 사용이 끝난 동적 메모리는 반드시 delete
연산자를 사용하여 명시적으로 해제해야 합니다.
만약 해제하지 않으면 메모리 누수(Memory Leak) 가 발생하여 프로그램이 점점 더 많은 메모리를 점유하게 되고, 이는 시스템 성능 저하나 프로그램 충돌로 이어질 수 있습니다.
delete 포인터변수; // 단일 변수 할당 해제
delete[] 포인터변수; // 배열 할당 해제 (대괄호[]를 반드시 붙여야 함!)
new
로 단일 변수를 할당했다면delete
를 사용하고,new
로 배열을 할당했다면delete[]
를 사용해야 합니다. 짝을 맞춰야 합니다.delete
연산자를 호출한 후에도 포인터 변수는 여전히 해제된 메모리의 주소를 가리키고 있습니다 (이를 댕글링 포인터(Dangling Pointer) 라고 합니다). 따라서 메모리 해제 후에는 해당 포인터를nullptr
로 설정하는 것이 안전한 프로그래밍 습관입니다.
#include <iostream>
int main() {
// 단일 변수 동적 할당 및 해제
int* pValue = new int;
*pValue = 42;
std::cout << "단일 변수 값: " << *pValue << std::endl;
delete pValue; // 메모리 해제
pValue = nullptr; // 댕글링 포인터 방지
// 해제된 메모리에 접근 시도 (위험! 런타임 오류 발생 가능)
// if (pValue != nullptr) {
// std::cout << "해제 후 값: " << *pValue << std::endl; // 오류 발생 가능
// }
std::cout << "\n--------------------------\n";
// 배열 동적 할당 및 해제
int arrSize;
std::cout << "배열 크기 입력: ";
std::cin >> arrSize;
int* dynamicArray = new int[arrSize];
for (int i = 0; i < arrSize; ++i) {
dynamicArray[i] = (i + 1) * 100;
}
std::cout << "동적 배열 값: ";
for (int i = 0; i < arrSize; ++i) {
std::cout << dynamicArray[i] << " ";
}
std::cout << std::endl;
delete[] dynamicArray; // 배열 메모리 해제 (대괄호[] 필수!)
dynamicArray = nullptr; // 댕글링 포인터 방지
return 0;
}
메모리 할당 실패 처리
new
연산자는 메모리 할당에 실패할 경우 std::bad_alloc
예외를 발생시킵니다.(C++98 이전에는 NULL
을 반환하는 방식도 있었으나, 현대 C++에서는 예외 방식이 표준입니다.)
따라서 견고한 프로그램을 만들려면 메모리 할당 실패에 대비하여 예외 처리를 해주는 것이 좋습니다.
#include <iostream> // std::cout, std::endl
#include <new> // std::bad_alloc
int main() {
int* largeArray = nullptr;
try {
// 매우 큰 배열을 할당 시도 (메모리가 부족할 경우 예외 발생)
largeArray = new int[10000000000ULL]; // 100억 개의 int (40GB!)
std::cout << "메모리 할당 성공!" << std::endl;
// ... 할당된 메모리 사용 ...
} catch (const std::bad_alloc& e) {
std::cerr << "메모리 할당 실패: " << e.what() << std::endl;
}
if (largeArray != nullptr) {
delete[] largeArray;
largeArray = nullptr;
}
return 0;
}
실제로는 10000000000ULL
과 같은 매우 큰 상수를 직접 할당 시도하기보다는, 시스템에서 사용 가능한 메모리를 초과하는 상황에서 std::bad_alloc
예외가 발생할 수 있음을 알아두는 것이 중요합니다.
동적 메모리 관리의 어려움
동적 메모리 할당과 해제는 매우 강력한 기능이지만, 동시에 다음과 같은 문제를 유발할 수 있습니다.
- 메모리 누수 (Memory Leak):
new
로 할당하고delete
로 해제하는 것을 잊어버린 경우. - 댕글링 포인터 (Dangling Pointer): 이미 해제된 메모리를 가리키는 포인터. 이를 역참조하면 오류 발생.
- 이중 해제 (Double Free): 이미 해제된 메모리를 다시 해제하려고 시도하는 경우. 이 역시 오류 발생.
- 잘못된 해제 (Mismatch
new
/delete
):new
로 할당한 것을delete[]
로 해제하거나,new[]
로 할당한 것을delete
로 해제하는 경우.
이러한 문제들은 디버깅하기 매우 어려우며, 프로그램의 안정성을 심각하게 저해할 수 있습니다.
그래서 현대 C++에서는 이러한 수동적인 메모리 관리의 어려움을 극복하기 위해 스마트 포인터(Smart Pointers) 라는 개념을 제공합니다.
스마트 포인터는 RAII(Resource Acquisition Is Initialization) 원칙을 활용하여 메모리 해제를 자동으로 관리해 줍니다.
스마트 포인터에 대해서는 이후 과정에서 다루겠습니다.
지금 단계에서는 new
와 delete
의 올바른 짝 사용, nullptr
로 초기화 및 해제 후 nullptr
설정, 그리고 메모리 누수 방지의 중요성을 정확히 이해하는 것이 핵심입니다.