파일 입출력

유석현(SeokHyun Yu)·2022년 7월 24일

C

목록 보기
23/26
post-thumbnail

1. 파일과 스트림, 그리고 기본적인 파일의 입출력

프로그램상에서 파일에 저장되어 있는 데이터를 참조하길 원한다고 가정해 보자.

이 때 제일먼저 해야할 일은 무엇일까?

그것은 우리가 구현한 프로그램과 참조할 데이터가 저장되어 있는 파일 사이에 데이터가 이동할 수 있는 다리를 놓는 일이다.

이러한 데이터 이동의 경로가 되는 다리를 가리켜 '스트림'이라 하는데, 스트림에 대해서는 앞서 설명한 바 있다.


#include <stdio.h>

FILE * fopen(const char * filename, const char * mode);

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

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

위 함수의 첫 번째 인자로는 스트림을 형성할 파일의 이름을, 두 번째 인자로는 형성할 스트림의 종류에 대한 정보를 문자열의 형태로 전달한다.

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

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

즉, 이 포인터를 이용해서 파일에 데이터를 저장하거나 파일에 저장된 데이터를 읽게 된다.

fopen 함수를 호출했을 때 일어나는 일들을 요약하면 다음과 같다.


앞서 한 차례 언급하였지만 스트림은 '한 반향으로 흐르는 데이터의 흐름'을 의미한다.

때문에 스트림은 데이터를 파일로부터 읽어 들이기 위한 '입력 스트림'과 데이터를 파일에 쓰기 위한 '출력 스트림'으로 구분된다.

자! 그럼 스트림의 형성을 위한 fopen 함수의 호출 방법을 설명하겠다.

fopen 함수를 호출할 때에는 다음 두 가지가 인자로 전달되어야 한다.

- 첫 번째 전달인자: 스트림을 형성할 파일의 이름

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

먼저 출력 스트림의 형성을 요청하는 fopen 함수의 호출문을 보이겠다.

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

이것이 의미하는 바는 다음과 같다.

"파일 data.txt와 스트림을 형성하되 wt 모드로 스트림을 형성해라!"

위의 문장에서 말하는 'wt 모드의 스트림''텍스트 데이터를 쓰기 위한 출력 스트림'을 뜻하는데, 이에 대해서는 잠시 후에 별도로 설명하겠으니, 일단은 출력 스트림이 형성된다는 사실에만 주목을 하자.

지금 형성한 스트림은 출력 스트림이기 때문에 파일에 데이터를 쓸 수는 있어도 읽지는 못한다.

만약에 파일로부터 데이터를 읽기 원한다면 다음의 문장을 통해서 별도로 입력 스트림을 형성해야 한다.

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

이것이 의미하는 바는 다음과 같다.

"파일 data.txt와 스트림을 형성하되 rt 모드로 스트림을 형성해라!"

위의 문장에서 말하는 'rt 모드의 스트림''텍스트 데이터를 읽기 위한 입력 스트림'을 뜻하는데, 이 역시도 잠시 후에 별도로 설명하겠으니, 일단은 입력 스트림이 형성된다는 사실에만 주목을 하자.


아직 fopen 함수의 두 번째 전달인자에 대해서는 설명하지 않았지만 "rt"를 전달함으로써 텍스트 데이터 입력용 스트림이, "wt"를 전달함으로써 텍스트 데이터 출력용 스트림이 형성된다는 사실 정도는 확인을 하였다.

따라서 이번에는 실제로 스트림을 형성해서 파일에 데이터를 써보고자 한다.

그리고 이를 위해서 fputc 함수를 다음과 같이 호출할 것이다.

fputc('A', fp);

파일대상의 fputc 함수에 대해서는 이후에 다시 설명을 하니, 여기서는 실제로 파일에 데이터가 저장되는지를 확인하는 도구로만 사용을 하자.

#include <stdio.h>

int main(void)
{
	FILE * fp = fopen("data.txt", "wt");

	if(fp==NULL)
	{
	   puts("파일오픈실패");
	   return -1;
	}
	
	fputc('A', fp);
	fputc('B', fp);
	fputc('C', fp);
	
	fclose(fp);
	
	return 0;
}

위 코드에서는 파일 data.txt와의 출력 스트림을 형성하고 있다.

이렇듯 출력 스트림을 형성하는 경우에는 해당 파일이 생성된다.

따라서 여러분은 파일 data.txt가 생성됨을 확인할 수 있다.

실행방법에 따라서, 그리고 실행환경 및 설정에 따라서 파일이 생성되는 위치는 달라진다.

이렇게 해서 스트림이 형성되면, 이제부터 fp는 파일 data.txt를 지칭하는 포인터가 된다.

따라서 fputc('A', fp)가 실행되면, fp가 지칭하는 파일 data.txt에 문자 A가 저장된다.

이제 fopen 함수가 반환하는 FILE 구조체 포인터의 용도를 이해할 수 있지 않겠는가?

이렇게 해서 문자 A, B, C를 저장한 다음에 마지막으로 fclose 함수가 호출되면 데이터는 안정적으로 저장이 되고, data.txt와 연결되었던 출력 스트림은 소멸이 된다.

자! 그럼 코드를 실행한 다음에 파일 data.txt를 찾아서 열어보자.

실제로 데이터가 저장되었음을 확인할 수 있을 것이다.


위 코드에서 호출한 fclose 함수에 대해서 자세히 살펴보자.

간단히 설명하면 fclose 함수는 fopen 함수의 반대 기능을 제공한다.

fopen 함수가 스트림을 형성하는 함수라면, fclose 함수는 스트림을 해제하는 함수이다.

#include <stdio.h>

int fclose(FILE * stream);

그렇다면 이렇게 fclose 함수의 호출을 통해서 개방되었던 파일을 닫아줘야 하는 이유는 무엇일까?

여기에는 다음 두 가지 이유가 있다.

- 운영체제가 할당한 자원의 반환

- 버퍼링 되었던 데이터의 출력

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

그리고 운영체제는 스트림의 형성을 위해서 시스템의 자원(주로 메모리)을 할당한다.

그런데 이 자원은 파일을 닫아주지 않으면 할당된 채로 남아있게 되어, 그만큼의 자원손실을 초래하기 때문에 파일의 사용이 끝나는 즉시 fclose 함수를 호출해서 자원을 반환해줄 필요가 있다.

또한 fclose 함수의 호출을 통해서 파일을 닫아주면 출력버퍼에 저장되어 있던 데이터가 파일로 이동하면서 출력버퍼는 비워지게 된다.

때문에 사용이 끝난 파일은 곧바로 fclose 함수를 호출해주는 것이 좋다.


이번에는 위 코드에서 실행을 통해서 만든 파일 data.txt를 열어서 그 안에 저장된 문자를 읽어 들이는 프로그램을 작성해 볼 텐데, 이를 위해서 fgetc 함수를 다음의 형태로 호출할 것이다.

int ch=fgetc(fp);

fgetc 함수는 파일에 저장된 문자 하나를 반환하는 함수로서 자세한 설명은 이후에 다시 진행하겠으니, 여기서는 위의 함수호출로 인하여 FILE 구조체 포인터 fp가 지칭하는 파일에 저장된 문자 하나가 반환되어 변수 ch에 저장된다는 사실만을 기억하고 다음 코드를 관찰하자.

#include <stdio.h>

int main(void)
{
	int ch, i;

	FILE * fp = fopen("data.txt", "rt");
	
	if(fp==NULL)
	{
		puts("파일오픈 실패!");
		return -1;
	}
	
	for(int i=0; i<3; i++)
	{
	   ch=fgetc(fp);
	   putchar(ch);
	}
	
	fclose(fp);
	
	return 0;
}

이렇게 해서 파일을 생성하여 데이터를 저장도해봤고, 파일을 열어서 데이터를 읽어도 봤다.

그러니 이제부터는 스트림의 형성방법과 데이터의 입출력 방법을 보다 자세히 살펴볼 차례이다.


2. 파일의 개방 모드

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

하지만 형성할 수 있는 스트림의 종류는 훨씬 더 다양한데, 기본적으로 다음 두 가지 기준을 통해서 스트림을 구분하게 된다.

- 기준1: 읽기 위한 스트림인가? 쓰기 위한 스트림인가?

- 기준2: 텍스트 데이터를 위한 스트림인가? 바이너리 데이터를 위한 스트림인가?

스트림을 구분하는 기준은 두 가지인데, 이 중에서 데이터의 이동방향을 기준으로 다음과 같이 네 가지로 구분할 수 있다.

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

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

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

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

그러나 C언어는 이를 바탕으로 총 6가지로 스트림을 세분화한다.

그리고 이 6가지는 다음과 같다.

위 표를 참조하여 필요로 하는 스트림의 특성과 일치하는 '파일의 개방 모드'를 선택하면 된다.

그리고 이 모드의 이름이 fopen 함수의 두 번째 인자가 된다.

"파일의 개방 모드 중 r+, w+, a+는 읽기와 동시에 쓰기가 가능하므로 더 좋은 모드라고 생각할 수 있다. 그러나 이러한 모드를 기반으로 작업하는 경우에는 읽기에서 쓰기, 그리고 쓰기에서 읽기로 작업을 변경할 때마다 메모리 버퍼를 비워줘야 하는 등의 불편함과 더불어 잘못된 사용의 위험성도 따른다. 그래서 r, w, a 중에서 하나를 선택하여 스트림을 형성하는 것이 좋으며, 이것이 보다 일반적인 선택이다.


이어서 스트림을 구분하는 두 번째 기준에 대한 설명을 시작하겠다.

그리고 이를 위해서 파일에 담을 수 있는 데이터들의 유형을 나열해 보았다.

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

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

- 영상파일: 바이너리 데이터

- 음원파일: 바이너리 데이터

정리하면, 사람이 인식할 수 있는 문자를 담고 있는 파일을 가리켜 '텍스트 파일(text file)'이라 하며, 그 이외에 컴퓨터가 인식할 수 있는 데이터를 담고 있는 파일을 가리켜 '바이너리 파일(binary file)'이라 한다.

그렇다면 데이터의 입출력을 위해서 스트림을 형성할 때 이와 관련해서 특별히 신경 쓸 부분은 무엇일까?

그것은 바로 '문장의 끝을 의미하는 개행의 표현방식'이다.


개행은 일반적인 문자 데이터와 성격이 조금 다르다.

개행은 줄이 바뀌었다는 일종의 현상이지 그 자체가 하나의 데이터로 존재하는 대상은 아니기 때문이다.

예를 들어서 누구나 한 장의 종이에 문자 'A' 하나만을 표시할 수는 있다.

그러나 그 누구도 아무런 약속 없이 흰 종이에 개행 하나만을 표시할 수는 없다.

그래서 C언어에서는 개행을 \n으로 표현하도록 약속하였다.

그런데 중요한 사실은, 모든 컴퓨터 환경에서의 약속이 아닌 C언어만의 약속이라는 점이다.

그렇다면 다른 환경에서는 개행을 어떻게 표시할까?

개행에 대한 몇몇 약속을 소개하면 다음과 같다.

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

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

- Unix 계열의 파일 내 개행: \n

따라서 Windows 기반의 편집기는 파일에 \r\n이 나란히 등장할 때 개행으로 인식을 하고, 매킨토시 기반의 편집기는 파일에 \r이 등장할 때 개행으로 인식을 한다.

이렇듯 개행의 표현은 운영체제마다 차이가 있기 때문에 개행 문자가 포함되는 텍스트 데이터의 저장에는 주의가 필요하다.

C언어에서 개행을 의미하는 문자 \n을 그대로 파일에 저장하고, 이 파일은 Windows나 Mac의 편집기로 열어보면 \n이 개행으로 표시되지 않음을 확인할 수 있다.

물론 Unix는 C언어와 마찬가지로 \n을 개행으로 인식하므로, Unix 계열의 편집기에서는 개행으로 확인이 된다.

그렇다면 개행의 표시방법이 C언어와 다른 운영체제에서는 개행 정보를 파일에 어떻게 저장해야 할까?

이를 위해서는 C 프로그램의 실행과정에서 \n이 Windows의 파일에 저장될 때에는 \r\n으로, 그리고 Mac의 파일에 저장될 때에는 \r로 저장되어야 해당 운영체제에서 개행으로 인식이 된다.

그런데 막상 이러한 형태의 변환을 직접 하려니 귀찮다는 생각이 들지 않을 수 없다.

누군가 이러한 변환을 대신해줬으면 하는 바램도 든다.

그렇다면 파일을 텍스트 모드로 개방하면 된다.


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

예를 들면 Windows를 기반으로는 다음 두 가지의 변환이 자동으로 이뤄진다.

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

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

때문에 우리가 직접 개행 문자의 변환을 신경 쓸 필요가 없다.

그저 텍스트 모드로 파일을 개방만하면 된다.

그리고 텍스트 모드의 파일 개방을 위해서는 fopen 함수의 두 번째 인자로 다음 중 하나를 전달해야 한다.

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

이는 위에서 정리한 파일 개방 모드에 텍스트 모드를 의미하는 t가 붙은 형태이다.

반대로 바이너리 데이터를 저장하고 있는 파일의 경우에는 이러한 형태의 변환이 일어나면 안되기 때문에 바이너리 모드로 파일을 개방해야 한다.

그리고 이를 위해서는 fopen 함수의 두 번째 인자로 다음 중 하나를 전달해야 한다.

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

이 역시 위에서 정리한 파일 개방 모드에 바이너리 모드를 의미하는 b가 붙은 형태이다.

참고로 아무것도 붙이지 않으면 파일은 텍스트 모드로 개방된다.


3. 파일 입출력 함수의 기본

파일의 개방 모드에 대한 정리가 끝났으니, 개방된 파일 대상의 데이터 입력 및 출력 방법을 살펴볼 차례이다.

전에 다음 함수들에 대해 설명한 바 있다.

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);

당시에는 위 함수들의 매개변수 stream에 표준 입출력을 의미하는 stdin이나 stdout을 인자로 전달함으로써 키보드와 모니터를 대상으로 입출력을 진행하였다.

그러나 매개변수의 형이 의미하듯이 매개변수 stream에 FILE 구조체의 포인터를 인자로 전달하여 파일대상의 입출력을 진행할 수 있다.

그럼 예제를 통해서 위의 함수들을 기반으로 파일 입출력을 진행해 보겠다.

#include <stdio.h>

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

	fputc('A', fp);
	fputc('B', fp);
	fputs("My name is Hong \n", fp);
	fputs("Your name is Yoon \n", fp);

	fclose(fp);

	return 0;
}

위 코드에서는 문자 뿐만 아니라 문자열도 출력하고 있으며, 그 문자열에는 개행문자도 포함되어 있다.

따라서 반드시 텍스트 모드로 파일을 개방해야 한다.

그럼 이어서 위 예제의 실행을 통해서 생성된 파일에 저장된 데이터를 읽어보겠다.

#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); // A
	ch=fgetc(fp);
	printf("%c \n", ch); // B
	
	fgets(str, sizeof(str), fp);
	printf("%s", str); // My name is Hong
	
	fgets(str, sizeof(str), fp);
	printf("%s", str); // Your name is Yoon
	
	fclose(fp);
	
	return 0;
	
}

위의 두 코드를 통해서 주목할 사실이 하나 더 있다.

그것은 문자열의 끝에 \n이 존재한다는 것이다.

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

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

위 코드에서 총 두 개의 문자열을 읽어들이기 위해 fgets 함수를 두 번 호출했는데, 매번 호출이 될 떄마다 개행 문자를 만날 때까지 문자열을 읽어 들이게 된다.

따라서 fgets 함수의 호출을 통해서 읽어 들일 문자열의 끝에는 반드시 \n 문자가 존재해야 한다.


때로는 파일의 마지막에 저장된 데이터까지 모두 읽어 들여야 하는 상항이 존재한다.

그리고 이를 위해서는 파일의 끝을 확인하는 방법이 필요한데, 다음 함수가 이러한 목적으로 정의된 함수이다.

#include <stdio.h>

int feof(FILE * stream);

이 함수는 인자로 전달된 FILE 구조체의 포인터를 대상으로, 더 이상 읽어 들일 데이터가 존재하지 않으면(파일의 끝까지 모두 읽어 들인 상태이면) 0이 아닌 값을 반환한다.

다음은 텍스트 파일을 fgetc 함수를 통해 문자 단위로 복사하는 코드이다.

이 코드를 보며 feof 함수가 어떻게 쓰이는지 확인해보자.

#include <stdio.h>

int main(void)
{
	FILE * src=fopen("src.txt", "rt");
	FILE * dst=fopen("dst.txt", "wt");
	int ch;
	
	if(src==NULL || dst==NULL){
		puts("파일오픈 실패!");
		return -1;
	}
	
	while((ch=fgetc(src))!=EOF)
		fputc(ch, dst);
		
	if(feof(src)!=0) // 파일에 끝에 도달하면 0이 아닌 값을 반환
		puts("파일복사 완료!");
	else
		puts("파일복사 실패!");
		
	fclose(src);
	fclose(dst);
	
	return 0;
}

그럼 이어서 코드 하나를 더 보이겠다.

이 코드 역시 텍스트 파일을 복사한다.

다만 위의 코드와 달리 문자가 아닌 문자열 단위로 복사를 진행할 뿐이다.

#include <stdio.h>

int main(void)
{
	FILE * src = fopen("src.txt", "rt");
	FILE * des = fopen("des.txt", "wt");
	char str[20];
	
	while(fgets(str, sizeof(str), src)!=NULL)
		fputs(str, des);
	
	if(feof(src)!=0)
		puts("파일 복사 완료");
	
	fclose(src);
	fclose(des); 
	
    return 0;
}

fgets 함수는 파일의 끝에 도달해서 더 이상 읽을 데이터가 존재하지 않거나 오류가 발생하는 경우에 NULL을 반환한다.


지금까지 설명한 입출력 함수들은 텍스트 데이터의 입출력을 진행하는 함수들이었다.

반면에 이번에 설명하는 함수들은 바이너리 데이터의 입출력을 진행하는 함수들이다.

참고로 바이너리 데이터의 입출력은 텍스트 데이터의 입출력보다 단순하기 때문에 다양한 함수를 제공하지 않고 있다.

그럼 먼저 바이너리 데이터의 입력에 사용되는 fread 함수를 소개하겠다.

#include <stdio.h>

size_t fread(void * buffer, size_t size, size_t count, FILE * stream);

위 함수는 다음과 같이 호출이 된다.

int buf[12];

fread((void*)buf, sizeof(int), 12, fp); // fp는 FILE 구조체 포인터

그리고 위의 fread 함수의 호출문은 다음의 의미로 해석이 된다.

"sizeof(int) 크기의 데이터 12개를 fp로부터 읽어 들여서 배열 buf에 저장하라"

fread 함수는 두 번째 전달인자와 세 번째 전달인자의 곱의 바이트 크기만큼 데이터를 읽어 들이는 함수이다.

따라서 위의 fread 함수호출을 통해서 int형 데이터 12개를 fp로부터 읽어서 배열 buf에 저장하게 된다.

그리고 이 함수는 실제로 읽어 들인 데이터의 갯수를 반환하는데, 위 코드와 같이 sizeof(int) 크기의 데이터를 12개 읽어 들이는 경우에는, 함수의 호출이 성공을 하고 요청한 분량의 데이터가 모두 읽혀지면 12가 반환된다.

반면 함수의 호출이 성공을 했지만 파일의 끝에 도달을 해서 12개를 모두 읽어 들이지 못했거나 오류가 발생하는 경우에는 12보다 작은 값이 반환된다.

그럼 이어서 바이너리 데이터 출력에 사용되는 fwrite 함수를 소개하겠다.

#include <stdio.h>

size_t fwrite(const void * buffer, size_t size, size_t count, FILE * stream);

위 함수는 다음과 같이 호출을 한다.

int buf[7]={1,2,3,4,5,6,7};

fwrite((void*)buf, sizeof(int), 7, fp);

그리고 위의 fwrite 함수 호출문은 다음의 의미로 해석이 된다.

"sizeof(int) 크기의 데이터 7개를 buf로부터 읽어서 fp에 저장해라"

자 그럼 지금 설명한 두 함수를 이용해서 바이너리 파일을 복사하는 프로그램을 작성해보겠다.

#include <stdio.h>

int main(void)
{
	FILE * src=fopen("flower.jpeg", "rb");
	FILE * des=fopen("flower2.jpeg", "wb");
	char buf[20];
	int readCnt;
	
	while(1)
	{
		readCnt=fread((void*)buf, 1, sizeof(buf), src); // src로부터 데이터 읽기
		
		if(readCnt<sizeof(buf)) // 파일의 끝 도달 or 오류 발생
		{
			if(feof(src)!=0) // 파일의 끝 도달일 경우
			{
				fwrite((void*)buf, 1, readCnt, des); // buf에 있는 데이터 des에 쓰기
				puts("파일복사완료");
				break;
			}
			else // 오류 발생일 겨우
				puts("파일복사실패");
			break;
		}

		fwrite((void*)buf, 1, sizeof(buf), des); // src로부터 읽은 데이터 des에 쓰기
	}
	
	fclose(src);
	fclose(des);
	
	return 0;
}

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

하나의 파일을 대상으로 입출력 할 데이터가 텍스트 데이터와 바이너리 데이터 둘로 이뤄져 있다면 어떠한 방법을 택해서 입출력을 해야 할까?

지금부터는 텍스트 데이터인 문자와 문자열, 그리고 바이너리 데이터인 int형 정수 하나를, 하나의 파일을 대상으로 동시에 입출력 해야 하는 상황에 대해서 이야기하겠다.

두 개의 텍스트 데이터와 하나의 바이너리 데이터를 입출력 해야 하는 상황에서 제일먼저 생각할 수 있는 방법은 fscanf 함수와 fprintf 함수의 호출이다.

이 두 함수는 scanf, printf 함수와 유사하다.

다만 입출력의 대상이 콘솔이 아닌 파일이라는 점에서 차이가 있다.

char name[20]="홍길동";
char sex='M';
int age=24;

fprintf(fp, "%s %c %d", name, sex, age);

위의 fprintf 함수의 호출문이 printf 함수의 호출문과 차이를 보이는 부분은 FILE 구조체의 포인터가 첫 번째 전달인자라는 점이다.

그래서 pritnf 함수와 달리 fprintf 함수는 첫 번째 인자로 전달된 FILE 구조체의 포인터가 지칭하는 파일로 출력이 이뤄진다.

#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();
		fprintf(fp, "%s %c %d\n", name, sex, age);
	}
	
	fclose(fp);

	return 0;
}

fprintf 함수의 호출을 통해서 저장된 데이터는, 동일하게 서식을 지정해서 fscanf 함수의 호출을 통해서 읽어들일 수 있다.

fscanf 함수의 호출방식은 다음과 같다.

#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;
}

데이터를 읽는 순서는 데이터의 저장순서와 일치해야 한다.

앞서 %s %c %d 순으로 데이터를 저장했기 때문에 이 순서대로 데이터를 읽고 있다.

fscanf 함수가 EOF를 반환하면 while문을 빠져 나오게 된다.


앞서 보인 코드에서는 변수들을 대상으로 파일 입출력을 진행하였다.

그런데 실제 프로그램에서는 변수들을 구조체로 묶어서 정의하는 것이 보통이다.

때문에 구조체 변수 단위로의 파일 입출력에 대해 고민할 필요가 있다.

구조체 변수를 어떻게 통째로 저장하고 읽어 들일 수 있을까?

구조체 변수를 하나의 바이너리 데이터로 인식하고 처리하면 가능하다.

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

#include <stdio.h>

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

int main(void)
{
	FILE * fp;
	Friend f1;
	Friend f2;
	
	fp=fopen("friend.bin", "wb");
	scanf("%s %c %d", f1.name, &(f1.sex), &(f1.age));
	fwrite((void*)&f1, sizeof(Friend), 1, fp);
	fclose(fp);
	
	fp=fopen("friend.bin", "rb");
	fread((void*)&f2, sizeof(Friend), 1, fp);
	printf("%s %c %d", f2.name, f2.sex, f2.age);
	fclose(fp);
	
	return 0;
}

5. 임의 접근을 위함 '파일 위치 지시자'의 이동

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

예를 들어서 fgets 함수호출을 통해서 파일에 저장된 문자열을 읽어 들이는 경우, 이 멤버가 가리키는 위치를 시작으로 문자열을 읽어 들이게 되며, 총 20바이트 크기의 문자열이 읽혀졌다고 가정하면, 이 멤버는 20바이트 뒤를 가리키게 된다.

이처럼 이 멤버에 저장된 위치 정보의 갱신을 통해서 데이터를 읽고 쓸 위치 정보가 유지되는 것이다.

따라서 우리는 이 멤버를 가리켜 '파일 위치 지시자'라 부르기로 약속하겠다.

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

따라서 파일의 중간 혹은 마지막 부분에서부터 데이터를 읽거나 쓰기를 원한다면 '파일 위치 지시자'를 이동시켜야 한다.


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

#include <stdio.h>

int fseek(FILE * stream, long offset, int wherefrom)

위 함수는 총 세 개의 인자를 요구하는데, 인자가 의미하는 바를 문장으로 구성해서 설명하면 다음과 같다.

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

그리고 매개변수 offset에는 양의 정수뿐만 아니라 음의 정수도 전달될 수 있다.

그리고 SEEK_END가 전달되면 파일의 끝에서부터 이동이 시작된다고 한다.

그런데 여기서 말하는 파일의 끝은 파일의 마지막 데이터가 아닌, 파일의 끝을 표시하기 위해서 삽입이 되는 EOF를 의미함에 특히 주의해야 한다.

#include <stdio.h>

int main(void)
{
	FILE* fp=fopen("text.txt", "wt");
	fputs("123456789", fp);
	fclose(fp);
	
	fp=fopen("text.txt", "rt");
	
	fseek(fp, -2, SEEK_END);
	putchar(fgetc(fp));
	
	fseek(fp, 2, SEEK_SET);
	putchar(fgetc(fp));
	
	fseek(fp, 2, SEEK_CUR);
	putchar(fgetc(fp));

	fclose(fp);
	return 0;
}

위 코드의 출력 결과로는 836 이 나온다.

835가 아니라 836인 이유는, 두번째 fseek 함수 뒤에 fgetc 함수로 한 번 읽었기 때문에 파일 위치 지시자가 한 칸 다음칸인 4부터 시작하기 때문이다.


현재의 파일 위치 지시자 정보를 확인하고 싶다면 다음 함수를 호출하면 된다.

#include <stdio.h>

long ftell(FILE * stream);

이 함수는 파일 위치 지시자의 위치 정보를 반환하는데, 파일 위치 지시자가 첫 번재 바이트를 가리킬 경우 0을 반환하고, 세 번째 바이트를 가리킬 경우 2를 반환한다.

이처럼 가장 앞 부분의 바이트 위치를 0으로 간주한다는 점에 주의해야 한다.

#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;
}

예제에서 보여주듯이 ftell 함수는 파일 위치 지시자의 정보를 임시로 저장할 때 유용하게 사용이 된다.

profile
Backend Engineer

0개의 댓글