ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Store data without Scanf (C Program) 분석 :: 가변인수
    C/C++/Codeplaza 2013. 12. 2. 21:48



    http://www.codeplaza.org/2013/11/store-data-without-scanf-c-program.html

    ( 위의 그림이나 링크를 클릭하시면 해당 사이트의 원본 글이 새 창으로 열립니다 ^^ )


    codeplaza.org에 올라온 글입니다. jasmin shah라는 분이 작성해주셨네요.

    프로그램은 Omarmokhtar Altahwy ( omarmokhtar10@yahoo.com )이라는 분이 작성한 것입니다.

    해당 프로그램은 scanf 함수를 사용하지 않고, 사용자로부터 데이터를 입력받고 해당 데이터를 그에 맞는 자료형을 가진 변수에 저장하는 동작을 합니다.


    #include <stdio.h>

    #include <stdlib.h>

    #include <string.h>

    #include <stdarg.h>

     

    /*

     

    this program takes inputs from user and stores them into specific locations wherever  user specify

    without using scanf function.

     

    */

     

    void omarin(char *s, ... ); // this function do the functionality of scanf .

     

    int main()

    {

        printf("\n\t\t>> NOTES <<\n");

        printf("\nthe program will take inputs from you and stores them into variables without \

    scanf function.\n\n");

        printf("you can change types of input you want to enter from main. \n\n");

        printf("now please enter integer value , float value ,and  char ,please put between them\

    spaces and finally press enter to stores them in variables.\n\n ");

     

        int integer_value;

        float float_value;

        char c;

        omarin("d f c ",&integer_value,&float_value,&c);

        printf(" integer = %d\tfloat = %f\tchar = %c\n",integer_value,float_value,c);

     

        return 0;

    }

     


    위의 main 함수에서는 scanf 함수를 사용하지 않는 대신 사용자 정의 함수인 omarin 함수를 사용합니다.

    이는 프로그램 작성자의 이름인 Omarmokhtar와 input을 더해 명명된 이름이 아닐까 추측됩니다.....


    void omarin(char *s, ... );


    scanf 함수를 대체하는 omarin 함수의 원형은 위와 같은 형태로 가변 인수를 사용합니다. 고정적인 형태로 사용될 고정 인수를 먼저 주고 이후의 인수들은 ... 으로 표현함으로써 인수의 갯수가 들죽날죽할 수 있는 여지를 주는 것입니다. 가변 인수에 대해서는 아래의 블로그들을 참고하시면 도움이 될 것입니다.


    시다바리 :: 가변 인수 (va_start(), va_end(), va_arg, va_list)

    http://jangsalt.tistory.com/entry/%EA%B0%80%EB%B3%80-%EC%9D%B8%EC%88%98-vastart-vaend-vaarg-valist


    HardCore in Programming :: 가변인자를 이용한 함수(va_list)

    http://kukuta.tistory.com/10


    Store data without Scanf에서의 핵심은 바로 이 가변 인수에 있습니다. scanf 함수를 대체하는 omarin 함수는 가변인수를 사용하여 마치 scanf 함수와 같은 동작을 하게되는 것입니다. 어떻게 하나구요? 메인에서 omarin 함수를 호출하는 구문을 살펴보도록 하겠습니다.


    omarin("d f c ", &integer_value, &float_value, &c);


    첫 번째 인자는 입력받을 형식을 나타내는 것입니다. 기존의 scanf 함수였다면 "%d %f %c "였을 부분이 "d f c "로 표현되었습니다. 해석하자면 d = 정수, f = 실수, c = 문자의 순으로 각 자료형에 해당하는 값을 하나씩 입력 받기로하는데, 각 자료들은 space로 구분하겠다는 것입니다. 그리고 이후에는 입력받은 값을 저장할 변수들의 주소들을 순차적으로 나열하는 것입니다. 


    어떻습니까? scanf 함수의 구조와 매우 유사하지 않습니까? Store data without Scanf 라는 프로그램은 scanf를 사용하지 않은 대신 scanf와 유사한 함수를 만들어 사용하는 것입니다. 신박하네요. 그럼 이 scanf를 흉내내는 사용자 정의 함수 omarin 함수는 어떻게 동작하는지 알아보도록 하겠습니다.


    void omarin(char *s, ... )

    {

        char sr[20][20]={'\0'};

        char st[30]={'\0'};

        gets(st);

        int lenst=strlen(st);

        int counter=0;

        int arr[20]={0};

        int k;

        for(k=0;k<=lenst;k++)

        {

            if(st[k]==' '||st[k]=='\0')

            {

                arr[counter]=k;

                counter++;

     

            }

        }

        int h;

        int index=0;

        int jk=0;

        for(h=0;h<10;h++)

        {

            int jk=0;

     

            if(arr[h]!=0)

                {

                    int j;

                    for(j=index;j<arr[h];j++)

                    {

                        sr[h][jk]=st[j];

                        jk++;

                    }

                    index=arr[h];

     

                }

        }

     

     

     

        union Printable_t {

                                int *     i;

                                float *   f;

                                char *    c;

                                char *    s;

                          } Printable;

        va_list arguments;

        va_start(arguments,s);

        int len=strlen(s);

        int i;

        int w=0;

        for(i=0;i<len;i++)

        {

            switch(s[i])

            {

            case 'd':

     

                   Printable.i=va_arg(arguments,int*);

     

                   *(Printable.i)=atoi(sr[w]);

     

                   w++;

                   break;

            case 'f':

                   Printable.f=va_arg(arguments,float*);

                   *(Printable.f)=atof(sr[w]);

                   w++;

                   break;

            case 'c':

                     Printable.c=va_arg(arguments,char*);

                     *(Printable.c)=(sr[w][1]);

                     w++;

                     break;

     

            }

     

        }

     

    } 


    omarin 함수는 앞서 설명한 것과 같이 가변 인수를 사용합니다. main으로부터 호출될 때에 넘겨받은 인수는 차례대로 "d f c "와 &integer_value, &float_value 그리고 &c 입니다. 해당 인수들에 대한 사용은 뒤에서 알아보기로하고 초기화 해당 함수의 소스 코드를 살펴보도록 하겠습니다.


    char sr[20][20]={'\0'};

    char st[30]={'\0'};

    gets(st)


    가장 먼저 이차원 배열 sr과 배열 st를 각각 NULL 값으로 초기화합니다. 그리고 gets 함수를 이용하여 사용자 입력을 받아 st배열에 저장합니다. 여기서 gets 함수를 통해 입력받는 문자열은 '정수 실수 문자' 형태로 이루어진 문자열입니다. 예를 들면 "10 12.33 h"와 같은 형식이지요. 왜냐구요? gets 함수를 통해 사용자 입력을 받을 시점에 콘솔창에는 "now please enter integer value , float value ,and  char ,please put between them spaces and finally press enter to stores them in variables."라는 문구가 떠있습니다. 그건 이 프로그램의 작성자인 Omarmokhtar Altahwy님이 정해둔 것이기 때문에 어쩔 수 없습니다.


    int lenst=strlen(st);

    int counter=0;

    int arr[20]={0};

    int k;

    for(k=0; k<=lenst; k++)

    {

    if(st[k]==' ' || st[k]=='\0')

    {

    arr[counter]=k;

    counter++;

    }

    }


    이어서 입력받은 문자열 st의 길이를 lenst에 저장하고 배열 arr을 선언하고 초기화합니다. for문을 이용해 st의 요소 중에서 빈칸이나 널문자가 나오는 인덱스 값을 arr 배열에 순차적으로 저장합니다. 이때 arr의 인덱스를 나타내기위해 counter가 사용됩니다. for문이 종료되면 결과적으로 아래 그림과 같은 모습이 되는 것입니다.



    즉, arr 배열은 gets 함수를 통해 입력받은 문자열을 원래 목적인 정수, 실수, 문자로 나누기 위한 정보를 담는 것입니다. 이 정보를 만들어두고 다음 동작으로 넘어가게 됩니다.


    int h;

    int index=0;

    int jk=0;

    for(h=0; h<10; h++)

    {

    int jk=0;


    if(arr[h]!=0)

    {

    int j;

    for(j=index; j<arr[h]; j++)

    {

    sr[h][jk]=st[j];

    jk++;

    }

    index=arr[h];

    }

    }


    앞서 만들어둔 정보를 가지고 해야할 일은 무엇일까요? 바로 하나의 문자열 형태로 입력받은 값을 앞서 만들어둔 정보를 토대로 각각의 의미있는 값으로 나누어주는 것입니다. 여기서 사용되는 것이 이차원 배열 sr입니다. 앞서 만들어둔 빈칸의 위치 정보를 저장하는 arr 배열의 값을 이용하여 문자열 st로부터 각각의 의미있는 값을 구분짓게되고, 이 값들은 이차원배열 sr에 각각 문자열로 저장됩니다. 



    결과적으로는 위와 같은 형태로 각각의 의미있는 값들이 나누어 저장되는 것입니다. 이제 하나의 문자열로 입력받은 세 가지의 값들을 나누었으니 이것들을 각 자료형에 맞게 형변환하고 해당 자료형의 변수에 저장하는 일만 남았습니다. 여기서부터가 중요한 작업입니다.


    union Printable_t{

    int* i;

    float* f;

    char* c;

    char* s;

    } Printable;


    va_list arguments;

    va_start(arguments, s);


    이 중요한 작업을 위한 사전준비가 바로 위의 소스코드입니다. 먼저 Printable이라는 공용체를 선언합니다. 해당 공용체에는 각각 정수형, 실수형, 문자형에 해당하는 포인터 i, f, c, s가 속해 있습니다. 이 공용체는 이후 사용자가 입력한 의미를 가지는 각각의 값을 저장할 때 사용됩니다. 그리고 나타나는 코드들이 바로 가변 인수를 사용하기위한 코드들입니다.


    먼저 가변 인수를 사용하기위한 포인터 arguments를 선언합니다. 그리고 va_start 함수를 이용하여 가변 인수에 대한 포인터 arguments가 omarin 함수의 가변 인수 중에서 첫 번째 인수를 가리키도록 합니다. 첫 번째 가변 인수의 값을 알아내기 위해 va_start 함수의 인자에는 마지막 고정 인수가 함께 전달됩니다.



    위와 같은 순서가 되는 것입니다. 호출되는 함수의 인수들이 스택에 순서대로 쌓이는 원리를 이용하여 마지막 고정 인수인 s의 위치를 토대로 첫 번째 가변 인수의 값을 찾아낼 수 있는 것입니다. 그러한 원리를 이용해 찾아낸 첫 번째 가변 인수인 integer_value의 주소값은 포인터 argument에 저장됩니다. 이제 이 포인터 argument를 사용하여 사용자가 입력한 값들을 저장할 공간에 접근할 것입니다.


    int len=strlen(s);

    int i;

    int w=0;

    for(i=0;i<len;i++)

    {

        switch(s[i])

        {

        case 'd':

                Printable.i=va_arg(arguments,int*);

                *(Printable.i)=atoi(sr[w]);

                w++;

               break;

        case 'f':

               Printable.f=va_arg(arguments,float*);

               *(Printable.f)=atof(sr[w]);

               w++;

               break;

        case 'c':

                 Printable.c=va_arg(arguments,char*);

                 *(Printable.c)=(sr[w][1]);

                 w++;

                 break;

        }

    }


    마지막 하이라이트 부분입니다! 앞서 프로그램은 사용자로부터 입력받은 문자열을 빈칸을 기준으로 나누어 각각의 의미를 가지는 값들로 만들고, 이를 이차원 배열에 문자열로 저장하였습니다. 즉, 사용자가 "10 12.33 h"라고 입력한 경우에는 이차원 배열에 "10", " 12.33", " h"라는 문자열들이 저장되어있는 것이죠.


    하이라이트 부분에서는 이차원 배열에 저장된 문자열들을 정해진 자료형으로 형변환하여 가변 인수로 주어진 변수들에 순차적으로 저장합니다. 이 때, 각각의 문자열들을 어떤 자료형으로 형변환할 것인가를 결정하는 기준이 바로 omarin 함수의 고정인수로 주어진 문자열 s입니다. 


    초반에 한 번 언급한 적이 있습니다만, 기존의 scanf 함수는 이 정보를 "%d %f %c"와 같은 형태의 고정 인수로 받았습니다. 하지만 이 omarin 함수는 이 정보를 "d f c "와 같은 형태의 고정 인수로 받게됩니다. 해당 프로그램에는  바로 문자열 s, "d f c "가 이 고정 인수가 되는 것입니다.


    위의 하이라이트 부분에서는 이 문자열 s의 길이만큼 for문을 동작하게하여 문자열의 요소를 순차적으로 읽어들입니다. 이 때, switch문을 이용하여 읽어들인 요소의 값이 'd'인지, 'f'인지 혹은 'c'인지에 따라 정수로 저장, 실수로 저장, 문자로 저장에 해당하는 동작을 하게됩니다.


    해당 동작들은 모두 같은 맥락이므로 switch문이 읽어들인 문자열 s의 요소가 'd'인 경우에 대해서만 설명하도록 하겠습니다. 이 경우 case문 내에서는 va_arg 함수를 이용하여 현재 포인터 arguments가 가리키는 공간에서 int*에 해당하는 데이터를 가져와 Printable.i에 저장합니다. ( 이 과정 이후 포인터 arguments는 va_arg 함수에 의해 자동으로 다음 가변 인수를 나타내게 됩니다. )


    포인터 arguments는 이 시점에 첫 번째 가변인수인 integer_value의 주소값을 가지고 있습니다. 즉, integer_value 변수를 가리키고 잇는 것이죠. 여기서 int*에 해당하는 값은 바로 integer_value변수의 주소에 해당합니다. 결과적으로 Printable.i에는 integer_value 변수의 주소값이 저장되는 것입니다. 즉, Printable.i라는 정수형 포인터도 integer_value라는 값을 가리키게 되는 것이죠.


    이어서 *(Printable.i) 즉, Printable.i라는 정수형 포인터가 가리키는 변수(integer_value)의 값에 atoi(sr[w]) 값을 대입합니다. sr은 문자열 형태를 가지고 있는 각각의 의미있는 값들을 저장하는 이차원 배열입니다. 현재 w값은 0이므로 sr[w]는 sr[0]입니다. 정리하자면, integer_value 변수에 sr[0] 문자열("10")을 atoi 함수를 이용해 정수형으로 형변환한 값을 저장하는 것입니다. 이로써 사용자가 입력한 문자열 형태의 값을 그에 맞는 자료형으로 변환하여 저장한 것입니다. 


    마지막으로 해당 내용을 정리한 그림을 덧붙이며, Store data without Scanf 프로그램에 대한 분석을 마치도록 하겠습니다.






Designed by Tistory.