회고록 블로그

[C언어 공부] 가변인자 본문

6. 42서울/C

[C언어 공부] 가변인자

김간장 2022. 4. 30. 18:10

※ 필자는 초초초보자입니다.

※ 틀린 내용에 대한 피드백은 언제든지 환영합니다.

 

공부자료 및 출처 : https://dojang.io/mod/page/view.php?id=577 

 

C 언어 코딩 도장: 66.1 가변 인자 함수 만들기

66 함수에서 가변 인자 사용하기 C 언어에서 함수를 사용하다 보면 printf, scanf같이 매개변수의 개수가 정해지지 않은 함수가 있습니다. 이렇게 매번 함수에 들어가는 인수(argument)의 개수가 변하

dojang.io

 

 

✅ 가변인자(varargs)

매개변수의 개수가 정해지지 않은 함수가 있을 때,

매번 함수에 들어가는 인수의 개수가 변하는 것을 가변 인자라고 한다.

#include <stdio.h>

void countIntegers (int args, ...)
{
	printf("%d", args);
}

int main()
{
	countIntegers(1, 100);
	countIntegers(2, 100, 200);
	countIntegers(3, 100, 200, 300);
	return (0);
}

- 여기에서 가변 인자 함수는 counterIntegers이다.

- 가변 인자 함수는 반드시 매개변수가 한 개 이상 있어야 한다. (반드시 한 개는 있어야 하는데, 이를 고정인수라고 한다)

- 그 이후에는 ...을 통해 매개변수의 개수가 정해지지 않았다는 표시를 해준다.

 

 

✅ 매개변수는 어떻게 사용할 수 있나

매개변수는 반드시 한 개 이상 있어야 하고, 나머지는 매개변수의 개수가 정해지지 않았다는 의미에서 ...을 표시해준다고 했었다.

각각의 매개변수는 다음과 같이 매크로를 통해 사용할 수 있다.

#include <stdio.h>
#include <stdarg.h> // va_list, va_start, va_arg, va_end

void addIntegers (int args, ...)
{
	va_list ptr; // 가변 인자 목록 포인터
	int result = 0;

	va_start(ptr, args);
	for (int i = 0; i < args; i++) {
		result += va_arg(ptr, int);
	}
	va_end(ptr);
	printf("%d", result);
}

int main()
{
	addIntegers(1, 100);
	printf("\n");
	addIntegers(2, 100, 200);
	printf("\n");
	addIntegers(3, 100, 200, 300);
	return (0);
}

 

- 일단 va_list, va_start, va_arg, va_end가 정의된 헤더, stdarg.h를 인클루드 해야한다.

- va_list, va_start, va_arg, va_end를 사용해서 각 인자에 접근하고 사용한다.

 

❖ va_list

- 가변 인자 목록을 의미하며, 가변 인자의 메모리 주소를 저장하는 포인터이다.

- 포인터라는 사실이 중요한 듯 싶다.

❖ va_start

- 가변 인자를 가져올 수 있도록 포인터를 설정하는 것이라고 한다.

- va_list 타입으로 선언된 포인터를 첫번째 인자로 넣고, 가변 인자의 개수인 args(고정인수)를 두번째 인자로 넣는다.

❖ va_arg

- 가변 인자 포인터에서 특정 자료형 크기만큼 값을 가져올 때 사용하는 매크로이다.

❖ va_end

- 가변 인자 처리가 끝났을 때 포인터를 NULL로 초기화하는 매크로이다.

 

정리하자면, va_start를 통해 ptr의 위치를 잡고, va_arg를 통해 두번째 인자(int) 만큼 순방향으로 이동하며 값을 가져온다.

 

 

※ 참고.궁금해서 <stdarg.h> 헤더를 찾아봤는데 다음과 같이 매크로가 정의되어 있었다.

 

- 이 코드를 보고 _buitin_va_list가 어디에 정의되어 있는 것인가 찾아보려고 했는데

   _builtin_va_list는 gcc 컴파일러 내부에서 처리된다고 한다.

   (출처 : https://stackoverflow.com/questions/49733154/how-is-builtin-va-list-implemented)

   즉, 이 _builtin_va_list가 어떻게 되어있는지 보고싶다면 gcc 컴파일러(일종의 프로그램)를 리버싱해서 봐야한다는 의미 같다.

 

‣ 다행히도 우리는 인터페이스를 통해 해당 코드의 내부 로직을 모두 이해하지 않아도 변수, 함수 등을 사용할 수 있는 편리한 세상에 살고 있다.

_builtin_va_list 등 코드에 대해서 근본까지 뜯어보고 싶다면, 직접 gcc 컴파일러(프로그램)을 다 뜯어볼 수 밖에 없다고 한다.

굳이 뜯어본다면 말리지는 않겠지만 명심해야 할 점은, gcc 컴파일러(프로그램)는 아주 오랜 시간동안 몇 십, 몇 백 명, 몇 천 명의 전문가들이 붙어서 만든 프로그램이며, 그런 프로그램을 분석하려면 아주 오랜 노력과 시간이 들어간다고 한다.

 

✅  va_start의 매개변수에 대해서

va_start의 두번째 매개변수는 정수 타입(가변인자의 개수)만 들어갈 수 있는 줄 알았는데, 꼭 그렇지도 않은 것 같다.

문자형 포인터가 들어가는 경우도 있었다.

 

그래서 정확하게 공부해봤다.

 

#1.

일단, 가변인자는 최소 1개 이상의 고정 인수가 있어야 한다.

때문에 아래와 함수를 선언한다.

void addIntegers (int args, ...)

 

#2.

가변인수들을 저장하는 스택 주소 포인터가 va_list로 정의한 ptr이고

va_list ptr; // 가변 인자 목록 포인터

 

#3. 

ptr이 첫번째 가변 인수를 가리킬 수 있도록 초기화 하는 것이 va_start

va_start(ptr, args);

 

사실 va_start의 첫번째 매개변수는 va_list 타입이 들어가고,

두번째는 "마지막 인수의 이름"이 들어가는 것이다.

즉, 고정인수가 2개 이상인 경우에는 "마지막 인수의 이름"이 들어간다.

 

따라서 아래와 같은 코드도 가능했다.

#include <stdio.h>
#include <stdarg.h> // va_list, va_start, va_arg, va_end

void addIntegers (int args, char *str, ...)
{
	va_list ptr;

	va_start(ptr, str); // 두번째 매개변수의 이름(str)을 넣어주었고, 가변인자의 시작 주소로 ptr을 설정해주었음
	printf("%d ", va_arg(ptr, int)); // 첫번째 가변인자(고정인수 아님)는 숫자이므로 int형으로
	for (int i = 0; i < 5; i++) {
		printf("%s ", va_arg(ptr, char *)); // 두번째부터 다섯번째까지의 가변인자는 문자열이므로 char *형으로
	}
	va_end(ptr);
}

int main()
{
	addIntegers(1, "Hello", 100, "H", "E", "L", "L", "O"); // 첫번째 인자를 '가변인자의 갯수'로 설정하지 않음
	printf("\n");
	addIntegers(2, "World", 200, "W", "O", "R", "L", "D"); // 첫번째 인자를 '가변인자의 갯수'로 설정하지 않음
	printf("\n");
	return (0);
}

 

실행결과

이 코드로 알 수 있는 것은

1) 꼭 고정인수가 "가변인자의 갯수가 아니여도 된다"는 점이고

2) va_start의 두번째 인자는 "마지막 인수의 이름이 들어가야 한다"는 점이다.

 

따라서 두번째 인수의 자료형이 int이든지 char 포인터이든지 상관없다는 이야기이다.

 

va_start는 함수가 아니라 매크로 이기 때문이며 (함수에서 적용되는) 일반적인 규칙이 적용되지 않기 때문이라고 한다.

컴파일러가 va_start(인자1, 인자2) 코드를 만나면 인자1과 인자2를 통해 가변인자의 시작점(위치)을 찾게 된다.

 

 

출처 : https://stackoverflow.com/questions/28768581/using-a-pointer-to-const-char-as-a-second-argument-for-va-start

 

Using a pointer to const char as a second argument for va_start

I am working on Advanced Programming in the UNIX Environment 3-Edition , and I find this code err_msg(const char *fmt, ...) { va_list ap; va_start(ap, fmt); err_doit(0, 0, fmt, a...

stackoverflow.com

 

✅  va_arg에 대해서

va_start와 va_arg로 여러가지 코드를 작성하다가

va_arg() 매크로의 두번째 인자로 쓸 수 있는 자료형이 제한되어 있다는 사실을 알게 되었다.

 

[참고 사진] 이런 경고들이..?!

찾아보니 va_arg()의 두번째 인자로 올 수 있는 "type"은 제한이 되어있다고 한다.

바로 아래와 같이 말이다.

 

가능한 type

  • int (int, unsigned int, signed int)
  • long (long, unsigned long, signed long)
  • double (double, long double)
  • 포인터 (모든 타입의 포인터; void *, char *, int * 등)

불가능한 type

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

 

불가능하다고 했지만, 사실 불가능한건 아니다.

정확하게 말하면 "불가능한 type을 받기 위해서 형을 변환한다"고 한다.

char은 int로, unsigned char은 unsigned int형으로! float는 double형으로!

 

위의 [참고 사진]의 경고(warning)는 바로 이런 것을 이야기하고 있는 것이다.

char 타입은 받을 수 없다. int 타입으로 받아야 한다고 말이다.

 

실제로 모든 경고를 다 무시하고 컴파일을 한다고 했을 때,

va_arg(ap, char)로 반환 받은 값을 sizeof로 확인해보면 4바이트가 출력된다.

 

참고자료 : https://wiki.kldp.org/wiki.php/CLanguageVariableArgumentsList

 

KLDPWiki: CLanguage Variable Arguments List

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

wiki.kldp.org

 

✅ 왜 va_arg의 두번째 인자는 자료형이 제한되어있을까

왜 두번째 인자는 자료형이 제한되어있을까 너무 궁금해서 나름대로 추측을 해봤다. (100% 개인적인 생각이다)

 

스택오버플로우 글을 찾아보니 

컴파일러는 가변인자로 오는 값들의 유형(자료형)을 알지 못하고

컴파일러가 작업을 더 쉽게 하기 위해서 int 보다 좁은 유형은 모두 int나 unsigned int로 승격된다고 한다.

 

https://stackoverflow.com/questions/28054194/char-type-in-va-arg

 

char type in va_arg

I have the following function which writes passed arguments to a binary file. void writeFile(FILE *fp, const int numOfChars, ...) { va_list ap; va_start(ap, numOfChars); for(int i = 0; i ...

stackoverflow.com

 

 

예전에 자바(Java)에 대해서 공부할 때, 윤성우 선생님이 강의에서 이런 비슷한 말씀을 하셨다.

'컴파일러가 코드를 다 체크하면서, 위험한 코드에 대해서 컴파일 에러를 다 띄워주면 너무 좋겠지만

그렇게 되면 굉장히 많은 조건들을 다 체크해줘야 하고, 그게 모이다보면 컴파일 한번 할때 마다 굉장히 오랜시간이 걸린다'

 

예를 들어서 만약 아래와 같은 코드가 있다고 하자.

printf("%d", 10);

 

숫자 10(=리터럴)도 분명 메모리의 어느 공간에 기록되어 있어야 한다.

 

컴파일러는 이 숫자 10이라는 값을 메모리에 기록할 때 기본적으로 int형으로 저장을 한다.

실제로 sizeof를 이용해서 확인해보면 메모리 공간을 4바이트 차지하고 있는 것을 확인할 수 있다.

printf("%d", sizeof(10));

값 자체는 분명 char형의 범위이지만, (숫자 10은 char형의 범위에 들어간다)

 

컴파일러가 지금 들어온 값이 char형인지, int형인지, long형인지, long long형인지

값이 들어올 때마다 조건문으로 체크하고 그에 맞춰서 딱 그 바이트(byte)만큼만 메모리에 기록하게 되면

리터럴이 들어올 때마다 조건문을 돌아야하고

이런 작업이 쌓이고 쌓이면 컴파일 한번 하는데만 몇 시간이 걸릴 수도 있기 때문에(??)

값(숫자 10)이 char형의 범위안에 있다고 하더라도 그냥 int형으로 저장해버린다고 들었다.

 

그래서 추측하기로는, 가변인자로 들어오는 값의 자료타입 또한 어느 것(int, double 등)이 될지 모르고

그걸 컴파일러가 컴파일 할 때마다 하나하나 체크하면서 저장해놓으면 너무 비효율적이니

(e.g. 현재 들어온 값이 char형 범위인가? → 그렇다면 1바이트로 메모리에 저장

         int형 범위인가? → 그렇다면 4바이트로 메모리에 저장

         ...)

그냥 기본적으로 리터럴이 들어올 때 저장하는 타입(int형, double형)으로 처리를 해버린게 아닐까 싶다.

 

즉, 컴파일러를 설계한 설계자가 

va_arg의 두번째 인자로 올 수 있는 타입(char, float 등)을 제한해 놓은 것은 

컴파일러의 효율 (컴파일러는 컴파일을 빠르게 해내야한다! 너무 조건을 많이 걸어둬서 컴파일 하는데만 하루가 걸리면 그 어떤 프로그래머도 이 컴파일을 안 쓸 것 같다! 등을 고려)을 위해서이며, 구현하지 "못"하는게 아니라 "안" 한 것이 아닐까. 

 

답은 컴파일러 설계자만 알겠지만..

'6. 42서울 > C' 카테고리의 다른 글

[C언어 공부] read와 write 함수  (0) 2022.03.28
[C언어 공부] restrict 키워드  (0) 2022.03.09
[C언어 공부] static 함수와 static 변수  (0) 2022.03.09
Comments