티스토리 뷰


C언어 가변 인자(가변 파라미터)를 사용해보자

1. 가변인자란 무엇일까?

printf 함수를 써보셨나요? 우리는 자연스럽게 printf("%d * %d = %d", 3, 5, 3*5)라고 쓰고 있습니다. 가만보면 printf라는 함수는 인자를 1개만 넣어도 되고, 2개만 넣어도 되고, 3개, 4개 그 이상을 넣어도 문제없이 돌아갑니다. c언어에서 이런것이 가능한가요?

오늘은 이런 마법을 부릴 수 있게 하는 가변 인자라는 것에 대해 포스팅을 하겠습니다.

2. printf의 원형

printf 함수의 원형을 찾아보신 분이 계실지 모르겠습니다. printf의 원형을 찾아보면 다음과 같습니다.

int printf(const char* format, ...)
  • 참조: 컴파일러에 따라 printf의 실제 구현과는 원형(prototype)이 다를 수 있습니다.

printf 함수의 두 번째 인자로 사용되는 ...가변 인자 혹은 가변 파라미터라고 불리는 것입니다. 본 포스팅 서두에 언급한것처럼 printf를 쓸 때, 인자(파라미터)로 아무것도 넘겨주지 않을 수도 있고, 혹은 여러 개의 인자를 넘겨줄 수도 있습니다. 가변 인자가 무엇인지 알았으니 이제 차근차근 설명해 드리도록 하겠습니다.

먼저 가변 인자 함수를 만들기 위해서는 stdarg.h 헤더파일을 포함해야합니다. 이 헤더 파일에 가변인자함수를 만들 때 필요한 각종 매크로 들이 정의되어 있습니다. 그리고 최소 1개 이상의 고정 인수가 있어야 합니다. 가변인자를 나타내는 ...파라미터 순서 상 가장 뒤에 있어야 합니다.

3. 가변 인자를 사용하는 첫 번째 예제

말로만 설명하면 헷갈리실테니 먼저 소스코드 예제를 보여드리겠습니다.

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

int sum(int count, ...)
{
    int res = 0;
    va_list ap;
    int i;

    va_start(ap, count);
    
    for(i=0; i<count; i++)
        res += va_arg(ap, int);

    va_end(ap);

    return res;
}

int main()
{
    printf("%d\n", sum(10, 1,2,3,4,5,6,7,8,9,10));

    return 0;
}

[ 출력 결과 ]

55

위 예제는 가변 인자를 이용해서 모든 파라미터를 더해주는 sum() 함수를 만든 것입니다. 자, 그럼 지금부터 가변 인자에 필요한 애들을 소개하겠습니다.

  • va_list : 각 가변 인자의 시작 주소를 가리킬 포인터입니다. 모양은 멋있게 생겼지만 내부적으로는 char * 로 정의되어 있는 녀석입니다.

  • va_start : va_list로 만들어진 포인터에게 가변인자 중 첫 번째 인자의 주소를 가르쳐주는 중요한 매크로입니다. 이 녀석의 모양은 사실 이렇게 생겼습니다. (Microsoft Visual Studio 기준)

    #define va_start(ap, v)  ( (ap) = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v) )
    
    • ap: va_list 로 만든 포인터가 담깁니다.
    • v: 마지막 고정인수가 담깁니다.
    • _ADDRESSOF(v) => &(v), 즉 주소로 바꿔주는 매크로입니다.
    • _INTSIZEOF(n) => ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) ), 비트 연산이 들어가는데 자세한 계산까지는 알 필요 없습니다. 마지막 고정인수의 사이즈를 구해서 그 다음 인자의 시작주소. 즉, 가변인자의 시작주소까지의 메모리상의 거리 를 구해주는 매크로입니다.
  • va_arg : 특정 가변인자를 가리키고 있는 va_list의 포인터를 다음 가변인자로 이동시켜 주는 매크로입니다. 이 녀석의 모양은 아래와 같습니다.

    #define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
    
    • ap: va_list로 만든 포인터가 담깁니다.
    • t: intlong, double과 같은 타입 이름이 담깁니다.
    • [참고] char, short 의 경우에는 int로 대신 쓰고, flaot의 경우에는 double로 대신 쓴 이후 형 변환을 해주어야 한다고 합니다. (예. char ch = (char) va_arg(ap, int); )
  • va_end : 사용한 가변인자 변수를 끝낼때 사용합니다. 단순히 모양을 보면 NULL 포인터로 돌려주는 매크로인데, 프로그램상 어떤 경우가 생길 지 모르니까 놓치지 말고 써주도록 합시다.

    #define va_end(ap)      ( ap = (va_list)0 )
    
    • ap: va_list로 만든 포인터가 담깁니다.

4. vsprintf/vnsprintf 의 사용

그리고 가변인자함수를 사용할 때, 많이 사용하는 함수가 하나 더 있는데요.

int vsprintf(char* dest, const char* format, va_list args)
int vsnprintf(char* dest, size_t maxCount, const char* format, va_list args)
  • 함수명: vsprintf / vnsprintf
  • 필요헤더: stdio.h
  • 리턴타입: int
  • 파라미터:
    • dest: format에 따라 만들어진 내용이 담길 버퍼
    • format: 포맷
    • args: 가변 파라미터
    • 리턴값: 문자열의 길이
  • 함수설명: dest 변수에 형식에 따라 만들어진 문자열이 저장된다.

바로 요놈입니다! sprintf와 상당히 비슷하게 생긴 이놈. 이 놈은 다양한 타입의 가변 인자들을 %d, %s, %c 등의 형식을 읽어서 알아서 예쁘게 포장해주는 함수입니다.

만약 이런 함수가 없고, 가변인자에 따라 우리가 수동으로 코딩을 해야 한다고 가정하면 다음과 같은 작업을 해야할 겁니다. %d 를 발견했을 때는 타입이 int 형이므로 다음 가변 인자를 va_arg(ap, int) 로 받아와서 담고, %s를 발견했을 때는 char * 형이므로 va_arg........

물론 이렇게 직접 만들어주어도 되지만!! 자동으로 포장해주는 함수가 만들어져 있잖아요! 그럼 우린 얘를 쓰면 되는겁니다. ㅎㅎ 어디다 쓰는지는 아래 예제를 보도록 하겠습니다. 제가 가변인자를 사용하는 90%는 아래와 같은 목적때문입니다!

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

void errorPrintf(char* fmt, ...)
{
    char buf[512] = {0,};
    va_list ap;

    strcpy(buf, "[ERROR] ");
    va_start(ap, fmt);
    vsprintf(buf + strlen(buf), fmt, ap);
    va_end(ap);

    puts(buf);
}

int main()
{
    int a = 10, b = 0;

    if(b != 0)
        printf("%d\n", a / b);
    else
        errorPrintf("Don't divide by %d\n", b);

    return 0;
}

[출력결과]

[ERROR] Don't divide by 0

아주 간단하죠? 위와 같이 에러 처리용 함수로 만들어 사용할 수 있습니다. 이름도 errorPrintf 이런식으로 바꿔서 사용하게 됨으로써, 프로그램 코드를 볼 때 "아! 이 부분은 에러처리부분!" 하며 넘어갈 수 있도록 코드 가시성이 높아지구요.

프로그램 동작 중 발생하는 에러에 대해 errno로 저장하는 프로그래밍이 되어 있다면 어떤 에러가 발생했는지 앞, 또는 뒤에 덧붙여서 출력해 줄 수도 있습니다. (물론 출력이 아니고 fprintf 등을 이용하여 프로그램 에러로그 안에 담을 수도 있습니다.)

아무튼 이렇게 가변인자에 대한 사용을 알아보았습니다! 이상으로 오늘의 포스팅을 마치도록 하겠습니다.

  • 참조: 원래 매개변수와 인자(parameter, argument) 등의 용어는 명백히 구별되지만, 본문에서는 거의 같은 의미로 놓고 사용 되었기에 용어가 왔다갔다 할 수 있습니다. 두 용어의 의미 상 구별을 하지 않았다는 점, 다시 한 번 말씀드립니다.

댓글
댓글쓰기 폼