22 출력
먼저 알아야 할 것
돌아보기
10장에서 프린터 터미널은 행 버퍼링 — 종이에 찍기 전에 한 줄이 모일 때까지 기다리는 것 — 이 자연스러웠다고 했다. 오늘의 printf에도 그 버릇이 남아 있는가?
답. 남아 있다 — 표준 출력이 터미널로 이어져 있을 때, 출력은 보통 \n을 만날 때까지 내부 창고(버퍼)에 모였다가 한 줄씩 흘러 나간다. 19장의 시연에서 printf("Hello, ")의 글자들이 두 번째 호출의 \n이 오고서야 화면에 나타났던 것이 바로 이것이다. 종이 타자기의 사정이 사라진 지 반세기 뒤에도, 그 리듬은 소프트웨어의 기본값으로 살아 있다 — 이 버릇이 실전에서 일으키는 소동(출력이 안 보여요!)과 다스리는 법은 입출력을 본격적으로 다룰 때 만난다.
이 장의 필요성과 맥락
printf 가 앞의 셋을 전부 쓰기 때문이다 — 문장(19장) 안에서, 상수(20장)를 재료로, 호출(21장)의 꼴로 일어난다. 그래서 이 장이 끝나면 헬로 월드에 남은 외상이 두 줄뿐이다.이 장이 끝나면
printf를 정면으로 다룬다 — 서식 문자열이라는 틀, %d와 %s라는 빈칸, 그리고 서식과 재료의 짝 맞추기까지. 이 장이 끝나면 제4부의 목표가 달성된다: 헬로 월드 한 편이 (두 줄의 외상만 남기고) 전부 아는 문장이 된다.이 장에서 답할 질문
- 화면에 찍힌 것이 왜 곧바로 안 보일 때가 있는가 — 프로그램이 멈춘 것처럼 보이다가 한꺼번에 쏟아지는 일이 있는데.
- 시연의 실행 결과에서
stderr줄이 왜 맨 위에 나왔는가?
22.1 printf — 서식이 있는 출력
printf라는 이름은 print formatted — “서식대로 찍어라” — 의 줄임이다. 첫 재료가 서식 문자열(format string)이고, 나머지 재료들이 서식의 빈칸에 끼워질 값들이다:
printf(서식 문자열, 값1, 값2, ...)서식 문자열은 틀이다. 보통 글자는 그대로 흘러 나가고, %로 시작하는 자리 — 변환 지정(conversion specification) — 만 빈칸으로 취급되어, 뒤따르는 값이 글자로 바뀌어 끼워진다. 이 책의 최소 집합은 셋이다:
%d— 정수 값을 십진법 글자로 바꿔 끼운다 (decimal).%s— 문자열을 그대로 끼운다 (string).%%— 빈칸이 아니라, 퍼센트 글자 자체를 찍고 싶을 때.
시연한다. 빈칸 둘에 값 둘이 순서대로 끼워진다:
examples/ch22/fmt.c
#include <stdio.h>
int main(void)
{
printf("The answer to %s is %d.\n", "life, the universe and everything", 6 * 7);
return 0;
}
실행 결과
The answer to life, the universe and everything is 42.
%s 자리에 문자열 리터럴이, %d 자리에 수식 6 * 7의 값이 들어가 문장이 완성됐다. 서식은 틀, 재료는 순서대로 — 이것이 printf 읽기의 전부다. (변환 지정에는 자릿수 맞춤 같은 잔가지 선택지가 많지만, 필요할 때마다 하나씩 소개한다 — 전체 목록은 부록의 참조 자료다.)
문. 화면에 찍힌 것이 왜 곧바로 안 보일 때가 있는가 — 프로그램이 멈춘 것처럼 보이다가 한꺼번에 쏟아지는 일이 있는데.
답. 돌아보기에서 본 버퍼링 때문이다. 출력이 터미널로 갈 때는 줄 단위로 흘려보내지만, 파일이나 다른 프로그램으로 갈 때(10장의 리다이렉션)는 더 큰 덩어리가 찰 때까지 모았다가 한꺼번에 내보낸다 — 왕복 횟수를 줄이는 성능 요령이다. 그래서 진행 상황을 찍는 프로그램이 파이프로 이어지면 “멈춘 듯 보이다 몰아서 출력”되는 일이 생긴다. 급히 내보내야 할 때는 fflush(stdout)으로 창고를 비우라고 지시할 수 있고, 프로그램이 정상 종료하면 남은 것은 자동으로 비워진다 — 다만 갑자기 죽으면 그 창고의 내용이 사라진다는 것이, 사고 직전의 로그가 안 남는 흔한 이유다.
22.2 빈칸의 종류 — 값마다 다른 자리
%d와 %s만으로도 꽤 가지만, 값의 종류마다 빈칸이 따로 있다. 자주 쓰는 것부터 익혀 둔다.
examples/ch22/convert.c
/* 자주 쓰는 변환 지정과, 폭·정밀도로 줄을 맞추는 법.
제4부는 아직 변수를 배우기 전이라, 재료는 전부 상수와 수식이다. */
#include <stdio.h>
int main(void)
{
puts("[each kind of value has its own conversion]");
printf(" integer %%d -> %d\n", 42);
printf(" float %%f -> %f\n", 3.14);
printf(" char %%c -> %c\n", 'A');
printf(" string %%s -> %s\n", "hello");
printf(" hex %%x -> %x\n", 255);
printf(" percent %%%% -> %%\n");
puts("\n[width - reserve columns so things line up]");
printf(" |%d|%d|\n", 7, 1234);
printf(" |%6d|%6d| <- six columns, right-aligned\n", 7, 1234);
printf(" |%-6d|%-6d| <- a minus sign left-aligns\n", 7, 1234);
printf(" |%06d| <- a leading 0 pads with zeros\n", 42);
puts("\n[precision - how many digits after the point]");
printf(" %%f -> %f <- six digits by default\n", 2.0 / 3.0);
printf(" %%.2f -> %.2f <- rounded to two digits\n", 2.0 / 3.0);
printf(" %%8.2f -> |%8.2f| <- width and precision together\n", 2.0 / 3.0);
printf(" %%.3s -> %.3s <- on a string it means 'the first N bytes'\n", "abcdef");
puts("\n[this is how you lay out a table]");
printf(" %-8s %6s\n", "item", "count");
printf(" %-8s %6d\n", "apple", 3);
printf(" %-8s %6d\n", "banana", 12);
printf(" %-8s %6.1f\n", "weight", 4.25);
puts("\n[but width counts BYTES - non-ASCII text drifts]");
printf(" %-8s %6d\n", "사과", 3);
printf(" %-8s %6d\n", "바나나", 12);
puts(" a Hangul syllable is 3 bytes in UTF-8, so eight columns are not eight letters.");
puts("\n[other ways out]");
puts(" puts prints one string and adds the newline for you");
putchar(' '); putchar(' '); putchar('p'); putchar('c'); putchar('\n');
fprintf(stdout, " fprintf(stdout, ...) is the same as printf\n");
fprintf(stderr, " fprintf(stderr, ...) goes out on the error stream\n");
return 0;
}
실행 결과
fprintf(stderr, ...) goes out on the error stream
[each kind of value has its own conversion]
integer %d -> 42
float %f -> 3.140000
char %c -> A
string %s -> hello
hex %x -> ff
percent %% -> %
[width - reserve columns so things line up]
|7|1234|
| 7| 1234| <- six columns, right-aligned
|7 |1234 | <- a minus sign left-aligns
|000042| <- a leading 0 pads with zeros
[precision - how many digits after the point]
%f -> 0.666667 <- six digits by default
%.2f -> 0.67 <- rounded to two digits
%8.2f -> | 0.67| <- width and precision together
%.3s -> abc <- on a string it means 'the first N bytes'
[this is how you lay out a table]
item count
apple 3
banana 12
weight 4.2
[but width counts BYTES - non-ASCII text drifts]
사과 3
바나나 12
a Hangul syllable is 3 bytes in UTF-8, so eight columns are not eight letters.
[other ways out]
puts prints one string and adds the newline for you
pc
fprintf(stdout, ...) is the same as printf
| 빈칸 | 무엇을 끼우는가 | 이름의 유래 |
|---|---|---|
%d | 정수를 십진법으로 | decimal |
%f | 실수를 소수점 표기로 — 기본은 소수점 아래 여섯 자리 | floating |
%c | 글자 하나 | character |
%s | 문자열 | string |
%x | 정수를 16진법으로 — %X 는 대문자로 | hexadecimal |
%% | 빈칸이 아니라 퍼센트 글자 자체 | — |
표 22.1
정수·실수·글자·문자열 — 20장에서 상수를 적는 네 갈래를 배웠으니, 그 넷을 찍는 빈칸이 하나씩 있는 셈이다. 나머지 변환(부호 없는 정수, 포인터, 크기 등)은 그 타입을 배우는 자리에서 하나씩 만나고, 전체 목록은 부록 B에 있다.
22.3 자리를 맞추는 법 — 폭과 정밀도
빈칸에는 얼마나 넓게, 얼마나 자세히를 함께 적을 수 있다. 표를 만들 때 곧바로 쓸모가 있다.
| 적는 법 | 뜻 | 결과 |
|---|---|---|
%6d | 여섯 칸을 잡고 오른쪽 맞춤 | |␣␣␣␣␣7| |
%-6d | 빼기를 붙이면 왼쪽 맞춤 | |7␣␣␣␣␣| |
%06d | 빈자리를 0 으로 | |000042| |
%.2f | 소수점 아래 두 자리로 — 반올림한다 | 0.67 |
%8.2f | 폭과 정밀도를 함께 | |␣␣␣␣0.67| |
%.3s | 문자열에 쓰면 앞에서 몇 글자 | abc |
표 22.2
읽는 순서를 외우면 헷갈리지 않는다 — % 다음에 맞춤(-·0) → 폭 → 점과 정밀도 → 변환 글자 차례다.
흔한 오해. “폭을 여덟로 주면 여덟 글자가 들어간다”
바이트로 센다. 시연의 마지막 표가 그것을 보인다 — 영문 표는 가지런한데 한글 표는 어긋난다. 한글 한 글자가 UTF-8 로 세 바이트라(9장), %-8s 가 잡아 주는 여덟 칸에 한글은 두 글자 남짓밖에 못 들어간다.
터미널에서 한글 표를 가지런히 맞추려면 글자 수를 세어 직접 여백을 붙이는 수밖에 없고, 그것도 「한 글자가 화면에서 몇 칸을 차지하는가」라는 또 다른 문제를 부른다 (동아시아 문자는 두 칸을 차지한다). 72장에서 이 문제를 다시 만난다.
22.4 출력의 창구는 하나가 아니다
printf 말고도 셋이 더 있고, 쓰는 자리가 다르다.
| 함수 | 하는 일 | 언제 |
|---|---|---|
printf | 서식대로 찍는다 | 값을 끼워 넣을 때 |
puts | 문자열 하나를 찍고 줄바꿈까지 | 고정된 한 줄. printf 보다 빠르고 안전하다 |
putchar | 글자 하나 | 한 글자씩 만들어 낼 때 |
fprintf | 어느 스트림에 찍을지 골라서 | 표준 출력이 아닌 곳으로 |
표 22.3
마지막이 중요하다. 프로그램에는 나가는 띠가 둘이다(10장) — 결과가 흐르는 표준 출력(stdout)과, 오류 메시지가 흐르는 표준 오류(stderr).
printf("결과: %d\n", 42); /* 표준 출력 */
fprintf(stderr, "파일을 열 수 없습니다\n"); /* 표준 오류 */둘을 가르는 이유는 리다이렉션(10장) 때문이다. 결과를 파일로 돌려 두어도 오류 메시지는 화면에 남아야 하고, 파이프로 다음 프로그램에 넘길 때 오류 메시지가 섞여 들어가면 안 된다. 「결과는 표준 출력으로, 사람에게 하는 말은 표준 오류로」가 도구다운 프로그램의 규율이다(53장의 종료 상태와 같은 결이다).
문. 시연의 실행 결과에서 stderr 줄이 왜 맨 위에 나왔는가?
답. 표준 오류는 버퍼링하지 않기 때문이다. 표준 출력은 돌아보기에서 본 대로 창고에 모았다가 흘려보내는데, 표준 오류는 모으지 않고 즉시 내보낸다 — 오류 메시지는 프로그램이 죽기 직전에도 남아야 하니까.
그래서 두 띠를 한 파일로 합쳐 담으면(이 책의 예제 검증이 그렇게 한다) 나온 순서가 적은 순서와 다를 수 있다. 이것은 사고가 아니라 설계이고, 로그를 읽을 때 알아 두면 좋은 성질이다.
22.5 짝 맞추기 — 서식과 재료의 계약
서식 문자열은 곧 계약이다. 빈칸의 개수와 종류가, 뒤따라야 할 재료의 개수와 종류를 약속한다. %d 두 개를 적었으면 정수 값 두 개가 따라와야 하고, %s 자리에는 문자열이 와야 한다.
약속을 어기면 — %d 자리에 문자열을 넣거나 재료 개수가 모자라면 — 계약 밖, 즉 정의되지 않은 동작이다(7장에서 만난 그 개념이 함수 사용 설명서에도 있는 것이다). 다행히 현대의 컴파일러는 17장에서 켜 둔 경고 (-Wall)로 이 어긋남을 대부분 잡아 준다 — printf의 서식 검사는 경고 기능 중에서도 가장 오래되고 믿음직한 축이다.
실제 사례. 서식 문자열이 무기가 된 사건 — format string 취약점
이 짝 맞추기 계약은 보안의 역사에도 등장한다. 사용자가 입력한 글을 그대로 찍고 싶을 때,printf(사용자입력)처럼 입력을 서식 문자열 자리에 넣은 프로그램들이 있었다. 입력이 평범한 글이면 문제가 없지만, 악의적인 사용자가 % 기호들을 섞어 보내면 — printf는 그것을 빈칸 으로 해석하고, 있지도 않은 재료를 찾아 메모리를 뒤지기 시작한다. 이 원리로 프로그램의 내부를 훔쳐보거나 망가뜨리는 공격이 2000년대에 대유행했고, “format string 취약점”이라는 고유한 이름까지 얻었다. 올바른 코드는 printf("%s", 사용자입력) — 입력을 서식이 아니라 재료로 두는 것이다. 데이터와 틀을 섞지 말 것 — 이 원칙은 보안 전반의 후렴구이기도 하다.흔한 오해. “printf는 화면에 찍는 함수다”
실용적으로는 대개 맞지만, 정확히는 아니다 —printf는 표준 출력 스트림에 흘려보내는 함수다(10장, 15장). 그 띠의 끝이 터미널이면 화면에 보이고, 운영체제의 기능으로 띠를 파일로 돌려 두면(리다이렉션) 같은 프로그램이 파일에 쓴다. 프로그램은 자신의 출력이 어디로 가는지 모르고, 알 필요도 없다 — 이 무심함이야말로 스트림 설계의 힘이다. 하나의 프로그램이 고치지 않고도 화면용·파일용·다른 프로그램의 입력용 으로 두루 쓰이는 것이 전부 이 덕분이다.22.6 제4부를 닫으며 — 헬로 월드 재독
약속을 지킬 시간이다. 15장의 여섯 줄을 다시 싣는다.
#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}읽어 보면 — #include는 전처리 지시로 stdio 도구 상자의 명단을 붙여 넣고(16·19장), main의 블록이 문장 목록을 묶으며(19장), printf 호출 이라는 수식문이 문자열 리터럴을 재료로 표준 출력에 글을 흘려보내고 (20·21·22장), \n이 줄을 바꾼다(10장). 처음 만났을 때 “주문”이던 것이 거의 전부 아는 문장이 됐다.
남은 외상은 정확히 두 줄이다 — int main(void)의 int와 (void), 그리고 return 0. 이 둘은 선언의 세계, 즉 이름과 타입을 스스로 만드는 법을 알아야 풀리고, 그것이 바로 다음 부다. 다음 부의 끝에서 이 외상은 완전히 청산된다 — 그리고 그때, 드디어 입력도 받게 된다.