Dynamic Allocating Memory
동적 메모리 할당
- 컴파일 시간이 아닌, 실행시간 동안에 메모리를 대입하는 방법
(정적 포인터 변수의 경우, 소스코드 상에서 주소를 할당(초기화)하는데 이와 같은 메모리는 컴파일 시간에 할당된다.)
- OOP에서 중요한 테크닉 중 하나로 손꼽힌다.
- 포인터가 메모리를 위한 대용 이름을 제공하는 기능이라면, 동적 메모리 할당은 실행시간 동안에 unnamed(이름없는) 메모리를 대입하는 기능이다. 이 경우에 포인터가 그 메모리에 접근할 수 있는 유일한 통로이다.
C Style: malloc() & free()
- C Style이라고 표기했지만 C++에서도 cstdlib 헤더파일을 포함시키면 사용할 수 있다.
- 동적인 메모리 할당 후, 반드시 Memory Leak(메모리 누수) 방지를 위해 free함수를 통해 반환해주어야 한다.
함수 원형
void* malloc(동적으로_할당할_메모리_크기);
void free(반납할_포인터_변수명);
Ex. C Style 동적 메모리 할당 & 반환 예시
#include <stdlib.h> // malloc()과 free()가 포함되어 있는 헤더파일
...
int arr[] = {1, 2, 3);
int* ptr;
ptr = (int*)malloc(sizeof(int)*3);
// malloc은 void return type 함수이므로, 형변환이 필수적이다.
for int i = 0; i < 3; i++)
ptr[i] = arr[i];
free(ptr);
// 메모리 할당 후, memory leak을 방지하기 위해 꼭 반환해주어야 한다.
C++ Style: new, delete Operator
- C++에서 제공하는 new 연산자는 데이터형을 지정해주면 컴파일러가 알아서 판단하여 적절한 크기의 메모리 블럭의 주소를 리턴한다. (malloc()은 데이터형과 메모리 크기를 명시해야 된다는 점에서 대비적이다.)
- delete 연산자는 사용한 메모리를 다시 메모리 풀로 환수하는 연산자이다. malloc()과 free()함수와 마찬가지로, new 연산자를 사용한 이후에는 해당 포인터 변수를 반드시 delete연산자를 통해 해제해야 한다. (반드시 new 연산자를 통해 할당받은 메모리만을 해제할 수 있다.)
- 이미 해제한 포인터를 다시 한 번 delete를 통해 해제할 경우 어떤 결과가 발생할지 모른다.
- delete 연산자는 NULL Pointer에 사용가능하다.
- 컴퓨터에서 사용할 수 있는 메모리가 부족하여 new의 메모리 대입 요청이 거부되는 경우가 있는데, 이 경우에 new는 0(NULL, NULL Pointer)을 리턴하거나 "bad_alloc" exception을 리턴할 수도 있다.
* NULL Pointer: 값이 0인 포인터를 지칭하는 용어이며 어떠한 데이터도 지시하지 않는 포인터라서 연산자나 함수에서 무언가 잘못되었을 경우에 리턴하는 값으로 이용된다.
* Data Object(데이터 객체): 어떤 데이터를 저장하기 위해 대입된 메모리 블럭, "변수"보다 더 일반적인 용어이다.
new, delete 연산자 사용법
// 일반형
typeName* pointerName = new typeName;
delete PointerName;
// 사용예시
int* ptr = new int;
...
delete ptr;
Ex. delete 연산자 응용 예시
int* ps = new int;
int* pq = ps;
delete pq;
- 위 코드처럼, delete연산자는 꼭 new 연산자를 통해 할당받은 변수를 통해서만 메모리를 해제할 수 있는 것이 아니라,
동일한 주솟값을 갖고 있는 포인로를 해제하는 것이 가능하다.
The Rules for \(\texttt{new, delete}\) (연산자 이용 규칙)
- new로 대입하지 않은 메모리는 delete로 해제하지 않는다.
- 같은 메모리 블럭을 연달아 두 번 delete로 해제하지 않는다.
- "new []"(동적배열을 의미함) 로 메모리를 대입한 경우에는 "delete []" 로 해제한다.
- new를 대괄호 없이 사용했으면 delete도 대괄호 없이 사용한다.
- NULL Pointer에는 delete를 사용해도 안전하다. (아무 일도 일어나지 않는다.)
- \(\texttt{new}\) 연산자 함수의 첫 번째 매개변수는 요청한 메모리의 크기(Byte 단위)를 지정하는 \(\texttt{std::size_t}\) 매개변수이며, 두 번째 매개변수는 저장시킬 메모리의 위치이며, 이것은 첫 번째 매개변수와 달리 선택요소이다.
\(\texttt{new}\)가 메모리 할당에 실패할 경우
- 필요한 메모리 양을 확보할 수 없는 경우, \(\texttt{new}\)를 통한 메모리 할당에 실패하게 된다.
- 과거 C++에서는 메모리 할당에 실패할 경우, \(\texttt{new}\)가 NULL 포인터를 리턴해서 메모리 할당에 실패함을 알렸다.
- 현재 C++에서는 \(\texttt{new}\)가 메모리 할당에 실패하면 \(\texttt{std::bad_alloc}\) Exception을 리턴하게 된다.
+ GPF (General Protection Fault; 일반 보호 오류)는 프로그램이 금지된 메모리 위치에 접근하려고 시도했을 때를 표시하는 오류코드이다.
Dynamic Variables with External Linkage (외부 링크를 갖는 동적 변수)
- 동적으로 메모리를 할당 받은 변수 또한 다른 변수들과 같이 전역변수로 선언한 다음, \(\texttt{extern}\) 키워드를 통해 외부 파일에서 사용이 가능하다.
Ex. 외부 링크 동적 변수 사용 예시
// File_1.cpp
float* p_fees = new float[20];
...
// File_2.cpp
extern float* p_fees;
...
The Placement \(\texttt{new}\) Operator (위치 지정 \(\texttt{new}\) 연산자)
- 할당받을 메모리의 영역을 사용자가 지정할 수 있게하는 \(\texttt{new}\) 연산자의 기능이다.
- 메모리 관리 절차를 설정하거나, 특정 주소를 통해 접근하는 H/W를 다루거나, 특정 메모리 위치에 있는 객체를 생성하는 데에 이 기능을 이용할 수 있다.
- \(\texttt{new}\) 헤더파일을 포함시켜야한다.
- 원하는 주솟값을 전달하는 매개변수와 함께 \(\texttt{new}\) 연산자를 사용할 수 있게 된다.
// Ex. 위치 지정 new의 일반형
dataType pointerName = new (address) dataType;
Ex. 위치 지정 \(\texttt{new}\) 연산자 사용 예시
#include <new>
struct chaff {
char dross[20];
int slag;
};
char buffer1[50]; // buffer1은 배열이름이자 그 자체로 주솟값을 의미한다.
int buffer2[500]; // buffer2는 배열이름이자 그 자체로 주솟값을 의미한다.
int main(){
chaff *p1, *p2;
int *p3, *p4;
// Normal "new" // 우리가 익히 아는 방식
p1 = new chaff;
p3 = new int[20];
// Placement "new"
p2 = new (buffer1) chaff; // chaff 구조체를 지정된 buffer1 주소에 놓는다.
p4 = new (buffer2) int[20]; // int 변수를 지정된 buffer2 주소에 지정하여 놓는다.
...
Ex. \(\texttt{new}\)의 다른 표현 방식 예시
// 여기서, buffer는 임의의 메모리 주솟값이다.
int* pi = new int; // new(sizeof(int))
int* pi = new(buffer) int; // new(sizeof(int), buffer)
int* pi = new(buffer) int [40]; // new(40 * sizeof(int), buffer)
- 표준배치 \(\texttt{new}\) 함수는 기본적으로 두 개의 매개변수를 필요로 하는데,
첫 번째 매개변수는 요청한 메모리의 크기(Byte 단위)를 지정하는 \(\texttt{std::size_t}\) 매개변수이며,
두 번째 매개변수는 저장시킬 메모리의 위치이며, 이것은 첫 번째 매개변수와 달리 선택요소이다.
※ 위치지정 \(\texttt{new}\) 클래스 객체에 적용할 때의 주의사항
- 사용자 정의 클래스 객체도 위치 지정 \(\texttt{new}\)를 통해 지정된 위치의 메모리를 할당할 수 있다.
- 일반적으로, 위치 지정시에 사용한 주솟값을 저장한 포인터 변수의 사용이 끝나고 \(\texttt{delete}\) 연산을 수행하게 되는데, \(\texttt{delete}\) 연산을 수행해도 그 포인터와 연계된 클래스 객체의 파괴자는 호출되지 않는다.
(이 때문에, 해당 객체들의 파괴자를 명시적으로 호출해야 한다.)
- 위치지정 \(\texttt{new}\) 연산자를 사용한 이상, 해당 메모리 위치를 관리하는 것을 프로그래머의 몫이다.
// Ex. 위치지정 new를 통한 메모리 할당작업 예시
E_Object *pc1, *pc2; // E_Object 는 임의의 사용자 정의 클래스 이름이다.
char* buffer = new char[512]; // 추후에, buffer는 할당할 메모리 주소의 대용이름으로 쓰인다.
pc1 = new (buffer) E_Object;
pc2 = new (buffer + sizeof(E_Object)) E_Object; // pc1이 차지하는 공간 뒤에 pc2를 저장한다.
pc2->~E_Object(); // 파괴자를 명시적으로 호출하여 처리한다.
pc1->~E_Object(); // 위치 지정 new가 생성한 객체들은 생성 순서의 역순으로 파괴되어야 한다.
// 후에 생성된 객체가 먼저 생성된 객체에 의존할 수 있기 때문이다.
delete [] buffer; // 이 구문이 실행되어도, pc1과 pc2에 생성된 E_Object 객체의 파괴자는 호출되지 않는다.
// 그렇기 때문에, 파괴자를 명시적으로 호출했던 것이다.