C 포인터 · 배열 E02

C 기출의 절반이 포인터와 배열이다. 주소를 그림으로 그리고 첨자를 손으로 세면 답이 하나로 좁혀진다

왜 이 장을 하나
C 문제의 함정은 문법이 아니라 지금 포인터가 어디를 가리키는지다. 배열 이름이 주소라는 것, *(p+1)*p+1 의 차이, 2차원 배열의 행 · 열 첨자, 매개변수로 넘어간 배열이 포인터가 되는 것 — 이 넷이 15문항의 전부다. 종이에 칸을 그리며 추적하는 절차를 세운다.
다루는 것포인터 산술 · 배열 첨자 · 2차원 배열 · 주소 전달 · 정렬 코드
문항기출 15 · 예상 3 · 훈련 10
끝나면포인터 식을 보고 칸을 그려 값을 읽는다

01왜 이 장인가

왜 이걸 하나 — 프로그래밍 91문항 중 C 가 42문항이고, 그중 15문항이 포인터 · 배열이다. 11회 중 9회에 나왔다.

11회분 220문항 중 이 장의 기출은 15문항이다. 2023년 2회에는 4문항(순환 출력 · 스택 · 공유 버퍼 · 선택 정렬)이 몰렸고, 2026년 2회 12번(sizeof)까지 이어진다. 코드 추적 훈련 10문항(훈련 C 1~4 · 23~26 · 28 · 29)이 이 장에 붙는다.

몇 문항무엇을 적나보기
문자열 포인터 출력4%s · %c 로 여러 줄2023-1회 2번 · 2023-3회 3번 · 2025-1회 10번 · 2025-3회 2번
배열 · 2차원 배열 계산5합 · 평균 · 특정 칸 값2024-2회 11번 · 15번 · 2025-1회 11번 · 2026-1회 2번 · 2026-2회 12번
빈칸 채우기3변수명 · 첨자 식 · 비교 연산자2023-1회 14번 · 2023-2회 9번 · 14번
포인터로 자료구조3스택 · 공유 버퍼 · 이중 포인터2023-2회 11번 · 12번 · 2024-3회 19번

→ 답이 여러 줄인 문항이 4개다(2023-1회 2번 5줄 · 2023-3회 3번 5줄 · 2025-1회 10번 2줄 · 2026-2회 12번 4줄). 한 줄이라도 빠지거나 순서가 바뀌면 그 문항이 통째로 나가므로, 줄 수를 먼저 세고 시작한다.

E01 진단의 C 2문항 중 12번(sizeof 와 배열 매개변수)이 이 장이다. 코드 추적은 머릿속으로 하면 첨자 하나가 밀린다 — 칸을 그리고 화살표를 옮기는 습관이 03절이다.

문항 수는 이 노트의 문제 데이터에서 ch=E02 인 것을 센 값이다(2026-09-14). 기출 코드는 grandlife.co.kr 복원에서 옮겼고, 15문항 전부 gcc(도커 alpine)로 실행해 정답을 확인했다. 빈칸 문제 3개는 답을 넣어 실행하거나 컴파일해 확인했다.

02한눈에

왜 이걸 하나 — 시험 전날 규칙 8개를 다시 본다. 전부 「주소냐 값이냐」의 변주다.

배열 이름 = 첫 칸 주소int a[5] 에서 a&a[0]. a[i]*(a + i) 와 같다. 문자열 char w[] = "Art" 는 A · r · t · \0 네 칸.
포인터 산술p + 1 은 한 칸(자료형 크기만큼) 옆의 주소. *(p + 1) 은 그 칸의 값, *p + 1 은 첫 칸 값에 1 을 더한 것. p++ 는 포인터를 옮기고, (*p)++ 는 값을 올린다.
%s 와 %c%s 에 주소를 주면 그 자리부터 \0 전까지 출력. %c 에 값을 주면 글자 하나. %c*p + 4 를 주면 코드가 4 큰 글자(K → O).
2차원 배열a[2][3] 은 행 2 열 3, 메모리에는 한 줄로 1 2 3 4 5 6. a[1][2] = *(*(a + 1) + 2). int *p = a[0] 이면 p[4] 는 5(한 줄로 센다).
함수에 넘기면배열을 넘기면 포인터가 된다 — int arr[] 매개변수의 sizeof(arr) 는 포인터 크기(8). 값을 넘기면 복사본, 주소를 넘기면 원본이 바뀐다. 이중 포인터 int **pp 는 포인터의 주소.
전역 배열 공유같은 전역 버퍼를 돌려주는 함수를 세 번 부르면 세 포인터가 같은 곳을 가리켜 마지막 값이 세 번 찍힌다.
정렬 · 스택 뼈대선택 정렬은 if (a[i] > a[j]) swap 이 오름차순. 버블은 이웃끼리 a[j] > a[j+1]. 스택은 top 을 올리며 넣고 내리며 뺀다. 순환 첨자는 (i + 1) % n.
sizeof배열 자체는 전체 바이트, 포인터는 8(64비트). 원소 수 = sizeof(a) / sizeof(a[0]). 배열 포인터 int (*p)[8]*p 는 배열 전체(32).

03밑바닥

왜 이걸 하나 — 규칙 8개는 메모리를 칸으로 그리면 전부 한 그림에서 나온다. 그림이 있으면 처음 보는 코드도 같은 절차다.

3-1. 배열은 칸의 줄이고 이름은 첫 칸의 번지다

char word[] = "Art"; 는 메모리에 A · r · t · \0 네 칸을 나란히 만들고, word 라는 이름은 그 첫 칸의 주소다. 그래서 printf("%s", word) 는 첫 칸부터 \0 전까지 Art 를 찍고, *word 는 첫 칸의 값 A 다. char *p = word; 로 포인터를 만들어도 같은 칸을 가리키니 *p 도 A 다(2023년 1회 2번).

포인터에 1 을 더하면 한 칸 옆의 주소가 된다. word + 1 은 r 의 주소라 %s 로 찍으면 rt 이고, *(word + 3) 은 넷째 칸 값이다. 2023년 3회 3번은 KOREA 로 이 다섯 가지를 한 문제에 넣었다 — %s word(KOREA), %s word+1(OREA), %c *word(K), %c *(word+3)(E), %c *word+4(K 의 코드 75에 4 를 더한 O).

마지막 것이 가장 자주 틀린다. *(word + 3) 은 「3칸 옆의 값」이고 *word + 3 은 「첫 칸 값에 3 을 더한 것」이다. 괄호가 있으면 주소를 먼저 옮기고, 없으면 값을 먼저 꺼낸다 — * 가 + 보다 먼저이기 때문이다. 2025년 3회 2번도 같은 갈림에서 C 가 답이었다.

3-2. 2차원 배열은 한 줄이다

int a[2][3] = {{1,2,3},{4,5,6}}; 는 그림으로는 2행 3열이지만 메모리에는 1 2 3 4 5 6 이 한 줄로 놓인다. a[1][2] 는 「둘째 행의 셋째 칸」이고 한 줄로 세면 1 × 3 + 2 = 5번째(0부터)라 6 이다. 그래서 int *p = a[0]; 처럼 첫 행의 주소를 한 줄 포인터에 담으면 p[4] 는 5, p[3] 은 4 다(예상 1회 9번).

포인터 식으로 쓰면 *(*(a + 1) + 2) 다. a + 1 은 둘째 행의 주소, *(a + 1) 은 그 행의 첫 칸 주소, 거기에 2 를 더해 셋째 칸, 마지막 * 로 값. 2024년 2회 15번과 훈련 3번이 이 식을 그대로 묻는다. 행 첨자에는 열 개수를 곱한다는 것만 기억하면 어느 칸이든 한 줄 번호로 바꿀 수 있다.

3-3. 함수에 넘기면 배열은 포인터가 된다

매개변수 자리의 int arr[] 는 배열이 아니라 포인터다. 그래서 함수 안에서 sizeof(arr) 는 배열 크기가 아니라 포인터 크기(64비트에서 8)이고, 2026년 2회 12번의 function3 이 8 / 4 = 2 를 찍는다. 배열 전체를 가리키는 int (*arr)[8] 로 받으면 *arr 가 배열 자체라 32 / 4 = 8 이 된다(function4).

같은 문제의 앞부분이 값 전달과 주소 전달이다. function1(&value) 는 주소를 받아 *p = 50 으로 원본을 바꾸고, function2(value) 는 복사본만 60 으로 바꿔 원본은 50 그대로다. 훈련 28번(값 전달 vs 주소 전달)이 같은 원리이고, 2024년 3회 19번은 포인터의 주소(int **)를 넘겨 원본 포인터를 바꾼다.

전역 버퍼도 같은 함정이다. 2023년 2회 12번은 scanf 로 읽어 전역 배열 buffer 의 주소를 돌려주는 함수를 세 번 부른다. 세 포인터가 같은 버퍼를 가리키므로 마지막에 읽은 gamma 가 세 번 찍힌다. 「포인터를 세 개 만들었으니 값도 세 개」라고 생각하면 틀린다.

3-4. 첨자 식과 정렬 뼈대는 손으로 한 바퀴 돌린다

빈칸 문제는 뼈대를 알아야 채운다. 첫 값을 맨 뒤로 보내는 순환 출력은 values[(i + 1) % 5] — i 가 4 일 때 5 % 5 = 0 으로 첫 칸에 돌아온다(2023년 2회 9번, 5 4 3 2 1 을 넣으면 4 3 2 1 5). 선택 정렬의 오름차순은 if (values[i] > values[j]) 에서 바꾸는 것이라 빈칸은 > 다(2023년 2회 14번). 버블 정렬의 swap 은 arr[idx2] = temp 로 끝나고 호출은 bubbleSort(values, size) 다(2023년 1회 14번).

스택은 top 하나로 돌아간다. push 는 top 을 올린 뒤 넣고, pop 은 값을 꺼낸 뒤 top 을 내린다. 2023년 2회 11번은 push 1 · 2, pop → 2, push 3, pop → 3, push 4 · 6, pop → 6, pop → 4, push 5, pop → 5, pop → 1 로 213465 다. pop 이 찍는 것은 가장 최근에 넣은 것이라 순서를 표로 적으면 틀리지 않는다.

통과 기준은 넷이다. 첫째, 문자열 포인터 식 5종(%s p · %s p+1 · %c *p · %c *(p+n) · %c *p+n)의 출력을 KOREA 로 안 보고 적는다. 둘째, 2차원 배열의 칸을 한 줄 번호로 바꾼다. 셋째, 값 전달 · 주소 전달 · 배열 매개변수의 sizeof 를 가른다. 넷째, 스택 · 순환 첨자 · 정렬 뼈대를 손으로 한 바퀴 돌린다. 기출 15문항이 이 넷 안에 있다.
*p + 1 과 *(p + 1) 은 같다.*+ 보다 먼저다. *p + 1 은 첫 칸 값에 1 을 더한 것(K → L), *(p + 1) 은 둘째 칸의 값(O). 2023년 3회 3번의 마지막 줄 O 는 *word + 4 라 K 에 4 를 더한 것이다.
함수 안에서도 sizeof(arr) 는 배열 크기다.매개변수 int arr[] 는 포인터라 sizeof 가 8 이다. 2026년 2회 12번의 3.2 가 그 결과다. 배열 크기를 함수 안에서 알려면 길이를 따로 넘기거나 배열 포인터 int (*arr)[8] 로 받는다.

04기출

왜 이걸 하나 — 코드를 종이에 칸으로 그리며 푸는 연습이다. 출력이 여러 줄이면 줄 수를 먼저 센다.

변수마다 칸을 그리고 포인터는 화살표로 그린다. printf 를 만날 때마다 답 칸에 한 줄씩 적고, 정답을 연 뒤 몇째 줄에서 어긋났는지를 보면 규칙 8개 중 무엇이 빠졌는지 나온다.

2023-1회 · 2번C

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

#include <stdio.h> int main(void) { char word[] = "Art"; char *p = word; printf("%s\n", word); printf("%c\n", *p); printf("%c\n", *word); printf("%s\n", p); for (int i = 0; word[i] != '\0'; i++) { printf("%c", word[i]); } return 0; }
정답Art
A
A
Art
Art

— 다섯 줄: Art / A / A / Art / Art. 배열 이름 word 와 포인터 p 가 같은 자리를 가리키므로 같은 것을 두 번씩 찍는 셈이다.

원리 — C 에서 문자열은 글자를 한 칸에 하나씩 넣은 상자 줄이고 끝에 보이지 않는 끝표('\0')가 붙는다. "Art" 는 A · r · t · 끝표 네 칸이다. 배열 이름 word 는 그 줄의 첫 칸 주소이고, char *p = word 는 그 주소를 종이쪽지 p 에 베껴 적은 것이다. 그래서 word 로 가든 p 로 가든 도착하는 칸은 같다. %s 는 "그 주소부터 끝표 전까지 전부 찍어라", %c 는 "그 칸의 글자 하나만 찍어라"다. 별표 *p 는 "p 가 가리키는 칸의 내용"이라 첫 글자 A 다.

따라가기

  1. printf("%s\n", word) — 첫 칸부터 끝표 전까지 → Art, 줄바꿈.
  2. printf("%c\n", *p) — p 가 가리키는 칸의 글자 → A.
  3. printf("%c\n", *word) — 배열 이름도 첫 칸 주소라 *word 는 첫 글자 → A.
  4. printf("%s\n", p) — p 부터 끝표 전까지 → Art.
  5. for 문 — word[0] 부터 끝표를 만날 때까지 한 글자씩, 줄바꿈 없이 → Art.

헷갈리는 자리 — 줄바꿈 수다. 앞 넷은 \n 이 있고 마지막 for 문은 없으니 답은 다섯 줄이고, 마지막 줄 뒤에 빈 줄이 없다. *word 를 "배열 전체"로 읽어 Art 라고 적는 것이 흔한 오답이다 — 별표는 언제나 칸 하나다.

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

2023-1회 · 14번C

다음 C 프로그램에서 ①과 ②에 들어갈 변수명을 순서대로 쓰시오.

#include <stdio.h> void swap(int *arr, int idx1, int idx2) { int temp = arr[idx1]; arr[idx1] = arr[idx2]; arr[①] = temp; } void bubbleSort(int *arr, int length) { for (int i = 0; i < length - 1; i++) { for (int j = 0; j < length - 1 - i; j++) { if (arr[j] > arr[j + 1]) { swap(arr, j, j + 1); } } } } int main(void) { int values[] = { 85, 75, 50, 100, 95 }; int size = 5; bubbleSort(values, ②); return 0; }
정답① idx2, ② size

— ① idx2 ② size. ① 은 두 값을 맞바꾸는 마지막 단계에서 비어 있는 자리이고, ② 는 정렬 함수에 넘길 배열의 길이다.

원리 — 두 컵의 물을 맞바꾸려면 빈 컵이 하나 더 필요하다. temp = arr[idx1](첫째 컵을 빈 컵에), arr[idx1] = arr[idx2](둘째 컵을 첫째 컵에), 마지막으로 빈 컵의 물을 둘째 컵에 부어야 하니 arr[idx2] = temp 다. 이 세 줄은 순서가 정해져 있어 한 줄만 보여도 나머지가 정해진다. 버블 정렬은 이웃한 두 칸을 비교해 앞이 크면 맞바꾸는 일을 배열 길이만큼 반복하는 것이라, 함수는 배열과 그 길이 두 가지를 알아야 한다.

따라가기

  1. swap 의 첫 두 줄에서 idx1 자리는 이미 idx2 의 값으로 덮였다. 아직 안 채워진 곳은 idx2 자리뿐 → ① idx2.
  2. bubbleSort(int *arr, int length) — 둘째 인자는 길이. main 에 길이를 담은 변수는 size = 5 하나 → ② size.
  3. 넣고 돌리면 85 75 50 100 95 → 50 75 85 95 100 으로 정렬된다(출력문은 없어 화면엔 안 찍힌다).

헷갈리는 자리 — ② 에 숫자 5 를 적는 것. 문제가 "변수명"을 물었으므로 5 는 틀린다. ① 에 idx1 을 적으면 첫째 컵에 두 번 붓는 꼴이라 둘째 컵이 그대로 남는다 — 맞바꾸기가 안 된다.

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

2023-2회 · 9번C

정수 다섯 개를 배열에 입력한 뒤 첫 번째 값을 맨 뒤로 보내 출력하려고 한다. ①에 들어갈 식을 쓰시오.

#include <stdio.h> int main(void) { int values[5]; int i; for (i = 0; i < 5; i++) { scanf("%d", &values[i]); } for (i = 0; i < 5; i++) { printf("%d", values[( ① )]); } return 0; }
정답(i + 1) % 5

(i + 1) % 5. i 가 0 · 1 · 2 · 3 일 때는 다음 칸(1 · 2 · 3 · 4)을, i 가 4 일 때는 5 % 5 = 0 으로 첫 칸을 가리켜 첫 값이 맨 뒤로 간다.

원리 — 나머지 연산 % 는 회전판이다. 어떤 수를 5 로 나눈 나머지는 0 · 1 · 2 · 3 · 4 만 나오고, 5 가 되는 순간 다시 0 으로 돌아간다. 그래서 "첨자 하나 앞으로, 끝에 닿으면 처음으로"라는 움직임은 언제나 (첨자 + 1) % 크기 로 적는다. 원형 큐나 시계의 시침이 같은 규칙이다. 첫 번째 값을 맨 뒤로 보낸다는 말은 출력 순서를 1번 칸부터 시작해 마지막에 0번 칸을 찍는다는 뜻이다.

따라가기 — 입력이 1 2 3 4 5 라 하면 values 는 [1, 2, 3, 4, 5].

  1. i = 0 → (0 + 1) % 5 = 1 → values[1] = 2
  2. i = 1 → 2 → 3, i = 2 → 3 → 4, i = 3 → 4 → 5
  3. i = 4 → (4 + 1) % 5 = 0 → values[0] = 1
  4. 화면: 23451 — 첫 값 1 이 맨 뒤로 갔다.

헷갈리는 자리i + 1 만 적으면 i = 4 에서 values[5] 를 읽어 배열 밖(쓰레기 값)이다. (i % 5) + 1 은 5 가 나올 수 있어 역시 밖으로 나간다. 괄호를 빼고 i + 1 % 5 로 적으면 % 가 먼저 계산돼 i + 1 과 같아진다 — 괄호가 답의 일부다.

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

2023-2회 · 11번C

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

#include <stdio.h> int stack[10]; int top = -1; void push(int value) { stack[++top] = value; } int pop(void) { return stack[top--]; } int main(void) { push(5); push(2); printf("%d", pop()); push(4); push(1); printf("%d", pop()); push(3); printf("%d", pop()); printf("%d", pop()); push(6); printf("%d", pop()); printf("%d", pop()); return 0; }
정답213465

— 213465. 넣은 순서의 거꾸로 꺼내는 스택을 push · pop 순서대로 그리면 한 자리씩 나온다.

원리 — 스택은 접시 더미다. 접시를 올릴 때(push) 맨 위에 놓고, 꺼낼 때(pop)도 맨 위 것을 집는다. 마지막에 넣은 것이 먼저 나온다(LIFO). 코드의 top 은 맨 위 접시가 놓인 칸 번호이고 처음엔 −1(접시 없음)이다. stack[++top] = value 는 top 을 하나 올린 뒤 그 칸에 넣는 것, stack[top--] 는 맨 위 칸을 읽은 뒤 top 을 하나 내리는 것이다. 읽은 값을 지우지는 않지만 top 이 내려갔으니 다음 push 가 그 칸을 덮는다.

따라가기 — 더미를 아래부터 적는다.

  1. push 5, push 2 → [5, 2]. pop → 2, 더미 [5]
  2. push 4, push 1 → [5, 4, 1]. pop → 1, 더미 [5, 4]
  3. push 3 → [5, 4, 3]. pop → 3, pop → 4, 더미 [5]
  4. push 6 → [5, 6]. pop → 6, pop → 5, 더미 []
  5. 찍힌 순서: 2 1 3 4 6 5 → 줄바꿈 없이 213465

헷갈리는 자리 — 꺼낸 값을 지웠다고 생각해 다음 push 의 자리를 잘못 잡는 것과, 큐(먼저 넣은 것이 먼저)로 착각해 5 부터 적는 것. printf 에 줄바꿈이 없으니 여섯 자리를 붙여 적는다.

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

2023-2회 · 12번C

다음 C 프로그램에 alpha, beta, gamma를 차례대로 입력했을 때 출력 결과를 줄마다 쓰시오.

#include <stdio.h> char buffer[30]; char *readValue(void) { scanf("%29s", buffer); return buffer; } int main(void) { char *first = readValue(); char *second = readValue(); char *third = readValue(); printf("%s\n", first); printf("%s\n", second); printf("%s", third); return 0; }
정답gamma
gamma
gamma

— gamma / gamma / gamma. 세 포인터가 전부 같은 배열 buffer 를 가리키고, 그 배열엔 마지막 입력만 남아 있다.

원리readValue 는 값을 새로 만들어 주는 함수가 아니다. 전역 배열 buffer 한 채에 입력을 써넣고, 그 집의 주소를 돌려줄 뿐이다. 집은 하나인데 주소를 적은 쪽지(first · second · third)가 세 장인 셈이다. 두 번째 입력이 들어오면 같은 집의 내용이 지워지고 beta 로, 세 번째에 gamma 로 바뀐다. 나중에 세 쪽지를 따라가 보면 전부 같은 집이라 같은 글자가 보인다. 값을 따로 보관하려면 strcpy 로 다른 배열에 복사해야 한다.

따라가기

  1. first = readValue() → buffer 에 "alpha", first = buffer 의 주소
  2. second = readValue() → buffer 가 "beta" 로 덮임, second = 같은 주소
  3. third = readValue() → buffer 가 "gamma" 로 덮임, third = 같은 주소
  4. 세 printf 는 모두 buffer 를 찍는다 → gamma 세 번. 앞 둘엔 줄바꿈이 있고 마지막엔 없다.

헷갈리는 자리 — alpha / beta / gamma 라고 적는 것. 포인터는 값을 담는 그릇이 아니라 주소를 적은 쪽지라는 것을 잊으면 그렇게 된다. 함수가 지역 배열을 돌려주지 않고 전역 배열을 쓰는 것이 이 코드의 함정 장치다.

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

2023-2회 · 14번C

선택 정렬로 배열을 오름차순 정렬하려고 한다. ①에 들어갈 비교 연산자를 쓰시오.

#include <stdio.h> int main(void) { int values[] = { 64, 25, 12, 22, 11 }; int size = sizeof(values) / sizeof(values[0]); for (int i = 0; i < size - 1; i++) { for (int j = i + 1; j < size; j++) { if (values[i] ( ① ) values[j]) { int temp = values[i]; values[i] = values[j]; values[j] = temp; } } } return 0; }
정답>

>. 오름차순은 앞 칸이 뒤 칸보다 클 때 자리를 바꿔야 한다.

원리 — 선택 정렬의 이 꼴은 i 번째 칸을 고정하고 그 뒤의 모든 칸 j 와 비교해, 조건이 맞으면 두 칸을 맞바꾼다. 안쪽 반복이 끝나면 i 칸에는 뒤쪽 전체에서 가장 작은 값이 남는다. 오름차순(작은 것부터)이 되려면 "앞이 뒤보다 크다"가 교환 조건이어야 하므로 부등호는 > 다. 내림차순이면 반대로 <. 부등호를 "정렬 결과와 반대인 상태를 잡아내는 조건"으로 읽으면 헷갈리지 않는다.

따라가기 — 64 25 12 22 11 에서 i = 0 만 본다.

  1. j = 1: 64 > 25 → 교환 → 25 64 12 22 11
  2. j = 2: 25 > 12 → 교환 → 12 64 25 22 11
  3. j = 3: 12 > 22 아니다, j = 4: 12 > 11 → 교환 → 11 64 25 22 12
  4. i = 0 자리에 최소값 11 이 앉았다. 같은 식으로 i 를 늘리면 11 12 22 25 64.

헷갈리는 자리>= 도 결과는 같지만 답은 문제가 준 꼴대로 > 하나를 적는다. < 를 넣으면 64 25 22 12 11 로 내림차순이 된다. 이 코드는 출력문이 없어 화면엔 아무것도 안 찍힌다는 것도 눈여겨본다.

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

2023-3회 · 3번C

다음 C 프로그램의 실행 결과를 줄바꿈까지 맞춰 쓰시오.

#include <stdio.h> int main(void) { char *word = "KOREA"; printf("%s\n", word); printf("%s\n", word + 1); printf("%c\n", *word); printf("%c\n", *(word + 3)); printf("%c", *word + 4); return 0; }
정답KOREA
OREA
K
E
O

— KOREA / OREA / K / E / O. 괄호가 있느냐 없느냐로 넷째 줄과 다섯째 줄이 갈린다.

원리char *word = "KOREA" 는 K · O · R · E · A · 끝표 여섯 칸의 첫 칸 주소다. 주소에 1 을 더하면(word + 1) 한 칸 옆의 주소이고, 거기서 %s 로 찍으면 그 칸부터 끝까지 나온다. 별표는 "그 주소의 칸을 열어라"이고, 열어 나온 것은 글자인데 글자는 사실 숫자(문자 코드)라 더하기가 된다. K 는 75, 75 + 4 = 79 는 O 다. *(word + 3) 은 "3칸 옆으로 간 뒤 연다", *word + 4 는 "첫 칸을 연 뒤 그 글자에 4 를 더한다" — 괄호가 순서를 바꾼다.

따라가기

  1. %s, word → 첫 칸부터 → KOREA
  2. %s, word + 1 → 둘째 칸부터 → OREA
  3. %c, *word → 첫 칸 글자 → K
  4. %c, *(word + 3) → 넷째 칸(0 · 1 · 2 · 3) 글자 → E
  5. %c, *word + 4 → K(75) + 4 = 79 → O. 마지막 줄엔 줄바꿈이 없다.

헷갈리는 자리 — 다섯째 줄을 A(4칸 옆 글자)로 적는 것. 별표가 먼저 붙어 글자를 꺼낸 뒤 4 를 더하므로 자리 이동이 아니라 글자 값 이동이다. 알파벳은 코드가 이어져 있어 K 에서 네 글자 뒤가 O 라고 세도 된다.

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

2024-2회 · 11번C

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

#include <stdio.h> #include <string.h> int main(void) { char text[10] = "abc"; strcpy(text + 3, "de"); printf("%d", (int)strlen(text) + (text[4] - 'a') + 1); return 0; }
정답10

— 10. "abc" 뒤에 "de" 를 이어 붙여 "abcde" 가 되고, 5 + (e − a) + 1 = 5 + 4 + 1 이다.

원리char text[10] = "abc" 는 열 칸짜리 상자 줄에 a · b · c · 끝표를 넣고 나머지는 0 으로 채운 것이다. text + 3 은 넷째 칸(첨자 3, 지금은 끝표가 있는 자리)의 주소이고, strcpy(text + 3, "de") 는 그 자리부터 d · e · 끝표를 써넣는다. 그러면 줄은 a b c d e 끝표가 되어 strlen(끝표 전까지 글자 수)이 5 다. 글자는 코드값이라 text[4] - 'a' 는 e 와 a 의 거리, 알파벳에서 e 는 a 로부터 네 번째이니 4 다.

따라가기

  1. 처음: [a, b, c, \0, 0, 0, 0, 0, 0, 0]
  2. strcpy 뒤: [a, b, c, d, e, \0, …] → strlen(text) = 5
  3. text[4] = 'e', 'e' − 'a' = 101 − 97 = 4
  4. 5 + 4 + 1 = 10

헷갈리는 자리 — strcpy 가 "이어 붙이기"가 아니라 "그 자리부터 덮어쓰기"라는 것. 시작 주소를 text 로 주면 "de" 가 a · b 를 덮어 "de" 만 남아 strlen 이 2 가 된다. 이 문제는 시작을 text + 3 으로 잡아 이어 붙이기처럼 보이게 한 것이다. 배열 크기 10 은 답에 안 쓰인다 — 크기와 길이는 다르다.

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

2024-2회 · 15번C

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

#include <stdio.h> int main(void) { int values[] = { 3, 5, 7, 9 }; int *p = values + 1; printf("%d", *p + *(p + 1) + values[3]); return 0; }
정답21

— 21. p 는 둘째 칸(5)을 가리키고, *p = 5, *(p + 1) = 7, values[3] = 9 를 더하면 21 이다.

원리 — 배열 이름 values 는 첫 칸의 주소다. 주소에 1 을 더하면 "한 칸 옆"이지 "1바이트 옆"이 아니다 — int 한 칸이 4바이트라도 values + 1 은 둘째 칸이다. 그래서 int *p = values + 1 은 p 를 둘째 칸(5)에 세워 두는 것이다. 별표는 그 칸을 여는 것이므로 *p 는 5, *(p + 1) 은 p 에서 한 칸 더 간 셋째 칸 7 이다. values[3] 은 첨자로 직접 넷째 칸을 여는 것이라 9 다.

따라가기

  1. values: 첨자 0 → 3, 1 → 5, 2 → 7, 3 → 9
  2. p = values + 1 → 첨자 1 의 주소. *p = 5
  3. *(p + 1) → 첨자 2 → 7
  4. values[3] → 9. 합 5 + 7 + 9 = 21

헷갈리는 자리*p + 1(값 5 에 1 을 더한 6)과 *(p + 1)(한 칸 옆의 값 7)의 차이다. 괄호가 있으면 자리를 옮기고, 없으면 값을 더한다. 또 p 가 첫 칸이 아니라 둘째 칸에서 시작한다는 것을 놓치면 3 + 5 + 9 = 17 로 틀린다.

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

2024-3회 · 19번C

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

#include <stdio.h> void update(int **values, int size) { for (int i = 0; i < size; i++) { *(*values + i) = (*(*values + i) + i) % size; } } int main(void) { int values[] = { 3, 1, 4, 1, 5 }; int *p = values; int **pp = &p; update(pp, 5); printf("%d", values[2]); return 0; }
정답1

— 1. update 가 배열을 제자리에서 (값 + 첨자) % 5 로 바꾸고, 셋째 칸은 (4 + 2) % 5 = 1 이 된다.

원리int *p = values 는 배열 첫 칸의 주소를 적은 쪽지 p, int **pp = &p 는 "쪽지 p 가 놓인 자리"의 주소를 적은 두 번째 쪽지다. 그래서 *values(함수 안 이름)는 쪽지 p 의 내용, 즉 배열 첫 칸의 주소이고, *values + i 는 그 주소에서 i 칸 옆, 앞에 별표를 하나 더 붙인 *(*values + i) 가 그 칸의 값이다. 이중 포인터라 해도 결국 두 번 따라가서 원래 배열의 칸에 도착한다는 것이 전부다. 복사본이 아니라 원래 배열을 고치므로 main 의 values 가 바뀐다.

따라가기 — values = [3, 1, 4, 1, 5], size = 5. 새 값 = (옛 값 + i) % 5.

  1. i = 0: (3 + 0) % 5 = 3
  2. i = 1: (1 + 1) % 5 = 2
  3. i = 2: (4 + 2) % 5 = 6 % 5 = 1
  4. i = 3: (1 + 3) % 5 = 4, i = 4: (5 + 4) % 5 = 4 → 배열 [3, 2, 1, 4, 4]
  5. values[2]1

헷갈리는 자리 — 별표 두 개를 보고 겁먹어 값을 안 바꾸는 함수라고 여기는 것. 주소를 넘겼으니 원본이 바뀐다. 또 % 를 빼먹고 6 을 적는 것 — 나머지 연산이 마지막에 있다.

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

2025-1회 · 10번C

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

#include <stdio.h> int main(void) { char letters[5] = { 'B', 'A', 'D', 'E', '\0' }; char target = 'C'; int pos; printf("%d\n", letters[3] - letters[1]); for (pos = 0; pos < 4; pos++) { if (letters[pos] > target) break; } for (int i = 4; i > pos; i--) { letters[i] = letters[i - 1]; } letters[pos] = target; for (int i = 0; i < 5; i++) printf("%c", letters[i]); return 0; }
정답4
BACDE

— 두 줄: 4 / BACDE. 첫 줄은 글자 코드의 차이, 둘째 줄은 C 를 제자리에 끼워 넣은 결과다.

원리 — 글자는 숫자(코드)라 빼기가 된다. E 는 69, A 는 65 이니 letters[3] - letters[1] 은 4 다. 둘째 부분은 "끼워 넣기"다. 배열 B A D E 에서 target C 보다 큰 첫 글자를 찾아(그 자리가 pos) 그 자리부터 뒤의 글자들을 한 칸씩 뒤로 밀고 빈 자리에 C 를 놓는다. 뒤에서부터 미는 이유는 앞에서부터 밀면 밀린 글자가 다음 글자를 덮어 버리기 때문이다. 다섯째 칸(끝표)이 밀려 사라지지만 출력은 다섯 글자를 그대로 찍는다.

따라가기

  1. 첫 printf: 'E' − 'A' = 69 − 65 = 4, 줄바꿈
  2. pos 찾기: B > C 아니다, A > C 아니다, D > C → break, pos = 2
  3. 밀기: i = 4 → letters[4] = letters[3] = 'E'; i = 3 → letters[3] = letters[2] = 'D' → [B, A, D, D, E]
  4. letters[2] = 'C' → [B, A, C, D, E] → BACDE

헷갈리는 자리 — B A 가 정렬돼 있지 않은데 "정렬된 배열에 끼워 넣기"로 넘겨짚어 ABCDE 라 적는 것. 코드는 정렬하지 않고 C 보다 큰 첫 글자 앞에 끼울 뿐이다. 첫 줄의 4 는 첨자 차이(3 − 1 = 2)가 아니라 글자 값 차이다.

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

2025-1회 · 11번C

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

#include <stdio.h> void place(int grid[3][3], const int values[9]) { for (int i = 0; i < 9; i++) { grid[((i + 1) / 3) % 3][(i + 1) % 3] = values[i]; } } int main(void) { int grid[3][3]; int values[9] = { 5, 2, 7, 4, 1, 8, 3, 6, 9 }; int total = 0; place(grid, values); for (int i = 0; i < 9; i++) { total += grid[i / 3][i % 3] * (i % 2 == 0 ? 1 : -1); } printf("%d", total); return 0; }
정답13

— 13. 아홉 값을 3×3 격자에 한 칸 밀린 자리로 채운 뒤 짝수 첨자는 더하고 홀수 첨자는 뺀다.

원리 — 2차원 배열의 칸 번호를 한 줄 번호로 바꾸는 규칙은 행 = 번호 / 3, 열 = 번호 % 3 이다(3열짜리). place 는 값 i 를 번호 i + 1 자리에 넣되 행에 % 3 을 한 번 더 걸어, 번호 9 는 행 3 이 아니라 행 0 으로 돌아간다. 즉 values 를 격자에 순서대로 넣되 한 칸 뒤로 밀고 마지막 값이 맨 앞으로 돈다. 합을 구할 때 i % 2 == 0 ? 1 : -1 은 첨자가 짝수면 +, 홀수면 − 라는 뜻이다.

따라가기

  1. i = 0 → grid[0][1] = 5, i = 1 → grid[0][2] = 2, i = 2 → grid[1][0] = 7
  2. i = 3 → grid[1][1] = 4, i = 4 → grid[1][2] = 1, i = 5 → grid[2][0] = 8
  3. i = 6 → grid[2][1] = 3, i = 7 → grid[2][2] = 6, i = 8 → (9 / 3) % 3 = 0, 9 % 3 = 0 → grid[0][0] = 9
  4. 격자를 한 줄로: 9 5 2 / 7 4 1 / 8 3 6
  5. 부호 번갈아: +9 −5 +2 −7 +4 −1 +8 −3 +6 = 13

헷갈리는 자리 — i = 8 에서 행이 3 이 되어 배열 밖이라고 보는 것. % 3 때문에 0 행으로 돈다. 그리고 부호는 값의 첨자(i)가 아니라 합칠 때의 i 로 정해진다 — 격자를 한 줄로 편 뒤에 번갈아 붙인다.

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

2025-3회 · 2번C

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

#include <stdio.h> struct Item { int offset; const char *text; }; int main(void) { struct Item items[] = { {1, "AB"}, {2, "DC"}, {3, "EB"} }; struct Item *p = &items[1]; printf("%s", p->text + (p->offset - 1)); return 0; }
정답C

— C. p 는 둘째 원소 {2, "DC"} 를 가리키고, "DC" 의 주소에서 한 칸 옆부터 끝까지 찍으면 C 다.

원리 — 구조체 배열의 원소 하나는 offset 과 text 두 칸을 가진 묶음이다. &items[1] 은 둘째 묶음의 주소이고, p->text 는 그 묶음 안의 text, 즉 문자열 "DC" 의 첫 글자 주소다. 문자열 주소에 수를 더하면 그만큼 옆 글자부터 시작하는 문자열이 된다. %s 는 시작 주소부터 끝표 전까지 찍으므로 "DC" + 1 은 "C" 다. 화살표 -> 는 "포인터가 가리키는 구조체의 그 칸"이라는 뜻으로 (*p).text 와 같다.

따라가기

  1. items[0] = {1, "AB"}, items[1] = {2, "DC"}, items[2] = {3, "EB"}
  2. p = &items[1] → p->offset = 2, p->text = "DC"
  3. p->offset − 1 = 1 → p->text + 1 = 'C' 의 주소
  4. %s → 거기서 끝표까지 → C

헷갈리는 자리 — items[1] 을 "1번째"로 읽어 첫 묶음 {1, "AB"} 로 보는 것(첨자는 0 부터). 그리고 %c 가 아니라 %s 라 글자 하나가 아니라 그 자리부터 끝까지인데, 여기선 남은 글자가 C 하나라 결과가 같아 보일 뿐이다.

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

2026-1회 · 2번C

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

#include <stdio.h> double arr1(int p[], int len) { double av = 0; int i; for (i = 0; i < len; i++) { av += (double)p[i]; } return av / len; } double arr2(int *p, int len) { double av = 0; int i; for (i = 0; i < len; i++) { av += (double)(*(p + i)); } return av / len; } int main(void) { int arr[10] = { 80, 20, 50, 55, 45, 95, 55, 10, 40, 80 }; int len = 10; printf("%.2f", arr1(arr, len) + arr2(arr, len)); return 0; }
정답106.00

— 106.00. 두 함수는 같은 배열의 평균 53.0 을 각각 돌려주고, %.2f 가 소수 둘째 자리까지 찍는다.

원리 — 함수 매개변수의 int p[]int *p 는 완전히 같은 것이다. 배열을 넘기면 복사되지 않고 첫 칸 주소만 넘어가므로, 어느 쪽으로 적어도 함수 안의 p 는 포인터다. 따라서 p[i]*(p + i) 도 같은 칸을 읽는다 — 대괄호는 "주소에 i 를 더해 연다"의 다른 표기일 뿐이다. 두 함수는 이름과 표기만 다르고 하는 일이 같아 결과가 같다. (double) 은 정수를 실수로 바꿔 나눗셈에서 소수점이 잘리지 않게 한다.

따라가기

  1. 합: 80 + 20 + 50 + 55 + 45 + 95 + 55 + 10 + 40 + 80 = 530
  2. arr1 = 530 / 10 = 53.0, arr2 = 53.0
  3. 53.0 + 53.0 = 106.0 → %.2f106.00

헷갈리는 자리 — 106 이나 106.0 으로 적는 것. 서식 문자열이 답의 일부라 소수점 아래 두 자리까지 적어야 한다. 또 "arr2 가 포인터라 다른 값이 나온다"고 넘겨짚는 것 — 표기가 다를 뿐 같은 칸이다. (double) 을 빼면 av 가 double 이라 어차피 실수 나눗셈이지만, av 까지 int 였다면 53 이 아니라 소수점이 잘린 값이 나온다 — 나눗셈은 두 쪽 중 하나만 실수여도 실수로 계산된다.

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

2026-2회 · 12번C

다음 C 프로그램의 실행 결과를 줄 단위로 쓰시오. 단, sizeof(int)=4, sizeof(int*)=8이다.

#include <stdio.h> void function1(int *p) { *p = 50; } void function2(int p) { p = 60; } void function3(int arr[]) { printf("3.%zu\n", sizeof(arr) / sizeof(arr[0])); } void function4(int (*arr)[8]) { printf("4.%zu\n", sizeof(*arr) / sizeof((*arr)[0])); } int main(void) { int value = 10; int arr[8] = { 0 }; function1(&value); printf("1.%d\n", value); function2(value); printf("2.%d\n", value); function3(arr); function4(&arr); return 0; }
정답1.50
2.50
3.2
4.8

— 네 줄: 1.50 / 2.50 / 3.2 / 4.8. 주소를 넘긴 함수만 원본을 바꾸고, 배열 매개변수의 sizeof 는 포인터 크기가 된다.

원리 — 함수에 값을 넘기면 복사본이 만들어져 함수 안에서 아무리 바꿔도 원본은 그대로다(function2). 주소를 넘기면 함수가 그 주소로 찾아가 원본을 고친다(function1). 배열을 매개변수로 받을 때 int arr[] 라고 써도 실제로는 int *arr, 즉 주소 하나만 받는다. 그래서 sizeof(arr) 는 배열 전체(32)가 아니라 포인터 크기 8 이다. 반면 int (*arr)[8] 은 "int 8개짜리 배열을 가리키는 포인터"라 *arr 가 배열 자체이고 sizeof(*arr) 는 4 × 8 = 32 다.

따라가기

  1. function1(&value) → *p = 50 → 원본 value = 50 → 1.50
  2. function2(value) → 복사본 p 만 60 → value 는 여전히 50 → 2.50
  3. function3(arr) → sizeof(arr) = 8(포인터), sizeof(arr[0]) = 4 → 8 / 4 = 2 → 3.2
  4. function4(&arr) → sizeof(*arr) = 32, / 4 = 8 → 4.8

헷갈리는 자리 — 둘째 줄을 2.60 으로 적는 것(값 전달은 원본을 못 바꾼다)과 셋째 줄을 3.8 로 적는 것(배열 매개변수는 포인터). "1." "2." 같은 앞 글자는 프로그램이 찍는 것이라 답에 그대로 들어간다. 문제가 sizeof(int) = 4, sizeof(int*) = 8 을 준 이유가 셋째 줄이다.

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

예상 1회 · 2번C빈출 원리: 포인터·문자 연산

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

char *s = "INFORMATION"; printf("%s\n", s + 6); printf("%c\n", *(s + 2)); printf("%c\n", *s + 2);
정답ATION / F / K

— ATION / F / K (세 줄). 포인터에 더하는 것과 글자에 더하는 것이 다르다는 것을 한 문제에 넣었다.

원리 — s 는 문자열 "INFORMATION" 의 첫 글자 주소를 담은 포인터다. s + 6 은 "여섯 글자 뒤의 주소"이고 %s 는 그 주소부터 널 문자까지 찍으니 7번째 글자부터 끝까지 나온다. *(s + 2) 는 "두 글자 뒤 주소의 내용" = 세 번째 글자 하나다. *s + 2 는 우선순위가 다르다 — * 가 + 보다 먼저라 "첫 글자의 내용"에 2 를 더한 것이고, 문자는 정수(아스키 코드)라 'I' 의 코드 73 + 2 = 75 = 'K' 가 된다. 괄호 위치 하나가 "주소를 옮기나, 글자를 바꾸나"를 가른다.

따라가기

  1. I(0) N(1) F(2) O(3) R(4) M(5) A(6) T I O N — 번호를 매긴다
  2. s + 6 → 6번부터 끝까지 → ATION
  3. *(s + 2) → 2번 글자 → F
  4. *s + 2 → 'I'(73) + 2 = 75 → K

헷갈리는 자리 — *s + 2 를 *(s + 2) 로 읽어 F 라고 적는 것. 괄호가 없으면 * 가 먼저 붙는다. 그리고 %c 로 찍으니 숫자 75 가 아니라 글자 K 다 — %d 였다면 75 가 찍힌다. 번호는 0 부터 세는 것도 늘 확인한다.

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

예상 1회 · 9번C빈출 원리: 2차원 배열 + 포인터

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

int a[2][3] = {{1,2,3},{4,5,6}}; int *p = a[0]; printf("%d %d", *(p+4), a[1][0]);
정답5 4

— 5 4. 2차원 배열도 메모리에는 한 줄로 놓이므로, 첫 칸 주소에서 4칸 뒤는 두 번째 행의 두 번째 원소다.

원리 — int a[2][3] 은 "3칸짜리 행이 2개"이고 메모리에는 1 2 3 4 5 6 이 이어서 놓인다(행 우선). a[0] 은 첫 행의 시작 주소이므로 p = a[0] 은 1 이 있는 칸을 가리킨다. p + 4 는 int 4개 뒤, 곧 통째로 세었을 때 5번째(0부터 4번) 칸이라 값은 5 다. a[1][0] 은 두 번째 행의 첫 원소 4 다. 2차원 첨자 [i][j] 를 1차원 번호로 바꾸면 i × 열 수 + j 이고, 거꾸로 번호 4 는 4 ÷ 3 = 1 행, 4 % 3 = 1 열 → a[1][1] = 5 다.

따라가기

  1. 메모리 한 줄: [1 2 3 4 5 6], 번호 0~5
  2. p = a[0] → 번호 0. *(p + 4) → 번호 4 → 5
  3. a[1][0] → 1 × 3 + 0 = 번호 3 → 4
  4. 출력 5 4

헷갈리는 자리 — p 가 int * 이므로 p + 4 는 4바이트가 아니라 int 4개(16바이트)만큼 간다는 것. 그리고 a + 1 은 "행 하나(int 3개)" 를 건너뛰지만 a[0] + 1 은 "원소 하나"를 건너뛴다 — 같은 + 1 인데 타입에 따라 보폭이 다르다.

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

예상 2회 · 2번C빈출 원리: 배열 순회 + 조건 분리

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

int a[] = {3, 8, 5, 2, 7}; int s1 = 0, s2 = 0; for (int i = 0; i < 5; i++) { if (a[i] % 2 == 0) s1 += a[i]; else s2 += a[i]; } printf("%d,%d", s1, s2);
정답10,15

— 10,15. 짝수 8 + 2 = 10, 홀수 3 + 5 + 7 = 15 다.

원리 — % 는 나머지 연산이고, a[i] % 2 가 0 이면 짝수다. 반복문이 배열 5칸을 차례로 보며 짝수는 s1 에, 홀수는 s2 에 더한다. if 에 중괄호가 없으면 바로 다음 문장 하나만 if 에 속하고, else 는 그 if 의 짝이다 — 한 줄에 적혀 있어도 구조는 if { s1 += } else { s2 += } 다. printf 서식이 "%d,%d" 라 두 수 사이에 쉼표가 공백 없이 붙는다 — 서식 문자열의 글자는 그대로 찍힌다.

따라가기

  1. 3 → 홀수 → s2 = 3
  2. 8 → 짝수 → s1 = 8
  3. 5 → 홀수 → s2 = 8
  4. 2 → 짝수 → s1 = 10
  5. 7 → 홀수 → s2 = 15
  6. 출력 10,15

헷갈리는 자리 — 서식의 쉼표를 빼고 "10 15" 로 적는 것. 시험은 출력을 글자 그대로 채점하니 서식 문자열의 구두점 · 공백을 그대로 옮긴다. 음수가 섞였다면 C 의 % 는 부호가 피제수를 따라 -3 % 2 = -1 이라 "== 0" 판정은 그대로 되지만 "== 1" 판정은 깨진다는 것도 알아 둔다.

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

05틀리는 자리

왜 이걸 하나 — 코드 추적은 원리를 알아도 표기와 줄에서 깎인다.

자리새는 꼴적는 법기출
줄 수5줄 출력에서 한 줄을 빼먹는다printf 마다 \n 이 있는지 보고 줄 수를 먼저 센다2023-1회 2번 · 2023-3회 3번
괄호*p + 1 을 옆 칸으로 읽는다* 가 먼저. 괄호가 있어야 주소가 먼저 움직인다2023-3회 3번 · 2025-3회 2번
소수점 · 서식%.2f 를 정수로, %zu 앞의 「3.」을 소수점으로서식을 그대로 옮긴다. 2026-2회 12번의 3.2 는 「3.」 뒤에 22026-1회 2번 · 2026-2회 12번
공유 버퍼세 포인터에 세 값을 적는다같은 전역 배열이면 마지막 값이 전부2023-2회 12번
순환 첨자(i + 1) % 5i + 1마지막에 첫 칸으로 돌아가는 % 가 빈칸의 핵심2023-2회 9번
비교 방향오름차순에 < 를 적는다앞이 뒤보다 크면 바꾼다 → >2023-2회 14번
변수명빈칸에 비슷한 이름(idx1 · length)을 적는다코드에 선언된 이름 그대로. swap 의 마지막은 idx2, 호출은 size2023-1회 14번
공백 · 줄바꿈「4 BACDE」를 한 줄로「두 줄로 쓰시오」면 두 줄. printf 의 \n 을 따른다2025-1회 10번

C 출력 문제는 printf 의 서식 문자열이 곧 답의 꼴이다. 답을 적기 전에 서식(%d %c %s %.2f, 공백, \n)을 답 칸 옆에 베껴 두면 표기 감점이 사라진다.

06훈련

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

훈련 10문항은 포인터 문자열 3종, 포인터 누적, 2차원 이중 역참조, swap, 최솟값 · 최댓값, 버블 1회전, 선택 정렬, 포인터 순회, 값 · 주소 전달, 인덱스 체인이다. 정답은 전부 gcc 로 실행해 확인했다.

훈련 C · 1번C포인터 문자열 3종 세트
char *p = "COMPUTER"; printf("%s\n", p + 3); printf("%c\n", *(p + 1)); printf("%c\n", *p + 1);
정답PUTER / O / D (3줄)

— PUTER / O / D (세 줄). P-1-02 와 같은 세 가지 — 주소 이동 · 글자 하나 · 글자 더하기다.

원리 — p 는 "COMPUTER" 의 첫 글자 주소다. p + 3 은 세 글자 뒤 주소이고 %s 는 거기서부터 끝까지 찍는다. *(p + 1) 은 한 글자 뒤의 내용, 곧 두 번째 글자다. *p + 1 은 * 가 먼저 붙어 첫 글자 'C'(67) 에 1 을 더한 68 = 'D' 다. 문자는 정수로 다뤄지므로 더하기가 되고, %c 가 그 정수를 글자로 찍는다.

따라가기

  1. C(0) O(1) M(2) P(3) U T E R
  2. p + 3 → 3번부터 → PUTER
  3. *(p + 1) → 1번 → O
  4. *p + 1 → 'C' + 1 → D

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

훈련 C · 2번C포인터 + 건너뛰기 누적
int a[5] = {2, 4, 6, 8, 10}; int *p = a, s = 0; for (int i = 0; i < 5; i += 2) s += *(p + i); printf("%d", s);
정답18

— 18. i 가 0 · 2 · 4 일 때의 원소 2 + 6 + 10 이다.

원리 — 배열 이름 a 는 첫 원소의 주소로 쓰이므로 p = a 는 p 가 a[0] 을 가리키게 한다. *(p + i) 는 a[i] 와 완전히 같은 뜻이다 — 포인터 산술이 곧 첨자다. 반복문은 i += 2 라 0 · 2 · 4 만 돌고 5 에서 멈춘다(5 < 5 거짓). 그래서 짝수 번 칸만 더한다. 조건식이 i < 5 인지 i <= 5 인지에 따라 마지막 칸 포함 여부가 갈리니 반드시 확인한다.

따라가기

  1. i = 0 → a[0] = 2 → s = 2
  2. i = 2 → a[2] = 6 → s = 8
  3. i = 4 → a[4] = 10 → s = 18
  4. i = 6 → 5 미만 아님 → 종료, 출력 18

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

훈련 C · 3번C2차원 배열 이중 역참조
int a[2][3] = {{1,2,3},{4,5,6}}; printf("%d %d", *(*(a+1)+2), a[0][1] + *(a[1]));
정답6 6

— 6 6. *(*(a+1)+2) 는 a[1][2] = 6 이고, a[0][1] + *(a[1]) 은 2 + 4 = 6 이다.

원리 — 2차원 배열에서 a + 1 은 "두 번째 행"의 주소다(행 하나만큼 건너뜀). *(a + 1) 은 그 행 자체, 곧 a[1] 이고 이것은 다시 첫 원소의 주소로 쓰인다. 거기에 + 2 하고 * 를 붙이면 a[1][2] 다. 즉 a[i][j] 는 *(*(a + i) + j) 의 줄임이다. *(a[1]) 은 a[1] 행의 첫 원소 a[1][0] = 4 다.

따라가기

  1. a = [[1 2 3] [4 5 6]]
  2. *(*(a+1)+2) = a[1][2] = 6
  3. a[0][1] = 2, *(a[1]) = a[1][0] = 4 → 2 + 4 = 6
  4. 출력 6 6

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

훈련 C · 4번C포인터 swap
void swap(int *x, int *y) { int t = *x; *x = *y; *y = t; } int main() { int a = 3, b = 7; swap(&a, &b); printf("%d %d", a, b); }
정답7 3

— 7 3. 주소를 넘겼으므로 함수 안에서 바꾼 것이 main 의 a · b 에 그대로 반영된다.

원리 — C 는 값 복사로 인자를 넘긴다. swap(a, b) 로 값을 넘기면 함수 안의 복사본만 바뀌고 main 의 a · b 는 그대로다. 그래서 &a · &b 로 "주소"를 넘기고, 함수는 *x · *y 로 그 주소의 내용을 직접 읽고 쓴다. t 에 *x 를 보관하고 *x 에 *y 를 넣고 *y 에 t 를 넣으면 두 칸의 내용이 맞바뀐다. 이것이 C 에서 함수가 호출자의 변수를 바꾸는 유일한 방법이다.

따라가기

  1. a = 3, b = 7. swap(&a, &b) → x 는 a 의 주소, y 는 b 의 주소
  2. t = *x = 3 → *x = *y → a = 7 → *y = t → b = 3
  3. 출력 7 3

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

훈련 C · 23번C최솟값·최댓값 동시 탐색
int a[] = {7, 3, 9, 1, 5}; int min = a[0], max = a[0]; for (int i = 1; i < 5; i++) { if (a[i] < min) min = a[i]; if (a[i] > max) max = a[i]; } printf("%d %d %d", min, max, max - min);
정답1 9 8

— 1 9 8. 최솟값 1, 최댓값 9, 차 8 이다.

원리 — 최솟값 · 최댓값 찾기의 정석은 "첫 원소로 시작해 나머지를 하나씩 비교"다. min 과 max 를 a[0] = 7 로 두고 i = 1 부터 본다. 더 작으면 min 을 갈아 끼우고, 더 크면 max 를 갈아 끼운다. 두 if 가 독립이라 한 원소가 둘 다 갱신할 수도, 둘 다 안 할 수도 있다. 0 이 아니라 a[0] 으로 시작하는 이유는 전부 음수이거나 전부 큰 수일 때도 맞게 하기 위해서다.

따라가기

  1. 시작 min = max = 7
  2. 3 → min 3 · 9 → max 9 · 1 → min 1 · 5 → 변화 없음
  3. 출력 1 9 (9 − 1) → 1 9 8

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

훈련 C · 24번C버블 정렬 1회전 (중간 상태!)
int a[] = {5, 2, 4, 1, 3}; for (int j = 0; j < 4; j++) if (a[j] > a[j+1]) { int t = a[j]; a[j] = a[j+1]; a[j+1] = t; } for (int j = 0; j < 5; j++) printf("%d", a[j]);
정답24135

— 24135. 버블 정렬을 "한 바퀴만" 돌린 결과라 완전히 정렬되지 않았다.

원리 — 버블 정렬은 이웃한 두 칸을 비교해 앞이 크면 바꾸며 오른쪽으로 간다. 한 바퀴가 끝나면 가장 큰 값이 맨 끝에 가 있다(거품이 떠오르듯). 완전 정렬은 바깥 반복이 n−1 바퀴 필요한데 이 코드는 바깥 반복이 없이 j 반복 한 번뿐이다. 그러니 "가장 큰 5 가 끝으로 간 것" 까지만 보장되고 나머지는 어정쩡하다. 문제는 그 어정쩡한 상태를 정확히 적으라는 것이다.

따라가기

  1. [5 2 4 1 3] j=0: 5 > 2 교환 → [2 5 4 1 3]
  2. j=1: 5 > 4 교환 → [2 4 5 1 3] · j=2: 5 > 1 교환 → [2 4 1 5 3]
  3. j=3: 5 > 3 교환 → [2 4 1 3 5] → 출력 24135

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

훈련 C · 25번C선택 정렬 완주
int a[] = {4, 1, 3, 2}; for (int i = 0; i < 3; i++) for (int j = i + 1; j < 4; j++) if (a[i] > a[j]) { int t = a[i]; a[i] = a[j]; a[j] = t; } printf("%d%d%d%d", a[0], a[1], a[2], a[3]);
정답1234

— 1234. 선택 정렬 꼴의 이중 반복이라 끝까지 돌면 오름차순이 된다.

원리 — 바깥 i 가 "지금 채울 자리", 안쪽 j 가 "그 뒤의 모든 칸"이다. a[i] 보다 작은 a[j] 를 만날 때마다 바꾸므로 안쪽 반복이 끝나면 a[i] 에는 i 번부터 끝까지 중 가장 작은 값이 와 있다. i 를 0 · 1 · 2 로 옮기며 반복하면 앞에서부터 차례로 최솟값이 채워져 정렬이 끝난다. 마지막 칸은 남은 하나라 자동으로 맞다(그래서 i < 3).

따라가기

  1. i=0: [4 1 3 2] → j=1 교환 [1 4 3 2] → j=2 · 3 변화 없음
  2. i=1: j=2 교환 [1 3 4 2] → j=3 교환 [1 2 4 3]
  3. i=2: j=3 교환 [1 2 3 4] → 출력 1234

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

훈련 C · 26번C포인터 순회 + 모음 세기
char *p = "informatics"; int c = 0; while (*p) { if (*p=='a' || *p=='e' || *p=='i' || *p=='o' || *p=='u') c++; p++; } printf("%d", c);
정답4

— 4. "informatics" 의 모음 i · o · a · i 를 센다.

원리 — while (*p) 는 "p 가 가리키는 글자가 널('\0', 값 0)이 아닌 동안"이다. C 문자열은 끝에 널이 있어 이 조건이 자연스럽게 끝을 찾는다. 안에서 글자를 다섯 모음과 비교해 맞으면 c 를 올리고, p++ 로 다음 글자로 옮긴다. 대문자 모음은 검사에 없으니 소문자만 센다 — 이 문자열은 전부 소문자라 상관없다. || 로 이은 다섯 비교 중 하나라도 참이면 전체가 참이다.

따라가기

  1. i n f o r m a t i c s
  2. 모음: i(1) o(2) a(3) i(4)
  3. 출력 4

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

훈련 C · 28번C값 전달 vs 주소 전달
void f(int n) { n = n + 10; } void g(int *n) { *n = *n + 10; } int main() { int a = 5; f(a); printf("%d ", a); g(&a); printf("%d", a); }
정답5 15

— 5 15. 값으로 넘긴 f 는 원본을 못 바꾸고, 주소로 넘긴 g 는 바꾼다.

원리 — f(a) 는 a 의 값 5 를 복사해 n 에 넣는다. n = n + 10 은 그 복사본만 15 로 만들고 함수가 끝나면 사라진다 — main 의 a 는 5 그대로다. g(&a) 는 a 의 주소를 넘긴다. *n = *n + 10 은 "그 주소의 내용"을 바꾸므로 main 의 a 가 15 가 된다. 같은 "+10" 인데 무엇을 넘겼느냐로 결과가 갈린다. 이것이 T-C-04 swap 이 포인터를 쓰는 이유이기도 하다.

따라가기

  1. f(a) → 복사본 15, a 는 5 → 출력 "5 "
  2. g(&a) → a 자체가 15 → 출력 "15"
  3. 합쳐 5 15

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

훈련 C · 29번C인덱스 체인 (연결리스트의 원리)
int data[] = {10, 20, 30, 40}; int next[] = {2, -1, 3, 1}; int cur = 0; while (cur != -1) { printf("%d ", data[cur]); cur = next[cur]; }
정답10 30 40 20

— 10 30 40 20. 배열 두 개로 흉내 낸 연결 리스트를 next 를 따라 도는 것이다.

원리 — data 는 값, next 는 "다음 칸 번호"다. cur 이 지금 칸이고, 값을 찍은 뒤 cur = next[cur] 로 다음 칸으로 건너뛴다. −1 은 "끝"이라는 약속이라 while (cur != −1) 이 멈춘다. 배열 번호 순서(0 1 2 3)가 아니라 next 가 가리키는 순서(0 → 2 → 3 → 1)로 도는 것이 핵심이다. 포인터 없이 배열 인덱스로 링크를 표현한 연결 리스트다.

따라가기

  1. cur 0 → 10, next[0] = 2
  2. cur 2 → 30, next[2] = 3 · cur 3 → 40, next[3] = 1
  3. cur 1 → 20, next[1] = −1 → 끝. 출력 10 30 40 20

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

직접 돌려 보는 법

이 컴퓨터에는 gcc 가 없어 도커로 돌렸다. 파워셸에서 코드를 main.c 로 저장한 뒤 아래를 치면 컴파일과 실행이 한 번에 된다(도커 데스크톱이 떠 있어야 한다).

docker run --rm -v "${PWD}:/w" -w /w alpine:3.20 sh -c "apk add --no-cache gcc musl-dev >/dev/null; gcc -w main.c -o a.out && ./a.out"

이 장의 기출 15개 · 훈련 10개는 위와 같은 방법(alpine 3.20 의 gcc)으로 실행해 답을 확인했다. sizeof(int)=4 · sizeof(int*)=8 인 64비트 환경이라 2026-2회 12번의 전제와 같다.

07백지 재현

왜 이걸 하나 — 아래를 안 보고 적을 수 있어야 포인터 문제가 끝난다.