C언어

16-1) 파일 열기와 닫기

SleeveStar 2021. 12. 30. 15:06
반응형

파일 입출력 함수의 도우미! FILE 구조체

이제 본격적으로 표준 입출력 라이브러리를 사용하여 파일에 데이터를 저장하거나 읽는 방법에 대해 설명하겠다.

 

표준 입출력 라이브러리는 FILE 구조체로 포인터 변수를 선언하고 파일 입출력 함수를 호출할 때마다 이 변수를 넘겨주도록 만들어져 있다. FILE 구조체는 사용하려는 디스크상의 파일이 어떤 상태로 사용중인지에 대한 정보를 담고 있으며, 파일을 좀 더 편하게 사용할 수 있도록 도와준다.

 

File *p_file;
/* 파일 열기 생략함 */
fseek(p_file, 0, SEEK_SET);

FILE 구조체의 기본형은 'FILE *변수 이름' 형태이다. 이렇게 'FILE 구조체로 선언한 포인터 변수'를 줄여서 '파일 포인터' 라고 부른다.

 

위와 같은 방법으로 fseek 함수를 호출하면, 프로그램에서 파일을 사용하며 기억해야 할 내부 상태 값을 p_file 파일 포인터에 저장한다. 즉 프로그래머가 파일 처리에 관련된 내부 정보를 몰라도 파일 포인터만 넘겨주면 파일 입출력 함수가 알아서 처리하도록 만들어져 있다.

 

 

 

파일 열기 : fopen 함수

파일 열기를 할 때는 사용할 '파일 이름'과 '파일을 어떤 형식으로 사용할 것인지'를 먼저 결정하고 fopen 함수를 사용하면 된다. fopen 함수는 이 두가지 정보를 문자열 형식의 매개 변수로 받아서 처리한다.

함수 원형: FILE *fopen(const char *filename, const char *mode);
함수 사용 형식 : fopen(사용할 파일 이름, 파일 사용 형식) 

파일을 성공적으로 열면 FILE* 형식의 메모리 주소값을 반환한다. 만약 파일이 존재하지 않거나 파일 형식을 잘못사용해서 파일 열기에 실패하면 NULL 값을 반환한다.

 

FILE *p_file = fopen("tipssoft.dat", "r");
/*프로그램 작업 경로에 해당 파일이 있는 경우에 읽기 모드로 tipssoft.dat 파일을 연다.*/
if(NULL != p_file){
/*성공한 경우*/
} else { 
/*실패한 경우*/
}

fopen 함수에 사용할 파일 이름은 문자열로 지정해야 하며 시스템은 프로그램의 작업 경로에서 해당 파일을 찾게 된다. 예를 들어 c:\temp 경로에서 exam.exe 파일을 실행했다면 exam 프로그램의 작업 경로는 c:\temp이다. 그리고 이 작업 경로에서 fopen 함수에 사용한 tipssoft.dat 파일을 찾게 된다. 즉 c:\temp\tipssoft.dat경로에서 파일을 찾는다.

 

그런데 만약 사용할 파일이 현재 작업 경로에 없다면 파일 이름을 표기할 때 경로까지 같이 표기해 주면 된다. 현재 작업 경로가 c:\temp인데 자신이 사용할 tips.dat 파일이 c:\tipssoft 경로에 있다면 "c:\\tipssoft\\tips.dat"라고 표기하면 된다.

FILE *p_file = fopen("c:\\tipssoft\\tips.dat", "r");
if(NULL != p_file){
/*성공한 경우*/
} else { 
/*실패한 경우*/
}

 

파일 사용 형식 알아보기

파일 사용 형식이란 파일을 어떻게 사용할 것인지 지정하는 형식을 말한다. 즉 '파일 읽기'를 할 것인지, '파일 쓰기'를 할 것인지를 정하는 것이다. 파일 사용 형식은 fopen 함수의 두번째 매개변수인 문자열 형식으로 지정한다.

 

먼저 파일이 다루는 속성에 따라 기본적으로 나누어지는 형식을 살펴보자. 이 두 가지 형식은 단독으로 쓸 수는 없고 앞으로 배우게 될 다른 형식들과 함께 써야 한다.

 

형식 설명
t 텍스트 속성으로 파일을 사용하겠다는 뜻이다. 만약 이 형식으로 바이너리 파일을 열면 파일 열기는 성공하겠지만 파일 입출력 함수를 사용하면 오류가 발생한다. 그 이유는 바이너리 파일은 파일의 실제 크기를 사용하고 텍스트 파일은 EOF(end of file)라는 아스키 값을 사용해서 파일의 끝을 구별하기 때문이다. 그래서 바이너리 파일은 텍스트 속성으로 열면 파일의 끝을 찾는 데 문제가 생긴다.
b 바이너리 속성의 파일을 사용한다는 뜻이다. 이 형식이 기본값이기 때문에 형식을 지정할 때 t 또는 b를 포함하고 있지 않다면 기본적으로 이 형식을 사용한다고 보면 된다.

 

 

파일 내용 읽기 모드 "r"

이 형식을 사용하면 파일의 내용을 읽기(read)위한 목적으로 파일을 엽니다. 이 형식을 지정하여 fopen 함수를 사용했는데 파일이 없으면 파일 열기에 실패하고 NULL값을 반환한다. 바이너리 피일을 여는 경우에 다음과 같이 "rb"를 사용한다.

 

FILE *p_file = fopen("tips.dat","rb"); /*rb 대신 r만 사용해도 됨*/

텍스트 파일을 열 때는 다음과 같이 "rt"를 사용한다.

FILE *p_file = fopen("tips.txt","rt");

 

파일에 데이터 쓰기 모드 "w"

이 형식을 사용하면 파일에 데이터를 쓰기(write) 위한 목적으로 파일을 연다. 만약 fopen 함수에 명시한 파일이 작업 경로에 없다면 그 이름으로 파일을 만든 후에 파일을 열기 때문에 "w" 형식을 사용하면 파일 열기에 실패하지 않는다. 하지만 같은 이름을 가진 파일이 이미 존재하는 경우에는 파일을 열면서 그 파일이 가지고 있던 내용을 모두 지우고 시작하기 때문에 주의해야 한다.

바이너리 파일을 여는 경우에 다음과 같이 "wb"를 사용한다.

FILE *p_file = fopen("tips.dat","wb"); /*wb대신 w만 사용하도 됨*/

텍스트 파일을 열때는 다음과 같이 "wt"를 사용한다.

FILE *p_file = fopen("tips.txt","wt");

그런데 쓰기 모드 형식을 제대로 사용해도 디스크 용량이 부족해서 파일을 만들 수 없거나, cd와 같이 읽기 전용 디스크에 쓰기 모드로 사용하면 파일열기에 실패한다.

 

파일에 데이터 이어 쓰기 모드 "a"

이 형식을 사용하면 파일에 데이터를 확장(append, 이어쓰기)하기 위한 목적으로 파일을 연다. 만약 fopen 함수에 명시한 파일이 작업 경로에 없다면 그 이름으로 파일을 만든 후에 파일을 열기 때문에 "a" 형식을 사용하면 파일 열기에 실패하지 않는다. 하지만 "w" 속성과 달리 기존에 파일이 존재하더라도 파일 내용을 지우지 않고 기존 파일 내용에 이어 쓰기를 한다. 바이너리 파일을 여는 경우에 다음과 같이 "ab"를 사용한다.

 

FILE *p_file = fopen("tips.dat","ab"); /*ab대신 a만 사용하도 됨*/

텍스트 파일을 열 때는 다음과 같이 "at"를 사용한다.

FILE *p_file = fopen("tips.txt","at");

이 형식도 디스크에 용량이 부족하거나 읽기 전용 디스크에 사용하면 파일 읽기에 실패한다.

 

 

파일 사용 형식에서 읽기와 쓰기를 같이 사용하기

 

읽기 강조 "r+"

읽기와 쓰기를 같이 사용할 때 '읽기'를 더 강조하는 형식이다. 이 형식으로 파일을 여는 경우 기존 파일이 없으면 파일을 새로 만들지 않고 파일 읽기에 실패한다. 기존 파일이 있는 경우에는 해당 파일의 내용을 지우지는 않지만 기존 데이터의 위치로 이동해서 해당 위치의 내용을 덮어쓸 수 있다. 이 형식을 바이너리 파일에 사용하는 경우에 "r+", "rb+"또는 "r+b"라고 쓰며, 텍스트 파일에 사용하는 경우에는 "rt+" 또는 "r+t"라고 쓴다.

 

쓰기 강조 "w+"

읽기와 쓰기를 같이 사용할 때 '쓰기'를 더 강조하는 형식이다. 이 형식으로 파일을 여는 경우 기존 파일이 없으면 파일을 새로 만들고, 파일이 이미 존재하면 기존 파일의 내욜을 모두 지우고 시작한다. 이 형식을 바이너리 파일에 사용하는 경우에 "w+", "wb+"또는 "w+b"라고 쓸 수 있으며 텍스트 파일에 사용하는 경우에는 "wt+" 또는 "w+t"라고 쓴다.

 

읽기와 이어 쓰기를 같이 사용하기 "a+"

읽기 모드와 이어 쓰기 모드를 같이 사용해야 하는 경우에 사용하며 '확장'을 더 강조하는 형식이다. 확장을 더 강조한다는 뜻은 이 형식으로 파일을 여는 경우에 기존 파일이 없으면 파일을 새로 만들고 파일이 존재하면 파일의 내용을 지우지 않고 기존 내용에 이어서 시작한다는 뜻이다. 하지만 "r+"와 달리 기존 데이터 위치로 이동할 수 있고 읽기도 가능하지만 쓰기를 사용하면 현재 위치와 상관없이 파일의 끝에 내용이 추가된다. 이 형식을 바이너리 파일에 사용하는 경우에 "a+", "ab+" 또는 "a+b"라고 쓸 수 있으며 텍스트 파일에 사용하는 경우에는 "at+"또는 "a+t"라고 쓴다.

 

읽기와 쓰기를 같이 사용하려면 "r+"나 "w+"와 같이 적어야 하는데 "rw"가고 적기도 한다. 하지만 "rw"는 표준이 아니기 때문에 컴파일러에 따라 "r+"나 "w+"로 자동 변환하거나 fopen 함수의 실행이 실패한다. 따라서 가능하면 위에 나열한 형식중 하나를 선택해서 사용하기 바란다.

 

파일 닫기 : fclose 함수

이렇게 fopen 함수를 사용하여 파일을 열어서 사용하다가 사용이 끝나면 fclose 함수를 사용하여 파일을 닫아야 한다. 만약 파일을 열어 놓고 파일을 닫지 않으면 파일의 내용이 지워지거나 파일을 사용할 수 없는 상태가 될 수 있으니 주의해라. 그리고 파일을 열지 않은 상태에서 파일 닫기를 시도하거나 이미 닫은 FILE *주소(파일 포인터 주소)로 파일 닫기를 다시 시도하면 프로그램 실행에 오류가 발생할 수 있으니 이 또한 주의해야한다.

FILE *p_file = fopen("tipssoft.dat","r+b");  /*읽기 + 쓰기 모드로 바이너리 파일을 오픈함*/
if(NULL!=p_file) { /*파일 열기에 성공한 경우*/
     fclose(p_file); /*파일을 닫음*/
} else {
     /*실패한 경우*/
}

 

텍스트 파일에 데이터 읽고 쓰기

 

텍스트 파일에 문자열 저장하기 : fprintf 함수

텍스트 파일에 데이터를 읽고 쓰는 개념은 콘솔에서 문자열을 입력 또는 출력하는 개념과 비슷하기 때문에 매우 쉽다. 모니터 화면에 문자 또는 숫자를 출력하고 싶으면 printf 함수를 사용한다. 파일 입출력 함수에는 printf 함수와 모든 기능이 비슷하고 이름까지 비슷한 fprintf 함수가 있다. fprintf 함수는 첫 매개변수에 파일 포인터를 받아서 출력할 문자열을 파일에 저장한다. 예를 들어 화면에 "abc" 문자열을 출력하고 싶으면 prontf("abc"); 라고 사용하지만 파일에 "abc" 문자열을 저장하고 싶다면 fprintf(파일 포인터, "abc"); 라고 사용한다.

함수 원형: int fprintf(FILE *stream, const char *format [, argument]...);
함수 사용 형식 : fprintf(파일 포인터, 파일에 입력할 문자열 형식, 출력할 값들, ...)

다음은 파일 포인터가 가리키는 파일에 "Hello" 문자열을 출력하고 줄을 바꾸는 코드다.

fprintf(p_file, "Hello\n");  /*파일에 "Hello"문자열을 쓰고 줄 바꿈을 함*/

fprintf 함수를 사용해서 파일에 문자열을 출력하는 예제를 하나 만들어 보자. 다음은 tipssoft.txt 파일에 "Hello" 문자열을 저장하는 예제이다. 그리고 fopen 함수에서 파일 사용 형식에 "w"가 있을 경우에 첫 번째 매개변수로 넘겨준 파일(tipssoft.txt)이 없으면 파일을 만들어서 사용하고 파일이 존재하면 덮어쓰기를 한다.

 

 

바이너리 형태를 문자열 형태로 저장하기 : fprintf 함수(2)

 

int형 변수에 들어 있는 값은 바이너리 데이터이기 때문에 텍스트 파일에 저장하려면 문자열 형식으로 변환해서 저장해햐 한다. fprintf 함수와 마찬가지로 변수 값을 문자열로 출력할 수 있다. 따라서 표준 입출력 함수에서 제공하는 %d, %f같은 형식 지정 키워드를 사용해 파일에 문자열 형태로 저장한다.

 

다음처럼 코드를 작성하면 별도의 변환 작업 없이 data 변수의 값을 파일에 저장할 수 있다.

 

short int data = 0x0412;
fprintf(p_file, "%x\n" , data);   /*파일에 412라고 저장하고 줄 바꿈을 함*/

fprintf 함수는 호출될 때마다 자신이 파일에 저장한 문자열의 개수만큼 파일 포인터를 이동시킨다. 즉 파일의 현재 사용 상태를 가리키는 정보 중에서 '파일 내부 데이터를 읽거나 쓰기 시작하는 위치'가 문자열의 개수만큼 이동한다는 뜻이다.

따라서 연속적으로 fprintf 함수를 호출하면 문자열이 차례대로 각 파일에 저장된다.

 

short int data = 0x0412;
fprintf(p_file, "Hello\n");
fprintf(p_file, "%x\n" , data);

 

텍스트 파일에서 문자열 읽기 : fscanf 함수

 

텍스트 파일에서 문자열을 얻으려면 fscanf 함수를 사용하면 된다. 이 함수는 키보드로 문자 또는 숫자를 입력 받는 scanf 함수와 비슷하지만 첫 번째 매개변수에 어떤 파일에서 입력 값을 가져올 것인지를 명시하는 점이 다르다.

함수 원형 : int fscanf(FILE *stream, const char *format [, argument]...);
함수 사용 형식 : fscanf(파일 포인터, 파일에서 데이터를 입력받을 형식, 입력 받을 변수 목록);

아래는 파일에 저장된 문자열을 읽어 10진 정수 값으로 변환하여 data 변수에 대입하는 코드다.

int data;
fscanf(p_file , "%d", &data);

파일에 저장된 문자열을 fscanf 함수로 읽어 오는 예제에 사용하기 위해 오른쪽 이미지처럼 tipssoft.txt파일을 만들어보자. 이 파일은 예제 소스 파일과 같은 경로에 있어야 프로그램이 정상적으로 수행되기 때문에 파일 생성 경로에 주의해야 한다.

 

tipssoft.txt 파일에서 첫줄에 있는 3개의 값을 int 형 변수에 3개 저장하고 싶다면 다음과 같이 코드를 구성하면 된다.

 

위 예제를 tipssoft.txt 파일에 있는 모든 숫자를 다 출력하도록 수정해보자.

 

텍스트 파일의 끝은 EOF 문자로 구별하는데 fscanf 함수가 EOF문자를 만나면 EOF값을 반환한다. 따라서 EOF를 반환할 때까지 반복하면서 숫자값을 읽어온다.

 

fscanf 함수로 문자열을 읽을 때 주의 사항

fscanf 함수는 기본적으로 공백 문자를 만나면 다음 입력이 시작된것으로 처리한다. 예를 들어 txt파일에 문자열이 들어있다고 가정했을 때, fscanf 함수를 사용하면 우리가 예상하는 결과는 한줄씩 출력하는 석이다 하지만 공백으로 입력을 구별하는 fscanf 함수의 특성 때문에 예상과 다른 결과가 나온다.

 

 

텍스트 파일에서 한 줄 단위로 문자열 읽기: fgets 함수

fscanf 함수는 문자열 사이에 공백이 있기 때문에 한 줄 단위로 입력 받지 못하고 단어 단위로 파일에서 읽어온다. 따라서 텍스트 파일에서 한 줄 단위로 문자열을 처리하고 싶은 경우에는 fgets 함수를 사용한다. 이 함수는 gets  함수와 비슷하며 함수의 세 번째 매개변수에 어떤 파일에서 입력 값을 가져올 것인지 파일 포인터를 표기하면 된다.

 

함수 원형: char *fgets(char *string, int n, FILE *stream);
함수 사용 형식: fgets(파일에서 읽은 문자열을 저장할 메모리의 주소, 첫 번째 매개변수로 사용한 메모리의 크기, 파일 포인터);

 오른쪽은 텍스트 파일에서 문자열 한 줄을 읽어와서 temp 배열에 저장하는 코드이다.

char temp[64];
fgets(temp, sizeof(temp), p_file);

fgets 함수를 사용하여 tips.txt 파일을 읽으면 fscanf 함수는 줄 바꿈뿐만 아니라 공백 문자 입력도 구별한다. 하지만 fgets 함수는 입력의 구분을 줄 바꿈으로만 판단하기 때문에 파일에 저장된 문자열을 한 줄 단위로 읽어온다.

그 외에도 fscanf 함수는 EOF문자를 만나면 EOF를 반환하지만 fgets 함수는 EOF문자를 만나면 NULL을 반환한다. 그리고 fscanf 함수는 읽은 문자열에서 \n을 제외하는데 fgets 함수는 \n 을 문자열에 포함한다는 것이 다르다.

 

 

 

 

 

 

 

 

 

 

 

 

반응형

'C언어' 카테고리의 다른 글

17) 함수 포인터  (0) 2022.02.03
16-2)바이너리 파일에 데이터 읽고 쓰기  (0) 2022.01.08
16)파일 입출력  (0) 2021.11.15
15-3) 구조체를 활용한 연결 리스트  (0) 2021.10.15
15-2)구조체로 만든 자료형의 크기  (0) 2021.10.05