Programming/C

C언어 - 파일 입출력

JunsuKim 2021. 10. 16.
728x90

프로그램상에서 파일에 저장되어 있는 데이터를 참고할 때 제일 먼저 해야 하는 것은 프로그램과 참조할 데이터가 작성돼 있는 파일 사이에 다리를 놓는 것이다. 이처럼 데이터 이동의 경로가 되는 다리를 "스트림"이라 한다.

스트림에 대해 잘 모르겠다면 이 글을 봐보는 것도 좋을 것이다.

https://jjunsu.tistory.com/26

 

C언어 - 스트림(Stream)

스트림(Stream) 스트림은 데이터의 이동 수단이다. 프로그램상에서 모니터로 문자열을 출력할 수 있는 이유가 무엇일까? 이는 문자열을 출력하는 printf 함수가 있기 때문이다. 그럼 printf 함수를 호

jjunsu.tistory.com

파일은 운영체제에 의해 구조가 결정, 관리되는 대상이기 때문에 파일 뿐만 아니라 스트림의 형성도 온전히 운영체제의 몫이다.

fopen함수

fopen함수는 스트림을 형성할 때 호출하는 함수이다.

함수의 호출을 통해 프로그램상에서 파일과의 스트림을 형성할 수 있다. 

#include<stdio.h>
FILE *open(const char*filename, const char*mode);

// 성공 시 해당 파일의 FILE 구조체 변수의 주소 값, 실패 시 NULL 포인터 반환

fopen함수의 첫번째 인자는 스트림을 형성할 파일의 이름이 온다.

두 번째 인자는 형성할 스트림의 종류에 대한 정보를 문자열의 형태로 전달한다.

이렇게 되면 해당 파일과의 스트림을 형성하고 스트림 정보를 FILE 구조체 변수에 담아 변수의 주소 값을 반환한다.

FILE이라는 이름은 기본 자료형이 존재하지 않으므로 구조체의 이름이다.

하지만 FILE 구조체 변수의 멤버에 직접 접근할 일이 없으므로 어떻게 정의되어 있는지 알 필요는 없다.

위 함수가 반환하는 FILE 구조체의 포인터는 파일을 가리키기 위한 용도로 사용된다.

이 포인터를 이용해 파일에 데이터를 저장 또는 저장된 데이터를 읽는다.

입력 스트림과 출력 스트림의 생성

fopen 함수에서 두 번째 인자는 형성할 스트림의 종류에 대한 정보를 문자열의 형태로 전달한다.

출력 스트림의 형성을 요청하는 fopen 함수의 호출문은 다음과 같다.

FILE * fp = fopen("data.txt", "wt");

이는 data.txt 파일과 wt모드로 스트림을 형성하는 것을 의미한다.

wt모드의 스트림은 텍스트의 데이터를 쓰기 위한 출력 스트림을 뜻한다.

출력 스트림은 데이터를 쓸 수는 있어도 읽지는 못한다. 

데이터를 읽기 위해서는 입력 스트림을 형성해야 한다.

 

입력 스트림의 형성을 요청하는 fopen 함수의 호출문을 보자.

FILE * fp = fopen("data.txt", "rt");

이는 data.txt 파일과 rt모드로 스트림을 형성하는 것을 의미한다.

rt모드의 스트림은 텍스트 데이터를 읽기 위한 입력 스트림이다.

이는 wt모드와 반대로 읽을 수는 있어도 쓰지는 못한다.

fclose 함수

파일을 여는 fopen 함수가 있었다면, 파일을 닫는 함수는 fclose 함수이다.

fopen함수는 스트림을 형성하는 함수이며, fclose 함수는 스트림을 해제한다.

#include<stdio.h>
int fclose(FILE *stream);

// 성공시 0, 실패 시 EOF 반환

개방되었던 파일을 닫아주는 이유에는 두 가지가 있다.

  • 운영체제가 할당한 자원의 반환
  • 버퍼링 되었던 데이터의 출력

함수의 호출을 통해 스트림 형성을 요청하는 것은 개인이지만, 스트림을 형성하는 주체는 운영체제이다.

운영체제는 스트림의 형성을 위해 시스템의 자원을 할당하는데, 파일을 닫지 않으면 할당한 채로 남아있게 되어

자원 손실이 나게 된다.

또한 파일 스트림에도 입출력 버퍼가 존재한다. fputc와 같은 함수의 호출로 데이터를 파일로 전송할 때 파일에 바로 저장되는 것이 아닌, 출력 버퍼에 저장되었다가 운영체제가 정해놓은 버퍼링 방식에 따라 파일에 저장이 된다.

만약 출력버퍼에 문자가 저장된 채로 컴퓨터의 전원이 꺼지면 파일에 저장이 되지 않는다. 하지만 fclose 함수의 호출을 통해 파일을 닫아주면 출력 버퍼에 저장되어 있던 데이터가 파일로 이동하면서 출력 버퍼가 비워지게 된다.

fflush 함수

이 함수는 이전 글에서 봤던 함수이다.  이 함수를 잘 모르겠다면 이 글을 참고해보자.

https://jjunsu.tistory.com/30

 

C언어 - 표준 입출력과 버퍼

앞에서 공부했던 printf, scanf, putc, getc 등의 입출력 함수들을 "표준 입출력"이라 한다. ANSI C의 표준에서 정의된 함수이기 때문이다. 입출력 함수들에 대해 잘 모르겠다면 https://jjunsu.tistory.com/4?cat..

jjunsu.tistory.com

fflush 함수는 출력 버퍼를 지우는 용도 이외에도 스트림을 종료하지 않고 버퍼만 비우고 싶을 때 호출한다.

#include<stdio.h>
int fflush(FILE *stream);

// 함수 호출 성공 시 0, 실패 시 EOF 반환

파일의 개방 모드

앞에서 fopen 함수의 두 번째 인자로 "wt"와 "rt"를 전달하여 스트림을 형성했다.

이 뿐만 아닌 형성할 수 있는 스트림의 종류는 훨씬 더 다양하다.

기본적으로 두 가지 기준을 통해 스트림을 구분하게 된다.

  • 읽기 위한 또는 쓰기 위한 스트림
  • 텍스트 데이터를 위한 또는 바이너리 데이터를 위한 스트림

스트림을 구분하는 기준1: 읽기 위한 스트림 vs 쓰기 위한 스트림

모드(mode) 스트림의 성격 파일이 없으면?
r 읽기 가능 에러
w 쓰기 가능 생성
a 파일의 끝에 덧붙여 쓰기 가능 생성
r+ 읽기/쓰기 가능 에러
w+ 읽기/쓰기 가능 생성
a+ 읽기/덧붙여 쓰기 가능 생성

C언어는 이와 같이 6가지로 스트림을 세분화한다.

이를 참조하여 스트림의 특성과 일치하는 파일의 개방 모드를 선택하면 된다.

각각의 모드에 +가 붙는다면 읽기와 쓰기가 모두 가능한 스트림의 형성을 의미한다.

 

스트림을 구분하는 기준2: 텍스트 모드 vs 바이너리 모드

위에서 6가지 스트림에 대해 보았다.

텍스트 모드의 파일 개방을 위해서는 fopen 함수의 두 번째 인자로 6가지 중 하나의 스트림 뒤에 t를 붙여 전달하면 된다.

ex) rt, wt, at, r+t, w+t, a+t

바이너리 모드의 파일 개방을 위해서는 t대신 b를 붙이면 된다.

ex) rb, wb, ab, r+b, w+b, a+b

만약 아무것도 붙어있지 않다면 파일은 텍스트 모드로 개방이 된다.

파일 입출력 함수들

https://jjunsu.tistory.com/27

 

C언어 - 문자와 문자열 함수

getchar, putchar getchar와 putchar는 문자 입출력 함수이다. getchar는 문자 입력 함수이며, stdin으로 표현되는 표준 입력 스트림으로부터 하나의 문자를 입력받아 반환한다. 함수 호출을 성공할 시 쓰인

jjunsu.tistory.com

이 글에서 fputc, fgetc, fputs, fgets에 대해 간략하게 보았었다. 

 

예제를 통해 파일 입출력을 진행해 보자.

우선 simple.txt라는 파일을 생성하여 문자와 문자열을 저장할 것이다.

#include<stdio.h>

int main() {
    FILE *fp = fopen("simple.txt", "wt");
    if(fp == NULL) {
        puts("파일오픈 실패!");
        return -1;
    }
    
    fputc('H', fp);
    fputc('i', fp);
    fputs("My name is junsu\n", fp);
    fputs("What your name?\n", fp);
    fclose(fp);
    return 0;
}

fputs 함수를 사용할 때 개행 문자가 포함되어 있어 반드시 텍스트 모드로 파일을 개방해야 한다. 

파일에 문자, 문자열을 저장하였으니, 이제는 생성된 파일에 저장된 데이터를 읽어보자.

#include<stdio.h>

int main() {
    char str[30];
    int ch;
    FILE *fp = fopen("simple.txt", "rt");
    if(fp == NULL) {
        puts("파일오픈 실패!");
        return -1;
    }
    
    ch = fgetc(fp);
    printf("%c\n", ch);
    ch = fgetc(fp);
    printf("%c\n", ch);
    
    fgets(str, sizeof(str), fp);
    printf("%s", str);
    fgets(str, sizeof(str), fp);
    printf("%s", str);
    
    fclose(fp);
    return 0;
}

출력 결과를 보면 문자열의 끝에 \n이 존재하는 것을 볼 수 있다.

문자열이 파일에 저장될 때는 문자열의 끝을 의미하는 널(NULL) 문자는 저장되지 않는다.

때문에 파일에서는 개행을 기준으로 문자열을 구분한다.

feof 함수

feof 함수는 파일의 끝에 도달했는지의 여부를 확인하는 목적으로 정의된 함수이다.

#include<stdio.h>
int feof(FILE *stream);

// 파일의 끝에 도달한 경우 0이 아닌 값 반환

인자로 전달된 FILE 구조체의 포인터를 대상으로, 더 이상 읽어 들일 데이터가 존재하지 않으면 0이 아닌 값을 반환한다.

fread, fwrite 함수

이 함수들은 바이너리 데이터의 입출력을 진행하는 함수들이다.

fread 함수는 바이너리 데이터의 입력에 사용된다.

#include<stdio.h>
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);

// 성공 시 전달인자 count, 실패 또는 파일의 끝 도달 시 count보다 작은 값 반환
int buf[12];
fread(void *buffer, sieof(int), 12, fp); // fp는 FILE 구조체 포인터

위의 fread 함수 호출문은 "sizeof(int) 크기의 데이터 12개를 fp로부터 읽어 들여 배열 buf에 저장"이라는 의미를 가진다.

결론적으로 fread 함수는 두 번째 인자와, 세 번째 인자의 곱의 바이트 크기만큼 데이터를 읽어 들인다.

이번에는 fwrite 함수를 보자.

 

fwrite 함수는 바이너리 데이터의 출력에 사용된다.

#include<stdio.h>
size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream);

// 성공 시 전달인자 count, 실패 시 count보다 작은 값 반환
int buf[7];
fwrite((void*)buf, sizeof(int), 7, fp);

위의 호출문은 "sizeof(int) 크기의 데이터 7개를 buf로부터 읽어 buf에 저장"이라는 의미이다.

fprintf, fscanf 함수

하나의 파일을 대상으로 입출력을 할 때 텍스트 데이터와 바이너리 데이터 둘로 이뤄져 있다면 이 함수들을 쓰면 된다.

이 두 함수는 print, scanf 함수와 유사하지만 입출력이 대상이 콘솔이 아닌 파일이라는 차이가 있다.

char name[10] = "Junsu";
char sex = 'M';
int age = 23;
fprintf(fp, "%s %c %d", name, sex, age); // fp는 FILE 구조체 포인터

위의 fprintf 함수 호출문을 보면 printf 함수의 호출문과 차이를 보이는 부분이 있다.

바로 FILE 구조체의 포인터가 첫 번째 전달 인자인 것이다.

#include<stdio.h>

int main() {
    char name[10];
    char sex;
    int age;
    
    FILE* fp = fopen("friend.txt", "wt");
    
    for (int i = 0; i < 3; i++) {
        printf("이름 성별 나이 순 입력: ");
        scanf("%s %c %d", name, &sex, &age);
        getchar();
        fprintf(fp, "%s %c %d", name, sex, age);
    }
    fclose(fp);
    return 0;
}

위 예제에서 생성된 text파일에 다음과 같은 데이터를 입력하여 저장시켰다 하자.

이제 파일에 저장된 데이터를 fscanf 함수의 호출을 통해 확인해보자.

fscanf 함수의 호출 방식도 fprintf 함수와 크게 다를 것이 없다.

char name[10];
char sex;
int age;
fscanf(fp, "%s %c %d", name, &sex, &age); // fp는 FILE 구조체 포인터

scanf 함수 호출문과의 차이점은 역시나 첫 번째 인자로 FILE 구조체의 포인터가 전달되는 것이다.

#include<stdio.h>

int main() {
    char name[10];
    char sex;
    int age;
    
    FILE* fp = fopen("friend.txt", "rt");
    int ret;

    while (1) {
        ret = fscanf(fp, "%s %c %d", name, &sex, &age);
        if (ret == EOF) break;
        printf("%s %c %d\n", name, sex, age);
    }
    fclose(fp);
    return 0;
}

위에서 본 예제들에서 다음의 데이터들을 대상으로 파일 입출력을 진행했다.

char name[10];
char sex;
int age;

실제 프로그램에선 보통 구조체로 묶어 정의한다.

typedef struct friend {
    char name[10];
    char sex;
    int age;
} Friend;

구조체 변수를 통째로 저장하고 읽어 들이려면 구조체 변수를 하나의 바이너리 데이터로 인식하여 처리하면 된다.

#include<stdio.h>

typedef struct friend {
    char name[10];
    char sex;
    int age;
} Friend;

int main() {
    FILE *fp;
    Friend fr1;
    Frined fr2;
    
    // file write
    fp = fopen("friend.bin", "wb");
    printf("이름 성별 나이 순 입력: ");
    scanf("%s %c %d", fr1.name, &(fr1.sex), &(fr1.age));
    fwrite((void*)&fr1, sizeof(fr1), 1, fp);
    fclose(fp);
    
    // file read
    fp = fopen("friend.bin", "rb");
    fread((void*)&fr2, sizeof(fr2), 1, fp);
    printf("%s %c %d\n", fr2.name, fr2.sex, fr2.age);
    fclose(fp);
    return 0;
}

파일 위치 지시자

FILE 구조체의 멤버 중에는 파일의 위치 정보를 저장하고 있는 멤버가 있다.

이 멤버의 값은 파일 입출력 함수들이 호출될 때마다 갱신된다.

이처럼 저장된 위치 정보의 갱신을 통해 데이터를 읽고 쓸 위치 정보가 유지되는 멤버를 "파일 위치 지시자"라고 한다.

fseek 함수

fseek 함수는 파일 위치 지시자를 직접 이동시키고자 할 때 호출한다.

#include<stdio.h>
int fseek(FILE *stream, long offset, int wherfrom);

// 성공 시 0, 실패 시 0이 아닌 값을 반환

fseek 함수는 총 3개의 인자를 요구한다.

인자가 의미하는 것은 다음과 같다.

"stream으로 전달된 파일 위치 지시자를 wherefromset으로부터 offset 바이트만큼 이동시켜라"

 

wherfrom에 전달될 수 있는 상수와 그 의미는 다음과 같다.

매개변수 wherefrom이 파일 위치 지시자는
SEEK_SET 이라면 파일 맨 앞에서부터 이동을 시작
SEEK_CUR 이라면 현재 위치에서부터 이동을 시작
SEEK_END 이라면 파일 맨 끝에서부터 이동을 시작

 

매개변수 offset에는 양의 정수뿐만 아니라 음의 정수도 전달될 수 있다. 양의 정수가 전달되면 파일의 끝을 향해 이동을 하고, 음의 정수가 전달되면 파일의 시작 위치를 향해 이동을 한다.

ftell 함수

ftell 함수는 현재의 파일 위치 지시자 정보를 확인할 때 호출한다.

#include<stdio.h>
int ftell(FILE * stream);

// 파일 위치 지시자의 위치 정보 반환

파일 위치 지시자의 위치 정보를 반환할 때 첫 번째 바이트를 가리킬 경우 0을 반환, 세 번째 바이트를 가리킬 경우 2를 반환한다. 이처럼 가장 앞부분의 바이트 위치를 0으로 간주하여 한 바이트가 진행될 때마다 1씩 증가한다.

728x90

댓글