1. 파일과 스트림(Stream), 그리고 기본적인 파일의 입출력
-저기 저파일에 저장되어 있는 데이터를 읽고싶을때
프로그램상에서 파일에 저장되어 있는 데이터를 참조하길 원한다고(읽기원한다고) 할때, 이 때 가장 먼저해야 할일은 우리가 구현한 프로그램과 참조할 데이터가 저장되어 있는 파일 사이에 데이터가 이동할 수 있는 다리를 놓는 일이다.
이러한 데이터의 이동의 경로가 되는 다리를 가리켜 '스트림(Stream)'이라 한다. 프로그램과 파일사이에 스트림을 형성해야 데이터를 주고 받을 수 있다.
스트림이라는 것은 운영체제에 의해서 형성되는 소프트웨어 적인 상태를 의미하는 것일 뿐이다. 따라서 프로그램과 파일 사이에 스트림이 형성되었다고 하면 다음과 같이 이해하면 된다.
"파일로 부터 데이터를 읽어 들일(파일에 데이터를 쓸) 기본적인 준비가 완료됨"
파일은 운영체제에 의해서 그 구조가 결정되고 관리되는 대상이기 때문에, 파일 뿐만 아니라 스트림의 형성도 운영체제의 몫임을 기억해야 한다.
-fopen 함수 호출을 통한 파일과의 스트림 형성과 FILE구조체
스트림을 형성할때 호출하는 함수이다. 이 함수의 호출을 통해서 프로그램상에서 파일과의 스트림을 형성할 수 있다.
FILE *fopen(const char *filename, const char*mode);
-> 성공 시 해당 파일의 FILE 구조체 변수의 주소값, 실패 시 NULL포인터 반환
위 함수의 첫 번째 인자로는 스트림을 형성할 파일의 이름을, 두 번째 인자로는 형성할 스트림의 종류에 대한 정보를 문자열의 형태로 전달한다. 그러면 이 함수는 해당 파일과의 스트림을 형성하고 스트림 정보를 FILE구조체 변수에 담아서 그 변수의 주소값을 반환한다.
fopen함수의 반환형을 다시보자. FILE이라는 이름의 기본 자료형이 존재하지 않으니 이는 분명 구조체으 ㅣ이름이다. 그렇다면 이 구조체는 어떻게 정의 되어 있으며 무엇에 사용되는 것일까?
사실 FILE구조체가 어떻게 정의되어 있는지는 알필요가 없다. FILE구조체 변수의 멤버에 직접접근할 일이 없기 때문이다. 우 ㅣ함수가 반환하는 FILE구조체의 포인터는 파일을 가리키기 위한 용도로 사용된다. 즉 이 포인터를 이용해서 파일에 데이터를 저장하거나 파일에 저장된 데이터를 읽게된다.
fopen 함수가 호출되면 FILE 구조체 변수가 생성된다.
생성된 FILE 구조체 변수에는 파일에 대한 정보가 담긴다.
FILE 구조체의 포인터는 사실상 파일을 가리키는 '지시자' 역할을 한다.
-입력 스트림과 출력 스트림의 생성
스트림은 '한 방향으로 흐르는 데이터의 흐름'을 의미한다. 때문에 스트림은 데이터를 파일로부터 읽어 들이기 위한 '입력 스트림 '과 데이터를 파일에 쓰기 위한 '출력 스트림'으로 구분된다. 스트림의 형성을 위한 fopen함수의 호출방법을 설명한다. fopen함수를 호출 할 때에는 다음 두가지가 인자로 전달되어야 한다.
첫 번째 전달인자 : 스트림을 형성할 파일의 이름
두 번째 전달인자 : 형성하고자 하는 스트림의 종류
먼저 출력 스트림의 형성을 요청하는 fopen함수의 호출문을 보인다.
FILE *fp = fopen("data.txt", "wt"); //출력 스트림의 형성
이것이 의미하는 바는
"파일 data.txt 와 스트림을 형성하되 wt모드로 스트림을 형성해라"
wt모드의 스트림은 텍스트 데이터를 쓰기 위한 출력 스트림을 뜻한다.
FILE *fp =fopen("data.txt", "rt"); //입력 스트림의 형성
이것이 의미하는 바는
"파일 data.txt.와 스트림을 형성한되 rt 모드로 스트림을 형성해라"
rt모드의 스트림은 텍스트 데이터를 읽기 위한 입력 스트림을 뜻한다.
fopen함수의 호출을 통해서 파일과의 스트림이 형성되었을 때, '파일이 개방(오픈)되었다'라고 표현한다.
-파일에 데이터를 써보자
#include <stdio.h>
int main(){
FILE *fp = fopen("data.txt", "wt");
if(fp==NULL){
puts("파일 오픈 실패!);
return -1;
}
fputc('A', fp);
fputc('B', fp);
fputc('C', fp);
fclose(fp); //스트림의 종료
return 0;
}
위 예제에서 fopen 함수를 사용해 파일 data.txt와의 출력 스트림을 형성하고 있따. 이렇듯 출력스트림을 형성하는 경누에는 해당 파일이 생성된다. 따라서 우리는 파일 data.txt가 생성됨을 확인할 수 있다. 그런데 실행 방법에 따라서 , 그리고 실행환경 및 설정에 따라서 파일이 생성되는 위치는 달라진다.
이렇게 해서 스트림이 형성되면 ,이제부터 fp는 파일 data.txt를 지칭하는 포인터가된다. 따라서 다음의 문장이 실행되면 ,
fputc('A', fp);
fp가 지칭하는 파일 data.txt에 문자 A가 저장된다. 이제 fopen함수가 반환하는 FILE 구조체 포인터의 용도를 이해할수 있지 않는가?
이렇게 해서 문자 A, B, C를 저장한 다음에 마지막으로 fclose 함수가 호출되면 데이터는 안정적으로 저장이 되고, data.txt와 연결되었던 출력 스트림은 소멸이 된다.
-스트림의 소멸을 요청하는 fclose 함수
위 예제에서 호출한 fclose 함수에 대해서 살펴보자. 간단히 설명하면 fclose 함수는 fopen함수의 반대기능을 제공한다. fopen 함수가 스트림을 형성하는 함수라면, fclose함수는 스트림을 해제하는 함수이다. fopen 함수가 파일을 개방하는 함수라면, fclose함수는 파일을 닫는 함수이다.
int fclose(FILE *stream);
->성공 시 0, 실패 시 EOFㅂ ㅏㄴ환
그렇다면 이렇게 fclose함수의 호출을 통해서 개방되었던 파일을 닫아줘야 하는 이유는 무엇인가?
1. 운영체제가 할당한 자원의 반환
2. 버퍼링 되었던 데이터 출력
함수의 호출을 통해서 스트림의 형성을 요청하는 것은 우리지만, 실제로 스트림을 형성하는 주체는 운영체제 이다. 그리고 운영체제는 스트림의 형성을 위해서 시스템의 자원(주로 메모리)을 할당한다. 그런데 이 자원은 파일을 닫아주지 않으면 할당된 채로 남아있게 되어 , 그만큼 자원손실을 초래하기 때문에 파일의 사용이 끝나는 즉시 fclose함수를 호출해서 자원을 반환해줄 필요가 있다.
두번째 이유는 파일스트림의 경우 중간에 입력버퍼와 출력버퍼가 존재한다.
운영체제는 프로그램과 파일 사이에 입출력 버퍼를 둬서 성능의 향상을 도모하고 있다. 때문에 우리가 fputc 같은 함수의 호출로 데이터를 파일로 전송한다고 해서 파일에 바로 저장되는 것이 아니라, 일단은 출력버퍼에 저장이 되었다가 운영체제가 정해놓은 버퍼링 방식에 따라서 뒤늦게 파일에 저장이 된다.
그런데 이러한 버퍼링 방식이 지니는 문제점이 하나 있다. 예를 들어서 문자 A와 B가 출력버퍼에 존재하는 상태에서(파일에 저장되기 직전의 상태에서)컴퓨터의 전원이 꺼졌다고 가정하자. 문자 A와 B는 파일에 저장이 되는가? 저장이 되지 않는다. 그러나 fclose함수의 호출을 통해서 파일을 닫아주면 출력버퍼에 저장되어있던 데이터가 파일로 이동하면서 출력버퍼는 비워지게 된다. 물론 그 이후에는 컴퓨터의 전원이 꺼져도 파일에 저장된 데이터는 소멸되지 않는다. 때용에 사용이 끝난 파일은 곧바로 fclose함수를 호출해 닫는 것이 좋다.
-fflush 함수
int fflush(FILE * stream);
-> 함수 호출 성공시 0, 실패시 EOF반환
입출력 버퍼를 비우는 것은 다음과 같다.
출력 버퍼를 비운다는 것은 출력버퍼에 저장된 데이터를 목적지로 전송한다는 의미
입력 버퍼를 비운다는 것은 입력버퍼에 저장된 데이터를 소멸시킨다는 의미
fflush 함수는 출력버퍼를 비우는 함수이다.
fflush함수는 입력버퍼를 대상으로 호출할 수 없다.
그리고 위의 내용은 파일 스트림에도 그대로 적용이 된다. 다음과 같이 파일의 출력버퍼를 비우면, 출력버퍼에 저장된 데이터가 실제로 파일에 저장이 된다.
FILE *fp =fopen("data.txt", "wt");
....
fflush(fp); /출력 버퍼를 비움
fflush함수는 출력버퍼를 위한 함수이다.
파일 스틀림의 입력버퍼를 비우는 함수는 필요가 없다. 파일에 저장된 데이터는 원할 때 언제든지 읽을 수 있을 뿐만 아니라 (읽혀진 데이터는 입력버퍼에서 지워진다), 파일대상의 입력버퍼를 비워야만 하는 상황이 특별히 존재하지 않기 때문이다.
-파일로부터 데이터를 읽어보지/
fgetc함수는 파일에 저장된 문자 하나를 반호나하는 함수이다.
int ch = fgetc(fp);
위의 함수 호출로 인하여 FILE 구조체의 포인터 fp가 지칭하는 파일에 저장된 문자 하나가 반환되어 변수 ch에 저장된다는 사실을 기억해야 한다.
2. 파일의 개방모드( MODE)
형성할 수 있는 스트림의 종류는 다양한다. 기본적으로 두 가지 기준을 통해서 스트림을 구분한다.
기준 1: 읽기 위한 스트림인가? 쓰기 위함 스트림인가?
기준 2 : 텍스트 데이터를 위한 스트림이냐? 바이너리 데이터를 위한 스트림이냐?
-스트림을 구분하는 기준1: 읽기 위한 스트림? 쓰기위한 스트림?
스트림을 구분하는 기준은 두 가지인데, 이 중에서 데이터의 이동방향을 기준으로 다음과 같이 네 가지로 구분할 수 있다.
데이터 READ스트림 : 읽기만 가능
데이터 WRITE 스트림 : 쓰기만 가능
데이터 APPEND 스트림 : 쓰되 덧붙여 쓰기만 가능
데이터 READ/ WRITE 스트림 : 읽기 , 쓰기 모두 가능
그러나 C언어는 이를 바탕으로 총 6가지의 스트림을 세분화 한다. 그리고 이 6가지는 다음과 같다.
모드(mode) |
스트림의 성격 |
파일이 없으면? |
r |
읽기 가능 |
에러 |
w |
쓰기 가능 |
생성 |
a |
파일의 끝에 덧붙여 쓰기 가능 |
생성 |
r+ |
읽기/쓰기 가능 |
에러 |
w+ |
읽기/쓰기 가능 |
생성 |
a+ |
읽기/ 덧붙여 쓰기 가능 |
생성 |
위 표를 참조하여 필요로 하는 스트림의 특성과 일치하는 '파일의 개방모드(mode)'를 선택하면 된다.
그리고 모드의 이름이 fopen 함수의 두 번째 인자가 된다. 다음 두 가지 사실을 기억해 놓으면 선택이 한결 수월하다.
1. 모드의 +는 읽기, 쓰기가 모두 가능한 스트림의 형성을 의미한다.
2. 모드의 a는 쓰기가 가능한 스트림을 형성하는데, 여기서 말하는 쓰기는 덧붙이기이다.
웬만하면 r, w ,a 중에서 선택하세요.
파일의 개방모드 중 r+, w+, a+는 읽기와 동시에 쓰기가 가능하므로 더 좋은 모드라고 생각할 수 있다. 그러나 이러한 모드를 기반으로 작업하는 경우에는 읽기에서 쓰기, 끄리고 쓰기에서 읽기로 작업을 변경할 떄마다 메모리 버퍼를 비워줘야 하는 등의 불편함과 더불어 잘못된 사용의 위험성도 따른다. 그래서 r, w,a 중에서 하나를 선택하여 스트림을 형성하는 것이 좋으며, 이것이 일반적인 선택이다.
-텍스트파일과 바이너리 파일
파일에 담을수 있는 데이터의 유형
개인이 소유하는 도서의 목록 : 문자데이터
슈퍼마켓의 물품 가격 : 문자데이터
제임스 캐머런 감독의 타이타닉 영상파일 : 바이너리 데이터
소녀시대의 히트곡 음원파일 : 바이너리 데이터
정리하면, 사람이 인식할 수 있는 문자를 담고 있는 파일을 가리켜 '텍스트(text file)'이라 하며, 그 이외에 컴퓨터가 인식할 수 있 는 데이터를 담고 있는 파일을 '바이너리(binary file)'이라 한다.
그렇다면 데이터의 입출력을 위해서 스트림을 형성할 때 이와 관련해서 특별히 신경 쓸 부분은 무엇인가?
바로 '문장의 끝을 의미하는 개행의 표현방식'이다.
-개행이 \n이 아니다?
개행은 일반적인 문자 데이터와 성격이 조금 다르다. 개행은 줄이 바뀌었다는 일종의 현상이지 그 자체가 하나의 데이터로 존재하는 대상은 아니기 때문이다. C언어에서는 개행을 \n으로 표현하도록 약속하였다.
'C언어에서는 개행을 \n으로 표시한다. 그런데 이는 C언어만의 약속이다'
그렇다면 다른 환경에서는 개행을 어떻게 표시할까?
Windows : \r\n
Mac : \r
Unix : \n
따라서 windows 기반의 편집기는 파일에 \r과 \n이 나란히 등장할 때 개행으로 인식한다. 이렇듯 개행의 표 현은 운영체제마다 차이가 있기 떄문에 개행 문자가 포함되는 텍스트 데이터의 저장에는 주의가 필요하다.
C언어에서 개행을 의미하는 \n을 그대로 파일에 저장하고, 이 파일을 Windows나 Mac의 편집기로 열어보면 \n이 개행으로 표시되지 않음을 확인할 수 있다.
막상 이러한 형태의 변환을 직접하려니 귀찮을 수 있다. 그렇다면 파일을 텍스트 모드로 개방하면 된다.
-스트림을 구분하는 기준 2: 텍스트 모드와 바이너리 모드
파일을 텍스트 모드로 개방하면 바로 위에서 말한 형태의 변환이 자동으로 이뤄진다. 때문에 우리가 직접 개행 문자의 변환에 신경을 쓸 필요가 없다. 그저 텍스트 모드로 파일을 개방만 하면 된다. 그리고 텍스트 모드의 파일 개방을 위해서 fopen 함수의 두번째 인자로 다음 중 하나를 전달해야 한다.
rt, wt, at, r+t, w+t, a+t
이는 위에 정리한 파일 개방 모드에 텍스트 모드를 의미하는 t가 붙은 형태이다. 반대로 바이너리 데이터를 저장하고 있는 파일의 경우 이러한 형태의 변환이 일어나면 안되기 때문에(아무런 변환도 일어나면 안되기 때문에) 바이너리 모드로 파일을 개방해야 한다. 그리고 이를 위해서는 fopen함수의 두번째 인자로 다음 중 하나를 전달하면 된다.
rb, wb, ab , r+b, w+b, a+b
이 역시 위에서 정리한 개방모드에 바이너리 모드를 의미하는 b가 붙은 형태이다. 개방모드에 t도 b도 붙여지지 않으면 파일은 텍스트 모드로 개방된다는 사실도 알아둬야 한다.
정리하면 바이너리 모드로 파일을 개방하면 아무런 변환도 발생하지 않는다. 그러나 텍스트 모드로 파일을 개방하면 운영체제에 따른 표현 차로 인한 변환이 발생한다.
w+t = wt+는 같은 표현이므로 나머지 모든 개방모드에도 적용할 수 있다.
3. 파일 입출력 함수의 기본
int fputc(int c, FILE *stream); //문자출력
int fgetc(int c, FILE *stream); //문자 입력
int fputs(const char *s, FILE *stream); //문자열 출력
int fgets(const char *s, int n, FILE *stream); // 문자열 입력
이전에는 위 함수들의 두 번째 또는 세 번째 매개변수 stream에 표준 입출력을 의미하는 stdin이나 stdout으로 인자로 전달함 으로써 키보드와 모니터를 대상으로 입출력을 진행하였다. 그러나 매개변수의 형이 의미하듯이 매개변수 stream에 FILE구조체의 포인터를 인자로 전달하여 파일 대상의 입출력을 진행할 수 있다.
문자열이 파일에 저장될 때에는 끝을 의미하는 널 문자는 저장되지 않는다. 때문에 파일에서는 개행을 기준으로 문자열을 구분한다. c총 두개의 문자열을 읽어들이기 위해서 fgets함수를 두번 호출했을때, 매번 호출이 될 때마다 개행 문자를 만날 때까지 문자열을 읽어들이게 된다. 딸서 fgets를 함수의 호출을 통해서 읽어 들일 문자열의 끝에는 반드시 \n문자가 존재해야 한다.
-feof 함수 기반의 파일 복사 프로그램
때로는 파일의 마지막에 저장된 데이터까지 모두 읽어 들여야 하는 상황이 존재한다. 그리고 이를 위해서는 파일의 끝을 확인하는 방법이 필요한데, 다음 함수가 이러한 목적으로 정의된 함수이다.
int feof(FILE *stream);
-> 파일의 끝에 도달한 경우 0이 아닌 값 반환
이 함수는 인자로 전달된 FILE구조체의 포인터를 대상으로, 더 이상 읽어들일 데이터가 존재하지 않으면(파일의 끝까지 모두 읽어들인 상태이면) 이 아닌 값을 반환한다. 따라서 다음의 파일복사 프로그램과 같이 파일의 끝을 확인해야 하는 경우에 유용하게 사용이 된다.
#include <stdio.h>
int main(){
FILE * src = fopen("stc.txt", "rt");
FILE * des = fopen("dst.txt", "wt);
int ch;
if(src==NULL || des== NULL){ //파일 오픈에 실패하면 NULL값을 반환하기 때문에 확인 가능
pus("파일 오픈 실패!");
return -1;
}
//src에 값을 한문자씩 ch에 저장하고 des파일구조체 변수에 저장한다.
while ((ch=fgetc(src))!=EOF) //파일의 내용을 한 문자씩 복사한다. 이는 fgetc함수가 EOF를 반환할 때까지 반복하게 된다.
fputc(ch, des); // fgetc함수는 파일의 끝에 도달을 해서 더이상 읽을 데이터가 없거나 오류가 발생하는 경우 EOF을 반환한다.
if(feof(src)!=0) // fgetc함수로 인해 파일의 끝에 도달하게 되면 0이 아닌값을 반환하게 된다. !연산자를 사용해 결과출력
puts("파일 복사 완료"); //오류가 발생하는 경우 EOF를 반환되기 떄문에 반환의 원인 확인
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){ //파일 오픈에 실패하면 NULL값을 반환하기 때문에 확인 가능
pus("파일 오픈 실패!");
return -1;
}
while(fgets(str, sizeof(str), src)!=NULL) //src으로부터 문자열 입력 받아서 str에 저장
//fgets함수는 파일의 끝에 도달하거나 함수호출 실패시 NULL포인터를 반환한다.
fputs(str, des); //str내용을 des에 출력한다.
if(feof(src)!=0) // fgetc함수로 인해 파일의 끝에 도달하게 되면 0이 아닌값을 반환하게 된다. !연산자를 사용해 결과출력
puts("파일 복사 완료"); //오류가 발생하는 경우 EOF를 반환되기 떄문에 반환의 원인 확인
else
puts("파일 복사 실패");
fclose(src);
fclose(des);
return 0;
}
fgets함수는 파일의 끝에 도달해서 더이상 읽을 데이터가 존재하지 않거나 오류가 발생하는 경우에 NULL을 반환한다. 따라서 이예제에서도 파일복사의 성공을 확인하기 위해서 feof함수를 호출하였다.
-바이너리 데이터의 입출력 : fread, fwrite
지금까지 설명한 입출력 함수들은 텍스트 데이터의 입출력을 진행하는 함수들이 였다. 반면에 이번에 설명하는 함수들은 바이너리 데이터의 입출력을 진행하는 함수들이다. 참고로 바이너리 입출력은 텍스트 데이터의 입출력보다 단순하기 때문에 다양한 함수를 제공하지 않는다.
size _t fread(void *buffer, size_t size, size_t count, FILE *stream);
-> 성공 시 전달인자 count, 실패 또는 파일의 끝 도달 시 count보다 작은 값 반환
위 함수는 다음과 같이 호출이 된다.
int buf[12];
fread((void *)buf, sizeof(int), 12, fp); //fp는 FILE구조체 포인터
그리고 위의 fread함수의 호출문은 다음의 의미로 해석이된다.
"sizeof(int) 크기의 데이터 12개를 fp로부터 읽어 들 여서 배열 buf에 저정해라!"
즉 fread함수는 두 번째 전달인자와 세 번째 전달인자의 곱의 바이트 크기만큼 데이터를 읽어 들이는 함수이다. 따라서 위의 fread함수 호출을 통해서 int형 데이터 12개를 fp로부터 읽어서 배열 buf에 저장하게 된다.
그리고 이 함수는 실제로 읽어 들인 데이터의 갯수를 반환하는데 (읽어 들인 바이트 수가 아니라 갯수이다). 위의 문장은 sizeof(int) 크기의 데이터를 12개 읽어 들이는 경우이니, 함수의 호출이 성공을 하고 요청한 분량의 데이터가 모두 읽혀지면 12가 반환된다.
반면 함수의 호출이 성공을 했지만 파일의 끝에 도달을 해서 12개를 모두 읽어 들이지 못했거나 오류가 발생하는 경우에는 12보다 작은 값이 반환된다. 그럼 이어서 바이너리 데이터의 출력에 사용되는 fwrite함수를 소개한다.
size_t fwrite (const void*buffer, size_t size, size_t count, FILE *stream);
-> 성공 시 전달인자 count , 실패시 count 보다 작은 값 반환
위 함수는 다음과 같이 호출을 한다.
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("des.bin", "wb"); //src -> des로 복사
char buf[20]; //src로 부터 읽어들일 데이터를 저장하는 공간
int readCnt; //count 를 세는 변수
if (src == NULL || des == NULL)
{ //파일 오픈 오류시 알려주는 함수
puts("파일 오픈 실패!");
return -1;
}
while (1) {
readCnt = fread((void*)buf, 1, sizeof(buf), src); //src로 부터 1xsizeof(buf) 만큼 데이터를 읽어서 buf 배열에
// 저장해라 읽어 들인 데이터 만큼 cnt가 반환되므로 readCnt에 저장한다.
if (readCnt < sizeof(buf))
{ //sizeof(buf)보다 cnt값이 작으면 오류가 발생한 것 또는 파일의 끝에 도달한 것
if (feof(src) != 0)
{ //feof함수를 사용해 모두 읽어들인 상태면 0이 아닌값이 반환되기 때문에, 다음 함수가 실행된다.
fwrite((void*)buf, 1, readCnt, des); //readCnt값 x1만큼의 데이터를 buf에서 받아들여 des에 저장한다.
puts("파일 복사 완료");
break;
}
else
puts("파일 복사 실패");
break;
}
fwrite((void*)buf, 1, sizeof(buf), des); // sizeof(buf)x1 만큼의 데이터를 buf에서 받아들여 des에 저장한다.
// 이 함수는 readCnt 값이 같으면 실행되는 함수로 16행 이후 실행된다.
} //while문 종료
fclose(src);
fclose(des);
return 0;
}
복사 프로그램은 다음의 성격을 지닌다.
READ / WRITE 를 동시에 진행해야 한다.
파일의 끝에 도달했는지를 확인해야한다.
그런데 이 두 가지는 파일 입출력에 가장 기본이 되는 중요한 내용이다(파일 입출력의 전부로도 볼수 있다), 따라서 하나의 간결한 프로그램 안에서 이 둘을 동시에 보여주기 위해 복사 프로그램을 예로 든것이다.
4. 텍스트 데이터와 바이너리 데이터를 동시에 입출력 하기
하나의 파일을 대상으로 입출력할 데이터가 텍스트 데이터와 바이너리 데이터 둘로 이뤄졌다면 어떠한 방법을 택해서 입출력을 해야하는가?
지금부터는 텍스트 데이터인 문자와 문자열, 그리고 바이너리 데이터인 int형 정수 하나를 하나의 파일을 대상으로 동시에 입출력 해야 하는 상황에 대해서 애기한다.
-서식에 따른 데이터 입출력 : fprintf, fscanf
두 개의 텍스트 데이터와 하나의 바이너리 데이터를 입출력 해야 하는 상황에서 제일 먼저 생각할 수 있는 방법은 fscanf함수와 fprintf함수의 호출이다. 이 두 함수는 scanf와 printf함수와 유사하다. 다만 입출력의 대상이 콘솔이 아닌 파일이라는 점에서 차이가 있다. 그럼 fprintf함수의 호출방법을 보이겠다.
char name[20] = "홍길동"; //텍스트 데이터
char sex = 'M' ; //텍스트 데이터
int age =24; //바이너리 데이터
fprintf(fp, %s %c %d" ,name, sex, age); //fp는 FILE 구조체 포인터
위의 fprintf함수의 호출문이 printf 함수의 호출문과 차이를 보이는 부분은 FILE구조체와 포인터가 첫번째 전달 인자인 점이다. 그래서 printf함수와 달리 fprintf함수는 첫 번째 인자로 전달된 FIEL구조체의 포인터를 지칭하는 파일로 출력이 이뤄진다. 위의 fprintf함수 호출 문장도 두 번째 이후의 전달인자를 통해서 다음 문자열이 만들어 진다.
"홍길동 M 24"
그리고 이렇게 만들어진 문자열이 첫번째 전달인자가 가리키는 파일에 저장이 된다. 결국 텍스트 데이터와 바이너리 데이터를 하나의 문자열로 묶어서 저장하는 셈이다. 예제를 통해 이러한 내용을 확인하자. 다음 예제에는 텍스트 데이터인 이름과 성별, 그리고 바이너리 데이터인 나이 정보를 하나의 파일에 저장하는 방법을 fprintf함수를 기준으로 보여준다.
#include <stdio.h>
int main()
{
char name[20];
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);//scanf함수는 엔터 키의 입력을 읽어 들이지 않고 입력 버퍼에 남겨둔다
getchar(); //버퍼에 남아있는 \n의 소멸을 위해서
fprintf("fp, "%s %c %d", name, sex, age);
}
fclose(fp);
return 0;
}
위의 예제를 통해서 생성된 파일 friend.txt를 직접 열어서 프로그램 사용자가 입력한 데이터가 저장되었음을 확인할 수 있다. 그러나 우리는 fscanf 함수의 호출을 통해서 이를 확인 하고자 한다. fprintf함수의 호출을 통해서 저장된 데이터는, 동일하게 서식을 지정해서 fscanf함수의 호출을 통해서 읽어들일 수 있다. fscanf 함수의 호출방식은 다음과 같다.
char name[10];
char sex;
int age;
fscanf(fp, "%s %c %d", name, &sex, &char);
위의 fscanf함수 호출문이 scanf함수 호출문과 차이를 보이는 부분은 첫 번째 인자로 FILE구조체의 포인터가 전달된다는 점이다. 따라서 scanf함수와 달리 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); //fp로부터 데이터를 읽어들임
if(ret ==EOF)
berak;
printf("%s %c %d \n", name, sex, age);
}
fclose(fp);
return 0;
}
-텍스트와 바이너리 데이터의 집합체인 구조체 변수의 입출력
앞서 보인 예제에서는 다음의 데이터를 대상으로 파일 입출력을 진행하였다.
char name[10], char sex, int age
그런데 실제 프로그램에서는 이들을 다음과 같이 구조체로 묶어서 정의하는 것이 보통이다. 때문에 구조체 변수 다누이로의 파일 입출력에 대해 고민할 필요가 있다.
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("이름, 성별, 나이 순 입력 : ");
scanf("%s %c %d", myfren1.name, &(myfren1.sex), &(myfren1.age));
fwrite((void*)&myfren1, sizeof(myfren1), 1, fp); //1크기의 데이터 sizeof(myfren1)개를 myfren1에서 읽어서 fp에 저장해라
fclose(fp);
// file read
fp= fopen("friend.bin", "rb");
fread((void*)&myfren2, sizeof(myfren2), 1, fp); //1크기의 데이터sizeof(myfren2)개를 fp로부터 읽어들여서 myfren2에 저장
printf("%s %c %d \n", myfren2.name, myfren2.sex, myfren2.age);
fclose(fp);
return 0;
}
5. 임의 접근을 위한 '파일 위치 지시자'의 이동
경우에 따라서는 파일의 중간 또는 마지막 부분에 저장된 데이터의 일부를 읽어야 하는 경우도 있다. 그 리고 이러한 경우에는 '파일 위치 지시자'라는 것이 파일의 중간 또는 마지막 부분으로 이동시켜야 한다.
-파일 위치 지시자란?
FILE 구조체의 멤버 중에는 파일의 위치 정보를 저장하고 있는 멤버가 있는데, 이 멤버의 값은 fgets, fputs 또는 fread, fwrite 와 같은 함수가 호출될 때마다 참조 및 갱신된다. 예를 들어서 fgets함수 호출을 통해서 파일에 저장된 문자열을 읽어 들이는 경우 , 이 멤버가 가리키는 위치를 시작으로 문자열을 읽어 들이게 되며, 총 20바이트 크기의 문자열이 읽혀졌다고 가정하면, 이 멤버는 20바이트 뒤를 가리키게 된다.
이처럼 이 멤버에 저장된 위치 정보의 갱신을 통해서 데이터를 읽고 쓸 위치 정보가 유지되는 것이다. 따라서 우리는 이 멤버를 가리켜 '파일 위치 지시자' 라고 부른다.
'파일 위치 지시자'는 파일이 처음 개방되면 무조건 파일의 맨 앞부분을 가리킨다. 따라서 파일의 중간 혹은 마지막 부분에서부터 데이터를 읽거나 쓰기를 원한다면 '파일 위치 지시자'를 이동시켜야 한다.
파일 위치 지시자의 이동 : fseek
파일 위치 지시자를 직접 이동시키고자 할 때에는 다음 함수를 호출해야 한다. 이 함수를 호출하면 파일 위치 지시자를 원하는 곳으로 이동시킬 수 있다.
int fseek(FILE *stream, long offset, int wherefrom);
위 함수는 총 3개의 인자를 요구하는데, 인자가 의미하는 바를 문장으로 구성해서 설명하면 다음과 같다.
"stream으로 전달된 파일 위치 지시자를 wherefrom에서부터 offset바이트 만큼 이동시켜라"
wherefrom에 전달되는 상수들이다.
SEEK_SET : 파일 맨 앞에서부터 이동 시작
SEEK_CUR : 현재 위치에서부터 이동 시작
SEEK_END : 파일 맨 끝에서부터 이동 시작
그리고 매개변수 offset에는 양의 정수 뿐만 아니라 음의 정수도 전달될 수 있다. 양의 정수가 전달되면 파일의 마지막을 향해서 파일 위치 지시자가 이동을 하지만, 음의 정수가 전달되면 파일의 시작 위치를 향해서 파일 위치 지시자가 이동을 한다.
SEEK_SET 전달시 첫 번째 바이트에서부터 이동을 시작한다.
SEEK_END 전달시 EOF에서부터 이동을 시작한다.
fseek함수의 두 번째 인자로 음수가 전달되면 왼쪽으로(앞 부분으로)이동을 한다.
일반적으로 SEEK_END가 전달되면 파일의 끝에서부터 이동이 시작된다. 여기서 말하는 파일의 끝은 마지막 데이터가 아닌, 파일의 끝을 표시하기 위해서 삽입되는 EOF를 의미한다.
-현재 파일 위치 지시자의 위치는? : ftell
현재의 파일 위치 지시자의 정보를 확인하고 싶다면 다음 함수를 호출하면 된다.
long ftell(FILE *stream);
->파일 위치 지시자의 정보 반환
이 함수는 파일 위치 지시자의 위치 정보를 반환하는데, 파일 위치 지시자가 첫 번째 바이트를 가리킬 경우 0을 반환하고, 세 번째 바이트를 가리킬 경우 2를 반환한다. 즉 n번째 바이트를 가리킬 경우 n-1을 반환한다.
int fops= ftell(fp); 이 함수로 현재 위치를 저장할 수있다.
'휴지통 > C 프로그래밍' 카테고리의 다른 글
메모리 관리와 메모리의 동적할당 (0) | 2021.01.27 |
---|---|
문자와 문자열 관련 함수 (0) | 2021.01.26 |
구조체와 사용자 정의 자료형2 (0) | 2021.01.22 |
구조체와 사용자 정의 자료형1 (0) | 2021.01.21 |
문자와 문자열 관련 함수 (0) | 2021.01.21 |