C언어 가변인자

C언어 가변인자


가변 인자란?

 - 인수의 개수와 타입이 미리 정해져 있지 않다는 뜻

 - 대표적인 함수 : printf

 - printf 함수는 전달되는 인수의 개수와 타입이 모두 다르지만 정상적으로 컴파일 되고 실행된다.


● printf 함수 원형

 - int printf(const char *format, ...);

format이라는 이름의 문자열 상수인데 흔히 서직 문자열이라고 부른다.

... 생각 하기로는 컴파일러에게 이후의 인수에 대해서는 개수와 타입을 점검하지 않도록 하는데 이 기호에 의해 가변 인수가 가능해진다.


가변 인수 함수의 개략적인 구조는 다음과 같다.

1
2
3
4
5
6
7
8
9
10
void varFunc( int fix, ... )
{
    va_list ap;
    va_start( ap, fix );
    while( 모든 인수를 읽을 때 까지 )
    {
        va_arg( ap, 인수타입 ) ;
    }
    va_end(ap);
}
cs

함수의 이름이나 원형, 고정 인수의 개수 등은 필요에 따라 마음대로 작성할 수 있고, 마지막 인수 자리에 ...만 있으면 가변 인수 함수가 된다.

:: va_list ap
함수로 전달되는 인수들은 스택(stack)이라는 기억 장소에 저장되며 함수는 스택에서 인수를 꺼내 쓴다. 스택에 있는 인수를 읽을 때 포인터 연산을 해야 하는데 현재 읽고 있는 번지를 기억하기 위해 va_list형의 포인터 변수 하나가 필요하다. va_list 타입은 char* 형으로 정의되어 있다.

:: va_start(ap, 마지막 고정 인수)
이 명령은 가변 인수를 읽기 위한 준비를 하는데 ap 포인터 변수가 첫 번째 가변 인수를 가리키도록 초기화한다. 첫 번째 가변 인수의 번지를 조사하기 위해서 마지막 고정 인수를 전달한다. va_start 내부에서는 ap가 마지막 고정 인수 다음 번지를 가리키도록 해 주므로 이후부터 ap 번지를 읽으면 순서대로 가변 인수를 읽을 수 있다.

:: va_arg(ap, 인수타입)
가변 인수를 실제로 읽는 명령이다. va_start가 ap를 첫 번째 가변 인수 번지로 맞추어 주므로 ap 위치에 있는 값을 읽기만 하면 된다. 단, ap 번지에 있는 값이 어떤 타입인지를 지정해야 이 매크로가 값을 제대로 읽을 수 있으므로 두 번째 인수로 읽고자 하는 값의 타입을 지정한다. 예를 들어 ap 위치에 있는 정수값을 읽고자 한다면 va_arg(ap, int)를 호출하고 실수값을 읽고자 한다면 va_arg(ap, double)이라고 호출하면 된다.

그런데 int 나 double 같은 타입 이름이 어떻게 함수의 인수로 전달될 수 있는가 하는 점이다. 함수의 인수로는 값이 전달된느 것이 정상인데 타입명이 어떻게 함수의 인수가 될 수 있을까??? va_arg는 함수가 아니라 매크로 함수이기 때문이다. va_arg의 두번째 인수는 내부적으로 sizeof 연산자와 캐스트 연산자로 전달되기 때문에 타입명이 될 수 있다.

::va_end(ap)
이 명령은 가변 인수를 다 읽은 후 뒷정리를 하는데 별다른 동작은 하지 않으며 실제로 없어도 전혀 지장이 없다. 이 명령이 필요한 이유는 호환성 때문인데 플랫폼에 따라서는 가변 인수를 읽은 후에 뒷처리르 해야 하는 경우도 있기 때문이다. va_end가 중요한 역할을 할수도 있으므로 호환성을 위해서는 관례적으로 넣어 주는 것이 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>
#include <stdarg.h>        // 가변인자 헤더
int getSum(int num, ...)
{
    int sum = 0;
    int i;
    va_list ap;
    int arg;
 
    va_start(ap, num);
 
    for (i = 0; i < num; i++) {
        arg = va_arg(ap, int);
        sum += arg;
    }
    va_end(ap);
    return sum;
}
void main(void)
{
    printf("1+2=%d\n", getSum(212));
    printf("3+4+5+6=%d\n", getSum(43456));
    printf("10~15=%d\n", getSum(6101112131415));
}
cs

첫 번째 인수 num은 전달될 정수 인수의 개수를 가지는 고정 인수

가변 인수 함수의 조건

1. 가변 인수 함수는 반드시 하나 이상의 고정 인수를 가져야 한다. 왜냐하면 가변 인수를 읽기 위한 포인터 ap를 초기화하기 위해서 마지막 고정 인수의 번지를 알아야 하기 때문이다.

2. 함수 내부에서 자신에게 전달된 가변 인수의 개수를 알 수 있도록 해야 한다. 전달될 수 있는 인수의 개수에는 제한이 없으며 컴파일러는 함수가 호출될 때 인수의 개수를 점검하지 않는다. 그래서 호출측에서 가변 인수가 몇 개나 전달되었는지를 알려주지 않으면 함수 내부에서 인수의 개수를 알 수 있는 방법이 없다. 함수 스스로 인수의 개수를 파악할 수 있도록 호출측이 정보를 제공해야 한다.

3. 개수와 마찬가지로 함수 내부에서 각각의 가변 인수 타입을 알 수 있어야 한다. %d가 제일 처음 나왔으면 첫번째는 정수, %f가 나오면 실수라는 것을 알게 된다. 가변 인수들의 타입을 알아야 하는 이유는 va_arg 매크로가 ap번지에서 가변 인수를 읽을 때 얼마만큼 읽어서 어떤 타입으로 해석해야 할지를 알아야 하기 때문이다. 가변 인수의 타입을 전달하는 방식도 여러 가지를 생각할 수 있는데 printf와 같이 하나의 고정 인수를 통해 모든 가변 인수의 타입을 판단할 수 있는 힌트를 제공하는 방식이 가장 좋다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <stdio.h>
#include <stdarg.h>
 
void printSum( const char *msg, ... )
{
    int sum = 0;
    va_list ap;
    int arg;
 
    va_start(ap, msg);
 
    for(;;) {
        arg = va_arg(ap, int);
        if ( arg == 0 )
            break;
        sum += arg;
    }
va_end(ap);
printf(msg, sum);
}
void main(void)
{
    printSum("1+2 = %d\n"1,2,0);
    printSum("3+4+5+6 = %d\n"3,4,5,6,0);
    printSum("10~15 = %d\n"10,11,12,13,14,15,0);
}
cs

printf는 첫 번째 고정 인수로 전달되는 서식 문자열에서 %d, %f, %s 같은 서식의 개수만큼 가변 인수를 읽음으로써 사실상 가변 인수의 개수를 전달받는다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdarg.h>
double getSum2(const char *types, ...)
{
    double sum = 0;
    va_list ap;
    const char *p;
 
    va_start(ap, types);
 
    for (p = types; *p; p++) {
        switch (*p) {
        case 'i':
            sum += va_arg(ap, int);
            break;
        case 'd':
            sum += va_arg(ap, double);
            break;
        }
    }
    va_end(ap);
    return sum;
}
void main(void)
{
    printf("1+2=%f\n", getSum2("ii"12));
    printf("2.5+3.8+4=%f\n", getSum2("ddi"2.53.84));
}
cs

1
2
3
4
5
6
7
8
9
#include <stdio.h>
void main(void)
{
    printf("%d%d\n"12);
    printf("%d%d%d\n"12345);
    printf("%d%d\n"13.14);
    printf("%f%f\n"12);
    printf("%s\n"1); // 1을 포인터로 인식, 1번지에 접근
}
cs

마지막 호출문은 %s로 되어 있어 가변 인수가 문자열인 것으로 전달되지만 실제 전달된 인수는 정수형이다. printf는 정수 1을 포인터로 해석하여 이 위치의 문자열을 읽으려고 시도하는데 절대 번지 2은 시스템 여역이기 때문에 다운되버린다.

매크로 분석

가변 인수에 대한 모든 지원은 표준 헤더파일 stdarg.h에 정의되어 있는 매크로에 의해 구현된다.
typedef char* va_list;
#define _INTSIZEOF(n) ( (sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
#define va_start(ap,v) ( ap = (va_list)&v + _INTSIZEOF(v) )
#define va_arg(ap,t) ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
#define va_end(ap) ( ap = (va_list)0 )

va_list는 단순히 char*형으로 정의되어 있다. 포인터 증감할 때 1바이트씩 증감하도록 하기 위해 char* 형으로 선언되어있다.
va_list 타입은 스택의 인수를 가리키는 포인터 타입이다.
_INTSIZEOF(n) 매크로는 인수로 전달된 타입 n의 길이를 계산하는데 n의 값에 따라 이 매크로의 계산 결과가 달라진다

printf("char = %d\n", _INTSIZEOF(char));                // char = 4
printf("short = %d\n", _INTSIZEOF(short));                // short = 4
printf("int = %d\n", _INTSIZEOF(int));                        // int = 4
printf("float = %d\n", _INTSIZEOF(float));                // float = 4
printf("double = %d\n", _INTSIZEOF(double));                // double = 8

이 매크로가 하는 일은 타입의 크기를 4의 배수로 올림한다고 할 수 있는데 좀 더 정확하게 표현하자면 정수형의 크기에 대한 배수로 올림한다.
정수형의 크기는 시스템마다 다른데 16비트 환경에서는 2바이트 32비트 환경에서는 4바이트이며 이 크기는 스택 하나의 크기이기도 하다

va_start 매크로는 가변 인수의 위치를 가리키는 포인터 ap를 초기화하는데 이 초기화를 위해 마지막 고정 인수 v를 전달해야 한다. ap는 마지막 고정 인수 v의 번지에 v의 크기를 더한 번지로 초기화된다. 스택에 인수가 들어갈 때는 전달된 역순으로 들어가므로 가변 인수들이 먼저 전달(높은 번지)되고 고정 인수가 가장 제일 끝에 전달(낮은 번지)된다.


이 상태에서 &v는 고정 인수의 번지를 가리키며 이 번지를 char*로 캐스팅한 후 고정 인수의 길이만큼 더하면 바로 아래에 있는 첫 번째 가변 인수의 번지를 구할 수 있다. va_start 매크로는 이 연산을 통해 ap를 가변 인수의 시작 번지로 초기화하여 가변 인수를 일기 위한 준비를 마친다. 이후 ap에 있는 값을 읽기만 하면 가변 인수의 값을 구할 수 있는데 이 동작을 하는 매크로가 바로 가변 인수 액세스의 핵심인 va_arg 매크로이다.

va_arg함수는 ap를 일단 가변 인수의 길이만큼 더해 다음 가변 인수 번지로 이동시킨다. 그리고 다시 길이를 빼서 원래 자리로 돌아온 후 이 번지를 t타입의 포인터로 캐스팅하여 *연산자로 그 값을 읽는다. 이 매크로는 ap의 값을 읽기만 하는 것이 아니라 다음 번 va_arg 호출을 위해 ap를 방금 읽은 가변 인수 다음의 번지로 옮겨 주는 동작까지 해야 하기 때문에 길이를 더했다가 다시 뺀 후 그 위치를 읽도록 되어 있다.


va_arg(ap,t) 호출문은 ap 번지에 있는 가변 인수를 t타입으로 읽고 그 길이만큼 ap를 증가시켜 다음 가변 인수를 읽을 수 있도록 준비한다. 그래서 va_arg를 계속 호출하면 가변 인수들을 연속적으로 액세스 할 수 있다. 단 va_arg가 인수를 정확하게 읽고 그 길이만큼 다음 위치로 이동하기 위해서는 가변 인수의 타입을 반드시 알려 주어야 한다. va_arg 매크로의 동작을 좀 더 잘게 분할해 본다면 다음과 같다.


ret = *(*t)ap;

ap += _INTSIZEOF(t);

return ret;


ap 포인터를 t*로 캐스팅한 후 이 자리에 있는 값을 읽어 ret에 대입해 놓고 ap는 t의 크기만큼 증가시켜 다음 위치로 이동한다. 그리고 전체 결과로 ret를 리턴한다.

va_end 매크로는 가변 인수를 가리키던 ap 포인터를 NULL로 만들어 무효화시킨다


가변 인수 함수의 활용


가변 인수 함수는 한 번 호출로 여러 개의 정보를 다양한 방법으로 다룰 수 있다는 면에서 편리하다. 특히 printf 함수는 다양한 타입의 변수들을 한꺼번에 출력할 수 있어 변수값을 확인해 볼 때 아주 유용하다.


출처 : 고니

댓글

Designed by JB FACTORY