brunch

매거진 Sinclair

You can make anything
by writing

C.S.Lewis

by Sinclair Aug 19. 2016

Advanced Pointer II

이제, 저 높은 곳을 향하여 III




void * 의 꽃 가변인자와 <stdarg.h>


printf() 함수나 scanf() 함수를 사용하면서 이상하다고 느끼는 사람은 결코 많지 않습니다. (저는 그 프로토타입이 항상 궁금했습니다.) 일반적으로 C의 함수들은 인자를 넘겨줄 때 인자의 개수와 타입을 검사하도록 되어있습니다. 그런데 놀랍게도 이 두 함수는 결코 인자의 타입과 개수의 제한을 받지 않습니다. 대체 프로토타입이 어떻길래 그럴 수 있을까 생각이 들어 찾아보니 모양새가 이러합니다. 뙇~


int printf(const char * , ...) ;

int scanf(const char * , ...) ;


헐, 헐퀴... 저는 처음에 이것들을 보고 깜짝 놀랐습니다. 세상에 ...이라니, 이게 국어야? 아님 컴파일러가 지금 장난하나? 라고 말입니다. 하지만 ...은 가변인자를 처리하는 일종의 연산자입니다.

흔히 가변인자를 C언어가 제공하는 다형성(polymorphism)이라고 말합니다.


register라고 지정하는 경우를 제외하면 모든 자동변수 인자들은 스택에 빈 공간 없이 순서대로 저장됩니다. 그렇기 때문에 가변인자들의 시작 주소와 각 인자의 타입을 알면 void*에서 원하는 타입의 데이터를 꺼내왔던 방식 그대로 인자를 구별해 낼 수 있습니다.


우리가 이런 메커니즘을 이해하지 못한다면 가변인자를 사용할 수 없습니다. 그리고 앞의 글타래들 중 함수의 활용3에서 설명한대로 반드시 호출한 함수 쪽에서 몇 개의 인자를 사용하는지 기억하고 스택을 정리해주는 cdecl방식을 지원해야 합니다.


우선 가변인자를 사용하려면 stdarg.h를 알아야 합니다. <stdarg.h>에는 typedef와 몇 가지 매크로함수가 포함되어 있습니다.


typedef do-type va_list;

가변인자의 시작 주소를 지정할 변수의 타입선언

주로 char* 나 void*


#define va_start(ap, last-par)

가변인자 처리의 시작 가변인자의 시작 주소를 ap에 저장

각 컴파일러 참조


#define va_arg(ap, T)

가변인자의 주소 값인 ap로부터 T타입의 데이터를 가져오고 ap는 T타입 사이즈만큼 증가

각 컴파일러 참조


#define va_end(ap)

가변인자 처리의 종료 주로 ap에 널 포인터 대입

ap = (va_list)0 ;



아래는 위에 선언된 타입과 매크로를 사용하여 가변인자를 처리하는 일반적인 패턴입니다. 별로 어렵지 않으니 잘 외워 두었다가 사용하면 됩니다.

가변인자를 처리하려면 반드시 기준이 되는 인자가 필요하고 그것은 주로 문자열일 경우가 대부분입니다.


일반적으로 대부분의 함수는 프로토타입 선언으로 인자의 타입을 미리 검사 하기 때문에 함수를 설계하는 프로그래머가 그 안에서 직접 거의 모든 에러처리를 해야 합니다.

하지만 가변인자 처리의 경우 함수를 작성하는 동안에 어떤 타입들이 입력될지 전혀 알 수 없기 때문에 잘못 사용하는 모든 책임을 가변인자 함수의 실제 사용자가 감당해야 합니다. 그렇기 때문에 가변인자는 함수 설계자와 사용자가 함께 지켜야 할 약속입니다.



// little endian을 따르는 CPU에서 테스트해야 합니다.

// big endian에서는 다른 결과가 나올 수 있습니다.

printf("%d %d\n" , 3.141592); // -57999238 1074340346

printf("%f\n", -57999238, 1074340346) ; // 3.141592


라고 작성하여 실행해 보면 왜 약속을 지켜야 하는지 잘 알 수 있습니다.



// 아래는 turbo C 3.0에 정의된 <stdarg.h> 파일 내용의 일부입니다.


/* stdarg.h

Definitions for accessing parameters in functions that accept

a variable number of arguments.

Copyright (c) 1987, 1991 by Borland International

All Rights Reserved.

*/

typedef void *va_list; // <- 가변인자를 void*의 꽃이라고 하는 이유 by Sinclair

#define __size(x) ((sizeof(x)+sizeof(int)-1) & ~(sizeof(int)-1))

#define va_start(ap, parmN) \

    ((void)((ap) = (va_list)((char *)(&parmN)+__size(parmN))))

#define va_arg(ap, type) \

    (*(type *)(((*(char **)&(ap))+=__size(type))-(__size(type))))

#define va_end(ap) ((void)0)   




// 여기부터는 가변인자 처리의 일반적인 패턴의 pseudocode

#include <stdarg.h>

#include <string.h>

//다른 헤더파일들

/*

* copyleft (l) 2006 - 2017 programmed by Sinclair

*/

int parsing(const char * , ...) ;

int parsing(const char * format, ...) {

    // format은 각 데이터 타입을 구별하기 위한 문자열

    va_list ap ; // 가변인자를 저장할 변수 선언


    // 필요한 나머지 변수들 선언 여기에


    va_start(ap, format) ;

    // 우선 format변수로부터 가변인자의 시작주소를 알아낸다.


    while(*format) {

    // format문자열이 끝나면 가변인자 처리도 끝난다.

    // 문자열의 끝은? 널 문자요~

        if( !strncmp(format, "문자열", 사이즈) ) {

            va_arg(ap,원하는타입) ;

            // do something here

            format += 사이즈 ; // 다음 포맷문자열로

        } // end if

        else if(!strncmp(format, "다른문자열", 사이즈) ) {

            va_arg(ap,다른타입) ;

            // do something here

            format += 사이즈 ; // 다음 포맷문자열로

        } // end if

        else { // 포맷문자열아닐경우

            // do something here

            format++ ;

        } // end else

    } // end while

    va_end(ap) ; // 가변인자 처리의 종료

    return 값또는수식 ;

} // end parsing()   



이제 위의 방식대로 포맷문자열을 설계하고 간단한 가변인자 처리를 해보도록 하겠습니다. 우선은 다른 작업 없이 그냥 출력만 할겁니다. 자, 이제 갑니다.


#include <stdio.h>

#include <stdarg.h>

#include <string.h>

/*

* copyleft (l) 2006 - 2017 programmed by Sinclair

*/

// prototype

int parsing(const char * format , ...) ;


int parsing(const char * format , ...) {

    register count = 0 ;

    va_list ap ; // 가변인자의 시작 주소를 저장

    // 처리할 변수들과 포맷문자열...

    char cData ;        /* @c */

    short sData ;        /* @hd */

    int iData ;        /* @d */

    long lData ;        /* @ld */

    float fData ;        /* @f */

    double dData ;     /* @lf */

    char * string ;     /* @s */


    va_start( ap , format ) ; // 가변인자 처리의 시작을 알린다..

    // 가변인자는 일반적으로 포맷문자열 이후에 오니까..


    while(*format) { // format이 끝나면 가변인자 처리도 쫑나니까..

        if( !strncmp( format , "@d" , 2 )) {

            // 정수형데이터

            iData = va_arg(ap ,int) ;

            // do something here..

            printf("%d", iData) ;

            //printf("%d" , va_arg(ap ,int) ) ;

            // 위에 있는 두 실행문을 한 줄로~ 맹가노니

            format += 2 ; // 다음 포맷문자열로 가세요~

        } // end if

        else if( !strncmp( format , "@hd" , 3 )) {

            // short정수형데이터

            sData = (short)va_arg(ap ,int) ;

            printf("%d", sData) ;

            format += 3 ;

        } // end if

        else if( !strncmp( format , "@ld" , 3 )) {

            // long정수형데이터

            lData = va_arg(ap ,long) ;

            printf("%d", lData) ;

            format += 3 ;

        } // end if

        else if( !strncmp( format , "@c" , 2 )) {

            // 문자형데이터

            cData = (char)va_arg(ap ,int) ;

            printf("%c", cData) ;

            format += 2 ;

        } // end if

        else if( !strncmp( format , "@f" , 2 )) {

            // float실수형데이터

            fData = (float)va_arg(ap ,double) ;

            printf("%f", fData) ;

            format += 2 ;

        } // end if

        else if( !strncmp( format , "@lf" , 3 )) {

            // double실수형데이터

            dData = va_arg(ap ,double) ;

            printf("%f", dData) ;

            format += 3 ;

        } // end if

        else if( !strncmp( format , "@s" , 2 )) {

            // 문자열 데이터

            string = va_arg(ap ,char *) ;

            printf("%s", string) ;

            format += 2 ;

        } // end if

        else { // 그외에 다른 문자들은 걍 찍어준다..

            putchar(*format++) ;

            count -- ;

        }// end else

        count ++ ;

    } // end while


    va_end(ap) ;

    // 가변인자 처리 끝


    return count ;

} // parsing()  



// 이건 테스트해 볼 메인함수예요~

#include <stdio.h>

extern int parsing( const char * , ...) ;


int main() {

    printf("*************\n1: %d\n" ,

        parsing("@d @f @s\t\t머여? 씽클레어도 씨를 짠다구? @hd****",

            1000 , 3.141592 , "Everything but your dreams",

            11)) ;

    printf("*************\n2: %d\n" ,

        parsing("@ld @lf @c\t\t\t\a", 10000000L, 1.414213,

             "Sinclair"[2])) ;

    parsing("@d @d\n", 3.141592) ;        

    // -57999238 1074340346

    parsing("@lf", -57999238, 1074340346) ;    

    // 3.141592


    return 0 ;

} // end main()   




에이~ 별거 아니라구요? 그죠, 별거 아니죠? 하지만 가변인자가 고작 printf() 흉내 내자고 존재하는  아닙니다. 그래서 이번에는 기본 수치형 데이터를 종류나 개수에 상관없이 합과 평균과 분산을 구하는 프로그램을 만들어 보겠습니다. 분산은 편차의 제곱의 평균이고 계산은 제곱의 평균에서 평균 제곱 빼면 됩니다.  


#include <stdio.h>

#include <string.h>

#include <stdarg.h>

/*

* copyleft (l) 2006 - 2017 programmed by Sinclair

*/

// prototypes

int compute(double * const, size_t, const char *, ...) ;

int compute2(double * const, size_t, const int *, ...) ;  


int compute(double * const r , size_t rsize ,

                     const char * format , ...)

{

    va_list ap ; // 가변인자의 위치를 저장할 변수

    register count = 0 ;

    if (rsize ^ 3) {

        fprintf(stderr , "아흑.. 처리할 인자나 주고 시키숑~ 쓰읍") ;

        return -1 ;

    } // end if


    r[0] = r[1] = r[2] = 0.0 ;


    va_start(ap , format) ; // 가변인자의 시작주소를 ap에 저장


    while(*format) {

        if( !strncmp( format , "@d" , 2 ) ) {

            r[1] = (double) va_arg(ap ,int) ;

            // 가변인자로부터 정수형 추출

            format += 2 ;

        } // end if

        else if( !strncmp( format , "@c" , 2 ) ) {

            r[1] = (double) va_arg(ap ,int) ;

            format += 2 ;

        } // end if

        else if( !strncmp( format , "@hd" , 3 ) ) {

            r[1] = (double) va_arg(ap ,int) ;

            format += 3 ;

        } // end if

        else if( !strncmp( format , "@ld" , 3 ) ) {

            r[1] = (double) va_arg(ap ,long) ;

            format += 3 ;

        } // end if

        else if( !strncmp( format , "@f" , 2 ) ) {

            r[1] = (double)va_arg(ap ,double) ;

            format += 2 ;

        } // end if

        else if( !strncmp( format , "@lf" , 3 ) ) {

            r[1] = va_arg(ap ,double) ;

            format += 3 ;

        } // end if

        else {

            r[1] = 0.0 ;

            count -- ;

            format++ ;

        } // end else

        r[0] += r[1] ; // 합

        r[2] += r[1] * r[1] ; // 제곱의 합

        count ++ ;

    } // end while

    va_end(ap) ;

    if( count ) {

        r[1] = r[0] / count ; // 평균

        r[2] = r[2] / count ; // 제곱의 평균

        r[2] = r[2] - r[1] * r[1] ; // 분산 = 제곱의 평균 - 평균의 제곱

    } // end if

    else return -2 ;

    return count ;

} // end compute()   



// 두 번째 함수는 포맷 문자열이 아닌 정수형 배열을 사용해 봤습니다.

// enum, 이넘(?)은 나중에 자세히 배울 겁니다.

enum TYPES { LAST , INT , CHAR , SHORT , DOUBLE , FLOAT , LONG } ;


int compute2(double * const r , size_t rsize ,

                     const int * format , ...)

{

    va_list ap ; // 가변인자의 위치를 저장할 변수

    register count = 0 ;

    if (rsize ^ 3) {

        fprintf(stderr , "아흑.. 처리할 인자나 주고 시키숑~ 쓰읍") ;

        return -1 ;

    } // end if


    r[0] = r[1] = r[2] = 0.0 ;

    va_start(ap , format) ; // 가변인자의 시작주소를 ap에 저장


    while(*format) {

        if( *format == INT ) {

            r[1] = (double) va_arg(ap ,int) ;

            // 가변인자로부터 정수형 추출

            //format++;

        } // end if

        else if( *format == CHAR ) {

            r[1] = (double) va_arg(ap ,int) ;

        } // end if

        else if( *format == SHORT ) {

           r[1] = (double) va_arg(ap ,int) ;

        } // end if

        else if( *format == LONG ) {

            r[1] = (double) va_arg(ap ,long) ;

        } // end if

        else if( *format == FLOAT) {

            r[1] = (double) va_arg(ap ,double) ;

        } // end if

        else if( *format == DOUBLE ) {

            r[1] = va_arg(ap ,double) ;

        } // end if

        else {

            r[1] = 0.0 ;

            count -- ;

            //format++ ;

        } // end else

        r[0] += r[1] ; // 합

        r[2] += r[1] * r[1] ; // 제곱의 합

        count ++ ;

        format ++ ;

    } // end while

    va_end(ap) ;

    if( count ) {

        r[1] = r[0] / count ; // 평균

        r[2] = r[2] / count ; // 제곱의 평균

        r[2] = r[2] - r[1] * r[1] ; // 분산 = 제곱의 평균 - 평균의 제곱   

    } // end if

    else return -2 ;

    return count ;

} // end compute2()


   

가변인자의 타입을 구별해주는 포맷이 반드시 문자열이라는 고정관념을 가질 필요는 없습니다.

그리고 일반적으로 기준이 되는 인자인 포맷과 가변인자 사이에는 어떤 다른 인자도 넣지 않습니다.

기준이 되는 인자를 제외하고 ...앞 뒤에 다른 인자가 있으면 그것도 고정 타입의 의미 있는 인자로 인식하지 않고 가변인자로 포함됩니다.

그렇기 때문에 타입이 고정되고 의미 있는 인자라면 반드시 가변인자 처리의 기준이 되는 포맷 앞에 적어야 합니다.


그뿐만 아니라 가변인자는 여러 데이터를 일정한 메모리 공간에 하나의 데이터 묶음으로 만들어 줄 때도 사용 가능합니다. 물론 일반적인 경우라면 대부분 미리 구조체로 묶어 두었겠지만 구조체 멤버가 아닌 데이터가 각각 따로 존재하는 상황에서 각 타입의 데이터를 모두 각각 네트워크 전송을 해야 합니다. (보통 네트워크 전송은 매우 많은 비용을 지불해야 합니다.) 이 때 각 데이터마다 따로따로 여러 번 나눠 보내는 것보다는 가변인자 처리를 통해 모든 데이터를 하나로 묶어 한 번에 보낼 수 있다면 매우 효율적일 겁니다. 물론 이때 전송되는 데이터들의 종류나 순서는 미리 약속이 되어 있어야 합니다. 우리는 이 약속을 protocol이라고 부르기도 합니다.


게다가 표준함수는 친절하게도 포맷문자열 설계하기가 귀찮은(?) 분들을 위해 printf()의 포맷문자열을 그대로 사용하는 vprintf(), vsprintf(), vfprintf() 등의 함수들도 함께 제공하고 있습니다.  



#include <stdarg.h>

/*

* copyleft (l) 2006 - 2017 programmed by Sinclair

*/

// 사용 방법은 printf()랑 똑같습니다.

int myPrintf(const char * f, ...) {

    va_list ap ;

    char buf[256] ;

    va_start(ap,f) ;

    // 여기에서 원하는 만큼 데이터를 미리 추출할 수도 있죠.

    // vprintf(f, ap) ;         // printf()처럼 동작

    vfprintf(stderr, f, ap) ;     // fprintf()처럼 동작

    /*********************

    vsprintf(buf, f, ap) ;     // sprintf()처럼 동작

    presentation(buf) ;         // 문자열로 만들어 놓고 표현 방식을 다양하게 만들땐

    **********************/

    // wanna do something here~

    va_end(ap) ;

} // end myPrintf()   



// 글구 구조체의 멤버 데이터가 빈 공간 없이 배치되었다면...

// or packed structure...

// 구조체를 한꺼번에 통채로 출력하는 이런 것도 가능합니다.

#include <stdarg.h>

/*

* copyleft (l) 2006 - 2017 dprogrammed by Sinclair

*/  

void func() {

    struct datum     {

        int i ;

        int j ;

        double d ;

        char c[4] ;

        char * p ;

    } data    = { 19, 26, 3.141592, { 0x41, 0x42, 0x43, 0 } } ;

    //    = { 19, 26, 3.141592, { 0x41, 0x42, 0x43, 0 }, data.c } ;

    data.p = data.c ;

    vprintf("%d\t%d\t%f\t%x\t%s\n", (va_list)&data) ;

} // end func()  











C언어 및 기타 프로그래밍 관련 질문은 오픈 카톡으로

group talk - https://is.gd/yourc

1:1 talk - https://is.gd/aboutc

#Sinclair #씽클레어 #싱클레어 #씽클레어도씨 #씨언어 #씨프로그래밍  #C언어 #Cprogramming #C_Programming #C #Programming #Clanguage #C_Language

매거진의 이전글 구조체, 공용체, 열거형
작품 선택
키워드 선택 0 / 3 0
댓글여부
afliean
브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari