1619, 1/81 회원가입  로그인  
   sihun1113
   [C기초] 08 - 배열과 포인터

http://www.hackerschool.org/HS_Boards/zboard.php?AllArticle=true&no=1743 [복사]


*----------------------------------------------------------------------------------------------------------*

이 카테고리는 그동안 배웠던것이라던가 공부하면서 알게된 내용들을 중심으로 혹여 참고가 될 수 있을까 싶은 내용들을

모아둔 곳입니다. 따라서 이 글들을 대상이 되는 분들은.. 프로그래밍을 갓 시작했거나 개념이 마구마구 흔들리시는 분들

정도가 되겠습니다. 그러니 너무 욕하지는 말아주세요 =_=;;;



혹시 제가 잘못 알고 있는 부분들이라던가 궁금한 것이 있으면 쪽지 남겨주시거나 리플을 달아주시면 감사하겠습니다.

*----------------------------------------------------------------------------------------------------------*



오랜만에 스터디 재개합니다 =_=;



지난 스터디까지는 변수와 포인터에대해 개략적으로 공부했었죠?

이번엔 배열에 대해 알아보고, 배열이 포인터와 무슨 관계가 있는지 알아보도록 하죠.



자~, 프로그램을 작성하다보면 우리는 무수히 많은 변수들을 만나게 됩니다.

그리고, 필요에 따라서 각종 이름들을 붙여서 사용하게 되지요.



정수형 변수 100개가 필요한 어떤 프로그램이 있다고 해봅시다.



int num1, num2, num3, num4, num5, num6, num7, num8, ... , num99, num100;



num1 = 0;

num2 = 0;

num3 = 0;

num4 = 0;

...

num99 = 0;

num100 = 0;



이게 뭔짓입니까?

정수변수 100개가 필요하다고해서 이렇게 사용할 생각을 하면.. 아찔하지 않나요?



만약 배열이라는 녀석이 없었다면 아마 대부분 프로그래머들은 이렇게 프로그램을 짜고 있었을겁니다.(물론 고급 유저라면 다른 방법을 만들어냈을지도 ㅋ..)



우리는 배열이라는 녀석을 사용해



int num[100];



for( int i=0; i<100; i++ )

    num[i] = 0;



(또는 int num[100] = {{0}}; 으로 초간단 사용 가능 +_+ )

이렇게 간단하게 사용 할 수 있게되었습니다.



배열 참 간편하죠? 그리고 우리는 배열을 사용해서 엄청나게 많은 변수라도 쉽게 접근하고 사용할 수 있게 되었습니다.



배열의 선언이나 사용법에 관해서는 쉬우니 아실것이라 생각하고 넘어가도록 하겠습니다.



제가 관심을 가지고자 하는 부분은 배열과 포인터와의 연관 관계입니다.

우리는 늘상 배열의 이름인 num[](위의 코드의 경우)이 변수의 이름인것으로 알고 있었습니다. 그리고 num[32], num[79] 이런식으로 변수를 읽어들이거나 저장하는데 사용하였죠.

어찌보면 맞는 말이지만 정확하게 따지면 배열의 변수명은 일반변수가 아닌 포인터변수 입니다. 그리고 []안의 숫자는 포인터로부터의 인덱스번호라고 합니다.



여러분들 중에는 배열을 처음 배울때 왜 하필 인덱스 번호는 1,2,3,... 순으로 쓰이질 않고 0,1,2,... 식으로 쓰이느냐라고 불평하신 분들이 계실겁니다. 그냥 직관적으로 첫번재 변수면 val[1], 두번째 변수면 val[2] 등으로 쓰면 되지 구지 첫번째를 val[0], 두번째는 val[1] 등으로 하나씩 차이가 나게 쓰는지에 대해서 말이지요.

분명 보기에 따라서는 그냥 쓰는것이 나을 수도 있습니다. 하지만 배열변수의 변수명이 포인터변수라는 것을 이해하게 되면 왜 인덱스 번호의 시작번호가 1이 아닌 0인지를 이해 할 수 있습니다.



배열 변수가 선언되는 모양을 이해하기 위해 다시 메모리 블럭 그림을 보도록 하지요.



(□ 한칸은 1Byte, 초기화되지 않은 상태로 무슨 값이 들어가 있는지는 알수 없음)

       0 1 2 3  4 5 6 7  8 9 a b  c d e f  0 1 2 3  4 5 6 7  8 9 a b  c d e f

0x0- □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□

0x2- □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□

0x4- □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□

0x6- □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□



여기에 6개의 길이를 갖는 정수형 변수배열을 선언해 봅시다.



int num[6];



아래와 같은 메모리 블럭이 할당될 것입니다.



       0 1 2 3  4 5 6 7  8 9 a b  c d e f  0 1 2 3  4 5 6 7  8 9 a b  c d e f

0x0- ■■■■ ■■■■ ■■■■ ■■■■ ■■■■ ■■■■ □□□□ □□□□

0x2- □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□

0x4- □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□

0x6- □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□

각 변수의 주소

num[0]:0x00 num[1]:0x04 num[2]:0x08 num[3]:0x0c num[4]:0x10 num[5]:0x14



또 char형 변수 10개를 선언해 보죠.



char ch[10];



       0 1 2 3  4 5 6 7  8 9 a b  c d e f  0 1 2 3  4 5 6 7  8 9 a b  c d e f

0x0- ■■■■ ■■■■ ■■■■ ■■■■ ■■■■ ■■■■ ■■■■ ■■■■

0x2- ■■□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□

0x4- □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□

0x6- □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□ □□□□

각 변수의 주소

ch[0]:0x18  ch[1]:0x19  ch[2]:0x1a  ch[3]:0x1b  ch[4]:0x1c

ch[5]:0x1d  ch[6]:0x1e  ch[7]:0x1f  ch[8]:0x20  ch[9]:0x21



저는 위에서 배열변수의 변수명은 일반변수가 아닌 포인터 변수라고 하였습니다.

포인터 변수라면 주소를 가지고 있겠지요? 그렇다면 num이라는 포인터 변수는 주소를 가지고 있을것입니다. 그것이 바로 저 배열의 대표주소인 0x00을 가지고 있다는 것입니다.



그렇다면 뒤에 붙어있는 인덱스번호의 의미는 무엇일까요?



포인터변수는 그 주소가 가리키는 변수의 타입을 알고 있다고 했습니다. 즉, 포인터 변수가 사용되어질 때 컴퓨터는 그 변수의 크기를 알고 있다는 얘기인데 포인터변수는 독특한 특징을 가지고 사용됩니다.



일반 정수형변수나 실수형 변수등은 변수에 +1을 하게되면 해당 값에 +1 된 값이 저장됩니다.

즉, 100을 입력하고 +1을 하면 101이, 13.2를 입력하고 +1을 하면 14.2가 저장되는 것이지요.

마찬가지로 +2를 하던 -100을 하던.. 결과는 흔히 말하는 계산식의 결과를 얻게 됩니다.



하지만 이 배열, 즉, 포인터의 연산과정은 우리의 예상과는 다소 다른 점이 있습니다.

만약 num[2]의 주소인 0x08에 +1을 했을때와 ch[1]의 주소인 0x19에 +1을 했을때 과연 어떤 차이가 나타날 까요?



아래 코드를 직접 컴파일 해서 확인 해 보세요.

*---------------------------------------------------------------------------*

#include <stdio.h>



int num[4];
char ch[10];



int main(void)
{
    int i;


    for( i=0; i<4; i++ )
        printf( "num[%d]의 주소 : %d\n", i, &num[i] );



    for( i=0; i<10; i++ )
        printf( "ch[%d]의 주소 : %d\n", i, &ch[i] );



    printf( "********************************************************\n" );



    printf( "num[2]의 주소에 +1을 했을때의 결과 : %d+1 = %d\n", &num[2], &num[2]+1 );
    printf( "ch[1]의 주소에 +1을 했을때의 결과 : %d+1 = %d\n", &ch[1], &ch[1]+1 );



    return 0;
}

*---------------------------------------------------------------------------*

결과

*---------------------------------------------------------------------------*

num[0]의 주소 : 4336376
num[1]의 주소 : 4336380
num[2]의 주소 : 4336384
num[3]의 주소 : 4336388
ch[0]의 주소 : 4336392
ch[1]의 주소 : 4336393
ch[2]의 주소 : 4336394
ch[3]의 주소 : 4336395
ch[4]의 주소 : 4336396
ch[5]의 주소 : 4336397
ch[6]의 주소 : 4336398
ch[7]의 주소 : 4336399
ch[8]의 주소 : 4336400
ch[9]의 주소 : 4336401
***********************************************************
num[2]의 주소에 +1을 했을때의 결과 : 4336384+1 = 4336388
ch[1]의 주소에 +1을 했을때의 결과 : 4336393+1 = 4336394
*---------------------------------------------------------------------------*



뭔가 이상하죠? 일반적인 예상대로라면 num[2]의 주소인 4336384에 +1이 되면 ch[1]의 경우처럼

+1만 된 4336385가 되어야 할 것 같은데 +4가 된 4336388이 되어 있습니다. 왜 이런일이 생길까요?



그 이유는 포인터 연산이라는 특이한 녀석에서 찾을 수 있습니다. 포인터 변수는 그 저장 된 값을 증/감 연산할 경우 해당 포인터 변수가 참조하는 변수의 데이터 타입의 크기를 참조하여 연산을 수행하게 됩니다. 따라서 num[2] 변수의 주소에 +1을 했을때 num 변수의 데이터타입인 int형의 크기인 4(Byte)만큼 증가가 된 것 입니다. 마찬가지로 ch[1]의 주소에 +1을 했을때는 char형의 크기인 1(byte)만큼 증가 된 것이지요. 만약 double형 배열이라면? 당연히 8(Byte)만큼씩 증가나 감소를 하게 됩니다.



즉, &num[2]+1은 &num[3]과, &ch[1]+1은 &num[2]와 동일한 의미를 갖게 됩니다.

연장선상으로 생각해 볼때 &num[0]+3은 무엇과 동일할까요?



num[0]의 주소 4336376에 3*4(Byte)인 12가 더해진 4336388, 즉, num[3]의 주소값과 동일해집니다.



여기서 뭔가 하나 삘~이 딱 꽂히지 않습니까?



네.. 맞습니다. num[0]+X 의 주소는 num[X]의 주소와 동일합니다.

그리고 num[X]+Y의 주소는 num[X+Y]와 동일하구요.



여기서 우리는 한가지 사실을 유추 해 볼 수 있습니다.

처음에 우리는 num 포인터 변수라고 했으며 변수값은 num[0]의 주소와 동일하다고 했습니다.

이런 사실들을 연관지어 생각해 볼 때 num+1이라는 녀석은 &num[1]과 동일한 의미를 갖는다고 볼 수 있습니다. num+3은 &num[3]과 동일한 의미를 갖게 되겠지요.





정리합니다.

위의 사실들에서 짐작해 볼 수 있듯, num이라는 포인터 변수에 +/-를 통해 연산을 할 수 있는데

증감 된 값이 무엇이냐에 따라 해당 순번의 변수의 주소를 얻어 올 수 있습니다.

결론적으로 말하면 num[]의 괄호 속의 인덱스는 num[0] 변수의 주소로부터 얼마나 떨어져 있는 변수냐와 같은 의미를 갖습니다.



    &num[0]+0 = &num[0]

    &num[0]+1 = &num[1]

           :              :

    &num[0]+x = &num[x]

           :              :



따라서 인덱스 번호가 0부터 시작하도록 지정한 데에는 그 나름의 의미가 있다는 뜻이지요.





또 한가지 특이한 실험을 하나 해 볼까요?

저 위의 코드에선 int형 배열변수인 num이 4개만 선언 되어 있습니다. 따라서 num[5]나 num[7]은 사용 할 수 없겠죠. 그러나 아래 코드를 입력해서 컴파일 해 볼까요?

*---------------------------------------------------------------------------*

#include <stdio.h>



int num[4];
char ch[10];



int main(void)
{
    int i;


    for( i=0; i<4; i++ )
        printf( "num[%d]의 주소 : %d\n", i, &num[i] );

    for( i=0; i<10; i++ )
        printf( "ch[%d]의 주소 : %d\n", i, &ch[i] );



    printf( "**********************************************\n" );



    printf( "num[5]의 주소 : %d\n", &num[5] );
    printf( "ch[13]의 주소 : %d\n", &ch[13] );



    return 0;
}

*---------------------------------------------------------------------------*

결과

*---------------------------------------------------------------------------*

num[0]의 주소 : 4336376
num[1]의 주소 : 4336380
num[2]의 주소 : 4336384
num[3]의 주소 : 4336388
ch[0]의 주소 : 4336392
ch[1]의 주소 : 4336393
ch[2]의 주소 : 4336394
ch[3]의 주소 : 4336395
ch[4]의 주소 : 4336396
ch[5]의 주소 : 4336397
ch[6]의 주소 : 4336398
ch[7]의 주소 : 4336399
ch[8]의 주소 : 4336400
ch[9]의 주소 : 4336401
**********************************************
num[5]의 주소 : 4336396
ch[13]의 주소 : 4336405
*---------------------------------------------------------------------------*

분명 존재하지도 않는 num[5]변수와 ch[13]변수의 주소를 참조하고 있습니다. 어째서 이런 일들이 벌어지는 것일까요?

이것이 바로 포인터의 특징이자.. 위험한 요소인것이지요.

위에서 설명했던 포인터 연산의 특징으로 인해 시작 주소를 알고 있으면 위로든 아래로든 자기 멋대로 위치를 바꾸어 참조 할 수 있다는 뜻 입니다.



심지어는 음수 인덱스를 이용해서 num[-3]이나 num[-100] 같은 어처구니없는 참조까지 가능합니다 =_=;; 물론 인덱스 스탭(자료형 크기)는 유지가 되지요.

때로는 이런 부분을 간과해서 치명적인 메모리 참조 오류를 낼 수도 있고, 심지어는 한대의 PC를 완전 바보 멍텅구리로 만들 수도 있습니다.(물론 요즘의 OS들이 워낙 좋아져서 왠만한 참조 오류나 시스템에 치명적일 수 있는 부분은 다 커버를 하고 있죠.)

그리고, 이를 고의적으로 악용한 예가 바로 흔히 이야기하는 해킹툴, 핵 입니다.

물론 어느정도 수준 이상의 메모리 관리 기술을 필요로 하지요.





최종으로 정리합니다.

배열변수는 포인터입니다. 따라서 일반 실수연산이 적용되지 않고 포인터 연산이라는 특이한 연산을 할 수 있습니다. 그리고, 배열변수가 갖고 있는 인덱스는 포인터 시작 주소로 부터 인덱스크기 만큼 떨어져 있는 주소의 변수를 참조하라는 의미를 갖습니다.

때문에 인덱스의 값에 따라 범위를 자기 마음대로 넘나들 수 있으며 이를 얼마나 잘 관리하느냐에 따라서 배열을 마스터 했는지가 판가름 난다고할 수 있습니다.





다음 스터디는 원래 다차원 포인터/배열을 다뤄 볼 생각이었는데, 먼저 C의 기본을 다시 짚고 넘어가야 할 필요성이 느껴지더군요..

그래서 다음번 스터디의 주제는 기초중의 기초인 if, if/else, switch/case 문에 대해서 알아볼까 합니다.

그럼 좋은 하루 되시길..



  Hit : 7588     Date : 2011/05/01 11:30