1단원 — 파일 입출력
지금까지의 프로그램은 종료하면 모든 데이터가 사라졌습니다. 파일 입출력으로 **디스크에 데이터를 저장하고 다시 읽어**올 수 있습니다.
이 강의에서 배우는 것
- 1`fopen`/`fclose`로 파일을 열고 닫는다.
- 2텍스트 모드와 이진 모드를 구분해 사용한다.
- 3`fprintf`/`fscanf`/`fgets`로 텍스트 입출력을 한다.
- 4`fread`/`fwrite`로 이진 입출력을 한다.
왜 파일 입출력이 필요한가? — "메모리는 휘발성"
프로그램 실행 중 프로그램 종료 후
┌──────────────┐ ┌──────────────┐
│ 변수 a = 10 │ │ │
│ 배열 [...] │ ──── 종료 ────► │ 사라짐 │
│ 구조체 │ │ │
└──────────────┘ └──────────────┘
(RAM, 휘발성) (정전·종료에 약함)디스크 파일에 저장해 두면 다음 실행에서도 읽을 수 있습니다. 설정 파일, 로그, 데이터 백업, 결과 보고서 등 모든 영속적 자료의 기반.
핵심 개념
1) 파일 열기와 닫기
FILE *fp = fopen("data.txt", "r");
if (fp == NULL) {
perror("열기 실패");
return 1;
}
/* ... 사용 ... */
fclose(fp);`fopen`은 성공 시 `FILE *`, 실패 시 `NULL`을 돌려줍니다. **닫지 않으면** 운영체제 자원 누수 + 버퍼가 디스크에 안 써질 수 있음.
2) 모드 문자열
| 모드 | 의미 | 파일이 없으면 | 파일이 있으면 |
|---|---|---|---|
| `"r"` | 읽기 | 실패 (NULL 반환) | 처음부터 읽기 |
| `"w"` | 쓰기 | 새로 생성 | **내용 삭제 후** 처음부터 |
| `"a"` | 이어 쓰기 | 새로 생성 | 끝에 추가 |
| `"r+"` | 읽기+쓰기 | 실패 | 그대로 열기 |
| `"rb"`/`"wb"` | 이진 모드 | 위와 동일 | 위와 동일 |
3) 텍스트 입출력
사람이 읽을 수 있는 형식. 한 줄에 한 레코드 같은 패턴이 흔합니다.
fprintf(fp, "%s,%d\n", name, age); // 쓰기
fscanf(fp, "%s %d", name, &age); // 읽기
fgets(buf, sizeof(buf), fp); // 한 줄 읽기 (공백 포함)4) 이진 입출력
메모리의 바이트를 그대로 저장. **속도가 빠르고 정밀**하지만 사람이 읽을 수 없습니다.
fwrite(&value, sizeof(value), 1, fp);
fread (&value, sizeof(value), 1, fp);메모리 (Record 구조체) 파일 (data.bin)
┌──────────────────┐ ┌──────────────────┐
│ id: 1 │ fwrite │ 01 00 00 00 │
│ name: "Kim..." │ ────────► │ 4B 69 6D 00 ... │
│ score: 90.5 │ │ 00 00 00 00 ... │
└──────────────────┘ └──────────────────┘5) 파일 끝(EOF) 검사
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
/* 한 줄씩 처리 */
}`feof(fp)`보다 **읽기 함수의 반환값**으로 판정하는 게 표준 권장.
예제로 보기
예제 1 — `ex01_write_text.c` : CSV 형식 텍스트 쓰기
fprintf(fp, "이름,점수\n");
fprintf(fp, "%s,%d\n", "Kim", 90);**실행 결과 (콘솔)**
output.txt 작성 완료**output.txt**
이름,점수
Kim,90
Lee,85
Park,78핵심: `"w"` 모드는 **기존 내용을 지우므로** 주의.
예제 2 — `ex02_read_text.c` : 한 줄씩 읽기
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%2d: %s", ln++, line);
}**실행 결과**
1: 이름,점수
2: Kim,90
3: Lee,85
4: Park,78핵심: `fgets`는 줄바꿈 `\n`까지 읽어 옵니다.
예제 3 — `ex03_append.c` : 이어 쓰기 (로그)
FILE *fp = fopen("log.txt", "a");
fprintf(fp, "[%ld] 프로그램 실행 기록\n", (long)time(NULL));**실행 결과 (두 번 실행 후 log.txt)**
[1778403024] 프로그램 실행 기록
[1778403024] 프로그램 실행 기록핵심: `"a"` 모드는 **끝에 추가**하므로 기존 내용이 보존됩니다.
예제 4 — `ex04_binary.c` : 구조체를 이진으로
typedef struct { int id; char name[32]; double score; } Record;
fwrite(out, sizeof(Record), n, fp);
fread (in, sizeof(Record), n, fp);**실행 결과**
읽은 레코드 수: 3
1 Kim 90.50
2 Lee 85.00
3 Park 77.50핵심: 같은 구조체 정의를 쓰는 프로그램끼리만 안전하게 교환 가능. 다른 OS/컴파일러로 옮기면 **패딩과 엔디언** 차이로 깨질 수 있습니다.
다른 시각으로 보기 — 텍스트 vs 이진
| 항목 | 텍스트 모드 (`"r"`/`"w"`) | 이진 모드 (`"rb"`/`"wb"`) |
|---|---|---|
| 사람이 읽기 | 가능 | 불가 |
| 정수 1234 저장 | "1234" (4 byte 문자) | `0xD2 0x04 0x00 0x00` (4 byte) |
| 줄바꿈 처리 | OS별 변환 | 변환 없음 |
| 호환성 | 매우 좋음 | 같은 환경 권장 |
| 속도/크기 | 큼 | 작고 빠름 |
설정 파일이라면 텍스트, 게임 세이브 데이터라면 이진이 자연스럽습니다.
자주 하는 실수
- **`fopen` 결과 검사 누락**: 파일이 없는데 `fp`를 그대로 사용 → 세그폴트.
- **`fclose` 누락**: 데이터가 디스크에 안 써져 보일 수 있음(버퍼링).
- **`"w"` 모드 오용**: 기존 데이터를 의도치 않게 날림.
- **상대 경로 혼란**: 현재 작업 디렉터리가 어디인지 확인 (`pwd` 또는 `getcwd`).
- **이진 파일을 텍스트 도구로 비교**: `diff` 같은 도구는 이진 파일에 부적절. `cmp` 사용.
정리
- `fopen`-`fclose`는 짝을 이루며, 결과 NULL 검사는 필수.
- 텍스트 모드는 사람이 보기 좋고, 이진 모드는 빠르고 정확.
- `fprintf`/`fscanf`/`fgets`는 텍스트, `fwrite`/`fread`는 이진.
- `"w"`는 덮어쓰기, `"a"`는 이어쓰기.
- 이진 파일은 같은 환경(컴파일러/OS)에서 읽고 쓰는 것이 안전.
직접 해 보기
cd src
gcc -std=c11 -Wall -o ex01 ex01_write_text.c && ./ex01 && cat output.txt
gcc -std=c11 -Wall -o ex02 ex02_read_text.c && ./ex02
gcc -std=c11 -Wall -o ex03 ex03_append.c && ./ex03 && ./ex03 && cat log.txt
gcc -std=c11 -Wall -o ex04 ex04_binary.c && ./ex04응용:
- `ex01`을 실행한 뒤 `output.txt`를 텍스트 에디터로 열어 직접 줄을 추가하고 `ex02`를 실행.
- `ex04`의 `Record`에 새 멤버를 추가하면 기존 `data.bin`을 읽을 수 있을지 시도.
💻 예제 (examples)
실제로 컴파일·실행해 결과를 확인할 수 있는 예제입니다.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("output.txt", "w");
if (!fp) { perror("output.txt"); return 1; }
fprintf(fp, "이름,점수\n");
fprintf(fp, "%s,%d\n", "Kim", 90);
fprintf(fp, "%s,%d\n", "Lee", 85);
fprintf(fp, "%s,%d\n", "Park", 78);
fclose(fp);
printf("output.txt 작성 완료\n");
return 0;
}
#include <stdio.h>
int main(void) {
FILE *fp = fopen("output.txt", "r");
if (!fp) {
perror("output.txt");
printf("ex01_write_text를 먼저 실행하세요.\n");
return 1;
}
char line[256];
int ln = 1;
while (fgets(line, sizeof(line), fp) != NULL) {
printf("%2d: %s", ln++, line);
}
fclose(fp);
return 0;
}
1: 이름,점수
2: Kim,90
3: Lee,85
4: Park,78#include <stdio.h>
#include <time.h>
int main(void) {
FILE *fp = fopen("log.txt", "a");
if (!fp) { perror("log.txt"); return 1; }
time_t t = time(NULL);
fprintf(fp, "[%ld] 프로그램 실행 기록\n", (long)t);
fclose(fp);
printf("log.txt에 한 줄 추가됨\n");
return 0;
}
#include <stdio.h>
typedef struct {
int id;
char name[32];
double score;
} Record;
int main(void) {
Record out[] = {
{1, "Kim", 90.5},
{2, "Lee", 85.0},
{3, "Park", 77.5},
};
int n = (int)(sizeof(out) / sizeof(out[0]));
FILE *fp = fopen("data.bin", "wb");
if (!fp) { perror("data.bin"); return 1; }
fwrite(out, sizeof(Record), n, fp);
fclose(fp);
Record in[3];
fp = fopen("data.bin", "rb");
if (!fp) { perror("data.bin"); return 1; }
size_t r = fread(in, sizeof(Record), n, fp);
fclose(fp);
printf("읽은 레코드 수: %zu\n", r);
for (size_t i = 0; i < r; i++) {
printf("%d %-6s %.2f\n", in[i].id, in[i].name, in[i].score);
}
return 0;
}
읽은 레코드 수: 3
1 Kim 90.50
2 Lee 85.00
3 Park 77.50📝 과제 (exercises)
직접 풀어보고, 막힐 때 정답을 펼쳐 비교해보세요.
문제 1 (hw01.c)
목표: 사용자에게 정수 5개를 입력받아 `numbers.txt`에 한 줄에 하나씩 기록하세요.
- 파일명: hw01.c
▶정답 코드 펼치기 / 접기
#include <stdio.h>
int main(void) {
FILE *fp = fopen("numbers.txt", "w");
if (!fp) { perror("numbers.txt"); return 1; }
printf("정수 5개 입력: ");
for (int i = 0; i < 5; i++) {
int n;
scanf("%d", &n);
fprintf(fp, "%d\n", n);
}
fclose(fp);
printf("numbers.txt 저장 완료\n");
return 0;
}
문제 2 (hw02.c)
목표: `numbers.txt`를 읽어 합계와 평균을 출력하세요.
- 파일명: hw02.c
▶정답 코드 펼치기 / 접기
#include <stdio.h>
int main(void) {
FILE *fp = fopen("numbers.txt", "r");
if (!fp) { perror("numbers.txt"); return 1; }
int n, sum = 0, count = 0;
while (fscanf(fp, "%d", &n) == 1) {
sum += n;
count++;
}
fclose(fp);
if (count == 0) { printf("데이터가 없습니다.\n"); return 0; }
printf("합계: %d\n", sum);
printf("평균: %.2f\n", (double)sum / count);
return 0;
}
문제 3 (hw03.c)
목표: 실행할 때마다 `count.bin`에 저장된 정수 카운터를 1 증가시키고 그 값을 출력하세요. 파일이 없으면 0부터 시작합니다.
- 파일명: hw03.c
첫 실행: 1
두 번째: 2
세 번째: 3▶정답 코드 펼치기 / 접기
#include <stdio.h>
int main(void) {
int count = 0;
FILE *fp = fopen("count.bin", "rb");
if (fp) {
fread(&count, sizeof(int), 1, fp);
fclose(fp);
}
count++;
fp = fopen("count.bin", "wb");
if (!fp) { perror("count.bin"); return 1; }
fwrite(&count, sizeof(int), 1, fp);
fclose(fp);
printf("실행 횟수: %d\n", count);
return 0;
}
첫 실행: 1
두 번째: 2
세 번째: 3