하지만, 저는 여러분이 조금만 더 힘을 내어 이러한 유혹을 이겨내고 C 언어의 성지에 도달하시기 바랍니다. 사실 배열과 앞으로 나오는 포인터 부분만 넘어간다면 더이상 어려울 부분이 없기 때문이죠. 그 부분이 넘어가 C 언어 성지에 도달하게 되면, 윈도우 API 를 공부하여 실감나는 2D 게임도 만들고, Direct X 나 OpenGL 등을 공부해서 3D 게임 까지. 뿐만 아니라 소켓 프로그래밍을 공부한다면 친구들과 채팅할 수 있는 프로그램도 만들 수 있고 게임 서버들도 만들 수 있습니다. 그 뿐만이 아닙니다. 시스템 프로그래밍을 공부하면 후킹과 같은 방법을 통해 게임 핵도 만들고 (딱히 좋은 예는 아니지만, 유혹이 가지 않습니까? ㅎㅎㅎ), 리버스 엔지니어링을 공부해 크랙이나 키젠도 만들어 보고, 순위 조작도 해 볼 수 있습니다. 게다가 어셈블리어와 함께 공부한다면 운영체제 까지 정말 무궁무진한 세상이 열리는 것 입니다. 뿐만 아니라 더욱 놀라운 사실은 대부분의 전자 제품은 C 언어로 프로그램되어 있으므로 이를 조작하여 전기 밥솥에서 "굿모닝' 이라 던지 세탁기에서 비프음으로 '나비야~ 나비야~' 가 나올 수 도 있습니다.
C 언어를 배움으로써 얻을 수 있는 위 많은 것들을 쉽게 포기하실 것 입니까? 물론, 나가실 분은 조용히 뒤로가기를 누르셔도 상관 없습니다. 다만, 이 순간의 클릭이 당신의 앞날을 좌우 할 지도 모른다는 사실을 잊지 마세요. 그리고 참, 이전 내용이 기억이 나지 않는다고 머리를 쥐어 뜯지 마세요. 그냥 이전 강좌를 다시 보면 됩니다. 이전 강좌의 내용이 잘 기억이 나지 않는 것은 '지극히 정상' 이니 다시 한 번 읽어 보므로써 기억을 강화시키도록 하세요~~
암튼, 잡담을 끝내고 본론으로 들어가도록 합시다. 이전 강좌에서 배열은 변수 들의 모임이라고 했습니다. 이 때 arr 이라는 배열의 i 번째 원소를 참조 하기 위해선 arr[i] 라고 써야 한다는 것도 알았습니다. 그런데 똑똑한 사람이라면 이 아이디어를 확장해서 다음과 같은 생각을 할 수 도 있을 것 입니다.
"배열의 배열을 만들면 어떨까? "
정말로 놀라운 생각 입니다. 일단 여기서 우리는 '배열의 배열'의 의미를 좀 더 명확하게 해야 겠습니다. 'int 형의 배열' 이란 말은 '배열의 각 원소가 int 형 변수 인 것!' 입니다.
그렇다면 'int 형의 배열의 배열' 이란 말은 무엇일까요? 이 말은, '배열의 각 원소가 int 형의 배열 인 것' 을 말합니다. C 언어 에서 이러한 형태의 배열을 정의하기 위해선 아래와 같이 하면 써주면 됩니다.
(배열의 형) (배열의 이름)[?][?]; //위 경우 (배열의 형) 부분에 int 가 오면 된다.? 에는 임의의 수
이전에는 (배열의 이름)[?] 였지만 2 차원 배열은 옆에 [?] 하나가 더 붙었지요.
먼저 배열의 배열이 무엇인지 확실하게 알기 위해서 다음과 같은 배열을 정의해 봅시다.
int arr[3][2];
앞에서 말했듯이 위 배열은 '배열의 각 원소 3 개가 원소를 2 개 가지는 int 형의 배열이고 이름은 arr 이다.' 을 의미 합니다. 따라서,
가 됩니다.
즉, arr[0] 이라 하면 int 형의 원소를 2 개 가지는 배열을 말하는 것이며 그 배열의 원소는 각각 arr[0][0], arr[0][1] 이 되겠지요.
일차원 배열과 이차원 배열을 한 눈에 비교하자면 아래와 같습니다.
어때요, 간단하죠? 따라서, arr[m][n]; 과 같이 배열을 선언한다면 (m 과 n 은 임의의 정수값), 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;
}
#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};
4,5,6,
7,8,9};
위에서 2 차원 배열을 정의 하였습니다. 그런데 사실 아래와 같이 모두 한 줄에
int arr[3][3] = {1,2,3,4,5,6,7,8,9};
다 써도 큰 문제는 없지만 보기 좋기 위해 위와 같이 나열한 것 입니다.왜냐하면 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차원 이라는 것이 존재할 수 가 없습니다. 단지 선형으로 된 데이터들의 나열일 뿐이지요. 위 배열의 경우 컴퓨터 메모리 상에 다음과 같이 존재합니다.
하지만 우리가 이렇게 메모리 상에 선형으로 배열되어 있음에도 불구하고 '이차원 배열' 이라고 부르는 이유는 위 처럼 메모리 상에 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;
}
#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 차원 배열' 이라고 볼 수 도 있습니다. 이를 그림으로 나타내면
꼴로 보면 됩니다.
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(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]);
}
{
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]);
}
{
printf("%d 번째 학생의 국어 점수 : %d, 수학 점수 : %d \n", i+1, score[i][0], score[i][1]);
}
를 통해 입력 받은 값을 깔끔하게 보여주게 됩니다.
2 차원 배열 정의하기 |
앞선 예제에서
int arr[2][3] = {1,2,3,
4,5,6};
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[];
이 것은 왜 안될까요? 아마, 이전 강의를 잘 보신 분들게서는 단박에 알아 차릴 수 있을 것 입니다. 그 이유는 '배열의 크기는 임의로 정해지지 않기 때문! 1' 입니다. 즉, 위와 같이 배열을 정의한다면 컴파일러는 우리가 어떠한 크기의 배열을 정의하고 싶은지 모릅니다. 따라서 아래와 같은 오류를 내뿜게 됩니다.
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: 이니셜라이저가 너무 많습니다.
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 으로 처리하게 된 것입니다 2. 결론적으로 이것도 맞고 저것도 맞기 때문에 컴파일러는 위와 같이 알 수 없다는 오류를 뿜게 됩니다.
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,y,z) 을 통해서 원소에 접근하는 것 입니다. 네, 맞아요. 우리가 원소가 몇 번째 층에 있고 (x), 그 층에 해당하는 평면에 몇 행(y), 그리고 몇 열(z) 를 알면 int 변수에 정확하게 접근할 수 있지 않습니까?
4 차원도 같습니다. 4 차원 배열은 4 개의 값 (x,y,z,w) 을 통해서 원소에 접근할 수 있습니다. 마찬가지로 5 차원은 5 개, n 차원은 n 개의 값을 통해서 원소에 접근하게 되는 것이지요. 이 아이디어를 적용시키면 어떠한 차원의 배열이 실제 프로그래밍 상에 필요하다고 하더라도 문제 없이 해결할 수 있으리라 생각합니다.
댓글 없음:
댓글 쓰기