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


안녕하세요 파일입니다.

저번에 이어 파일 입출력을 하는 방법에 대해 계속 알아봅시다!

콘솔 입출력과 파일 입출력편은 오늘이 마지막입니다. 달려보아요~

 

fread() 함수와 fwrite() 함수

3번째 이야기 하는것이지만 파일은 텍스트 파일과 바이너리 파일로 나누어진다고 했습니다.

지금까지 앞에서 학습한 함수들은 전부 텍스트모드에서 작동합니다.

 

지금까지 배운 함수들론 순수한 이진파일 (0과 1로만 이루어진)을 적어낼 수 없습니다. 텍스트 모드로 적히기 때문에 텍스트만 적을 수 있죠. 그러나 바이너리로 읽기/쓰기를 할 수 있는 함수를 배우면 달라집니다.

 

$cf ) binary : $ 2진법의

 

이제 바이너리 파일의 파일 입출력을 지원하는 함수를 다뤄봅시다.

이들에는 fread() 함수와 fwrite() 함수가 있습니다.

*fread = file read, fwrite = file write

 

우선 fread() 함수의 원형을 봅시다.

함수 원형 설명
#include <stdio.h>
size_t fread (void * buffer, size_t size, size_t count, FILE * stream);
파일로부터 바이너리 데이터를 받아 버퍼에 입력
성공 : count(반복 횟수) 반환
실패 : count보다 작은 값 반환

buffer : 파일로부터 입력받은 데이터를 저장하는 버퍼를 가리키는 포인터

size : 한 번에 입력받을 데이터의 바이트 크기

count : 반복 횟수

stream : 파일 입력 스트림

 

오와.. 인자값이 4개나 되네요..

fread() 함수는 파일 입력 스트림 stream이 가리키는 파일로부터 size 크기 만큼의 바이트를 buffer가 가리키는 영역으로 count가 지정한 횟수만큼 입력받습니다. size의 크기를 하나의 데이터 블록이라고 할 때 반환값은 입력받은 횟수, 즉 블록의 갯수입니다.

 

파일에서 입력받는 함수이므로 파일 입력 스트림을 이용합니다.

 

다음으로 fwrite() 함수입니다.

함수 원형 설명
#include <stdio.h>
size_t fwrite (const void * buffer, size_t size, size_t count, FILE * stream);

버퍼에 저장된 데이터를 파일에 출력
성공 : count(반복 횟수) 반환
실패 : count보다 작은 값 반환

buffer : 파일로부터 입력받은 데이터를 저장하는 버퍼를 가리키는 포인터

size : 한 번에 입력받을 데이터의 바이트 크기

count : 반복 횟수

stream : 파일 출력 스트림

 

fwrite() 함수는 파일 출력 스트림 stream이 가리키는 파일 내부에 buffer가 저장하고 있는 데이터를 size 크기로 count가 지정한 횟수만큼 출력합니다. fread() 함수와 마찬가지로 size의 크기를 하나의 데이터 블록이라고 하면, 반환값은 출력한 횟수, 즉 블록의 갯수입니다.

 

파일에 쓰는 함수이므로 파일 출력 스트림을 이용합니다.

 

지금 간단하게 알아본 함수 fread() 함수와 fwrite() 함수는 한꺼번에 많은 데이터를 입출력할때 탁월한 성능을 발휘합니다. 주목할점은 buffer가 void* (void 형 포인터)이라는 점입니다. 즉 포인터이기 때문에 버퍼를 넘길게 아니라 저장하는 버퍼의 주소를 넘겨야 할것입니다.

 

또 void형 포인터기 때문에 어떤 유형의 buffer을 사용할지 자유롭게 선택 가능합니다.

 

솔직히 이번건 설명만 들어서 잘 감이 안오시죠?

예제를 보고 익혀봅시다.

 

#include <stdio.h>

int main(){
	int buffer1[5] = {0xff, 0x56, 0x78, 0xfa, 0xf1};
	int buffer2[5];
	
	FILE * stream1 = fopen("student.dat", "wb"); //바이너리 쓰기 모드
	fwrite(buffer1, sizeof(int), 5, stream1); //buffer1에 저장된 데이터를 4바이트 크기만큼 5번 읽어서 stream에 저장한다. 
	fclose(stream1);
    
	FILE * stream2 = fopen("student.dat", "rb"); //바이너리 읽기 모드
	fread(buffer2, sizeof(int), 5, stream2); //stream2에 저장된 데이터를 4바이트 크기만큼 5번 읽어서 buffer2에 저장한다. 
	printf("%x %x %x %x %x", buffer2[0], buffer2[1], buffer2[2], buffer2[3], buffer2[4]);
	fclose(stream2);
	
	
	return 0;
}

ff 56 78 fa f1
--------------------------------
Process exited after 0.004963 seconds with return value 0
계속하려면 아무 키나 누르십시오 . . .

다음 예제는 fread() 와 fwrite() 를 사용하는 예제입니다.

 

FILE * stream1 = fopen("student.dat", "wb"); //바이너리 쓰기 모드

fread() 와 fwrite() 는 바이너리 파일을 읽고 쓰는 함수기 때문에 모드에 바이너리 모드를 명시해줘야 합니다.

wb 의 경우 write + binary 로 바이너리 쓰기 모드로 동작합니다.

 

fwrite(buffer1, sizeof(int), 5, stream1); //buffer1에 저장된 데이터를 4바이트 크기만큼 5번 읽어서 stream에 저장한다.

 

buffer1의 경우 16진수가 5개 저장되어 있는 int형 배열입니다. 각 원소가 int형이므로 4바이트, 총 5개로 구성되어 있습니다.

 

fwrite() 함수는 '파일 출력 스트림 stream이 가리키는 파일 내부에 buffer가 저장하고 있는 데이터를 size 크기로 count가 지정한 횟수만큼 출력합니다' 라고 했습니다.

 

간단히 이야기 해서 buffer에서 size크기로 count 횟수 만큼 읽어서 파일에 기록하는 것입니다.

배열 같이 연속적인 데이터를 기록할때 유용합니다.

 

해당 라인에서 fwrite() 함수를 이용해 buffer1에 저장된 데이터를 4바이트 크기만큼 5개 읽어서 stream에 저장합니다.

(배열의 경우 연속적인 메모리 공간을 가지기 때문에 4바이트로 5번 반복을 하면 buffer1의 모든 데이터를 기록할 수 있을것입니다.)

stream에 해당하는 파일이 student.dat 이므로 이 파일에 저장될 것입니다.

 

또한 인자값 처음으로 버퍼를 넘기는데 buffer1은 배열의 시작 주소(배열 이름) 입니다. void형 포인터로 되어 있으므로 버퍼의 자료형은 신경쓰지 않아도 됩니다.

 

fread(buffer2, sizeof(int), 5, stream2); //stream2에 저장된 데이터를 4바이트 크기만큼 5번 읽어서 buffer2에 저장한다.

역으로 읽어오는 fread() 함수의 경우 fwrite() 함수가 버퍼에서 읽어와서 파일에 기록했다면,

fread() 함수의 경우 파일에서 읽어와서 버퍼에 저장합니다.

 

*함수 이름이 왜 filewrite, fileread 인지 감이 잡히시죠?

 

위 코드의 경우 stream2에 저장된 데이터를 4바이트 크기만큼 5번 읽어서 buffer2에 저장하고 있습니다.

기본적인 인수는 fwrite() 함수와 동일하며 버퍼에 저장하므로 데이터가 다 담길만큼 버퍼크기가 충분해야합니다.

 

위 코드의 경우 stream2에 저장된 데이터를 4바이트 크기만큼 5번 읽어서 buffer2에 저장하고 있습니다.

printf("%x %x %x %x %x", buffer2[0], buffer2[1], buffer2[2], buffer2[3], buffer2[4]);

마지막으로, 버퍼에 저장된 16진수들을 출력하고 있습니다.

저장시켰던 값과 출력된 값이 동일한지 확인해야 합니다.

 

저장된 student.dat 을 텍스트 에디터로 열어보면 파일이 깨지는걸 알 수 있습니다.

텍스트 파일 형태로 저장하지 않고 바이너리 파일로 저장했기 때문입니다.

이럴땐 HxD 유틸리티를 이용하면 이진 데이터로 이루어진 데이터를 그대로 까볼 수 있습니다.

 

* HxD는 이진 파일을 읽을 수 있는 무료 에디터 프로그램입니다.

https://yum-history.tistory.com/134

 

[Tool] HxD Editor

[Tool] HxD Editor (1) HxD란? 일반 텍스트 타입 파일은 텍스트 편집기를 이용하여 쉽게 확인할 수 있지만, 대부분 파일 내부에는 텍스트가 아닌 데이터로 존재 HxD는 이진 파일을 읽을 수 있는 무료 에

yum-history.tistory.com

 

HxD는 개별 바이트를 16진수 값으로 보여주는게 특징입니다. 아까 기록했던 0xff, 0x56 과 같은 데이터들이 보이네요

 

저도 이 프로그램을 거의 써본적이 없어서 사용법은 정확히 모르는데 추정컨데 아마 저 숫자 한칸당 1바이트가 아닌가 싶네요. fwrite() 로 16진수를 4바이트씩 썼으니 나머지 필요 없는 부분은 00으로 채워준거 같습니다.

 

확장자 EXE, BIN, DAT과 같은 파일을 메모장과 같은 텍스트 에디터로 열면 문자가 다깨져서 나옵니다. 이제 그 이유를 아시겠죠? 이 파일들은 텍스트 파일이 아니라 대부분 바이너리 파일의 형태로 저장되어 있기 때문입니다.

 

 

파일 위치 지정자(File Position Indicator)

우리가 지금까지 공부한 파일 입출력 함수들은 파일의 처음부터 끝까지 모두 순차적으로 정직하게 데이터를 입력받거나 출력합니다. 예를 들어서 fgetc() 와 같은 함수는 여러번 호출하면 첫번째 글자만 계속해서 읽는게 아니라 알아서 다음 글자를 읽어냅니다.

 

그리고 그건 파일 위치 지정자가 다음 읽을걸 미리 가르키게 되고, 또 읽으면 다음걸 가리켜서 읽을 수 있는것이였죠. 

 

영어로는 File Position Indicator 인데 어떤 분은 지시자라고도 부르기도 합니다. 아마 영어 번역의 차이같은데 저는 파일 위치 지정자로 통일하겠습니다.

 

#include <stdio.h>
 
int main(){
	FILE * stream = fopen("newfile.txt", "r"); //파일 스트림 생성 & 열기 
	int input = 0;
	
	while(1){
		if(feof(stream)) break;
		input = fgetc(stream);
		if(input != EOF)
			printf("%c", input);
	}
	
	fclose(stream);
	
	return 0;
}

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

 다음 예제는 이 앞전에 배웠던 fgetc() 와 feof() 함수를 이용해서 파일을 전체 탐색 하고있습니다.

 

출력값을 보시면 알겠지만 newfile.txt엔 HelloWorld라는 문자열이 저장되어 있구요.

저번 강의에서 파일 위치 지정자와 fgetc() 의 동작 원리에 대해 간단히 설명드렸었는데요.

 

fgetc() 는 한문자씩 파일을 읽어내는 함수인데 신기하게 호출을 계속하면 다음 문자를 계속해서 읽어옵니다.

 

예를 들어서 fgetc() 가 3번 호출되었을때 파일 위치 지정자는 이 문자를 가리키게 됩니다.

왜 3번째 문자가 아니라 4번째 문자냐고 하면

 

fgetc() 함수 호출전, 처음에는 우선 파일 위치 지정자가 첫번째 문자 H를 가리켰다가

fgetc() 함수가 호출되면 H를 읽고 파일 위치 지정자는 다음 문자 e를 가리키게 됩니다.

 

다시 호출하면 e를 읽고 파일 위치 지정자는 l을 가리키게 됩니다.

다음에 읽을걸 미리 가리켜놓는 셈이죠. 

 

이렇게 파일 위치 지정자가 다음으로 한 칸씩 움직여주는 덕분에 우리는 데이터를 순차적으로 읽어낼 수 있습니다. 그런데 이 전에도 말씀드렸듯이 C에선 파일 위치 지정자를 사용자가 옮겨낼 수 있는 레퍼런스 함수들을 제공합니다.

 

오늘 배워볼 함수가 fseek() 함수와 ftell() 함수인데 fseek() 함수가 그 역할을 합니다.

 

fseek() 함수

fseek() 함수는 파일 위치 지정자의 위치를 옮기는 함수입니다.

 

아래는 함수 원형입니다.

함수 원형 설명
#include <stdio.h>
int fseek(FILE* stream, long int offset, int origin);
stream : 파일 스트림
offset : 얼마만큼 파일 위치 지정자를 옮길지, origin을 기준으로 offset만큼 옮긴다.
origin : 어디에서부터 파일 위치 지정자를 옮길지

일단 항상 하던대로 첫번째 인자값엔 파일 스트림 주면 되겠고,

offset 은 얼만큼 파일 위치 지정자를 옮길지 정하는거네요.

 

+값을 주면 위 HelloWorld 사진을 기준으로 오른쪽으로 옮기는 것이고, - 값을 주면 왼쪽으로 파일 위치 지정자를 옮기는 것입니다. x좌표 생각하시면 됩니다.

 

중요한건 origin을 기준으로 옮긴다고 합니다.

 

그럼 origin은 어떻게 줘야 할까요?

 

어디에서부터 파일 위치 지정자를 옮긴다니.. 그야 파일 시작부터 옮기면 되겠다만 파일의 끝을 기준으로도 옮기고 싶을 수 있겠죠.

 

origin엔 실제로 SEEK_SET, SEEK_CUR, SEEK_END 이 있는데 순서대로

파일의 시작, 현재 파일 위치 지정자의 위치, 파일의 끝을 의미합니다.

 

아래 예제를 통해 사용법을 익혀봅시다.

 

참고로 newfile.txt 는 아까 사용한 파일로

HelloWorld란 내용이 들어가 있습니다.

#include <stdio.h>
int main() {
  FILE * stream = fopen("newfile.txt", "r");
  char buffer[10];
  char c;

  fgets(buffer, 5, stream); //파일 스트림에서 5바이트 입력받아 data에 저장한다. 
  printf("입력 : %s \n", buffer);

  c = fgetc(stream);
  printf("다음 문자 입력 : %c \n", c);

  fseek(stream, -1, SEEK_CUR);

  c = fgetc(stream);
  printf("What Character? : %c \n", c);

  fclose(stream);
}

입력 : Hell
다음 문자 입력 : o
What Character? : o

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

소스코드를 Line-Read 해가며 한번 어떤 코드인지 확인해보겠습니다.

 

fgets(buffer, 5, stream); //파일 스트림에서 5바이트 입력받아 data에 저장한다.

우선 이전 배운 fgets() 를 통해서 파일 스트림에서 5바이트를 입력받아서 buffer에 저장하고 있습니다.

gets() 는 fgets() 와 동일한 기능을 하는 함수라고 했으므로 일단 문자열을 받을것이고, 문자열을 받을때 필연적으로 끝에 Null문자를 추가합니다.

 

5바이트를 가져오지만 끝에 Null문자를 자동으로 추가해 받을것이기 때문에 실상 받을 수 있는건 4바이트(4글자) 를 받습니다.

 

그래서 HelloWorld 의 4글자인 Hell 까지 받았을것이고 출력결과는 Hell 입니다.

파일 위치 지정자는 다음 읽을것을 가리킨다고 했으니깐 문자 'o' 를 가리키고 있겠네요.

 

  c = fgetc(stream);
  printf("다음 문자 입력 : %c \n", c);

그래서 fgetc() 로 문자를 캐치해보니 당연지사 파일 위치 지정자가 가리키고 있는 o가 출력될 것 입니다.

이제 o 다음 문자인 W를 파일 위치 지정자가 가리키고 있게 되겠군요.

 

  fseek(stream, -1, SEEK_CUR);

다음에 fgetc() 를 한번더 호출하면 이제 W가 출력되야 되겠지만 fseek() 함수를 한번 이용해봅시다.

SEEK_CUR은 현재 파일 위치 지정자의 위치를 나타내고 -1을 줬으니 파일 위치 지정자를 왼쪽으로 옮기라는 말이 됩니다.

 

그림으로 옮기는 과정을 표현하면 이렇게 되겠죠.

파랑색이 옮기기 전이고, 주황색이 fseek() 호출 이후 옮겨진 파일 위치 지정자의 모습입니다.

 

  c = fgetc(stream);
  printf("What Character? : %c \n", c);

파일 위치 지정자가 제대로 옮겨졌는지 확인하기 위해, 마지막으로 fgetc() 를 해서 현재 파일 위치 지정자가 가리키고 있는 문자를 Catch 해 봅니다.

 

출력값은 어떤가요? 문자 'o' 입니다! 원래는 'W'가 출력되야 맞았겠죠.

 

#include <stdio.h>

int main() {
  FILE * stream = fopen("newfile.txt", "r");
  char c;
  
  fseek(stream, -1, SEEK_END); //파일 위치 지정자를 맨 끝문자로 옮긴다. 
  c = fgetc(stream);
  printf("파일 마지막 문자 : %c \n", c);

  fclose(stream);
}

파일 마지막 문자 : d

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

fseek() 함수를 조금더 유용하게 사용해보겠습니다.

다음 예제는 파일의 끝 문자를 가져오는 예제입니다.

 

fseek(stream, -1, SEEK_END); //파일 위치 지정자를 맨 끝문자로 옮긴다.

이 소스코드에서 가장 중요한 내용은 다음 부분인데,

SEEK_END(파일의 끝)을 기준으로 파일 위치 지정자를 -1만큼 옮기게 됩니다.

 

왜 파일의 끝으로 안옮기고 파일의 끝에서 -1만큼 옮기냐고 하면, 맨 끝으로 옮기게 되면 그 부분에는 EOF(파일의 끝)을 나타내는 것이 들어가 있어서 우리가 원하는 결과가 아니게 됩니다.

그래서 -1만큼 땡겨서 HelloWorld 의 끝글자인 d를 가져오게 되는것이죠.

 

fseek() 함수의 장점은 이렇게 기준을 잡고 거기서 +, - 로 값을 줘서 파일 위치 지정자를 옮길 수 있다는 겁니다.

fseek() 함수의 기준이 파일 위치 지정자로 고정되어 있다면 이렇게 끝을 읽어내는게 쉽지 않았겠죠.

 

 

 

 

내용 참고 : https://modoocode.com/123

 

ftell() 함수

지금까지는 파일 위치 지정자가 어떻게 움직이는지 눈에 보이진 않지만,

어떻게 움직이는지 알기 때문에 생각을 해가면서 논리적으로 따졌었죠.

 

근데 이게 지금 어디에 위치해있는지 알면 좋잖아요?

ftell() 함수를 사용하면 스트림의 위치 지정자의 현재 위치를 구할 수 있습니다.

 

워낙 사용이 간단해서 원형 선언은 귀찮으니 생략하도록 하겠습니다.  (솔직히 이제 힘들어요 . ㅜ ㅜ)

 

#include <stdio.h>
int main() {
  FILE * stream = fopen("newfile.txt", "r");
  char buffer[10];
  char c;

  printf("파일 위치 지정자 초기 위치 : %d\n", ftell(stream));
	
  fgets(buffer, 5, stream); //파일 스트림에서 5바이트 입력받아 data에 저장한다. 
  printf("입력 : %s \n", buffer);
  printf("파일 위치 지정자(pos) : %d\n", ftell(stream));

  c = fgetc(stream);
  printf("다음 문자 입력 : %c \n", c);
  
  printf("파일 위치 지정자(pos) : %d\n", ftell(stream));

  fseek(stream, -1, SEEK_CUR);
  
  printf("파일 위치 지정자(pos) : %d\n", ftell(stream));

  c = fgetc(stream);
  printf("What Character? : %c \n", c);
  
  printf("파일 위치 지정자(pos) : %d\n", ftell(stream));

  fclose(stream);
}

파일 위치 지정자 초기 위치 : 0
입력 : Hell
파일 위치 지정자(pos) : 4
다음 문자 입력 : o
파일 위치 지정자(pos) : 5
파일 위치 지정자(pos) : 4
What Character? : o
파일 위치 지정자(pos) : 5

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

ftell() 함수를 써주면 파일 위치 지정자가 어떻게 움직이고 있는지 이렇게 볼 수 있습니다.

그리고 이 내용은 아까 저희가 이론적으로 확인했던 내용과 동일하게 움직이고 있구요.

 

Hell 까지 fgets()로 읽어냈으면 파일 위치 지정자는 Hell 다음 글자인 'o' 를 가리키고 있고 이건 5번째 글자일 것인데

파일 위치 지정자가 5가 아니라 4로 뜨는 이유는 

 

항상 컴퓨터가 0부터 읽는 습성때문에 그렇습니다.

파일 위치 지정자도 첫번째 위치를 우리는 암묵적으로 1번째로 받아들였을 것이지만

컴퓨터는 0부터 읽어서 0이 첫번쨰 입니다 -.-

 

0부터 세나 1부터 세나 뭐 표현만 다른거지 이해했다는 사실이 중요하구요!

어쨌든 축하드립니다. 

콘솔 입출력과 파일 입출력 편은 여기서 끝이니깐요.

 

여러분은 파일 입출력을 정복하신겁니다! 우선 강의를 열심히  쓴 저에게 스스로 칭찬을 해주고 싶고 여러분들에게도 축하를 드리고 싶네요.

 

이제 다음에 알아볼 것은 동적 메모리 할당과 가변 인자입니다.

다음 강의에서 뵙시다.

 

 

COMMENT WRITE