이 편은 실제 기출 문제가 아닙니다. 독학사 2단계 C프로그래밍 관련 reference/ 기출 원문은 확보되어 있지 않습니다. 아래 22개 문항은 국가평생교육진흥원 독학학위제 출제기준(평가영역) 중 “구조체·공용체·열거형”, “파일 입출력”, “동적 메모리 관리” 영역의 세부 항목을 근거로 새로 구성한 예상 문항입니다. 실제 시험의 문장·코드·보기 배열과 다를 수 있으므로, 코드를 암기하기보다 한 줄씩 실행을 추적하는 방법 자체를 익히는 데 써야 합니다.
회차별 실제 출제 문항 수·배점·시간은 매 공고마다 다를 수 있으므로, 응시 전 국가평생교육진흥원 공고 원문을 반드시 확인하십시오.
이 편이 다루는 범위와 접근법
이 편은 14편(구조체·공용체·열거형), 16편(파일 입출력), 17편(동적 메모리 할당), 18편(구조체·포인터·파일 통합)에서 다룬 내용을 하나로 묶어 문제 형태로 점검합니다. 이 세 영역은 독학사 C프로그래밍에서 가장 배점이 크고 실수가 잦은 구간이며, 크게 세 가지 문제 유형으로 나뉩니다.
- 출력 예측형 — 구조체 멤버 접근, 공용체의 메모리 공유, 파일에 쓰고 다시 읽는 코드, malloc으로 할당한 배열을 다루는 코드의 실행 결과를 정확히 추적하는 유형입니다. 반드시 변수 값이 한 줄씩 어떻게 바뀌는지 표를 그려 가며 확인해야 합니다.
- 오류 찾기형 — free 이후 포인터 사용(댕글링 포인터), free를 두 번 호출(이중 해제), malloc 이후 반환값을 확인하지 않는 코드, realloc 실패를 처리하지 않는 코드처럼 컴파일은 되지만 실행 시점에 위험한 코드를 찾는 유형입니다.
- 개념 비교형 — 구조체와 공용체의 차이, malloc과 calloc의 차이, 텍스트 모드와 이진 모드의 차이처럼 “겉보기에 비슷하지만 목적이 다른 개념 쌍”을 정확히 구분하는 유형입니다.
풀 때는 항상 다음 순서를 지키십시오. 먼저 코드에 등장하는 변수와 포인터를 나열하고, 한 줄씩 실행하면서 값이 바뀌는 시점을 적습니다. 구조체·공용체는 멤버가 메모리의 어느 위치에 자리 잡는지 머릿속으로(또는 표로) 그려 보고, 포인터가 나오면 “이 포인터가 지금 가리키는 대상이 유효한가”를 항상 확인하는 습관을 들이십시오. 해설은 정답 근거뿐 아니라 각 오답이 어떤 개념과 착각되어 나온 오답인지까지 적어 두었으니, 틀린 문항은 해당 개념이 다뤄진 14·16·17·18편으로 돌아가 다시 확인하십시오.
마무리 복습
유형 1 — 구조체 정의·멤버 접근·메모리 배치
이 유형은 이렇게 접근한다: 구조체(struct)는 서로 다른 자료형을 하나로 묶어 새 자료형을 만들지만, 멤버들이 메모리에 차례로 놓일 때 자료형별 정렬(alignment) 규칙 때문에 패딩(padding) 이라는 빈 바이트가 끼어들 수 있습니다. 그래서 구조체의 크기는 “멤버 크기의 단순 합”이 아니라 “정렬 규칙까지 반영한 합”입니다.
다음 코드로 구조체 포인터의 두 접근 방식을 확인해 보겠습니다.
#include <stdio.h>
struct Point {
int x;
int y;
};
int main(void) {
struct Point p1 = {3, 4};
struct Point *pp = &p1;
pp->x = pp->x + 10;
(*pp).y = (*pp).y * 2;
printf("%d %d\n", p1.x, p1.y);
return 0;
}13 8#include <stdio.h>
struct Sample {
char c;
int i;
char c2;
};
int main(void) {
printf("%d\n", (int)sizeof(struct Sample));
return 0;
}유형 2 — 공용체·열거형
이 유형은 이렇게 접근한다: 공용체(union)는 모든 멤버가 같은 메모리 주소를 공유합니다. 그래서 한 멤버에 값을 쓰면 다른 멤버로 읽을 때 그 비트 패턴을 다른 자료형으로 재해석한 값이 나옵니다. 열거형(enum)은 이름 붙인 정수 상수의 모음이며, 값을 명시하지 않은 멤버는 바로 앞 멤버보다 1 큰 값을 자동으로 받습니다.
#include <stdio.h>
union Data {
int i;
char c[4];
};
int main(void) {
union Data d;
d.i = 65;
printf("%d %c\n", d.i, d.c[0]);
return 0;
}#include <stdio.h>
enum Weekday { MON, TUE, WED, THU, FRI, SAT = 10, SUN };
int main(void) {
printf("%d %d %d\n", WED, SAT, SUN);
return 0;
}유형 3 — 구조체 배열·포인터·함수 인자
이 유형은 이렇게 접근한다: 구조체를 함수 인자로 넘길 때 아무 표시 없이 그냥 넘기면 구조체 전체가 복사되어 함수 안에서 값을 바꿔도 원본은 그대로입니다. 원본을 바꾸려면 반드시 구조체의 주소(포인터)를 넘겨야 합니다.
#include <stdio.h>
struct Student {
char name[10];
int score;
};
int main(void) {
struct Student list[3] = {
{"Kim", 80},
{"Lee", 90},
{"Park", 70}
};
int sum = 0;
for (int i = 0; i < 3; i++) {
sum += list[i].score;
}
printf("%d\n", sum);
return 0;
}#include <stdio.h>
struct Point { int x, y; };
int main(void) {
struct Point arr[3] = {{1, 1}, {2, 2}, {3, 3}};
struct Point *p = arr;
p++;
p->x += 10;
printf("%d %d %d\n", arr[0].x, arr[1].x, arr[2].x);
return 0;
}#include <stdio.h>
struct Point { int x, y; };
void moveByValue(struct Point p) {
p.x += 100;
}
int main(void) {
struct Point a = {1, 2};
moveByValue(a);
printf("%d\n", a.x);
return 0;
}유형 4 — 파일 입출력 기초
이 유형은 이렇게 접근한다: fopen은 모드 문자열에 따라 동작이 크게 달라집니다. “r”은 파일이 반드시 있어야 하고, “w”는 있어도 내용을 지우며, “a”는 없으면 만들고 있으면 끝에 이어 씁니다. 파일을 다루는 모든 코드는 fopen이 NULL을 반환할 가능성을 먼저 의심해야 합니다.
#include <stdio.h>
int main(void) {
FILE *fp = fopen("data.txt", "w");
fprintf(fp, "%d %d\n", 3, 5);
fclose(fp);
int a, b;
fp = fopen("data.txt", "r");
fscanf(fp, "%d %d", &a, &b);
fclose(fp);
printf("%d\n", a + b);
return 0;
}#include <stdio.h>
int main(void) {
FILE *fp = fopen("not_exist.txt", "r");
char buf[100];
fscanf(fp, "%s", buf);
fclose(fp);
return 0;
}유형 5 — 파일 입출력 심화(이진 파일)
이 유형은 이렇게 접근한다: 구조체나 실수처럼 이진 데이터를 있는 그대로 저장·복원하려면 텍스트 모드가 아니라 이진 모드(“b” 포함) 로 파일을 열고, fscanf·fprintf 대신 fread·fwrite 를 써야 합니다.
#include <stdio.h>
struct Item { int id; double price; };
int main(void) {
struct Item a = {1, 1000.0};
FILE *fp = fopen("item.bin", "wb");
fwrite(&a, sizeof(struct Item), 1, fp);
fclose(fp);
struct Item b;
fp = fopen("item.bin", "rb");
fread(&b, sizeof(struct Item), 1, fp);
fclose(fp);
printf("%d %.1f\n", b.id, b.price);
return 0;
}유형 6 — 동적 메모리 할당: malloc·calloc·realloc
이 유형은 이렇게 접근한다: malloc은 요청한 크기만큼 힙(heap) 메모리를 할당하지만 내용을 초기화하지 않고, calloc은 할당과 동시에 모든 바이트를 0으로 채웁니다. realloc은 기존 데이터를 보존한 채 크기만 조정합니다.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int n = 5;
int *arr = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
arr[i] = i * i;
}
int sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
printf("%d\n", sum);
free(arr);
return 0;
}#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *arr = (int *)malloc(3 * sizeof(int));
arr[0] = 1; arr[1] = 2; arr[2] = 3;
arr = (int *)realloc(arr, 5 * sizeof(int));
arr[3] = 4;
arr[4] = 5;
int sum = 0;
for (int i = 0; i < 5; i++) sum += arr[i];
printf("%d\n", sum);
free(arr);
return 0;
}#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *p = (int *)malloc(sizeof(int));
*p = 10;
free(p);
printf("%d\n", *p);
return 0;
}유형 7 — 동적 메모리 오류: 누수·이중 해제·댕글링 포인터
이 유형은 이렇게 접근한다: 컴파일은 되지만 실행 시점에 위험한 코드를 찾는 문제입니다. “할당한 메모리를 되찾을 방법이 사라졌는가(누수)”, “이미 해제한 메모리를 또 해제하는가(이중 해제)”, “이미 사라진 지역 변수의 주소를 계속 쓰는가(댕글링)“를 순서대로 점검하십시오.
#include <stdio.h>
#include <stdlib.h>
void makeArray(int n) {
int *arr = (int *)malloc(n * sizeof(int));
for (int i = 0; i < n; i++) {
arr[i] = i;
}
printf("%d\n", arr[0]);
}
int main(void) {
makeArray(5);
return 0;
}#include <stdio.h>
int *makePointer(void) {
int local = 42;
return &local;
}
int main(void) {
int *p = makePointer();
printf("%d\n", *p);
return 0;
}#include <stdlib.h>
int main(void) {
int *p = (int *)malloc(sizeof(int));
*p = 1;
free(p);
free(p);
return 0;
}참고 자료
- 국가평생교육진흥원 독학학위제: https://bdes.nile.or.kr
- cppreference — C 표준 라이브러리·문법 레퍼런스: https://en.cppreference.com/w/c