BLOG ARTICLE pointer | 2 ARTICLE FOUND

  1. 2007.06.17 9. 배열 (array)
  2. 2007.06.16 8. 포인터 (pointer) (4)

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

8. 포인터
 
이번 장은 C를 배울 때 가장 어렵다는 포인터에 대한 내용입니다. 포인터는 메모리에 대한 이해만 있으면, 그다지 어렵지 않습니다.

컴퓨터는 아래와 같이 동작합니다. 하드디스크에서 메모리로 데이터를 읽거나 쓰고, CPU는 메모리에 있는 명령어들과 데이터를 레지스터라고 불리우는 저장소로 불러와 처리합니다.
사용자 삽입 이미지
우리가 만든 하드디스크에 있는 실행파일은 실행시에 코드와 데이터 모두 메모리에 저장된 후 사용됩니다. 그렇기 때문에 선언하는 전역, 지역 변수들 모두 할당된 크기로 메모리에 저장됩니다. 

CPU는 메모리에 접근하기 위하여 1byte단위로 메모리에 주소를 할당해 놓고 사용합니다. 우리는 사용하기 쉽게 int a; 로 선언하고 a라는 변수명으로 사용하지만, 실제 컴퓨터는 a라는 이름 대신 메모리 주소로 접근합니다. 이 때 사용하는 주소를 포인터라고 보시면 됩니다.


8.1 변수의 메모리 주소

사용자 삽입 이미지
포인터는 간단히 이야기 하면 메모리 상에서 주소를 저장하는 변수 입니다.  좌측 그림과 같이 메모리가 1byte 단위로 길게 늘어선 구조로 생각하고, 메모리에 있는 데이터에 접근을 하기 위해서는 메모리 상의 주소가 있어야 합니다.

int a = 3;
위의 코드가 실행되면 int 크기인 4byte만큼의 메모리가 할당되고, 그 할당된 메모리에는 3이 저장됩니다. 이것을 그림으로 좌측과  같습니다.

1011로 시작되는 숫자가 메모리상의 주소를 나타냅니다. 이 숫자는 제가 임의로 나타낸 것이고, 프로그램 실행 시 OS에 의해서 할당됩니다. 한 칸은 1byte입니다.

a의 주소는 1014이며, int는 4바이트이므로 1014~1017까지 위치합니다.
 


 이제 실제로 a변수의 주소를 출력해 보겠습니다.
#include <stdio.h>

int main()
{
    int a = 3;

    printf("A addr: 0x%u => %d", &a, a);

    return 0;
}

printf("A addr: 0x%u => %d", &a, a);
%u는 unsigned int를 출력합니다. 메모리 주소는 일반적으로 10진수 보다 16진수(%x)를 일반적으로 사용하지만, 지금은 10진수를 사용하겠습니다.

눈여겨 보아야 할 것은 주소 연산자(&) 입니다. 변수 앞에 &를 붙이면 그 변수에 들어 있는 값이 아니라, 그 변수가 메모리에서 위치한 주소를 나타냅니다. 위를 컴파일하면 아래와 같이 나옵니다.  
 
> A addr: 1014 => 3

1014가 변수 a의 메모리상의 위치 입니다. 1014는 제가 만든 임의의 수이고 실행 해 보시면, 실제 a의 메모리 주소가 출력됩니다.


8.2 포인터(*)의 이해

1. 포인터의 선언

위와 같은 메모리상의 주소를 보관하기 위한 변수가 포인터입니다. 위의 소스에 포인터 변수를 사용해 보겠습니다. 포인터는 변수 앞에 *를 넣어 선언합니다.

#include <stdio.h>

int main()
{
    int a = 3;
    int *p;
    
    p = &a;
    
    printf("A addr: 0x%u => %d", p, *p);

    return 0;
}
 
int *p;
a 변수의 주소를 저장하기 위해 포인터 변수 p를 선언합니다. 포인터 변수의 선언에서 *의 위치는 변수형과 변수명 사이에 위치하며, 아래의 선언 모두 유효합니다.
int* p;
int *p;
int * p;

p = &a;
포인터 변수 p에 a의 주소를 넣습니다.주의할 점은 a의 값 3이 들어가는 것이 아니라, 메모리 주소가 들어 갑니다.

printf("A addr: 0x%u => %d", p, *p);
포인터 변수는 p처럼 그냥 사용하면 주소가 들어 가지만, 앞에 * 표시를 해주면 그 주소에 있는 값을 나타냅니다. p는 a 변수의 주소를 가지고 있기 때문에 *p는 a의 값인 3입니다.


2) 포인터 변수의 크기

포인터는 주소를 나타내기 때문에 동일한 크기를 가지고 있으면 됩니다. 32비트 주소 체계를 가지고 있는 컴퓨터에선 32비트의 크기만 있으면 됩니다.

그런데 포인터는 모든 변수 타입에 줄 수 있습니다. 그러면 char *, int *, float*는 어떤 의미가 있을 까요? 일단 아래의 소스로 각 타입의 포인터 변수의 크기를 알아 보겠습니다.

#include <stdio.h>
int main()
{
    printf("%d, %d, %d\n", sizeof(int *), sizeof(char *), sizeof(float *));

    return 0;
}

위의 소스를 실행하면 셋 모두 동일한 크기를 출력하며, 대부분의 컴퓨터에서 4(4byte = 32bit)를 출력합니다.

그러면 포인터 변수는 int와 char등과 달리 같은 크기를 가지고 있는데, 선언 시에 변수타입은 어떤 의미가 있는지 알아 보겠습니다.

#include <stdio.h>

int main()
{
    int n = 3;
    char c = 'c';

    int* pn = &n;
    char* pc = &c;

    printf("char point: %u, %u, %c\n", pc, pc + 1, *pc + 1);
    printf("int point: %u, %u, %d\n", pn, pn + 1, *pn + 1);

    return 0;
}

위의 코드를 실행 시키면 아래와 같이 출력됩니다.

> char point: 3221224580, 3221224581, d
> int point: 3221224576, 3221224580, 4

위에서 주소의 숫자는 중요하지 않고, 컴퓨터 마다 다르게 나옵니다.

주소 부분을 눈여겨 보면 char형 포인터는 1 증가시 3221224580에서 3221224581로 1이 증가되었습니다. 하지만 int형 포인터는 3221224576에서 3221224580로 4가 증가 되었습니다.

포인터 변수에 1을 더하면 그 타입의 크기만큼 주소가 증가 합니다. pn + 2이면 2X4=8로 12가 증가되게 됩니다. 이유는 int형 데이터를 다루기 위해서는, 4byte가 한 단위가 되어야 합니다. 1씩 증가되면 올바르게 메모리에서 int형 자료들을 다룰 수가 없습니다.

이는 아래의 그림과 같습니다.
사용자 삽입 이미지


3) 포인터 변수의 초기화

모든 변수들이 정확한 값을 가져야 하지만, 특히 메모리에 직접 접근하는 포인터 변수는 반드시 초기화 된 후에 사용하여야 합니다. 아래의 소스를 보겠습니다.

#include <stdio.h>

int main()
{
    int* pn;
    
    printf("%u, %d", pn, *pn);
    
    return 0;
}  

위와 같이 포인터 변수가 초기화 되지 않고 사용할 경우에는 매우 위험합니다.  지역변수 int* pn은 스택 메모리에 자리잡기 때문에, int * 를 위해 4byte가 할당되지만, 그 안에 내용은 이전에 쓰던 내용이 있을지 어떤 값이 있을지 모릅니다.
 
 *pn으로 그 주소의 4byte int형을 출력하는데, 주소에 들어있는 값이 초기화 되지 않은 쓰레기값이기 때문에 어떤 주소를 참조할지, 그 주소에는 어떤 값이 있을지 모르기 때문입니다.
 
잘못된 포인터(메모리)의 사용은 프로그램 실행시에 치명적인 오류를 일으킬 수 있기 때문에, 항상 주의하여 사용하여야 합니다.


4) 메모리 할당과 해제

우리가 지금까지 보았던 변수들의 최고 크기는 double로 8바이트까지 저장할 수 있습니다. 그러면 10mb되는 이미지를 메모리로 불러와서 작업을 할려면 어떻게 해야 할까요?
 
답은 malloc이란 함수로 시스템으로 부터 메모리를 할당 받아야 합니다. malloc은 다음과 같이 선언되어 있습니다.

void * malloc(size_t size);
여기서 size_t는 unsigned int로 보시면 됩니다. (후에 typedef가 나올 때, 설명하겠습니다.) size는 필요한 메모리의 byte수 입니다.

함수에서 void는 반환값이 없다는 뜻이지만, void 포인터(void*) 는 형없이 반환할테니, 필요에 따른 변수타입의 포인터로 형변환(type casting)해서 사용하라는 뜻입니다. 시스템은 size만큼의 메모리 할당에 성공하면 그 메모리가 시작하는 포인터를 반환하고, 메모리 부족 등의 이유로 할당에 실패할 경우에는 NULL을 반환합니다. NULL은 0(typedef)이며 포인터 등 메모리와 관련해선 0 대신 NULL로 사용합니다. 

아래의 소스를 보겠습니다.

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int i;  
    char* p;

    p = (char *)malloc(8);
    if(p == NULL)
    {
        printf("fail to alloc\n");
        return 0;
    }

    *p = 'H';
    *(p+1) = 'e';
    *(p+2) = 'l';
    *(p+3) = 'l';
    *(p+4) = 'o';
    *(p+5) = '\n';
    *(p+6) = '\0';

    printf("%s", p);
    
    free(p);

    return 0;
}

p = (char *)malloc(8);
malloc으로 8byte의 메모리를 요구합니다. 시스템은 다른 곳에서 사용하지 않는 8byte 메모리를 할당해서 그 시작 주소를 반환합니다.

 NULL이 반환되어 메모리 할당에 실패하였을 경우에는 오류메시지를 출력하고, 종료합니다.

사용자 삽입 이미지
*p = 'H';              // *(1013) = 'H';
*(p+1) = 'e';        // *(1014) = 'e';
*(p+2) = 'l';         // *(1015) = 'l';
*(p+3) = 'l';         // *(1016) = 'l';
*(p+4) = 'o';        // *(1017) = 'o';
*(p+5) = '\n';      // *(1018) = '\n';
*(p+6) = '\0';       // *(1019) = '\0';

메모리에 "Hello\n" 문자를 한바이트 씩 할당 합니다. *p+1로 하면 포인터(*)가 우선순위에 있으므로 값(*p)에 1을 더하는 의미가 됩니다.

그러므로 좌측 변수가 올자리에 와서는 안되기 때문에 컴파일 시 오류가 발생합니다. 그래서 주소가 1 증가되는 곳에 할당 시에는 괄호를 사용하여 *(p+1) = 'e'; 로 사용하여야 합니다.

C에서 문자열은 '\0'로 끝나는 char형 배열입니다. 배열은 이 다음장에 설명을 하겠습니다. 문자열은 마지막에 반드시 '\0'로 막아야 한다는 것을 명심해야 합니다. (NULL == '\0' == 0)

printf("%s", p);
printf의 %s는 문자열을 출력할때 사용합니다. 위의 소스를 실행하면 Hello\n를 출력합니다.

free(p);
마지막 free 함수는 이제 메모리 사용이 끝났으니, 반환하겠다는 의미입니다. freep이 후로는 포인터 p를 사용하여서는 안됩니다. free는 malloc과 한쌍으로 malloc으로 할당 받은 메모리는 반드시 free로 반환을 해야 합니다.

특히 반복되는 작업에서 사용한 메모리를 반환하지 않으면, 메모리 누수가 계속 일어나 프로그램과 OS에 치명적인 오류를 일으킬 수 있습니다.

이상 포인터에 대해서 간단히 설명하였습니다. 포인터는 다음 장에 설명 할 배열과 밀접한 관계에 있기 때문에, 다음 장에서 다시 논해 보도록 하겠습니다.

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

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
5. 연산자  (0) 2007.06.13