계산기 프로그램과 같이 단순 질문(입력)에 답변(출력)만 하는 프로그램의 경우 데이터를 따로 저장하진 않는다.
다만, 우리가 일반적으로 접하는 거의 대부분의 프로그램에서는 다양한 형태와 종류의 데이터를 저장하고 잇다.
파일을 대상으로 데이터를 입력하고 또 출력할 수 있어야 프로그램다운 프로그램이다.
프로그램상에서 파일에 저장되어 있는 데이터를 참조하길 원한다(읽기 원한다)면 프로그램과 참조할 데이터가 저장되어 있는 파일 사이에 데이터가 이동할 수 있는 다리를 놓아야한다.
이때, 데이터 이동의 경로가 되는 다리를 가리켜 스트림(stream)
이라 한다.
파일과의 스트림 형성은 데이터 입출력의 기본이다.
파일은 운영체제에 의해서 그 구조가 결정되고 관리되는 대상이기 때문에 파일 뿐만 아니라 스트림의 형성도 운영체제의 몫이다.
위 사진 속 문장은 스트림을 형성할 때 호출하는 fopen
함수다.
이 함수의 호출을 통해서 프로그램상에서 파일과의 스트림을 형성할 수 있다.
위 함수의 첫 번째 인자로는 스트림을 형성할 파일의 이름을,
두 번째 인자로는 형성할 스트림의 종류에 대한 정보를 문자열의 형태로 전달한다.
따라서, 이 함수는 해당 파일과의 스트림을 형성하고 스트림 정보를 FILE 구조체 변수에 담아서 그 변수의 주소 값을 변환한다.
FILE이라는 구조체는 어떻게 정의되어 있으며 무엇에 사용되는 것일까?
사실 어떻게 정의되어 있는지는 자세히 이해할 필요는 없다. FILE 구조체의 포인터는 파일을 가리키기 위한 용도로 사용된다 정도로만 이해하면 된다. 즉, 이 포인터를 이용해서 파일에 데이터를 저장하거나 파일에 저장된 데이터를 읽게 된다.
위 그림은 프로그램상에서 fopen 함수를 호출했을 때 일어나는 일들을 그림으로 나타낸 것이다.
여기서 주목해야할 부분은 세 가지다.
스트림은 한 방향으로 흐르는 데이터의 흐름이기 때문에 데이터를 파일로부터 읽어 들이기 위한 '입력 스트림'과 데이터를 파일에 쓰기 위한 '출력 스트림'으로 구분된다.
fopen 함수
의 호출할 때는 다음 두 가지가 인자로 전달되어야 한다.
먼저 출력 스트림의 형성을 요청하는 fopen 함수
의 호출문은 다음과 같다.
FILE * fp = fopen("data.txt", "wt"); // 출력 스트림의 형성
이것은 "파일 data.txt와 스트림을 형성하되 wt 모드로 스트림을 형성해라!"라는 의미이다.
출력 스트림의 형성 결과 아래와 같이 프로그램과 파일 사이의 스트림이 형성된다.
포인터 변수 fp에 저장된 값이 data.txt의 스트림에 데이터를 전송하는 도구가 된다.
입력 스트림의 형성을 요청하는 fopen 함수
의 호출문은 다음과 같다.
FILE * fp = fopen("data.txt", "rt"); // 입력 스트림의 생성
이것은 "파일 data.txt와 스트림을 형성하되 rt 모드로 스트림을 형성해라!"라는 의미이다.
입력 스트림의 형성 결과 아래와 같이 프로그램과 파일 사이의 스트림이 형성된다.
포인터 변수 fp에 저장된 값이 data.txt의 스트림으로부터 데이터를 수신하는 도구가 된다.
참고로, fopen 함수의 호출을 통해서 파일과 스트림이 형성되었을 때 '파일이 개방(오픈)되었다'라고 일반적으로 표현한다.
스트림을 형성해서 파일에 데이터를 써보고자 하는데 이를 위해 rputc 함수
를 호출해보자.
fputc('A', fp);
예제를 통해 실제로도 파일에 데이터가 저장되는지 확인해보자.
#include <stdio.h>
int main()
{
FILE * fp = fopen("data.txt", "wt");
if(fp == NULL)
{
printf("파일오픈 실패!");
return -1; // 비정상적 종료를 의미하기 위해 -1 반환
}
fputc('A', fp);
fputc('B', fp);
fputc('C', fp);
fclose(fp); // 스트림 종료
return 0;
}
> data.txt
ABC
파일명 부분에 파일의 위치를 지정해두어도 무관하다. (예. "C:\Project\data.txt"
fclose 함수
는 fopen 함수
의 반대 기능을 제공한다.
fopen 함수
가 스트림을 형성하는 함수라면, fclose 함수
는 스트림을 해제하는 함수이다.
fclose 함수
의 호출을 통해서 개방되었던 파일을 닫아줘야 하는 이유가 뭘까? 다음 두 가지 이유가 있다.
운영체제는 스트림의 형성을 위해서 시스템의 자원(주로 메모리)을 할당해서 이 자원은 파일을 닫아주지 않으면 할당된 채로 남아있게 된다. 즉, 그 할당된 자원만큼 손실이 이뤄지기 때문에 파일의 사용이 끝나는 즉시 fclose 함수
를 호출해서 자원을 반환해줄 필요가 있다.
파일 스트림의 경우에도 중간에 입력버퍼와 출력버퍼가 존재한다.
운영체제는 위 그림과 같이 프로그램과 파일 사이에 입출력 버퍼를 둬서 성능의 향상을 도모하고 있다.
우리가 fputc
와 같은 함수의 호출로 데이터를 파일로 전송한다고 해서 파일에 바로 저장되는 것이 아니라, 일단은 출력버퍼에 저장되었다가 운영체제가 정해놓은 버퍼링 방식에 따라서 뒤늦게 파일에 저장이 된다.
fclose 함수
의 호출을 통해서 파일을 닫아주면 혹시나 버퍼링때문에 출력 버퍼에 저장되어 있던 데이터가 파일로 이동하면서 출력버퍼가 비워지게 된다.
때문에 사용이 끝난 파일은 곧바로 fclose 함수
를 호출해 주는 것이 좋다.
Chapter 21에서 fflush 함수
가 잠깐 나왔었다.
스트림을 종료하지 않고 버퍼만 비우고 싶을 때 fflush 함수
를 호출하면 된다.
Chapter 21에서 입출력 버퍼를 비우는 것과 관련해서 다음과 같이 배운 적이 있다.
출력 버퍼를 비우게 되면 출력 버퍼에 저장된 데이터가 파일 스트림을 따라 실제 파일에 저장된다.
int main()
{
FILE * fp = fopen("data.txt", "wt");
....
fflush(fp); // 출력 버퍼를 비우라는 요청
....
}
파일 스트림의 입력 버퍼를 비우는 방법은 뭘까?
사실 파일 스트림의 입력 버퍼를 비우는 함수는 필요가 없다.
파일에 저장된 데이터는 원할 때 언제든지 읽을 수 있고 읽혀진 데이터는 입력 버퍼에서 지워진다. 또한 파일 대상의 입력 버퍼를 비워야만하는 상황이 특별히 존재하지 않기 때문이다.
이번에는 위 예제에서 만든 data.txt 파일을 열어서 그 안에 저장된 문자를 읽어 들이는 프로그램을 작성해보자.
이 때 사용되느 ㄴ것이 fgetc 함수
이다.
fgetc 함수
는 파일에 저장된 문자 하나를 반환하는 함수로서 아래 예제에서 함수의 호출로 인해 FILE 구조체의 포인터 fp가 지칭하는 파일에 저장된 문자 하나가 반환되어 변수 ch에 저장된다는 사실만 기억하고 예제를 살펴보자.
#include <stdio.h>
int main()
{
int ch, i;
FILE * fp = fopen("data.txt", "rt");
if(fp==NULL)
{
puts("파일 오픈 실패!");
return -1;
}
for(i=0; i<3; i++) // 한 문자씩 출력하므로 파일 내 총 3문자 다 출력하기 위해 반복문 사용.
{
ch = fgetc(fp);
printf("%c \n", ch);
}
fclose(fp);
return 0;
}
> 출력
A
B
C
파일을 생성해서 데이터를 저장하는 방법과, 파일을 열어서 데이터를 읽는 방법 모두 살펴봤다.
이제는 스트림의 형성방법과 데이터의 입출력 방법을 알아보자.
형성할 수 있는 스트림의 종류는 굉장히 많다.
(지금까지는 "wt"와 "rt"만 사용해봤다.)
스트림은 다음 두 가지 기준으로 구분하게 된다.
기준 1. 읽기 위한 스트림인지, 쓰기 위한 스트림인지?
기준 2. 텍스트를 위한 스트림인지, 바이너리 데이터를 위한 스트림인지?
스트림을 구분하는 기준 중 1번 기준에서 데이터의 이동방향을 기준으로 스트림을 다시 네 가지로 구분할 수 있다.
c언어는 이를 바탕으로 총 6가지 스트림으로 세분화한다.
모드의 +
는 읽기, 쓰기가 모두 가능한 스트림의 형성을 의미한다.
모드의 a
는 쓰기가 간으한 스트림을 형성하는데, 여기서 말하는 쓰기는 덧붙이기이다. (append의 약어로 생각하면 좋다.)
파일에 담을 수 있는 데이터의 유형은 아래와 같다.
그렇다면 데이터의 입출력을 위해서 스트림을 형성할 때 이와 관련해서 특별히 신경 쓸 부분은 무엇일까?
바로 '문장의 끝을 의미하는 개행의 표현 방식'이다.
개행
은 일반적인 문자 데이터와 성격이 조금 다르다.
개행은 줄이 바뀌었다는 일종의 현상이지 그 자체가 하나의 데이터로 존재하는 대상은 아니다.
C언어에서는 이 개행을 \n
으로 표현하도록 했다.
이것은 C언어에서만의 약속이다.
따라서 개행의 표현은 운영체제마다 차이가 있기 때문에 개행 문자가 포함되는 텍스트 데이터의 저장에는 주의가 필요하다.
모든 운영체제에서 통일하기 위해서는 형태의 변환이 필요하다.
형태의 변환을 일일이 다 직접 하려면 귀찮다...
따라서 파일을 텍스트 모드로 개방하면 이러한 귀찮음이 사라진다!
파일을 텍스트 모드로 개방하면 바로 위에서 말한 형태의 변환이 자동으로 이뤄진다.
예를 들면 Windows를 기반으로 다음 두 가지의 변환이 자동으로 이뤄진다.
\n
을 파일에 저장하면 \r\n
으로변환되어 저장됨 \r\n
을 C 프로그램상에서 읽으면 \n
으로 변환되어 읽혀짐때문에 우리가 직접 개행 문자의 변환에 신경 쓰지 않아도 된다.
다만 텍스트 모드의 파일 개방을 위해서는 fopen 함수
의 두 번째 인자로 다음 중 하나를 전달해야 한다.
rt, wt, at, r+t, w+t, a+t
이는 아까 정리한 파일 개방 모드에 텍스트 모드를 의마하는 t
가 붙은 형태이다.
반대로 바이너리 데이터를 저장하고 있는 파일의 경우에는 이러한 형태의 변환이 일어나면 안되기 때문에 (아무런 변환이 안 일어나기 때문에) 바이너리 모드로 파일을 개방해야 한다.
이를 위해서는 fopen 함수
의 두 번째 인자로 다음 중 하나를 전달해야 한다.
rb, wb, ab, r+b, w+b, a+b
이 역시 아까 정리한 개방 모드에 바이너리 모드를 의마하는 b
가 붙은 형태이다.
참고로 아까 정리한 개방 모드에 t
도 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 * strea); // 문자열 입력
이전에는 위 함수들의 두 번째 또는 세 번째 매개변수 stream에 표준 입출력을 의미하는 stdin이나 stdout을 인자로 전달함으로써 키보드나 모니터를 대상으로 입출력을 진행했다.
그러나 매개변수의 형이 의미하듯 매개변수 stream에 FILE 구조체의 포인터를 인자로 전달하여 파일대상의 입출력을 진행할 수 있다.
예제를 통해서 위 함수들을 기반으로 파일 입출력을 진행해보자.
먼저 파일을 생성해서 문자와 문자열을 저장해보자.
#include <stdio.h>
int main()
{
FILE * fp = fopen("simple.txt", "wt");
if(fp == NULL)
{
puts("파일 오픈 실패!");
return -1;
}
fputc('A', fp);
fputc('B', fp);
fputs("My name is Kim \n", fp);
fputs("Your name is Lee \n", fp);
fclose(fp);
return 0;
}
그 다음 위 예제를 통해 생성된 파일에 데이터를 읽어보자.
#include <stdio.h>
int main()
{
char str[30];
int ch;
FILE * fp = fopen("simple.txt", "rt");
if(fp==NULL)
{
puts("파일 오픈 실패!");
return -1;
}
ch = fgetc(fp);
printf("%c \n", ch);
ch = fgetc(fp);
printf("%c \n", ch);
fgets(str, sizeof(str), fp);
printf("%s", str);
fgets(str, sizeof(str), fp);
printf("%s", str);
fclose(fp);
return 0;
}
> 출력
A
B
My name is Kim
Your name is Lee
두 예제를 통해서 주목할 사실이 하나 더 있다.
그것은 첫 번째 예제에서 출력한 문자열 끝에 \n
이 존재한다는 것이다.
문자열이 파일에 저장될 때에는 문자열의 끝을 의미하는 널 문자는 저장되지 않는다.
즉, 두 번째 예제에서는 총 두 개의 문자열을 읽어 들이기 위해서 fgets 함수
를 두 번 호출했는데, 매번 호출이 될 때마다 개행 문자를 만날 때까지 문자열을 읽어 들이게 된다.
따라서, fgets 함수
의 호출을 통해서 읽어 들일 문자열의 끝에는 반드시 \n
문자가 존재해야 한다.
때로는 문자나 문자열 단위 뿐만 아니라 파일의 마지막에 저장된 데이터까지 모두 읽어들여야 하는 상황이 존재한다.
이를 위해서 파일의 끝을 확인하는 방법이 필요하다.
다음 feot 함수
가 이러한 목적으로 정의된 함수다.
이 함수는 인자로 전달된 FILE 구조체의 포인터를 대상으로 더 이상 읽어 들일 데이터가 존재하지 않으면(파일의 끝ㄲ자ㅣ 모두 읽어 들인 상태이면) 0이 아닌 값을 반환한다.
따라서 다음 예제와 같은 파일복사 프로그램과 같이 파일의 끝을 확인해야 하는 경우에 유용하다.
#include <stdio.h>
int main()
{
FILE * src = fopen("src.txt", "rt");
FILE * des = fopen("dst.txt", "wt");
int ch;
if(src == NULL || des == NULL)
{
puts("파일 오픈 실패!");
return -1;
}
while((ch = fgetc(src)) != EOF)
fputc(ch, des);
if(feof(src)!=0)
puts("파일 복사 성공!");
else
puts("파일 복사 실패!");
fclose(src);
fclose(des);
return 0;
}
복사가 제대로 완료된다면 "파일 복사 성공!"이라는 문자열의 출력을 확인하게 된다.
(난 왜 출력이 파일 오픈 실패! 일까,,,)
다음 예제로 텍스트 파일을 복사하지만 문자가 아닌 문자열 단위로 복사다.
#include <stdio.h>
int main()
{
FILE * src = fopen("src.txt", "rt");
FILE * des = fopen("des.txt", "wt");
char str[20];
if(src == NULL || des == NULL)
{
puts("파일 오픈 실패!");
return -1;
}
while(fgets(str, sizeof(str), src)!=NULL)
fputs(str, des);
if(feof(src)!=0)
puts("파일 복사 성공!");
else
puts("파일 복사 실패!");
fclose(src);
fclose(des);
return 0;
}
난 왜 두 예제 모두 출력이 "파일 오픈 실패!" 일까...
이번에는 바이너리 데이터의 입출력을 진행하는 함수들이다.
참고로 바이너리 데이터의 입출력은 텍스트 데이터의 입출력보다 단순하기 때문에 다양한 함수를 제공하지 않고 있다.
먼저, fread 함수
는 바이너리 데이터의 입력에 사용되는 함수이다.
위 함수는 다음과 같은 형태로 호출된다.
int buf[12];
fread((void*)buf, sizeof(int), 12, fp); // fp 는 FILE 구조체 포인터
위 fread 함수
의 호출문은 다음과 같은 의미이다.
"sizeof(int)크기의 데이터 12개를 fp로부터 읽어 들여서 배열 buf에 저장하라!"
즉, fread 함수
는 두 번째 전달인자와 세 번째 전달인자의 곱의 바이트 크기만큼 데이터를 읽어 들이는 함수이다.
(위 예시로 보면 int형 데이터 12개를 fp로부터 읽어서 배열 buf에 저장하는 것이다.)
이 함수는 실제로 읽어 들인 데이터의 갯수를 반환하는데 (읽어 들인 바이트 수가 아니라 갯수), 위 문장은 sizeof(int) 크기의 데이터 12개를 읽어 들이는 경우이니, 함수의 호출이 성공을 하고 요청한 분량의 데이터가 모두 읽혀지면 12가 반환된다.
함수의 호출이 성공을 했지만 파일의 끝에 도달해서 12개를 모두 읽어 들이지 못했거나 오류가 발생하는 경우에는 12보다 작은 값이 반환된다.
fwrite 함수
는 바이너리 데이터의 출력에 사용되는 함수이다.
위 함수는 다음과 같은 형태로 호출된다.
int buf[7] = {1, 2, 3, 4, 5, 6, 7};
fwrite((void*)buf, sizeof(int), 7, fp);
위 fwrite 함수
의 호출문은 다음과 같은 의미다.
"sizeof(int)크기의 데이터 7개를 buf로부터 읽어서 fp에 저장하라!"
이 두 함수를 이용해서 바이너리 파일을 복사하는 프로그램을 다음 예제에서 작성해보겠다.
#include <stdio.h>
int main()
{
FILE * src = fopen("src.bin", "rb");
FILE * des = fopen("dst.bin", "wb");
char buf[20];
int readCnt;
if(src == NULL || des == NULL)
{
puts("파일 오픈 실패!");
return -1;
}
while(1)
{
readCnt = fread((void*)buf, 1, sizeof(buf), src);
if(readCnt<sizeof(buf))
{
if(feof(src)!=0)
{
fwrite((void*)buf, 1, readCnt, des);
puts("파일 복사 성공!");
break;
}
else
puts("파일 복사 실패!");
break;
}
fwrite((void*)buf, 1, sizeof(buf), des);
}
fclose(src);
fclose(des);
return 0;
}
왜 안되는지 이제 알았다.
제대로 실행하기 위해서는
FILE * src = fopen("src.bin", "rb");
FILE * des = fopen("dst.bin", "wb");
이 부분에서 첫 번째 인자(파일명)을 기존 가지고 있는 파일이름으로 변경해서 사용해야한다.
바이너리 파일은 음원이나 이미지 파일로 넣으면 된다.
하나의 파일을 대상으로 입출력 할 데이터가 텍스트 데이터와 바이너리 데이터 둘로 이뤄져 있다면 어떠한 방법을 택해서 입출력을 해야 할까?
지금부터는 텍스트 텍스트 데이터인 문자와 문자열, 그리고 바이너리 데이터인 int형 정수 하나를, 하나의 파일을 대상으로 동시에 입출력 해야 하는 상황에 대해서 알아볼 것이다.
두 개의 텍스트 데이터와 하나의 바이너리 데이터를 입출력 해야 하는 상황에서 제일 먼저 생각할 수 있는 방법은 fprintf 함수
와 fscanf 함수
의 호출이다.
이 두 함수는 printf 함수
와 scanf 함수
와 유사하다.
다만, 입출력의 대상이 콘솔이 아닌 파일이란 점에서 차이가 있다.
그럼 먼저 fprintf 함수
의 호출 방법에 대해 알아보자.
char name[10] = "홍길동"; // 텍스트 데이터
char sex = 'M'; // 텍스트 데이터
int age = 24; // 바이너리 데이터
fprintf(fp, "%s %c %d", name, sex, age); // fp는 FILE 구조체 포인터
fprintf 함수
와 printf 함수
의 호출문 차이는 FILE 구조체의 포인터가 첫 번째 전달이라는 점이다.
따라서 fprintf 함수
는 첫 번째 인자로 전달된 FILE 구조체의 포인터가 지칭하는 파일로 출력이 이뤄진다.
위 함수의 호출로 다음 문자열이 만들어진다.
홍길동 M 24
이렇게 만들어진 문자열이 첫 번째 전달인자가 가리키는 파일에 저장된다.
결국 텍스트 데이터와 바이너리 데이터를 하나의 문자열로 묶어서 저장하는 셈이다.
예제를 통해 이러한 내용을 확인해보자.
#include <stdio.h>
int main()
{
char name[10];
char sex;
int age;
FILE * fp = fopen("friend.txt", "wt");
int i;
for(i=0; i<3; i++)
{
printf("이름 성별 나이 순 입력: ");
scanf("%s %c %d", name, &sex, &age);
getchar(); // 버퍼에 남아있는 \n의 소멸을 위해서
fprintf(fp, "%s %c %d\n", name, sex, age);
}
fclose(fp);
return 0;
}
실행하면 friend.txt 파일이 생성되고 데이터가 안에 저장된 것을 볼 수 있다. (한글 말고 영어로 값을 입력하는 것을 추천드립니다... 한글은 저장이 잘 안되요 ㅠㅠ)
이렇게 만든 파일을 직접 열어서 데이터가 저장되었는지 확인이 가능하지만, 우리는 fscanf 함수
의 호출을 통해서 콘솔에서 이를 확인하려 한다.
fscanf 함수
의 호출 방식은 다음과 같다.
char name[10];
char sex;
int age;
fscanf(fp, "%s %c %d", name, &sex, &age);
위의 fscanf 함수
와 scanf 함수
의 호출문 차이는 fprintf 함수
와 printf 함수
와의 차이와 동일하다.
fscanf 함수
는 첫 번째 인자로 전달된 포인터가 지칭하는 파일로부터 데이터를 읽어 들인다.
그리고 fscanf 함수
는 파일의 끝에 도달하거나 오류가 발생하면 EOF를 반환한다.
이를 예제를 통해 더 알아보자.
#include <stdio.h>
int main()
{
char name[10];
char sex;
int age;
FILE * fp = fopen("friend.txt", "rt");
int ret;
while(1)
{
ret = fscanf(fp, "%s %c %d", name, &sex, &age);
if(ret==EOF)
break;
printf("%s %c %d \n", name, sex, age);
}
fclose(fp);
return 0;
}
> 출력
JEY F 22
HSJ F 26
LYH M 31
실제 프로그램에서 구조체로 데이터들을 정의한다.
때문에 구조체 변수 단위로의 파일 입출력에 대해 알아야한다.
typedef struct fren
{
char name[10];
char sex;
int age;
} Friend;
구조체 변수 단위의 입출력은 구조체 멤버 단위의 입출력보다 입출력의 형태가 단순하다.
어떻게 구조체 변수를 통째로 저장하고 통째로 읽어들일 수 있을까?
구조체 변수를 하나의 바이너리 데이터로 인식하고 처리하면 가능하다.
fwrite 함수
를 통해서 통째로 저장하고 fread 함수
를 통해서 통째로 복원할 수 있다.
예제에서 이를 확인해보자.
#include <stdio.h>
typedef struct fren
{
char name[10];
char sex;
int age;
} Friend;
int main()
{
FILE * fp;
Friend myfren1;
Friend myfren2;
/*** file write ***/
fp = fopen("friend.bin", "wb");
printf("name, sex, age input: ");
scanf("%s %c %d", myfren1.name, &myfren1.sex, &myfren1.age);
fwrite((void*)&myfren1, sizeof(myfren1), 1, fp);
fclose(fp);
/*** file read ***/
fp = fopen("friend.bin", "rb");
fread((void*)&myfren2, sizeof(myfren2), 1, fp);
printf("%s, %c, %d \n", myfren2.name, myfren2.sex, myfren2.age);
fclose(fp);
return 0;
}
> 출력
name, sex, age input: Jungs M 27
Jungs, M, 27
경우에 따라서는 파일의 중간 또는 마지막 부분에 저장된 데이터의 일부를 읽어야 하는 경우도 있다.
그리고 이러한 경우에는 '파일 위치 지시자'라는 것을 파일의 중간 또는 마지막 부분으로 이동시켜야 한다.
FILE 구조체의 멤버 중에는 파일의 위치 정보를 저장하고 있는 멤버가 있는데, 이 멤버의 값은 fgets 함수
, fputs 함수
, fread 함수
, fwrite
와 같은 함수가 호출될 때마다 참조 및 갱신된다.
예를 들어서 fgets 함수
호출을 통해서 파일에 저장된 문자열을 읽어 들이는 경우, 이 멤버가 가리키는 위치를 시작으로 문자열을 읽어 들이게 되며, 총 20바이트 크기의 문자열이 읽혀졌다고 가정하면, 이 멤버는 20바이트 뒤를 가리키게 된다.
이처럼 이 멤버에 저장된 위치 정보의 갱신을 통해서 데이터를 읽고 쓸 위치 정보가 유지되는 것이다.
따라서 우리는 이 멤버를 가리켜 파일 위치 지시자
라 부르기로 한다.
read 모드로 오픈 된 파일 위치 지시자는 "어디까지 읽었더라"에 대한 답이고,
write 모드로 오픈 된 파일 위치 지시자는 "어디부터 이어서 쓰더라?"에 대한 답이다.
즉, Read/Write에 대한 위치 정보를 갖고 있다.
파일 위치 지시자
는 파일이 처음 개방되면 무조건 파일의 맨 앞부분을 가리킨다.
따라서, 파일의 중간 혹은 마지막 부분에서부터 데이터를 읽거나 쓰기를 원한다면 파일 위치 지시자를 이동시켜야 한다.
fseek
함수는 파일 위치 지시자를 직접 이동시키고자 할 때 호출하는 함수이다.
fseek
함수는 총 세 개의 인자를 요구하는데, 각 인자가 의미하는 바를 문장으로 설명하면
"stream으로 전달된 파일 위치 지시자를 wherefrom에서부터 offset 바이트만큼 이동시켜라."
매개변수 offset에는 양의 정수, 음의 정수 모두 전달될 수 있다.
양의 정수가 전달되면 파일의 마지막 방향을 향해 파일 위치 지시자가 이동하고,
음의 정수가 전달되면 파일의 처음 방향을 향해 파일 위치 지시자가 이동한다.
위 그림은 fseek
함수의 호출결과를 보여주는데, 중요하게 살펴볼 내용은 다음과 같다.
일반적으로 SEEK_END가 전달되면 파일의 끝에서부터 이동이 시작된다.
그런데 여기서 말하는 파일의 끝은 파일의 마지막 데이터가 아닌 파일의 끝을 표시하기 위해 삽입되는 EOF를 의미함을 주의해야 한다.
예제를 통해 fseek 함수가 동작하는 방식에 대해 살펴보자.
#include <stdio.h>
int main()
{
/* 파일 생성 */
FILE * fp = fopen("text.txt", "wt");
fputs("123456789", fp);
fclose(fp);
/* 파일 개방 */
fp = fopen("text.txt", "rt");
/* SEEK_END test */
fseek(fp, -2, SEEK_END); // 1234567"8"0e(eof)
putchar(fgetc(fp));
/* SEEK_SET test */
fseek(fp, 2, SEEK_SET); // 12"3"456789e(eof)
putchar(fgetc(fp));
/* SEEK_CUR test */
fseek(fp, 2, SEEK_CUR); // 12345"6"789e(eof)
putchar(fgetc(fp));
fclose(fp);
return 0;
}
> 출력
836
ftell
함수는 현재의 파일 위치 지시자 정보를 확인하고 싶을 때 호출하면 된다.
이 함수는 파일 위치 지시자의 위치 정보를 반환하는데 파일 위치 지시자가 첫 번째 바이트를 가리킬 경우 0을 반환하고, 세 번째 바이트를 가리킬 경우 2를 반환한다.
가장 앞 부분의 바이트 위치를 0으로 간주하는 것에 주의해야한다.
아래 예제를 통해 ftell
함수가 유용하게 사용될 수 있는 상황을 살펴보자.
#include <stdio.h>
int main()
{
long fpos;
int i;
/* 파일 생성 */
FILE * fp = fopen("text.txt", "wt");
fputs("1234-", fp);
fclose(fp);
/* 파일 개방 */
fp = fopen("text.txt", "rt");
for(i=0; i<4; i++)
{
putchar(fgetc(fp));
fpos = ftell(fp);
fseek(fp, -1, SEEK_END);
putchar(fgetc(fp));
fseek(fp, fpos, SEEK_SET);
}
fclose(fp);
return 0;
}
> 출력
1-2-3-4-
ftell
함수는 파일 위치 지시자의 정보를 임시로 저장할 때 유용하게 사용된다.
<Review>
간만에 공부하는데 오래 걸린 chapter였던 것 같다.
구조체를 공부하고 일주일 만에 공부하니 구조체의 포인터에서 이해력이 잠깐 주춤했었다.
하지만 다시 예제와 함께 보다보니 이해가 됐다.
fopen, fputs, fclose, putchar, fgetc, fseek, ftell 기억 기억, 메모 메모!📝