[42 Seoul 성장기] ft_printf

EOH·2023년 5월 29일
1

42Seoul 성장기

목록 보기
2/5
post-thumbnail

✏️ 과제 요약

C언어를 처음 배웠을 때 hello world! 를 찍기위해 사용하는 printf 함수
이 printf를 구현해보자?! (응?)

🗺️ 순서도


🔑 공부한 Key word

  • 가변인자
  • printf 함수

🌋 고비와 극복한 방법

고비 1️⃣ : 가변인자 ... 이거 어떻게 작동하는 거야?

처음 과제를 열었을 때 프로토타입이

int ft_printf(const char *arg, ...)

이렇게 되어있어서 ...은 뒤에 아무거나 더 넣을 수 있다는 것인가? 라고 추측했었다.
여기서 ... 은 가변인자를 뜻하는 것이다.
가변인자란 인자의 갯수가 변하는 인자를 의미하며 받을 수 있는 갯수와 type이 정해지지 않는다.
위 프로토타입은 실제 printf의 함수형태와 같다.

printf("hello, %s is %dth", "today", 18);

printf에 이렇게 갯수와 타입에 상관없이 값을 넣어볼 수 있는 것도 가변인자를 매개변수로 받기 때문이다.

🧐 그래서 가변인자 어떻게 쓰는데?
가변인자는 va_list 타입으로 선언하고 stdarg.h 헤더에 들어있는 va_start, va_arg, va_end 매크로 함수를 이용해서 쓸 수 있다.

#include <stdarg.h>
void va_start(va_list ap, last);
void va_arg(va_list ap, type);
void va_end(va_list ap);

va_start

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

va_list로 만들어진 포인터에 고정인수의 크기를 더한 위치로 ap를 초기화 해준다. 즉 가변인자가 처음 시작하는 위치를 포인터에 담아주어야하기 때문이다.

va_arg

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

: ap포인터가 위치한 부분의 데이터를 va_list에 저장된 type의 사이즈만큼 주소값을 증가시켜 읽고 반환한다.

va_end

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

va_list타입에 담긴 값을 null로 초기화한다.

요약하자면 va_start로 첫 번째 가변인자의 위치를 찾아주고, va_arg로 각각 가변인자를 꺼내서 쓰고, va_end로 가변인자 사용을 종료해준다.
위 printf함수에서의 "today", 18처럼 가변인자 자리에 인자를 넘겨주면 이 인자들은 연속된 메모리 공간에 위치한다.

int	ft_printf(const char *arg, ...)
{
	va_list ap;
    int n;
    
    va_start(ap, arg);
    n = va_arg(ap, int);
    va_end(ap);
}

위 코드는 가변인자를 사용하는 간단한 예시이다.

고비 2️⃣ 어떤 자료형을 무엇때문에 선택해야할까?

가변인자의 값을 가져올 때 알맞은 자료형을 선택해서 그 자료형만큼 값을 더해주어야한다. 이 때 어떤 자료형을 선택해주어야할지에 대해 고민이 많았다.
각 자료형들은 비트 수 차이가 있다. char는 1비트, int는 4비트, unsigned int도 똑같이 4비트이다. 각 비트 수와 부호비트의 여부에 따라 표현할 수 있는 범위가 달라지기 때문에 모자라지도 낭비하지도 않는 알맞는 자료형을 선택해야한다.
비트에 대한 개념과 char형과 unsigned char형에 대한 차이는 아래 내가 쓴 글을 확인해보면 될 것이다.

고비 3️⃣ : 문자열 -> 정수로 만드는 putnbr구현시 재귀함수 사용하기

putnbr은 라피신에서도 구현했었고, 그 때도 재귀를 이용해 구하는 방법을 들어었으나 재귀가 어려웠던 나는 나만의 방식으로 구현했었다.

int	print_n(int n)
{
	int		i;
	int		l;
	char	str[10];

	i = 0;
	l = 0;
	while (n > 0)
	{
		str[i] = n % 10 + '0';
		n /= 10;
		i++;
		l++;
	}
	i = 1;
	while (i <= l)
	{
		if (write(1, &str[l - i], 1) == -1)
			return (-1);
		i++;
	}
	return (l);
    
int	ft_putnbr(int n)
{
	int	len;

	if (n == -2147483648)
	{
		if (write(1, "-2147483648", 11) == -1)
			return (-1);
		return (11);
	}
	else if (n == 0)
	{
		if (write(1, "0", 1) == -1)
			return (-1);
		return (1);
	}
	else if (n < 0)
	{
		n *= -1;
		if (write(1, "-", 1) == -1)
			return (-1);
		len = print_n(n);
		return (len + 1);
	}
	else
		len = print_n(n);
	return (len);
}

-부호는 따로 프린트하고 int 범위안의 수를 프린트하는 것이니 아무리 커도 10자리의 수일것이다.(int max = 2147483647) 10자리의 int배열을 하나 선언한 후 여기에 역순으로 넣어주고 뽑을 때도 역순으로 뽑아주는 것이다. 하지만 재귀를 이용하면 간단해보이게 코드를 짤 수 있다.

void	ft_putnbr(long long n)
{
	if (n < 0)
	{
		write (1, "-", 1);
		n *= -1;
	}
	if (n > 9)
	{
		ft_putnbr(n / 10);
	}
	write (1, &"0123456789"[n % 10], 1);
}

먼저 int max값을 예외로 빼지 않기 위해 매개변수의 자료형을 long long으로 받았다. 그리고 재귀를 이용해서 뽑아주는 것이다.
재귀를 이용하면 이전의 값이 차곡차곡 스택(stack)에 쌓인다. 스택은 후입선출의 방식이라 가장 마지막에 들어온 값이 처음으로 실행된다.
만약 123이라는 값이 들어왔으면 if 문에 걸려서 ft_putnbr(123/10)이 실행된다. 또 이 함수에서 ft_putnbr(12/10)이 실행된다.
즉 현재 재귀를 쌓아두는 스택안에 아래 처럼 들어있는 것이다.

ft_putnbr(1)
ft_putnbr(12)
ft_putnbr(123)

스택의 후입선출 방식을 따르면 ft_putnbr(1)이 먼저 실행된다. if문까지는 지났으니 write가 있는 행이 실행돼 1이 찍힌다. -> ft_putnbr(12)가 실행된다. 마찬가지로 if문까지는 지났으니 write행이 실행돼어 2가 찍힌다. -> 마지막으로 ft_putnbr(123)이 실행된다. 3이 찍힌다.
이런 방식으로 재귀 함수가 수행되는 것이다.

❗️ 느낀 점

이번 과제에서는 메모리와 자료형의 관계에 대해 많이 생각해 보게 되었다. 가변인자를 넘겨줄 때 적당한 타입을 설정해주어야하는데 적당한 타입을 고민하는 과정에서 cs적인 사고가 많이 늘었다. 포인터의 사이즈가 무엇인지, 포인터를 담아내는 적절한 자료형이 있다는것과 왜 적절한지를 메모리에 변수와 변수의 주소가 저장되는 방법, 운영체제별 저장하는 크기의 차이점을 이해하니 이해가 되었다.
앞으로 개발공부를 하면서 왜? 라는 의문이 들 때 cs지식에 기반해 해답을 고민해보면 좋을 것 같다.

🖥 소스코드 & 스터디 노션페이지

📚 reference

profile
에-오

0개의 댓글