[C++] 동적 할당, malloc()/free() vs. new/delete

2024. 3. 15. 11:24Programming Language/C++

아래 링크 클릭 시 해당 본문으로 이동

정적 할당 vs. 동적 할당

malloc()/free() vs. new/delete

malloc()는 free()를, new는 delete를 사용해야 하는 이유와 그렇지 않을 경우 발생하는 문제


정적 할당 vs. 동적 할당

1) 정적 할당

프로그램 실행 전 컴파일 단계에서 메모리 공간을 할당받는 것

프로그램 실행 전에 메모리의 크기가 결정되고 할당된 메모리는 운영체제가 알아서 해제시킨다.

장점 메모리 누수를 걱정할 필요가 없다.
  ➜ 할당한 메모리는 운영체제가 알아서 해제한다.
단점 메모리 공간의 크기가 정해져있어서 프로그램 실행 중에 수정할 수 없다.
  ➜ 메모리 공간을 낭비할 가능성이 있다.
[ ] : 메모리 공간
처음에 [100]을 만들고 [5]만 사용해도 프로그램 실행 중에 바꿀 수 없으므로 무조건 100만큼의 공간을 받아놓아야 한다.
이로 인해 나머지 [95]만큼의 낭비가 발생한다.

 

2) 동적 할당

프로그램 실행 중(런타임)에 메모리 공간을 할당받는 것

프로그램 실행 중에 필요할 때 원하는 크기의 메모리를 직접 할당받을 수 있다.

또한 동적 할당은 실행할 수도 있고 안 할 수도 있다.

포인터 힙(Heap) 영역에 지정한 만큼의 공간을 할당하고 그것의 시작 주소를 받는다.

힙 영역에서 변수(지역 · 전역 · 정적 · 외부)를 사용하지 않고 포인터를 사용하는 이유
변수는 프로그램이 시작되거나 함수를 호출할 때 자료형과 변수명이 정해지고 그에 따른 메모리가 할당 · 해제된다.
이미 정해진 부분이라서 프로그램 실행 중에 변수의 메모리 공간을 수정할 수 없고, 프로그램을 만들어놓은 상태에서 다시 코드를 수정하고 재빌드하는 건 말이 안 된다.
장점 • 메모리의 크기를 원하는 만큼 할당할 수 있다.
메모리 사용에 있어서 더 효율적이고 유연한 이유
: 정적 할당은 코드로 작성한 부분들이 그대로 실행되지만 동적 할당은 필요할 때 원하는 만큼의 메모리를 할당하고 해제할 수 있다.
단점 • 할당하고 사용하지 않는 메모리 공간은 프로그래머가 직접 해제해야 한다.
  ➜ 제대로 해제하지 않으면 메모리가 계속 쌓이고 결국 메모리 공간이 부족해지는 문제가 발생한다.
[ ] : 메모리 공간
[100]에서 [5]만 필요하면 [95]만큼 해제하고, [100]에서 [150]이 필요하면 [50]을 더 할당받으면 된다.

▷ 동적 할당이 필요한 이유 (예시)

• RPG 게임 - 보스몹 잡기 전까지 랜덤으로 계속 잡몹을 생성하는 경우

각각의 잡몹들이 나올 때마다 메모리를 할당해 주고 잡몹이 죽으면 해제해야 한다.

정적 할당의 경우 잡몹을 잡아도 메모리가 해제되지 않고, 메모리는 계속 쌓이다가 결국 운영체제는 이 게임을 터뜨려버린다.

따라서 동적 할당을 사용하여 잡몹을 잡으면 메모리도 해제되게끔 해야 한다.


`malloc()`/`free()` vs. `new`/`delete`

  malloc() / free() new / delete
언어 C++에서도 사용 가능한 C 함수 C++ 연산자
타입 타입 지정 없이 void* 반환 명확한 타입이 지정된다.
메모리 할당 메모리를 할당한다. 메모리를 할당하고 생성자를 호출한다.
메모리 해제 메모리를 해제한다. 메모리를 해제하고 소멸자를 호출한다.
메모리 할당 시
메모리 크기
메모리 크기를 지정해 줘야 한다. 컴파일러가 메모리 크기를 계산해서 지정한다.
할당 실패 시 NULL 반환 std::bad_alloc 예외를 throw한다.

 


`malloc()` / `free()`

헤더 파일

#include <stdlib.h>  // C 방식
#include <cstdlib>  // C++ 방식

1) `malloc()`

• Memory Allocation(메모리 할당)의 약자로, 동적 메모리를 할당하는 함수이다.

• `malloc()` 함수 괄호 안에 할당하고 싶은 만큼의 메모리 byte 수를 입력하면 된다.

즉, 괄호 안의 메모리 크기 단위는 byte이다.

• 이 함수가 실행되면 힙 영역에 할당된 메모리의 시작 주소를 주고, 이를 포인터로 받는다.

※ 변수명을 준 것이 아니다.

• `malloc()` 함수의 반환 타입은 void*이다.

힙 영역에 할당된 메모리 공간은 목적을 갖고 있지 않아서 프로그래머가 해당 공간을 어떻게 사용하느냐에 따라 달라진다.

예를 들어, `int`로 보고 싶다면 `int*` 변수를, `float`으로 보고 싶다면 `float*` 변수를 사용하여 주소를 받는다.

 

▷ 예시

#include <cstdlib>

int main()
{
	// 힙 영역에 4byte 메모리 공간을 만들고 float*로 받는다.
	float* pF = (float*)malloc(4);
    // float*로 받은 메모리 공간을 int*로 가리킨다.
    int* pI = (int*)pF;  // 이때 pF와 pI는 같은 공간을 가리키고 있는 상태이다.
    
    // float* 변수인 pF로 접근해서 2.4f라는 값을 넣고, 그 값을 int*로 접근해서 읽어온 값을 i에 저장한다.
    *pF = 2.4f;
    int i = *pI;  // 1075419546
    // 2.4f가 1075419546으로 나온 이유 : 2.4f를 int* 변수로 저장했기 때문에 int의 관점으로 해석한 값이다.
    
	return 0;
}

 

2) `free()`

• `malloc()`으로 할당한 동적 메모리 공간을 해제하는 함수이다.

메모리 누수로 메모리 공간 부족 문제를 방지하기 위해 `malloc()` 함수로 할당한 메모리 중 사용하지 않는 메모리 공간은 프로그래머가 직접 `free()`로 해제해야 한다.

• `free()` 괄호 안에는 반드시 힙 영역의 주소를 넣어줘야 한다.

※ 다른 변수(지역 · 전역 · 정적 · 외부)는 넣을 수 없다.

 

▷ 잘못된 코드 예시

#include <cstdlib>
#include <stdio.h>

int main()
{
	int i = 0;
    scanf_s("%d", &i);
    
    if (100 == i)
    {
    	malloc(100);
    }
    
    return 0;
}

`scanf_s()`가 실행돼서 입력 창에 100을 입력하면 힙 영역에 100byte의 메모리 공간이 할당되고 프로그램은 종료된다.

100byte를 사용하지 않는다면 해제해야 되는데, 이는 프로그램을 실행시켜 봐야 알 수 있는 부분이다.

따라서 프로그램이 실행 중에는 해제할 수 없다.

 

▷ 올바른 코드 예시

#include <cstdlib>
#include <stdio.h>

int main()
{
	int i = 0;
    scanf_s("%d", &i);
    
    int* pI = nullptr;  // pI의 초기값을 nullptr로 설정
    // scanf_s() 입력 창에 100을 입력하면 아래 if문이 실행된다.
    if (100 == i)
    {
    	pI = (int*)malloc(100);  // 메모리 100byte 할당
    }
    
    
    // pI가 nullptr가 아닐 때(힙 영역에 100byte가 할당되었을 때) 메모리를 해제한다.
    if (nullptr != pI)
    {
    	free(pI);  // 메모리 해제
    }
    
    return 0;
}

※ `free(pI)`에서 `pI`가 해제되는 것이 아니라 `free()` 함수로 `pI`에 들어있는 값에 해당하는 주소로 접근해서 그곳을 해제한다.


`new` / `delete`

1) `new`

• 함수가 아니라 연산자이다.

• 객체나 배열에 동적으로 메모리를 할당하고 초기화한다.

• 타입을 제공하고, 필요한 경우 메모리를 할당할 요소 수를 제공한다.

▷ 예시

// 자료형* 포인터변수명 = new 자료형;
int* ptr = new int(30);
// 타입* 배열명 = new 자료형[배열크기];
int* arr = new int[50];

• 힙 영역에 메모리를 할당하고, 할당된 메모리의 시작 주소에 대한 포인터를 반환한다.

• `new`는 해당 자료형에 대한 포인터를 반환한다.

  ex) int*, float*

• `new`는 생성자를 호출하여 객체를 초기화한다.

• `new`가 메모리 할당에 실패하면 `std::bad_alloc` 예외가 발생한다.

 

2) `delete`

• 메모리 누수를 방지하기 위해 `new`로 할당된 동적 메모리 공간을 해제하고 해당 객체의 소멸자를 호출한다.

• 해제하려는 메모리에 포인터를 전달한다.

• 메모리를 삭제한 후, 포인터는 dangling pointer가 되므로 역참조 하거나 다시 `delete`하면 오류가 발생한다.

• dangling pointer(댕글링 포인터, 허상 포인터)
이미 해제된 메모리 영역을 가리키는 포인터

※ `new` 배열로 할당된 배열에는 `delete[]`를 사용해야 한다.

만약 `new[]`로 할당된 메모리를 `delete`로 해제하면 오류가 발생한다.

// 포인터변수명 delete;
ptr delete;
// delete[] 배열명;
delete[] arr;

 

▷ 예시1

#include <iostream>

using namespace std;

int main() {
    // new로 메모리 할당
    int* ptr = new int;  
    *ptr = 30;  // 할당된 메모리에 값 할당

    cout << "데이터 : " << *ptr << endl;  // 데이터 출력

    // delete를 사용하여 할당된 메모리 해제
    delete ptr;

    return 0;
}

▽ 출력 결과

데이터 : 35

 

▷ 예시2

#include <iostream>

using namespace std;

int main() {
    // new로 정수 배열 5개 메모리 할당
    int* arr = new int[5];  // 정수 5개 배열 메모리 할당

    // 배열에 값 할당
    for (int i = 0; i < 5; ++i) {
        arr[i] = i * 10;
    }

    // 배열의 각 데이터를 출력한다.
    cout << "배열 데이터 : ";
    for (int i = 0; i < 5; ++i) {
        cout << arr[i] << " ";
    }
    cout << endl;

    // delete[]를 사용하여 배열 메모리 해제
    delete[] arr;

    return 0;
}

▽ 출력 결과

배열 데이터 : 0 10 20 30 40

`malloc()`은 `free()`를, `new`는 `delete`를 사용해야 하는 이유

1) 메모리 관리 방식의 차이

• `malloc()`은 단순히 메모리 공간을 할당하며, C에서는 메모리 할당과 해제가 개별적이다.

• `new`는 C++의 객체 지향 특성을 반영하여 초기화와 소멸차 호출을 포함한 할당을 처리하므로, 객체의 생성과 소멸을 함께 관리한다.

 

2) 타입과 관리 방식

• `malloc()`은 `void*`를 반환하기 때문에 타입 정보를 알지 못한다.

그래서 `free()`는 타입에 상관없이 단순히 메모리 주소를 해제하는 방식으로 동작한다.

• 반면, `new`는 타입에 맞춰 메모리 할당을 하며, C++에서 객체의 생성자와 소멸자도 함께 관리하기 때문에 `delete`는 메모리 해제 시 객체의 소멸자 호출까지 처리해야 한다.

 

∴ `malloc()`과 `new`는 각각 다른 방식으로 메모리를 관리하므로 그에 맞는 해제 방법인 `free()`와 `delete`를 사용해야 한다.

`malloc()`은 `delete`를, `new`는 `free()`를 사용하면 발생하는 문제

정의되지 않은 동작 및 메모리 손상, 충돌 또는 메모리 누수를 포함한 심각한 문제가 발생할 수 있다.

1) `malloc()` 및 `delete`

• `malloc()`은 `new`처럼 소멸자를 호출하거나 객체 메모리를 관리하는 메커니즘이 포함되어 있지 않으므로 이로 인한 충돌이나 메모리 손상이 발생할 수 있다.

또한 해제했음에도 불구하고 힙 영역에 메모리가 남아있을 수도 있다.

• 발생하는 문제 정리 : 정의되지 않은 동작, 충돌 가능성, 메모리 손상 및 잘못된 소멸자 처리

 

2) `new()` 및 `free()`

• `new`는 생성자를 호출하지만 `free()`는 소멸자를 호출하지 않아서 메모리 누수 또는 리소스 누수가 발생할 수 있다.

또한 메모리 할당이 제대로 해제되지 않아 메모리 손상도 발생할 수 있다.

• 발생하는 문제 정리 : 정의되지 않은 동작, 메모리/리소스 누수 및 객체에 대한 소멸자 호출 실패

 

+) 만약 사용자 정의 할당자(allocator)로 할당했다면 일치하는 할당 해제자로 메모리를 해제해야 한다.

#include <iostream>
#include <cstdlib>

class MyAllocator {
public:
    // 사용자 정의 할당자: 단일 정수를 위한 메모리 할당
    void* allocate(size_t size) {
        std::cout << size << " 바이트 할당 중\n";
        return malloc(size);  // malloc을 사용하여 메모리 할당
    }

    // 사용자 정의 해제기: 메모리 해제
    void deallocate(void* pointer) {
        std::cout << "메모리 해제 중\n";
        free(pointer);  // free를 사용하여 메모리 해제
    }
};

int main() {
    MyAllocator allocator;

    // 사용자 정의 할당자를 사용하여 메모리 할당
    int* ptr = (int*) allocator.allocate(sizeof(int));

    // 할당된 메모리 사용
    *ptr = 100;
    std::cout << "할당된 값: " << *ptr << std::endl;

    // 사용자 정의 해제기를 사용하여 메모리 해제
    allocator.deallocate(ptr);

    return 0;
}

'Programming Language > C++' 카테고리의 다른 글

[C++] 문자  (0) 2024.03.24
[C++] namespace, using, 입출력 구현 (cin, cout)  (0) 2024.03.19
[C++] 아스키(ASCII) 코드  (0) 2024.03.14
[C++] void 포인터(void*)  (0) 2024.03.14
[C++] const와 포인터  (0) 2024.03.14