`printf`, `fprintf`, `sprintf`, `snprintf` 함수

printf, fprintf, sprintf, snprintf 함수

C에서 무언가를 출력할 때 가장 많이 쓰는 형식은 서식 지정(formatting)이에요. printf는 stdout에, fprintf는 지정한 파일 스트림에, sprintf/snprintf는 문자열 버퍼에 출력합니다. 겉보기엔 이름만 다른 것처럼 보이지만, 어디에 결과를 쓰느냐에 따라 동작과 위험이 확실히 달라져요.

출처: cppreference

본문

함수 원형

<stdio.h>에 정의돼 있어요.

int printf( const char* format, ... );                                   // (1)
int fprintf( FILE* stream, const char* format, ... );                    // (2)
int sprintf( char* buffer, const char* format, ... );                    // (3)
int snprintf( char* restrict buffer, size_t bufsz,
              const char* restrict format, ... );                        // (4)  (since C99)
int printf_s( const char* restrict format, ... );                        // (5)  (since C11)
int fprintf_s( FILE* restrict stream, const char* restrict format, ... );// (6)  (since C11)
int sprintf_s( char* restrict buffer, rsize_t bufsz,
               const char* restrict format, ... );                       // (7)  (since C11)
int snprintf_s( char* restrict buffer, rsize_t bufsz,
                const char* restrict format, ... );                      // (8)  (since C11)

설명 (Explanation)

주어진 위치에서 데이터를 읽어 문자 문자열로 변환한 뒤, 여러 목적지(sink/stream)에 결과를 써요.

  1. 결과를 출력 스트림 stdout에 써요.
  2. 결과를 출력 스트림 stream에 써요.
  3. 결과를 문자 문자열 버퍼 buffer에 써요. 쓰려는 문자열(종료 널 문자 포함)이 buffer가 가리키는 배열 크기를 넘으면 동작이 정의되지 않아요.
  4. 결과를 문자 문자열 버퍼 buffer에 써요. 최대 bufsz - 1 글자가 쓰이고, bufsz가 0이 아니면 결과 문자열은 널 문자로 끝나요. bufsz가 0이면 아무것도 쓰지 않고 buffer는 널 포인터여도 되지만, 반환값(널 종료 문자를 빼고 쓰였을 문자 수)은 여전히 계산해서 돌려줘요. 5-8) (1-4)와 같지만, 다음 오류들을 런타임에 감지해 현재 설치된 제약 처리 함수(constraint handler)를 호출해요.
  • format에 변환 지정자 %n이 있음
  • %s에 해당하는 인자 중 하나가 널 포인터
  • stream/format/buffer가 널 포인터
  • bufsz가 0이거나 RSIZE_MAX보다 큼
  • 문자열·문자 변환 지정자에서 인코딩 오류 발생
  • (sprintf_s만) buffer에 저장할 문자열(끝 널 포함)이 bufsz를 넘음

다른 경계 검사 함수와 마찬가지로, printf_s, fprintf_s, sprintf_s, snprintf_s는 구현이 __STDC_LIB_EXT1__을 정의하고, 사용자가 <stdio.h>를 포함하기 전에 __STDC_WANT_LIB_EXT1__을 정수 상수 1로 정의했을 때만 쓰는 게 보장돼요.

매개변수 (Parameters)

  • stream — 쓸 출력 파일 스트림
  • buffer — 쓸 문자 문자열을 가리키는 포인터
  • bufsz — 최대 bufsz - 1 글자를 쓸 수 있고, 거기에 널 종료 문자가 더해져요
  • format — 데이터를 어떻게 해석할지 지정하는 널 종료 바이트 문자열
  • ... — 출력할 데이터를 지정하는 인자들. 기본 인자 승격(default argument promotions) 후의 인자가 해당 변환 지정자가 기대하는 타입이 아니거나, format이 요구하는 것보다 인자가 적으면 동작이 정의되지 않아요. format이 요구하는 것보다 인자가 많으면 남는 인자들은 평가된 뒤 무시돼요.

형식 문자열 (Format string)

형식 문자열은 %를 제외한 보통 바이트 문자(그대로 출력에 복사됨)와 변환 지정자(conversion specification)로 이뤄져요. 각 변환 지정자는 다음 구조를 가져요.

  • 시작 % 문자
  • (선택) 변환 동작을 바꾸는 flags
    • -: 결과를 필드 안에서 왼쪽 정렬(기본은 오른쪽 정렬)
    • 0: 부동소수점·정수 변환에만 적용. 공백 대신 0으로 필드를 채워요. -가 있으면 무시되고, 정수 변환에서 정밀도가 명시되면 무시되며, 부동소수점에서 NaN이나 무한대면 무시돼요. 다른 변환에서 쓰면 동작이 정의되지 않아요.
    • +: 부동소수점·부호 있는 정수 변환에만 적용. 음이 아닌 수 앞에 +를 붙여요.
    • space: 부동소수점·부호 있는 정수 변환에만 적용. 음이 아닌 수 앞에 공백을 붙여요. +가 있으면 무시돼요.
    • #: 부동소수점·10진이 아닌 정수 변환에만 적용. 대체 형식으로 변환해요. 다른 변환에서 쓰면 동작이 정의되지 않아요.
  • (선택) 정수 값 또는 *: 최소 필드 폭 지정. 기본은 공백으로 채우고, 오른쪽 정렬일 때는 왼쪽에, 왼쪽 정렬일 때는 오른쪽에 채워요. *를 쓰면 폭을 추가 int 인자로 지정해요. 그 값이 음수면 -를 의미하고 최소 필드 폭으로는 절댓값을 써요.
  • (선택) . 뒤에 음이 아닌 정수 또는 *: 변환 정밀도 지정. *를 쓰면 정밀도를 추가 int 인자로 지정해요. 음수면 무시되고, 숫자도 *도 없으면 정밀도는 0으로 봐요.
  • (선택) 인자 크기를 지정하는 길이 수정자(length modifier)
  • 변환 형식 지정자

변환 지정자가 잘못되면 동작이 정의되지 않아요.

숫자가 아닌 변환 (Non-numeric)

  • %: % 문자를 출력. 인자가 없고 수정자를 지원하지 않으며, 완전한 변환 지정자는 %%여야 해요.
  • p: 포인터 출력. 인자는 void*, char*, signed char*, unsigned char*(C23부터)이고, 출력 형식은 구현이 정의해요.
  • c: 문자 출력. 인자 타입은 길이 수정자에 따라 달라요. (없음)intunsigned char로 변환해 출력. l이면 wint_twchar_t로 변환해 널 종료 배열에 넣고 %ls처럼 출력.
  • s: 문자열 출력. 인자는 문자 배열의 첫 요소를 가리키는 포인터. (없음)char*, signed char*, unsigned char*의 문자들을 출력. l이면 wchar_t*wcrtomb(0으로 초기화된 변환 상태)으로 좁은 문자열로 변환해 출력. %n과 정밀도에서의 문자 수는 변환된 넓은 문자열이 아니라 좁은 문자열 기준이에요.

정밀도는 최대 출력 문자 수를 지정해요. 최대에 도달하거나 널 종료자를 만날 때까지 문자를 복사하는데, 널 종료자는 %n이 보고하는 문자 수에 포함되지 않아요.

%cint 인자를 기대하지만, 가변 인자 함수에서 일어나는 정수 승격 덕에 char를 넘겨도 안전해요.

정수 변환 (Integer)

길이 수정자에 따라 부호 있는 타입 SInt와 부호 없는 타입 UInt가 결정돼요.

  • hh: signed char, unsigned char (C99부터)
  • h: short, unsigned short
  • (없음): int, unsigned
  • l: long, unsigned long
  • ll: long long, unsigned long long
  • j: intmax_t, uintmax_t
  • z: size_t의 부호 있는 버전, size_t
  • t: ptrdiff_t, ptrdiff_t의 부호 없는 버전 (C99부터)
  • wN: 비트 폭이 N인 모든 부호·무부호 정수 타입. wfN: int_fastN_t, uint_fastN_t (C23부터)

그 다음에 따라오는 지정자:

  • d/i: SInt를 10진으로 출력
  • u: UInt를 10진으로 출력
  • x/X: UInt를 16진으로 출력. 대체 형식은 인자가 0이 아니면 0x를 붙여요. 대문자면 출력도 대문자.
  • b: UInt를 2진으로 출력. 대체 형식은 인자가 0이 아니면 0b를 붙여요. (C23부터)
  • o: UInt를 8진으로 출력. 대체 형식은 첫 숫자가 0이 되도록 정밀도를 충분히 늘려요.
  • n: 지금까지 이 호출이 쓴 문자 수(최소 필드 폭 패딩 포함)를 반환. 결과를 SInt*에 써요. 수정자는 길이 수정자만 지원돼요.

정밀도는 최소 자릿수를 지정하고, 필요하면 앞에 0을 붙여요. 지정하지 않으면 정밀도 1을 써요. 정밀도가 0이고 인자가 0이면 숫자는 쓰지 않아요(+/space 플래그가 지정됐다면 그 문자는 여전히 써요).

고정 폭 정수 타입(int8_t 등)을 위한 올바른 변환 지정자는 <inttypes.h> 헤더에 정의돼 있어요(PRIdMAX, PRIuMAX 등은 %jd, %ju와 동의어).

메모리에 쓰는 변환 지정자 %n은 형식 문자열이 사용자 입력에 의존할 때 보안 공격의 흔한 표적이에요. 그래서 경계 검사를 하는 printf_s 계열 함수에서는 지원되지 않아요.

각 변환 지정자의 동작 뒤에는 순서점(sequence point)이 있어서, 하나의 변수에 여러 %n 결과를 저장하거나(경계 예로) 같은 호출 안에서 앞선 %n이 수정한 문자열을 출력하는 것이 허용돼요.

부동소수점 변환 (Floating-point)

길이 수정자에 따라 인자 타입이 달라져요.

  • (없음): double
  • l: double (C99부터)
  • L: long double
  • H: _Decimal32, D: _Decimal64, DD: _Decimal128 (C23부터)

인자가 무한대면 inf 또는 infinity를 써요. 어느 쪽을 쓸지는 구현이 정의해요. NaN이면 nan 또는 nan(char_sequence)를 써요. 이것도 구현이 정의해요.

그 외에는:

  • f/F: [-]ddd.ddd 형태의 10진 표기. 정밀도가 소수 자릿수를 지정하고, 필요하면 끝에 0을 붙여요. 지정하지 않으면 정밀도 6을 써요.
  • e/E: [-]d.ddde±dd 형태의 10진 지수 표기. 지수가 10보다 작으면 앞에 0을 붙여요. 정밀도 기본값은 6.
  • g/G: 인자와 정밀도에 따라 10진 또는 지수 표기를 선택해요. P = max(0, precision - 1)(정밀도 미지정 시 5)이고 EXP%e일 때의 지수라 하면, -4 ≤ EXP ≤ P이면 f/F 형식을 정밀도 P - EXP로, 아니면 e/E 형식을 정밀도 P로 써요.
  • a/A: [-]0xh.hhhp±d 형태의 16진 지수 표기. 정밀도가 소수점 이하 자릿수를 지정해요. (C99부터)

대체 형식은 정밀도가 0이어도 소수점을 써요. 형식 지정자가 대문자면 출력도 대문자예요.

반환값 (Return value)

1,2) 출력 스트림으로 전달된 문자 수를 돌려주고, 출력 오류나(문자열·문자 변환 지정자의) 인코딩 오류가 있었다면 음수 값을 돌려줘요. 3) buffer에 쓴 문자 수(종료 널 문자 제외)를 돌려주고, 인코딩 오류가 있었다면 음수 값을 돌려줘요. 4) bufsz를 무시했다면 buffer에 쓰였을 문자 수(종료 널 제외)를 돌려주고, 인코딩 오류가 있었다면 음수 값을 돌려줘요. 5,6) 출력 스트림으로 전달된 문자 수를 돌려주고, 출력 오류·런타임 제약 위반·인코딩 오류가 있었다면 음수 값을 돌려줘요. 7) buffer에 쓴 문자 수(널 문자 제외. buffer가 널이 아니고 bufsz가 0도 RSIZE_MAX보다 크지도 않다면 항상 널이 쓰여요)를 돌려주고, 런타임 제약 위반 시 0, 인코딩 오류 시 음수 값을 돌려줘요. 8) bufsz를 무시했다면 buffer에 쓰였을 문자 수(종료 널 제외)를 돌려주고, 런타임 제약 위반이나 인코딩 오류가 있었다면 음수 값을 돌려줘요.

주의할 점 (Notes)

C 표준과 POSIX는 인자가 목적지 버퍼와 겹치면 sprintf 및 그 변형의 동작을 정의되지 않은 것으로 규정해요. 예시:

sprintf(dst, "%s and %s", dst, t); // <- broken: undefined behavior

POSIX는 오류 시 errno가 설정된다고 규정하고, 추가 변환 지정자(특히 % 바로 뒤의 n$로 인자 순서를 재배치하는 기능)를 정의해요.

snprintfbufsz 0과 버퍼 널 포인터로 호출하면 출력을 담을 버퍼 크기를 알아내는 데 유용해요.

const char fmt[] = "sqrt(2) = %f";
int sz = snprintf(NULL, 0, fmt, sqrt(2));
char buf[sz + 1]; // note +1 for terminating null byte
snprintf(buf, sizeof buf, fmt, sqrt(2));

snprintf_ssnprintf처럼 bufsz - 1에 맞게 출력을 잘라내지만, sprintf_s는 그렇지 않아요.

예제 (Example)

다양한 변환 지정자를 한눈에 보는 예시예요.

#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>

int main(void)
{
    const char* s = "Hello";
    printf("Strings:\n"); // same as puts("Strings");
    printf(" padding:\n");
    printf("\t[%10s]\n", s);
    printf("\t[%-10s]\n", s);
    printf("\t[%*s]\n", 10, s);
    printf(" truncating:\n");
    printf("\t%.4s\n", s);
    printf("\t%.*s\n", 3, s);
    printf("Characters:\t%c %%\n", 'A');

    printf("Integers:\n");
    printf("\tDecimal:\t%i %d %.6i %i %.0i %+i %i\n",
                         1, 2,   3, 0,   0,  4,-4);
    printf("\tHexadecimal:\t%x %x %X %#x\n", 5, 10, 10, 6);
    printf("\tOctal:\t\t%o %#o %#o\n", 10, 10, 4);

    printf("Floating-point:\n");
    printf("\tRounding:\t%f %.0f %.32f\n", 1.5, 1.5, 1.3);
    printf("\tPadding:\t%05.2f %.2f %5.2f\n", 1.5, 1.5, 1.5);
    printf("\tScientific:\t%E %e\n", 1.5, 1.5);
    printf("\tHexadecimal:\t%a %A\n", 1.5, 1.5);
    printf("\tSpecial values:\t0/0=%g 1/0=%g\n", 0.0 / 0.0, 1.0 / 0.0);

    printf("Fixed-width types:\n");
    printf("\tLargest 32-bit value is %" PRIu32 " or %#" PRIx32 "\n",
                                     UINT32_MAX,     UINT32_MAX );
}

가능한 출력:

Strings:
 padding:
        [     Hello]
        [Hello     ]
        [     Hello]
 truncating:
        Hell
        Hel
Characters:     A %
Integers:
        Decimal:        1 2 000003 0  +4 -4
        Hexadecimal:    5 a A 0x6
        Octal:          12 012 04
Floating-point:
        Rounding:       1.500000 2 1.30000000000000004440892098500626
        Padding:        01.50 1.50  1.50
        Scientific:     1.500000E+00 1.500000e+00
        Hexadecimal:    0x1.8p+0 0X1.8P+0
        Special values: 0/0=-nan 1/0=inf
Fixed-width types:
        Largest 32-bit value is 4294967295 or 0xffffffff

같이 보기 (See also)

  • vprintf/vfprintf/vsprintf/vsnprintf — 가변 인자 목록을 쓰는 서식 출력
  • fputs — 파일 스트림에 문자 문자열 쓰기
  • scanf/fscanf/sscanf — 서식 입력