BLOG ARTICLE 2차원 배열 | 1 ARTICLE FOUND

  1. 2007.06.17 9. 배열 (array)

9. 배열 (array)

이번 장에서는 배열에 대해서 알아 보겠습니다. 배열과 포인터는 비슷하게 사용하지만, 메모리상에서의 의미는 많이 다릅니다. 중간 중간의 메모리에 관련된 이미지들을 설명과 함께 유심히 보시면, 이해가 쉬울 것입니다.

9.1 선언과 초기화

1) 배열의 선언

배열은 동일한 타입의 변수들의 집합체이며, 아래와 같이 선언합니다.

[변수 타입] 변수명[[배열크기]];

[변수 타입]과 변수명은 이전에 본 변수 선언과 같습니다. 배열은 변수명에 []를 붙입니다.
[배열크기]는 배열의 원소 갯수를 나타냅니다.

#include <stdio.h>

int main()
{
    int i; 
    char num[10];

    for(i = 0; i < 10; i++)
    {
        num[i] = i;

        printf(">> num[%d] = %d =>", i, num[i]);
        printf("*(num+i) = %d\n", i, *(num + i));
    }      
   
    return 0;
}

char num[10];
위는 char형 데이터를 10개 담을 수 있는 배열을 선언 한 것 입니다. 위와 같이 10개의 배열을 만들면 0 부터 9까지 사용할 수 있습니다. nums[10]은 사용할 수 없습니다. 0 부터 시작하기 때문에 주어진 크기 - 1까지 사용할 수 있습니다.

사용자 삽입 이미지
메모리를 보면 좌측과 같이 char 형이기 때문에 1바이트를 차지하고, 연속되게 자리하고 있습니다.

배열 num은 메모리상에서 시작되는 주소를 가지고 있습니다. 주소라는 의미에서 배열 num은 char* num과 의미가 같습니다.

그렇기 때문에 사용 시에도 포인터와 똑 같이 사용할 수 있습니다.  이는 포인터도 마찬가지 입니다.









2) 배열의 초기화

배열은 일반 변수와 마차가지로 선언시 값을 설정할 수 있습니다.

int a[5] = { 0, 2, 4, 6, 8 };

위를 보면 0, 2, 4, 6, 8로 배열원소가 다섯개인 것을 알수 있습니다. 그렇기 때문에 이와 같이 초기화 하면서 배열을 선언할 때에는 크기를 생략할 수 있습니다. 그렇기 때문에 아래와 같은 표현도 가능합니다.

int a[] = { 0, 2, 4, 6, 8 };

또한 아래와 같이 선언한 크기보다 적은 값으로 사용할 수도 있습니다.  이 경우에는 앞의 a[0] 부터 차례로 값이 들어 가며, a[5] 부터 마지막 배열인 a[9]까지는 0으로 설정됩니다.

int a[10] = { 0, 2, 4, 6, 8 };

9.2 문자열
C에서의 문자열의 NULL ('\0')로 끝나는 char형 배열입니다. 문자열은 char형의 배열과 포인터를 사용하여 아래와 같이 선언될 수 있습니다.

#include <stdio.h>

int main()
{
    char* p = "hello";
    char a1[6] = "hello";
    char a2[6] = {'h', 'e', 'l', 'l', 'o', '\0' };

    printf("%s\n", p);
    printf("%s\n", a1);
    printf("%s\n", a2);

    return 0;
}

위의 p와 a1의 경우에는 "hello" 뒤에 자동으로 '\0'을 붙여 줍니다. 그러므로 "hello" 문자열이 메모리에서 차지하는 크기는 6입니다.

사용자 삽입 이미지
문자열을 포인터와 배열로 선언할 때 아래와 같은 차이점이 있습니다. 사용시에는 거의 차이점이 없습니다.

char* p = "hello";
"hello"를 스택 메모리에 할당하고 이 주소(1051)를 p에 반환합니다.

char a1[6] = "hello";
a1에 6바이트를 할당하고 문자열 "hello"를 저장합니다.

좌측의 이미지를 보면 위의 의미를 이해하실 수 있습니다. 좌측이 배열의 경우이고 우측이 포인터의 경우 입니다.




char a2[6] = {'h', 'e', 'l', 'l', 'o', '\0' };
1바이트씩 수동으로 설정하는 a2는 사용자가 위와 같이 문자열 마지막에 '\0'으로 막아 주어야 합니다. 막아 주지 않을 경우 printf는 a2 다음 메모리 영역에서 '\0'이 올 때 까지 출력합니다. C에서는 아래와 같이 문자열을 출력한다고 생각하시면 됩니다.

while(1)
{
    if(*p == '\0')
       break;

    printf("%c", *p);
    p++;
}

위와 같이 '\0'을 만날 때 까지 메모리를 1증가해 가면서 계속 한문자씩 출력합니다. 이는 언제 '\0'이 나올지 모르고, p의 크기를 넘어가는 경우에는 정당한 메모리 영역이 아닌 부분에 접근하기 때문에 심각한 오류를 불러 올 수 있습니다.

그렇기 때문에 C에서 문자열을 사용할 때는 마지막에 '\0'으로 막는 다는 것을 가장 신경써야 하는 부분입니다.


9.3 다차원 배열
위에서 본 배열은 1차원 배열입니다. 다음과 같이 다차원 배열을 선언할 수 있습니다. 일반적으로 4차원 이상의 배열은 사용하는 경우가 거의 없습니다.

int n1[4][4];          // 2차원 배열
int n2[4][4][4];      // 3차원 배열

2차원 배열의 설명을 위해 2D 게임에서 맵을 놓고 생각해 보겠습니다. 5X5 크기로 맵을 만든다고 가정합니다. 0으로 되어 있는 부분은 캐릭터가 갈 수 없고, 1로 된 부분이 길로 캐릭터가 지나갈 수 있습니다.

사용자 삽입 이미지
int map[5][5] =
{
    { 0, 0, 1, 0, 0 },
    { 0, 0, 1, 0, 0 },
    { 0, 0, 1, 1, 0 },
    { 0, 0, 0, 1, 0 },
    { 0, 0, 0, 1, 0 }
};

2차원 배열을 위에 그림으로 보면, 흰색으로 된 부분이 갈 수 있는 길 입니다. 만약 캐릭터가 갈 수 있는 길인지 없는지 판단해야 하는 함수가 있다면 아래와 같이 구현됩니다.

/* y는 세로, x는 가로 위치 */
int can_go(int x, int y)
{
   /* 0 or 1 반환 */
    return map[y][x];
}

아래의 소소로 맵을 실제로 출력해 보도록 하겠습니다.

include <stdio.h>

int map[5][5] =
{
    { 0, 0, 1, 0, 0 },
    { 0, 0, 1, 0, 0 },
    { 0, 0, 1, 1, 0 },
    { 0, 0, 0, 1, 0 },
    { 0, 0, 0, 1, 0 } 
};

void show_map()
{
    int i, j;

    for(i = 0; i < 5; i++)  /* 세로 */
    {
       for(j = 0; j < 5; j++) /* 가로 */
       {
          /* 출력 */
          printf("%d ", map[i][j]);
       }
       printf("\n");  /* 다음 행으로 */
    }  
}

int main()
{
    show_map();

    return 0;
}


9.4 포인터 배열

1) main의 포인터 배열 char* argv[]

int main(int argc, char* argv[]);
위와 같은 main의 선언을 많이 보았습니다. 이전에 배운대로 argc는 인자의 갯수이며, 두번째 인자 char* argv[]는 실행 시 입력정보를 넘기는 포인터의 배열입니다.  argv, argc에 관한 자세한 정보는 이전 3장. C 기초문법 3.2 함수  3) 인자 부분을 참고하세요.

포인터는 배열과 같은 의미로 보면 되기 때문에 이는 아래와 같이 보고, 사용할 수 있습니다.

char argv[][]

실제 이렇게 선언할 수는 없지만 위의 의미로 보면 argv[]가 한 문자열을 나타내기 때문에, char[][]는 문자열들의 배열 입니다.

> ./[실행파일 명] abc def [enter]
로 어떤 실행파일을 실행하면 시스템에서 아래와 같이 두 변수를 만든 후에 넘겨 준다고 생각하시면 됩니다.

int argc = 3;
char* argv[] =

    "[실행파일명]",
    "abc",
    "def"
};

아무 소스코드에서나 main에 아래와 같은 코드를 넣으면, 입력값을 확인할 수 있습니다.
for(i = 0; i < argc; i++)
{
    printf("%s\n", argv[i]);
}

2) 포인터와 배열의 차이

위에서 포인터 배열과 2차원 배열은 동일하다고 했으나, 몇 가지 중요한 차이점이 있습니다. 아래의 소스를 보겠습니다.

#include <stdio.h>
#include <string.h>

int main()
{
    char a[2][3];
    char* p[2];

    strcpy(a[0], "ab");
    strcpy(a[1], "cd");

    p[0] = "ab";
    p[1] = "cd";

    printf("%s\n", a[0]);
    printf("%s\n", a[1]);
    printf("%s\n", p[0]);
    printf("%s\n", p[1]);

    return 0;
}

#include <string.h>
strcpy 함수 사용을 위해 string.h 파일을 인클루드 합니다.

char a[2][3];
char* p[2];
위의 두 선언은 메모리에서 각각 아래와 같이 할당됩니다. char형 2차원 배열은 선언한 만큼 6바이트(2X3)의 메모리를 갖습니다. 그에 비해 char포인터 형 변수는 주소(4바이트)를 2개 가질 수 있는 4바이트 영역을 할당 받습니다.

사용자 삽입 이미지

strcpy(a[0], "ab");
strcpy(a[1], "cd");
"ab" 문자열을 배열 a[0]으로 복사 합니다.
"cd" 문자열을 배열 a[1]으로 복사 합니다.

strcpy(p[0], "ab");
위와 같은 소스는 컴파일은 되지만, 심각한 오류가 일어 날 수 있습니다. 초기화 되지 않은 포인터 변수 p[0]에는 현재 어떤 쓰레기 값이 있는지 알 수 없습니다. p[0]에 들어있는 값(주소)로 "ab"를 복사하기 때문입니다.

위와 같이 사용할려면 아래와 같이 전장에서 배운 malloc으로 할당 받고 정당한 주소를 돌려 받은 후, 사용하여야 합니다.
p[0] = (char *)malloc(3);

p[0] = "ab";
p[1] = "cd";
"ab"를 스텍 메모리에 할당한 후, 주소값을 p[0]에 대입합니다.
"ab"를 스텍 메모리에 할당한 후, 주소값을 p[1]에 대입합니다.

반대로 배열 a[0] = "ab"로는 사용할 수 없습니다. 시스템에서 이미 a[0]에 1012라는 변경이 불가능한 주소를 할당 받고 위치하고 있기 때문입니다.

위의 작업이 완료되면 메모리는 아래와 같이 값들이 들어 갑니다.
사용자 삽입 이미지
printf("%s\n", a[0]);
printf("%s\n", a[1]);
printf("%s\n", p[0]);
printf("%s\n", p[1]);

배열과 포인터는 의미는 틀리지만, 위와 같이 사용은 똑 같이 할 수 있습니다. 그렇기 때문에 아래와 같이도 사용할 수 있습니다.

printf("%s\n", *(a+0));
printf("%s\n", *(a+1));
printf("%s\n", *(p+0));
printf("%s\n", *(p+1));

이제 소스를 컴파일 하고 출력하여, 결과를 확인하여 봅니다.

'프로그래밍 강좌 > C 언어 기초' 카테고리의 다른 글

11. define과 디버깅  (1) 2007.07.08
10. struct, union, enum, typedef  (2) 2007.06.21
9. 배열 (array)  (0) 2007.06.17
8. 포인터 (pointer)  (4) 2007.06.16
7. C 함수 (function)  (4) 2007.06.15
6. 제어문  (0) 2007.06.14