va_arg(가변인자) 함수의 사용 및 커스텀 printf 구현

C언어를 배울 때 가장 먼저 배우는 것은 아마도 콘솔에 “Hello, World!”를 출력해보는 것일 것이다.
저 문장을 출력하기 위해서 우리는 printf(“Hello, World!\n”); 를 사용해서 출력했을 것이다.

그런데, 가만 생각해 보면 늘 사용하는 printf에는 여러 종류의 파라미터를 넘길 수 있고, 또한 이 파라미터의 갯수는 내가 정하기 나름이다.
하지만, 일반적으로 C계열에서 코딩을 할 때 사용자가 작성한 함수에서는 파라미터의 타입과 갯수를 엄격하게 제한하고 있다. 그러면 printf는 어떻게 이것이 가능할까?

이 문제를 해결하기 위해 가변인자 함수를 사용한다.
이 가변인자 함수를 통해서 직접 printf를 구현해 보자.
(사실 구현이라고 쓰긴 했지만 printf의 래핑 함수에 불과하자. 내부적으로 sprintf와 fprintf를 사용하였다.)

먼저 전체 코드는 다음과 같다.

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

#define MAX_BUF_SIZE    4096

void customPrintFormat(const char *format, ...);
void customPrintFormat2(const char *format, ...);


int main() {
    char a = 'A';
    int b = 2;
    float c = 4.0;
    double d = 5.0;
    char *e = "hello";

    customPrintFormat("No Argument\n");
    customPrintFormat("%c\n", a);
    customPrintFormat("%c, %d\n", a, b);
    customPrintFormat("%c, %d, %f\n", a, b, c);
    customPrintFormat("%c, %d, %f, %lf\n", a, b, c, d);
    customPrintFormat("%c, %d, %f, %lf, %s\n", a, b, c, d, e);

    customPrintFormat2("No Argument\n");
    customPrintFormat2("%c\n", a);
    customPrintFormat2("%c, %d\n", a, b);
    customPrintFormat2("%c, %d, %f\n", a, b, c);
    customPrintFormat2("%c, %d, %f, %lf\n", a, b, c, d);
    customPrintFormat2("%c, %d, %f, %lf, %s\n", a, b, c, d, e);

    return 0;
}

void customPrintFormat(const char *format, ...) {
    va_list ap;
    char buf[MAX_BUF_SIZE];

    va_start(ap, format);
    vsprintf(buf, format, ap);
    va_end(ap);

    fprintf(stdout, "%s: %s", __FUNCTION__, buf);
}

void customPrintFormat2(const char *format, ...) {
    va_list ap;
    char *buf, p;

    buf = malloc(sizeof(char) * MAX_BUF_SIZE);
    if (buf == NULL) {
        fprintf(stderr, "Memory allocation Error: %s[line: %d]", __FUNCTION__, __LINE__);
        return;
    }

    va_start(ap, format);

    while(*format) {
        p = *format++;
        if (p == '%') {
            p = *format++;
            switch (p) {
                case 'c':
                    sprintf(buf, "%s%c", buf, va_arg(ap, int));
                    break;
                case 'd':
                    sprintf(buf, "%s%d", buf, va_arg(ap, int));
                    break;
                case 'f':
                    sprintf(buf, "%s%f", buf, va_arg(ap, double));
                    break;
                case 'l':
                    if (*format++ == 'f') {
                        sprintf(buf, "%s%lf", buf, va_arg(ap, double));
                    }
                    break;
                case 's':
                    sprintf(buf, "%s%s", buf, va_arg(ap, char*));
                    break;
                default:
                    sprintf(buf, "%s%%", buf);
                    break;
            }
        } else {
            sprintf(buf, "%s%c", buf, p);
        }
    }

    va_end(ap);

    fprintf(stdout, "%s: %s", __FUNCTION__, buf);
}

const char* format 으로 출력할 스트링 포멧 문자열을 받는다.
*가변 인자 함수의 경우 항상 첫번째 파라미터에는 가변값이 아닌 고정값이 와야한다.
그리고 두번째부터 말줄임표인 … 을 사용하여 이 함수가 가변인자 함수임을 나타낸다.

customPrintFormat2의 구현 부분을 보자.
va_list ap;
를 통해서 말줄임표된 가변 인자를 넘겨받는다.
그리고, va_start 라는 매크로를 통해서 미리 정의된 스트링 출력 포멧 바로 이후로 가변인자가 시작됨을 설정해 준다. (매크로 내부에서 포인터 이동을 통해 위치시키고 있다.)
그 다음부터는 while 루프를 돌면서 const char *format을 하나하나 파싱해 나간다.
파싱을 하는 도중 va_arg라는 매크로를 다시 만날 수 있는데, 이 부분을 통해서 가변인자 리스트로부터 지정된 데이터 타입의 메모리 크기만큼을 읽어오게 된다.
* 여기에서 int형과 char형, double형과 float형을 같은 메모리 크기로 읽어오게 된다. 이 부분은 va_arg 매크로의 size 계산법에 의해 동작하며, 아마도 4바이트 단위의 메모리 사용을 위한 것으로 보인다.
while 루프를 빠져나오게 되면, va_end(ap) 매크로를 통해 가변인자의 파싱이 끝났음을 알려준다.
그리고 지정된 버퍼에 담겨있는 온전한 문자열을 fprintf를 통해 standard output으로 출력한다. (stdout)
이러한 파싱과 출력을 통해 커스텀한 printf를 구현해 보았다.

이를 통해 C에 미리 정의된 포멧 지정자 이외의 특별한 포멧 지정자를 활용할 수 있을 듯 하다.

위의 과정을 좀 더 간략하게 줄인 함수가 customPrintFormat 이다.
vsprintf 함수를 통해서 간단하게 지정된 포멧으로 가변인자 ap를 이용한 스트링을 buf에 기록하게 해 준다.
* va_start, va_arg, va_end, va_list 등을 사용하기 위해서는 헤더파일 stdarg.h 을 include 해주어야만 한다.
* 위 매크로에 대한 자세한 사용법은 unix계열에서 man va_start 를 통해 확인하실 수 있다.
* sprintf를 사용하는 것보다 strcat, strcpy, strncpy, memcpy 등을 사용하는 것이 퍼포먼스 측면에서는 훨씬 좋을 수 있다. 구현의 편의를 위해 sprintf를 사용한 것임을 밝힌다.

위 코드의 실행 결과는 다음과 같다.