Skip to content

59. 함수 가변 인자


1. 키워드

  • 가변 인자(Variable Argument)


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

  • C에서 함수를 사용하다 보면 printf, scanf와 같이 매개변수의 개수가 정해지지 않은 함수가 있다.
  • 이렇게 매번 함수에 들어가는 인수의 개수가 변하는 것을 가변 인자라고 한다.


3. 가변 인자 함수 만들기

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


반환값자료형 함수이름(자료형 고정매개변수, ...)
{
}
#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에 들어가고 나머지 값들은 ... 부분에 들어간다.


  • 이제 ... 부분의 매개변수를 사용해 보자.


#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에 정의된 가변 인자 처리 매크로는 다음과 같다.


1] va_list

  • 가변 인자 목록. 가변 인자의 메모리 주소를 저장하는 포인터이다.

2] va_start

  • 가변 인자를 가져올 수 있도록 포인터를 설정한다.

3] va_arg

  • 가변 인자 포인터에서 특정 자료형 크기만큼 값을 가져온다.

4] 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(Argument Pointer)와 가변 인자 개수 args를 넣어 가변 인자를 가져올 수 있도록 준비한다.


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

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


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


001


  • 이제 반복문으로 가변 인자 개수만큼 반복하면서 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바이트만큼 순방향으로 이동시킨다.


002


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


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


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


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


stdarg.hvarargs.h

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


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

  • 앞에서는 가변 인자의 자료형이 모두 int 타입이었다.
  • 자료형이 다른 가변 인자를 처리할 때는 switch와 가변 인자를 함께 사용하면 된다.


#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!"는 문자열 리터럴이기 때문에 문자열이 그대로 들어가지 않고 문자열의 주소가 들어간다.


003


  • 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로 초기화


004


  • 즉, 반복문에서 반복할 때마다 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 타입을 사용할 수 있다.


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


  • 하지만, GCC에서는 char 타입 문자일 때 va_arg 매크로에 char 타입 대신 int 타입을 사용해야 한다.


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


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


1] char, bool 타입 → int 타입

2] short 타입 → int 타입

3] float 타입 → double 타입


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

References