Skip to Content
독학사독학사 2단계C프로그래밍15. 파일 입출력 기초

이번 문서의 목표: 이 파일을 다 읽으면 파일을 열고(fopen) 읽고 쓰고(fscanf, fprintf, fread, fwrite) 닫는(fclose) 전체 절차를 코드로 작성할 수 있고, 파일 모드와 반환값을 정확히 검사하는 오류 처리 패턴을 설명할 수 있다.

왜 이 주제가 필요한가

지금까지 다룬 입출력은 04편에서 배운 표준 입출력(키보드로 입력받고 화면에 출력)뿐이었다. 하지만 프로그램이 끝나도 데이터를 남겨두려면 파일(file, 보조기억장치에 저장되는 이름 붙은 데이터 묶음)에 읽고 써야 한다. C는 파일을 다루는 함수들을 표준 라이브러리로 제공하며, 이 함수들은 04편의 printf/scanf와 이름·사용법이 매우 비슷하다는 특징이 있다. 이 편에서는 파일을 여는 절차부터, 텍스트·이진 파일을 구분해 읽고 쓰는 방법, 그리고 시험에서 자주 나오는 “파일 열기 실패 확인”과 “파일 끝 확인” 오류 처리 패턴까지 정리한다.

FILE 포인터와 파일 스트림

쉽게 말하면: 파일을 다루려면 먼저 그 파일과 프로그램을 연결하는 통로 하나를 확보해야 하는데, 그 통로의 정체가 FILE 포인터다.

C에서 파일은 직접 다루지 않고, 스트림(stream, 데이터가 순서대로 흐르는 통로라는 개념)을 통해 다룬다. 이 스트림을 프로그램 안에서 가리키는 것이 FILE 구조체를 가리키는 포인터, 줄여서 FILE 포인터다. FILEstdio.h에 정의된 구조체 자료형으로, 그 내부 구현(파일의 현재 위치, 버퍼 등)은 표준에서 감춰져 있어 프로그래머가 직접 멤버에 접근하지 않고 전용 함수들을 통해서만 다룬다.

FILE *fp;

이 선언은 “fp라는 이름의, FILE을 가리키는 포인터”를 만든다는 뜻이다. 아직 어떤 파일과도 연결되지 않은 상태이며, 다음에 배울 fopen 함수가 이 포인터에 실제 파일과 연결된 값을 대입해 준다.

fopen과 fclose: 파일 열고 닫기

쉽게 말하면: fopen은 파일이라는 문을 여는 열쇠를 만드는 함수이고, fclose는 다 쓴 뒤 그 문을 닫는 함수다.

fopen 함수는 파일을 열어, 그 파일과 연결된 FILE 포인터를 반환한다. 함수 시그니처와 반환값 규칙은 다음과 같다.

FILE *fopen(const char *filename, const char *mode);
  • 첫 번째 인자 filename: 열고자 하는 파일의 이름(경로)을 담은 문자열.
  • 두 번째 인자 mode: 어떤 방식으로 열지 지정하는 문자열(“r”, “w” 등).
  • 반환값: 성공하면 그 파일과 연결된 FILE 포인터, 실패하면 널 포인터(NULL, 아무것도 가리키지 않는다는 뜻의 특수한 포인터 값)를 반환한다.

파일을 다 쓴 뒤에는 반드시 fclose 함수로 닫아야 한다.

int fclose(FILE *stream);

fclose는 성공하면 0을, 실패하면 EOF(End Of File, 파일의 끝을 나타내는 정수 상수, 보통 -1)를 반환한다. 파일을 닫지 않으면 버퍼(buffer, 실제 디스크에 쓰기 전 임시로 데이터를 모아두는 메모리 공간)에 남아 있는 내용이 디스크에 반영되지 않을 수 있고, 운영체제가 허용하는 파일 핸들(운영체제가 열린 파일마다 부여하는 관리 번호) 개수를 다 써버려 이후 파일을 못 여는 문제도 생길 수 있다.

파일 모드

모드의미파일이 없을 때파일이 있을 때
r읽기 전용fopen이 실패(NULL 반환)처음부터 읽기 가능
w쓰기 전용새로 생성기존 내용을 모두 지우고 새로 씀
a추가(append) 쓰기새로 생성기존 내용 끝에 이어서 씀
r+읽기·쓰기(파일이 있어야 함)fopen이 실패읽기·쓰기 모두 가능, 기존 내용 유지
w+읽기·쓰기(내용을 비움)새로 생성기존 내용을 모두 지움
rb, wb위 모드에 b를 붙이면 이진 모드(r/w와 동일한 규칙)(r/w와 동일한 규칙)

시험 함정: “w 모드로 기존 파일을 열면 내용을 유지한 채 이어 쓴다”는 설명은 옳지 않은 것 고르기의 단골 오답이다. 기존 내용을 유지하며 뒤에 이어 쓰려면 a(append) 모드를 써야 하고, w 모드는 기존 내용을 즉시 전부 삭제한다.

파일 열기 실패를 반드시 확인해야 하는 이유

#include <stdio.h> int main(void) { FILE *fp = fopen("없는파일.txt", "r"); if (fp == NULL) { printf("파일을 열 수 없습니다.\n"); return 1; } /* 파일이 성공적으로 열렸을 때만 실행되는 코드 */ fclose(fp); return 0; }
파일을 열 수 없습니다.

한 줄씩 실행 추적: 존재하지 않는 파일을 r 모드로 열려고 시도하면 fopen은 파일을 만들지 않고 즉시 NULL을 반환한다. fp == NULL 검사를 하지 않고 곧바로 fscanf(fp, ...) 같은 코드를 실행하면, fp가 어떤 파일도 가리키지 않는 상태에서 함수를 호출하는 정의되지 않은 동작(undefined behavior, 표준이 결과를 보장하지 않는 위험한 상태)이 발생해 프로그램이 예기치 않게 종료될 수 있다. 따라서 fopen 직후 반환값이 NULL인지 검사하는 코드는 실전 코드는 물론 시험 문제에서도 “빠지면 틀리는” 필수 패턴으로 취급된다.

텍스트 파일 읽기·쓰기: fprintf와 fscanf

**fprintf**와 **fscanf**는 04편에서 배운 printf, scanf와 사용법이 거의 같다. 차이는 딱 하나, 어디로 출력하고 어디서 입력받을지를 지정하는 FILE 포인터를 첫 번째 인자로 추가한다는 점뿐이다.

FILE *fp; int fprintf(FILE *stream, const char *format, ...); int fscanf(FILE *stream, const char *format, ...);

fprintf는 성공하면 출력한 문자 개수를, fscanf는 성공적으로 읽어들인 항목 개수를 반환한다(04편에서 배운 scanf의 반환값 규칙과 동일하다).

#include <stdio.h> int main(void) { FILE *fp = fopen("점수.txt", "w"); if (fp == NULL) { return 1; } fprintf(fp, "%s %d\n", "김민준", 90); fprintf(fp, "%s %d\n", "이서연", 85); fclose(fp); fp = fopen("점수.txt", "r"); if (fp == NULL) { return 1; } char 이름[20]; int 점수; while (fscanf(fp, "%s %d", 이름, &점수) == 2) { printf("%s: %d\n", 이름, 점수); } fclose(fp); return 0; }
김민준: 90점 이서연: 85점

한 줄씩 실행 추적:

단계실행 위치상태
1fopen(..., "w")”점수.txt”를 쓰기 모드로 새로 생성
2fprintf 두 번파일에 “김민준 90\n이서연 85\n” 내용 기록
3fclose(fp)버퍼 내용을 디스크에 반영하고 스트림 닫기
4fopen(..., "r")같은 파일을 읽기 모드로 다시 열기
5fscanf 1회차”김민준”을 이름에, 90을 점수에 저장. 2개 항목을 읽었으므로 반환값 2
6반복문 조건 확인반환값이 2이므로 참, 반복 계속
7fscanf 2회차”이서연”, 85를 읽어 반환값 2
8fscanf 3회차더 이상 읽을 내용이 없어 EOF 반환(2와 다르므로 반복 종료)

fscanf의 반환값을 반복문 조건으로 쓰는 이 패턴은 “파일 끝까지 반복해서 읽기”의 표준 관용구로, 시험에서 자주 나오는 코드 형태다.

시험 함정: “fscanf가 파일의 끝에 도달하면 0을 반환한다”는 설명은 틀렸다. 파일의 끝에 도달해 더 이상 읽을 데이터가 없으면 EOF(보통 -1)를 반환한다. 0을 반환하는 경우는 형식에 맞지 않는 데이터를 만나 하나도 읽지 못했을 때이며, 이는 파일 끝 도달과는 다른 상황이다.

이진 파일 읽기·쓰기: fread와 fwrite

텍스트 파일(text file)은 사람이 읽을 수 있는 문자로 데이터를 저장하고, 이진 파일(binary file)은 변수의 메모리 표현(비트 패턴)을 그대로 저장한다. 예를 들어 정수 12345를 텍스트 파일에 저장하면 1, 2, 3, 4, 5라는 5개의 문자 코드로 저장되지만, 이진 파일에 저장하면 int가 차지하는 4바이트의 비트 패턴이 그대로 저장된다.

이진 파일을 다룰 때는 fscanf/fprintf 대신 fread/**fwrite**를 쓴다.

size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream); size_t fread(void *ptr, size_t size, size_t count, FILE *stream);
  • ptr: 쓸 데이터가 있는(또는 읽은 데이터를 저장할) 메모리 주소.
  • size: 데이터 원소 하나의 크기(바이트).
  • count: 원소 개수.
  • 반환값: 실제로 읽거나 쓴 원소의 개수. 요청한 count보다 적게 반환되면 파일 끝에 도달했거나 오류가 발생한 것이다.
#include <stdio.h> struct 학생 { char 이름[20]; int 점수; }; int main(void) { struct 학생 s1 = {"박지훈", 95}; FILE *fp = fopen("학생.bin", "wb"); if (fp == NULL) { return 1; } fwrite(&s1, sizeof(struct 학생), 1, fp); fclose(fp); struct 학생 s2; fp = fopen("학생.bin", "rb"); if (fp == NULL) { return 1; } size_t 읽은개수 = fread(&s2, sizeof(struct 학생), 1, fp); fclose(fp); if (읽은개수 == 1) { printf("%s, %d\n", s2.이름, s2.점수); } return 0; }
박지훈, 95

fwrite(&s1, sizeof(struct 학생), 1, fp);는 “s1의 주소부터, 구조체 학생 하나 크기만큼, 1개를 파일에 쓰라”는 뜻이다. 구조체 전체가 멤버 하나하나 변환되는 과정 없이 메모리에 있는 그대로(구조체의 패딩까지 포함해) 파일에 저장된다는 점이 텍스트 방식과 근본적으로 다르다. 읽을 때도 fread가 파일의 바이트를 그대로 s2의 메모리 위치에 복사하므로, 저장할 때와 같은 구조체 정의를 쓰는 프로그램에서만 올바르게 해석된다.

비교 항목텍스트 파일(fprintf/fscanf)이진 파일(fwrite/fread)
저장 형태사람이 읽을 수 있는 문자(숫자도 문자로 변환)변수의 메모리 비트 패턴 그대로
파일 크기자료형에 따라 문자 개수만큼(가변적)자료형 크기만큼 고정적
사람이 열어 확인텍스트 편집기로 바로 확인 가능편집기로 열면 알아볼 수 없는 문자로 보임
변환 과정있음(숫자를 문자로, 문자를 숫자로)없음(메모리 내용 그대로 복사)
이식성상대적으로 높음낮음(CPU·컴파일러의 바이트 순서·패딩에 의존)

파일 끝 확인: feof와 오류 확인

파일의 끝에 도달했는지 확인하는 함수로 **feof**가 있다.

int feof(FILE *stream);

feof는 스트림에서 마지막으로 시도한 읽기 연산이 파일 끝을 만나 실패했을 때만 참(0이 아닌 값)을 반환한다.

시험 함정: while (!feof(fp))로 파일 끝까지 반복하는 코드는 한 줄을 더 처리하는 흔한 오류를 일으킨다. feof는 읽기를 시도해서 실패해야 비로소 참이 되므로, 마지막 데이터를 성공적으로 읽은 시점에는 아직 파일 끝에 도달했다는 신호가 켜지지 않는다. 그 상태에서 루프 조건을 통과해 본문을 한 번 더 실행하면, 이미 처리한 마지막 데이터를 중복 처리하는 오류가 생긴다. 앞서 본 while (fscanf(...) == 2)처럼 읽기 함수 자체의 반환값으로 반복을 제어하는 것이 올바른 관용구다.

자주 틀리는 점

  • fopen의 반환값이 NULL인지 확인하지 않고 바로 파일 포인터를 사용해, 정의되지 않은 동작을 일으키는 코드를 정상으로 착각하는 경우가 많다.
  • w 모드가 기존 파일 내용을 완전히 지운다는 사실을 놓쳐, “이어 쓰기”가 필요한 상황에 w를 잘못 선택하는 문제가 자주 나온다.
  • feof를 반복문의 직접적인 조건으로 쓰면 마지막 데이터가 중복 처리되는 오류가 난다는 점을 놓친다.
  • 텍스트 모드와 이진 모드를 섞어 쓰는 경우(예: fwrite로 저장한 파일을 fscanf로 읽으려는 시도)는 데이터가 올바르게 해석되지 않는다.
  • 파일을 다 쓴 뒤 fclose를 호출하지 않아, 버퍼에 남은 내용이 디스크에 반영되지 않는 문제를 놓친다.

핵심 정리

  • 파일은 FILE 포인터로 대표되는 스트림을 통해 다루며, fopen으로 열고 fclose로 닫는다.
  • fopen은 실패하면 NULL을 반환하므로, 파일을 연 직후 반드시 NULL 검사를 해야 한다.
  • w 모드는 기존 내용을 지우고 새로 쓰고, a 모드는 기존 내용 뒤에 이어 쓴다.
  • 텍스트 파일은 fprintf/fscanf로, 이진 파일은 fread/fwrite로 다루며, 이진 파일은 변수의 메모리 비트 패턴을 그대로 저장한다.
  • 파일 끝까지 반복해서 읽을 때는 feof가 아니라 읽기 함수 자체의 반환값을 반복 조건으로 써야 마지막 데이터 중복 처리를 피할 수 있다.

마무리 복습

문제 14지선다
fopen 함수의 반환값에 대한 설명으로 옳은 것은?
문제 24지선다
이미 내용이 들어 있는 파일을 w 모드로 열었을 때 일어나는 일은?
문제 34지선다
fscanf 함수가 파일의 끝에 도달해 더 이상 읽을 데이터가 없을 때 반환하는 값은?
문제 44지선다
텍스트 파일과 이진 파일의 차이에 대한 설명으로 옳지 않은 것은?
문제 54지선다
while (!feof(fp))를 파일 전체를 읽는 반복문의 조건으로 사용할 때 흔히 발생하는 문제는?
문제 64지선다
fwrite(&s1, sizeof(struct 학생), 1, fp) 호출에서 세 번째 인자 1이 의미하는 것은?
문제 74지선다
fclose를 호출하지 않고 프로그램을 종료했을 때 발생할 수 있는 문제로 옳은 것은?

참고 자료

  • C프로그래밍 강의 노트 – 구조체·공용체·열거형 및 파일 입출력 성격의 대학 강의 자료 — 파일 스트림과 입출력 함수 사용 예제 설명에 참고
  • 국가평생교육진흥원 독학학위제: https://bdes.nile.or.kr 
Last updated on