printf는 모든 C 프로그래머가 가장 먼저 배우면서도 대부분은 끝까지 설명서를 읽지 않는 함수입니다. 10분을 들일 가치는 있습니다. 형식 문자열 자체가 하나의 작은 언어이고, 서식 지정자를 틀리는 것은 오타가 아니라 미정의 동작이기 때문입니다.
<stdio.h>에 들어 있으므로, 이 함수를 쓰는 모든 프로그램은 #include <stdio.h>로 시작합니다.
형식 문자열 모델
printf는 형식 문자열을 한 글자씩 따라갑니다. %가 아닌 것은 전부 적힌 그대로 출력됩니다. %는 변환 지정을 시작하며, 다음 인자 하나를 먹어 치우고 요청한 형태로 출력합니다.
인자는 오로지 위치로만 서식 지정자와 짝지어집니다. 첫 번째 %는 count를, 두 번째는 name을 가져가는 식입니다. printf는 타입을 볼 수 없으므로 약속한 것을 실제로 넘겼는지 확인할 방법이 없습니다. 형식 문자열은 여러분이 반드시 지켜야 하는 계약입니다. 그래서 -Wall을 켤 가치가 있습니다. GCC가 컴파일 시점에 리터럴 형식 문자열을 읽고 인자가 맞지 않으면 경고합니다.
줄바꿈은 저절로 붙지 않습니다. printf("Hello")는 o 뒤에 커서를 남기며, 거의 모든 형식 문자열이 \n으로 끝나는 이유가 이것입니다.
매일 쓰는 서식 지정자
다섯 개면 대부분의 코드를 감당합니다.
| 지정자 | 출력 대상 | 호출 예 | 출력 |
|---|---|---|---|
%d | 부호 있는 int | printf("%d", -42) | -42 |
%f | double | printf("%f", 2.5) | 2.500000 |
%s | 문자열 (char *) | printf("%s", "hi") | hi |
%c | 문자 하나 | printf("%c", 'A') | A |
%p | 포인터 주소 | printf("%p", (void*)&x) | 0x7ffd... |
이 예제에는 두 가지 규칙이 숨어 있습니다. 리터럴 %는 %%로 씁니다. 형식 문자열 끝에 %가 홀로 남으면 미정의 동작입니다. 그리고 %p는 void *를 요구하므로 포인터를 캐스팅하세요. int *를 그대로 넘기면 실제로는 잘 출력되더라도 표준상으로는 미정의입니다.
전체 표 - 모든 지정자, 모든 길이 수식어, scanf용 열까지 - 는 서식 지정자 페이지에 있습니다. 이 페이지는 손에 익은 것들만 다룹니다.
폭, 정밀도, 정렬
%와 글자 사이에는 플래그, 폭, 정밀도를 넣을 수 있습니다. 반복문을 표로 바꾸는 방법이 바로 이것입니다.
폭은 최솟값이지 최댓값이 아닙니다. %3d에 123456을 주면 여섯 자리를 모두 출력합니다. 정밀도는 타입마다 뜻이 다릅니다. %f에서는 소수점 아래 자릿수, %s에서는 최대 문자 수, %d에서는 최소 자릿수입니다.
둘 다 *로 실행 시점에 넘길 수 있으며, 이때 int 인자 하나를 읽습니다.
width를 바꾸면 표 전체가 다시 배치됩니다. C에서 열을 맞춰 출력하는 표준적인 방법이며, 문자열을 채워 주는 별도 도우미 함수가 필요 없습니다.
실수를 보기 좋게 출력하기
정밀도가 없는 %f는 항상 소수점 아래 여섯 자리를 출력하는데, 이것을 원하는 사람은 거의 없습니다.
선택지는 셋이고, 각각에 대한 경험칙이 있습니다.
%.Nf는 돈, 측정값처럼 자연스러운 소수 자릿수가 정해진 값에 씁니다. 통화라면%.2f가 정답입니다.%e는 과학적 표기법으로, 항상d.dddddde±dd형태입니다.%g는 값의 크기를 미리 알 수 없을 때 씁니다.%f와%e중 짧은 쪽을 골라 뒤쪽의 0을 떼어 냅니다. 처음 보는 값을 로그로 남길 때 가장 무난한 기본값입니다.
분명히 짚어 둘 함정이 하나 있습니다. printf에는 %lf가 없습니다. float 인자는 printf에 닿기 전에 double로 승격되므로 %f가 둘 다 처리하며, %lf는 목적지의 크기가 중요한 scanf에서만 의미가 있습니다. (C99는 printf에서도 %lf를 동의어로 받아 주지만, 그렇게 쓰는 습관이 scanf에 %f를 쓰게 만들고 그쪽은 진짜로 깨집니다.)
다른 폭의 정수들
%d는 int용입니다. 더 크거나 부호 없는 타입에는 길이 수식어가 필요하며, 이를 어긋나게 쓰는 것은 반올림 오차가 아니라 미정의 동작입니다.
사람들이 가장 자주 틀리는 것은 size_t용 %zu입니다. sizeof는 int가 아니라 size_t를 반환하므로, printf("%d", sizeof x)는 32비트 시스템에서 우연히 동작하고 64비트에서 오작동하는 버그입니다.
반환값
printf는 자신이 출력한 문자 수를 반환하며, 실패하면 음수를 반환합니다. 보통은 무시되고 대체로 그래도 괜찮지만, 출력 폭을 알아야 할 때는 진짜로 유용합니다.
사촌뻘인 sprintf와 snprintf는 화면 대신 버퍼에 쓰고, 만들어 낸 길이를 반환합니다. 버퍼 크기를 받아서 넘칠 수 없는 snprintf 를 항상 우선하세요.
snprintf는 전체 문자열이 필요로 했을 길이를 반환합니다. 따라서 반환값이 버퍼 크기 이상이면 출력이 잘렸다는 뜻입니다.
형식 문자열 취약점
printf에 얽힌 유일한 보안 문제이며, 심각합니다. 여러분이 직접 쓰지 않은 데이터를 형식 문자열로 넘기지 마세요.
/* DANGEROUS - never do this */
printf(user_input);
/* Correct */
printf("%s", user_input);
user_input에 %s가 들어 있으면 printf는 전달된 적 없는 인자를 읽고, 포인터처럼 보이는 아무 쓰레기 값이든 따라가다가 대개 크래시가 납니다. %x를 반복하면 스택 내용이 화면에 쏟아지는데, 여기에 비밀번호나 키가 섞여 있을 수 있습니다. 그리고 문자 수를 인자에 쓰는 %n은 역사적으로 이 문제를 임의 메모리 변조와 원격 코드 실행으로 만들어 왔습니다.
규칙은 절대적이고 비용도 들지 않습니다. 형식 문자열은 언제나 여러분이 직접 쓴 리터럴이어야 합니다. 변하는 것은 전부 인자로 넘기세요. GCC의 -Wformat-security가 이 실수를 잡아 주며, -Wall -Wextra에 포함되어 있습니다.
버퍼링: 출력이 가끔 늦게 나타나는 이유
printf는 터미널에 즉시 쓰지 않습니다. 출력은 버퍼에 담겼다가 버퍼가 차거나, 터미널에서 줄바꿈이 나타나거나, 프로그램이 정상 종료할 때 비워집니다. 여기서 두 가지가 따라옵니다.
- 크래시가 난 프로그램은 "이미 출력된" 내용을 잃을 수 있습니다.
printf로 디버깅하는데 마지막 메시지가 끝내 나타나지 않는다면, 크래시는 그 줄 이전이 아니라 이후에 일어난 것입니다.fflush(stdout)을 넣거나, 버퍼링되지 않는stderr로 디버그 출력을 보내세요. - 오류를 출력하는 올바른 방법은
fprintf(stderr, ...)입니다. 버퍼링되지 않으며, 사용자가 일반 출력과 별개로 리다이렉트할 수 있는 별도 스트림으로 나갑니다.
자주 묻는 질문
C에서 printf는 어떻게 동작하나요?
printf는 형식 문자열을 받아 한 글자씩 훑습니다. 보통 문자는 그대로 출력되고, 각 %는 뒤따르는 인자 하나를 소비해 요청한 형태로 출력하는 변환 지정을 시작합니다. printf("%d apples\n", 5)는 5 apples와 줄바꿈을 출력합니다.
C에서 실수를 소수점 둘째 자리까지 출력하려면 어떻게 하나요?
정밀도를 쓰면 됩니다. printf("%.2f\n", 3.14159)는 3.14를 출력합니다. 점 뒤의 숫자가 소수점 아래 몇 자리를 출력할지를 정합니다. 정밀도 없는 %f는 항상 6자리를 출력하는데, 서식을 지정하지 않은 실수가 그토록 지저분해 보이는 이유가 바로 이것입니다.
printf가 이상한 값을 출력하거나 크래시가 나는 이유는 무엇인가요?
거의 항상 서식 지정자와 인자가 어긋난 경우입니다. printf("%d", 3.5)나 printf("%s", 42)는 미정의 동작입니다. printf는 실제 타입을 볼 수 없고 형식 문자열을 믿을 뿐입니다. -Wall로 컴파일하세요. GCC가 형식과 인자가 맞는지 검사해 줍니다.
printf(user_input)은 왜 위험한가요?
사용자가 넣은 텍스트에 % 시퀀스가 들어 있으면 printf는 그것을 그대로 따르면서 전달된 적 없는 인자를 읽습니다. 스택 메모리가 새어 나가고, %n이라면 메모리에 쓰기까지 합니다. 이것이 고전적인 형식 문자열 취약점입니다. 항상 printf("%s", user_input)으로 쓰세요.