본문 바로가기
프로그래밍 언어/C, C++의 기본적인 설명

[C/C++] 기본적인 파일 입출력 함수

by UltraLowTemp-Physics 2022. 3. 7.
728x90

요약

1. C 프로그램은 입력을 바이트들의 스트림으로 인식한다. 
  - 파일, 키보드와 같은 입력장치, 다른 프로그램의 출력은 스트림의 소스가 될 수 있음 
2. C에서 특정 파일에 접근하기 위해선, 
   1) 파일 포인터(FILE *형)을 생성 
   2) 그 포인터에 하나의 특정 파일 이름을 연결 
3. 파일의 끝
   - C의 입력 함수들은 파일의 끝을 나타내는 EOF 문자를 읽으면, 파일의 끝에 도달했다고 인식한다. 

1. 텍스트 인식 vs 바이너리 인식

ANSI C는 파일을 읽을 때, 두 가지 인식을 제공한다; 텍스트 인식(text)과 바이너리(binary) 인식 

2. 입출력 수준

두 가지 입출력 수준: 저수준 입출력 (Low-level I/O)와 표준 고수준 입출력 (Standard high-level I/O) 
 1) 저수준 입출력: 운영체제가 제공하는 기본적인 입출력 서비스 
 2) 표준 고수준 입출력: C라이브러리 함수들의 표준 패키지와 stdio.h 헤더파일 정의들을 사용함 
     - 모든 운영체제가 동일한 저수준 입출력을 사용한다고 보장하지 않기 때문에, ANSI C는 표준 입출력 패키지만 제공함 
     - 즉, ANSI C가 표준 입출력 모델의 이식성을 보장

3) 표준 파일: 표준입력 (standard input), 표준 출력 (standard output), 표준 에러 출력 (standard error output) 
   - C 프로그램은 사용자를 위해, 위 3개의 파일들을 자동으로 연다. 

표준 파일  설명
표준 입력  - 디폴트로서, 일반적으로 시스템이 사용하는 키보드와 같은 입력 장치 
- 프로그램에 입력을 제공 
- C 함수: getchar(), gets(), scanf()
표준 출력 - 디폴트로서, 일반적으로 시스템이 사용하는 디스플레이 스크린과 같은 출력 장치
- 프로그램의 정상적인 출력을 나타냄 
- C 함수: putchar(), puts(), printf()
표준 에러 - 디폴트로서, 일반적으로 시스템이 사용하는 디스플레이 스크린과 같은 출력 장치 
- 에러 메시지를 출력할 논리적으로 구분되는 어떤 장소를 제공함 

* 리다이렉션은 다른 파일들을 표준 입출/출력으로 인식하게 함 

3. 표준 입출력

표준 입출력 패키지는 저수준 입출력에 비해 다음 세 가지 장점을 가진다. 
  (1) 이식성 
  (2) 다양한 입출력을 처리하는 전문화된 함수들 제공 
  (3) 입력과 출력에 버퍼*(buffer)를 사용 

1) 버퍼란?  
 - 정보가 한번에 한 바이트씩 전달되지 않고, 큰 덩어리로 버퍼라는 공간에 전송이 됨. 
 - 이러한 버퍼링은 데이터의 전송 속도를 크게 증가 시킴 

파일 포인터 

1) 파일포인터란?
  - stdio.h에 정의되어있는 유도 데이터 
  - 파일 포인터는 실제 파일을 가리키지 않은다. 대신, 그 파일의 입출력에 사용되는 버퍼 정보를 포함하여 그 파일에 관련된 정보가 들어있는 데이터들을 가리킴 
2) stdin, stdout, stderr 
  - stdio.h 헤더 파일은 다음 세 개의 파일 포인터를 표준 파일에 연결한다. 
  - stdin, stdout, stderr는 모두 FILE을 가리키는 포인터형이다.

 표준파일 파일 포인터  일반적으로
표준입력 stdin 키보드
표준 출력 stdout 스크린
표준 에러 stderr 스크린

 

함수 1: fopen() 함수  

 1) Desciption: 
   (a) 해당 함수를 이용하여 파일을 연다.  
   (b) Reference: https://www.cplusplus.com/reference/cstdio/fopen/   
 2) Header file: stdio.h
 3) Syntax: fopen(const char *filename, const char *mode)    
   (a) const char *filename: 열려고 하는 파일의 이름 (또는 파일 이름을 가지고 있는 문자열의 주소)
   (b) const char *mode: 파일을 열 때, 사용되는 모드 문자열들 
 4) Return: 
   (a) 해당 함수가 정상적으로 파일을 열게되면, 파일 포인터를 리턴한다. 정상적으로 열지 못한 경우, NULL 포인터를 리턴함.
   (b) 파일 포인터: 해당 파일을 나타내는데 사용되는 포인터 

* fopen의 모드 

모드 문자열 읽기
"r" - 읽기를 위해 파일을 텍스트 모드로 연다 
- "r+": 갱신을 하기 위해 텍스트 모드로 연다 
"w" - 쓰기를 위해 파일을 텍스트 모드로 연다. 
- 파일이 이미 존재하면 파일의 길이를 0으로 만든다. 
- 파일이 없으면 새 파일을 만든다.  
- "w+": 파일을 갱신하기 위해 텍스트 모드로 연다 
"a" - 쓰기를 위해 파일을 텍스트 모드로 연다. 
- 파일이 이미 존재하면 그 파일의 끝에 덧붙임 
- 파일이 없으면 새 파일을 만듬
"rb" - 읽기를 위해 바이너리 모드로 연다. 
 "wb" - 읽기를 위해 바이너리 모드로 연다. 
- 파일이 존재하면 파일의 길이를 0으로 만든다.  
"ab" - 파일을 읽기 위해 바이너리 모드로 연다. 
- 파일이 이미 존재하면 그 파일의 끝에 덧붙인다. 


 함수 2: fclose() 

- fclose(fp)
- 필요할 때 버퍼를 비우면서 fp가 가리키는 파일을 닫음 
- 리턴값: fclose()가 파일을 성공적으로 닫으면 0을 리턴하지만 그렇지 않을 경우, EOF를 리턴함 
- 파일이 정상적으로 닫혔는지 확인하는 코드 

if (fclose(fp) != 0) {
	printf("There is an error when a file closes");
}

 

Note: 파일 끝 (EOF: End of File)
1) EOF라는 특별한 문자를 통해, 프로그램은 파일의 끝에 도달했음을 확인할 수 있음. 
2) EOF를 확인하는 코드 
   - 파일 스트림으로부터 문자를 하나씩 읽는 과정에서, 파일의 끝에 도달했는지를 확인하는 코드 

int chl
FILE *fp;
fp = fopen("example.txt", "r");
while((ch = getc(fp)) != EOF){
	putchar(ch);
}

 

 

파일 입/출력 함수 1: getc() 및 putc() 함수 

 1) getc()와 putc()는 getchar()와 putchar()와 비슷하게 동작함. 
  - 차이점: getc()와 putc()의 경우, 어느 파일을 사용할 것인지 알려줘야 함. 
  - getchar()와 putchar()는 getc()와 putc()로부터 각각 표준입력과 표준출력을 사용하여 정의한다. 

2) getc(*FILE file_in_pointer); 
  - file_in_pointer가 가리키는 파일로부터 하나의 문자를 얻음 
3) putc(char ch, *FILE file_out_pointer): 
  - 문자 ch를 file_out_pointer이 가리키는 파일에 출력한다. 
 ※ NOTE
  - stdout: 표준 출력에 사용되는 파일 포인터로 정의 
  - 아래의 두 표현은 동일하다. 

char ch = 'a';
putc(ch, stdout);
putchar(ch);

 

 

 

파일 입출력 함수: fprintf(), fscanf(), fgets(), fputs()
- 아래의 파일 입출력 함수들은 문자와 문자열을 다루는 텍스트 지향적인 함수들이다. 
- 만일 수치 데이터를 저장한다면, fread() 및 fwrite()와 같이 바이너리 데이터를 입출력할 수 있는 함수를 사용해야 한다. 
- 아래 함수들은 순차 접근 (Sequential Access)* 함수들이다. 
   * 순차접근: 파일 내용을 순차적으로 처리함 

   
   
fgets(char *string, int num, FILE *fptr) 1. parameters
 a. 첫번째 인자:
   - 입력을 저장할 주소 (파일로부터 읽은 문자열을 저장할 배열) 
   - char형 배열의 이름
 b. 두번째 인자: 입력 문자열의 최대 크기를 나타내는 정수 
 c. 세번째 인자: 읽을 파일을 가리키는 파일 포인터
2. 설명 
- 상한(upper limit)보다 하나 적은 수의 문자들을 읽을 때까지 또는 파일을 끝을 만날 때까지 문자들을 읽되, 첫 개행 문자까지 읽는다. 그 후 읽은 것이 문자열이 되도록 종결 널문자(\0)을 추가한다. 
3. gets()와 차이점: 
 a. gets()은 개행문자를 버린 후, 널문자를 추가하지만, fgets() 함수는 만약 개행문자를 읽는다면, 개행문자 뒤에 널문자를 추가한다. 
 b. fgets()는 기억 공간 오버플로를 방지할 수 있기 때문에, gets()보다 중요한 프로그램에서 더 사용하기 좋음 
 c. fgets()는 개행을 읽어 문자열 안에 넣고 puts()는 출력에 개행을 넣기 때문에, fgets()는 fputs()과 함께 사용해야 한다. 
4. 리턴값
  - EOF를 만났을 경우, NULL 값을 리턴함 
  - 그렇지 않은 경우, 함수에 전달된 주소를 리턴함  
   

예시) fgets()를 사용하여, 표준 입력(키보드)으로부터 문자들을 읽은 후, 표준 출력으로 다시 출력

#incldue <stdio.h>
#define MAXLINE 20

int main(void){
	char line[MAXLINE];
    while (fgets(line, MAXLINE, stdin) != NULL && line[0] != '\n') fputs(line, stdout);
}

 

바이너리 파일 입출력: fread(), fwrite() 함수 

파일이 저장될 때, 만일 fprintf() 함수와 같은 텍스트 지향적인 함수를 사용하게 되면, 수치데이터가 문자열로 변환 후 저장이 되는 것이기에 값이 변하거나 원래 수치데이터의 정밀도를 정확하게 회복할 수 없다. 따라서 수치 데이터를 저장할 때에는 바이너리 데이터 그 자체를 저장하여 데이터 손실이 없도록 해야 한다. 

함수 설명
size_t fwrite() • 바이너리 데이터를 파일에 저장한다. 
• size_t fwrite(const void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp); 
  - ptr: 저장할 데이터 덩어리의 주소 
  - size: 저장할 그 데이터의 덩어리 크기 (바이트 단위) 
  - nmemb: 저장할 데이터 덩어리들의 수 
  - fp: 그 데이터가 저장될 파일 
• 리턴값: 성공적으로 저장한 항목의 수를 리턴 (nmemb). 단, 에러가 발생하면, nmemb보다 작을 수 있음 
• example 
   char buffer[256];
   fwrite(buffer, sizeof(char), 256, fp)

size_t fread() • 바이너리 데이터를 파일로부터 읽음 
• size_t fread(void * restrict ptr, size_t size, size_t nmemb, FILE * restrict fp); 
  - 전달인자는 fwrite()와 동일 
• 리턴값:
  - 성공적으로 읽은 항목의 수를 리턴 (nmemb).
  - 단 에러가 발생하면, nmemb보다 작을 수 있음

 

 

파일의 임의 접근: fseek(), ftell(), fgetpos(), fsetpos()

함수 설명
fseek() • fseek(FILE * stream, long int offset, mode);
  a. steam: 처리할 파일을 가리키는 FILE 포인터 
  b. offset:
    - 시작 위치로부터 얼마나 멀리 가야하는지를 알려줌 
    - 해당 값은 long으로 정의되어야 함 
    - 양수: 앞으로 이동/ 0: 현재 위치/ 음수: 뒤로 이동 
  c. 모드: 시작 위치를 나타냄 (자세한 내용은 아래 참조)
• 리턴값: 모든 것이 성공적이면 0을 리턴, 그렇지 않으면 -1를 리턴 
ftell() • 파일의 현재 위치를 리턴함 
  - 파일의 시작으로부터 첫 바이트를 0으로 하는 바이트 수를 리턴함으로써 파일 위치를 지정 
fgetpos() fseek() 및 ftell()의 문제점: 파일 크기를 long형으로 나타낼 수 있는 값으로 제한함  
• fgetpos(FILE * restrict stream, fpos_t * restrict pos); 
 - 호출되었을 때, fpos_t값을 pos가 가리키는 위치에 넣는다. 해당 위치는 파일 안의 어떤 위치를 나타냄 
 - 성공하면 0을 실패하면 0이 아닌 값을 리턴함 
 - fpos_t: 해당 변수나 데이터 객체는 파일 안에서 어떤 위치를 지정할 수 있다. 
fsetpos() • int fsetpos(FILE *stream, const fpost_t *pos);
 - pos가 가리키는 위치에 있는 fpos_t 값을 사용하여, 파일 포인터를 그 값이 나타내는 위치로 설정한다. 
 - fpos_t 값은 바로 전의 fgetpos() 호출에서 얻은 값이어야 함 
• 리턴값: 
 - 성공하면 0, 실패하면 0이 아닌 값을 리턴 

fseek() 및 ftell()의 모드 

mode  시작 위치
SEEK_SET 파일의 시작
SEEK_CUR 현재 위치
SEEK_END 파일의 끝

예시) 

FILE *fp;
fp = fopen("example.txt", "r");
fseek(fp, 0L, SEEK_SET);      // 파일의 시작으로 간다.
fseek(fp, 10L, SEEK_SET);     // 파일의 시작위치에서 10바이트 앞으로 이동 
fseek(fp, 2L, SEEK_CUR);      // 파일의 현재 위치에서부터 2바이트 앞으로 이동 
fseek(fp, 0L, SEEK_END);      // 파일의 마지막위치로 이동 
fseek(fp, -10L, SEEK_END);    // 파일의 마지막 위치에서 10바이트 뒤로 이동

파일의 에러: feof() 및 ferror() 함수

표준 입력 함수가 EOF를 반환할 때의 두 가지 가능성: 
   1) 파일의 끝에 도달 
   2) 예기치 못한 에러로 인한 EOF 반환 
따라서, 두 가능성을 구별할 수 있는 함수들이 필요하다. 

함수 설명
int feof(FILE *fp); - 마지막 입력 호출에서
  a. 파일의 끝을 만나면, 0이 아닌 값을 리턴함 
  b. 파일의 끝이 아닐 경우, 0을 리턴
int ferror(FILE *fp) - 읽기 에러나 쓰기 에러가 
  a. 발생하는 경우: 0이 아닌 값을 반환 
  b. 발생하지 않은 경우: 0을 반환 

 

 

 

Appendix 1: 바이너리 입출력을 사용하는 임의 접근

#include <stdio.h>
#include <stdlib.h>
#define ARSIZE 1000

int main(){
    double number[ARSIZE];
    double value;
    const char *file = "number.dat";
    
    
    long pos;
    FILE *iofile;
    
    // double 형으로 이루어진 한 집합을 만듦
    for (int i =0; i < ARSIZE; i++)
    	number[i] = 100.0 + i + 1.0/(i + 1);
    
    // 파일을 열기 위해 시도함 
    if ((iofile = fopen(file, "wb")) == NULL){
    // stderr로 메시지를 출력함 
    	fprintf(stderr, "출력을 위한 %s 파일을 열 수 없습니다.\n", file);
        exit(1);
    }
    // 배열을 바이너리 포맷으로 파일에 저장 
    fwrite(number, sizeof(double), ARSIZE, iofile);
    fclose(iofile);
    
    // 저장한 파일을 다시 읽어서 확인하는 절차
	if ((iofile = fopen(file, "rb")) == NULL){
    	fprintf(stderr, "임의 접근을 위한 %s 파일을 열 수 없습니다.\n", file);
        exit(1);
    }
    
    // 파일로부터 선택된 항목을 읽음 
    printf("0-%d 범위 내에서 하나의 인덱스를 입력하시오.\n", ARSIZE - 1);
    int i;
    scanf("%d", &i);
    while(i > 0 && i < ARSIZE){
    	pos = (long) i * sizeof(double);          // 오프셋을 계산함 
        fseek(iofile, pos, SEEK_SET);             // 오프셋 위치로 이동함 
        fread(&value, sizeof(double), 1, iofile); // 해당 오프셋에 위치한 값을 읽음
        printf("그 위치에 있는 값은 %f입니다.\n", value);
        printf("다음 인덱스를 입력하시오:");
        scanf("%d", &i);
    }
	fclose(iofile);
}
728x90

댓글