C의 입출력 라이브러리는 표준 라이브러리 중에서 가장 중요하고 거대한 라이브러리다. 따라서 이번 문서에서는 <stdio.h>를 다루는데 대부분의 분량을 할애할 것이다.(다만 perror 과 같은 함수는 뒤에서 다룰 것이다.)
스트림(stream) 이란 입력의 출처 혹은 출력의 목적지(destination)을 말한다. 큰 프로그램들은 추가적인 스트림을 필요로 한다. 이 스트림들은 종종 다양한 미디어에 저장된 파일들을 나타낸다. 하지만 기기에 한정되지 않고 네트워크 포트, 프린터와 같은데서도 사용된다. 여기서는 파일에 주목할 것이다.(이게 좀 더 흔하기도 하고, 이해도 쉽다.)
C 프로그램에서는 파일 포인터(FILE * 형)를 통해 스트림에 접근한다. 특정 스트림은 표준 이름을 통해 파일 포인터로 표현된다. 우리는 추가적인 파일 포인터를 필요에 따라 선언할 수 있다. 예를 들어
FILE *fp1, *fp2;
같이 말이다. 프로그램은 FILE * 변수를 선언하는데 몇 개를 선언해도 제한이 없다. 운영체제가 일반적으로 한번에 열 수 있는 스트림의 수를 제한하는 것과 다르게 말이다.
<stdio.h>는 세 가지 표준 스트림을 지원한다. 이 스트림들은 우리가 선언하지 않아도 사용할 수 있으며, 우리가 열고 닫을 수가 없다.
| File Pointer | Stream | Default Meaning |
|---|---|---|
| stdin | Standard input | Keyboard |
| stdout | Standard oupput | Screen |
| stderr | Standard error | Screen |
이 것들은 기본 설정이다. 하지만 많은 운영체제들은 이 Default Meaning을 소위 리디렉션을 통해 변경할 수 있다.
< 문자를 커맨드 라인에 입력하여 표준 입력이 아닌 파일을 통해 입력을 받도록 바꿀 수 있다.
demo < in.dat
여기서 demo 프로그램은 in.dat 파일로부터 표준 입력을 받게된다. demo 프로그램은 파일로부터 입력을 받는다는 사실을 모른다.
마찬가지로 >를 통해 출력 리디렉션을 할 수 있다.
demo >out.dat
이제 stdout을 통한 출력은 전부 out.dat에서 이뤄질 것이다.(화면에 출력되지는 않는다.) 입출력 리디렉션을 다음과 같이 결합 할 수도 있다.
demo <in.dat >out.dat
여기서 리디렉션 문자가 파일 이름 바로 옆에 붙을 필요도 없으며 순서도 상관없다.
<stdio.h>는 텍스트, 바이너리 파일을 지원한다. 텍스트 파일은 사람이 이해할 수 있는 문자로 구성되어 있으며, C 코드는 텍스트 파일에 저장된다. 반면 바이너리 파일에서는 바이트는 더이상 문자로 표현되지 않으며, 다른 종류의 데이터를 나타낸다. 실행가능한 C 프로그램은 바이너리 파일로 저장된다.(이미 알다시피)
텍스트 파일은 바이너리 파일이 가지지 않는 두 가지 특징을 갖는다.
(1) 텍스트 파일은 줄로 나뉘어 있다.
(2) 텍스트 파일은 파일의 끝부분을 나타내는 표식을 갖는다.(UNIX에서는 가지지 않는다.)
바이너리 파일은 줄로 나뉘지 않으며, 끝을 나타내는 표식도 없다.
우리가 파일에 데이터를 작성할 때, 우리는 텍스트 혹은 바이너리 형식으로 작성 할 지를 결정해야한다. 왜냐하면 32767을 텍스트로 작성하는 것과 바이너리로 작성하는데 필요한 바이트 크기가 다르기 때문이다.(5byte vs 2byte)
그 밖에도 프로그램은 파일의 내용을 나타낼때 텍스트 파일로 간주한다. 반대로 파일 복사 프로그램은 텍스트 파일로 간주하지 않는다.
FILE *fopen(const char * restrict filename,
const char * restrict mode)
파일을 스트림으로 사용하기 위해선 fopen 함수를 호출해야한다. fopen의 첫 입력변수는 열어야 하는 파일의 이름을 포함한 스트링이다.(여기에는 파일의 경로가 포함되기도 한다.) 두 번째 입력변수는 mode string으로, 우리가 파일에 대해 수행하고 싶은 연산을 정의한다. r은 데이터가 파일로부터 읽히지만, 아무것도 파일에 적히지 않는다.
restrict 가 두 번이나 함수에 등장함에 주의하라. restrict는 filename과 mode가 반드시 메모리 주소를 공유해서는 안됨을 지시한다.
fopen은 파일 포인터를 반환한다. 일반적인 사용 예시는 다음과 같다.
fp = fopen("in.dat", "r");
프로그램이 입력 함수를 in.dat을 읽기 위해 호춣한 경우에, fp는 입력변수로 쓰일 수 있다.
파일을 열 수 없는 경우에 fopen은 널 포인터를 반환한다.
우리가 fopen에 전달하느 모드 스트링은 우리가 사용하려는 연산 외에도 파일이 어떤 데이터를 포함하는지에 따라 달라지기도 한다.
| String | Meaning |
|---|---|
| r | Open for reading |
| w | Open for writing(file need not exist) |
| a | Open for appending(file need not exist) |
| r+ | Open for reading and writing. starting at beginning |
| w+ | Open for reading and writing.(truncate if file exists) |
| a+ | Open for reading and writing.(append if file exists) |
만약 바이너리 파일에 fopen을 사용하는 경우, 반드시 b 문자를 모드 스트링에 포함해야 한다.
특별한 규칙이 파일을 열 때 적용된다. 읽기+쓰기 모드를 쓰는 경우에 말이다. 우리는 file-positioning 함수 호출 없이 읽기에서 쓰기로 전환 할 수 없다.(읽기 연산이 파일의 끝에 도달하기 전에는) 그 반대도 마찬가지다.(fflush가 있는데 이는 뒤에서 다룬다.)
int fclose(FILE *stream);
fclose 함수는 프로그램이 더이상 사용하지 않는 파일을 닫을 수 있게 만든다. fclose 입력변수는 반드시 fopen 혹은 freopen에 의한 파일 포인터여야만 한다. fclose는 성공적으로 파일을 닫으면 0을 반환한다. 아니라면 EOF 에러코드를 반환한다.(<stdio.h>에 정의된 매크로다.)
FILE *freopen(const char * restrict filename,
const char * restrict mode,
FILE * restrict stream);
freopen는 다른 파일을 이미 열려있는 스트림에 첨부한다. 흔히, 표준 스트림에 파일을 연결할 때 사용한다.
C99에선 새로운 변형이 추가되었다. 만약 filename이 널 포인터인 경우, freopen은 스트림의 모드를 mode 매개변수로 바꿔버린다.
우리가 파일을 열어야하는 프로그램을 작성할 때, 한 가지 문제점이 있다. 바로 어떻게 파일 이름을 프로그램에 전달하느냐다. 이미 해본것처럼 다음과 같이 할 수도 있다.
int main(int argc, char *argv[])
{
...
}
argc는 명령줄 입력변수의 갯수고, argv는 입력 스트링의 포인터를 담는 배열이다.
FILE *tmpfile(void);
char *tmpnam(char *s);
우리는 종종 프로그램이 실행되는 동안만 존재하는 임시파일을 필요로 한다. tmpfile은 임시 파일을 "wb+"모드로 생성한다. tmpfile 호출은 파일 포인터를 반환한다.
FILE *tempptr;
...
tempptr = tmpfile();
파일 생성에 실패하는 경우 널 포인터를 반환한다.
tmpfile은 사용하기 쉽지만, 두 가지 단점을 가진다. (1) 생성된 임시 파일의 이름을 알 수 없다는 점과 (2) 나중에 해당 파일을 영구적으로 저장하는 것으로 바꿀 수 없다는 점이다.
이를 해결하기 위해 fopen으로 임시 파일을 생성할 수 있다. 하지만 이전 파일과 같은 이름을 쓰면 안된다. 이 때 tmpnam이 등장한다.
tmpnam은 임시 파일의 이름을 생성한다. 만약 입력변수가 널 포인터인 경우, tmpnam은 파일 이름을 static 변수에 저장하고 포인터를 반환한다.
char *filename;
...
filename = tmpnam(NULL);
tmpnam은 파일 이름을 문자 배열에 복사하한다.
char filename[L_tmpnam];
...
tmpnam(filename);
후자의 경우 tmpnam은 배열의 첫 문자에 포인터를 반환한다. L_tmpnam은 <stdio.h>에서 정의된 매크로로서, 임시 파일 이름을 담는 문자 배열의 길이를 결정한다.
int fflush(FILE *stream);
void setbuf(FILE * restrict stream,
char * restrict buf);
int setvbuf(FILE * restrict stream,
char * restrict buf,
int mode, size_t size);
디스크 드라이브를 통한 데이터 이동은 상대적으로 속도가 느리다. 그 결과 프로그램이 읽기/쓰기가 필요할 때마다 디스크 드라이브에 접근하는 것은 불가능했다. 하지만 이를 가능케 하는 것이 바로 버퍼링(buffering)이다. 스트림에 쓰인 데이터는 메모리의 버퍼 영역에 저장된다. 그리고 가득차거나 스트림이 종료되면, 버퍼는 실제 출력 장치로 "비워"진다. 입력 스트림도 비슷한 방식이 적용될 수 있다.
버퍼링을 통해 상당한 효율성을 달성할 수 있다. 왜냐하면 바이트 단위로 버퍼를 통해 읽고 쓰는데 걸리는 시간이 거의 없기 때문이다. 물론 디스크로부터 혹은 디스크로 데이터를 이동하는데 시간이 걸리지만 바이트 단위로 하는 것보다 블록 단위로 한번에 이동시키는 것이 훨씬 빠르다.
<stdio.h>의 함수는 자동적으로 버퍼링을 수행한다. 따라서 평소에 우리가 이를 신경써줄 필요는 없다. fflush를 통해 우리가 원할때, 버퍼를 비워줄 수도 있다.
fflush(fp);
다음과 같은 경우.
fflush(NULL);
출력 스트림 전부를 비워버린다. ffulsh는 버퍼를 성공적으로 비우면 0을 반환하고, 실패하는 경우 EOF를 반환한다.
setvbuf는 스트림이 버퍼에 저장되는 방식을 변경하고, 버퍼의 크기와 위치를 정할 수 있게 만들어준다. 이 함수의 세 번째 입력변수는 버퍼의 종류를 정의할 수 있게 만들며 다음 세 매크로 중 하나가 들어가야 한다.
_IOFBF(full buffering)
_IOLBF(line buffering)
_IONBF(no buffering)
상호 작용 기기에 연결되어있지 않은 스트림의 경우 full buffering이 기본 설정이다.
setvbuf의 두 번째 입력변수는(널 포인트가 아닌한) 의도된 버퍼의 주소다. 버퍼는 정적 스토리지-듀레이션, 동적 스토리지-듀레이션을 가질 수 있고, 동적으로 할당이 이뤄질 수도 있다. setvbuf의 마지막 입력변수는 버퍼의 바이트 크기를 정한다. 버퍼가 크면 성능이 좋아진다.
예를 들어 다음의 setvbuf 호출은 stream의 버퍼링을 full buffering, N바이트의 버퍼로 변경한다.
char buffer[N];
...
setvbuf(stream, buffer, _IOFBF, N);
주의: setvbuf는 반드시 stream을 연 뒤에, 그리고 다른 연산이 수행되기 전에 호출되어야 한다.
setvbuf에 널 포인터를 두 번째 입력변수로 넣는것도 허용된다. 이는 setvbuf로 하여금 특정 크기의 새로운 버퍼를 생성하게 만든다. setvbuf는 성공적으로 작동하면 0을 반환한다. 그 밖의 경우 0이 아닌 값을 반환한다.
setbuf는 버퍼링 모드와 사이즈를 기본으로 가정하는 오래된 함수다. 만약 buf가 널 포인터인 경우 setbuf(stream, buf) 호출은 다음과 동일하다.
(void) setvbuf(stream, NULL, _IONBF, 0);
다르게는 아래와 같이 표현할 수 있다.
(void) setvbuf(stream, buf, _IOFBF, BUFSIZ);
BUFSIZ는 <stdio.h>에 정의된 매크로다. setbuf함수는 사실 쓸모가 없으므로 사용하지 말 것을 권장한다.
주의: setvbuf를 사용할 때, 반드시 버퍼 할당이 해제되기 전에 스트림을 닫아야한다. 튻히 버퍼가 함수에 대해 (to a function) 국부적인(local) 경우, 그리고 자동 스토리지 듀레이션인 경우에 함수 반환이 이뤄지기 전에 스트림을 닫아야한다.
int remove(const char *filename);
int rename(const char *old, const char *new);
위 함수들은 파일을 삭제하고, 이름을 변경할 때 사용된다. 만약 프로그램이 임시파일을 생성하기 위해 fopen을 사용하는 경우, remove를 통해 프로그램 종료 전에 해당 파일을 삭제할 수 있다.
rename은 임시파일의 이름을 다시 지을때 유용하다.
주의: 만약 열려있는 파일의 이름을 다시 짓는경우, 파일을 먼저 닫아야한다.
여기서는 포맷 스트링을 통해 읽기와 쓰기를 통제하는 라이브러리 함수를 살펴볼 것이다. 이 함수들은 문자 형태와 숫자 형태 간으로 데이터를 서로 전환 할 수 있다. 다른 입출력 함수들은 이런거 못한다.
int fprintf(FILE * restrict stream, const char * restrict format, ...);
int printf(const char * restrict format, ...);
fprintf와 printf는 포맷 스트링을 통한 출력 형식을 제어함으로써, 가변 데이터를 출력 스트림에 작성할 수 있다.
printf와 fprintf의 차이점은 printf는 항상 표준출력(stdout)에 쓰기를 수행하는 반면, fprintf는 첫 입력변수가 가리키는 스트림에 쓰기를 수행한다.
printf("Total: %d\n", total);
fprintf(fp, "Total: %d\n", total);
printf 호출은 fprintf 에 stdout이 입력변수로 주어진 경우와 그 효과가 동일하다.
fprintf가 단순히 디스크 파일에 데이터를 쓴다고 생각하지 않았으면 좋겠다. <stdio.h>의 수많은 다른 함수들 처럼, fprintf는 다른 출력 스트림에 대해서도 작동한다. 사실 fprintf가 쓰이는 대부분의 경우는 , 에러 메시지를 stderr에 쓰는 경우다.
fprintf(stderr, "Error: data file can't be opened.\n");
sterr에 메시지를 쓰는 것은 stdout이 리디렉션 되더라도, 화면에 에러 메시지가 출력되는 것을 보장해준다.
<stdio.h>에는 포맷 출력을 스트림에 작성하는 다른 두 함수가 존재한다. 이들은 vprintf와 vfprintf다. 이 둘은 다른 헤더와 연관이 있어서, 뒤에서 다룰 예정이다.
knk에서 좀 더 자세하게 다루고 있으나, 간단한 버전을 원한다면 이전에 작성한 글을 참고하면 된다.
더 자세한 내용은 여기를 참고해도 된다.
int fscanf(FILE * restrict stream,
const char * restrict format, ...);
int scanf(const char * restrict format, ...);
fscanf와 scanf는 입력 스트림으로부터 입력 레이아웃을 가리키는 포맷 스트링을 사용하여 데이터를 읽어들인다. 포맷 스트링 뒤에는 다른 입력변수들이 추가적으로 입력된다. 입력은 포맷 스트링의 변환 서식에따라 변환되고 입력 변수들에 저장된다.
scnaf는 항상 stdin을 통해 읽어들인다.
scanf('%d%d", &i, &j);
fscanf(fp, "%d%d", &i, &j)l
scanf 호출은 fscanf에 stdin을 입력변수로 하여 호출하는 것과 동일하다. scanf 함수는 더 이상 읽을 수 있는 문자가 없거나(input falilure), 입력문자가 포맷스트링에 알맞지 않은 경우(matching failure) 조기에 반환이 이뤄진디. C99에선 input failure가 encoding error에 의해 일어나기도 한다. 두 함수는 읽어들인 데이터의 갯수를 반환하며, 이를 객체에 할당한다.(실패한 경우 EOF를 반환한다.)
scanf의 반환 값을 검사하는 루프는 C에서 쉽게 찾아볼 수 있다. 아래의 루프는, 일련의 정수를 하나씩 읽고 문제가 생긴 시점에서 멈춘다.
while (scanf("%d", &i) == 1) {
...
}
scanf 호출은 printf 호출과 비슷한 부분이 많다. 하지만 이는 오해를 낳을 수 있다. scanf 함수는 printf와 상당히 다른 방식으로 작동한다. scanf와 fscanf는 "pattern-matching" 함수다. 포맷 스트링은 scanf가 입력을 받아들이는 패턴을 나타내고 이는 일치해야한다. 만약 입력이 포맷 스트링 패턴과 일치하지 않음을 감지하면 그 즉시 반환이 이뤄진다. 그리고 패턴과 맞지 않은 문자는 "뒤로 밀리게" 된다.
scanf 포맷 스트링은 다음과 같은 세 가지를 포함한다.
scanf의 변환 서식은 printf와 비슷해보인다. 대부분의 변환 서식은 시작 지점의 공백 문자를 건너뛴다. 하지만 변환 서식은 절대로 'trailing' 공백문자를 건너뛰지 않는다.scanf의 변환 서식은 printf의 그것보다 좀 더 간단하다. scanf 변환 서식은 % 문자와 다른 문자의 조합으로 이뤄진다.
이 문서를 참고하면된다.
void clearerr(FILE *stream);
int feof(FILE *stream);
int ferror(FILE *stream);
scanf 함수를 통해 n개를 저장하려고 할 때 우리는 n을 반환받는다. 만약 반환 값이 n보다 작다면 뭔가 문제가 있는 것이다. 여기엔 세 가지 가능성이 있다.
(1) EOF
(2) Read error
(3) Matching Failure
어떤 실패/오류가 발생했는지 알 수 있을까? 많은 경우에 이는 문제가 되지 않는것이, 만약 문제가 생기면 프로그램을 버리면(abondon) 된다. 하지만 실패 원인을 정확하게 짚어야만 할 때가 있다.
모든 스트림은 이것과 관련하여 두 indicator를 가진다. 바로 error indicator와 end-of-file indicaotr다. 이 indicaotr들은 스트림이 열릴때 초기화된다. EOF는 end-of-file indicator를 설정하며, read error은 error indicator를 설정한다. matching failure는 어떤 indicaor도 설정하지 않는다.
한번 end-of-file indicator가 설정되면, 그 상태는 그대로 유지되며, clearerr 함수 호출이 이뤄질때까지 말이다. clearerr 함수는 두 indicator를 초기화 해준다.
clearerr(fp);
clearerr는 그렇게 자주 필요하지 않다. 왜냐하면 다른 라이브러리 함수들을 통해서도 indicator를 변경할 수 있기 때문이다.
우리는 feor와 ferror 함수를 통해 스트림의 indicator들을 왜 앞선 스트림 연산이 실패했는지 indicator를 검사할 수 있다. feor(fp) 는 end-of-file indicator가 설정되면 0이 아닌 값을 반환한다. ferror(fp)는 error indicator가 설정되어 있으면, 0이 아닌 값을 반환한다. 그 외의 경우 두 indicator 모두 0을 반환한다.
scanf가 기대보다 작은 값을 반환하는 경우 우리는 feof와 ferror를 통해 이유를 찾을 수 있다. (둘 중 하나가 0이 아니면 해당 함수에 대응하는 에러가 원인이고, 둘 다 0이라면 mismatching이 원인이다.)
여기선 단일 문자 읽기/쓰기 라이브러리 함수들에 대해 알아볼 것이다. 이 함수들은 텍스트 스트림과 바이너리 스트림에서 동일하게 작동한다. 여기서 소개하는 함수들은 문자를 정수처럼 취급한다.
int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
putchar는 stdout 스트림에 문자 하나를 쓴다.
putchar(ch);
fputc와 putc는 putchar을 임의 스트림에 사용하는 버전이다.
fputc(ch, fp);
putc(ch, fp);
이 두 개가 동일한 것으로 보이지만, putc는 fputc가 함수로서 기능하는 것에 반해 매크로로 구현된다. putchar 그 자체는 보통 다음과 같은 방식으로 매크로 정의가 이뤄진다.
#define putchar(c) putc((c), stdout)
굳이 putc와 fputc 이렇게 두 개가 있어도 될 필요가 있을까 싶다. 하지만 매크로는 몇가지 잠재적인 문제점을 가지고 있다. C 표준은 putc 매크로를 통해 스트림 입력면수를 한 번 더 검사한다.(fputc는 이렇게 검사할 수가 없다.)
만약 쓰기 에러가 발생하는 경우. 세 함수들이 error indicator을 스트림에 대해 설정하고 EOF를 반환한다.
int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);
int ungetc(int c, FILE *stream);
getchar는 stdin 스트림에서 문자 하나를 읽어들인다.
ch = getchar();
fgetc와 getc는 임의 스트림에서 문자 하나를 읽어들인다.
ch = fgetc(fp);
ch = getc(fp);
세 함수 모두 문자를 unsigned char로 다룬다. 그 결과,EOF 가 아니면 절대 음수를 반환하지 않는다.
getc와 fgetc의 관계는 putc와 fputc의 관계와 유사하다. 전자는 매크로로 구현되고, 후자는 함수로서 기능한다. getchar는 다음과 같이 매크로 정의된다.
#define getchar() getc(stdin)
파일에서 문자를 읽어들이기 위해, 프로그래머들은 fgetc보다 getc 사용을 선호한다. getc가 일반적으로 매크로 형태이기에 좀 더 빠르다. fgetc는 getc가 적절하지 않을때 사용한다.
fgetc 와 getc 그리고 getchar 함수는 문제가 발생하는 경우 똑같이 indicator들을 설정한다.
에러를 감지하기 위해 다음과 같이 함수를 사용하낟.
while ((ch = getc(fp)) != EOF) {
...
}
읽기를 마치고 변수에 저장이 이뤄진뒤, while문은 ch와 EOF를 비교하여 검사를 수행한다.
입력 함수로 ungetc 함수도 있다. 이는 스트림에서 읽어들인 문자를 뒤로 밀어내며, 스트림의 end-of-file indicator를 초기화한다. 이는 입력 중 문자를 미리 보아야 할 때 유용하다. 예를 들어 일련의 숫자들을 읽을때, 숫자가 아닌 지점에서 멈출것이다. 우리는 다음과 같이 쓸 수 있다.
while (isdigit(ch = getc(fp)) {
...
}
ungetc(ch, fp)
ungetc의 연속 호출로 여러 문자를 뒤로 밀어둘 수 있으나, 이는 구현과 스트림의 종류에 의존한다. 오직 첫 번째 호출만이 성공을 보장한다. 파일 포지셔닝 함수는 미뤄둔 문자들을 유실시킨다.
int fputs(const char * retrict s,
FILE * restrict stream);
int puts(const char *s);
puts는 이미 앞서 만난 적이 있다.
puts("Hi, there!");
puts는 문자열을 출력하고 항상 개행문자를 마지막에 출력한다.
fputs는 puts의 일반화 버전이다. 두 번째 입력면수가 스트림이 작성될 곳을 가리킨다.
fputs("Hi, there!", fp);
puts와 달리 fputs는 개행 문자를 마지막에 붙이지 않는다. 만약 에러가 발생하는 경우 , 두 함수 모두 EOF를 반환한다. 에러가 없는경우, 0이 아닌 값을 반환한다.
char *fgets(char * restrict s, int n,
FILE * restrict stream);
char *gets(char *s);
gets 함수는 이미 앞서 본 적이 있다. stdin으로부터 한 줄을 입력받는다.
gets(str);
gets는 문자를 하나씩 읽으며, str이 가리키는 배열에 개행문자를 읽을때까지 저장한다.(개행문자는 버려진다.)
fgets는 gets의 일반화 버전으로, 입력될 문자의 수를 제한한다.
fgets(str, sizeof(str), fp);
fgets 호출은 gets와 달리 개행문자를 감지하면, 개행문자도 배열에 저장할 때가 있다. 두 함수는 read error가 발생하면 널 포인터를 반환하며, 문자를 저장하기 전에 입력 스트림 끝에 도달해도 널 포인터를 반환한다. 성공하면 첫 입력변수를 반환한다. 알다시피 두 함수 모두 널 문자를 문자열 끝에 저장한다.
되도록이면 fgets를 쓰는 것이 좋다. 왜냐하면 입력 수를 제한 할 필요가 많기 떄문이다.
size_t fread(void * restrict ptr,
size_t size, size_t nmemb,
FILE * restrict stream);
size_t fwrite(const * restrict ptr,
size_t size, size_t nmemb,
FILE * restrict stream);
fread와 fwrite 함수는 프로그램이이 큰 블록의 데이터를 한 번에 읽고 쓸 수 있게 만들어 준다. 일반적으로 바이너리 스트림에 사용되기는 하나, 텍스트 스트림에도 사용될 수 있다.
fwrite는 메모리에서 스트림으로 배열을 복사하기 위해서 설계되었다. 첫 입력변수는 배열의 주소다. 두 번째 입력변수는 배열의 크기다. 세 번째 입력변수는 작성할 원소의 수다. 네 번째 입력변수는 파일 포인터로, 데이터가 작성될 위치를 가리킨다. 배열 a 전부를 작성하기 위해 우리는 다음과 같이 함수를 쓸 수 있다.
fwrite(a, sizeof(a[0]), sizeof(a)/sizeof(a[0]), fp);
이것 말고도 다르게 작성해도 된다. fwrite는 작성이 이뤄진 원소의 갯수를 반환한다. 그 수가 기대보다 적다면 에러가 있는것이다.
fread는 스트림에서 배열의 원소를 읽어들인다. fwrote와 입력변수 구성이 비슷하다.
n = fread(a, sizeof(a[0]), sizeof(a)/sizeof(a[0]), fp);
fread의 반환 값은 실제로 읽어들인 원소의 갯수를 의미하므로 그 값을 검사하는 것은 매우 중요하다.
int fgetpos(FILE * restrict stream,
fpos_t * restrict pos);
int fseek(FILE *stream, long int offset, int whence);
int fsetpos(FILE *stream, const pfos_t *pos);
long int ftell(FILE *stream);
void rewind(FILE *stream);
모든 스트림은 file position과 관련있다. 파일이 열리면, 파일 포지션은 파일의 시작부분에 설정된다.(하지만 append 모드라면 끝 부분에 설정될 수도 있다.) 그리고 읽기나 쓰기 연산이 수행되면 파일 포지션은 자동적으로 순차 이동한다.
많은 응용에서 순차적으로 이동하는 것은 괜찮지만, 몇 프로그램은 어떤 구간을 건너뛰어야 할 때가 있다. <stdio.h>는 이를 다섯가지 함수를 통해 지원한다.
fseek 함수는 파일 포지션을 첫 입력변수의 위치(파일의 시작지점)으로 위치시키며, 세 번째 입력변수는 시작지점으로 부터 오프셋을 더한 위치를 정한다.(혹은 현재 위치 또는 파일의 끝 부분일 수 있다.) 이를 위한 매크로가 세 개 있다.
SEEK_SET Beginning of file
SEEK_CUR Current file position
SEEK_END End of file
두 번째 입력변수는 바이트 카운트다(음수일 수도 있다.) 파일의 시작지점으로 이동하기 위해, 탐색 방향은 SEEK_SET이고 바이트 카운트는 0이 될 것이다.
fseek(fp, 0L, SEEK_SET);
파일 끝 부분으로 이동하기 위해선
fseek(fp, 0L, SEEK_END);
10 바이트 뒤로 이동하기 위해선
fseek(fp, -10L, SEEK_CUR);
에러가 발생하면 0이 아닌 값을 반환한다.
fseek은 바이너리 스트림 뿐만 아니라 텍스트 스트림에서도 쓰일 수 있으나 굉장한 주의를 요구한다. (1) 오프셋은 반드시 0이어야 하고, (2) 세 번째 입력변수는 반드시 SEEK_SET이며, 오프셋은 ftell에 의한 값이어야 한다.
ftell 함수는 현재 파일 포지션을 long int로 반환한다. 에러가 발생하면 -1L을 반환한다. 만약 텍스트 스트림이 입력변수로 주어진 경우, 오프셋이 바이트 단위로 주어지지 않는다. 따라서 이에 대해 산술 연산을 수행하는 것은 바람직하지 않다.
rewind 함수는 파일포지션을 시작지점으로 설정한다. 앞에서 본 fseek 과 뭐가 다를까 싶을텐데, rewind는 반환을 수행하지 않지만, error indicator를 초기화한다.
fseek과 ftell은 문제점을 하나 가진다. 이 둘은 파일 포지션을 long int로 나타낼 수 있는 파일만을 다룰 수 있다. 매우 큰 파일의 경우, C는 두 함수를 추가적으로 지원한다. 바로 fgetpos와 fsetpos다. 이 함수들은 매우 큰 파일을 다룰 수 있다. 왜냐하면 fpos_t 가 파일 포지션을 나타내기 때문이다. fpos_t는 반드시 정수여야 할 필요가 없다. 구조체일 수도 있다. 예를들어
fgetpos(fp, &file_pos)가 file_pos에 파일 포지션을 저장한다고 해보자. fsetpos(fp, &file_pos)는 파일 포지션을 file_pos에 저장된 포지션으로 설정한다.
만약 식패한다면 errno에 에러 코드를 저장한다. 성공하면 0을 반환하며, 실패하면 0이 아닌 값을 반환한다. 사용 예시는 다음과 같다.
fpos_t file_pos;
...
fgetpos(fp, &file_pos);
...
fsetpos(fp, &file_pos);
이 문서를 참고하라.