66 함수에서 가변 인자 사용하기

C 언어에서 함수를 사용하다 보면 printf, scanf같이 매개변수의 개수가 정해지지 않은 함수가 있습니다. 이렇게 매번 함수에 들어가는 인수(argument)의 개수가 변하는 것을 가변 인자(가변 인수, variable argument)라고 합니다.

이번에는 가변 인자 함수를 직접 만들어보겠습니다. 참고로 가변 인자는 개념이 다소 어렵고 사용할 일이 많지 않으므로 완벽하게 이해하지 않아도 됩니다.

66.1 가변 인자 함수 만들기

함수에서 가변 인자를 정의할 때는 고정 매개변수가 한 개 이상 있어야 하며 고정 매개변수 뒤에 ...을 붙여 매개변수의 개수가 정해지지 않았다는 표시를 해줍니다. 단, ... 뒤에는 다른 매개변수를 지정할 수 없습니다.

반환값자료형 함수이름(자료형 고정매개변수, ...)
{
}

다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요.

variable_argument.c

#include <stdio.h>

// args는 고정 매개변수
void printNumbers(int args, ...)
{
    printf("%d ", args);
}

int main()
{
    printNumbers(1, 10);
    printNumbers(2, 10, 20);
    printNumbers(3, 10, 20, 30);
    printNumbers(4, 10, 20, 30, 40);

    return 0;
}

실행 결과

1 2 3 4

함수 printNumbers를 호출할 때 값을 한 개에서 네 개까지 넣었습니다. printNumbers(4, 10, 20, 30, 40);이면 처음에 오는 4는 매개변수 args에 들어가고 나머지 값들은 ... 부분에 들어갑니다.

이제 ... 부분의 매개변수를 사용해보겠습니다.

variable_argument_print_numbers.c

#include <stdio.h>
#include <stdarg.h>    // va_list, va_start, va_arg, va_end가 정의된 헤더 파일

void printNumbers(int args, ...)    // 가변 인자의 개수를 받음, ...로 가변 인자 설정
{
    va_list ap;    // 가변 인자 목록 포인터

    va_start(ap, args);    // 가변 인자 목록 포인터 설정
    for (int i = 0; i < args; i++)    // 가변 인자 개수만큼 반복
    {
        int num = va_arg(ap, int);    // int 크기만큼 가변 인자 목록 포인터에서 값을 가져옴
                                      // ap를 int 크기만큼 순방향으로 이동
        printf("%d ", num);           // 가변 인자 값 출력
    }
    va_end(ap);    // 가변 인자 목록 포인터를 NULL로 초기화

    printf("\n");    // 줄바꿈
}

int main()
{
    printNumbers(1, 10);                // 인수 개수 1개
    printNumbers(2, 10, 20);            // 인수 개수 2개
    printNumbers(3, 10, 20, 30);        // 인수 개수 3개
    printNumbers(4, 10, 20, 30, 40);    // 인수 개수 4개

    return 0;
}

실행 결과

10
10 20
10 20 30
10 20 30 40

...으로 들어온 가변 인자를 사용하려면 stdarg.h 헤더 파일에 정의된 매크로를 이용해야 합니다. 따라서 #includestdarg.h 헤더 파일을 포함해줍니다. stdarg.h에 정의된 가변 인자 처리 매크로는 다음과 같습니다.

  • va_list: 가변 인자 목록. 가변 인자의 메모리 주소를 저장하는 포인터입니다.
  • va_start: 가변 인자를 가져올 수 있도록 포인터를 설정합니다.
  • va_arg: 가변 인자 포인터에서 특정 자료형 크기만큼 값을 가져옵니다.
  • va_end: 가변 인자 처리가 끝났을 때 포인터를 NULL로 초기화합니다.

먼저 함수를 정의할 때 void printNumbers(int args, ...)와 같이 첫 번째 매개변수에서 가변 인자의 개수를 받을 수 있도록 지정하고, 두 번째 매개변수 부분에서 가변 인자를 받을 수 있도록 ...으로 지정합니다.

void printNumbers(int args, ...)    // 가변 인자의 개수를 받음, ...로 가변 인자 설정

printNumbers 함수를 호출할 때는 다음과 같이 맨 처음에 인수 개수를 넣어준 뒤 인수 개수에 맞게 인수를 콤마로 구분하여 넣어줍니다.

int main()
{
    printNumbers(1, 10);                // 인수 개수 1개
    printNumbers(2, 10, 20);            // 인수 개수 2개
    printNumbers(3, 10, 20, 30);        // 인수 개수 3개
    printNumbers(4, 10, 20, 30, 40);    // 인수 개수 4개

    return 0;
}

다시 printNumbers 함수 안을 살펴보겠습니다. 함수 안에서는 va_start 매크로에 가변 인자 목록 포인터 ap와 가변 인자 개수 args를 넣어 가변 인자를 가져올 수 있도록 준비합니다(ap는 argument pointer를 줄여서 쓴 것입니다).

va_list ap;    // 가변 인자 목록 포인터

va_start(ap, args);    // 가변 인자 목록 포인터 설정

만약 가변 인자가 4개 들어있는 printNumbers(4, 10, 20, 30, 40);를 호출한 뒤 va_start 매크로를 실행하면 다음과 같은 모양이 됩니다.

그림 66‑1 va_start로 ap 준비

이제 반복문으로 가변 인자 개수만큼 반복하면서 va_arg 매크로로 값을 가져오면 됩니다. 이때 va_arg에는 가변 인자의 자료형을 지정해줍니다.

for (int i = 0; i < args; i++)    // 가변 인자 개수만큼 반복
{
    int num = va_arg(ap, int);    // int 크기만큼 가변 인자 목록 포인터에서 값을 가져옴
                                  // ap를 int 크기만큼 순방향으로 이동
    printf("%d ", num);           // 가변 인자 값 출력
}

int num = va_arg(ap, int);를 실행하면 현재 ap에서 4바이트(int 크기)만큼 역참조하여 값을 가져온 뒤 ap를 4바이트만큼 순방향으로 이동시킵니다.

그림 66‑2 va_arg로 ap에서 값을 가져온 뒤 포인터 이동

즉, 반복문에서 반복할 때마다 ap는 4바이트만큼 순방향으로 이동하므로 10, 20, 30, 40을 순서대로 가져올 수 있습니다.

마지막으로 va_end 매크로를 사용하여 apNULL로 초기화합니다.

va_end(ap);    // 가변 인자 목록 포인터를 NULL로 초기화

인텔/AMD x86, x86-64 플랫폼에서는 va_end 매크로를 사용하지 않아도 동작에 지장이 없습니다. 하지만 다른 플랫폼에서는 문제가 생길 수도 있으므로 호환성을 위해서 va_end로 마무리를 해주는 것이 좋습니다.

참고 | stdarg.h와 varargs.h

varargs.h는 오래된 컴파일러에서 제공하는 헤더 파일이며 stdarg.h는 C89 표준부터 추가된 헤더 파일입니다. varargs.hstdarg.h와 사용법이 거의 비슷합니다.