본문 바로가기

Programming Languages/C++

메모리 할당 및 반환

동적 메모리 할당 ▶ 변수를 선언하면 그 변수에 대한 기억공간이 메모리의 적절한 위치에 할당된다. 일반적으로 변수는 함수 내부에 선언되어 함수가 실행되는 동안만 존재하거나, 프로그램 시작과 함께 생성되어 프로그램을 종료할 때 소멸된다. 함수 내부에서 선언되는 지역변수들은 대부분 전자에 해당되며, 함수의 외부에 선언되는 전역변수나 함수 내부에서 static 키워드와 함께 선언되는 변수는 후자에 해당된다.

그런데 때에 따라서는 필요할 때 기억공간을 할당하고 더 이상 그 공간이 필요하지 않으면 반환할 수 있어야 한다. 이와 같은 기능을 동적 메모리 할당(dynamic memory allocation)이라고 한다. 그런데 동적 메모리 할당으로 생성된 저장공간은 이름이 없어 변수처럼 그 이름을 통해 액세스할 수 없다. 이러한 문제는 포인터를 이용하여 해결한다. 즉, 동적으로 할당된 저장공간을 포인터 변수가 가리키게 하면 그 포인터를 이용하여 액세스할 수 있다.

new와 delete ▶ 동적 메모리 할당은 다음과 같이 new 연산자를 이용한다. 

1. ptrVar = new TypeName;

2. ptrVar = new TypeName[n];

형식1은 지정된 자료형의 데이터 1개를 저장할 수 있는 공간을 할당하여, 그 주소를 포인터 변수에 넣는다. 이때 자료형은 포인터의 자료형과 일치해야 한다. 형식2는 지정된 자료형의 데이터를 n개 저장할 수 있는 배열을 할당한다. n은 양의 정숫값을 내는 수식이면 되며, 상수가 아니어도 된다.

사용이 끝난 메모리 공간은 시스템에 반납하여 다른 용도로 할당해 사용할 수 있도록 한다. 이때에는 다음과 같이 delete 연산자를 사용한다. 

1. delete ptrVar;

2. delete [] ptrVar;

delete 연산자는 new 연산자의 형식1로 할당한 메모리 공간을 반환할 때 사용하며, delete [] 연산자는 형식2의 방법으로 new 연산자를 사용하여 할당한 메모리 공간을 반환할 때 사용한다. 만일 ptrVar에 nullptr가 저장되어 있다면 delete는 아무 일도 하지 않는다. 

메모리의 동적 할당
반환


int *intPtr; // int형 포인터의 선언

intPtr = new int; // int 값을 저장할 공간 할당

*intPtr = 10; // 할당된공간을 사용

.......

delete intPtr; // intPtr가 가리키는 공간 반환

intPtr = nullptr; // intPtr를 nullptr로 지정


위 예에서는 new 연산자로 int형 값을 저장할 공간 하나를 할당한다. 그 결과 시스템의 자유공간에서 4바이트가 할당되며, 그 주소가 intPtr에 저장된다(위의 메모리 동적 할당, 반환 그림 참조). 사용이 끝나고, 더 이상 그 공간이 필요하지 않으면 이를 시스템에 반환하여 필요할 때 그 공간을 재활용할 수 있게 한다. 이렇게 공간을 반환하더라도 intPtr는 여전히 그 공간을 가리키고 있다. 그러나 그 공간은 이미 반환된 것이므로, intPtr에 nullptr를 넣어 포인터가 가리키는 곳이 없음을 의미하도록 하는 것이 좋다.

배열의 할당

위 그림은 다음과 같은 4개의 int 값을 저장할 수 있는 공간을 할당하는 것을 보여 준다.


int *intPtr; // int형 포인터의 선언

intPtr = new int[4]; // 4개의 int를 저장할 공간 할당

*intPtr = 10; // 할당된 공간을 사용

*(intPtr + 1) = 20; 

intPtr[2] = 30; // *(intPtr + 2) = 30; 과 동일함

.....

delet [] intPtr; // intPtr가 가리키는 공간 반환

intPtr = nullptr; // intPtr를 nullptr로 지정


new int[4]는 int형 값 4개를 저장할 수 있는 배열을 할당하도록 지시하는 명령어이다. 이때 intPtr에는 할당된 공간의 첫 바이트의 주소가 전달된다. 나머지 3개의 저장공간은 intPtr로부터의 상대 위치를 지정하여 액세스할 수 있다. 사용 후 공간을 반납할 때에는 delete [] 연산자를 사용하는 것에 주의하자. 만일 new 연산자로 요청한 저장공간을 할당할 수 없을 때는 std::bad_alloc이라는 예외가 발생하며, C++의 예외처리 방식에 때라 대응한다. 만일 예외처리 방식을 원하지 않는다면 new 대신 new(nothrow)를 사용할 수 있는데, 이때 저장공간 할당에 실패하면 new 연산자는 nullptr를 내놓으며, 이를 검사하여 다음과 같은 형식으로 대응할 수 있다.

int *pt = new(nothrow) int[1000];

if(pt == nullptr){

..... // 할당 실패에 대응하는 처리를 함

}

포인터 연산 ▶ 할당된 배열 공간에서 각각의 저장공간을 액세스하려면 포인터 연산을 이용하여 기준 위치의 포인터 값으로부터의 상대 위치를 지정한다. 

int형 및 char형 포인터에 대한 연산

임의 포인터 ptr를 기준으로 첫째 값의 주소는 ptr, 둘째 값의ㅏ 주소는 ptr + 1, 셋째 값의 주소는 ptr + 2 등으로 표현한다. 또한 그 위치의 값은 각각 *ptr, *(ptr+1), *(ptr+2) 등으로 표현한다.

ptr, ptr+1, ptr+2 등의 실제 주소는 ptr가 어떤 자료형의 포인터인가에 따라 다르다. 예를 들어 위 그림의 (가)와 같이 100번지부터 시작하는 int형 데이터 블록을 가리키는 포인터 intPtr의 경우 int형이 4바이트를 차지하므로 intPtr가 가리키는 주소는 100, intPtr + 1은 104, intPtr + 2는 108번지가 된다. 반면 char형은 1바이트이므로 그림(나)에서 chPtr가 가리키는 주소는 100, chPtr + 1은 101, chPtr + 2는 102번지 등이 된다. 

이와 같이 포인터에 대한 연산은 그 형에 따라 실제로는 다르게 구현된다. 이것은 일종의 연산자 다중정의에 해당된다. -, ++, -- 등의 연산도 동일한 개념에 따라 처리된다. C++에서는 포인터의 형에 대해 매우 엄격하다. 따라서

int *intPtr;

char *charPtr;

charPtr = intPrt; // 오류 - 포인터의 형이 다름

과 같이 다른 형의 포인터를 대입하는 것은 컴파일할 때 오류로 처리된다. 궅이 형 변환을 해야 할 경우는 reinterpret_cast를 사용해야 한다. static_cast는 사용할 수 없다. 

다음은 포인터 ptr에 대한 연산들을 정리해 본 것이다.

연산 처리
ptr + n ptr의 n번째 뒤의 저장공간에 대한 포인터
ptr - n ptr의 n번째 앞의 저장공간에 대한 포인터
ptr++ ptr가 현재 위치의 다음 값을 가리키도록 한다
ptr-- ptr가 현재 위치의 앞의 값을 가리키도록 한다
*(ptr + n) 및 ptr[n] ptr로부터 n번째 뒤에 저장된 값
*(ptr - n) 및 ptr[-n] ptr로부터 n번째 앞에 저장된 값
*ptr++ 및 *ptr-- *ptr의 값을 현재 수식에 사용하고, ptr가 다음 또는 이전 값을 가리키도록 한다.
*++ptr 및 *--ptr 먼저 ptr가 다음 또는 이전 값을 가리키도록 한 후, 그 위치의 값을 수식에 사용한다.
(*ptr)++ 및 (*ptr)-- ptr가 가리키는 곳의 값을 현재 수식에 사용한 후 그곳의 값을 1 증가 또는 감소시킨다.
++(*ptr) 및 --(*ptr) ptr가 가리키는 곳의 값을 1 증가 또는 감소시킨 후 그 값을 현재 수식에 사용한다.

배열과 포인터 ▶ 배열과 포인터는 매우 밀접한 관계에 있다. 앞에서 배열의 이름은 배열의 첫 원소의 주소를 의미한다고 언급한 바 있다. 이는 포인터의 개녕과 거의 같다. 물론 배열변수는 포인터 변수가 아니므로 배열 이름이 가리키는 주소를 인위적으로 바꿀 수는 없다.

ArrayPtr.cpp는 배열과 포인터를 함께 사용하는 예를 보여 준다.

#include <iostream>
using namespace std;

int main() {
	char str[14] = "Hello, world!";
	char* pt;

	cout << str << endl;
	pt = str; // pt기 배열 str를 가리킴
	while (*pt) { // 문자열의 끝이 아니면 반복
		if (*pt >= 'a' && *pt <= 'z') // 소문자인 경우
			*pt = *pt - 'a' + 'A'; // 대문자로 바꿈
		pt++; // 다음 문자로 포인터 이동
	}
	cout << str << endl;
	return 0;
}

====== 결과 ======
Hello, world!
HELLO, WORLD!

char str[14]는 str에 "Hello, world!"라는 문자열을 저장하기 위한 문자 배열이다. 이 문자열에는 13개의 문자만 보여 문자열의 길이는 13이지만, 실제로는 문자열의 끝을 알 수 있도록 널문자('/\0', 문자코드 0)가 끝에 포함되어 있다. 그러므로 총 14개의 문자를 저장할 공간이 필요하다. 이러한 문자열 표현 방식은 C언어 이후 계속 사용되어 오던 것으로, 흔히 C 스타일 문자열이라고 한다.

pt = str은 배열 str의 시작 주소를 나타내므로 포인터 pt는 배열의 시작 위치를 가리킨다. while문은 pt가 가리키고 있는 곳의 값이 0이 아니면, 즉 문자열의 끝이 아니면 반복 실행할 것을 지시하고 있다. 

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

[함수]-함수의 정의와 호출-함수의 정의  (0) 2020.05.12
참조  (0) 2020.05.11
포인터  (0) 2020.05.06
배열  (0) 2020.04.30
클래스  (0) 2020.04.29