1. 메모리 할당
C와 C++에서 동적 메모리 할당은 프로그램의 유연성과 효율성을 결정하는 핵심 기능이다. C언어의 malloc 함수와 C++의 new 연산자는 힙(Heap) 영역에 메모리를 할당한다는 공통점을 갖지만, 그 철학과 내부 동작 방식, 프로그래밍 패러다임에 미치는 영향은 근본적으로 다르다.
2. malloc / free: C 스타일의 저수준 메모리 관리
malloc은 C 표준 라이브러리(stdlib.h)에 정의된 함수로, 순수한 메모리 블록(raw memory)을 할당하는 역할만 수행한다.
2.1. 내부 구조 및 구현 방식
malloc은 시스템 콜(System Call)이 아니다. malloc 라이브러리 함수는 커널로부터 큰 메모리 덩어리를 미리 할당받아, 이를 자체적인 자료구조로 관리하며 사용자에게 잘게 나누어주는 메모리 관리자이다.
2.1.1. 커널로부터 메모리 확보: brk vs mmap
malloc이 관리하는 메모리 풀(pool)이 부족해지면, 커널에 메모리를 요청해야 한다. 이때 주로 두 가지 시스템 콜이 사용된다.
brk/sbrk:- 프로세스의 데이터 세그먼트(Data Segment) 끝에 위치한 힙(Heap)의 끝(program break)을 이동시키는 전통적인 방식이다.
sbrk(size)는 힙의 크기를size만큼 증가시키고, 이전 break 위치를 반환한다. - 장점: 구현이 간단하다.
- 단점:
free로 메모리를 해제해도 힙 중간에 구멍(fragment)이 생길 뿐, 힙의 전체 크기를 줄이기는 어렵다. 이는 메모리 단편화를 유발하고, 한번 늘어난 메모리는 프로세스 종료 전까지 반환되기 어렵다.
- 프로세스의 데이터 세그먼트(Data Segment) 끝에 위치한 힙(Heap)의 끝(program break)을 이동시키는 전통적인 방식이다.
mmap(Memory MAP):- 파일이나 디바이스를 메모리에 매핑하는 함수지만, 익명(anonymous) 매핑을 통해 파일과 무관한 순수한 메모리 공간을 커널로부터 할당받을 수 있다.
- 장점:
brk와 무관한 독립된 메모리 영역을 할당받는다. 따라서munmap시스템 콜을 통해 해당 영역만 정확히 커널에 반환할 수 있어, 단편화 문제에 더 유연하게 대처할 수 있다. - 현대
malloc의 선택: 현대의malloc구현체(glibc의 ptmalloc 등)는 작은 크기의 할당은brk로 관리하는 힙에서 처리하고, 일정 크기(예: 128KB) 이상의 큰 할당은mmap을 사용하는 하이브리드 전략을 취한다.
2.1.2. 힙 관리 알고리즘
커널로부터 받아온 메모리 덩어리를 효율적으로 관리하기 위해 malloc은 가용 리스트(Free List) 라는 자료구조를 사용한다. 이는 할당 가능한 메모리 블록들을 연결 리스트로 관리하는 방식이다.
- 블록 구조: 각 메모리 블록은 사용자에게 할당될 데이터 영역 외에, 블록의 크기, 이전/다음 블록 포인터 등의 메타데이터를 포함하는 헤더(header)를 가진다.
- 할당 전략:
- First-fit: 가용 리스트를 처음부터 탐색하여 크기가 맞는 첫 번째 블록을 할당. 빠르지만 리스트 앞부분에 작은 조각들이 누적될 수 있다.
- Best-fit: 전체 리스트를 탐색하여 요청된 크기와 가장 근접한(가장 작은) 블록을 할당. 메모리 낭비는 적지만 탐색 시간이 길다.
- Next-fit: 마지막으로 할당된 위치부터 탐색을 시작. First-fit의 단점을 일부 보완한다.
- 분할(Splitting)과 병합(Coalescing):
- 분할: 할당 요청보다 큰 블록을 찾으면, 요청된 크기만큼 잘라내어 할당하고 남은 조각은 다시 가용 리스트에 추가한다.
- 병합:
free된 블록의 인접한 블록이 가용 상태라면, 이들을 하나의 큰 블록으로 합쳐서 큰 메모리 요청에 대응하고 외부 단편화를 줄인다.
2.1.3. 멀티스레딩과 malloc 구현체
ptmalloc2(glibc): 멀티스레드 환경에서 단일 힙을 사용하면 락(lock) 경쟁으로 성능이 저하된다.ptmalloc은 아레나(Arena) 라는 독립적인 힙 관리 영역을 여러 개 만들어, 각 스레드가 다른 아레나에 접근하게 함으로써 락 경쟁을 줄인다.tcmalloc(Google): 스레드별 로컬 캐시(Thread-Caching)를 두어, 자주 사용되는 작은 크기의 메모리 할당은 락 없이 로컬 캐시에서 바로 처리한다. 성능이 매우 뛰어나다.jemalloc(Facebook):ptmalloc과 유사하게 아레나 기반이지만, 단편화 방지와 동시성 확장성에 더 중점을 두어 설계되었다.
3. new / delete: C++의 객체 지향 메모리 관리
new는 C++의 연산자(operator) 로, 메모리 할당과 객체 생성이라는 두 가지 핵심 작업을 수행한다.
3.1. 내부 구조: 2단계 프로세스
new T는 내부적으로 다음과 같이 동작한다.
- 메모리 할당:
operator new(sizeof(T))함수를 호출하여 객체를 저장할 순수 메모리 공간을 확보한다. - 객체 생성: 할당된 메모리 공간 위에서
T의 생성자(constructor) 를 호출하여 객체를 초기화하고 생명을 부여한다.
delete ptr는 역순으로 동작한다.
- 객체 소멸:
ptr이 가리키는 객체의 소멸자(destructor) 를 호출한다. - 메모리 해제:
operator delete(ptr)함수를 호출하여 메모리를 시스템에 반환한다.
3.2. operator new와 operator delete
이 함수들은 malloc, free와 유사한 역할을 하지만, C++에서는 이들을 재정의(overloading) 할 수 있다.
- 전역 재정의:
operator new를 전역으로 재정의하면 프로젝트의 모든new연산자의 메모리 할당 방식을 변경할 수 있다. (예: 메모리 사용량 추적, 디버깅) - 클래스별 재정의: 특정 클래스 내에
operator new를 재정의하면, 해당 클래스의 객체에 대해서만 특별한 메모리 할당 전략을 사용할 수 있다. 메모리 풀(Memory Pool) 기법이 대표적인 예로, 동일한 크기의 객체를 빈번하게 생성/삭제할 때malloc의 일반적인 힙 관리 오버헤드를 피하고 매우 빠른 할당/해제를 구현할 수 있다.
3.3. 다양한 new 연산자
- 배열
new[]/delete[]:T* arr = new T[10];operator new[]를 호출하여 10개 객체를 저장할 공간을 할당한다.- 컴파일러는 할당된 공간의 시작 부분에 배열의 크기(10)를 기록해둔다.
- 각 원소에 대해 생성자를 순차적으로 호출한다.
delete[] arr;는 배열 크기 정보를 읽어, 정확히 10개의 소멸자를 역순으로 호출한 뒤 전체 메모리를 해제한다.delete arr;을 사용하면 첫 번째 원소의 소멸자만 호출되어 심각한 메모리 누수가 발생한다.
- 배치
new(Placement new):new (address) T();- 이미 할당된 특정 메모리 주소(
address) 위에 객체를 생성(생성자 호출)만 하는 역할이다. 메모리를 새로 할당하지 않는다. - 메모리 풀, 하드웨어 특정 주소에 객체를 매핑하는 임베디드 시스템 등에서 필수적이다.
- 배치
new로 생성된 객체는delete로 해제하면 안 된다. 소멸자만 명시적으로 호출(ptr->~T();)해야 한다.
- 이미 할당된 특정 메모리 주소(
nothrow new:new (std::nothrow) T;- 메모리 할당 실패 시
std::bad_alloc예외를 던지는 대신,nullptr를 반환하여malloc과 유사하게 동작한다. 예외 처리를 사용하지 않는 코드베이스에서 유용하다.
- 메모리 할당 실패 시
4. 계층 구조로 본 동작 방식
C++ 코드 MyClass* p = new MyClass; 한 줄이 실행될 때 시스템 내부에서 일어나는 일이다.
- User-Space (Application):
new MyClass가 호출된다. - User-Space (C++ Runtime):
new연산자는operator new(sizeof(MyClass))를 호출한다. - User-Space (Global
operator new): 전역operator new의 기본 구현은 내부적으로 C 라이브러리의malloc(sizeof(MyClass))를 호출한다. - User-Space (C Library -
malloc):malloc은 자신의 힙 관리 영역(아레나)에서 적절한 크기의 가용 블록을 찾는다.- Case A (블록 있음): 가용 리스트에서 블록을 찾아 분할하고, 포인터를 반환한다. 커널 개입 없음.
- Case B (블록 없음): 커널에 메모리를 요청하기 위해 시스템 콜 인터페이스로 넘어간다.
- System Call Interface:
malloc이brk()또는mmap()시스템 콜을 호출한다. CPU는 유저 모드에서 커널 모드로 전환된다. - Kernel-Space (VMM - Virtual Memory Manager):
- 커널은 해당 프로세스의 주소 공간(task_struct)에서 비어있는 가상 주소(Virtual Address) 영역을 찾는다.
- 페이지 테이블(Page Table)에 이 가상 주소에 대한 항목을 생성하지만, 아직 물리 메모리(Physical Memory) 페이지에 매핑하지는 않는다. (Demand Paging)
- Return to User-Space:
- 커널은 할당된 가상 주소의 시작점을
malloc에 반환한다. malloc은 이 새로운 메모리 덩어리를 자신의 힙에 추가하고, 요청된 크기만큼 잘라 포인터를operator new에 반환한다.operator new는 이 포인터를 C++ 런타임에 반환한다.
- 커널은 할당된 가상 주소의 시작점을
- User-Space (C++ Runtime & Application):
- 런타임은 반환된 주소에서
MyClass의 생성자를 호출한다. - 생성자가 객체의 멤버 변수(예:
int a = 10;)에 최초로 쓰기(write) 접근을 시도하는 순간!
- 런타임은 반환된 주소에서
- Page Fault (Trap to Kernel):
- CPU의 MMU(Memory Management Unit)는 해당 가상 주소가 물리 메모리에 매핑되지 않았음을 감지하고 페이지 폴트(Page Fault) 예외를 발생시킨다. CPU는 다시 커널 모드로 전환된다.
- Kernel-Space (Page Fault Handler):
- 커널은 페이지 폴트가 유효한 접근임을 확인한다.
- 물리 메모리(RAM)에서 비어있는 프레임(frame)을 찾는다.
- 해당 물리 프레임을 프로세스의 가상 주소에 매핑하도록 페이지 테이블을 업데이트한다.
- Return to User-Space:
- 커널은 페이지 폴트를 유발한 명령(쓰기 접근)을 다시 실행하도록 제어권을 프로세스에 넘긴다.
- 이제 쓰기 작업은 성공적으로 완료되고, 생성자 실행이 계속된다.
- 생성자 실행이 끝나면, 완전히 초기화된 객체를 가리키는 포인터
p가 최종적으로 반환된다.
5. 비교 분석 및 사용 전략
| 구분 | malloc / free |
new / delete |
|---|---|---|
| 종류 | C 라이브러리 함수 | C++ 연산자 |
| 핵심 역할 | 순수 메모리 블록 할당/해제 | 메모리 할당/해제 + 객체 생성/소멸 |
| 객체 지향 | 미지원 (생성자/소멸자 미호출) | 완벽 지원 (생성자/소멸자 자동 호출) |
| 타입 안전성 | 취약. void* 반환 후 위험한 (T*) 캐스팅 필요. |
강력. 타입에 맞는 포인터(T*)를 반환. 컴파일 타임 체크. |
| 에러 처리 | NULL 포인터 반환. 매번 체크 필요. |
std::bad_alloc 예외 발생. 중앙화된 에러 처리 가능. |
| 확장성 | 표준적인 재정의 방법 없음. | 전역 또는 클래스별 operator new/delete 재정의 가능. |
| 배열 처리 | malloc(sizeof(T) * n). 크기 n을 직접 관리해야 함. |
new T[n], delete[]. 런타임이 크기를 기억하고 소멸자 호출. |
- 언제 어디에 사용하는가?
new/delete: 모든 C++ 코드에서 기본 원칙이다. 특히 클래스 객체와 같이 생성과 소멸이 중요한 non-POD 타입에는 반드시 사용해야 한다.malloc/free:- C 라이브러리 연동: C API가
malloc으로 할당한 메모리를 받거나, C API에malloc으로 할당한 메모리를 전달해야 할 때. - 저수준 메모리 조작: 객체가 아닌 순수한 바이트 버퍼를 다룰 때. (파일 입출력 버퍼, 네트워크 패킷 버퍼 등)
- C++ 코드이지만 객체 개념이 없을 때: C언어 스타일로 작성된 C++ 코드베이스를 유지보수할 때. (권장되지 않음)
- C 라이브러리 연동: C API가
6. 요약
malloc과 new의 차이점에 대해 깊이 있게 설명해 드리겠습니다.
malloc은 C언어의 함수이고 new는 C++의 연산자입니다. 이 둘은 힙에 메모리를 할당한다는 공통점이 있지만, 가장 큰 차이는 '객체'의 개념을 아느냐 모르느냐에 있습니다.
malloc은 그냥 요청한 크기의 메모리 덩어리를 운영체제로부터 받아와서 주소만 툭 던져주는 저수준 함수에 가깝습니다. 내부적으로는 brk나 mmap 같은 시스템 콜을 사용해서 커널에게 메모리를 요청하고, 받은 메모리를 자체적인 자료구조(가용 리스트 등)로 관리하다가 나눠주는 방식으로 동작합니다. 하지만 얘는 객체의 생성자나 소멸자 같은 건 전혀 신경 쓰지 않아요. 그래서 C++에서 malloc으로 클래스 객체를 만들면 초기화가 제대로 안 되는 심각한 문제가 생길 수 있습니다.
반면에 new는 고수준 연산자입니다. new는 두 단계로 동작하는데, 먼저 operator new라는 함수를 호출해서 필요한 메모리를 할당하고(이때 내부적으로 malloc을 쓸 수도 있습니다), 그 다음에 할당된 메모리 위에서 클래스의 '생성자'를 호출해서 객체를 완벽하게 초기화합니다. 타입도 정확히 알고 있어서 형변환 같은 위험한 작업을 할 필요도 없죠. 메모리 할당에 실패하면 예외(exception)를 던져서 에러 처리도 훨씬 세련되게 할 수 있습니다.
계층 구조로 보면, 우리가 C++ 코드에서 new를 쓰면 C++ 런타임이 operator new를 부르고, 얘가 다시 C 라이브러리의 malloc을 부릅니다. malloc은 자기가 관리하는 메모리 풀에 공간이 없으면 brk나 mmap 같은 시스템 콜로 커널에게 요청하죠. 그럼 커널은 가상 메모리 주소를 할당해주고, 실제로 그 주소에 접근할 때 페이지 폴트가 발생하면 비로소 물리 메모리에 연결해줍니다. 이 모든 복잡한 과정이 new 한 줄에 담겨있는 겁니다.
결론적으로, C++에서는 객체의 생명주기를 올바르게 관리하고 타입 안전성을 지키기 위해 반드시 new를 써야 합니다. malloc은 C 라이브러리와 연동하거나 아주 특수한 저수준 메모리 조작이 필요할 때만 제한적으로 사용해야 합니다. 그리고 현대 C++에서는 new조차 직접 쓰기보다는 스마트 포인터(std::unique_ptr, std::shared_ptr) 를 사용하는 게 메모리 누수를 막는 가장 좋은 방법입니다.
'Computer Science' 카테고리의 다른 글
| Red-Black 트리 (0) | 2025.11.13 |
|---|---|
| 교착 상태 (Deadlock) (0) | 2025.11.12 |
| 물리 메모리의 계층 구조 (0) | 2025.11.10 |
| AVL 트리 (0) | 2025.11.10 |
| Dedicated Server와 Listen Server (0) | 2025.11.05 |
