본문 바로가기

malloc과 new의 차이

@iamrain2025. 11. 12. 11:46

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)이 부족해지면, 커널에 메모리를 요청해야 한다. 이때 주로 두 가지 시스템 콜이 사용된다.

  1. brk / sbrk:
    • 프로세스의 데이터 세그먼트(Data Segment) 끝에 위치한 힙(Heap)의 끝(program break)을 이동시키는 전통적인 방식이다. sbrk(size)는 힙의 크기를 size만큼 증가시키고, 이전 break 위치를 반환한다.
    • 장점: 구현이 간단하다.
    • 단점: free로 메모리를 해제해도 힙 중간에 구멍(fragment)이 생길 뿐, 힙의 전체 크기를 줄이기는 어렵다. 이는 메모리 단편화를 유발하고, 한번 늘어난 메모리는 프로세스 종료 전까지 반환되기 어렵다.
  2. 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는 내부적으로 다음과 같이 동작한다.

  1. 메모리 할당: operator new(sizeof(T)) 함수를 호출하여 객체를 저장할 순수 메모리 공간을 확보한다.
  2. 객체 생성: 할당된 메모리 공간 위에서 T생성자(constructor) 를 호출하여 객체를 초기화하고 생명을 부여한다.

delete ptr는 역순으로 동작한다.

  1. 객체 소멸: ptr이 가리키는 객체의 소멸자(destructor) 를 호출한다.
  2. 메모리 해제: operator delete(ptr) 함수를 호출하여 메모리를 시스템에 반환한다.

3.2. operator newoperator 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; 한 줄이 실행될 때 시스템 내부에서 일어나는 일이다.

  1. User-Space (Application): new MyClass가 호출된다.
  2. User-Space (C++ Runtime): new 연산자는 operator new(sizeof(MyClass))를 호출한다.
  3. User-Space (Global operator new): 전역 operator new의 기본 구현은 내부적으로 C 라이브러리의 malloc(sizeof(MyClass))를 호출한다.
  4. User-Space (C Library - malloc):
    • malloc은 자신의 힙 관리 영역(아레나)에서 적절한 크기의 가용 블록을 찾는다.
    • Case A (블록 있음): 가용 리스트에서 블록을 찾아 분할하고, 포인터를 반환한다. 커널 개입 없음.
    • Case B (블록 없음): 커널에 메모리를 요청하기 위해 시스템 콜 인터페이스로 넘어간다.
  5. System Call Interface: mallocbrk() 또는 mmap() 시스템 콜을 호출한다. CPU는 유저 모드에서 커널 모드로 전환된다.
  6. Kernel-Space (VMM - Virtual Memory Manager):
    • 커널은 해당 프로세스의 주소 공간(task_struct)에서 비어있는 가상 주소(Virtual Address) 영역을 찾는다.
    • 페이지 테이블(Page Table)에 이 가상 주소에 대한 항목을 생성하지만, 아직 물리 메모리(Physical Memory) 페이지에 매핑하지는 않는다. (Demand Paging)
  7. Return to User-Space:
    • 커널은 할당된 가상 주소의 시작점을 malloc에 반환한다.
    • malloc은 이 새로운 메모리 덩어리를 자신의 힙에 추가하고, 요청된 크기만큼 잘라 포인터를 operator new에 반환한다.
    • operator new는 이 포인터를 C++ 런타임에 반환한다.
  8. User-Space (C++ Runtime & Application):
    • 런타임은 반환된 주소에서 MyClass의 생성자를 호출한다.
    • 생성자가 객체의 멤버 변수(예: int a = 10;)에 최초로 쓰기(write) 접근을 시도하는 순간!
  9. Page Fault (Trap to Kernel):
    • CPU의 MMU(Memory Management Unit)는 해당 가상 주소가 물리 메모리에 매핑되지 않았음을 감지하고 페이지 폴트(Page Fault) 예외를 발생시킨다. CPU는 다시 커널 모드로 전환된다.
  10. Kernel-Space (Page Fault Handler):
    • 커널은 페이지 폴트가 유효한 접근임을 확인한다.
    • 물리 메모리(RAM)에서 비어있는 프레임(frame)을 찾는다.
    • 해당 물리 프레임을 프로세스의 가상 주소에 매핑하도록 페이지 테이블을 업데이트한다.
  11. 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:
      1. C 라이브러리 연동: C API가 malloc으로 할당한 메모리를 받거나, C API에 malloc으로 할당한 메모리를 전달해야 할 때.
      2. 저수준 메모리 조작: 객체가 아닌 순수한 바이트 버퍼를 다룰 때. (파일 입출력 버퍼, 네트워크 패킷 버퍼 등)
      3. C++ 코드이지만 객체 개념이 없을 때: C언어 스타일로 작성된 C++ 코드베이스를 유지보수할 때. (권장되지 않음)

6. 요약

 mallocnew의 차이점에 대해 깊이 있게 설명해 드리겠습니다.

 malloc은 C언어의 함수이고 new는 C++의 연산자입니다. 이 둘은 힙에 메모리를 할당한다는 공통점이 있지만, 가장 큰 차이는 '객체'의 개념을 아느냐 모르느냐에 있습니다.

 malloc은 그냥 요청한 크기의 메모리 덩어리를 운영체제로부터 받아와서 주소만 툭 던져주는 저수준 함수에 가깝습니다. 내부적으로는 brkmmap 같은 시스템 콜을 사용해서 커널에게 메모리를 요청하고, 받은 메모리를 자체적인 자료구조(가용 리스트 등)로 관리하다가 나눠주는 방식으로 동작합니다. 하지만 얘는 객체의 생성자나 소멸자 같은 건 전혀 신경 쓰지 않아요. 그래서 C++에서 malloc으로 클래스 객체를 만들면 초기화가 제대로 안 되는 심각한 문제가 생길 수 있습니다.

 반면에 new는 고수준 연산자입니다. new는 두 단계로 동작하는데, 먼저 operator new라는 함수를 호출해서 필요한 메모리를 할당하고(이때 내부적으로 malloc을 쓸 수도 있습니다), 그 다음에 할당된 메모리 위에서 클래스의 '생성자'를 호출해서 객체를 완벽하게 초기화합니다. 타입도 정확히 알고 있어서 형변환 같은 위험한 작업을 할 필요도 없죠. 메모리 할당에 실패하면 예외(exception)를 던져서 에러 처리도 훨씬 세련되게 할 수 있습니다.

 계층 구조로 보면, 우리가 C++ 코드에서 new를 쓰면 C++ 런타임이 operator new를 부르고, 얘가 다시 C 라이브러리의 malloc을 부릅니다. malloc은 자기가 관리하는 메모리 풀에 공간이 없으면 brkmmap 같은 시스템 콜로 커널에게 요청하죠. 그럼 커널은 가상 메모리 주소를 할당해주고, 실제로 그 주소에 접근할 때 페이지 폴트가 발생하면 비로소 물리 메모리에 연결해줍니다. 이 모든 복잡한 과정이 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
iamrain
@iamrain :: Annals of Unreal

iamrain 님의 블로그 입니다.

공감하셨다면 ❤️ 구독도 환영합니다! 🤗

목차