1. 파일 입출력


오늘은 매우 중요하게 쓰이는 파일 입출력에 대해서 알아보도록 하겠습니다. 우선 본격적으로 들어가기 전, 파일 입출력은 뭘까요? 프로그램에서 리디렉션을 사용하지 않고도 어느 파일에 대한 입출력을 할 수 있습니다. 이 파일 입출력을 이용하여 어느 파일에 저장되어 있는 데이터를 읽어들이거나 저장시킬 수가 있습니다. 주의하여야 할 것은 이 파일 입출력은 메모리 공간에 데이터를 쓰거나 읽는것과 달리 직접 데이터를 내보내지 않고 '스트림(Stream)'을 이용하여 입출력합니다. 그렇다면 스트림은 또 뭘까요?

C언어에서의 스트림(Stream)은 바이트들이 순서대로 입출력되는 논리적인 장치이며, 입출력 장치와 프로그램 간의 데이터를 주고받는 인터페이스 역할을 합니다. 더 생각해보자면, 우리는 어떻게 하여 키보드 같은 장치로부터 함수로 데이터를 주고받을 수 있었을까요? 이런 것은 다 '스트림(Stream)'란 녀석이 고맙게도 가능하게 해 주었습니다. 그럼 이 스트림이라는 것을 어떻게 형성할까요?

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

이 fopen란 함수를 살펴보자면 첫번째 인자는 파일의 이름을, 두번째 인자는 파일 개방 모드를 말하는 것으로 즉 데이터를 주고받을수 있는 스트림의 생성을 의미하며 파일 개방 모드는 파일 접근 모드와 데이터 입출력 모드가 같이 쓰이며 파일 접근 모드는 아래 그림을 통하여 알 수 있으며, 데이터 입출력 모드는 t와 b, 두 모드가 존재하는데 t는 텍스트 모드, 그리고 b는 바이너리 즉 2진 모드입니다. 그렇다면 이 텍스트 모드와 바이너리 모드는 또 뭘까요?

fclose, fwrite, fread 함수를 알아본 뒤, 텍스트 모드와 바이너리 모드의 차이점을 알아보도록 하겠습니다. 


모드

의미

r

● 파일을 읽기 위해서 개방한다. 오로지 읽는 것만 가능.

w

● 데이터를 쓰기 위해 개방한다. 오로지 쓰는 것만 가능.
● 만약에 fopen 함수 호출 시 지정해 준 파일이 존재하지 않으면, 새로운 파일을 생성해서 데이터를 쓰게 된다.
● 지정해 준 파일이 존재하면, 그 파일의 데이터를 지워버리고 데이터를 쓰게된다.

a

● w 모드와 달리, 지정해 준 파일이 존재하면 데이터를 지우지 않고 파일의 끝에서 부터 데이터를 추가한다.
● 나머지 특징은 w모드와 같다.

r+

● 파일을 읽고 쓰기 위해 개방.

● 파일이 존재하지 않는 경우, 새로운 파일을 생성.

● 파일이 존재하는 경우, 파일의 데이터를 지우지는 않지만 원래 존재하는 파일의 데이터를 덮어쓰게 된다.

w+

● r+ 모드와 달리, 지정해 준 파일이 존재하면 모든 데이터를 지워버리고 데이터를 기록.
● 나머지 특징은 r+와 같다.

a+

● r+ 모드와 달리, 지정해 준 파일이 존재하면 파일의 끝에서부터 데이터를 추가.

(나머지 특징은 r+와 같다)

<파일 접근 모드>

한번 직접 사용해볼까요?

EXAMPLE1.c:
#include <stdio.h>

int main()
{
    int num=50;
    FILE * fp=fopen("OUTPUT.txt", "wt");
    
    if(fp==NULL) {
     printf("파일을 열 수 없습니다!\n");
     return -1;
    }
    
    fputs("ABCDEFG\n", fp);
    fprintf(fp, "%d", num);
    
    fclose(fp);
    return 0;
}
OUTPUT.txt:

ABCDEFG
50


우선 코드를 살펴보시면 6행에서 해당 파일과의 스트림을 형성하고 그 정보를 FILE 구조체 변수에 담아서 그 변수의 포인터를 반환합니다. (생성된 FILE 구조체 변수에 파일의 정보가 담김)  8~11행은 fopen 함수가 실패하면 NULL 포인터를 반환하므로 '파일이 열리지 않으면 프로그램을 끝냄'라는 역할을 하는 부분이며, 13행과 14행을 보시면 fputs과 fprintf가 있는데 이는 우리가 알고있는 표준입출력 함수인 puts 함수와 printf함수와 같이 비슷합니다. "ABCDEFG"와 변수 num의 값을 구조체 포인터 fp가 가르키는 파일(OUTPUT.txt)에 저장하는 부분입니다.

그렇다면 16행은 무슨 기능을 할까요? fclose는 fopen 함수로 형성된 스트림을 소멸시키는 함수이며, 이렇게 소멸시키는 이유는 파일을 닫아주지 않는다면 할당된 채로 남아있겠죠? 그럼 그만큼의 자원손실을 초래하는 것은 당연합니다. 그렇기 때문에 파일을 다 사용했다면 재빨리 닫아줘야 합니다.

우리가 지금까지 본 fputs, fprintf 의 선언방법은 이렇습니다. 추가로 fgets, fgetc, fputc, fscanf의 선언방법도 같이 나열하겠습니다.

파일 입출력 함수:

int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
int fputs(const char *string, FILE *stream);
char *fgets(char *string, int n, FILE *stream);
int fprintf(FILE *stream, const char *string, ...);
int fscanf(FILE *stream, const char *string, ...);
fgetc는 getchar, fputc는 putchar, fputs는 puts, fgets는 gets, fprintf는 printf, fscanf는 scanf 정도로 생각해 두시면 됩니다.

파일 스트림 소멸 함수:
int fclose(FILE *stream);


fclose 함수에 대한 설명은 앞에서 이미 드렸으니 잘 아실거라 생각합니다. 다시 한번 요약하면 OS(운영체제)가 할당한 자원의 반환을 위해 형성된 스트림을 소멸시키는 함수라고 생각하시면 됩니다.

그럼 이제 텍스트 모드가 뭔지, 바이너리 모드가 뭔지 그리고 이 둘의 차이점이 무엇인지 알아볼까요? 우리가 알고있는 텍스트 파일(text file)이란 사람이 이해할 수 있는 글자들로 구성되어 있는 데이터는 문자 데이터이고 이 문자 데이터를 담고 있는 파일을 가리켜 '텍스트 파일(text file)'이라고 합니다. 반대로 바이너리 파일은 뭘까요?

바이너리 파일(binary file)이란 컴퓨터가 인식할 수 있는 데이터를 담고 있는 파일입니다. 이 둘은 무슨 차이점을 지니고 있을까요? MS-DOS(Windows) 환경에서의 개행은 \r\n라는 것을 알고 계시나요? 바이너리 모드에선 캐리지 리턴과 라인 피드의 처리가 텍스트 모드와 다릅니다. 텍스트 모드로 개방하면 \n(라인 피드)를 자동으로 \r\n(캐리지 리턴, 라인 피드)로 변환되게끔 해줍니다. 그런데 이 바이너리 모드는 \n(라인 피드) 하나로만 읽히고 개행이 되지 않습니다.

또하나의 차이점은 EOF(End of File) 문자가 존재합니다. 텍스트 모드로 파일을 연 경우 0x1A(ctrl-z)가 중간에 존재하면 그 곳을 파일의 끝으로 인식하며, 바이너리 모드에서는 EOF가 아닌 단순한 데이터로 인식을 하게 됩니다.
바이너리 모드를 우리가 직접 사용해 보도록 할까요?

EXAMPLE2.c:

#include <stdio.h>

int main()
{
     char buffer[20];
     char temp[20]="File I/O binary";
     
     FILE *handleWrite=fopen("Test.dat","wb");
     fwrite(temp, 1, 13, handleWrite);
     fclose(handleWrite);

     FILE *handleRead=fopen("Test.dat","rb");
     fread(buffer,1,13,handleRead);
     printf("%s",buffer);
     fclose(handleRead);

     return 0;
}

Test.dat:

File I/O bina


코드에서 쭉 내려가다 보면 8행에서 개방 모드가 '쓰기 가능한 바이너리 모드'이며 Test.dat 파일과의 스트림을 형성 후 구조체 변수 handleWrite에 파일의 정보가 담깁니다. 그리고 9행과 13행을 보시면 fwrite, fread 라는 처음보는 함수가 나왔는데 이 함수들은 바이너리 데이터의 입출력에 사용이 되는 함수입니다. 선언방법을 한번 볼까요?

size_t fread(void *ptr, size_t size, size_t cnt, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t cnt, FILE *stream);

보시면 블럭단위로 입출력을 수행하는 것을 볼수 있으며 size는 블록의 바이트크기, cnt는 그 바이트의 갯수를 말합니다. 즉,

fwrite(temp, 1, 13, handleWrite);
fread(buffer ,1, 13, handleRead);
fwrite는 1바이트 크기의 데이터 13개를 배열 temp로부터 읽어서 handleWrite에 저장하는 역할을 하며,
fread는 1바이트 크기의 데이터 13개를 handleRead로부터 읽어들여서 배열 buffer에 저장하는 역할을 합니다.
이해가 가시나요? 하나의 예제를 더 보도록 할까요?

EXAMPLE3.c:
#include <stdio.h>

int main()
{
   FILE *fp_source, *fp_dest;
   char oneByte;
   char source[50], dest[50];
   scanf("%s %s", source, dest);
   fp_source = fopen(source, "rb");
   fp_dest   = fopen(dest, "wb");
   while(!feof(fp_source)) {
      oneByte = fgetc(fp_source);
      fputc(oneByte, fp_dest);
   }
   printf("성공적으로 복사가 완료되었습니다!\n"); 
   fclose(fp_source);
   fclose(fp_dest);
   
   return 0;
}

EXAMPLE3.exe:

C:\Users\HOME\Desktop\copyimage\5.png C:\Users\HOME\Desktop\copyimage\binary\5.p
ng
성공적으로 복사가 완료되었습니다!
계속하려면 아무 키나 누르십시오 . . .


성공적으로 복사가 되었는지, 폴더로 가서 직접 확인해볼까요?


그럼 확인했으니, 코드를 분석해보도록 할까요? 6행에서 보시면 바이너리 데이터에서 1바이트씩 가져오기 위해 char 형을 사용했으며, 7행에선 복사될 파일이 있는 경로, 그 파일이 복사될 경로를 사용자에게 입력받기 위해 크기가 50인 source, dest를 배열로 선언했습니다. 그다음 10~11행에선 해당 경로에 있는 복사될 파일 '읽기 가능한 바이너리 모드'로 개방하고, 복사가 이루어질 파일을 생성 후 '쓰기 가능한 바이너리 모드'로 개방이 된걸 확인할 수 있습니다. 13~16행에선 파일의 끝을 만나기 전까지는 계속 루프를 돌며 fgetc로 1바이트씩 가져오면서 그 데이터를 oneByte에 저장합니다. 이렇게 저장된 oneByte로 fputc 함수에 의해 차례대로 복사가 진행되게 됩니다. 그다음 빠져나온 뒤 스트림을 소멸시키고 있습니다.

중간에 feof란 함수를 보았는데, 이 함수는 어떠한 기능을 할까요? 눈치채신 분들도 있겠지만 이것은 파일의 끝을 확인할때 사용하는 함수로 선언방법은 이렇습니다.

int feof(FILE *stream) // 파일의 끝에 도달한 경우 0이 아닌 값을 반환

2. 파일 위치 지시자


이 파일 위치 지시자는 파일의 규모가 상당히 방대해지고 원하는 데이터가 이곳저곳에 흩어져 있을때 우리가 원하는 자료가 있는 곳으로 파일 포인터를 옮긴 뒤 원하는 크기 만큼의 자료를 읽어들일 수 있습니다.
int fseek(FILE *stream, long offset, int wherefrom);

파일 위치 지시자를 이동시키는 함수는 대표적으로 fseek가 있으며 좀더 살펴보면 offset이 의미하는 것은 whereform 위치부터 새로운 위치까지 떨어진 거리(바이트 수)를 의미합니다. 여기서 whereform에 전달될 수 있는 상수를 알아보면,

SEEK_SET(0) // 파일의 맨 앞으로 이동
SEEK_CUR(1) // 현재 위치에서 이동하지 않음
SEEK_END(2) // 파일의 맨 끝으로 이동

만약 파일 포인터 fp를 시작 위치에서 50바이트 옮기고 싶다면 아래와 같습니다.

fseek(fp, 50, SEEK_SET);

그럼 현재 위치에서 20바이트 이전은 어떻게 할까요?

fseek(fp, -20, SEEK_CUR);
확실한 이해를 위해 한번 직접 써보도록 할까요?


EXAMPLE4.c:

#include <stdio.h>

int main()
{
    FILE * fp=fopen("OUTPUT.txt", "wt");
    fputs("123456789", fp);
    fclose(fp);
    
    fp=fopen("OUTPUT.txt", "rt");
    
    fseek(fp, -3, SEEK_END);
    putchar(fgetc(fp));
    
    fseek(fp, 3, SEEK_SET);
    putchar(fgetc(fp));
    
    fseek(fp, 3, SEEK_CUR);
    putchar(fgetc(fp));
    
    fclose(fp);
    return 0;
}

EXAMPLE4.exe:

748


살펴보면, 123456789이 파일 구조체 fp가 가르키는 파일에 저장되었고 11~12행을 보시면 파일의 끝에서 3칸 뒤로 이동을 하여 7이 출력되었고, 14~15행을 보시면 파일의 맨 앞에서 3칸 앞으로 이동하였으므로 4가 출력됩니다. 4가 출력되면서 파일 위치 지시자는 5를 가르키게 되었습니다. 다시 17~18행을 보시면 현재 위치에서 3칸 앞으로 이동시키니 8이 나온것입니다. 이해가 가셨나요?

이 파일 위치 지시자의 위치를 정수로 반환하고 싶을땐 ftell라는 함수를 사용하면 되는데, 선언방법은 아래와 같습니다.

long ftell(FILE *stream);
ftell를 한번 사용해볼까요?


EXAMPLE5.c:

#include <stdio.h>

int main ()
{
  FILE * pFile;
  long size;
  pFile = fopen("myfile.txt","rb");
  if (pFile==NULL) perror ("파일을 열지 못했습니다!");
  else
  {
    fseek (pFile, 0, SEEK_END);
    size=ftell (pFile);
    fclose (pFile);
    printf ("myfile.txt의 크기 %ld 바이트\n",size);
  }
  return 0;
}

EXAMPLE5.exe:

myfile.txt의 크기 53 바이트
계속하려면 아무 키나 누르십시오 . . .


12행에서 myfile.txt의 끝으로 이동하여 움직이지 않습니다. 13행에서 size 변수에 ftell 함수를 이용하여 파일 위치 지시자의 위치를 정수로 반환하여 저장시키고 있습니다. 15행에서 그 파일의 크기를 출력하고 프로그램이 종료됩니다.