부록 B — printf·scanf 서식 완전 정리
22장에서 최소 집합(%d, %s, %%)을 익혔고, 61장에서 서식 문자열을 해부했다. 이 부록은 찾아보기 위한 자리다 — 필요한 줄만 짚어 읽으면 된다. 규칙 하나만 앞세운다: 서식과 인자의 타입이 어긋나면 계약 밖(정의되지 않은 동작)이다(47·61장).
서식 지정의 뼈대
출력과 입력의 문법은 닮았지만 뜻이 다르다. 먼저 두 뼈대를 나란히 둔다.
출력(printf 계열) | 입력(scanf 계열) |
|---|---|
% [플래그] [폭] [.정밀도] [길이] 변환 | % [*] [최대폭] [길이] 변환 |
| 폭 = 최소 글자 수 — 모자라면 채우고, 넘쳐도 자르지 않는다 | 폭 = 최대 읽을 글자 수 — 버퍼를 지키는 수단이다 |
정밀도가 있다(.) | 정밀도가 없다 |
*는 폭·정밀도를 인자로 받는다 | *는 읽되 저장하지 않는다(억제) |
표 98.1
출력 변환 — 전체 목록
| 변환 | 인자 타입 | 출력 모양 | 메모 |
|---|---|---|---|
%d %i | int | 십진 부호 있는 정수 | 출력에서는 둘이 같다 |
%u | unsigned int | 십진 부호 없는 정수 | |
%o | unsigned int | 8진 | #를 붙이면 앞에 0 |
%x %X | unsigned int | 16진 소문자·대문자 | #를 붙이면 0x·0X |
%f %F | double | 123.456000 | 기본 정밀도 6 |
%e %E | double | 1.234560e+02 | 지수 표기 |
%g %G | double | 짧은 쪽 자동 선택 | 뒤따르는 0을 지운다 |
%a %A | double | 0x1.edp+6 | 16진 부동소수(C99) — 비트를 정확히 본다 |
%c | int(문자로) | 문자 하나 | 인자는 승격되어 int로 온다 |
%s | char * | NUL까지의 문자열 | 정밀도로 최대 길이를 제한할 수 있다 |
%p | void * | 구현 정의 표기 | 대개 16진 주소 |
%n | int * | (출력 없음) | ★ 찍은 글자 수를 쓴다 — 보안상 쓰지 않는다 |
%% | — | % 한 글자 |
표 98.2
플래그, 폭, 정밀도
| 자리 | 표기 | 뜻 |
|---|---|---|
| 플래그 | - | 왼쪽 정렬(기본은 오른쪽) |
0 | 남는 자리를 0으로 채운다(-와 함께면 무시) | |
+ | 양수에도 부호를 붙인다 | |
| 빈칸 | 양수 앞에 빈칸 하나 | |
# | 대체 형식 — %#o→0, %#x→0x, %#f→소수점 항상 | |
| 폭 | %8d | 최소 8칸 |
%*d | 폭을 인자로 받는다: printf("%*d", 8, n) | |
| 정밀도 | %.3f | 소수 셋째 자리까지(반올림) |
%.5d | 정수를 최소 5자리로(앞을 0으로) | |
%.4s | 문자열을 최대 4글자만 | |
%.*s | 최대 길이를 인자로: printf("%.*s", len, p) |
표 98.3
%.*s는 이 책에서 자주 쓴 형태다 — 길이를 따로 지니고 다니는 문자열(58·90장의 뷰)을 NUL 종단 없이 그대로 찍을 수 있기 때문이다.
다만 한 가지를 지켜야 한다. *가 받는 폭·정밀도 인자의 타입은 int다. 길이를 size_t로 들고 다니는 코드(뷰가 대개 그렇다)에서 그대로 넘기면 가변 인자의 타입이 어긋난다 — 58장에서 본 그 불일치다. 길이를 확인한 뒤 캐스트해 넘기는 것이 정석이다.
if (v.size <= INT_MAX)
printf("%.*s", (int)v.size, (const char *)v.ptr);길이 수식어 — 타입의 폭을 알려 주는 자리
가변 인자에는 타입 정보가 실려 가지 않으므로(58장), 이 글자가 곧 계약 이다. 틀리면 스택을 잘못 읽는다.
| 수식어 | 함께 쓰는 변환 | 출력 인자 | 입력 인자 |
|---|---|---|---|
| (없음) | d i u o x X | int / unsigned int | int * / unsigned * |
hh | d i u o x X | int(char 폭으로 해석) | signed char * |
h | d i u o x X | int(short 폭으로 해석) | short * |
l | d i u o x X | long | long * |
ll | d i u o x X | long long | long long * |
j | d i u o x X | intmax_t | intmax_t * |
z | d i u o x X | size_t | size_t * |
t | d i u o x X | ptrdiff_t | ptrdiff_t * |
L | f e g a | long double | long double * |
l | c / s | wint_t / wchar_t * | wchar_t * |
표 98.4
실수에는 특별한 주의가 필요하다. 출력에서 float은 기본 진급으로 double이 되므로 %f 하나면 되고 %lf도 같은 뜻이다. 입력에서는 진급이 없으므로 %f는 float *, %lf는 double *로 반드시 갈라야 한다(61장의 오개념 상자).
고정 폭 정수의 서식
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 * | 음수 부호도 받아들여 감긴다 |
%o %x | 8진·16진 | unsigned * | |
%f %e %g | 실수 | float * | ★ double에는 %lf |
%c | 글자 그대로 | char * | ★ 공백을 건너뛰지 않는다. 폭을 주면 그만큼 읽는다 |
%s | 공백 전까지 | char[] | ★ 반드시 최대 폭을 준다(%63s) |
%[...] | 집합에 속한 글자들 | char[] | %[^,]는 쉼표가 아닌 글자. 공백도 읽는다 |
%p | 포인터 표기 | void ** | %p로 찍은 것만 되읽는다 |
%n | (읽지 않음) | int * | 지금까지 소비한 글자 수를 쓴다 |
%% | % 글자 | — |
표 98.5
억제 문자 *를 끼우면 읽되 저장하지 않는다 — sscanf(s, "%*d %d", &n)은 첫 수를 버리고 둘째 수만 받는다. 억제된 항목은 반환값에도 세지 않는다.
집합 지정자는 작은 도구 하나에 가깝다. %[abc]는 a·b·c만, %[^,\n]은 쉼표와 개행이 아닌 글자를 모은다. 범위 표기(%[a-z])는 널리 쓰이지만 표준이 보장하지는 않는다.
입력에서만 통하는 규칙들
- 서식의 공백 한 칸은 “공백 몇 개든(없어도 좋다)”이다. 개행도 공백이다.
- 서식의 보통 글자는 입력과 그대로 맞아야 한다. 어긋나면 매칭 실패로 그 자리에서 멈춘다.
- 반환값은 성공적으로 채운 항목 수다. 입력이 이미 끝났으면
EOF(음수) 이고, 이는 “형식이 틀렸다”(0)와 다른 뜻이다. - 실패한 인자는 손대지 않는다. 확인 없이 쓰면 이전 값을 새 입력으로 착각한다(85장의 반례가 그것이다).
%c와%[만 공백을 건너뛰지 않는다. 앞의 줄에서 남은 개행이 다음%c로 들어가는 사고가 여기서 나온다 —" %c"처럼 앞에 빈칸을 넣으면 건너뛴다.- 오버플로를 알려 주지 않는다.
%d에99999999999를 주는 것은 계약 밖이다. 범위 검사가 필요하면strtol계열이나 91장의 스캐너를 쓴다.
반환값 정리
| 함수 | 성공 시 | 실패·경계 |
|---|---|---|
printf fprintf | 찍은 글자 수 | 음수 |
sprintf | 쓴 글자 수(NUL 제외) | 음수 |
snprintf | 필요했던 글자 수 | 음수. 반환값이 버퍼 크기 이상이면 잘렸다 |
scanf sscanf | 채운 항목 수 | 0(매칭·변환 실패) 또는 EOF |
fgets | 버퍼 포인터 | 널(파일 끝 또는 오류) |
fputs puts | 음수가 아닌 값 | EOF |
표 98.6
snprintf의 반환값 규칙이 특히 중요하다 — 쓴 글자 수가 아니라 필요했던 글자 수이므로, 잘림 검사는 need >= (int)sizeof buf로 한다(85장).
자주 하는 실수 모음
| 틀린 코드 | 무슨 일이 일어나는가 | 고침 |
|---|---|---|
printf("%d", sz) — size_t | 64비트에서 폭이 어긋난다 | %zu |
printf("%s", 42) | 정수를 주소로 읽는다 — 붕괴 | %d |
printf(user) | 형식 문자열 취약점(61장) | printf("%s", user) |
scanf("%d", n) | 주소가 아니라 값을 넘겼다 | &n |
scanf("%s", buf) | 경계를 넘어 쓴다 | %63s처럼 폭 지정 |
scanf("%f", &d) — double | 절반만 채워진다 | %lf |
scanf("%c", &c) 연달아 | 남은 개행을 읽는다 | " %c" |
| 반환값 무시 | 실패가 조용히 지나간다 | 항목 수를 확인 |
%n 사용 | 메모리 쓰기 통로가 된다 | 쓰지 않는다 |
표 98.7