모노산달로스의 행보

[C programming] 다차원 배열(2차원 배열)의 정의, 선언, 초기화, 주소와 값의 참조 본문

ProgrammingLanguage/C

[C programming] 다차원 배열(2차원 배열)의 정의, 선언, 초기화, 주소와 값의 참조

모노산달로스 2024. 4. 4. 10:52

C programming - 2차원 배열

 

리눅스 환경에서 네트워크 프로그래밍을 공부하기 위해서 C언어를 다시 복습해야 할 필요성을 느꼈습니다. 따라서 이번 기회에 배열부터 전처리기까지 내용들을 정리하겠습니다.

 

 


다차원 배열이란?

출처: https://techvidvan.com/tutorials/multidimensional-arrays-in-c/

 

다차원 배열은 2차원 이상의 배열을 일컫는 용어입니다. 즉, 하나의 배열이 다른 배열을 가지고 있는 형태입니다. 처음 다차원 배열을 접하신다면 이해하기 힘든 개념으로 생각됩니다만, 이번 기회에 확실하게 정리하고 넘어가면 좋을 것 같습니다.

 

해당 포스트에서는 2차원 배열을 중심으로 살펴보겠습니다.

 


 

2차원 배열의 선언과 초기화

int array[4][3];

 

기존 배열과 선언 방법은 크게 다르지 않습니다. 자료형배열의 이름 그리고 배열의 크기를 선언합니다. 여기서 주목할 점은 배열의 크기 부분이 행(Row)열(Column)으로 나누어져 있습니다. 해당 내용을 예제 코드를 통해서 조금 더 자세히 알아보겠습니다.

 

#include <stdio.h>
int main(void)
{
    int array[4][3];

    array[0][0] = 1; array[0][1] = 2; array[0][2] = 3;
    array[1][0] = 4; array[1][1] = 5; array[1][2] = 6;
    array[2][0] = 7; array[2][1] = 8; array[2][2] = 9;
    array[3][0] = 10; array[3][1] = 11; array[3][2] = 12;

    printf("%2d %2d %2d \n",array[0][0], array[0][1], array[0][2]);
    printf("%2d %2d %2d \n",array[1][0], array[1][1], array[1][2]);
    printf("%2d %2d %2d \n",array[2][0], array[2][1], array[2][2]);
    printf("%2d %2d %2d \n",array[3][0], array[3][1], array[3][2]);
    
    return 0;
}

예제 코드의 출력 결과

 

해당 코드는 2차원 배열을 선언한 다음 상수를 하나하나 초기화하는 방법을 사용하고 있습니다. 마치 좌표에 있는 요소를 찾아가는 것처럼 2차원 배열의 index가 사용되고 있습니다. 예를 들어 array[2][1][2]번째 Row[1]번째 Column에 위치하는 요소인 8을 의미하는 것입니다.

 

이번에는 앞서 사용된 초기화 방법을 사용하지 않고 배열을 초기화하겠습니다. 2차원 배열 또한 1차원 배열과 마찬가지로 선언과 동시에 초기화할 수 있습니다. 

#include <stdio.h>
int main(void)
{
    int array[4][3] = {1, 2, 3, 4, 5};

    printf("%2d %2d %2d \n",array[0][0], array[0][1], array[0][2]);
    printf("%2d %2d %2d \n",array[1][0], array[1][1], array[1][2]);
    printf("%2d %2d %2d \n",array[2][0], array[2][1], array[2][2]);
    printf("%2d %2d %2d \n",array[3][0], array[3][1], array[3][2]);
    
    return 0;
}

예제 코드의 출력 결과

 

1차원 배열과 마찬가지로 배열이 index 순서대로 초기화되는 모습을 확인 가능합니다. 그리고 명시적으로 초기화하지 않은 부분은 0으로 초기화된 것 또한 확인할 수 있습니다.

 

그런데 위와 같은 방법을 2차원 배열에서는 행 단위로도 적용이 가능합니다.

#include <stdio.h>
int main(void)
{
    int array[4][3] = {{1, 2}, {3}, {4}, {5, 6, 7}};

    printf("%2d %2d %2d \n",array[0][0], array[0][1], array[0][2]);
    printf("%2d %2d %2d \n",array[1][0], array[1][1], array[1][2]);
    printf("%2d %2d %2d \n",array[2][0], array[2][1], array[2][2]);
    printf("%2d %2d %2d \n",array[3][0], array[3][1], array[3][2]);
    
    return 0;
}

예제 코드의 실행 결과

 

2차원 배열을 초기화하는 중괄호 { } 내부에 새로운 중괄호 { }를 표기하여 행을 구분하는 것이 가능합니다. 새로운 괄호가 열릴 때마다 다음 행으로 넘어가는 모습입니다.

 

int array1[][] = {1, 2, 3, 4, 5, 6}; // error
int array2[3][3] = {1, 2, 3, 4, 5, 6}; // error
int array3[][3] = {1, 2, 3, 4, 5, 6};
int array4[][2] = {1, 2, 3, 4, 5, 6};
int array5[][1] = {1, 2, 3, 4, 5, 6};

 

마지막으로, 2차원 배열을 선언하는 경우 반드시 열의 길이(Column length)를 지정해야 합니다. 컴파일러가 열의 길이를 기반으로 행의 길이를 추정하는데, 그 이유는 C언어에서 다차원 배열을 메모리에 연속적으로 할당하기 때문입니다. 그렇다면 행의 길이(Row length)를 기반으로는 왜 열의 길이를 추정할 수 없을까요?

 

2차원 배열의 물리적 메모리 구조를 통해서 해답을 쉽게 얻을 수 있습니다.

2차원 배열의 물리적 메모리 구조

 

위 그림을 통해 메모리에 연속적으로 값을 할당한다는 의미를 확인할 수 있습니다. 열의 길이만 주어진 경우 해당하는 길이만큼 값을 할당하고 다음 행으로 넘어가면 됩니다. 만약 행의 길이만 주어진 경우 컴파일러가 열의 길이를 추정할 방법이 없습니다.

 


 

2차원 배열의 주소 참조

#include <stdio.h>
int main(void)
{
    int array[2][3]={1, 2, 3, 4, 5, 6};
    printf("%x %x %x\n", &array[0][0], &array[0][1], &array[0][2]);
    printf("%x %x %x\n", &array[1][0], &array[1][1], &array[2][2]);
    return 0;
}

 

1차원 배열과 마찬가지로 2차원 배열의 주소는 &연산자를 통해 참조할 수 있습니다. 주소 값의 변화를 통해 메모리에 값이 연속적으로 할당되는 것을 다시 한번 확인 가능합니다.

 

2차원 배열의 주소 참조에는 두드러지는 세 가지 특징이 존재합니다.

 

2차원 배열의 이름은 2차원 배열의 시작 주소이다.

#include <stdio.h>
int main(void)
{
    int array[2][2]={1, 2, 3, 4};
    printf("%x %x\n", array, array+0);
    printf("%x \n", array+1);
    return 0;
}

예제 코드의 출력 결과

 

2차원 배열의 이름으로 주소를 참조할 수 있습니다. 1차원 배열과 동일하지만 array+1의 주소를 출력하면 1행의 주소가 참조되는 것이 확인됩니다.

 

2차원 배열의 행의 요소는 행을 대표하는 주소이다.

#include <stdio.h>
int main(void)
{
    int array[2][2]={1, 2, 3, 4};
    printf("%x %x\n", array[0], &array[0][0]);
    printf("%x %x \n", array[1], &array[1][0]);
    return 0;
}

예제 코드의 출력 결과

 

비슷한 예제로 2차원 배열의 행의 주소를 출력해 보았습니다. 그 결과 각 행의 첫 번째 열의 주소 값과 동일하게 출력되는 것이 확인됩니다.

 

2차원 배열에서 array[i] == *(array+i)는 주소이다.

#include <stdio.h>
int main(void)
{
    int array[2][2]={1, 2, 3, 4};
    printf("%x %x %x\n", array[0], *(array+0), *array);
    printf("%x %x \n\n", array[1], *(array+1));

    printf("%x %x \n", *(array+0)+0, *(array+0)+1);
    printf("%x %x \n", *(array+1)+0, *(array+1)+1);
    return 0;
}

예제 코드의 출력 결과

 

*연산자는 기존에 값을 참조하기 위해 사용되었기 때문에 헷갈릴 수 있는 부분이 있습니다. 2차원 배열에서 *(array+i)와 같이 사용되는 경우 주소를 가리키게 됩니다.

 


2차원 배열의 값 참조

물론 2차원 배열에서 *연산자를 값의 참조에도 사용하는 것은 동일합니다.

#include <stdio.h>
int main(void)
{
    int array[2][2]={10, 20, 30, 40};

    printf("%d %d \n", *&array[0][0], *&array[0][1]);
    printf("%d %d \n\n", *&array[1][0], *&array[1][1]);

    printf("%d %d \n", *array[0]+0, *array[0]+1);
    printf("%d %d \n\n", *array[1]+0, *array[1]+1);

    printf("%d %d \n", **(array+0)+0, **(array+0)+1);
    printf("%d %d \n", **(array+1)+0, **(array+1)+1);
    return 0;
}

예제 코드의 출력 결과

*&array[i][j] 와 같은 표현 방식은 정확하게 값을 출력하고 있습니다. &array[0][0]에 의해서 배열의 0번째 행의 0번째 열의 값의 주소가 반환됩니다. 해당 주소 값에 *연산자를 사용하면 주소가 가리키는 값을 참조하게 됩니다.

 

반면 *array[i]+j 와 같이 표현하면 엉뚱한 값이 출력됩니다. *연산자가 array[0]가 가리키는 주소의 값인 배열의 시작 주소를 반환합니다. 이후 상수가 더해지면서 주소 값의 변화가 아닌 정수형 값의 변화가 일어나게 됩니다.

 

**(array+i)+j 또한 위와 같은 흐름으로 출력이 진행됩니다. *(array+i)의 주소만을 먼저 값으로 반환했기 때문에 정수형 값 사이의 연산이 일어나게 됩니다.

 

위와 같은 상황을 해결해주기 위해서는 *(array[0] + 0) 혹은 *(*(array + 0) + 0)와 같이 소괄호를 한 번 더 사용해 주어야 합니다.

 

#include <stdio.h>
int main(void)
{
    int array[2][2]={10, 20, 30, 40};

    printf("%d %d \n", *&array[0][0], *&array[0][1]);
    printf("%d %d \n\n", *&array[1][0], *&array[1][1]);

    printf("%d %d \n", *(array[0]+0), *(array[0]+1));
    printf("%d %d \n\n", *(array[1]+0), *(array[1]+1));

    printf("%d %d \n", *(*(array+0)+0), *(*(array+0)+1));
    printf("%d %d \n\n", *(*(array+1)+0), *(*(array+1)+1));

    printf("%d %d \n", **((array+0)+0), **((array+0)+1));	// 잘못된 표현 방식
    printf("%d %d \n", **((array+1)+0), **((array+1)+1));	// 잘못된 표현 방식
    return 0;
}

예제 코드의 출력 결과

 

소괄호를 통해서 출력하려는 값의 주소를 정확하게 참조할 수 있습니다. 가장 아래와 같은 표현 방식은 잘못된 결과를 출력하는데, 내부의 소괄호가 unnecessary 한 상태가 되어 **((array+0)+1)**(array+0+1)과 같이 표현되기 때문입니다.


이로서 배열에 관한 내용을 모두 정리했습니다. 다음으로는 C언어의 꽃인 포인터에 관한 내용으로 넘어가겠습니다.