C 파일 입출력: fopen, fprintf, fread, fseek 마스터하기
C 파일 입출력
이 튜토리얼은 C 언어 파일 I/O를 표준 및 바이너리 연산 중심으로 명확하고 실용적인 예제와 함께 안내합니다.
파일은 컴퓨터 저장 장치에 데이터를 장기 보관하기 위한 논리적 컨테이너입니다.
왜 파일을 사용하는가?
- 프로그램 종료 후에도 데이터가 지속되어 정보 손실을 방지합니다.
- 대용량 데이터 세트를 한 번 저장하고 몇 개의 C 명령으로 빠르게 접근할 수 있습니다.
- 파일은 수정 없이 컴퓨터 간에 이동 가능합니다.
파일 유형
C에서는 두 가지 주요 파일 유형을 다룹니다:
- 텍스트 파일
- 바이너리 파일
텍스트 파일
일반 텍스트 파일(예: .txt)은 모든 텍스트 편집기로 편집 가능합니다. 읽기, 유지보수, 전송이 쉽지만 공간을 더 차지하고 보안성은 낮습니다.
바이너리 파일
바이너리 파일(보통 .bin)은 데이터를 원시 바이트(0과 1)로 저장합니다. 컴팩트하고 사람이 읽을 수 없으며 데이터 무결성과 보안성이 더 좋습니다.
핵심 파일 연산
유형에 관계없이 C 파일 처리는 네 가지 연산을 중심으로 돌아갑니다:
- 새 파일 생성
- 기존 파일 열기
- 파일 닫기
- 파일 읽기/쓰기
파일 포인터 준비
모든 파일 연산은 열린 파일을 참조하는 FILE * 포인터가 필요합니다:
FILE *fptr;
파일 열기: 생성과 편집
fopen() 함수(stdio.h에 선언)는 파일을 열고 FILE * 포인터를 반환합니다. 문법:
fptr = fopen("filename", "mode");
사용 예:
fopen("E:\\cprogram\\newprogram.txt", "w");
fopen("E:\\cprogram\\oldprogram.bin", "rb");
설명:
newprogram.txt는 없으면 생성되고 쓰기 모드 'w'로 열리며 기존 내용은 덮어씁니다.
oldprogram.bin은 바이너리 읽기 전용 모드 'rb'로 열리며 쓰기 시도는 실패합니다.
표준 I/O 열기 모드
| 모드 | 의미 | 파일 없을 때 동작 |
r | 읽기 전용 열기 | NULL 반환 |
rb | 바이너리 읽기 전용 열기 | NULL 반환 |
w | 쓰기 전용 열기(덮어쓰기) | 없으면 생성 |
wb | 바이너리 쓰기 전용 열기(덮어쓰기) | 없으면 생성 |
a | 추가 모드 열기 | 없으면 생성 |
ab | 바이너리 추가 모드 열기 | 없으면 생성 |
r+ | 읽기/쓰기 | NULL 반환 |
rb+ | 바이너리 읽기/쓰기 | NULL 반환 |
w+ | 읽기/쓰기(덮어쓰기) | 없으면 생성 |
wb+ | 바이너리 읽기/쓰기(덮어쓰기) | 없으면 생성 |
a+ | 읽기/추가 | 없으면 생성 |
ab+ | 바이너리 읽기/추가 | 없으면 생성 |
파일 닫기
모든 I/O 연산 후 fclose()로 파일을 닫아 자원을 해제하고 버퍼를 플러시하세요:
fclose(fptr);
텍스트 파일 I/O
텍스트 파일은 fprintf()와 fscanf()로 처리하며, printf()/scanf()의 파일 버전입니다. 첫 인자로 FILE * 포인터가 필요합니다.
예제 1: 텍스트 파일에 쓰기
#include <stdio.h>
#include <stdlib.h>
int main() {
int num;
FILE *fptr;
// OS에 맞게 경로 조정
fptr = fopen("C:\\program.txt", "w");
if (fptr == NULL) {
printf("Error opening file\n");
exit(EXIT_FAILURE);
}
printf("숫자 입력: ");
scanf("%d", &num);
fprintf(fptr, "%d", num);
fclose(fptr);
return 0;
}
이 프로그램은 사용자로부터 정수를 입력받아 program.txt에 씁니다.
예제 2: 텍스트 파일에서 읽기
#include <stdio.h>
#include <stdlib.h>
int main() {
int num;
FILE *fptr;
fptr = fopen("C:\\program.txt", "r");
if (fptr == NULL) {
printf("Error opening file\n");
exit(EXIT_FAILURE);
}
fscanf(fptr, "%d", &num);
printf("읽은 값: %d\n", num);
fclose(fptr);
return 0;
}
첫 예제 실행 후 이 프로그램을 돌리면 저장된 정수가 출력됩니다.
fgetc(), fputc() 같은 추가 유틸리티도 같은 패턴을 따릅니다.
바이너리 파일 I/O
바이너리 파일은 쓰기에 fwrite(), 읽기에 fread()를 사용합니다. 두 함수 모두 네 인자(데이터 포인터, 요소 크기, 요소 개수, FILE * 포인터)를 받습니다.
바이너리 파일에 쓰기
fwrite(addressData, sizeData, numberData, filePointer);
예제 3: fwrite()로 바이너리 파일에 쓰기
#include <stdio.h>
#include <stdlib.h>
struct threeNum {
int n1, n2, n3;
};
int main() {
int i;
struct threeNum num;
FILE *fptr;
fptr = fopen("C:\\program.bin", "wb");
if (fptr == NULL) {
printf("Error opening file\n");
exit(EXIT_FAILURE);
}
for (i = 1; i < 5; ++i) {
num.n1 = i;
num.n2 = 5 * i;
num.n3 = 5 * i + 1;
fwrite(&num, sizeof(struct threeNum), 1, fptr);
}
fclose(fptr);
return 0;
}
바이너리 파일에서 읽기
fread() 시그니처는 fwrite()와 동일합니다:
fread(addressData, sizeData, numberData, filePointer);
예제 4: fread()로 바이너리 파일 읽기
#include <stdio.h>
#include <stdlib.h>
struct threeNum {
int n1, n2, n3;
};
int main() {
int i;
struct threeNum num;
FILE *fptr;
fptr = fopen("C:\\program.bin", "rb");
if (fptr == NULL) {
printf("Error opening file\n");
exit(EXIT_FAILURE);
}
for (i = 1; i < 5; ++i) {
fread(&num, sizeof(struct threeNum), 1, fptr);
printf("n1: %d\tn2: %d\tn3: %d\n", num.n1, num.n2, num.n3);
}
fclose(fptr);
return 0;
}
이 프로그램은 예제 3에서 쓴 데이터를 재현합니다.
fseek()로 임의 접근
파일에 많은 레코드가 있을 때 특정 항목까지 순차적으로 이동하는 것은 비효율적입니다. fseek()는 파일 포인터를 임의 위치로 이동시켜 직접 접근을 가능하게 합니다.
문법
fseek(FILE *stream, long int offset, int whence);
stream: fopen()이 반환한 포인터.
offset: 이동할 바이트 수.
whence: 기준점 – SEEK_SET, SEEK_CUR, SEEK_END.
fseek() whence 값
| 상수 | 의미 |
SEEK_SET | 파일 처음부터 오프셋. |
SEEK_CUR | 현재 커서 위치부터 오프셋. |
SEEK_END | 파일 끝부터 오프셋. |
예제 5: fseek()로 역순 읽기
#include <stdio.h>
#include <stdlib.h>
struct threeNum {
int n1, n2, n3;
};
int main() {
int i;
struct threeNum num;
FILE *fptr;
fptr = fopen("C:\\program.bin", "rb");
if (fptr == NULL) {
printf("Error opening file\n");
exit(EXIT_FAILURE);
}
// 마지막 레코드로 이동
fseek(fptr, -sizeof(struct threeNum), SEEK_END);
for (i = 1; i < 5; ++i) {
fread(&num, sizeof(struct threeNum), 1, fptr);
printf("n1: %d\tn2: %d\tn3: %d\n", num.n1, num.n2, num.n3);
// 현재 위치에서 두 레코드만큼 뒤로(현재 + 하나 더) 이동해 이전 엔트리 읽기
fseek(fptr, -2 * sizeof(struct threeNum), SEEK_CUR);
}
fclose(fptr);
return 0;
}
이 스니펫은 마지막 레코드부터 시작해 바이너리 파일을 역순으로 순회하는 방법을 보여줍니다.