널리 알려지지는 않았지만, C 언어는 함수가 가변 인자를 (즉 인자의 갯수가 정해지지 않은) 받을 수 있는 기능을 제공합니다. Variable-length argument list(가변 인자 리스트)는, 드물기는 하지만, printf와 같은 함수들에게는 꼭 필요한 것입니다. (variable-length argument list는 ANSI C 표준에서 공식적으로 지원하지만, ANSI C 표준 이전에는 엄격히 말해서 정의되어 있지 않습니다.)
Variable-length argument list를 처리하는 방법은 상당히 기묘하기까지 합니다. 정식으로 varaible-length argument list는 fixed part(고정된 부분)와 variable-length part(가변 길이)의 두 부분으로 나누어져 있습니다. 우리는 “variable-length argument list의 variable-length part”라는 과장된 용어를 쓰고 있다는 사실을 발견했지만, 어쩔 수 없습니다 (혹시 여러분이 “variadic”이나 “varargs”라는 용어가 쓰이는 것을 보신 적이 있을지도 모르겠습니다: 두 가지 용어 모두 “having a variable number of arguments.15.1”을 뜻합니다. 따라서 “vararg function” 또는 “varargs argument”라고 말할 수도 있습니다.)
Variable-length argument list를 쓰는 방법은 크게 세 단계로 이루어 집니다. 먼저 va_list 타입의 특별한 pointer type을 써서 선언하고, 그 다음 va_start를 불러서 초기화합니다.
#include <stdio.h>
를 쓰라고
하더군요. 꼭 그럴 필요가 있을까요?
어떤 컴파일러에서는 가변 인자 리스트를 쓰는 함수에는 일반 방식과는
다른 호출 순서(calling sequence)를 사용합니다.
(It might do so if calls using variable-length argument lists were
less efficient than those using fixed-length.)
그러므로 함수의 프로토타입이 (즉 “...
”를 포함한 선언)
영역 안에 있어야 컴파일러가 가변 인자 리스트 처리 메커니즘을
사용할 수 있습니다.
%f
가 float과 double 인자 모두에
쓰일 수 있는 이유는 무엇인가요?
%f
포맷은 항상 double 타입만 받아들이는 셈이
됩니다. (비슷한 이유에서 %c
, %hd
포맷은 항상 int만을
받아 들이게 됩니다.) 덧붙여 질문
12.9,
12.13도 참고하시기 바랍니다.
printf("%d", n);그렇지만 ANSI 함수 프로토타입을 제공했으니, 자동으로 형 변환이 적용될 거라고 생각합니다. 무엇이 잘못되었나요?
어떤 컴파일러들과 (gcc 포함), 어떤 lint 프로그램은, format string이 문자열 상수(string literal)인 경우에 한해서, printf와 같은 함수들에 전달된 가변 인자가 정말로 올바른 타입으로 전달되었는지 검사해 주기도 합니다.
<stdarg.h>
헤더 파일에 있는 기능을 사용합니다.
아래의 함수는 주어진 여러 개의 문자열을 붙여서 malloc()으로 할당한 메모리에 저장해서 리턴하는 함수입니다:
#include <stdlib.h> /* for malloc, NULL, size_t */ #include <stdarg.h> /* for va_ stuff */ #include <string.h> /* for strcat et al. */ char *vstrcat(char *first, ...) { size_t len; char *retbuf; va_list argp; char *p; if (first == NULL) return NULL; len = strlen(first); va_start(argp, first); while ((p = va_arg(argp, char *)) != NULL) len += strlen(p); va_end(argp); retbuf = malloc(len + 1); /* +1 for trailing \0 */ if (retbuf == NULL) return NULL; /* error */ (void)strcpy(retbuf, first); va_start(argp, first); /* restart; 2nd scan */ while ((p = va_arg(argp, char *)) != NULL) (void)strcat(retbuf, p); va_end(argp); return retbuf; }(인자 리스트를 처음부터 다시 검색할 때에는 위에서처럼
va_start
를 다시 불러 주어야 합니다. 또한 va_end
가
실제로 하는 일이 없을 수는 있지만, 이식성을 위해서 반드시 불러주어야 합니다.)
사용법은 다음과 같습니다:
char *str = vstrcat("Hello, ", "world!", (char *)NULL);마지막 인자를 캐스팅한 것을 꼭 주의깊게 보시기 바랍니다; 질문 5.2, 15.3을 참고하기 바랍니다. (또한 이 함수를 부른 함수는 이 함수가 리턴한 문자열을 free시켜 주어야 할 책임이 있습니다.)
위의 예는 모든 가변 인자의 타입이 char *입니다. 이제, 가변 인자의 갯수와
타입이 정해지지 않는 함수를 만들어 보겠습니다; 이 함수는 아주 간단한 기능만
지원하는 printf입니다. 특히 va_arg()
에서 원하는 타입을 미리
지정한 부분에 대해 주의깊게 보시기 바랍니다.
(아래 miniprintf 함수는 질문
20.10에서 쓴 baseconv를
씁니다. 또한 이 것은 INT_MIN
과 같은, 가장 작은 정수 값을 올바르게
출력하기에 적당하지 않습니다.)
#include <stdio.h> #include <stdarg.h> extern char *baseconv(unsigned int, int); void miniprintf(char *fmt, ...) { char *p; int i; unsigned u; char *s; va_list argp; va_start(argp, fmt); for (p = fmt; *p != '\0'; p++) { if (*p != '%') { putchar(*p); continue; } switch (*++p) { case 'c': i = va_arg(argp, int); /* not va_arg(argp, char); 질문 15.10 참고 */ putchar(i); break; case 'd': i = va_arg(argp, int); if (i < 0) { /* XXX won't handle INT_MIN */ i = -i; putchar('-'); } fputs(baseconv(i, 10), stdout); break; case 'o': u = va_arg(argp, unsigned int); fputs(baseconv(u, 8), stdout); break; case 's': s = va_arg(argp, char *); fputs(s, stdout); break; case 'u': s = va_arg(argp, unsigned int); fputs(baseconv(u, 10), stdout); break; case 'x': u = va_arg(argp, unsigned int); fputs(baseconv(u, 16), sttout); break; case '%': putchar('%'); break; } va_end(argp); } }덧붙여 질문 15.7도 참고하시기 바랍니다.
va_list
타입에
대한 포인터를 받습니다.
예를 들어, 아래의 error() 함수는 에러 메시지를 받아들여 그 메시지 앞에 “error: ”를 덧붙이고 newline을 붙여서 출력해주는 함수입니다:
#include <stdio.h> #include <stdarg.h> void error(char *fmt, ...) { va_list argp; fprintf(stderr, "error: "); va_start(argp, fmt); vfprintf(stderr, fmt, argp); va_end(argp); fprintf(stderr, "\n"); }덧붙여 질문 15.7도 참고하시기 바랍니다.
<stdarg.h>
가 없는데
어떻게 하죠?
<stdarg.h>
에 해당하는 오래된 헤더파일인 <varargs.h>
를
쓰면 됩니다. 예를 들어, 질문
15.4에서 만든 vstrcat을,
<varargs.h>
를 쓰도록 고치면 다음과 같습니다:
#include <stdio.h> #include <varargs.h> #include <string.h> extern char *malloc(); char *vstrcat(va_alist) va_dcl /* no semicolon */ { int len = 0; char *retbuf; va_list argp; char *p; va_start(argp); while ((p = va_arg(argp, char *)) != NULL) len += strlen(p); va_end(argp); retbuf = malloc(len + 1); /* +1 for trailing '\0' */ if (retbuf == NULL) return NULL; /* error */ retbuf[0] = '\0'; va_start(argp); /* restart for second scan */ while ((p = va_arg(argp, char *)) != NULL) strcat(retbuf, p); va_end(argp); return retbuf; }(
va_dcl
뒤에 세미콜론(`;')이 없는 것에 주의하기 바랍니다.
그리고, 이 경우, 첫번째 인자에 대한 특별한 처리가 필요없습니다.) 또,
<string.h>
을 포함시키는 대신, 직접 문자열 처리 관련 함수들을
선언해 주어야 할 지도 모릅니다.
만약, 시스템이 vfprintf를 제공하며, <stdarg.h>
를 제공하지
않을 경우, 아래에 (질문
15.5에서 쓴) <varargs.h>
를 쓴
error 함수를 보입니다:
#include <stdio.h> #include <varargs.h> void error(va_alist) va_dcl /* no semicolon */ { char *fmt; va_list argp; fprintf(stderr, "error: "); va_start(argp); fmt = va_arg(argp, char *); vfprintf(stderr, fmt, argp); va_end(argp); fprintf(stderr, "\n"); }(
<stdarg.h>
에서와는 달리, <varargs.h>
에서는
모든 인자가 가변인자입니다. 따라서 fmt 인자도, va_arg
를 써서
얻어야 합니다.)
가변 인자를 받아 처리하는 함수는 그 자체만으로 인자의 갯수를 파악할 수 있어야 합니다. printf 계열의 함수들은 포맷 문자열에서 (%d와 같은) 포맷 specifier를 보고 그 갯수를 파악합니다. (그렇기 때문에 printf()에 전달된 인자의 갯수가 포맷 문자열과 맞지 않을 경우에 오류를 일으킵니다.) 또 다른 방법으로, 가변 인자가 모두 같은 타입일 경우, 마지막 인자를 (0, -1, 또는 적절한 널 포인터와 같은) 어떤 특정한 값으로 설정해서 인자의 갯수를 파악합니다 (질문 5.2, 15.4에서 execl()과 vstrcat() 함수의 사용법을 참고하시기 바랍니다). 마지막으로, 인자의 갯수를 미리 파악할 수 있다면, 전체 인자의 갯수를 인자로 전달하는 것도 좋은 방법입니다. (although it's usually a nuisance for the caller to supply).
int f(...) { }
va_start()
를 쓰려면 적어도 하나의 고정된 인자가
있어야 한다고 말하고 있습니다. (어쨌든, 가변 인자의 갯수 또는 타입을 알려면,
하나 이상의 인자가 필요합니다.) 덧붙여 질문
15.10도 참고하시기 바랍니다.
va_arg(argp, float)
을 쓰면 동작하지 않을까요?
따라서, va_arg(argp, float)
은
잘못된 코드이며, 대신 va_arg(argp,
double)을
써야 합니다.
비슷한 이유로 char, short, int를 받기 위해서는
va_arg(argp, int)
를 써야 합니다.
덧붙여 질문
11.3,
15.2도 참고하시기 바랍니다.
va_arg()
로 얻을려고 하는데, 잘 안됩니다.
va_arg()
와 같은 매크로는 함수 포인터와 같은 복잡한 타입을
사용할 때, 곤란을 겪기도 합니다 (be stymied). 이 문제를 이해하기 위해,
아래에 간단한 va_arg()
구현 예를 보입니다:
#define va_arg(argp, type) \ (*(type *)(((argp) += sizeof(type)) - sizeof(type)))위에서, argp는 (즉,
va_list
타입)
char *입니다. 이 때, 만약 다음과 같이 불렀다면:
va_arg(argp, int (*)())
va_arg
는 다음과 같이 확장됩니다:
(*(int (*)() *)(((argp) += sizeof(int (*)())) - sizeof(int (*)())))위 결과를 자세히 보면 알겠지만, syntax error입니다. (첫번째 (int (*)() *)로 캐스트하는 것은 의미가 없습니다.)15.2
만약, 함수 포인터를 다른 이름으로 typedef했다면 모든 문제가 해결됩니다. 주어진 함수 포인터를 다음과 같이 typedef를 만들었다면:
typedef int (*funcptr)();그리고 아래와 같이 불렀다면:
va_arg(argp, funcptr)다음과 같이 확장됩니다:
(*(funcptr *)(((argp) += sizeof(funcptr)) - sizeof(funcptr)))이 경우, 정상적으로 동작합니다.
va_list
를
받는 함수를 만들어야 합니다.
예를 들어, 치명적인(fatal) 에러 메시지를 출력하고 프로그램을 끝내는 함수인 faterror를 만든다고 가정해 봅시다. 여러분은 질문 15.5에서 쓴 error 함수를 쓰길 원한다고 합시다:
void faterror(char *fmt, ...) { error(fmt, ????); exit(EXIT_FAILURE); }위에서 보면 알겠지만, faterror가 받은 가변 인자를 error에 전달할 방법이 없습니다.
이 문제를 처리하기 위해서, 첫째, error 함수를 분해해서 가변 인자 대신
하나의 va_list
를 받는 verror 함수를 만듭니다.
(별로 어렵지 않습니다. 왜냐하면 verror의 대부분은 error와 같으며,
일단 verror를 만들고 나면, error 함수는 verror를 써서
아주 쉽게 만들 수 있습니다.)
#include <stdio.h> #include <stdarg.h> void verror(char *fmt, va_list argp) { fprintf(stderr, "error: "); vfprintf(stderr, fmt, argp); fpritnf(stderr, "\n"); } void error(char *fmt, ...) { va_list argp; va_start(argp, fmt); verror(fmt, argp); va_end(argp); }위와 같이 만들었으면, 이제 faterror를 다음과 같이 만들 수 있습니다:
void faterror(char *fmt, ...) { va_list argp; va_start(argp, fmt); verror(fmt, argp); va_end(argp); exit(EXIT_FAILURE); }자세히 보면 error와 verror의 관계는 print와 vprintf의 관계와 같습니다. Chris Torek씨가 제안한 것에 따르면, 여러분이 가변 인자를 처리하는 함수를 만들때마다, 두 가지 버전을 제공하는 것이 좋습니다: 하나는 (verror와 같이)
va_list
를 받아서 처리하는 함수와,
(error와 같이) 앞 함수의 간단한 wrapper 역할을 하는 함수, 두 가지입니다.
이 방식의 한가지 단점은, verror와 같은 함수는 가변 인자를 단 한번씩만
scan할 수 있다는 것입니다; va_start
를 다시 부를 방법은 없습니다.
만약, (faterror와 같은) 가변 인자를 받아서 이 것을 가변 인자를 받는 다른
함수에 전달하려고 하는데, (error와 같이) va_list
를 받는
저수준 함수를 다시 만들 수 없다면, 이 문제를 해결하기 위한 portable한 방법은
존재하지 않습니다. (이 문제를 시스템의 어셈블리 언어를 써서 해결할 가능성은
있습니다. 덧붙여 질문
15.13도 참고하시기 바랍니다.)
다음과 같은 방법은 동작하지 않습니다:
void faterror(char *fmt, ...) { va_list argp; va_start(argp, fmt); error(fmt, argp); /* WRONG */ va_end(argp); exit(EXIT_FAILURE); }
va_list
자체는 가변 인자 리스트가 아닙니다; 내부적으로
이 것은 가변 인자 리스트를 가리키는 포인터입니다. 따라서 va_list
를
받는 함수는 가변 인자 리스트를 받는 함수가 아니며, 그 역도 성립하지 않습니다.
Another kludge that is sometimes used and that sometimes works even though it is grossly nonportable is to use a lot of int arguments, hoping that there are enough of them and that they can somehow pass through pointer, floating-point, and other arguments as well:
void faterror(fmt, a1, a2, a3, a4, a5, a6) char *fmt; int a1, a2, a3, a4, a5, a6; { error(fmt, a1, a2, a3, a4, a5, a6); /* VERY WRONG */ exit(EXIT_FAILURE); }위 예는, 이런 식으로 하지 말라는 것을 보여주기 위해 만든 것입니다. 이 글에서 봤다는 이유로 위 코드를 쓰려고 하지 말기 바랍니다.
실제 인자 리스트를 처리하는 대신 generic (void *
) 포인터의 배열을
넘겨주는 방법을 생각할 수 있습니다. 그리고 불려진 함수는 (main()이
argv 인자를 처리하듯이) 이 배열 요소를 하나씩 조사해서 원하는
정보를 얻을 수 있습니다. (물론, 이 방식은 여러분이 불려진 모든 함수를 직접
제어할 수 있을 때에만 의미가 있습니다.)
Seong-Kook Shin