Proven C Book←↑→

부록 B — printf·scanf 서식 완전 정리

23장에서 최소 집합(%d, %s, %%)을 익혔고, 66장에서 서식 문자열을 해부했다. 이 부록은 찾아보기 위한 자리다 — 필요한 줄만 짚어 읽으면 된다. 규칙 하나만 앞세운다: 서식과 인자의 타입이 어긋나면 계약 밖(정의되지 않은 동작)이다(48·66장).

서식 지정의 뼈대#

출력과 입력의 문법은 닮았지만 뜻이 다르다. 먼저 두 뼈대를 나란히 둔다.

출력(printf 계열)입력(scanf 계열)
% [플래그] [폭] [.정밀도] [길이] 변환% [*] [최대폭] [길이] 변환
폭 = 최소 글자 수 — 모자라면 채우고, 넘쳐도 자르지 않는다폭 = 최대 읽을 글자 수 — 버퍼를 지키는 수단이다
정밀도가 있다(.)정밀도가 없다
*는 폭·정밀도를 인자로 받는다*는 읽되 저장하지 않는다(억제)

표 105.1 — 출력과 입력 서식의 뼈대

출력 변환 — 전체 목록#

변환인자 타입출력 모양메모
%d %iint십진 부호 있는 정수출력에서는 둘이 같다
%uunsigned int십진 부호 없는 정수
%b %Bunsigned int2진 — %#b 면 앞에 0bC23 에서 추가
%ounsigned int8진#를 붙이면 앞에 0
%x %Xunsigned int16진 소문자·대문자#를 붙이면 0x·0X
%f %Fdouble123.456000기본 정밀도 6
%e %Edouble1.234560e+02지수 표기
%g %Gdouble짧은 쪽 자동 선택뒤따르는 0을 지운다
%a %Adouble0x1.edp+616진 부동소수(C99) — 비트를 정확히 본다
%cint(문자로)문자 하나인자는 승격되어 int로 온다
%schar *NUL까지의 문자열정밀도로 최대 길이를 제한할 수 있다
%pvoid *구현 정의 표기대개 16진 주소
%nint *(출력 없음)★ 찍은 글자 수를 쓴다 — 보안상 쓰지 않는다
%%—% 한 글자

표 105.2 — 출력 변환 전체 목록

플래그, 폭, 정밀도#

자리표기뜻
플래그-왼쪽 정렬(기본은 오른쪽)
0남는 자리를 0으로 채운다(-와 함께면 무시)
+양수에도 부호를 붙인다
빈칸양수 앞에 빈칸 하나
#대체 형식 — %#b→0b, %#o→0, %#x→0x, %#f→소수점 항상
폭%8d최소 8칸
%*d폭을 인자로 받는다: printf("%*d", 8, n)
정밀도%.3f소수 셋째 자리까지(반올림)
%.5d정수를 최소 5자리로(앞을 0으로)
%.4s문자열을 최대 4글자만
%.*s최대 길이를 인자로: printf("%.*s", len, p)

표 105.3 — 플래그·폭·정밀도의 자리

%.*s는 이 책에서 자주 쓴 형태다 — 길이를 따로 지니고 다니는 문자열(63·95장의 뷰)을 NUL 종단 없이 그대로 찍을 수 있기 때문이다.

다만 한 가지를 지켜야 한다. *가 받는 폭·정밀도 인자의 타입은 int다. 길이를 size_t로 들고 다니는 코드(뷰가 대개 그렇다)에서 그대로 넘기면 가변 인자의 타입이 어긋난다 — 63장에서 본 그 불일치다. 길이를 확인한 뒤 캐스트해 넘기는 것이 정석이다.

if (v.size <= INT_MAX)
    printf("%.*s", (int)v.size, (const char *)v.ptr);

길이 수식어 — 타입의 폭을 알려 주는 자리#

가변 인자에는 타입 정보가 실려 가지 않으므로(63장), 이 글자가 곧 계약 이다. 틀리면 스택을 잘못 읽는다.

수식어함께 쓰는 변환출력 인자입력 인자
(없음)d i u o x Xint / unsigned intint * / unsigned *
hhd i u o x Xint(char 폭으로 해석)signed char *
hd i u o x Xint(short 폭으로 해석)short *
ld i u o x Xlonglong *
lld i u o x Xlong longlong long *
jd i u o x Xintmax_tintmax_t *
zd i u o x Xsize_tsize_t *
td i u o x Xptrdiff_tptrdiff_t *
Lf e g along doublelong double *
lc / swint_t / wchar_t *wchar_t *

표 105.4 — 길이 수식어

실수에는 특별한 주의가 필요하다. 출력에서 float은 기본 진급으로 double이 되므로 %f 하나면 되고 %lf도 같은 뜻이다. 입력에서는 진급이 없으므로 %f는 float *, %lf는 double *로 반드시 갈라야 한다(66장의 오개념 상자).

고정 폭 정수의 서식#

int32_t 같은 타입은 플랫폼마다 실제 타입이 달라서, 서식을 손으로 적으면 어긋날 수 있다. 표준은 <inttypes.h>에 매크로를 두었다.

#include <inttypes.h>

uint64_t total = 1234567890123ULL;
printf("total = %" PRIu64 "\n", total);   /* 문자열 이어붙이기다 */

int32_t n = 0;
sscanf(line, "%" SCNd32, &n);

이름 규칙은 단순하다 — 출력은 PRI + 변환 + 폭(PRId32, PRIu64, PRIx16), 입력은 SCN + 변환 + 폭(SCNd32, SCNu64). 폭 자리에는 8·16·32·64 외에 MAX(PRIdMAX)와 PTR(PRIxPTR)도 온다.

입력 변환 — 전체 목록#

변환읽는 것인자메모
%d십진 정수int *앞선 공백을 건너뛴다
%i정수(진법 자동)int *0x면 16진, 0이면 8진
%u부호 없는 십진unsigned *음수 부호도 받아들여 감긴다
%b2진 정수unsigned *C23 에서 추가
%o %x8진·16진unsigned *
%f %e %g실수float *★ double에는 %lf
%c글자 그대로char *★ 공백을 건너뛰지 않는다. 폭을 주면 그만큼 읽는다
%s공백 전까지char[]★ 반드시 최대 폭을 준다(%63s)
%[...]집합에 속한 글자들char[]%[^,]는 쉼표가 아닌 글자. 공백도 읽는다
%p포인터 표기void **%p로 찍은 것만 되읽는다
%n(읽지 않음)int *지금까지 소비한 글자 수를 쓴다
%%% 글자—

표 105.5 — 입력 변환 전체 목록

억제 문자 *를 끼우면 읽되 저장하지 않는다 — sscanf(s, "%*d %d", &n)은 첫 수를 버리고 둘째 수만 받는다. 억제된 항목은 반환값에도 세지 않는다.

집합 지정자는 작은 도구 하나에 가깝다. %[abc]는 a·b·c만, %[^,\n]은 쉼표와 개행이 아닌 글자를 모은다. 범위 표기(%[a-z])는 널리 쓰이지만 표준이 보장하지는 않는다.

입력에서만 통하는 규칙들#

반환값 정리#

함수성공 시실패·경계
printf fprintf찍은 글자 수음수
sprintf쓴 글자 수(NUL 제외)음수
snprintf필요했던 글자 수음수. 반환값이 버퍼 크기 이상이면 잘렸다
scanf sscanf채운 항목 수0(매칭·변환 실패) 또는 EOF
fgets버퍼 포인터널(파일 끝 또는 오류)
fputs puts음수가 아닌 값EOF

표 105.6 — 서식 함수의 반환값

snprintf의 반환값 규칙이 특히 중요하다 — 쓴 글자 수가 아니라 필요했던 글자 수이므로, 잘림 검사는 need >= (int)sizeof buf로 한다(90장).

자주 하는 실수 모음#

틀린 코드무슨 일이 일어나는가고침
printf("%d", sz) — size_t64비트에서 폭이 어긋난다%zu
printf("%s", 42)정수를 주소로 읽는다 — 붕괴%d
printf(user)형식 문자열 취약점(66장)printf("%s", user)
scanf("%d", n)주소가 아니라 값을 넘겼다&n
scanf("%s", buf)경계를 넘어 쓴다%63s처럼 폭 지정
scanf("%f", &d) — double절반만 채워진다%lf
scanf("%c", &c) 연달아남은 개행을 읽는다" %c"
반환값 무시실패가 조용히 지나간다항목 수를 확인
%n 사용메모리 쓰기 통로가 된다쓰지 않는다

표 105.7 — 자주 하는 서식 실수와 고침