언어 자료구조 알고리즘/C 언어 문법

24. 배열의 사용

언제나휴일 2009. 8. 19. 05:47
반응형
배열의 사용(정적)

 

 다루는 내용

  - 배열과 포인터

  - 포인터와 관련된 연산

  - 배열의 사용


C언어에서 배열명은 원소 타입의 포인터 상수 취급을 받는다고 앞선 항목에서 얘기를 하였다.

이러한 이유로 배열과 포인터는 따로 떼어내서 설명을 하는 것보다 같이 하는 것이 이해하기 쉬울 수 있다.

그리고 포인터와 관련된 연산(산술 연산 중에 가감 연산, 간접 연산, 인덱스 연산, 주소 연산)의 동작 원리를 명확하게 이해를 하고 있어야 능숙하게 배열을 사용할 수가 있다.

 

먼저 목적에 따라 배열을 선언하는 예를 살펴보자. 

50명의 국어 성적                                                        int kor_scores[50];

50명의 국어,영어,수학 성적                                          int scores[50][3];

50명의 국어 성적의 위치 정보                                       int *rkor_scores[50];

50명의 국어,영어,수학 성적의 위치 정보                         int *rscores[50][3];

 

 잠깐!

여러분은 주위에서 "3차원 이상의 배열을 자주 사용되지 않는다"라는 얘기를 흔히 들었을 것이다.  틀린 얘기는 아니다.  다차원 배열보다 데이터를 관리하기 용이하게 사용자 정의 타입을 정의하여 사용하면 편의성과 가독성을 높일 수 있기 때문에 맞는 얘기다.  하지만, 단순히 그러한 얘기때문에 알 필요가 없다는 것에 대해서는 동의할 수가 없다.  다차원 배열에 대해서 명확한 이해를 통해 보다 나은 사용자 정의 타입을 정의 할 수 있고 이의 장점을 이해를 할 수 있다고 생각한다. (이에 대한 얘기는 구조체에서 다룰 것이다.)


이번에는 선언된 배열을 보고 원소 개수와 원소 타입에 대해 살펴보자.

선언

원소 개수

원소 타입

int kor_scores[50]; 50 int
int scores[50][3]; 50 int [3]
int *rkor_scores[50]; 50 int *
int *rscores[50][3]; 50 int *  [3]

 

 

Look & Feel & Think

다차원 배열을 입력 매개변수로 받기 위해서는 다차원 배열 혹은 배열 포인터를 사용을 한다.  그렇다면 반환을 하고 싶을 때에는 어떻게 해야 할까?


위의 예제처럼 그대로 표현을 구문오류가 발생을 한다.  이러한 경우에는 아래의 예제처럼 typedef을 통해 새로운 타입명을 정의하여 이를 사용하면 문제를 해결할 수 있다.


배열의 선언의 의미를 파악을 했으니 이번에는 배열명을 인자로 넘겨서 사용하는 예들에 대해 살펴보도록 하자.

 

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void InitialDemo(int *base,size_t size);
void PrintDemo(int base[],size_t size);
void main()
{
    int kor_scores[50]; 
    int i = 0;

    InitialDemo(kor_scores,50);
    for(i=0;i<5;i++)
    {
        PrintDemo(kor_scores+(i*10),10);
    }
}

void InitialDemo(int *base,size_t size)
{
    srand((unsigned)time(NULL));
 
    while(size)
    {
         *base = rand()%100+1;
         base++,size--;
    }
}

void PrintDemo(int base[],size_t size)
{
    size_t lcnt = 0;

    for(lcnt=0;lcnt<size;++lcnt)
    {
         printf("%3d ",base[lcnt]);
    } 
    printf("\n");
}


위의 예를 보면 int kor_scores[50]; 으로 선언된 배열을 활용함에 있어 호출하는 형태를 두 가지 형태의 예를 들었다.

InitialDemo(kor_scores,50); 

PrintDemo(kor_scores+(i*10),10); 

첫 번째 호출 예에 대한 설명은 생략을 하고 두 번째의 예에 대해서만 얘기를 해 보자.

배열명은 원소 타입에 대한 포인터 상수 타입으로 취급이 된다고 앞서 설명하였다. 

또한, 포인터 + 정수의 경우 연산 결과는 포인터이며 의미는 포인터가 갖는 주소에서 포인터 원소 타입의 사이즈가 정수개 지나고 난 뒤의 메모리 주소가 연산 결과라는 얘기를 산술 연산자에서 얘기를 했었다.  정확히 이해가 가지 않는다면 산술 연산자 부분을 확인하라.

즉, kor_scores + (i*10) 의 연산 결과는 kor_scores에서 i*10 원소가 있는 메모리 주소이며 타입의 변화는 없다.

 

피호출 함수의 선언을 보면 원소에 대한 포인터 형식으로 인자를 받거나 혹은 원소에 대한 배열 형식으로 인자를 받는 두 가지 형태를 예를 보이고 있다.  이 두가지 형태의 차이는 입력 인자명을 변경이 가능한가 그렇지 않은가의 차이 외에는 별반 차이가 없다.  이는 배열명은 포인터 상수 취급을 받기 때문에 변경이 불가능하다는 것에서 부터 발생한 것이다.

 

또한, 피호출 함수 내부에서 사용하는 것을 보면

*base 와 같이 간접 연산을 통해 원소에 접근을 하거나 base[lcnt]와 같이 인덱스 연산을 통해 원소에 접근을 하고 있는 것을 알 수 있다.  여러분이 배열과 포인터를 제대로 사용하기를 원한다면 목적에 따른 선언, 선언에 대한 의미(원소 타입, 원소 사이즈 등), 배열과 포인터에 관련된 연산자들의 연산 행위에 대한 정확한 이해를 요구한다.  이번 기회에 다시 한 번 지시/주소/인덱스/간접 연산자 부분에 대해 살펴보도록 하자.

 

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


void InitialDemo(int (*base)[3],size_t size);
void PrintDemo(int base[][3],size_t size);
void PrintAStu(int *base,size_t size);

 

void main()
{
    int scores[50][3];
    int i=0;
 
    InitialDemo(scores,50);
    for(i=0;i<5;i++)
    {
         printf("%d~%d 번 성적\n",i*10+1,(i+1)*10);
         PrintDemo(scores+(i*10),10);
         getch();
    }
}

 

void InitialDemo(int (*base)[3],size_t size)
{
    size_t subcnt=0;
    srand((unsigned)time(NULL));
 
    while(size)
    {
         for(subcnt = 0;subcnt<3;++subcnt)
         {
              (*base)[subcnt] = rand()%100+1;
         }
         base++,size--;
    }
}

 

void PrintDemo(int base[][3],size_t size)
{
    size_t lcnt = 0;

    for(lcnt=0;lcnt<size;++lcnt)
    {
         PrintAStu(base[lcnt],3);
    } 
}

 

void PrintAStu(int *base,size_t size)
{
    while(size)
    {
         printf("%3d ",*base);
         base++, size--;
    }
    printf("\n");
}


 위의 예도 별반 다른 내용이 없다.  선언된 배열이 int scores[50][3]; 이고 원소 타입이 int [3]이기 때문에 이러한 원소에 대한 포인터혹은 이러한 원소에 대한 배열을 통해 인자를 받아 처리하고 있음을 알 수 있다.

void InitialDemo(int (*base)[3],size_t size); //원소에 대한 포인터로 인자를 받음
void PrintDemo(int base[][3],size_t size);   //원소에 대한 배열로 인자를 받음

 

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


void InitialDemo(int *base,size_t size);
void PrintDemo(int base[],size_t size);
void IndirectSorted(int *base,int **sbase,size_t size);
void SortedScore(int *base,int **sbase,size_t size);
void Swap(int **in1,int **in2);
int **GetMaxPos(int **sbase,size_t asize);


void main()
{
    int kor_scores[50];
    int *rkor_scores[50];
    int i = 0;

    InitialDemo(kor_scores,50);
    for(i=0;i<5;i++)
    {
        PrintDemo(kor_scores+(i*10),10);
    }
    printf("\n성적순\n");

    IndirectSorted(kor_scores,rkor_scores,50);
    SortedScore(kor_scores,rkor_scores,50);
}

 

void InitialDemo(int *base,size_t size)
{
    srand((unsigned)time(NULL));
 
    while(size)
    {
         *base = rand()%100+1;
         base++,size--;
    }
}

 

void PrintDemo(int base[],size_t size)
{
    size_t lcnt = 0;

    for(lcnt=0;lcnt<size;++lcnt)
    {
         printf("%3d ",base[lcnt]);
    }
    printf("\n");
}

 

void IndirectSorted(int *base,int **sbase,size_t size)
{
    size_t lcnt = 0;
    for(lcnt=0;lcnt<size;++lcnt)
    {
         sbase[lcnt]=base+lcnt;
    }

    for(lcnt=0;lcnt<size-1;++lcnt)
    {
         Swap(sbase,GetMaxPos(sbase,size-lcnt));
         sbase++;
    }
}


void Swap(int **in1,int **in2)
{
    int *temp = *in1;
    *in1 = *in2;
    *in2 = temp;
}


int **GetMaxPos(int **sbase,size_t asize)
{
    int **maxpos = sbase;
    sbase++,asize--;
    while(asize)
    {
         if(**maxpos < **sbase)
         {
              maxpos = sbase;
         }
         sbase++,asize--;
    }
    return maxpos;
}


void SortedScore(int *base,int **sbase,size_t size)
{
    int cnt=1;
    while(size)
    {
         printf("%2dth:%2d번:%3d ",cnt,(*sbase-base)+1,**sbase);
         sbase++,size--;
         if(cnt%5 == 0)
         {
              printf("\n");
         }
         cnt++;

    }
}


위 예제는 위치 정보를 원소로 하는 경우의 예이다.

 int kor_scores[50];  여기에는 번호순으로 50명의 성적을 보관을 하기 위한 목적의 배열이다.  번호순의 성적을 변화를 주지 않으면서 성적순으로 자료를 출력을 하기 위해 int *rkor_scores[50]; 성적순으로 성적의 위치정보를 보관할 목적의 배열을 선언하였다. 

void IndirectSorted(int *base,int **sbase,size_t size); 을 통해 간접 정렬을 하는데 base에는 원본 데이터 들이 있는 버퍼(배열)이고 sbase는 원본 데이터들의 위치 정보들을 보관하기 위한 버퍼(배열)이다.  그리고 size는 버퍼(배열)의 사이즈이다.

호출부를 보면 IndirectSorted(kor_scores,rkor_scores,50); 이와같이 목적에 맞게 선언된 배열명을 통해 버퍼의 시작 주소를 넘기는 것을 볼 수 있다. 

 

void IndirectSorted(int *base,int **sbase,size_t size) 함수 내부를 보면 다음과 같은 표현들이 있다.

 

sbase[lcnt]=base+lcnt;
Swap(sbase,GetMaxPos(sbase,size-lcnt));

 

sbase는 int *를 원소로 하는 포인터형식의 변수이므로 sbase[lcnt]는 원소 타입인 int *가 되며 base+lcnt 또한 연산 결과가 int *이다.  sbase의 목적이 원본 데이터들의 위치 정보들을 보관하기 위한 목적이므로 적당한 표현이라 할 수 있다.

sbase의 목적이 원본 데이터의 복사본을 보관하기 위함이 아니며 그러한 복사본을 보관하기 위한 메모리를 할당 받은 적도 없기 때문에 *sbase[lcnt] = base[lcnt]; 와 같은 표현은 논리적 오류에 빠지게 된다.(문법적으로 맞는 것과 논리적으로 타당한 것은 다른 얘기이다.)

 

이와 같이 배열과 포인터를 사용할 때에는 사용하기 위한 목적에 맞게 선언을 하고 선언한 것에 맞게 사용을 해야 한다.  C와 C++언어에서는 컴파일러에서 특정 표현이 문법적으로 맞는지에 대한 점검을 해 주지만 실제 할당된 메모리 공간인지에 대한 모든 책임은 개발자에게 주어진다.  유연한 표현을 제공하는 대신 신뢰성에 대한 많은 부분은 개발자의 몫이 되므로 이를 간과하지 말아야 겠다.

 

본 항목에 대한 이해는 배열과 포인터의 형식에 대한 이해와 산술 연산자 , 지시/주소/인덱스/간접 연산자 에 대한 정확한 이해를 바탕으로 이루어져야 하므로 다시 한 번 짚고 넘어가길 바란다.  그리고, 이를 이해했다는 것을 스스로 증명하기 위해 어떠한 프로그램을 작성하면 좋을 지 고민을 하고 그러한 프로그램을 작성해 보라.  그 고통의 깊이가 깊다면 성장 또한 클 것이다.  (물론, 짚고 넘어갈 항목을 배제하고 생기는 고통은 잘못된 학습 방법으로 인한 소모적인 고통일 뿐 성장을 위한 고통이라 생각하지 않는다.-본인 생각) 
 

 

 

반응형

'언어 자료구조 알고리즘 > C 언어 문법' 카테고리의 다른 글

23.배열  (0) 2009.08.19
22. 제어문 - 반복문  (0) 2009.08.19
21.제어문 - 선택문  (0) 2009.08.19
20. 제어문 - 조건문  (0) 2009.08.19
19. 기본입출력 - 입력  (0) 2009.08.19
18. 기본 입출력 - 출력  (0) 2009.08.19
17.기본 입출력 개요  (0) 2009.08.19
16. 지시/주소/인덱스/간접연산자  (0) 2009.08.19
15. 비트/ 쉬프트 연산자  (0) 2009.08.19
14. 비교/논리 연산자  (0) 2009.08.19