Subject
temp link
0. ft_printf 문제 이해하기
Show All
Search
•
ft_printf의 프로토타입은 int ft_printf(const char , ...); 이어야 합니다.
The prototype of ft_printf should be int ft_printf(const char *, ...);
•
당신은 libc의 printf 함수를 다시 구현해야 합니다.
You have to recode the libc’s printf function
•
실제 printf 함수처럼 버퍼 관리를 수행해서는 안 됩니다.
It must not do the buffer management like the real printf
•
다음과 같은 서식 지정자들을 구현할 것입니다: cspdiuxX%
It will manage the following conversions: cspdiuxX%
•
모든 서식문자에서 0.* 플래그와 최소 필드 너비의 조합을 어떤 조합도 처리할 것입니다.
It will manage any combination of the following flags: ’-0.*’ and minimum field width with all conversions
•
실제 printf 함수와 비교될 것입니다.
It will be compared with the real printf
•
라이브러리를 생성할 때에는 ar 명령어를 이용하세요. libtool 명령어는 허용되지 않습니다.
You must use the command ar to create your librairy, using the command libtool is forbidden.
1. 구조 설계
이번 프로젝트를 진행하기 전 다짐한 내용이 있었다. 이번 프로젝트 과제부터는 절대로 코드먼저 잡지 않고 구조 설계와 psudo code를 먼저 구상한 다음 코드작성 작업을 하기로 다짐했다. 하지만 ft_print과제 특성상 혼자서 모든 예외처리를 다 잡는 구조를 만들어 내기엔 아직 역량이 부족해서 훌륭한 두 친구 찬호, 현준과 함께 구조설계를 진행하였다.
2. C library printf()의 line buffering
코드를 짜면서 write()와 printf()를 혼용하며 사용한 경우가 있을 것이다. 이때 분명 printf()함수를 먼저 작성하였는데 뒤에나온 write()가 먼저 출력되는 경우가 생긴다. 아래 코드를 보자!
#include <stdio.h>
#include <unistd.h>
int main(void)
{
printf("a");
write(1, "b", 1);
printf("c");
}
C
복사
왜 이런 상황이 발생하는 것일까??
바로 printf()함수는 line buffering을 사용하기 때문이다!
Line Buffering이란?
버퍼에 개행 문자('\n')가 입력 될 때 마다 출력한다. 즉 개행문자가 들어오지 않는다면 버퍼에 계속 쌓아두다가 더이상 실행할 명령이 없다면 해당 버퍼의 내용을 출력하는 것이다.
2. Error와 Warning
printf를 연습하면서 다양한 에러를 마주하게 된다. 아예 컴파일 자체가 안되는 경우부터 시작해서 컴파일은 되지만 printf가 처리하지 못하는 warning에러가 뜨는 경우이다.
우리는 printf 함수의 기능을 구현하면서 어디까지 구현을 하면 되는 것일까?
결과적으로 말하면 실제 라이브러리에 있는 printf도 처리하지 못한 예외사항을 굳이 따라해서 만들 필요가 없다. warnig 메세지가 뜬다는 것은 printf함수도 처리하지 못해서 예상치 못한 값을 출력한다는 것인데 굳이 그렇게 출력되는 내용까지 따라서 만들 필요가 없다는 것이다.
우리는 printf의 기능을 구현하는 것이지 에러났을때 문구까지 똑같이 나오게 만들 필요가 없는 것이다!!
테스트를 진행 하면서 warning 메세지를 마주하게 된다면 쿨하게 해당 처리는 넘어가도록 하자!
2. struct 선언 및 초기화
이번 printf과제에서는 구조체 활용이 거의 필수적이다. norminette규칙을 피해서 다양한 플래그를 활용하기 위해서는 구조체 내부 변수를 활용해야 한다. 피씬에서도 구조체를 사용한 적이 있기 때문에 선언 자체는 어렵지 않았다.
typedef struct s_name
{
int op1;
int op2;
int op3;
} t_name;
C
복사
하지만 위와 같이 설계한 구조체를 초기화 하는 과정에서 의문점이 생겼다. 모든 변수를 하나하나 초기화를 해줘야 하는데 만약 구조체 내부 변수의 개수가 많아지면 또 하나하나 초기화를 해주어야하고 혹시나 중간에 변경 사항이 생긴다면 초기화함수를 찾아가 그부분을 수정해 주어야 한다.
하지만 구조체도 결국 메모리 공간에 존재한다. 그렇다면 우리는 구조체가 가지는 공간을 모두 0으로 초기화 해주면 된다. 우리는 이미 libft과제에서 메모리 공간을 원하는 값으로 초기화하는 라이브러리를 만들어 두었다. 바로 memset함수를 사용하는 것이다.
memset(&{선언한 구조체}, 0, sizeof({정의된 구조체}));
C
복사
실제로 memset을 사용해서 구조체 멤버 변수들이 모두 0으로 초기화 되는지 알아보자!
#include <stdio.h>
typedef struct s_name
{
int op1;
int op2;
int op3;
} t_name;
int main(void)
{
t_name tp;
memset(&tp, 0, sizeof(t_name));
printf("op1 : %d\n", tp.op1);
printf("op2 : %d\n", tp.op2);
printf("op3 : %d\n", tp.op3);
}
C
복사
실행된 결과를 보면 실제로 모든 값들이 0으로 초기화가 되어 있는 것을 볼 수 있다. 이런식으로 작성한다면 동적할당된 구조체도 어렵지 않게 초기화할 수 있다. 아래 코드를 살펴보자.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct s_name
{
int op1;
int op2;
int op3;
} t_name;
int main(void)
{
t_name *tp = malloc(sizeof(t_name));
memset(tp, 0, sizeof(t_name));
printf("op1 : %d\n", tp->op1);
printf("op2 : %d\n", tp->op2);
printf("op3 : %d\n", tp->op3);
}
C
복사
동적할당을 하게 되면 구조체 포인터형 변수를 선언해야 한다. tp가 구조체 포인터형이기 때문에 memset내부 변수로 넣을때 따로 &를 넣지 않고 그대로 넣어주면 기대한 대로 모든 값이 제대로 초기화가 된다.
위 코드에서.을 활용한 변수 접근과 →를 활용한 접근이 있는데 아래에서 두가지의 차이점을 살펴보자.
•
구조체 변수 접근 : .
위의 예시에서 나왔듯이 우리가 만약 구조체를 선언해서 멤버 변수의 값을 초기화하거나 호출하려고 한다면 . 을 활용해서 접근할 수 있다.
•
구조체 포인터 변수 접근 : →
만약 우리가 동적할당 등 구조체 포인터를 선언했다면, 멤버 변수를 호출하기 위해 → 를 사용해야 한다.
하지만 우리는 함수의 역참조를 활용해서 다양한 방법으로 구조체 멤버에 접근할 수 있다.
처음 사용할 때에는 헷갈릴 수 있는 부분이지만, 구조체와 구조체 포인터에 대한 이해가 생긴다면 어렵지 않은 표현식이다.
2. 바이트 패딩과 가변 매크로
42 study에 있는 ft_printf내용을 공부하다 보면 가변인자 매크로를 공부할 때 _INTSIZEOF라는 매크로를 보았을 것이다. 이 매크로의 역할은 모든 자료형의 크기를 4Byte의 배수 형태로 만들어 주는 것이다. 예를들어 char 자료형의 크기는 1 Byte이지만 _INTSIZEOF를 사용하면 4라는 결과를 얻을 수 있다.
이러한 처리를 하는 이유는 바로 바이트 패딩에 있다.
바이트 패딩(Byte Padding)
바이트 패딩이란 클래스(구조체)에 바이트를 추가해 CPU 접근에 부하를 덜어주는 기법입니다.
대체 바이트를 추가하면 왜 CPU접근에 부하가 줄어드는 걸까?
아래의 예제를 보면서 살펴보자!
결국 CPU의 부하를 덜어주기위해 자료형을 메모리 공간에 저장할 때 바이트 패딩기법을 활용하는 것이고 따라서 우리는 가변인자에 접근할 때 인자의 자료형만큼만 주소를 이동시키는 것이 아니라 4Byte의 배수 단위로 옮겨주어야 하는 것이다.
3. printf가 -1을 리턴할 때
printf의 매뉴얼을 보면 아래와 같이 나와있다.
These functions return the number of characters printed (not including the trailing '\0' used to end output to strings). (중략)... These functions return a negative value if an error occurs.
정상적으로 작동할 때, 출력한 character들의 개수를 return하고 만약 error가 발생했다면 음수 값을 return한다. 그렇다면 대체 어떤 경우에서 음수 값을 return하는 것일까??
일단 처리 불가능한 옵션이 들어오면 음수값을 반환한다. 예를들어 width옵션에 너무 큰 수가 들어오면 에러를 반환한다.
int a = printf("%2147483649d\n", 42);
printf("a : %d", a);
C
복사
위의 코드로 실험해보면 a가 -1이 출력되는 것을 알 수 있다.
왜 이런 현상이 발생하는 것일까? 이유는 내부적으로 옵션으로 들어온 값이 atoi와 유사한(? 혹은 같은?) 방식으로 처리되기 때문인데 int max 값보다 큰 값이 들어오면 overflow가 발생해서 width의 값이 -가 되게 된다. 이 경우를 대비해 처리할 수 있어야 한다.
하지만 우리의 궁금증은 여기서 멈추지 않는다. 이 값이 다시 underflow가 발생해서 양수가 되면 어떻게 될까?
그래서 long long max값보다 1큰 값을 활용해(atoi 함수 내부변수로 long long을 활용하기 때문에) overflow가 돼서 -가 된 값을 다시 underflow시켜서 +값으로 돌려 놓았다.
int a = printf("%9223372036854775808d\n", 42);
printf("a : %d", a);
C
복사
그 결과 정상적으로 출력이 되는 것을 볼 수 있었다!! 하지만 여기서 의문이 생긴다. 내가 위에서 atoi와 유사한 함수라고 했던 이유는 이렇게 overflow와 underflow가 일어나 다시 양수가 된 값이 width에 저장되는게 아니라 그냥 0이 저장된다.
만약 여기서 다시 양수가 된 값이 그대로 width에 적용되었더라면 width옵션에 해당 값이 들어가게 될 것이고 그러면 예상하지 못할 개수의 공백(혹은 0)으로 채워진 공간들이 출력될 것이다. 그래서 이걸 정확하게 처리하기 위해서 기존에 작성한 atoi가 아니라 새로운 atoi함수를 만들었다.
이 함수를 작성할 때 고려한 부분은 아래와 같다.
•
애초에 음수는 들어오지 않는다(왜냐하면 -는 witdh가 아니라 flag로 처리되어 버린다)
•
만약 * flag로 받은 숫자에 음수가 들어오면 절대값으로 바꾼 값을 atoi에서 처리하고 구조체의 minus값을 1로 저장한다
•
숫자가 무한으로 들어와도 다 처리가 가능해야한다. 따라서 atoi와 동일하게 작동한다.
•
대신 while로 이동한 만큼 실제 format문자열을 가리키는 index도 동일하게 이동시켜주어야한다.
•
들어오기 전 함수의 while문에서 마지막에 index를 증가시키기 때문에 함수를 탈출하기 전 index의 값을 하나 줄여주어야 한다.
•
overflow와 underflow가 나는 경우는 비트 연산을 활용해 부호비교를 해서 체크한다.
•
long long변수를 사용하기 때문에 INT MAX ~ LONG LONG MAX 사이의 값에 대한 예외처리를 해주어야 한다.
위 사항들을 고려해서 만든 코드는 아래와 같다. atoi와 유사하지만 우리가 원하는 width값을 얻기 위해서 조금 수정한 코드이다.
int pf_utils_atoi(const char *format, size_t *i)
{
long long ret;
long long tmp;
int of_cnt;
ret = 0;
of_cnt = 0;
while (ft_isdigit(format[*i]))
{
tmp = ret * 10 + (format[*i] - '0');
if ((((1 << 31) & tmp) ^ ((1 << 31) & ret)) == 0)
of_cnt++;
ret = tmp;
++(*i);
}
--(*i);
if (of_cnt >= 1 && ret >= 2147483647)
return (-1);
if (of_cnt >= 1 && ret < 0)
return (0);
return ((int)ret);
}
C
복사
해당 atoi를 사용하면 printf가 -1을 return하는 경우에 우리가 기대한 결과값을 return한다.
5. return의 위치에 대한 고려
함수를 작성하다보면 예쁘게 짜고 싶어서 if문안에 함수를 호출하고 에러가 나면 바로 return을 하는 방식의 코드를 작성한 적이 많을 것이다. 예를들어 아래와 같은 방식이다.
int ft_printf(const char* format, ...)
{
int print_size;
va_list ap;
va_start(ap, format);
if ((print_size = pf_printf(format, ap)) == -1)
return (-1);
va_end(ap);
return (print_size);
}
C
복사
하지만 이 경우에 나는 va_end를 처리하는 부분에서 내가 실수했다는 것을 알았다. 이렇게 작성할 경우 pf_printf함수에서 에러가 발생한다면 ap포인터를 초기화하지 않고 함수를 종료해버리는 것이다.
사실 이러한 경우는 va_end가 아니라 동적할당을 해제하는 경우에도 많은 실수가 일어나는 코드이다.
아래는 위의 문제점을 보완한 코드이다. (norminette가 이후 삼항연산을 제한하기 때문에 if문으로 처리하였다)
int ft_printf(const char* format, ...)
{
int print_size;
va_list ap;
va_start(ap, format);
print_size = pf_printf(format, ap);
va_end(ap);
if (print_size < 0)
return (-1);
return (print_size);
}
C
복사
4. Main Function
ft_printf를 위한 main function은 너무 간단하다. 기존 printf와 똑같은 형태로 사용되기 때문이다.
아래는 테스트 할때 사용한 main fuction이다.
char부터 하나씩 테스트 하면서 모듈을 완성시켰다.
int main(void)
{
// c with no fl
ft_printf("%c%c%c%c%c", 'i','n','i','t','6');
// c with - fl
return (0);
}
C
복사
5. ft_printf Pseudo Code
#include /* header */
int ft_printf(const char* format, ...)
{
ap // 가변인자 포인터 선언
len // 총 출력 길이 저장할 변수 선언
while( /* format[i] 가 '\0'만날때까지 */)
{
// 구조체 초기화
if (/* format[i] 가 '%' 만났다면 */)
{
while (/* format[i] 가 type 만날때까지 */)
// 구조체에 각 플래그 할당!
if (/* 현재 인덱스에서 format[i] 가 type인가?? */)
// 출력모듈로 이동
}
else /* format[i] 가 '%' 안만났다면 */
{
// 그냥 char 출력 && 출력 길이 + 1
}
}
return (/* 총 출력 길이 */);
}
C
복사