헤더와 소스 파일을 나눠 관리할 수 있게 되었다면, 이제는 프로그램이 외부 세계와 데이터를 주고받는 방법을 익힐 차례입니다. C에서는 stdio.h가 표준 입출력과 파일 처리 기능을 제공합니다. 이번 글에서는 콘솔 입력(scanf, fgets)과 파일 읽기·쓰기(fopen, fgets, fprintf, fclose)를 묶어 실습합니다.
이번 글에서 새로 나오는 용어
- 표준 입력 (Standard Input): 프로그램이 기본으로 읽는 입력 스트림, 보통 키보드
- 표준 출력 (Standard Output): 프로그램이 기본으로 쓰는 출력 스트림, 보통 터미널
- 파일 포인터 (File Pointer):
FILE *형태로, 파일 위치와 상태를 보관하는 구조체 포인터 - 버퍼 (Buffer): 데이터를 임시로 저장해 두는 메모리 공간
핵심 개념
학습 메모
- 소요 시간: 70~80분
- 준비물: 함수, 배열, 포인터, 헤더 분리 경험
- 학습 목표: 표준 입력으로 값 읽기, 파일 열기/닫기, 텍스트 파일을 안전하게 처리하기
이번 글에서 다룰 흐름은 네 가지입니다.
scanf와fgets의 차이를 이해하고 적절한 상황에 고르기FILE *을 얻기 위해fopen을 사용하고, 사용 후 반드시fclose로 닫기- 파일을 한 줄씩 읽어 누적 계산을 수행하기
- 오류 상황을 체크하고 적절한 메시지를 출력하기
코드로 따라하기
표준 입력으로 값 읽기
#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는 터미널에서 직접 컴파일하고 다시 실행하는 흐름이 중요하니, 이번 글의 코드를 파일로 만들고 빌드-실행 사이클을 다시 따라가 보세요.
💬 댓글
이 글에 대한 의견을 남겨주세요