본문 바로가기

컴퓨터/언어,프로그래밍

C언어 :: 파일 입출력

1      파일 입출력 함수의 사용

프로그래밍에 있어 파일 입출력은 매우 중요한 요소라는 것은 틀림이 없습니다.  다만, 우리가 C언어에서 익혀야 할 파일 입출력은 관련 라이브러리를 효율적으로 사용하는 것이지 전체 파일 시스템을 정확히 이해하는 것은 아닙니다.  파일은 파일 시스템에서 관리하는 부분과 O/S에서 관리하는 부분, 응용 프로세서에서 관리하는 부분이 서로 긴밀한 관계속에 진행이 됩니다.  파일 시스템에서는 사용자가 생각하는 논리적인 파일을 실제 물리적인 H/W공간에 저장하여 관리를 하는 것을 얘기합니다.  여기서는 하나의 파일 시스템을 관리하는 슈퍼 블럭이나 부팅이미지를 관리하는 부트 블럭, 논리적 파일과 저장된 물리적 파일 매체간의 1:1 매핑을 하는 inode 블럭, 파일들이 저장되는 프리블럭으로 나누어 관리를 하고 있습니다.  이 책에서는 응용에서 사용할 파일 입출력 라이브러리에 대해서 설명은 하지만 파일 시스템 O/S, 응용 프로세서 사이에 파일을 관리하는 것에 대해서는 논하지 않겠습니다. 또한, 저수준 I/O에 해당하는 I/O시스템 호출도 이 책에서는 다루지 않겠습니다.  이에 대한 사항이 궁금하신 분은 UNIX 시스템 프로그래밍에 관련된 책이나 문서를 참조하시기 바랍니다.

 

1.1           File 구조체

응용 프로세서에서 파일을 관리하는 것은 해당 프로세서에서 작업하기 위해 열려져 있는 파일들에 관한 것입니다.  하나의 파일을 두 번 열 수도 있기 때문에 응용 프로세서에서 관리하는 구분자는 파일명이 될 수가 없습니다.  C언어에서는 파일을 열면 해당 파일을 관리할 수 있는 FILE구조체를 형성하여 해당 포인터를 반환하고 있습니다.  해당 구조체의 멤버 중에 열려져 있는 파일들을 구분할 수 있는 멤버가 있고 이를 파일 디스크립터러 합니다.  FILE타입은 TC, VC, GCC에 따라 조금씩 차이를 보이고 있는데 여기서는 공통적인 요소들에 대해서만 얘기를 하기로 하겠습니다.

 

/* vc에서의 정의 */ 

typedef struct  {

        unsigned           _flag;                    /* File status flags    */

        char               _file;                     /* File descriptor      */

        unsigned char      _cnt;                     /* Ungetc char if no buffer */

        short              _bufsiz;                  /* Buffer size          */

        unsigned char      *_base;                   /* Data transfer buffer */

        unsigned char      *_ptr;                    /* Current active pointer */

        unsigned           _tmpfname;             /* Temporary file indicator */

} FILE;

 

FILE *stdin, *stdout, *stderr;

 

 

 

 

 

1.1.1      파일 구분자(File descriptor)

파일 구분자는 응용에서 열려져 있는 파일에 대한 구분자로 같은 파일을 두 번 열었을 경우에 각각 다른파일 구분자를 갖게 됩니다.  , 같은 파일을 열어도 작업에 따라 유지되어야 하는 값들이 다르다라는 것입니다.  VC의 경우 FILE 구조체에 _file이라는 멤버가 있는데 해당 멤버가 파일 구분자를 갖는 멤버입니다.  참고로 모든 응용은 시작하면서 기본적으로 0,1번에 해당하는 stdin stdout파일을 열어줍니다.  이에 우리는  getchar printf와 같은 기본 입출력 함수를 바로 사용할 수 있는 것입니다.

 

#include <stdio.h>

 

int main()

{

printf("[%d]",stdin->_file);

printf("[%d]",stdout->_file);

return 0;

}

 

위의 코드를 실행해 보면 [0][1]가 출력이 됨을 알 수가 있습니다.

 

1.1.2      파일 버퍼

  파일의 데이터를 처리할 때에는 일정의 규칙이 있습니다.  키보드나 모니터 같은 터미널 장치 파일(문자 파일이라고도 함) O/S사이에서는 stream단위로 처리를 하고 있습니다.  이를 LINE BUFFERING이라고 하는데 정규파일이나 블럭 장치파일(메모리)등을 처리할 때는 버퍼가 꽉차면 처리하게 되어 있습니다.(FULL BUFFERING)  물론, getch함수를 사용할 때 처럼 즉시 반응하는 NULL BUFFERING정책을 사용하는 경우도 있습니다.  특정 파일 작업을 하는 버퍼의 시작 위치가 FILE 구조체 멤버에 _base이며 현재 처리할 버퍼의 위치가 _ptr이고 현재 처리되지 않고 남아있는 데이터의 개수가 _cnt멤버입니다.

 

#include <stdio.h>

 

int main()

{

    getchar();

printf("%s",stdin->_base);

printf("%s",stdin->_ptr);

printf(“%d\n”,stdin->_cnt);

return 0;

}

 

위와 같은 코드가 있고 만약 사용자가 ‘a’, ’b’, ‘c’, ‘d’, ‘\n’순으로 입력을 한다면 어떠한 값들이 출력이 될까요?

 

abcd   

//base값이기 때문에 버퍼에 있는 전체 데이터가 출력이 됩니다. ‘\n’도 출력이 됨

 

bcd   

//prt값은 현재 하나의 문자는 getchar에 의해 처리되었기 때문에 b부터 출력이 됨. 

 

4    

// ‘\n’를 포함하여 현재 처리되지 않은 문자는 4개입니다.

 

 그리고, fflush함수를 통해 현재 버퍼에 있는 것을 바로 처리할 수도 있습니다.

 

1.1.3      파일 상태 flag

파일 작업을 함에 있어 해당 작업이 읽기 위해 열려져 있는지 작업중에 파일의 끝을 만났는지 등에 대한 메타정보를 _flag멤버가 갖고 있습니다.  _flag멤버의 각 비트는 각각 다른 의미를 파악하기 위해 약속되어 있는데 다음의 상수는 _flag멤버의 몇번째 비트가 어떠한 의미로 사용되는지 짐작할 수 있게 해주고 있습니다. 

 

#define _IOREAD    0x0001   // 읽기 모드로 열려져 있다.

#define _IOWRT     0x0002   // 쓰기 모드로 열려져 있다.

 

#define _IOFBF      0x0000  // 디폴트로 FULL Buffering이다.

#define _IOLBF      0x0040  // Line Buffering으로 데이터를 처리를 한다.

#define _IONBF      0x0004  // Buffering을 하지 않는다.

 

#define _IOMYBUF   0x0008  // 버퍼를 직접 설정하였다.

#define _IOEOF      0x0010  // 파일의 끝을 만났다.

#define _IOERR      0x0020  // 파일 작업 중 Error가 난 적이 있다.

 

이 외에도 좀 더 많은 상수가 정의되어 있는데 여기서는 몇가지 중요한 것만 기재하였습니다.  만약 _flag값이 0x??13으로 되어 있다면 현재 파일을 읽기 모드와 쓰기 모드 혼용으로 열었고 현재 파일 작업중에 파일의 끝을 만났다는 것으로 인식하면 될 것입니다.

 

1.2           파일 열기 및 닫기(fopen, fclose)

이제 파일 입출력 라이브러리 함수들에 대해서 하나씩 알아보기로 합시다.  먼저 파일에 어떠한 작업을 하기 위해서는 먼저 열어야 할 것입니다.  이 때 사용하는 라이브러리 함수가 fopen입니다.  그리고, 해당 파일을 닫기 위한 함수가 fclose입니다.

 

포맷:

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

void fclose(FILE *fp);

 

파일 열기(fopen)는 열려고 하는 파일의 이름(상대 경로 혹은 절대 경로)과 작업할 목적에 맞게 mode설정값을 입력 매개변수로 넘겨주면 해당 파일 입출력에 필요한 FILE 구조체를 내부적으로 형성한 후에 그 위치 정보를 넘겨줍니다.  그리고, fopen에서 반환받은 FILE 포인터를 통해 모든 파일 입출력 작업을 하게 되며 작업후에 파일을 닫을 때도 입력 매개변수로 해당 FILE 포인터를 넘겨주게 됩니다.  만약, 파일 열기가 실패를 하게 되면 NULL값이 반환이 됩니다.  이에 파일 입출력을 할 때의 기본적인 코드의 모습은 다음과 같습니다.

 

FILE *fp = 0;

fp = fopen(“test.dat”,”r”);

if(fp)

{

    //해야 할 작업

    fclose(fp);

}

 

 fopen함수의 첫번째 입력 매개변수는 절대 경로 혹은 상대경로 둘 다 사용이 가능합니다.

 

fopen의 두번째 인자는 작업할 목적에 맞게 다음의 문자의 조합으로써 사용할 수가 있습니다.

 

문자

의미

r

읽기 모드, 작업 포인터는 맨 앞, 파일이 없으면 실패(NULL반환)

w

쓰기 모드, 작업 포인터는 맨 앞, 파일이 없으면 새로 생성

a

첨가 모드, 작업 포인터는 맨 뒤, 파일이 없으면 새로 생성

+

혼용 모드

b

바이너리 파일로

t

텍스트 파일로

 

 

 

 

 

r, w, a, +는 작업할 목적에 따라 선택을 하여 조합하게 됩니다.  특히, w모드로 열었을 경우 해당 파일이 없을 경우 새로운 파일을 생성하며 파일이 있을 경우에는 파일의 내용을 비우고 연다는 것에 주의합시다.

 

다음은 프로그램 데이터를 파일에 저장하는 부분과 파일에 내용을 프로그램 데이터로 로딩하는 부분에 대한기본적인 형태입니다.

 

void fnLoad(const char *fname)

{

    FILE *fp = 0;

    fp = fopen(fname,”r”);

    if(fp)

    {

        //로딩 작업

        fclose(fp);

    }

}

 

void fnSave(const char *fname)

{

    FILE *fp = 0;

    fp = fopen(fname,”w”);

    if(fp)

    {

        //저장 작업

        fclose(fp);

    }

}

 

파일 모드중에 b, t는 파일의 형태를 선택하는 것인데 b는 바이너리 파일 형태로 t는 텍스트 파일 형태로 처리한다는 의미입니다.  바이너리 파일이라 하면 실행파일, 목적파일 등 이진파일을 의미하고 텍스트 파일은 일반 파일을 의미를 합니다.  다만, 최근에 파일의 포맷은 각 회사에 제품에 따라 정하고 있어서 큰 의미를 갖지 못하고 있습니다.  이 책에서도 해당 부분에 대해서는 언급하지 않고 넘어가도록 하겠습니다.

 

1.3           파일에 쓰기, 읽기

이제 실제 파일 입출력을 하는 쓰기 및 읽기에 관련된 라이브러리 함수에 대해서 알아보기로 합시다.  라이브러리를 통한 파일 입출력에서는 크게 두 가지 방법에 의해 입출력 작업을 할 수가 있습니다.  첫째는 메모리 단위로 입출력 하는 것이고 둘째는 ASCII코드에 의거해서 데이터를 처리하는 방법입니다.  이중에 두번째 방식은 이미 scanf, printf등의 함수들에 의해 사용하고 있는 방법입니다.

 

size_t fread(void *, size_t, size_t, FILE *);

size_t fwrite(void *, size_t, size_t, FILE *);

 

fread, fwirte함수는 메모리 단위로 입출력을 하는 함수이고 입력 매개변수나 리턴 타입이 동일합니다.  첫번째 입력 매개변수는 프로그램 데이터를 보관하는 메모리 주소이고 두번째 매개변수는 유닛하나의 크기, 세번째 매개변수는 유닛의 개수, 네번째 매개변수는 작업할 FILE 포인터입니다. 

 

 다음은 member base[MAX_MEMBER]; 와 같이 회원데이터 MAX_MEMBER개로 구성된 base배열로 회원 데이터를 관리하고 있을 때 Load Save에 관련된 예제입니다.

 

void fnLoad(const char *fname)

{

           FILE *fp;

 

           if((fp = fopen(fname,"r"))!=NULL)

           {

                     fread(base,sizeof(member),MAX_MEMBER,fp);

                     fclose(fp);

           }

           else

           {

                     printf("환영!!  처음 사용하시는 군요.\n");

           }

}

 

void fnSave(const char *fname)

{

           FILE *fp;

 

           if((fp = fopen(fname,"w"))!=NULL)

           {

                     fwrite(base,sizeof(member),MAX_MEMBER,fp);

                     fclose(fp);

           }

           else

           {

                     printf("저장하지 못했습니다.\n");

           }

}

 

fread fwrite의 리턴 값은 몇 개의 유닛에 대해 처리가 되었는지에 대한 부분입니다.  쓰기 작업에서 정상적인 상황이라면 fwrite의 세번째 매개변수와 리턴값은 동일할 것입니다.  하지만, 읽기 작업에서는 요구하는 유닛의 수보다 저장된 내용이 작다라면 리턴값이 세번째 매개변수보다 작을 수 있습니다.  이 때는 파일의 끝을 만난 것으로 파악할 수 있습니다.

 

int fscanf(FILE *, const char *, ...);

int fprintf(FILE *, const char *, ...);

int fgetc(FILE *);

int fputc(int, FILE *);

char *fgets(char *, int, FILE *);

int fputs(const char *, FILE *);

 

위의 함수들은 기본 입출력에서 이미 학습한 함수들과 비슷할 것입니다.  scanf함수가 stdin으로부터 포맷에 맞게 데이터를 읽는 함수라면 fscanf함수는 첫번째 입력매개변수로 온 FILE 포인터로부터 포맷에 맞게 데이터를 읽는 함수라는 차이를 갖을 뿐 처리하는 모든 동작은 동일합니다.  , 앞에서 배웠던 기본 입출력 함수들은 stdin이나 stdout으로부터 데이터를 읽거나 쓰는 작업을 하는 함수였는데 여기에 있는 함수들은 작업할 파일 포인터를 개발자가 선택하여 매개변수로 넘겨준다는 차이가 있을 뿐입니다.  다음은 fscanf fpritnf함수를 통해 위의 fnSave, fnLoad를 구현한 예제입니다.

 

void fnLoad(const char *fname)

{

           FILE *fp;

           int index = 0;

 

           if((fp = fopen(fname,"r"))!=NULL)

           {

                     fscanf(fp,"%d ",&index);

                     while(!feof(fp))  //파일의 끝을 만나지 않았다면

                     {

                                fscanf(fp,"%s ",arr[index].name);

                                fscanf(fp,"%d ",&arr[index].sex_info);

                                fscanf(fp,"%d ",&arr[index].age);

                                g_data[index].mnum = index+1;

 

                                fscanf(fp,"%d ",&index);

                     }

                     fclose(fp);

           }

           else

           {

                     printf("환영!!  처음 사용하시는 군요.\n");

           }

}

 

void fnSave(const char *fname)

{

           FILE *fp;

           int index = 0;

           if((fp = fopen(fname,"w"))!=NULL)

           {

                     while(index < MAX_STUDENT)

                     {

                                if(arr[index].mnum)

                                {

                                          fprintf(fp,"%d ",index);

                                          fprintf(fp,"%s ",arr[index].name);

                                          fprintf(fp,"%d ",arr[index].sex_info);

                                          fprintf(fp,"%d ",arr[index].age);

                                }

                                index++;

                     }

                     fclose(fp);

           }

           else

           {

                     printf("저장하지 못했습니다.\n");

           }

}

 

  두 가지 방법으로 파일 입출력에 관한 간단한 예제를 보여주었는데 이들을 보면 확연한 차이점을 알 수가 있을 것입니다.  fread, fwirte를 사용하는 경우에는 파일에 쓰거나 읽는 것이 메모리 단위로 이루어지기 때문에 저장할 데이터가 연속적으로 되어 있다면 한 번의 호출에 의해 파일 입출력을 할 수가 있습니다.  하지만, fscanf, fprintf를 사용하는 경우에는 약속된 포맷에 맞추어 파일 입출력을 하는 것이기 때문에 개발자가 포맷을 잘 정하고 사용해야 합니다.  특히, 위처럼 구조체에 있는 데이터를 입출력 하고자 할 때 쓰려고 하는 단위 사이사이에 공백문자를 넣어야 한다.(“%s ")   만약 3번 회원의 이름이 “jang”이고 성별이 남자(1)이고 나이가 23이라고 할 때 공백을 주지 않고 저장한다면 “jang123”이라고 쓰게 되는데 이를 로딩을 할 때에는 이름을 얻는 부분에서 “jang123”을 하나의 문자열로 로딩을 하여 엉뚱하게 처리가 되어 버릴 것입니다.  , 포맷에 따라 처리할 때 처리 기준이 공백, , ‘\n’라는 것을 고려하여 사용해야 합니다.  그리고, fread fwrite로 파일에 저장한 내용을 notepad와 같은 응용으로 잘 저장되었는지 확인하는 것은 무의미 합니다.  notepad같은 응용은 텍스트 파일을 읽는 응용이지 바이너리 포맷을 읽는 응용이 아니라서 제대로 저장이 되었다 하더라도 마치 깨진 파일처럼 보일테니 말입니다. 

 

int sscanf(const char *, const char *, ...);

int sprintf(const char*, const char *, ...);

 

위의 함수는 파일 입출력이 아니라 버퍼 입출력에 관련된 함수입니다.  기본적으로 파일 입출력이 buffered I/O로 수행하다 보니 위와 같은 라이브러리가 생기게 되었습니다.  이를 잘 활용하면 많은 부분에서 데이터 변환이 유용하니 잘 사용하시기 바랍니다.  사용 예는 다음과 같습니다.

 

static char cmsbuf[MAX_BLEN];

 

int cmSatoi(char *str)

{

int re=0;

sscanf(str,"%d",&re);

return re;

}

 

float cmSatof(char *str)

{

float re=0;

sscanf(str,"%f",&re);

return (float)re;

}

 

int cmSMergeKey(int skey1,int skey2)

{

int key=0;

sprintf(cmsbuf,"%x%x",skey1,skey2);

sscanf(cmsbuf,"%x",&key);

return key;

}

 

char *cmSStrcat(char *str1,char *str2)

{

sprintf(str1+strlen(str1),"%s",str2);

return str1;

}

 

 

이 외에도 작업 위치를 확인하거나 변경하는 ftell, fseek함수나 파일 작업 중 에러가 났었는지를 확인하는 ferror, 파일 작업 중 파일의 끝을 만났는지 확인하는 feof등의 함수들이 있습니다.  각자가 msdn과 같은 도움 기능을 통해 각 함수의 signature를 확인하고 사용방법을 익히시기 바랍니다.


제주삼다수, 2L,... 오뚜기 진라면 매운... 상하목장 유기농 흰... 남양 프렌치카페 카... 고려인삼유통 홍삼 ... 종근당건강 오메가3... 요이치 카링 유무선...