[42서울 / ft_printf] ft_printf 정리

Hans Park·2021년 6월 29일
0

42서울 본과정

목록 보기
3/15
post-thumbnail

Born to Code

42서울 본과정

토글이 없어 보기 힘듭니다.
노션과 깃 페이지를 남겨두니 참고하세요.

보너스 안함

노션이 보기 편합니다.

🚀 개발 전 기록사항


  • 개발 전 기록사항

    헤더와 파일

    • 굳이 헤더를 여러개 만들 필요가 있을까? 우선 하나만 만들어보자.

    • 파일 하나에 전부 구현하고, 파일을 나누어 보자.

      문서

    • 버퍼관리를 하지 말라는 것은 문자열 관리 없이 바로 출력하라는 것일까?

      개발사항

    • %를 만나기 전까지는 출력하고 %를 만나면 처리하면 되려나?

    • 저번에 멘토님이 말씀주신 LRparser에 대해 알아보자 (안함)

    • 반환값은 출력한 문자열의 길이, \n포함, 에러시 아무것도 출력안하고 0반환

      직접테스트사항

    • 플래그 중복 가능 여러개 중복 가능

    • width든 뭐든 %와 타입 사이에 플래그는 옮

    • width에서 int범위를 초과하면 오버플로우가 아니라 그냥 출력이안됨

  • 기능구현목록

    출력할 문자열 (string)과 데이터의 문자열 (data_str)은 서로 다름.

    • ft_printf (메인파일)
      • 가변인자 va_list에 저장
      • myprintf 호출
    • SRC folder
      • myprintf (조건 확인하고 출력하는 파일)
        • %를 만나지 않는다면 계속 출력.
        • % 를 만났다면, flag, width, precision 순으로 파싱.
        • 유효한 type 을 만났다면, 해당 출력 함수 호출.
      • PARSER folder
        • check_width
          • 플래그스타 (FLAG_STAR)가 세워져 있다면, 가변인자에서 문자를 받음.
          • 플래그스타 (FLAG_STAR)가 세워져 있지 않고 포인터의 문자가 digit 이라면, 숫자 파싱.
          • width 가 세워져 있지 않다면 0.
        • check_precision
          • 현재 포인터가 .을 가리키고 있다면, 플래그프리시전 (FLAG_PREC)을 세우고, precision을 0으로 초기화. (.만 나오고 숫자가 안나올 수 있기 때문)
          • 다음 포인터가 *을 가리키고 있다면, 가변인자에서 숫자를 받아와서 저장, 플래스스타프리시전 (FLAG_STAR_PREC)을 세움.
          • 다음 포인터가 가리키고 있는 것이 *이 아니면서 숫자라면, 숫자 파싱.
          • precision이 없을 경우 -1.
        • check_flag
          • - , 0 , * 이 그만 나올때까지 포인터 이동.
          • 해당 플래그가 있다면 변수 flag 에 비트로 저장.
      • PRINT folder
        • print_c
          • 가변인자를 숫자로 받아와서 문자로 출력.
          • width값을 비교하여 1 or width 값만큼 출력길이 지정.
          • 출력할 문자열에 flag에 맞게 0 혹은 ' ' 값 저장.
          • 문자를 출력할 문자열의 처음 혹은 끝에 저장.
          • 출력문자열 출력.
        • print_int
          • 가변인자를 숫자로 받아와 itoa함수를 사용하여 문자열로 변환.
            (이때, 음수일 경우 마이너스 고려하지 않고 양수로 진행)
          • 정밀도가 가변인자 문자열보다 길다면, 그 차이만큼 앞에 0 추가.
          • 가변인자로 받아온 숫자가 음수면, 문자열 앞에 - 추가.
          • 정밀도가 0이거나 문자가 null일 경우 문자열을 널로 채움.
            (문자열 1칸짜리로 바꾸어 널로 채움)
          • 가변인자 문자열의 길이와 width를 비교하여 출력할 문자열 할당.
          • 출력할 문자열에 flag에 맞게 0 혹은 ' ' 값 저장.
          • flag에 맞추어 가변인자 문자열을 복사.
            (이때, 만약 0플래그가 세워져 있으면서 가변인자가 음수일 경우, 출력할 문자열의 맨 앞을 -로 바꾸고 가변인자 문자열의 -0으로 바꿈.)
          • 출력
        • print_percent
          • print_c와 같으나, 가변인자를 받지 않고 %를 넣음.
        • print_pointer
          • 가변인자가 null일 경우 2칸 할당 뒤 숫자 0, 아닐 경우 16칸을 할당하고 데이터를 16진수로 변환하여 넣음.
            (평상시 주소값은 12자리로 나오나, 가변인자로 엄청난 큰 수가 올 경우 16자리임.)
          • 출력문자열의 길이를 가변인자 문자열의 +2 만큼 저장.
          • width의 길이와 출력문자열의 길이를 비교하여 문자열 할당.
          • 출력할 문자열에 flag에 맞게 0 혹은 ' ' 값 저장.
          • flag에 맞추어 가변인자 문자열 복사.
            (이때, 마이너스 플래그가 유효할 경우 앞 2자리를 확보해야함.)
          • 복사된 문자열 앞 두자리에 각각 0x를 넣음.
          • 출력
        • print_s
          • 가변인자가 null일 경우 (null) 문자열, 아닐경우 그대로 저장.
          • 정밀도가 음수이거나 가변인자 문자열의 길이보다 길다면 가변인자 문자열 그대로 복제하여 저장, 아니라면 정밀도만큼만 복제하여 저장.
          • width값과 문자열의 길이를 비교하여 출력문자열의 길이 할당.
          • 이하 위 함수들과 같은 출력처리.
        • print_unsigned_int
          • print_int 와 같으나 마이너스 부분만 삭제.
        • print_x
          • print_p와 같으나 0x 부분만 삭제. 대문자소문자는 put_number_base에서 처리
      • UTILS folder
        • utils
        • utils2
        • utils3
    • INCLUDE folder
      • 헤더파일

🚀 고려사항


피신때부터 악명높았던 printf가 왜 그만한 평을 받고 있는지 잘 알게 되었다.

각 타입의 출력형식 뿐 아니라 플래그의 적용이나 옵션의 유무도 달랐다.

🖋 문제해결순서

  1. % 를 만나기 전까진 한글자씩 출력.
  2. % 를 만나면, 플래그 길이 정밀도 순으로 파싱 후 타입 확인.
  3. 데이터와 타입별로 데이터를 조작하여 출력.

🖋 파싱 ; 변수와 구조체

  • % 부터 플래그, 길이, 정밀도 순으로 파싱을 하고 각 값을 저장한다.
  • 파싱할 내용이 없다면 default값으로 저장된다.
  • 플래그는 변수의 비트를 이용하여 값을 저장한다.
    처음엔, 구조체를 사용하지 않을 목적으로 변수를 사용했으나, 42서울 norm 규정에 따라 함수의 매개변수를 최대 4개까지만 담을 수 있어, 파싱된 데이터 값을 넘겨줄 수 없어 구조체를 사용하게 되었음.
    구조체를 사용할 것이라면, 이러한 방법은 오히려 코드의 길이만 길어지는 문제를 야기할 수 있음.
#  define FLAG_MINUS 1
#  define FLAG_ZERO 2
#  define FLAG_STAR 4
#  define FLAG_STAR_PREC 8
#  define FLAG_PREC 16

/*
typedef struct		s_options
{
	unsigned char	flag;
	int				width;
	int				precision;
	char			type;
}					t_options;
*/

unsigned char	flag;

flag = 0; // 0000 0000

while (**p == '-' || **p == '0' || **p == '*')
	{
		if (**p == '-')
			flag |= FLAG_MINUS; // => flag = flag | FLAG_MINUS
		else if (**p == '0')
			flag |= FLAG_ZERO;  // 0000 0010
		else if (**p == '*')  
			flag |= FLAG_STAR;  // 0000 0100
		(*p)++;
	}

/*
* `|` 는 OR 연산으로 flag | FLAG_MINUS 의 값은 0000 0001이 됨.
*/

if (op.flag & FLAG_ZERO)
			string[i++] = '0';
/*
* '&' 는 AND연산으로 flag & FLAG_ZERO 의 반환값은 FLAG_ZERO, 즉 양수임
*  만약 flag가 0000 0000 이라면(오른쪽 두번째 숫자가 0일 경우), 반환값은 0임.
*/

🖋 플래그

  • 플래그의 경우 타입별로 플래그가 적용되지 않는 경우가 있다.

단 x(적용되지 않음)의 경우, undefinded behavior이지 결과값은 플래그가 적용된 상태로 나옴.(클러스터 맥 기준)

플래그가 두개 동시에 나오는 경우, 0 플래그는 제외하고 -플래그만 사용한다.

또한, 플래그가 아래 예시처럼 여러개가 나올 경우에도 작동된다.

물론 위 경우처럼 -0 이 동시에 나올 경우, undefined behavior이다.

하나만 여러개 나올 경우 경고조차 발생하지 않는다.

플래그는 어느 위치에서나 적용될 수 있지만, 경고 메세지가 뜬다.
(%--10.3--s 의 경우, invalid conversion specifier '-')

따라서, 이번에 작성한 ft_printf 코드는 undefinded behavior에 관하여 출력내용을 따라하기도, 에러로 판단하고 출력하지 않기도 한다.

while (**p == '-' || **p == '0' || **p == '*')
	{
		if (**p == '-')
			flag |= FLAG_MINUS;
		else if (**p == '0')
			flag |= FLAG_ZERO;
		else if (**p == '*')
			flag |= FLAG_STAR;
		(*p)++;
	}

🖋 길이(폭, width)

  • % 에서부터 type 까지의 부분에 대하여 출력될 총 문자열의 길이를 뜻한다.
  • 출력될 데이터의 길이(가변인자로 받았고, 가공되어 출력 전의 상태의 데이터)보다 값이 크다면 width만큼, 아닐 경우 데이터의 길이만큼 출력된다.
  • 파싱 중 *이 나오면 가변인자에서, 나오지 않았다면 숫자가 있을 경우 파싱하여 값을 저장한다.
  • *과 숫자가 같이 올 수 없다.
  • 값이 음수일 경우 - 플래그 + 숫자로 간주한다.
if (*flag & FLAG_STAR)
	{
		width = va_arg(ap, int);
		if (width < 0)
		{
			*flag |= FLAG_MINUS;
			width *= -1;
		}
	}
	else if (ft_isdigit(**p))
		width = ft_printf_atoi(p);

🖋 정밀도(precision)

  • 길이와 구분하기 위해 .을 사용.
  • width와 마찬가지로 *을 이용하여 가변인자에서 불러올 수 있다.
  • 정수의 경우 (d, i) 출력될 데이터 길이보다 짧으면 무시된다.
  • . 뒤에 숫자 없이 바로 type 이 올 수 있다. (이때, 0으로 간주 ex)%.d)
if (**p == '.')
	{
		(*p)++;
		*flag |= FLAG_PREC;
		precision = 0;
		if (**p == '*')
		{
			(*p)++;
			precision = va_arg(ap, int);
			*flag |= FLAG_STAR_PREC;
		}
		else if (ft_isdigit(**p))
			precision = ft_printf_atoi(p);
	}

🚀 ft_printf


코드를 넣으면 글이 너무 길어져 삭제하였습니다.
코드가 필요하신 분은 노션을 참고해주세요.

구현방법은 정말 여러가지입니다. 참고용으로만 보시길 바랍니다.

데이터 문자열과 출력문자열은 서로 다릅니다.

🖋 가변인자

  • 가변인자란?

    • 함수의 매개변수의 개수가 유동적인 것을 가변인자라 한다.
    • 가변인자 전달 시 고정 매개변수가 하나 이상 있어야 하며,
      마지막에 ...을 사용하여 가변인자임을 알린다.
      ex) int ft_printf(const char *str, ...)
  • 헤더 : <stdarg.h>

  • 사용 :

    • va_list 변수를 만든다.
      해당 변수는 각 가변인자의 시작 주소를 가리키는 포인터이다.

    • va_start 함수를 이용하여,
      해당 함수는 가변인자를 가져올 수 있도록 포인터를 설정(초기화)하는 함수이다.
      - va_start(ap, str)에서 ap는 가변인자(va_list) 변수, 그 뒤 고정인자를 넣는다.
      이때 가변인자는 고정인자의 뒤에 위치하게 된다.

      https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbvHfVu%2FbtqX6f7CFPq%2FKFQZtMzOm2boU7hkuXLfik%2Fimg.png

      int				ft_printf(const char *str, ...)
      {
      	va_list ap;
      	int		count;
      
      	va_start(ap, str);
      	count = myprintf(str, ap);
      	va_end(ap);
      	return (count);
      }
    • va_arg함수를 이용하여 가변인자를 가져온다.
      va_arg(va_list, 자료형)은 va_list가 가리키는 값을 자료형의 크기만큼 리턴한 후, va_list를 자료형 크기만큼 뒤로 옮긴다.

    • va_end 함수를 사용하여 va_list를 초기화한다.

<출처>

[C, C++] 가변인자(...)를 가지는 함수 구현

🖋 반환값

출력된 길이를 반환한다.

오류 발생 시 -1을 리턴한다.

🖋 %c (char)

  • 코드 삭제함.

  • data의 크기를 int로 받아야 한다.
    이에 관련한 설명은 아래 링크로 대신한다.

char type in va_arg stack_overflow

  • width의 길이가 1보다 길면, 출력할 길이를 width만큼 확보한다.
  • 0플래그가 적용될 사항일 경우 0, 아닐 경우 빈칸을, 확보한 string에 넣는다.
  • -플래그 유무에 따라 문자를 앞 혹은 뒤에 넣고 출력한다.

🖋 %s (string, char *)

  • 코드 삭제함.

  • 가변인자를 char * 자료형으로 받아온다.
    저장되는 데이터는 문자열의 주소값이다.

  • datanull(0)일 경우 문자열은 (null)이 된다. 이때, 괄호가 포함된다.

  • 정밀도가 음수이거나 문자열의 길이보다 길 경우 문자열을 그대로 두고, 양수이면서 문자열의 길이보다 짧을 경우 문자열을 정밀도의 길이만큼만 자른다.

if (op.precision < 0 || op.precision > ft_strlen(data))
		pro_data = ft_strndup(data, ft_strlen(data));
	else
		pro_data = ft_strndup(data, op.precision); 
  • width이 현재 가공된 데이터(pro_data)보다 길 경우 width만큼, 짧을 경우 가공된 데이터 길이만큼 확보한다.
  • 0플래그가 적용될 사항일 경우 0, 아닐 경우 빈칸을, 확보한 string에 넣는다.
  • -플래그 유무에 따라 문자열를 앞 혹은 뒤에 넣고 출력한다.

🖋 %d, %i (integer)

  • 코드 삭제함.

  • data를 int형으로 받아온다.

  • data를 itoa함수를 사용하여 문자열로 바꾼다. 이때 음수는 고려하지 않는다.
    (문자열에 '-' 안넣음)

  • 정밀도가 문자열의 길이보다 길다면 그 차만큼 앞에 0을 추가한다.

  • data가 음수면 앞에 -를 추가한다.

  • 정밀도가 0이면서 data가 null이면 문자열을 널값이 들어있는 1칸짜리로 바꾼다.

  • width값이 데이터 문자열의 길이보다 길다면 width, 아니라면 데이터문자열 길이만큼 출력할 문자열의 길이를 확보한다.

  • 0플래그가 적용될 사항일 경우 0, 아닐 경우 빈칸을, 확보한 string에 넣는다.

  • -플래그 유무에 따라 문자열를 앞 혹은 뒤에 넣는다.

  • -플래그가 적용되지 않으면서 때 데이터가 음수라면, 출력할 문자열의 맨 앞을 -로 바꾸고 중간에 들어있는 -를 0으로 치환한다.

  • 출력

🖋 %u (unsigned integer)

  • 코드 삭제함.

  • integer 출력방법에서 -와 관련된 부분을 삭제하고 출력.

🖋 %p (pointer, 주소값 출력)

주소값 출력이지만 가변인자로 일반변수를 넣으면, 값을 16진수로 변환하여 출력한다.

  • 코드 삭제함.

  • datavoid *형으로 받아오고 unsigned long으로 캐스팅한다.

  • data가 null이라면 1칸, 아닐 경우 16칸의 데이터 문자열의 길이를 확보한다.

  • data가 null이라면 문자 0, 아니라면 put_number_base 함수로 16진수로 변환한 값을 데이터 문자열에 저장한다.

  • data가 null이면서 정밀도가 0인 경우 문자열을 널값이 들어간 1칸짜리로 바꾼다.

  • 정밀도가 문자열의 길이보다 길다면 그 차만큼 앞에 0을 추가한다.

  • 문자열의 길이 + 2만큼 출력할 문자열의 길이를 확보한다.
    문자열의 길이 + 2보다 width길이가 길다면 그만큼 더 확보한다.

  • 0플래그가 적용될 사항일 경우 0, 아닐 경우 빈칸을, 확보한 string에 넣는다.

  • -플래그 유무에 따라 문자열를 앞 혹은 뒤에 넣는다.
    이때, -플래그의 적용을 받는다면 출력할 문자열의 앞 2칸을 확보한다. (2칸 뒤에 데이터를 넣는다.)

  • 데이터문자열이 들어간 위치 앞 2칸을 0x로 채운다.

  • 출력

🖋 %x, %X (16진수)

  • 코드 삭제함.

  • %p에서 출력할 문자열 2칸 더 확보, 0x 추가 부분을 제외하고 작성한다.

🖋 %% (%출력)

  • 코드 삭제함.

  • %c에서 가변인자를 받아오는 부분과 문자를 출력할 문자열에 삽입하는 과정을 %로 바꾸면 된다.

🚀 기타


🖋 프로젝트 구조, Makefile

  • 프로젝트 구조는 jaeseokim님의 블로그와 깃을 참고하여 작성하였다.

[42Seoul] ft_printf 프로젝트 기록.

🖋 기타 참고사항

  • warning

    • 리눅스 토르발즈의 printf를 참고하는 도중, 실제 printf에서 가변인자의 개수나 경고를 발생시키는 주체 등에 관한 궁금증이 생겼다.

    • 결론은, printf에서 경고 등은 컴파일에서 발생시키는 것으로 현재 나의 실력으로는 구현을 하거나 더 알아보기 힘들다고 판단했다.

      torvalds/linux

  • 가변인자의 끝

기타 참고

ft_printf 서브젝트와 테스터 링크

hidaehyunlee/ft_printf

ft_printf

profile
장안동 개발새발

0개의 댓글