정보처리기사 실기 코딩 문제는 "다음 C 코드의 실행 결과(출력)를 쓰시오" 형태로 출제됩니다. 즉 코드가 화면에 무엇을 출력하는지 손으로 추적하는 능력이 핵심입니다. 아래는 실기 빈출 C 문법을 주제별로 정리한 것입니다.
자료형은 변수에 저장할 데이터의 종류를 정하는 것입니다. 실기에서는 자료형별 정확한 바이트 수보다, 자료형에 따라 나눗셈·출력 서식이 어떻게 달라지는지가 중요합니다.
정수형과 실수형
| 분류 | 자료형 | 크기 | 설명 |
|---|---|---|---|
| 정수형 | int | 4바이트 | 가장 기본. 약 -21억 ~ +21억 |
| 정수형 | char | 1바이트 | 문자 하나 / 작은 정수(내부는 ASCII 값) |
| 정수형 | long | 32~64비트 | int보다 큰 정수 |
| 정수형 | unsigned int | 4바이트 | 양수만(0 포함). 약 0 ~ 42억 |
| 실수형 | float | 4바이트 | 소수점 이하 약 6~7자리 |
| 실수형 | double | 8바이트 | 소수점 이하 약 15~16자리 |
int age = 25; // 정수 char grade = 'A'; // 문자 하나 (작은따옴표) float pi = 3.14f; // f를 붙이면 float double d = 3.141592; // 소수점 리터럴은 기본이 double
정수 리터럴 접미사
코드에 직접 쓴 값을 리터럴이라 하고, 뒤에 붙는 문자가 접미사입니다. 접미사는 값의 자료형을 지정합니다.
int a = 5; // 기본: int unsigned int b = 5u; // u : unsigned int long c = 5L; // L : long unsigned long d = 5UL; // UL: unsigned long
연결 리스트 등 실기 기출에서 구조체 초기화에 5u, 7u처럼 나오는데, 이는 멤버가 unsigned int로 선언되어 타입을 맞춘 것입니다.
char는 사실 정수형
컴퓨터는 문자를 ASCII 코드(숫자)로 저장합니다. 'A'는 내부적으로 65가 저장되므로, char로 작은 정수를 다룰 수 있습니다.
char c = 'A'; printf("%c", c); // A (문자로 출력) printf("%d", c); // 65 (숫자로 출력)
출력 함수와 서식 지정자
printf는 큰따옴표 안의 내용을 그대로 출력하고, %로 시작하는 서식 지정자 자리에 값이 들어갑니다.
| 서식 | 의미 | 예시 | 출력 |
|---|---|---|---|
| %d | 정수(10진수) | printf("%d", 42); | 42 |
| %c | 문자 한 개 | printf("%c", 'A'); | A |
| %s | 문자열 | printf("%s", "Hi"); | Hi |
| %f | 실수(기본 6자리) | printf("%f", 3.14); | 3.140000 |
| %x / %X | 16진수(소/대문자) | printf("%x", 255); | ff |
| %o | 8진수 | printf("%o", 8); | 10 |
| %e | 지수 표기 | printf("%e", 12345.6789); | 1.234568e+04 |
소수점 자릿수 지정 (%.Nf)
%f는 기본이 소수점 6자리입니다. %.Nf로 자릿수를 지정하며, 부족한 자리는 0으로 채웁니다. 실기에 자주 나옵니다.
double pi = 3.141592; printf("%.0f\n", pi); // 3 printf("%.2f\n", pi); // 3.14 printf("%.4f\n", pi); // 3.1416 (반올림) printf("%f\n", pi); // 3.141592 (기본 6자리) printf("%.2f", 2795.1); // 2795.10 (부족한 자리 0으로 채움)
같은 값, 다른 진법 출력
값은 하나인데 서식만 바꾸면 진법 표기가 달라집니다. 메모리에 저장된 비트는 동일합니다.
int n = 255; printf("%d\n", n); // 255 (10진수) printf("%x\n", n); // ff (16진수 소문자) printf("%X\n", n); // FF (16진수 대문자) printf("%o\n", n); // 377 (8진수)
putchar / puts
putchar('A'); // A (문자 한 개) putchar(65); // A (ASCII 65 = 'A') puts("Hello"); // Hello + 자동 줄바꿈
puts는 문자열 출력 후 자동 줄바꿈을 하고, printf는 \n을 직접 넣어야 합니다.
입력 함수 (scanf)와 & 의 이유
scanf는 입력값을 변수에 직접 저장해야 하므로, 변수의 주소(&)를 전달합니다. 값을 그대로 넘기면 복사본만 전달되어 원본이 바뀌지 않습니다.
int a; scanf("%d", &a); // a의 주소를 전달 double y; scanf("%lf", &y); // double은 %lf char name[20]; scanf("%s", name); // 배열 이름 자체가 주소라 & 불필요
| 함수 | 입력 단위 | & 사용 | 공백 처리 |
|---|---|---|---|
| scanf | 다양한 형식 | 필요(문자열 제외) | %s는 공백 전까지만 |
| getchar | 문자 한 개 | 불필요(반환값 사용) | 공백도 입력 가능 |
| gets | 문자열(한 줄) | 불필요(배열 이름=주소) | 공백 포함 입력 |
scanf("%s", name)으로 "Hello World"를 입력하면 공백 전 "Hello"만 저장됩니다. 공백 포함 입력은 gets를 씁니다.
sizeof
sizeof는 자료형·변수가 차지하는 크기를 바이트로 돌려주는 연산자입니다. 실기에서는 배열 길이 관용구가 핵심입니다.
int arr[] = {10, 20, 30, 40, 50}; int len = sizeof(arr) / sizeof(arr[0]); // 5 (원소 개수)
포인터에 sizeof를 쓰면 가리키는 대상이 아니라 주소를 담는 공간의 크기가 나옵니다. 배열을 함수 인자로 넘기면 포인터로 변환되어, 함수 안에서는 sizeof로 길이를 구할 수 없습니다.
형변환 (Casting)
(자료형)으로 명시적 변환을 합니다. double → int는 반올림이 아니라 소수점 절단(0 방향)입니다.
int n = (int)3.7; // 3 (4 아님) int m = (int)-3.7; // -3
실기 단골 함정: 정수끼리 나누면 정수 나눗셈이라 소수점이 버려집니다. 대입 좌변이 double이어도 우변이 먼저 계산됩니다.
int a = 7, b = 3; double r1 = a / b; // 2.0 (정수 나눗셈 2가 먼저 계산됨) double r2 = (double)a / b; // 2.333333 (한쪽을 double로 → 실수 나눗셈)
산술 연산자
/(나눗셈)와 %(나머지)가 핵심입니다. 정수끼리 나누면 몫만 남습니다.
int d = 5 / 3; // 1 (소수점 버림) int e = 5 % 3; // 2 (나머지)
나머지 연산 활용: 짝수/홀수 판별(n % 2), 자릿수 추출(n % 10), 순환((i+1) % 3 → 0,1,2,0,...).
증감 연산자 (전위 / 후위)
++a는 먼저 증가 후 값 사용, a++는 먼저 값 사용 후 증가입니다. 단독으로 쓰면(예: for 증감식) 차이가 없지만, 값을 대입·사용할 때 결과가 달라집니다.
int i = 5; int a = ++i; // i를 먼저 증가(6) → a = 6, i = 6 int j = 5; int b = j++; // j를 먼저 사용 → b = 5, 그 뒤 j = 6
int x = 10; printf("%d\n", ++x); // 11 (먼저 증가 후 출력) printf("%d\n", x); // 11 printf("%d\n", x++); // 11 (먼저 출력 후 증가) printf("%d\n", x); // 12
비교 연산자
C에서 참은 1(또는 0이 아닌 값), 거짓은 0입니다.
int a = 5, b = 3; printf("%d", a == b); // 0 printf("%d", a != b); // 1 printf("%d", a > b); // 1
기출 단골 패턴: 문자열 끝('\0') 찾기. while (str[a] != '\0') ++a; 실행 후 a는 문자열 길이가 됩니다.
논리 연산자와 단축 평가
&&(AND), ||(OR), !(NOT). 왼쪽만으로 결과가 확정되면 오른쪽을 평가하지 않는 단축 평가를 합니다.
printf("%d", !0); // 1 printf("%d", !5); // 0 (0이 아닌 값은 참 → 반대는 거짓) int a = 0; if (a != 0 && 10 / a > 1) { } // 왼쪽이 거짓 → 오른쪽(10/a) 평가 안 함, 에러 없음
삼항 연산자
조건식 ? 참일때값 : 거짓일때값. if-else를 한 줄로 줄입니다. 0만 거짓, 음수를 포함한 나머지는 모두 참입니다.
int score = 85; int result = (score >= 60) ? 1 : 0; // 1 int a = -1 ? 10 : 20; // 10 (-1은 참)
중첩 삼항은 오른쪽에서 왼쪽으로 결합합니다. a ? b : c ? d : e는 a ? b : (c ? d : e)로 해석됩니다.
연산자 우선순위 (C)
숫자가 작을수록 먼저 계산됩니다.
| 순위 | 연산자 | 결합 방향 |
|---|---|---|
| 1 | () [] -> . | 왼→오 |
| 2 | ! ~ ++ --(전위) *(역참조) &(주소) | 오→왼 |
| 3 | * / % | 왼→오 |
| 4 | + - | 왼→오 |
| 5 | << >> (시프트) | 왼→오 |
| 6 | < <= > >= (크기 비교) | 왼→오 |
| 7 | == != (같음 비교) | 왼→오 |
| 8 | & (비트 AND) | 왼→오 |
| 9 | ^ (비트 XOR) | 왼→오 |
| 10 | | (비트 OR) | 왼→오 |
| 11 | && (논리 AND) | 왼→오 |
| 12 | || (논리 OR) | 왼→오 |
| 13 | ?: (삼항) | 오→왼 |
| 14 | = += -= (대입) | 오→왼 |
실기 최다 함정: 비교 > 비트
비교 연산자(==, !=, <, >)가 비트 연산자(&, ^, |)보다 우선순위가 높습니다. 그리고 비트 연산끼리는 & > ^ > | 순입니다.
int a = 3 & 2 == 2; // 3 & (2 == 2) → 3 & 1 → 1 (괄호 없으면 == 먼저!) int b = (3 & 2) == 2; // 2 == 2 → 1 int z = 2 & 2 >> 1; // 2 & (2 >> 1) → 2 & 1 → 0 (시프트가 & 보다 먼저)
복합식 단계별 풀이
int x = 7, y = 4, z; z = y % 3 < 3 ? 2 : 1; // 1단계: (4%3=1)<3 → 참 → z=2 z = z & z >> 1; // 2단계: z & (z>>1) = 2 & 1 = 0 z = x > 5 && z <= 3 ? z * x : z / x; // 3단계: (1 && 1) → z*x = 0*7 = 0 printf("%d", z); // 출력: 0
풀이 순서: 괄호 → 산술(% * /) → 비교(< > ==) → 비트(& ^ |) → 논리(&& ||) → 삼항(?:) → 대입(=).
비트 연산 문제는 10진수·16진수를 2진수로 바꿔 자리별로 계산한 뒤 다시 10진수로 되돌리는 과정을 손으로 해야 합니다.
2진수 변환
2진수 1101 → 8 + 4 + 0 + 1 = 13
외워둘 값: 4비트 최댓값 1111 = 15, 8비트 최댓값 1111 1111 = 255.
16진수
0 ~ 9와 A(10) ~ F(15)를 씁니다. 코드에서는 0x 접두사를 붙입니다. 16진수 1자리 = 2진수 4비트입니다.
int a = 0xA5; // 10*16 + 5 = 165 int b = 0xFF; // 255
| 16진수 | 계산 | 10진수 |
|---|---|---|
| 0xA0 | 10*16 + 0 | 160 |
| 0xA5 | 10*16 + 5 | 165 |
| 0xDB | 13*16 + 11 | 219 |
| 0xFF | 15*16 + 15 | 255 |
ASCII 코드
| 문자 | ASCII | 비고 |
|---|---|---|
'\0' | 0 | 널 문자(문자열 끝) |
| '0' ~ '9' | 48 ~ 57 | 숫자 문자 |
| 'A' ~ 'Z' | 65 ~ 90 | 대문자 |
| 'a' ~ 'z' | 97 ~ 122 | 소문자 |
문자는 숫자이므로 연산이 가능합니다. 대소문자 차이는 32입니다.
int diff = 'E' - 'A'; // 69 - 65 = 4 char lower = 'A' + 32; // 'a' int num = '7' - '0'; // 55 - 48 = 7 (문자 숫자 → 실제 정수)
비트 연산자
| 연산자 | 이름 | 규칙 | 예시 |
|---|---|---|---|
& | AND | 둘 다 1이면 1 | 5 & 3 = 1 |
| | OR | 하나라도 1이면 1 | 5 | 3 = 7 |
^ | XOR | 서로 다르면 1 | 5 ^ 3 = 6 |
~ | NOT | 비트 반전 | ~5 = -6 (int) |
<< | 왼쪽 시프트 | 1칸당 x2 | 5 << 1 = 10 |
>> | 오른쪽 시프트 | 1칸당 /2 | 5 >> 1 = 2 |
0101 (5) 0101 (5) 0101 (5) & 0011 (3) | 0011 (3) ^ 0011 (3) ------ ------ ------ 0001 (1) 0111 (7) 0110 (6)
~5는 int(부호 있는 32비트)에서 -6이지만, unsigned char에서 ~5는 8비트가 반전되어 250(1111 1010)입니다.
16진수 + 비트 연산 종합
int result = 0xDB & 0xA5; // 129 // 1) 0xDB = 219 = 1101 1011, 0xA5 = 165 = 1010 0101 // 1101 1011 // & 1010 0101 // --------- // 1000 0001 = 128 + 1 = 129
시프트는 곱셈·나눗셈과 같습니다. a << n = a * 2^n, a >> n = a / 2^n(소수점 버림). 시프트는 비트 AND/OR보다 우선순위가 높습니다.
if / else if
여러 조건을 위에서부터 순서대로 검사하고, 처음 참인 블록만 실행합니다.
int score = 75; if (score >= 90) printf("A"); else if (score >= 80) printf("B"); else if (score >= 70) printf("C"); // 75 >= 70 참 → 실행 else printf("F"); // 출력: C
75는 >= 70도 참이지만 위의 조건을 먼저 검사해 거짓이면 건너뛰고, 처음 참인 곳에서 멈춥니다. 범위가 좁은 조건을 위에 써야 합니다.
switch-case와 fall-through
switch는 값이 일치하는 case로 진입한 뒤 break를 만날 때까지 아래 case들을 연속 실행합니다. 이것이 fall-through입니다.
int a = 2; switch (a) { case 1: printf("A"); case 2: printf("B"); // 여기 진입 case 3: printf("C"); // break 없어서 계속 default: printf("D"); } // 출력: BCD
break가 모두 있으면 출력은 B 하나뿐입니다. default는 어느 위치에 있어도 동작하며, default에도 break가 없으면 fall-through됩니다. 값이 없는 case를 연속으로 쓰면(case 10: case 9:) 여러 값을 묶을 수 있습니다.
복합 대입 + fall-through 기출 패턴
int i = 3, k = 1; switch (i) { case 0: case 1: case 2: case 3: k = 0; // 진입, k=0 (초기값 1은 대입으로 사라짐) case 4: k += 3; // k=3 case 5: k -= 10; // k=-7 default: k--; // k=-8 } // 출력: -8
case 3에서 k = 0은 대입(=)이라 초기값 1이 사라지고 0부터 시작하는 점이 함정입니다.
for / while / do-while
for (int i = 0; i < 3; i++) printf("%d ", i); // 0 1 2 int i = 0; while (i < 3) { printf("%d ", i); i++; } // 0 1 2 int j = 5; do { printf("%d ", j); j++; } while (j < 3); // 5 (조건 거짓이어도 최소 1회 실행)
for문은 초기식·조건식·증감식을 각각 생략할 수 있으나 세미콜론은 유지합니다. 기출 단골: int i = 3; for (; i < 5; i++) ..., while (str[a] != '\0') ++a;.
break / continue
for (int i = 0; i < 5; i++) { if (i == 2) continue; // 2만 건너뜀 printf("%d ", i); } // 0 1 3 4
break는 루프를 완전히 종료, continue는 현재 반복만 건너뛰고 다음 반복을 계속합니다.
중첩 반복문
바깥 루프 1회마다 안쪽 루프가 처음부터 끝까지 돕니다.
for (int i = 0; i < 2; i++) { for (int j = 0; j < 3; j++) printf("(%d,%d) ", i, j); printf("\n"); } // (0,0) (0,1) (0,2) // (1,0) (1,1) (1,2)
1차원 배열
같은 자료형을 나란히 저장합니다. 인덱스는 0부터 시작합니다.
int scores[3] = {85, 90, 78}; printf("%d", scores[0]); // 85 scores[2] += 10; // scores[2] = 88 int arr[] = {10, 20, 30, 40}; // 크기 생략 시 4로 자동 결정
크기 3인 배열의 유효 인덱스는 0, 1, 2입니다. scores[3]은 범위를 벗어나 쓰레기 값이 나오거나 비정상 종료될 수 있습니다.
배열 이름은 주소다
C에서 배열 이름은 첫 번째 요소의 주소입니다. 즉 a와 &a[0]은 같고, a[i]는 *(a + i)와 완전히 동일합니다.
int a[3] = {10, 20, 30}; int *ptr = a; // = &a[0], 그래서 & 없이 대입 *a; // = a[0] = 10
2차원 배열
행과 열로 이루어진 표이지만, 메모리에는 행 순서대로 한 줄로 연속 저장됩니다.
int arr[2][3] = {1, 2, 3, 4, 5, 6}; // 0행부터 채움 // {{1,2,3}, {4,5,6}} 과 동일 printf("%d", arr[0][0]); // 1 printf("%d", arr[1][2]); // 6
행 이름(arr[0], arr[1])은 각 행의 시작 주소입니다. 실기에서는 2차원 배열을 이중 for문(바깥=행, 안쪽=열)으로 순회하는 문제가 자주 나옵니다.
문자 배열 = 문자열
C에는 문자열 타입이 없고, char 배열이 문자열입니다. 끝에는 널 문자 '\0'(ASCII 0)이 자동으로 붙습니다.
char str[] = "HELLO"; // 실제 저장: 'H' 'E' 'L' 'L' 'O' '\0'
"HELLO"의 길이는 5이지만 배열 크기는 널 문자 포함 6입니다. printf, strlen 등은 널 문자를 만날 때까지 처리합니다.
문자열 길이 직접 세기 (기출 단골)
char str[] = "REPUBLICOFKOREA"; int a = 0; while (str[a] != '\0') ++a; // 널 문자 전까지 세기 // 루프 종료 후 a = 15 (문자열 길이)
이렇게 구한 길이로 특정 위치 문자를 묻습니다. str[a - 2]는 str[13] = 'E'입니다.
문자열 복사 (strcpy 직접 구현)
실기에서는 표준 strcpy 대신 포인터로 직접 구현한 복사 함수가 자주 나옵니다.
void fn(char *d, const char *s) {
while (*s) { // s가 가리키는 값이 '\0'이 아닌 동안
*d = *s; // 문자 복사
d++; // 대상 포인터 이동
s++; // 원본 포인터 이동
}
*d = '\0'; // 마지막에 널 문자
}
const char *s는 원본을 읽기만 하고 수정하지 않는다는 표시입니다.
포인터는 실기 배점이 가장 큰 영역입니다. 시험장에서는 요소마다 임의 주소를 1씩 증가시켜(예: 100, 101, 102) 포인터의 움직임을 손으로 추적하는 것이 정석입니다.
주소(&)와 역참조(*)
포인터는 다른 변수의 주소를 저장하는 변수입니다. &는 주소를 가져오고(참조), *는 주소가 가리키는 값에 접근합니다(역참조).
int num = 10; int *p = # // p에 num의 주소 저장 printf("%d", *p); // 10 (p가 가리키는 값) *p = 20; // p가 가리키는 곳의 값을 변경 printf("%d", num); // 20 (원본이 바뀜)
선언할 때의 *(int p)는 "포인터 선언" 표시이고, `p`처럼 쓰면 "역참조"입니다.
선언 함정: 콤마
*는 자료형이 아니라 각 변수 이름에 붙습니다.
char *p1, *p2; // 둘 다 char 포인터 char* p1, p2; // p1만 포인터, p2는 char 변수 (함정!)
기출: struct node* h = NULL, *n;은 h와 n 모두 구조체 포인터입니다.
포인터 산술
포인터에 정수를 더하면 가리키는 자료형 크기만큼 이동합니다. 시험에서는 바이트 수를 몰라도 되고, p + n이 p[n]의 주소라는 것만 알면 됩니다.
int arr[] = {10, 20, 30, 40, 50}; int *p = arr; *p; // 10 ( = p[0] = arr[0]) *(p + 1); // 20 ( = p[1]) *(p + 2); // 30
a[i]와 *(a + i)는 완전히 같습니다. arr[2] = *(arr + 2) = *(2 + arr).
괄호 위치 함정: (p + n) vs p + n
*(역참조)가 +보다 우선순위가 높습니다.
int arr[] = {10, 20, 30, 40, 50}; int *p = arr; *(p + 3); // 40 (3칸 이동한 위치의 값 = arr[3]) *p + 3; // 13 (p가 가리키는 값 10에 3을 더함)
포인터 증감: p++ vs (p)++
int arr[] = {10, 20, 30}; int *p = arr; int a = *p++; // *(p++): 값 10을 읽고 p를 arr[1]로 이동 → a=10 int b = (*p)++; // p가 가리키는 값(20)을 b에, 그 뒤 arr[1]을 21로 증가 → b=20, arr[1]=21
*p++는 포인터를 이동시키고, (*p)++는 가리키는 값을 증가시킵니다.
문자열 포인터 (char *)
문자열 리터럴 "KOREA"는 그 자체가 시작 주소입니다.
char *p = "KOREA"; // p는 'K'의 주소 printf("%s", p); // KOREA (주소부터 '\0'까지) printf("%s", p + 3); // EA (인덱스 3부터) printf("%c", *p); // K (한 문자) printf("%c", *(p + 3)); // E
%s는 주소를 받아 널 문자까지 출력하고, %c는 역참조한 문자 값 하나를 출력합니다. 문자열 리터럴에 바로 +도 가능합니다: printf("%s", "HELLO" + 1); → ELLO.
포인터 배열
포인터를 요소로 갖는 배열입니다. 2차원 배열 행 주소 저장에 자주 쓰입니다.
int arr[3][3] = {1,2,3,4,5,6,7,8,9}; int *parr[2] = {arr[1], arr[2]}; // parr[0]=arr[1], parr[1]=arr[2] parr[0][0]; // arr[1][0] = 4 parr[1][1]; // arr[2][1] = 8 **parr; // *(parr[0]) = arr[1][0] = 4
빠른 풀이: parr[i]를 저장된 행으로 치환합니다. parr[1][1] → arr[2][1] = 8.
이중 포인터
포인터의 주소를 저장하는 포인터입니다.
int num = 10; int *ptr = # // 단일 포인터 int **pptr = &ptr; // 이중 포인터 *pptr; // ptr (포인터) **pptr; // 10 (최종 값)
함수 정의 / 호출 / 프로토타입
C 프로그램은 항상 main에서 시작합니다. 컴파일러가 위에서 아래로 읽으므로, 함수 정의가 호출보다 아래에 있으면 프로토타입(본문 없이 선언 후 세미콜론)이 필요합니다.
#include <stdio.h>
int add(int a, int b); // 프로토타입 (정의가 아래에 있을 때 필요)
int main() {
printf("%d", add(3, 5)); // 8
return 0;
}
int add(int a, int b) { return a + b; }
정의가 main보다 위에 있으면 프로토타입은 필요 없습니다. #include <stdio.h>는 printf 등의 프로토타입을 가져오는 것입니다.
Call by Value (값 복사)
기본은 값을 복사해서 전달하므로 함수 안에서 매개변수를 바꿔도 원본은 그대로입니다.
void swap(int a, int b) { int t = a; a = b; b = t; }
int main() {
int a = 11, b = 19;
swap(a, b);
printf("%d %d", a, b); // 11 19 (교환 안 됨!)
}
swap의 a, b와 main의 a, b는 이름만 같은 별개의 변수입니다.
Call by Reference (주소 전달)
주소를 넘기면 원본을 직접 수정할 수 있습니다.
void swap(int *a, int *b) { int t = *a; *a = *b; *b = t; }
int main() {
int a = 11, b = 19;
swap(&a, &b);
printf("%d %d", a, b); // 19 11 (교환됨!)
}
배열 전달
배열 이름 자체가 주소이므로 & 없이 넘겨도 원본이 수정됩니다.
void change(int arr[]) { arr[0] = 100; }
int main() {
int arr[] = {1, 2, 3};
change(arr);
printf("%d", arr[0]); // 100
}
재귀 함수
자기 자신을 호출하는 함수입니다. 반드시 멈추는 기저 조건이 있어야 합니다.
int fn(int n) {
if (n <= 1) return 1; // 기저 조건
return n * fn(n - 1); // 재귀 호출
}
// fn(5) = 5*4*3*2*1 = 120
풀이법: 기저 조건에 도달할 때까지 호출을 펼친 뒤, 가장 안쪽부터 거꾸로 값을 채웁니다.
fn(1)=1 → fn(2)=2*1=2 → fn(3)=3*2=6 → fn(4)=4*6=24 → fn(5)=5*24=120
재귀 호출이 두 번 이상이면 각 호출값을 모두 계산합니다. 기저 조건의 반환값(return 1 인지 return n인지)을 꼼꼼히 확인해야 합니다.
int gamja(int n) {
if (n <= 1) return n; // n <= 1이면 n 반환 (1이 아님!)
return gamja(n - 1) + gamja(n - 3);
}
// gamja(7) = 2
정의와 멤버 접근
여러 자료형을 하나로 묶는 사용자 정의 자료형입니다. 구조체 변수는 점(.), 구조체 포인터는 화살표(->)로 멤버에 접근합니다.
struct Student {
int id;
char name[20];
float score;
};
struct Student s1 = {1001, "김철수", 85.5};
printf("%d", s1.id); // 1001
printf("%s", s1.name); // 김철수
struct Student *p = &s1;
printf("%d", p->id); // 1001
p->id는 (*p).id와 동일합니다. 중괄호 초기화 값은 멤버 선언 순서대로 들어갑니다. 문자 배열 멤버는 s1.name = "..."가 안 되고 strcpy를 써야 합니다.
구조체 배열과 포인터
struct Student students[3] = {
{1001, "김철수", 85.5},
{1002, "이영희", 90.0},
{1003, "박민수", 78.5}
};
struct Student *p = &students[1];
printf("%s", p->name); // 이영희
printf("%s", (p + 1)->name); // 박민수 (다음 구조체 요소)
printf("%d", (p - 1)->id); // 1001
바깥 중괄호를 생략한 초기화도 기출로 나옵니다. 컴파일러가 멤버 순서대로 채우므로 어디서 잘리는지 파악하는 게 핵심입니다.
-> 체인 (연결 리스트 패턴)
struct node { int n1; struct node *n2; };
struct node a = {10, 0}, b = {20, 0}, c = {30, 0};
struct node *head = &a;
a.n2 = &b; b.n2 = &c;
head->n2->n1; // (head->n2)->n1 = (&b)->n1 = b.n1 = 20
->는 왼쪽부터 결합하고, 포인터 멤버를 만날 때만 씁니다.
중첩 구조체
구조체 멤버가 또 다른 구조체이면, 포인터는 처음 한 번만 ->이고 이후는 .으로 이어갑니다.
struct Date { int year, month, day; };
struct Student { int id; struct Date birth; };
struct Student s = {1001, {2025, 3, 19}};
struct Student *p = &s;
printf("%d/%d/%d", p->birth.year, p->birth.month, p->birth.day); // 2025/3/19
p->birth는 구조체(값)를 돌려주므로 그 다음은 .year입니다. p->birth->year는 컴파일 에러입니다.
typedef
struct 키워드를 매번 안 써도 되게 별칭을 붙입니다. 기출에서 typedef struct 패턴이 매우 자주 나옵니다.
typedef struct {
int acc;
double bal;
} Acc;
Acc ba; // struct 없이 사용
자기 참조 구조체는 정의가 끝나기 전에는 별칭을 못 쓰므로 내부에서 struct 태그를 붙여야 합니다.
typedef struct Node {
int data;
struct Node *next; // 자기 참조 시 struct Node 필수
} Node;
포인터 타입 별칭 함정: typedef int* IntPtr; IntPtr p1, p2;는 둘 다 포인터지만, int *p1, p2;는 p2가 일반 int입니다.
ctype.h는 문자 판별·변환 함수를 제공합니다. #include <ctype.h>가 필요합니다. 판별 함수는 참이면 0이 아닌 값, 거짓이면 0을 반환합니다(반드시 1은 아님).
| 함수 | 설명 | 예 |
|---|---|---|
| isupper(c) | 대문자인지 | isupper('A') → 참 |
| islower(c) | 소문자인지 | islower('t') → 참 |
| isdigit(c) | 숫자 문자인지 | isdigit('8') → 참 |
| isalpha(c) | 알파벳인지 | isalpha('A') → 참 |
| toupper(c) | 소문자→대문자 | toupper('a') → 'A' |
| tolower(c) | 대문자→소문자 | tolower('A') → 'a' |
#include <ctype.h>
char c = 'A';
if (isupper(c)) printf("대문자입니다"); // 출력됨
함정: isdigit는 문자 '0'~'9'만 참입니다. isdigit(5)는 거짓이고, 문자 '5'(ASCII 53)를 넣어야 참입니다.
string.h의 문자열 함수도 함께 알아두면 좋습니다: strlen(길이), strcpy(복사). 실기에서는 이 함수들을 포인터로 직접 구현한 코드(5번 항목 참고)가 더 자주 출제됩니다.
실기는 "출력 결과 쓰기"가 핵심이므로, 아래 함정들을 특히 조심해야 합니다.
코드 읽는 순서
C는 컴파일러가 위에서 아래로 한 번 읽습니다(1-pass). 그래서 main을 맨 아래에 두고, 호출되는 함수는 그 위에 정의하거나 프로토타입을 씁니다. 코드 추적은 main부터 시작해서 함수 호출을 만나면 그 정의로 이동, 인자를 매개변수에 대입하고, 반환값을 호출 자리에 넣어 이어 읽습니다.
자주 틀리는 함정 체크리스트
int / int는 소수점 버림. 10 / 3 = 3, 7 / 2 = 3. double이 필요하면 한쪽을 (double)로 변환.a = i++(먼저 대입) vs a = ++i(먼저 증가). 단독 사용은 차이 없음.== !=)가 비트(& ^ |)보다 먼저. 3 & 2 == 2 = 3 & 1 = 1(C 기준).break 없으면 아래 case로 fall-through. default 위치는 무관.'0'(48)과 숫자 0은 다름. '\0'(0)과 문자 '0'(48)도 다름.2795.1을 %.2f로 출력하면 2795.10.*(p + n)(n칸 뒤 값) vs *p + n(값에 n 더함).return 1인지 return n인지 반환값을 정확히 확인.->, 값은 .. p->birth.year(중첩은 첫 단계만 ->).