모두의 코드
씹어먹는 C 언어 - <11 - 2. C 언어의 아파트2 (고차원의 배열)>

이번 강좌에서는

씹어먹는 C 언어

안녕하세요, 여러분. Psi 입니다. 아마 이쯤 되면 여태까지 공부하셨던 사람들 중 일부는 '아, 내 머리는 컴퓨터 언어를 배우기에 최적화 되어 있지 않나 보다' 하고 포기하는 사람들과 '오오... 내 맘대로 프로그램을 만들고 주물럭 주물럭 거릴 수 있는 것이 신기한데??' 하는 두 가지 부류의 사람들로 나뉘게 됩니다. 사실, 여기 까지 온 것 만으로도 정말 대단하다고 말 할 수 있습니다. 왜냐하면 인터넷을 통해 무언가 짬짬히 보아서 공부해 나가는 것은 쉬운 일이 아니기 때문이죠. 여러 게임의 유혹도 있고, 내가 이걸 배우는 시간에 채팅이나 하면 좋을 것을.. 와 같은 생각도 들기 때문이죠.

하지만, 저는 여러분이 조금만 더 힘을 내어 이러한 유혹을 이겨내고C 언어의 성 C 언어의 성지에 도달하시기 바랍니다. 사실 배열과 앞으로 나오는 포인터 부분만 넘어간다면 더이상 어려울 부분이 없기 때문이죠. 그 부분이 넘어가 C 언어 성지에 도달하게 되면, 윈도우 API 를 공부하여 실감나는 2D 게임도 만들고, Direct XOpenGL 등을 공부해서 3D 게임 까지. 뿐만 아니라 소켓 프로그래밍을 공부한다면 친구들과 채팅할 수 있는 프로그램도 만들 수 있고 게임 서버들도 만들 수 있습니다.

그 뿐만이 아닙니다. 시스템 프로그래밍을 공부하면 후킹과 같은 방법을 통해 게임 핵도 만들고 (딱히 좋은 예는 아니지만, 유혹이 가지 않습니까? ㅎㅎㅎ), 리버스 엔지니어링을 공부해 크랙이나 키젠도 만들어 보고, 순위 조작도 해 볼 수 있습니다. 게다가 어셈블리어와 함께 공부한다면 운영체제 까지 정말 무궁무진한 세상이 열리는 것 입니다. 뿐만 아니라 더욱 놀라운 사실은 대부분의 전자 제품은 C 언어로 프로그램되어 있으므로 이를 조작하여 전기 밥솥에서 "굿모닝' 이라 던지 세탁기에서 비프음으로 '나비야~ 나비야~' 가 나올 수 도 있습니다.

C 언어를 배움으로써 얻을 수 있는 위 많은 것들을 쉽게 포기하실 것 입니까? 물론, 나가실 분은 조용히 뒤로가기를 누르셔도 상관 없습니다. 다만, 이 순간의 클릭이 당신의 앞날을 좌우 할 지도 모른다는 사실을 잊지 마세요. 그리고 참, 이전 내용이 기억이 나지 않는다고 머리를 쥐어 뜯지 마세요. 그냥 이전 강좌를 다시 보면 됩니다. 이전 강좌의 내용이 잘 기억이 나지 않는 것은 '지극히 정상' 이니 다시 한 번 읽어 보므로써 기억을 강화시키도록 하세요

암튼, 잡담을 끝내고 본론으로 들어가도록 합시다. 이전 강좌에서 배열은 변수 들의 모임이라고 했습니다. 이 때 arr 이라는 배열의 i 번째 원소를 참조 하기 위해선 arr[i] 라고 써야 한다는 것도 알았습니다. 그런데 똑똑한 사람이라면 이 아이디어를 확장해서 다음과 같은 생각을 할 수 도 있을 것 입니다.

"배열의 배열을 만들면 어떨까? "

정말로 놀라운 생각 입니다. 일단 여기서 우리는 '배열의 배열'의 의미를 좀 더 명확하게 해야 겠습니다. 'int 형의 배열' 이란 말은 '배열의 각 원소가 int 형 변수 인 것!' 입니다.

그렇다면 'int 형의 배열의 배열' 이란 말은 무엇일까요? 이 말은, '배열의 각 원소가 int 형의 배열 인 것' 을 말합니다. C 언어 에서 이러한 형태의 배열을 정의하기 위해선 아래와 같이 하면 써주면 됩니다.

(배열의 형) (배열의 이름)[?][?]; //위 경우 (배열의 형) 부분에 `int` 가 오면 된다.? 에는 임의의 수

이전에는 (배열의 이름)[?] 였지만 2 차원 배열은 옆에 [?] 하나가 더 붙었지요.먼저 배열의 배열이 무엇인지 확실하게 알기 위해서 다음과 같은 배열을 정의해 봅시다.

int arr[3][2];

앞에서 말했듯이 위 배열은 '배열의 각 원소 3 개가 원소를 2 개 가지는 int 형의 배열이고 이름은 arr 이다.' 을 의미 합니다. 따라서,

배열을 원소로 하는 배열을 생각하면 됩니다. 즉, 원소가 두 개인 배열을 원소로 하여, 이 원소를 3 개 가진 배열이 바로 arr[3[2 에 해당하는 이차원 배열 입니다.

가 됩니다.

즉, arr[0] 이라 하면 int 형의 원소를 2 개 가지는 배열을 말하는 것이며 그 배열의 원소는 각각 arr[0][0], arr[0][1] 이 되겠지요.

일차원 배열과 이차원 배열을 한 눈에 비교하자면 아래와 같습니다.

arr[3 은 그냥 아파트에 집이 3개, arr[3[2 는 집이 3 개 인데, 각 집에 방이 2개

어때요, 간단하죠? 따라서, arr[m][n]; 과 같이 배열을 선언한다면 (mn 은 임의의 정수값), m * n 개의 변수를 가지는 배열을 선언한 것이 됩니다.

그렇다면, 2 차원 배열을 가지고 무슨 짓을 할 수 있을 까요? 사실, 여러분도 이미 예상한 바 있지만 오히려 일차원 배열에 비해 활용도가 훨씬 높아지게 됩니다. 예를 들면, 33 명의 학생에 대해 국어, 수학, 영어, 과학 점수를 보관하는 배열을 만든다 (이전의 배열 하나만 이용해선 한 과목의 점수 밖에 보관할 수 없었죠) 도서 입출 관리 프로그램에서 개개의 도서에 대해서 이 도서를 빌려간 날짜, 반납한 날짜 등을 보관하는 배열을 만든다 등등이 있습니다.

아! 그런데 아직 왜 이러한 배열을 '2차원' 배열이라고 말하는지 이야기 하지 않았군요. 사실, 메모리에는 모든 배열이 일차원 배열과 다름없이 들어갑니다. 그런데, 아래 예제를 보고 나면 왜 '2차원' 배열이라 이야기 하는지 감이 확 올 것 입니다.

/* 2 차원 배열 */
#include <stdio.h>
int main() {
  int arr[3][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9};

  printf("arr 배열의 2 행 3 열의 수를 출력 : %d \n", arr[1][2]);
  printf("arr 배열의 1 행 2 열의 수를 출력 : %d \n", arr[0][1]);
  return 0;
}

성공적으로 컴파일 하였으면

와 같이 나옵니다. 처음에 2 차원 배열을 정의 할 때 부터 확 와닿는 느낌이 듭니다. 왜냐하면 정말로 2 차원 상에 배열된 배열이라고 생각할 수 있고, 배열의 선언 자체가 2 차원 적으로 정의 되었기 때문이죠

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

위에서 2 차원 배열을 정의 하였습니다. 그런데 사실 아래와 같이 모두 한 줄에

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

다 써도 큰 문제는 없지만 보기 좋기 위해 위와 같이 나열한 것 입니다.왜냐하면 2 차원 배열을 아래와 같은 모습으로 존재한다고 상상 할 수 있기 때문이죠.

3 곱하기 3 정사각형에 위에서 부터 차례로 arr[0[0, arr[0[1, arr[0[2 가 쭉 써있는 것을 생각해보세요

마치 우리가 배열을 정의했던 것 처럼 이차원 상에 배열이 있다고 생각해서 그려볼 수 있습니다. 이 때, arr[x][y] 라고 한다면 x 는 몇 번째 줄에 있는지, y 는 몇 번째 열에 있는지를 나타냅니다. 예를 들어서 arr[0][1] 은 0 번째 줄, 1 번째 열에 있으므로 2 가 되겠지요. 결과적으로 말하자면 '일차원 배열은 한 개의 값(x)으로 원소에 접근하는 것이고, 이차원 배열은 두 개의 값(x,y)으로원소에 접근하는 것이다!' 라고 생각할 수 있게 됩니다.

한가지 눈 여겨 볼 점은 arr 이 '배열의 배열' 이라는 것이 맞다는 것 입니다. 왜냐하면 arr[0] 을 하나의 배열의 이름이라고 생각해 보면 3 개의 원소 arr[0][0], arr[0][1], arr[0][2] 를 가진다고 볼 수 있다는 것 이지요. 마찬가지로 arr[1], arr[2] 도 하나의 배열이라고 생각할 수 있습니다. 그런데 arr 이 배열이므로 우리는 2 차원 배열을 '배열의 배열' 이라고 볼 수 있지요.

기존의 배열과 마찬가지로 arr[3] 이라 하면 arr[0] ~ arr[2] 까지 사용 가능했듯이 arr[3][3] 이라 하면 arr[0][0] ~ arr[0][2],arr[1][0] ~ arr[1][2], arr[2][0] ~ arr[2][2] 까지 사용 가능 합니다.

그런데 한 가지 지적해야 할 부분은 컴퓨터 메모리 상에선 절대로 이차원 적으로 만들어 지지 않는 다는 것 입니다. 사실, 컴퓨터 메모리 상에선 2차원 이라는 것이 존재할 수 가 없습니다. 단지 선형으로 된 데이터들의 나열일 뿐이지요. 위 배열의 경우 컴퓨터 메모리 상에 다음과 같이 존재합니다.

arr[0[0, arr[0[1, arr[0[2, arr[1[0, arr[1[1, arr[1[2, arr[1[3, arr[2[0, arr[2[1, arr[2[2 순으로 메모리 상에 선형으로 배열됩니다.

하지만 우리가 이렇게 메모리 상에 선형으로 배열되어 있음에도 불구하고 '이차원 배열' 이라고 부르는 이유는 위 처럼 메모리 상에 2 차원으로 배열되어 있다고 생각하면 정말로 간편하기 때문 입니다. 예를 들어서 arr[2][1] 은 메모리 상에서 배열의 시작 부분 (arr[0][0]) 에서 부터 3*2 + 1 = 7 번째에 있는 값 이라고 생각해야 되지만 사실 이 데이터를 2 차원 상에 배열해 놓고 2 행, 1 열의 값 이라고 생각하면 훨씬 편하기 때문입니다. (물론 컴퓨터는 전자의 경우로 계산하게 됩니다)

/* 학생 점수 입력 받기 */
#include <stdio.h>
int main() {
  int score[3][2];
  int i, j;

  for (i = 0; i < 3; i++)  // 총 3 명의 학생의 데이터를 받는다
  {
    for (j = 0; j < 2; j++) {
      if (j == 0) {
        printf("%d 번째 학생의 국어 점수 : ", i + 1);
        scanf("%d", &score[i][j]);
      } else if (j == 1) {
        printf("%d 번째 학생의 수학 점수 : ", i + 1);
        scanf("%d", &score[i][j]);
      }
    }
  }

  for (i = 0; i < 3; i++) {
    printf("%d 번째 학생의 국어 점수 : %d, 수학 점수 : %d \n", i + 1,
           score[i][0], score[i][1]);
  }

  return 0;
}

성공적으로 컴파일 하였다면

와 같이 됩니다. 사실 작동 원리는 간단 합니다.

int score[3][2];

일단 위 구문을 통해 3 행, 2 열의 크기를 가지는 2 차원 배열 score 을 선언 하였습니다. 사실 우리가 프로그래밍 하고자 하는 목표에 따라 해석해 보면 '3 명의 학생의 2 과목의 데이터를 보관하는 score 2 차원 배열' 이라고 볼 수 도 있습니다. 이를 그림으로 나타내면

학생 1 의 국어 점수는 score[0[0, 수학 점수는 score[0[1, 학생 2 의 국어 점수는 score[1[0, 수학은 score[1[1 등등

  꼴로 보면 됩니다.

for (i = 0; i < 3; i++)  // 총 3 명의 학생의 데이터를 받는다
{
  for (j = 0; j < 2; j++) {
    if (j == 0) {
      printf("%d 번째 학생의 국어 점수 : ", i + 1);
      scanf("%d", &score[i][j]);
    } else if (j == 1) {
      printf("%d 번째 학생의 수학 점수 : ", i + 1);
      scanf("%d", &score[i][j]);
    }
  }
}

이제 for 문을 통해서 3 명의 학생의 데이터를 입력 받게 됩니다. 일단 오래간만에 두 개의 for 문이 같이 돌아가는데 어떠한 형식으로 작동되는 지는 알고 있겠지요? i = 0 일 때, j = 0 ~ 1, i = 1 일 때, j = 0 ~ 1, i = 2 일 때, j = 0 ~ 1 로 돌아가게 됩니다. 즉 위 부분을 통해 2 차원 score 배열의 값을 집어 넣게 되는 것 이지요.

if (j == 0) {
  printf("%d 번째 학생의 국어 점수 : ", i + 1);
  scanf("%d", &score[i][j]);
} else if (j == 1) {
  printf("%d 번째 학생의 수학 점수 : ", i + 1);
  scanf("%d", &score[i][j]);
}

for 문 안의 위 부분을 살펴 보면 j 가 0 이면 국어점수를 입력해라, j 가 1 이면 수학 점수를 입력해라 라고 물어 보는 것이 달라 집니다.

마지막으로

for (i = 0; i < 3; i++) {
  printf("%d 번째 학생의 국어 점수 : %d, 수학 점수 : %d \n", i + 1, score[i][0],
         score[i][1]);
}

를 통해 입력 받은 값을 깔끔하게 보여주게 됩니다.

2 차원 배열 정의하기

앞선 예제에서

int arr[2][3] = {1, 2, 3, 4, 5, 6};

와 같이 2 차원 배열을 선언하였습니다. 그런데, 프로그래밍시 줄 수를 절약하고 싶은 사람들은 아래와 같이 해도 큰 문제는 없습니다.

int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};

위 처럼 정의하는 것은 우리가 앞서 써 왔던 방법과 전혀 차이가 없으니 그냥 알아 두시면 됩니다. 여러분들 께서 원하는 방법으로 정의하시면 됩니다.

참고로, 이전 강의에서 이야기를 하지 않았는데, 다음과 같이 배열을 정의할 수 도 있습니다.

int arr[] = {1, 2, 3, 4};

음... 무언가 이상하다는 느낌이 드나요? 사실, 알고보면 단순합니다. 위와 같이 정의할 수 있는 이유는 컴파일러가 원소의 개수를 정확하게 알기 때문입니다. 컴파일러는 우리가 배열을 정의한 것을 보고 '아, 이 사람이 원소를 4 개 가지는 int 배열을 정의하였구나!' 라고 알아서 대괄호 안에 자동적으로 4 를 집어 넣어서 생각하게 됩니다. 따라서 위와 같이 정의하나

int arr[4] = {1, 2, 3, 4};

로 정의하나 같은 말이 되겠지요.하지만 아래와 같이 정의하는 것은 안됩니다.

int arr[];

이 것은 왜 안될까요? 아마, 이전 강의를 잘 보신 분들게서는 단박에 알아 차릴 수 있을 것 입니다. 그 이유는 '배열의 크기는 임의로 정해지지 않기 때문입니다. 즉, 위와 같이 배열을 정의한다면 컴파일러는 우리가 어떠한 크기의 배열을 정의하고 싶은지 모릅니다. 따라서 아래와 같은 오류를 내뿜게 됩니다.

 error C2133: 'arr' : 알 수 없는 크기입니다.

이 아이디어를 2 차원 배열에도 그대로 적용 시킬 수 있습니다.일단, 아래와 같은 2 차원 배열의 정의를 살펴 보도록 합시다.

int arr[][3] = {{4, 5, 6}, {7, 8, 9}};

위 정의를 보면 여러분은 비어있는 대괄호 안에 무슨 값이 들어갈지 맞출 수 있을 것입니다. 무엇이냐고요? 바로 2 이지요. 왜냐하면 {4,5,6} 를 가지는 arr[3] 배열 하나와, {7,8,9} 를 가지는 또다른 arr[3] 배열을 정의하여 총 2 개의 arr[3] 배열을 정의하였기에, int arr[2][3] 이 되어야 하는 것이지요.

그렇다면 아래와 같은 문장이 유효한지 살펴보세요.

int arr[][2] = {{1, 2}, {3, 4}, {5, 6}, {7}};

어! 이상하네요. 마지막에 그냥 {7} 이라고 되어 있잖아요? 아마 여러분들 중 대다수는 이 것을 보고 위 문장이 틀렸고 성공적으로 컴파일 되지 않으리라 생각할 것입니다. 하지만, 위 2 차원 배열은 배열 정의시 arr[][2] 라고 하였기 때문에 무조건 원소가 2 인 1 차원 배열들이 생기게 됩니다. 즉, 7 이 속한 1 차원 배열에는 원소가 한 개인 것이 아니라 마치 arr[3] = {1} 고 해도 상관 없는 것 처럼 8 이 들어갈 자리를 비워놓게 되지요. 따라서, 위 문장은 틀린 것이 아닙니다. 그렇다면 아래 문장을 봐보세요.

int arr[2][] = {{4, 5, 6}, {7, 8, 9}};

과연 될까요? 아마 여러분들 중 대다수는 될 것이라 생각하고 있을 것 입니다. 하지만 놀랍게도 컴파일해보면

error C2087: 'arr' : 첨자가 없습니다.
error C2078: 이니셜라이저가 너무 많습니다.

와 같은 오류들을 만나게 됩니다. 도대체 이게 뭔가요? 앞에선 하나가 모잘라도 잘만 컴파일 되던데 말이죠. 사실 곰곰히 생각해 보면 컴파일러가 이러한 오류를 내는 것은 당연하게 됩니다. 이는 임의의 크기를 가진 1 차원 배열 2 개를 가지는 2 차원 배열을 생성하라는 말 입니다. 다시 말해, '임의의 크기를 가진 1 차원 배열' 을 생성하겠다는 것이지요. 당연히 오류가 발생하게 됩니다. 배열의 크기는 임의의 크기가 결코 될 수 없기 때문이죠.

아마 여러분은 위 정의가 '저건 임의의 크기를 가진 배열을 생성하라는 것이 아니라, 크기가 3 인 1 차원 배열 두 개를 생성하라는 것이잖아요!' 라고 말할 것 입니다. 하지만, 그렇지 않습니다. 우리는 int arr[2][] = {{4,5,6},{7,8,9}}; 를 다음과 같은 의미로 사용하였습니다.

int arr[2][3] = {{4, 5, 6}, {7, 8, 9}};

하지만 어떤 괴상한 사람들의 경우 다음과 같이 생각하고 썼을 수 도 있겠지요.

int arr[2][4] = {{4, 5, 6}, {7, 8, 9}};

처음에 "저게 왜 되지..." 라는 생각이 들기도 하지만 알고 보면 당연하다는 사실을 알 수 있습니다. 이는 단지 int arr[][2] = {{1,2},{3,4},{5,6},{7}};  의 경우와 동일한 것이지요. 즉, 크기가 4 인 1 차원 배열 2 개를 정의하였는데 각각 원소 3 개 씩만 정의하고 나머지는 0 으로 처리하게 된 것입니다. 결론적으로 이것도 맞고 저것도 맞기 때문에 컴파일러는 위와 같이 알 수 없다는 오류를 뿜게 됩니다 . 결론적으로 이것도 맞고 저것도 맞기 때문에 컴파일러는 위와 같이 알 수 없다는 오류를 뿜게 됩니다.

 3 차원, 그 이후 차원의 배열들

2 차원 배열을 잘 이해하였다면 3 차원 배열을 이해하는 것은 그리 어려운 것이 아니라 생각됩니다. 사실, 보통의 프로그래밍에서 3 차원 배열을 쓰는 경우는 그렇게 많지 않습니다. (물론 제가 만들어본 프로그램들에 한해서...) 그렇지만 쓸 수 도 있기에 간단하게 집고 넘어가기만 합시다.

3 차원의 배열의 정의는 2 차원 배열과 거의 동일합니다. (그 이후의 차원들도 마찬가지)

(배열의 형)(배열의 이름)[x][y][z];  // 여기서 x,y,z 는 배열의 크기를 말합니다.

이제, 머리속으로 상상의 나래를 펼쳐 봅시다. 제가 그림판으로 3 차원 적인 그림을 그릴 수는 없으므로 여러분의 지능을 믿겠습니다! 일단, 아래의 배열을 머리에 그려 봅시다.

int arr[3][4];

이는 가로 길이가 4 이고 세로 길이가 3 인 평면위에 int 변수들이 하나씩 놀고 있는 것을 상상하면 됩니다. 그렇다면 이제 아래 배열을 머리에 그려 봅시다.

int brr[2][3][4];

아아악! 모르겠다고요? 아니요, 어렵지 않습니다. 위에서 상상한 평면 위에 동일한 평면이 한 층 더 있다고 생각하면 됩니다. 즉, 위에서 생각했던 평면이 2 개의 층으로 생겼다고 하면 됩니다. 어때요, 간단하죠?

하지만, 문제는 4차원 배열 부터 입니다. 뭐 우리는 3 차원 적인 세상에서 살고 있기 때문에 4 차원에 무엇인지 몸에 와닿기는 힘듭니다. (사실, 우리는 3차원 상의 공간에 시간의 축이 더해진 4차원 세상에서 살고 있다고 합니다) 그렇기에 4 차원 배열, 그리고 그 보다 더 놓은 차원의 배열이 무엇인지 머리속으로 그려보기란 고역이 아닐 수 없습니다.

그런데 말이죠. 제가 아까 굵은 글씨로 써 놓았던 것이 기억나시나요?

'일차원 배열은 한 개의 값(x)으로 원소에 접근하는 것이고, 이차원 배열은 두 개의 값(x,y)으로 원소에 접근하는 것이다!'

이를 확장해서 생각해 보면 삼차원 배열은 세 개의 값 (x,y,z) 을 통해서 원소에 접근하는 것 입니다. 네, 맞아요. 우리가 원소가 몇 번째 층에 있고 (x), 그 층에 해당하는 평면에 몇 행(y), 그리고 몇 열(z) 를 알면 int 변수에 정확하게 접근할 수 있지 않습니까?

4 차원도 같습니다. 4 차원 배열은 4 개의 값 (x,y,z,w) 을 통해서 원소에 접근할 수 있습니다. 마찬가지로 5 차원은 5 개, n 차원은 n 개의 값을 통해서 원소에 접근하게 되는 것이지요. 이 아이디어를 적용시키면 어떠한 차원의 배열이 실제 프로그래밍 상에 필요하다고 하더라도 문제 없이 해결할 수 있으리라 생각합니다.

그렇다면 이번 강좌는 여기에서 마치도록 하겠습니다.

생각해 보기

문제 1

제 강좌 제목에서 배열이 왜 C 언어의 아파트 인지 설명해 보세요. 즉, '동' 의 개념, '층' 에 개념, '호' 의 개념이 어떠한 배열을 형상화 하고 있는 지도 생각해 보세요. (난이도 : 下)

강좌를 보다가 조금이라도 궁금한 것이나 이상한 점이 있다면꼭 댓글을 남겨주시기 바랍니다. 그 외에도 강좌에 관련된 것이라면 어떠한 것도 질문해 주셔도 상관 없습니다. 생각해 볼 문제도 정 모르겠다면 댓글을 달아주세요.

현재 여러분이 보신 강좌는 <<씹어먹는 C 언어 - <11 - 2. C 언어의 아파트2 (고차원의 배열)>>> 입니다. 이번 강좌의모든 예제들의 코드를 보지 않고 짤 수준까지 강좌를 읽어 보시기 전까지 다음 강좌로 넘어가지 말아주세요


 다음 강좌 보러가기
프로필 사진 없음
댓글에 글쓴이에게 큰 힘이 됩니다