[C언어 강좌] #17-2 콘솔 입출력과 파일 입출력


저번 편에선 파일 출력을 하는 소스를 간단하게 실습해보았고 스트림, 파일스트림에 대한 개념

fopen() 을 통해 파일 스트림을 생성 & 파일을 여는것 등을 알아보았습니다.

오늘은 파일 입출력 과정에서 사용되는 함수에 대해 다뤄보겠습니다.

 

fopen() 함수와 fclose() 함수

먼저 파일을 열고 닫는 fopen() 함수와 fclose() 함수에 대해서 공부해보겠습니다. 아래 표는 fopen 함수의 원형입니다.

함수 원형 설명
#include <stdio.h>
FILE* fopen(const char * filename, const char * mode)
파일 스트림을 생성하고 파일을 연다
실패 : NULL 반환

 

fopen() 함수는 함수의 인자로 filename(파일 경로)과 mode(파일 열기 모드)를 전달 받아 파일 스트림(FILE 구조체 포인터)을 생성하는 동시에 파일을 열게 됩니다. 만약 파일을 열 수 없다면 NULL 포인터를 반환합니다. 다음은 fopen() 함수의 호출 예입니다.

 

#include <stdio.h>

int main(){

	FILE *stream = fopen("newfile.txt", "rt"); //파일 스트림 생성 & 파일 열기
	
	if(stream == NULL)
		puts("파일 열기 오류"); 
    
    	
	return 0;
}

 

다음은 newfile.txt 를 여는 예제입니다. 파일을 쓰는게 아니라 이미 존재하는 newfile.txt를 읽는 것 입니다. 앞에 경로를 따로 적어주지 않으면 newfile.txt는 지금 실행되고 있는 c언어 파일과 동일한 위치에 있는 파일을 찾아서 엽니다.

 

만약에 D에 있는 newfile.txt 일경우 D:\\newfile.txt 로 지정해 주셔야 합니다. \\라고 쓰는 이유는 C언어에서 \를 이스케이프 시퀀스 조합에 사용하기 때문입니다. (\를 그냥 출력하려면 \\로 출력해야한다. printf() 편에서 다루던 아주 옛날 내용입니다.)

 

newfile.txt를 여는건 알겠는데 fopen() 의 2번째 인자인 "rt"는 무슨뜻일까요?

fopen()의 2번째 인자이름은 mode로 파일을 열때 어떤 모드(방식)로 열것인지 나타내기 위해

파일 접근 모드 X 파일 입출력 모드로 표현합니다.

 

파일 접근 모드에는 read(읽기), write(쓰기), append(추가) 의 맨 앞글자만 따서 r,w,a 세가지의 모드가 존재하고, 여기에 + 기호가 붙으면 읽기와 쓰기 모두 할 수 있도록 r+, w+ a+ 같이 세 가지 모드가 더 존재합니다.

(파일 접근 모드 = 총 6개)

 

모드 설명
r 읽기 전용으로 파일을 연다. 파일이 없거나 찾을 수 없으면 호출이 실패함
w 쓰기 전용으로 파일을 연다, 지정한 파일이 있으면 파일의 내용을 모두 지우고 새 파일을 쓰고,
지정한 파일이 없으면 새로운 파일을 생성해서 데이터를 쓴다.
a 추가 쓰기 전용으로 파일을 열고 지정한 파일이 있으면 끝에서부터 내용을 추가한다.
r+ 파일을 읽고 쓰기 위해 연다. 지정한 파일이 있으면 기존의 내용을 덮어쓰게 되고,
지정한 파일이 없으면 새로운 파일을 생성해서 데이터를 쓴다.
w+ 파일을 읽고 쓰기 위해 연다. 나머지는 w와 동일
a+ 파일을 읽고 추가 쓰기 위해 연다. 지정한 파일이 있으면 파일의 끝에서부터 내용을 추가한다. 

 

뒤에 +가 붙으면 읽기, 쓰기가 모두 가능한 스트림을 형성하게 됩니다.

그런데 강의를 제대로 보셨다면 여기서 한가지 의문을 가지실겁니다.

 

스트림의 방향은 양방향이 아니라 단방향이라서 입출력이 동시에 가능한 스트림은 없다고 했는데 여기선 어떻게 읽기/ 쓰기를 동시에 할 수 있는걸까요?

실제로 인풋과 아웃풋이 동시에 이루어지진 않습니다. a+를 예로 들어보면 처음엔 파일의 시작위치를 가리켰다가 쓰기 행위가 감지되면 파일의 맨끝으로 이동해서 추가하고 이런 방법을 사용합니다.

 

그래서 다시 원하는 부분부터 읽으려면 설명은 드리지 않았지만 fseek() 이나 rewind() 함수를 이용해서 위치를 다시 지정해줘야 합니다. 

 

즉 양방향 작업을 위해서는 메모리의 버퍼를 지워줘야 하거나 위와 같은 함수를 이용해야 하는 불편함이 있습니다. 

작업이 이루어지면서 실수를 할 수도 있고 불편함도 있기 때문에 웬만하면 r+, w+, a+는 이용하지 않고 r, w, a 중에서 하나를 선택해서 스트림을 형성하는게 좋습니다.

 

일반적으로 r, w, a 모드를 더 많이 사용합니다.

 

*파일 입출력 모드

모드 설명
t 텍스트 파일 모드입니다.
b 바이너리 파일 모드입니다.

 

파일 입출력 모드의 경우엔 간단합니다. 2가지밖에 없어요.

#17-1편에서 파일이 텍스트 파일, 바이너리 파일로 나누어진다는것을 배웠었습니다.

(뭔지 기억 안나시면 복습하시고 오세요)

 

어떤 파일이라고 생각하고 읽을지 컴퓨터에게 지정해주는 것입니다.

 

text, binary의 앞글자 약자로 따서 t와 b를 붙입니다.

또한 t는 생략이 가능합니다. 꼭 붙여줄 필요가 없어요

 

예를 들어서 wt와 w는 같은 표현입니다. w라고 써주면 기본적으로 텍스트 모드로 읽습니다.

대신 바이너리 모드로 읽을땐 wb라고 꼭 써줘야 겠죠.

 

fopen() 함수의 두번째 인자 mode는 파일 접근 모드 x 파일 입출력 모드의 조합이라고 했습니다. 그림으로 나타내면 아래와 같습니다.

 

2개를 조합해서 적절히 사용해주시면 됩니다.

그런데 아까도 말했듯이 r, w, a 모드를 쓰는게 더 좋다고 했으므로 파일 오픈 모드는 + 조합을 제외한 6개 정도만 참고해주면 되겠네요.

 

FILE *stream = fopen("newfile.txt", "r");  //읽기 모드(텍스트 모드)

FILE *stream = fopen("newfile.txt", "w");  //쓰기 모드(텍스트 모드)

FILE *stream = fopen("newfile.txt", "ab");  //추가 모드(바이너리 모드)

 

함수 원형 설명
#include <stdio.h>
int fclose(FILE* stream)
파일을 닫음
실패 : EOF(-1) 반환

위 표는 fclose()의 원형입니다.

fclose() 함수는 인자로 파일 스트림을 지정해서 파일을 닫고, 만약 파일을 닫을 수 없으면 EOF를 반환합니다. 다음은 fclose() 함수의 호출 예시 입니다.

 

#include <stdio.h>

int main(){

	FILE *stream = fopen("newfile.txt", "rt"); //파일 스트림 생성 & 파일 열기
	if(stream == NULL)
    	puts("파일을 여는중 오류가 발생했습니다.");
    
	int file_state = fclose(stream); //파일 스트림 닫기
	if(file_state == EOF)
		puts("파일을 닫는중 오류가 발생했습니다."); 
    
    	
	return 0;
}

fopen() 함수에선 오류가 발생하면 NULL을, fclose() 함수에선 문제가 발생하면 EOF(-1)을 발생시키므로

이렇게 오류를 검사하면 어디서 문제가 발생한지 알 수 있습니다.

 

이렇게 예외처리를 하는게 필수는 아닙니다만 나쁜 습관은 아닙니다.

 

C언어에서는 C++나 Java에서 지원하는 try-catch 같은 예외처리(오류처리) 문법이 없어서 이렇게 if문으로 일일히 검사해줘야 합니다.

 

디버깅할때 오류가 검출되지 않으면 위처럼 예외처리를 해서 문제가 어디서 발생할 수 있는지 집어낼 수 있습니다.

#include <stdio.h>

int main(){

	FILE *stream = fopen("newfile.txt", "rt"); //파일 스트림 생성 & 파일 열기
	fclose(stream); //파일 스트림 닫기
    	
	return 0;
}

다음은 예외처리 없이 구성한 코드입니다. 

if문 부분이 빠져서 코드가 매우 간결해졌습니다.

방금도 말씀드렸듯이 오류검사가 꼭 필수는 아닙니다.

 

fopen() 함수로 생성된 파일스트림을 fclose() 함수로 닫아줘야 된다고 말씀 드렸습니다. 

파일을 열었으면 당연히 닫아야 하는 것과 같습니다. 파일을 열고 닫는 법을 배웠으므로 이제 파일로부터 데이터를 입력받고, 파일에 데이터를 출력하는 코드만 추가하면 끝입니다.

 

표준 파일 입출력 함수

우리는 이미 앞에서 키보드와 모니터를 이용하는 표준 입출력 함수를 배웠습니다. 우리가 흔히 쓰던 printf(), scanf() 함수들이였죠. 표준 입출력 함수 뿐만 아니라 C언어에서는 파일에 해당하는 표준 파일 입출력 함수를 제공합니다.

 

표준 파일 입출력 함수는 파일로부터 데이터를 입력받는 기능과 파일에 데이터를 출력하는 기능을 제공합니다.

 

지금까지 입력/출력을 수행하기 위해 putchar(), gets(), puts(), scanf(), printf() 와 같은 표준 입출력 함수를 사용했습니다.

이 함수들은 표준 입출력은 가능한데 파일 입출력 기능은 없었죠.

 

그런데 표준 파일 입출력 함수는 파일 입출력과 표준 입출력이 모두 가능합니다.

아래는 대표적으로 많이 사용되는 표준 파일 입출력 함수 입니다.

표준 입출력 함수 표준 파일 입출력 함수 설명
int getchar(void); int fgec(FILE * stream); 문자 단위 입력
int putchar(int c); int fputc(int c, FILE * stream); 문자 단위 출력
char * gets(char * s); char * fgets(char * s, int n, FILE * stream); 문자열 단위 입력
int puts(char * str); int fputs(const char * s, FILE * stream); 문자열 단위 출력
int scanf(const char * format, ...); int fscanf(FILE * stream, const char * format, ...); 자료형에 맞춘 입력
int printf(const char * format, ...); int fprintf(FILE * stream, const char * format, ...); 자료형에 맞춘 출력

 

위 표에서 예를 들어보자면, fgetc() 함수는 문자 단위 입력 함수인데, 함수에 인자에 파일 스트림을 입력하는 경우에 파일로부터 데이터를 입력받을 수 있고(읽어올 수 있고), 함수에 인자에 stdin 을 입력하는 경우 표준 입력스트림을 통해 키보드로부터 데이터를 입력받을 수 있습니다. 신기하죠?

 

더 확장해서 생각해서

표준 입력 스트림(다리)의 이름이 stdin, 표준 출력 스트림(다리)의 이름이 stdout 이라고 했으니깐

파일에 데이터를 출력하는 함수에 stdout을 하면 모니터에 출력도 가능해지겠네요!

 

fgect() 와 같은 표준 파일 입출력 함수들은 다음 그림과 같이 입력 버퍼와 출력 버퍼를 이용해서 다음과 같은 네 가지 경우를 선택적으로 적용할 수 있습니다.

 

Case1. 파일(stream)로부터 데이터를 입력받아 파일(stream)에 데이터를 출력

Case2. 파일(stream)로부터 데이터를 입력받아 모니터(stdout)에 데이터를 출력

Case3. 키보드(stdin)로부터 데이터를 입력받아 파일(stream)에 데이터를 출력

Case4. 키보드(stdin)로부터 데이터를 입력받아 모니터(stdout)에 데이터를 출력

 

이제부터 본격적으로 파일 입출력 함수에 대해 살펴보겠습니다.

 

fgetc() 함수와 fputc() 함수

fgetc() 와 fputc()는 문자 단위 표준 파일 입출력 함수 입니다.

지금까지 파일을 다루면서 항상 인클루드하던 stdio.h 이외에 따로 무언가를 추가했던 기억은없죠?

 

stdio.h에 파일을 다루는 대부분의 함수들 또한 포함되어있기 때문에 파일을 다룰때 이것외에 따로 추가해주지 않으셔도 됩니다. (무언가 따로 추가해서 외울필요 없다는 반가운 소식입니다.)

 

함수 원형 설명
#include <stdio.h>
int fgetc(FILE* stream);
키보드/파일로부터 한 문자를 입력 받는다.
파일 끝에 도달 : EOF 반환
#include <stdio.h>
int fputc(FILE* stream);
모니터/파일에 한 문자를 출력 한다.
실패 : EOF 반환

 

fgetc() 문자 단위 입력 함수로 getchar() 함수와 동일한 기능을 하며 추가적으로 파일 스트림을 지정하거나 표준 입력 스트림(stdin)을 지정할 수 있는 특징이 있습니다.

 

따라서 fgetc() 함수를 이용하면 키보드(stdin)뿐만 아니라 파일에서도 데이터를 입력받을 수 있습니다.

 

getchar()에 파일 입력 기능이 추가된 형태이므로 함수 이름 앞에 파일을 의미하는 f를 붙이고 함수이름이 너무 길어졌으므로 fgetc() 로 쓴다.. 라고 이해하셔도 좋습니다. 

 

fputc() 함수는 문자 단위 출력 함수로 putchar() 함수와 동일한 기능을 하며 추가적으로 파일 스트림을 지정하거나 표준 출력 스트림(stdout)을 지정할 수 있는 특징이 있습니다.

따라서 fputc() 함수를 이용하면 모니터뿐만 아니라 파일에 데이터를 출력할 수 있습니다.

 

#include <stdio.h>

int main(){
	FILE * stream = fopen("newfile.txt", "w"); //파일 스트림 생성 & 열기 
	int input = 0;
	
	if(stream == NULL)
		puts("파일을 여는 중 오류가 발생했습니다."); 
	printf("데이터를 입력 해주세요 : ");
	
	while(input != EOF){
		input = fgetc(stdin); //표준 입력 스트림(키보드) 이용해서 입력
		fputc(input, stream); //입력받은 한글자 파일 스트림에 출력 
	}
	
	int file_state = fclose(stream); //파일 닫기(파일 스트림 닫기)
	if(file_state == EOF)
		puts("파일을 닫는 중 오류가 발생했습니다."); 
	
	return 0;
}

데이터를 입력 해주세요
Welcome to the C
We are finally able to write files!!
Celebrate
^Z

--------------------------------
Process exited after 28.82 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

다음 예제를 손수써보고 원하는 데이터를 입력한 후 Ctrl + Z로 입력을 마쳐봅시다.

 

오류 없이 잘 실행되었다면 다음과 같이 입력값과 동일하게 파일이 생성된 것을 확인하실 수 있습니다!

C언어 강의 총 29강만에 드디어 험난한 길을 거쳐서 파일을 쓸 수 있게 되었네요.

 

사실 쓸만한 프로그램을 만들려면 네트워킹 처리(웹 요청 등)도 알아야 하고 아직 갈길이 멉니다만 파일 쓰기가 C언어에서 그나마 이렇게 간편하게 이루어지는게 감사를 표해야 할거 같습니다.

(C를 오래하다보니 printf() 함수 같은것도 정말 고맙게 느껴집니다.)

 

어쨌든 각설하고 한줄씩 알아보겠습니다.

 

FILE * stream = fopen("newfile.txt", "w"); //파일 스트림 생성 & 열기

우선 해당 라인에서 파일 스트림을 생성함과 동시에 파일을 열게됩니다.

여는 파일은 newfile.txt 고 mode는 w(텍스트 쓰기) 모드이므로 newfile.txt 가 새로 써지게 될것입니다.

 

*모드에서 텍스트 모드 t의 경우 생략이 가능하다고 했으므로 w == wt입니다. 기본적으로 텍스트모드 라는것입니다.

 

while(input != EOF){
    input = fgetc(stdin); //표준 입력 스트림(키보드) 이용해서 입력
    fputc(input, stream); //입력받은 한글자 파일 스트림에 출력 
}

중간에 오류 처리는 이미 설명드렸으므로 생략하고 중요한건 이 부분입니다.

이곳에서 키보드 입력과 동시에 파일 스트림 출력이 이루어집니다.

 

input의 초기값이 0이므로 while문에서 input != EOF(-1)조건에 의해 0과 -1은 다르므로 != 연산자가 참이 되어 While문이 돌아가게 될 것입니다.

 

그리고 input의 값을 받는데 기존에 한문자를 입력받을때 사용했던 getchar() 을 쓰지 않고 fgetc() 를 썼습니다.

표준 파일 입출력 함수들은 인자값에 파일 스트림 말고 표준 입출력 스트림을 넣어서 키보드 입력 / 모니터 처리도 가능하다고 했죠? 

 

바로 그 방법을 이용했습니다. fgetc() 함수를 통해서 문자를 입력받아 input에 저장하는데 선택한 스트림은 stdin(표준 입력 스트림)으로 키보드로부터 문자를 받습니다.

 

문자의 입력의 끝을 알리려면 Ctrl + Z로 강제로 EOF를 발생시키면 됩니다. 그러면 input에 -1이 저장되게 되어 -1 != -1, -1과 -1은 다르지 않으므로(같으므로) != 연산자는 False => While문 False로 종료되게 됩니다.

 

fputc() 함수에선 변수 input에 저장된 문자를 출력하고 있습니다. 선택한 스트림은 파일 스트림 stream 으로 newfile.txt 파일에 입력된 문자를 출력합니다.

현재 프로그램의 디렉터리에 가서 newfile.txt에 키보드로 입력한 문자들이 저장되어있는지 확인합시다.

 

int file_state = fclose(stream); //파일 닫기(파일 스트림 닫기)

그리고 마지막에 파일을 닫습니다.

우리가 메모장으로 파일 열어서 뭔가 썼으면 마지막에 반드시 닫아줘야겠죠. 그 작업을 시행합니다.

fclose() 함수가 실행되면 파일 스트림이 닫히게 됩니다. (소멸)

 

 

그런데 결과물에 의문을 가지시는 분들도 있을겁니다.

분명 fputc()나 fgetc() 나 한문자씩 입력받고, 한문자씩 출력하는 함수 아닌가?

왜 이렇게 문자열처럼 여러 문자들이 출력이 된거지?

 

네 맞습니다. 충분히 들만한 의문입니다. 위 예제는 while문을 통해서 돌리고 있기 때문에 그 원리를 보면 ABC 이렇게 문자 3개를 입력하면 한문자씩 A 입력 A 쓰기  B입력 B쓰기 C입력 C 쓰기 이런식으로 진행되고 있는겁니다.

 

while(input != EOF){
    input = fgetc(stdin); //표준 입력 스트림(키보드) 이용해서 입력
    fputc(input, stream); //입력받은 한글자 파일 스트림에 출력 

    printf("%c 출력 완료", input);
}

아까 위 예제에서 while문안에 끝에 printf 하나만 써주면 어떻게 돌아가고 있는지 알 수 있습니다.

보시면 문자열을 입력해도 한문자씩 쓰기를 진행하고 있음을 알 수 있습니다.

 

그리고 여기서 fputc() 의 특징도 알았는데 ABCD 라고 입력하면 한글자씩 쓸때 매번 덮어써서 마지막 D만 남는게 아니라 파일 끝에 ABCD 라는 내용을 순차적으로 추가하는걸 알 수 있습니다.

 

 

Case3. 키보드(stdin)로부터 데이터를 입력받아 파일(stream)에 데이터를 출력

 

방금까진 Case3에 해당하는 것으로, 키보드(stdin)에서 데이터를 입력받아서 파일 스트림에 출력해 위와 같이 newfile.txt 에 내용을 적었습니다.

 

Case1. 파일(stream)로부터 데이터를 입력받아 파일(stream)에 데이터를 출력

 

아래 예제는 Case1에 해당하는 것으로, 방금만들었던 newfile.txt에서 데이터를 입력받아서 (파일 스트림에 데이터를 입력받아서) newfile2.txt에 데이터를 출력해보겠습니다. 

#include <stdio.h>

int main(){
	FILE * stream1 = fopen("newfile.txt", "r"); //읽기 전용 파일 스트림 
	FILE * stream2 = fopen("newfile2.txt", "w"); //쓰기 전용 파일 스트림 
	
	int input = 0;
	
	while(input != EOF){
		input = fgetc(stream1); //newfile.txt 로부터 EOF가 아닐때까지 문자를 읽는다 
		fputc(input, stream2); //newfile.txt에서 읽어들인 문자를 newfile2.txt에 쓴다. 
	}
	
	fclose(stream1); fclose(stream2); //파일 닫기(파일 스트림 닫기)

	return 0;
}

프로그램이 정상적으로 실행되었다면 위와 같이 newfile.txt와 동일한 내용이 적힌 newfile2.txt가 생성되었을 것입니다. 

(직접 코드를 타이핑 중이신 특히 스트림 모드 적을때 오타 주의하세요. r써야 할거 w로 잘못적으면 새로 쓰기 되서 다 날라갑니다..)

 

이제 소스코드를 분석해봅시다.

 

FILE * stream1 = fopen("newfile.txt", "r"); //읽기 전용 파일 스트림 
FILE * stream2 = fopen("newfile2.txt", "w"); //쓰기 전용 파일 스트림

우선 해당 라인에서 newfile.txt을 읽기 모드로, newfile2.txt 를 쓰기 모드로 열게됩니다.

만약에 newfile.txt가 없으면 파일 열기 에러가 발생하게 됩니다.

newfile2.txt는 쓰기 모드인데, newfile2.txt 가 없으면 생성하고 이미 있으면 새파일로 덮어쓰기를 진행합니다.

 

while(input != EOF){
    input = fgetc(stream1); //newfile.txt 로부터 EOF가 아닐때까지 문자를 읽는다 
    fputc(input, stream2); //newfile.txt에서 읽어들인 문자를 newfile2.txt에 쓴다. 
}

그리고 해당라인에서 input이 EOF가 아닌지 검사하면서 While문을 돌립니다.

fgetc() 함수를 통해서 문자를 입력받아서 input에 저장하고 있습니다. 선택한 파일 스트림은 stream1 이므로 newfile.txt에서 문자를 한글자씩 입력받습니다.

 

그리고 fputc() 함수를 이용해서 stream2에 한글자씩 쓰기를 진행하는데 선택한 파일 스트림은 stream2 이므로 newfile2.txt 에 문자를 한글자씩 씁니다.

 

여기서 또한 신기한걸 볼 수 있는데요. fgetc() 로 While문을 통해 데이터를 가져오고 있는데 생각해보면 fgetc() 의 경우 '한 문자' 씩 데이터를 가져오는거 잖아요?

 

그런데 반복호출할때마다 한글자씩 읽을때 예를 들어서 읽을 파일 스트림이 ABCD 라고 되어있으면 fgetc() 를 4번 호출했다고 가정시 앞의 A만 읽어와서 AAAA 처럼 읽는게 아니라 자기가 알아서 다음 위치를 찾아가서 A 읽고 B읽고 C 읽고 D를 읽는 형태로 진행이 됩니다.

 

이런게 가능한 이유는 '파일 위치 지정자' 가 있기 때문입니다.

파일 위치 지정자에 대해 간단히 설명드리자면 예를 들어서 test.txt 란 파일에 abcdefg 가 적혀있을때, 파일 위치 지정자는 우선 파일의 맨 첫부분을 가리키고 있습니다. 지금 예제에선 a가 맨 앞글자이므로 a를 가리키고 있겠네요.

 

만약에 fgetc() 를 호출해서 입력을 받는다면 일단 첫글자 a를 입력받고 파일 위치지정자는 한칸 넘어가 다음 문자인 b를 가리키고 있게 됩니다.

fgetc() 를 다시 호출하면 a를 입력받는게 아니라 그 다음인 b를 입력받고 다시 파일 위치 지정자는 한칸 이동해 c를 가리키게 됩니다.

 

그래서 딱히 처리를 해주지 않았는데도 파일 위치 지정자 때문에 fgetc() 를 계속 호출하면 파일 위치 지정자가 한칸씩 움직여서 파일의 끝(EOF)을 만날때까지 입력을 받을 수 있는것 입니다.

 

C언어에선 이 파일 위치 지정자를 임의로 옮길 수 있는 기능을 제공하는데 fseek() 함수를 이용해서 옮길 수 있습니다.

fgetc() 함수를 여러번 호출해서 abcdefg 까지 다 읽었는데 b부터 다시 읽고 싶다고하면 파일 위치 지정자를 b로 옮겨버리면 됩니다.

 

우선은 여기까지 간단하게 이해하고 넘어갑시다. 뒤에서 파일 위치 지정자에 대한 자세한 설명을 더 다루겠습니다.

 

fclose(stream1); fclose(stream2); //파일 닫기(파일 스트림 닫기)

마지막은 만들어진 파일 입력 스트림, 파일 출력 스트림을 소멸시키고 파일을 닫습니다.

C언어는 컴파일을 진행하며 세미콜론 기준으로 읽기 때문에 이렇게 세미콜론으로 구분시켜서 한줄에 써도 무방합니다.

물론 이게 싫으시면 두줄에 fclose() 를 나눠 쓰셔도 됩니다.

 

저는 주석 포함 한줄에 깔끔하게 다 적는것을 선호해서 이렇게 적었습니다.

 

fgets() 함수와 fputs() 함수

파일에 문자단위로 읽고/쓰는걸 배워봤으니 이제 문자열로 읽고/쓰는것도 봐야겠죠?

fgets() 함수와 fputs() 함수가 그런 역할을 합니다.

아래는 이들 함수에 대한 원형입니다.

 

함수 원형 설명
#include <stdio.h>
char * fgets(char * s, int n, FILE * stream);
키보드/파일로부터 문자열을 입력받는다
파일 끝에 도달 : NULL 포인터 반환
#include <stdio.h>
int fputs(const char * s, FILE * stream);
모니터/파일에 문자열을 출력
실패 : EOF 반환

* 사용을 위해 <stdio.h>를 인클루드 해야 합니다

 

fgets() 함수는 문자열 입력 함수로 gets() 함수와 동일한 기능을 하며 추가적으로 파일 스트림을 지정하거나 표준 입력 스트림(stdin) 을 지정할 수 있는 특징이 있습니다.

 

따라서 fgetc() 함수를 이용하면 키보드 뿐만이 아니라 파일로부터 데이터를 입력받을 수 있습니다.

 

fgets의 원형은 다음과 같습니다 char * fgets(char * s, int n, FILE * stream);

입력된 stream에서 문자열을 받아서 최대 n-1 개의 문자를 입력 받을때 까지나, 개행 문자 / 파일의 끝에 도달할때까지 입력받아서 문자열 s 에 저장합니다.

 

n개가 아니라 n-1개인 이유는 문자열을 입력받을때 끝에 자동으로 널문자가 붙기 때문입니다.

fgets() 는 gets() 함수와 같은 기능을 하며 파일 스트림 기능이 추가된 함수인데, 개행 문자에 의해서 입력이 끝나는 특징 역시 동일합니다. scanf() 는 공백으로 구분해서 띄어쓰기가 있는 문자열은 받을 수 없었죠.

 

fputs() 함수는 문자열 출력 함수 puts() 함수와 동일한 기능을 하며 추가적으로 파일 스트림을 지정하거나 표준 출력 스트림(stdout)을 지정할 수 있는 특징이 있습니다. 따라서 fputs() 함수를 이용하면 모니터 뿐만이 아닌 파일에 데이터를 출력할 수 있습니다.

 

백문이불여일예! (백번 듣는거보다 예제 한번이 낫다)

농담이구요 ㅎㅎ

예제를 한번 보고 사용법을 익혀봅시다.

 

#include <stdio.h>

int main(){
	FILE * stream = fopen("newfile3.txt", "w");
	char buffer[50];
	
	fgets(buffer, sizeof(buffer), stdin);
	fputs(buffer, stream);
	
	fclose(stream);
	return 0;
}

파일 쓰기 테스트

--------------------------------
Process exited after 2.556 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

 

FILE * stream = fopen("newfile3.txt", "w");

우선 항상 하듯이 fopen() 함수를 이용해서 파일 스트림 만들어주고 , 쓰기 모드로 엽니다.

 

char buffer[50];
fgets(buffer, sizeof(buffer), stdin);

 

그리고 fgets() 로 입력을 받는데 입력 스트림을 stdin(표준 입력 스트림)으로 줬기 때문에 키보드에서 데이터를 입력받습니다. 그리고 buffer의 크기인 50. 널문자를 제외하고 총 49글자 만큼 키보드에서 읽어오고 그것을 buffer에 저장합니다.

 

그리고 fputs() 를 통해서 buffer에 저장된 문자열들을 stream에 출력하고 있습니다.

즉 newfile3.txt에 키보드로 입력한 내용이 파일로 써지게 되겠죠.

 

마지막에 fclose() 당연합니다.

 

fprint() 함수와 fscanf() 함수

이제까지는 문자, 문자열로 파일 쓰는걸 다뤄봤지만 이제 슬슬 scanf() 나 printf() 가 그리워지지 않으신가요?

 

지금 바로 등장합니다!

 

fprint() 함수와 fscanf() 함수는 %c, %d, %s 와 같은 서식문자를 이용해서 파일에 입출력이 가능합니다. 

지금까지 배워본 모든 파일 입출력 함수와 동일하게 스트림만 바꿔주면 키보드/모니터 입출력, 파일 입출력 모두 가능합니다.

 

자료형 단위 표준 파일 입출력 함수 fprintf() 와 fscanf()의 원형은 아래와 같습니다.

 

함수 원형 설명
#include <stdio.h>
int fscanf(FILE * stream, const char * format, ...);
키보드/파일로부터 자료형에 맞춰 데이터를 입력
(텍스트 데이터와 바이너리 데이터를 동시에 입력합니다.)
파일 끝이나 에러 발생 시 : EOF 반환
#include <stdio.h>
int fprintf(FILE * stream, const char * format, ...);
모니터/파일에 자료형을 맞춰 데이터를 출력
(텍스트 데이터와 바이너리 데이터를 동시에 출력합니다.)

 

예제를 통해 사용방법을 알아보겠습니다.

 

#include <stdio.h>

int main(){
	char name[10]; //이름 
	int kor, eng, total; //국어 점수, 영어 점수, 총점
	
	printf("이름을 입력해주세요 : ");
	fscanf(stdin, "%s", name); //== scanf("%s", name);
	
	printf("국어 점수, 영어 점수 입력 (공백 구분) : ");
	fscanf(stdin, "%d %d", &kor, &eng);
	total = kor + eng;
	
	FILE * stream = fopen("student_data.txt", "w");
	fprintf(stream, "%s %d %d %d \n", name, kor, eng, total); //파일에 데이터 출력
	//fprinf(stdout, "%s %d %d %d \n", name, kor, eng, total);  모니터에 출력
	fclose(stream); 
	
	puts("파일 저장이 완료되었습니다.");
	
	return 0;
}

이름을 입력해주세요 : File
국어 점수, 영어 점수 입력 (공백 구분) : 100 100
파일 저장이 완료되었습니다.

--------------------------------
Process exited after 4.917 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

예제를 실행해주고 데이터를 입력해주신 다음, 다음과 같이 프로그램 경로에 student_data.txt 가 생성되었고 내용이 제대로 들어갔는지 확인해봐야 합니다.

 

소스코드를 한줄씩 분석해보겠습니다.

 

fscanf(stdin, "%s", name); //== scanf("%s", name);

해당 라인에서 fscanf() 함수에서 입력 스트림을 stdin 으로 주고있습니다. 

표준 입력 스트림을 줬기 때문에 키보드로부터 데이터에서 입력을 받습니다.

 

사실 scanf("%s", name); 과 동일한 표현입니다.

 

키보드 입력의 경우 fscanf() 로 굳이 받을 필요가 없으나 표준 파일 입출력 함수의 경우 스트림만 바꿔주면 파일 입출력 뿐만 아니라 표준 입출력도 모두 가능하단것을 보여드리고 있는겁니다.

(지금까지 모든 예제에서 그랬구요.)

 

fscanf() 함수나, fprintf() 함수나 기존 scanf(), printf() 에서 어느 스트림에서 입력을 받는지, 출력을 하는지 첫번째 인자값만 추가되었을뿐 사용방법은 scanf(), printf() 와 같습니다.

 

fprintf(stream, "%s %d %d %d \n", name, kor, eng, total); //파일에 데이터 출력
//fprinf(stdout, "%s %d %d %d \n", name, kor, eng, total);  모니터에 출력

그리고 fprintf() 함수를 이용해서 '서식에 맞춰' stream에 데이터를 출력하고 있습니다. (출력==저장으로 이해해도 무관하다고 말씀드렸습니다.)

stream과 연결되어 있는게 student_data.txt 이므로 이 파일에 데이터를 쓰게 됩니다.

아래에 stream을 stdout을 바꿔서 출력하는 함수가 있는데 이것의 경우 모니터에 출력하라는 것입니다.

 

당연히 기존에 printf()로 출력하던것과 동일한 실행입니다.

 

근데 저장만 하면 의미가 없잖아요? 가져올 수 있어야 겠죠.

방금 만들었던 데이터를 그대로 읽어서 출력하는 예제도 만들어봅시다.

 

#include <stdio.h>

int main(){
	char name[10]; //이름 
	int kor, eng, total; //국어 점수, 영어 점수, 총점
	
	FILE * stream = fopen("student_data.txt", "r"); //파일에서 데이터 읽어오기
	fscanf(stream, "%s %d %d %d", name, &kor, &eng, &total); //파일 스트림으로 부터 데이터를 입력받는다. 
	//fprintf(stdout, "%s %d %d %d \n", name, kor, eng, total);  //모니터에 출력
	
	printf("이름 : %s\n", name);
	printf("국어 점수 : %d\n", kor);
	printf("영어 점수 : %d\n", eng);
	printf("총점 :  %d", total);
	
	fclose(stream); 
	
	return 0;
}

이름 : File
국어 점수 : 100
영어 점수 : 100
총점 :  200
--------------------------------
Process exited after 0.004549 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

데이터를 가져올땐 fscanf() 함수를 가져오면 됩니다.

기존에 scanf("%d", &a); 라고 하면 키보드로부터 숫자를 입력받아서 변수 a에 저장하는 형태였죠.

 

그런데 fscanf() 는 스트림을 지정할 수 있어서 키보드 뿐만 아니라 파일에서도 입력을 받을 수 있었잖아요?

 

fscanf(stream, "%s %d %d %d", name, &kor, &eng, &total); //파일 스트림으로 부터 데이터를 입력받는다.

이렇게 쓰면 파일 스트림에서 데이터를 서식에 맞춰서 입력받고 변수에 각각 저장하게 됩니다.

키보드가 아니라 파일에서 입력을 받은것이죠.

 

기존에 printf(), scanf() 는 키보드 입력 / 모니터 출력으로 제한되어 있어서 파일 스트림에 출력(저장) / 입력 받는게 조금 생소할 수 있습니다만,

 

사실 sprintf() 나 sscanf() 를 배웠을때랑 거의 비슷합니다. 이것들은 문자열로 입력 / 출력이 가능했다면

fprintf() 나 fscanf() 는 스트림 입력 / 출력이 가능해진것이죠. 

 

feof() 함수

여러분이 만약 파일에 있는 내용을 모두 입력받아야 하는 프로그램을 작성한다고 가정하면, 파일의 끝이 어딘지를 알아야 합니다. 즉, 파일로부터 내용을 계속해서 입력받다가 파일의 끝을 만나면 입력 과정을 종료하는 프로그램을 작성할 때 파일의 끝을 검사하는 것이 가장 먼저 해야 할 일입니다.

 

지금까지 파일의 끝을 반환하는 함수들을 몇개 배웠습니다. 

함수 파일의 끝에서 반환하는 값
fgetc() EOF(-1)
fgets() NULL(0)
fscanf() EOF(-1)

 

fget() 같은건 while문으로 !=EOF 를 조건으로 한글자씩 반복을 돌려서 가져왔었고, fgets() 는 \n이나 EOF 기준으로 한번에 가져올 수 있었으며 fscanf() 는 서식에 맞춰서 가져올 수 있었었죠.

 

만약에 feof() 함수를 모를때, 파일의 끝까지 데이터를 전부 가져와야 한다면 아래와 같이 해야합니다.

 

// case1 : fgetc()

#include <stdio.h>

int main(){
	int input;
	while(input != EOF){
		input = fgetc(stream1);
		printf("%c", input);
	}
	return 0;
}

// case 2 : fgets()
#include <stdio.h>

int main(){
	char buffer[50];
	char * string;
	while(string != NULL){
		string = fgets(buffer, sizeof(buffer), stream1);
        printf("%s", string)
	}
	return 0;
}

// case 3 : fscanf()
#include <stdio.h>

int main(){
	int input;
	while(input != EOF){
		input = fscanf(stream1, "%s %d %d %d \n", name, &kor, &eng, &total);
	}
	return 0;
}

방금전에 fgets() 혼자만 NULL을 반환해서 함수 2개는 EOF고 1개는 NULL을 검사해줘야 하네요.

파일의 끝을 확인하기 위해 EOF인지 NULL인지 기억하기 어렵습니다

 

또 다른 함수가 나왔는데 파일 끝을 만났을때 EOF 인지 NULL인지 또 다를 수 있구요. 매번 외워줄 수는 없는 노릇이죠... 편한 방법을 찾아야 합니다.

 

이런 문제를 해결 하기 위해 feof() 함수가 사용됩니다.

feof() 함수는 파일에 끝에 도달했는지 아닌지를 반환합니다. 파일의 끝에 도달시 0이 아닌값 반환, 파일의 끝에 도달하지 못한 경우 0을 반환합니다.

 

함수 원형 설명
#include <stdio.h>
int feof(FILE * stream);
파일의 끝에 도달했는지 아닌지를 검사
파일의 끝에 도달 : 0이 아닌값 반환
파일의 끝에 도달하지 못한 경우 : 0 반환

 

이걸 활용해서 파일의 끝을 어떻게 읽을까요?

파일의 끝에 도달할때까지 While문을 계속 돌려주면 될 것 입니다.

While문은 조건문이 True일때만 작동하니 feof()==0 (파일의 끝에 도달하지 못한경우) 를 조건으로

계속 루프를 돌려주면 되겠군요.

 

파일의 끝에 도달해서 feof() 의 반환값이 0이 아닌값이 나오면 0이아닌값 == 0 -> False 로 While문이 종료될 것입니다.

#include <stdio.h>

int main(){
	FILE * stream1 = fopen("newfile.txt", "r");
	char buffer[50];
	
	while(feof(stream1) == 0){
		fgets(buffer, sizeof(buffer), stream1);
		fputs(buffer, stdout);
	}
    
    fclose(stream1);
	return 0;
}

Welcome to the C
We are finally able to write files!!
Celebrate
--------------------------------
Process exited after 0.004407 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

보시면 값이 잘 불러와지죠?

feof가 파일 스트림에서 EOF를 검사해 0또는 0이 아닌값을 반환해주기 때문에 

 

파일에 입출력할때 쓰는 함수가 EOF를 반환하던 NULL을 반환하던 암기할 필요 없이 편리하게 읽어올 수 있게 되었습니다 ㅎㅎ

 

그런데 말입죠, 강의를 쓰는 당일에는 몰랐는데 feof() 함수가 한가지 문제가 있더군요..

feof() 함수는 작동 원리상 파일의 끝을 가리켜도 함수가 한번 더 실행이 된다고 합니다..

예를 들어서 위 예제에서 fgets() 가 아니라 fgetc() 로 받는다고 하면, While문 조건으로 파일의 끝을 만나면 Ctrl + Z 문자가 나와버립니다. 

 

그니깐 조건이 끝났으면 이제 끝내야하는데, 한번더 While문 코드가 돌아버린다 이말입니다.

 

그래서 저렇게 While문 조건에 feof() 를 쓰지말고 While(1)로 무한루프를 돌리고 if문으로 조건을 검사하면서 break를 걸어버리면 된다고 합니다.

그래서 아래처럼 합니다.

 

feof() 함수를 쓰다가 문제가 생기면 자세한 내용은 아래를 참고해주세요.

feof() 를 쓸땐 무한 반복을 돌리고 if문으로 검사해 break한다.

While문 조건엔 넣지 않는다. 로 기억해주시면 됩니다.

 

참고 내용 : https://me.tistory.com/380

fflush() 함수

fflush() 함수는 버퍼를 비우는 함수입니다. 

함수 원형 설명
#include <stdio.h>
int fflush(FILE * stream);
버퍼를 비운다.
실패 : EOF 반환

스트림을 사용하는 입출력 함수들은 버퍼를 공유하면서 문제를 빈번히 일으킵니다. 어떤 문제가 있는지 다음 예제를 실행해보며 알아보겠습니다.

 

$cf ) flush : $ <물·액체를> 왈칵 흘리다, 쏟아내리다; <하수도·거리 등을> 물로 씻어 내리다

 

#include <stdio.h>

int main(){
	int a;
	char b;
	
	scanf("%d", &a);
	scanf("%c", &b);
	
	printf("a : %d\n", a);
	printf("b : %c\n", b);
	
	return 0;
}

123
a : 123
b :


--------------------------------
Process exited after 0.399 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

다음 예제를 실행해보면 뭔가 이상함을 느끼실겁니다.

분명 scanf() 를 2번써서 a랑 b의 값을 받아오려고 했는데 123을 입력하자마자 b는 입력할 겨를도 없이 프로그램이 종료되어 버립니다.

 

어라.. 이상하네요?

이 문제 분명 C에서 프로그래밍 하다가 scanf로 여러번 입력받을때 많이 발생한 문제일 겁니다.

 

이 그림 기억나시죠?

기본적으로 키보드에서 입력을 받을때 입력받은 데이터는 표준 입력 스트림을 따라서 입력 버퍼에 임시 저장되고, 다시 출력 될때는 출력 버퍼로 저장되서 모니터로 출력됩니다.

 

지금과 같은 문제는 바로 저기 입력 버퍼에 데이터가 남아서 그렇습니다.

 

위 예제의 경우 저희가 입력을 할때 123 입력하고 엔터를 칠겁니다.

근데 엔터도 개행 문자로 일종의 문자죠? 실제로는 키보드에서 123\n 을 입력한 셈이 되고 입력 버퍼에 123\n이 저장됩니다. 

 

그러면 a에서 %d로 정수값 123을 입력받고 입력 버퍼에 '\n'이 남아서

다음 scanf() 호출을 통해 b에 입력을 받으려고 할때 '\n'을 함수의 데이터로 받아들이고

프로그램이 종료되어 버리는 것입니다.

 

이런 버퍼문제를 해결하는 방법엔 여러가지가 있습니다.

 

1. 애초에 버퍼를 이용하지 않는 비표준 입출력 함수를 이용한다.

2. getchar() 로 끝에 남은 \n 문자 하나를 받아낸다.

3. 입력 버퍼를 비운다 

 

사실 2번은 크게봤을때 3번에 속하는것입니다.

뭔가 3번이 땡기기도 하구요.. ㅎㅎ 어떻게 할까요?

 

fflush() 함수는 버퍼를 비우는 함수입니다. 

이 한줄로 설명이 충분하군요!

fflush() 에 인자값으로 스트림을 주면 스트림 버퍼를 비워서 불필요한 데이터를 삭제해줍니다.

 

#include <stdio.h>

int main(){
	int a;
	char b;
	
	scanf("%d", &a);
	fflush(stdin); //버퍼 비우기 
	scanf("%c", &b);
	
	printf("a : %d\n", a);
	printf("b : %c\n", b);
	
	return 0;
}

123
a
a : 123
b : a

--------------------------------
Process exited after 0.9687 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

버퍼를 비우니 잘 출력이 되었습니다.

그러나 fflush(stdin); 을 쓸때 주의가 필요한데 사실 버퍼를 비운다는 개념은 데이터를 삭제시키는게 아니라 출력 위치로 보내는 것입니다.

 

컴퓨터는 일반적으로 입출력 처리를 할 때 데이터가 들어오면 즉시 처리를 하는것이 아닌 버퍼에 잠시 담아뒀다가, 버퍼가 꽉 차면 그제서야 버퍼에 있는 데이터를 활용해 입출력 처리를 진행합니다. 즉 a.txt에 "abcd" 라는 내용의 파일 쓰기를 요청해도 임의의 버퍼에 abcd를 포함하여 다른 데이터들이 들어와 버퍼가 꽉차야 쓰기가 진행됩니다. (이런 동작을 하는 이유는 매번 파일 쓰기 요청이 있을때마다 그때 그때 디스크에 파일 쓰기 요청을 하는 것은 매우 비효율 적이기 때문입니다. 파일 쓰기 동작 요청시, DISK는 CPU에 비해 매우 매우 느리기에 한꺼번에 모아서 처리하는게 훨씬 유리합니다.) 이렇게 버퍼가 기다림을 방지하기 위해, 버퍼를 즉시 출력 위치로 보내 입출력을 처리하게 해주는 함수가 fflush() 함수라고 이해하시면 됩니다. 버퍼를 즉시 출력 위치로 보내면, 버퍼의 내용은 지워질 것이기 때문에 "버퍼를 청소 한다" 라는 개념으로 설명한 것입니다.

 

또 컴파일러에 따라서 입력버퍼를 지우는 fflush(stdin); 라는 코드는 동작할 수도 있고 동작하지 않을 수도 있습니다. (어떤 라이브러리는 입력버퍼를 전부 지워주기도 하지만 동작을 예측할 수 없습니다.)

 

그 이유는 기본적으로 fflush() 는 출력버퍼를 지우는데 활용하는 것이며, 입력버퍼를 지우는 행위에 대해선 동작이 정의되지 않았기 때문입니다.

 

물론 예제를 위해 예외적으로 보여드린 것이고, 또 제가 사용하는 라이브러리가 운이 좋게 fflush(stdin) 을 하면 입력 버퍼를 비워주는 수행 동작을 하였지만, 애초에 fflush()가 비우는 것은 입력 버퍼가 아닌 출력 버퍼기 때문에 그 동작을 예측이 불가함을 다시 알려드립니다.

 

참고할만한 것 : https://blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=ilikebigmac&logNo=221633997990 

 

fflush(stdin)을 사용하면 안되는 이유

01. fflush 함수에 대한 일반적인 생각 fflush 함수는 인자로 주어진 변수에 해당하는 버퍼를 비우는데 사...

blog.naver.com

https://codingdog.tistory.com/entry/c%EC%96%B8%EC%96%B4-fflush-%ED%95%A8%EC%88%98-%EC%9E%85%EB%A0%A5-%EC%8A%A4%ED%8A%B8%EB%A6%BC%EC%97%90%EC%84%9C%EB%8A%94-%EC%82%AC%EC%9A%A9%ED%95%98%EC%A7%80-%EC%95%8A%EB%8A%94%EB%8B%A4

 

c언어 fflush 함수 : 입력 스트림에서는 사용하지 않는다.

 fflush(stdin) 정도는 책을 보시면서 한 두번쯤은 보셨을 거에요. 가끔, 왜 이 함수를 썼는데 동작을 하지 않느냐. 는 질문도 더러 받아본 적이 있습니다. 원형은 다음과 같습니다.  하도 많이 언급

codingdog.tistory.com

 

#include <stdio.h>

int main(){
	int a;
	char b;
	
	scanf("%d", &a);
	getchar();
	scanf("%c", &b);
	
	printf("a : %d\n", a);
	printf("b : %c\n", b);
	
	return 0;
}

123
a
a : 123
b : a

--------------------------------
Process exited after 0.7028 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

 

그러므로 입력 버퍼를 지우고 싶으시면 getchar() 로 한글자를 받아냅시다!

사실 여러 방법이 있는데 이게 코드도 간결하고 쓸만합니다.

 

또한 설명드리지 않았지만 fflush(stdout); 과 같이 사용하면 출력 버퍼에 모아놨다 모니터에 출력하는게 아닌, 바로 출력 버퍼에 있는걸 모니터(결과쪽) 로 보내서 바로 출력해줘라. 라고 요청하는 것 입니다.

 

 

다음 편에서 계속됩니다..

 

COMMENT WRITE