배열은 서로 타입이 같은 원소들을 변수 하나에 담아서 각 원소를 인덱스로 접근하게 해준다.
1. 기본 타입 배열
배열에 대한 메모리를 할당하면 실제 메모리에서도 연속된 공간을 할당한다.
이때 메모리의 한 칸은 배열의 한 원소를 담을 수 있는 크기로 할당된다.
예를 들어 다섯 개의 int 값으로 구성된 배열을 다음과 같이 로컬 변수로 선언하면 스택에 메모리가 할당된다
int myArray[5];
다음은 이 배열을 생성한 직후의 메모리 상태를 보여준다.
이때 스택에 생성한 배열의 크기가 컴파일 시간에 결정되도록 상숫값으로 지정해야 한다.
배열을 힙에 선언할 때도 비슷하다.
배열의 위치를 가리키는 포인터를 사용한다는 점만 다르다.
다음은 int 값 다섯 개를 담는 배열에 메모리를 할당해서 그 공간을 가리키는 포인터를 myArrayPtr란 변수에 저장한다.
int* myArrayPtr = new int[5];
힙에 저장한 배열은 원소가 저장된 위치만 다를 뿐 스택에 저장한 배열과 거의 같다.
myArrayPtr 변수는 배열의 0번째 원소를 가리킨다.
new[]를 호출한 횟수만큼 delete[]를 호출해서 배열에 할당했던 메모리를 해제하도록 코드를 작성해야 한다.
delete[] myArrayPtr;
myArrayPtr = nullptr
배열을 힙에 할당하면 실행 시간에 크기를 정할 수 있다는 장점이 있다.
예를 들어 다음 코드는 함수로부터 받은 문서 수 만큼의 크기를 가진 Document 객체 배열을 생성한다.
Document* createDocArray()
{
size_t numDocs = askUserForNumberOfDocuments();
Document* docArray = new Document[numDocs];
return docArray;
}
명심할 점은 new[]를 호출한 만큼 delete[]도 호출해야 한다.
따라서 createDocArray()를 호출한 측에서 배열을 다 쓰고 나면 delete[]를 호출해서 메모리를 해제해야 한다.
C 스타일 배열을 사용할 때 발생할 수 있는 다른 문제는 정확한 크기를 알 수 없다.
따라서 리턴된 배열에 담긴 원소 수를 createDocArray()를 호출한 측에서 알 수 없다.
위 코드에서 docArray는 동적으로 할당된 배열이다.
이는 동적 배열과는 다른다.
배열 자체는 한 번 할당되면 크기가 변하지 않기 때문에 동적이라 볼 수 없다.
단지 할당할 블록의 크기를 실행 시간에 지정할 수 있을 뿐이다.
따라서 더 많은 데이터를 추가하도록 크기를 알아서 조절할 수는 없다.
2. 객체 배열
객체에 대한 배열도 기본 타입 배열과 비슷하다.
new[]를 호출하면 객체마다 제로 인수(디폴트) 생성자가 호출된다.
그래서 new[]로 객체 배열을 할당하면 형식에 맞게 초기화된 객체 배열을 가리키는 포인터가 리턴된다.
class Simple
{
public:
Simple() { cout << "Simple constructor called!" << endl; }
~Simple() { cout << "Simple destructor called!" << endl; }
};
네 개의 Simple 객체로 구성된 배열을 할당하면 위 Simple 생성자가 네 번 호출된다.
Simple* mySimpleArray = new Simple[4];
다음은 위 배열의 메모리 상태를 표현한 것이다.
기본 타입 배열과 다르지 않다는 것을 알 수 있다.
3. 배열 삭제하기
배열에 대한 메모리를 new[]로 할당하면 반드시 new[]를 호출한 만큼 delete[]를 호출해서 메모리를 해제해야 한다.
delete[]를 호출하면 할당된 메모리를 해제할 뿐만 아니라 각 원소의 객체마다 소멸자를 호출한다.
Simple* mySimpleArray = new Simple[4];
// mySimpleArray 사용
delete[] mySimpleArray;
mySimpleArray = nullptr;
배열 버전의 delete인 delete[]를 사용하지 않으면 프로그램이 이상하게 동작할 수 있다.
객체를 가리키는 포인터만 삭제한다고 여기고 배열의 첫 번째 원소에 대한 소멸자만 호출하는데,
그러면 배열의 나머지 원소에 접근할 수 없게 된다. new[]에 대한 메모리 할당 방식이 서로 달라 메모리 손상이 발생하기도 한다.
배열의 원소가 객체일 때만 모든 원소에 대해 소멸자가 호출된다.
포인터 배열에 대해 delete[]를 호출할 때는 각 원소가 가리키는 객체를 일일이 해제해야 한다.
const size_t size = 4;
Simple** mySimplePtrArray = new Simple * [size];
// 포인터마다 객체를 할당한다.
for (size_t i = 0; i < size; i++) {
mySimplePtrArray[i] = new Simple();
}
// mySimplePtrArray 사용
// 할당된 객체를 삭제한다.
for (size_t i = 0; i < size; i++) {
delete mySimplePtrArray[i];
}
// 배열을 삭제한다.
delete[] mySimplePtrArray;
mySimplePtrArray = nullptr;
'💻프로그래밍 내용 정리 > C++17' 카테고리의 다른 글
[C++ 7.1.4] 포인터 다루기 (1) | 2022.08.22 |
---|---|
[C++ 7.1.3-2] 다차원 배열 (0) | 2022.08.22 |
[C++ 7.1.2] 메모리 할당과 해제(new, delete / malloc(), free()), nothrow (0) | 2022.08.22 |
[C++ 7.1.1] 동적 메모리 작동 과정 살펴보기 (0) | 2022.08.22 |
[C++ 2.1.4] std::string_view 클래스 (1) | 2022.08.20 |