1. 배열(Array)


앞으로 배우게 될 배열(Array)은 많은 수의 데이터를 관리할때 상당히 편리한 녀석입니다. 우선 우리가 배열을 모른다 가정하고, 예를 들어서 100개의 수를 모두 저장해야 한다고 해봅시다. 값을 우선 기억시키려면, 값을 기억할 변수를 선언해야만 하겠죠? 그렇다면, 100개의 수를 모두 저장한다고 했으니 100개의 변수를 선언해야 합니다. 아래와 같이 말입니다.

int num1;
int num2;
int num3;
...
...
int num99;
int num100;

위를 보시면, 일일히 변수 100개를 따로 선언하면서 값을 저장합니다. 만약 이런다면 값을 저장시킬때도 따로 저장시켜야 하며 이것은 문서 타이핑과 다를 바가 없어집니다. 그럼 어떻게 해야만 편리하게 데이터를 수정하고 저장할수 있을까요? 바로 우리가 이제 배우게 될 '배열(Array)'이라는 녀석을 사용하면 됩니다. 배열을 이용한다면, 단 한줄로 100개의 변수가 선언된 것과 같은 상황을 만들 수 있습니다.

int num[100];

위의 예를 보시면, 괄호 안에 있는 100은 배열의 길이를 의미하며, 이는 int형 데이터가 들어갈 공간이 100개나 메모리 공간에 할당된 것으로 이해를 하시면 됩니다. 배열을 어느 공간에 저장된 자료의 집합이라고 생각하셔도 무관합니다. 배열을 선언하려면 다음과 같은 방법으로 선언할수 있습니다.

자료형 변수명[배열의 길이];

위의 배열 선언형식을 보시면, 자료형에는 우리가 알고 있듯이 int, double, char 등이 모두 들어갈 수 있습니다. 변수명은 배열의 이름이라고 생각하시면 됩니다. 그리고 배열의 길이는 말 그대로 배열의 길이입니다. 특정 자료형의 변수가 들어갈 공간을 몇 개정도 할당할 것인지 결정합니다.


그렇다면, 배열을 우선 선언한 뒤에 이 배열에 어떻게 접근해야 할까요? 접근하는 방식은 생각보다 어렵지 않습니다. [ ] 사이에 숫자를 넣어 지정하면 됩니다. 이 숫자는 인덱스(Index, 첨자값)라 부르며, 주의할 점은 1이 아닌 0부터 시작한다는 겁니다. 아래 예제를 통해 배열의 접근방법을 더 쉽게 이해하실 수 있습니다.

#include <stdio.h>

int main()
{
 int array[3];
 int i;

 array[0]=100;
 array[1]=200;
 array[2]=300;

 for(i=0; i<3; i++)
 printf("array[%d]의 값: %d", i, array[i]);
return 0;
}

결과:

array[0]의 값: 100
array[1]의 값: 200
array[2]의 값: 300


살펴보면 5행에서 array란 이름을 가지며 길이가 3인 정수형 배열을 선언하고, 8~10행에서 배열에 접근하여 값을 초기화시키고 12~13행에서 배열 array에 저장된 데이터를 모두 출력하게 했습니다. 차근차근 정리해보면, 정수형 데이터가 기억될 수 있는 공간이 3개나 있다는 셈입니다. 그리고 이 공간을 통하여, 인덱스(index)를 이용하여 배열에 접근하여 값을 초기화 시킵니다. 이제 뭔가 좀 아실듯 하신가요? 참고로 배열을 선언할때 변수처럼 초기값을 줄수 있습니다.

int array[10]={4,1,7,64,3,18,9,2,1,33};

이렇게 되면 array[0]에 4가, array[1]에 1이, array[2]에 7이... array[8]엔 1이, array[9]엔 33이 저장되게 됩니다. 배열의 초기값을 지정해주면 배열의 길이를 굳이 적지 않으셔도 됩니다. 컴파일러가 자동으로 초기값 갯수에 따라 배열의 길이를 정해주기 때문입니다. 이게 무슨말이냐면, 아래와 같은 상황(배열의 길이가 별도로 명시되어 있지 않은 상황)에서 초기화 되는 데이터의 갯수에 따라 컴파일러가 배열의 길이를 정해준다는 말입니다.

int array[]={4,1,7,64,3,18,9,2,1,33};

위와 같이 선언과 동시에 초기화를 하여도 됩니다. 하지만 초기값을 주지 않고 배열의 크기도 적지 않는건 안됩니다. 그리고 배열의 크기는 상수로 주어야만 합니다. 만약에 다음과 같이, 배열의 길이가 명시되어 있는 상태에서 초기화를 하는데 일부만 초기화가 이루어지는 경우는 어떠할까요?

int array[10]={4,1,7,64,3};

위와 같이 작성한다면 초기화 목록이 배열의 길이보다 작으므로 초기값 5개를 제외하고 나머지는 모두 0으로 초기화 됩니다. 그렇다면 이번에는, 문자 배열을 통하여 배열에 대해 더 자세히 알아보도록 합시다.


2. 문자 배열


문자 배열은 자료형이 char형인 배열으로, 문자 배열에는 문자열이 들어갈 수 있습니다. 어디한번 문자 배열을 통하여, 문자열을 입력받도록 하는 예제를 작성해보도록 합시다.

#include <stdio.h>

int main()
{
	char arr[5];

	scanf("%s", arr);
	printf("arr : %s\n", arr);

	return 0;
}

한번 위의 코드를 컴파일 하여, 5글자의 영어 단어를 입력해 보세요. 갑자기 프로그램이 비정상 종료되거나, 이상한 문자들이 덧붙여 따라붙는 경우를 보실 수 있습니다. arr이란 문자형 배열은 5개의 문자를 입력받을 수 있을것 같은데 왜 이러한 상황이 벌어질까요? 바로 문자열의 마지막에는 문자열의 끝을 알리는 널(NULL, \0)문자가 삽입되기 때문입니다. apple이라고 입력한다면 아래와 같은 상황이 벌어지는 셈입니다.


이미 문자형 배열에는 문자가 모두 들어가 공간이 없는 상태에서, NULL문자가 삽입되려 하자 공간이 부족하여 삽입되지 못합니다. 컴파일러는 널 문자가 나올때까지 문자열을 출력하다가, 문자열의 끝을 몰라 아무 의미도 없는 쓰레기값들이 따라 붙습니다. (우연히 메모리 공간에서 널 문자를 만나 탈출할 수도 있습니다.) 모든 문자열은 NULL로 끝나며, 반드시 NULL 공간까지 생각해 주어야만 합니다. 5글자의 영단어를 입력받으려면, 배열의 길이를 5가 아닌 6으로 수정해야 한다는 말입니다.


참고로, 문자형 배열에는 scanf 함수로 입력받을때 & 연산자를 배열명 앞에다 붙여서는 안됩니다. scanf 함수에선 포인터(특정 주소를 가리키는 녀석)는 주소와 연관되어 있는 녀석이므로 & 연산자를 붙이지 않아도 되는데, 여기서 문자형 배열도 이미 포인터이기 때문에 문자형 배열을 scanf 함수로 입력받을때는 다른 타입의 배열과는 달리 & 연산자를 제외해야 한다는걸 기억해 두세요. 포인터에 대해서는 나중에 포인터를 다루게 될 때 자세히 설명합니다.


이어서, NULL 문자에 대해서 간단히 설명하도록 하겠습니다. NULL 문자는 ASCII 코드로 0에 해당하며, C언어에서는 \0이 NULL 문자를 의미합니다. 어디 한번, 컴파일러가 정말 널 문자가 나올때까지 문자를 출력하는지 예제를 살펴보도록 합시다.

#include <stdio.h>

int main()
{
	printf("ABCDE\0FGHI");
	printf("0123456\0 7891011");
	printf("ㄱㄴㄷ\0ㄹㅁㅂ");

	return 0;
} 

결과:

ABCDE0123456ㄱㄴㄷ계속하려면 아무 키나 누르십시오 . . .


위의 예제의 결과물을 보고 유추할때 5행에서 순차적으로 출력하다 문자열의 끝을 알리는 \0을 만나 그 뒤의 문자는 모두 버려지고 \0 전에 있는 문자열들만 출력됨을 알 수 있습니다. 6~7행도 마찬가지로 말이죠. 한가지 예를 더 보도록 하겠습니다. (참고로 문자를 출력할때 사용되는 서식문자는 %c, 문자열 출력에 사용되는 서식문자는 %s라고 앞서 설명드렸습니다.)

#include <stdio.h> int main() { char string1[10]="ABCDEF"; char string2[]="ABCDEF"; char string3[]={'A', 'B', 'C', 'D', 'F'}; char string4[]={'A', 'B', 'C', 'D', 'F', '\0'}; printf("%s\n", string1); printf("%s\n", string2); printf("%s\n", string3); printf("%s\n", string4); return 0; }

결과:

ABCDEF
ABCDEF
ABCDF?
ABCDF
계속하려면 아무 키나 누르십시오 . . . 


위의 예제를 보시면, 4행에서 배열 string1의 길이를 10으로 선언하고 ABCDEF란 문자열을 담았습니다. 9행에서 정확히 출력되었으며, 5행에서 배열 string2의 길이를 지정하지 않고 초기값을 주어 길이를 자동 지정하도록 하게 했습니다. 10행에서 정확히 출력되는걸 확인할수 있으며, 6행에서는 위와 다르게 자동으로 널문자가 삽입이 되지 않으며 11행에서 이상한 값이 출력되는것을 확인하실 수 있습니다. 이는 문자열의 끝을 알리는 널문자가 존재하지 않기 때문에 출력은 널문자를 만날때까지 계속되었으며 우연히 널문자를 만나 빠져나온 경우입니다. 7행에서 널문자를 삽입하고 12행을 보시면 11행과는 달리 ABCDF만 출력하고 프로그램이 종료됩니다.


이해 되시나요? 이미 알고계신 분들도 있으시겠지만, 참고로 문자열은 '문자들의 순서'를 의미합니다. a는 문자지만, ab는 문자열인 셈입니다. C언어에서는 문자는 작은 따옴표로 감싸주어 표시하고, 문자열은 큰 따옴표로 감싸주어 표시합니다.


3. 다차원 배열


우리가 여태까지 보아왔던 배열은 모두 1차원에 속하는 배열이였습니다. 그런데, C언어에서는 이런 1차원 배열 말고도 2, 3차원 이상의 배열을 표현할 수 있습니다. 2차원 배열은 2차원의 형태로, 3차원 배열은 3차원의 형태로 표현할때 구현이 용이합니다. 우선 2차원 배열이 어떻게 생겨먹은 녀석인지 잠깐만 살펴보도록 합시다.


<2차원 배열(2dimension array, 2D array)>

위 배열을 살펴보니, 2차원 배열이 어떤 녀석인지를 알 수 있을것 같나요? 1차원 배열에선 행이란 개념만 존재했다면, 2차원 배열에서는 열(Columns)이란 개념이 추가됩니다. 2차원 배열은 아래와 같이 선언할 수 있습니다.

반환형 배열명[행][열];

간단하죠? 예를 들어서, 2차원 정수형 배열을 선언하도록 하려면 아래와 같이 선언하고 초기화/접근 할 수 있습니다.

// 2차원 배열의 선언
int array2D[3][3];
..
// 2차원 배열의 선언과 초기화
int array2D[3][3]={{1,1,1},{1,2,4},{1,3,9}};
..
// 2차원 배열의 접근 방식
int array2D[3][3];
int i, j;

for(i=0; i<3; i++) {
 for(j=0; j<3; j++) {
  scanf("%d", array2D[i][j]);
 }
}

다차원 배열은 직접 자신이 코드를 작성하고 응용하면서 경험해 보시기를 권장합니다. 이번에는 3차원 배열입니다. 3차원 배열은 예를 들어, 큐브를 예로 들수 있겠네요.

<3차원 배열(3dimension array, 3D array)>

3차원 배열에서는 2차원 배열에서 높이라는 녀석이 추가되었습니다. 이 3차원 배열은 3차원의 형태를 표현하기 위해 용이하게 쓰이는 배열입니다. 이런 3차원 배열은 아래와 같이 선언하고 초기화하고 접근합니다. 2차원 배열과 마찬가지로 3차원 배열도 직접 코드를 작성하면서 연습을 해보시길 바랍니다.

// 3차원 배열의 선언
int array3D[2][3][4];
..
// 3차원 배열의 선언과 초기화
int array3D[2][3][4]={{{1,2,3,4},{4,5,6,7},{8,9,1,2}},{{4,5,6,7},{5,6,7,8},{9,1,2,3}}};
..
// 3차원 배열의 접근 방식
int array3D[2][3][4];
int i, j, k;
for(i=0; i<2; i++) {
 for(j=0; j<3; j++) {
  for(k=0; k<4; k++) { 
  scanf("%d", array3D[i][j][k]);
  }
 } 
}
다차원 배열이라고 해도 배열만 이해했다면 더 복잡해지는게 없습니다. 2차원 배열이 주로 많이 쓰이며, 그다음 1차원 배열, 그다음 3차원 배열 순으로 많이 쓰입니다. 4차원 배열은 상식적으로 생각할수 있는 형태의 배열이 아니며 존재하지 않는다고 생각하셔도 됩니다.


4. 배열을 인수(argument)로 넘기는 방법


마지막으로 1차원 배열을 인수(argument)로 넘기는 방법부터 시작하여 다차원 배열을 인수로 넘기는 방법까지 알아보도록 하겠습니다. 먼저, 1차원 배열에 대한 예시를 보도록 하겠습니다.

반환형 함수명(자료형 배열명[], ...)
반환형 함수명(자료형 *배열명, ...)

2행과 같이 배열명 앞에 *를 붙이는 방법은 우리가 추후에 배우게 될 포인터와 연관되어 있습니다. 우선은 '아, 이런 방법도 있구나!' 하고 넘어가시고 포인터를 배우신 다음에 다시 이 부분을 보셔도 무관합니다.


제대로 감이 오지 않으시는 분들을 위해 곧바로 아래의 예제를 보도록 합시다.

#include <stdio.h>

void func(int arr[], int len);

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

	func(arr, sizeof arr / sizeof(int));

	return 0;
}

void func(int arr[], int len)
{
	int i;

	for (i = 0; i < len; i++)
		printf("%d ", arr[i]);
	printf("\n");
}

결과:

1 2 3 4 5 6

계속하려면 아무 키나 누르십시오 . . .


위의 예제를 보면 정수형 배열 arr이 선언과 동시에 초기화가 되어있고, 그 다음에 func 함수를 호출하면서 배열 arr과 배열의 크기를 인수로 넘기고 있는 것을 확인할 수 있습니다. 배열의 길이를 함께 넘긴 이유는, 배열을 넘겨받은 함수의 입장에서는 이 배열의 길이가 어느정도인지 가늠할 수 없기 때문입니다. 물론, 배열의 크기가 가변적이지 않고 고정적인 경우는 길이를 넘겨받지 않고 상수로 아래와 같이 작성해도 무방하겠죠?

...
for (i = 0; i < 6; i++)
... 

그리고 함수의 원형을 (int arr[], int len)가 아닌 (int *arr, int len)으로도 한번 바꾸어보시기 바랍니다. 


이어서 2차원 배열을 함수의 인수로 넘기는 방법에 대해서 알아보도록 하겠습니다.

반환형 함수명(자료형 배열명[행의 수][열의 수], ...)
반환형 함수명(자료형 배열명[][열의 수], ...)

그리고 한꺼번에 3차원 배열을 함수의 인수로 넘기는 방법도 알아보도록 하겠습니다.

반환명 함수명(자료형 배열명[가로][세로][높이])
반환명 함수명(자료형 배열명[][세로][높이])
이렇게 보니까 다차원 배열을 함수로 넘기는 방법이 비슷하다고 생각되지 않나요? 4차원 이상의 배열은 잘 사용할 일이 없으나, 4차원을 넘어가는 배열 역시도 위에 작성한 것과 같이 배열을 인수로 넘기는 것과 다르지 않습니다.
#include <stdio.h>

void func(int arr[][4], int rows);

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

	func (arr, 2);

	return 0;
}

void func(int arr[][4], int rows)
{
	int i, j;

	for (i = 0; i < rows; i++)
	{
		for (j = 0; j < 4; j++)
			printf("%d\t", arr[i][j]);
		printf("\n");
	}
}

결과:

1       2       3       4

5       6       7       8

계속하려면 아무 키나 누르십시오 . . .


행이 2, 열이 4인 2차원 배열인 arr를 선언하였음을 확인할 수 있고, 함수 func의 함수원형에 행의 수가 생략된 방식으로 쓰여졌음을 확인할 수 있습니다. 위의 예제를 참고하여, 자신이 여러가지 예제를 구현해보면서 배열을 인수(argument)로 넘기는 방법에 대해 숙달이 되었으면 좋겠습니다.


Q. 왜 앞의 길이를 생략해도 무방합니까?