85.20 입출력 버퍼 활용하기

C 언어의 입출력 함수들은 내부적으로 입출력 버퍼를 사용하여 데이터를 처리합니다. 입출력 버퍼는 겉으로 드러나지 않으며 다음과 같이 함수를 사용할 때 선언하는 변수 buffer는 입출력 버퍼가 아닙니다. 프로그래머 입장에서 데이터를 임시로 저장한다고 해서 버퍼라고 이름을 지은 것이죠.

char buffer[100];
gets_s(buffer, sizeof(buffer));

표준 입력(키보드)은 입력되는 문자를 입력 버퍼에 저장했다가 엔터 키(\n)가 입력되면 지정된 변수(배열, 할당한 메모리)로 옮깁니다(버퍼가 비워짐). 마찬가지로 표준 출력(화면)은 출력 버퍼에 문자가 저장되었다가 특정 조건에 의해 버퍼가 비워지면 화면에 출력됩니다(입출력 버퍼가 비워지는 시점은 운영체제나 설정에 따라 달라집니다).

즉, 프로그램과 운영체제는 데이터를 효율적으로 처리하기 위해 입출력 버퍼를 사용합니다. 만약 입출력 버퍼 없이 데이터를 1바이트씩 처리하면 입출력이 될 때마다 매번 데이터를 처리해야 되므로 CPU 사용 횟수와 메모리 접근 횟수도 많아집니다. 하지만 입출력 버퍼를 사용하면 일정 크기만큼 데이터를 모아두었다가 처리하므로 그만큼 CPU를 덜 사용하고 메모리 접근 횟수도 줄어듭니다.

이처럼 입출력 버퍼에 데이터를 저장하는 행동을 버퍼링(buffering)이라고 부릅니다.

이제 입출력 버퍼가 동작하는 모양을 살펴보겠습니다. 다음은 출력 버퍼의 크기를 설정한 뒤 printf로 문자열을 출력합니다.

  • setvbuf(파일포인터, 사용자지정입출력버퍼, 모드, 크기);
    • int setvbuf(FILE *_Stream, char *_Buffer, int _Mode, size_t _Size);
    • 설정 변경에 성공하면 0을 반환, 실패하면 0이 아닌 값을 반환

output_buffer.c

#include <stdio.h>
#include <time.h>

void delay(unsigned int sec)     // 특정 시간(초)만큼 기다리는 함수
{
    clock_t ticks1 = clock();
    clock_t ticks2 = ticks1;
    while ((ticks2 / CLOCKS_PER_SEC - ticks1 / CLOCKS_PER_SEC) < (clock_t)sec)
        ticks2 = clock();
}

int main()
{
    setvbuf(stdout, NULL, _IOFBF, 10);    // 출력 버퍼의 크기를 10으로 설정

    printf("Hello, world!\n");

    delay(3);    // 3초간 기다림
    
    return 0;
}

소스를 컴파일 한 뒤 실행해보면 다음 같이 "Hello, wor"까지만 나옵니다.

실행 결과

Hello, wor

여기서 3초가 지나면 남은 "ld!"까지 모두 출력됩니다.

실행 결과

Hello, world!

이렇게 출력이 되는 이유는 setvbuf(stdout, NULL, _IOFBF, 10);와 같이 표준 출력 stdout의 출력 버퍼를 10으로 설정했기 때문입니다. 따라서 printf("Hello, world!\n");로 출력해도 버퍼 크기 10만큼 "Hello, wor"가 출력되고 3초가 지난 뒤 버퍼가 비워져서 "ld!"까지 모두 출력됩니다(Visual Studio, Windows).

setvbuf 함수의 첫 번째 인수에는 입출력 버퍼의 설정을 변경할 파일 포인터(파일 스트림)을 넣어줍니다. stdin, stdout, stderr도 파일 포인터이므로 그대로 넣으면 됩니다. 두 번째 인수에는 입출력 버퍼로 사용할 배열(메모리)을 넣는데 NULL을 지정하면 내부적으로 버퍼 공간을 생성합니다. 세 번째 인수는 버퍼링 모드이고, 네 번째 인수는 입출력 버퍼 크기입니다.

  • _IOFBF: Full buffering, 버퍼가 가득 차면 버퍼를 비웁니다(fflush로 비울 수도 있음).
  • _IOLBF: Line buffering, \n을 만나거나 버퍼가 가득 차면 버퍼를 비웁니다(fflush로 비울 수도 있음).
  • _IONOBUF: No buffering, 버퍼를 사용하지 않습니다. 입출력 버퍼로 사용할 배열(메모리)와 크기는 무시됩니다.

이번 예제에서는 출력 버퍼가 비워지는 시점을 조절하기 위해 특정 시간(초)만큼 기다리는 함수 delay를 만들어서 사용했습니다. 실제로는 입출력 버퍼가 비워지는 시점을 조절할 상황은 많지 않을 것입니다.

출력 버퍼를 강제로 비우려면 다음과 같이 fflush를 사용하면 됩니다.

  • fflush(파일포인터);
    • int fflush(FILE *_Stream);
    • 성공하면 0을 반환, 실패하면 EOF(-1)를 반환

flush_output_buffer.c

setvbuf(stdout, NULL, _IOFBF, 10);     // 출력 버퍼의 크기를 10으로 설정

printf("Hello, world!\n");
fflush(stdout);    // 표준 출력의 출력 버퍼를 강제로 비움

delay(3);    // 3초간 기다림

실행 결과

Hello, world!

앞에서는 3초가 지난 뒤에 "Hello, world!"가 모두 출력되었지만 fflushstdout의 출력 버퍼를 강제로 비우면 "Hello, world!"가 즉시 출력됩니다.

여기서 fflush는 출력 버퍼만 비울 수 있고, fflush(stdin);과 같이 입력 버퍼를 비우는 것은 C 언어 표준에서 정의되지 않은 행동이라 컴파일러마다 동작이 다릅니다. Visual Studio(Windows)에서는 입력 버퍼를 비우지만 리눅스에서는 아무 동작도 하지 않습니다.

참고로 fflush(NULL); 와 같이 NULL 을 넣으면 열려 있는 모든 출력 버퍼를 비웁니다.

이번에는 입력 버퍼의 동작을 알아보겠습니다. 예를 들어 scanf 함수로는 전화번호를 입력받고 fgets 함수로는 이름을 입력받는 프로그램이 있습니다.

input_buffer.c

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

int main()
{
    char phoneNumber[14];
    char name[10];

    fputs("전화번호를 입력하세요: ", stdout);
    scanf("%s", phoneNumber);    // scanf로 입력을 받음

    // scanf로 입력받은 다음에는 입력 버퍼에 \n이 남아있음
    
    fputs("이름을 입력하세요: ", stdout);
    fgets(name, sizeof(name), stdin);    // 입력 버퍼의 \n 때문에 fgets는 그냥 넘어감

    printf("전화번호: %s\n", phoneNumber);
    printf("이름: %s\n", name);

    return 0;
}

소스를 컴파일한 뒤 전화번호와 이름을 입력합니다.

실행 결과

전화번호를 입력하세요: 030-4321-9876 (입력)
이름을 입력하세요: 전화번호: 030-4321-9876
이름: 홍길동
 

전화번호를 입력한 뒤 엔터 키를 누르면 이름 입력 부분은 그대로 넘어가버립니다. 결과가 엉망이 되었죠.

왜냐하면 scanf 함수는 %s로 문자열 부분만 골라서 배열 phoneNumber에 저장하기 때문에 입력 버퍼에는 아직 \n이 남아있습니다. 이때 fgets 함수를 사용하면 입력 버퍼의 \n까지 가져오므로 입력이 그냥 끝나버립니다.

입력 받을 때 scanf 함수만 사용하면 별 문제가 없지만 이처럼 scanf 함수 다음에 fgets 함수를 사용하면 입력을 받을 수 없습니다.

이런 문제를 해결하려면 다음과 같이 입력 버퍼를 비우는 함수를 만들어주면 됩니다.

clear_input_buffer.c

#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>

void clearInputBuffer()
{
    // 입력 버퍼에서 문자를 계속 꺼내고 \n를 꺼내면 반복을 중단
    while (getchar() != '\n');
}

int main()
{
    char phoneNumber[14];
    char name[10];

    fputs("전화번호를 입력하세요: ", stdout);
    scanf("%s", phoneNumber);     // scanf로 입력을 받음
    clearInputBuffer();           // 입력 버퍼를 비움
    
    fputs("이름을 입력하세요: ", stdout);
    fgets(name, sizeof(name), stdin);    // fgets로 입력을 받을 수 있음

    printf("전화번호: %s\n", phoneNumber);
    printf("이름: %s\n", name);

    return 0;
}

소스를 컴파일한 뒤 전화번호와 이름을 입력합니다.

실행 결과

전화번호를 입력하세요: 030-4321-9876 (입력)
이름을 입력하세요: 홍길동 (입력)
전화번호: 030-4321-9876
이름: 홍길동
 

이제 전화번호와 이름이 정상적으로 입력됩니다. 즉, scanf 함수를 사용한 다음에 clearInputBuffer 함수로 입력 버퍼를 비웠기 때문에 그다음에 오는 fgets 함수도 정상적으로 동작합니다.

입력 버퍼를 비우는 방법은 간단합니다. 다음과 같이 getchar 함수로 입력 버퍼에서 문자를 계속 가져오다가 \n일 때 중단하면 됩니다.

void clearInputBuffer()
{
    // 입력 버퍼에서 문자를 계속 꺼내고 \n를 꺼내면 반복을 중단
    while (getchar() != '\n');
}

얼핏 보면 while (getchar() != '\n');\n이 아닐 때까지만 반복하므로 \n은 꺼내지 않을 것 같지만 일단 != '\n'으로 비교하려면 getchar 함수로 먼저 문자를 꺼내야 하므로 실제로는\n까지 꺼냅니다. 즉, 코드를 풀어보면 다음과 같은 모양이 됩니다.

int c = 0;
while (c != '\n')     // 꺼낸 문자가 \n이면 반복 중단
    c = getchar();    // 문자를 꺼냄