C 문자열 · 구조체 · 재귀 E03

글자는 숫자이고, 구조체는 칸의 묶음이고, 재귀는 되감기다. 세 원리로 C 기출 16문항이 풀린다

왜 이 장을 하나
C 의 나머지 절반이다. 문자열 문제는 글자가 곧 코드 값이라는 것에서, 구조체 · 연결 리스트 문제는 화살표를 따라가는 그림에서, 재귀 문제는 가장 깊은 호출부터 되감는 표에서 답이 나온다. 11회 중 10회에 나온 장이라 3회에도 나온다고 보고 만든다.
다루는 것문자 코드 · 문자열 함수 · 구조체 · 연결 리스트 · 큐 · 재귀 · 트리
문항기출 16 · 예상 2 · 훈련 10
끝나면연결 리스트를 화살표로 그려 순회한다

01왜 이 장인가

왜 이걸 하나 — C 42문항 중 16문항이 이 장이고, 2025년에는 세 회 연속 연결 리스트가 나왔다.

11회분 220문항 중 이 장의 기출은 16문항이다. 11회 중 10회에 나왔고(2023년 2회만 빠짐), 2025년 2회에는 4문항(환형 큐 · 이중 포인터 구조체 · 연결 리스트 · 동적 문자열 역순)이 몰렸다. 훈련 C 10문항(7~13 · 17~19)이 붙는다.

몇 문항무엇을 적나보기
문자열 · 문자 코드5글자 순회 · 대소문자 변환 · 역순 · 시저 암호2023-1회 3번 · 2024-1회 4번 · 19번 · 2025-2회 18번 · 2025-3회 7번
구조체 값 계산4멤버 접근 · 화살표 · 배열 구조체 · 함수 포인터 멤버2023-3회 4번 · 2024-1회 11번 · 2024-2회 19번 · 2026-1회 8번
연결 리스트 · 큐5순회 출력 · 교환 · 환형 큐 · 이중 포인터2024-3회 12번 · 2025-1회 18번 · 2025-2회 12번 · 14번 · 16번
재귀 · 트리2팩토리얼 · 후위 순회 세 번째 노드2023-3회 15번 · 2026-2회 7번

→ 연결 리스트가 5문항이다. 2024년 3회부터 2025년 2회까지 세 회 연속이었고, 2026년 2회 7번의 트리도 구조체 포인터 둘(a · b)이 자식을 가리키는 같은 꼴이다. 노드를 그리고 화살표를 따라가는 절차 하나로 여섯 문항이 풀린다.

E01 진단의 7번(트리 후위 순회, 답 12)이 이 장이다. 「재귀」와 「구조체 포인터」가 한 문제에 있어 어렵게 보이지만, 03절의 되감기 표와 화살표 그림을 합치면 순서가 하나로 정해진다.

문항 수는 이 노트의 문제 데이터에서 ch=E03 인 것을 센 값이다(2026-09-14). 기출 코드는 grandlife.co.kr 복원에서 옮겼고, 16문항 전부 gcc(도커 alpine)로 실행해 정답을 확인했다.

02한눈에

왜 이걸 하나 — 시험 전날 규칙 8개를 다시 본다.

글자는 숫자다'A' = 65, 'a' = 97, '0' = 48. 대문자 ↔ 소문자는 32 차이. c + 1 은 다음 글자, c - 'a' 는 알파벳 몇 번째. %c 로 찍으면 글자, %d 로 찍으면 숫자.
문자열의 끝\0 이 끝. while (s[i] != '\0') · while (*p) 가 끝까지 순회. strlen 은 \0 앞까지 개수, sizeof 는 \0 포함 바이트.
문자열 함수strcpy(a, b) 복사 · strcat(a, b) 이어 붙임 · strcmp(a, b) 같으면 0, 앞이 크면 양수 · strlen 길이 · toupper · tolower · isalpha · isdigit.
구조체 접근변수면 s.member, 포인터면 p->member(= (*p).member). typedef struct { … } Name; 로 이름을 만든다. 구조체 배열은 arr[i].member.
연결 리스트노드 = 값 + 다음 노드 주소(next). 순회는 for (p = head; p != NULL; p = p->next). 삽입 · 교환은 화살표를 다시 잇는 것. malloc 으로 노드를 만들면 free 로 돌려준다.
환형 큐front(꺼낼 자리) · rear(넣을 자리)가 (x + 1) % SIZE 로 돈다. 넣으면 rear 가, 꺼내면 front 가 움직인다. 두 값을 매 연산마다 적는다.
재귀종료 조건(if (n <= 1) return …)까지 내려간 뒤 되감으며 계산. 표를 아래(가장 깊은 호출)부터 채운다. 팩토리얼 · 피보나치 · 자릿수 합 · 트리 순회.
트리 순회전위(자기 → 왼 → 오른) · 중위(왼 → 자기 → 오른) · 후위(왼 → 오른 → 자기). 코드에서 func(n->a); func(n->b); 뒤에 자기 처리가 있으면 후위다.

03밑바닥

왜 이걸 하나 — 세 원리가 왜 그런지를 알면, 함수 이름이 낯설어도 코드가 무엇을 하는지 읽힌다.

3-1. 글자는 숫자라서 더하고 뺄 수 있다

C 의 char 는 8비트 정수다. 'A' 는 65, 'a' 는 97, '0' 은 48 이고 그 차이가 규칙을 만든다. 대문자에 32 를 더하면 소문자, c - '0' 은 글자 숫자를 진짜 숫자로, c + 3 은 알파벳 세 칸 뒤다. 2024년 1회 19번의 시저 암호가 이 덧셈이고, 범위를 넘으면 % 26 으로 돌린다.

순회는 \0 을 만날 때까지다. while (s[i] != '\0')while (*p) 든 같은 뜻이고, 2023년 1회 3번은 두 문자열을 이중 반복문으로 대조해 있는 글자만(qwe) 찍는다. 2024년 1회 4번은 마지막 첨자부터 두 칸씩 건너뛰어(6 · 4 · 2 · 0) GECA 를 만든다 — 첨자를 종이에 0 1 2 … 로 적고 조건을 대 보면 된다.

역순 출력은 두 가지 꼴로 나온다. 첨자를 len - 1 부터 내려오거나(훈련 8번), malloc 으로 새 칸을 잡아 거꾸로 복사하거나(2025년 2회 18번, TSEB). 어느 쪽이든 strlen 으로 길이를 먼저 재고 마지막 첨자가 len − 1 이라는 것이 출발점이다.

3-2. 구조체는 칸의 묶음이고 화살표는 「포인터가 가리키는 묶음의 칸」이다

struct Item { int price; int count; }; 는 두 칸을 한 묶음으로 만든 새 자료형이다. 변수면 item.price 로, 포인터면 p->price 로 칸을 연다 — 화살표는 (*p).price 의 줄임이고, 2023년 3회 4번의 빈칸이 바로 -> 다. 2024년 1회 11번은 구조체의 잔액에서 100 을 빼고 1.1 을 세 번 곱해 %.2f 로 2795.10 을 찍으므로 소수 둘째 자리까지 그대로 적는다.

2026년 1회 8번은 구조체 멤버가 함수 포인터다. int *(*fn)(int *) 는 「int 포인터를 받아 int 포인터를 돌려주는 함수」를 가리키는 포인터이고, s.fn(&x) 로 부른다. 선언이 낯설어도 괄호 안의 * 가 함수 포인터, 밖의 것이 반환형으로 읽으면 된다.

3-3. 연결 리스트는 화살표를 따라가고, 바꾸는 것은 화살표다

노드는 값 하나와 next 주소 하나로 된 구조체다. 순회는 head 에서 시작해 p = p->next 로 화살표를 따라가다 NULL 에서 멈춘다. 2025년 1회 18번은 노드 5개를 잇고 순회해 35421 을 찍는데, 이어 붙인 순서와 값의 순서가 다르다는 것이 함정이다 — 코드가 next 를 어디에 꽂는지를 그림에 그려야 한다.

교환 문제(2024년 3회 12번, 312)는 값을 바꾸는지 화살표를 바꾸는지를 먼저 본다. 값을 바꾸면 노드는 그대로이고 값만 오가고, 화살표를 바꾸면 노드의 순서가 바뀐다. 2025년 2회 16번(3 1 2)은 화살표를 두 번 다시 이은 뒤 head 를 c 로 잡는 꼴이라, 마지막에 이은 화살표가 순서를 정한다.

환형 큐(2025년 2회 12번, 2 그리고 3)는 배열에 front · rear 두 첨자를 두고 (x + 1) % SIZE 로 돌린다. 넣을 때 rear 가, 꺼낼 때 front 가 움직이므로 연산마다 두 값을 표로 적으면 무엇이 남는지 보인다. 이중 포인터 구조체(2025년 2회 14번, 5 그리고 6)는 **pp 가 「포인터의 포인터」라 두 번 따라가 원본 구조체에 닿는다.

3-4. 재귀는 내려갔다 되감는 표다

재귀 함수는 자기를 부르며 내려가다 종료 조건에서 멈추고, 거기서부터 값을 돌려주며 되감는다. 표를 아래부터 채운다 — factorial(1) = 1, (2) = 2, (3) = 6 … (7) = 5040(2023년 3회 15번). 자릿수 합(예상 1회 3번)은 n % 10 + f(n / 10) 로 뒤 자리부터 더한다.

트리 순회는 재귀와 구조체 포인터가 합쳐진 것이다. 2026년 2회 7번의 func(n->a); func(n->b); if (++c == 3) result = n->v;왼쪽 → 오른쪽 → 자기 순서라 후위 순회이고, 방문 순서 35 → 53 → 12 → 64 → 21 에서 세 번째인 12 가 답이다. 자기 처리가 두 재귀 호출 앞에 있으면 전위, 사이에 있으면 중위다.

통과 기준은 넷이다. 첫째, 'A' 65 · 'a' 97 · '0' 48 과 대소문자 32 차이로 글자 덧셈 문제를 푼다. 둘째, .-> 를 가르고 구조체 멤버 값을 계산한다. 셋째, 연결 리스트 · 환형 큐를 그림과 표로 순회한다. 넷째, 재귀를 되감기 표로 계산하고 트리 순회 3종을 코드에서 구분한다. 기출 16문항이 이 넷 안에 있다.
연결 리스트는 노드를 만든 순서대로 출력된다.next 를 어디에 꽂았느냐가 순서다. 2025년 2회 16번은 화살표를 다시 이어 head 를 c 로 잡아 3 1 2 가 되고, 2025년 1회 18번은 앞에 끼워 넣은 뒤 3 을 맨 앞으로 옮겨 35421 이다. 화살표를 그리지 않으면 틀린다.
재귀는 위에서부터 계산하면 된다.종료 조건까지 내려간 뒤 아래부터 되감아야 값이 정해진다. 2026년 2회 14번(Java)처럼 음수까지 내려가는 재귀는 가장 깊은 호출의 반환값(음수)이 위로 올라오며 답을 바꾼다.

04기출

왜 이걸 하나 — 그림(노드 · 화살표)과 표(재귀 되감기)를 종이에 그리며 푸는 연습이다.

문자열 문제는 첨자를 0 부터 적어 두고, 구조체 · 리스트는 노드를 상자로 그려 화살표를 잇고, 재귀는 아래부터 표를 채운다. 세 가지 종이 도구가 이 장의 답이다.

2023-1회 · 3번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> int main(void) { char *left = "qwer"; char *right = "qwtety"; for (int i = 0; left[i] != '\0'; i++) { for (int j = 0; right[j] != '\0'; j++) { if (left[i] == right[j]) { printf("%c", left[i]); } } } return 0; }
정답qwe

— qwe. 왼쪽 문자열의 글자를 하나씩 들고 오른쪽 문자열 전체와 견주어, 같은 글자를 만날 때마다 그 글자를 찍는다.

원리 — 이중 for 문은 "바깥 글자 하나마다 안쪽 문자열을 처음부터 끝까지 훑는다"는 뜻이다. 바깥은 left 의 q · w · e · r 네 글자, 안쪽은 right 의 q · w · t · e · t · y 여섯 글자라 비교는 4 × 6 = 24번 일어난다. 그중 두 글자가 같은 순간에만 printf 가 돈다. 그러니 답은 "left 의 글자 중 right 에도 있는 것"을 left 순서대로, 그리고 right 에 그 글자가 여러 번 있으면 그 횟수만큼 찍은 것이다. '\0' 은 문자열 끝표라 거기서 각 반복이 멈춘다.

따라가기

  1. i = 0, q — right 에 q 가 1개 → q
  2. i = 1, w — right 에 w 가 1개 → w
  3. i = 2, e — right 에 e 가 1개(q w t e t y) → e
  4. i = 3, r — right 에 r 없음 → 아무것도 안 찍힘
  5. 합쳐서 qwe

헷갈리는 자리 — right 에 t 가 두 개라 t 를 두 번 적는 것. t 는 left 에 없으므로 바깥 반복이 t 를 들고 오는 일이 없다. 찍히는 글자는 언제나 left 쪽 순서이고, 중복 횟수는 right 쪽 개수를 따른다는 두 규칙을 나눠 기억한다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2023-3회 · 4번C

다음 C 코드에서 구조체 포인터가 멤버에 접근하도록 ①에 들어갈 기호를 쓰시오.

#include <stdio.h> struct Member { int score; }; int main(void) { struct Member data = { 90 }; struct Member *pointer = &data; printf("%d", pointer ( ① ) score); return 0; }
정답->

->. 포인터를 거쳐 구조체의 멤버로 들어갈 때 쓰는 화살표다.

원리 — 구조체 변수에 바로 붙을 때는 점(data.score)을 쓴다. 그런데 pointer 는 구조체가 아니라 구조체의 주소를 적은 쪽지라, 먼저 쪽지를 따라가서 구조체를 연 다음(*pointer) 점을 붙여야 한다. 그 두 동작 (*pointer).score 를 한 글자로 줄인 것이 pointer->score 다. 화살표는 "따라가서 그 안의"라고 읽는다. 괄호가 필요한 이유는 점이 별표보다 먼저 붙어 *pointer.score 가 "pointer.score 를 연다"로 읽히기 때문이다.

따라가기

  1. struct Member data = {90} — data.score = 90
  2. pointer = &data — pointer 는 data 의 주소
  3. pointer->score → data.score → 90 이 찍힌다

헷갈리는 자리 — 답 칸에 점을 적는 것. 점은 구조체 변수 자체에만 붙고 포인터에는 못 붙는다(컴파일 오류). (*pointer).score 도 뜻은 같지만 문제는 "기호 하나"를 물었으므로 화살표만 적는다. 화살표는 빼기와 부등호를 붙여 쓰며 사이에 띄어쓰기가 없다.

빈칸에 답을 넣어 gcc(도커 alpine)로 컴파일 · 실행해 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2023-3회 · 15번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> int factorial(int number) { if (number <= 1) { return 1; } return number * factorial(number - 1); } int main(void) { printf("%d", factorial(7)); return 0; }
정답5040

— 5040. factorial(7) = 7 × 6 × 5 × 4 × 3 × 2 × 1 이다.

원리 — 재귀 함수는 자기 자신을 더 작은 문제로 부른다. "7 의 계승은 7 곱하기 6 의 계승"이고, "6 의 계승은 6 곱하기 5 의 계승"… 이렇게 내려가다 1 이하가 되면 더 부르지 않고 1 을 돌려준다(멈춤 조건). 그다음은 돌아오는 길이다 — 안쪽에서 돌려받은 값에 자기 수를 곱해 위로 올린다. 재귀는 "내려갈 때는 아무것도 계산하지 않고, 바닥에 닿은 뒤 올라오면서 곱한다"고 보면 값이 틀리지 않는다.

따라가기

  1. factorial(7) = 7 × factorial(6) … = 7 × 6 × 5 × 4 × 3 × 2 × factorial(1)
  2. factorial(1) → number ≤ 1 이라 1
  3. 올라오며: 2 × 1 = 2, 3 × 2 = 6, 4 × 6 = 24, 5 × 24 = 120, 6 × 120 = 720, 7 × 720 = 5040

헷갈리는 자리 — 멈춤 조건이 number <= 1 이라 factorial(0) 도 1 이고, 1 을 곱하는 단계가 하나 더 있다고 생각해 두 번 곱하는 것. 곱은 7 부터 2 까지 여섯 번이다. 계승은 빨리 커져 13! 부터는 int 를 넘치지만 7! 은 안전하다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2024-1회 · 4번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> #include <string.h> int main(void) { char word[] = "ABCDEFG"; int n = (int)strlen(word); for (int i = n - 1; i >= 0; i -= 2) putchar(word[i]); return 0; }
정답GECA

— GECA. 마지막 글자부터 두 칸씩 건너뛰며 앞으로 온다.

원리strlen("ABCDEFG") 은 7 이고 첨자는 0 부터 6 까지다. 반복은 i = n - 1(6, 마지막 글자)에서 시작해 i -= 2 로 두 칸씩 줄어들며 i >= 0 인 동안 돈다. 즉 6 · 4 · 2 · 0 네 자리를 차례로 찍는다. putchar 는 글자 하나를 줄바꿈 없이 찍는 함수다. "역순 + 건너뛰기"는 시작 첨자, 감소량, 멈춤 조건 세 가지를 적어 두면 틀릴 데가 없다.

따라가기

  1. 첨자: 0 A, 1 B, 2 C, 3 D, 4 E, 5 F, 6 G
  2. i = 6 → G, i = 4 → E, i = 2 → C, i = 0 → A
  3. i = −2 → 조건 i >= 0 거짓 → 끝. GECA

헷갈리는 자리 — n 을 7 로 두고 첨자 7 부터 시작하는 것(끝표 자리라 아무것도 안 찍히거나 엉뚱한 값). 마지막 첨자는 길이 − 1 이다. 그리고 i > 0 이었다면 0 번 A 가 빠져 GEC 가 된다 — 등호 유무를 본다. putchar 는 줄바꿈을 넣지 않으므로 네 글자가 한 줄에 붙고, strlen 을 쓰려면 string.h 가 있어야 한다는 것도 시험에서 빈칸으로 나올 수 있는 자리다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2024-1회 · 11번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> typedef struct { int balance; double savings; } Account; void withdraw(Account *a, int amount) { a->balance -= amount; } void add_interest(Account *a, double rate) { a->savings *= (1.0 + rate); } int main(void) { Account a = { 10000, 2713.69 }; withdraw(&a, 19); add_interest(&a, 0.03); printf("%d and %.2f", a.balance, a.savings); return 0; }
정답9981 and 2795.10

9981 and 2795.10. 잔액에서 19 를 빼고, 저축액에 3% 이자를 붙여 소수 둘째 자리까지 찍는다.

원리 — 함수에 구조체의 주소(&a)를 넘기면 함수 안에서 a->balance 처럼 원본의 칸을 직접 고칠 수 있다. 값을 넘겼다면 복사본만 바뀌어 main 의 a 는 그대로였을 것이다. a->savings *= (1.0 + rate) 는 "저축액에 1.03 을 곱해 다시 저축액에 넣어라"다. %d 는 정수를 그대로, %.2f 는 실수를 소수 둘째 자리에서 반올림해 찍는다. 문자열 안의 " and " 는 서식이 아니라 그대로 찍히는 글자다.

따라가기

  1. a = {balance 10000, savings 2713.69}
  2. withdraw(&a, 19) → balance = 10000 − 19 = 9981
  3. add_interest(&a, 0.03) → savings = 2713.69 × 1.03 = 2795.1007
  4. %.2f → 2795.10 → 화면 9981 and 2795.10

헷갈리는 자리 — 2795.1 로 적는 것. 서식이 둘째 자리까지라 끝의 0 도 적어야 한다. 이자를 더하기(2713.69 + 0.03)로 착각하는 것 — 곱셈 대입(*=)이다. 답에는 " and " 까지 문제의 출력 그대로 들어간다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2024-1회 · 19번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> #include <ctype.h> int main(void) { char text[] = "It is 8"; for (int i = 0; text[i] != '\x00'; i++) { if (isupper((unsigned char)text[i])) text[i] = (char)('A' + (text[i] - 'A' + 5) % 26); else if (islower((unsigned char)text[i])) text[i] = (char)('a' + (text[i] - 'a' + 10) % 26); else if (isdigit((unsigned char)text[i])) text[i] = (char)('0' + (text[i] - '0' + 3) % 10); } printf("%s", text); return 0; }
정답Nd sc 1

Nd sc 1. 대문자는 5칸, 소문자는 10칸, 숫자는 3칸 뒤로 미는 자리바꿈(시저 암호)이다.

원리 — 글자는 코드값이라 text[i] - 'A' 는 "A 로부터 몇 번째 글자인가"다. 거기에 밀 칸 수를 더하고 26 으로 나눈 나머지를 취하면 Z 를 넘어도 A 로 돌아온다. 마지막에 'A' + … 로 다시 글자로 만든다. 숫자도 같은 꼴로 '0' 기준 10칸이다. isupper · islower · isdigit 은 글자 종류를 가르는 함수이고, 공백은 셋 다 아니라 그대로 남는다. 세 종류가 밀리는 칸 수가 다르다는 것이 이 문제의 핵심이다.

따라가기

  1. I(대문자): (8 + 5) % 26 = 13 → 'A' + 13 = N
  2. t(소문자): (19 + 10) % 26 = 3 → 'a' + 3 = d
  3. 공백 → 그대로
  4. i: (8 + 10) % 26 = 18 → s, s: (18 + 10) % 26 = 2 → c
  5. 공백 → 그대로, 8(숫자): (8 + 3) % 10 = 1 → 1
  6. 합쳐서 Nd sc 1

헷갈리는 자리 — 대문자와 소문자를 같은 5칸으로 밀어 t 를 y 로 적는 것. 조건문이 셋으로 갈라진 이유가 밀 칸 수가 다르기 때문이다. s 는 18 + 10 = 28 이 26 을 넘어 2 로 돌아온다 — 나머지 연산을 잊으면 알파벳 밖으로 나간다. 공백 두 개도 답에 그대로 있다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2024-2회 · 19번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> struct Item { int price; int count; }; int main(void) { struct Item item = { 4, 5 }; struct Item *p = &item; printf("%d", p->price * p->count); return 0; }
정답20

— 20. 포인터 p 가 가리키는 구조체의 price 4 와 count 5 를 곱한다.

원리 — 구조체는 이름 붙은 칸들을 한 묶음으로 만든 것이다. {4, 5} 로 초기화하면 선언한 순서대로 price = 4, count = 5 가 들어간다. struct Item *p = &item 은 그 묶음의 주소를 p 에 적은 것이고, p->price 는 "p 를 따라가서 그 안의 price"다. 화살표 접근은 값을 복사하는 것이 아니라 원본 칸을 읽는 것이라 item.price 와 같은 값이다. 이 문제는 구조체 포인터 문법을 아는지 하나만 묻는다.

따라가기

  1. item = {price 4, count 5}
  2. p = &item
  3. p->price × p->count = 4 × 5 = 20

헷갈리는 자리 — 초기화 순서를 바꿔 읽는 것(price 가 먼저 선언됐으니 4 가 price). p.price 는 컴파일 오류이고 (*p).price 는 화살표와 같다. 곱셈이라 답은 20 이지 "4 5" 가 아니다. 구조체는 배열과 달리 칸마다 이름과 자료형이 따로 있어 첨자가 아니라 이름으로 꺼낸다 — 그래서 p[0] 같은 꼴은 안 쓰고 p->price 로 쓴다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2024-3회 · 12번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> struct Node { int value; struct Node *next; }; void swap_pairs(struct Node *node) { while (node != NULL && node->next != NULL) { int temp = node->value; node->value = node->next->value; node->next->value = temp; node = node->next->next; } } int main(void) { struct Node n1 = { 1, NULL }; struct Node n2 = { 2, NULL }; struct Node n3 = { 3, NULL }; n1.next = &n3; n3.next = &n2; swap_pairs(&n1); for (struct Node *p = &n1; p != NULL; p = p->next) { printf("%d", p->value); } return 0; }
정답312

— 312. 리스트가 1 → 3 → 2 순서로 이어져 있고, 앞의 두 노드 값만 맞바꾼 뒤 순서대로 찍는다.

원리 — 연결 리스트는 각 노드가 "다음 노드의 주소"를 들고 있는 사슬이다. 순서는 변수 이름(n1 · n2 · n3)이 아니라 next 가 정한다. n1.next = &n3; n3.next = &n2 이므로 사슬은 n1 → n3 → n2 → NULL 이다. swap_pairs 는 두 노드씩 짝지어 값(value)만 맞바꾸고, node = node->next->next 로 두 칸 건너뛴다. 남은 노드가 하나뿐이면(node->next == NULL) 짝이 없어 그대로 둔다. 사슬(next)은 안 바뀌고 값만 바뀐다는 것이 핵심이다.

따라가기

  1. 사슬: n1(1) → n3(3) → n2(2) → NULL
  2. node = n1: n1 과 n3 의 값 교환 → n1 = 3, n3 = 1. node = n3->next = n2
  3. node = n2: n2->next 가 NULL → 반복 끝(값 그대로 2)
  4. 출력: n1(3) → n3(1) → n2(2) → 312

헷갈리는 자리 — 사슬을 n1 → n2 → n3 으로 넘겨짚고 213 이라 적는 것. 초기화의 NULL 은 나중 대입으로 덮였으니 마지막 대입 두 줄이 사슬이다. 또 "교환"을 노드 순서 교환으로 읽는 것 — 이 코드는 값만 바꾼다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-1회 · 18번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> #include <stdlib.h> typedef struct Node { int value; struct Node *next; } Node; Node *push(Node *head, int value) { Node *node = malloc(sizeof(Node)); node->value = value; node->next = head; return node; } Node *move_front(Node *head, int target) { if (head == NULL || head->value == target) return head; Node *previous = NULL; Node *current = head; while (current != NULL && current->value != target) { previous = current; current = current->next; } if (current != NULL) { previous->next = current->next; current->next = head; head = current; } return head; } int main(void) { Node *head = NULL; for (int i = 1; i <= 5; i++) head = push(head, i); head = move_front(head, 3); for (Node *p = head; p != NULL; p = p->next) { printf("%d", p->value); } return 0; }
정답35421

— 35421. 1~5 를 앞에 끼워 5 4 3 2 1 이 된 리스트에서 3 을 떼어 맨 앞에 붙인다.

원리push 는 새 노드를 만들어 next 를 지금의 머리에 걸고 새 노드를 머리로 돌려준다. 앞에 끼우는 방식이라 1 · 2 · 3 · 4 · 5 순으로 넣으면 5 → 4 → 3 → 2 → 1 로 거꾸로 선다. move_front 는 목표 노드를 찾을 때까지 previous(바로 앞 노드)와 current 를 나란히 옮기고, 찾으면 앞 노드의 next 를 목표의 다음으로 잇고(사슬에서 빼기), 목표의 next 를 옛 머리로 걸고(맨 앞에 끼우기), 목표를 새 머리로 삼는다. 노드를 새로 만들지 않고 화살표 두 개만 바꾼다.

따라가기

  1. push 다섯 번: 5 → 4 → 3 → 2 → 1
  2. move_front(3): current 5(아님) → previous 5, current 4(아님) → previous 4, current 3(찾음)
  3. previous(4).next = 3.next = 2 → 5 → 4 → 2 → 1
  4. 3.next = 옛 머리 5, head = 3 → 3 → 5 → 4 → 2 → 1
  5. 출력 35421

헷갈리는 자리 — push 가 뒤에 붙인다고 보고 1 2 3 4 5 에서 시작하는 것. node->next = head 가 "새 노드 뒤에 옛 머리"라 앞 끼우기다. 그리고 목표를 뺀 뒤 앞 노드(4)의 next 를 2 로 잇는 줄을 빼먹으면 4 → 3 이 남아 3 이 두 번 나온다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-2회 · 12번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> #define SIZE 3 typedef struct { int data[SIZE]; int front; int rear; } Queue; void enqueue(Queue *queue, int value) { queue->data[queue->rear] = value; queue->rear = (queue->rear + 1) % SIZE; } int dequeue(Queue *queue) { int value = queue->data[queue->front]; queue->front = (queue->front + 1) % SIZE; return value; } int main(void) { Queue queue = { {0}, 0, 0 }; enqueue(&queue, 1); enqueue(&queue, 2); dequeue(&queue); enqueue(&queue, 3); int first = dequeue(&queue); int second = dequeue(&queue); printf("%d 그리고 %d", first, second); return 0; }
정답2 그리고 3

2 그리고 3. 크기 3 짜리 원형 큐에 1 · 2 를 넣고 하나 빼고 3 을 넣은 뒤 두 번 꺼내면 2 와 3 이다.

원리 — 큐는 줄서기다. 뒤(rear)로 들어가 앞(front)으로 나온다. 원형 큐는 칸이 끝에 닿으면 % SIZE 로 첫 칸으로 돌아와 다시 쓴다. enqueue 는 rear 칸에 넣고 rear 를 한 칸 앞으로(3 이면 0 으로), dequeue 는 front 칸을 읽고 front 를 한 칸 앞으로 옮긴다. 값은 지우지 않아도 front 가 지나갔으니 없는 것과 같다. 넣은 순서대로 나온다(FIFO)는 것만 지키면 칸 번호는 회전판으로 따라가면 된다.

따라가기 — data[3], front = 0, rear = 0.

  1. enqueue 1 → data[0] = 1, rear = 1. enqueue 2 → data[1] = 2, rear = 2
  2. dequeue → data[0] = 1 을 꺼냄(버림), front = 1
  3. enqueue 3 → data[2] = 3, rear = (2 + 1) % 3 = 0
  4. first = dequeue → data[1] = 2, front = 2
  5. second = dequeue → data[2] = 3, front = 0

헷갈리는 자리 — 첫 dequeue 의 결과 1 을 first 로 적는 것. 그 값은 변수에 안 담고 버렸다. 스택으로 착각해 3 · 2 로 적는 것도 흔하다. 출력에 " 그리고 " 글자가 그대로 들어간다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-2회 · 14번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> struct Pair { int x; int y; }; int main(void) { struct Pair values[] = { {1, 2}, {3, 4}, {5, 6} }; struct Pair *pointer = values; struct Pair **doublePointer = &pointer; (*doublePointer)[1] = (*doublePointer)[2]; printf("%d 그리고 %d", values[1].x, values[1].y); return 0; }
정답5 그리고 6

5 그리고 6. 이중 포인터를 풀면 values 그 자체라, 셋째 원소를 둘째 원소에 통째로 복사한 것이다.

원리pointer = values 는 배열 첫 칸의 주소, doublePointer = &pointer 는 그 쪽지의 주소다. *doublePointer 는 쪽지 pointer 를 다시 꺼낸 것이므로 values 첫 칸 주소와 같고, 거기에 [1] 을 붙이면 values[1], [2] 를 붙이면 values[2] 다. 구조체는 = 로 통째 복사가 되므로 values[1] = values[2] 는 x 와 y 를 한꺼번에 덮어쓴다. 별표가 두 개라도 "쪽지를 따라 원래 배열로 돌아온다"는 것이 전부다.

따라가기

  1. values = [{1,2}, {3,4}, {5,6}]
  2. (*doublePointer) = pointer = values
  3. values[1] = values[2] → values = [{1,2}, {5,6}, {5,6}]
  4. values[1].x = 5, values[1].y = 6 → 5 그리고 6

헷갈리는 자리 — 대입 방향을 거꾸로 읽어 values[2] 가 바뀌었다고 보는 것(3 그리고 4). 왼쪽이 바뀐다. 이중 포인터를 보고 값이 아니라 주소가 복사됐다고 넘겨짚는 것 — 대괄호가 붙는 순간 구조체 값이다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-2회 · 16번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> struct Node { int value; struct Node *next; }; int main(void) { struct Node a = { 1, NULL }; struct Node b = { 2, NULL }; struct Node c = { 3, NULL }; a.next = &b; b.next = &c; c.next = NULL; c.next = &a; a.next = &b; b.next = NULL; struct Node *head = &c; printf("%d %d %d", head->value, head->next->value, head->next->next->value); return 0; }
정답3 1 2

3 1 2. 마지막에 남은 화살표는 c → a → b → NULL 이고 머리가 c 다.

원리 — 연결 리스트의 순서는 오직 next 가 정하고, 같은 next 에 여러 번 대입하면 마지막 대입만 남는다. 코드가 화살표를 여섯 번 바꾸지만 노드마다 마지막 줄만 보면 된다: a.next 는 마지막에 &b, b.next 는 NULL, c.next 는 &a. 머리(head)를 c 로 잡았으니 c 에서 출발해 화살표를 따라간다. 노드가 선언된 순서(a · b · c)는 아무 상관이 없다.

따라가기

  1. a.next: &b → (다시) &b. 최종 b
  2. b.next: &c → NULL. 최종 NULL
  3. c.next: NULL → &a. 최종 a
  4. head = c: c(3) → a(1) → b(2) → NULL → 3 1 2

헷갈리는 자리 — 앞의 세 줄(a → b → c)만 보고 1 2 3 으로 적는 것. 뒤의 세 줄이 덮어쓴다. head->next->next->value 까지만 찍으므로 b.next 가 NULL 이어도 오류가 없다 — 네 번째를 찍었다면 NULL 을 따라가 죽는다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-2회 · 18번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> #include <stdlib.h> struct Node { char value; struct Node *next; }; struct Node *make_list(const char *text) { struct Node *head = NULL; while (*text) { struct Node *node = malloc(sizeof(struct Node)); node->value = *text++; node->next = head; head = node; } return head; } int main(void) { struct Node *current = make_list("BEST"); while (current) { putchar(current->value); struct Node *old = current; current = current->next; free(old); } return 0; }
정답TSEB

— TSEB. 글자를 하나씩 리스트 맨 앞에 끼우면 순서가 뒤집혀, 앞에서부터 찍으면 거꾸로 나온다.

원리make_list 는 문자열을 한 글자씩 읽으며 새 노드를 만들고 node->next = head; head = node 로 맨 앞에 끼운다. 첫 글자 B 가 제일 안쪽에, 마지막 글자 T 가 맨 앞에 서므로 리스트는 T → S → E → B 다. *text++ 는 "지금 글자를 읽고 나서 한 칸 옆으로"이고, while (*text) 는 끝표(0)를 만나면 멈춘다. main 은 머리부터 화살표를 따라가며 글자를 찍고 지나온 노드를 free 로 돌려준다 — 출력에는 영향이 없다.

따라가기

  1. B → 리스트 [B]
  2. E → [E, B], S → [S, E, B], T → [T, S, E, B]
  3. 머리부터 찍기: T S E B → TSEB

헷갈리는 자리 — 리스트를 "만든 순서대로" 보고 BEST 를 적는 것. 뒤에 붙이는 코드였다면 node->next = NULL 과 꼬리 포인터가 있어야 한다. 앞에 끼우기는 늘 뒤집힌다. free 뒤에 current 를 쓰지 않도록 미리 old 에 빼 두는 것도 눈여겨본다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2025-3회 · 7번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> int main(void) { char word[] = "REPUBLICOFKOREA"; int n = 0; while (word[n] != '\0') ++n; putchar(word[n - 2]); return 0; }
정답E

— E. 글자 수를 센 뒤 끝에서 둘째 글자를 찍는다.

원리 — while 문은 끝표('\0')를 만날 때까지 n 을 하나씩 올리므로, 끝나면 n 은 글자 수다. "REPUBLICOFKOREA" 는 15글자라 n = 15. 첨자는 0 부터라 마지막 글자는 word[n - 1], 그 앞 글자는 word[n - 2] 다. strlen 을 직접 만든 것이라고 보면 된다. ++nn++ 는 여기서 값을 쓰지 않으니 결과가 같다.

따라가기

  1. R E P U B L I C O F K O R E A → 15글자, n = 15
  2. word[14] = A(마지막), word[13] = E

헷갈리는 자리 — n − 2 를 "끝에서 두 번째"가 아니라 "13번째 글자"로 세다가 하나 어긋나는 것. 첨자와 순번은 1 차이다. 글자 수를 세다 하나 빠뜨리면 R 이 나온다 — REPUBLIC(8) + OF(2) + KOREA(5) = 15 로 묶어 센다. 반복이 끝난 순간 word[n] 은 끝표이므로 n 자체는 글자가 아니라는 것도 같은 함정이다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2026-1회 · 8번C

다음 C 프로그램을 실행했을 때 출력되는 값을 쓰시오.

#include <stdio.h> struct fns { int *(*fn)(int *); }; int *dummy(int *d) { return d + 1; } int main(void) { struct fns mine; int n[] = { 16, 32 }; mine.fn = dummy; printf("%x", *mine.fn(n)); return 0; }
정답20

— 20. 함수 포인터로 dummy 를 불러 배열의 둘째 칸 주소를 받고, 그 값 32 를 16진수로 찍는다.

원리int *(*fn)(int *) 는 "int 포인터를 받아 int 포인터를 돌려주는 함수"를 가리키는 포인터다. 구조체 멤버 fn 에 dummy 를 넣으면 mine.fn(n)dummy(n) 과 같다. dummy 는 받은 주소에 1 을 더해 돌려주므로 배열의 다음 칸(&n[1]) 주소다. 앞의 별표가 그 칸을 열어 32 가 나온다. %x 는 값을 16진수로 찍는 서식이라 32 는 0x20, 화면에는 20 이다.

따라가기

  1. n = [16, 32], mine.fn = dummy
  2. mine.fn(n) = dummy(n) → n + 1 → n[1] 의 주소
  3. * → 32
  4. %x → 32 = 2 × 16 + 0 → 20

헷갈리는 자리 — 32 를 그대로 적는 것. 서식이 %d 가 아니라 %x 다. 함수 포인터 선언이 낯설어 d + 1 을 값 17 로 읽는 것 — d 는 주소라 +1 은 한 칸 옆이다. 첫 칸 16 을 16진수로 바꾼 10 도 오답이다(dummy 가 한 칸 옮겼다).

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

2026-2회 · 7번C

다음 C 프로그램의 실행 결과를 쓰시오.

#include <stdio.h> typedef struct N { int v; struct N *a; struct N *b; } N; int c = 0; int result; void func(N *n) { if (!n) return; func(n->a); func(n->b); if (++c == 3) result = n->v; } int main(void) { N ne = { 35, 0, 0 }; N nd = { 64, 0, 0 }; N nc = { 53, 0, 0 }; N nb = { 12, &ne, &nc }; N na = { 21, &nb, &nd }; func(&na); printf("%d", result); return 0; }
정답12

— 12. 후위 순회(왼쪽 → 오른쪽 → 자기)에서 세 번째로 도착하는 노드가 nb 이고 그 값이 12 다.

원리 — 구조체 N 은 값 v 와 자식 두 개(a 왼쪽, b 오른쪽)를 가진 트리 노드다. na(21) 아래 nb(12) · nd(64), nb 아래 ne(35) · nc(53) 가 있다. func 는 자식 a, 자식 b 를 먼저 다 처리한 다음에 자기 차례에 ++c 를 센다 — 이것이 후위 순회다. 전역 c 는 "자기 차례가 온 횟수"이고, 그것이 3 이 되는 순간의 노드 값이 result 에 들어간다. if (!n) return 은 자식이 없을 때(0) 되돌아가는 멈춤 조건이다.

따라가기

  1. func(na) → func(nb) → func(ne): 자식 둘 다 0 → ne 차례, c = 1
  2. nb 의 둘째 자식 func(nc): 자식 0 → nc 차례, c = 2
  3. nb 차례, c = 3 → result = nb.v = 12
  4. 이어서 nd 차례 c = 4, na 차례 c = 5 — 조건이 ==3 이라 result 는 안 바뀐다

헷갈리는 자리 — 전위 순회(자기 먼저)로 세어 세 번째를 ne(35)로 적는 것. 자기 처리(++c)가 두 재귀 호출 뒤에 있으니 후위다. c 가 3 을 지난 뒤에도 순회는 계속되지만 result 는 그대로다.

복원 코드를 gcc(도커 alpine)로 실행해 같은 출력을 확인했다. 복원 출처 grandlife.co.kr(2026-09-14) — 공식 원문이 아니라 문장 · 변수명이 실제와 다를 수 있다.

예상 1회 · 3번C빈출 원리: 재귀 + 자릿수 분리

다음 코드의 출력을 쓰시오.

int f(int n) { if (n == 0) return 0; return n % 10 + f(n / 10); } int main() { printf("%d", f(1234)); }
정답10

— 10. 1234 의 각 자리 4 + 3 + 2 + 1 을 재귀로 더한 것이다.

원리 — n % 10 은 마지막 자리, n / 10 은 마지막 자리를 뗀 나머지 수다(정수 나눗셈이라 소수점이 버려진다). f(n) 은 "마지막 자리 + 나머지 수의 자릿수 합"으로 자기 자신을 부르고, n 이 0 이 되면 더할 것이 없으니 0 을 돌려준다. 재귀는 "지금 한 자리만 처리하고 나머지는 같은 함수에 맡긴다"는 발상이다. 호출은 f(1234) → f(123) → f(12) → f(1) → f(0) 순으로 쌓였다가 f(0) = 0 부터 거꾸로 값이 돌아오며 더해진다.

따라가기

  1. f(1234) = 4 + f(123)
  2. f(123) = 3 + f(12), f(12) = 2 + f(1), f(1) = 1 + f(0), f(0) = 0
  3. 되돌아오며 1 + 0 = 1 → 2 + 1 = 3 → 3 + 3 = 6 → 4 + 6 = 10

헷갈리는 자리 — 종료 조건을 n == 1 로 착각해 마지막 1 을 빠뜨리는 것. 이 코드는 n == 0 에서 멈추므로 1 의 자리도 더해진다. 그리고 순서 — 자릿수 합은 순서가 상관없지만, 자리를 "출력"하는 재귀라면 호출 순서(4 3 2 1)인지 반환 순서(1 2 3 4)인지가 답을 가른다.

이전 자료의 예상 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

예상 2회 · 9번C빈출 원리: 아스키코드 + 문자열 순회

다음 코드의 출력을 쓰시오.

char s[] = "KoreA"; int cnt = 0; for (int i = 0; s[i] != '\0'; i++) if (s[i] >= 'A' && s[i] <= 'Z') cnt++; printf("%d%c", cnt, s[1] - 32);
정답2O

— 2O. 대문자 K · A 두 개를 세고, s[1] 인 'o' 에서 32 를 빼 'O' 로 만든다.

원리 — 아스키 코드에서 대문자 A~Z 는 65~90, 소문자 a~z 는 97~122 로 정확히 32 차이다. 그래서 소문자 − 32 = 대문자, 대문자 + 32 = 소문자다. 반복문은 널 문자까지 돌며 'A' 이상 'Z' 이하인 글자만 센다 — "KoreA" 에서 K 와 A 다. s[1] 은 'o'(111) 이고 111 − 32 = 79 = 'O' 를 %c 가 글자로 찍는다. 서식이 "%d%c" 라 숫자와 글자가 붙어서 나온다.

따라가기

  1. K → 대문자 cnt 1 · o · r · e → 아님 · A → cnt 2
  2. s[1] = 'o' → 'o' − 32 = 'O'
  3. 출력 2O

헷갈리는 자리 — 2 뒤의 글자를 숫자 0 으로 적는 것. 알파벳 O 다. 그리고 s[1] 을 "첫 글자"로 읽어 K 로 계산하는 것 — 첨자는 0 부터다. 대소문자 변환은 ±32 또는 toupper() · tolower() 함수로도 나온다.

이전 자료의 예상 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

05틀리는 자리

왜 이걸 하나 — 원리는 알아도 글자 하나 · 순서 하나에서 깎인다.

자리새는 꼴적는 법기출
대소문자E 를 e 로, TSEB 를 tseb 로코드의 변환(toupper · +32)을 따라 그대로. 출력은 대소문자가 답이다2025-3회 7번 · 2025-2회 18번
소수 자리2795.1 · 2795.104%.2f 면 둘째 자리까지 반올림2024-1회 11번
「그리고」 표기「2 그리고 3」을 「2, 3」으로printf 문자열에 한글이 있으면 그 글자까지 답이다2025-2회 12번 · 14번
공백3 1 2 를 312 로서식에 공백("%d ")이 있으면 공백까지2025-2회 16번 · 2024-3회 12번
순회 순서노드를 만든 순서대로 적는다next 화살표를 그려 head 부터 따라간다. 마지막에 이은 화살표가 이긴다2025-1회 18번 · 2025-2회 16번
순회 종류후위를 전위로 센다자기 처리가 두 재귀 호출 뒤에 있으면 후위2026-2회 7번
시저 범위z 를 넘어도 그대로 더한다% 26 이 있으면 a 로 돌아간다. 없으면 코드값 그대로2024-1회 19번
화살표 기호->. 이나 =>포인터면 ->, 변수면 .2023-3회 4번

이 장의 답에는 한글 · 공백 · 소수점이 섞인 출력이 많다. printf 의 서식 문자열을 답 칸 옆에 베껴 놓고 그 틀에 값만 넣으면 표기 감점이 없어진다.

06훈련

왜 이걸 하나 — 세 원리를 짧은 코드 10개로 돌린다. 매일 5문항씩 곁들이는 자리다.

훈련 10문항은 문자 세기, 역순 복사, 재귀 누적합, 피보나치, 구조체 화살표, 구조체 배열, 대소문자 반전, strcmp 원리, strcat + strlen, 그리고 static 지역변수다. 정답은 전부 gcc 로 실행해 확인했다.

훈련 C · 7번C문자 세기 + 길이
char s[] = "ENGINEER"; int cnt = 0, len = 0; for (int i = 0; s[i] != '\0'; i++) if (s[i] == 'E') cnt++; while (s[len] != '\0') len++; printf("%d %d", cnt, len);
정답3 8

— 3 8. "ENGINEER" 에서 E 는 3개, 길이는 8 이다.

원리 — C 문자열은 끝에 널 문자 '\0' 이 붙어 있어, 첨자를 0 부터 올리며 널을 만날 때까지 도는 것이 문자열 순회의 기본형이다. 첫 반복문은 각 글자를 'E' 와 비교해 세고, 둘째 while 은 널이 나올 때까지 len 을 올려 길이를 잰다 — strlen 을 손으로 만든 것이다. 널 문자 자체는 길이에 안 들어간다. 두 반복문의 조건이 같은 모양이라는 것을 보면 둘 다 "끝까지 훑는다"는 것이 보인다.

따라가기

  1. E N G I N E E R → E 는 0 · 5 · 6번 → cnt 3
  2. 널은 8번 칸 → len 8
  3. 출력 3 8

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

훈련 C · 8번C문자열 역순 복사
char s[] = "CODE", r[5]; int len = 4; for (int i = 0; i < len; i++) r[i] = s[len - 1 - i]; r[len] = '\0'; printf("%s", r);
정답EDOC

— EDOC. "CODE" 를 뒤집어 새 배열에 담았다.

원리 — r[i] = s[len − 1 − i] 는 "r 의 i 번 칸에 s 의 뒤에서 i 번째 글자"를 넣는다. i = 0 이면 s[3] = 'E', i = 3 이면 s[0] = 'C' 다. 글자 4개를 옮긴 뒤 r[4] = '\0' 로 끝을 표시해야 %s 가 어디서 멈출지 안다 — 이 줄이 없으면 쓰레기 값이 이어져 찍힐 수 있다. r 을 5칸으로 잡은 이유도 널 자리 때문이다.

따라가기

  1. i=0 → s[3] E · i=1 → s[2] D · i=2 → s[1] O · i=3 → s[0] C
  2. r[4] = '\0'
  3. 출력 EDOC

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

훈련 C · 9번C재귀 누적합
int f(int n) { if (n == 1) return 1; return n + f(n - 1); } int main() { printf("%d", f(10)); }
정답55

— 55. 1 부터 10 까지의 합을 재귀로 구한 것이다.

원리 — f(n) = n + f(n − 1) 이고 f(1) = 1 이 바닥이다. f(10) 은 10 + f(9), f(9) 는 9 + f(8) … f(2) 는 2 + f(1) = 3 까지 내려갔다가 위로 값이 돌아온다. 결국 10 + 9 + … + 1 = 10 × 11 ÷ 2 = 55 다. 재귀 문제는 "바닥이 무엇인가"와 "한 단계가 무엇을 더하나"만 잡으면 손으로 안 펼쳐도 식으로 계산된다.

따라가기

  1. f(10) = 10 + f(9) = 10 + 9 + f(8) = …
  2. = 10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1
  3. = 55

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

훈련 C · 10번C피보나치 변형 (초기값 주의)
int f(int n) { if (n <= 2) return 1; return f(n - 1) + f(n - 2); } int main() { printf("%d", f(7)); }
정답13

— 13. 피보나치 수열의 7번째 항이다.

원리 — f(n) = f(n − 1) + f(n − 2), f(1) = f(2) = 1 은 피보나치 정의다. 재귀로 펼치면 호출이 나무처럼 갈라져 f(7) 하나에 f 가 25번 불리지만, 값은 앞에서부터 표로 채우면 금방이다 — 1 1 2 3 5 8 13. 시험은 이 모양의 코드를 보면 "피보나치"임을 알아보고 표로 푸는 것이 빠르다. 바닥이 n <= 2 라 f(1) · f(2) 둘 다 1 이다.

따라가기

  1. f(1)=1 f(2)=1 f(3)=2 f(4)=3
  2. f(5)=5 f(6)=8 f(7)=13
  3. 출력 13

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

훈련 C · 12번C구조체 + 화살표 연산자
struct Student { int no; int score; }; int main() { struct Student s = {1001, 88}; struct Student *p = &s; p->score += 7; printf("%d %d", s.no, p->score); }
정답1001 95

— 1001 95. 구조체 포인터로 score 에 7 을 더했고, 그 변화가 s 에 그대로 반영된다.

원리 — 구조체 변수 s 의 멤버는 s.no · s.score 로 접근하고, 구조체를 가리키는 포인터 p 로는 p->score(또는 (*p).score)로 접근한다. p = &s 이므로 p->score 는 s.score 와 같은 칸이다. p->score += 7 은 88 을 95 로 바꾸고, 그 값은 s.score 로 읽어도 p->score 로 읽어도 95 다. 화살표 -> 는 "포인터가 가리키는 구조체의 멤버"라는 뜻이다.

따라가기

  1. s = {1001, 88}, p → s
  2. p->score += 7 → 95
  3. 출력 s.no 1001, p->score 95 → 1001 95

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

훈련 C · 13번C구조체 배열 + 조건 누적
struct P { char g; int v; }; struct P arr[3] = {{'a',10},{'b',20},{'c',30}}; int s = 0; for (int i = 0; i < 3; i++) if (arr[i].v >= 20) s += arr[i].v; printf("%d%c", s, arr[1].g);
정답50b

— 50b. v 가 20 이상인 20 + 30 = 50 이고, arr[1].g 는 'b' 다.

원리 — 구조체 배열 arr 은 {g, v} 짝이 3개 있는 배열이다. arr[i].v 로 i 번째 구조체의 v 를 읽는다. 조건 v >= 20 은 첫 번째(10)를 걸러내고 20 · 30 을 더한다. arr[1] 은 두 번째 원소 {'b', 20} 이라 g 는 'b' 다. 서식 "%d%c" 라 50 과 b 가 붙어 찍힌다. 구조체 배열 문제는 "몇 번째 원소의 어느 멤버인가"를 표로 그리면 틀리지 않는다.

따라가기

  1. arr[0].v 10 → 제외 · arr[1].v 20 → s 20 · arr[2].v 30 → s 50
  2. arr[1].g = 'b'
  3. 출력 50b

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

훈련 C · 17번C대소문자 반전
char s[] = "Korea"; for (int i = 0; s[i] != '\0'; i++) { if (s[i] >= 'a' && s[i] <= 'z') s[i] -= 32; else s[i] += 32; } printf("%s", s);
정답kOREA

— kOREA. 소문자는 대문자로, 대문자는 소문자로 서로 바꾸었다.

원리 — 아스키에서 대소문자는 32 차이다. 소문자면 32 를 빼 대문자로, 아니면(대문자면) 32 를 더해 소문자로 만든다 — 대소문자 토글이다. "Korea" 는 K 만 대문자라 K → k, o r e a → O R E A 가 된다. s[] 는 문자 배열이라 제자리에서 고칠 수 있다 — char *s = "Korea" 처럼 문자열 리터럴을 가리키는 포인터였다면 고치는 순간 오류가 난다는 것이 짝으로 나오는 함정이다.

따라가기

  1. K → 대문자 → +32 → k
  2. o r e a → 소문자 → −32 → O R E A
  3. 출력 kOREA

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

훈련 C · 18번Cstrcmp의 원리 (불일치 지점)
char a[] = "apple", b[] = "apply"; int i = 0; while (a[i] != '\0' && a[i] == b[i]) i++; printf("%d %c %c", i, a[i], b[i]);
정답4 e y

— 4 e y. 두 문자열이 처음으로 달라지는 자리가 4번이고, 그 자리 글자가 e 와 y 다.

원리 — while 조건은 "a 가 안 끝났고, 두 글자가 같다"이다. 같을 동안 i 를 올리다가 다른 글자를 만나면 멈춘다 — strcmp 가 하는 일의 앞부분이다. "apple" 과 "apply" 는 a p p l 네 글자가 같고 5번째(첨자 4)에서 e 와 y 로 갈린다. 멈춘 시점의 i 가 4 이고, a[4] = 'e', b[4] = 'y' 다. 문자열이 완전히 같았다면 a[i] 가 널이 되는 순간 앞 조건이 거짓이 되어 멈춘다.

따라가기

  1. i=0 a=a · i=1 p=p · i=2 p=p · i=3 l=l → 계속
  2. i=4 e ≠ y → 멈춤
  3. 출력 4 e y

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

훈련 C · 19번Cstrcat + strlen
char a[10] = "AB"; char b[] = "CD"; strcat(a, b); strcat(a, "E"); printf("%s %d", a, strlen(a));
정답ABCDE 5

— ABCDE 5. strcat 으로 두 번 이어 붙여 다섯 글자가 됐다.

원리 — strcat(a, b) 는 a 의 널 자리부터 b 를 복사해 붙이고 끝에 널을 다시 놓는다. a 를 10칸으로 잡은 것은 붙일 공간을 미리 두기 위해서다 — 2칸짜리였다면 넘쳐서 사고가 난다. "AB" + "CD" = "ABCD", 거기에 "E" 를 붙여 "ABCDE" 다. strlen 은 널 앞까지의 글자 수라 5 다(널 포함 아님). 이 둘은 string.h 함수라 헤더가 필요하다.

따라가기

  1. a = "AB" → strcat(a, "CD") → "ABCD"
  2. strcat(a, "E") → "ABCDE"
  3. strlen = 5 → 출력 ABCDE 5

이 노트에서 만든 변형 문제다. 코드를 손으로 따라가고 docker gcc(alpine 13.2) 실행 결과와 대조했다.

07백지 재현

왜 이걸 하나 — 아래를 안 보고 적을 수 있어야 이 장이 끝난다.