C 4주차 - 파일 입출력

Gunter·2024년 3월 21일
0

C

목록 보기
12/13

파일 입출력의 개념과 필요성

컴퓨터의 주기억 장치인 램(RAM, Random Access Memory)은 컴퓨터의 전원이 켜져 있을 때만 데이터를 유지할 수 있다.
그런데 컴퓨터를 365일 계속 켜놓을 수는 없기 때문에 전원이 꺼져도 데이터를 유지할 수 있는 보조기억 장치(디스크, HDD, SSD)가 필요하다.
많은 프로그램이 사용자가 입력한 데이터를 보관하기 위해 보조기억 장치에 데이터를 저장하고 있다. 하지만 보조기억 장치의 종류가 너무 다양하기 때문에 프로그래머가 일일이 보조 기억 장치의 특성을 파악해서 프로그래밍 한다는 것은 불가능하다.
그래서 운영체제는 보조기억 장치의 종류에 상관없이 같은 함수로 데이터를 저장할 수 있도록 파일 입출력 라이브러리를 제공하고 있다.

그런데 파일 입출력 라이브러리도 운영체제에 따라 조금씩 차이가 있다.
운영체제별로 제공하는 입출력 함수는 이름뿐만 아니라 사용법도 다르다.
이런 문제를 해결하기 위해 C언어는 '표준 입출력 라이브러리(Standard I/O Library)'를 제공한다.
이 라이브러리를 사용하면 보조기억 장치에 파일 단위로 데이터를 저장하거나 읽을 수 있다.

표준 입출력 라이브러리는 데이터의 형식에 따라 다른 함수를 제공한다.
프로그램이 사용하는 데이터 형식은 '텍스트(문자열)'와 '바이너리(이진)'로 나누어지는데, 자신이 다루는 데이터가 텍스트 형식이면 텍스트 관련 함수를 사용해야 하고 바이너리 형식이면 바이너리 관련 함수를 사용해야 한다.

 











파일 열기와 닫기

표준 입출력 라이브러리는 'FILE 구조체'로 포인터 변수를 선언하고 파일 입출력 함수를 호출할 때마다 이 변수를 넘겨주도록 만들어져 있다.

FILE 구조체는 사용하려는 디스크상의 파일이 어떤 상태로 사용 중인지에 대한 정보를 담고 있으며, 파일을 좀 더 편하게 사용할 수 있도록 도와준다.

FILE *p_file;
// 파일 열기 생략
fseek(p_file, 0, SEEK_SET);

위와 같은 방법으로 fseek 함수를 호출하면, 프로그램에서 파일을 사용하며 기억해야 할 내부 상태 값은 p_file 파일 포인터에 저장한다.

 

 







파일 열기 : fopen 함수

함수 원형 : FILE *fopen(const char *filename, const char *mode);
함수 사용 형식 : fopen(사용할 파일 이름, 파일 사용 형식)
FILE *p_file = fopen("c:\\tips\\tips.dat", "r");
if(NULL != p_file({
	// 파일 열기에 성공한 경우
} else {
	// 파일 열기에 실패한 경우
}

 


파일 사용 형식 알아보기

파일 사용 형식이란 파일을 어떻게 사용할 것인지 지정하는 형식을 말한다.
파일 사용 형식은 fopen 함수의 두 번째 매개변수인 문자열 형식으로 지정한다.

 


파일 내용 읽기 모드 "r"

이 형식을 사용하면 파일의 내용을 읽기 위한 목적으로 파일을 연다.

FILE *p_file = fopen("tips.dat", "rb");

바이너리 파일을 여는 경우

FILE *p_file = fopen("tips.dat", "rt");

텍스트 파일을 여는 경우



파일에 데이터 쓰기 모드 "w"

이 형식을 사용하면 파일에 데이터를 쓰기 위한 목적으로 파일을 연다.

FILE *p_file = fopen("tips.dat", "wb");

바이너리 파일을 여는 경우

FILE *p_file = fopen("tips.dat", "wt");

텍스트 파일을 여는 경우

그런데 쓰기 모드 형식을 제대로 사용해도 디스크에 용량이 부족해서 파일을 만들 수 없거나, 읽기 전용 디스크에 쓰기 모드로 사용하면 파일 열기에 실패한다.



파일에 데이터 이어 쓰기 모드 "a"

이 형식을 사용하면 파일에 데이터를 확장(Append, 이어 쓰기)하기 위한 목적으로 파일을 연다.
w 속성과 달리 기존에 파일이 존재하더라도 파일 내용을 지우지 않고 기존 파일 내용에 이어 쓰기를 한다.

FILE *p_file = fopen("tips.dat", "ab");

바이너리 파일을 여는 경우

FILE *p_file = fopen("tips.dat", "at");

텍스트 파일을 여는 경우

이 형식도 디스크에 용량이 부족하거나 읽기 전용 디스크에 사용하면 파일 읽기에 실패한다.

 


읽기 쓰기 같이 사용 : 읽기 강조 "r+"

읽기와 쓰기를 같이 사용할 때 '읽기'를 더 강조하는 형식.
이 형식으로 파일을 여는 경우 기존 파일이 없으면 파일을 새로 만들지 않고 파일 읽기에 실패한다.
기존 파일이 있는 경우엔 해당 파일의 내용을 지우지는 않지만 기존 데이터의 위치로 이동해서 해당 위치의 내용을 덮어쓴다.



읽기 쓰기 같이 사용 : 쓰기 강조 "w+"

읽기와 쓰기를 같이 사용할 때 '쓰기'를 더 강조하는 형식.
이 형식으로 파일을 여는 경우 기존 파일이 없으면 파일을 새로 만들고, 파일이 이미 존재하면 기존 파일의 내용을 모두 지우고 시작한다.

 


읽기 쓰기 같이 사용 : "a+"

읽기 모드와 이어 쓰기 모드를 같이 사용해야 하는 경우에 사용하며 '확장'을 더 강조하는 형식.
r+와 달리 기존 데이터 위치로 이동할 수 있고 읽기도 가능하지만 쓰기를 사용하면 현재 위치와 상관없이 파일의 끝에 내용이 추가된다.











파일 닫기 : fclose 함수

이렇게 fopen 함수를 사용하여 파일을 열어서 사용하다가 사용이 끝나면 fclose 함수를 사용하여 파일을 닫아야 한다.

FILE *p_file = fopen("tips.dat", "r+b");
if(NULL != p_file({
	fclose(p_file);
} else {
	// 파일 열기에 실패한 경우
}

 














텍스트 파일에 데이터 읽고 쓰기

 

텍스트 파일에 문자열 저장하기 : fprintf 함수(1)

파일 입출력 함수에는 printf 함수와 모든 기능이 비슷하고 이름까지 비슷한 fprintf 함수가 있다.
fprintf 함수는 첫 매개변수에 파일 포인터를 받아서 출력할 문자열을 팡리에 저장한다.

fprintf(p_file, "Hello\n");

다음은 파일 포인터가 가리키는 파일에 "Hello" 문자열을 출력하고 줄을 바꾸는 코드이다.

다음은 tip.txt 파일에 "Hello" 문자열을 저장하는 예제이다.

#include <stdio.h>

void main(){
	FILE *p_file = fopen("tip.txt", "wt");
    if(NULL != p_file){
    	fprintf(p_file, "Hello\n");
        fclose(p_file);
    }
}

fopen 함수에서 파일 사용 형식에 w가 있을 경우에 첫 번째 매개변수로 넘겨 준 파일(tip.txt)이 없으면 파일을 만들어서 사용하고 파일이 존재하면 덮어쓰기를 한다.

 






바이너리 형태를 문자열 형태로 저장하기 : fprintf 함수 (2)

int형 변수에 들어있는 값은 바이너리 데이터이기 때문에 텍스트 파일에 저장하려면 문자열 형식으로 변환해서 저장해야 한다.
표준 입출력 함수에서 제공하는 %d, %f와 같은 형식 지정 키워드를 사용해 파일에 문자열 형태로 저장한다.

short int data = 0x0412;
fprintf(p_file, "%x\n", data);

 






텍스트 파일에서 문자열 읽기 : fscanf 함수

이 함수는 키보드로 문자 또는 숫자를 입력 받는 scanf 함수와 비슷하지만 첫 번째 매개변수에 어떤 파일에서 입력값을 가져올 것인지를 명시하는 점이 다르다.

int data;
fscanf (p_file, "%d", &data);

위는 파일에 저장된 문자열을 읽어 10진 정수 값으로 변환하여 data 변수에 대입하는 코드다.

fscanf 함수는 기본적으로 공백(space)문자를 만나면 다음 입력이 시작된 것으로 처리한다.

 






텍스트 파일에서 한 줄 단위로 문자열 읽기 : fgets 함수

fscanf 함수는 문자열 사이에 공백이 있기 때문에 한 줄 단위로 입력받지 못하고 단어 단위로 파일에서 읽어온다.

따라서 텍스트 파일에서 한 줄 단위로 문자열을 처리하고 싶은 경우에는 fgets 함수를 사용한다.
이 함수는 gets 함수와 비슷하며 함수의 세 번째 매개변수에 어떤 파일에서 입력 값을 가져올 것인지 파일 포인터를 표기하면 된다.

char temp[64];
fgets(temp, sizeof(temp), p_file);

또한 fscanf 함수는 EOF 문자를 만나면 EOF를 반환하지만 fgets 함수는 EOF 문자를 만나면 NULL을 반환한다.
그리고 fscanf 함수는 읽은 문자열에서 \n을 제외하는데 fgets 함수는 \n을 문자열에 포함한다는 것이 다르다.

 














바이너리 파일에 데이터 읽고 쓰기

문자열 속성은 문자열에 포함된 NULL 문자 0을 찾아서 데이터 크기를 체크하기 때문에 문자열 길이를 추가로 적을 필요가 없다.

하지만 바이너리 속성은 데이터를 그냥 숫자로만 판단하기 때문에 표준 입출력 함수가 데이터를 분석해서 길이나 크기를 알아낼 수 없다.
따라서 바이너리 속성으로 데이터를 읽거나 쓰려면 반드시 크기를 적어야 한다.

 



바이너리 파일에 데이터 저장하기 : fwrite 함수

fwrite 함수는 다음과 같은 형식으로 호출해서 데이터를 저장한다.

함수 원형 : size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stram);
사용 형식 : fwrite(저장할 데이터의 시작주소, 저장할 데이터의 기준 단위 크기, 반복 횟수, 파일 포인터);

예를 들어 int형 변수 data에 저장되어 있는 16진수 값을 파일에 저장하고 싶다면 다음과 같이 코드를 구성하면 된다.

int data = 0x00000412;

fwrite(&data, sizeof(int), 1, p_file);
// data 변수가 할당된 메모리를 4바이트 크기만큼 1회만 p_file 포인터가 가리키는 파일에 저장함

fwrite 함수는 저장할 데이터의 시작 주소부터 데이터의 기준 단위 크기고 반복 횟수만큼 파일에 데이터를 쓰게 된다.
따라서 실제로 파일에 저장되는 크기는 '기준 단위 크기 X 반복 횟수' 이다.
앞의 예시에서 4바이트가 저장된 이유는 fwrite 함수를 호출할 때 사용한 두 번째와 세 번째 매개변수를 곱한 크기만큼 데이터가 저장되기 때문이다.
즉, 데이터가 sizeof(int) X 1 크기로 저장된다.

fwrite 함수를 사용해서 데이터를 저장하면 파일 포인터가 가리키는 정보 중에서 '파일 내부 데이터를 읽거나 쓰기 시작하는 위치'가 데이터를 저장한(단위 크기 X 횟수)크기만큼 자동으로 증가한다.
따라서 파일에 데이터를 저장한 만큼 이동하는 함수를 추가로 사용할 필요가 없다.

즉, 현재 열어 놓은 파일의 내부 위치를 이동시키는 작업을 하지 않고 나열식으로 fwrite 함수를 사용하더라도 같은 위치에 계속 데이터를 덮어쓰지 않고 순차적으로 저장된다.

 






바이너리 파일에서 데이터 읽기 : fread 함수

함수 원형 : size_t fread(void *buffer, size_t size, size_t count, FILE *stram);
사용 형식 : fread(읽은 데이터를 저장할 주소, 저장할 데이터의 기준 단위 크기, 반복 횟수, 파일 포인터);

예를 들어 파일에 저장되어 있는 데이터를 읽어서 int형 변수 data에 저장하고 싶다면 아래와 같이 코드를 구성하면 된다.

int data;

fread(&data, sizeof(int), 1, p_file);
// p_file이 가리키는 파일에서 4바이트 크기만큼 1회만 데이터를 읽어 와서 data 변수에 저장ㅎ마

 














파일 위치 지시자 - fseek, ftell 함수

파일에 저장된 데이터를 꼭 순차적으로 읽을 필요는 없다.
필요에 따라 fseek 함수를 사용하여 원하는 위치로 건너뛰거나, 읽은 위치로 돌아가서 읽었던 데이터를 다시 읽을 수도 있다.

fseek 함수는 다음과 같은 형식으로 호출한다.

함수 원형 : int fseek(FILE *stream, long offset, int origin);
함수 사용 형식 : fseek(파일 포인터, 이동 거리, 기준 위치);

이 함수는 파일의 데이터를 읽을 기준 위치로 SEEK_SET(파일의 시작), SEEK_END(파일의 끝), SEEK_CUR(현재 위치)를 사용할 수 있고 지정한 기준 위치로부터 사용자가 지정한 '이동 거리'만큼 이동한다.

이동 거리는 양수 또는 음수로 지정할 수 있으며 양수를 명시하면 지정한 기준 위치에서 뒤로 이동하며 음수를 명시하면 앞으로 이동한다.
실제 이동한 위치는 파일 포인터에 저장된다.

fseek(p_file, 0, SEEK_SET); // 파일의 시작 위치로 이동
fseek(p_file, 32, SEEK_CUR); // 현재 위치에서 32바이트만큼 뒤로 이동

이렇게 이동한 위치를 값으로 확인하고 싶으면 ftell 함수를 사용하면 된다.
ftell 함수가 반환하는 값은 시작 위치를 0으로 계산한 값이기 때문에 파일의 끝으로 이동한 후에 ftell 함수를 사용하면 파일의 전체 크기를 알아낼 수도 있다.

함수 원형 : long ftell(FILE *stream);
함수 사용 형식 : 현재 열려 있는 파일 내에서 데이터를 읽거나 저장할 위치 = ftell(파일 포인터);
// fseek 함수와 ftell 함수로 바이너리 파일 크기 알아내기 
#include <stdio.h>

void main(){
    int file_size = 0;
    FILE *p_file = fopen("tip.dat", "rb");
    if(NULL != p_file){
        fseek(p_file, 0, SEEK_END);
        file_size = ftell(p_file);
        printf("파일 크기 : %d\n", file_size);
        fclose(p_file);
    }
}

 






파일 디스크립터

https://twofootdog.tistory.com/51








연습해보기

// 모든 줄을 읽을 수 있도록 코드를 수정하여 봅시다.

코드를 입력하세요
```#include <stdio.h>

int main() {
    FILE *fp;
    char str[20];

    fp = fopen("test.txt", "r");

    if(fp == NULL) {
        printf("파일 열기 실패");
        return 0;
    }

    fscanf(fp, "%s", str);

    printf("%s\\n", str);

    fclose(fp);

    return 0;
}

이 코드는 파일에서 첫번째 단어만 읽음.

#include <stdio.h>

int main() {
    FILE *fp;
    char str[256];

    fp = fopen("test.txt", "r");

    if(fp == NULL) {
        printf("파일 열기 실패\\n");
        return 0;
    }

    while(fgets(str, sizeof(str), fp) != NULL) {
        printf("%s", str);
    }

    fclose(fp);

    return 0;
}

fgets 함수를 사용하여 파일의 각 줄을 읽고 화면에 출력한다.
fgets는 파일의 끝에 도달하면 NULL을 반환하므로, 이를 이용하여 파일의 모든 줄을 읽을 수 있다.

0개의 댓글