가변인자 뭐예여
가변인자 (Variable Arguments)
함수에 들어가는 인자의 개수가 고정되어 있지 않고, 호출할 때마다 다를 수 있는 인자
가변인자는 타입과 개수가 정해지지 않는다.가변인자 예시
c printf("%d년 %d월 %d일 현재 날씨: %s", 2025, 04, 12, "비");
첫번째 인자 "%d년 %d월 %d일 현재 날씨: %s"(포멧 문자열)은 고정이고, 그 뒤에 오는 2025, 04, 12, "비"가 가변인자다. (필요할 때마다 인자 수를 달리 넘길 수 있음)
특정 함수를 사용할 때 가변인자를 사용해야 한다면,c stdarg.h에 포함된c va_list타입va_arg, va_start, va_end함수를 이용해 가변인자를 사용할 수 있다.
#include <stdarg.h>
#include <stdio.h>
int my_sum(int count, ...)
{
va_list args; //가변인자 리스트 선언
int sum = 0; // 누적할 합계 변수
int i;
va_start(args, count); //가변인자 처리를 시작 (count부터 읽는다.)
for (i = 0; i < count; i++) //count만큼 반복
sum += va_arg(args, int); //인자를 int 타입으로 하나씩 꺼내서 sum에 더함
va_end(args); //가변 인자 처리 종료
return sum; //누적된 합계를 반환
}
int main()
{
printf("%d\n", my_sum(3, 10, 20, 30)); // 10 + 20 + 30 = 60 출력
}
함수에서 두 번째 인자로 사용되는 '...'이 가변인자 또는 가변 파라미터라고 불린다.
매개변수로 아무것도 넘겨주지 않을 수도 있고 혹은 여러개로 넘겨줄 수 있다.
va_list는 내부적으로 레지스터와 스택에 저장된 인자들에 대한 정보를 유지한다.
typedef struct {
unsigned int gp_offset;
unsigned int fp_offset;
void *overflow_arg_area;
void *reg_save_area;
} va_list;
AMD64 아키텍쳐에서 정의되어 있는 va_list이다.
gp_offset: reg_save_area에서 다음으로 사용 가능한 범용 인수 레지스터가 저장되는 위치까지 오프셋을 바이트 단위로 보유한다. 모든 인수 레지스터가 소진된 경우 값 48(6*8)로 설정된다.
fp_offset: reg_save_area에서 다음으로 사용 가능한 부동 소수점 인수 레지스터가 저장되는 위치까지 오프셋을 바이트 단위로 보유한다. 모든 인수 레지스터가 소진된 경우 값 304(68+1616)로 설정된다.
overflow_arg_area: 스택에 저장된 인자들의 시작 주소, 스택에 전달된 첫 번째 인수의 주소 (있는 경우)로 초기화 되며 스택에서 다음 인수의 시작을 가르키도록 항상 업데이트 된다.
reg_save_area: 레지스터에 저장된 인자들의 백업 영역
va_start는 va_list로 만들어진 포인터에게 고정인자의 주소를 가르쳐주어 va_list의 값을 초기화하여 가변 인자들을 순회할 수 있도록 준비한다.
내부적으로는 컴파러의 내장 함수인 __builtin_va_start를 사용한다.
#define va_start(ap, last) __builtin_va_start(ap, last)
va_arg는 va_list에 저장된 var_type 값을 검색해 반환하고, va_list에서 다음 인수를 가리키도록 va_list의 주소를 이동시켜 다음 인수가 시작되는 위치로 변경시킨다.
즉, 특정 가변인자를 가리키고 있는 va_list의 포인터를 다음 가변인자로 이동시키고 기존에 가리키고 있던 값을 var_type으로 캐스팅하여 반환한다.
내부적으로는 컴파러의 내장 함수인 __builtin_va_start를 사용한다.
#define va_arg(ap, type) __builtin_va_arg(ap, type)
va_end는 va_list인 ap의 값을 NULL로 변경하여 정리한다.
사용한 가변인자 변수를 끝낼때 사용한다.
내부적으로는 컴파러의 내장 함수인 __builtin_va_start를 사용한다.
#define va_end(ap) __builtin_va_end(ap)
#include <stdarg.h>
void va_copy(va_list dest, va_list src);
va_copy는 va_start를 dest에 적용한 후 dest를 src의 사본으로 초기화 해준다.
"가변인자들은 연속된 메모리 공간에 할당된다." 는 보장은 없다.
하지만, 일반적으로는 스택에 연속적으로 저장되며, va_arg가 이를 전제롤 동작한다.
일반적인 컴파일러/플랫폼에서는 고정인자는 스택에 push
가변인자도 (...) 그 뒤에 연속해서 스택에 push된다.
위의 예시 코드를 예로 들면, int my_sum(int count, ...)
이 함수에서는 메모리 구조가 이렇게 생길 수 있다.
[ top of stack ]
---------------------
arg3 (가변)
arg2 (가변)
arg1 (가변)
count (고정 인자)
---------------------
즉, count 다음부터 연속된 인자들이 가변인자로 저장된다.
C standard를 보면 C표준은 "가변인자들이 연속된 메모리에 저장된다"는 것을 명시적으로 보장하지 않는다.
그래서 직접 주소 연산 등으로 접근하는 것은 금지되어 있으며, va_arg로만 접근해야 한다.
va_list는 내부적으로 포인터나 레지스터 백업 구조를 갖고 있으며 va_arg() 호출 시 타입 크기만큼 메모리를 이동하며 꺼낸다.
그래서 "인자들은 내부적으로 연속된 구조를 갖는 것처럼 보이게" 동작한다.
이 구조는 ABI에 따라 다를 수도 있다.
참고자료