728x90

printf()와 scanf()와 같은 가변 인자를 받는 함수를 만들거나 이 함수를 덮어 쓰는 wrapper를 만들려면 가변 인자를 처리할 줄 알아야 합니다.


가변 인자를 처리하려면 다음과 같은 data type과 함수(또는 매크로 함수)를 써야 합니다.


• va_list : type

• va_start(va_list argptr, last-arg) : 가변 인자 처리 시작

• va_end(va_list argptr) : 가변 인자 처리 끝

• T va_arg(va_list argptr, T) : 가변 인자 얻기


이들은 모두 표준 헤더 파일인 <stdarg.h>에 선언되어 있습니다. 일단 간단히 N개의 정수(int type)를 받아서 더하는 함수를 만들어 봅시다:

#include <stdarg.h>

int
sum(int nargs, ...)
{
  va_list argptr;
  int i, total = 0;

  va_start(argptr, nargs);        /* 가변 인자 처리 시작 */
  for (i = 0; i < nargs; i++)
    total += va_arg(argptr, int); /* 하나씩 가변 인자 얻기 */
  va_end(argptr);                 /* 가변 인자 처리 끝 */

  return total;
}


위와 같이 정의해 놓고 다음과 같이 쓸 수 있습니다 (주의: sum()의 첫번째 인자는 두번째부터 나올 가변 인자의 갯수입니다):

int sum(int nargs, ...);

int
main(void)
{
  int s1, s2;

  /* 1부터 5까지 더한 값을 s1에 저장. */
  s1 = sum(512345);

  /* 100, 200, 300을 더한 값을 s2에 저장. */
  s2 = sum(3100200300);

  /* ... */
  return 0;
}


일단 가변 인자를 처리하기 위해서는 va_list type의 변수 하나를 선언해야 합니다:

va_list argptr;


그리고, 가변 인자를 처리할 부분 앞에 다음과 같이 써 줍니다:

va_start(argptr, nargs);


va_start()의 첫번째 인자는 아까 선언한 va_list type의 변수 이름이며, 두번째 인자는 이 함수에서 가변 인자를 받는 부분의 바로 앞의 (고정된) argument입니다. 즉, 위에서 예로 든 sum()의 경우, 가변 인자를 나타내는 "..." 앞에 오는 argument인 nargs가 두번째 인자가 됩니다.

눈치가 빠른 분은 이미 알고 있겠지만, 가변 인자를 받는 함수는 여러 가지 제한이 붙습니다. 첫째, va_start()에 가변 인자 바로 앞의 고정된 인자를 주어야 하기 때문에, 가변 인자를 받는 함수는 고정된 인자가 하나 이상 나와야 합니다. 즉, void foo(...)와 같은 함수는 만들 수 없습니다. 둘째, 가변 인자를 나타내는 "..."는 반드시 마지막 인자이어야 합니다. "..."가 중간이나 처음에 나올 수는 없습니다. 예를 들어 void bar(..., int a)와 같은 함수는 만들 수 없습니다.

실제 함수에 전달된 가변 인자를 얻기 위해서는 va_arg()를 불러서 처리합니다. va_arg()는 가변 인자로 전달된 값을 호출할 때마다 하나씩 얻어서 줍니다. va_arg()는 첫번째 인자로, va_start()로 초기화한 va_list type을 받으며, 두번째 인자로는 가변 인자로 전달된 값의 type을 써 주어야 합니다. 우리가 만든 sum()의 경우, 함수의 목적이 모든 int를 더한 값을 계산하는 것이므로, va_arg()의 두번째 인자로 int를 주면 됩니다.

va_arg(argptr, int);


눈치가 더욱 빠른 분이면 아시겠지만, 여기서 가변 인자를 받는 함수에 또다른 제한이 붙습니다. 아시겠지만, 가변 인자로 전달되는 값의 타입에는 제한이 없습니다 (printf(), scanf()를 생각해보세요). 아쉽게도 va_arg()는 전달되는 값의 타입을 알아낼 방법이 없습니다. 따라서 미리 그 type을 알아서 va_arg()의 두번째 인자로 전달해 주어야 합니다.

게다가, 가변 인자를 받는 함수의 입장에서, 몇개의 가변 인자가 전달되었는지 알아낼 방법은 없습니다. 따라서 고정된 인자로 가변 인자의 갯수를 전달하는 등 (위의 sum()의 예처럼)의 방법으로 va_arg()를 몇번을 불러야 되는가를 알아낼 방법을 제공해야 합니다.

더 골치 아픈 것은 va_arg()의 두번째 인자로 쓸 수 있는 type에 제한이 있다는 것입니다. 이건 뒤에서 다루겠습니다.

이제 가변 인자를 처리하는 함수를 만들어 보았으니, printf()의 wrapper를 만들어 봅시다.

노파심에서 말씀드리지만, 다음과 같은 함수는 동작하지 않습니다:

int
my_printf(const char *fmt, ...)
{
  return printf(fmt, ...);
}


일단 먼저, printf의 계열에는 다음과 같은 함수들이 있다는 것을 알아둡시다:

int printf(const char *fmt, ...);
int fprintf(FILE fp, const char *fmt, ...);
int sprintf(char *sr, const char *fmt, ...);
int snprintf(char *s, size_t n, const char *fmt, ...);
/* 이상 <stdio.h>에 선언되어 있는 함수들 */

int vprintf(const char *fmt, va_list ap);
int vfprintf(FILE fp, const char *fmt, va_list ap);
int vsprintf(char *sr, const char *fmt, va_list ap);
int vsnprintf(char *s, size_t n, const char *fmt, va_list ap);
/* 이상 <stdarg.h>에 선언되어 있는 함수들 */


잘 보면 알겠지만, <stdarg.h>에 있는 함수들은 <stdio.h>에 있는 printf() 계열 함수 이름 앞에 'v'가 붙고, "..." 대신에 va_list type의 인자가 온다는 것을 알 수 있습니다.

printf() style의 문자열을 받으려면, 이 v*printf() 계열의 함수를 쓰면 편리합니다. 예를 들어 단순히 printf()와 똑같은 기능을 하는 wrapper는 다음과 같이 만들 수 있습니다:

int
my_printf(const char *fmt, ...)
{
  va_list argptr;
  int ret;

  va_start(argptr, fmt);
  ret = vprintf(fmt, argptr);
  va_end(argptr);

  return ret;
}


즉, 가변 인자 처리를 위한 va_list type 변수를 선언, va_start()로 초기화한 다음에 v*printf() 계열의 함수에 이 va_list type의 변수를 넘겨주기만 하면 됩니다. (va_end()를 불러 끝내는 것을 잊지 마세요!!)

좀더 복잡하게 하나 더 만들어 봅시다. 함수는 error()입니다. 이 함수는 주어진 printf() style의 에러 메시지를 받아서 stderr로 출력하고, 필요하면 errno의 내용도 알려주고, 필요하면 exit()를 불러서 프로그램을 종료하는 함수입니다. 선언은 다음과 같습니다:

void error(int status, int ecode,
           const char *fmt, ...);


이 함수는 ecode가 0이 아닌 경우, strerror(ecode) 값을 출력하고, fmt와 "..."으로 전달된 printf() style의 에러 메시지도 출력한 다음, status가 0이 아닌 경우, exit(status)를 호출합니다. 함수 정의는 다음과 같습니다.

void
error(int status, int ecode,
      const char *fmt, ...)
{
  va_list argptr;

  fflush(stdout);
  fprintf(stderr"error: ");
  if (ecode)
    fprintf(stderr"%s: ", strerror(ecode));

  va_start(argptr, fmt);
  vfprintf(stderr, fmt, argptr);
  va_end(argptr);

  fputc('\n'stderr);

  fflush(stderr);  /* redundant */

  if (status)
    exit(status);
}


다음과 같이 쓸 수 있습니다.

#include <stdio.h>
#include <errno.h>

...

void
foo(const char *filename)
{
  FILE *fp;
  fp = fopen(filename, "r");
  if (!fp)
    error(1, errno, "cannot open file %s.", filename);
  ...
}


참고로 여기서 예로 만든 error()는 GNU C library (glibc)에 이미 포함되어 있습니다. 여기에서는 좀더 간략화해서 만들어 본 것이죠. 실제 여러분의 코드에 <error.h>를 포함시키면 위 error()를 정의할 필요없이 바로 쓸 수 있습니다. (출력 형태는 약간 다를 수 있습니다만, 인자 type이나 갯수는 같으니 똑같이 쓸 수 있습니다)

마지막으로 아까 va_arg()의 두번째 인자로 나올 수 있는 type에 제한이 있다고 했는데 그 얘기를 해보겠습니다. 결론부터 말하면, va_arg()의 두번째 인자로 나올 수 있는 type은 다음과 같습니다:

  • int 계열 (int, unsigned int, signed int)
  • long 계열 (long, unsigned long, signed long)
  • double 계열 (double, long double)
  • 포인터 계열 (모든 타입의 포인터, 예를 들어 void *, char *, 등등) 

다음과 같은 타입은 올 수 없습니다:

  • char 계열 (char, unsigned char, signed char)
  • short 계열 (short, unsigned short, signed short)
  • float 계열 (float) 

그럼 가변 인자로 char나 float type은 올 수 없느냐, 그렇지는 않습니다. 이런 타입을 받기 위해서는, va_arg()에 다음과 같이 전달합니다.

  • char -> int (unsigned char는 unsigned int, 이런 식으로)
  • short -> int (unsigned short는 unsigned short, 이런 식으로)
  • float -> double 

그 이유는 다음과 같습니다. C 언어가 처음 만들어질 때에는 function prototype이란게 없었습니다. 인자가 몇개이고, 어떤 타입이냐에 상관없이 단순히 함수가 있고, 리턴 타입이 뭐다. 정도만 알려 줬죠. 예를 들면 다음과 같습니다:

double bar();

double bar(a, f, ch)
  int a;
  float f;
  char ch;
{
  ...
}


이런 식으로 함수를 선언하게 되면, 컴파일러가 함수의 정의를 보기 전에는 이 함수에 전달되는 인자의 타입이 뭔지 알 수 없습니다. 만약 컴파일러가 bar()의 정의를 보기 전에 다음과 같은 코드를 컴파일해야 한다고 가정해 보기 바랍니다:

void
foo(void)
{
  bar(42.59);
}


일단 bar()의 인자가 3개인데,, "void bar();"만 보고서는 인자가 적절하게 전달되었는지 알 방법이 없습니다. 따라서 인자 갯수 체크를 할 수 없습니다.

둘째로, 각각의 인자가 알맞는 타입인지도 알 수 없습니다. 함수 호출을 위해서는 (stack에 인자를 push해야 하는데), 인자의 type을 알 수 없으니, 첫번째 인자인 4를 push할 때, 8 bit로 4를 push할 것인지, 16 bit로 4를 push할 것인지, 32 bit로 할 것인지 알 수가 없습니다.

따라서 C 언어에는 "integral promotion"이란 용어? 개념?이 있고, 여기에 따라서 모든 인자는 다음과 같은 type으로 변환해서 stack에 push합니다.

  • char, short -> int
  • float -> double
  • int -> int (그대로)
  • long -> long (그대로)
  • double -> double (그대로)
  • long double -> long double (그대로)
  • 모든 pointer type -> 모든 pointer type (그대로) 

즉, 인자로 전달된 값이 character 값이라 할 지라도 함수를 호출할 때에는 int type으로 변환해서 전달합니다.

지금까지는 ANSI C에서 function prototype이 나오기 전의 old style (K&R) C에 필요한 내용입니다. 자, ANSI C에서는 function prototype이 있으니, 컴파일러가 함수 호출을 하기 전에 함수의 인자 갯수와 타입을 모두 알 수 있습니다.

double bar(int a, float f, char ch);


따라서 더 이상 이러한 변환이 필요없게 되었지만, 하나 예외가 있는데, 바로 가변 인자를 처리하는 함수입니다. 가변 인자는 실제로 어떠한 타입이 몇개가 들어올 지 알 수 없기 때문에, 이러한 변환이 계속 수행됩니다. 따라서 va_arg()로 이러한 타입을 받을려면 이 integral conversion을 미리 생각해 두어야 합니다.

예를 들어 문자 하나씩 받아서 출력하는 함수를 만들어 보겠습니다:

void
putchars(int nargs, ...)
{
  va_list argptr;
  char ch;
  int i;

  va_starg(argptr, nargs);
  for (i = 0; i < nargs; i++) {
    ch = (char) va_arg(argptr, int);
    putchar(ch);
  }
  va_end(argptr);
}


즉, 들어올 가변 인자가 char type일지라도 va_arg()에서는 integral promotion에 따라 int type으로 받아서 처리해야 합니다.

잘 생각해 보면, 이미 여기에 대해 알고 있었을 것입니다. printf()와 scanf()를 생각해봅시다.

printf에서 float이나 double을 출력하기 위해서는 "%f"를 씁니다. 그런데, scanf에서는 float은 "%f", double은 "%lf"를 씁니다. 아래 예 참고:

double d;
float f;

printf("%f", d);
printf("%f", f);

scanf("%f", &f);
scanf("%lf", &d);


그 이유는, integral promotino에 의해 printf의 경우, float을 double로 받기 때문입니다. 따라서 float과 double에 관한 차이가 없습니다. 그러나 scanf의 경우에는 모든 인자가 pointer type이기 때문에 이러한 변환이 필요없습니다. 즉, 모든 타입을 각각 구별해야 하는 것이죠.

분명 printf 코드안에서 float을 출력하는 부분은 다음과 같은 코드를 포함합니다:

  value = (float) va_arg(argptr, double);


또한 scanf 코드 안에서 float을 출력하는 부분은 다음과 같은 코드를 포함합니다:

  ptr = va_arg(argptr, float *);


이 정도면 가변 인자를 처리하는 방법은 완벽히! 익히셨을 것입니다. 그럼 다음 기회에...

끝 -- cinsk



  • TODO: Function prototype이 ANSI C에서 소개된 것 맞나?
  • TODO: Integral Promotion이라고 했는데, 여기에 float, double이 포함되는 것이 확실한가?
이거 알아보고 글 고칠 것.. -- cinsk.

함수 선언(정의가 아닌)의 경우에는 그 전에도 있었던 것 같습니다. [http]http://www.ifi.uio.no/forskning/grupper/dsb/Programvare/Xite/ProgrammersManual/node11.html#SECTION000111000000000000000 그러나 매개변수의 타입들을 함께 적어주는 function prototype은 ANSI C에서 새로이 나온 것인 듯 합니다.

가변인자 함수에서 적용되는 것은 Integral Promotion을 포함하는 Default argumnt promotion입니다.

C99 6.5.2.2 Function calls 중에서...

If the expression that denotes the called function has a type that does not include a prototype, the integer promotions are performed on each argument, and arguments that have type float are promoted to double. These are called the default argument promotions.

그리고 표준문서에서는 Integer promotion이라는 용어를 씁니다.

Integer promotion은 이름 그대로 정수형의 승급만을 의미합니다. double이나 포인터는 포함되지 않습니다. int형 또는 unsigned int형보다 rank가 낮은 (즉 표현범위가 좁은) 정수형에 한해서 int 형 또는 unsigned 형으로 승급됩니다. 해당되지 않는 데이터 형들은 그대로 남습니다.

6.3.1.1 Boolean, characters, and integers 중에서...

The following may be used in an expression wherever an int or unsigned int may be used:

  • An object or expression with an integer type whose integer conversion rank is less than the rank of int and unsigned int.
  • A bit-field of type _Bool, int, signed int, or unsigned int. 

If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.



가변 인자 매크로 


ISO C 표준을 지원하는 컴파일러에서는 가변 인자를 처리하는 매크로도 정의할 수 있습니다. 함수 형태의 매크로를 정의할 때, "..."을 써 주고, 매크로 정의 부분에서 VA_ARGS 써주면 그 자리에 확장됩니다. 예를 들면, 다음과 같습니다:

#define debug(s, ...)    fprintf(stderr, s, __VA_ARGS__)

void
foo(void)
{
  debug("Entered the function, %s\n"__func__);
  /* ... */
}


간단하게 printf 형태의 메시지를 입력받은 표준 함수 assert()와 비슷한 매크로 함수를 만들어 봅시다; 함수 이름은 ASSERT()로 하겠습니다. ASSERT()의 첫 인자는, assert()의 인자와 같습니다. 즉 거짓인 경우, abort()를 부릅니다. ASSERT()의 두번째 인자부터는 printf()와 같습니다. 또한 NDEBUG 매크로가 정의된 경우, ASSERT()는 아무런 영향을 주지 않습니다.

일단, ASSERT()가 쓰인 시점의 파일 이름, 줄 번호, 그리고 함수 이름을 자동으로 출력하게 합시다. 따라서 미리 정의된 매크로인 FILE과 LINE을 쓰며, 미리 정의된 이름, func를 씁니다.

아래에 ASSERT()의 정의가 나갑니다:

#ifndef NDEBUG
# define ASSERT(exp, s, ...)  assert_(__FILE____LINE____func__, \
                                      #exp, s, __VA_ARGS__)
#else
# define ASSERT(exp, s, ...)  ((void)0)
#endif /* NDEBUG */


간단합니다. NDEBUG가 정의된 경우에는 ASSERT()가 assert_()를 호출합니다. assert_()의 처음 세 인자는 ASSERT()를 부른 곳의 파일 이름, 줄 번호, 함수 이름을 자동으로 전달하게 됩니다. 다음으로 ASSERT()의 첫 인자를 문자열로 바꾸어 전달하며(#exp), 그 다음으로, printf() 스타일의 format string인 s가 들어오며, 그 뒤에 가변 인자가 들어옵니다.

이제 assert_()의 정의가 나갑니다:

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

void
assert_(const char *filename, int lineno, const char *func,
        const char *exp, const char *fmt, ...)
{
  va_list argptr;

  fflush(stdout);
  fprintf(stderr"%s:%d: assertion, (%s) failed in function, %s\n",
          filename, lineno, exp, func);
  fprintf(stderr"\treason: ");
  va_start(argptr, fmt);
  vfprintf(stderr, fmt, argptr);
  va_end(argptr);
  putc('\n'stderr);
  abort();
}


예를 들어 다음 함수를 생각해 봅시다:
void
set_age(int age)
{

ASSERT(age > MIN_AGE, "age should be larger than %d.", MIN_AGE);
/* ... */

}
MIN_AGE는 10으로 정의된 매크로이고, 사용자가 set_age(3)을 호출했다면, 다음과 같은 ASSERTion이 발생합니다:

tmp.c:36: assertion, (age > minimum) failed in function, set_age
        reason: age should be larger than 10.
Aborted


자, 그럭 저럭 ASSERT() 쓸만하죠? assert()보다는 좀 나을 것 같습니다.

참고: ISO C 표준 6.4.2.2, 6.10.3



출처 :

https://wiki.kldp.org/whttps://wiki.kldp.org/wiki.php/CLanguageVariableArgumentsListiki.php/CLanguageVariableArgumentsList

728x90
728x90

DXUT.cpp에서 처음 발견한 이 매크로는 아직 유용하게 사용하고 있다.



클래스들의 멤버 변수들에 대해 항상 만들기 귀찮던 Get, Set 함수를 만들어주는 매크로이다.


이것은 DXUT의 뮤텍스와 구조체를 사용하므로, 다음과 같이 나에게 맞게 수정하여 사용하고 있다.


#define SET_ACCESSOR(x, y)    inline void Set##y(x t)    { m_##y = t; };

#define GET_ACCESSOR(x, y)    inline void Get##y(x t)    { return m_##y; };

#define GET_SET_ACCESSOR(x, y)    SET_ACCESSOR(x, y)    GET_ACCESSOR(x, y)


728x90
728x90




728x90
728x90

예전에 다른 팀과 연동하는 과정에서 매개 변수로 넘긴 값이 함수 내부로 넘어가면서 값이 혼자 변하는 버그를 경험했다.


그림 - test.cpp


그림 - main.cpp


위와 같은 상황에서 abc()를 통해 값을 넘겼을 때, 매개변수 a[3]은 모두 제대로 값이 넘어갔지만, 매개변수 b의 경우 계속해서 0의 값이 들어가는 것을 확인했었다.


이런저런 테스트 결과 "test.h"에서 abc()의 선언을 추가함으로 써 문제가 해결되었다.


이런 일이 발생한 정확한 이유는 모르겠지만, C 형태로 코딩을 할 때에는 함수의 선언이 빠졌을 때 이런 일이 발생할 수 있으므로 주의하도록 하자.

728x90
728x90

object라는 클래스가 있다는 가정하에 헤더파일에 선언할 수 있는 4가지 타입의 전역변수는 아래와 같습니다.

1. object var;
2. static object var;
3. exturn object var;
4. typedef Singleton <object> var;

1번과 같은 경우 2개 이상의 cpp파일에서 해당 헤더파일을 포함하는 경우, 변수가 미리 정의되어 있다는 에러를 발생시킵니다. 즉, 1개의 cpp파일을 위해서만 정의할 수 있는, 단순히 cpp파일에 선언한 것을 헤더파일로 옮긴거 외에는 의미를 가지지 않습니다.

뭐랄까? 1개의 cpp파일을 위한 전역변수가 되겠죠.

2번과 같은 경우 2개 이상의 cpp파일에서 해당 변수를 사용할 수 있습니다. 왜냐하면 헤더파일을 포함한 cpp파일의 개수만큼 객체가 생성되기 때문입니다. 전역변수기는 한데 각각의 cpp파일마다 각각의 전역변수가 생성됩니다. 뭐, 좀 더 정확하게 표현하자면 헤더파일을 포함하는 각각의 cpp파일에서만 사용하는 로컬변수라고 부를 수 있겠죠. 용어에서 헷갈리는 이유는 변수의 생존 범위인데, 쥔장의 개념에서 지역변수의 개념은 함수안에서만 존재하는 변수를 지역변수라고 생각했기 때문입니다. 그리고는 단순하게 로컬변수가 아닌 변수는 전역변수라고 생각했기 때문에... (흠... 지금도 헷갈림...켁~)

3번과 같은 경우는 cout, cin과 같은 우리가 흔희 보는 STL의 전역변수들이 사용하는 기법입니다. 진정으로 여러 cpp파일에서 공유하는 한개의 전역변수가 생성됩니다. 즉, C/C++에서 전역변수라고 불리는 것은 3번과 같은 경우가 될 것입니다.

4번과 같은 경우는 Design Pattern의 하나로서 1개의 객체만을 생성하게 됩니다. 전역변수의 개념이나 3번과 같이 compiler에 의해서가 아닌 테크닉에 의하여 생성되게 됩니다. 실제적으로는 클래스의 typedef이며 var::Instance() 와 같이 클래스의 static함수를 사용하여 1개의 전역변수를 사용하게 됩니다.

패턴주의자라면 4번을 선호할 것이며 C에서 부터 C++로 넘어오신 분이면 3번을 선호할 것 입니다.

초급을 넘어서면 심심할 때 고민할 이 둘의 차이에 대해서 말하자면 구글형이 가르켜준 Singleton VS global object 라는 토론에 잘 나와 있습니다. 인상깊었던 내용의 일부를 이야기하자면 표면적으로 비슷하지만 Singleton 쪽이 좀 깔끔하게 정의할 수 있고 (extern은 특정 cpp파일에서 실제 객체의 정의해야 하며, 전역객체가 많아지면 특정 객체의 정의 위치를 찾는 것도 한 일 되겠죠?) 그 외에 config파일 사용, 늦은 초기화 등의 테크닉을 사용할 수 있다는 장점이 있습니다. 개인적으로 그 외의 차이점은 전역변수 같은 경우는 전역변수를 다루는 메모리에 들어가는데 비하여 Singleton은 생성된 객체가 힙메모리에 위치하는 차이점이 있는듯 보입니다.


- 출처 : http://linkmemo.tistory.com/entry/C-%ED%97%A4%EB%8D%94%ED%8C%8C%EC%9D%BC%EC%97%90-%EC%84%A0%EC%96%B8%ED%95%A0-%EC%88%98-%EC%9E%88%EB%8A%94-4%EA%B0%80%EC%A7%80-%ED%83%80%EC%9E%85%EC%9D%98-%EC%A0%84%EC%97%AD%EB%B3%80%EC%88%98

728x90

+ Recent posts