[C 시리즈 12편] 표준 입력과 파일로 데이터 읽고 쓰기

English version

헤더와 소스 파일을 나눠 관리할 수 있게 되었다면, 이제는 프로그램이 외부 세계와 데이터를 주고받는 방법을 익힐 차례입니다. C에서는 stdio.h가 표준 입출력과 파일 처리 기능을 제공합니다. 이번 글에서는 콘솔 입력(scanf, fgets)과 파일 읽기·쓰기(fopen, fgets, fprintf, fclose)를 묶어 실습합니다.

이번 글에서 새로 나오는 용어

  1. 표준 입력 (Standard Input): 프로그램이 기본으로 읽는 입력 스트림, 보통 키보드
  2. 표준 출력 (Standard Output): 프로그램이 기본으로 쓰는 출력 스트림, 보통 터미널
  3. 파일 포인터 (File Pointer): FILE * 형태로, 파일 위치와 상태를 보관하는 구조체 포인터
  4. 버퍼 (Buffer): 데이터를 임시로 저장해 두는 메모리 공간

핵심 개념

학습 메모

  • 소요 시간: 70~80분
  • 준비물: 함수, 배열, 포인터, 헤더 분리 경험
  • 학습 목표: 표준 입력으로 값 읽기, 파일 열기/닫기, 텍스트 파일을 안전하게 처리하기

이번 글에서 다룰 흐름은 네 가지입니다.

  1. scanffgets의 차이를 이해하고 적절한 상황에 고르기
  2. FILE *을 얻기 위해 fopen을 사용하고, 사용 후 반드시 fclose로 닫기
  3. 파일을 한 줄씩 읽어 누적 계산을 수행하기
  4. 오류 상황을 체크하고 적절한 메시지를 출력하기

코드로 따라하기

표준 입력으로 값 읽기

#include <stdio.h>

int main(void) {
    int age;
    double height;

    printf("나이와 키를 입력하세요: ");
    if (scanf("%d %lf", &age, &height) != 2) {
        printf("입력 형식이 올바르지 않습니다.\n");
        return 1;
    }

    printf("age = %d, height = %.1f\n", age, height);
    return 0;
}

scanf는 공백을 기준으로 값을 읽습니다. 반환값으로 성공적으로 읽은 항목 수를 알려 주므로, 항상 비교해 오류를 잡는 습관을 들이세요.

또 한 가지 주의할 점은, scanf를 사용한 뒤 입력 버퍼에 줄바꿈 문자가 남을 수 있다는 것입니다. 그래서 바로 다음 줄에 fgets를 쓰면 빈 줄이 읽히는 것처럼 보일 수 있습니다.

문자열 줄 단위로 읽기: fgets

#include <stdio.h>

int main(void) {
    char buffer[32];

    printf("이름을 입력하세요: ");
    if (fgets(buffer, sizeof(buffer), stdin) == NULL) {
        printf("입력 오류\n");
        return 1;
    }

    printf("입력된 이름: %s", buffer);
    return 0;
}

fgets는 공백을 포함한 한 줄을 통째로 읽습니다. 항상 배열 크기와 입력 버퍼 크기를 함께 넘겨서 버퍼 오버플로를 예방하세요. 마지막 줄바꿈 문자가 함께 들어올 수 있다는 점도 기억해 두세요.

예를 들어 scanf 다음에 fgets를 이어서 쓰고 싶다면, 중간에 남아 있는 개행을 먼저 비워 주는 패턴이 자주 쓰입니다.

int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
}

stdin은 표준 입력 스트림을 뜻합니다. 지금은 "키보드에서 들어오는 기본 입력 통로" 정도로 이해해도 충분합니다.

파일 열고 쓰기

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("report.txt", "w");
    if (fp == NULL) {
        printf("파일을 열 수 없습니다.\n");
        return 1;
    }

    fprintf(fp, "C study report\n");
    fprintf(fp, "score = %d\n", 92);

    fclose(fp);
    printf("report.txt 생성 완료\n");
    return 0;
}

"w" 모드는 파일을 새로 만들거나 기존 내용을 덮어씁니다. 파일 포인터가 NULL인지 먼저 확인한 뒤, 사용이 끝나면 반드시 fclose로 닫아야 합니다. 그렇지 않으면 버퍼에 남은 데이터가 저장되지 않은 채 프로그램이 끝날 수 있습니다. 실전에서는 perror("fopen")처럼 운영체제가 알려 주는 오류 메시지를 함께 출력하기도 합니다.

파일 읽기와 합계 계산

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("scores.txt", "r");
    int value;
    int total = 0;
    int count = 0;

    if (fp == NULL) {
        printf("scores.txt를 열 수 없습니다.\n");
        return 1;
    }

    while (fscanf(fp, "%d", &value) == 1) {
        total += value;
        count++;
    }

    fclose(fp);

    if (count > 0) {
        printf("평균 = %.1f\n", (double)total / count);
    } else {
        printf("데이터가 없습니다.\n");
    }

    return 0;
}

fscanf는 파일에서 값을 읽을 때 scanf와 같은 서식 지정자를 사용합니다. 반환값을 항상 확인해야 하며, 읽기 루프가 끝난 뒤에는 나눗셈이 0으로 나뉘지 않도록 조건을 두는 것이 안전합니다. 이 패턴은 "파일에 정수만 들어 있다"는 전제가 있을 때 특히 잘 맞고, 중간에 숫자가 아닌 문자열이 섞이면 그 지점에서 읽기가 멈출 수 있다는 점도 기억해 두세요.

한 줄씩 읽으며 문자열 처리하기

#include <stdio.h>

int main(void) {
    FILE *fp = fopen("names.txt", "r");
    char line[64];
    int line_number = 1;

    if (fp == NULL) {
        printf("names.txt를 열 수 없습니다.\n");
        return 1;
    }

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

    fclose(fp);
    return 0;
}

이 예제는 줄바꿈 문자를 포함한 문자열을 그대로 출력합니다. fgets 결과가 NULL이면 파일 끝이거나 오류이므로 루프를 멈춥니다.

왜 중요한가

입출력은 프로그램과 사용자, 프로그램과 다른 시스템을 이어 주는 다리입니다. 표준 입력/출력을 통해 테스트 데이터를 빠르게 주고받을 수 있고, 파일 입출력을 익히면 간단한 로그, 설정, 보고서를 직접 생성할 수 있습니다. 또한 FILE * 관리 습관을 들이면 이후 동적 메모리나 시스템 호출을 다룰 때도 "자원을 얻으면 반드시 해제한다"는 규칙을 자연스럽게 적용할 수 있습니다.

CodeSandbox로 이어서 실습하기

아래 샌드박스는 CodeSandbox의 Universal starter입니다. C는 터미널에서 직접 컴파일하고 다시 실행하는 흐름이 중요하니, 이번 글의 코드를 파일로 만들고 빌드-실행 사이클을 다시 따라가 보세요.

Live Practice

C Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Universal starterCterminal
  1. starter를 fork한 뒤 hello.c 같은 실습 파일을 만든다
  2. 본문 코드를 붙여 넣고 clang 또는 gcc가 있으면 직접 컴파일한다
  3. 코드를 고친 뒤 다시 빌드하고 실행 결과를 비교한다

C는 브라우저 미리보기보다 터미널 빌드 흐름이 핵심입니다. Universal starter의 컴파일러 구성이 환경에 따라 다를 수 있으니, 먼저 clang이나 gcc 사용 가능 여부부터 확인하세요.

실습

  • 따라 하기: scanf, fgets, fprintf, fscanf 예제를 각각 실행해 입력/출력 흐름을 기록합니다.
  • 확장하기: 사용자에게 파일 이름을 입력받아, 해당 파일에서 점수를 읽어 평균을 계산하도록 변형해 보세요.
  • 디버깅: fclose를 일부러 빼고 프로그램을 실행한 뒤, 파일 내용이 왜 비어 있을 수 있는지 설명합니다.
  • 완료 기준: 표준 입력과 파일 입출력의 차이, FILE *를 얻고 닫는 순서를 각각 한 문장으로 설명할 수 있으면 됩니다.

마무리

이번 글에서는 표준 입출력과 파일 처리로 데이터를 읽고 쓰는 기본 흐름을 연습했습니다. 다음 글에서는 동적 메모리 할당으로 프로그램 실행 중에 필요한 크기만큼 메모리를 예약하고 해제하는 방법을 살펴보겠습니다.

💬 댓글

이 글에 대한 의견을 남겨주세요