C++/C++ 기초

[C++] 동적 메모리 할당 : new / delete 연산자

Song 컴퓨터공학 2023. 7. 6. 16:17
 

동적 메모리 할당(Dynamic Memory Allocation) 및 해제

동적 메모리 할당을 알아보기에 앞서 정적 메모리 할당의 한계점을 하나 짚고 넘어가겠습니다. 앞선 포스팅...

blog.naver.com

C 언어에서는 malloc() 이나 free() 와 같은 표준 C 함수를 이용해 동적 메모리 할당 및 반환을 했습니다. 위 포스팅에 동적 메모리 할당과 반환은 무엇을 의미하는지 C 언어 기준으로 더 상세히 설명해놨습니다. C++ 에서는 newdelete 연산자를 통해 동적 메모리 할당/반환을 수행합니다.

 

new 연산자는 힙 으로부터 메모리를 할당 받고, delete 연산자는 할당 받은 메모리를 힙으로 반환합니다.

 

 

 

 기본 자료형 동적 할당/반환

 

기본 형식은 다음과 같습니다.

데이터타입 *포인터변수 = new 데이터타입;

... // 사용이 끝난 후

delete 포인터 변수

new 연산자는 데이터타입의 크기만큼 힙으로부터 메모리를 할당받아 주소를 리턴합니다. 따라서 포인터 변수와 항상 세트로 붙어다닙니다. malloc 이랑 사용법이 동일하죠? 또한 이 때의 데이터 타입은 char, int, double 같은 기본 자료형 뿐만 아니라 구조체, 클래스도 포함됩니다. delete 연산자는 포인터 변수가 가리키는 메모리를 힙으로 반환합니다.

 

또한 힙 메모리가 부족한 경우 new 는 NULL 을 리턴하기 때문에 NULL 체크 또한 포함해주는 것이 좋습니다. malloc 을 사용할 때는 형변환도 해줘야했고, sizeof도 사용해야 했지만 new 는 그런 작업들을 안해줘도 됩니다.

int *p = new int;
if(!p) { return; } // NULL 이면 바로 return
*p = 10;
// ...
delete p;



// ------ 다양한 타입에 적용 가능 -------
int *pint = new int;
int *pchar = new char;
int *pCircle = new Circle();

delete pint;
delete pchar;
delete pCircle;

 

new 를 사용해 메모리를 할당 받을 때, 바로 초기값을 지정해서 초기화해줄 수 있습니다. 

데이터타입 *포인터변수 = new 데이터타입(초깃값);
int *pInt = new int(20);	// 20으로 초기화된 int 공간 할당
char *pChar = new char('a')	// 'a'로 초기화된 char 공간 할당

 

 

 

 배열 동적 할당/반환

 

배열을 동적 할당/반환할 때의 형식은 다음과 같습니다.

데이터타입 *포인터변수 = new 데이터타입 [배열의 크기];
delete[] 포인터변수;

할당 받는 new 형태는 뭔가 익숙한데, delete[] 형태는 좀 익숙치 않죠. 사용자로부터 몇 개의 정수를 입력 받을건지 먼저 입력받고 그에 맞는 배열을 동적 할당 받아 평균를 출력하고 다시 반환하는 예제 코드를 통해 살펴봅시다.

 

#include <iostream>
using namespace std;

int main()
{
	int n, sum = 0;
	cout << "입력할 정수 개수 : ";
	cin >> n;
	if (n <= 0) return 0;	// 0이하 정수 입력하면 종료
	int* p = new int[n];	// n개의 정수를 저장할 배열 동적 할당
	if (!p) { cout << "메모리 동적 할당 오류"; return 0; }

	for (int i = 0; i < n; i++) {
		cin >> p[i];
		sum += p[i];
		// cin >> *(p+i);
		// sum += *(p+i);
	}

	cout << "평균 = " << sum / n << endl;

	delete[] p;
}

delete[] 를 안 쓰고 delete 로 초기화를 하면 오류가 발생하게 됩니다. 

 

 

배열을 할당받자마자 초기화할 때는 생성자를 통해 직접 초기값을 지정할 수 없습니다.

int *ptr1 = new int[10](20) // 틀린 코드
int *ptr2 = new int(20)[10] // 틀린 코드

int *ptr3 = new int[] {1, 2, 3, 4} // 1, 2, 3, 4 가 저장된 배열이 생성된다. 옳은 초기화

 

 

 

 객체 / 객체배열 동적 할당/반환

 

객체나 객체 배열의 동적 할당/반환은 사실 위랑 같습니다. 데이터타입 자리에 클래스이름 으로 대체될 뿐입니다.

클래스이름 *포인터변수 = new 클래스이름;
클래스이름 *포인터변수 = new 클래스이름(생성자매개변수리스트);

delete 포인터변수;


클래스이름 *포인터변수 = new 클래스이름[배열크기];
클래스이름 *포인터변수 = new 클래스이름[] { 생성자 하나씩 각각 써주면 초기화 가능 };

접근 방법 또한 포인터 사용법 완전 동일 !

내용이 동일하니 예제 코드를 위주로 살펴보겠습니다.

 

// 객체 동적 생성 및 반환 예시
#include <iostream>
using namespace std;

class Circle {
	int radius;
public:
	Circle() { radius = 1; cout << "생성자 실행 radius = " << radius << endl; }
	Circle(int r) { radius = r;  cout << "생성자 실행 radius = " << radius << endl; }
	~Circle() { cout << "소멸자 실행 radius = " << radius << endl; }
	void setRadius(int r) { radius = r; }
	double getArea() { return 3.14 * radius * radius; }
};

int main() {
	Circle* p, * q;
	p = new Circle();
	q = new Circle(30);
	cout << "p가 가리키는 객체 면적 : " << p->getArea() << endl;
	cout << "q가 가리키는 객체 면적 : " << q->getArea() << endl;
	delete p;
	delete q;

	// 정수 값으로 반지름 입력 받고 Circle 객체를 동적 생성하여 면적을 출력. 음수 입력하면 종료
	cout << endl << endl;
	int r;
	while (1) {
		cout << "정수 반지름 입력(음수이면 종료) >> ";
		cin >> r;
		if (r < 0) break;
		Circle* p = new Circle(r);
		cout << "원의 면적 = " << p->getArea() << endl;
		delete p;
	}
}

원래 동적할당이 아닌 경우는 가장 나중에 생성된 객체가 가장 먼저 소멸됩니다. 그러나 delete 명령을 통해 우리가 순서를 부여하고 있기 때문에 먼저 실행된 반지름 1 객체가 먼저 소멸되는 것을 확인할 수 있습니다. 그 밑은 단순 응용 입니다.

 

 

다음으로 객체 배열을 동적 생성하는 아주 간단한 예를 보겠습니다.

#include <iostream>
using namespace std;

class Circle {
	int radius;
public:
	Circle() { radius = 1; cout << "생성자 실행 radius = " << radius << endl; }
	Circle(int r) { radius = r;  cout << "생성자 실행 radius = " << radius << endl; }
	~Circle() { cout << "소멸자 실행 radius = " << radius << endl; }
	void setRadius(int r) { radius = r; }
	double getArea() { return 3.14 * radius * radius; }
};

int main() {
	Circle* p = new Circle[3];
	for (int i = 0; i < 3; i++) p[i].setRadius(10*(i+1));

	Circle* ptr = p;
	for (int i = 0; i < 3; i++) cout << (p + i)->getArea() << endl;

	delete[] p;
}

동적 배열의 경우, 따로 설정해주지 않으면 각 객체마다 기본 생성자가 호출되는데, 따로 설정해주는 경우는 매개변수 있는 생성자가 호출됩니다. 배열의 경우 소멸자가 원래 순서대로 가장 나중에 생성된 객체가 가장 먼저 소멸하는 것을 확인할 수 있습니다. 그리고 만약 저기서 delete[] p; 가 아니라 delete p를 사용한다면?

이러한 오류가 발생하게 됩니다. 같은 메모리 공간에 대해서 반복적으로 delete 를 수행하거나 이렇게 잘못된 delete 연산자를 사용하는 경우 Expression: _CrtlsValidHeapPointer(block) 이라는 요류가 발생하게 됩니다. 이 오류가 발생한다면 C에서는 free 가 잘못되거나 이미 free 가 된 메모리에 free 를 한 경우가 많고, C++ 에서는 delete 를 했는데 또 하거나 배열인데 delete[] 연산자를 쓰지 않고 delete 연산자를 쓴 경우, 배열이 아닌데 delete[] 연산자를 쓴 경우가 많아서 이를 점검하면 대부분 해결됩니다.


번외로, 객체 배열을 생성하고 포인터 변수를 통해 접근하는 다양한 방법을 총 정리한 예시 코드입니다. 동적 생성/반환은 아니지만 객체나 객체 배열에 대한 포인터 변수의 접근을 다 써놨기 때문에 이 코드만 알아도 포인터는 사용할 줄 알게 됩니다.

#include <iostream>
using namespace std;

class Circle {
	int radius;
public:
	Circle() { radius = 1; cout << "생성자 실행 radius = " << radius << endl; }
	Circle(int r) { radius = r;  cout << "생성자 실행 radius = " << radius << endl; }
	~Circle() { cout << "소멸자 실행 radius = " << radius << endl; }
	void setRadius(int r) { radius = r; }
	double getArea() { return 3.14 * radius * radius; }
};


int main()
{
	Circle c[3] = { Circle(10), Circle(20), Circle() };
	Circle* p = c;
	cout << endl;

	for (int i = 0; i < 3; i++) {
		cout << "circleArray[] " << i << "의 면적은 " << c[i].getArea() << endl;
	}
	cout << endl;
	for (int i = 0; i < 3; i++) {
		cout << "*(circleArray+i) " << i << "의 면적은 " << (*(c + i)).getArea() << endl;
	}
	cout << endl;
	for (int i = 0; i < 3; i++) {
		cout << "p[i] " << i << "의 면적은 " << p[i].getArea() << endl;
	}
	cout << endl;
	for (int i = 0; i < 3; i++) {
		cout << "*(p+i) " << i << "의 면적은 " << (*(p + i)).getArea() << endl;
	}
	cout << endl;
	for (int i = 0; i < 3; i++) {
		cout << "p-> " << i << "의 면적은 " << (p + i)->getArea() << endl;
	}

	cout << endl << "c->getArea() = " << c->getArea() << endl;
	cout << "(*(c + 1)).getArea() = " << (*(c + 1)).getArea() << endl << endl;
	return 0;
}