C 언어 | 고급 기능 | 파일 및 스트림 - fopen(), fclose(), fflush() 등

표준 C 언어의 함수를 이용하여 디스크에 임의의 파일에서 문자 데이터를 읽고 쓰는 방법을 소개한다.

데이터 로드

지금까지는 메모리(주기억 장치)에 데이터를 읽거나 써왔다. 숫자나 문자 배열 등을 변수로 저장하거나, 참조할 수 있었다. 변수를 처리하는 작업은 프로그램의 가장 기본적인 항목이며, 복잡하지 않다.

그러나 메모리에 저장된 정보는 프로그램을 종료되면 사라져 버린다. 사용자가 편집 소프트웨어에서 만든 데이터나 프로그램의 로그, 설정 정보 등을 프로그램 종료 후에도 저장하고 싶은 경우 보조 기억 장치에 기록해야 한다. 즉, 하드 디스크나 플로피 디스크 등이 있다. 이러한 보조 기억 장치에 데이터를 저장하기 위해서는 파일을 조작해야 한다.

파일과 데이터를 취급할 때의 한묶음 단위로, 디스크에 있는 프로그램이나 데이터를 말한다. 디스크에 데이터를 저장하려면 변수가 아닌, 이 파일에 데이터를 저장해야 한다.

고수준 프로그래밍 언어는 어떤 입출력 장치이든지 같은 방법으로 조작할 수 있다. 만약, 하드웨어에 따라 다른 작업을 수행해야 한다면, 프로그래머에게는 큰 부담이 되어 버린다. 따라서 시스템이나 프로그래밍 언어가 이를 감추고 있는 것이다. 액세스하는 대상이 디스크 파일에서도, 네트워크에서도, 스크린이라해도 데이터의 입출력 목적은 동일에서 일관된 조작으로 실행할 수 있어야 한다는 생각이다.

이것은 스트림라는 추상적인 인터페이스를 조작하는 것으로 실현되고 있다. C 언어 프로그래머는 논리적 인 인터페이스인 “스트림"을 통해서 파일에 액세스한다. 개발자는 네트워크나 다른 장치와 입출력을 하는 경우도, 스트림을 사용하는 것만으로 프로그램 할 수 있기 때문에, 코드 재사용 및 유지 보수가 용이하게 된다.

따라서 파일 작업을 수행하려면 먼저 스트림과 파일을 연결해야 한다. 그러기 위해서는 FILE 구조체를 사용한다. FILE 구조체는 스트림의 상태에 대한 정보를 저장하고, 파일 함수에서 사용한다. FILE 구조체는 stdio.h 헤더 파일에 정의된 typedef 이름이다.

FILE *fp;

이러한 파일 구조체의 포인터 변수를 선언한다. 이 구조가 어떻게 선언되어 있는지 개발자가 신경 쓸 필요는 없다. 이 구조체를 이용하는 것은 표준 함수이고, 개발자가 멤버를 호출하는 것은 아니기 때문이다. 파일을 열려면 fopen() 함수를 사용한다. 표준 라이브러리 파일 관련 함수는 접두사 f로 시작하는 명명 규칙이 마련되어 있다.

fopen() 함수

FILE *fopen( const char *filename, const char *mode );

이 함수는 FILE 구조체에 대한 포인터를 반환한다. 이렇게 FILE 구조체의 메모리를 할당하여 초기화하는 것은 fopen() 함수의 일이며, 개발자가 FILE 구조체를 초기화하는 일은 없다. 우리는 FILE 구조체에 대한 포인터를 받아서, 이를 파일로 처리만 하면 된다.

filename에는 열려는 파일 이름을 지정한다. 파일 이름만의 경우는 기본적으로 대상이 되는 작업 디렉토리(working directory)의 파일을 검색한다. 다른 디렉토리를 검색 할 경우, 시스템 고유의 방법으로 디렉토리를 지정한다. 예를 들어 Microsoft MS-DOS라면 C:\DirName\test.txt과 같이 지정한다.

mode에는 파일을 열 때에 읽기 전용, 쓰기 전용, 또는 모두의 액세스 모드를 문자열로 지정한다. ASCII 문자로 문자 변환(개행 코드 등)하는 텍스트 파일과 변환이 되지 않는 이진 파일의 선택 등을 지정할 수 있다. 바이너리 파일에 대해서는 “바이너리 파일"에서 설명한다.

표1 액세스 모드

모드 의미
“r” 읽기 모드로 파일을 열린다. 파일이 존재하지 않으면 NULL을 반환한다.
“w” 쓰기 모드로 파일을 열린다. 파일이 존재한다면 그 내용을 파괴하고 없으면 새로 만든다.
“a” 추가 모드로 파일을 열린다. 파일이 존재하지 않으면 만든다.
“r+” 기존 파일을 대상으로 읽기/쓰기 모드 모두에서 열린다. 파일이 존재하지 않으면 NULL을 반환한다.
“w+” 파일을 만들고 읽기/쓰기 모드 모두에서 열린다. 파일이 존재한다면 그 내용을 파괴한다.
“a+” 읽기/쓰기 모드의 두 가지 모드로 열린다. 파일이 있으면 추가하고, 존재하지 않는 경우 만든다.
“t” 텍스트 모드에서 열린다.
“b” 이진 모드로 열린다.

fopen() 함수는 파일을 여는데 실패하면 NULL을 반환한다.

FILE 구조체를 초기화하는 표준 함수의 역할이므로, 이를 해제하는 것도 표준 함수의 역할이다. 필요한 작업을 마치고 스트림이 필요하지 않은 경우 마지막으로 해제해야 한다. 파일을 닫으려면 fclose() 함수를 사용한다.

fclose() 함수

int fclose( FILE *stream );

stream에는 닫기 스트림을 지정한다. 이는 fopen() 함수에서 취득한 유효한 FILE 구조체에 대한 포인터이어야 한다. 또한 fclose() 함수에는 버퍼를 플래시하는 역할도 한다.

많은 파일 시스템은 데이터의 쓰기는 느리다. 디스크 파일 및 메모리의 큰 차이 중 하나에 액세스 속도의 격차가 있고, 디스크 액세스는 메모리 액세스보다 훨씬 느리다. 따라서 데이터를 쓸 때는 쓰기가 끝날 때까지 프로그램은 처리를 중지하고 기다려야 할 필요가 있다. 이를 효율적으로 하기 위해 파일 시스템은 일정한 데이터 양이 모이기까지는 디스크에 기록하고, 버퍼라고 하는 일시적인 저장 영역에 데이터를 대기시킨다. 버퍼에 남아있는 데이터를 쓰는 작업을 “버퍼를 플래시한다"고 표현한다.

덧붙여서, fclose() 함수 이외에, 명시적으로 플래시를 수행하려면 fflush() 함수를 사용한다.

fflush() 함수

int fflush( FILE *stream );

stream에는 플래시하는 대상의 스트림을 지정한다. 이 함수를 호출하면, 지금까지의 파일 조작이 이행되는 것을 보장된다. 스트림이 입력용으로 열려있는 경우는 버퍼의 내용을 지운다. 또한 NULL을 전달하면 출력용으로 열려있는 모든 스트림을 플래시한다.

코드1

#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
 FILE *file;

 file = fopen(FILENAME , "r");

 if (file == NULL) {
   printf("%s: 파일이 열리지 않습니다.\n" , FILENAME);
    return 0;
 }

 printf("%s: 파일이 열립니다.\n" , FILENAME);
  fclose(file);
 return 0;
}

코드1는 fopen() 함수를 사용하여 파일을 열고 있다. 특별히 파일 작업을 하고 있지 않기 때문에 파일에 영향을 주지 않는다. 데이터의 입출력은 아무것도 하지 않고, 파일을 열 수 있는지 확인만 한다. 파일을 열 수 있다면 fclose() 함수를 사용하여 스트림을 닫은 후 프로그램을 종료한다. 이 프로그램을 실행하려면 test.txt라는 파일을 실행 파일과 같은 디렉토리에 배치해야 한다.

파일에 기록된 정보를 이용하려면 파일의 데이터를 일단 메모리에(즉, 변수에) 로드해야 한다. 읽은 파일의 내용을 검색하는 방법은 여러 가지가 있지만 그 중 하나가 fgetc() 함수이다. fgetc() 함수는 파일의 데이터를 문자로 받을 수 있기 때문에 텍스트 데이터의 로드할 때 사용한다.

fgetc() 함수

int fgetc( FILE *stream );

이 함수는 지정된 스트림의 다음 문자를 int 형으로 반환한다. 정확하게는 unsigned char 형으로 읽은 문자를 int로 캐스팅하여 반환한다. 스트림은 이 같은 파일 조작 함수를 사용하면 데이터의 읽고 쓰기의 대상이 되는 파일 위치를 자동으로 이동한다.

오류가 발생하거나 파일의 마지막에 이르렀을 경우 EOF를 반환한다. EOF는 매크로로 정의되어 있으며 보통 -1의 값이다. EOF는 End Of File의 약자이다. fgetc() 함수가 돌려주는 것은 int 형이지만, 하위 8바이트는 파일에서 로드된 내용이므로 char 형에 캐스트하여 이용할 수 있다.

코드2

#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
  FILE *file;

 file = fopen(FILENAME , "r");

 if (file == NULL) {
   printf("%s: 파일이 열리지 않습니다.\n" , FILENAME);
    return 0;
 }

 while(1) {
    int iValue = fgetc(file);
   if (iValue == EOF) break;
   else printf("%c" , iValue);
 }
 printf("\n");

  fclose(file);
 return 0;
}

코드2는 test.txt라는 이름의 텍스트 파일을 읽고, 화면에 표시한다. 텍스트 파일은 텍스트 편집기 등을 사용하여 적당한 것을 준비한다. while 문의 루프로 fgetc() 함수를 사용하여 문자를 읽고, EOF이 아니면 printf() 함수를 사용하여 한 글자 씩 표시하고 있다.

다만, 이러한 파일 입출력 함수가 반환하는 EOF를 검출하는 방법은 확실하지 않다. 이 방법에는 EOF가 에러를 발생한건지 파일의 맨 끝까지 도달했는지를 확인할 수 없다.

그래서 지정된 스트림의 위치가 파일의 마지막을 조사하기 위해 feof() 함수를 사용한다. 이 함수를 사용하면 확실하게 파일 맨 끝을 구할 수 있다.

feof() 함수

int feof( FILE *stream );

stream에는 유효한 FILE 구조체에 대한 포인터를 지정한다. 지정된 스트림의 위치가 EOF라면 0이 아닌 값을 반환하고 그렇지 않으면 0을 반환한다.

또한 에러를 확인하려면 ferror() 함수를 사용한다.

ferror() 함수

int ferror( FILE *stream );

stream에는 유효한 FILE 구조체에 대한 포인터를 지정한다. 이 함수는 스트림에서 에러가 발생하면 0이 아닌 값을 반환하고 그렇지 않으면 0을 반환한다.

코드3

#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
 FILE *file;

 file = fopen(FILENAME , "r");

 if (file == NULL) {
   printf("%s: 파일이 열리지 않습니다.\n" , FILENAME);
    return 0;
 }

 while(1) {
    int iValue = fgetc(file);
   if (feof(file)) break;

    if (ferror(file)) {
     printf("스트림에서 에러가 발생하였다.\n");
      break;
    }
   else printf("%c" , iValue);
 }
 printf("\n");

  fclose(file);
 return 0;
}

코드3와 코드2를 개량한 것으로, 스트림 작업에서 발생한 에러를 감지할 수 있다. 별 문제없이 파일 가져 오기를 실시할 수 있으면, 결과는 코드2와 동일하다.

데이터 쓰기

데이터를 디스크에 저장하려면 파일에 기록해야 한다. 문자 데이터의 저장은 fgetc() 대신에 문자를 쓸 함수를 사용하기만하면 된다. 문자 쓰기는 fputc() 함수를 사용한다.

fputc() 함수

int fputc( int c, FILE *stream );

c에는 기입하는 문자를, stream에 쓰기를 대상으로 하는 스트림을 지정한다. 함수가 성공하면 쓴 문자를, 실패하면 EOF를 반환한다. fputc()는 스트림의 현재 위치에 문자를 기록하여 다음 위치로 이동한다.

다만, 데이터를 쓰는 경우는 fopen() 함수의 액세스 모드가 쓰기 모드에 있어야 한다. 일반적으로 “w"를 지정하면 된다.

코드4

#include <stdio.h>
#define FILENAME "test.txt"

int main(int argc , char *argv[]) {
 FILE *file;
 int iCount;
 const char *str = "Stand by Ready!";

  file = fopen(FILENAME , "w");

 if (file == NULL) {
   printf("%s: 파일이 열리지 않습니다.\n" , FILENAME);
    return 0;
 }

 for(iCount = 0 ; str[iCount] ; iCount++) {
    fputc(str[iCount] , file);
  }

 printf("%s: 정상적으로 쓰기를 하였습니다.\n", FILENAME);

  fclose(file);
 return 0;
}

코드4는 문자열을 기록하는 파일 이름 “test.txt"과 파일에 쓸 문자열 “Stand by Ready!“을 지정한다. for 루프에서 문자 배열의 끝이 감지될 때까지 순차적으로 fputc() 함수에서 파일에 문자를 쓰는 것을 알 수 있다.

문자열의 입출력

지금까지의 파일에 대한 입출력은 문자 단위로 되어 있었다. 물론, 바이트 단위의 입출력로 할 수 있다면 문자열의 입출력이 가능하지만, 루프를 만들고 EOF를 검출하는 등 귀찮은 프로그램을 써야한다. 이것은 비생산적이다.

텍스트 파일을 다룰 때는 문자열로 입출력을 할 수 있어야 한다. 문자열의 입출력을 할 시에는 fgets() 함수 와 fputs() 함수를 사용한다. 이것들은 fgetc()와 fputc() 함수와 기본적으로 동일하지만 문자열 단위로 입출력을 할 수 있다는 점이 다르다.

fgets() 함수

char *fgets( char *string, int n, FILE *stream );

fputs() 함수

int fputs( const char *string, FILE *stream );

fgets() 함수는 string에 문자열을 포함하는 버퍼에 대한 포인터를, n에는 읽을 수 있는 최대 문자 수를 지정한다. fputs()이라면 string에 쓸 문자열에 대한 포인터를 지정한다. stream에는 대상 스트림을 지정한다.

fgets() 함수는 지정된 입력 스트림 stream에서 문자열을 읽어 들여, string에 저장한다. 문자의 해독은 현재의 스트림 위치에서 첫번째 개행 문자가 나타날지, 스트림의 끝에 도달하거나 읽은 문자가 n -1이 되는지 조건 중에 하나가 충족 될 때까지 계속된다. 함수가 성공하면 string을 반환하고, 오류가 발생하거나 스트림의 끝에 도착한다면 NULL을 반환한다.

fputs() 함수는 성공하면 음수가 아닌 값을, 에러가 발생하면 EOF를 반환한다.

코드5

#include <stdio.h>
#define READ_FILENAME "in.txt"
#define WRITE_FILENAME "out.txt"
#define BUFFER_SIZE 1024

int main(int argc , char *argv[]) {
 FILE *readFile, *writeFile;

 readFile = fopen(READ_FILENAME , "r");
  if (readFile == NULL) {
   printf("%s: 파일이 읽기 모드로 열리지 않습니다.\n" , READ_FILENAME);
    return 0;
 }

 writeFile = fopen(WRITE_FILENAME , "w");
  if (writeFile == NULL) {
    printf("%s: 파일이 쓰기 모드로 열리지 않습니다.\n" , WRITE_FILENAME);
   return 0;
 }

 while(1) {
    char chLine[BUFFER_SIZE];
   if (fgets(chLine , BUFFER_SIZE , readFile) != NULL) 
      fputs(chLine , writeFile);
    else break;
 }

 printf("%s -> %s: 정상적으로 파일 복제되었습니다.\n", READ_FILENAME, WRITE_FILENAME);
  fclose(readFile);
 fclose(writeFile);

  return 0;
}

코드5는 단순히 텍스트 파일을 복사하는 프로그램이다. 명령 라인 인수에서 복사한 텍스트 파일과 출력 파일 이름을 지정한다. 그러면 프로그램은 원본 텍스트에서 문자열을 읽어 들여, 지정된 이름의 파일을 생성하여 문자열을 복사한다. 프로그램에서는 fgets() 함수를 호출하여 문자열을 검색하고 fputs() 함수로 기록하는 작업을 반복한다.