[C언어] 2차원 배열의 크기를 입력받아서 정하기 (2차원 배열의 동적할당)
C언어로 배열을 다루다 보면 곤란한 상황에 놓일 때가 있습니다.
배열의 크기를 미리 지정해두고 실행하는 것이 아니라 프로그램을 실행한 후 사용자의 입력에 따라 배열의 크기를 정하고 싶을 경우에 아래처럼 실행하게 됩니다.
int A, B;
scanf("%d %d",&A,&B);
int Arr[A][B];
프로그램을 실행시켜보신 분들은 알겠지만 , 오류가 나신다는 것을 알 수 있습니다.
왜냐면 C언어에서 배열의 크기는 변수로 받아 지정할 수 없기 때문입니다.
그럼 프로그램을 실행시킨 뒤 사용자의 입력에 따라 , 그리고 경우에 따라 배열의 크기를 입력받은 크기대로 만들고 싶다면 어떻게 해야 할까요?
포인터와 malloc을 이용해야 합니다.
malloc 이란 함수는 메모리를 프로그램 내에서 크기만큼 할당해주고 , 포인터는 이 할당된 공간을 가리키는 도구입니다.
이를 C언어에서 동적 할당이라고 부릅니다.
우선 1차원 배열을 예시로 들어보겠습니다.
일차원 배열을 변수의 크기만큼 공간을 만들어 선언을 하고 싶다면 아래처럼 해야 합니다.
int N;
int *Arr = (int*)malloc(sizeof(int)*N);
우선 코드의 의미부터 해석해보겠습니다.
*Arr이란 변수는 int 타입 포인터 변수라는 것이고 포인터 변수는 특정 메모리의 주소를 담고 있는 변수입니다.
또한 말록 앞에는 (int*) 인트 포인터임을 통일시키고 malloc () <- 이 괄호 안의 부분은 정수형 값이 담기면 그 정수 X byte만큼의 공간이 할당된다는 의미입니다. 따라서 sizeof(int) * N Byte
즉 int형 변수의 크기인 4바이트 * N 개만큼의 크기가 할당되는 것이죠.
아래 그림으로 보겠습니다.
왼쪽의 1칸짜리 공간은 Arr이라는 포인터 변수가 담긴 포인터 변수의 메모리 공간입니다.
그 포인터의 메모리 공간이 가진 값이 N칸짜리 메모리의 시작 주소를 담고 있다고 보시면 되고,
Arr이라는 변수가 메모리 공간의 시작 주소를 담고 있다 보니 이 주소를 이용하여 저 공간을 다룰 수 있다고 보시면 됩니다.
그림은 4칸이지만 N의 입력에 따라서 10칸이 될 수도 , 100칸이 될 수도 있다고 보시면 됩니다.
그리고 포인터 변수라는 것을 짚고 넘어가 보겠습니다.
포인터 변수는 여러 가지 특징을 가지고 있지만 포인터 변수를 배열처럼 다룰 수 있습니다.
예를 들면 Arr [0], Arr [1] ~~ Arr [N-1]으로 위의 메모리 공간을 하나의 배열처럼 다룰 수 있습니다.
이게 어떻게 가능한 것인지, 무슨 원리인지 보겠습니다.
int 타입 변수는 한 칸에 4바이트의 공간을 가지고 있습니다.
위의 N칸짜리 메모리 공간의 첫 시작 주소를 1000이라고 가정해보겠습니다.
그럼 2번째 칸은 1004 , 3번째 칸은 1008의 주소를 가지고 있습니다.
Arr [0]이라고 하면 시작 주소 + (0 x 4) = 1000
Arr [1]이라고 하면 시작 주소 + (1 x 4) = 1004
Arr [N-1]이라고 하면 시작 주소 + ( (N-1) x 4) = 1000 +4*N -4
의 주소 값을 가지는 원리라고 보시면 됩니다
Arr의 인덱스가 늘 때마다 주소 값이 4바이트씩 증가하게 되는데
이는 선언한 포인터 변수의 타입이 int 형이기 때문입니다.
또 Arr++; 를 하게 되면 1001 이아니라 1004
Arr = Arr +1; 을 하게되면 마찬가지로 1001 이아니라 1004 가 됩니다.
포인터 변수와 메모리는 대충 이 정도로 다루기로 하고 추가 자료는 각자 추가로 찾아보시거나 질문 남겨주시기 바랍니다.
그러면 우리는 이 N 칸짜리 메모리 공간을 할당하고 배열처럼 다룰 수 있기 때문에
1차원 배열의 동적 할당을 하는 방법을 배운 것입니다.
그러면 2차원 배열의 경우는 어떻게 될까요?
예를 들어 A x B 칸짜리의 배열을 입력받는 크기대로 생성하고 싶다고 가정해봅시다.
아까 일차원 배열에서는 N 칸 짜리 배열을 할당하기 위해선 N 칸짜리 메모리 공간 + 그 N칸짜리 메모리를 가리키는 포인터 변수의 메모리 공간 이 필요했습니다
A x B 배열은
B칸짜리 가 A 개가 있는 배열입니다.
그럼 B칸짜리 배열을 A 번 동적 할당하고, 그 배열을 가리키는 포인터 변수가 A 개가 있어야 할 것입니다.
근데 포인터 변수 A 개를 각각 따로 선언하여 이름을 불러 다루게된다면 그것은 2차원 배열이 아니라 1차원 배열이 A 개 있는 것입니다.
따라서 A 개의 포인터도 각각의 변수로 다루는것이아니라 연속된 메모리를 할당하여 포인터들을 저장하고 이를 배열처럼 다루어야 합니다.
그럼 이 메모리 공간은 각각 인트 포인터의 크기만큼 이 되어야 하고 , 칸수는 A칸이 됩니다.
그럼 이 포인터들을 가리키는 포인터가 또 있어야겠죠?
이것을 포인터의 포인터 , 이중 포인터라고 합니다.
위에서 int **Arr라고 선언이 되어있는데
Arr[0] <- 0번 인덱스에 들어있는 포인터
Arr[1] <- 1번 인덱스에 들어있는 포인터
Arr[A-1] <- A-1 번째 인덱스에 들어있는 포인터
그럼 각각의 포인터가 이름을 갖고 , 이 이름을 각 각 불러야 하는 불상사를 막을 수 있습니다.
덤으로 Arr[i] 번 포인터가 가리키고 있는 메모리 공간의 j 번째 요소가 가진 실제 값을 다루는 것은
Arr [i][j]로 다루어 2차원 배열처럼 다룰 수 있는 것이지요.
선언은 순서대로 코드를 보여드리겠습니다.
int **Arr = (int **)malloc (sizeof(int*)*A); //A칸짜리 인트포인터를 담는 배열 생성
for (int i=0;i<A;i++){
Arr[i] = (int *)malloc (sizeof(int)*B); // B칸짜리 인트공간을 담는 배열 생성 여기서 Arr[i]는포인터를 담은 배열의 i번째칸을 의미하고 이는 포인터기때문에 다시 B칸짜리 배열을 생성하고 가리키게한다.
}
왼쪽부터 포인터를 가리키는 더블 포인터를 담는 메모리 그리고 A칸짜리 메모리에 각 각 B칸짜리 메모리를 가리키는 포인터가 들어가 잇습니다.
오른쪽에 B칸짜리 메모리 A 개가 실제 데이터가 닮기는 int 형 공간이라고 보시면 됩니다.
또 int **Arr = (int **) malloc (sizeof(int*)*A); 여기 보시면 sizeof(int*) * A는 int포인터를 A 개만큼 담을 공간을 할당한다는 개념입니다.
일차원 배열 때와 마찬가지로 Arr [i][j]로 2차원 배열처럼 다룰 수 있으며
Arr [i]로 사용하면 i번째 포인터를 가리키게 됩니다.
이 원리를 응용하여 다차원 배열 또는 다차원 포인터를 이해하실 수 있으니 질문 있으시면 질문 댓글로 남겨주시기 바랍니다.