파일 입출력은 매우 중요한 부분이고, 학교에 다니던 때 파일 입출력을 가볍게 생각하고 제대로 이해/공부하지 않은 것이 나중에 큰 어려움이 되었었다. 지금 복습하는 이유도 파일 입출력, 메모리 할당, 문자열 처리 등을 소홀히 했던 것이 크다. 이번에는 후회하지 않도록 차근차근 공부해야겠다.
파일은 운영체제에 의해 관리된다. 따라서 운영체제와 파일의 관계를 이해할 필요가 있다.
프로그램 상에서 파일에 저장된 데이터를 읽어오고 싶을 때, 프로그램과 참조할 데이터가 있는 파일 사이에 데이터가 이동할 수 있는 다리가 필요하다. 이는 이전에 공부했던 스트림의 역할이다. 즉 프로그램과 데이터 사이에 스트림이 형성되어있어야 데이터를 주고받을 수 있다.
다음의 함수를 호출하면 프로그램 상에서 파일과의 스트림을 형성할 수 있다.
#include <stdio.h>
File * fopen(const char *filename, const char * mode);
//함수 호출 성공 시 해당 파일의 FILE 구조체 변수의 주소값을, 실패 시 NULL 포인터 반환
함수의 첫 번째 인자로 스트림을 형성할 파일의 이름을, 두 번째 인자로 형성할 스트림의 종류에 대한 정보를 문자열의 형태로 전달한다. 이후 함수는 해당 파일과의 스트림을 형성하고 스트림 정보를 FILE 구조체 변수에 담아 그 변수의 주소값을 반환한다.
프로그램 상에서 fopen 함수를 호출했을 때, 다음과 같은 일이 발생한다.
fopen 함수가 호출되면 FILE 구조체 변수가 생성되고, 이렇게 생성된 구조체 변수에는 파일과 관련된 정보가 담긴다.
스트림은 한 방향으로 흐르는 데이터의 흐름이라 했다. 따라서 스트림은 데이터를 파일로부터 읽어들이기 위한 입력 스트림과 데이터를 파일에 쓰기 위한 출력 스트림으로 구분된다.
스트림의 형성을 위해 fopen 함수를 호출할 때, 두 가지 인자가 전달되어야 한다. 스트림을 형성할 파일의 이름과 형성하려는 스트림의 종류이다.
FILE * fp = fopen("data.txt", "wt"); //출력 스트림 형성
파일 data.txt와 wt 모드의 스트림을 생성하라는 의미이다. wt 모드는 텍스트 데이터를 쓰기 위한 출력 스트림 모드이다.
이렇게 생성된 출력 스트림은 파일에 데이터를 쓸 수 있도록 하며, 데이터를 읽어올 수는 없다.
FILE *fp = fopen("data.txt", "rt"); //입력 스트림 형성
파일 data.txt와 rt 모드의 스트림을 생성하라는 의미이다. rt 모드는 텍스트 데이터를 읽기 위한 입력 스트림 모드이다.
데이터 읽기만 가능한 스트림이 형성되었다.
fopen 함수와 반대의 기능을 하는 fclose 함수에 대해 알아보겠다. fopen 함수는 스트림을 형성하는 함수이므로, fclose 함수는 스트림을 해제하는 함수임을 알 수 있다. fclose 함수는 다음과 같이 선언되어있다.
int fclose(FILE * stream);
//함수 호출 성공 시 0, 실패 시 EOF 반환
fclose 함수를 호출하여 스트림을 해제, 즉 열려있던 파일을 닫는 데에는 두가지 이유가 있다. 운영체제가 할당한 자원을 반환하면서 버퍼에 임시 저장되었던 데이터를 출력하는 것이다.
스트림을 형성할 때 운영체제가 할당하는 메모리 등의 자원은 파일을 닫지 않으면 계속 남아있으므로 자원 손실을 야기할 수 있어 fclose를 통해 자원을 반환하는 것이다.
콘솔 스트림에 대해 공부했을 때
(참고 : https://velog.io/@aiden_lee/CString-Handling#:~:text=Stream,standard%20stream, https://velog.io/@aiden_lee/CString-Handling#:~:text=Standard%20I/O,%EB%8D%94%20%EB%B9%A0%EB%A5%B4%EA%B3%A0%20%ED%9A%A8%EC%9C%A8%EC%A0%81%EC%9D%B4%EB%8B%A4.), 콘솔 스트림의 중간에 존재하는 입력 버퍼와 출력 버퍼에 대해 알아봤었다. 파일 스트림에도 중간에 입력 버퍼와 출력 버퍼가 존재한다.
이 때, 버퍼링 방식에서 문제가 발생하는데, 어떠한 문자가 출력 버퍼에 존재하는 상태(파일이 저장되기 직전의 상태)에서 컴퓨터의 전원이 꺼진다면 문자는 파일에 저장되지 않는다. fclose 함수의 호출을 통해 파일을 닫아준다면 출력 버퍼에 저장되어있던 데이터가 파일로 이동하면서 버퍼는 비워지게 된다. 따라서 사용이 끝난 파일은 바로 fclose 함수를 호출해주는 것이 좋다.
스트림을 종료하지 않고 버퍼만 비우고 싶을 때 fflush 함수를 호출하면 된다. 함수 선언은 다음과 같다.
#include <stdio.h>
int fflush(FILE * stream);
fflush 함수는 출력 버퍼를 비우는 함수이며 출력 버퍼를 비운다는 것은 버퍼에 저장된 데이터를 목적지로 전달한다는 것이다. 입력 버퍼를 대상으로는 이 함수를 호출할 수 없으며 입력 버퍼를 비운다는 것은 버퍼의 데이터를 소멸시킨다는 것이다.
출력 버퍼를 비우는 함수 fflush를 호출한다면 출력버퍼에 저장된 데이터는 파일로 전달되어 저장된다.
fopen 함수에서 두 번째 인자로 모드를 전달하여 스트림을 형성했다. 스트림의 종류는 여러가지로, 어떤 스트림을 형성할 지 결정하는 두 기준이 있다.
- 읽기 / 쓰기
- 텍스트 데이터 / 바이너리 데이터
읽기 위한 스트림, 쓰기 위한 스트림 등 데이터의 이동 방향을 기준으로 스트림을 구분할 수 있다.
+가 붙은 모드는 읽기, 쓰기가 모두 가능한 모드이다. 하지만 이러한 모드를 선택했을 때 읽기/쓰기 변환 시 불편함이 발생하므로 r, w, a 모드 중 하나를 고르는 것이 좋으며 일반적인 방식이다.
문자 데이터를 담고 있는 파일을 텍스트 파일, 0과 1의 이진 정보로 구성된 영상, 음성, 이미지 파일과 같이 텍스트 파일이 아닌 파일을 바이너리 파일이라 한다.
개행은 그 자체로 데이터라 보기는 어려우며, C언어에서 개행을 \n
으로 표현하지만 운영체제에 따라 개행을 표시하는 방식이 다르다.
따라서 개행의 표시 방식이 C언어와는 다른 운영체제에서는 다음과 같은 변환이 필요하다.
이 때, 파일을 텍스트 모드로 열면 이러한 변환이 자동으로 이뤄진다. 텍스트 모드로 파일을 열려면 fopen 함수의 두 번째 인자로 다음 중 하나를 전달해야 한다.
rt, wt, at, r+t, w+t, a+t
바이너리 파일의 경우 변환이 일어나면 안되기 때문에 바이너리 모드로 파일을 열어야하고, 이를 위해 fopen 함수의 두 번째 인자로 다음 중 하나를 전달해야 한다.
rb, wb, ab, r+b, w+b, a+b
문자열 처리에서 공부한 함수들을 다시 보자.
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을 인자로 전달해 키보드와 모니터를 대상으로 입출력을 했었다. 하지만 인자로 파일 구조체의 포인터를 전달해 파일 대상의 입출력을 할 수 있다.
#include <stdio.h>
int feof(FILE *stream);
//파일의 끝에 도달한 경우 0이 아닌 값 반환
문자 단위 파일복사
문자열 단위 파일 복사
다음은 바이너리 데이터 입력에 사용되는 fread 함수이다.
#include <stdio.h>
size_t fread(void *buffer, size_t size, size_t count, FILE *stream);
//성공 시 count, 실패 또는 파일의 끝 도달 시 count보다 작은 값 리턴
fread 함수는 다음과 같이 호출된다.
다음은 바이너리 데이터 출력에 사용되는 fwrite 함수이다.
#include <stdio.h>
size_t fwrite(const void *buffer, size_t size, size_t count, FILE *stream);
아래와 같이 호출한다.
fscanf 함수와 fprintf 함수를 이용하여 서식에 따른 데이터 입출력을 할 수 있다.
바이너리 데이터와 텍스트 데이터를 구조체로 묶어 구조체 변수의 입출력을 하는 방법이 있다.
파일의 중간이나 마지막 부분의 일부만을 읽어야 되는 경우가 생긴다. 파일은 항상 앞에서부터 읽는 것이 아니라 임의 접근 가능하다.
파일 위치 지시자를 이동시키면 된다.
FILE 구조체의 멤버 중 파일의 위치 정보를 저장하고 있는 멤버가 존재한다. 이 멤버 변수의 값은 fgets, fputs, fread, fwrite와 같은 입출력 함수가 호출될 때마다 참조, 갱신된다.
파일 위치 지시자는 파일이 처음 열리면 파일의 맨 앞을 가리키게 되어있다. 따라서 맨 앞부분이 아닌 부분에 접근하고 싶다면 파일 위치 지시자를 원하는 위치에 옳기면 된다.
다음 함수의 호출을 통해 파일 위치 지시자의 위치를 옮길 수 있다.
#include <stdio.h>
int fseek(FILE *stream, long offset, int wherefrom);
//함수 호출 성공시 0, 실패 시 0이 아닌 값 반환
함수의 세 개 인자 중 wherefrom은 시작점, offset은 이동할 거리를 의미한다.
여기서 파일의 맨 끝은 마지막 데이터가 아닌 EOF을 의미한다.
offset에 양의 정수가 전달되면 파일의 뒤쪽으로 이동하지만, 음의 정수가 전달되면 앞쪽으로 이동한다.
다음 함수를 호출하면 현재 파일 위치 지시자의 위치를 알 수 있다.
#include <stdio.h>
long ftell(FILE *stream);
//파일 위치 지시자의 위치 정보 리턴
이 함수는 파일 맨 앞의 위치를 0 바이트로 고려하여 파일 위치를 리턴한다.