본문 바로가기

진리는어디에

가변인자를 이용한 함수(va_list)

가변인자 함수

ANSI C 는 인수의 타입이나 개수를 다양하게 취할 수 있는 함수를 선언하기 위해 '가변인자 함수(varages 함수 or variadic 함수)'라는 구문을 정의 하고 있다.

1. 왜 가변인자 함수들이 사용되는가?

원래 C 함수들은 고정된 개수의 인수들을 취한다. 함수를 정의할 때, 인수의 개수와 인수의 데이터 타입을 정의하면 변경이 불가능 하다. '가변인자 함수'는 함수가 호출 될때 마다 인수의 개수를 변경 할 수 있고 타입에 있어서의 변경도 가능하다.

2. 어떻게 가변 인자 함수를 정의하고 사용하는가?

1) #include <stdarg.h >

'가변인자 함수'라는 것을 사용하기 위해서는 'stdarg.h' 헤더 파일이 필요하다.
※ 오래된 C 에서는 'varargs.h'를 사용해서 다양한 개수의 변수를 정의하는 함수 메터니즘을 제공하지만 호환 성이 없다.

2) 생략표시(...) 사용

매개변수의 리스트에 생략표시('...')를 사용하고, 가변 인수들을 엑세스할 수 있도록 특별한 매크로를 사용하여서 가변인수 함수를 정의한다.

3) 가변인수 들을 위한 구문

가변인수를 받아 들이는 함수는 올바른 프로토타입으로 선언되어야만 한다. ANSI C 구문은 '...'가 나오기 이전에 적어도 한 개의 고정 인수를 필요로 한다.

예를 들어,
int Func(const char* pCh, int b, ...) {
                   ...
}
고정된 두 개의 인수로써, const char*, int 인수를 취하고 int 형의 값을 반환하는 Func함수의 정의이다.

4) 인수 값들을 받기

보통 고정된 인수들은 개별적 이름을 가지고,그들의 값을 억세스 하기위해 그들의 이름을 이용 한다. 하지만 가변인수들은 아무런 이름이 없으므로 일반적인 방법으로는 그들에게 억세스 할 방법이없다.

가변인수를 억세스 하기위한 유일한 방법은 그들이 기록된 순서대로, 순차 억세스를 하고 다음에 열거되는 매크로들을 사용해야만 한다(매크로는 'stdarg.h'에 선언 되어져 있다).


   1. va_start를 사용해서 va_list 형의 포인터 변수를 인수로써 초기화한다.
      초기화된 인수 포인터는 첫 번째 가변인수를 가리킨다.

   2. va_arg를 호출함으로써 가변인수들을 억세스 한다.
       va_arg를 첫 번째 호출하면, 첫 번째 인수를 반환하게 되고, 다음 호출은 두 번째 인수를 반환
       하는 형식으로 호출 되는 횟수에 따라 순차적으로 다음 인수를 반환한다.

        만일 남겨진 가변인수를 무시하기를 원한다면 언제든지 멈출 수 있다. 호출로 공급된 인수들
      보다는 소수의 인수들을 억세스 하는 함수를 위해서 아주 좋지만, 만일 너무 많은 인수들을 억
      세스 하려 시도한다면 쓰레기 값을 얻게 될 것이다.

   3. va_end를 호출해서 포인터 변수인 인수를 끝냈음을 알린다.
     ※ 실제로, 대부분 C 컴파일러(GNU C 등)에서, va_end의 호출은 아무 일도 하지 않고 그것을
       실제로 호출할 필요가 없다. 하지만 항상 예외 상황은 있을 수 있다.

단계 1과 3은 가변 인수를 받아들이는 함수에서 반드시 수행되어야만 한다. 그렇지만, 다른 함수에 인수로써 va_list 변수를 줄 수 있고 전부 또는 단계 2를 수행할 수 있다.

단일한 함수 호출에서 여러 번 세 단계의 전부를 반복해서 수행 할 수 있다.
만일 가변 인수를 무시하기를 원한다면, 세단계를 하지 않을 수 있다.

만일 원한다면 포인터 변수인 한 개의 인수보다 더 많은 것을 가질 수 있다.
원할 때 va_start로 각 변수를 초기화 할 수 있고, 그러고 나면 원하는 각각의 포인터 인수를 추출할 수 있다.
각 포인터 변수인 인수는 인수 값들의 같은 집합을 통해서 진행되지만, 그것은 자신만의 페이스(pace)를 갖는다.

이식성 어떤 컴파일러로, 서브루틴(subroutine)에 인수로써 포인터 변수를 사용한다면,
서브루틴이 반환한 후에 같은 포인터 변수인 인수를 사용해서 기록하지 않아야만 한다.

완벽한 이식성을 위해서, va_end에 그것을 주어야한다.
이것은 실제로 ANSI C의 권장사항이지만, 대부분 ANSI C 컴파일러는 다행이 상관없이 작업한다.


3. 어떻게 많은 인수들이 공급되는가?

가변 인수들의 타입과 개수를 알 수 있는 일반적인 방법은 없다. 그래서 누구든 그 가변인수가 얼마나 많은 인수들을 가졌고, 그것이 무슨 종류인지를 알아낼 수 있는 특별한 방법의 함수를 고안해야한다.
그것은 가변인수 함수의 호출 관습에 적당하게 정의되어야 하고, 그것에 근거하여 프로그램에서 가변 인수 함수를 호출해야 한다.

호출관습의 한가지는 한 개의 고정된 인수를 사용해서 가변인수의 개수를 공급하는 것이다.  이 방법은 공급된 가변 인수들이 모두 같은 형일 경우에 가능한 방법이다.

그와 유사한 방법으로는 가변인수가 공급될 가능성에 대한 정보를 한 비트에 담은, 비트 마스크가될 고정인수를 인수중에 하나로 갖는 것이다. 고정된 인수는 가변 인수들의 개수와 타입, 이 둘을 지정하는 패턴으로써 사용될 수 있다. printf에서 형식화된 문자열 인수는 이것의 한 예가 된다.

다른 가능성은 마지막 가변 인수로써 "끝 표시"값을 사용하는 것이다.
예를 들어, 예측할 수 없는 포인터 인수들의 개수를 처리하는 함수가 있다면, 널 포인터는 인수 리스트의 끝을 지적할 것이다. (이것은 널 포인터가 함수에게 의미 있는 값이 아니라고 가정한다. )
execl 함수는 이 방법으로 작업한다;.

4. 가변인수 함수들을 호출하기

  가변인수 함수를 호출할 때 특정한 어떤 것을 써서는 안된다. 단지 괄호안에 보통, 콤마에 의해 분리된 인수들(가변으로써, 요청된 인수)만 사용하라. 그러나 프로토타입으로 그 함수를 선언함으로써 준비하고, 그 인수의 값들이 어떻게 변환되는지를 알아야만 한다. 원칙적으로, 가변으로써 정의된 함수들은 그들을 호출할 때마다 함수 프로토타입을 사용해서 가변이 되도록 선언 되어야 한다. 이것은 함수가 가변 인수 또는 고정된 인수를 취하는지의 여부에 의존하여 함수에 인수 값들을 부여하는 다른 호출 관습을 가진 C 컴파일러 때문이다. 실제로, GNU C 컴파일러는 항상 가변인수 또는 요청된 인수를 사용하는지에 상관없이 같은 방법으로 인수형의 주어진 집합을 부여한다.


  그래서, 인수들의 타입이 자체-진행인 동안, 그들의 선언을 안전하게 생략할 수 있다.
보통 가변함수를 위해서 인수의 형을 선언하는 것은 좋은 방법이고, 모든 함수들을 위해서는 물론 당연한 것이다.

  그런데 몇 개의 함수는 그렇지 않은 경우가 있다_예를 들어, open과 printf함수의 프로토타입이 가변인수들의 타입을 정하지 않았을 때, 가변인수 함수를 호출하면, 함수의 가변 인수 값들은 디폴트 인수 승급이 수행된다.

디폴트 인수 승급이란 char 또는 short int (부호가 있던지 없던지)의 형을 가진 오브젝트들은 int 나 unisgned int로 승급되고, float의 형을 가진 오브젝트들은 double로 승급되는 것을 말한다. 그래서, 가변인수에 char형의 값을 넣으면,그것은 int로 승급되어 진다.

고정 인수들은 보통 함수의 원형을 통해서 제어된다: 인수 표현식은 마치 그형의 변수로 할당되었던 것 선언된 인수의 형으로 변환 된다.


5. 인수 억세스 매크로들

다음은 가변 인수들을 가져오기 위해서 사용되는 매크로에 대한 기술이다.
그 매크로들은 헤더파일 `stdarg. h'에 정의되어 있다.

데이터 타입 : va_list

va_list는 포인터 변수들인 인수를 위해서 사용된다.
매크로 : void va_start (va_list ap, last_required)

이 매크로는 현재 함수의 가변 인수들의 첫 번째를 가리키는 포인터 변수 ap를 초기화한다;
lastrequired는 함수에 있는 마지막 고정인수가 되어야 한다.
매크로 : type va_arg (va_list ap, type)

va_arg 매크로는 다음 가변 인수의 값을 반환하고, 다음 인수를 가리키도록 ap의 값을 갱신한다.
그래서, va_arg의 성공적인 사용은 가변 인수들을 성공적으로 반환한다.

va_arg에 의해 반환된 값의 타입은 호출에서 정했던 타입이다.
type은 반드시 실제 인수의 타입과 매치되는 자체-승급 타입 (char나 short int 나 float가 아닌)이 되어야 한다.
매크로 : void va_end (va_list ap)

이것은 ap의 사용을 끝낸다.
va_end 호출 후에, 다음에 같은 ap를 사용해서 va_arg를 호출하면 작업하지 않을 것이다.
같은 ap 인수를 사용하는 va_start를 호출했던 함수를 반환하기 전에 va_end를 호출해야만 한다.
GNU C 라이브러리에서, va_end는 아무 일도 하지 않기 때문에 이식성의 이유가 아니라면 va_end를 호출할 필요가 없다.


6. 가변인수 함수의 예제

다음은 인수들을 가변적인 개수로 받아들이는 함수에 대한 예이다.
함수의 첫 번째 인수는 반환된 결과와 합산된, 남겨진 인수들의 개수이다.
이 함수는 가변 인수 기능을 어떻게 사용하는지 설명하는데 충분하다.

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

int add_em_up (int count, . . . )
{
   va_list ap;
   int i, sum;

   va_start (ap, count);
   /* 인수 목록을 초기화하라. */
   sum = 0;
   for (i = 0; i < count; i++)
       sum += va_arg (ap, int);
   /* 다음 인수값을 얻어라. */

   va_end (ap);
   /* 정리하라. */

   return sum;
}

int main (void)
{
   /* 이 호출은 16을 출력한다. */
   printf ("%d\n", add_em_up (3, 5, 5, 6));

   /* 이 호출은 55를 출력한다. */
   printf ("%d\n", add_em_up (10, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10));
   return 0;
}




부 록

 ANSI C 이전에, 프로그래머들은 가변함수들을 쓰기 위해서 완전히 다른 기능을 사용했었고, 'varargs. h'라는 헤더 파일을 사용 했었다.

  `varargs. h'를 사용하는 것은 `stdarg. h'를 사용하는것과 거의 같다. 가변인수 함수의 호출 부분에서는 거의 다를바가 없으나 정의 방법에서 그 차이를 보인다.

무엇보다도, 오래된 형태의 비 프로토타입 구문을 사용해야만 한다.

오래된 형태의 가변인수 함수들을 정의하기 위해서는 특정한 매크로가 사용된다:

매크로 : va_alist
이 매크로는 가변인수 함수에 있는 고정 인수이름 목록을 나타낸다.

매크로 : va_decl
이 매크로는 가변인수 함수를 위한 인수들이나 또는 암묵적인 인수를 선언한다.

매크로 : void va_start (va_list ap)
`varargs. h'에 선언된 이 매크로는 현재 함수의 첫 번째 인수를 가리키는 포인터로 포인터 변수인 인수를 초기화한다.


다른 인수 매크로, va_arg 와 va_end는 `varargs. h' 와 `stdarg. h'의 것이 서로 같다;
동일한 컴파일 단위에서 `varargs. h'와 `stdarg. h'가 둘다 인클루드 되어서는 안된다;
va_start가 서로 충돌하게 된다.

내용출처 : http://cespc1.kumoh.ac.kr/~ygkim/voop/vararg.txt

유익한 글이었다면 공감(❤) 버튼 꾹!! 추가 문의 사항은 댓글로!!