본문 바로가기

별걸다하는 IT/프로그래밍언어

[C,C++] 가변인자 함수의 사용(va_start, va_arg, va_list등등) 함수에 불특정 여러개의 인자를 넘기고 싶을 때, C언어 유사 오버로딩

반응형

[C, C++ 프로그래밍 강좌 목차]

안녕하세요~ 양햄찌 주인장입니다.

오늘은 오랜만에 프로그래밍 언어에 관련된 포스팅을 들고왔어요.

 

오늘의 주제 포스팅을 들어가기 전 'C++의 오버로딩'에 대한 개념을 알고 계시다면 좀 더 이해하기 수월합니다!

오버로딩에 관한 포스팅은 아래 링크를 참고해주세요! 

https://jhnyang.tistory.com/75

 

[C, C++, java 공통] 오버로딩이란? Overloading 개념

[C언어, C++언어 완전 정복! 강의 목차 링크] 이번에는 overloading 오버로딩 vs overriding 오버라이딩 맨날 비교하는 질문 유명하죠? 말이 비슷해서 비교대조 문제로 종종 출제되는(?) 오버로딩에 대해��

jhnyang.tistory.com

가변인자 언제 써? 가변인자 이해하기 

계산기를 만든다고 생각해봅시다. 계산기하면 덧셈이죠?

int sum(int a, int b) { return a+b; }

그래서 간단하게 두 수를 더하는 뭐 이런 코드를 하나 만들었어요.

근데 우리 실제 계산기 만들 때 두개만 더 하나요? 1+2+3+4+5 한꺼번에 5개의 인자를 받아서 덧셈을 수행하게 하고 싶을 수도 있잖아요! 물론 1과 2를 먼저 더한 후 그 결과에 3 더하고 그 결과에 4를 더하고... 두 인자만 받는 코드를 반복문으로 돌려 해결할 수도 있겠지만, 현재 짚고 넘어가고자 하는 부분은 이부분이 아니죵. 우리가 하고 싶은건 하나의 함수 프로토타입으로 여러 인자를 받고 싶은 겁니다. 

printf("%d\n", 1);                     //인자 2개
printf("%d, %d\n", 1, 2);              //인자 3개 
printf("%d, %d, %s\n", 1, 2, "HELLO"); //인자 4개

C언어의 printf 출력함수만 해도, 내가 원하는 개수의 인자만큼 출력할 수 있어요. 어떻게 이게 가능할까요?!

바로 이 가변인자 때문입니다.

'가변 - 변할 수 있다'라는 뜻이니까 가변인자는 인자의 개수가 변할 수 있다! 라는 의미가 되죠. 

 

즉 우리는 우리가 정의한 sum 함수 또한 

sum(1);
sum(1,2);
sum(1,2,3);
sum(5,6,7,8);

인자의 개수와 상관없이 요구되는대로 자유자재로 결과를 도출하고 싶은거예요!

가변인자와 오버로딩 

얼핏 보면 가변인자와 오버로딩이 비슷한개념인가? 생각하실 수 있어요.

 

사실 오버로딩은 '겹겹히 쌓는다'라는 뜻이잖아요.. 그래서 이름은 한개인데 여러 함수를 동명이인처럼 겹겹이 정의할 수 있음을 뜻해요. 함수명이 한개라도 인자의 개수나 타입에 따라 똑똑이가 딱 맞는애를 뽑아 수행하기 때문에 동일인이 있어도 누가 누군지 구분해 쓸 수 있는거죠! 원리상으로 컴파일 하면 결국 다른 이름의 메서드가 되기 때문에 똑같은 함수처럼 보여도 같은 함수가 아닙니다! (이전 오버로딩 포스팅 참고) 

 

가변인자는 비슷한듯 하지만 달라요. 가변인자는 포인터의 작용으로 이뤄지는 거거든요. 인자 개수가 몇개인지 모르니까 스택(stack)에 쌓았다가 하나씩 빼서 쓰는 격입니다. 결론적으로는 배열에 인자들을 저장해서 쓰는거죠.~ 둘이 동작 원리는 엄연히 다르기 때문에 가변인자를 이용해 오버로딩을 구현할 수 있을 것 같다 해도 오버로딩이랑 별개의 개념으로 생각해야 맞는 이유입니다.  

헤더파일, 라이브러리

기본적으로 가변인자 함수를 정의해서 쓰겠다 하시는 분들은 아래 헤더파일을 추가해주시면 돼요~!

#include <stdarg.h>

이 헤더 파일에는 아래와 같은 타입과 매크로들이 정의되어있는데, 가변인자 함수를 작성하는데 필요한 것들이예요.

이것들이 뭔지에 대해서는 아래 차근차근 살펴볼거랍니당.

가변인자 함수 정의하기, 매크로 사용하기 

가변인자는 몇개가될지 모르니까~ 이렇게 '...'으로 표시합니다. 

void func(int num_args, ...);
int sum(int num_args, ...);

가변인자를 갖는 함수 프로토타입을 정의해봤어요. 주의해야할 점은 최소 1개 이상의 고정 인수가 있어야 하며 '...'은 파라미터 순서 상 가장 마지막에 위치해 있어야 한다는 거~!

#include <stdio.h>
#include <stdarg.h> //가변인자 함수 쓰려면 필요한 헤더야.
int sum(int num_args, ...);  //여러개의 인자를 받을 때에는 '...'를 사용해.
int main()
{
	int a = 1, b = 2, c = 3, d = 4, e = 5; 
	printf("%d\n", sum(5, a, b, c, d, e));   
	return 0;
}
int sum(int num_args, ...)
{
	int result; 
        //여기에 sum 함수가 할 역할을 코딩해줘야해
	return result;
}

이해하기 쉽게 샘플로 전체 레이아웃을 짜봤어요. 자 문제는 저기 sum 함수 내부를 어떻게 정의할 것이냐...  '...'부분에 들어올 인자들을 지칭할 만한 그런 변수명이 없잖아요!

 

이때 필요한게 헤더파일에 있는 매크로 정의들입니다.

매크로를 사용해 구현한 코드를 먼저 봅시다. (직관적이라서 이해가 더 빨라져요) 배열에 저장된 값을 출력할 때 반복문을 썼던 걸 생각하면 훨 이해가 쉽습니다.

가변 인수 사용 예시 1 - 덧셈 함수

#include <stdio.h>
#include <stdarg.h>
int sum(int num_args, ...); //함수 프로토타입 선언 
int main()
{
	int a = 1, b = 2, c = 3, d = 4, e = 5;
	printf("%d\n", sum(5, a, b, c, d, e));
	return 0;
}
int sum(int num_args, ...)
{
	//추후 인자리스트에서 추출하고 싶은 인자의 주소를 가리키는 포인터 역할을 합니다. 
	//여기서는 가변인수들을 저장하는 스택 주소 포인터예요~
	va_list ap; 
	va_start(ap, num_args); // ap가 맨 첫 번째 가변인수를 가리킬 수 있도록 초기화! 
	int arg = 0, result = 0;
	for (int i = 0; i < num_args; i++)
	{
		arg = va_arg(ap, int); //반복문 돌리면서 인자값 한 개 한 개씩 뽑기!
		result += arg;
	}
	va_end(ap); //가변인자 사용 끝!
	return result;
}

va_list 타입과, va_start, va_args 와 같은 매크로들은 stdarg.h 에 정의되어 있습니다. 

간략하게 매크로 역할 살펴보기

va_list

we need a variable capable of storing a variable-length argument list--this variable will be of type va_list. For example, the following code declares a list that can be used to store a variable number of arguments.

길이가 변할 수 있는 인수들을 저장하려면 가변의 저장공간이 필요합니다. va_list라는 타입이 이 가변공간이 될건데, 예를 들어, 'va_list ap;' 이 코드는 여러 인수를 저장하는데 쓰일 타입을 선언하는 코드인거죠. 

 

Type to hold information about variable arguments. This type is used as a parameter for the macros defined in cstarg to retrieve the additional arguments of a function

va_list는 가변 안수들에 대한 정보를 홀드 하기 위한 타입입니다. 이 타입은 저장된 인수들을 쓰기 위해 cstdarg 헤더파일에 정의된 매크로에서 매개변수로 사용돼요.

 

va_start

그 다음에 나오는 va_start는 두 개의 인자를 받아요. va_list 인스턴스(ap) 그리고 고정인수를 받습니다.

va_start가 하는 역할이 va_list를 초기화하는거기 때문에 va_list 타입이 선언되어 이전에 인스턴스가 있어야 해요. 

두 번째 매개변수의 경우 va_list를 초기화하는데 왜 필요하느냐~

This fixed argument allows 'va_start' to determine the location of the first variable argument

첫 번째 가변인자 주소를 알려면 고정인수가 필요하기 때문입니다. 

#define va_start(ap, v) ( (ap) = (va_list)_ADDRESSOF(v) + _INTSIZEOF(v)

이렇게 실제 코드를 보면 va_list 주소 값에다가 고정인수 크기를 더한 위치로 ap를 초기화하기 때문입니다.

이렇게 ap를 초기화 하는 이유는 아래 그림을 보면 바로 이해할 수 있어요 ㅎㅎ ap는 가변인수 함수에 들어오는 고정인수 다음부터 위치되어야 하니까요~

va_arg

ap포인터가 위치한 부분의 데이터를 읽어 반환합니다.

그 다음에 ap 포인터를 타입길이만큼 뒤로 옮기기 때문에 그 다음 값이 이어서 출력될 수 있는거~

#define va_arg(ap, t) (*(t*)((ap += _INTSIZEOF(t)) - _INTSIZEOF(T)))

좀 더 자세히 설명하자면, va_arg는 위처럼 정의되어있는데 자칫 보면 응? 더했다가 왜 고대로 빼? 이렇게 생각할 수 있어요 ㅎㅎ

ap += _INTSIZEOF(t) 부분에 의해서 ap 주소값은 t사이즈만큼 증가합니다.

하지만 그 다음, 값을 빼고 앞에 덧셈할때처럼 ap에 대입하지는 않아요, 즉 ap 사이즈는 증가한 위치에 고대로 있지만, 실제 우리가 출력하는 부분은 기존 부분이 되는 겁니다.

그림을 보면 좀 더 이해가 수월할 수 있는데요, ap는 104번지에 가있지만 실제 우리가 데이터르 출력하는건 100번지에 있는 값이라는거죠!! 그래서 다음에 이 매크로를 또 호출했을 땐 104번지 값을 호출하고 ap는 type만큼 뒤로 밀려있게 되는거~~

이렇게 하는 이유는 타입마다 사이즈가 다른데 이 타입별로 ap 포인터를 뒤로 옮기기 위해서예요.

 

va_end

#define va_end(ap) (ap = (va_list)0);

아스키코드 0은 null을 뜻하죠. va_list 타입을 널로 초기화합니다. 다 끝났단 뜻이예요. 

가변인수 사용 예시 2- 평균 구하는 함수 

이제 각 매크로의 뜻을 알았으니 또 다른 예시코드로 이해했는지 확인해봅시다.

#include <stdio.h>
#include <stdarg.h>
double avg(double value, ...);
int main()
{
	printf("result :%.3lf", avg(1.01, 34.02, 7.45, 0.0));
	return 0;
}
double avg(double value, ...)
{
	double sum = 0; int num = 0;
	va_list ap; 
	va_start(ap, value);
	while (value != 0.0)
	{
		sum += value; num++;
		value = va_arg(ap, double);		
	}
	va_end(ap);
	return (sum / num);	
}

이번엔 첫 번째 고정인수에 총 개수가 아닌 값을 넣어줬어요.

va_start에서 포인터를 뒤로 옮기기 때문에 va_arg 사용때는 두 번째 값부터 추출된다는 거~~ (즉 고정 인수 다음 첫 번째 가변인수가 추출)

ref.

더 자세하게 알고싶은 분들이 참조하면 좋은 사이트 링크를 남기고 가요! 디테일하게 설명하려다가 풀이가 쉽게는 안될 것 같아서..

1. https://books.google.co.kr/books?id=Z9aNTafcb3IC&pg=PT458&lpg=PT458 해당 페이지에서 'va_list'검색

2. http://blog.aaronballman.com/2012/06/how-variable-argument-lists-work-in-c/

3. https://web.archive.org/web/20160801075139/http://www.x86-64.org/documentation/abi.pdf (Varaible argument 검색)

 

오늘은 간단하게 가변 인수에 대해서, 그리고 가변인수를 받는 함수를 정의하는 법을 살펴보았습니다.

다음에 또 봐요~ :) 오늘도 수고하셨습니다.

반응형
  • stm초보 2021.02.02 23:26

    좋은 글 잘 보았습니다. 감사합니다.

  • sudo 2021.07.04 02:56 신고

    가변인자 글중에 제일 이해가 잘되고 좋은 설명이었습니다. 덕분에 이해가 됐습니다. 감사합니다 :)
    다름이 아니라 매크로 부분에 INTSIZEOF는 정수형의 배수로 리턴하고, 이건 스택 한칸을 나타내려는 의도인 것 같은데 64비트에선 어떻게 쓰이나요? 64비트에선 정수형 크기(4byte)와 스택 한칸의 크기(8byte)가 다르니까요