[자료구조 A to Z] 배열 (Arrays)

Lee Jae-Baek·2023년 6월 18일
0

자료구조 A to Z

목록 보기
2/16
post-thumbnail

배열이라는 자료구조

가장 먼저 만나볼 자료구조는 다름아닌 배열(Array)이다.

배열은 C++을 비롯한 프로그래밍 언어를 공부해본 적이 있다면 모두가 한 번쯤 접해봤을 것이다.

그만큼 가장 보편적인 자료구조이다.

그러나 처음 접할 경우 초심자를 당황시키기도 한다. 특히 첫 번째 인덱스가 0으로 시작하는 것은 작년의 나에게 신선한 충격이었다. 배열에 익숙해진 뒤 부터는 첫 시작이 0이 아닌 1일때 더 불편함을 느끼게 되었다.

C도, C++도, 자바도, 자바스크립트도 형태는 약간씩 다르지만 배열을 가지고 있다. 아래는 C++의 간단한 정수형 배열 예시이다.

int main() {
	int arr1[3] = {1, 2, 3}; // 사이즈(3)와 초기화를 동시에
    int arr2[] = {1, 2, 3}; // 사이즈가 자동으로 3이 됨
    int arr3[3]; // 사이즈 3짜리 배열을 선언. 전역에서 선언시 자동으로 0으로 초기화. main에선 x.
    arr3[0] = 1;
    arr3[1] = 2;
    arr3[3] = 3; // 선언된 배열에 1, 2, 3을 대입
}


그럼 이렇게 연속된 1차원 메모리 공간에 정수 값들이 들어가게 된다.
대부분의 운영체제에서 정수(int) 자료형은 4byte를 차지하기 때문에 메모리에서도 각각 4byte씩 할당되는 것을 확인할 수 있다.

* 실제로 int타입이 차지하는 공간은 OS의 bit 체계에 따라 가변적이다. 상세는 아래 게시글에서 다룬다.
https://velog.io/@thinkingvincent/int%ED%83%80%EC%9E%85%EC%9D%80-4byte

정적 배열

int main() {
	int a = 3;
    int arr[a] = {1, 2, 3}; // 에러! 변수를 사이즈로 지정할 수 없음.
    
    const int b; // 에러! 상수는 반드시 선언과 함께 초기화 되어야 함.
	b = 3; // 상수를 변경할 수 없음. 물론 이미 위에서 에러 상태.
	int arr[b] = { 1, 2, 3 }; // 에러
    
    const int c = 3; // 상수 선언과 함께 초기화
    // c = 4 상수는 재정의가 불가능하므로 주석처리
    int arr[c] = { 1, 2, 3 }; // 성공!
}

그러나 지금까지 살펴 본 방식은 "정적(static)"이다. 위 코드처럼 반드시 컴파일 단계(콘솔창을 띄우기 전에)에서 배열의 사이즈가 정해져야하고, 사이즈로는 변수가 들어갈 수 없다. 즉 사이즈가 변경될 여지가 없다. 미리 확실하게 해줘야 컴파일러가 메모리에 배열 만큼의 공간을 할당하기 때문이다.

배열의 size를 컴파일 이후, 즉 런타임에, 사용자가 직접 콘솔 창에 입력하여 동적으로 정할 수 있게 하면 얼마나 좋을까? 필요에 따라 사이즈를 늘리거나 줄일 수도 있다면?

동적 배열과 동적 할당

이러한 질문에 답하고 보다 유동적으로 배열을 활용하기 위해 고안된 것이 동적 배열 (Dynamic Array)이다.
이는 포인터를 활용한 동적 메모리 할당 덕에 가능해진다.
동적 배열은 사용자가 직접 런타임에 배열의 크기를 늘렸다 줄였다 할 수 있다.

동적 배열에 대한 구체적인 내용은 챕터 7에서 벡터를 설명하며 함께 다루도록 하고, 여기서는 기초적인 동적 할당 부분만 짚고 넘어가겠다.

int main() {
	int* arr; // int타입을 가리키는 포인터 변수 arr 생성. 초기화 하지 않으면 쓰레기 값을 가리킨다.
    
    int size; // 배열의 사이즈를 받을 정수
    cin >> size; // 사이즈를 런타임에 사용자에게 입력받음.
    
    arr = new int[size];
    // 임의의 힙 공간 메모리에 입력받은 사이즈 만큼의 배열을 할당하고, arr 포인터가 가리키게 만듦.
    
    for (int i = 0; i < size; ++i) {
    	arr[i] = i + 1;
        cout << arr[i] << ' ';
    } // 각 요소를 1 ~ size로 초기화
    
 	delete[] arr; //힙 공간에 할당된 메모리를 해제
}

다들 포인터 개념에 대해 공부를 하고 왔다면 이해가 쉬울 것이다. arr는 int 타입 메모리 주소를 저장할 수 있는 변수이다. 이름 그대로 int타입의 임의의 주소를 "가리킨다"고 생각하면 더 이해가 쉽다.

그림과 같이,

  • int타입을 가리킬 수 있는 포인터 변수를 하나 만들고, 실행자로부터 size를 런타임에 입력받는다.
  • 이후 해당 사이즈만큼 차지하는 배열을 힙 메모리에 할당하고,
  • for문을 사용하여 내부 값을 초기화한다.
  • 배열의 쓸모가 다 하면 마지막으로 힙공간에 할당된 메모리를 delete를 통해 반환해준다.

이런 프로세스를 통해 배열의 사이즈를 런타임에 실행자가 지정할 수 있게 된다. 그리고 런타임에 입력을 받아 동적으로 배열을 힙공간에 할당하기 때문에, 이러한 형식을 "동적 메모리 할당(Dynamic Memory Allocation)"이라 부르는 것이다.

여기까지가 배열을 다룰때 필요한 가장 기본적인 부분이다.

다음 챕터에서는 배열과 가장 많이 비교되는 자료구조, 링크드 리스트를 만나보자.

정확한 정보를 전달하기 위해 최선을 다하고 있지만, 그럼에도 부족함을 알고 있습니다. 언제나 피드백을 기다리고 있으니 잘 부탁드립니다!
감사합니다 :)

profile
코드를 예술로 볼 수 있는가?

0개의 댓글