본문 바로가기

프로그래밍/C, C++

scanf, sscanf, fscanf는 어떤 차이일까?

지난번에 포스팅한 C언어의 출력 함수들에 이어서 이번에는 입력 함수들에 대해 포스팅해 보겠다.

2018/11/12 - [컴퓨터 공학/C,C++] - printf, fprintf, sprintf는 어떤 차이일까?

 

printf, fprintf, sprintf는 어떤 차이일까?

C/C++ 에서는 여러가지 출력 방법이 있다. C언어의 printf부터 C++의 cin까지 다양한 함수, 객체와 메소드가 있지만 그 중 서식화된 출력에 유용하게 사용할 수 있었던 여러가지 함수를 소개해 보려 ��

haruhiism.tistory.com

사실 이런 함수들은 포맷 스트링 등에서 큰 차이가 없고 입출력의 대상이 어딘가에 따라 차이가 있으므로 한 함수에 대해 잘 기억하고 있다면 다른 함수를 봐도 매개변수 등을 통해 그 용도를 쉽게 추측할 수 있다.

- scanf()

C언어의 시작부터 함께했던 이 함수는 기본적으로 표준 입력 스트림(stdin)에서 사용자 입력을 받아 포맷 스트링에 따라 각 변수에 저장하는 지극히 단순하면서도 유용한 기능을 수행하고 있다. 표준 입출력 라이브러리(stdio.h)를 인클루드하여 사용할 수 있으며 반환값으로 읽은 값의 수, 정확히 말하면 변환되고 지정된 필드 수를 제공한다.

int scanf(const char *format-string, argument-list);

표준 입력 스트림에서 매개변수 리스트에 지정된 각 항목의 위치로 데이터를 읽어들여 저장한다. 데이터가 저장될 곳은 포맷 스트링에 의해 지정된 양식에 맞는 자료형의 변수의 포인터여야 한다. 이렇게 말하면 복잡해보이지만 위에서 말한대로 그냥 (대부분의 경우)사용자의 키보드 입력에서 받은 데이터를 포맷 스트링대로 분리해서 각 변수에 저장해주는 함수다. 대부분의 경우라는 게 어떤 의미냐면 리눅스에서 값을 표준 입력으로 건네줄 수 있는 파이프를 활용할 수도 있기 때문이다. 아래와 같은 예를 보자.

#include <stdio.h>

int main()
{
        char string[30];
        scanf("%s", string);
        printf("%s\n", string);

        return 0;
}

위의 프로그램에서는 단순히 표준 입력에서 문자열(공백 제외)을 받아 표준 출력으로 출력하고 있다. 따라서 이를 실행한다면 다음과 같이 동작할 것이다.

keyboard input

하지만 쉘의 파이프를 통해 값을 전달해주면 사용자가 직접 키보드에 입력하지 않고도 값을 입력할 수 있다.

pipe input

이는 간단히 설명하자면 "Hello!" 라는 문자열을 터미널에 출력시키고 이를 input 프로그램의 입력으로 전달하라는 명령이다. 여기서 이를 더 설명하면 리눅스의 쉘과 파일 디스크립터 등에 대해서 다루어야 하기 때문에 주제에 어긋나 생략하지만 이렇듯 꼭 사용자의 키보드 입력으로부터 데이터를 전달받아야 하는 것은 아니라는 것이다.

프로그램끼리 입출력을 주고받는다

아무튼 다시 본론으로 돌아가서, scanf()에서는 포맷 스트링을 왼쪽에서 오른쪽으로 순서대로 읽으면서 매개변수 리스트에 순서대로 저장한다. 그래서 사용자 입력 데이터가 순서에 어긋날 경우(단추가 잘못 끼워지는 것처럼) 각 데이터들은 다른 변수에 저장된다. 또한 포맷 스트링에서 올바르지 못한 형식 스펙(아래의 표에 존재하지 않는 형식)을 요구할 경우 이를 읽긴 하지만 실제로 값에 저장하지는 않는다. 이것을 보이면 다음과 같다.

존재하지 않는 자료형 형식 사용

#include <stdio.h>

int main()
{
        char string[30];
        int a, b;
        int n=123;
        scanf("%s %d %d %Y", string, &a, &b, &n);
        printf("%s %d %d %d\n", string, a, b, n);

        return 0;
}

위의 scanf 함수의 포맷 스트링을 보면 '%Y'라는 존재하지 않는 자료형 형식을 지정하고 있다. 이를 실행시켜보면 값이 받아들여지지 않은 것을 알 수 있다. 어떤 자료형으로 저장해야 할 지 모르기 때문에 값을 읽었지만 무시했다는 것을 알 수 있다. 비교를 위해 저장해둔 4번째 변수 n의 초기값 123을 그대로 가지고 있다는 점에서 이를 확인할 수 있다.

output of input2

그런데 이 무시된 값은 어디로 갈까? 이는 버려지는 것이 아니라 표준 입력의 버퍼에 그대로 남아있다. 즉, 다음 scanf() 함수 호출 시 자동적으로 이를 받아들이게 되는데 이는 대부분 의도하지 않은 행위기 때문에 버그의 주 원인이 된다. 이를 보이면 다음과 같다.

#include <stdio.h>

int main()
{
        char string[30];
        int a, b;
        int n=123;
        scanf("%s %d %d %Y", string, &a, &b, &n);
        scanf("%s", string);
        printf("%s %d %d %d\n", string, a, b, n);

        return 0;
}

먼저 위처럼 올바르지 않은 포맷 스트링을 사용하고 '%s'로 대부분의 키보드 입력을 문자열로 받아들일 수 있는 구문을 추가한다. 만약 첫번째 scanf()에서 누락된 값이 있다면 이는 두번째 scanf()가 받아들일 것이며 그에 따라 string 문자열이 변화할 것이므로 누락된 값이 어떤 영향을 끼치는지 알 수 있다.

output of input3

위의 예시에서 볼 수 있듯이 'hello 1 2 3'에서 '%Y'에 의해 읽히지 못한 4번째 매개변수 3이 버퍼에 남아있다가 두번째 scanf()에 의해 읽혀 string에 저장되기 때문에 최종적으로 출력이 'hello 1 2 123'이 아닌 '3 1 2 123'이 되는 것을 볼 수 있다. 그렇기 때문에 scanf()를 사용할때는 버퍼에 남은 값을 제거하는 데 유의해야 한다.

누락된 자료형 형식 지정

그렇다면 포맷 스트링에서 자료형 형식이 누락되어 포맷 스트링과 짝을 이루지 못하는 변수가 있다면 어떨까?

#include <stdio.h>

int main()
{
        char string[30];
        int a, b;
        int n=123;
        scanf("%s %d %d", string, &a, &b, &n);
        printf("%s %d %d %d\n", string, a, b, n);

        return 0;
}

output of input2

역시 상응하는 포맷이 존재하지 않는 변수에 대해서는 입력값을 전달해줘도 무시된다. 위의 경우와 마찬가지로 해당 값이 표준 입력에서 읽히지 않고 버퍼에 남아있기 때문에 나중에 입력 함수를 호출할 때 이 남아있는 입력을 자동적으로 받아들이게 된다.

scanf()를 한번 더 넣어서 확인한 결과

형식 스펙

그렇다면 이런 '%d', '%s' 같은 형식 스펙들은 어떻게 정의될까? IBM Knowledge Center를 참고하면 다음과 같다.

% (*) (width) (h / L / l / ll / H / D / -DD- /) type
  • * : 입력 필드 지정의 생략. 값을 읽어들이지만 무시한다
  • width : 읽혀질 최대 문자의 개수 지정
  • h / L / l / ll / H / D / -DD- : 수신 오브젝트의 크기 지정
  • type : d, f, lf, s 등 자료형 타입 지정

입력값의 길이를 조정할 수 있는 width 필드는 다음과 같이 작동한다(예시로 %5d를 사용).

scanf("%s", string)을 추가한 결과

입력값을 주어진 크기만큼 자르고 나머지는 입력 버퍼에 남겨두는 것을 볼 수 있다.

마지막의 'type' 필드는 입력값을 포맷 스트링에 의해 지정된 해당 변수에 어떤 자료형으로 변환하여 저장할지 결정한다.

왼쪽 컬럼부터 문자, 요구되는 데이터, 저장될 인수 유형에 해당한다.

d 부호가 있는 십진 정수 정수에 대한 포인터
o 부호가 없는 8진 정수 부호가 없는 정수에 대한 포인터.
x, X 부호가 없는 16진 정수 부호가 없는 정수에 대한 포인터.
i 십진, 16진 또는 8진 정수 정수에 대한 포인터
u 부호가 없는 십진 정수 부호가 없는 정수에 대한 포인터
a, A, e, E, f, F, g, G 십진/십진이 아닌 부동 소수점 부동 소수점에 대한 포인터.
c 문자: c가 지정되면 기본적으로 건너뛴 공백 문자가 읽힙니다. 입력 필드에 충분한 문자에 대한 포인터.
s 스트링 입력 필드 및 끝 널 문자에 충분한 문자 배열에 대한 포인터(\0)로, 자동으로 추가됩니다.
n stream 또는 버퍼에서 읽힌 입력이 없습니다. 정수에 대한 포인터로, scanf()에 대한 호출에서 해당 포인트까지 버퍼나 stream에서 읽힌 문자의 수를 저장합니다.
p 일련의 문자로 변환된 void에 대한 포인터 void에 대한 포인터.

자주 사용되는 타입에 대해 간추리면 위와 같다. 특이한 타입으로 '%n'이 있는데 이는 사용자의 입력에서 값을 읽어들이는 것이 아니라 함수에서 읽은 바이트의 갯수를 해당 변수에 저장하는, scanf() 유일의 출력 기능을 가진 타입이다.

#include <stdio.h>

int main()
{
        char string[30];
        int a, b;
        int n=123;
        scanf("%s %d %d %n", string, &a, &b, &n);
        printf("%s %d %d %d\n", string, a, b, n);

        return 0;
}

아까처럼 코드를 구성하고, 이번엔 n 변수에 '%n' 형식을 지정해보자. 이를 실행시켜보면 다음과 같은 결과를 볼 수 있다.

output of input4

이전과는 달리 변수 n에 10이란 값이 들어간 것을 볼 수 있다. 이는 scanf()에서 입력값 'hello 1 2 ', 공백포함 총 10바이트를 읽었다는 것을 의미한다. 유의할 것은 scanf()가 포맷 스트링 '%s %d %d '에 따라 읽기 때문에 해당 포맷을 벗어난 데이터는 얼마가 됐든 읽지 않는다는 것이다.

4번째 매개변수인 '3'은 위의 예제처럼 여전히 무시되어 버퍼에 남아있게 된다.

기타로 '$' 등을 사용하여 입력 데이터들이 저장될 인수들의 위치를 직접 지정할 수 있다(IBM Knowledge Center 참고).

- sscanf()

자칫 오타로 착각할수도 있는 이 함수는 scanf()와 동일하지만 입력 대상이 표준 입력이 아닌 매개변수로 전달되는 문자열 버퍼라는 차이가 있다. 잘 이해가 안될수도 있지만 다음과 같은 함수 시그니처를 보자.

int sscanf(const char *buffer, const char *format, argument-list);

이전 scanf()와는 달리 첫번째 매개변수에 const char* 형 buffer가 추가되었는데 이것이 우리가 입력한 문자열이라고 생각하면 된다. 그래서 이 '입력된 문자열'에서 sscanf() 함수가 혼자 알아서 포맷 스트링에 맞게 데이터를 읽어서 인수들에 저장하는 것이다. 확실하게 이해하기 위해 아래 예제를 보자.

#include <stdio.h>

int main()
{
        const char* string =  "Hello! 101 202 303";
        char msg[30];
        int number1, number2, number3;

        sscanf(string, "%s %d %d %d", msg, &number1, &number2, &number3);
        printf("%s %d %d %d\n", msg, number1, number2, number3);

        return 0;

}

이전에서 터미널에 직접 입력했던 "Hello! 101 202 303" 같은 데이터들이 프로그램 내부에 문자열로 존재하고 있다. 이를 sscanf()로 포맷에 맞게 읽어서 해당 변수에 각각 저장하는 것을 볼 수 있다. 그리고 이들을 다시 포맷대로 출력시켜보면 기존 데이터와 동일하게 출력되는 것을 볼 수 있다.

이는 프로그램 내부에서 생성된 문자열이나 파일에서 읽은 문자열을 내부 변수에 저장한 후 분리해야 하는 그런 경우에 유용할 것이다. 이를 위해서는 포맷 스트링을 정교하게 작성해야 하며 위의 scanf() 내용에서 설명한 것과 동일한 구조를 가지고 있다. 반환값도 동일하게 리턴값으로 읽어들인 필드 수를 리턴한다.

- fscanf()

이 함수도 sscanf()와 비슷하게 입력 데이터를 읽어들이는 부분이 다르다. 'f' 라는 단어에서 볼 수 있듯이 파일(file), 정확히는 파일 스트림에서 포맷스트링에 맞게 데이터를 읽어들인다.

int fscanf (FILE *stream, const char *format-string, argument-list);

파일 스트림의 현재 위치에서 포맷 스트링에 맞게 데이터를 읽어 인수에 저장하는 함수로 다른 함수들과 큰 차이점이 없으므로 다음과 같은 예제를 보면 바로 이해할 수 있을 것이다.

#include <stdio.h>

int main()
{
        char name[30];
        char id[30];
        float value;

        FILE *stream;
        stream = fopen("read_file", "r");

        while(fscanf(stream, "%s %s %f", name, id, &value) > 0){
                printf("User name: %s\nUser ID: %s\nUser Value: %f\n\n", name, id, value);
        }

        return 0;
}

읽을 파일인 read_file에는 다음과 같은 데이터가 담겨있다.

Kwonkyu 202007221701 1.2354
Jason 202005131332 3.1542

이를 실행시키면 다음과 같이 데이터를 포맷대로 읽어서 변수에 저장, 출력하는 것을 볼 수 있다.

 

[출처 | https://www.ibm.com/support/knowledgecenter/ko/ssw_ibm_i_73/rtref/scanf.htm]

[출처 | https://www.ibm.com/support/knowledgecenter/ko/ssw_ibm_i_73/rtref/sscanf.htm]

[출처 | https://www.ibm.com/support/knowledgecenter/ko/ssw_ibm_i_73/rtref/fscanf.htm]