70.4 파일에서 문자열 읽기

이제 앞에서 생성한 hello.txt 파일의 내용을 읽어보겠습니다. 파일을 읽을 때는 fopen 함수로 파일을 열어서 파일 포인터를 얻은 뒤 fgets 함수로 파일의 내용을 읽습니다(stdio.h 헤더 파일에 선언되어 있습니다).

  • fgets(버퍼, 버퍼크기, 파일포인터);
    • char *fgets(char *_Buffer, int _MaxCount, FILE *_Stream);
    • 성공하면 읽은 문자열의 포인터를 반환, 실패하면 NULL을 반환

다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요.

file_get_string.c

#define _CRT_SECURE_NO_WARNINGS    // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>     // fopen, fgets, fclose 함수가 선언된 헤더 파일

int main()
{
    char buffer[20];    // 파일을 읽을 때 사용할 임시 공간

    FILE *fp = fopen("hello.txt", "r");    // hello.txt 파일을 읽기 모드로 열기.  
                                           // 파일 포인터를 반환

    fgets(buffer, sizeof(buffer), fp);    // hello.txt에서 문자열을 읽음

    printf("%s\n", buffer);    // Hello, world!: 파일의 내용 출력

    fclose(fp);    // 파일 포인터 닫기

    return 0;
}

실행 결과

Hello, world!

먼저 파일을 읽을 때 사용할 임시 공간(버퍼)를 선언합니다. 여기서는 크기가 20인 char 배열을 선언했습니다(char 포인터를 선언한 뒤 동적 메모리를 할당해도 됩니다).

char buffer[20];    // 파일을 읽을 때 사용할 임시 공간

fopen 함수를 사용하여 hello.txt 파일을 읽기 모드(r)로 엽니다.

FILE *fp = fopen("hello.txt", "r");    // hello.txt 파일을 읽기 모드로 열기.
                                       // 파일 포인터를 반환

이제 파일 포인터를 얻었으니 fgets 함수로 파일의 내용을 읽습니다. 먼저 버퍼에는 앞에서 선언한 buffer를 넣고, 버퍼 크기에는 sizeof(buffer)와 같이 버퍼의 크기를 구해서 넣습니다. 그리고 마지막에는 파일 포인터 fp를 넣어줍니다.

fgets(buffer, sizeof(buffer), fp);     // hello.txt에서 문자열을 읽음

printfbuffer의 내용을 출력해보면 hello.txt에 저장되어 있던 문자열이 출력되는 것을 볼 수 있습니다.

printf("%s\n", buffer);    // Hello, world!: 파일의 내용 출력

파일 읽기가 끝났으면 반드시 fclose 함수로 파일 포인터를 닫아줍니다.

fclose(fp);    // 파일 포인터 닫기

지금까지 파일에서 문자열을 읽은 과정을 그림으로 표현하면 다음과 같습니다.

그림 70-5 파일에서 문자열 읽기

한가지 특이한 점은 fgets 함수가 파일을 읽는 방식입니다. 예를 들어 다음과 같이 hello.txt에 문자열이 저장되어 있습니다.

hello.txt

Hello, world! Hello, world! Hello, world! Hello, world!

이 상태에서 fgets의 버퍼 크기를 20바이트로 지정한 뒤 파일을 읽으면 딱 버퍼 크기만큼만 읽습니다. 따라서 널 문자를 포함하여 20바이트를 읽으며 실제 문자열은 19바이트입니다.

// 파일 열기, 닫기 생략
char buffer[20];
fgets(buffer, sizeof(buffer), fp);    // 버퍼 크기 20바이트
fputs(buffer, stdout);    // Hello, world! Hello: 널 문자를 포함하여 20바이트를 읽음. 문자열은 19바이트

실행 결과

Hello, world! Hello

하지만 다음과 같이 hello.txt에 줄바꿈(\n)이 있으면 버퍼 크기와는 상관 없이 \n까지 문자열을 읽습니다(\n도 포함).

hello.txt

Hello, wo
rld!

즉, 이 상태에서 fgets로 파일을 읽으면 buffer에는 "Hello, wo\n"까지만 들어갑니다.

// 파일 열기, 닫기 생략
char buffer[20];
fgets(buffer, sizeof(buffer), fp);    // 버퍼 크기 20바이트
fputs(buffer, stdout);    // Hello, wo\n: \n까지 문자열을 읽음(\n도 포함)

실행 결과

Hello, wo
 

이처럼 fgets 함수는 \n에 따라 읽은 결과가 달라지므로 사용에 주의해야 합니다.

fgets 함수도 stdin을 지정하면 사용자가 입력한 문자열을 버퍼에 저장합니다. 다음과 같이 버퍼 크기를 20으로 지정하면 사용자가 입력한 문자열 중에서 널 문자 포함 20바이트만 버퍼에 저장합니다(실제 문자열은 19바이트).

char buffer[20];
fgets(buffer, sizeof(buffer), stdin);    // 표준 입력(stdin)에서 20바이트만큼 문자열 읽기
                                         // 널 문자 1바이트, 실제 문자열 19바이트

물론 지정한 버퍼크기 보다 작은 상태에서 엔터 키를 누르면 \n이 되므로 fgets 함수는 \n까지 입력을 받습니다(\n도 포함).

이번에는 fread 함수를 사용하여 파일에서 문자열을 읽어보겠습니다(stdio.h 헤더 파일에 선언되어 있습니다).

  • fread(버퍼, 읽기크기, 읽기횟수, 파일포인터);
    • size_t fread(void *_Buffer, size_t _ElementSize, size_t _ElementCount, FILE *_Stream);
    • 성공한 읽기 횟수를 반환, 실패하면 지정된 읽기 횟수보다 작은 값을 반환

다음 내용을 소스 코드 편집 창에 입력한 뒤 실행해보세요.

file_read_string.c

#define _CRT_SECURE_NO_WARNINGS    // fopen 보안 경고로 인한 컴파일 에러 방지
#include <stdio.h>      // fopen, fread, fclose 함수가 선언된 헤더 파일

int main()
{
    char buffer[20] = { 0, };    // 파일을 읽을 때 사용할 임시 공간, 미리 0으로 전부 초기화

    FILE *fp = fopen("hello.txt", "r");     // hello.txt 파일을 읽기 모드로 열기.
                                            // 파일 포인터를 반환

    fread(buffer, sizeof(buffer), 1, fp);   // hello.txt에서 버퍼 크기(20바이트)만큼 1번 값을 읽음

    printf("%s\n", buffer);    // Hello, world!: 파일의 내용 출력

    fclose(fp);    // 파일 포인터 닫기

    return 0;
}

실행 결과

Hello, world!

파일을 읽을 때 사용할 임시 공간(버퍼)를 선언해야 하는데 fread 함수를 사용할 때는 char 배열을 선언한 뒤 반드시 0으로 초기화해야 합니다(char 포인터에 동적 메모리를 할당한 뒤 0으로 초기화해도 됩니다).

char buffer[20] = { 0, };    // 파일을 읽을 때 사용할 임시 공간, 미리 0으로 전부 초기화
참고 | 버퍼의 크기?

hello.txt에 저장된 문자열은 13자(13바이트)입니다. 그런데 왜 20바이트로 버퍼를 만들었을까요? 정확하게 구현하자면 미리 파일 크기(문자열 길이)를 구해와야 하지만 여기서는 fgets, fread 함수의 사용 방법을 보여주기 위해 간단하게 13바이트보다 큰 20바이트로 설정했습니다. 파일 크기만큼 버퍼를 만들고 내용을 읽는 방법은 'Unit 71 파일 포인터 활용하기'에서 설명하겠습니다.

fopen 함수를 사용하여 hello.txt 파일을 읽기 모드(r)로 엽니다.

FILE *fp = fopen("hello.txt", "r");     // hello.txt 파일을 읽기 모드로 열기.
                                        // 파일 포인터를 반환

이제 파일 포인터를 얻었으니 fread함수로 파일의 내용을 읽습니다. 먼저 앞에서 선언한 buffer를 넣어줍니다. 그리고 읽기 크기에는 sizeof(buffer)와 같이 버퍼의 크기를 구해서 넣고, 읽기 횟수에는 1을 지정하여 버퍼 크기(20바이트)만큼 읽습니다. 마지막에는 파일 포인터 fp를 넣어줍니다.

fread(buffer, sizeof(buffer), 1, fp);   // hello.txt에서 버퍼 크기(20바이트)만큼 1번 값을 읽음

즉, fread 함수에서 파일을 읽는 크기는 읽기 크기 * 읽기 횟수입니다.

특히 fread 함수는 fgets 함수와는 달리 \n이 있든 없든 무조건 지정된 크기만큼 읽습니다.

참고 | fread로 21바이트만큼 읽으면?

fread(buffer, 21, 1, fp);처럼 buffer의 크기보다 큰 값인 21로 읽으면 어떻게 될까요? 문제없이 "hello, world!" 문자열이 읽어집니다. fread는 버퍼 크기와는 상관없이 파일에서 읽을 수 있는 최대 크기만큼 읽기 때문입니다. hello.txt에는 13바이트만큼 저장되어 있으므로 20이나 21을 지정해도 13바이트만큼 읽습니다.

fread는 파일 크기에 따라 읽기 성공 여부를 반환하는데 파일 크기 13바이트보다 큰 20이나 21을 지정하면 지정한 값만큼 못 읽었기 때문에 0이 반환됩니다. 읽은 횟수 1을 반환하려면 fread(buffer, 13, 1, fp);처럼 파일 크기 13을 지정해주면 됩니다.

printfbuffer의 내용을 출력해보면 hello.txt에 저장되어 있던 문자열이 출력되는 것을 볼 수 있습니다.

printf("%s\n", buffer);    // Hello, world!: 파일의 내용 출력

만약 앞에서 buffer를 0(NULL)으로 초기화하지 않고 fread로 파일을 읽으면 "Hello, world!"이외에도 쓸데없는 값들이 함께 출력됩니다. 왜냐하면 C 언어로 만든 프로그램의 문자열 포인터, 배열에 저장된 문자열은 끝에는 NULL이 들어가 있지만 파일에 저장된 문자열은 끝에 NULL이 들어있지 않습니다. 그러다 보니 파일에서 문자열만 읽어서 buffer에 가져오면 이전에 메모리에 저장되어 있던 값과 합쳐지게 됩니다. 즉, 문자열 끝에 NULL이 없어서 buffer의 내용을 모두 출력하게 되는 것이죠.

그림 70-6 buffer 초기화 여부에 따른 차이

파일 읽기가 끝났으면 반드시 fclose 함수로 파일 포인터를 닫아줍니다.

fclose(fp);    // 파일 포인터 닫기

지금까지 파일에서 문자열을 읽은 과정을 그림으로 표현하면 다음과 같습니다.

그림 70-7 파일에서 문자열 읽기

fread 함수도 stdin을 지정하면 사용자가 입력한 문자열을 버퍼에 저장합니다. 단, fread 함수는 무조건 지정된 크기만큼만 읽으므로 버퍼 끝에 NULL이 들어갈 수 있도록 읽기 크기는 버퍼 크기보다 1이 적도록 만들어줍니다.

char buffer[20] = { 0, };
fread(buffer, sizeof(buffer) - 1, 1, stdin);    // 표준 입력(stdin)에서 문자열 읽기
                     // 버퍼 끝에 NULL이 들어갈 수 있도록 sizeof(buffer) - 1을 지정
참고 | stdin, stdout, stderr도 사실은 파일 포인터입니다.

stdinstandard input, stdoutstandard out, stderr는 standard error를 뜻하는데 보통 stdin은 키보드 입력, stdoutstderr는 콘솔 출력입니다. 특히 C 언어에서는 stdin, stdout, stderr가 파일 포인터(FILE *)이므로 fprintf, fscanf, fread, fwrite 등 파일 포인터를 받는 함수에 사용할 수 있습니다. 즉, fscanf, fgets 함수로 stdin을 읽으면 키보드 입력 값을 가져오게 되며 fprintf, fputs 함수로 stdout, stderr에 값을 쓰면 콘솔에 값을 출력하게 되죠.

이 책에서 파일 쓰기와 관련된 심사 문제는 stdout을 사용하겠습니다.

FILE *fp = stdout;                // stdout도 파일 포인터
char *s1 = "Hello, world!";
fwrite(s1, strlen(s1), 1, fp);    // 화면(콘솔)에 파일 쓰기 결과를 출력

지금까지 파일을 읽고 쓰는 방법을 배웠습니다. 파일 처리는 프로그램을 만들 때 자주 사용되므로 사용 방법을 정확히 익히는 것이 좋습니다.