`scanf`, `fscanf`, `sscanf` 함수

scanf, fscanf, sscanf 함수

값을 입력받을 때, 단순히 문자나 문자열을 읽는 걸로는 부족해요. "정수 하나, 실수 하나, 문자열 하나"처럼 타입별로 파싱하고 싶잖아요. 그걸 도와주는 게 scanf 계열 함수예요. scanf는 stdin에서, fscanf는 파일 스트림에서, sscanf는 문자열 버퍼에서 서식에 맞춰 읽어 들이죠.

출처: cppreference

본문

함수 원형

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

int scanf( const char* format, ... );    // (1)
int fscanf( FILE* stream, const char* format, ... );   // (2)
int sscanf( const char* buffer, const char* format, ... ); // (3)
int scanf_s(const char* restrict format, ...);   // (4)  (since C11)
int fscanf_s(FILE* restrict stream, const char* restrict format, ...); // (5)  (since C11)
int sscanf_s(const char* restrict buffer, const char* restrict format, ...); // (6)  (since C11)

설명 (Explanation)

여러 원천에서 데이터를 읽어 format에 따라 해석하고, 결과를 주어진 위치에 저장해요.

  1. stdin에서 데이터를 읽어요.
  2. 파일 스트림 stream에서 데이터를 읽어요.
  3. 널 종료 문자열 buffer에서 데이터를 읽어요. 문자열 끝에 도달하는 것은 fscanf의 파일 끝 조건에 도달하는 것과 같아요. 4-6) (1-3)과 같지만, %c, %s, %[ 변환 지정자가 각각 두 개의 인자(보통 포인터와, 받는 배열의 크기를 나타내는 rsize_t 값 — %c로 단일 char에 읽을 때는 1이 될 수 있음)를 기대한다는 점이 달라요. 또한 다음 오류들을 런타임에 감지해 현재 설치된 제약 처리 함수(constraint handler)를 호출해요.
  • 포인터 타입 인자 중 하나가 널 포인터
  • format, stream, buffer가 널 포인터
  • %c, %s, %[가 썼을 문자 수에 종료 널 문자를 더한 값이, 각 변환 지정자에 준 두 번째(rsize_t) 인자를 넘음
  • (선택) 알 수 없는 변환 지정자 같은 그 외 감지 가능한 오류

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

매개변수 (Parameters)

  • stream — 읽어올 입력 파일 스트림
  • buffer — 읽어올 널 종료 문자열을 가리키는 포인터
  • format — 입력을 어떻게 읽을지 지정하는 널 종료 문자열
  • ... — 결과를 받는 인자들

형식 문자열 (Format string)

형식 문자열은 다음 요소들로 이뤄져요.

  • %가 아닌 공백이 아닌 멀티바이트 문자: 각 문자는 입력에서 정확히 같은 문자 하나를 소비해요. 다음 오는 문자가 같지 않으면 함수는 실패해요.
  • 공백 문자: 형식 문자열의 공백 문자 하나는 입력의 연속된 모든 공백 문자를 소비해요(isspace를 반복 호출하는 것처럼 판단). 여기서 "\n", " ", "\t\t"는 구분이 없어요.
  • 변환 지정자. 각 변환 지정자는 다음 구조를 가져요.
    • 시작 % 문자
    • (선택) 할당 억제 문자 *. 있으면 이 변환의 결과를 어떤 받는 인자에도 저장하지 않아요.
    • (선택) 0보다 큰 정수: 최대 필드 폭. 현재 변환 지정자가 소비할 수 있는 최대 문자 수를 제한해요. %s%[는 폭을 주지 않으면 버퍼 오버플로로 이어질 수 있어요.
    • (선택) 받는 인자 크기를 지정하는 길이 수정자. 변환 정확도와 오버플로 규칙에 영향. 기본 목적지 타입은 변환 타입마다 달라요.
    • 변환 형식 지정자

변환 지정자 표

지정자 설명 기본 목적지 타입 (길이 수정자에 따라 달라짐)
% 리터럴 %와 일치 해당 없음
c 문자 또는 문자 시퀀스와 일치. 폭 지정 시 정확히 폭만큼 일치. %s/%[와 달리 배열에 널 문자를 붙이지 않아요 char* / (l)wchar_t*
s 공백이 아닌 문자들의 시퀀스(문자열)와 일치. 폭 지정 시 폭까지 또는 첫 공백까지. 일치한 문자 외에 항상 널 문자도 저장 char* / (l)wchar_t*
[set] set에 포함된 문자들의 비어있지 않은 시퀀스와 일치. 첫 문자가 ^면 set에 없는 문자들과 일치. set이 ]^]로 시작하면 ]도 set에 포함. -가 비초기 위치에서 범위를 나타낼 수 있는지는 구현이 정의. 항상 널 문자도 저장 char* / (l)wchar_t*
d 10진 정수와 일치. strtol에 base 10으로 기대하는 형식과 같음 int* 계열
b (C23) 부호 없는 2진 정수와 일치. strtoul에 base 2로 기대하는 형식과 같음 unsigned int* 계열
i 정수와 일치. strtol에 base 0으로 기대하는 형식과 같음(첫 문자들로 base 결정) int* 계열
u 부호 없는 10진 정수와 일치. strtoul에 base 10 unsigned* 계열
o 부호 없는 8진 정수와 일치. strtoul에 base 8 unsigned* 계열
x/X 부호 없는 16진 정수와 일치. strtoul에 base 16 unsigned* 계열
n 지금까지 읽은 문자 수를 반환. 입력을 소비하지 않고 할당 카운트도 늘리지 않아요. *(할당 억제)가 있으면 동작이 정의되지 않아요 int* 계열
a/A/e/E/f/F/g/G 부동소수점 숫자와 일치. strtof가 기대하는 형식과 같음 float* / (l)double* / (L)long double*
p 포인터를 정의하는 구현 정의 문자 시퀀스와 일치. printf 계열이 %p로 만들어 낸 것과 같은 시퀀스여야 함 void**

길이 수정자(hh, h, l, ll, j, z, t, L)에 따라 위의 목적지 타입이 signed/unsigned char*, short*, long*, long long*, intmax_t*/uintmax_t*, size_t*, ptrdiff_t*, long double* 등으로 바뀌어요. 그중 일부는 C99부터 사용 가능해요.

동작 규칙 (Notes)

n을 제외한 모든 변환 지정자는, 지정된 필드 폭을 넘지 않으면서 변환 지정자가 기대하는 것과 정확히 일치하거나 그 접두어인 입력 문자들의 가장 긴 시퀀스를 스트림에서 소비해요. 소비된 시퀀스 뒤의 첫 문자는 읽히지 않은 채 남아요. 소비된 시퀀스의 길이가 0이거나, 변환할 수 없으면 일치 실패(matching failure)가 발생해요. 단 파일 끝·인코딩 오류·읽기 오류로 입력이 막혔다면 입력 실패(input failure)예요.

%를 제외하면, 입력 항목(또는 %n의 경우 입력 문자 수)은 먼저 변환 지정자에 맞는 타입으로 변환돼요. *로 할당이 억제되지 않았다면, 변환 결과는 아직 쓰이지 않은 다음 인자가 가리키는 객체에 기록돼요. 객체 타입이 맞지 않거나 결과가 객체에 표현될 수 없으면 동작이 정의되지 않아요.

[, c, n을 제외한 모든 변환 지정자는 입력을 파싱하기 전에 앞의 연속된 공백 문자를 모두 소비하고 버려요(isspace 호출처럼 판단). 이 소비된 문자들은 최대 필드 폭에 세지 않아요.

lc, ls, l[ 변환 지정자는 첫 문자가 변환되기 전에 0으로 초기화된 mbstate_t 객체로 mbrtowc를 호출하는 것처럼 멀티바이트→넓은 문자 변환을 수행해요.

s[ 변환 지정자는 일치한 문자 외에 항상 널 종료자를 저장해요. 목적지 배열의 크기는 지정된 필드 폭보다 최소 1은 커야 해요. 목적지 배열 크기를 지정하지 않고 %s%[를 쓰는 것은 gets만큼이나 안전하지 않아요.

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

각 변환 지정자의 동작 뒤에는 순서점이 있어서, 같은 "sink" 변수에 여러 필드를 저장하는 것이 허용돼요.

지수가 있는데 숫자가 없는 불완전한 부동소수점 값(예: %f"100er" 파싱)을 파싱할 때는, 아마 유효한 부동소수점 숫자의 가장 긴 접두어인 "100e"가 소비되고 일치 오류가 나면서 "r"이 남아요. 일부 구현(예: glibc bug 1765)은 이 규칙을 따르지 않고 "100"만 소비하고 "er"를 남기기도 해요.

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

반환값 (Return value)

1-3) 성공적으로 할당된 받는 인자의 수를 돌려줘요(첫 받는 인자 할당 전에 일치 실패가 발생하면 0일 수 있어요). 첫 받는 인자 할당 전에 입력 실패가 발생하면 EOF를 돌려줘요. 4-6) (1-3)과 같지만, 런타임 제약 위반이 있으면 EOF도 돌려줘요.

복잡도 (Complexity)

보장되지 않아요. 특히 일부 sscanf 구현은 O(N)이에요(N = strlen(buffer)).

주의할 점 (Notes)

대부분의 변환 지정자가 먼저 연속된 모든 공백을 소비하기 때문에, 다음과 같은 코드는 서로 다른 줄에 입력된 두 정수(두 번째 %d가 첫 번째가 남긴 개행을 소비)나, 공백·탭으로 구분된 같은 줄의 두 정수 모두 정상적으로 읽어요.

scanf("%d", &a);
scanf("%d", &b);

앞 공백을 소비하지 않는 %c 같은 변환 지정자는 형식 문자열에 공백 문자를 넣어 그렇게 만들 수 있어요.

scanf("%d", &a);
scanf(" %c", &c); // consume all consecutive whitespace after %d, then read a char

예제 (Example)

sscanf로 다양한 지정자를 한 번에 써 보는 예시예요.

#define __STDC_WANT_LIB_EXT1__ 1
#include <locale.h>
#include <stddef.h>
#include <stdio.h>

int main(void)
{
    int i, j;
    float x, y;
    char str1[10], str2[4];
    wchar_t warr[2];

    setlocale(LC_ALL, "en_US.utf8");

    char input[] = "25 54.32E-1 Thompson 56789 0123 56ß水";
    /* parse as follows:
       %d: an integer
       %f: a floating-point value
       %9s: a string of at most 9 non-whitespace characters
       %2d: two-digit integer (digits 5 and 6)
       %f:  a floating-point value (digits 7, 8, 9)
       %*d: an integer which isn't stored anywhere
       ' ': all consecutive whitespace
       %3[0-9]: a string of at most 3 decimal digits (digits 5 and 6)
       %2lc: two wide characters, using multibyte to wide conversion  */
    int ret = sscanf(input, "%d%f%9s%2d%f%*d %3[0-9]%2lc",
                     &i, &x, str1, &j, &y, str2, warr);

    printf("Converted %d fields:\n"
           "i = %d\n"
           "x = %f\n"
           "str1 = %s\n"
           "j = %d\n"
           "y = %f\n"
           "str2 = %s\n"
           "warr[0] = U+%x\n"
           "warr[1] = U+%x\n",
           ret, i, x, str1, j, y, str2, warr[0], warr[1]);

#ifdef __STDC_LIB_EXT1__
    int n = sscanf_s(input, "%d%f%s", &i, &x, str1, (rsize_t)sizeof str1);
    // writes 25 to i, 5.432 to x, the 9 bytes "Thompson\0" to str1, and 3 to n.
#endif
}

가능한 출력:

Converted 7 fields:
i = 25
x = 5.432000
str1 = Thompson
j = 56
y = 789.000000
str2 = 56
warr[0] = U+df
warr[1] = U+6c34

같이 보기 (See also)

  • vscanf/vfscanf/vsscanf — 가변 인자 목록을 쓰는 서식 입력
  • fgets — 파일 스트림에서 문자열 가져오기
  • printf/fprintf/sprintf/snprintf — 서식 출력