본문 바로가기
C++

[C++]new delete 동적할당

by Junk_Seo 2018. 1. 24.
반응형

지역변수

우리가 일반적으로 사용하는 지역 변수 선언을 정적으로 선언하였다고 한다.

즉, 다음과 같은 변수와 배열의 선언을 정적으로 선언하였다고 한다. 

int nData = 0;
int arData[10] = {};

자료형과 배열의 크기에 맞춰서 메모리 영역에 공간을 할당해 주는 것이다.

 

이런 지역 변수가 할당되는 메모리의 영역을 스택(stack) 영역이라고 하며 선언된 지역 내에서만 사용가능하며 선언된 지역을 벋어나면 스택 영역에서 사라지게 된다.

이러한 지역변수의 경우에는 컴파일 타임에 크기를 결정하기 때문에 런 타임 시기(프로그램이 실행되고 있는 중)에 지역 변수를 새로 생성하고 사용하는 것은 불가능하다.

따라서 런 타임 시기에 메모리를 잡아서 사용하기 위해서는 동적할당을 사용해야 한다.

 

*****

스택 영역은 이름 그대로 스택의 성질을 가지고 있기 때문에 나중에 선언된 메모리 영역이 먼저 없어진다.

동적할당

동적할당은 프로그래머의 필요에 의해 동적으로 메모리를 할당할 때 사용한다는 의미로 프로그래머가 필요하다고 생각한 그 순간에 필요한 만큼 메모리를 할당할 때 사용하는 것이다.

즉, 할당해야 할 메모리의 크기를 런 타임 중에 결정해야 하는 경우에 사용한다.

 

예를 들면 다음과 같은 코드가 있다.

int nLen = 0;
scanf_s("%d", &nLen);
int arData[nLen] = {};

코드를 보자면 int 형 변수 nLen을 선언하고 프로그램 실행 중에 값을 입력받아 int형 배열 arData의 길이로 사용하는 코드로 볼 수 있다.

하지만 이 코드는 잘못되었다.

왜냐하면 지역변수는 컴파일 타임에 메모리에 그 크기만큼 할당해 주어야 하는데, 위 코드에서 배열 arData의 크기를 런 타임 시기에 지정해주고 있기 때문이다. arData는 지역변수이기 때문에 컴파일 타임에 크기가 할당되어야 하는데 런 타임 시기에 크기가 할당되고 있기 때문에 컴파일 error가 발생하게 된다.

 

이렇게 되면 다음과 같은 코드는 되지 않느냐 하고 할 수 있다.

int nLen = 10;
int arData[nLen] = {};

여기서의 문제는 int형 변수 nLen의 값이 언제 설정되는지에 있다.

nLen이라는 변수는 분명히 컴파일 타임에 메모리의 스택 영역에 그 크기만큼 할당되는 것이 맞다. 하지만 그 값이 설정되는 것은 컴파일 타임이 아닌 런 타임 시기이다. 

따라서 컴파일 타임에 nLen의 값이 얼마인지는 알 수 없기 때문에 error가 발생하게 된다.

그래서 배열을 지역변수로 선언하기 위해서는 그 길이를 상수로 알려주어야 한다.

 

이러한 이유로 동적할당이라는 것을 사용한다.

동적할당의 경우 프로그래머가 원하는 경우 원하는 만큼 메모리를 할당하고 싶은 경우에 사용하며 지역변수와는 다르게 힙(Heap) 영역에 할당됩니다.

 

사용법은 new 키워드를 사용하여 메모리를 할당하고 delete 키워드를 사용하여 할당했던 메모리를 해제합니다.

동적할당을 통해 메모리를 할당한 경우 메모리를 해제해 주지 않으면 계속 메모리의 힙 영역에 남아있게 되므로 사용이 끝났다면 반드시 메모리를 해제해 주어여 합니다. 그렇지 않을 경우 프로그램 실행 중에 문제가 발생할 수 있습니다.

int* pData = new int(0);
int* pArray = new int[<lenght>]{};
...
...
delete[] pArray ;
pArray = nullptr;
delete pData ;
pData = nullptr

위의 코드가 동적할당의 기본 형식입니다.

new 키워드를 사용하여 메모리의 힙 영역에 할당한 후 그 메모리의 시작 주소 값을 포인터 변수에 저장하여 사용하는 방식입니다.

(new 키워드는 메모리의 힙 영역에 크기를 할당한 후 그 메모리의 시작 주소값을 반환합니다.)

동적할당한 메모리를 다 사용했다면 delete 키워드를 사용하여 메모리를 해제합니다.

동적할당으로 배열을 할당한 경우 메모리를 해제하기 위해서 [] 키워드를 붙여서 해제합니다.

그리고 동적할당 하면서 (0)과 {}를 붙이는 것은 동적할당과 동시에 초기화를 해주는 것이므로 함께 해주어야 합니다.

 

그리고 동적할당한 포인터 변수의 주소값을 변경하는 짓은 하면 안 됩니다. 그럴 경우 힙 영역에 할당한 메모리의 위치를 잃어버리기 때문에 나중에 메모리를 해제하지 못하게 됩니다.

2차원 배열 동적할당

(2차원 배열 동적할당이지만 다차원 배열의 동적할당이라고 이해하시면 됩니다.)

2차원 배열의 동적할당은 기본적으로 제공해 주는 방식이 있습니다.

int (*p)[3] = nullptr;
p=new int[<len>][3]{};
...
delete[] p;

위 코드의 모습이 기본적으로 제공해 주는 2차원 배열을 동적할당하는 모습입니다.

하지만 이러한 방식으로 2차원 배열을 동적할당하는 경우는 없다고 생각하면 됩니다.

왜냐하면 이 방식은 2차원 배열의 세로 길이는 가변적으로 동적할당이 가능하지만 가로의 길이는 고정되어 있기 때문입니다.

 

따라서 다른 방식으로 2차원 배열을 동적할당하며 이는 2차원 배열뿐 아니라 다차원 배열에도 적용됩니다.

int ** ppData = nullptr;
ppData = new int*[2]{};
ppData[0] = new int[3]{};
ppData[1] = new int[3]{};
...
...
delete[] ppData[0];
ppData[0] = nullptr;
delete[] ppData[1];
ppData[1] = nullptr;
delete[] ppData;
ppData = nullptr;

위 코드와 같은 방식으로 2차원 배열을 동적할당하여 사용합니다.

int**(더블 포인터)에 int* 배열을 원하는 길이만큼 동적할당 한 다음 이 배열의 각 인자마다 다시 int형 배열을 다시 동적할당 하여 사용하는 것입니다.

이렇게 사용할 경우 2차원 배열의 세로 길이와 가로길이 모두 가변적으로 동적할당이 가능합니다.

대신 메모리를 해제를 정확히 해주어야 합니다. 메모리 해제는 할당한 역순으로 해제합니다.

 

조금 더 디테일한 사용법은 다음과 같습니다.

int ** ppData = nullptr;
int nWidth = 0;
int nHeight = 0;
nWidth = 5;
nHeight = 4;

ppData = new int*[nHeight] {};
for (int i = 0; i < nHeight; i++) {
	ppData[i] = new int[nWidth] {};
}

for (int i = 0; i < nHeight; i++) {
	for (int j = 0; j < nWidth; j++) {
		printf("%d ", ppData[i][j]);
	}
	printf("\n");
}

...
...

for (int i = 0; i < nHeight; i++) {
	delete[] ppData[i];
	ppData[i] = nullptr;
}
delete[] ppData;
ppData = nullptr;

반복문을 사용하시면 더 편리하게 사용할 수 있게 됩니다.

new/delete 와 malloc/free

malloc과 free는 C언어에서 사용하던 동적할당 키워드입니다. C++로 넘어와서는 new와 delete로 변경되었는데, 두 동적할당의 방식에는 차이점이 있습니다. 바로 생성자와 소멸자입니다.

C++의 경우 기본 자료형도 생성자를 호출하여 초기화한다는 것을 알고 계실 것입니다. 

따라서 원래는 기본 자료형을 초기화할 땐 다음과 같은 형식을 사용합니다.

int nData(0);
int arData[10]{};

하지만 C style에 익숙한 사람들을 위해 C style의 방식도 허용하는 것이지요.

new는 생성자를 delete는 소멸자를 호출해 줍니다. 그래서 new 키워드를 통해 동적할당할 경우 동시에 초기화가 가능한 것입니다.  

(malloc의 경우는 생성자라는 것이 없기 때문에 동적할당과 동시에 초기화가 불가능합니다.)

<추가 1>

동적할당하여 사용한 다음에 delete를 사용하여 메모리를 해제하는데, 제가 보여준 코드에는 delete를 하고 난 다음에 동적할당 했던 모든 포인터 변수에 다시 nullptr로 초기화해주고 있습니다. 이유는 메모리를 해제하였다고 하더라고 이 포인터 변수가 가지고 있는 값이 어떤 값을 가지고 있을지 장담할 수 없기 때문에 nullptr로 다시 초기화하여 이후에 발생할 수 있는 문제를 사전에 예방하는 것입니다. 

따라서 반드시 사용할 필요는 없지만 프로그래머 스스로의 안정성을 위해서는 추가하여 사용하는 것이 좋다고 합니다. 

<추가 2>

new 키워드의 연산속도에 관한 내용입니다. new 키워드의 연산 속도는 C++에 있는 모든 연산자를 통틀어 가장 느립니다. 따라서 new 키워드를 함부로 사용하면 프로그램 속도에 영향을 줄 수 있습니다.

반응형