경기과학고등학교 2026 직보 2주
변수는 값을 담는 이름 붙은 공간이다.
printf는 출력, scanf는 입력이다.
scanf에서 &를 빠뜨리면 런타임 오류가 난다. &는 변수의 메모리 주소를 넘겨서 scanf가 그 위치에 값을 쓸 수 있게 한다.
| 지정자 | 의미 | 예시 |
|---|---|---|
%d |
정수 (10진수) | printf("%d", 42); → 42 |
%f |
실수 | printf("%f", 3.14); → 3.140000 |
%c |
문자 | printf("%c", 'A'); → A |
%s |
문자열 | printf("%s", "Hi"); → Hi |
%% |
% 문자 자체 | printf("%%"); → % |
정수끼리 나누면 정수가 나온다. 10 / 3 = 3이지 3.333...이 아니다.
나머지 연산 %는 생각보다 자주 쓴다.
홀짝 판별 n % 2, 자릿수 분해 n % 10, 범위 제한 n % MAX 같은 패턴은 수시로 나온다. 음수에 %를 쓰면 결과의 부호가 구현마다 다를 수 있으니 주의한다. C99부터는 피제수(왼쪽)의 부호를 따른다: -7 % 3 = -1.
수학에서처럼 곱셈/나눗셈이 덧셈/뺄셈보다 먼저 계산된다.
| 우선순위 | 연산자 | 결합 방향 |
|---|---|---|
| 높음 | () |
왼→오 |
*, /, % |
왼→오 | |
+, - |
왼→오 | |
<, <=, >, >= |
왼→오 | |
==, != |
왼→오 | |
&& |
왼→오 | |
| 낮음 | \|\| |
왼→오 |
헷갈리면 괄호를 쓰는 게 가장 안전하다.
x++ (후위): 현재 값을 먼저 돌려주고 나중에 1 증가++x (전위): 먼저 1 증가시키고 증가된 값을 돌려줌단독으로 x++;와 ++x;를 쓸 때는 차이가 없다. 다른 식 안에서 쓸 때만 다르다.
비교 결과는 참(1) 또는 거짓(0)이다.
| 연산자 | 의미 | 예시 |
|---|---|---|
&& |
AND (둘 다 참) | (a > 0 && a < 10) |
\|\| |
OR (하나라도 참) | (a == 0 \|\| b == 0) |
! |
NOT (반전) | (!done) |
&&와 ||는 왼쪽만으로 결과가 정해지면 오른쪽을 아예 실행하지 않는다.
&&: 왼쪽이 거짓이면 오른쪽을 건너뛴다 (어차피 거짓)||: 왼쪽이 참이면 오른쪽을 건너뛴다 (어차피 참)부수 효과(side effect)가 있는 식을 &&/|| 오른쪽에 넣으면 실행 여부가 달라지므로 주의해야 한다.
단축 평가는 null 검사에서 자주 쓴다.
왼쪽 조건이 거짓이면 오른쪽을 건너뛰므로, p가 NULL일 때 *p를 읽는 사고를 막을 수 있다. Java, Python, JavaScript 등에서도 같은 방식으로 동작한다.
C는 필요하면 알아서 타입을 바꾼다.
정수끼리 나누면 정수 결과가 나온다. 7 / 2 = 3이 되고, 그 3을 double에 넣으면 3.0이다.
(타입) 캐스트로 원하는 타입으로 바꿀 수 있다.
서로 다른 타입끼리 연산하면, 더 넓은 타입 쪽으로 맞춰진다.
char → int → long → float → double
len - 1은 unsigned int에서 계산되므로 0 - 1 = 4294967295가 된다. 양수이므로 조건이 참이다. unsigned 변수를 쓸 때는 0 근처에서 빼기 연산을 하면 안 된다. 실제 코드에서 이런 버그가 꽤 많다.
같은 정수를 다른 진법으로 출력할 수 있다.
| 지정자 | 진법 | 접두사 출력 |
|---|---|---|
%d |
10진수 | 없음 |
%o |
8진수 | %#o → 0253 |
%x |
16진수 (소문자) | %#x → 0xab |
%X |
16진수 (대문자) | %#X → 0XAB |
2진수를 그대로 쓰면 너무 길다. 16진수 한 자리가 2진수 4비트에 대응하므로, 비트 패턴을 읽기 편하다.
2진수: 1100 1010 0011 1011
16진수: C A 3 B → 0xCA3B
메모리 주소, 색상 코드(#FF8800), 네트워크 패킷, 파일 포맷 헤더 등에서 16진수를 쓴다. 8진수(%o)는 Unix 파일 권한(chmod 755)에서 주로 쓰인다. 그 밖에는 거의 쓰지 않는다.
10진수 171을 8진수와 16진수로 바꾸는 과정:
8진수: 8로 반복 나누기
171 ÷ 8 = 21 나머지 3
21 ÷ 8 = 2 나머지 5
2 ÷ 8 = 0 나머지 2
→ 아래에서 위로 읽으면: 253
16진수: 16으로 반복 나누기
171 ÷ 16 = 10 나머지 11 (= B)
10 ÷ 16 = 0 나머지 10 (= A)
→ 아래에서 위로 읽으면: AB
1바이트 = 8비트
┌─┬─┬─┬─┬─┬─┬─┬─┐
│0│1│0│0│1│0│1│0│ = 74 (10진수)
└─┴─┴─┴─┴─┴─┴─┴─┘
7 6 5 4 3 2 1 0 ← 비트 번호
컴퓨터가 음수를 나타내는 방식이다.
양수 → 음수 바꾸기 (3단계)
예: 8비트에서 -42 구하기
42 = 00101010 ← 1단계
11010101 ← 2단계 (비트 반전)
+ 00000001 ← 3단계 (+1)
──────────
11010110 ← -42의 2의 보수 = 0xD6
1의 보수(비트 반전만)로 음수를 나타내면 0이 두 개 생긴다: +0 = 00000000, -0 = 11111111. 덧셈할 때도 올림(carry)을 다시 더해줘야 해서 회로가 복잡해진다.
2의 보수는 0이 하나뿐이고, 같은 덧셈 회로로 양수/음수를 다 처리할 수 있어서 요즘 CPU는 거의 다 이 방식을 쓴다.
덧셈 회로 하나로 뺄셈까지 처리할 수 있다.
42 + (-42) 를 2의 보수로 계산:
00101010 (42)
+ 11010110 (-42)
──────────
1 00000000 (8비트 범위를 넘으면 버림 → 0)
음수를 다시 양수로 바꿀 때도 같은 절차를 쓴다.
-42 = 11010110
00101001 (반전)
+ 00000001 (+1)
──────────
00101010 = 42
MSB(최상위 비트)가 1이면 음수이다.
예: 0xE7의 10진수 값
0xE7 = 11100111
MSB = 1 → 음수
11100111 (원본)
00011000 (반전)
+ 00000001 (+1)
──────────
00011001 = 25 → 원래 값은 -25
| 타입 | 크기 | signed 범위 | unsigned 범위 |
|---|---|---|---|
char |
1바이트 (8비트) | -128 ~ 127 | 0 ~ 255 |
short |
2바이트 (16비트) | -32768 ~ 32767 | 0 ~ 65535 |
int |
4바이트 (32비트) | -2^31 ~ 2^31-1 | 0 ~ 2^32-1 |
long long |
8바이트 (64비트) | -2^63 ~ 2^63-1 | 0 ~ 2^64-1 |
표현 범위를 넘으면 값이 돌아간다.
signed char 127 + 1:
01111111 + 1 = 10000000 = -128 (2의 보수)
unsigned char 0 - 1:
00000000 - 1 = 11111111 = 255
int 최댓값(2,147,483,647)을 넘어서 Google이 조회수 카운터를 long long으로 바꿨다.unsigned char(8비트)로 저장되어 레벨 256에서 overflow가 나고 화면이 깨진다.출력: b가 크다
unsigned와 signed를 비교하면, signed 쪽이 unsigned로 바뀐다. -1의 비트 패턴 0xFFFFFFFF를 unsigned로 읽으면 4294967295다. 1 > 4294967295는 거짓이므로 “b가 크다”가 나온다.
| 연산자 | 이름 | 동작 |
|---|---|---|
& |
AND | 둘 다 1이면 1 |
\| |
OR | 하나라도 1이면 1 |
^ |
XOR | 서로 다르면 1 |
~ |
NOT | 비트 반전 |
| 연산자 | 의미 | 효과 |
|---|---|---|
<< |
왼쪽 시프트 | ×2 |
>> |
오른쪽 시프트 | ÷2 |
특정 비트를 꺼내거나 설정할 때 비트 연산을 쓴다.
| 동작 | 코드 |
|---|---|
| k번째 비트 읽기 | (n >> k) & 1 |
| k번째 비트 켜기 | n \| (1 << k) |
| k번째 비트 끄기 | n & ~(1 << k) |
| k번째 비트 뒤집기 | n ^ (1 << k) |
XOR swap: 임시 변수 없이 두 변수를 교환할 수 있다.
XOR은 재미있는 성질이 있다.
a ^ a = 0 (같은 값끼리 XOR하면 0)a ^ 0 = a (0과 XOR하면 원래 값)위에서 아래로 조건을 보고, 처음 참인 블록만 실행한다.
x = 75는 x >= 70에 걸리므로 "C"가 나온다. 그 아래는 검사하지 않는다.
정수 값에 따라 분기할 때 쓴다. break가 없으면 아래로 떨어진다(fall-through).
case 2에서 시작해서 break를 만날 때까지 계속 실행된다.
여러 값을 같은 동작으로 묶을 때 쓸 수 있다.
else는 들여쓰기와 관계없이 가장 가까운 if와 짝이 된다.
실행 순서: 초기화 → 조건 → 본문 → 증감 → 조건 → …
출력:
*
***
*****
*******
바깥 반복이 행, 안쪽 반복이 열을 담당한다.
continue: 남은 본문을 건너뛰고 다음 반복으로break: 반복문을 바로 빠져나옴중첩 반복에서 break는 가장 안쪽 반복문만 빠져나온다.
| 반복 | n (나누기 전) | n /= 10 | count |
|---|---|---|---|
| 1 | 30562 | 3056 | 1 |
| 2 | 3056 | 305 | 2 |
| 3 | 305 | 30 | 3 |
| 4 | 30 | 3 | 4 |
| 5 | 3 | 0 | 5 |
goto는 지정한 레이블로 무조건 점프한다.
goto loop은 loop: 레이블로 되돌아가고, goto done은 done: 레이블로 건너뛴다. 반복문이 없던 시절에는 이렇게 반복을 만들었다.
위 코드는 for문으로 더 깔끔하게 쓸 수 있다.
goto는 코드 흐름을 따라가기 어렵게 만든다. 대부분 for, while, break, continue로 대체할 수 있고, 그렇게 쓰는 게 좋다.
1968년 Dijkstra가 “Go To Statement Considered Harmful”이라는 글을 쓴 뒤로 goto를 피하는 게 업계 관행이 되었다.
중첩 반복을 한 번에 빠져나올 때 goto가 유일하게 쓸 만하다.
break는 안쪽 반복만 빠져나오므로, 중첩을 한 번에 빠져나오려면 플래그 변수를 쓰거나 goto를 쓴다.
Linux 커널 소스에서는 에러 처리 후 자원 해제(cleanup)에 goto를 쓴다. goto cleanup; 같은 형태로, 함수 끝의 정리 코드로 점프한다.
char는 1바이트 정수이다. ASCII 코드표에서 문자와 숫자가 대응된다.
| 범위 | 문자 | ASCII |
|---|---|---|
'0' ~ '9' |
숫자 | 48 ~ 57 |
'A' ~ 'Z' |
대문자 | 65 ~ 90 |
'a' ~ 'z' |
소문자 | 97 ~ 122 |
대문자와 소문자의 차이는 항상 32이다. 'a' - 'A' = 32
C에서 문자열은 char 배열이다. 끝에 널 문자('\0')가 붙는다.
널 문자 '\0'이 문자열의 끝을 알려준다. C의 문자열 함수는 전부 이 약속을 전제로 동작한다.
대문자를 세는 코드:
C 문자열은 길이 정보가 없고 널 문자에만 의존한다. 입력 크기를 검사하지 않으면 버퍼 오버플로우가 생길 수 있다.
ASCII 코드표는 우연이 아니라 의도적으로 설계되었다.
'0'~'9'의 하위 4비트가 0~9인 이유: c - '0'이 아니라 c & 0x0F로도 숫자 값을 얻을 수 있다.<)로 할 수 있다.같은 타입의 값 여러 개를 연속된 메모리에 저장한다.
배열은 메모리에 연속으로 배치된다.
arr[0] arr[1] arr[2] arr[3] arr[4]
┌──────┬──────┬──────┬──────┬──────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
└──────┴──────┴──────┴──────┴──────┘
0x1000 0x1004 0x1008 0x100C 0x1010
int가 4바이트이므로 주소가 4씩 증가한다.
배열 이름 arr는 첫 번째 원소의 주소(&arr[0])와 같다. 이 성질이 나중에 포인터를 배울 때 중요해진다.
C는 배열 인덱스 범위를 검사하지 않는다.
범위 밖에 쓰면 다른 변수나 리턴 주소를 덮어쓸 수 있다. 프로그램이 이상하게 동작하거나, 보안 취약점이 생긴다.
Java나 Python은 범위를 벗어나면 바로 오류를 내지만, C는 아무런 경고 없이 실행한다. 직접 주의해야 한다.
max를 0으로 초기화하면 배열에 음수만 있을 때 틀린다. 항상 arr[0]이나 INT_MIN으로 초기화해야 한다.
메모리에는 행 우선(row-major)으로 일렬로 저장된다.
m[0][0] m[0][1] m[0][2] m[1][0] m[1][1] m[1][2]
┌──────┬──────┬──────┬──────┬──────┬──────┐
│ 1 │ 2 │ 3 │ 4 │ 5 │ 6 │
└──────┴──────┴──────┴──────┴──────┴──────┘
2중 반복문으로 전체를 순회한다.
int add(int a, int b): int 두 개를 받아서 int를 돌려준다return: 값을 돌려주고 함수를 빠져나온다void를 쓴다C는 함수에 인자를 넘길 때 값을 복사한다.
a의 값이 x로 복사되므로, 함수 안에서 x를 바꿔도 원본 a는 그대로다.
원본을 바꾸려면 포인터를 넘겨야 한다 (다음 시간에 배운다).
함수를 호출하기 전에 컴파일러가 그 함수의 존재를 알아야 한다.
함수가 자기 자신을 호출하는 것이다.
factorial(5) = 5 * factorial(4)
= 5 * 4 * factorial(3)
= 5 * 4 * 3 * factorial(2)
= 5 * 4 * 3 * 2 * factorial(1)
= 5 * 4 * 3 * 2 * 1
= 120
기저 조건(base case)이 없으면 무한히 호출되다가 스택 오버플로우로 죽는다.
같은 문제를 재귀와 반복 두 가지로 풀 수 있는 경우가 많다.
재귀는 코드가 깔끔하지만, 호출마다 스택 프레임이 쌓여서 메모리를 더 쓴다. factorial(100000) 같은 큰 입력에서는 스택이 넘칠 수 있다.
트리 구조나 분할 정복 같은 문제는 재귀가 자연스럽고, 단순 반복은 for/while이 낫다.
변수를 만들면 메모리 어딘가에 공간이 잡힌다.
&는 변수의 메모리 주소를 구하는 연산자이다. scanf에서 &를 쓰는 이유가 바로 이것이다.
포인터는 주소를 값으로 저장하는 변수이다.
int *p: “int를 가리키는 포인터” 선언&x: x의 주소*p: p가 가리키는 곳의 값 (역참조)*는 선언할 때와 쓸 때 의미가 다르다. 선언에서는 “포인터 타입”, 식에서는 “역참조”이다.
주소를 넘기면 함수 안에서 *a, *b로 원본에 직접 접근할 수 있다. 앞에서 try_change로 안 되던 것이 포인터로 가능한 이유가 이것이다.
배열 이름은 첫 번째 원소의 주소와 같다.
포인터에 정수를 더하면 sizeof(타입) 단위로 이동한다.
p + 2는 주소가 p + 2 * sizeof(int) = p + 8바이트만큼 이동한다. 배열의 세 번째 원소를 가리키게 된다.
아무것도 가리키지 않는 포인터는 NULL로 설정한다.
NULL 포인터를 역참조하면 segmentation fault로 프로그램이 비정상 종료한다. 포인터를 쓰기 전에 반드시 NULL 여부를 검사해야 한다.
초기화하지 않은 포인터는 쓰레기 주소를 갖고 있어서 더 위험하다. 사용하지 않는 포인터는 항상 NULL로 둔다.
포인터는 C를 배울 때 많이 막히는 부분이다. 혼동하기 쉬운 선언들:
선언을 읽을 때는 변수 이름에서 시작해서 오른쪽 → 왼쪽 순서로 읽는다 (시계 방향 규칙, clockwise/spiral rule).
이중 포인터, 함수 포인터 등은 나중에 더 다룬다. 지금은 “포인터 = 주소를 담는 변수”만 확실히 이해하면 된다.
컴파일 전에 소스 코드를 텍스트 수준에서 바꾸는 단계이다.
#include <...>: 시스템 경로에서 찾는다#include "...": 현재 디렉토리에서 먼저 찾고, 없으면 시스템 경로#define: 이름을 값으로 치환한다. 타입 검사가 없다.#define PI 3.14159이면, 코드에서 PI가 나올 때마다 컴파일러가 보기 전에 3.14159로 바뀐다.
#define으로 함수처럼 쓸 수 있다. 하지만 함정이 많다.
#ifdef / #ifndef는 헤더 파일의 중복 포함을 막는 데 주로 쓰인다. 이 패턴을 include guard라고 한다.
C 전처리기는 쓸 수 있는 게 많지만, 남용하면 읽을 수 없는 코드가 된다.
이건 C를 Pascal처럼 보이게 만들려는 시도인데, 디버깅하기가 매우 어렵다. 전처리기 매크로는 꼭 필요한 곳에만 쓰고, const 변수나 inline 함수로 대체하는 편이 낫다.
매년 열리는 IOCCC(International Obfuscated C Code Contest)에서는 전처리기를 한계까지 활용한 코드를 볼 수 있다.
관련 있는 변수 여러 개를 하나로 묶는다.
struct를 매번 쓰기 번거로우면 typedef로 별명을 붙인다.
구조체의 크기는 멤버 크기의 합보다 클 수 있다.
지금까지 배운 변수는 크기가 컴파일 때 정해진다. 실행 중에 크기를 정하려면 malloc을 쓴다.
malloc(크기): 바이트 단위로 메모리를 요청한다. 성공하면 주소, 실패하면 NULL을 돌려준다.free(포인터): malloc으로 받은 메모리를 반납한다.free를 빠뜨리면 메모리 누수(memory leak)가 생긴다.free를 빠뜨리면 프로그램이 메모리를 계속 먹는다. 짧은 프로그램에서는 티가 안 나지만, 서버처럼 오래 도는 프로그램에서는 큰 문제가 된다.
C는 1972년 Bell Labs에서 Dennis Ritchie가 만들었다. Ken Thompson이 만든 B 언어를 개선한 것이라서 C라는 이름이 붙었다.
원래 목적은 Unix 운영체제를 다시 작성하기 위해서였다. 그 전에 Unix는 어셈블리로 쓰여 있어서, 다른 기종으로 옮기려면 처음부터 새로 짜야 했다. C로 다시 쓰고 나서는 컴파일러만 만들면 다른 기종에서도 돌릴 수 있게 되었다.
1978년 Ritchie와 Kernighan이 쓴 “The C Programming Language” (K&R)는 C 프로그래밍의 바이블로 통한다. 이 책에 처음 나온 “Hello, World” 예제가 이후 대부분의 프로그래밍 교재에서 첫 예제로 쓰이고 있다.
배열 인덱스가 0부터 시작하는 건 C의 포인터 산술 때문이다.
arr[i]는 *(arr + i)와 같다. arr이 배열의 시작 주소일 때, 첫 번째 원소는 시작 주소에서 0칸 떨어져 있으므로 arr[0]이 된다.
Dijkstra도 1982년에 “Why numbering should start at zero”라는 글에서 0부터 세는 게 수학적으로 더 깔끔하다고 주장했다. 범위를 [0, n)으로 쓰면 원소 개수가 n - 0 = n으로 바로 나온다.
Pascal이나 Lua처럼 1부터 시작하는 언어도 있지만, C 계열 언어(C++, Java, Python, JavaScript)는 전부 0부터 시작한다.
2014년에 발견된 OpenSSL의 버그로, 인터넷 서버의 약 17%가 영향을 받았다.
TLS heartbeat 메시지를 처리할 때, 클라이언트가 보낸 길이 값을 검증하지 않고 memcpy로 그만큼 복사해 돌려보냈다. 공격자가 길이를 크게 적으면 서버 메모리에 남아 있는 비밀 키, 비밀번호 등이 새어나왔다.
연산
&, |, ^, ~, <<, >>)정수 표현
제어 흐름
입출력과 문자열
& 연산자자료구조와 함수
그 외