
💡 1분 핵심 요약 (TL;DR)
- ✅ 구조체는 서로 다른 타입의 데이터를 하나로 묶어 새로운 자료형을 만드는 문법이다
- ✅ 구조체 멤버 접근은 점(.) 연산자를 쓴다 — student.name, student.score
- ✅ 파일 입출력은 열기 → 처리 → 닫기 3단계가 기본이며, 열기와 닫기는 반드시 짝을 맞춰야 한다
- ✅ fopen의 모드에 따라 읽기(r)/쓰기(w)/추가(a)가 결정되고, 실패하면 NULL을 반환한다
- ✅ fscanf는 EOF(파일의 끝)를 반환값으로 확인할 수 있어, 데이터 개수를 모를 때 반복문과 함께 쓰기 좋다
01 구조체란 무엇인가
하나의 값으로 표현하기 애매한 데이터를 묶는 방법
일상 속 대부분의 사물은 여러 정보를 함께 가지고 있습니다. 사람이라면 이름, 키, 몸무게 같은 정보가 있고, 나무라면 종류, 크기, 나이 같은 정보가 있습니다. 이런 데이터를 int나 char 같은 단일 타입 하나로 표현하기는 어렵습니다. 이럴 때 여러 개의 변수를 하나로 묶어 새로운 자료형을 직접 만들 수 있는 문법이 바로 구조체(struct)입니다.

📖 구조체 정의 기본 구조
struct 구조체이름 { 타입 속성1; 타입 속성2; ... };
함수 정의와 생김새가 비슷하지만, 앞에 struct가 붙고 반환값(return)이 없다는 점이 다릅니다. 이렇게 정의한 구조체는 int, double처럼 하나의 타입으로 사용할 수 있게 됩니다.
02 구조체 선언과 사용법
점(.) 연산자로 멤버에 접근한다
학생의 이름과 점수를 담는 Student 구조체를 만들어보겠습니다. 값을 채우는 방법은 순서대로 나열하는 방법과, 필드 이름을 직접 지정하는 방법 두 가지가 있습니다.
구조체 생성과 두 가지 초기화 방법
#include <stdio.h>
#include <string.h> // strcpy 함수 사용을 위해 필수
struct Student {
char name[20];
int score;
};
int main() {
struct Student a = {"Spencer", 100}; // 순서대로 초기화
struct Student b = {.name = "Dpencer", .score = 90}; // 이름을 지정해서 초기화
struct Student c = {.score = 75, .name = "Cpencer"}; // 순서를 바꿔도 된다
printf("%s %d\n", a.name, a.score);
printf("%s %d\n", b.name, b.score);
printf("%s %d\n", c.name, c.score);
a.score -= 15; // 점수 필드에 접근해서 직접 수정
strcpy(a.name, "Mark"); // 문자열 필드는 strcpy로 수정
printf("수정 후 -> %s %d\n", a.name, a.score);
return 0;
}
▶ 실행 결과
Spencer 100
Dpencer 90
Cpencer 75
수정 후 -> Mark 85
✓ 이름을 지정하면 순서가 바뀌어도 안전하다
{.name = ..., .score = ...}처럼 필드명을 직접 지정하면, 나열 순서가 바뀌어도 어떤 값이 어떤 필드로 들어가는지 명확합니다. 필드가 많아질수록 순서대로 초기화하는 방식은 헷갈리기 쉬우므로, 이 방식이 더 안전합니다.
코드 뒷부분에서는 점(.) 연산자로 만들어진 구조체 데이터에 접근해서 값을 직접 수정했습니다. a.score -= 15;로 점수를 깎았고, a.name은 배열이라 strcpy로 내용을 바꿨습니다.
📖 name 필드는 왜 strcpy로 바꿔야 할까
a.name = "Mark";처럼 문자열을 바로 대입할 수는 없습니다. name은 char 배열이고, 배열 이름은 재대입이 불가능한 포인터 상수이기 때문입니다(배열 강의에서 다룬 내용과 같은 원리입니다). 그래서 배열 안의 내용을 바꾸려면 strcpy 같은 문자열 함수를 사용해야 합니다.
03 구조체 배열 만들기
여러 명의 학생 데이터를 한 번에 관리하자
학생이 한 명뿐이라면 문제없지만, 여러 명이라면 구조체 배열이 필요합니다. 만드는 방법은 일반 배열과 똑같습니다 — 구조체이름 배열이름[크기]; 형태입니다.
구조체 배열로 여러 학생 정보 입력받기
#include <stdio.h>
#define MAX_STUDENT 100 // 다음 강의에서 배울 매크로를 미리 사용해본다
struct Student {
char name[20];
int score;
};
int main() {
struct Student students[MAX_STUDENT];
int numStudent;
printf("학생은 몇 명입니까? ");
scanf("%d", &numStudent);
for (int i = 0; i < numStudent; i++) {
printf("%d번째 학생 정보 입력 (이름 점수): ", i);
scanf("%s %d", students[i].name, &students[i].score);
}
printf("=== 학생정보 출력 ===\n");
for (int i = 0; i < numStudent; i++) {
printf("%s : %d\n", students[i].name, students[i].score);
}
return 0;
}
▶ 실행 결과
학생은 몇 명입니까? 3
0번째 학생 정보 입력 (이름 점수): 스펜서 99
1번째 학생 정보 입력 (이름 점수): 홍길동 88
2번째 학생 정보 입력 (이름 점수): 마크 66
=== 학생정보 출력 ===
스펜서 : 99
홍길동 : 88
마크 : 66

📖 students[i].name에는 왜 &가 없을까
students[i].score는 정수형이라 주소가 필요해서 &를 붙였지만, students[i].name은 char 배열이라 이름 자체가 이미 주소값입니다. 배열과 scanf의 관계를 그대로 기억하고 있다면 자연스러운 부분입니다.
그런데 학생 수가 100명인데 매번 직접 입력하는 것은 비효율적입니다. 여기서 파일 입출력이 등장합니다 — 메모장에 학생 명단을 미리 적어두고, 프로그램이 그 파일을 읽어와 처리하도록 만들 수 있습니다.
04 파일 입출력의 기본 흐름
열기 → 처리 → 닫기, 이 세 단계가 전부다
파일을 다루는 프로그램은 예외 없이 세 단계를 거칩니다. 파일을 열고(open), 읽거나 쓰는 처리(read/write)를 한 다음, 반드시 닫아야(close) 합니다.
| ① 열기 (fopen) 파일 포인터를 생성한다 |
→ | ② 처리 (읽기/쓰기) fgets, fputs, fscanf, fprintf 등을 사용 |
→ | ③ 닫기 (fclose) 반드시 짝을 맞춰야 한다 |
📖 파일 포인터는 빨대와 같다
파일을 조작하려면 FILE* 타입의 파일 포인터가 필요합니다. 우유팩에 꽂은 빨대를 떠올리면 이해하기 쉽습니다. 빨대로 빨아들이면 안의 내용(데이터)을 읽어오는 것이고, 빨대로 무언가를 불어넣으면 안에 데이터가 채워지는 것과 같습니다. fopen()이 이 빨대를 만들어내는 함수입니다.

⚠️ 열었으면 반드시 닫아야 한다
fopen으로 열었는데 fclose를 빠뜨리면 리소스 누수가 발생할 수 있습니다. 여는 것과 닫는 것은 항상 세트라고 기억해두세요.
05 파일 열기 모드 총정리
r, w, a — 어떤 모드로 여느냐가 전부를 바꾼다
| 모드 | 의미 | 파일이 없을 때 / 있을 때 |
| r | 읽기 모드 | 없으면 NULL 반환 / 있으면 그대로 읽기 |
| w | 쓰기(생성) 모드 | 없으면 새로 생성 / 있으면 기존 내용을 전부 지우고 새로 씀 |
| a | 추가(append) 모드 | 없으면 새로 생성 / 있으면 기존 내용 뒤에 이어서 씀 |
| r+ / w+ / a+ | 읽기+쓰기 복합 모드 | 각 모드의 특성 + 반대 기능까지 함께 사용 |
| t / b | 텍스트 / 바이너리 모드 | t는 기본값(생략 가능), b는 이미지·동영상 등 이진 데이터용 |

일반적인 텍스트 파일을 다룰 때는 t가 기본값이라 굳이 명시하지 않아도 됩니다. 반면 이미지·영상처럼 내부가 순수 0과 1 데이터로 구성된 파일을 다룰 땐 rb, wb처럼 바이너리 모드를 명시해야 합니다.
06 파일에 문자열 쓰기 — fputs
쓰기 모드로 열어서 내용을 채워 넣는다
output.txt에 문자열 쓰기
#include <stdio.h>
int main() {
FILE *fp = fopen("output.txt", "w");
if (fp == NULL) {
printf("파일을 열지 못했습니다.\n");
return 1; // 0이 아닌 값은 비정상 종료를 의미한다
}
fputs("Hello World!", fp); // fprintf(fp, "Hello World!"); 로 써도 동일하게 동작한다
fclose(fp);
printf("파일 데이터가 생성되었습니다.\n");
return 0;
}
▶ 실행 결과
파일 데이터가 생성되었습니다.
(같은 폴더에 output.txt 파일이 생기고, 안에는 "Hello World!"가 들어있다)
⚠️ return 1이 의미하는 것
일반적으로 return 0은 정상 종료를, 0이 아닌 값(관례적으로 1)은 비정상 종료를 의미합니다. 파일을 못 열었을 때 return 1로 프로그램을 즉시 끝내는 것은 흔한 관례입니다.
07 파일에서 문자열 읽기 — fgets
모드만 r로 바꾸면 나머지 구조는 동일하다
방금 만든 output.txt를 다시 읽어보겠습니다. 구조는 쓰기 예제와 거의 동일하고, 모드와 처리 함수만 바뀝니다.
output.txt에서 문자열 읽기
#include <stdio.h>
int main() {
FILE *fp = fopen("output.txt", "r");
if (fp == NULL) {
printf("파일을 열지 못했습니다.\n");
return 1;
}
char data[100];
fgets(data, 100, fp);
fclose(fp);
printf("읽어온 데이터: %s\n", data);
return 0;
}
▶ 실행 결과
읽어온 데이터: Hello World!
⚠️ 왜 fscanf가 아니라 fgets를 선택했을까
처음엔 fscanf(fp, "%s", data);로 시도해볼 수도 있습니다. 하지만 fscanf는 scanf와 마찬가지로 공백을 만나면 읽기를 멈춥니다. "Hello World!"처럼 띄어쓰기가 포함된 문장이라면 "Hello"까지만 읽히고 "World!"는 놓치게 됩니다. 그래서 한 줄 전체(공백 포함)를 읽어야 하는 상황에서는 fgets를 쓰는 것이 맞습니다.
📖 fputs/fgets 짝 vs fprintf/fscanf 짝
fputs·fgets 대신 fprintf·fscanf로도 똑같은 결과를 만들 수 있습니다. 인자 순서가 조금 다른데, fprintf(파일포인터, 포맷, 값)처럼 파일 포인터가 맨 앞에, fscanf(파일포인터, 포맷, &변수)도 마찬가지로 파일 포인터가 맨 앞에 옵니다. 결과물은 동일해도, 상황에 따라 더 적합한 함수가 다르다는 점은 다음 섹션에서 확인해보겠습니다.
08 실전 예제 — 점수 평균 계산 후 새 파일 생성
fscanf와 EOF로 데이터 개수를 몰라도 끝까지 읽는다
score.txt에 공백으로 구분된 점수들이 들어있다고 해보겠습니다.
score.txt
10 20 30 40 50 60 70 80 90 100
이 데이터가 몇 개인지 미리 세어보지 않아도, fscanf가 EOF(파일의 끝)를 반환할 때까지 반복하면 전부 읽어올 수 있습니다.
EOF까지 반복하며 평균 구하고 새 파일 쓰기
#include <stdio.h>
int main() {
FILE *inFp = fopen("score.txt", "r");
if (inFp == NULL) {
printf("score.txt를 열지 못했습니다.\n");
return 1;
}
int num, sum = 0, count = 0;
while (fscanf(inFp, "%d", &num) != EOF) {
sum += num;
count++;
printf("현재 값: %d, 누적합: %d, 개수: %d\n", num, sum, count);
}
fclose(inFp);
double avg = (double)sum / count;
printf("평균: %.2lf\n", avg);
FILE *outFp = fopen("average.txt", "w");
if (outFp == NULL) {
printf("average.txt 생성에 실패했습니다.\n");
return 1;
}
fprintf(outFp, "Average = %.2lf", avg);
fclose(outFp);
printf("average.txt 파일이 생성되었습니다.\n");
return 0;
}
▶ 실행 결과
현재 값: 10, 누적합: 10, 개수: 1
현재 값: 20, 누적합: 30, 개수: 2
현재 값: 30, 누적합: 60, 개수: 3
... (생략) ...
현재 값: 100, 누적합: 550, 개수: 10
평균: 55.00
average.txt 파일이 생성되었습니다.
(average.txt 안에는 "Average = 55.00"이 들어있다)

✓ fscanf가 EOF를 확인할 수 있어서 유리한 상황
파일 안의 데이터 개수를 미리 모를 때는, "끝날 때까지 반복"하는 fscanf + EOF 조합이 정확한 횟수를 세는 for문보다 훨씬 편합니다. 반면 fgets는 한 줄 전체를 통째로 읽는 용도라, 공백으로 구분된 숫자를 하나씩 꺼내 계산해야 하는 지금 같은 상황에는 fscanf가 더 적합합니다.
⚠️ (double) 형변환을 빠뜨려도 티가 안 날 수 있다
만약 double avg = sum / count; 처럼 (double) 형변환 없이 작성했다면 어떻게 될까요? sum과 count가 둘 다 int라 정수 나눗셈이 먼저 일어난 뒤 double에 대입됩니다. 그런데 위 데이터는 550 ÷ 10 = 55로 나머지 없이 딱 떨어지기 때문에, 형변환을 빠뜨려도 결과가 우연히 55.00으로 똑같이 나옵니다. 데이터가 조금만 달라져 나누어떨어지지 않았다면 바로 오류가 드러났을 겁니다. 이런 "우연히 맞는 결과"에 속지 않으려면, 정수를 나눌 때는 습관적으로 (double) 형변환을 붙이는 것이 안전합니다.
09 구조체 + 파일 입출력 결합 — 학생 성적 처리
오늘 배운 두 가지를 한 번에 써보는 종합 예제
이번엔 student_list.txt 파일을 읽어서, 각 학생의 국어·수학 점수 합계와 평균을 구조체에 채운 다음, 그 결과를 student_list_v2.txt라는 새 파일로 저장해보겠습니다. 파일의 첫 줄에는 학생 수가 적혀있고, 그 아래로 "이름 국어점수 수학점수"가 한 줄씩 이어지는 형식입니다.
student_list.txt
3
spencer 20 30
max 30 40
john 40 50
학생 명단 파일을 읽어서 계산하고, 결과를 새 파일로 저장하기
#include <stdio.h>
#define MAX_STUDENT 100
struct Student {
char name[20];
int kor;
int math;
int total;
double avg;
};
int main() {
struct Student students[MAX_STUDENT];
int numStudent;
FILE *fp = fopen("student_list.txt", "r");
if (fp == NULL) {
printf("파일을 열지 못했습니다.\n");
return 1;
}
fscanf(fp, "%d", &numStudent); // 첫 줄: 학생 수부터 읽는다
printf("%d명 데이터를 가져옵니다.\n", numStudent);
for (int i = 0; i < numStudent; i++) {
fscanf(fp, "%s %d %d", students[i].name, &students[i].kor, &students[i].math);
students[i].total = students[i].kor + students[i].math;
students[i].avg = (double)students[i].total / 2.0; // 실수 나눗셈임을 명시
}
fclose(fp);
FILE *outFp = fopen("student_list_v2.txt", "w");
if (outFp == NULL) { // outFp를 검사한다 (fp를 다시 검사하지 않도록 주의)
return 1;
}
for (int i = 0; i < numStudent; i++) {
fprintf(outFp, "%s >> kor : %d, math : %d, total : %d, average : %.2lf\n",
students[i].name, students[i].kor, students[i].math,
students[i].total, students[i].avg);
}
fclose(outFp);
printf("student_list_v2.txt 파일이 생성되었습니다.\n");
return 0;
}
▶ 실행 결과
3명 데이터를 가져옵니다.
student_list_v2.txt 파일이 생성되었습니다.
(student_list_v2.txt 안에는 아래 세 줄이 들어있다)
spencer >> kor : 20, math : 30, total : 50, average : 25.00
max >> kor : 30, math : 40, total : 70, average : 35.00
john >> kor : 40, math : 50, total : 90, average : 45.00

⚠️ 코드가 길어지면 실제로 자주 나오는 실수들
이런 종합 예제를 직접 작성하다 보면 아래와 같은 실수가 특히 잦습니다. 미리 알아두면 디버깅 시간을 크게 줄일 수 있습니다.
① fscanf 포맷 문자열에 콤마를 넣는 실수 — "%s, %d, %d"처럼 콤마를 넣으면 파일 내용과 형식이 안 맞아 입력이 실패합니다. 공백으로 구분되어 있다면 포맷도 "%s %d %d"로 맞춰야 합니다.
② students[i].kor에 &를 빠뜨리는 실수 — kor, math는 배열이 아니라 int이므로 주소를 넘기려면 &가 반드시 필요합니다. name만 배열이라 &가 필요 없다는 걸 헷갈리지 않아야 합니다.
③ 반복문 안에서 인덱스 대신 계속 [0]만 쓰는 실수 — 복사-붙여넣기로 코드를 작성하다 보면 students[0]을 반복문 전체에서 그대로 쓰는 실수가 나오기 쉽습니다. 인덱스 변수(i)로 제대로 바뀌었는지 항상 확인해야 합니다.
④ 평균 필드를 int로 선언하는 실수 — avg를 int로 선언하면 소수점이 전부 잘려나갑니다. 평균은 double로 선언해야 합니다.
⑤ 출력용 파일 포인터를 열고 나서, 입력용 파일 포인터를 다시 검사하는 복붙 실수 — outFp를 새로 fopen한 다음 if (fp == NULL)처럼 앞서 쓰던 변수명을 그대로 복사해오면, 정작 outFp가 실패했는지는 확인하지 못합니다. 파일 포인터를 새로 열 때마다 그 포인터 자신을 검사하고 있는지 확인하는 습관이 필요합니다.
※ 이 글은 아래 동영상으로 학습한 내용을 요약한 글입니다.
출처 동영상: https://youtu.be/rpKMUJOR6_U?si=HCeDxKvL5YJ4dOd6
📌 핵심 정리
· 구조체 : 여러 타입의 변수를 하나로 묶어 새로운 자료형을 만드는 문법
· 멤버 접근 : 점(.) 연산자로 접근한다 — student.name, student.score
· 구조체 배열 : 일반 배열과 동일한 방식으로 만들 수 있다 — struct Student list[100];
· 파일 입출력 3단계 : 열기(fopen) → 처리(읽기/쓰기) → 닫기(fclose), 열고 닫기는 항상 세트다
· 파일 모드 : r(읽기)/w(쓰기, 덮어쓰기)/a(추가), 실패 시 NULL을 반환한다
· fgets/fputs : 한 줄 전체를 다룰 때, fscanf/fprintf : 형식이 정해진 데이터를 다룰 때 유리하다
· EOF : fscanf가 파일의 끝에 도달하면 반환하는 값으로, 데이터 개수를 몰라도 반복 종료 조건으로 쓸 수 있다
· 구조체와 파일 입출력을 결합하면, 외부 데이터를 읽어와 가공하고 다시 파일로 저장하는 실용적인 프로그램을 만들 수 있다
태그 : C언어 구조체struct 사용법C언어 파일 입출력fopen fclose 사용법fgets fputs 차이fscanf fprintf 사용법구조체 배열파일 모드 r w aEOF 활용법C언어 코딩 기초 강의
'학과 수업 노트 > C언어 기초 배우기' 카테고리의 다른 글
| 13. 문자열과 string.h (0) | 2026.07.18 |
|---|---|
| 12. 포인터 (0) | 2026.07.14 |
| 11. 배열 (0) | 2026.07.12 |
| 10. 함수 (0) | 2026.07.05 |
| 9. Switch Case문 (0) | 2026.07.03 |

