잇창명 개발 블로그

printf와 scanf 제대로 알고 가기

이 글은 단축링크 https://eatch.dev/s/printf로도 들어올 수 있습니다.

혹시 C를 배우신 적이 있나요? 그렇다면 C를 배우면서 처음으로 작성했던 프로그램을 기억하시나요? 아마 거의 모든 분들이 헬로월드를 기억할 것 같습니다.

#include <stdio.h>

int main(void) {
	printf("Hello, world!\n");
	return 0;
}

main 함수에 void가 없었거나 void main이었을 수도(하지 마세요), H가 소문자거나 ,이나 !\n이 빠졌을 수도, return 0;이 없었을 수도 있겠지만 C를 처음 배울 때는 항상 이 코드를 작성합니다. 그리고 이 헬로 월드 프로그램에는 항상 printf가 들어가 있었습니다.

어느 날은 C로 코딩을 하다가 문득 '내가 printf를 잘 알고 있다고 확신할 수 있나?' 하는 생각이 들었습니다. printfscanf는 항상 쓰기 때문에 익숙하지만, 오히려 익숙하기 때문에 더 자세히 알아보려고 하지 않는 것 같다는 느낌이 듭니다. 1년 반 전에 비슷한 고민을 하다가 C 타입 시스템을 통째로 다룬 글을 써버렸으니, 이번에도 글 하나를 작성하지 않으면 찜찜할 것 같습니다. 사실은 2021년 10월에 이미 쓰기 시작했다가 귀찮아서 지금까지 미루고 처음부터 다시 쓰고 있습니다.

C 타입 시스템 글과 달리 이번 글은 우선 커다란 치트시트 한 장을 올리고 한 부분씩 뜯어가면서 무엇을 의미하는지 자세히 설명하는 구조로 작성했습니다. 당연히 저장하거나(우클릭을 하거나 길게 눌러주세요) 인쇄하거나 공유해도 되지만, 출처는 지우지 말고 남겨주세요. HTML 버전도 있습니다.

printf/scanf 치트시트 이미지

피드백은 글 맨 아래에 있는 댓글창으로 부탁드립니다.

기본 문법

printfscanf 모두 첫 인자로 받는 문자열에 %로 시작하는 서식 지정자를 넣을 수 있습니다. 자세한 문법은 printfscanf가 조금 다릅니다.

printf

printf의 서식 지정자 문법은 %[플래그][최소 폭][.정밀도][길이](변환 지정자)입니다. (...)는 필수, [...]는 선택입니다.

각각에 대한 더 자세한 설명은 별도의 문단에서 각각 하겠습니다.

scanf

scanf의 서식 지정자 문법은 %[*][최대 폭][길이](변환 지정자)입니다. (...)는 필수, [...]는 선택입니다.

세부 사항

printf

부호 표시

부호 있는 정수/실수 서식(d/i/f/e/a/g)에서 출력하는 값이 음수일 경우에는 항상 맨 앞에 -를 붙이지만, 0이거나 양수일 때는 다음과 같은 방식으로 처리할 수 있습니다.

지수 부분의 부호는 항상 출력됩니다.

C 코드와의 호환성

보통 이 부분은 "대체"(alternative) 서식이라고만 설명하지만, 이 방식으로 설명하는 것이 이해하기 더 쉬울 것 같아서 이렇게 작성합니다. 참고만 해 주세요.

특정한 변환 지정자로 출력한 문자열은 C 코드에 그대로 붙여넣으면 잘못된 타입이나 값으로 인식될 수 있습니다.

int oct_123 = 0123; // = 83
printf("%o\n", oct_123); // 123
123; // = 123

double double_123_point = 123.; // = 123 (double)
printf("%.0f\n", double_123_point); // 123
123; // = 123 (int)

이때 # 플래그를 추가하면 C 코드에서 올바르게 인식될 수 있도록 진법 접두사와 소숫점이 보존됩니다.

int oct_123 = 0123; // = 83
printf("%#o\n", oct_123); // 0123
0123; // = 83

double double_123_point = 123.; // = 123 (double)
printf("%#.0f\n", double_123_point); // 123.
123.; // = 123 (double)

더 정확히는 다음과 같은 효과를 보입니다.

최소 폭

서식 지정자에 최소 폭 표시가 있고 실제 변환된 문자열의 길이가 이것보다 짧다면 특정한 방법을 사용해서 출력하는 길이를 강제로 늘립니다. 말 그대로 최소 폭이기 때문에 변환된 문자열이 최소 폭보다 길다고 해서 잘린 채로 출력되지는 않습니다.

정밀도

특정한 변환 지정자는 정밀도를 추가로 입력받습니다. 정확한 의미와 생략했을 때의 기본값은 각각 다릅니다.

scanf

매치 실패

대부분의 서식 지정자와 일반 문자는 특정한 모양의 입력만 처리할 수 있으며, 그 형태와 맞지 않는 입력이 들어오면(매치 실패) 입력을 더 이상 읽지 않고 바로 반환합니다.

서식 지정자가 아닌 일반 문자는 1글자의 자기 자신과 같은 문자를 매치합니다. 단, 공백 문자일 경우 0개 이상의 공백 문자를 버리고 항상 매치에 성공합니다. 서식 지정자가 무엇을 매치하는지는 아래의 변환 지정자 문단에 따로 정리해 두었습니다.

최대 폭

서식 지정자에 최대 폭 표시가 있을 경우, 그 서식 지정자는 최대 그만큼의 문자만 읽을 수 있습니다.

%n을 제외하고 모든 변환 지정자에 적용할 수 있으며, 기본값은 무한대입니다. 단, 변환 지정자 c는 예외적으로 기본값이 1입니다.

공백 입력

대부분의 변환 지정자는 실제 읽을 문자 앞에 공백 문자가 있을 경우 공백 문자를 모두 버리며, 이 공백 문자는 최대 폭에 포함되지 않습니다.

예외적으로 변환 지정자 c/[...]/[^...]/n은 공백 문자를 버리지 않습니다. 공백/줄바꿈으로 구분된 문자들을 입력받으려고 하는데 이것 때문에 오류가 생긴다면 scanf("%c", ...)scanf(" %c", ...)으로 바꾸어 해결할 수 있습니다.

공백 문자 판정은 isspace와 같은 방법으로 이루어집니다. 이는 서식 문자열의 공백 문자에도 똑같이 적용됩니다.

16진 지수 표기

잘 알려져 있지는 않지만 C에서는 부동소숫점을 10진 지수로 표기하는 문법(1.23e+4) 이외에 16진 지수로 표기하는 문법(0x1.abp+4)도 있습니다. 이 문법의 구조는 다음과 같습니다.

16진 지수 리터럴을 읽을 때는 가수 부분을 일반적으로 16진 소수를 읽듯이 해석하고, 지표 \(p\)에 대해 \(2^p\)를 곱합니다. 예를 들어 위에 예시로 든 0x1.abp+4는 1.ab(10진수로 1.66796875)에 16을 곱해 26.6875가 됩니다.

printf("%a\n", 26.6875); // 0x1.abp+4

// C 코드에도 그대로 쓸 수 있습니다. 하이라이팅이 깨지는 건 무시해주세요.
double hex_float = 0x1.abp+4; // = 26.6875

변환 지정자

일반적으로 printfscanf 사이에는 변환 지정자의 의미가 비슷합니다. 두 함수 사이에서 의미가 어떻게 다른지는 위의 치트시트에서 확인해 주세요.

printf

scanf

작성의 편의를 위해 JavaScript 정규 표현식을 추가로 작성합니다. 정규식 플래그 /isy를 가정합니다.

반환값

printf

printf는 성공했을 때 이번 호출로 출력한 문자 수를 반환합니다. %n과 같이 snprintf의 버퍼 크기를 무시합니다.

출력 오류나 _s 오류가 발생했을 때는 음수를 반환합니다.

scanf

scanf는 성공하거나 매치 실패로 반환할 경우에 그때까지 대입에 성공한 서식 지정자의 개수를 반환합니다. %n이나 *이 있는 것은 세지 않습니다.

입력 오류나 _s 오류가 발생했을 때는 EOF를 반환합니다.

변종

C 표준 라이브러리에는 그냥 printfscanf 말고도 훨씬 많은 변종이 있다는 걸 알고 계셨나요? 원본과 조금씩 차이가 있는 것들을 합쳐 무려 30가지의 printf와 24가지의 scanf가 정의되어 있는데, 각각 printf/scanf와 어떻게 다른지 확인해 보겠습니다.

이 수십 가지의 입출력 함수의 차이점은 총 4종류로 구분할 수 있고, 아래에 나열한 순서대로 붙습니다(vswprintf_s처럼).

공통

아래 문단에서는 printf 위주로 설명하지만, scanf에도 똑같이 적용됩니다.

인자 전달 방식

vprintf는 가변 인자 대신 va_list를 받습니다.

일반적으로 첫 인자로 서식 문자열을 전달하고 나면 나머지 인자는 콤마로 구분해서 하나씩 전달합니다. 이 부분이 가변 인자이기 때문에 인자의 개수를 아무렇게나 전달할 수 있습니다.

// ... 부분이 가변 인자 자리입니다.
int printf(const char *restrict format, ...);

그런데 printf와 비슷한데 다른 동작을 하는 함수를 직접 만들려고 하면 가변 인자를 전달할 수 없다는 문제점에 부딪힙니다.

int printf_ln(const char *format, ...) {
	va_list args;
	va_start(args, format);
	int result = printf(format, /* ????? */);
	printf("\n");
	va_end(args);
	return result;
}

이때는 vprintf를 대신 사용할 수 있습니다.

int printf_ln(const char *format, ...) {
	va_list args;
	va_start(args, format);
	int result = vprintf(format, args);
	printf("\n");
	va_end(args);
	return result;
}

입출력 위치

fprintf는 서식 문자열 이전에 출력할 FILE *을 하나 더 받습니다. printffprintf(stdout, ...)과, scanffscanf(stdin, ...)과 같은 것으로 생각할 수 있습니다.

FILE *file = fopen("foo.txt", "w");
fprintf(file, "%d", 123);
fclose(file);

sprintf는 서식 문자열 이전에 char *를 하나 더 받고, 그 포인터가 가리키는 곳에 문자열로 출력합니다.

char buf[64];
sprintf(buf, "%d", 4567);

문자열의 종류

wprintf는 서식 문자열을 와이드 문자열로 받습니다. w가 붙은 함수는 헤더 <stdio.h> 대신 <wchar.h>에 정의되어 있습니다.

wprintf(L"The quick brown fox"); // 문자열 앞의 L은 와이드 문자열 표시입니다.

범위 확인

아마 'scanf': This function or variable may be unsafe. Consider using scanf_s instead. To disable deprecation, use _CRT_SECURE_NO_WARNINGS. See online help for details.2로 자주 접한 녀석들일 겁니다. 잠깐 삼천포로 빠지자면 _s로 끝나는 함수는 C 표준의 부록 K에 실려있긴 하지만 반드시 지원할 필요는 없고, 해당하는 헤더 이전에 매크로 __STDC_LIB_EXT1__이 정의되어 있고 사용자가 __STDC_WANT_LIB_EXT1__을 1로 정의해야 사용할 수 있음이 보장됩니다.3

printf_s계 함수는 기존의 printf계 함수와 같지만, 다음 상황에 적절하게 예외 처리를 합니다.

특히 sprintf_ssnprintf처럼 출력하는 문자열 바로 뒤에 그 문자열의 크기를 추가로 받습니다.

scanf_s계 함수는 기존의 scanf계 함수와 같지만, 인자를 받는 방식이 다음과 같이 차이가 있고...

char string[16];
// 16글자 이상의 문자열이 들어오면 예외 처리를 합니다.
scanf_s("%s", string, 16);

...다음 상황에 적절하게 예외 처리를 합니다.

printf

입출력 위치

printf의 경우에는 fprintfsprintf 이외에도 snprintf라는 변종이 존재하고, 출력 문자열 바로 다음에 그 문자열의 크기를 추가로 받습니다.

어째서인지 snwprintf, vsnwprintf는 표준 라이브러리에서 지원하지 않습니다. 이 함수를 쓰고 싶다면 snwprintf_s를 하든지, 성공할 때까지 문자열 크기를 계속 늘리면서 swprintf를 대신 해야 합니다(cppreference에서 권장하는 방법). 이 두 함수 이외에는 가능한 모든 조합을 사용할 수 있습니다.

연습 문제

정리할 내용은 이것으로 끝입니다. 복습할 겸 연습문제를 풀어보는 건 어떨까요? 사실 이 부분은 급하게 마무리하느라 문제 종류가 다양하지 않고, 문제 추천은 언제나 감사히 받겠습니다.

  1. C 표준에서는 코드에서의 형태가 다르면 같은 포맷과 값으로 변환하지 않아도 된다고 명시하고 있습니다. 즉, 1.231.230, 123e-2, 123e-02 등이 서로 다른 값을 표현할 수도 있습니다. 

  2. "'scanf': 이 함수나 변수는 불안전할 수 있습니다. 대신 scanf_s 사용을 고려하세요. 비권장 경고를 해제하려면 _CRT_SECURE_NO_WARNINGS를 사용하세요. 자세한 사항은 온라인 도움말에서 확인할 수 있습니다." 

  3. 더 삼천포로 빠지자면, 사실 이 표준을 제대로 지키는 C 컴파일러/라이브러리가 거의 없습니다. 툭하면 scanf_s를 쓰라는 비주얼 스튜디오도 __STDC_LIB_EXT1__도 정의가 안 돼있고 표준 함수 중 하나인 set_constraint_handler_s도 없습니다. 

printf와 scanf 제대로 알고 가기