정보처리기사 실기 개념정리 목차

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)
%x16진수 출력값이 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는 그 자체로 함수의 주소&dummydummy 둘 다 대입된다. 그리고 최종 출력이 %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)기저 → 00
c(1)기저 → 11
c(2)c(1) + c(-1) = 1 + (−1)0
c(3)c(2) + c(0) = 0 + 00
c(4)c(3) + c(1) = 0 + 11
c(5)c(4) + c(2) = 1 + 01

함정 — 기저 조건이 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. 시험장 체크리스트

  1. 서식 지정자 확인 — %.2f, %x, %u, %s+오프셋.
  2. p[i] ≡ *(p+i) 로 표기를 통일한 뒤 굴린다.
  3. 함수가 값을 바꾸려면 주소를 받아야 한다. 안 받았으면 그 함수는 장식이다.
  4. static은 누적, 지역은 리셋.
  5. switch에서 break 위치부터 표시한다.
  6. 연결리스트·트리는 무조건 그림. 화살표 재배선을 코드 한 줄마다 반영.
  7. 우선순위 애매하면 사슬(산술>시프트>비교>비트>논리) 암송.
  8. 재귀는 호출 트리 + 기저 반환값(음수 가능) 확인.
  9. 답 쓰기 전 마지막 질문: "출력 문장이 정확히 뭘, 어떤 서식으로 찍는가?"

출처

  • 2024-1 ~ 2026-2 실기 복원 기출 (이 트랙 수록 8회차) — 기사퍼스트·두목넷·수제비(공개 미러)·뉴비티·chobopark 교차 검증
  • 본문 코드 전량 clang 컴파일·실행 검증 완료
시험 구조·기출 경향Java — 바인딩 규칙