> [!date] published: 2022-01-20
매운맛 과제로 악명높았던 ft_printf 과제인데 2021년 7월인가 업데이트가 되어서 적어도 **Mandatory part**는 엄청나게 순한맛이 되었다.
잘 사용하지 않는 "가변인자"라는 개념을 사용해서 함수를 구현해야 하기 때문에 관련된 지식을 먼저 공부해보았다.
## 🚀 가변인자?
`printf`, `scanf` 처럼 인자의 개수와 자료형을 다양하게 쓸 수 있는 함수들이 있다.
이런 함수들을 가변인자 함수라고 하고, 고정적으로 명시된 함수 외에 그 가변적으로 사용할 수 있는 인자를 가변인자라고 한다.
가변인자를 사용하기 위해서는 함수를 정의할 때 인자 부분에 표시를 해 줘야 하는데 서브젝트에 있는 것처럼,
```c
int ft_printf(const char *format, ...)
{
...
}
```
이런 식으로 `...` 을 사용해서 format 뒤에 인자들이 더 들어올 것이라는 표시를 해 주면 된다.
함수 내에서 가변인자를 사용하기 위해서는 매크로들을 사용해 줘야 하는데 대표적인 것 (그리고 서브젝트에서 허용하는 것)으로는 `va_start`, `va_arg`, `va_copy`, `va_end` 가 있고, 이 매크로들은 `stdarg.h` 헤더에 정의되어 있다.
### ✨ stdarg.h
`stdarg.h` 코드 : [freebsd-src/sys/sys/\_stdarg.h at master · freebsd/freebsd-src · GitHub](https://github.com/freebsd/freebsd-src/blob/master/sys/sys/_stdarg.h)
#### va_list
`stdarg.h` 의 매크로를 사용할 때 쓰게 될 가변인자를 가리키는 주소를 저장하기 위해서 `va_list` 라는 자료형이 정의되어있다.
<details><summary>va_list의 정확한 정의(?)에 대한 생각</summary>
<div>
위의 opensource.apple.com의 코드에는 char * 로 되어있긴 한데,,, 과제를 진행하면서 여러군데에서 찾아보고 실제로 코드를 구현하면서 내린 결론은 컴파일러에 따라서 구현되는 방식이 다르기 때문에 그냥 <strong>가변인자의 주소를 가리키기 위한 어떤 자료형</strong>으로 이해하는 것이 마음이 편하겠다는 것이다. (그리고 평가자 분들도 이 점을 이해해주신 것 같다. 아주 다행..)
</div>
</details>
참고: [c - Which is the definition of va_list? - Stack Overflow](https://stackoverflow.com/questions/12855271/which-is-the-definition-of-va-list)
#### va_start
```c
va_start(va_list ap, format);
```
`va_list` ap 가 마지막 고정인수 다음 인수의 시작, 즉 첫번째 가변인수의 시작 주소를 가리키도록 초기화하는 매크로이다.
작동방식에서 가변인자함수의 필수 요구사항들을 알 수 있는데 다음과 같다.
1. 마지막 고정인수를 기준으로 하여 `va_list` 변수를 초기화하기 때문에 가변인자함수에는 반드시 적어도 1개 이상의 고정인수가 있어야 한다.
2. 마지막 고정인수의 끝 그 이후엔 모두 가변인자로 간주한다. 따라서 함수 인자의 순서는 모든 고정인수 끝에 가변인자가 들어와야 한다.
#### va_end
```c
va_end(va_list ap);
```
사용이 끝난 `va_list` ap 가 `NULL`을 가리키도록 하고 사용을 종료하는 매크로이다.
단순히 이 동작을 하는 매크로라서 굳이 사용하지 않아도 동작에 문제가 없을 수도 있지만 예기치 못한 문제를 방지하기 위하여 매뉴얼에도 `va_start`와 짝을 이루어서 사용하는 것을 권하고 있다.
#### va_copy
```
va_copy(va_list dest, va_list src)
```
dest에 src를 복사한다.
이 때 src는 `va_start`로 초기화되어있어야 하고, dest는 반드시 `va_start`로 초기화되어있지 않아야 한다.
이렇게 복사된 dest는 src와는 별개로 동일한 가변인자목록을 `va_arg`를 이용하여 조회할 수 있고, 이 역시도 사용이 끝났을 경우에는 `va_end`를 이용하여 무효화 해줘야 한다.
#### va_arg
~~이 녀석 때문에 거의 하루를 다 쓴 것 같다. 근데 결론도 허탈해서 더 슬프다.~~
```c
va_arg(va_list ap, TYPE);
```
`va_arg`의 호출은 ap에서부터 TYPE 자료형의 크기만큼 읽어서 TYPE 자료형의 가변인수를 반환하고, ap를 증가시켜 다음 가변인자의 시작을 가리키도록 한다.
평가 중에 이 내용을 말로 설명하려고 하니 좀 어려워서 급하게 그림을 그려봤다.
![[11d97f28-8f72-4b16-9039-fbd68b1eb402.png]]
이 그림에서 같은 색으로 형광펜이 칠해진 부분은 `va_arg`를 호출하는 쪽에서 책임을 지고 통일해야 하는 부분이다. 만약에 이 부분에서 문제가 생겼다면 `va_arg`는 random error를 뱉는다고 한다.
내가 고민한 이유는 대체 `va_arg`가 어떻게 작동하는건지 이해가 잘 안가서였다. 분명 위에서 많은 자료들에서 말하고있는 그 작동 방법을 잘 이해하긴 했는데 내 프로젝트에서는 위와 같은 방식으로 잘 작동하지 않았기 때문이다.
프로젝트 내에서 가변인자를 읽어오는 `va_arg` 매크로를 호출할 때 함수의 인자로 전달받은 `va_list` 변수를 이용했다. 내가 사용한 방법과 비슷하게 간단히 코드를 짜면 다음과 같다.
```c
#include <stdio.h>
#include <stdarg.h>
void print_var(va_list ap)
{
int b = va_arg(ap, int);
printf("second\t: %d\n", b);
return ;
}
void variadic_function(char *test, ...)
{
va_list ap;
va_start(ap, test);
int a = va_arg(ap, int);
printf("first\t: %d\n", a);
print_var(ap);
int c = va_arg(ap, int);
printf("third\t: %d\n", c);
return ;
}
int main()
{
variadic_function("test", 1, 2, 3);
return (0);
}
```
`variadic_function` 내부에서 `va_arg` 를 호출하고, `va_list`를 인자로 전달하여 함수 외부에서 `va_arg`를 호출하고, 그 다음에 다시 함수 내부에서 `va_arg`를 호출한다.
`va_list` 변수 자체의 주소를 전달 한 것이 아니기 때문에 만약 `va_list`가 흔히 알려진 대로 `char *`형이라면 call by value로 값이 전달되어 출력 결과가
```
first : 1
second : 2
third : 2
```
로 나올 것 같은데 과카몰리 ssh 환경에서는 아래와 같이 나온다.
![[548306ac-dc9b-42ae-9803-cbb6bb926a10.png]]
그런데 m1 맥북에서 동일한 코드를 돌려보면 처음 예상과 같은 결과가 나온다.
![[54c08cf5-98fb-4083-963a-5308d427d8e1.png]]
다른 분들의 코드를 보니 `va_list` 변수를 그대로 인자로 넘겨준 분도 있고 주소로 넘겨준 분도 있어서 엄청 혼란을 겪다가 [이 글](https://stackoverflow.com/questions/56412342/where-is-builtin-va-start-defined)을 보고 그냥 이 내용을 이해하는 것은 내 영역이 아니라는 결론을 (지금까지는) 내렸고, 평가받을 때도 내가 이해한대로 설명을 드렸다. (다행히 모두 납득을 해 주셨다..)
아무튼, 결론은 `va_arg` 매크로를 사용해서, 지금까지 읽은 가변인자 바로 다음 인자를 읽을 수 있다는 것이다!
## 🚀 함수 구현하기
printf로 어떤 서식지정자로 출력을 시도하느냐에 따라서 같은 인자를 주더라도 컴파일에러가 뜰 수도, 아니면 적절하게 변환이 되어서 출력될 수도 있다. 이런 경우들은 테스터가 있긴 하지만 그래도 일일이 체크를 해 주는게 좋을 것 같다.
여러가지 새로운 정보들에 대한 고민과, printf에 대한 생각만 정리가 되면 실제 구현 자체는 적어도 mandatory part는 굉장히 깔끔했다. 코드를 모두 올리긴 어려우니 설계하면서 대강 그려본 flow chart만 올려본다.
![[b20d634d-fd98-4678-b5ab-29a1e4ad7145.png]]
![[eb047979-68d7-4540-8584-e70e66a542a7.png]]
## 🚀 테스터
21년 7월에 과제가 변경되었기 때문에 변경된 과제에 맞게 업데이트 된 테스터를 사용해야 한다.
내가 아는 테스터는 아래의 3개이고 가능하면 모두 돌려보고 제출하기!
- [GitHub - chronikum/printf42_mandatorytester: Ugly Tester for 42 printf mandatory part. Does explicit NOT check for flags. Only types.](https://github.com/chronikum/printf42_mandatorytester)
- [GitHub - paulo-santana/ft_printf_tester: My tests for the ft_printf 42 project.](https://github.com/paulo-santana/ft_printf_tester)
- [GitHub - Tripouille/printfTester: Tester for the ft_printf project of 42 school](https://github.com/Tripouille/printfTester)