← C 강의 목록으로
🔥
심화 (Deep)
fopen · fread/fwrite · 텍스트/바이너리

1단원 — 파일 입출력

지금까지의 프로그램은 종료하면 모든 데이터가 사라졌습니다. 파일 입출력으로 **디스크에 데이터를 저장하고 다시 읽어**올 수 있습니다.

fopenfreadfwrite
소요 시간
1~2시간
난이도
📊 고급
선수 조건
🎯 고급 3단원
결과물
파일에 데이터를 영구 저장한다

이 강의에서 배우는 것

  • 1`fopen`/`fclose`로 파일을 열고 닫는다.
  • 2텍스트 모드와 이진 모드를 구분해 사용한다.
  • 3`fprintf`/`fscanf`/`fgets`로 텍스트 입출력을 한다.
  • 4`fread`/`fwrite`로 이진 입출력을 한다.

왜 파일 입출력이 필요한가? — "메모리는 휘발성"

text
프로그램 실행 중                    프로그램 종료 후
┌──────────────┐                    ┌──────────────┐
│  변수 a = 10 │                    │              │
│  배열 [...]  │   ──── 종료 ────►  │   사라짐     │
│  구조체      │                    │              │
└──────────────┘                    └──────────────┘
        (RAM, 휘발성)                    (정전·종료에 약함)

디스크 파일에 저장해 두면 다음 실행에서도 읽을 수 있습니다. 설정 파일, 로그, 데이터 백업, 결과 보고서 등 모든 영속적 자료의 기반.

핵심 개념

1) 파일 열기와 닫기

c
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) 텍스트 입출력

사람이 읽을 수 있는 형식. 한 줄에 한 레코드 같은 패턴이 흔합니다.

c
fprintf(fp, "%s,%d\n", name, age);     // 쓰기
fscanf(fp, "%s %d", name, &age);       // 읽기
fgets(buf, sizeof(buf), fp);            // 한 줄 읽기 (공백 포함)

4) 이진 입출력

메모리의 바이트를 그대로 저장. **속도가 빠르고 정밀**하지만 사람이 읽을 수 없습니다.

c
fwrite(&value, sizeof(value), 1, fp);
fread (&value, sizeof(value), 1, fp);
text
메모리 (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) 검사

c
char line[256];
while (fgets(line, sizeof(line), fp) != NULL) {
    /* 한 줄씩 처리 */
}

`feof(fp)`보다 **읽기 함수의 반환값**으로 판정하는 게 표준 권장.

예제로 보기

예제 1 — `ex01_write_text.c` : CSV 형식 텍스트 쓰기

c
fprintf(fp, "이름,점수\n");
fprintf(fp, "%s,%d\n", "Kim",  90);

**실행 결과 (콘솔)**

text
output.txt 작성 완료

**output.txt**

text
이름,점수
Kim,90
Lee,85
Park,78

핵심: `"w"` 모드는 **기존 내용을 지우므로** 주의.

예제 2 — `ex02_read_text.c` : 한 줄씩 읽기

c
while (fgets(line, sizeof(line), fp) != NULL) {
    printf("%2d: %s", ln++, line);
}

**실행 결과**

text
 1: 이름,점수
 2: Kim,90
 3: Lee,85
 4: Park,78

핵심: `fgets`는 줄바꿈 `\n`까지 읽어 옵니다.

예제 3 — `ex03_append.c` : 이어 쓰기 (로그)

c
FILE *fp = fopen("log.txt", "a");
fprintf(fp, "[%ld] 프로그램 실행 기록\n", (long)time(NULL));

**실행 결과 (두 번 실행 후 log.txt)**

text
[1778403024] 프로그램 실행 기록
[1778403024] 프로그램 실행 기록

핵심: `"a"` 모드는 **끝에 추가**하므로 기존 내용이 보존됩니다.

예제 4 — `ex04_binary.c` : 구조체를 이진으로

c
typedef struct { int id; char name[32]; double score; } Record;
fwrite(out, sizeof(Record), n, fp);
fread (in,  sizeof(Record), n, fp);

**실행 결과**

text
읽은 레코드 수: 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별 변환변환 없음
호환성매우 좋음같은 환경 권장
속도/크기작고 빠름

설정 파일이라면 텍스트, 게임 세이브 데이터라면 이진이 자연스럽습니다.

자주 하는 실수

  1. **`fopen` 결과 검사 누락**: 파일이 없는데 `fp`를 그대로 사용 → 세그폴트.
  2. **`fclose` 누락**: 데이터가 디스크에 안 써져 보일 수 있음(버퍼링).
  3. **`"w"` 모드 오용**: 기존 데이터를 의도치 않게 날림.
  4. **상대 경로 혼란**: 현재 작업 디렉터리가 어디인지 확인 (`pwd` 또는 `getcwd`).
  5. **이진 파일을 텍스트 도구로 비교**: `diff` 같은 도구는 이진 파일에 부적절. `cmp` 사용.

정리

  • `fopen`-`fclose`는 짝을 이루며, 결과 NULL 검사는 필수.
  • 텍스트 모드는 사람이 보기 좋고, 이진 모드는 빠르고 정확.
  • `fprintf`/`fscanf`/`fgets`는 텍스트, `fwrite`/`fread`는 이진.
  • `"w"`는 덮어쓰기, `"a"`는 이어쓰기.
  • 이진 파일은 같은 환경(컴파일러/OS)에서 읽고 쓰는 것이 안전.

직접 해 보기

bash
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)

실제로 컴파일·실행해 결과를 확인할 수 있는 예제입니다.

ex01_write_text.cCSV 형식 텍스트 쓰기
CODE
#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;
}
ex02_read_text.c한 줄씩 읽기
CODE
#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
ex03_append.c이어 쓰기 (로그)
CODE
#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;
}
ex04_binary.c구조체를 이진으로
CODE
#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

문제 1 (hw01.c)

목표: 사용자에게 정수 5개를 입력받아 `numbers.txt`에 한 줄에 하나씩 기록하세요.

요구사항
  • 파일명: hw01.c
정답 코드 펼치기 / 접기
SOLUTION
#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

문제 2 (hw02.c)

목표: `numbers.txt`를 읽어 합계와 평균을 출력하세요.

요구사항
  • 파일명: hw02.c
정답 코드 펼치기 / 접기
SOLUTION
#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

문제 3 (hw03.c)

목표: 실행할 때마다 `count.bin`에 저장된 정수 카운터를 1 증가시키고 그 값을 출력하세요. 파일이 없으면 0부터 시작합니다.

요구사항
  • 파일명: hw03.c
입출력 예시
첫 실행: 1
두 번째: 2
세 번째: 3
정답 코드 펼치기 / 접기
SOLUTION
#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
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗