Skip to main content

Command Palette

Search for a command to run...

파일과 스트림(Stream), 그리고 기본적인 파일의 입출력

Published
9 min readView as Markdown

저기 저 파일에 저장되어 있는 데이터를 읽고 싶어요.

프로그램 상에서 파일에 저장되어 있는 데이터를 참조하길 원한다고(읽기 원한다고) 가정했을 때, 우리가 구현한 프로그램과 참조할 데이터가 저장되어 있는 파일 사이에

데이터가 이동할 수 있는 다리를 놓는 일을 먼저 해야 한다.

이러한 데이터 이동의 경로가 되는 다리를 가리켜 ‘스트림(stream)’이라 함.

스트림이라는 것은 운영체제에 의해서 형성되는 소프트웨어적인 상태를 의미하는 것

따라서, 프로그램과 파일 사이에 스트림이 형성되었다고 하면 다음과 같이 이해

“파일로부터 데이터를 읽어 들일(파일에 데이터를 쓸) 기본적인 준비가 완료되었다!”

fopen 함수호출을 통한 파일과의 스트림 형성과 FILE 구조체

/* 스트림을 형성할 때 호출하는 함수 */
#include <stdio.h>
FILE * fopen(const char * filename, cont char * mode);
// 성공 시 해당 파일의 FILE 구조체 변수의 주소 값, 실패 시 NULL 포인터 반환
/* 
첫 번째 인자로는 스트림을 형성할 파일의 이름을,
두 번째 인자로는 형성할 스트림의 종류에 대한 정보를 문자열의 형태로 전달
*/

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

데이터를 파일로부터 읽어들이기 위한 ‘입력 스트림'

데이터를 파일에 쓰기 위한 ‘출력 스트림'

  • 첫 번째 전달인자 스트림을 형성한 파일의 이름

  • 두 번째 전달인자 형성하고자 하는 스트림의 종류

FILE * fp = fopen("data.txt", "wt");  // 출력 스트림의 형성
/* 
파일 data.txt와 스트림을 형성하되 wt 모드로 스트림을 형성해라! 
위의 스트림은 출력 스트림이기 때문에 파일에 데이터를 쓸 수는 있어도 읽지는 못한다.
*/
FILE * fp = fopen("data.txt", "rt");  // 입력 스트림의 형성
/*
파일 data.txt와 스트림을 형성하되 rt 모드로 스트림을 형성해라!
fopen 함수의 호출을 통해서 파일과의 스트림이 형성되었을 때 '파일이 개방되었다'라고 표현
*/

파일에 데이터를 써봅니다.

#include <stdio.h>

int main(void)
{
    FILE * fp = fopen("data.txt", "wt");
    if(fp==NULL) {
        puts("파일오픈 실패!");
        return -1;      // 비정상적 종료를 의미하기 위해서 -1을 반환
    }

    fputc('A', fp);
    fputc('B', fp);
    fputc('C', fp);
    fclose(fp);     // 스트림의 종료
    return 0;
}
/* 같은 디렉토리에 data.txt 생성, 내용에 ABC 저장 */

스트림의 소멸을 요청하는 fclose 함수

fclose 함수는 fopen 함수의 반대 기능을 제공

/* fclose 함수는 스트림을 해제하는 함수 */
#include <stdio.h>
int fclose(FILE * stream);
// 성공 시 O, 실패 시 EOF를 반환
/*
fclose 함수의 호출을 통해서 개방되었던 파일을 닫아줘야 하는 이유
- 운영체제가 할당한 자원의 반환
- 버퍼링 되었던 데이터의 출력
*/

Chapter 21에서 호출한 적 있는 fflush 함수

/* 스트림을 종료하지 않고 버퍼만 비우고 싶을 때에는 다음 fflush 함수를 호출 */
#include <stdio.h>
int fflush(FILE * stream);
// 함수호출 성공 시 O, 실패 시 EOF 반환

- 출력버퍼를 비운다는 것은 출력버퍼에 저장된 데이터를 목적지로 전송한다는 의미
- 입력버퍼를 비운다는 것은 입력버퍼에 저장된 데이터를 소멸시킨다는 의미
- fflush 함수는 출력버퍼를 비우는 함수
- fflush 함수는 입력버퍼를 대상으로 호출할 수 없다.
// 출력버퍼에 저장된 데이터가 실제로 파일에 저장
int main(void)
{
    FILE * fp = fopen("data.txt", "wt");  // 출력 스트림 형성
    . . . .
    fflush(fp);  // 출력 버퍼를 비우라는 요청!
    . . . .
}
/*
파일 스트림의 입력버퍼를 비우는 함수는 필요 없음.
파일에 저장된 데이터는 원할 때 언제든지 읽을 수 있을 뿐만 아니라(읽혀진 데이터는 입력버퍼에서 지워짐)
*/

파일로부터 데이터를 읽어 봅시다.

#include <stdio.h>

int main(void)
{
    int ch, i;
    FILE * fp = fopen("data.txt", "rt");
    if(fp==NULL) {
        puts("파일오픈 실패!");
        return -1;
    }

    for(i=0 ; i<3 ; i++)
    {
        ch = fgetc(fp);
        printf("%c\n", ch);
    }
    fclose(fp);
    return 0;
}

/* 결과
A
B
C
*/

파일의 개방 모드

스트림을 구분 하는 두 가지 기준

  • 기준1 읽기 위한 스트림이냐? 쓰기 위한 스트림이냐?

  • 기준2 텍스트 데이터를 위한 스트림이냐? 바이너리 데이터를 위한 스트림이냐?

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

  • 데이터 READ 스트림 읽기만 가능

  • 데이터 WRITE 스트림 쓰기만 가능

  • 데이터 APPEND 스트림 쓰되 덧붙여 쓰기만 가능

  • 데이터 READ/WRITE 스트림 읽기, 쓰기 모두 가능

모드

스트림의 성격

파일이 없으면?

r

읽기 가능

에러

w

쓰기 가능

생성

a

파일의 끝에 덧붙여 쓰기 가능

생성

r+

읽기/쓰기 가능

에러

w+

읽기/쓰기 가능

생성

a+

읽기/덧붙여 쓰기 가능

생성

  • 모드의 +는 읽기, 쓰기가 모두 가능한 스트림의 형성을 의미한다.

  • 모드의 a는 쓰기가 가능한 스트림을 형성하는데, 여기서 말하는 쓰기는 덧붙이기이다.

텍스트 파일과 바이너리 파일

스트림을 구분하는 두 번째 기준

  • 개인이 소유하는 도서의 목록 문자 데이터

  • 슈퍼마켓의 물품 가격 문자 데이터

  • 제임스 캐머런 감독의 타이타닉 영상파일 바이너리 데이터

  • 소녀시대의 히트곡 음원파일 바이너리 데이터

사람이 인식할 수 있는 문자를 담고 있는 파일을 가리켜 ‘텍스트 파일'

컴퓨터가 인식할 수 있는 데이터를 담고 있는 파일을 가리켜 ‘바이너리 파일'

개행이 \n 아니라고요?

“C 언어에서는 개행을 \n으로 표시, 그런데 이는 C 언어만의 약속"

  • MS-DOS(Windows)의 파일 내 개행 \r\n

  • Mac(Mackintosh)의 파일 내 개행 \r

  • Unix 계열의 파일 내 개행 \n

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

파일을 텍스트 모드로 개방하면 바로 위에서 말한 형태의 변환이 자동으로 이뤄진다

“C 프로그램에서 \n을 파일에 저장하면 \r\n으로 변환되어 저장됨"

“파일에 저장된 \r\n을 C 프로그램 상에서 읽으면 \n으로 변환되어 읽혀짐"

텍스트 파일 fopen 함수의 두 번째 인자

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

바이너리 파일 fopen 함수의 두 번째 인자

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


파일 입출력 함수의 기본

Chapter21에서 학습한 파일 입출력 함수들

int fputc(int c, FILE * stream);  // 문자 출력
int fgetc(FILE * stream);  // 문자 입력
int fputs(const char * s, FILE * stream);   // 문자열 출력
char * fgets(char * s, int n, FILE * stream);  // 문자열 입력
#include <stdio.h>

int main(void)
{
    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;
}

/* 결과
A
B
My name is Hong
Your name is Yoon
출력한 문자열의 끝에 \n이 존재한다.
*/

feof 함수 기반의 파일복사 프로그램

// 파일의 끝을 확인하는 방법
#include <stdio.h>
int feof(FILE * stream);
// 파일의 끝에 도달한 경우 O이 아닌 값 반환

바이너리 데이터의 입출력: fread, fwrite

#include <stdio.h>
size_t fread(void * buffer, size_t size, size_t count, FILE * stream);
// 성공 시 전달인자 count, 실패 또는 파일의 끝 도달 시 count보다 작은 값 반환
/* 위 함수는 다음과 같이 호출이 된다 */
int buf[12];
fread((void*)buf, sizeof(int), 12, fp);  // fp는 FILE 구조체 포인터
/* 
sizeof(int) 크기의 데이터 12개를 fp로부터 읽어 들여서 배열 buf에 저장하라! 
함수의 호출이 성공을 하고 요청한 분량의 데이터가 모두 읽혀지면 12가 반환
실패하면 12보다 작은 값 반환
*/
#include <stdio.h>
size_t fwrite(const void * buffer, size_t size, size_t count, FILE * stream);
// 성공 시 전달인자 count, 실패 시 count보다 작은 값 반환
/* 위 함수는 다음과 같이 호출을 한다 */
int buf[7]={1, 2, 3, 4, 5, 6, 7};
fwrite((vold*)buf, sizeof(int), 7, fp);
/* sizeof(int) 크기의 데이터 7개를 buf로부터 읽어서 fp에 저장해라! */

텍스트 데이터와 바이너리 데이터를 동시에 입출력 하기

서식에 따른 데이터 입출력: fprintf, fscanf

/* fprintf 함수의 호출방법 */
char name[10]="홍길동";  // 텍스트 데이터
char sex='M';  // 텍스트 데이터: 남자 M, 여자 F
int age=24;  // 바이너리 데이터
fprintf(fp, "%s %c %d", name, sex, age);  // fp는 FILE 구조체 포인터
/*
fprintf 함수 첫 번째 인자로 전달된 FILE 구조체의 포인터가 지칭하는 파일로 출력
그리고 이렇게 만들어진 문자열이 첫 번째 전달인자가 가리키는 파일에 저장
*/

/* fscanf 함수의 호출방법 */
char name[10];
char sex;
int age;
fscanf(fp, "%s %c %d", name, &sex, &age);
/*
위의 fscanf 함수 호출문이 scanf 함수 호출문과 차이를 보이는 부분은 첫 번째 인자로 FILE 구조체의 포인터가 전달된다는 점
fscanf 함수는 파일의 끝에 도달하거나 오류가 발생하면 EOF를 반환
#include <stdio.h>

int main(void)
{
    char name[10];
    char sex;
    int age;

    FILE * fp=fopen("friend.txt", "wt");
    int i;

    for(i=0 ; i<3 ; i++)
    {
        printf("이름 성별 나이 순 입력: ");
        scanf("%s %c %d", name, &sex, &age);
        getchar();      // 버퍼에 남아있는 \n의 소멸을 위해서
        fprintf(fp, "%s %c %d", name, sex, age);
    }
    fclose(fp);
    return 0;
}
#include <stdio.h>

int main(void)
{
    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;
}

/* 결과
정우영 F 22
한수정F 2 6
이영호M 3 1
*/

텍스트와 바이너리 데이터의 집합체인 구조체 변수의 입출력

/* 실제 프로그램에서는 이들을 다음과 같이 구조체로 묶어서 정의 */
typedef struct fren
{
    char name[10];
    char sex;
    int age;
} Friend;

어떻게 해야 구조체 변수를 통째로 저장하고 통째로 읽어 들일 수 있는가

구조체 변수를 하나의 바이너리 데이터로 인식

그러면 fwrite 함수를 통해서 통째로 저장하고 fread 함수를 통해서 통째로 복원할 수 있다.

#include <stdio.h>

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

int main(void)
{
    FILE * fp;
    Friend myfren1;
    Friend myfren2;

    /*** file write ***/
    fp=fopen("friend.bin", "wb");
    printf("이름, 성별, 나이 순 입럭: ");
    scanf("%s %c %d", myfren1.name, &myfren1.sex, &myfren1.age);
    fwrite((void*)&myfren1, sizeof(myfren1), 1, fp);
    fclose(fp);

    /*** file read ***/
    fp=fopen("friend.bin", "rb");
    fread((void*)&myfren2, sizeof(myfren2), 1, fp);
    printf("%s %c %d \n", myfren2.name, myfren2.sex, myfren2.age);
    fclose(fp);
    return 0;
}

/* 결과
이름, 성별, 나이 순 입럭: Jungs M 27
Jungs M 27 
*/

임의 접근을 위한 ‘파일 위치 지시자'의 이동

경우에 따라서는 파일의 중간 또는 마지막 부분에 저장된 데이터의 일부를 읽어야 하는 경우도 있다. 그러한 경우에는 ‘파일 위치 지시자'를 파일의 중간 또는 마지막 부분으로 이동시켜야 한다.

파일 위치 지시자란

FILE 구조체의 멤버 중에는 파일의 위치 정보를 저장하고 있는 멤버가 있는데, 이 멤버의 값은 fgets, fputs, fread, fwrite같은 함수가 호출될 때마다 참조 및 갱신

‘파일 위치 지시자'는 파일이 처음 개방되면 무조건 파일의 맨 앞부분을 가리킨다.

파일 위치 지시자의 이동: fseek

#include <stdio.h>
int fseek(FILE * stream, long offset, int wherefrom);
// 성공 시 0 실패 시 0이 아닌 값을 반환
/*
위의 함수는 총 세 개의 인자를 요구하는데, 인자가 의미하는 바를 문장으로 구성해서 설명하면 다음과 같다.
"stream으로 전달된 파일 위치 지시자를 wherefrom에서부터 offset 바이트만큼 이동시켜라"
*/

매개변수 wherefrom

파일 위치 지시자

SEEK_SET(0)

파일 맨 앞에서부터 이동을 시작

SEEK_CUR(1)

현재 위치에서부터 이동을 시작

SEEK_END(2)

파일 맨끝에서부터 이동을 시작

#include <stdio.h>

int main(void)
{
    /* 파일생성 */
    FILE * fp=fopen("text.txt", "wt");
    fputs("123456789", fp);
    fclose(fp);

    /* 파일개방 */
    fp=fopen("text.txt", "rt");

    /* SEEK_END test */
    fseek(fp, -2, SEEK_END);
    putchar(fgetc(fp));

    /* SEEK_SET test */
    fseek(fp, 2, SEEK_SET);
    putchar(fgetc(fp));

    /* SEEK_CUR test */
    fseek(fp, 2, SEEK_CUR);
    putchar(fgetc(fp));

    fclose(fp);
    return 0;
}

/* 결과
836
*/

현재 파일 위치 지시자의 위치는? : ftell

#include <stdio.h>
long ftell(FILE * stream);
// 파일 위치 지시자의 위치 정보 반환
/*
파일 위치 지시자가 첫 번째 바이트를 가리킬 경우 0을 반환하고,
세 번째 바이트를 가리킬 경우 2를 반환
*/
#include <stdio.h>

int main(void)
{
    long fpos;
    int i;

    /* 파일생성 */
    FILE * fp=fopen("text.txt", "wt");
    fputs("1234-", fp);
    fclose(fp);

    /* 파일개방 */
    fp=fopen("text.txt", "rt");

    for(i=0 ; i<4 ; i++)
    {
        putchar(fgetc(fp));
        fpos=ftell(fp);
        fseek(fp, -1, SEEK_END);
        putchar(fgetc(fp));
        fseek(fp, fpos, SEEK_SET);
    }
    fclose(fp);
    return 0;
}

/* 결과
1-2-3-4-
*/

More from this blog

선행처리기와 매크로

선행처리는 컴파일 이전의 처리를 의미 선행처리는 선행처리기에 의해서, 컴파일은 컴파일러에 의해서, 링크는 링커에 의해서 진행 소스파일 → 선행처리기 → 선행처리 거친 소스파일 → 컴파일러 → 오브젝트 파일 → 링커 → 실행파일 컴파일 과정을 거치게 되면 바이너리 데이터로 이루어진 오브젝트 파일이 생성 선행처리기가 하는 일 삽입해 놓은 선행처리 명령문대로 소스코드의 일부를 수정 여기서 수정이란, 단순 치환의 형태 /* 간단한 선행처리 명령문 */...

Oct 10, 20246 min read

C 언어의 메모리 구조

프로그램을 실행하면 해당 프로그램의 실행을 위한 메모리 공간이 운영체제에 의해서 미리 마련 그리고 바로 이 메모리 공간 내에서 변수가 선언되고, 문자열이 선언되는 것 메모리의 구성 프로그램 실행 시 운영체제에 의해서 마련되는 메모리의 구조는 네 개의 영역으로 구분 코드 영역 / 데이터 영역 / 힙 영역 / 스택 영역 메모리 공간을 나눠놓은 이유는 유사한 성향의 데이터를 묶어서 저장을 하면, 관리가 용이해지고 메모리의 접근속도가 향상 메모리 영역...

Oct 9, 20246 min read

JinGood

31 posts