관리 메뉴

공부 기록장 💻

[C언어] 파일 입출력 - FILE 파일 포인터, 파일 관련 함수 본문

# Language & Tools/C

[C언어] 파일 입출력 - FILE 파일 포인터, 파일 관련 함수

dream_for 2021. 1. 28. 17:00

 

파일의 개념

 

- 파일은 스트림(모든 입출력이 바이트 단위로 흐른다는 것)으로 취급되어, 일련의 연속된 바이트라고 볼 수 있다.

- 파일 포인터(FILE pointer)를 이용해 입출력 동작이 발생하는 위치를 나타낼 수 있다.

- 파일을 처음 열면 파일 포인터는 파일의 첫 번째 바이트를 가리키게 되고, 입출력 연산이 실행되며 파일 포인토가 자동적으로 이동된다.

- 텍스트 파일은 연속적인 줄로 구성되어 있어, 각 줄의 끝에는 줄바꿈 문자('\n' 또는 '\r'의 조합으로, 운영 체제마다 조금씩 다름)가 포함된다.

 

 

 

 

파일 처리

 

- 파일을 처리하기 위해서는 기본적으로 3가지의 동작이 필수적으로 요구된다: 파일 열기 / 읽고 쓰기 / 파일 닫기

- 가장 먼저 파일 포인터를 생성해야 한다. <stdio.h> 헤더 파일에 typedef를 이용해 선언된 구조체 자료형인 FILE을 사용한다.

- 각각 파일에 대하여 FILE 구조체가 하나씩 필요하다.

 

FILE *fp = NULL; // 파일 포인터 선언
( FILE 구조체 포인터 이름: fp, NULL값으로 초기화해주어 오류를 방지)

 

 

1) 파일 열기 / 닫기

 

FILE *fopen_s(&fp, const char *filename, const char *mode);
- 기능: 스트림 파일을 연다.
- 인수: 파일 포인터의 주소, 파일 이름, 개방 모드
- 반환값: 성공 시 파일 포인터, 실패 시 NULL값

int fclose(FILE *fp);
- 기능: 스트림 파일의 버퍼를 비우고 개방된 파일을 닫는다.
- 인수: 파일 포인터
- 반환값: 성공 시 0, 실패 시 EOF 

 

<개방 모드>

"r" - 읽기 모드. 파일이 존재하지 않는 경우 오류 발생.

"w" - 쓰기 모드로 새로운 파일 생성. 이미 존재하는 경우 기존 내용 지워지고 새로 덮어 씀.

"a" - 추가 모드. 파일이 없는 경우 새로 파일 생성하고 있는 경우 파일의 끝에 데이터 추가.

(각 모드 뒤에는 + 를 붙여 다른 모드로 전환 가능할 수 있고, t나 b를 붙여 파일의 형태가 텍스트 파일인지 이진 파일인지 구분할 수도 있다. 붙이지 않는 경우엔 텍스트 파일로 간주함.)

 

FILE *fp1 = NULL, *fp2 = NULL; // 두 개의 파일 포인터 생성

fopen_s(&fp1, "text1.txt", "rt"); // "rt" 읽기 모드로 "text1.txt" 이름의 파일을 연다.
fopen_s(&fp2, "text2.txt", "wt"); // "wt" 쓰기 모드로 "text2.txt" 이름의 파일을 연다.

위 예시에서는 두 개의 파일을 처리할 것이기 때문에, 각 파일에 대한 파일 포인터를 생성했다.

첫번째 "text1.txt" 이름의 파일은 읽기 모드로, 두번째 "text2.txt" 이름의 파일은 쓰기 모드로 파일을 열었다.

 

#include <stdio.h>

int main(void)
{
    FILE* fp1 = NULL, * fp2 = NULL; // 두 개의 파일 포인터 생성

    char fname1[100], fname2[100]; // 파일의 이름을 직접 입력받아 파일을 열 수 있도록 문자열 변수를 선언

    printf("첫번째 파일 이름: ");
    gets(fname1);
    fopen_s(&fp1, fname1, "rt");

    printf("두번째 파일 이름: ");
    gets(fname2);
    fopen_s(&fp2, fname2, "wt");

    if (fp1 = NULL || fp2 = NULL) {
        fprintf(stderr, "파일 열기 오류\n");
        exit(1);
    }

    fclose(fp); // 파일 닫기
    
    return 0;
  
}

이번에는 문자열을 입력받는 gets() 함수를 이용해 직접 파일의 이름을 입력받았다.

fopen_s() 함수의 파일명을 받든 두번째 인수에 그대로 문자열의 이름을 써주면 된다.

 

파일을 여는 과정에서 오류가 발생하여 NULL 값을 반환하는 경우도 있으므로,

오류 메시지를 출력하는 문장을 작성해 주어야 한다.

파일 열기에 실패한 경우, 형식화된 출력이 가능한 fpritnf() 함수의 stderr(표준 오류 스트림)을 이용하하면 화면에 오류 메시지 창이 나타난다.
그리고 exit(1) 문장이 호출됨과 동시에 프로그램은 종료가 된다.

 

 

 

 

2) 파일 읽기와 쓰기

 

- 파일을 연 후, 해당 파일로부터 내용을 읽거나, 파일에 내용을 쓰고 수정하는 작업을 하기 위해선 다음과 같은 함수를 사용해야 한다.

 

종류 입력 함수 원형 출력 함수 원형
문자 단위 int fgetc(FILE *fp); int fputc(int c, FILE *fp);
문자열 단위 char *fgets(char *buf, int n, FILE *fp); int fputs(const char *buf, FILE *fp);
서식화된 입출력 int fscanf(FILE *fp, const char *format); fprintf(FILE *fp, const char *format);
이진 데이터 size_t fread(void *ptr, int size, int count, FILE *fp); size_t(const void *ptr, it size, int count, FILE *fp);

 

위의 표에 나와있는 바와 같이,

여러 파일 입출력 라이브러리 함수들을 사용하여

문자/문자열 단위 또는 서식화된 입출력 방식으로 텍스트 파일을 처리하거나, 이진 데이터를 처리할 수 있다.

 

각 함수를 자세히 살펴보자.

 

 

<문자 단위 파일 입출력 함수>

 

int fgetc(FILE *fp);
- 기능: 스트림 파일로부터 하나의 문자를 입력받는다.
- 인수: 파일 포인터
- 반환값: 입력한 문자, 파일의 끝이거나 오류 발생시 EOF


int fputc(int c, FILE *fp);
- 기능: 스트림 파일에 하나의 문자를 쓴다.
- 인수: 출력할 문자, 파일 포인터
- 반환값: 출력한 문자, 오류 발생시 EOF

 

#include <stdio.h>

int main(void) {

	FILE* fp = NULL;
	int c; // 파일로부터 읽어드릴 문자를 저장할 변수

	// 쓰기 모드로 파일을 열어 문자 'a'를 해당 파일에 쓴다.
	fopen_s(&fp, "text.txt", "wt");
	fputc('a', fp);
	fclose(fp);

	// 읽기 모드로 파일을 열어 한 개의 문자를 파일로부터 읽는다.
	fopen_s(&fp, "text.txt", "rt");
	c = fgetc(fp);
	printf("%d: %c\n", c, c); // 해당 문자를 모니터에 정수, 문자 형태로 출력한다.
	fclose(fp);

	return 0;
}

 

쓰기 모드로 "text.txt" 파일을 생성하고

fputc() 함수를 이용하여 파일에 문자 'a'를 출력한 뒤, 파일을 닫는다.

이후 읽기 모드로 파일을 다시 열어, fgetc() 함수를 이요하여 문자 한 개를 입력받아 변수 c에 저장한다.

화면 실행창에 정수, 문자의 형태로 해당 변수를 출력하면

a의 아스키코드 값인 97과 문자형 형태의 a가 성공적으로 출력된 것을 확인할 수 있다.

 

 

 

<문자열 단위 파일 입출력 함수>

 

char *fgets(char *buf, int n, FILE *fp);
- 기능: 스트림 파일로부터 개행 문자를 포함한 한 줄의 문자열을 입력받는다.
- 인수: 입력 데이터를 저장할 배열의 포인터, 최대 입력 문자 수, 파일 포인터
- 반환값: buf 반환, 파일 데이터를 모두 읽으면 NULL


int *fputs(const char *buf, FILE *fp);
- 기능: 스트림 파일에 한 줄의 문자열을 쓴다.
- 인수: 출력할 문자열, 파일 포인터
- 반환값: 음수가 아닌 값, 오류 발생 시 EOF 

 

fgets() 함수는 줄바꿈 문자를 만나거나, n 바이트 이상의 문자가 읽히게 되는 순간 읽기를 중단한다.

줄바꿈 문자는 NULL 문자로 바뀌어 삽입된다. 

 

#include <stdio.h>
#include <string.h>

int main(void) {

	FILE* fp = NULL;
	char buf1[100], buf2[100];

	// 한 줄의 문자열을 입력받아 buf1에 저장한다.
	printf("문장 입력: ");
	gets(buf1);

	// buf1에 저장된 문자열을 쓰기 모드로 연 파일에 쓴다.
	fopen_s(&fp, "text.txt", "wt");
	fputs(buf1, fp);
	fclose(fp);

	// 이번엔 읽기 모드로 파일을 열어, 한 줄의 문자열을 읽어 buf2에 저장한다.
	fopen_s(&fp, "text.txt", "rt");
	if (fp == NULL) {
		fprintf(stderr, "파일 열기 실패\n");
		exit(1);
	}

	char str[100];
	strcpy_s(str,100,fgets(buf2, 100, fp)); // fgets()의 반환값 str은 buf2와 같음을 확인한다.
	printf("%s 파일로부터 읽은 문자열 : %s\n", str, buf2);
	fclose(fp);

	return 0;
}

 

 

 

 

 

 

fputs()와 fgets()함수가 잘 작동하는지 확인하기 위해

파일에 쓰고 파일로부터 읽어드릴 두 개의 문자열을 따로 선언해 보았다.

 

그리고 반환값이 정확히 무엇을 의미하는지 확인하기 위해 새로운 문자열 str을 선언하여

<string.h> 헤더 파일에 저장되어있는 문자열 처리 함수 strcpy_s() 를 이용하여 fgets() 함수의 반환값을 저장해보았다.

(나중에 여러 줄의 문자열을 읽어야 하는 프로그램을 작성할 때, fgets()의 반환값이 꽤나 중요한 역할을 한다.)

 

키보드로부터 입력받은 문자열 buf1인 "hello, everyone."이 파일에 출력되었고, 

다시 파일로부터 읽어드린 문자열이 buf2에 저장되어 모니터를 통해 올바르게 출력됨을 확인할 수 있다.

 

 

 

<서식화된 형식 파일 입출력 함수>

 

int fscanf(FILE *fp, const char *format, ...);
- 기능: 스트림 파일의 데이터를 형식에 따라 변환하여 변수 리스트에 입력한다.
- 인수: 파일 포인터, 형식 문자열, 변수 리스트
- 반환값: 입력에 성공한 데이터 수, 파일의 데이터를 모두 읽으면 EOF


int fprintf(FILE *fp, const char *format, ...);
- 기능: 데이터를 형식에 따라 변환하여 스트일 파일에 쓴다.
- 인수: 파일 포인터, 형식 문자열, 출력 데이터
- 반환값: 출력한 문자 수, 실패할 경우 음수

 

#include <stdio.h>

int main(void) {

	FILE* fp = NULL;
	// 학생의 이름, 학번, 학점을 저장할 변수 선언
	char name[50], name2[50];
	int num, num2;
	double score, score2;

	printf("학생의 이름, 학번, 점수 입력: ");
	scanf_s("%s %d %lf", name, 50, &num, &score);

	// 쓰기 모드로 파일을 열어 입력받은 학생의 정보들을 각 형식대로 파일에 쓴다.
	fopen_s(&fp, "score.txt", "wt");
	fprintf(fp, "%s %d %f", name, num, score);
	fclose(fp);

	// 일기 모드로 파일을 열어 파일로부터 읽은 학생의 정보들을 서식화된 형식대로 읽어 드린 후, 화면에 출력한다.
	fopen_s(&fp, "score.txt", "rt");
	if (fp == NULL) {
		fprintf(stderr, "파일 열기 오류\n");
		exit(1);
	}
	int c=fscanf_s(fp, "%s %d %lf", name2, 50, &num2, &score2);
	printf("파일로부터 입력받은 학생 정보(입력에 성공한 데이터 수 : %d)\n이름: %s\n학번: %d\n학점: %f\n",c, name2, num2, score2);
	fclose(fp);

	return 0;
}

 

 

 

 

 

 

학생의 이름, 학번, 학점을 키보드로부터 입력받아 문자열, 정수, 실수 형식의 변수에 각각 저장하여 파일에 쓰고,

생성된 파일로부터 정보들을 다시 읽어들여 화면에 출력하는 프로그램이다.

fprintf(), fscanf() 함수가 제대로 작동하는지 확인하기 위해 변수를 각각 두 개씩 생성하여 데이터를 따로 저장한다.

프로그램은 서식화된 각 변수의 형식대로만 입력하고 출력하므로 주의해야 한다.

 

버퍼 오버플로우 문제를 개선한 scanf_s() 함수와 마찬가지로,

fscanf_s() 함수를 사용할 때에도 문자열이나 문자를 입력 받을 때에는 버퍼의 크기도 함께 전달한다.

 

 

 

 

<이진 데이터 파일 입출력 함수>

 

size_t fread(void *ptr, int size, int count, FILE *fp);
- 기능: 스트림 파일에서 데이터를 읽어 void 포인터 ptr이 가리키는 배열에 저장한다.
- 인수: 읽은 데이터를 저장할 메모리 주소, 데이터 크기, 개수, 파일 포인터
- 반환값: 읽기에 성공한 데이터 개수


size_t fwrite(void *ptr, int size, int count, FILE *fp);
- 기능: void 포인터 ptr이 가리키는 배열의 데이터를 스트림 파일에 쓴다.
- 인수: 출력할 데이터의 메모리 주소, 데이터 크기, 개수, 파일 포인터
- 반환값: 출력에 성공한 데이터의 개수

 

모든 정보가 문자열로 변환되어 기록되는 텍스트 파일과 다르게,

이진 파일은 데이터가 직접 저장되는 파일이다. 

아스키 코드로 데이터가 저장되어 있지 않으므로 이식성이 낮으며,

직접 파일의 내용을 확인하거나 화면으로 출력하는 것은 불가능하다.

그러나 데이터가 상당히 크고 실행 속도가 중요한 경우 이진 파일의 형식으로 데이터를 저장하는 것이 효율적이다.

사운드, 이미지 파일이 이진 파일의 예이다.

 

첫번째 인수가 void 형 포인터인 이유는, 문자, 구조체, 배열 등 다양한 형태의 데이터를 처리할 수 있기 때문.

반환형 size_t는 unsinged int 정수의 크기를 나타낼 때 쓰이는 자료형이다. (보통 배열의 요소나 반복문의 반복 수를 셀 때 쓰인다)

 

 

이진 파일을 이용하여 학생들의 정보를 파일에 쓰고 읽는 프로그램을 작성해보자.

 

#include <stdio.h>

#define SIZE 3
// 문자열, 정수, 실수형 변수의 구조체를 새로운 자료형 STUD로 생성하여 선언
typedef struct{
	char name[20];
	int num;
	double score;
}STUD;


int main(void) {

	FILE* fp = NULL;
	// SIZE 크기의 STUD형 배열 선언
	STUD list[SIZE] = {
		{"KIM", 1, 4.5},
		{"PARK", 2, 4.3},
		{"LEE", 3, 3.7}
	};
	STUD s; // STUD형 변수 s 선언

	// 쓰기 모드로 이진 dat 파일을 열어 STUD형 배열 list에 저장된 데이터들을 파일에 쓴다.

	fopen_s(&fp, "score.dat", "wb");
	fwrite(list, sizeof(STUD), SIZE, fp); // 인수 (배열 list의 시작주소, STUD형의 크기, SIZE 개수, 파일 포인터 fp)
	fclose(fp);


	/* 읽기 모드로 파일을 열어 STUD형 배열 list에 저장된 데이터들을 파일로부터 읽어
	STUD형 변수 s에 저장한 뒤,저장된 배열 요소의 개수만큼 출력한다.*/

	fopen_s(&fp, "score.dat", "rb");
	if (fp == NULL) {
		fprintf(stderr, "파일 열기 오류\n");
		exit(1);
	}

	for (int i = 0;i < SIZE;i++)
	{
		fread(&s, sizeof(STUD), 1, fp); // STUD형 변수 s의 주소에 접근하여 하나의 레코드를 읽어드린다.
		printf("이름 = %s, 학번 = %d, 학점 = %f\n", s.name, s.num, s.score);
	}
	fclose(fp);

	return 0;
}

 

 

 

 

 

 

학생 3명에 대한 정보를 모두 같은 방법으로 처리할 것이기 때문에

구조체를 생성하고, 이를 새로운 자료형 STUD으로 정의하여 선언하였다.

그리고 STUD형 배열 list에 3명의 정보를 저장하였다.

쓰기 모드로 이진 파일을 생성하고, fwrite() 함수를 이용하여

배열에 저장되어 있는 3개의 STUD형 데이터들을 파일에 쓴다.

이후, 읽기 모드로 이진 파일을 열고 fread() 함수를 3번 호출하도록 한다.

호출될 때마다 STUD형 크기만큼의 데이터를 읽어 STUD형 변수 s에 저장하고, 이를 화면에 출력하도록 한다.

 

 

 

파일 관련 함수

 


int feof(FILE *fp);
- 기능: 파일의 데이터를 모두 읽었는지 확인한다.
- 인수: 파일 포인터
- 반환값: 파일의 데이터를 모두 읽은 경우 0, 그렇지 않으면 0이 아닌 값

 

파일의 끝에 도달했는지 검사하는 함수이다.

하지만 이 함수를 사용할 때 주의해야 할 점이 한 가지 있는데,

파일의 데이터를 전부 읽은 후 바로 0 값을 반환하는 것이 아니고,

더이상 데이터가 없는 상황에서 포인터가 작동할 때 에러를 발생시키면서 파일 읽는 동작을 중단한다는 점이다.

(이런 문제 때문에 feof() 함수를 사용하는 프로그램을 작성할 때 꽤나 애먹었다..ㅠㅠ

아래 링크를 통해 해답을 구할 수 있었으니 참고.)

 

stackoverflow.com/questions/5431941/why-is-while-feof-file-always-wrongme.tistory.com/380

 

Why is “while ( !feof (file) )” always wrong?

I've seen people trying to read files like this in a lot of posts lately: #include <stdio.h> #include <stdlib.h> int main(int argc, char **argv) { char *path = "stdin"; FILE *...

stackoverflow.com

me.tistory.com/380

 

C언어의 feof() 함수 사용에 대해

Studies 카테고리는 제가 공부하다가 기록하는 용도로 씁니다. 혹시 여기에 기록된 것이 잘못되었다면, 댓글로 꼭 알려주세요. feof, file - end of file 이라고 해서 파일의 끝에 도달하면 멈춘다고 생

me.tistory.com

 

 

 

예시를 살펴보자.

 

#include <stdio.h>

int main(void) {
	
	FILE* fp;
	int cnt = 0;

	fopen_s(&fp, "text.txt", "wt");
	fputc('a', fp);
	fputc('b', fp);

	fclose(fp);

	fopen_s(&fp, "text.txt", "rt");
	while (!feof(fp)) {
		cnt++;
		printf("%c ", fgetc(fp));
	}
	printf("\ncnt = %d\n", cnt);
	
	fclose(fp);

	return 0;
}

 

 

 

텍스트 파일에는 a와 b라는 문자 두 개만을 파일에 출력하였다.

그러니 feof() 함수를 사용해서 파일의 끝에 도달할 때까지 파일에 저장되어 있는 문자를 입력 받으면,

반복문은 두 문자만을 읽을테니 두 번만 돌아가야 한다.

그러나 변수 cnt의 값을 살펴보면 반복문이 3번 실행됨을 확인할 수 있다.

다시 확인해보기 위해, 여기에 코드 두 줄을 추가 해보자.

 

#include <stdio.h>

int main(void) {
	
	FILE* fp;
	int cnt = 0;

	fopen_s(&fp, "text.txt", "wt");
	fputc('a', fp);
	fputc('b', fp);

	fclose(fp);

	fopen_s(&fp, "text.txt", "rt");
	while (!feof(fp)) {
		cnt++;
		printf("%d ", feof(fp));
		printf("%c ", fgetc(fp));
		printf("%d \n", feof(fp));
	}
	printf("\ncnt = %d\n", cnt);
	
	fclose(fp);

	return 0;
}

 

 

 

fgetc() 함수로 문자를 읽어드리기 전과 후에 feof() 값을 출력하는 코드를 추가하니

두번째 반복문에서 문자 b를 입력받고 난 뒤에 feof()값은 그대로 0임을 확인해 볼 수 있고,

이로 인해 반복문이 한 번 더 실행되었을 때 feof() 값에 변화가 생긴다는 것을 알 수 있다.

 

**따라서 feof() 함수를 사용할 때는 꽤나 주의가 필요하다.

 

 

<파일 포인터 관련 함수>

int fseek(FILE *fp, long int offset, int origin);
- 기능: 스트림 파일의 위치 지시자를 설정한다.
- 인수: 파일 포인터, 이동할 바이트 수, 이동할 기준 위치
- 반환값: 성공 시 0, 실패 시 0이 아닌 값

 

상수 설명
SEEK_SET 0 파일의 시작
SEEK_CUR 1 현재 위치
SEEK_END 2 파일의 끝

 

fseek()함수를 이용해 포인터의 위치 표시자를 정밀하게 제어 가능하다.

 

#include <stdio.h>

int main(void) {
	
	FILE* fp;
	char buffer[] = "hello";

	fopen_s(&fp, "text.txt", "wt");
	fputs(buffer, fp);
	fclose(fp);

	fopen_s(&fp, "text.txt", "rt");
	fseek(fp, 1, SEEK_SET); // 파일의 시작으로부터 1바이트만큼 떨어진 곳으로 파일 위치 표시자를 이동시킴
	printf("%c\n", fgetc(fp));
	fseek(fp, -1, SEEK_END); // 파일의 끝에서부터 -1바이트만큼 떨어진 곳으로 파일 위치 표시자를 이동시킴
	printf("%c\n", fgetc(fp));
	fclose(fp);

	return 0;
}

 

 

 

hello라는 문자열이 출력된 파일에는 마지막 EOF값을 포함하여 총 6개의 문자가 저장되어 있다.

따라서, fseek(fp, 1, SEEK_SET) 함수가 실행된 후에는 위치 표시자는 h 다음의 e를 가리키게 된다.

offset 값은 음수가 사용될 수도 있다. 왼쪽으로 이동하라는 의미이다.

fseek(fp, -1, SEEK_END) 함수가 실행된 후에는, 마지막 EOF값로부터 1만큼 왼쪽으로 위치 표시자가 이동되어

o의 값을 가리키게 된다.

 

보통 데이터를 순차적으로 읽게 되면 파일 포인터는 파일이 열릴 때 파일의 시작 위치에서 순차적으로 증가하며 파일의 끝으로 이동한다.

파일의 끝을 가리켜 새로운 데이터를 추가해야 할 때, 혹은 중간에 위치한 데이터를 수정하거나 삭제해야 하는 경우 

fseek() 함수를 사용하여 포인터의 위치를 조작하여 파일 처리를 쉽게 할 수 있다.

 

 

728x90
반응형
Comments