C 언어 — 코드 추적 함정 해부
실기 코딩 문항의 최대 지분이 C다 (2023-2회엔 한 회차에 7문항). 정의를 묻지 않는다 — 코드를 주고 출력값을 딱 하나 쓰라고 한다. 부분점수가 없으므로 마지막 한 글자까지 정확해야 5점이다. 이 편의 예제는 전부 2024~2026년 실제 기출 복원 코드이고, 전부 컴파일·실행으로 출력을 검증했다.
유일하게 작동하는 풀이법: 종이에 변수 표를 그리고 한 줄씩 굴린다. 눈으로 풀면 시프트 우선순위나 fall-through 같은 함정 하나에 5점이 통째로 날아간다.
1. 출력 서식이 정답의 절반이다
서식 지정자를 놓치면 계산을 다 맞히고도 오답이 된다. 실제로 채점 코멘트에 "106이라고 쓰면 오답"이 명시된 문항(2026-1회 1번)이 있다.
| 서식 | 의미 | 기출 함정 |
|---|---|---|
%.2f | 소수점 둘째 자리까지 | 106 ❌ → 106.00 ✓ (2026-1), 2795.1 ❌ → 2795.10 ✓ (2024-1) |
%x | 16진수 출력 | 값이 32면 출력은 20 (2026-1 함수 포인터 문항) |
%u | 부호 없는 10진수 | 답안에 u를 붙이지 말 것 — 숫자만 (2025-3) |
%s + 포인터 | 그 주소부터 끝까지 문자열 | "DC" + 1 → "C" (한 글자가 아니라 부분 문자열, 2025-3) |
%c | 문자 하나 | putchar(str[a-2]) — 인덱스 계산이 관건 (2025-3) |
/* 2026-1 기출 — 같은 평균을 배열 표기와 포인터 산술로 두 번 계산 */
double arr1(int p[], int len) { /* av += (double) p[i]; */ }
double arr2(int *p, int len) { /* av += (double)(*(p + i)); */ }
/* printf("%.2f", arr1(...) + arr2(...)); → 53.0 + 53.0 = 106.00 */
함정 —
p[i]와*(p + i)는 완전히 같은 표현이다. 두 함수가 다른 값을 낼 것처럼 보이게 만드는 연출일 뿐이다. 이 등가는 §2의 모든 문제에 깔려 있다.
2. 포인터 — 표기만 다른 같은 것, 진짜 다른 것
2.1 등가 관계 (표기 변환표)
p[i] ≡ *(p + i) 배열 인덱싱 = 포인터 산술
*(*arr + i) ≡ arr[0][i] 이중 포인터를 한 겹 벗기면 배열
(*pptr)[1] ≡ ptr[1] pptr = &ptr 일 때
2024-3회 19번: *(*arr + i) = (*(*arr + i) + i) % size — 겁나 보이지만
arr[i] = (arr[i] + i) % size일 뿐이다. arr[2] = (4+2) % 5 = 1.
2.2 포인터 배열 vs 배열 포인터 — sizeof가 가른다
/* 2024-2 기출 */
int arr[3][3] = {1,2,3,4,5,6,7,8,9};
int* parr[2] = {arr[1], arr[2]}; /* 포인터 '배열' — 행 주소 2개 */
/* parr[1][1]=8, *(parr[1]+2)=9, **parr=arr[1][0]=4 → 합 21 */
/* 2026-2 기출 — 매개변수에서 배열은 포인터로 붕괴한다 */
void function3(int arr[]) { sizeof(arr) } /* 포인터 크기 8 → 8/4 = 2 */
void function4(int (*arr)[8]) { sizeof(*arr) } /* 배열 전체 32 → 32/4 = 8 */
함정 —
int arr[]매개변수는 선언처럼 보여도 포인터다. sizeof로 원소 수를 구하는 코드가 함수 안에 있으면 무조건 의심하라. 배열 포인터int (*arr)[8]만이 배열 크기 정보를 유지한다.
2.3 이중 포인터 — 한 겹씩 벗겨라
/* 2025-2 기출 */
struct dat a[] = {{1,2},{3,4},{5,6}};
struct dat* ptr = a;
struct dat** pptr = &ptr;
(*pptr)[1] = (*pptr)[2]; /* = a[1] = a[2] — 구조체 통째 대입 */
/* a[1].x, a[1].y → 5 그리고 6 */
함정 — C의 구조체 대입은 모든 멤버를 복사한다. "x만 복사되나?" 같은 고민은 함정 설계자의 의도다. 그리고
*pptr은 결국 ptr이고, ptr은 결국 a다 — 겹수를 종이에 적으며 벗기면 평범한 배열 문제가 된다.
2.4 함수 포인터 (2026-1 첫 출제 — 최고 난도)
struct fns { int* (*fn)(int*); }; /* fn = "int*를 받아 int*를 반환하는 함수"의 포인터 */
int* dummy(int *d) { return d + 1; }
...
struct fns mine;
int n[] = {16, 32};
mine.fn = dummy; /* 함수 이름은 곧 그 함수의 주소 */
printf("%x", *mine.fn(n)); /* mine.fn(n) = dummy(n) = &n[1] → *(&n[1])=32 → %x → 20 */
mine.fn(n)은dummy(n)호출과 같다(구조체 멤버로 담긴 함수 포인터를 호출). 반환은n+1(둘째 원소 주소), 역참조하면 32,%x라서 출력은 20.- 읽는 법: 선언을 안쪽에서 바깥으로 —
(*fn)(포인터인데)(int*)(int를 받아)int*(int 반환). 괄호(*fn)가 없으면 "int*를 반환하는 함수"가 되어 의미가 완전히 달라진다.
함정 — 함수 이름
dummy는 그 자체로 함수의 주소라&dummy나dummy둘 다 대입된다. 그리고 최종 출력이%x이므로 32가 아니라 20을 써야 한다(§1 서식 함정).
3. 문자열 — 널 종단이 모든 것을 지배한다
/* 2024-2 기출 — "teststring"(10글자)에 "first"(5글자)를 복사 */
void func(char *d, char *s) {
while (*s) { *d = *s; d++; s++; }
*d = '\0'; /* ← 이 줄이 문자열을 5에서 자른다 */
}
/* 이후 길이는 5 — 인덱스 합 0+1+2+3+4 = 10 (45가 아니다) */
/* 2025-3 기출 — 길이 구하고 끝에서 두 번째 */
char str[] = "REPUBLICOFKOREA"; /* 길이 15 */
while (str[a] != '\0') ++a; /* a = 15 */
putchar(str[a - 2]); /* str[13] = 'E' */
체크리스트:
while (*s)는 널 문자에서 멈춘다 — 복사 후 원본보다 짧아졌는지 확인.- 인덱스는 0부터: 길이 n이면 마지막 글자는
[n-1], 끝에서 둘째는[n-2]. - 문자 변환(시저류)은
(c - 'A' + k) % 26 + 'A'골격 — 분기마다 오프셋이 다른지 (대문자 +5, 소문자 +10, 숫자 +3 — 2024-1 기출) 확인하고, 모듈러 순환을 계산한다.'t'(19) + 10 = 29 → 29 % 26 = 3 →'d'.
4. 구조체 + 연결리스트 — 그림 없이 풀면 진다
2024년 이후 최다 함정 유형. 노드를 상자로, next를 화살표로 그려 놓고 코드가 화살표를 바꿀 때마다 그림을 고친다.
4.1 머리 삽입 = 역순 (스택)
/* 2025-2 기출 — "BEST"를 머리 삽입 → 출력 TSEB */
while (*s) {
n = malloc(sizeof(struct node));
n->c = *s++;
n->p = h; /* 새 노드가 기존 head를 가리키고 */
h = n; /* 자기가 head가 된다 → 항상 맨 앞에 붙음 */
}
패턴 인식 —
new->next = head; head = new;조합이 보이면 출력은 입력의 역순이다. 2025-1회(1~5 삽입 → 5,4,3,2,1)도 같은 골격.
4.2 노드 재배치·스왑
- 값 스왑 (2024-3): 인접 노드의
value만 맞바꾸고 두 칸 전진 — 링크는 그대로. 1→3→2 리스트에서 첫 쌍만 스왑되고 종료 → 312. - 링크 재배치 (2025-1): 값 3 노드를 떼어(
prev->next = curr->next) 맨 앞에 붙임 (curr->next = head; head = curr) → 5,4,3,2,1이 3,5,4,2,1로.
함정 — "떼어낸 자리"의 연결을 그림에서 지우지 않으면 순회가 틀린다. 3을 떼면 4는 2로 직결된다.
4.3 순회 + 누적 (해싱 흉내)
/* 2025-3 기출 — t3(11) → t2(7) → t1(5) 순회 */
while (curr) { sum = sum * 3 + curr->x; curr = curr->next; }
/* ((0*3+11)*3+7)*3+5 = 125 → (125 ^ 42) + 100 = 187 */
연결 순서는 대입 순서가 아니라 화살표 방향이 결정한다. t3.next = &t2; t2.next = &t1;
이면 시작점이 t3일 때 11, 7, 5 순이다.
4.4 원형 큐 (2025-2 기출)
rear = (rear + 1) % SIZE — 인덱스가 순환해도 FIFO는 FIFO다.
enq 1, 2 → deq(1 나감) → enq 3 → 이후 deq는 2, 3 순. 스택으로 착각하게 만드는 문제.
4.5 트리 순회 (2026-2 기출)
구조체 초기화로 트리를 만들고 재귀로 순회한다. func(n->a); func(n->b); 방문; 은
후위 순회 — 21(root), 좌 12(35, 53), 우 64이면 방문 순서 35→53→12→64→21.
"3번째 방문"(++c == 3) 같은 카운터 조건은 순회 순서를 종이에 다 적은 뒤 세면 된다.
5. 스코프와 수명 — swap은 대부분 가짜다
/* 2024-2 기출 — 이 swap은 아무것도 바꾸지 못한다 */
void swap() {
int a = 11, b = 19; /* main과 이름만 같은 지역 변수 */
int t = a; a = b; b = t;
}
/* 2026-2 기출 — 바꾸려면 주소를 넘겨야 한다 */
void function1(int *p) { *p = 50; } /* 원본 변경 ✓ */
void function2(int p) { p = 60; } /* 복사본 변경 — 원본 그대로 */
/* 2024-3 기출 — static은 호출 사이에 살아남는다 */
int func() {
static int x = 0; /* 최초 1회만 초기화 */
x += 2;
return x; /* 2, 4, 6, 8 — 4회 합 20 */
}
3종 세트로 기억 — ① 값 전달은 원본 불변, ② 포인터 전달만 원본 변경, ③ static 지역 변수는 전역처럼 누적. main에 같은 이름의 변수를 둬서 헷갈리게 만드는 것(2024-3의
int x = 1)까지가 출제 공식이다.
6. 제어 흐름 함정
6.1 switch fall-through (2024-2 기출)
switch(a) { /* a = 11 */
case 1: b += 1;
case 11: b += 2; /* 여기로 진입, break 없음 → */
default: b += 3; /* 여기까지 실행 */
break;
}
/* b = 19 + 2 + 3 = 24 → a - b = -13 */
break가 없는 case는 아래로 흘러내린다. case 순서대로 진입점만 찾고, 진입점부터 break(또는 끝)까지 전부 실행한다고 굴려라.
6.2 삼항 연산자 — 조건식의 '값'을 먼저 구한다
/* 2024-1 기출 */
if (v1 > v2 ? v2 : v1) { ... }
/* v1=0, v2=35: 0 > 35 거짓 → 식의 값 = v1 = 0 → if(0) → else 분기 */
삼항이 if 조건 자리에 들어오면 두 단계다: ① 삼항을 계산해 값 하나로 줄이고 ② 그 값의 참/거짓으로 분기한다.
6.3 연산자 우선순위 — 시프트는 &보다 세다
/* 2025-3 기출 */
z = y % 3 < 3 ? 2 : 1; /* %(산술) > <(비교) > ?: → z = 2 */
z = z & z >> 1; /* >>가 & 보다 먼저: 2 & 1 = 0 */
z = x > 5 && z <= 3 ? z * x : z / x; /* 참 → 0 * 7 = 0 */
기출에 반복되는 우선순위 사슬: *산술(%, ) > 시프트(<<, >>) > 비교(<, >) > 비트(&, ^, |) > 논리(&&, ||) > 삼항 > 대입.
z & z >> 1을 (z & z) >> 1로 읽는 순간 5점이 사라진다.
7. 비트 연산 — 이진수로 내려가서 계산한다
/* 2025-1 기출 — & 0xA5 마스킹 */
0xDB & 0xA5: 1101 1011
& 1010 0101
= 1000 0001 = 0x81 = 129
x << n= ×2ⁿ,x >> n= ÷2ⁿ (2024-1:29 << 2= 116).&는 마스킹(공통 1만 생존),^는 다른 비트만 1 (2025-3: 125 ^ 42 = 87).- 16진수 한 자리 = 이진수 4비트. 0xA5 = 1010 0101을 즉시 펼칠 수 있어야 한다.
8. 재귀 — 기저 조건의 반환값을 의심하라
/* 2026-2 기출 */
int c(int n) {
if (n <= 1) return n; /* ← n '자신'을 반환: c(-1) = -1 이다! */
return c(n - 1) + c(n - 3);
}
호출 트리를 아래에서 위로 채운다 (c(5)를 손으로 굴리면):
| 호출 | 전개 | 값 |
|---|---|---|
| c(-1) | 기저 (−1 ≤ 1) → n 반환 | −1 |
| c(0) | 기저 → 0 | 0 |
| c(1) | 기저 → 1 | 1 |
| c(2) | c(1) + c(-1) = 1 + (−1) | 0 |
| c(3) | c(2) + c(0) = 0 + 0 | 0 |
| c(4) | c(3) + c(1) = 0 + 1 | 1 |
| c(5) | c(4) + c(2) = 1 + 0 | 1 |
함정 — 기저 조건이
return 1이 아니라return n이면 음수 인자는 음수를 반환한다 (c(-1) = −1).n - 3같은 큰 감소폭은 인자를 음수로 밀어 넣기 위한 장치다. 위 표처럼 작은 인자부터 채워 올라가면 중복 계산 없이 정확하다.
호출 트리 굴리기 절차: ① 최초 호출을 쓰고 ② 기저에 걸릴 때까지 가지를 치고 ③ 잎(가장 작은 인자)의 값부터 채운 뒤 ④ 아래에서 위로 합산한다. 2025-1회 Java 분할정복(§02편)과 같은 기술이다.
꼬리질문 대비 — "기저가
return 1이었다면?"(c(-1)=1, c(0)=1이 되어 답이 완전히 달라짐 — 기저의 반환식에 항상 밑줄). "전위 vs 후위 증감?"(++x는 올리고 쓰고,x++는 쓰고 올림 —arr[i++]류에서 인덱스 함정).
9. C 문법 빠른 참조 (코드 문항 어디서든)
- 진법 리터럴:
0x는 16진수(0xA5=165),0으로 시작하면 8진수(010=8), 그냥 숫자는 10진수. 출력 서식%d(10진),%x(16진),%o(8진). - char 산술: 문자는 아스키 정수다.
'A'=65,'a'=97,'0'=48.'A'+1='B','9'-'0'=9(문자 숫자를 정수로), 대소문자 차 32. - 정수 나눗셈·나머지:
7/2=3(정수끼리는 몫만),7%2=1. 실수 결과를 원하면 한쪽을(double)로 캐스팅((double)7/2=3.5). - 증감 연산자: 전위
++x(먼저 올리고 사용), 후위x++(먼저 쓰고 올림).a[i++]는 현재 i로 접근 후 i 증가. - 논리 vs 비트:
&&/||(논리, 단축 평가) ≠&/|(비트).!(논리 부정) ≠~(비트 반전). - 배열·포인터: 배열 이름은 첫 원소의 주소.
a[i] ≡ *(a+i). 매개변수의int a[]는 포인터. - 구조체 접근: 값이면
s.x, 포인터면p->x(=(*p).x). - 삼항:
조건 ? a : b. 대입·비교보다 우선순위가 낮아 괄호를 잘 본다.
10. 고난도 함정 (자체 출제 hard 세트 대비)
기출엔 아직 안 나왔지만 언제든 나올 수 있는 C 함정 모음. 각 항목은 hard 세트의 실제 문항이다.
10.1 unsigned char 8비트 절단
unsigned char x = 0xB3;
x = (x << 1) | (x >> 7); /* int로 승격돼 359지만, uchar에 담기며 359%256=103(0x67) */
함정 —
x << 1은 계산 중 int로 승격돼 큰 값이 되지만,unsigned char에 다시 대입되는 순간 하위 8비트만 남는다(mod 256). 비트 연산 문제에서 중간 대입이 있으면 매번 8비트로 자른다고 보고 굴려라. (0x67 → ^0x5A=0x3D → &0xF0=0x30=48.)
10.2 콤마 연산자 — 맨 오른쪽 값이 결과
int t = (a++, b += a, a + b); /* 왼쪽부터 실행, 결과는 마지막 식 a+b */
,는 왼쪽부터 차례로 실행하고 맨 오른쪽 식의 값을 돌려준다.a++·b+=a의 부수 효과는 다 반영되고,t에는 마지막a+b만 담긴다. (선언의 콤마와 헷갈리지 말 것.)
10.3 배열 부분 초기화 — 나머지는 0
int a[5] = {1, 2}; /* → {1, 2, 0, 0, 0} */
초기화 목록이 배열 크기보다 짧으면 나머지 원소는 전부 0으로 채워진다.
int a[100]={0};이 전체를 0으로 만드는 관용구인 이유다. 채워지지 않은 원소를 쓰레기값으로 착각하지 말 것.
10.4 중첩 루프의 break는 안쪽만 빠진다
for (i=0; i<4; i++)
for (j=0; j<4; j++) { if (i+j==4) break; cnt++; } /* break는 j 루프만 탈출 */
break는 자신을 감싼 가장 안쪽 루프 하나만 빠져나온다. 바깥 for는 계속 돈다. (i별로 4,3,2,1회 → 합 10.) 바깥까지 나가려면 플래그나 goto가 필요하다.continue는 해당 반복만 건너뛴다.
10.5 스택으로 후위 계산
push(3); push(7); push(2);
int a=pop(), b=pop(); /* a=2, b=7 (마지막에 넣은 게 먼저 나옴, LIFO) */
pop()은 마지막에 push한 것부터 꺼낸다(LIFO).a=pop(), b=pop()이면 a가 top, b가 그 아래.pop()+pop()처럼 한 식에 두 번 쓰면 둘 다 꺼내진다.
11. 시험장 체크리스트
- 서식 지정자 확인 —
%.2f,%x,%u,%s+오프셋. p[i] ≡ *(p+i)로 표기를 통일한 뒤 굴린다.- 함수가 값을 바꾸려면 주소를 받아야 한다. 안 받았으면 그 함수는 장식이다.
- static은 누적, 지역은 리셋.
- switch에서 break 위치부터 표시한다.
- 연결리스트·트리는 무조건 그림. 화살표 재배선을 코드 한 줄마다 반영.
- 우선순위 애매하면 사슬(산술>시프트>비교>비트>논리) 암송.
- 재귀는 호출 트리 + 기저 반환값(음수 가능) 확인.
- 답 쓰기 전 마지막 질문: "출력 문장이 정확히 뭘, 어떤 서식으로 찍는가?"
출처
- 2024-1 ~ 2026-2 실기 복원 기출 (이 트랙 수록 8회차) — 기사퍼스트·두목넷·수제비(공개 미러)·뉴비티·chobopark 교차 검증
- 본문 코드 전량 clang 컴파일·실행 검증 완료