티스토리 뷰

15-3.가변 인수

15-3-가.가변 인수 함수

여기서는 가변 인수 함수에 대해서 알아 본다. 가변 인수의 함수를 만드는 방법에 대해서는 물론이고 가변 인수 함수가 동작하는 원리에 대해서도 자세하게 분석해 볼 것이다. 조금 어렵기는 하지만 포인터를 적절하게 활용하는 예를 볼 수 있으며 포인터로 어떤 일이 가능한지를 경험할 수 있는 좋은 기회가 될 것이다. 가변 인수 함수가 어떻게 동작하는지를 설명할 수 있다면 포인터를 정복했다고 생각해도 좋다. 가변 인수를 읽어내는 포인터 연산식을 해석할 수 있다는 것은 포인터를 충분히 이해했다는 증거이다. 이 절의 내용은 다소 어렵고 함수에 대한 이해가 필요하므로 다음 장의 스택 프레임을 먼저 읽어 본 후 공부하는 것이 효율적이다.

가변 인수란 말 뜻 그대로 인수의 개수와 타입이 미리 정해져 있지 않다는 뜻이며 그런 인수를 사용하는 함수를 가변 인수 함수라고 한다. 가변 인수 함수의 가장 좋은 예는 C언어의 가장 기초 함수인 printf이다. C언어를 배우는 사람이 가장 먼저 배우는 친근한 함수이므로 이 함수를 통해 가변 인수 함수를 어떻게 사용하는지 연구해 보자. 이 함수는 서식 문자열과 서식에 대응되는 임의 타입의 인수들을 개수에 상관없이 전달받을 수 있다. 다음이 printf 함수의 호출 예이다.

 

printf("정수는 %d이고 실수는 %f이다.",i,d);

printf("이름=%s, 나이=%d, 키=%f","김상형",25,178.8);

printf("%d + %f = %f", 123, 3.14, 123+3.14);

 

printf 함수로 전달되는 인수의 개수와 타입이 모두 다르지만 정상적으로 컴파일되고 실행된다. 반면 gotoxy(10,15,"quickly")나 strcpy(src,dest,3) 따위의 호출은 당장 컴파일 에러로 처리된다. 이런 함수들은 가변 인수를 받아들이지 않기 때문에 헤더 파일에 적힌 원형대로 정확하게 인수의 개수와 타입을 맞춰서 호출해야 한다. 인수가 남아서도 안되며 모자라도 안되고 타입이 틀려도 에러로 처리된다. 그렇다면 printf 함수의 원형은 어떻게 선언되어 있길레 가변 인수를 처리할 수 있을까? 다음이 printf 함수의 원형이다.

 

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

 

이 함수의 첫 번째 인수는 format이라는 이름의 문자열 상수인데 흔히 서식 문자열이라고 부른다. 두 번째 이후의 인수에는 타입과 인수 이름이 명시되어 있지 않으며 대신 생략 기호(ellipsis)인 ...이 적혀 있다. 생략 기호는 컴파일러에게 이후의 인수에 대해서는 개수와 타입을 점검하지 않도록 하는데 이 기호에 의해 가변 인수가 가능해진다.

컴파일러는 ... 이후의 인수에 대해서는 개수가 몇개든지 어떤 타입이든지 상관하지 않고 있는 그대로 함수에게 넘기므로 임의 타입의 인수들을 개수에 상관없이 전달할 수 있다. 대신 전달된 인수의 정확한 타입을 판별하여 꺼내쓰는 것은 함수가 알아서 해야 한다. 컴파일러는 인수를 마음대로 취할 수 있도록 허락은 해 주지만(사실은 허락이 아니라 무관심이다) 뒷일에 대해서는 절대로 책임지지 않는다.

생략 기호 이전에 전달되는 인수를 고정 인수라고 하는데 printf 함수의 경우 format 인수가 바로 고정 인수이다. 고정 인수는 원형에 타입과 개수가 분명히 명시되어 있으므로 원형대로 정확하게 전달해야 한다. printf가 아무리 가변 인수를 지원한다고 하더라도 printf(1, 2)나 printf(3.14) 따위의 호출은 안된다. printf의 첫 번째 인수는 반드시 const char * 타입의 서식 문자열이어야 하며 두 번째 인수부터 가변 인수이다. 그래서 정수 하나를 출력할 때는 printf(i)가 아니라 printf("%d",i)로 호출해야 한다.

가변 인수 함수를 사용하는 것은 별로 어렵지 않다. printf 함수의 경우 고정 인수인 서식 문자열을 먼저 전달하고 서식의 개수와 타입에 맞는 인수들을 순서대로 전달하기만 하면 된다. 그렇다면 이런 가변 인수를 취할 수 있는 함수는 어떻게 만드는지 알아보자. 관건은 자신에게 전달된 임의 타입의 인수들을 순서대로 꺼내서 정확한 값을 읽는 것이다. 가변 인수 함수의 개략적인 구조는 다음과 같다.

 

void VarFunc(int Fix, ...)

{

     va_list ap;

     va_start(ap,Fix);

     while (모든 인수를 다 읽을 때까지) {

          va_arg(ap,인수타입);

     }

     va_end(ap);

}

 

물론 함수의 이름이나 원형, 고정 인수의 개수 등은 필요에 따라 마음대로 작성할 수 있다. 마지막 인수 자리에 ...만 있으면 가변 인수 함수가 된다. 가변 인수 함수 내부에서는 인수를 읽기 위해 이상한 모양의 매크로 함수들을 많이 사용하는데 이 문장들을 각각 분석해 보자.

 

va_list ap

함수로 전달되는 인수들은 스택(Stack)이라는 기억 장소에 저장되며 함수는 스택에서 인수를 꺼내 쓴다. 스택에 있는 인수를 읽을 때 포인터 연산을 해야 하는데 현재 읽고 있는 번지를 기억하기 위해 va_list형의 포인터 변수 하나가 필요하다. 변수 이름은 ap로 되어 있는데 아마도 Argument Pointer의 약자일 것이다. ap는 어디까지나 지역변수일 뿐이므로 이름은 마음대로 정할 수 있되 관습적으로 가변 인수를 다루는 매크로에서는 ap라는 이름을 사용한다. 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)이라고 호출하면 된다. 물론 리턴되는 값은 인수타입에 맞는 변수로 대입받아야 한다. 이 명령은 ap위치에서 타입에 맞는 값을 읽어 리턴하며 또한 ap를 다음 가변 인수 위치로 옮겨준다. 그래서 va_arg를 반복적으로 호출하면 전달된 가변 인수를 순서대로 읽을 수 있다.

그런데 이 명령에서 조금 이상한 점을 발견할 수 있는데 int나 double같은 타입 이름이 어떻게 함수의 인수로 전달될 수 있는가 하는 점이다. 함수의 인수로는 값이 전달되는 것이 정상인데 타입명이 어떻게 함수의 인수가 될 수 있는가 말이다. 타입명은 분명히 함수의 인수가 될 수 없다. 그럼에도 불구하고 va_arg가 타입명을 인수로 받아들일 수 있는 이유는 va_arg가 진짜 함수가 아니라 매크로 함수이기 때문이다. va_arg의 두 번째 인수는 내부적으로 sizeof 연산자와 캐스트 연산자로 전달되기 때문에 타입명이 될 수 있다.

va_end(ap)

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

 

여기까지 설명을 읽고 "음, 그렇군, 가변 인수 함수 만들기 무지 쉽군"이라고 한 번에 이해할 수 있는 사람은 많지 않을 것이다. 이 매크로들을 사용하는 방법과 정확한 동작 원리는 좀 더 연구해 봐야 할 과제이다. 일단 실제로 동작하는 가변 인수 함수를 하나 만들어 보자. 다음 예제의 GetSum 함수는 첫 번째 인수로 전달된 num 개수만큼의 정수 인수들의 합계를 구해 리턴한다.

 

: GetSum

#include <Turboc.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()

{

     printf("1+2=%d\n",GetSum(2,1,2));

     printf("3+4+5+6=%d\n",GetSum(4,3,4,5,6));

     printf("10~15=%d\n",GetSum(6,10,11,12,13,14,15));

}

 

GetSum 함수의 첫 번째 인수 num은 전달될 정수 인수의 개수를 가지는 고정 인수이며 이 인수 다음에 합계를 구하고 싶은 num개의 정수값을 나열하면 된다. 인수의 개수가 몇개이든간에 전달된 모든 값의 합계를 구해 리턴할 것이다. 실행 결과는 다음과 같다.

 

1+2=3

3+4+5+6=18

10~15=75

 

GetSum 함수에서 가변 인수들을 어떻게 읽는지 분석해 보자. va_list형의 포인터 ap를 선언하고 va_start(ap,num) 호출로 ap가 마지막 고정 인수 num 다음의 위치, 그러니까 첫 번째 가변 인수를 가리키도록 초기화했다. 그리고 num만큼 루프를 돌면서 va_arg(ap,int) 호출로 ap 위치에 있는 int값을 계속 읽어 sum에 누적시킨다. 모든 가변 인수를 다 읽었으면 va_end(ap)로 뒷정리를 하고 계산된 sum값을 리턴하였다. 앞에서 보인 기본 형식대로 va_ 매크로를 사용하여 가변 인수를 읽어 처리하기만 하면 되므로 사용만을 목적으로 한다면 그리 어렵지 않다.
 
-----------------------------------------------------------------------------------

va_start(ap, outbuf);
 while((va_arg(ap, int) & SEPS_FLAG) == SEPS_TYPE) nseps++;
 va_end(ap);

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/12   »
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
29 30 31
글 보관함