8. 파일 입출력

header


1. 파일 입출력의 이해

파일 입출력은 프로그램 실행 후에도 데이터를 보존하기 위한 방법입니다. 메모리의 데이터는 프로그램 종료 시 사라지지만, 파일에 저장하면 영구적으로 보관할 수 있습니다.

파일이란?

파일은 보조 기억장치에 저장된 데이터의 집합입니다.

구분 메모리 파일
저장 위치 RAM (주기억장치) HDD/SSD (보조기억장치)
데이터 보존 프로그램 종료 시 소멸 영구 보존
접근 속도 빠름 상대적으로 느림
용량 제한적 대용량
용도 실행 중 데이터 처리 데이터 저장 및 공유
파일 입출력의 필요성
• 프로그램 종료 후에도 데이터 유지
• 대용량 데이터 처리
• 프로그램 간 데이터 공유
• 설정 정보 저장

텍스트 파일 vs 바이너리 파일

C 언어는 두 가지 방식으로 파일을 처리합니다.

구분 텍스트 파일 바이너리 파일
저장 형식 문자 형태 (ASCII/UTF-8) 이진 형태 (바이트)
가독성 텍스트 에디터로 확인 가능 전용 프로그램 필요
크기 상대적으로 큼 상대적으로 작음
예시 .txt, .csv, .log .exe, .bin, .dat
변환 숫자 ↔ 문자열 변환 발생 메모리 그대로 저장
// 숫자 100을 저장할 때의 차이

// 텍스트 파일: '1', '0', '0' (3바이트)
fprintf(fp, "%d", 100);

// 바이너리 파일: 0x00000064 (4바이트, int 크기)
int num = 100;
fwrite(&num, sizeof(int), 1, fp);

2. 파일 열기와 닫기

파일 입출력의 기본은 파일을 열고 작업 후 닫는 것입니다.

fopen 함수

fopen 함수는 파일을 열고 FILE 포인터를 반환합니다.

#include <stdio.h>

FILE *fopen(const char *filename, const char *mode);
  • 매개변수 1: 파일 경로 및 이름
  • 매개변수 2: 파일 열기 모드
  • 반환값: 파일 포인터 (FILE *), 실패 시 NULL

파일 열기 모드

모드 설명 파일 없을 때 파일 있을 때
“r” 읽기 전용 실패 (NULL) 처음부터 읽기
“w” 쓰기 전용 새로 생성 기존 내용 삭제
“a” 추가 쓰기 새로 생성 끝에 추가
“r+” 읽기/쓰기 실패 (NULL) 처음부터 읽기/쓰기
“w+” 읽기/쓰기 새로 생성 기존 내용 삭제
“a+” 읽기/추가 새로 생성 끝에 추가

바이너리 모드: 위 모드에 b를 추가 (예: "rb", "wb", "ab")

⚠️ 주의사항
"w" 모드는 기존 파일 내용을 모두 삭제합니다!
• 파일 열기 실패 시 반드시 NULL 체크 필요
• 파일 경로는 절대 경로 또는 상대 경로 사용

fclose 함수

fclose 함수는 열린 파일을 닫고 버퍼를 비움니다.

#include <stdio.h>

int fclose(FILE *stream);
  • 매개변수: 닫을 파일 포인터
  • 반환값: 성공 시 0, 실패 시 EOF

기본 사용 예제

#include <stdio.h>

int main(void) {
    FILE *fp;

    // 파일 열기
    fp = fopen("example.txt", "w");

    if (fp == NULL) {
        printf("파일을 열 수 없습니다.\n");
        return 1;
    }

    printf("파일이 성공적으로 열렸습니다.\n");

    // 파일 닫기
    fclose(fp);

    printf("파일이 닫혔습니다.\n");

    return 0;
}
실행 결과 보기
파일이 성공적으로 열렸습니다.
파일이 닫혔습니다.

실행 후 현재 디렉터리에 빈 파일 example.txt가 생성됩니다.

파일 입출력 기본 패턴
1. fopen()으로 파일 열기
2. NULL 체크로 오류 확인
3. 파일 읽기/쓰기 작업
4. fclose()로 파일 닫기

3. 텍스트 파일 입출력

텍스트 파일은 사람이 읽을 수 있는 문자 형태로 데이터를 저장합니다.

fprintf 함수

fprintf 함수는 형식화된 데이터를 파일에 쓰기합니다.

#include <stdio.h>

int fprintf(FILE *stream, const char *format, ...);
  • 매개변수 1: 파일 포인터
  • 매개변수 2: 형식 문자열 (printf와 동일)
  • 매개변수 3~: 출력할 데이터
  • 반환값: 출력한 문자 수, 실패 시 음수
#include <stdio.h>

int main(void) {
    FILE *fp;

    fp = fopen("data.txt", "w");

    if (fp == NULL) {
        printf("파일 열기 실패\n");
        return 1;
    }

    // 파일에 데이터 쓰기
    fprintf(fp, "이름: %s\n", "홍길동");
    fprintf(fp, "나이: %d세\n", 25);
    fprintf(fp, "점수: %.2f점\n", 95.5);

    fclose(fp);

    printf("파일에 데이터를 저장했습니다.\n");

    return 0;
}
실행 결과 및 파일 내용
파일에 데이터를 저장했습니다.

data.txt 파일 내용:

이름: 홍길동
나이: 25세
점수: 95.50점

fscanf 함수

fscanf 함수는 파일에서 형식화된 데이터를 읽기합니다.

#include <stdio.h>

int fscanf(FILE *stream, const char *format, ...);
  • 매개변수 1: 파일 포인터
  • 매개변수 2: 형식 문자열 (scanf와 동일)
  • 매개변수 3~: 데이터를 저장할 변수의 주소
  • 반환값: 읽은 항목 수, 파일 끝이면 EOF
#include <stdio.h>

int main(void) {
    FILE *fp;
    char name[50];
    int age;
    double score;

    fp = fopen("data.txt", "r");

    if (fp == NULL) {
        printf("파일 열기 실패\n");
        return 1;
    }

    // 파일에서 데이터 읽기
    fscanf(fp, "이름: %s\n", name);
    fscanf(fp, "나이: %d세\n", &age);
    fscanf(fp, "점수: %lf점\n", &score);

    printf("=== 파일에서 읽은 데이터 ===\n");
    printf("이름: %s\n", name);
    printf("나이: %d세\n", age);
    printf("점수: %.2f점\n", score);

    fclose(fp);

    return 0;
}
실행 결과 보기
=== 파일에서 읽은 데이터 ===
이름: 홍길동
나이: 25세
점수: 95.50점

fgets와 fputs 함수

fputs: 문자열을 파일에 쓰기

int fputs(const char *str, FILE *stream);

fgets: 파일에서 문자열 읽기

char *fgets(char *str, int size, FILE *stream);
#include <stdio.h>
#include <string.h>

int main(void) {
    FILE *fp;
    char buffer[100];

    // 파일 쓰기
    fp = fopen("memo.txt", "w");
    if (fp == NULL) {
        printf("파일 열기 실패\n");
        return 1;
    }

    fputs("첫 번째 줄\n", fp);
    fputs("두 번째 줄\n", fp);
    fputs("세 번째 줄\n", fp);

    fclose(fp);

    // 파일 읽기
    fp = fopen("memo.txt", "r");
    if (fp == NULL) {
        printf("파일 열기 실패\n");
        return 1;
    }

    printf("=== 파일 내용 ===\n");
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }

    fclose(fp);

    return 0;
}
실행 결과 보기
=== 파일 내용 ===
첫 번째 줄
두 번째 줄
세 번째 줄
💡 텍스트 파일 함수 선택
• 형식화된 데이터: fprintf, fscanf
• 문자열 단위: fputs, fgets
• 문자 단위: fputc, fgetc
fgets는 개행 문자(\n)까지 읽음

4. 바이너리 파일 입출력

바이너리 파일은 메모리의 데이터를 그대로 저장하여 효율적입니다.

fwrite 함수

fwrite 함수는 메모리 블록을 파일에 쓰기합니다.

#include <stdio.h>

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
  • 매개변수 1: 쓸 데이터의 주소
  • 매개변수 2: 각 항목의 크기 (바이트)
  • 매개변수 3: 항목 개수
  • 매개변수 4: 파일 포인터
  • 반환값: 쓴 항목 개수

fread 함수

fread 함수는 파일에서 메모리 블록으로 읽기합니다.

#include <stdio.h>

size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  • 매개변수 1: 읽은 데이터를 저장할 주소
  • 매개변수 2: 각 항목의 크기 (바이트)
  • 매개변수 3: 항목 개수
  • 매개변수 4: 파일 포인터
  • 반환값: 읽은 항목 개수

기본 사용 예제

#include <stdio.h>

int main(void) {
    FILE *fp;
    int numbers[5] = {10, 20, 30, 40, 50};
    int read_numbers[5];
    int i;

    // 바이너리 쓰기
    fp = fopen("numbers.dat", "wb");
    if (fp == NULL) {
        printf("파일 열기 실패\n");
        return 1;
    }

    fwrite(numbers, sizeof(int), 5, fp);
    fclose(fp);

    printf("데이터를 바이너리 파일에 저장했습니다.\n");

    // 바이너리 읽기
    fp = fopen("numbers.dat", "rb");
    if (fp == NULL) {
        printf("파일 열기 실패\n");
        return 1;
    }

    fread(read_numbers, sizeof(int), 5, fp);
    fclose(fp);

    printf("읽은 데이터: ");
    for (i = 0; i < 5; i++) {
        printf("%d ", read_numbers[i]);
    }
    printf("\n");

    return 0;
}
실행 결과 보기
데이터를 바이너리 파일에 저장했습니다.
읽은 데이터: 10 20 30 40 50

구조체 저장하기

바이너리 파일은 구조체를 통째로 저장하기에 매우 유용합니다.

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[30];
    int age;
    double score;
} Student;

int main(void) {
    FILE *fp;
    Student students[3] = {
        {"김철수", 20, 85.5},
        {"이영희", 22, 90.0},
        {"박민수", 21, 88.3}
    };
    Student read_students[3];
    int i;

    // 구조체 배열 저장
    fp = fopen("students.dat", "wb");
    if (fp == NULL) {
        printf("파일 열기 실패\n");
        return 1;
    }

    fwrite(students, sizeof(Student), 3, fp);
    fclose(fp);

    printf("학생 데이터를 저장했습니다.\n\n");

    // 구조체 배열 읽기
    fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        printf("파일 열기 실패\n");
        return 1;
    }

    fread(read_students, sizeof(Student), 3, fp);
    fclose(fp);

    printf("=== 읽은 학생 데이터 ===\n");
    for (i = 0; i < 3; i++) {
        printf("이름: %s, 나이: %d세, 점수: %.1f점\n",
               read_students[i].name,
               read_students[i].age,
               read_students[i].score);
    }

    return 0;
}
실행 결과 보기
학생 데이터를 저장했습니다.

=== 읽은 학생 데이터 ===
이름: 김철수, 나이: 20세, 점수: 85.5점
이름: 이영희, 나이: 22세, 점수: 90.0점
이름: 박민수, 나이: 21세, 점수: 88.3점
바이너리 파일의 장점
• 메모리 구조 그대로 저장 (빠름)
• 텍스트 변환 없이 효율적
• 구조체, 배열 등 복잡한 데이터 저장에 유리
• 파일 크기가 작음

5. 파일 위치 제어

파일의 특정 위치로 이동하여 읽기/쓰기를 할 수 있습니다.

fseek 함수

fseek 함수는 파일 위치 지시자를 이동합니다.

#include <stdio.h>

int fseek(FILE *stream, long offset, int origin);
  • 매개변수 1: 파일 포인터
  • 매개변수 2: 이동할 바이트 수 (offset)
  • 매개변수 3: 기준 위치
    • SEEK_SET: 파일 시작 (0)
    • SEEK_CUR: 현재 위치
    • SEEK_END: 파일 끝
  • 반환값: 성공 시 0, 실패 시 -1

ftell 함수

ftell 함수는 현재 파일 위치를 반환합니다.

#include <stdio.h>

long ftell(FILE *stream);
  • 반환값: 파일 시작부터의 바이트 수

rewind 함수

rewind 함수는 파일 위치를 처음으로 이동합니다.

#include <stdio.h>

void rewind(FILE *stream);

사용 예제

#include <stdio.h>

int main(void) {
    FILE *fp;
    int numbers[5] = {10, 20, 30, 40, 50};
    int value;

    fp = fopen("position.dat", "wb");
    fwrite(numbers, sizeof(int), 5, fp);
    fclose(fp);

    fp = fopen("position.dat", "rb");

    // 세 번째 요소로 이동 (0부터 시작하므로 인덱스 2)
    fseek(fp, sizeof(int) * 2, SEEK_SET);
    fread(&value, sizeof(int), 1, fp);
    printf("세 번째 값: %d\n", value);

    // 현재 위치 확인
    printf("현재 파일 위치: %ld 바이트\n", ftell(fp));

    // 파일 끝에서 두 번째 요소로 이동
    fseek(fp, -sizeof(int) * 2, SEEK_END);
    fread(&value, sizeof(int), 1, fp);
    printf("끝에서 두 번째 값: %d\n", value);

    // 처음으로 이동
    rewind(fp);
    fread(&value, sizeof(int), 1, fp);
    printf("첫 번째 값: %d\n", value);

    fclose(fp);

    return 0;
}
실행 결과 보기
세 번째 값: 30
현재 파일 위치: 12 바이트
끝에서 두 번째 값: 40
첫 번째 값: 10
💡 파일 위치 제어 활용
• 대용량 파일에서 특정 부분만 읽기
• 파일 크기 계산: fseek(fp, 0, SEEK_END); size = ftell(fp);
• 랜덤 접근이 필요한 데이터베이스 구현
• 파일 수정 시 특정 위치만 업데이트

6. 실전 예제

예제 1: 성적 관리 프로그램

#include <stdio.h>
#include <string.h>

typedef struct {
    char name[30];
    int math;
    int english;
    int science;
} Student;

void saveStudents(Student students[], int count) {
    FILE *fp = fopen("students.dat", "wb");
    if (fp == NULL) {
        printf("파일 저장 실패\n");
        return;
    }

    fwrite(students, sizeof(Student), count, fp);
    fclose(fp);
    printf("학생 데이터를 저장했습니다.\n");
}

void loadStudents(Student students[], int *count) {
    FILE *fp = fopen("students.dat", "rb");
    if (fp == NULL) {
        printf("파일이 없습니다.\n");
        *count = 0;
        return;
    }

    *count = fread(students, sizeof(Student), 100, fp);
    fclose(fp);
    printf("%d명의 학생 데이터를 불러왔습니다.\n", *count);
}

void printStudents(Student students[], int count) {
    int i;
    printf("\n=== 학생 성적표 ===\n");
    printf("이름\t\t수학\t영어\t과학\t평균\n");
    printf("==========================================\n");

    for (i = 0; i < count; i++) {
        double avg = (students[i].math + students[i].english + students[i].science) / 3.0;
        printf("%s\t\t%d\t%d\t%d\t%.1f\n",
               students[i].name,
               students[i].math,
               students[i].english,
               students[i].science,
               avg);
    }
}

int main(void) {
    Student students[3] = {
        {"김철수", 85, 90, 88},
        {"이영희", 92, 88, 95},
        {"박민수", 78, 85, 82}
    };
    Student loaded_students[100];
    int count;

    // 데이터 저장
    saveStudents(students, 3);

    // 데이터 불러오기
    loadStudents(loaded_students, &count);

    // 출력
    printStudents(loaded_students, count);

    return 0;
}
실행 결과 보기
학생 데이터를 저장했습니다.
3명의 학생 데이터를 불러왔습니다.

=== 학생 성적표 ===
이름		수학	영어	과학	평균
==========================================
김철수		85	90	88	87.7
이영희		92	88	95	91.7
박민수		78	85	82	81.7

예제 2: 로그 파일 작성

#include <stdio.h>
#include <time.h>

void writeLog(const char *message) {
    FILE *fp;
    time_t now;
    struct tm *timeinfo;

    fp = fopen("system.log", "a");  // 추가 모드
    if (fp == NULL) {
        printf("로그 파일 열기 실패\n");
        return;
    }

    // 현재 시각 가져오기
    time(&now);
    timeinfo = localtime(&now);

    // 시각과 메시지 기록
    fprintf(fp, "[%04d-%02d-%02d %02d:%02d:%02d] %s\n",
            timeinfo->tm_year + 1900,
            timeinfo->tm_mon + 1,
            timeinfo->tm_mday,
            timeinfo->tm_hour,
            timeinfo->tm_min,
            timeinfo->tm_sec,
            message);

    fclose(fp);
}

void readLog(void) {
    FILE *fp;
    char buffer[200];

    fp = fopen("system.log", "r");
    if (fp == NULL) {
        printf("로그 파일이 없습니다.\n");
        return;
    }

    printf("=== 시스템 로그 ===\n");
    while (fgets(buffer, sizeof(buffer), fp) != NULL) {
        printf("%s", buffer);
    }

    fclose(fp);
}

int main(void) {
    writeLog("시스템 시작");
    writeLog("사용자 로그인");
    writeLog("파일 업로드 완료");
    writeLog("시스템 종료");

    printf("로그를 기록했습니다.\n\n");

    readLog();

    return 0;
}
실행 결과 보기 (예시)
로그를 기록했습니다.

=== 시스템 로그 ===
[2024-01-15 14:30:25] 시스템 시작
[2024-01-15 14:30:25] 사용자 로그인
[2024-01-15 14:30:25] 파일 업로드 완료
[2024-01-15 14:30:25] 시스템 종료

7. 종합 실습

문제 1 - fopen 모드 (기초)

문제 1

기존 파일 내용을 지우고 새로 쓰는 모드는?

A. "r"
B. "w"
C. "a"
D. "r+"
파일 입출력

문제 2 - fprintf 사용 (기초)

문제 2

다음 코드 실행 후 파일에 저장되는 내용은?

{% capture code_block2 %}

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("test.txt", "w");

    fprintf(fp, "%d + %d = %d", 10, 20, 30);

    fclose(fp);

    return 0;
}

{% endcapture %}

파일 입출력

문제 3 - fwrite 바이트 수 (중급)

문제 3

다음 코드에서 파일에 쓰인 총 바이트 수는?

{% capture code_block3 %}

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("data.bin", "wb");
    int arr[4] = {1, 2, 3, 4};

    fwrite(arr, sizeof(int), 4, fp);

    fclose(fp);

    return 0;
}

{% endcapture %}

파일 입출력
int는 4바이트, 4개이므로 4 × 4 = 16바이트

문제 4 - fscanf 사용 (중급)

문제 4

다음 코드의 실행 결과는? (파일 내용: “100 200”)

{% capture code_block4 %}

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("numbers.txt", "r");
    int a, b;

    fscanf(fp, "%d %d", &a, &b);

    printf("%d", a + b);

    fclose(fp);

    return 0;
}

{% endcapture %}

파일 입출력

문제 5 - fseek 활용 (고급)

문제 5

다음 코드의 실행 결과는?

{% capture code_block5 %}

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("test.bin", "wb");
    int arr[5] = {10, 20, 30, 40, 50};
    int value;

    fwrite(arr, sizeof(int), 5, fp);
    fclose(fp);

    fp = fopen("test.bin", "rb");
    fseek(fp, sizeof(int) * 3, SEEK_SET);
    fread(&value, sizeof(int), 1, fp);

    printf("%d", value);

    fclose(fp);

    return 0;
}

{% endcapture %}

파일 입출력
인덱스 3 위치(네 번째 요소)로 이동하여 읽음

문제 6 - 구조체 바이너리 저장 (고급)

문제 6

다음 코드의 실행 결과는?

{% capture code_block6 %}

#include <stdio.h>

typedef struct {
    int x;
    int y;
} Point;

int main(void) {
    FILE *fp = fopen("point.bin", "wb");
    Point p1 = {5, 10};
    Point p2;

    fwrite(&p1, sizeof(Point), 1, fp);
    fclose(fp);

    fp = fopen("point.bin", "rb");
    fread(&p2, sizeof(Point), 1, fp);
    fclose(fp);

    printf("%d", p2.x + p2.y);

    return 0;
}

{% endcapture %}

파일 입출력
구조체 {5, 10}을 저장 후 읽어서 5 + 10 = 15

핵심 요약

1. 파일 열기/닫기
fopen(filename, mode): 파일 열기, FILE * 반환
fclose(fp): 파일 닫기
• 주요 모드: "r"(읽기), "w"(쓰기), "a"(추가)
• 바이너리: "rb", "wb", "ab"

2. 텍스트 파일 입출력
fprintf(fp, format, ...): 형식화된 쓰기
fscanf(fp, format, ...): 형식화된 읽기
fputs(str, fp): 문자열 쓰기
fgets(str, size, fp): 문자열 읽기

3. 바이너리 파일 입출력
fwrite(ptr, size, count, fp): 메모리 블록 쓰기
fread(ptr, size, count, fp): 메모리 블록 읽기
• 구조체, 배열 저장에 효율적
• 변환 없이 원본 데이터 그대로 저장

4. 파일 위치 제어
fseek(fp, offset, origin): 위치 이동
ftell(fp): 현재 위치 반환
rewind(fp): 처음으로 이동
• origin: SEEK_SET, SEEK_CUR, SEEK_END

5. 주의사항
• 파일 열기 후 반드시 NULL 체크
• 사용 후 반드시 fclose() 호출
"w" 모드는 기존 내용 삭제
• 바이너리 모드에서는 반드시 "b" 추가

6. 활용 예시
• 데이터 영구 저장 (설정, 게임 저장)
• 로그 파일 작성
• 대용량 데이터 처리
• 프로그램 간 데이터 공유