C, C++ 문법

가변인수 다루기

디버그정 2009. 10. 22. 16:27

가변인수, va_start, va_list, va_end 1

.가변 인수 함수

여기서는 가변 인수 함수에 대해서 알아 본다. 가변 인수의 함수를 만드는 방법에 대해서는 물론이고 가변 인수 함수가 동작하는 원리에 대해서도 자세하게 분석해 것이다. 조금 어렵기는 하지만 포인터를 적절하게 활용하는 예를 있으며 포인터로 어떤 일이 가능한지를 경험할 있는 좋은 기회가 것이다. 가변 인수 함수가 어떻게 동작하는지를 설명할 있다면 포인터를 정복했다고 생각해도 좋다.

가변 인수란 그대로 인수의 개수와 타입이 미리 정해져 있지 않다는 뜻이며 그런 인수를 사용하는 함수를 가변 인수 함수라고 한다. 가변 인수 함수의 가장 좋은 예는 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 함수의 경우 고정 인수인 서식 문자열을 먼저 전달하고 서식의 개수와 타입에 맞는 인수들을 순서대로 전달하기만 하면 된다. 그렇다면 이런 가변 인수를 취할 있는 함수는 어떻게 만드는지 알아보자. 관건은 자신에게 전달된 임의 타입의 인수들을 순서대로 꺼내서 정확한 값을 읽는 것이다. 가변 인수 함수의 개략적인 구조는 다음과 같다.

 

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 되어 있는데 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

 

num 다음의 가변 인수가 1개든, 10개든, 100개든 GetSum 함수는 전달된 모든 인수의 합계를 구해 것이다. GetSum 함수에서 가변 인수들을 어떻게 읽는지 분석해 보자. va_list형의 포인터 ap 선언하고 va_start(ap,num) 호출로 ap 마지막 고정 인수 num 다음의 위치, 그러니까 번째 가변 인수를 가리키도록 초기화했다. 그리고 num만큼 루프를 돌면서 va_arg(ap,int) 호출로 ap 위치에 있는 int값을 계속 읽어 sum 누적시킨다.

모든 가변 인수를 읽었으면 va_end(ap)


=================== 다른 사용 례 ===================================

int PrintToEdit(HWND hWnd, TCHAR * pzChar, ...)
{
 TCHAR szBuffer[1024];
 va_list Arg_List;

 va_start(Arg_List, pzChar);
 wvsprintf(szBuffer, pzChar, Arg_List);
 va_end(Arg_List);

 SendMessage(hWnd, EM_SETSEL, (WPARAM) -1, (LPARAM) -1);
 SendMessage(hWnd, EM_REPLACESEL, FALSE, (LPARAM)szBuffer);
 return 0;
}


================ va_list 선언하지 않고 바로 대입하는 식,,, 간편 ============

개요

윈도우에서 모 하나 출력하려면,
콘솔환경처럼 쉽게 printf 할 수가 없다.
그래서 버퍼를 써서 그 버퍼안에 wsprintf 같은 평션을 써서
버퍼로 저장하고 그것을 메시지박스나 화면에 출력한다.
이것을 쉽게 하기위해서
자신만의 출력함수를 만들어보자.

wsprintf()
wvsprintf()

wsprintf

이 함수를 사용하면,
간단히 문자를 숫자로 바꿀 수 있을 뿐 아니라,
숫자 이외에도 쉽게 문자열에다가 추가할 수 있다. (%s나 %c)로 말이다.
아래와 같이 하면, strTemp에 2000 이라는 문자값이 들어갈 것이다.

어느날 수미가 물은 적이 있는데,
그럼 문자열 배열을 하나의 문자열에다 저장할 순 없을까?
나는 문자열 반환하는 함수를 만들려구 했는뎅,
가만히 생각해보니까, 이렇게 해도 되었다..
문자열 배열이든, 일반 숫자 배열이든,
콘솔환경에서는 그대로 for문 안에서 화면에 출력하면 되지만,
윈도우에서는 버퍼에 저장해서 그 버퍼를 출력해야 한다.
여기서는 배열값을 wsprintf 함수로 저장하는 팁을 소개한다.
얼핏보면 strcat 함수랑 비슷하당^^
( 여기에 나오는 사람이름은 본 강좌와 관련이 깊음,, 내가 적을 때 있던 사람들.. )

 


wvsprintf

긴말 필요 없구
밑의 평션 정의를 보라


그리고 호출시에는

WinPrintf("내이름:%s 내나이:%d", "김태영", 24);

이렇게 하면 메시지 박스가 뜬다, 물론 그냥 Text로 윈도우에 문자열 출력해도 된다.