File High-Level I/O에 대해 알아보자

File: High-level I/O

I/O Stream

  • Stream: 물줄기, 개울, 시내

    • I/O stream: 데이터가 흘러가는 것. 데이터의 이동을 표현하는 말
      • 예) Media streaming
      • Source 에서 Destination 으로 데이터가 이동함
      • 프로그램은 데이터가 이동하는 통로로, source 이자 destination 일 수 있음
  • Input stream: Source로부터 데이터가 들어오는 경로

    • 예) Keyboard -> Program (scanf() 로 stream 을 연결할 수 있음)
  • Output stream: Destination으로 데이터가 나가는 경로

    • 예) Program -> Console (printf() 로 stream 을 연결할 수 있음)
  • 입력 스트림(input stream): 다른 곳에서 프로그램으로 들어오는 경로

    • 자료가 떠나는 시작 부분이 자료 원천부 (data source)
    • 표준입력: 원천부가 키보드
      • 파일입력: 파일이면 파일로부터 자료를 읽는 것
      • 스크린입력: 터치스크린이면 스크린에서 터치 정보
      • 네트워크입력: 다른 곳에서 프로그램으로 네트워크를 통해 자료가 전달
  • 출력 스트림(output stream) : 프로그램에서 다른 곳으로 나가는 경로

    • 자료의 도착 장소가 자료 목적부 (data destination)
    • 표준출력: 목적부가 콘솔
      • 파일출력: 파일이면 파일에 원하는 값을 저장
      • 프린터출력: 프린터이면 프린터에 출력물
      • 네트워크출력: 네트워크이면 네트워크 출력이 되어 다른 곳으로 자료가 이동

텍스트 파일과 이진파일

  • 텍스트 파일: 메모장(notepad) 같은 편집기로 작성된 파일

    • 내용이 아스키 코드(ascii code)와 같은 문자 코드값으로 저장
    • 메모리에 저장된 실수와 정수와 같은 내용도 문자 형식으로 변환되어 저장
    • 텍스트 편집기를 통하여 그 내용을 볼 수 있고 수정 가능
  • 이진 파일: 실행파일과 그림 파일, 음악 파일, 동영상 파일 등

    • 목적에 알맞은 자료가 이진 형태(binary format)로 저장되는 파일
    • 자료는 메모리 자료 내용에서 어떤 변환도 거치지 않고 그대로 파일에 기록
    • 입출력 속도도 텍스트 파일보다 빠름
    • 메모장과 같은 텍스트 편집기로는 그 내용을 볼 수 없음
    • 내용을 이미 알고 있는 특정한 프로그램에 의해 인지될 때 의미가 있음

파일 스트림과 포인터

  • 표준 입출력 라이브러리에서 제공하는 고수준 파일 입출력
    • File stream 이라는 서비스를 사용자에게 제공한다.
      • 저수준IO: 파일 vs. 고수준IO: 파일 스트림
  • FILE* : 파일 포인터
    • 고수준 파일 입출력에서 열린 파일을 가리키는 포인터
    • 자료형으로 FILE * 형을 사용 -> 구조체에 대한 포인터

image

파일 스트림 열기

  • 파일 스트림 열기: fopen(3)
#include <stdio.h>

FILE *fopen(const char *pathname, const char *mode);
  • Pathname으로 지정한 파일을 mode로 지정한 모드에 따라 열고 파일 포인터를 리턴
  • mode 값
    • 파일모드에서 +의 삽입은 수정(update) 모드 의미
      • 원래의 모드에서 읽기 또는 쓰기가 추가되는 모드
      • 수정(update) 모드에서는 모드 간의 전환이 가능
    • 파일모드 r+
      • 처음에 읽기 모드로 파일을 열어 쓰기 모드로 전환 가능
      • 파일이 없으면 오류가 발생
    • 파일모드 w+
      • 처음에 쓰기 모드로 파일을 열어 필요하면 읽기 모드로 전환 가능
      • 만일 파일이 존재한다면 이전의 내용은 모두 사라짐
    • 파일모드 a+
      • 처음에 추가 모드로 파일을 열어 필요하면 읽기 모드로 전환 가능

파일 스트림 닫기

  • 파일 스트림 닫기: fclose(3)
#include <stdio.h>

int fclose(FILE *stream);
  • Fopen() 으로 오픈한 파일 스트림을 닫는다.
  • Close() 와 마찬가지로, 파일 스트림의 사용 종료를 알리는 역할이며, 저장 장치에 내용이 기록되는 것을 보장하지는 않는다
    • 저장 장치에 즉시 기록하려면: fflush(3)를 사용
  • RETURN VALUE
    • On success: 0
    • On error: EOF
      • 성공하든, 실패하든 파일 접근은 더 이상 하면 안 됨

버퍼 기반 읽기 및 쓰기

  • 버퍼 기반 입력함수: fread(3)
    • Stream으로 지정한 파일로부터, 항목의 크기가 size인 데이터를 nitems에 지정한 개수만큼 읽어 ptr에 저장 (구조체의 입출력에 적합함)
    • 성공하면 읽어온 항목 수를 리턴
    • 읽을 항목이 없으면 0을 리턴
#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t nitems, FILE *stream);
  • 버퍼 기반 출력함수: fwrite(3)
    • 항목의 크기가 size인 데이터를 nitems에서 지정한 개수만큼 ptr에서 읽어서 stream으로 지정한 파일에 출력
    • 성공하면 출력한 항목의 수를 리턴
    • 오류가 발생하면 EOF를 리턴
#include <stdio.h>

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

파일 오프셋 지정

  • 파일 오프셋 이동: fseek(3)
    • stream이 가리키는 파일에서 offset에 지정한 크기만큼 오프셋을 이동
    • whence는 lseek()와 같은 값을 사용
    • 성공하면 0을, 실패하면 EOF를 리턴
#include <stdio.h>

int fseek(FILE *stream, long offset, int whence);
  • 현재 오프셋 구하기: ftell(3)
    • 현재 오프셋을 리턴. 오프셋은 파일의 시작에서 현재 위치까지의 바이트 수
#include <stdio.h>

long ftell(FILE *stream);
  • 함수 fseek(f, 100L, SEEK_SET)의 호출
    • 파일 위치를 파일의 처음 위치에서 100바이트 떨어진 위치로 이동
  • 함수 fseek(f, 100L, SEEK_CUR)의 호출
    • 파일의 현재 위치에서 100바이트 떨어진 위치로 이동
  • 함수 fseek(f, -100L, SEEK_END)의 호출
    • 파일 끝 위치에서 앞으로 100바이트 떨어진 위치로 이동
  • 함수 fseek()에서 offset
    • 양수이면 파일의 끝점으로,
    • 음수이면 파일의 시작점으로의 이동방향을 표시

image

  • 처음 위치로 오프셋 이동: rewind(3)
    • 오프셋을 파일의 시작 위치로 즉시 이동
#include <stdio.h>

void rewind(FILE *stream);
  • 오프셋의 저장과 이동: fsetpos(3), fgetpos(3)
    • fgetpos() : 파일의 현재 오프셋을 pos가 가리키는 영역에 저장
    • fsetpos() : pos가 가리키는 위치로 파일 오프셋을 이동
#include <stdio.h>

int fsetpos(FILE *stream, const fpos_t *pos);
int fgetpos(FILE *stream, fpos_t *pos);

Text File I/O

fprintf() and fscanf()

  • 함수 fprintf()와 fscanf() 또는 fscanf_s()를 이용
    • 텍스트 파일에 자료를 쓰거나 읽기 위하여
    • 헤더 파일 stdio.h를 포함
      • 첫 번째 인자는 입출력에 이용될 파일
      • 두 번째 인자는 입출력에 이용되는 제어 문자열
      • 다음 인자들은 입출력될 변수 또는 상수 목록
#include <stdio.h>

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

int fscanf(FILE *stream, const char *format, ...);
  • 함수 fprintf()와 fscanf() 또는 fscanf_s()의 첫 번째 인자에 각각 stdin 또는 stdout를 이용하면 표준 입력, 표준 출력으로 이용이 가능
  • Return value
    • fprintf(): 기록한 문자 수. 문자열 마지막의 NULL 문자는 제외
    • fscanf(): 읽은 아이템 수 혹은 파일의 끝에 도달한 경우 EOF

feof() and ferror()

  • 함수 feof(): 파일 스트림의 EOF(End Of File) 표시를 검사하는 함수
    • 읽기 작업이 파일의 이전 부분을 읽으면 0을 반환하고 (EOF 아님)
      • 그렇지 않으면 0이 아닌 값을 반환 (파일 끝!)
      • 이전 읽기 작업에서 EOF 표시에 도달하면 0이 아닌 값으로 지정 (파일 끝!)
    • 단순히 파일 지시자가 파일의 끝에 있더라도 feof()의 결과는 0
      • 파일 끝에 도달한 다음, 한 번 더 읽기 동작이 수행되어야 EOF 가 셋팅됨
      • 이 동작 때문에 실제 사용 시, 잘못 사용하는 경우가 많음 (사용 지양)
  • 함수 ferror(): 파일 처리에서 오류가 발생했는지 검사하는 함수
    • 이전 파일 처리에서 오류가 발생하면 0이 아닌 값을 반환 (오류!)
      • 오류가 발생하지 않으면 0을 반환 (정상)
    • 헤더파일 stdio.h 필요

Additional services: fgetc() and fputc()

#include <stdio.h>
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
  • 함수 fgetc()
    • 파일로부터 문자 하나를 입력받는 함수
    • Return value
      • int 형 으로 casting 된 문자 하나를 반환
      • 만약 파일 끝인 경우, EOF 를 반환하며, 에러에는 0이 아닌 값
  • 함수 fputc()
    • 문자 하나를 파일로 출력하는 함수
    • 함수들은 문자 하나의 입출력의 대상인 파일 포인터를 인자로 이용
  • getc()putc()도 존재함. 그러나 잊어버릴 것.
    • Macro 로 구현되어 있을 가능성이 있어, fgetc(), fputc()보다 빠를 수도 있지만, 예상치 못한 문제점이 생길 수 있음

Additional services: fgets() and fputs()

#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
int fputs(const char *s, FILE *stream);
  • 함수 fgets(): 파일로부터 한 행의 문자열을 입력 받는 함수
    • 파일로부터 문자열을 개행문자(\n)까지 읽어 마지막 개행문자를 \0문자로 바꾸어 입력 버퍼 문자열에 저장
      • 첫 번째 인자는 문자열이 저장될 문자 포인터
      • 두 번째 인자는 입력할 문자의 최대 수
      • 세 번째 인자는 입력 문자열이 저장된 파일
    • 텍스트 파일을 처리할 때, 라인 별로 처리가 가능하여 아주 편리함
  • 함수 fputs(): 파일로 한 행의 문자열을 출력하는 함수
    • 문자열을 한 행에 출력
      • 첫 번째 인자는 출력될 문자열이 저장된 문자 포인터
      • 두 번째 인자는 문자열이 출력되는 파일
    • fprintf() 면 충분하지 않을까?

Binary file I/O

Text vs. Binary

  • Text I/O: fscanf() and fprintf()
    • 본래 파일은 “Collection of Bytes”
    • 텍스트 파일은? 자료의 입출력을 텍스트 모드(아스키 코드)로 처리
      • 텍스트 파일의 내용은 모두 지정된 아스키 코드와 같은 문자 코드값
    • 함수 fprintf()를 이용
      • int 형 변수 cnt의 값을 파일 f에 출력하는 과정
      • 실제로 파일에 저장되는 자료는 정수값 10에 해당하는 각 문자의 아스키 값
        • 각각의 문자 ‘1’과 ‘0’을 아스키 코드값으로 변환: 0x31, 0x30
        • 변환한 결과값을 저장함

image

  • Binary I/O: fread() and fwrite()
    • C 언어의 자료형을 유지하면서, 변환없이 그대로 바이트 단위로 저장
    • 입출력은 Low-level I/O(read() and write())와 같이 바이트 단위로 수행되지만,
    • 자료형에 따라 데이터 개체 단위로 이용할 수 있도록 인터페이스가 다름
      • Low-level I/O: file, buf, size
      • High-level I/O: file, buf, size and number of data objects
    • 예) char[4] 형 자료의 쓰기
      • char name[4]; // sizeof(name) = 4
      • Low-level: write(fd, name, 4); // 4B 사이즈의 데이터
      • High-level: fwrite(name, 4, 1, fp); // 4B 사이즈의 데이터를 1개 쓰기
    • 구조체 데이터를 읽고 쓰기에 적합함
      • C에서는 연관있는 데이터를 구조체로 엮어서 사용하는 경우가 많고,
      • Binary I/O 는 Text I/O 보다 성능 및 용량 면에서 효율적으로 I/O가 가능함

image

예제

예제 1) fread(), fwrite()

#include <stdlib.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
        FILE *rfp, *wfp;
        char buf[80];
        int i=0, count;

        if(argc != 3) {
                printf("< Usage: ./file4 file_for_read file_for_write >\n");
                return 1;
        }
        rfp = fopen(argv[1], "r");
        if (rfp == NULL) {
                perror("Open file for read");
                exit(1);
        }
        wfp = fopen(argv[2], "w");
        if (wfp == NULL) {
                perror("Open file for write");
                exit(1);
        }
        printf("%s and %s are opened! rfp = %p wfp = %p\n", argv[1], argv[2], rfp, wfp);

        while((count = fread(buf, 1, 10, rfp)) > 0 ) {
                printf("%d: count=%d\n", i++, count);
                fwrite(buf, 1, count, wfp);
        }
        fclose(rfp);
        fclose(wfp);
        return 0;

예제 2) fseek(), ftell()

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

char buf[80];
int  count;
FILE* fp;

void read_five_bytes() {
        if ((count = fread(buf, 1, 5, fp)) <= 0) {
                perror("Read Error");
                exit(1);
        }
}

int main(int argc, char* argv[]) {
        if(argc != 2) {
                printf ("< Usage: ./high2 filename >\n");
                return 1;
        }

        fp = fopen(argv[1], "r");
        if (fp == NULL) {
                perror("Open");
                exit(1);
        }
        printf("%s is opened! fp = %p\n", argv[1], fp);
        read_five_bytes();
        printf("\n%d: %s\n", count, buf);
        printf("Current position: %ld\n", ftell(fp));

        fseek(fp, 1, SEEK_SET);
        read_five_bytes();
        printf("\n%d: %s\n", count, buf);
        printf("Current position: %ld\n", ftell(fp));

        fseek(fp, 2, SEEK_SET);
        read_five_bytes();
        printf("\n%d: %s\n", count, buf);
        printf("Current position: %ld\n", ftell(fp));

        fclose(fp);

        return 0;
}

예제 3) fprintf()

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

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

        if(argc != 2) {
                printf("< Usage: ./high3 filename >\n");
                return 1;
        }
          
        fp = fopen(argv[1], "w"); 
        if(fp == NULL) {
                perror("Open");
                exit(1);
        }

        fprintf(fp, "%s %d %d %d %2.2f\n", "2015123", 80, 90, 100, 90.00);
        fprintf(fp, "%s %d %d %d %2.2f\n", "2016123", 50, 80, 60, 66.22);
        fprintf(fp, "%s %d %d %d %2.2f\n", "2017123", 70, 20, 70, 55.55);

        fclose(fp);
        return 0;
}    

예제 4) fscanf()

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
	FILE *fp;
	char id[10];
	int grade1, grade2, grade3;
	float avg;
	if(argc != 2) {
		printf("< Usage: ./high4 filename >\n");
		return 1;
	}

	fp = fopen(argv[1], "r");
	if(fp == NULL) {
		perror("Open");
		exit(1);
	}
	
	fscanf(fp, "%s %d %d %d %f\n", id, &grade1, &grade2, &grade3, &avg);
	fprintf(stdout, "%s %d %d %d %.3f\n", id, grade1, grade2, grade3, avg);

	fscanf(fp, "%s %d %d %d %f\n", id, &grade1, &grade2, &grade3, &avg);
	fprintf(stdout, "%s %d %d %d %.3f\n", id, grade1, grade2, grade3, avg);

	fscanf(fp, "%s %d %d %d %f\n", id, &grade1, &grade2, &grade3, &avg);
	fprintf(stdout, "%s %d %d %d %.3f\n", id, grade1, grade2, grade3, avg);

	fclose(fp);
	return 0;
} 

예제 5) fscanf() with feof()

#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char* argv[]) {
        FILE *fp;
        char id[10];
        int grade1, grade2, grade3;
        float avg;
        if(argc != 2) {
                printf("< Usage: ./high5 filename >\n");
                return 1;
        }

        fp = fopen(argv[1], "r");
        if(fp == NULL) {
                perror("Open");
                exit(1);
        }

        while(!feof(fp)) {
                fscanf(fp, "%s %d %d %d %f\n", id, &grade1, &grade2, &grade3, &avg);
                fprintf(stdout, "%s %d %d %d %.3f\n", id, grade1, grade2, grade3, avg);
        }

        fclose(fp);
        return 0;
}

예제 6-1) fgets() with feof()

#include <stdio.h>
#include <stdlib.h>

#define MAX_BUF	80

int main(int argc, char* argv[]) {
	FILE *fp;
	int line=0;
	char buf[MAX_BUF];

	if(argc != 2) {
		printf("< Usage: ./high6 filename >\n");
		return 1;
	}

	if((fp = fopen(argv[1], "r")) == NULL) {
		perror("Open");
		exit(1);
	}
	
	while(!feof(fp)) {
		fgets(buf, MAX_BUF, fp);
		fprintf(stdout, "%3d: %s", line++, buf);
	}

	fclose(fp);
	return 0;
}

예제 6-2) fgets()

#include <stdio.h>
#include <stdlib.h>

#define MAX_BUF	80

int main(int argc, char* argv[]) {
	FILE *fp;
	int line=0;
	char buf[MAX_BUF];

	if(argc != 2) {
		printf("< Usage: ./high6 filename >\n");
		return 1;
	}

	if((fp = fopen(argv[1], "r")) == NULL) {
		perror("Open");
		exit(1);
	}
	
	while(fgets(buf, MAX_BUF, fp) != NULL) {
		fprintf(stdout, "%3d: %s", line++, buf);
	}

	fclose(fp);
	return 0;
} 

예제 7) 구조체 파일 쓰기

#include <stdio.h>
#include <stdlib.h>

typedef struct {
        char id[10];
        int grade1, grade2, grade3;
        float avg;
} student;

int main(int argc, char* argv[]) {
        FILE *fp;
        int i;
        student data[3];

        if(argc != 2) {
                printf("< Usage: ./high7 filename >\n");
                return 1;
        }

        if((fp = fopen(argv[1], "w")) == NULL) {
                perror("Open");
                exit(1);
        }

        for (i=0; i<3; i++) {
                if(fscanf(stdin, "%s %d %d %d", data[i].id, &data[i].grade1, &data[i].grade2, &data[i].grade3) > 0) {
                        data[i].avg = (data[i].grade1+data[i].grade2+data[i].grade3)/3;
                        fwrite(&data[i], sizeof(student), 1, fp);
                }       else {
                        break;
                }
        }

        fclose(fp);
        return 0;
}

이러한 소스코드로 바이너리 파일을 생성한 뒤 vim 편집기로 :%!xxd로 hex 데이터로 확인하면 해당 값을 볼 수 있다. 이 때, 구조체 크기를 보면 원래는 10, 4, 4, 4,4 를 통해서 26이여야하나, 파일에서는 sizeof(student)는 28이다. 이 이유는 memory alignment로, 인텔 CPU는 데이터의 메모리 시작 주소를 항상 4의 배수가 되도록 정렬한다.

  • 이로 인해 10B 크기의 char id[10] 데이터가 저장된 다음, 2B 만큼의 padding space 가 생긴 것
  • 다음 int grade1 은 12B 위치부터 저장됨. 이후에는 모든 데이터가 memory aligned 된 위치이므로 padding 이 없음

댓글