ft_printf - 가변인자

유제민·2025년 4월 15일

42경산

목록 보기
16/17

가변인자 뭐예여

가변인자 (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

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_start는 va_list로 만들어진 포인터에게 고정인자의 주소를 가르쳐주어 va_list의 값을 초기화하여 가변 인자들을 순회할 수 있도록 준비한다.
내부적으로는 컴파러의 내장 함수인 __builtin_va_start를 사용한다.

#define va_start(ap, last) __builtin_va_start(ap, last)

  • ap: va_list로 만든 포인터
  • last: 가변인자의 첫 번째 인자 바로 앞에 오는 매개 변수의 이름. 즉, '...' 바로 전에 있는 변수명

va_arg

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)

  • ap: va_list로 만든 포인터
  • var_type: int나 long, double과 같은 타입 이름

va_end

va_end는 va_list인 ap의 값을 NULL로 변경하여 정리한다.
사용한 가변인자 변수를 끝낼때 사용한다.
내부적으로는 컴파러의 내장 함수인 __builtin_va_start를 사용한다.

#define va_end(ap) __builtin_va_end(ap)

va_copy

#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에 따라 다를 수도 있다.

참고자료

profile
무진장 게으른

0개의 댓글