본문 바로가기

Data Structure [C]/문돌이도 할 수 있는 [C언어 자료구조]

#12 [C 자료구조] 2차원 배열: 행렬이 된 배열

이전에 1차원 배열을 했을 때 직감했겠지만 불길한 느낌은 왜... 1차원 배열이 있으면 2,3,n 차 배열도 있을 것이다. 오늘은 다차원 배열에 대해서 간략하게 배워 보도록 하자.

 

알다시피 2차원은 행렬과 비슷하게 표기한다.

int 배열이름[행 개수][열 개수] 

int abc[3][2] = {0,1,10,11,21,22,31,32} // or {{0,1},{10,11}, ~ }} 이렇게 표현할 수 도 있음

 

초기화 방법

int abc[][2] --> 2열 초기화

int abc[3][] --> 3행 초기화

int abc[3][2] = {0}, --> 전체행 초기화

 

그렇다면!!!

배열에서의 포인터는 어떻게 쓰일까?

배열 역시 포인터를 가질 수 있다. 예를 들어 num[3]의 1차원 배열의 주소는 첫 번째 배열 요소의 주소 값을 가진다.

다음부터는 4byte씩 건너 뛰며 순서대로 주소를 할당받는다.

여기에서 값 30이 752의 주소를 가졌다면 40은 756, 50은 760을 갖게 된다.

*(num+1)은 num[1]을 가리킨다! +1이 정확이 4byte를 의미하기 때문에 주소 값+1은 다음 원소를 가리킨다.

 

 

포인터 배열과 배열 포인터

포인터 배열과 배열 포인터! 듣기만 해도 난해한 이 두 가지는 어떻게 구분하고 왜 써야 할까.

영어로 풀어보면 좀 더 쉬워질 수 있다.

 

배열: arr[3] = {1,2,3}

배열을 가리키는 포인터: *ap

 

- 포인터 배열(Array of Pointers)은 말 그대로 포인터들을 모아둔 배열! 각 원소 하나하나가 포인터가 되는 배열이다.

> int *ap[3] - 3개의 포인터 원소를 가지는 배열 ap

 

- 배열 포인터(Pointer to Array)는 배열 각 부분을 가리키는 포인터로써, 2차원 이상일 때 주로 사용한다.

> int (*ap)[3] - *ap 배열이 3개의 행을 가리킨다. 자세한 설명은 뒤에서!

 

포인터 배열

포인터 배열은 단순히 포인터를 병렬적으로 모아둔 것으로 그들이 표시하는 값들은 어떠한 규칙이 없다. 사용자가 스스로 규칙을 만들어놓지 않는 한!!

#include <stdio.h>

int main(void) {
    int arr[3] = {1,2,3};
    int num = 5;
    int *ap[2];
    
    ap[0] = arr;
    ap[1] = &num;
    
    printf("%d %d\n", ap[0], ap[1]);
    printf("%d %d\n", *ap[0], *ap[1]);
    printf("%d %d %d\n", ap[0][0], ap[0][1], ap[0][2]);
    printf("%d %d %d\n", *ap[0], *(ap[0]+1), *(ap[0]+2));
    return 0;
}

여기서 답은 

주소a, 주소a+4

1 5

1 2 3

1 2 3

이렇게 나온다. 이는

위와 같은 규칙을 따른 결과이다.

ap[0]은 단순 주소 값이지만 ap[0][0] 이렇게 배열 내부를 한차례 더 짚어주게 되면 실제 값을 출력한다.

이렇게 포인터 별로 가변적인 변수를 선택할 수 있다.

 

래그드 배열(Ragged Array) *Ragged: 울퉁불퉁한

포인터 배열 중에는 각 행의 길이가 위와 같이 가변적인 것들이 있다.

이 중 문자열 상수를 저장하는 방법을 래그드 배열이라 한다.

2차원 배열에 비해 효율적으로 문자열을 저장한다.

문자열을 강조하는 것은 int는 그냥 2차원이 편하기 때문이다ㅎㅎ

 

다음은 길이가 다른 4개의 과일을 각각 출력하는 방식을 포인터로 구현한 것이다.

#include <stdio.h>

int main(void){
    char *fruits[4] = {
        "apple",
        "blueberry",
        "orange",
        "melon",
    };
    int i,j;
    for (i=0; i<=3; i++){
        for (j=0; j<=8; j++){
            if (fruits[i][j] == '\0') break;
            printf("%c", fruits[i][j]);
        }
        printf("\n");
    }
    return 0;
}

fruit[0]에 속한 원소 j를 하나씩 뽑아내는 함수다!


 

 

 

배열 포인터

배열의 각 부분을 가리키는 포인터를 배열 포인터라고 한다.

 

그러나 위와 같이 만약 배열이 2차원 이상이라면 어떻게 표기해야 할까? *ap는 1만을 가리킬 것이다.

실제로 2차원 배열은 사람들이 알아듣기 쉽게 2차원으로 나타냈지만 행렬 구분이 되어있지 않다. 따라서 우리는 배열을 행단위로 구분해서 가리키는 방법을 사용한다!

 

int arr[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int (*ap)[3]; //ap는 어떤 배열의 3행을 가르키는 포인터
ap = arr; //여기서 arr또한 포인터로써 기능한다. arr을 ap에 연결시켜줌

여기서는 ap를 arr전체로 함으로써 ap가 3x3전체 행렬을 가리키도록 만들었다.

그러나 다음과 같이 코딩하면,

int arr[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int (*ap)[3];
ap = arr[1];

ap = arr[1] 을 가리키면 arr 1행의 3열까지 가리킨다는 것이다.

 

여기서 특정 수를 가리키고 싶다면 어떻게 해야할까?

우선 일반 포인터를 이용해보자

int main(void){
  int arr[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
  int *ap;
  ap = arr;
  
  printf( "%d %d %d\n", *(ap+6), *(ap+7), *(ap+8) );
  return 0;
}

이렇게 하면 7, 8, 9의 출력값을 얻는다.

ap는 배열의 1행 1열을 가르키고, 열 순서 행 순서대로 7,8,9 번째의 값을 출력하는 방식이다.

 

이제는 배열 포인터를 이용해보자

int main(void){
  int arr[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
  int (*ap)[3];
  ap = arr[1];
  
  printf( "%d %d %d\n", *(arr[0]+6), *(arr[0]+7), *(arr[0]+8) );
  
  printf( "%d %d %d\n", *(ap[0]+3), *(ap[0]+4), *(ap[0]+5) );
  return 0;
}

아래 위 둘다 7, 8 ,9의 값을 얻을 수 있다.

3개의 원소를 가지는 ap 배열 포인터와 몇 번째인지 이용하는 방식을 이용해 출력하는 것이다.


 

 

오늘은 배열과 포인터의 끔찍한 혼종에 대해서 살펴보았다. 다음 시간엔 이러한 혼종을 이용해서 응용하는 혼종 업그레이드 버전을 공부해보도록 하자!