혼자 정리

ft_printf 구현 과제 본문

42/ft_printf

ft_printf 구현 과제

tbonelee 2021. 5. 16. 19:56

(틀린 내용 있으면 댓글 바랍니다.)

문제 언급 필수 사항

  • 프로토 타입 : int ft_printf(const char *, ...);
  • libc(c 표준 라이브러리)의 printf를 구현해라(허나 man 3 printf를 참고하라는 것을 감안하여 c 표준에 언급되지 않은 Undefined behavior에 대한 부분은 BSD manual의 '구현 방법에 따라 정의된 행동'(Implementation-defined behavior)을 참고하려고 한다. 명시되지 않은 행동(Unspecified behavior)은 할지 안할지 모르겠다..)
  • cspdiuxX의 서식 지정자를 구현해라.
  • '-0.*'의 어떤 플래그 조합도 다룰 수 있어야 하고, minimum width는 모든 서식 지정자와 결합되어 사용될 수 있어야 한다.
  • 실제 pritnf와 비교해서 채점할 것.
  • 라이브러리 생성시 ar을 사용해서 생성할 것(libtool 금지)

문제 언급 보너스 사항

  • 필수 사항을 다 끝낸 다음 할 것.
  • 모든 보너스를 다룰 필요는 없다.
  • nfge의 변환 중 하나 이상을 다루어라.
  • l ll h hh의 플래그 중 하나 이상을 다루어라.
  • '# +'의 변환 중 하나 이상을 다루어라. (sharp, space, plus sign)

printf의 반환값

  • 해당 printf의 호출에서 출력하게 되는 모든 캐릭터의 갯수를 반환
  • 서식 지정자에 의해 변환된 문자열에 있는 문자 갯수도 모두 카운트

플래그 및 기타 옵션들

'%'문자 다음에 다음의 순서대로 등장한다.

  • 0개 이상의 플래그(순서는 상관없다)
  • (bsd 기준 직접 확인한 것) 동일 플래그나 min width/ precision이 여러 번 들어오면 나중에 들어온 것으로 덮어씌워지는 듯하다.
  • minimum field width : 있을 수도 있고 없을 수도 있고. 변환된 값이 minimum field width보다 적은 캐릭터를 갖는 경우 왼쪽에 모자란만큼 공백으로 채워준다. 좌측 정렬 플래그(left-adjustment flag)가 있는 경우 오른쪽에 공백으로 채워준다.

ex)

printf("%10d", 123); // 필드 크기가 10 -> "       123" 출력
  • precision(정밀도) : 있을 수도 있고 없을 수도 있고. '.'과 바로 뒤에 오는 숫자 스트링으로 이루어진다. 숫자 스트링이 생략되는 경우 정밀도는 0으로 간주된다. diouxX 변환에는 출력되어야 하는 최소한의 자릿수를 의미. aAeEfF변환에는 소숫점 이하로 나타나야 하는 자릿수를 의미. gG변환에는 유효 자릿수의 최대 숫자를 의미. s변환에는 최대로 출력될 수 있는 스트링의 문자갯수를 의미.

ex)

printf("%d", 123); // 정밀도 옵션 없음 -> "123"출력
printf("%.d"); // period뒤에 숫자 오지 않았으므로 정밀도 == 0인 옵션 
				// -> 정밀도보다 출력할 숫자 스트링 길이가 더 크므로 그대로 "123"출력
printf("%.5d"); // 정밀도 == 3인 옵션
				// 정밀도(==최소한 출력해야 할 '숫자로만 이루어진' 스트링 길이)가
                // 출력할 숫자 스트링 길이보다 크므로 "00123"출력

ex) minimum field width와 정밀도 같이 있는 경우

printf("%10.2d", 123); // "       123"출력
						// 정밀도보다 실제 출력해야 될 숫자 스트링 길이(3)이 더 크므로 정밀도 없는 것처럼 출력
                        // 10칸의 필드 출력해야 하므로 10 - 7 == 3만큼 왼쪽에 공백으로 채워줌.
printf("%10.4d", 123); // "      0123"출력
						// 정밀도 옵션으로 인해 0123출력해야 하고 나머지 6칸은 공백으로 채운다.
printf("%10.11d", 123); // "00000000123" 출력
						// 정밀도 옵션으로 00000000123 출력해야 하는데,
                        // 이미 필드 열 칸을 초과하므로 그대로 출력
printf("%3.7d", 12345); // "0012345" 출력
printf("%3.4d", 12345); // "12345" 출력
printf("%3.2d", 12345); // "12345" 출력
  • length modifier(길이 변환) : 마찬가지로 옵션이다. 각각의 서식 지정자에 대해 다음의 형으로 지정된다(문제에서 요구되는 것만)
    Modifier d, i o, u, x, X n f, g, e c s
    hh signed char unsigned char signed char *      
    h short unsigned short short *      
    l (ell) long unsigned long long * double (없을 때나 있을 때나 동일하므로 무시 가능) wint_t(유니코드 쓰지 않으므로 무시해도 될까) wchar_t *(wint_t와 비슷한 고민)
    ll (ell ell) long long unsigned long long long long *      
    .

서식 지정자

<diouxX>

  • int타입의 인자는 'di' 변환에서는 signed decimal(부호형 십진수), 'o' 변환에서는 unsigned octal(비부호형 8진수), 'u' 변환에서는 unsigned decimal(비부호형 10진수), 'xX'변환에서는 unsigned hexadecimal(비부호형 16진수)로 변환된다. 16진수의 경우 소문자 x인 경우 10~15의 숫자에 대해 'abcdef'가, 대문자 X인 경우 'ABCDEF'가 사용된다.
  • 정밀도(precision) 옵션이 있는 경우 출력되어야 하는 숫자의 최소 자릿수를 의미한다. 만약 변환된 숫자가 더 적은 자릿수를 필요로 하면 나머지는 왼쪽에 0으로 채워진다.

<e>

  • (생략)구현하게 되면 추가

<c>

  • int타입의 인자를 unsigned char로 변환한 후 대응되는 문자를 출력한다.

<s>

  • char *타입의 인자를 받아서 해당 포인터에 있는 문자부터 NUL문자 직전까지 출력한다. 정밀도 옵션이 있는 경우 문자열에 null문자가 꼭 있을 필요는 없다. 정밀도 옵션이 없거나 정밀도가 문자열의 크기보다 큰 경우, 문자열은 종료하는 null문자가 있어야 한다.

<p>

  • void *타입의 인자를 받아서 16진수로 출력한다('%#x'나 '%#lx'와 동일)

<n>

  • int *타입의 인자를 받은 다음, 해당 변환 직전까지 출력하게 되는 문자열의 갯수를 해당 포인터를 역참조하여 할당한다. 

<%>

  • %문자가 출력된다.

플래그(상세 사항)

< '#' >

  • 'cdinpsu'에 대해서는 아무 효과 없다.
  • 'o'변환에 대해서는 출력 스트링의 첫 글자에 '0'을 넣기 위해 정밀도를 하나 카운트 업 해준다. 
printf("%#o", 123); // "0173" (앞에 붙은 0까지 정밀도 체크할 때 카운트해준다)
printf("%#.1o", 123); // "0173"
printf("%#.2o", 123); // "0173"
printf("%#.3o", 123); // "0173"
printf("%#.4o", 123); // "0173"
printf("%#.5o", 123); // "00173" (정밀도 옵션 > 옵션 없을 때 출력될 글자수)
printf("%#.6o", 123); // "000173" (               "              )
  • 'xX'변환에 대해서는 원래 출력하려던 숫자에 '0x' 또는 '0X'을 앞에 붙여준다. 정밀도는 원래 출력하려던 숫자에 대해서만 체크

ex)

printf("%#x", 123); // "0x7b"출력
printf("%#.1x", 123); // "0x7b"출력 (정밀도 == 1 -> 두 글자이므로 그냥 출력)
printf("%#.2x", 123); // "0x7b"출력 (정밀도 == 2 -> 두 글자이므로 그냥 출력)
printf("%#.3x", 123); // "0x07b"출력 (정밀도 == 3 -> 두 글자이므로 0을 하나 붙여서 출력)
printf("%#.4x", 123); // "0x007b"출력
printf("%#.5x", 123); // "0x0007b"출력

< '0' >

  • 'n'을 제외한 모든 변환에 대해 space로 채워야 하는 경우에 '0'으로 대신 채워준다.
  • 숫자형 자료 변환(diouixX)에 대해 정밀도 옵션이 주어지는 경우 '0'플래그는 무시된다.
  • 아래 설명에 의해 '-'플래그가 있는 경우에도 무시된다.

< '-' >

  • negative field width flag 
  • 변환된 값은 field(minimum field width에서 정의되는 필드)에서 왼쪽 정렬되어 출력된다.
  • 이 옵션이 있는 경우 '0'플래그는 덮어 씌워진다. (즉 무시된다?)

< ' '(space) >

  • 부호형 변환에서 비음수가 출력되는 경우 숫자 앞에 공백이 붙는다.
printf("% d", 123); // " 123"출력

< '+' >

  • 스페이스 플래그를 덮어씌운다.
  • 스페이스 플래그에서 공백 대신 '+'가 붙는다.

별표 ( '*' ; asterisk)가 오는 경우

  • '.'뒤에 오는 경우 정밀도 옵션의 숫자를 의미
  • 그렇지 않은 경우 minimum field width의 숫자를 의미
  • 대체할 숫자 값은 printf에 인자로 주어진 값을 사용한다
  • 필드 크기로 음수가 들어오는 경우, 절댓값이 필드 크기이고 '-'플래그가 추가적으로 붙는 것으로 간주
  • 정밀도 사이즈로 음수가 들어오는 경우 정밀도가 없는 것으로 간주 -> 정밀도 옵션이 없는 것으로 간주 (bsd man에는 "A negative precision is treated as though it were missing". c 표준 문서에는 "A negative precision argument is taken as if the precision were omitted.". 여기서 precision이 정밀도의 크기값을 의미하는 것인지 정밀도 옵션을 의미하는 것인지 헷갈렸지만 직접 테스트해보니 정밀도 옵션이 없을 때와 동일한 결과로 출력되는 것을 확인함)
  • (bsd 기준) 숫자로 명시된 필드 옵션과 별표를 통해로 인자로 된 필드 옵션이 같이 있는 경우 뒤에 나오는 옵션으로 덮어 씌워진다. 그런데 앞에 나오는 것이 별표로 받는 필드 옵션이고 뒤에 나오는 것이 숫자로 명시된 필드 옵션인데 앞에 받은 인자가 음수인 경우, '-'옵션은 덮어 씌워지지 않고 필드 크기만 덮어 씌워진다. 

ex)

char	*str = "hello";
printf("%s\n", str);       // "hello" 출력
printf("%.*s\n", -12, str); // "hello" 출력
printf("%.s\n", str);      // "" 출력
printf("%.0s\n", str);     // "" 출력

ex)

printf("%*15d", -12, 123); // 별표를 통해 '-'플래그 && 필드 크기 12
                           // 뒤에 나오는 필드 옵션 통해 필드 크기 15로 갱신
                           // 최종적으로 '-' 플래그 && 필드 크기 15

ex)

printf("%*.*d", 10, 4, 123); // 10의 필드, 3의 정밀도 -> "      0123"출력

구현 전략

(전체적 흐름)

  1. 스트링 인자를 받아서 '%'문자가 나오기 전까지는 문자 하나하나를 출력(while루프 사용)(한 글자마다 반환값 하나씩 카운트해준다.
  2. '%'문자를 만나는 경우 플래그/필드/정밀도 체크하는 함수로 가서 한 글자씩 전진하며 읽어들이고, 동시에 플래그/필드/정밀도를 체크해준다. 플래그를 위한 구조체를 정의하여 한 번의 체크 함수 호출에서 하나의 구조체를 선언하고 이를 해당 형식 변환이 끝날 때까지 사용한다.
  3. 플래그/필드/정밀도 체크하는 함수에서 루프를 돌면서 체크하다가 플래그/필드/정밀도에서 나올 수 없는 문자가 나오면 일단 루프를 탈출. 만난 문자가 서식 지정자에 해당하는 문자면 해당 서식 지정자에 맞는 변환. 그렇지 않으면 해당 변환 무시.

(변환한 것을 최종적으로 출력할 때 고려할 점)

  • 각 서식 지정자마다 변환한 것이 스트링으로 되도록 구현. 출력할 때는 write함수를 통해 출력, 스트링의 길이만큼 반환값도 카운트, 그 후 스트링 free.

(서식 지정자 만났을 때 변환이 이루어지는 과정)

  • 스트링으로의 변환은 일차적으로 필드/정밀도까지 고려한 것이 아니라 온전히 인자로 받은 것으로 인해 체크되는 부분만 스트링으로 만든다. (ex. %d변환에서 인자로 123을 받은 경우 "123", %s 변환에서 인자로 "hello"를 받은 경우 "hello")
  • 그 후 위에서 만든 스트링을 정밀도에 따라 가공해준다. (ex. %.10.4d에 123의 인자인 경우 "123"을 "0123"으로. %10.3s에 "hello"인 경우 "hel"로 가공)(이 과정에서 플래그도 같이 고려할 수도 있고 이 다음 단계에서 플래그를 고려하여 가공해줄 수 있음)
  • 정밀도 단계에서 플래그를 고려해주지 않았으면 이 단계에서 플래그를 고려하여 스트링을 다시 가공
  • 마지막으로 필드 크기를 고려하여 필드 크기보다 모자르다면 '-'플래그와 '0'플래그를 고려하여 모자란 칸만큼 채워준다.

(그 외 고려할 점들)

  • 각 변환마다 공통적인 부분을 찾아 함수를 가능한 적게 사용하기
  • 메모리 누수 항상 고려 -> 할당 해제를 어느 시점에서 할 것인지 일관성을 갖자(함수를 호출한 곳에서 해제? 더 윗 호출 단계에서 해제?)
  • gnu 라이브러리에서는 man이나 c 표준에 언급된 순서를 지키지 않으면 변환이 무효화되었으나 bsd에서는 다 허용해주는듯하다.
  • bsd기준 s변환에 널 포인터 주면, "(null)"스트링과 동일하게 처리. p변환에 널 포인터 주면 %#x변환에 인자로 0 준 것과 동일하게 처리(사실 동일하지는 않은 것이 %#x 변환에 0의 인자를 주면 예외적으로 "0x"가 안 붙어서 "0x0"이 아니라 "0"이 출력된다. 하지만 포인터는 예외적으로 "0x0"이 출력).
  • duxXo변환에서 숫자 0이 인자로 들어왔을 때 정밀도 옵션이 없으면 최소한 '0'이라는 문자를 출력. 그런데 정밀도 옵션이 0으로 들어오면 최소 출력할 문자 사이즈를 0으로 인식해서 0이라는 숫자가 들어왔을 때 아무 것도 출력하지 않아도 된다고 인식하는 듯하다(min width field가 양수이면 그 크기만큼 빈 칸만 출력될 것이다). 

각 서식지정자별 플래그의 작용

  '-' '+' 'space' '#' '0' 정밀도
di 필드 안에서 왼쪽으로 정렬 / '0'플래그는 덮어 씌우기(무효화) 비음수인 경우 앞에 '+'부호 붙인다. / 'space'플래그는 무효화 비음수인 경우 앞에 ' ' 붙인다. / '+'플래그와 같이 있으면 무효 무시 '-', 정밀도 옵션 둘 중 하나 이상 있으면 무효화된다. / 없는 경우 필드에서 빈 칸을 '0'문자로 채워준다. 최소 출력해야 하는 문자 갯수
c 'd' 변환과 동일 무시 무시 무시 'd' 변환과 동일 무시
u 'd' 변환과 동일 무시 무시 무시 'd' 변환과 동일 'd' 변환과 동일
o 'd' 변환과 동일 무시 무시 숫자 앞에 '0' 붙여준다. / 정밀도 옵션이 있는 경우 자릿수 셀 때 맨 앞 '0'도 같이 세준다. 'd' 변환과 동일 'd' 변환과 동일
xX 'd' 변환과 동일 무시 무시 숫자 앞에 '0x'나 '0X' 붙여준다. / 정밀도 옵션 있을 때 자릿수에 카운트하지 않는다. 'd' 변환과 동일. / #과 같이 쓰이는 경우 0은 '0x'와 숫자 사이에 들어간다. 'd' 변환과 동일
s 'd' 변환과 동일 무시 무시 무시 (bsd기준)'d' 변환과 동일(0으로 채워준다). 최대 출력 문자 갯수
p 'd' 변환과 동일 무시 무시 무시(기본적으로 '0x'가 붙어있다) 'd' 변환과 동일 'd' 변환과 동일
n 플래그/정밀도/필드 옵션 있으면 undefined behavior