파일 개방과 입출력
파일 개방과 폐쇄
파일을 입출력하려면 먼저 어떤 용도로 사용할지를 결정한 후 원하는 데이터 파일을 하드디스크에서 찾아야 합니다. 이렇게 데이터를 입출력하기 전에 준비하는 과정이 파일 개방입니다. 또한 사용이 끝난 파일은 닫는 과정인 파일 폐쇄도 필요합니다. 이 두 과정 모두 함수 호출로 수행합니다. 이때 사용하는 함수가 fopen과 fclose 함수로 각각 file open과 file close를 의미합니다.
#include <stdio.h>
int main(void)
{
FILE *fp;
fp = fopen("a.txt", "r");
if (fp == NULL)
{
printf("파일이 열리지 않습니다.\n");
return 1;
}
printf("파일이 열렸습니다.\n");
fclose(fp);
return 0;
}
파일을 개방할 때는 7행과 같이 fopen 함수를 사용합니다.
FILE *fopen(const char * __filename, const char * __mode);
fopen 함수의 원형에서 보이듯이 파일명과 개방 모드를 문자열로 주면 원하는 파일을 찾아 개방합니다.
fopen 함수가 개방할 파일을 찾는 기본 위치는 [현재 작업 디렉터리]입니다. 현재 작업 디렉터리는 실행 파일이 있는 곳으로 프로그램이 실행되는 위치입니다. 만약 다른 곳에 있는 파일을 개방하고 싶다면 파일명에 경로를 함께 적어주면 됩니다.
개방 모드는 개방할 파일의 용도를 표시하며 기본적인 개방 모드는 다음과 같습니다.

개방 모드의 r은 read, w는 write, a는 append를 의미합니다.
fopen 함수가 파일을 찾아 개방하면 파일 포인터를 반환합니다. fopen 함수는 실제 파일이 있는 장치와 연결되는 스트림 파일을 메모리에 만듭니다. 그리고 스트림 파일에 접근할 수 있도록 파일 포인터를 반환합니다. 이 포인터를 가지면 입출력 함수를 통해 원하는 작업을 수행할 수 있습니다.

따라서 7행과 같이 포인터에 fopen 함수가 반환하는 값을 저장해 둡니다. 만약 fopen 함수가 파일을 개방하지 못하면 0번지의 이름을 의미하는 NULL(널 포인터)를 반환합니다.
개방한 파일을 더 이상 사용하지 않으면 fclose 함수로 닫습니다.
int fclose(FILE *);
함수의 인자로 닫을 파일의 파일 포인터를 전달하며 해당 파일을 성공적으로 닫았을 때는 0을 반환하고, 오류가 발생하면 EOF(-1)를 반환합니다.
파일 개방을 통해 만들어진 스트림 파일은 메모리를 사용합니다. 따라서 파일 입출력이 끝나면 이들을 회수해 재활용하기 위해 파일을 닫아야 합니다. 또한 스트림 파일에 남아 있는 중요한 데이터가 장치에 기록되기 전에 시스템 사고로 지워질 수 있으므로 사용이 끝난 파일은 즉시 닫아 스트림 파일의 데이터를 장치에 기록하는 것이 좋습니다.
스트림 파일과 파일 포인터
스트림 파일은 프로그램과 입출력 장치 사이의 다리 역할을 하는 논리적인 파일입니다. 프로그램은 일단 메모리에 있는 스트림 파일로 입출력을 수행하고 그 파일이 다시 키보드, 모니터 하드디스크와 같은 물리적인 장치와 연결되어 실제적인 입출력을 수행합니다.

스트림 파일은 문자 배열 형태의 버퍼를 갖고 있습니다. 버퍼는 프로그램이 출력한 데이터를 모아서 한꺼번에 출력 장치로 보내거나 입력 장치에서 한 번에 많은 데이터를 읽어 저장해 놓고 프로그램이 필요한 데이터를 바로 꺼내 쓸 수 있도록 준비합니다.
그런데 버퍼만 가지고는 저장된 데이터를 관리할 수 없습니다. 버퍼에서 데이터를 읽거나 쓸 때 그 위치를 알아야 하고 버퍼의 메모리 위치와 크기도 필요합니다. 결국 입출력 함수들은 버퍼를 사용하기 전에 이런 정보를 통해 버퍼의 상태를 파악하고 데이터를 입출력합니다.
스트림 파일은 이들 정보를 구조체로 묶어 보관합니다. 이때 스트림 파일이 사용하는 구조체의 이름이 FILE 입니다. 따라서 fopen 함수는 메모리에 스트림 파일을 만들고 프로그램에서 사용할 수 있도록 FILE 구조체 변수의 주소를 반환합니다. 이 값을 사용하면 스트림 파일을 통해 쉽게 파일 입출력을 수행할 수 있습니다.

FILE 구조체 변수는 위에서 언급한 내용 외에도 구현에 따라 여러 가지 정보를 더 포함하며 컴파일러에 따라 차이가 있습니다.
👉 스트림 파일 사용의 장점
- 스트림 파일을 사용하면 입출력 효율을 높이고 장치로부터 독립된 프로그래밍이 가능합니다.
- 스트림 파일을 사용하면 프로그램과 장치의 입출력 속도 차이를 줄일 수 있습니다.
입출력 함수들은 표준화된 스트림 파일로 입출력하고 스트림 파일과 입출력 장치의 연결은 하드웨어 특성에 따라 운영체제가 담당하도록 합니다. 그리고 하드디스크의 동작 속도는 프로그램의 데이터 처리 속도보다 훨씬 느립니다. 따라서 스트림 파일의 버퍼에 출력 데이터를 모아 한꺼번에 장치로 보내면 장치가 기록하는 시간에 프로그램이 다시 버퍼를 채울 수 있습니다.
문자 입력 함수: fgetc
파일이 개방되면 데이터를 입출력할 준비가 끝난 겁니다. 실질적인 데이터 입출력은 함수를 통해 수행되며 이때 파일 포인터를 함수의 인수로 줍니다. fgetc 함수는 파일에서 하나의 문자를 입력해 반환합니다.
#include <stdio.h>
int main(void)
{
FILE *fp;
int ch;
fp = fopen("a.txt", "r");
if (fp == NULL)
{
printf("파일이 열리지 않았습니다.\n");
return 1;
}
while (1)
{
ch = fgetc(fp);
if (ch == EOF)
{
break;
}
putchar(ch);
}
fclose(fp);
return 0;
}
8행부터 13행까지가 파일을 개방하고 확인하는 부분입니다. 파일 개방에 실패하면 오류 메시지를 출력하고 1을 반환하여 프로그램을 종료합니다.
17행의 fgetc 함수는 파일 포인터와 연결된 파일에서 하나의 문자를 읽어 반환해 ch 변수에 저장한 후 화면에 출력합니다. 따라서 이 과정을 반복하면 파일의 데이터를 모두 화면에 출력할 수 있습니다. 단, 데이터를 모두 읽은 경우 반복을 끝내야 하므로 fgetc 함수의 반환값을 반복문의 종료 조건으로 사용합니다. fgetc 함수는 파일의 데이터를 모두 읽으면 EOF를 반환합니다.
스트림 파일의 데이터 입력 과정
fgetc 함수는 먼저 파일 포인터와 연결된 스트림 파일의 버퍼에서 데이터를 가져옵니다. 처음에는 버퍼가 비어 있으므로 하드디스크에서 데이터를 가져와 버퍼를 채우게 되는데 이때 한 번에 버퍼의 크기만큼 가져와 저장합니다. 물론 파일의 크기가 버퍼 크기보다 작으면 모든 데이터를 한 번에 버퍼에 저장합니다. 그 후 fgetc 함수는 버퍼에서 첫 번째 문자를 가져와 반환합니다.

그리고 fgetc 함수가 두 번째 호출될 때는 이미 버퍼에 저장된 데이터가 있으므로 버퍼로부터 바로 문자를 읽어들입니다. 이런 입력 방식이 가능한 이유는 스트림 파일에는 문자를 입력할 버퍼의 위치를 알려주는 지시자가 있습니다. 위치 지시자는 파일이 개방되면 0으로 초기화되며 입력 함수가 데이터를 읽을 때 그 크기만큼 증가합니다. fgetc 함수는 한 문자씩 읽으므로 데이터를 읽을 때마다 위치 지시자의 값은 1씩 증가합니다.

결국 fgetc 함수가 EOF를 반환하면 파일의 데이터를 모두 읽었음을 뜻합니다. EOF는 컴파일 과정에서 stdio.h 에 정의된 상수 -1로 바뀌므로 -1을 직접 사용해도 되나, 시스템에 따라 EOF의 정의가 다를 수 있으므로 호환성을 위해 EOF를 쓰는 것이 좋습니다. fgetc 함수가 파일의 입력이 끝났음을 확인하는 방법은 파일의 크기와 현재까지 읽어 들인 데이터의 크기를 비교해 판단하는 것입니다. 파일에는 파일의 끝을 표시하는 어떤 정보도 포함되지 않습니다.
이러한 입출력 과정은 fgetc 함수뿐 아니라 스트림 파일을 사용하는 모든 입력 함수에 똑같이 적용됩니다.
문자 출력 함수: fputc
한 문자를 파일로 출력할 때는 fputc 함수를 사용합니다. fputc 함수에 출력할 문자와 파일 포인터를 인수로 주면 파일로 문자를 출력합니다. 반환값은 출력한 문자를 다시 반환하며 에러가 발생하면 EOF를 반환합니다.
#include <stdio.h>
int main(void)
{
FILE *fp;
char str[] = "banana";
int i;
fp = fopen("b.txt", "w");
if (fp == NULL)
{
printf("파일을 만들지 못했습니다.\n");
return 1;
}
i = 0;
while (str[i] != '\0')
{
fputc(str[i], fp);
i++;
}
fputc('\n', fp);
fclose(fp);
return 0;
}
fputc 함수도 출력 과정에서 스트림 파일의 버퍼를 사용합니다. 즉, 문자가 하나씩 파일에 직접 저장되는 것이 아니고 버퍼에 데이터를 모은 후에 한 번에 출력합니다.

버퍼가 모두 채워지면 파일에 출력하며, 버퍼가 모두 채워지지 않더라도 개행 문자(\n)를 출력하거나 새로운 입력을 수행하는 경우 버퍼의 데이터를 장치로 출력합니다. 이런 규칙은 시스템에 따라 차이가 있을 수 있습니다. 만약 버퍼의 데이터를 즉시 장치로 출력해야 한다면 fflush 함수를 사용합니다.
표준 입출력 스트림 파일
운영체제는 프로그램을 실행할 때 기본적으로 3개의 스트림 파일을 만듭니다. 그리고 이들을 키보드와 모니터 등에 연결해서 입출력 함수들이 파일 포인터 없이 사용할 수 있도록 제공합니다.
#include <stdio.h>
int main(void)
{
int ch;
while (1)
{
ch = getchar();
if (ch == EOF)
{
break;
}
putchar(ch);
}
return 0;
}
운영체제에 따라 기본적으로 개방하는 스트림 파일 수는 다를 수 있지만, 다음과 같은 3개의 스트림 파일은 공통적으로 개방합니다.
- stdin : 표준 입력 스트림
- stdout : 표준 출력 스트림
- stderr : 표준 에러 스트림
스트림 파일의 이름은 stdin, stdout, stderr 은 운영체제가 개방한 파일의 주소를 의미합니다.

9행의 getchar 함수는 내부적으로 stdin을 사용하므로 표준 입력 스트림 파일의 버퍼를 통해 입력합니다. 따라서 getchar 함수가 처음 호출되면 키보드에서 입력하는 데이터는 개행 문자와 함께 stdin 스트림 파일의 버퍼에 한꺼번에 저장됩니다. 그리고 버퍼에서 첫 번째 문자를 가져다 반환하고, 그 이후에 호출되는 getchar 함수는 버퍼로부터 차례로 다음 문자를 반환합니다.
Ctrl + Z 를 눌러 나오는 ^Z는 EOF 문자이며 End Of File을 의미합니다. 텍스트 파일의 끝을 나타내며 이 문자가 입력되면 키보드 입력이 끝났음을 의미합니다. 리눅스 시스템에서는 Ctrl + D를 사용합니다.
14행의 putchar 함수는 모니터와 연결된 표준 출력 스트림을 사용하며, 표준 에러 스트림은 풀력 과정에서 발행하는 오류 메시지 등을 화면으로 확인할 수 있도록 또 다른 출력 경로를 제공합니다.
운영체제가 기본적으로 개방하는 스트림 파일을 scanf, printf, getchar, putchar, gets, puts 등 표준 입출력 함수들이 사용하지만, 파일 포인터를 인수로 받는 함수도 사용할 수 있습니다. 바로 stdin, stdout, stderr을 직접 fgetc 함수나 fputc 함수 등의 인수로 사용하면 됩니다.
#include <stdio.h>
int main(void)
{
int ch;
while (1)
{
ch = fgetc(stdin);
if (ch == EOF)
{
break;
}
fputc(ch, stdout);
}
return 0;
}
이 예제는 프로그램 내에서 별도의 파일을 개방하지 않고 운영체제가 개방한 스트림 파일을 사용합니다. 물론 이 경우 운영체제가 연결해 놓은 장치로 입출력을 수행합니다.
텍스트 파일과 바이너리 파일
파일은 데이터의 기록 방식에 따라 텍스트(text) 파일과 바이너리(binary) 파일로 나눕니다. 바이너리 파일은 데이터를 그 자체의 이진 값(비트열)로 저장한 것이고, 텍스트 파일은 데이터에 대응하는 아스키 코드값으로 저장한 것입니다. 텍스트 파일은 아스키 코드 값에 따라 데이터를 읽고 저장하는 메모장 같은 프로그램에서 확인할 수 있으며 바이너리 파일은 해당 기록 방식을 적용한 별도의 프로그램을 사용해야 합니다.
예를 들어 텍스트 파일은 메모장 프로그램에서 그 내용을 확인할 수 있으나, 그림 파일을 보기 위해서는 그림판 프로그램을 사용해야 합니다. 파일 입출력 함수들도 파일의 형태에 따라 데이터를 읽고 쓰는 방식이 다르기 때문에 파일을 개방할 때 모드에 파일의 형태도 함께 표시해야 합니다.
개방 모드에 텍스트 파일은 t, 바이너리 파일은 b를 추가해 개방하며, 파일의 형태를 별도로 표시하지 않으면 자동으로 텍스트 파일로 개방합니다.
- rt, rb
- wt, wb
- at, ab
+ 개방 모드
파일 개방 모드는 기본적으로는 r, w, a 3가지 모드가 있으나 + 를 사용하면 읽고 쓰는 작업을 함께 할 수 있습니다.
- r+ : 텍스트 파일에 읽고 쓰기 위해 개방
- w+ : 텍스트 파일의 내용을 지우고 읽거나 쓰기 위해 개방
- a+ : 텍스트 파일을 읽거나 파일의 끝에 추가하기 위해 개방
- rb+ : 바이너리 파일에 읽고 쓰기 위해 개방
- wb+ : 바이너리 파일의 내용을 지우고 읽거나 쓰기 위해 개방
- ab+ : 바이너리 파일을 읽거나 파일의 끝에 추가하기 위해 개방
예제를 통해 + 모드의 사용법을 살펴보겠습니다.
#include <stdio.h>
#include <string.h>
int main(void)
{
FILE *fp;
char str[20];
fp = fopen("a.txt", "a+");
if (fp == NULL)
{
printf("파일을 만들지 못했습니다.\n");
return 1;
}
while (1)
{
printf("과일 : ");
scanf("%s", str);
if (strcmp(str, "end") == 0)
{
break;
}
else if (strcmp(str, "list") == 0)
{
fseek(fp, 0, SEEK_SET);
while (1)
{
fgets(str, sizeof(str), fp);
if (feof(fp))
{
break;
}
printf("%s", str);
}
}
else
{
fprintf(fp, "%s\n", str);
}
}
fclose(fp);
return 0;
}
이 프로그램은 키보드로 과일 이름을 입력해 파일에 출력합니다. 최초 실행할 때 파일이 없으면 빈 파일을 만들어서 출력하고 파일이 있으면 데이터를 추가하기 위해 a 모드를 사용합니다. 또한 데이터 기록 중 언제든지 파일의 내용을 다시 읽어서 확인할 수 있도록 + 모드를 함께 사용합니다. 우선 입력한 과일 이름이 20행의 end와 24행의 list와 같지 않으면 파일에 과일 이름일 계속 출력합니다. 이 과정에서 'list'를 입력하면 그동안 출력한 과일 이름을 다시 읽어 화면에 보여 줍니다.
fseek 함수
이때 파일의 입력과 출력을 서로 전환할 때마다 fseek 함수를 꼭 호출해야 합니다. 39행의 fprintf 함수는 스트림 파일의 버퍼에 데이터를 출력해 놓는데, 이때 버퍼에 데이터가 있는 상태에서 파일로부터 데이터를 입력하게 되면 입출력 순서사 꼬입니다. 따라서 버퍼의 데이터를 파일로 옮기고 버퍼를 읽기 위한 공간으로 설정한 후에 파일의 데이터를 처음부터 다시 읽도록 해야 합니다.

이 과정에서 fseek 함수가 쓰이며 그 원형은 다음과 같습니다.
int fseek(FILE * stream, long offset, int whence);
각 인수를 보면 첫 번째 인수인 stream 파일의 버퍼에서 whence를 기준으로 offset만큼 위치 지시자를 옮깁니다. 위치 이동에 실패하면 0, 성공하면 0이 아닌 값을 반환합니다. whence에 사용할 수 있는 값과 의미는 다음과 같습니다.

예를 들어 fseek(fp, -5, SEEK_END); 는 파일의 끝에서 다섯 문자 앞쪽으로 위치 지시자를 옮깁니다. 매크로명은 값 대신에 사용할 수 있는 이름인데 전처리 과정에서 약속된 정수로 바뀝니다.
rewind 함수
26행의 fseek 함수는 스트림 파일의 위치 지시자를 시작 위치로 옮기며 그 전에 버퍼의 내용을 파일에 출력합니다. 이때 위치 지시자를 맨 처음으로 설정하는 rewind 함수를 사용해도 됩니다.
rewind(fp); // 26행
이후에 29행에서 fgets 함수는 비워진 버퍼를 사용해 파일로부터 데이터를 처음부터 입력합니다.
a+ 모드에서 출력하는 데이터는 항상 파일의 맨 뒤에 붙여 넣기가 되지만, w+ 모드는 데이터를 읽다가 중간에 다시 쓰는 경우 fseek 함수로 설정한 위치부터 내용을 덮어 씁니다. r+ 모드는 읽기를 먼저 하든 쓰기를 먼저 하든 상관없지만, 읽기와 쓰기를 서로 바꿀 때는 fseek 함수로 파일에서 읽고 쓸 위치를 알려줘야 합니다.
feof 함수
30행의 feof 함수는 스트림 파일의 데이터를 모두 읽었는지 확인할 때 유용합니다. 파일의 끝이면 0이 아닌 값을 반환하고 끝이 아니면 0을 반환합니다. feof 함수는 입력 함수가 데이터 입력에 실패한 이후에 그 결과를 알 수 있으므로 입력 함수 다음에 사용합니다.
다양한 파일 입출력 함수
파일 데이터는 fgetc 함수 하나로 모두 읽을 수 있습니다. 한 바이트씩 읽더라도 반복해서 읽으면 파일의 모든 데이터를 읽을 수 있기 때문입니다. 출력할 때도 fputc 함수 하나로 충분합니다. 그러나 한 번의 함수 호출로 한 줄 씩 읽거나 쓰면 더 편하고, 파일의 데이터는 기본적으로 모두 문자인데 정수, 실수 등으로 자동 변환해 준다면 훨씬 편하게 입출력을 수행할 수 있을 겁니다.
fgets, fputs
파일에서 데이터를 한 줄씩 입력할 때는 fgets 함수를 사용합니다. 반면에 문자열을 파일에 출력할 때는 fputs 함수를 사용합니다. fgets 함수는 읽을 데이터의 크기가 큰 경우 저장 공간의 크기까지만 입력할 수 있으므로 할당하지 않은 메모리를 침범할 가능성을 차단합니다. 예제를 통해 살펴보겠습니다.
#include <stdio.h>
#include <string.h>
int main(void)
{
FILE *ifp, *ofp;
char str[80];
char *res;
ifp = fopen("a.txt", "r");
if (ifp == NULL)
{
printf("입력 파일을 열지 못했습니다.\n");
return 1;
}
ofp = fopen("b.txt", "w");
if (ofp == NULL)
{
printf("출력 파일을 열지 못했습니다.\n");
return 1;
}
while (1)
{
res = fgets(str, sizeof(str), ifp);
if (res == NULL)
{
break;
}
str[strlen(str) - 1] = '\0';
fputs(str, ofp);
fputs(" ", ofp);
}
fclose(ifp);
fclose(ofp);
return 0;
}
fgets 함수는 인수가 3개로, 문자열을 저장할 배열과 그 크기를 주고 마지막으로 입력할 파일의 포인터를 줍니다.
기본 기능은 개방한 파일에서 공백을 포함해 한 줄씩 읽어 배열에 저장합니다. 만약 한 줄의 크기가 배열의 크기보다 크면 할당하지 않은 메모리를 쓰지 못하도록 배열의 크기까지만 입력합니다. 이 경우 '배열의 크기 -1'개의 문자만 입력하고 마지막에 널 문자를 붙여 문자열을 완성합니다.
만약 입력된 개행 문자가 필요 없다면 31행처럼 문자열의 길이를 계산하는 방법으로 제거합니다. 입력된 문자열의 길이에서 1을 뺀 값은 개행 문자가 저장된 배열의 위치며 그곳에 널 문자를 저장해 개행 문자를 제거합니다.
fgets 함수는 버퍼의 주소를 반환하기 때문에 파일의 끝을 검사하기 위해서는 27행과 같이 반환값을 NULL과 비교해야 합니다.
fgetc 함수는 읽은 문자를 int로 반환하기 때문에 파일의 끝을 검사하기 위해서는 EOF(-1)와 비교했습니다.
32행의 fputs 함수는 문자열을 파일에 출력하며, 성공하면 시스템에 따라 0 또는 출력한 문자의 수를 반환하고 실패하면 EOF를 반환합니다. fputs 함수는 파일 포인터와 연결된 파일로 문자열을 출력하며, 개행 문자를 출력하지 않으면 자동으로 줄을 바꾸지 않습니다.
fgets, fputs vs gets, puts
gets와 puts 대신 fgets, fputs 함수를 사용하는게 좋습니다. gets 함수는 문자열을 입력할 때 개행 문자를 제거하지 않아도 되며, puts 함수는 출력할 때 자동으로 줄을 바꿔주므로 나름대로 편리합니다. 그러나 이들 함수는 편리함 뒤에 문제점이 있습니다.
gets 함수는 입력할 저장 공간의 크기를 인수로 줄 수 없으므로 문자열을 입력할 때 할당하지 않은 메모리 공간을 침범할 가능성이 있습니다. 이것은 프로그램을 실행할 때 다른 용도로 사용되는 데이터를 임의로 바꾸거나 허용되지 않은 메모리에 접근해서 런타임 에러를 일으킬 수 있습니다.
puts 함수는 항상 줄을 바꾸므로 문자열을 이어서 출력할 수 없습니다.
따라서 문자열의 입출력은 안전하고 정확하게 수행되는 fgets, fputs 함수를 사용하는 것이 좋습니다.
fgets(str, sizeof(str), stdin);
fputs(str, stdout);
이들 함수에 stdin과 stdout을 파일 포인터로 주면 키보드와 모니터로 데이터의 입출력이 가능합니다.
fscanf, fprintf
파일에 저장된 문자열을 숫자로 변환해서 입력할 때는 fscanf 함수를 사용합니다. 반대로 정수나 실수를 쉽게 파일에 출력할 때는 fprintf 함수를 사용합니다. 이들은 scanf, printf 함수와 같은 기능을 수행하지만, 파일을 지정할 수 있습니다.
#include <stdio.h>
int main(void)
{
FILE *ifp, *ofp;
char name[20];
int kor, eng, math;
int total;
double avg;
int res;
ifp = fopen("a.txt", "r");
if (ifp == NULL)
{
printf("입력 파일을 열지 못했습니다.\n");
return 1;
}
ofp = fopen("b.txt", "w");
if (ofp == NULL)
{
printf("출력 파일을 열지 못했습니다.\n");
return 1;
}
while (1)
{
res = fscanf(ifp, "%s%d%d%d", name, &kor, &eng, &math);
if (res == EOF)
{
break;
}
total = kor + eng + math;
avg = total /3.0;
fprintf(ofp, "%s%5d%7.1lf\n", name, total, avg);
}
fclose(ifp);
fclose(ofp);
return 0;
}

입력 파일 a.txt의 데이터를 char 배열과 int 형 변수 3개에 각각 저장한 후에 총점과 평균을 구해 b.txt에 출력합니다.
a.txt 파일은 텍스트 파일이므로 세 과목의 점수는 모두 숫자 형태의 문자열입니다. 따라서 파일에서 데이터를 읽어 int형 변수에 저장할 때는 문자열을 정수로 바꾸는 과정이 필요합니다. 만약 한 줄을 모두 문자열로 입력하면 전체 문자열을 데이터 수로 쪼개고 각 데이터 형태에 따라 변환하는 작업 코드를 직접 작성해야 합니다. 그런데 fscanf 함수를 사용하면 쉽게 처리할 수 있습니다.
fscanf 함수는 파일의 데이터를 모두 읽으면 EOF를 반환하므로 이 값을 반복문의 종료 조건으로 사용합니다.
출력할 때는 fscanf 함수와 반대의 변환 과정을 수행하는 fprinf 함수를 사용합니다. fprintf 함수는 각 변수의 데이터를 모아 문자열로 변환해 파일에 출력합니다. 쉽게 정리하면 printf 함수가 화면에 출력하는 형식을 그대로 파일로 출력하는 것과 같습니다. fprintf 함수는 출력한 문자의 바이트 수를 반환하며, 출력 과정에서 오류가 발생하면 음수를 반환합니다.
스트림 파일의 버퍼 공유 문제와 fflush
스트림 파일을 사용하는 입출력 함수들이 버퍼를 공유하면 예상과 다른 결과가 나올 수 있습니다. 다음 예제를 통해 문제점과 해결책을 살펴보겠습니다.
#include <stdio.h>
int main(void)
{
FILE *fp;
int age;
char name[20];
fp = fopen("a.txt", "r");
fscanf(fp, "%d", &age);
fgets(name, sizeof(name), fp);
printf("나이 : %d, 이름 : %s", age, name);
fclose(fp);
return 0;
}
위 코드를 실행해보면 결과로 나이만 출력됩니다.
그 이유는 fscanf 함수와 fgets 함수가 개행 문자를 처리하는 방식이 다르기 때문입니다. 즉, 11행의 fscanf 함수가 나이를 입력한 후에 버퍼에 남겨 놓은 개행 문자를 다음에 호출되는 fgets 함수가 이어서 입력하기 때문입니다. fgets 함수는 개행 문자가 나올 때까지 문자열을 입력하는데 처음부터 개행 문자가 있으므로 개행 문자만 입력합니다.

이 문제는 이름을 입력할 때 화이트 스페이스를 데이터로 입력하지 않고 건너뛰는 fscanf 함수를 쓰면 해결됩니다. 그러나 이 경우 fscanf 함수가 공백을 입력 데이터로 구분하는 용도로 사용되므로 이름으로 'gil' 까지만 입력하는 문제가 생깁니다. 따라서 fgetc 함수를 11행과 12행 사이에 끼워 넣어 스트림 버퍼에서 개행 문자를 읽어 내는 방법으로 해결합니다.

그러나 상황에 따라 버퍼에서 개행 문자 하나만 제거하는 것으로 끝나지 않는 경우가 있습니다. 예를 들어 나이를 입력할 때 '30 years old' 라고 입력한다면 정수 부분을 제외한 나머지 부분은 이름을 입력하기 전에 버퍼에서 제거해야 합니다.

이 경우 반복문을 사용하여 제거해도 되지만, 스트림 파일의 버퍼를 비우는 fflush 함수를 사용할 수 있습니다.
int fflush(FILE *);
fflush 함수는 파일 포인터를 인수로 주면 파일 포인터와 연결된 스트림 파일의 버퍼를 비웁니다. 반환값은 0이며 버퍼를 비우지 못했을 때는 EOF를 반환합니다. fflush 함수는 입력 파일에 대해서는 표준이 정의되어 있지 않고, 출력 파일에 사용하면 버퍼를 비우면서 남은 데이터를 연결된 장치로 바로 출력합니다.
fread, fwrite
fread와 fwrite 함수는 입출력할 데이터의 크기와 개수를 인수로 줄 수 있으므로 구조체나 배열과 같이 데이터양이 많은 경우도 파일에 쉽게 입출력할 수 있습니다. 또한 숫자와 문자 사이의 변환 과정을 수행하지 않으므로 입출력 효율을 높일 수 있습니다. 그러나 파일의 내용을 메모장 같은 편집기로 직접 확인할 수는 없습니다.
#include <stdio.h>
int main(void)
{
FILE *afp, *bfp;
int num = 10;
int res;
afp = fopen("a.txt", "wt");
fprintf(afp, "%d", num);
bfp = fopen("b.txt", "wb");
fwrite(&num, sizeof(num), 1, bfp);
fclose(afp);
fclose(bfp);
bfp = fopen("b.txt", "rb");
fread(&res, sizeof(res), 1, bfp);
printf("%d", res);
fclose(bfp);
return 0;
}
13행의 fwrite 함수는 6행의 변수 num에 저장된 값을 b.txt 파일로 출력합니다. 출력할 데이터의 주소, 크기, 개수, 파일 포인터를 차례로 주어 fwrite 함수를 사용할 수 있습니다. fwrite 함수의 반환값은 출력한 데이터 수가 됩니다.
fprintf vs fwrite
이 예제는 int 형 변수에 저장된 값 10을 fprintf 함수와 fwrite 함수로 각각 다른 파일에 출력했을 때의 차이점을 보여줍니다. 먼저 fprintf 함수는 변수 num에 저장된 값을 파일에 출력할 때 '1'과 '0', 2개의 아스키 문자로 변환해 저장합니다. 즉, 데이터의 크기와 비트열이 모두 바뀌어 출력됩니다.

반면 fwrite 함수는 변환 과정 없이 메모리에 있는 데이터를 그대로 파일에 저장합니다.

따라서 출력 후의 b.txt 파일은 아스키 코드 값으로 저장되지 않은 바이너리 파일이고 메모장과 같은 텍스트 파일 편집기로 보거나 편집할 수 없습니다. 데이터를 읽을 때는 fread 함수로 b.txt 파일의 데이터를 그대로 읽어 다시 변수 num에 쉽게 저장할 수 있습니다.
fread와 fwrite 함수가 데이터를 있는 그대로 입출력할 수 있도록 파일은 항상 바이너리 모드로 개방해야 합니다.
'Lang > C' 카테고리의 다른 글
| C (12) - 전처리와 분할 컴파일 (0) | 2025.09.08 |
|---|---|
| C (10) - 사용자 정의 자료형 (1) | 2025.09.01 |
| C (9) - 메모리 동적 할당 (2) | 2025.08.28 |
| C (8) - 다차원 배열, 포인터 배열, 응용 포인터 (2) | 2025.08.25 |
| C (7) - 변수 영역과 데이터 공유 (0) | 2025.08.21 |