66.2 자료형이 다른 가변 인자 함수 만들기

앞에서는 가변 인자의 자료형이 모두 int형이었습니다. 그럼 각각 자료형이 다른 가변 인자는 어떻게 처리할까요? 이때는 switch와 가변 인자를 함께 사용하면 됩니다.

먼저 가변 인자를 처리할 때 사용할 각 자료형의 약칭입니다.

  • 정수(int): 'i'
  • 실수(double): 'd'
  • 문자(char): 'c'
  • 문자열(char *): 's'

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

variable_argument_multiple_types.c

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

void printValues(char *types, ...)    // 가변 인자의 자료형을 받음, ...로 가변 인자 설정
{
    va_list ap;    // 가변 인자 목록
    int i = 0;

    va_start(ap, types);        // types 문자열에서 문자 개수를 구해서 가변 인자 포인터 설정
    while (types[i] != '\0')    // 가변 인자 자료형이 없을 때까지 반복
    {
        switch (types[i])       // 가변 인자 자료형으로 분기
        {
        case 'i':                                // int형일 때
            printf("%d ", va_arg(ap, int));      // int 크기만큼 값을 가져옴
                                                 // ap를 int 크기만큼 순방향으로 이동
            break;
        case 'd':                                // double형일 때
            printf("%f ", va_arg(ap, double));   // double 크기만큼 값을 가져옴
                                                 // ap를 double 크기만큼 순방향으로 이동
            break;
        case 'c':                                // char형 문자일 때
            printf("%c ", va_arg(ap, char));     // char 크기만큼 값을 가져옴
                                                 // ap를 char 크기만큼 순방향으로 이동
            break;
        case 's':                                // char *형 문자열일 때
            printf("%s ", va_arg(ap, char *));   // char * 크기만큼 값을 가져옴
                                                 // ap를 char * 크기만큼 순방향으로 이동
            break;
        default:
            break;
        }
        i++;
    }
    va_end(ap);    // 가변 인자 포인터를 NULL로 초기화

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

int main()
{
    printValues("i", 10);                                       // 정수
    printValues("ci", 'a', 10);                                 // 문자, 정수
    printValues("dci", 1.234567, 'a', 10);                      // 실수, 문자, 정수
    printValues("sicd", "Hello, world!", 10, 'a', 1.234567);    // 문자열, 정수, 문자, 실수

    return 0;
}

실행 결과

10
a 10
1.234567 a 10
Hello, world! 10 a 1.234567

먼저 printValues 함수를 만들 때 가변 인자의 자료형 types는 문자열 포인터로 지정해주고 그 뒤에는 …를 사용하여 가변 인자로 지정해줍니다. 여기서 가변 인자의 자료형은 "i", "dci"처럼 약칭을 조합한 문자열로 받겠습니다.

void printValues(char *types, ...)    // 가변 인자의 자료형을 받음, ...로 가변 인자 설정

printValues 함수를 호출할 때는 다음과 같이 맨 처음에 가변 인자 자료형의 약칭을 순서대로 넣어줍니다. 그리고 자료형 약칭의 개수에 맞게 인수를 콤마로 구분하여 넣어주면 됩니다.

int main()
{
    printValues("i", 10);                                       // 정수
    printValues("ci", 'a', 10);                                 // 문자, 정수
    printValues("dci", 1.234567, 'a', 10);                      // 실수, 문자, 정수
    printValues("sicd", "Hello, world!", 10, 'a', 1.234567);    // 문자열, 정수, 문자, 실수
    
    return 0;
}

다시 printValues 함수 안을 살펴보겠습니다. 함수 안에서는 va_start 매크로에 가변 인자 목록 포인터 ap와 가변 인자 자료형 문자열 types을 넣습니다. va_start 매크로는 문자열을 넣으면 문자의 개수를 구해서 포인터를 설정해줍니다.

va_list ap;   // 가변 인자 목록
int i = 0;

va_start(ap, types);        // types 문자열에서 문자 개수를 구해서 가변 인자 포인터 설정

만약 가변 인자로 문자열, 정수, 문자, 실수가 들어있는 printValues("sicd", "Hello, world!", 10, 'a', 1.234567);를 호출한 뒤 va_start 매크로를 실행하면 다음과 같은 모양이 됩니다. "Hello world!"는 문자열 리터럴이기 때문에 문자열이 그대로 들어가지 않고 문자열의 주소가 들어갑니다.

그림 66‑3 va_start로 ap 준비

while 반복문을 사용하여 types의 문자가 '\0'(NULL)이 아닐 때까지 반복합니다(types는 문자열이므로 반드시 NULL로 끝납니다). 그리고 switch 분기문을 사용하여 types에 들어있는 문자로 분기한 뒤 va_argap에서 자료형 크기만큼 값을 가져옵니다. 예를 들어 types[i]'d'이면 double 크기만큼 값을 가져옵니다.

while (types[i] != '\0')    // 가변 인자 자료형이 없을 때까지 반복
{
    switch (types[i])    // 가변 인자 자료형으로 분기
    {
    case 'i':                                // int형일 때
        printf("%d ", va_arg(ap, int));      // int 크기만큼 값을 가져옴
                                             // ap를 int 크기만큼 순방향으로 이동
        break;
    case 'd':                                // double형일 때
        printf("%f ", va_arg(ap, double));   // double 크기만큼 값을 가져옴
                                             // ap를 double 크기만큼 순방향으로 이동
        break;
    case 'c':                                // char형 문자일 때
        printf("%c ", va_arg(ap, char));     // char 크기만큼 값을 가져옴
                                             // ap를 char 크기만큼 순방향으로 이동
        break;
    case 's':                                // char *형 문자열일 때
        printf("%s ", va_arg(ap, char *));   // char * 크기만큼 값을 가져옴
                                             // ap를 char * 크기만큼 순방향으로 이동
        break;
    default:
        break;
    }
    i++;
}
va_end(ap);    // 가변 인자 포인터를 NULL로 초기화
그림 66‑4 va_arg로 ap에서 값 또는 메모리 주소를 가져온 뒤 포인터 이동

즉, 반복문에서 반복할 때마다 ap"sicd" 순서대로 char 포인터, int, char, double 크기만큼 순방향으로 이동하므로 "Hello, world!", 10, 'a', 1.234567을 가져올 수 있습니다.

참고 | GCC에서 va_arg

va_arg 매크로는 Visual Studio와 GCC에서 허용하는 자료형이 조금 다릅니다. 먼저 Visual Studio에서는 va_arg(ap, char)처럼 char를 사용할 수 있습니다.

Visual Studio

    case 'c':                               // char형 문자일 때
        printf("%c ", va_arg(ap, char));    // char 크기만큼 값을 가져옴
        break;

하지만, GCC에서는 char형 문자일 때 va_arg 매크로에 char 대신 int를 사용해야 합니다.

GCC

    case 'c':                              // char형 문자일 때
        printf("%c ", va_arg(ap, int));    // int 크기만큼 값을 가져옴
        break;

즉, GCC에서 가변 인자로 받은 값의 자료형이 int보다 작다면 int로, float라면 double로 지정해야 합니다.

  • char,boolint
  • shortint
  • floatdouble

GCC 쪽이 C 언어 표준이며 Visual Studio에서는 예외적으로 허용하고 있는 상황입니다.

지금까지 함수에서 가변 인자를 사용하는 방법을 배웠는데 코드가 복잡하고 개념이 어려웠습니다. 실제로 가변 인자 함수는 만들 일이 그렇게 많지 않으므로 나중에 가변 인자 함수가 필요할 때 다시 돌아와서 찾아보면 됩니다.