[C++] 가변 배열

2024. 3. 17. 14:39Programming Language/C++

동적 배열이라고도 하며, 메모리 공간을 계속 확장하면서 데이터를 넣는 형태의 자료구조이다.

• 가변 배열을 사용하려면 동적 할당 즉, 힙 영역을 사용해야 한다.

• 가변 배열을 잘 설계해 놓으면 여러 개 만들 수 있다.

▶ 정적 배열 vs. 가변 배열

  정적 배열(Static Array) 가변 배열(Dynamic Array, 동적 배열)
설명 배열의 크기가 고정되어있으며, 컴파일 시 크기가 결정된다. 배열의 크기를 조절할 수 있다.
프로그램 실행 중 메모리를 힙 영역에 할당하여 생성된다.
메모리
영역
스택 또는 데이터 영역에 저장된다. 힙 영역에 저장된다.
특징 한 번 크기가 결정되면 프로그램 실행 중 크기를 변경할 수 없다.
지역 변수로 선언된 정적 배열은 스택 영역에 저장된다.
static 키워드를 사용하거나 전역 변수로 선언된 배열은 데이터 영역에 저장된다.
배열의 크기를 런타임에 변경할 수 있다.
new 또는 malloc()을 사용하여 메모리를 할당하고, delete로 해제한다.
동적으로 할당된 메모리는 크기 변경이 불가능하지만, 다른 메모리를 재할당하는 등의 방법으로 크기를 조정할 수 있다.

▷ 정적 배열

void func() {
    int arr[10];  // 스택 영역에 저장된다.
}

static int staticArr[10];  // 데이터 영역에 저장된다.

int main()
{
	int iArr[10] = {};  // 스택 영역에 저장된다.

	return 0;
}

▷ 가변 배열

int main() {
    int size = 5;
    int* dynamicArray = new int[size]; // 동적 배열 할당 (힙 영역)
    
    // 필요에 따라 다른 크기로 재할당
    delete[] dynamicArray;
    size = 10;
    dynamicArray = new int[size]; // 새로운 크기로 다시 할당

    delete[] dynamicArray; // 메모리 해제
    
    return 0;
}

가변 배열 만드는 법

① 동적 배열의 초기화

• 동적 배열을 사용하기 위해 포인터를 선언하고, 필요한 크기의 메모리를 동적으로 할당한다.

구조체(가변 배열 자료형)의 경우 포인터(힙 영역의 시작 주소값), 현재 데이터 개수, 최대치를 멤버로 포함한다.

② 데이터 추가

• 배열에 데이터를 추가할 때 추가하기 전에 배열이 꽉 찼는지 확인하고, 배열의 현재 크기를 증가시키며 데이터를 추가한다.

③ 공간이 모자르면 재할당한다.

• 현재 배열의 크기가 최대라면, 더 큰 메모리 공간을 동적으로 할당한다.

보통은 기존 용량의 2배로 확장한다.

④ 데이터 복사 및 기존 메모리 해제

• 기존 배열의 데이터를 새 배열로 복사한 후, 기존 메모리를 해제한다.

⑤ 데이터 추가 완료 및 크기 변경

• 새 데이터를 추가하고 배열 크기를 변경한다.

⑥ 추가적인 데이터 추가 반복

• 이후 데이터 추가 시, 위 과정을 반복한다.

▷ 예시

#include <iostream>

using std::cout;
using std::endl;

int main() {
    int init = 2;  // 초기 배열 크기
    int* arr = new int[init];  // 가변 배열 초기화
    int size = 0;           // 현재 데이터 개수
    int capacity = init;  // 배열 용량

    // 데이터 삽입 함수 역할
    void insertData(int*& arr, int& size, int& capacity, int value) {
        if (size == capacity) {
            int newCapacity = capacity * 2;
            int* newArr = new int[newCapacity];
            for (int i = 0; i < size; ++i) {
                newArr[i] = arr[i];
            }
            delete[] arr;
            arr = newArr;
            capacity = newCapacity;
        }
        arr[size++] = value;
    }

    // 데이터 삽입 테스트
    insertData(1);
    insertData(2);
    insertData(3);  // 재할당 필요
    insertData(4);

    // 배열 데이터 출력
    cout << "배열 데이터";
    for (int i = 0; i < size; ++i) {
        cout << arr[i] << endl;
    }
    cout << endl;

    // 가변 배열 메모리 해제
    delete[] arr;

    return 0;
}

▽ 출력 결과

1
2
3
4
C++에서는 동적 배열보다 std::vector를 사용하는 것이 더 효율적이고 간단하다.
std::vector는 위 과정을 내부적으로 처리하고, 메모리 관리와 크기 확장을 자동화한다.

가변 배열 만들 수 있는 경우 (동적 할당 사용)

아래 예시들의 경우 배열 자체는 힙에 동적으로 할당되며, 차이점은 메모리 관리 방식이다.

▷ 지역 변수

void func() {
    int size = 10;
    int* dynamicArr = new int[size];  // 힙 영역에 동적으로 할당된 배열
    // 동적 배열 사용
    delete[] dynamicArr;  // 메모리 해제
}
// 포인터 dynamicArr는 지역 변수이지만, 배열 자체는 힙에 동적으로 할당되고 delete[]로 해제하기 전까지 유지된다.

 

▷ 전역 변수

int* globalArr;

void func() {
    int size = 10;
    globalArr = new int[size];  // 동적 배열 할당
    // 전역 배열 사용
}

int main() {
    func();
    delete[] globalArr;  // 메모리 해제
    
    return 0;
}
// 포인터 globalArr는 전역 변수이다.

 

▷ 정적 변수

void func() {
    static int* staticArr = nullptr;  // static 포인터
    int size = 10;
    if (staticArr == nullptr) {
        staticArr = new int[size];  // 한 번만 동적 할당
    }
    // static 배열 사용
    // 프로그램 종료 시 메모리 해제
}

int main() {
    func();
    delete[] staticArr;  // 한 번만 메모리 해제
    
    return 0;
}
// 포인터 staticArr는 정적 변수이고 함수 호출 간에 그 값을 유지하지만, 배열은 힙에 동적으로 할당된다.

 

▷ 외부 변수

ex1.cpp

extern int* externalArr;  // 외부 포인터 선언

void func() {
    int size = 10;
    externalArr = new int[size];  // 동적 배열 할당
}

ex2.cpp

int* externalArr;  // 외부 포인터 정의
// 포인터 externalArr는 외부 변수이다.

가변 배열을 만들 수 없는 경우

변수로는 가변 배열을 만들 수 없다.

멤버로 가지고 있는 배열의 크기가 정해져있지 않으면 컴파일러 입장에서 배열의 길이를 확신할 수 없다.

▷ 지역 변수

#include <stdio.h>

int main()
{
	// 예시 1
	int a = 100;  // 지역 변수 a : main() 함수 실행 시 생성되고 종료 시 해제된다.
    int iArr[a] = {};  // 오류 => 컴파일러 입장에서 배열의 길이를 확신할 수 없다.
    
    // 예시 2
    int b = 100;  // 지역 변수 b : main() 함수 실행 시 생성되고 종료 시 해제된다.
    
    int i = 0;
    scanf_s("%d", &b);
    
    int iArr[b] = {};  // 오류 => 컴파일러 입장에서 배열의 길이를 확신할 수 없다.
    
    return 0;
}

 

▷ 전역 변수

전역 변수는 런타임 중에 값이 변경될 수 있다.

따라서 전역 변수로 가변 배열을 만들려고 하면 구조체의 크기가 확정적이지 않고 컴파일러 입장에서도 정보가 명확하지 않기 때문에 오류가 발생한다.

// int는 4byte로 고정이다.
int gI = 100;

typedef struct _tagST
{
	//  int형으로 선언한 배열의 길이는 gI이지만, 그 크기가 확정적이지 않다.
	int iArr[gI];  // 오류
} ST;

 

▷ 정적 변수

#include <stdio.h>

int main() {
    static int b = 100;  // static 변수 b

    int iArr[b] = {};  // 오류 => 배열 크기를 결정할 수 없다.
    return 0;
}

b값이 런타임에 결정되기 때문에 컴파일 시 배열의 크기를 알 수 없어서 오류가 발생한다.