[C++] new와 delete


본 글은 C언어의 동적할당과 프로세스의 메모리 구조를 전부 알고 있다는 전제하에서 진행됩니다. 잘 모르신다면 아래 링크에서 #프로세스의 메모리 구조 부분을 읽어주세요.

 

https://pgh268400.tistory.com/383

 

[C언어 강좌] #18 동적 메모리 할당과 가변 인자

안녕하세요? 파일입니다. 어김없이 또 C언어 학습에 있어 새로운 챕터가 시작되었군요. 이번 챕터와 다음 챕터를 마치게 되면 제 C언어 강좌는 끝을 내게 됩니다. 지금 강의는 31편째지만 이 많은

pgh268400.tistory.com

 

C언어에서 동적 메모리 할당

메모리를 관리하는 문제는 언제나 중요한 문제입니다. C, C++ 는 언어 차원에서 메모리 관리를 프로그래머가 직접 하도록 설계 되어 있습니다, 또한 대부분의 객체지향 언어에서는 많은 부분을 런타임(프로그램 실행 중)에 처리하며 메모리를 자동으로 관리해주는 반면, C++에서는 컴파일 타임(컴파일때)에 처리하는 것을 지향하고 있습니다.

 

int arr[10]; //ok

int a = 0;
scanf("%d", &a);
int arr2[a]; //error

C언어에서는 표준적으로 배열의 크기를 동적으로 할당하는게 불가능했습니다.

예를 들어서 arr이라는 배열의 크기를 잡을땐 그 크기를 상수로 할당해줘야 하는게 일반적이였습니다.

만약에 변수로 a라는 값을 받아서 변수 크기만큼 배열의 크기를 만드는건 허용되지 않습니다.

 

하지만 이를 위해서는 프로그램에 많은 제약이 따르기 때문에 프로그래머가 자유롭게 사용할 수 있는 (그만큼 프로그래머가 직접 관리해야 하고 책임이 따르는) 힙(Heap) 공간이라는 곳에 메모리를 동적으로 할당하면 그 메모리에 값을 저장해낼 수 있었습니다.

 

(변수를 받아내고 그 변수 크기대로 메모리 공간을 마음대로 할당하는게 가능해집니다.)

 

배열의 경우에 연속적인 메모리 공간이니깐 힙 공간에 12바이트를 잡고 int형 포인터로 할당한 메모리 주소의 값을 4바이트식 읽어내면 int arr[3]; 짜리 배열을 만들어 사용하는것과 동일해 지는것이죠.

 

C언어에서는 이러한 동적할당을 위해 malloc() 과 free() 함수를 지원하여 힙 상에서 메모리의 할당과 제거를 다룰 수 있었습니다.

 

C++에서도 마찬가지로 malloc() 과 free() 함수를 사용할 수 있습니다만

언어 차원에서 지원하는 것으로 바로 newdelete가 있습니다.

new는 말 그대로 malloc() 과 대응되는 것으로 메모리를 할당하는 역할을 하고

delete는 free() 에 대응되는 것으로 메모리를 해제하는 역할을 합니다.

 

 

new와 delete

#include <iostream>

int main()
{
    int * p = new int; //4바이트 할당하고 포인터로 포인팅한다. 
    *p = 10;

    std::cout << *p << std::endl;
    delete p; //메모리 사용이 끝났으면 반드시 제거!!
    return 0;
}

위 예제는 new와 delete의 간단 사용법입니다.

 

int * p = new int;

다음과 같이 new 키워드를 사용하면 힙 공간에 메모리를 할당하고 주소를 반환해줍니다.

위의 경우엔 int 크기대로 할당하고 그 메모리 공간을 포인터로 가리키고 있습니다.

 

C언어에서는 힙 공간에 할당하고 그 메모리에 직접 접근하려면 포인터를 이용하는 방법 밖에 없었습니다.

C++에서도 그 사용법이 그대로 넘어왔다고 보시면 됩니다.

 

int * ptr = (int*)malloc(sizeof(int))

만약에 동일한 것을 malloc() 함수로 구현해야 했다면 위처럼 캐스트 연산자를 포함하여 매우 복잡한 코드가 되었을 겁니다. 게다가 C언어에서 사용한 malloc()은 C언어의 문법에 포함되지 않는, 컴파일러 이후에 추가적으로 만들어진 함수일뿐이기에 문법간의 표준도 존재하지 않습니다.

 

그러나 C++에서 사용하는 new 는 연산자인데, 연산자이기 때문에 컴파일러가 문법 형식을 약속하였습니다. 컴파일러가 약속한 문법을 사용하기 때문에 정해진 방식대로 처리가 가능해지고 malloc() 보다 표현이 훨씬 간결해진 것입니다.

 

(그리고 추가적으로 new int; 라는 키워드를 써서 C++ 에서는 자료형을 그대로 cout << int 처럼 찍으면 sizeof(int) 와 동일한 표현이 되는것인가 했는데 그건아닌거 같더군요. 그냥 new 키워드에서 이렇게 사용하도록 약속한 것 같습니다.)

 

*p = 10;

그리고 p 위치에 할당된 공간에 10을 저장하고 이를 출력했습니다.

 

 delete p;

마지막으로 delete p 를 하게 되면 p에 할당된 공간이 해제 됩니다. 

메모리 누수를 막기 위해 동적할당을 하고 꼭 free() 로 해제해줬던 것 처럼

delete 로 메모리를 해제해주세요.

 

delete 는 내가 가리키는 주소의 메모리를 해제하는 명령어 입니다.

 

중요한점은 delete 로 해제할 수 있는 메모리 공간은 사용자가 new를 통해서 할당한 공간(힙 공간)만 가능합니다.

 

 

#include <iostream>

int main()
{
	int a = 5;
	delete &a;
	
	return 0;
}

예를 들어서 힙 공간이 아닌 스택 공간에 할당되어 있는 변수 a를 지우는 코드를 vs에서 실행해보면

 

이렇게 예외가 발생하면서 프로그램이 튕깁니다.

(Dev C++에서는 문제가 안뜨고 넘어가버리더군요.. 이 컴파일러는 항상 쓰면서 느끼는데 너무 관대합니다.)

 

 

 

new로 배열 할당하기

#include <iostream>
using std::cout;
using std::cin;

int main()
{
	int arr_size;
	cout << "동적할당할 배열 크기를 입력해주세요 : ";
	cin >> arr_size;
	int * ptr_arr = new int[arr_size]; //입력받은 크기대로 동적할당 
	
	//0으로 초기화 
	for(int i = 0; i < arr_size; i++){
		ptr_arr[i] = 0;
	}
	
	//출력 
	for(int i = 0; i < arr_size; i++){
		cout << ptr_arr[i] << " ";
	}
	
	delete[] ptr_arr; //동적할당 메모리 해제 
	
	return 0;
}

동적할당할 배열 크기를 입력해주세요 : 20
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
--------------------------------
Process exited after 0.596 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

다음 예제는 값을 cin으로 입력 받아 그 크기대로 배열을 동적 할당하는 예시입니다.

(크기가 자유롭게 변하는 동적 배열이 아니라 배열을 동적 할당한 것입니다. 엄연히 다르니 주의하세요.)

 

 

cin >> arr_size;
int * ptr_arr = new int[arr_size]; //입력받은 크기대로 동적할당

배열을 할당할때 핵심적인 부분은 바로 이 부분입니다.

arr_size 에 값을 입력받아서 new 를 이용해 크기가 arr_size 인 int 배열을 생성하고 있습니다.

 

연속적인 메모리 공간이 할당되고(배열과 같습니다) new는 그 할당한 메모리 공간의 시작 주소를 던질것이므로 포인터로 받고, 하나씩 탐색해주시면 됩니다. 

 

배열을 생성할때에는 [] 를 이용해 배열의 크기를 넣어주시면 됩니다.

 

T * pointer = new T[size];

T를 임의의 타입이라고 생각하면 다음과 같이 일반화 할 수 있습니다.

 

int * ptr = (int*)malloc(sizeof(int) * size)

만약에 malloc() 으로 썼다면 다음과 같이 써야했겠죠.

물론 배열이 연속된 메모리 공간을 할당하는걸 안다면 int 크기 * 갯수 대로 할당하는게 그렇게 어색하진 않을 거 같습니다만 저렇게 위에서 new []로 배열처럼 잡는게 훨씬 편리하고, 한눈에 들어옵니다.

 

delete[] ptr_arr; //동적할당 메모리 해제

마지막으로 delete 하는 부분으로 new [] 로 괄호를 활용해서 배열을 할당했으므로

제거할때도 delete[] 와 같이 괄호를 활용해서 메모리를 해제시켜줍니다.

 

delete[]가 아닌 delete로 메모리를 해제 시키면 절대 안됩니다!!

delete 를 사용하면 배열의 첫 번째 원소에 해당하는 메모리만 해제되고 그 뒤에 메모리는 해제가 되지 않습니다 또한 이후 메모리에 접근할 방법이 없어서 메모리 누수가 일어납니다.

 

new - delete 가 짝을이룬다면, new[] - delete[] 역시 짝을이룬다고 기억해주시면 되겠습니다.

 

new로 초기화 하기

#include <iostream>
using std::cout;
using std::cin;

int main()
{
	int arr_size;
	cout << "동적할당할 배열 크기를 입력해주세요 : ";
	cin >> arr_size;
	int * ptr_arr = new int[arr_size] {}; //입력받은 크기대로 동적할당과 0으로 초기화
	
	//출력 
	for(int i = 0; i < arr_size; i++){
		cout << ptr_arr[i] << " ";
	}
	
	delete[] ptr_arr; //동적할당 메모리 해제 
	
	return 0;
}

아까 코드를 그대로 가져와봤습니다.

그런데 0으로 초기화 하는 부분이 없어졌는데도 프로그램을 실행해보면 0으로 잘 초기화 되어 있습니다.

 

int array1[5] = {10,20,30,40,50};

C/C++ 에서 배열을 사용할때 다음과 같이 선언과 동시에 중괄호({, }) 를 사용해 값을 넣어 초기화 해주는걸 알고 계시나요? new에서도 이런것을 지원합니다.

 

int * ptr_arr = new int[arr_size] {};

이런식으로 써주시면 arr_size대로 동적할당과 동시에 중괄호 안에 아무것도 안써줬으므로 자동 0으로 초기화 합니다.

 

만약에 int 크기로 동적할당하여 배열 요소를 3개 잡고 10,20,30을 넣어주고 싶으시면

int* ptr_arr1 = new int[3]{10,20,30}; 
int* ptr_arr2 { new int[3] {10,20,30} };

위 코드 중 둘중에 하나를 써주시면 됩니다

 

new는 객체를 생성한다

new 키워드는 HEAP 공간에 공간을 할당할 뿐만 아니라 객체를 생성해줍니다.

 

C++에서는 new를 붙여줬으면 힙 공간에 객체를 동적할당하는것이고, new없이 만들면 스택 영역에 만들어집니다.

즉, C++에서는 객체를 생성할때 스택/힙 공간에 할당하는 두가지 방법을 지원한다고 보면 됩니다.

 

new로 만들었으면 할당된 메모리를 주소로 반환하므로 당연히 포인터 형태로 받아야 합니다.

 

일반적으로 Python, Java 와 같은 보편적인 언어는 클래스 객체 생성을 Heap 영역에 한다고 합니다. Java를 예로 들어보면 객체 생성시 관용적으로 사용하는 new 키워드를 통한 객체 생성은 힙 공간에 만들어지게 되고, 스택 영역에 만드는건 허용되지 않습니다.

참고 : https://leemoney93.tistory.com/61, https://kangworld.tistory.com/66

 

* 다른 C#, Java 같은 객체지향언어를 하면서 객체 생성시 습관적으로 new를 사용하셨을건데, C++에선 객체 생성 방법이 두가지고 new 사용시에 힙 생성까진 의미가 같다고 해도 포인터를 사용해야 하며, High-Level한 언어들에선 포인터를 사용하지 않으니 주의하셔야 합니다. 

 

#include <iostream>
class Car
{
public:
	Car()
	{
		std::cout << "Car is Making";
	}
};
int main()
{
	Car * new_car1 = new Car();
	delete new_car1;
	return 0;
}

Car is Making
--------------------------------
Process exited after 0.01069 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

다음 예제를 실행해보면 생성자의 코드인 Car is Making이 출력되는걸 알 수 있는데 생성자의 경우 객체 생성시 자동 호출되므로 객체 생성의 역할까지 하는걸 알 수 있습니다.

 

Car *p = (Car *)malloc(sizeof(Car));

물론 다음과 같이 malloc() 을 통해서 동적으로 메모리를 할당해볼 순 있습니다만

이렇게 하면 메모리는 할당이 되는데 정상적인 상황이 아니게 됩니다.

 

객체는 선언이 되면 메모리만 할당되는게 아니라 객체 내부의 생성자(Constructer)가 자동으로 호출되어 객체 스스로 초기화 되는 작업이 진행되는데, malloc() 을 사용하면 new처럼 heap 공간에 할당과 동시에 객체를 생성해주는게 아니라

그냥 메모리만 할당되고 끝나버립니다.

 

생성자는 호출되지 않는 것이지요.

 

delete new_car1;

또한 비슷한 맥락으로 delete 연산자를 사용하면 메모리 해제 전에 객체의 소멸자(파괴자)가 자동 호출되고 메모리가 해제되게 됩니다.

 

free() 함수를 사용하면 메모리 호출 시 객체 소멸자가 호출되지 않아서 문제가 발생할 수 있습니다.

 

결론

그래서 결론을 내려보자면 C++에는 new와 delete라는 좋은 녀석이 있으므로 굳이 malloc()과 free() 를 사용할 필요가 없습니다.

사실은 거의 권고사항, 반 강제가 아닌가 싶네요. 위에서 보시다 싶이 객체 생성에 있어서 생성자와 소멸자가 malloc()과 free()에선 호출 되지 않기 때문입니다.

이전의 동적할당 함수들은 잊고 C++ 에 추가된 연산자인 new 와 delete 를 이용하도록 합시다!

 

 

참고

씹어먹는 C++ 강좌

https://blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=tipsware&logNo=221067724735 

https://jhnyang.tistory.com/334

COMMENT WRITE