들어가며
지난주에는 값을 담는 법을 배웠습니다. int 상자에 정수를, double 상자에 실수를 넣고, printf 로 꺼내 보고, scanf 로 키보드에서 받아 넣었습니다. 그런데 지금까지 만든 프로그램은 모두 한 가지 공통점이 있습니다. 위에서 아래로 한 번 쭉 실행되고 끝난다는 것입니다. 입력이 무엇이든 같은 줄을 같은 순서로 실행합니다.
실제 프로그램은 그렇지 않습니다.
- 계산기는 입력한 두 수를 더하고, 곱하고, 나눕니다. → 연산자
- 게임은 체력이 0 이하가 되면 게임 오버 화면을 띄웁니다. → 조건문
- 음악 앱은 재생 목록의 곡을 끝날 때까지 하나씩 재생합니다. → 반복문
이번 주에 배우는 것이 바로 이 세 가지입니다. 연산자로 값을 계산하고, 조건문으로 갈림길을 만들고, 반복문으로 같은 일을 되풀이시킵니다. 이 셋이 갖춰지면 드디어 “입력에 따라 다르게 동작하는” 진짜 프로그램을 만들 수 있습니다. 이번 주 마지막에는 구구단 출력기, 소수 판별기, 별 찍기, 계산기와 숫자 맞추기 게임이 들어 있는 메뉴 프로그램을 만듭니다.
양이 많습니다. 연산자만 해도 40개 가까이 되고, 제어문도 여섯 가지입니다. 하지만 1주차에 그랬듯이 외우려 하지 말고 직접 쳐 보세요. 이 글의 모든 출력과 오류 메시지는 실제로 컴파일하고 실행해서 얻은 것입니다. 여러분의 터미널에서도 똑같이 나와야 합니다. 다르게 나오면 그것이 바로 배울 거리입니다.
이번 주차에서 배울 내용
| 순서 | 주제 | 한 줄 요약 |
|---|---|---|
| 1~3 | 산술, 증감, 대입 연산자 | 계산하고, 1씩 늘리고, 결과를 저장한다 |
| 4~5 | 관계, 논리 연산자 | 비교하고, 비교 결과를 “그리고/또는/아니다”로 묶는다 |
| 6 | 비트 연산자 | 수를 0과 1의 줄로 보고 한 자리씩 다룬다 |
| 7~8 | 3항 연산자, 우선순위 | 한 줄짜리 조건, 그리고 무엇부터 계산되는지 |
| 9~10 | if, switch |
조건에 따라 다른 길로 간다 |
| 11~13 | for, while, do-while |
같은 일을 되풀이한다 |
| 14~15 | break, continue, 무한 루프 |
반복을 중간에 끊거나 건너뛴다 |
| 16 | 실습 프로젝트 | 구구단, 소수, 별 찍기, 메뉴 프로그램 |
준비: 이번 주 작업 폴더
1주차에서 만든 폴더 옆에 이번 주 폴더를 만듭니다.
$ mkdir -p ~/c_programming/week03
$ cd ~/c_programming/week03
$ code .
예제는 모두 1주차에서 정한 기본 명령으로 컴파일합니다.
$ gcc -Wall -Wextra -std=c11 -g 파일이름.c -o 파일이름 && ./파일이름
이번 주에는 이 명령의 -Wall -Wextra 가 특히 큰일을 합니다. 연산자와 제어문에는 “문법은 맞는데 거의 확실히 실수인” 함정이 많고, GCC는 그중 상당수를 경고로 잡아 줍니다. 이 글에서 그런 경고를 스무 개 가까이 직접 만나 볼 겁니다.
먼저 용어 몇 개: 연산자, 피연산자, 식, 문
책마다 자주 나오는 단어라 짚고 가겠습니다.
sum = a + b;
- 연산자(operator): 무엇을 할지 나타내는 기호. 여기서는
+와=입니다. - 피연산자(operand): 연산자가 다루는 값.
+의 피연산자는a와b입니다. 피연산자가 둘인 연산자를 이항(binary) 연산자, 하나인 연산자(-x의-,!x의!)를 단항(unary) 연산자 라고 합니다. - 식(expression): 계산하면 값이 나오는 조각.
a + b도 식이고,a하나도 식이고,3도 식입니다. 놀랍게도sum = a + b전체도 값이 나오는 식입니다(3절에서 확인합니다). - 문(statement): 실행의 단위. 식 뒤에
;를 붙이면 문이 됩니다. 1주차에서 “세미콜론은 문장의 끝”이라고 했던 그 문장입니다.if,for같은 제어문도 문입니다.
“식을 평가(evaluate) 한다”는 말도 자주 나옵니다. 식을 계산해서 값을 얻는다는 뜻입니다. 이 네 단어만 알면 이 글을 읽는 데 막힘이 없습니다.
1. 산술 연산자
다섯 가지 기본 연산
C는 수학의 사칙연산에 나머지를 더한 다섯 연산을 기본으로 제공합니다.
| 연산자 | 이름 | 예 | 결과 | 주의할 점 |
|---|---|---|---|---|
+ |
덧셈 | 17 + 5 |
22 |
|
- |
뺄셈 | 17 - 5 |
12 |
앞에 하나만 붙이면 부호(-x) |
* |
곱셈 | 17 * 5 |
85 |
수학의 × 대신 *. 키보드에 × 가 없어서입니다 |
/ |
나눗셈 | 17 / 5 |
3 |
정수끼리면 몫만 남습니다 |
% |
나머지 | 17 % 5 |
2 |
정수에만 쓸 수 있습니다 |
17 / 5 가 3.4가 아니라 3이라는 점, % 라는 낯선 연산자가 있다는 점. 이 두 가지가 이 절의 핵심입니다.
예제: arithmetic.c
/*
* arithmetic.c - 기본 산술 연산자 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
int a = 17;
int b = 5;
printf("a = %d, b = %d\n", a, b);
printf("==================\n");
// 기본 산술 연산
printf("a + b = %d\n", a + b); // 덧셈: 22
printf("a - b = %d\n", a - b); // 뺄셈: 12
printf("a * b = %d\n", a * b); // 곱셈: 85
printf("a / b = %d\n", a / b); // 나눗셈: 3 (정수 나눗셈!)
printf("a %% b = %d\n", a % b); // 나머지: 2 (%%로 출력)
printf("\n");
// 정수 나눗셈 vs 실수 나눗셈
printf("=== 정수 나눗셈 vs 실수 나눗셈 ===\n");
printf("17 / 5 = %d (정수)\n", 17 / 5);
printf("17.0 / 5.0 = %f (실수)\n", 17.0 / 5.0);
printf("17 / 5.0 = %f (혼합)\n", 17 / 5.0);
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g arithmetic.c -o arithmetic && ./arithmetic
a = 17, b = 5
==================
a + b = 22
a - b = 12
a * b = 85
a / b = 3
a % b = 2
=== 정수 나눗셈 vs 실수 나눗셈 ===
17 / 5 = 3 (정수)
17.0 / 5.0 = 3.400000 (실수)
17 / 5.0 = 3.400000 (혼합)
한 줄씩 봅시다.
- 12번째 줄
printf("a = %d, b = %d\n", a, b);:%d가 두 개, 뒤의 값도 두 개입니다. 2주차에서 배운 대로 왼쪽%d부터 순서대로a,b가 끼워집니다. - 16~18번째 줄:
printf의 인자 자리에a + b같은 식을 바로 썼습니다.printf가 불리기 전에 식이 먼저 계산되고, 그 결과(22)가%d자리에 들어갑니다. 결과를 변수에 담지 않아도 됩니다. - 19번째 줄
a / b: 3.4가 아니라 3 이 나왔습니다. 아래에서 자세히 봅니다. - 20번째 줄
printf("a %% b = %d\n", a % b);: 1주차 6절에서 본%%입니다. 서식 문자열 안의%는 서식 지정자의 시작이라서, 글자%를 찍으려면 두 번 씁니다. 하지만 따옴표 밖의a % b는 나머지 연산자라서 한 번만 씁니다. 같은%가 자리에 따라 다른 뜻이라는 점을 구분하세요. - 27~28번째 줄:
%f는 실수(double)를 찍는 서식 지정자입니다. 소수점 아래 6자리까지 찍는 것이 기본값이라3.400000이 나옵니다.
정수 나눗셈: 17 / 5 는 왜 3일까?
C에서 정수와 정수를 나누면 결과도 정수입니다. 3.4를 담을 자리가 없으니 소수점 아래를 버립니다. 초등학교에서 배운 “17 ÷ 5 = 3 나머지 2″의 몫이 /, 나머지가 % 라고 생각하면 정확합니다.
17 ÷ 5 = 3 ... 2
│ └── 17 % 5
└──────── 17 / 5
그래서 두 연산 사이에는 언제나 이 관계가 성립합니다.
(a / b) * b + (a % b) == a 예: (17 / 5) * 5 + (17 % 5) = 3 * 5 + 2 = 17
정수 나눗셈은 초보자 버그 1순위입니다. 평균을 구하려고 int 합계를 int 개수로 나누면 소수점이 날아갑니다.
int sum = 7, count = 2;
double avg = sum / count; // 3.5 가 아니라 3.0 !
avg 를 double 로 선언했는데도 3.0입니다. 오른쪽 sum / count 가 먼저 정수끼리 계산되어 3이 되고, 그 3이 double 로 바뀌어 담기기 때문입니다. 받는 쪽 상자가 크다고 이미 잘려 나간 소수점이 돌아오지는 않습니다.
해결책은 나누기 전에 한쪽을 실수로 만드는 것입니다. 2주차 9절의 형 변환(cast)이 여기서 쓰입니다.
double avg = (double)sum / count; // 3.5
double avg = sum / 2.0; // 3.5 (상수에 .0 을 붙여 double 로)
예제 28번째 줄의 17 / 5.0 이 3.4가 된 것도 같은 원리입니다. 피연산자 중 하나라도 double 이면 나머지도 double 로 바뀐 뒤 계산됩니다. 이것을 통상 산술 변환(usual arithmetic conversion) 이라고 합니다.
실험: 음수를 나누면?
“소수점 아래를 버린다”고 했는데, 음수라면 어느 쪽으로 버릴까요? -3.5는 -3이 될까요, -4가 될까요? 직접 확인해 봅시다. div.c:
#include <stdio.h>
int main(void) {
printf(" 7 / 2 = %d, 7 %% 2 = %d\n", 7 / 2, 7 % 2);
printf("-7 / 2 = %d, -7 %% 2 = %d\n", -7 / 2, -7 % 2);
printf(" 7 / -2 = %d, 7 %% -2 = %d\n", 7 / -2, 7 % -2);
printf("-7 >> 1 = %d\n", -7 >> 1);
printf("5.0 / 0 = %f\n", 5.0 / 0);
return 0;
}
$ gcc -Wall -Wextra -std=c11 div.c -o div
div.c: In function ‘main’:
div.c:8:34: warning: division by zero [-Wdiv-by-zero]
8 | printf("5.0 / 0 = %f\n", 5.0 / 0);
| ^
$ ./div
7 / 2 = 3, 7 % 2 = 1
-7 / 2 = -3, -7 % 2 = -1
7 / -2 = -3, 7 % -2 = 1
-7 >> 1 = -4
5.0 / 0 = inf
결과를 표로 정리하면 이렇습니다.
| 식 | 수학 값 | C의 결과 | 규칙 |
|---|---|---|---|
7 / 2 |
3.5 | 3 | |
-7 / 2 |
-3.5 | -3 | 0 쪽으로 버림 (-4가 아님) |
-7 % 2 |
-1 | 나머지의 부호는 나누어지는 수(왼쪽)를 따름 | |
7 % -2 |
1 | 왼쪽이 양수이므로 양수 |
C99 표준부터 정수 나눗셈은 0 쪽으로 버림(truncation toward zero)으로 정해졌습니다. 그래서 -3.5는 -3이 됩니다. 나머지는 앞의 관계식 (a / b) * b + (a % b) == a 를 맞추도록 정해지므로, -7 % 2 는 (-3) * 2 + (-1) = -7 에서 -1이 됩니다. 음수의 홀짝을 x % 2 == 1 로 판단하면 음수 홀수를 놓친다(-7 % 2 는 1이 아니라 -1)는 뜻이니 기억해 두세요. 홀수 판정은 x % 2 != 0 이 안전합니다.
-7 >> 1 줄은 6절 비트 연산에서 다시 봅니다.
실험: 0으로 나누면?
마지막 줄 5.0 / 0 은 GCC가 “0으로 나눈다”고 경고했지만, 실행하면 inf(무한대, infinity)가 나왔습니다. 실수 나눗셈은 0으로 나누면 무한대라는 특별한 값이 됩니다. 그럼 정수를 0으로 나누면 어떨까요? zero.c:
#include <stdio.h>
int main(void) {
int a = 10;
int b = 0;
printf("나누기 전\n");
printf("%d\n", a / b);
printf("나눈 뒤\n");
return 0;
}
$ gcc -Wall -Wextra -std=c11 zero.c -o zero
$ ./zero
나누기 전
부동 소수점 예외 (코어 덤프됨)
$ echo $?
136
이번에는 경고도 없이 컴파일됐는데, 실행하자 프로그램이 죽었습니다. “나눈 뒤”는 끝내 찍히지 않았습니다. 몇 가지를 짚어 봅시다.
- 왜 경고가 없었을까? 앞의
div.c는5.0 / 0처럼 0을 코드에 직접 썼기 때문에 컴파일러가 알아챘습니다.zero.c는 0이 변수b에 들어 있어서, 컴파일러는 실행해 보기 전에는 모른다고 보고 넘어갑니다. 컴파일러의 경고는 모든 실수를 잡아 주지 않습니다. - “부동 소수점 예외”? 정수 나눗셈인데 이름이 이상하죠. 영어로는
Floating point exception입니다. 역사적인 이유로 리눅스는 정수를 0으로 나누는 사고도 이 이름(SIGFPE 신호)으로 부릅니다. 실수와는 상관없습니다. - 종료 코드 136: 1주차에서
return 0이 종료 코드가 된다고 했습니다. 프로그램이return에 도달하지 못하고 운영체제에 의해 강제로 끝나면, 쉘은 128 + 신호 번호를 종료 코드로 보여 줍니다. SIGFPE 는 8번이라 136입니다. 15절에서Ctrl + C로 멈추면 130(128 + 2)이 나오는 것도 보게 됩니다.
이상한 일: 출력이 사라질 때가 있습니다. 이 프로그램을
./zero | cat처럼 출력을 다른 프로그램으로 넘겨서(파이프) 실행하면, “나누기 전”조차 보이지 않습니다. 화면으로 출력할 때printf는 줄바꿈마다 바로 내보내지만, 파이프나 파일로 출력할 때는 효율을 위해 버퍼에 모았다가 한꺼번에 내보냅니다. 버퍼를 비우기 전에 프로그램이 죽으면 모아 둔 글자가 함께 사라집니다. 나중에 “분명printf를 넣었는데 안 찍혔으니 그 줄까지 가지도 못했다”고 착각하기 쉬운 함정이라 알아 두세요. 9주차 파일 입출력에서 이 버퍼를 직접 다룹니다.
그래서 정수 나눗셈을 할 때는 나누는 수가 0이 아닌지 먼저 확인해야 합니다. 5절의 단락 평가와 9절의 if 에서 그 방법을 봅니다.
실험: int 의 끝을 넘으면?
2주차에서 int 는 4바이트이고 INT_MAX(2,147,483,647)까지 담을 수 있다고 배웠습니다. 거기에 1을 더하면? ovf.c:
#include <stdio.h>
#include <limits.h>
int main(void) {
int big = INT_MAX;
printf("INT_MAX = %d\n", big);
big = big + 1;
printf("INT_MAX + 1 = %d\n", big);
return 0;
}
$ gcc -Wall -Wextra -std=c11 ovf.c -o ovf && ./ovf
INT_MAX = 2147483647
INT_MAX + 1 = -2147483648
약 21억에 1을 더했더니 약 -21억이 됐습니다. 자동차 주행 거리계가 999999 다음에 000000으로 돌아가는 것과 비슷하게, 값이 반대쪽 끝으로 넘어간 것입니다. 이것을 오버플로(overflow) 라고 합니다.
그런데 이 결과를 믿으면 안 됩니다. C 표준은 부호 있는 정수의 오버플로를 “정의되지 않은 동작(undefined behavior)” 으로 정해 두었습니다. “무슨 일이 일어나도 컴파일러 책임이 아니다”라는 뜻입니다. 이번에는 우연히 -2147483648이 나왔지만, 최적화 옵션을 바꾸면 전혀 다른 결과가 나올 수도 있습니다. GCC에게 이런 사고를 실행 중에 잡아 달라고 할 수 있습니다.
$ gcc -fsanitize=undefined ovf.c -o ovf_s && ./ovf_s
INT_MAX = 2147483647
ovf.c:7:9: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
INT_MAX + 1 = -2147483648
-fsanitize=undefined 는 “정의되지 않은 동작이 일어나면 실행 중에 알려 달라”는 옵션입니다. 7번째 줄 9번째 칸에서 “2147483647 + 1 은 int 로 나타낼 수 없다”고 정확히 짚어 줍니다. 7주차에 동적 메모리를 배우면 메모리를 잘못 쓰는 곳을 이런 식으로 잡아 주는 도구(Valgrind, -fsanitize=address)를 자주 쓰게 됩니다.
나머지 연산자 % 는 어디에 쓸까?
“나머지”라고 하면 쓸모가 없어 보이지만, 사실 프로그래밍에서 가장 많이 쓰는 연산자 중 하나입니다. 네 가지 쓰임을 한 예제에서 봅시다. 아직 배우지 않은 for, if, while 이 나오는데, 11절과 9절에서 자세히 배우니 지금은 “반복한다”, “조건에 맞으면 실행한다” 정도로만 읽어 두세요.
/*
* modulo_uses.c - 나머지 연산자 활용 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
// 1. 짝수/홀수 판별
printf("=== 짝수/홀수 판별 ===\n");
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) {
printf("%d는 짝수\n", i);
} else {
printf("%d는 홀수\n", i);
}
}
printf("\n");
// 2. 배수 확인
printf("=== 배수 확인 ===\n");
int num = 15;
if (num % 3 == 0) {
printf("%d는 3의 배수입니다\n", num);
}
if (num % 5 == 0) {
printf("%d는 5의 배수입니다\n", num);
}
printf("\n");
// 3. 순환 (0, 1, 2, 0, 1, 2, ...)
printf("=== 순환 예시 ===\n");
printf("i %% 3 결과: ");
for (int i = 0; i < 10; i++) {
printf("%d ", i % 3);
}
printf("\n");
// 4. 자릿수 분리
printf("\n=== 자릿수 분리 ===\n");
int number = 1234;
int original = number;
printf("%d의 각 자릿수: ", original);
while (number > 0) {
printf("%d ", number % 10); // 마지막 자릿수
number = number / 10; // 마지막 자릿수 제거
}
printf("(역순)\n");
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g modulo_uses.c -o modulo_uses && ./modulo_uses
=== 짝수/홀수 판별 ===
1는 홀수
2는 짝수
3는 홀수
4는 짝수
5는 홀수
6는 짝수
7는 홀수
8는 짝수
9는 홀수
10는 짝수
=== 배수 확인 ===
15는 3의 배수입니다
15는 5의 배수입니다
=== 순환 예시 ===
i % 3 결과: 0 1 2 0 1 2 0 1 2 0
=== 자릿수 분리 ===
1234의 각 자릿수: 4 3 2 1 (역순)
| 쓰임 | 식 | 원리 |
|---|---|---|
| 짝수·홀수 | i % 2 == 0 |
2로 나눈 나머지가 0이면 짝수 |
| 배수 확인 | num % 3 == 0 |
3으로 나누어떨어지면 3의 배수 |
| 순환 | i % 3 |
0, 1, 2, 0, 1, 2 … 로 돌아옴. 요일 계산, 시계, 번갈아 색칠하기 |
| 자릿수 분리 | n % 10, n / 10 |
10으로 나눈 나머지 = 일의 자리, 몫 = 일의 자리를 뗀 수 |
마지막 자릿수 분리가 어떻게 돌아가는지 한 바퀴씩 따라가 봅시다. number 가 1234에서 시작합니다.
| 반복 | number (시작) |
number % 10 (출력) |
number / 10 (다음 값) |
|---|---|---|---|
| 1 | 1234 | 4 | 123 |
| 2 | 123 | 3 | 12 |
| 3 | 12 | 2 | 1 |
| 4 | 1 | 1 | 0 |
| (끝) | 0 | number > 0 이 거짓이라 멈춤 |
오른쪽부터 한 자리씩 떼어 내므로 출력이 4 3 2 1, 거꾸로 나옵니다. 44번째 줄에서 original 에 처음 값을 따로 저장해 둔 이유도 보입니다. 반복하는 동안 number 가 0까지 줄어들기 때문에, 처음 값을 찍으려면 미리 복사해 둬야 합니다.
“1는 홀수”? 출력을 보면
1는,3는처럼 조사가 어색합니다.printf는 숫자 뒤에 붙을 조사가 “은”인지 “는”인지 모릅니다. 한국어 프로그램에서 흔히 보는 문제이고, 보통"%d: 홀수"처럼 조사가 필요 없는 형식으로 피해 갑니다.
실험: 실수에 % 를 쓰면?
double d = 7.5;
printf("%f\n", d % 2);
$ gcc -Wall -Wextra -std=c11 fmod.c -o fmod
fmod.c: In function ‘main’:
fmod.c:5:22: error: invalid operands to binary % (have ‘double’ and ‘int’)
5 | printf("%f\n", d % 2);
| ^
이번에는 경고가 아니라 오류입니다. “% 의 피연산자로 double 은 쓸 수 없다(invalid operands).” % 는 정수 전용입니다. 실수의 나머지가 필요하면 <math.h> 의 fmod 함수를 씁니다(man 3 fmod 로 확인해 보세요).
2. 증감 연산자
1 늘리기, 1 줄이기
변수를 1 늘리는 일은 너무 자주 하기 때문에 전용 연산자가 있습니다.
| 연산자 | 이름 | 하는 일 |
|---|---|---|
++x |
전위(prefix) 증가 | x 를 1 늘리고, 늘어난 값을 식의 값으로 쓴다 |
x++ |
후위(postfix) 증가 | x 를 1 늘리지만, 식의 값은 늘기 전 값 |
--x |
전위 감소 | x 를 1 줄이고, 줄어든 값을 쓴다 |
x-- |
후위 감소 | x 를 1 줄이지만, 식의 값은 줄기 전 값 |
C++ 라는 언어 이름이 바로 여기서 왔습니다. “C에서 한 단계 나아간 언어”라는 말장난입니다.
++ 를 앞에 붙이든 뒤에 붙이든 변수 x 가 1 늘어나는 것은 똑같습니다. 다른 것은 “그 식 자체가 어떤 값으로 계산되는가” 하나뿐입니다. 말로는 헷갈리니 예제로 봅시다.
/*
* increment.c - 증감 연산자 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
int a, b;
// 전위 증가: 먼저 증가하고 값을 사용
printf("=== 전위 증가 (++a) ===\n");
a = 5;
b = ++a; // a를 먼저 6으로 만들고, b에 6 대입
printf("a = %d, b = %d\n", a, b); // a=6, b=6
// 후위 증가: 값을 먼저 사용하고 증가
printf("\n=== 후위 증가 (a++) ===\n");
a = 5;
b = a++; // b에 5 대입 후, a를 6으로 증가
printf("a = %d, b = %d\n", a, b); // a=6, b=5
// 전위 감소
printf("\n=== 전위 감소 (--a) ===\n");
a = 5;
b = --a;
printf("a = %d, b = %d\n", a, b); // a=4, b=4
// 후위 감소
printf("\n=== 후위 감소 (a--) ===\n");
a = 5;
b = a--;
printf("a = %d, b = %d\n", a, b); // a=4, b=5
// 단독 사용시에는 차이 없음
printf("\n=== 단독 사용 ===\n");
a = 5;
++a; // a = 6
printf("++a 후: a = %d\n", a);
a = 5;
a++; // a = 6
printf("a++ 후: a = %d\n", a);
// 반복문에서의 사용
printf("\n=== 반복문에서 사용 ===\n");
printf("for (i = 0; i < 5; i++): ");
for (int i = 0; i < 5; i++) { // i++나 ++i나 결과 동일
printf("%d ", i);
}
printf("\n");
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g increment.c -o increment && ./increment
=== 전위 증가 (++a) ===
a = 6, b = 6
=== 후위 증가 (a++) ===
a = 6, b = 5
=== 전위 감소 (--a) ===
a = 4, b = 4
=== 후위 감소 (a--) ===
a = 4, b = 5
=== 단독 사용 ===
++a 후: a = 6
a++ 후: a = 6
=== 반복문에서 사용 ===
for (i = 0; i < 5; i++): 0 1 2 3 4
두 경우를 나란히 놓으면 차이가 분명합니다.
| 코드 | 실행 순서 | 결과 |
|---|---|---|
a = 5; b = ++a; |
① a 를 6으로 늘린다 ② 6 을 b 에 넣는다 | a = 6, b = 6 |
a = 5; b = a++; |
① 지금 값 5 를 식의 값으로 챙겨 둔다 ② a 를 6으로 늘린다 ③ 챙겨 둔 5를 b 에 넣는다 | a = 6, b = 5 |
외우는 요령: 연산자가 앞에 있으면 먼저 바꾸고, 뒤에 있으면 나중에 바꾼다. 읽는 순서 그대로입니다. ++a 는 “늘리고 a”, a++ 는 “a 그리고 늘리기”.
“단독 사용” 부분과 반복문 부분을 보세요. ++a; 와 a++; 처럼 그 문장에서 증감만 할 때는 식의 값을 아무도 쓰지 않으므로 두 방식에 차이가 없습니다. for (int i = 0; i < 5; i++) 의 i++ 도 마찬가지라서 ++i 로 바꿔도 결과가 같습니다.
실험: i = i++ 는 어떻게 될까?
증감과 대입을 한 문장에서 같은 변수에 하면 무슨 일이 생길까요? seq.c:
#include <stdio.h>
int main(void) {
int i = 5;
i = i++;
printf("i = %d\n", i);
return 0;
}
$ gcc -Wall -Wextra -std=c11 seq.c -o seq
seq.c: In function ‘main’:
seq.c:5:7: warning: operation on ‘i’ may be undefined [-Wsequence-point]
5 | i = i++;
| ~~^~~~~
$ ./seq
i = 5
GCC가 “i 에 대한 연산이 정의되지 않았을 수 있다”고 경고합니다. 한 문장 안에서 i 를 두 번 바꾸려고(대입으로 한 번, ++ 로 한 번) 하는데, 어느 쪽이 먼저인지 C 표준이 정하지 않았기 때문입니다. 이 컴파일러에서는 5가 나왔지만, 6이 나오는 컴파일러가 있어도 틀린 게 아닙니다. 1절의 오버플로처럼 정의되지 않은 동작입니다. a[i] = i++; 처럼 같은 변수를 한 문장에서 바꾸면서 동시에 읽는 코드도 같은 문제입니다.
그래서 이 강좌의 규칙은 간단합니다.
증감 연산자는 독립된 문장으로만 쓴다.
i++;는 좋고,b = a++;는 가능하면b = a;와a++;두 줄로 나눈다.
두 줄로 나누면 읽는 사람이 순서를 고민할 필요가 없습니다. 기계어도 똑같이 나오므로 손해가 없습니다.
3. 대입 연산자와 복합 대입 연산자
= 는 “같다”가 아닙니다
수학에서 x = x + 1 은 말이 안 되는 식입니다. 어떤 수도 자기보다 1 큰 수와 같을 수 없으니까요. 하지만 C에서는 아주 흔한 코드입니다. C의 = 는 “같다”가 아니라 “오른쪽 값을 계산해서 왼쪽 상자에 넣어라” 라는 뜻이기 때문입니다. 그래서 이름도 대입(assignment) 연산자입니다.
x = x + 1;
// ① 오른쪽 x + 1 을 먼저 계산한다 (x 가 5 면 6)
// ② 그 값을 왼쪽 x 에 넣는다 (이제 x 는 6)
“같다”를 비교하는 연산자는 따로 있습니다. == 입니다(4절). 둘을 헷갈리는 것이 C에서 가장 유명한 실수 중 하나라서 4절에서 따로 다룹니다.
대입도 값을 가진 식입니다. x = 5 라는 식의 값은 대입된 값 5입니다. 그래서 이런 코드가 가능합니다.
int a, b, c;
a = b = c = 0; // c = 0 → 그 값 0 을 b 에 → 그 값 0 을 a 에
대입은 오른쪽부터 묶입니다(8절의 “결합 방향”). 그래서 c = 0 이 먼저 계산되고, 그 값이 차례로 왼쪽으로 전달됩니다. 여러 변수를 한 번에 초기화할 때 가끔 씁니다.
복합 대입 연산자
“x 에 무언가를 해서 다시 x 에 넣기”도 너무 흔해서 줄임 표기가 있습니다.
| 줄임 | 풀어 쓰면 | 뜻 |
|---|---|---|
x += 3 |
x = x + 3 |
3 더해서 넣기 |
x -= 3 |
x = x - 3 |
3 빼서 넣기 |
x *= 3 |
x = x * 3 |
3 곱해서 넣기 |
x /= 3 |
x = x / 3 |
3 으로 나눠서 넣기 |
x %= 3 |
x = x % 3 |
3 으로 나눈 나머지를 넣기 |
6절에서 배울 비트 연산자에도 &=, |=, ^=, <<=, >>= 가 있습니다. 규칙은 전부 같습니다. 연산자 뒤에 = 를 붙이면 “계산해서 자기 자신에 다시 넣기” 입니다.
/*
* compound_assignment.c - 복합 대입 연산자 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
int score = 100;
printf("초기 점수: %d\n", score);
printf("==================\n");
score += 10; // score = score + 10
printf("보너스 +10: %d (score += 10)\n", score);
score -= 5; // score = score - 5
printf("감점 -5: %d (score -= 5)\n", score);
score *= 2; // score = score * 2
printf("2배 보너스: %d (score *= 2)\n", score);
score /= 3; // score = score / 3
printf("1/3로 감소: %d (score /= 3)\n", score);
score %= 50; // score = score % 50
printf("50으로 나눈 나머지: %d (score %%= 50)\n", score);
// 다양한 타입에서 사용
printf("\n=== 실수에서 사용 ===\n");
double price = 1000.0;
printf("원래 가격: %.2f\n", price);
price *= 1.1; // 10% 인상
printf("10%% 인상: %.2f\n", price);
price *= 0.9; // 10% 할인
printf("10%% 할인: %.2f\n", price);
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g compound_assignment.c -o compound_assignment && ./compound_assignment
초기 점수: 100
==================
보너스 +10: 110 (score += 10)
감점 -5: 105 (score -= 5)
2배 보너스: 210 (score *= 2)
1/3로 감소: 70 (score /= 3)
50으로 나눈 나머지: 20 (score %= 50)
=== 실수에서 사용 ===
원래 가격: 1000.00
10% 인상: 1100.00
10% 할인: 990.00
두 군데를 짚어 봅시다.
score /= 3이 70: 210 ÷ 3 은 정확히 70이라 문제가 없지만,score가int이므로 1절의 정수 나눗셈 규칙이 그대로 적용됩니다. 만약 211이었다면 70.33…이 아니라 70이 됩니다.- 실수 부분의 마지막 줄: 1000원을 10% 올렸다가 10% 내렸더니 1000원이 아니라 990원입니다. 컴퓨터 오류가 아니라 수학입니다. 1100의 10%는 110이라서, 올린 금액(100)보다 내린 금액(110)이 큽니다. 1000 × 1.1 × 0.9 = 990. 할인 행사 문구를 계산하는 프로그램을 짤 때 실제로 헷갈리는 지점입니다.
4. 관계 연산자
비교하면 무엇이 나올까?
조건문을 만들려면 먼저 “비교”를 할 수 있어야 합니다. 두 값의 크기나 같음을 비교하는 연산자를 관계(relational) 연산자, 또는 비교 연산자라고 합니다.
| 연산자 | 뜻 | 예 (a = 10, b = 20) |
결과 |
|---|---|---|---|
== |
같다 | a == b |
0 |
!= |
같지 않다 | a != b |
1 |
< |
작다 | a < b |
1 |
> |
크다 | a > b |
0 |
<= |
작거나 같다 | a <= b |
1 |
>= |
크거나 같다 | a >= b |
0 |
키보드에 ≤, ≥, ≠ 가 없어서 두 글자로 씁니다. =< 나 => 처럼 순서를 바꾸면 안 됩니다. 등호는 항상 뒤에 옵니다.
결과 칸을 보세요. true, false 가 아니라 1과 0 입니다. C에서 비교의 결과는 int 형의 1(참) 또는 0(거짓) 입니다. 비교도 결국 값이 나오는 식이라서, 그 값을 %d 로 찍어 볼 수 있습니다.
/*
* relational.c - 관계 연산자 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
int a = 10, b = 20;
printf("a = %d, b = %d\n\n", a, b);
// 관계 연산자의 결과는 0 또는 1
printf("=== 관계 연산자 ===\n");
printf("a == b: %d (같다)\n", a == b); // 0 (거짓)
printf("a != b: %d (같지 않다)\n", a != b); // 1 (참)
printf("a < b: %d (작다)\n", a < b); // 1 (참)
printf("a > b: %d (크다)\n", a > b); // 0 (거짓)
printf("a <= b: %d (작거나 같다)\n", a <= b); // 1 (참)
printf("a >= b: %d (크거나 같다)\n", a >= b); // 0 (거짓)
// 같은 값 비교
printf("\n=== 같은 값 비교 ===\n");
int c = 10;
printf("a = %d, c = %d\n", a, c);
printf("a == c: %d\n", a == c); // 1 (참)
printf("a <= c: %d\n", a <= c); // 1 (참)
printf("a >= c: %d\n", a >= c); // 1 (참)
// 실수 비교 주의
printf("\n=== 실수 비교 주의 ===\n");
double x = 0.1 + 0.2;
double y = 0.3;
printf("0.1 + 0.2 = %.17f\n", x);
printf("0.3 = %.17f\n", y);
printf("0.1 + 0.2 == 0.3: %d (정확히 같지 않을 수 있음!)\n", x == y);
// 실수 비교의 올바른 방법
double epsilon = 0.0001;
double diff = x - y;
if (diff < 0) diff = -diff; // 절댓값
if (diff < epsilon) {
printf("epsilon(%f) 범위 내에서 같음\n", epsilon);
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g relational.c -o relational && ./relational
a = 10, b = 20
=== 관계 연산자 ===
a == b: 0 (같다)
a != b: 1 (같지 않다)
a < b: 1 (작다)
a > b: 0 (크다)
a <= b: 1 (작거나 같다)
a >= b: 0 (크거나 같다)
=== 같은 값 비교 ===
a = 10, c = 10
a == c: 1
a <= c: 1
a >= c: 1
=== 실수 비교 주의 ===
0.1 + 0.2 = 0.30000000000000004
0.3 = 0.29999999999999999
0.1 + 0.2 == 0.3: 0 (정확히 같지 않을 수 있음!)
epsilon(0.000100) 범위 내에서 같음
앞쪽은 표 그대로입니다. 흥미로운 곳은 “실수 비교 주의” 부분입니다.
0.1 + 0.2 는 0.3 이 아니다
0.1 + 0.2 == 0.3 이 0(거짓) 입니다. 소수점 아래 17자리까지 찍어 보니 이유가 보입니다. 0.1 + 0.2 는 0.30000000000000004, 0.3 은 0.29999999999999999 입니다. 둘 다 0.3이 아닙니다!
2주차 4절에서 이미 이 결과를 봤습니다. 이유는 컴퓨터가 실수를 2진수로 저장하기 때문입니다. 그런데 0.1은 2진수로 끝없이 이어지는 소수(0.000110011001100…)입니다. 10진수에서 1/3이 0.3333…으로 끝나지 않는 것과 같습니다. 8바이트 안에 무한한 자리를 담을 수 없으니 어딘가에서 자르고, 그 오차 때문에 0.1 + 0.2 와 0.3 이 서로 다른 근삿값이 됩니다.
그래서 실수는 == 로 비교하면 안 됩니다. 대신 “둘의 차이가 아주 작으면 같은 것으로 본다”고 비교합니다. 예제 39~45번째 줄이 그 방법입니다.
double epsilon = 0.0001; // 이 정도 차이는 같은 것으로 본다
double diff = x - y;
if (diff < 0) diff = -diff; // 차이의 절댓값
if (diff < epsilon) { ... } // 충분히 가까우면 같다
epsilon(엡실론)은 수학에서 “아주 작은 양”을 뜻하는 그리스 문자입니다. 어느 정도를 “작다”로 볼지는 다루는 값의 크기에 따라 달라서 정답은 없습니다. 0.0001은 이 예제를 위한 값일 뿐입니다. 돈 계산처럼 오차가 절대 없어야 하는 곳에서는 아예 실수를 쓰지 않고 원 단위 정수로 계산하는 것이 정석입니다.
= 와 == : C에서 가장 유명한 실수
= 는 대입, == 는 비교입니다(3절). 이 둘을 헷갈리면 무슨 일이 생길까요? assign.c:
#include <stdio.h>
int main(void) {
int a = 3;
if (a = 5) {
printf("a는 5입니다 (a = %d)\n", a);
}
return 0;
}
a 는 3이니까 “a 가 5 이면” 조건은 거짓이고 아무것도 출력되지 않아야 합니다. 그런데:
$ gcc -Wall -Wextra -std=c11 assign.c -o assign
assign.c: In function ‘main’:
assign.c:5:9: warning: suggest parentheses around assignment used as truth value [-Wparentheses]
5 | if (a = 5) {
| ^
$ ./assign
a는 5입니다 (a = 5)
출력됐고, 심지어 a 가 5로 바뀌었습니다. a = 5 는 비교가 아니라 대입이기 때문입니다. 3절에서 “대입도 값을 가진 식”이라고 했죠. a = 5 의 값은 5이고, 5는 0이 아니니 조건은 참입니다(5절에서 보듯 C는 0이 아닌 모든 값을 참으로 봅니다). 결국 이 if 는 항상 참이면서 a 를 망가뜨리는 코드가 됩니다.
GCC의 경고 문구를 읽어 봅시다. “참/거짓 값으로 쓰인 대입 둘레에 괄호를 치는 것을 제안한다(suggest parentheses).” 무슨 뜻일까요? 가끔은 일부러 조건 안에서 대입하는 코드가 있습니다. 그럴 때는 if ((a = 5)) 처럼 괄호를 한 겹 더 쳐서 “실수가 아니라 의도한 것”이라고 표시하라는 뜻입니다. 우리는 거의 항상 실수이므로, 이 경고가 나오면 == 로 고치면 됩니다.
옛날 요령: 상수를 왼쪽에.
if (5 == a)처럼 상수를 왼쪽에 쓰면, 실수로if (5 = a)라고 쳤을 때 “5에는 대입할 수 없다”는 오류가 나서 바로 알아챌 수 있습니다. 이런 스타일을 “요다 조건(Yoda condition)”이라고 부릅니다. 요즘은 컴파일러 경고가 좋아져서-Wall만 켜면 충분하므로, 이 강좌는 읽기 자연스러운a == 5순서를 씁니다.
실험: 1 < x < 5 는 수학처럼 동작할까?
수학에서는 “x가 1과 5 사이”를 1 < x < 5 로 씁니다. C에서도 될까요? chain.c:
#include <stdio.h>
int main(void) {
int x = 10;
if (1 < x < 5) {
printf("x 는 1과 5 사이입니다 (x = %d)\n", x);
}
if (1 < x && x < 5) {
printf("이 줄은 나오지 않아야 합니다\n");
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 chain.c -o chain
chain.c: In function ‘main’:
chain.c:5:15: warning: comparison of constant ‘5’ with boolean expression is always true [-Wbool-compare]
5 | if (1 < x < 5) {
| ^
chain.c:5:11: warning: comparisons like ‘X<=Y<=Z’ do not have their mathematical meaning [-Wparentheses]
5 | if (1 < x < 5) {
| ~~^~~
$ ./chain
x 는 1과 5 사이입니다 (x = 10)
x가 10인데 “1과 5 사이”라고 합니다. C는 1 < x < 5 를 왼쪽부터 두 번의 비교로 읽습니다.
1 < x < 5
= (1 < x) < 5 ← 왼쪽 비교부터
= (1 < 10) < 5
= 1 < 5 ← 1 < 10 은 참이라 1
= 1 ← 1 < 5 도 참
첫 비교의 결과는 0이나 1이고, 0과 1은 언제나 5보다 작으니 전체는 항상 참입니다. GCC도 두 경고로 정확히 짚어 줍니다. “상수 5를 참/거짓 값과 비교하면 항상 참이다”, “X<=Y<=Z 같은 비교는 수학적 의미를 갖지 않는다.” 범위 검사는 5절의 논리 연산자로 두 비교를 따로 쓰고 && 로 묶어야 합니다(예제의 두 번째 if).
5. 논리 연산자
그리고, 또는, 아니다
“18세 이상 이고 면허가 있으면 운전 가능”, “주말 이거나 공휴일이면 휴일” 처럼 여러 조건을 묶을 때 논리(logical) 연산자를 씁니다.
| 연산자 | 이름 | 읽는 법 | 결과가 참(1)인 경우 |
|---|---|---|---|
&& |
논리 AND | 그리고 | 양쪽이 모두 참일 때 |
\|\| |
논리 OR | 또는 | 하나라도 참일 때 |
! |
논리 NOT | 아니다 | 피연산자가 거짓일 때 (참과 거짓을 뒤집음) |
|| 는 키보드 Enter 위쪽(또는 Backspace 옆)의 \ 키를 Shift 와 함께 눌러 나오는 세로 막대 | 를 두 번 친 것입니다. 알파벳 l 이나 숫자 1이 아닙니다.
C의 참과 거짓: 0만 거짓이다
4절에서 비교 결과는 1 또는 0이라고 했습니다. 그런데 조건을 판단할 때 C는 조금 더 넓게 봅니다.
0 은 거짓, 0 이 아닌 모든 값은 참.
그래서 if (5), if (-1), if (x) 모두 가능합니다. if (x) 는 “x 가 0 이 아니면”이라는 뜻입니다. 앞의 if (a = 5) 가 참이 된 것도 이 규칙 때문입니다. 논리 연산자는 결과를 항상 0 또는 1 로 돌려줍니다. 6절의 실험에서 !!a 가 12가 아니라 1이 되는 것을 보게 됩니다.
아래 예제에서 int has_license = 1; 처럼 참/거짓을 정수로 담는 것도 이 규칙 덕분입니다. C99부터는 <stdbool.h> 를 포함하면 bool, true, false 라는 이름도 쓸 수 있는데, 속을 들여다보면 true 는 1, false 는 0입니다.
예제: logical.c
/*
* logical.c - 논리 연산자 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
int age = 25;
int has_license = 1; // 1: 참, 0: 거짓
int is_drunk = 0;
printf("나이: %d, 면허: %d, 음주: %d\n\n", age, has_license, is_drunk);
// AND 연산: 모든 조건이 참이어야 참
printf("=== AND 연산 (&&) ===\n");
printf("운전 조건: 18세 이상 AND 면허 있음 AND 음주 안함\n");
if (age >= 18 && has_license && !is_drunk) {
printf("-> 운전 가능합니다!\n");
} else {
printf("-> 운전할 수 없습니다.\n");
}
// OR 연산: 하나라도 참이면 참
printf("\n=== OR 연산 (||) ===\n");
int is_weekend = 0;
int is_holiday = 1;
printf("주말: %d, 공휴일: %d\n", is_weekend, is_holiday);
if (is_weekend || is_holiday) {
printf("-> 오늘은 쉬는 날입니다!\n");
} else {
printf("-> 오늘은 근무일입니다.\n");
}
// NOT 연산: 부정
printf("\n=== NOT 연산 (!) ===\n");
int is_raining = 0;
printf("비 오는 중: %d\n", is_raining);
if (!is_raining) {
printf("-> 비가 오지 않습니다. 산책 가능!\n");
}
// 복합 조건
printf("\n=== 복합 조건 ===\n");
int score = 85;
int attendance = 90;
printf("점수: %d, 출석률: %d\n", score, attendance);
// 점수가 60 이상이고, 출석률이 80 이상이거나 점수가 95 이상이면 통과
if ((score >= 60 && attendance >= 80) || score >= 95) {
printf("-> 통과!\n");
} else {
printf("-> 불통과\n");
}
// 논리 연산 진리표
printf("\n=== 진리표 ===\n");
printf("A\tB\tA&&B\tA||B\t!A\n");
printf("-------------------------------------------\n");
for (int a = 0; a <= 1; a++) {
for (int b = 0; b <= 1; b++) {
printf("%d\t%d\t%d\t%d\t%d\n", a, b, a && b, a || b, !a);
}
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g logical.c -o logical && ./logical
나이: 25, 면허: 1, 음주: 0
=== AND 연산 (&&) ===
운전 조건: 18세 이상 AND 면허 있음 AND 음주 안함
-> 운전 가능합니다!
=== OR 연산 (||) ===
주말: 0, 공휴일: 1
-> 오늘은 쉬는 날입니다!
=== NOT 연산 (!) ===
비 오는 중: 0
-> 비가 오지 않습니다. 산책 가능!
=== 복합 조건 ===
점수: 85, 출석률: 90
-> 통과!
=== 진리표 ===
A B A&&B A||B !A
-------------------------------------------
0 0 0 0 1
0 1 0 1 1
1 0 0 1 0
1 1 1 1 0
18번째 줄의 조건을 소리 내어 읽어 보세요. age >= 18 && has_license && !is_drunk 는 “나이가 18 이상 그리고 면허가 있음 그리고 음주가 아님“입니다. 코드가 거의 우리말처럼 읽힙니다. 좋은 조건식은 이렇게 소리 내어 읽었을 때 뜻이 통합니다.
52번째 줄은 괄호가 중요합니다. (score >= 60 && attendance >= 80) || score >= 95 는 “(60점 이상이면서 출석 80 이상) 또는 95점 이상”입니다. 8절에서 보겠지만 && 가 || 보다 먼저 계산되므로 이 경우 괄호가 없어도 결과는 같습니다. 하지만 괄호가 있으면 읽는 사람이 우선순위를 몰라도 뜻을 압니다.
마지막의 진리표(truth table)는 프로그램이 직접 계산해서 찍은 것입니다. \t(1주차 6절의 탭)로 칸을 맞췄습니다. 표를 외울 필요는 없고, 이렇게 읽으면 됩니다.
A&&B열: 1이 한 번만 나옵니다. 둘 다 1일 때뿐입니다.A||B열: 0이 한 번만 나옵니다. 둘 다 0일 때뿐입니다.!A열: A 를 뒤집은 것입니다.
단락 평가: 답이 나오면 나머지는 보지 않는다
논리 연산자에는 아주 중요한 성질이 하나 있습니다.
A && B에서 A 가 거짓이면, B 가 무엇이든 결과는 거짓입니다. 그래서 C는 B 를 아예 계산하지 않습니다.A || B에서 A 가 참이면, 결과는 이미 참입니다. 역시 B 를 계산하지 않습니다.
이것을 단락 평가(short-circuit evaluation) 라고 합니다. 전기 회로에서 합선(short circuit)되면 전류가 나머지 회로를 건너뛰는 것에 빗댄 이름입니다. 정말 건너뛰는지 확인해 봅시다. sc.c:
#include <stdio.h>
int main(void) {
int count = 0;
int x = 0;
if (x != 0 && 10 / x > 1) {
printf("10 / x 는 1보다 큽니다\n");
} else {
printf("x 가 0 이라서 나눗셈을 하지 않았습니다\n");
}
if (x == 0 || (count = 100) > 0) {
printf("count = %d\n", count);
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 sc.c -o sc && ./sc
x 가 0 이라서 나눗셈을 하지 않았습니다
count = 0
두 가지를 확인했습니다.
- 첫 번째
if:x가 0인데10 / x가 있습니다. 1절에서 봤듯 정수를 0으로 나누면 프로그램이 죽습니다. 그런데 멀쩡합니다.x != 0이 거짓이라서&&뒤의10 / x를 계산조차 하지 않았기 때문입니다. 이것이 단락 평가의 가장 중요한 쓰임입니다. “먼저 안전한지 확인하고, 안전할 때만 위험한 일을 한다” 를 한 줄로 쓸 수 있습니다. 6주차에 포인터를 배우면if (p != NULL && *p > 0)이라는 코드를 수없이 보게 되는데, 똑같은 원리입니다. - 두 번째
if:(count = 100)이라는 대입이||뒤에 있는데,count는 여전히 0입니다. 앞의x == 0이 참이라서 뒤쪽 대입이 실행되지 않았습니다.
두 번째 경우는 반대로 함정이기도 합니다. 조건식 안에 “무언가를 바꾸는 코드”(대입, ++, 함수 호출)를 넣으면, 앞쪽 조건에 따라 실행될 수도 있고 안 될 수도 있습니다. 그래서 조건식 안에서는 값을 바꾸지 않는 것이 좋습니다.
6. 비트 연산자
수를 0과 1의 줄로 보기
지금까지 12 는 그냥 12였습니다. 하지만 컴퓨터 안에서 12는 0과 1의 줄입니다. 2주차에서 sizeof(int) 가 4바이트라고 확인했죠. 1바이트는 8비트(bit, 0 또는 1 한 자리)이므로 int 는 32개의 0과 1로 이루어져 있습니다.
12를 2진수로 쓰면 1100 입니다. 각 자리의 값이 오른쪽부터 1, 2, 4, 8 … 로 두 배씩 커지기 때문입니다.
자리값: 8 4 2 1
12 = 1 1 0 0 → 8 + 4 = 12
10 = 1 0 1 0 → 8 + 2 = 10
5 = 0 1 0 1 → 4 + 1 = 5
1주차 2절에서 파일 권한 rwx 를 4, 2, 1로 더해 7을 만들었던 것 기억하나요? 그게 바로 3비트 2진수였습니다. r 이 4의 자리, w 가 2의 자리, x 가 1의 자리입니다.
비트 연산자는 이 0과 1을 한 자리씩 짝지어 계산합니다.
| 연산자 | 이름 | 자리마다 하는 일 | 5절 논리 연산자와 비교 |
|---|---|---|---|
& |
비트 AND | 둘 다 1이면 1 | && 는 값 전체를 참/거짓으로 봄 |
\| |
비트 OR | 하나라도 1이면 1 | \|\| 와 비슷하지만 자리마다 |
^ |
비트 XOR | 서로 다르면 1 | 논리 연산자에는 없음 |
~ |
비트 NOT | 0과 1을 뒤집음 | ! 는 값 전체를 뒤집음 |
<< |
왼쪽 시프트 | 모든 자리를 왼쪽으로 밀고 빈자리는 0 | |
>> |
오른쪽 시프트 | 모든 자리를 오른쪽으로 밈 |
XOR는 eXclusive OR, “배타적 또는”입니다. “둘 중 하나만“이라는 뜻입니다.
& 와 &&, | 와 || 는 한 글자 차이지만 전혀 다른 연산자입니다. 12 && 10 은 “둘 다 0이 아니냐?”라서 1이지만, 12 & 10 은 자리마다 계산해서 8입니다. 아래 예제로 확인합니다.
예제: bitwise.c
2진수를 찍어 주는 기능이 C에는 없어서(2주차의 printf 서식 지정자에 2진수는 없었습니다), 예제에 print_binary 라는 함수를 직접 만들어 넣었습니다. 함수는 5주차에 배우니, 지금은 “숫자를 받아 2진수 8자리로 찍어 주는 부품”이라고만 생각하세요. 안이 궁금하다면, 이 절을 다 읽고 나서 (num >> i) & 1 이 “i 번째 자리가 1인지”를 뽑아내는 식이라는 걸 확인해 보세요.
/*
* bitwise.c - 비트 연산자 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
// 2진수 출력 함수
void print_binary(unsigned int num, int bits) {
for (int i = bits - 1; i >= 0; i--) {
printf("%d", (num >> i) & 1);
if (i % 4 == 0 && i != 0) printf(" ");
}
}
int main(void) {
unsigned int a = 12; // 1100
unsigned int b = 10; // 1010
printf("a = %2u = ", a);
print_binary(a, 8);
printf("\n");
printf("b = %2u = ", b);
print_binary(b, 8);
printf("\n\n");
// AND 연산
printf("a & b = %2u = ", a & b);
print_binary(a & b, 8);
printf(" (둘 다 1인 비트만 1)\n");
// OR 연산
printf("a | b = %2u = ", a | b);
print_binary(a | b, 8);
printf(" (하나라도 1이면 1)\n");
// XOR 연산
printf("a ^ b = %2u = ", a ^ b);
print_binary(a ^ b, 8);
printf(" (서로 다르면 1)\n");
// NOT 연산 (8비트만 보기)
printf("~a = %2u = ", (unsigned char)~a);
print_binary((unsigned char)~a, 8);
printf(" (모든 비트 반전)\n");
printf("\n=== 시프트 연산 ===\n");
unsigned int x = 5; // 0101
printf("x = %2u = ", x);
print_binary(x, 8);
printf("\n");
printf("x << 1 = %2u = ", x << 1);
print_binary(x << 1, 8);
printf(" (2배)\n");
printf("x << 2 = %2u = ", x << 2);
print_binary(x << 2, 8);
printf(" (4배)\n");
printf("x >> 1 = %2u = ", x >> 1);
print_binary(x >> 1, 8);
printf(" (1/2배)\n");
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g bitwise.c -o bitwise && ./bitwise
a = 12 = 0000 1100
b = 10 = 0000 1010
a & b = 8 = 0000 1000 (둘 다 1인 비트만 1)
a | b = 14 = 0000 1110 (하나라도 1이면 1)
a ^ b = 6 = 0000 0110 (서로 다르면 1)
~a = 243 = 1111 0011 (모든 비트 반전)
=== 시프트 연산 ===
x = 5 = 0000 0101
x << 1 = 10 = 0000 1010 (2배)
x << 2 = 20 = 0001 0100 (4배)
x >> 1 = 2 = 0000 0010 (1/2배)
손으로 한 번 따라가 봅시다. 12(1100)와 10(1010)을 위아래로 놓고 세로로 한 자리씩 계산합니다.
12 = 1 1 0 0
10 = 1 0 1 0
────────────────
12 & 10 = 1 0 0 0 = 8 (둘 다 1인 자리만 1)
12 | 10 = 1 1 1 0 = 14 (하나라도 1인 자리는 1)
12 ^ 10 = 0 1 1 0 = 6 (서로 다른 자리만 1)
시프트는 자리를 통째로 미는 것입니다. 5(0101)를 왼쪽으로 한 칸 밀면 1010 = 10, 두 칸 밀면 10100 = 20입니다. 10진수에서 오른쪽에 0을 붙이면 10배가 되듯, 2진수에서 왼쪽으로 한 칸 밀면 2배가 됩니다. 오른쪽으로 밀면 반대로 반이 되고, 떨어져 나간 자리는 버려집니다(5 >> 1 은 2.5가 아니라 2).
~a 는 왜 243일까? 12의 모든 비트를 뒤집으면 0000 1100 이 1111 0011 이 되고, 8비트로 보면 243입니다. 그런데 예제 44번째 줄은 (unsigned char)~a 처럼 8비트로 잘라서 찍었습니다. 자르지 않으면 어떻게 될까요?
실험: ~ 와 ! 의 차이, 그리고 부호
notb.c:
#include <stdio.h>
int main(void) {
int a = 12;
unsigned int u = 12;
printf("~a (int) = %d\n", ~a);
printf("~u (unsigned int) = %u\n", ~u);
printf("!a = %d, !!a = %d\n", !a, !!a);
printf("sizeof(int) = %zu 바이트 = %zu 비트\n", sizeof(int), sizeof(int) * 8);
return 0;
}
$ gcc -Wall -Wextra -std=c11 notb.c -o notb && ./notb
~a (int) = -13
~u (unsigned int) = 4294967283
!a = 0, !!a = 1
sizeof(int) = 4 바이트 = 32 비트
~a가 -13:int는 32비트라서 12를 뒤집으면 앞쪽 28개의 0이 전부 1이 됩니다. 부호 있는 정수에서 맨 앞 비트가 1이면 음수로 읽기 때문에 -13이 됩니다. 규칙으로 쓰면~x == -x - 1입니다. 음수를 이렇게 “모든 비트를 뒤집고 1을 더한” 모양으로 저장하는 방식을 2의 보수(two’s complement) 라고 합니다. 거의 모든 CPU가 이 방식을 쓰고, 1절에서INT_MAX + 1이 가장 작은 음수로 넘어간 것도 이 모양 때문입니다.~u가 4294967283: 같은 비트 패턴을unsigned int(부호 없는 정수)로 읽으면 음수가 없으니 아주 큰 양수가 됩니다. 같은 32개의 0과 1이 자료형에 따라 다른 값으로 읽힌다는 것, 2주차에서 배운 자료형의 의미가 여기서 드러납니다.!a와!!a:!는 비트가 아니라 값 전체를 참/거짓으로 뒤집습니다. 12는 참이니!12는 0, 그걸 다시 뒤집은!!12는 1입니다.!!는 “0이 아닌 값을 1로 정리”하는 요령으로 가끔 쓰입니다.
그래서 비트 연산은 unsigned 자료형에 하는 것이 원칙입니다. 부호 비트가 끼어들면 결과를 읽기가 어려워지기 때문입니다. bitwise.c 도 unsigned int 로 선언했습니다.
비트 연산은 어디에 쓸까?
“0과 1을 한 자리씩” 다루는 일이 실제로 어디에 필요할까요? 가장 대표적인 쓰임이 플래그(flag) 입니다. 여러 개의 켜짐/꺼짐 상태를 정수 하나에 비트 하나씩 담는 방법입니다. 1주차의 파일 권한이 정확히 이 방식입니다.
/*
* bitwise_uses.c - 비트 연산자 실용적 활용 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
// 1. 플래그 관리
printf("=== 플래그 관리 ===\n");
unsigned int flags = 0;
#define FLAG_READ (1 << 0) // 0001 = 1
#define FLAG_WRITE (1 << 1) // 0010 = 2
#define FLAG_EXECUTE (1 << 2) // 0100 = 4
// 플래그 설정 (OR)
flags |= FLAG_READ;
flags |= FLAG_WRITE;
printf("읽기, 쓰기 권한 설정: %u (0b", flags);
for (int i = 3; i >= 0; i--) printf("%d", (flags >> i) & 1);
printf(")\n");
// 플래그 확인 (AND)
if (flags & FLAG_READ) {
printf("읽기 권한 있음\n");
}
if (flags & FLAG_EXECUTE) {
printf("실행 권한 있음\n");
} else {
printf("실행 권한 없음\n");
}
// 플래그 해제 (AND NOT)
flags &= ~FLAG_WRITE;
printf("쓰기 권한 해제: %u\n", flags);
// 플래그 토글 (XOR)
flags ^= FLAG_EXECUTE;
printf("실행 권한 토글 (켜기): %u\n", flags);
flags ^= FLAG_EXECUTE;
printf("실행 권한 토글 (끄기): %u\n", flags);
// 2. 빠른 2의 거듭제곱 연산
printf("\n=== 빠른 곱셈/나눗셈 ===\n");
int num = 10;
printf("%d * 2 = %d (시프트: %d << 1 = %d)\n", num, num * 2, num, num << 1);
printf("%d * 4 = %d (시프트: %d << 2 = %d)\n", num, num * 4, num, num << 2);
printf("%d * 8 = %d (시프트: %d << 3 = %d)\n", num, num * 8, num, num << 3);
printf("%d / 2 = %d (시프트: %d >> 1 = %d)\n", num, num / 2, num, num >> 1);
printf("%d / 4 = %d (시프트: %d >> 2 = %d)\n", num, num / 4, num, num >> 2);
// 3. 두 변수 교환 (XOR 스왑)
printf("\n=== XOR 스왑 ===\n");
int x = 5, y = 10;
printf("교환 전: x=%d, y=%d\n", x, y);
x = x ^ y; // x에 x^y 저장
y = x ^ y; // y = (x^y) ^ y = x
x = x ^ y; // x = (x^y) ^ x = y
printf("교환 후: x=%d, y=%d\n", x, y);
// 4. 홀수/짝수 판별 (비트 연산)
printf("\n=== 홀수/짝수 판별 (비트 연산) ===\n");
for (int i = 0; i < 10; i++) {
if (i & 1) { // 마지막 비트가 1이면 홀수
printf("%d: 홀수\n", i);
} else {
printf("%d: 짝수\n", i);
}
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g bitwise_uses.c -o bitwise_uses && ./bitwise_uses
=== 플래그 관리 ===
읽기, 쓰기 권한 설정: 3 (0b0011)
읽기 권한 있음
실행 권한 없음
쓰기 권한 해제: 1
실행 권한 토글 (켜기): 5
실행 권한 토글 (끄기): 1
=== 빠른 곱셈/나눗셈 ===
10 * 2 = 20 (시프트: 10 << 1 = 20)
10 * 4 = 40 (시프트: 10 << 2 = 40)
10 * 8 = 80 (시프트: 10 << 3 = 80)
10 / 2 = 5 (시프트: 10 >> 1 = 5)
10 / 4 = 2 (시프트: 10 >> 2 = 2)
=== XOR 스왑 ===
교환 전: x=5, y=10
교환 후: x=10, y=5
=== 홀수/짝수 판별 (비트 연산) ===
0: 짝수
1: 홀수
2: 짝수
3: 홀수
4: 짝수
5: 홀수
6: 짝수
7: 홀수
8: 짝수
9: 홀수

비트 연산은 어디에 쓰나
13~15번째 줄의 #define 은 1주차 7절에서 본 전처리기 지시입니다. FLAG_READ 라는 이름을 (1 << 0), 즉 1로 바꿔치기하라는 뜻입니다. 숫자 대신 이름을 쓰면 flags |= 2 보다 flags |= FLAG_WRITE 가 훨씬 읽기 쉽습니다. (1 << 1) 처럼 시프트로 쓴 것은 “몇 번째 비트인지”를 드러내기 위해서입니다. 보통 #define 은 파일 맨 위에 쓰지만, 여기서는 쓰는 곳 가까이에 두었습니다.
플래그를 다루는 네 가지 동작은 비트 연산자 넷에 하나씩 대응합니다. 이 표는 실무에서 그대로 쓰는 공식이니 기억해 둘 만합니다.
| 하고 싶은 일 | 코드 | 원리 |
|---|---|---|
| 켜기 (set) | flags \|= FLAG |
OR 는 1을 만나면 1. 다른 비트는 그대로 |
| 끄기 (clear) | flags &= ~FLAG |
~FLAG 는 그 비트만 0이고 나머지 1. AND 로 그 비트만 0으로 |
| 확인 (test) | flags & FLAG |
그 비트가 켜져 있으면 0이 아닌 값(참) |
| 뒤집기 (toggle) | flags ^= FLAG |
XOR 는 1과 만나면 뒤집힘. 두 번 하면 원래대로 |
출력을 따라가 보면, 읽기(1)와 쓰기(2)를 켜서 3(0b0011), 쓰기를 꺼서 1, 실행(4)을 뒤집어 켜서 5, 다시 뒤집어 꺼서 1이 됩니다. 1주차에서 chmod u+x 가 하던 일이 바로 flags |= 실행비트 입니다. 리눅스 커널 안에서 권한이 정말 이렇게 처리됩니다.
시프트로 곱셈·나눗셈을 대신할 때 주의
num << 1 은 양수에서 num * 2 와 같고, num >> 1 은 양수에서 num / 2 와 같습니다. 출력에서 확인한 그대로입니다. 하지만 음수에서는 오른쪽 시프트와 나눗셈이 다릅니다. 1절의 div.c 에서 이미 봤습니다.
-7 / 2 = -3 ← 0 쪽으로 버림
-7 >> 1 = -4 ← 음의 무한대 쪽으로 내림
게다가 부호 있는 음수의 오른쪽 시프트 결과는 C 표준이 “구현에 따라 다름(implementation-defined)”으로 남겨 두었습니다. GCC는 -4를 주지만 다른 컴파일러는 다를 수 있습니다. 그리고 요즘 컴파일러는 x * 2, x / 2 를 알아서 시프트로 바꿔 번역합니다(1주차 7절에서 printf 가 puts 로 바뀐 것처럼). “빠르니까 시프트”라는 이유로 손으로 바꿀 필요는 없습니다. 비트를 다루는 것이 의미상 자연스러울 때(플래그처럼)만 시프트를 쓰세요.
XOR 스왑은 “신기한 요령”일 뿐
임시 변수 없이 XOR 세 번으로 두 값을 맞바꾸는 요령이 출력에 있습니다. 한 줄씩 따라가면 신기하게 맞바뀝니다. 하지만 실무에서는 쓰지 않습니다.
- 두 변수가 같은 메모리를 가리키면 값이 0이 됩니다.
x ^ x는 항상 0이라서, 자기 자신과 바꾸는 순간 원래 값을 잃습니다. 6주차 포인터에서 “두 이름이 같은 상자를 가리키는” 상황을 배우면 이 위험이 실감 납니다. - 더 느리고 읽기 어렵습니다. 세 연산이 앞 결과를 기다려야 해서 CPU가 동시에 처리하지 못하고, 코드만 보고 의도를 알기 어렵습니다.
임시 변수를 쓰는 평범한 방법이 더 빠르고 안전하고 명확합니다.
int temp = x;
x = y;
y = temp;
i & 1 로 홀짝 판별
출력 마지막의 i & 1 은 맨 오른쪽 비트(1의 자리) 만 남깁니다. 홀수는 1의 자리가 항상 1, 짝수는 항상 0이므로 홀짝을 알 수 있습니다. 1절의 i % 2 와 같은 결과이고, 컴파일러가 둘을 똑같이 번역하는 경우가 많아서 속도 차이도 사실상 없습니다. 게다가 i & 1 은 1절에서 문제가 됐던 음수 홀수도 1을 줍니다(-7 & 1 은 1). 두 방법이 모두 있다는 것만 알아 두세요.
7. 3항 연산자 (조건 연산자)
한 줄짜리 if
“조건이 참이면 이 값, 아니면 저 값”을 한 줄로 쓰는 연산자입니다. 피연산자가 세 개라서 3항(ternary) 연산자라고 부릅니다. C에 하나뿐인 3항 연산자입니다.
조건 ? 참일_때의_값 : 거짓일_때의_값
? 는 “조건이 참이냐?”라는 물음, : 는 “아니면”이라고 읽으면 자연스럽습니다. (age >= 18) ? "성인" : "미성년자" 는 “18세 이상이냐? 그러면 성인, 아니면 미성년자”.
/*
* ternary.c - 3항 연산자 (조건 연산자) 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
int age = 20;
// 기본 사용법
printf("=== 기본 사용법 ===\n");
const char *status = (age >= 18) ? "성인" : "미성년자";
printf("%d세는 %s입니다\n", age, status);
// if-else 대신 사용
printf("\n=== if-else 대신 사용 ===\n");
int a = 5, b = 10;
int max = (a > b) ? a : b;
int min = (a < b) ? a : b;
printf("a=%d, b=%d 중 큰 값: %d, 작은 값: %d\n", a, b, max, min);
// 절댓값 계산
printf("\n=== 절댓값 계산 ===\n");
int numbers[] = {-7, 3, -15, 0, 8};
int count = sizeof(numbers) / sizeof(numbers[0]);
for (int i = 0; i < count; i++) {
int num = numbers[i];
int abs_value = (num >= 0) ? num : -num;
printf("|%d| = %d\n", num, abs_value);
}
// printf 안에서 직접 사용
printf("\n=== printf 안에서 사용 ===\n");
int score = 75;
printf("시험 결과: %s\n", (score >= 60) ? "합격" : "불합격");
// 여러 점수 테스트
int scores[] = {45, 60, 85, 90, 55};
for (int i = 0; i < 5; i++) {
printf("점수 %d: %s\n", scores[i],
(scores[i] >= 60) ? "합격" : "불합격");
}
// 중첩 (가독성을 위해 권장하지 않음)
printf("\n=== 중첩 3항 연산자 ===\n");
int x = 15;
const char *size = (x < 10) ? "small" :
(x < 20) ? "medium" : "large";
printf("%d는 %s입니다\n", x, size);
// 같은 로직을 if-else로 (더 읽기 쉬움)
printf("\n=== 같은 로직 if-else 버전 ===\n");
if (x < 10) {
size = "small";
} else if (x < 20) {
size = "medium";
} else {
size = "large";
}
printf("%d는 %s입니다 (if-else)\n", x, size);
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g ternary.c -o ternary && ./ternary
=== 기본 사용법 ===
20세는 성인입니다
=== if-else 대신 사용 ===
a=5, b=10 중 큰 값: 10, 작은 값: 5
=== 절댓값 계산 ===
|-7| = 7
|3| = 3
|-15| = 15
|0| = 0
|8| = 8
=== printf 안에서 사용 ===
시험 결과: 합격
점수 45: 불합격
점수 60: 합격
점수 85: 합격
점수 90: 합격
점수 55: 불합격
=== 중첩 3항 연산자 ===
15는 medium입니다
=== 같은 로직 if-else 버전 ===
15는 medium입니다 (if-else)
- 13번째 줄
const char *status = ...: 문자열을 담는 변수입니다.char *는 6주차 포인터에서 정체를 밝히니, 지금은 “문자열을 가리키는 이름표”라고만 알아 두세요. 3항 연산자가 두 문자열 중 하나를 골라 그 이름표에 붙여 줍니다. - 25~26번째 줄
int numbers[] = {...}와sizeof(numbers) / sizeof(numbers[0]): 4주차에서 배울 배열을 미리 썼습니다. 값 다섯 개를 한 이름으로 묶은 것이고,numbers[i]는 그중 i 번째 값입니다. 두 번째 식은 “배열 전체 크기 ÷ 한 칸 크기 = 칸 수”라는 요령입니다. 2주차의sizeof가 이렇게 쓰입니다. 자세한 것은 다음 주에 봅니다. - 37, 42~43번째 줄: 3항 연산자는 값이므로
printf의 인자 자리에 바로 넣을 수 있습니다.if는 문이라서 이렇게 할 수 없습니다. 이것이 3항 연산자가 있는 이유입니다. - 49~50번째 줄: 3항 연산자 안에 3항 연산자를 넣은 중첩입니다. 동작은 하지만, 53~61번째 줄의
if-else버전과 비교해 보세요. 어느 쪽이 읽기 쉬운가요?
언제 쓰고 언제 쓰지 말까
| 쓰기 좋은 곳 | 피해야 할 곳 |
|---|---|
두 값 중 하나 고르기: max = (a > b) ? a : b; |
3항 안에 3항 (중첩) |
printf 안에서 단어 고르기: (n == 1) ? "개" : "개들" |
조건이 길고 복잡할 때 |
| 변수 초기화 | 참/거짓 쪽에서 값을 바꾸는 일(대입, ++)을 할 때 |
규칙은 하나입니다. 한눈에 읽히면 3항, 한 번 더 읽어야 하면 if.
8. 연산자 우선순위와 결합 방향
무엇부터 계산할까?
2 + 3 * 4 는 20일까요, 14일까요? 수학에서 곱셈을 먼저 하듯, C도 연산자마다 먼저 계산되는 순서(우선순위, precedence) 가 정해져 있습니다. 그리고 우선순위가 같은 연산자가 이어지면 어느 쪽부터 묶을지(결합 방향, associativity) 도 정해져 있습니다. 10 - 3 - 2 는 (10 - 3) - 2 = 5 이지 10 - (3 - 2) = 9 가 아닙니다.
전체 표는 이렇습니다. 위에 있을수록 먼저 계산됩니다. 아직 배우지 않은 연산자(포인터, 구조체용)도 있는데, 자리만 봐 두세요.
| 순위 | 연산자 | 이름 | 결합 방향 |
|---|---|---|---|
| 1 (높음) | () [] -> . 후위 ++ -- |
함수 호출, 배열 첨자, 구조체 접근, 후위 증감 | 왼쪽 → 오른쪽 |
| 2 | 전위 ++ -- 단항 + - ! ~ * & (자료형) sizeof |
단항 연산자, 형 변환 | 오른쪽 → 왼쪽 |
| 3 | * / % |
곱셈류 | 왼쪽 → 오른쪽 |
| 4 | + - |
덧셈류 | 왼쪽 → 오른쪽 |
| 5 | << >> |
시프트 | 왼쪽 → 오른쪽 |
| 6 | < <= > >= |
대소 비교 | 왼쪽 → 오른쪽 |
| 7 | == != |
같음 비교 | 왼쪽 → 오른쪽 |
| 8 | & |
비트 AND | 왼쪽 → 오른쪽 |
| 9 | ^ |
비트 XOR | 왼쪽 → 오른쪽 |
| 10 | \| |
비트 OR | 왼쪽 → 오른쪽 |
| 11 | && |
논리 AND | 왼쪽 → 오른쪽 |
| 12 | \|\| |
논리 OR | 왼쪽 → 오른쪽 |
| 13 | ? : |
3항 | 오른쪽 → 왼쪽 |
| 14 | = += -= *= /= %= <<= >>= &= ^= \|= |
대입 | 오른쪽 → 왼쪽 |
| 15 (낮음) | , |
쉼표 | 왼쪽 → 오른쪽 |
외우지 않아도 되는 이유는 뒤에서 설명하고, 흐름만 잡아 두세요. 단항 → 산술 → 시프트 → 비교 → 비트 → 논리 → 3항 → 대입 순서입니다. “계산하고, 비교하고, 묶고, 저장한다”는 자연스러운 흐름과 거의 같습니다. 예외가 딱 하나 있는데, 그게 아래 예제의 함정입니다.
예제: precedence.c
이번에는 컴파일할 때 경고가 하나 나옵니다. 일부러 넣은 것입니다.
/*
* precedence.c - 연산자 우선순위 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
int a = 5, b = 10, c = 15;
// 산술 연산자 우선순위
printf("=== 산술 연산자 우선순위 ===\n");
printf("2 + 3 * 4 = %d (곱셈 먼저)\n", 2 + 3 * 4); // 14
printf("(2 + 3) * 4 = %d (괄호 먼저)\n", (2 + 3) * 4); // 20
printf("10 / 2 + 3 = %d\n", 10 / 2 + 3); // 8
printf("10 / (2 + 3) = %d\n", 10 / (2 + 3)); // 2
printf("10 - 3 - 2 = %d (좌에서 우로)\n", 10 - 3 - 2); // 5
// 관계 및 논리 연산자
printf("\n=== 관계 및 논리 연산자 ===\n");
printf("a=%d, b=%d, c=%d\n", a, b, c);
printf("a < b && b < c: %d\n", a < b && b < c); // 1
printf("a > b || c > b: %d\n", a > b || c > b); // 1
printf("!(a > b): %d\n", !(a > b)); // 1
// 비트 연산자와 관계 연산자 주의
printf("\n=== 비트 연산자 주의! ===\n");
printf("5 & 3 = %d\n", 5 & 3); // 1
printf("3 == 1 = %d\n", 3 == 1); // 0
// 주의: 비트 연산자는 관계 연산자보다 우선순위가 낮음!
printf("5 & 3 == 1 의 실제 계산: 5 & (3 == 1) = 5 & 0 = %d\n", 5 & 3 == 1);
printf("(5 & 3) == 1 이 의도한 것: %d\n", (5 & 3) == 1);
// 대입 연산자는 우선순위가 낮음
printf("\n=== 대입 연산자 ===\n");
int x;
x = a + b; // a + b 먼저, 그 후 x에 대입
printf("x = a + b: x = %d\n", x);
// 복합 예제
printf("\n=== 복합 예제 ===\n");
int result = 2 + 3 * 4 - 6 / 2; // 2 + 12 - 3 = 11
printf("2 + 3 * 4 - 6 / 2 = %d\n", result);
result = ((2 + 3) * 4 - 6) / 2; // (5 * 4 - 6) / 2 = 14 / 2 = 7
printf("((2 + 3) * 4 - 6) / 2 = %d\n", result);
// 괄호로 명확하게!
printf("\n=== 괄호 사용 권장 ===\n");
// 애매한 경우는 괄호로 명확하게
result = ((a + b) * c) / ((a - b) + c);
printf("((a + b) * c) / ((a - b) + c) = %d\n", result);
// 실용적 조언
printf("\n=== 실용적 조언 ===\n");
printf("1. 산술: * / %%는 + -보다 먼저\n");
printf("2. 관계: 산술보다 나중\n");
printf("3. 논리: 관계보다 나중, &&는 ||보다 먼저\n");
printf("4. 대입: 거의 마지막\n");
printf("5. 확실하지 않으면 괄호 사용!\n");
return 0;
}

연산자 우선순위
$ gcc -Wall -Wextra -std=c11 -g precedence.c -o precedence
precedence.c: In function ‘main’:
precedence.c:32:70: warning: suggest parentheses around comparison in operand of ‘&’ [-Wparentheses]
32 | printf("5 & 3 == 1 의 실제 계산: 5 & (3 == 1) = 5 & 0 = %d\n", 5 & 3 == 1);
| ^
$ ./precedence
=== 산술 연산자 우선순위 ===
2 + 3 * 4 = 14 (곱셈 먼저)
(2 + 3) * 4 = 20 (괄호 먼저)
10 / 2 + 3 = 8
10 / (2 + 3) = 2
10 - 3 - 2 = 5 (좌에서 우로)
=== 관계 및 논리 연산자 ===
a=5, b=10, c=15
a < b && b < c: 1
a > b || c > b: 1
!(a > b): 1
=== 비트 연산자 주의! ===
5 & 3 = 1
3 == 1 = 0
5 & 3 == 1 의 실제 계산: 5 & (3 == 1) = 5 & 0 = 0
(5 & 3) == 1 이 의도한 것: 1
=== 대입 연산자 ===
x = a + b: x = 15
=== 복합 예제 ===
2 + 3 * 4 - 6 / 2 = 11
((2 + 3) * 4 - 6) / 2 = 7
=== 괄호 사용 권장 ===
((a + b) * c) / ((a - b) + c) = 22
=== 실용적 조언 ===
1. 산술: * / %는 + -보다 먼저
2. 관계: 산술보다 나중
3. 논리: 관계보다 나중, &&는 ||보다 먼저
4. 대입: 거의 마지막
5. 확실하지 않으면 괄호 사용!
- 17번째 줄
10 - 3 - 2: 결합 방향이 “왼쪽 → 오른쪽”이라(10 - 3) - 2 = 5입니다. - 24번째 줄
!(a > b):!는 순위 2라서>보다 먼저 붙습니다. 괄호가 없으면!a > b, 즉(!a) > b가 되어 전혀 다른 뜻이 됩니다. 그래서 괄호를 쳤습니다. - 32번째 줄이 함정입니다. “
5 & 3이 1 과 같으냐”를 물으려고5 & 3 == 1이라고 썼습니다.5 & 3은 1(101 & 011 = 001)이니 결과가 1(참)이어야 할 것 같지만, 0이 나왔습니다. 표를 보면==(7위)가&(8위)보다 먼저입니다. 그래서5 & (3 == 1)→5 & 0→ 0 으로 계산됐습니다.
이것이 앞에서 말한 예외입니다. 비트 연산자 & ^ | 는 비교 연산자보다 순위가 낮습니다. 직관과 반대라서 많은 사람이 틀립니다. C를 만든 데니스 리치 스스로도 “& 와 | 의 우선순위는 잘못 정했다”고 회고한 적이 있습니다. C 초창기에는 && 와 || 가 없어서 & 와 | 가 그 역할까지 했고, 그 흔적이 남은 것입니다.
GCC의 경고 “& 의 피연산자 자리에 있는 비교 둘레에 괄호를 쳐라”가 바로 이 함정을 가리킵니다. 경고를 없애는 방법은 괄호로 의도를 분명히 하는 것입니다. 33번째 줄 (5 & 3) == 1 처럼요.
우선순위를 외우지 않아도 되는 이유
괄호를 치면 됩니다. 괄호는 순위 1이라 무엇보다 먼저 계산되고, 실행 속도에 아무 영향이 없습니다(괄호는 컴파일할 때만 쓰이고 기계어에는 흔적도 남지 않습니다).
if (a & b == c) // 나쁨: (a & (b == c)) 로 계산됨
if ((a & b) == c) // 좋음: 의도가 보임
x = a + b * c; // 괜찮음: 곱셈이 먼저라는 건 누구나 앎
x = (a << 2) + 1; // 좋음: 시프트와 덧셈 순서는 헷갈리니 괄호
기준은 이렇습니다. 수학 시간에 배운 곱셈 먼저, 덧셈 나중 정도만 괄호를 생략하고, 나머지는 헷갈리면 괄호를 친다. 이 강좌의 코드도 그렇게 씁니다.
9. 조건문 — if, else if, else
드디어 프로그램에 갈림길을 만들 차례입니다. 지금까지의 모든 프로그램은 1주차 6절에서 본 대로 “위에서 아래로 한 줄씩” 실행됐습니다. 조건문은 이 흐름을 바꿉니다. 조건에 따라 어떤 줄은 실행하고 어떤 줄은 건너뜁니다.
기본 모양 세 가지
// ① if: 조건이 참일 때만 실행
if (조건) {
// 조건이 참이면 실행
}
// ② if-else: 둘 중 하나는 반드시 실행
if (조건) {
// 조건이 참이면 실행
} else {
// 조건이 거짓이면 실행
}
// ③ if-else if-else: 여러 갈래 중 하나
if (조건1) {
// 조건1이 참이면
} else if (조건2) {
// 조건1은 거짓, 조건2가 참이면
} else {
// 위의 조건이 모두 거짓이면
}
모양의 각 부분을 봅시다.
if: 영어 “만약”입니다. 키워드이므로 변수 이름으로 쓸 수 없습니다.( ): 조건을 반드시 괄호로 감싸야 합니다.if x > 0처럼 쓰면 문법 오류입니다. 괄호 안에는 4~5절의 비교, 논리 연산이 들어가지만, 5절에서 봤듯 어떤 식이든 들어갈 수 있고 0이면 거짓, 아니면 참입니다.{ }: 조건이 참일 때 실행할 문장들의 묶음입니다. 이것을 블록(block) 이라고 합니다. 1주차의main몸통을 감싼{ }도 블록이었습니다.else: “그렇지 않으면”. 반드시 앞에if가 있어야 합니다.else if: 사실 새 문법이 아니라else뒤에 또if문을 붙인 것입니다. 원하는 만큼 이어 붙일 수 있습니다.
if 줄 끝에는 세미콜론을 붙이지 않습니다. if (x > 0); 라고 쓰면 “x 가 0 보다 크면 아무것도 하지 않는다(빈 문장 ;)”로 끝나 버리고, 뒤의 블록은 조건과 상관없이 항상 실행됩니다. 11절의 for 에서 똑같은 함정을 실험으로 봅니다.
예제: if_examples.c
이번 주 예제 중 처음으로 키보드 입력을 받습니다. 2주차 7절의 scanf 입니다.
/*
* if_examples.c - if, else if, else 조건문 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
int score;
printf("점수를 입력하세요 (0-100): ");
if (scanf("%d", &score) != 1) {
printf("잘못된 입력입니다.\n");
return 1;
}
// 단순 if
printf("\n=== 단순 if ===\n");
if (score >= 90) {
printf("우수합니다!\n");
}
if (score < 60) {
printf("분발이 필요합니다.\n");
}
// if-else
printf("\n=== if-else ===\n");
if (score >= 60) {
printf("합격입니다.\n");
} else {
printf("불합격입니다.\n");
}
// if-else if-else: 학점 계산
printf("\n=== if-else if-else: 학점 계산 ===\n");
char grade;
if (score >= 90) {
grade = 'A';
} else if (score >= 80) {
grade = 'B';
} else if (score >= 70) {
grade = 'C';
} else if (score >= 60) {
grade = 'D';
} else {
grade = 'F';
}
printf("점수: %d, 학점: %c\n", score, grade);
// 추가 메시지
if (grade == 'A') {
printf("장학금 대상입니다!\n");
} else if (grade == 'F') {
printf("재수강이 필요합니다.\n");
}
// 점수 범위 상세 분류
printf("\n=== 상세 등급 ===\n");
if (score >= 97) {
printf("A+\n");
} else if (score >= 93) {
printf("A\n");
} else if (score >= 90) {
printf("A-\n");
} else if (score >= 87) {
printf("B+\n");
} else if (score >= 83) {
printf("B\n");
} else if (score >= 80) {
printf("B-\n");
} else if (score >= 77) {
printf("C+\n");
} else if (score >= 73) {
printf("C\n");
} else if (score >= 70) {
printf("C-\n");
} else if (score >= 60) {
printf("D\n");
} else {
printf("F\n");
}
return 0;
}
컴파일하고 몇 가지 점수로 실행해 봅시다. 프롬프트가 나오면 점수를 치고 Enter 를 누르세요.
$ gcc -Wall -Wextra -std=c11 -g if_examples.c -o if_examples
$ ./if_examples
점수를 입력하세요 (0-100): 85
=== 단순 if ===
=== if-else ===
합격입니다.
=== if-else if-else: 학점 계산 ===
점수: 85, 학점: B
=== 상세 등급 ===
B
$ ./if_examples
점수를 입력하세요 (0-100): 59
=== 단순 if ===
분발이 필요합니다.
=== if-else ===
불합격입니다.
=== if-else if-else: 학점 계산 ===
점수: 59, 학점: F
재수강이 필요합니다.
=== 상세 등급 ===
F
$ ./if_examples
점수를 입력하세요 (0-100): abc
잘못된 입력입니다.
$ echo $?
1
세 번 실행한 결과를 코드와 짝지어 봅시다.
- 12~15번째 줄 (입력 검사): 2주차에서 배운 대로
scanf는 성공적으로 읽은 값의 개수를 돌려줍니다. 숫자 하나를 읽었으면 1입니다.abc를 넣으면 숫자로 읽을 수 없으니 0이 되고,!= 1이 참이 되어 “잘못된 입력입니다”를 찍고return 1로 끝납니다.echo $?로 보면 종료 코드가 1입니다. 1주차에서 “0이 아닌 종료 코드는 실패”라고 했던 약속을 우리 프로그램도 지키는 것입니다. - 단순 if (19~24번째 줄): 85점일 때는 두
if모두 조건이 거짓이라 아무것도 찍히지 않았습니다. 제목=== 단순 if ===아래가 비어 있는 이유입니다.if만 있고else가 없으면 “조건이 거짓일 때 할 일”이 없습니다. - if-else (28~32번째 줄): 둘 중 정확히 하나가 반드시 실행됩니다.
- if-else if-else (38~48번째 줄): 위에서부터 조건을 하나씩 검사하다가, 처음으로 참인 것 하나만 실행하고 나머지는 건너뜁니다. 85점은
score >= 90이 거짓,score >= 80이 참이므로 B가 되고, 그 아래score >= 70,score >= 60은 85점에서도 참이지만 검사조차 하지 않습니다.
char grade; 와 grade = 'A'; 는 2주차의 문자형입니다. 문자 하나는 작은따옴표로 감쌉니다(1주차 5절). 50번째 줄의 %c 는 문자 하나를 찍는 서식 지정자입니다.
실험: else if 의 순서를 바꾸면?
“처음 참인 것 하나만 실행”이라는 규칙 때문에, 조건의 순서가 결과를 바꿉니다. 학점 조건을 낮은 점수부터 쓰면 어떻게 될까요? order.c:
#include <stdio.h>
int main(void) {
int score = 95;
char grade;
if (score >= 60) {
grade = 'D';
} else if (score >= 70) {
grade = 'C';
} else if (score >= 80) {
grade = 'B';
} else if (score >= 90) {
grade = 'A';
} else {
grade = 'F';
}
printf("점수 %d 의 학점: %c\n", score, grade);
return 0;
}
$ gcc -Wall -Wextra -std=c11 order.c -o order && ./order
점수 95 의 학점: D
95점이 D를 받았습니다. 컴파일러는 아무 경고도 하지 않습니다. 문법적으로는 완벽한 코드이기 때문입니다. 95는 첫 조건 score >= 60 에서 이미 참이 되어 D로 정해지고, 뒤의 조건들은 영영 검사되지 않습니다. 사실 이 코드에서 C, B, A 에는 어떤 점수도 도달할 수 없습니다.
범위로 나누는 else if 사슬은 가장 좁은(까다로운) 조건부터 써야 합니다. 원래 예제처럼 90, 80, 70, 60 순서로요.
중괄호를 생략하면 생기는 일
블록 안의 문장이 하나뿐이면 { } 를 생략할 수 있습니다.
if (x > 0)
printf("양수입니다\n");
문법적으로는 맞습니다. 하지만 이 강좌는 한 줄이라도 항상 중괄호를 씁니다. 그 이유를 두 실험으로 보여 드리겠습니다.
실험 1: 들여쓰기에 속다
brace.c:
#include <stdio.h>
int main(void) {
int x = -3;
if (x > 0)
printf("양수입니다\n");
printf("x 는 %d\n", x);
return 0;
}
들여쓰기만 보면 두 printf 모두 if 에 속한 것 같습니다. x 가 -3이니 아무것도 찍히지 않아야 할 것 같지만:
$ gcc -Wall -Wextra -std=c11 brace.c -o brace
brace.c: In function ‘main’:
brace.c:5:5: warning: this ‘if’ clause does not guard... [-Wmisleading-indentation]
5 | if (x > 0)
| ^~
brace.c:7:9: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’
7 | printf("x 는 %d\n", x);
| ^~~~~~
$ ./brace
x 는 -3
중괄호가 없으면 if 는 바로 다음 문장 하나만 조건에 따라 실행합니다. 두 번째 printf 는 들여쓰기와 상관없이 if 밖에 있습니다. 1주차 11절에서 “컴파일러는 들여쓰기를 신경 쓰지 않는다”고 했던 것이 이런 뜻입니다. 사람은 들여쓰기를 보고, 컴파일러는 중괄호를 봅니다.
GCC의 경고가 아주 친절합니다. “이 if 는 …를 지키지 않는다(does not guard)”, “…이 문장을. 그런데 이 문장은 마치 if 가 지키는 것처럼 오해를 부르게 들여쓰여 있다(misleadingly indented).” 실제로 2014년 애플의 보안 코드에 이것과 똑같은 모양의 버그(goto fail; 이 두 번 쓰인 줄)가 들어가서 인터넷 암호화 검사가 통째로 무력화된 사건이 있었습니다.
실험 2: else 는 누구의 짝일까?
dangling.c:
#include <stdio.h>
int main(void) {
int a = 0, b = 1;
if (a == 1)
if (b == 1)
printf("a 도 1, b 도 1\n");
else
printf("a 가 1 이 아닙니다\n");
printf("끝\n");
return 0;
}
들여쓰기는 else 가 바깥 if (a == 1) 의 짝인 것처럼 되어 있습니다. a 가 0이니 “a 가 1 이 아닙니다”가 찍힐까요?
$ gcc -Wall -Wextra -std=c11 dangling.c -o dangling
dangling.c: In function ‘main’:
dangling.c:5:8: warning: suggest explicit braces to avoid ambiguous ‘else’ [-Wdangling-else]
5 | if (a == 1)
| ^
$ ./dangling
끝
찍히지 않았습니다. C의 규칙은 else 는 가장 가까운(짝이 없는) if 와 짝을 이룬다입니다. 그래서 이 else 는 안쪽 if (b == 1) 의 짝이고, 바깥 if (a == 1) 이 거짓이니 안쪽 전체를 건너뜁니다. 이 문제를 매달린 else(dangling else) 라고 부르고, GCC도 “모호한 else 를 피하도록 중괄호를 명시하라”고 경고합니다.
두 실험 모두 중괄호만 쳤으면 생기지 않았을 문제입니다. 그래서 규칙은 하나입니다. if, else, for, while 뒤에는 한 줄이라도 항상 { }.
중첩 조건문
블록 안에는 어떤 문장이든 들어갈 수 있으니, if 안에 또 if 를 넣을 수 있습니다. 이것을 중첩(nested) 이라고 합니다. 큰 갈래를 먼저 나누고, 그 안에서 작은 갈래를 나눌 때 씁니다. 나이와 학생 여부로 영화 관람료를 정하는 nested_if.c:
/*
* nested_if.c - 중첩 조건문 예제
* 3주차: 연산자와 제어문
*
* 나이와 학생 여부로 영화 관람료를 정합니다. (요금은 예시 값입니다)
* 바깥 if 가 나이 구간을 크게 나누고, 안쪽 if 가 그 안에서 다시
* 세부 요금을 고릅니다.
*/
#include <stdio.h>
int main(void) {
int age;
char student;
printf("나이를 입력하세요: ");
if (scanf("%d", &age) != 1) {
printf("잘못된 입력입니다.\n");
return 1;
}
printf("학생입니까? (y/n): ");
if (scanf(" %c", &student) != 1) { // 앞의 공백: 남아 있는 줄바꿈 건너뛰기
printf("입력이 없습니다.\n");
return 1;
}
printf("\n===== 요금 안내 =====\n");
if (age < 0) {
printf("유효하지 않은 나이입니다.\n");
} else if (age < 19) {
printf("19세 미만 구간입니다.\n");
if (age < 4) {
printf("유아는 무료입니다: 0원\n");
} else if (age < 14) {
printf("어린이 요금: 7000원\n");
} else {
printf("청소년 요금: 9000원\n");
}
} else {
printf("19세 이상 구간입니다.\n");
if (age >= 65) {
printf("경로 우대 요금: 7000원\n");
} else if (student == 'y' || student == 'Y') {
printf("대학생 할인 요금: 10000원\n");
} else {
printf("일반 요금: 14000원\n");
}
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g nested_if.c -o nested_if
$ ./nested_if
나이를 입력하세요: 25
학생입니까? (y/n): n
===== 요금 안내 =====
19세 이상 구간입니다.
일반 요금: 14000원
$ ./nested_if
나이를 입력하세요: 22
학생입니까? (y/n): y
===== 요금 안내 =====
19세 이상 구간입니다.
대학생 할인 요금: 10000원
$ ./nested_if
나이를 입력하세요: 10
학생입니까? (y/n): n
===== 요금 안내 =====
19세 미만 구간입니다.
어린이 요금: 7000원
$ ./nested_if
나이를 입력하세요: 70
학생입니까? (y/n): y
===== 요금 안내 =====
19세 이상 구간입니다.
경로 우대 요금: 7000원
결정 과정을 나무로 그려 보면 코드 구조와 똑같습니다.
나이 < 0 ? ── 예 → 유효하지 않음
│아니오
나이 < 19 ? ── 예 → 나이 < 4 ? ── 예 → 무료
│ │아니오
│ 나이 < 14 ? ── 예 → 어린이 7000
│ │아니오
│ └──────────→ 청소년 9000
│아니오
나이 >= 65 ? ── 예 → 경로 7000
│아니오
학생 == 'y' 또는 'Y' ? ── 예 → 대학생 10000
│아니오
└──────────→ 일반 14000
마지막 실행을 보세요. 70세이면서 학생이라고 답했는데 경로 우대가 나왔습니다. 안쪽 if 에서 age >= 65 를 학생 여부보다 먼저 검사하기 때문입니다. 앞의 “순서 실험”과 같은 원리이고, 이번에는 의도한 동작입니다. 학생 할인이 경로 우대보다 비싸니, 더 싼 쪽이 먼저 적용되게 순서를 정한 것입니다. 조건의 순서가 곧 규칙의 우선순위입니다.
23번째 줄의 scanf(" %c", &student) 앞의 공백을 눈여겨보세요. 2주차 7절 “scanf의 버퍼 문제”에서 본 그 공백입니다. 나이를 치고 누른 Enter 가 입력 버퍼에 \n 으로 남아 있어서, 공백 없이 %c 로 읽으면 y/n 대신 그 \n 을 읽어 버립니다. 서식 문자열의 공백은 “공백 문자(스페이스, 탭, 줄바꿈)를 모두 건너뛰어라”라는 뜻입니다.
중첩은 세 단계까지만. 중첩이 깊어지면 블록이 오른쪽으로 계속 밀려나 읽기 어려워집니다. 세 단계를 넘어가면 조건을
&&로 합치거나, 5주차에서 배울 함수로 쪼개는 것을 고민하세요.
조건문 스타일 정리
// 1. 한 줄이라도 중괄호 (위 두 실험의 교훈)
if (x > 0) {
printf("양수");
}
// 2. else 는 닫는 } 와 같은 줄에
if (x > 0) {
// ...
} else {
// ...
}
// 3. 범위 조건은 좁은 쪽부터 (순서 실험의 교훈)
if (score >= 90) { ... }
else if (score >= 80) { ... }
// 4. 조건 안에서는 값을 바꾸지 않는다 (= 와 ==, 단락 평가의 교훈)
if (a == 5) { ... } // 좋음
if (a = 5) { ... } // 나쁨
10. 조건문 — switch-case
값에 따라 여러 갈래로
“1이면 월요일, 2면 화요일, … 7이면 일요일”처럼 하나의 값을 여러 상수와 비교할 때 if-else if 를 일곱 번 이어 쓰면 길고 지루합니다. 이럴 때 switch 를 씁니다. 철도의 선로 전환기(switch)가 열차를 여러 선로 중 하나로 보내듯, 값에 따라 여러 갈래 중 하나로 보냅니다.
switch (정수식) {
case 상수1:
// 정수식 == 상수1 일 때
break;
case 상수2:
// 정수식 == 상수2 일 때
break;
default:
// 어느 case 와도 같지 않을 때
break;
}
부분마다 규칙이 있습니다.
switch ( ): 괄호 안의 식은 정수(또는 문자. 2주차에서 봤듯 문자도 속은 정수입니다)여야 합니다.case 상수:: “식의 값이 이 상수와 같으면 여기서부터 실행하라”는 표지판입니다. 끝에 세미콜론이 아니라 콜론(:) 입니다. 상수 자리에는 숫자나'A'같은 문자처럼 컴파일할 때 값이 정해진 것만 올 수 있습니다.break;: “switch를 빠져나가라.” 이것이 없으면 무슨 일이 생기는지가 이 절의 핵심입니다.default:: 어느case와도 맞지 않을 때 실행됩니다.if의 마지막else에 해당합니다. 생략할 수 있지만, 예상 밖의 값을 처리하려면 두는 것이 좋습니다.
예제: switch_example.c
/*
* switch_example.c - switch-case 조건문 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
int day;
printf("요일 번호를 입력하세요 (1-7): ");
if (scanf("%d", &day) != 1) {
printf("잘못된 입력입니다.\n");
return 1;
}
printf("\n");
switch (day) {
case 1:
printf("월요일입니다.\n");
printf("한 주의 시작! 화이팅!\n");
break;
case 2:
printf("화요일입니다.\n");
break;
case 3:
printf("수요일입니다.\n");
printf("주중 중간이에요.\n");
break;
case 4:
printf("목요일입니다.\n");
break;
case 5:
printf("금요일입니다.\n");
printf("불금! 주말이 다가옵니다!\n");
break;
case 6:
printf("토요일입니다.\n");
printf("주말입니다! 푹 쉬세요.\n");
break;
case 7:
printf("일요일입니다.\n");
printf("주말입니다! 푹 쉬세요.\n");
break;
default:
printf("잘못된 입력입니다. 1-7 사이의 숫자를 입력하세요.\n");
break;
}
// 문자로도 switch 가능
printf("\n=== 문자 switch 예제 ===\n");
char grade;
printf("학점을 입력하세요 (A/B/C/D/F): ");
if (scanf(" %c", &grade) != 1) {
printf("입력이 없습니다.\n");
return 1;
}
switch (grade) {
case 'A':
case 'a':
printf("우수합니다! (90-100점)\n");
break;
case 'B':
case 'b':
printf("좋습니다! (80-89점)\n");
break;
case 'C':
case 'c':
printf("보통입니다. (70-79점)\n");
break;
case 'D':
case 'd':
printf("노력이 필요합니다. (60-69점)\n");
break;
case 'F':
case 'f':
printf("재수강이 필요합니다. (60점 미만)\n");
break;
default:
printf("올바른 학점을 입력하세요.\n");
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g switch_example.c -o switch_example
$ ./switch_example
요일 번호를 입력하세요 (1-7): 5
금요일입니다.
불금! 주말이 다가옵니다!
=== 문자 switch 예제 ===
학점을 입력하세요 (A/B/C/D/F): b
좋습니다! (80-89점)
$ ./switch_example
요일 번호를 입력하세요 (1-7): 9
잘못된 입력입니다. 1-7 사이의 숫자를 입력하세요.
=== 문자 switch 예제 ===
학점을 입력하세요 (A/B/C/D/F): z
올바른 학점을 입력하세요.
- 5를 넣었을 때:
case 5:표지판으로 곧장 가서 두printf를 실행하고,break를 만나switch밖으로 나갑니다.case 1:부터 차례로 비교하는 게 아니라 해당 표지판으로 바로 점프한다고 생각하면 됩니다. 실제로 컴파일러는case가 많으면 표(jump table)를 만들어 한 번에 찾아갑니다. - 9를 넣었을 때: 맞는
case가 없으니default:로 갑니다. - 61~62번째 줄
case 'A':바로 아래case 'a':: 두 표지판이 같은 코드를 가리킵니다. 대문자든 소문자든 같은 처리를 하도록 표지판을 두 개 세운 것입니다.'A'에break가 없어서 가능한 일인데, 이것이 바로 다음에 볼 fall-through 입니다.
b 를 소문자로 넣었는데 “좋습니다”가 나온 이유가 이것입니다.
break 를 빠뜨리면: fall-through
case 는 “여기서부터 실행하라”는 시작 표지판일 뿐, 끝을 알리지 않습니다. break 를 만나기 전까지는 아래 case 의 코드도 계속 실행합니다. 이것을 fall-through(아래로 떨어짐)라고 합니다. 일부러 빼먹어 보겠습니다. fall.c:
#include <stdio.h>
int main(void) {
int n = 1;
switch (n) {
case 1:
printf("하나\n");
case 2:
printf("둘\n");
break;
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 fall.c -o fall
fall.c: In function ‘main’:
fall.c:7:13: warning: this statement may fall through [-Wimplicit-fallthrough=]
7 | printf("하나\n");
| ^~~~~~~~~~~~~~~~
fall.c:8:9: note: here
8 | case 2:
| ^~~~
$ ./fall
하나
둘
n 은 1인데 “둘”까지 찍혔습니다. case 1: 로 들어가 “하나”를 찍고, break 가 없으니 case 2: 표지판을 그냥 지나쳐 “둘”을 찍고, 거기서 break 를 만나 나갔습니다. GCC는 -Wextra 로 켜지는 경고로 “이 문장은 아래로 떨어질 수 있다(may fall through)”고 알려 줍니다.
이 경고가 C 역사상 수많은 버그를 막아 줬습니다. break 를 깜빡하는 것은 switch 에서 가장 흔한 실수이기 때문입니다.
fall-through 를 일부러 쓰는 경우
그런데 fall-through 가 쓸모 있을 때도 있습니다. switch_fallthrough.c 는 세 가지 쓰임을 보여 줍니다.
/*
* switch_fallthrough.c - switch fall-through 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
int month;
printf("월을 입력하세요 (1-12): ");
if (scanf("%d", &month) != 1) {
printf("잘못된 입력입니다.\n");
return 1;
}
int days;
// fall-through 활용: 같은 처리를 하는 case 그룹화
switch (month) {
case 1: case 3: case 5: case 7:
case 8: case 10: case 12:
days = 31;
break;
case 4: case 6: case 9: case 11:
days = 30;
break;
case 2:
days = 28; // 윤년은 고려하지 않음
printf("(윤년의 경우 29일)\n");
break;
default:
days = -1; // 잘못된 월
break;
}
if (days > 0) {
printf("%d월은 %d일까지 있습니다.\n", month, days);
} else {
printf("잘못된 월입니다.\n");
}
// break 없이 fall-through 예시 (누적 보너스)
printf("\n=== Fall-through 예시: 누적 보너스 ===\n");
int level;
printf("회원 레벨을 입력하세요 (1-5): ");
if (scanf("%d", &level) != 1) {
printf("잘못된 입력입니다.\n");
return 1;
}
printf("레벨 %d 보너스: ", level);
switch (level) {
case 5:
printf("VIP 라운지 ");
// fall-through
case 4:
printf("우선 탑승 ");
// fall-through
case 3:
printf("무료 수하물 ");
// fall-through
case 2:
printf("마일리지 적립 ");
// fall-through
case 1:
printf("기본 서비스");
break;
default:
printf("(해당 없음)");
}
printf("\n");
// 계절 판단
printf("\n=== 계절 판단 ===\n");
printf("월을 입력하세요 (1-12): ");
if (scanf("%d", &month) != 1) {
printf("잘못된 입력입니다.\n");
return 1;
}
printf("%d월은 ", month);
switch (month) {
case 3: case 4: case 5:
printf("봄");
break;
case 6: case 7: case 8:
printf("여름");
break;
case 9: case 10: case 11:
printf("가을");
break;
case 12: case 1: case 2:
printf("겨울");
break;
default:
printf("잘못된 월");
}
printf("입니다.\n");
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g switch_fallthrough.c -o switch_fallthrough
$ ./switch_fallthrough
월을 입력하세요 (1-12): 2
(윤년의 경우 29일)
2월은 28일까지 있습니다.
=== Fall-through 예시: 누적 보너스 ===
회원 레벨을 입력하세요 (1-5): 3
레벨 3 보너스: 무료 수하물 마일리지 적립 기본 서비스
=== 계절 판단 ===
월을 입력하세요 (1-12): 11
11월은 가을입니다.
$ ./switch_fallthrough
월을 입력하세요 (1-12): 7
7월은 31일까지 있습니다.
=== Fall-through 예시: 누적 보너스 ===
회원 레벨을 입력하세요 (1-5): 5
레벨 5 보너스: VIP 라운지 우선 탑승 무료 수하물 마일리지 적립 기본 서비스
=== 계절 판단 ===
월을 입력하세요 (1-12): 1
1월은 겨울입니다.
- 여러 값에 같은 처리 (21~28번째 줄):
case 1: case 3: case 5: ...처럼 표지판을 여러 개 세우고 코드는 한 번만 씁니다. 표지판 사이에 코드가 없으니 떨어지는 게 자연스럽고, GCC도 경고하지 않습니다. - 누적 혜택 (57~75번째 줄): 레벨 5는 5, 4, 3, 2, 1의 혜택을 모두 받고, 레벨 3은 3, 2, 1의 혜택을 받습니다. 높은 레벨의 표지판에서 들어가 아래로 떨어지며 혜택을 하나씩 찍으니, fall-through 가 딱 맞는 구조입니다. 그런데 이 파일을 컴파일해도 앞의
fall.c같은 경고가 나오지 않았습니다. 각case끝의// fall-through주석 덕분입니다. GCC는 이런 주석을 보면 “일부러 떨어뜨리는 것”으로 인정하고 경고를 끕니다. 주석이 컴파일러의 동작을 바꾸는 드문 예입니다. - 범위를 표지판으로 (87~102번째 줄): 3, 4, 5월은 봄.
switch는 범위 비교(>=)를 못 하니, 범위가 작을 때는 표지판을 나열하는 방법을 씁니다.
default 로 가는 경우도 확인해 봅시다. 13월을 넣으면 “잘못된 월입니다”가 나오고, 레벨 입력에서 숫자가 아닌 것을 넣으면 프로그램이 입력 오류를 알리고 끝납니다.
$ ./switch_fallthrough
월을 입력하세요 (1-12): 13
잘못된 월입니다.
=== Fall-through 예시: 누적 보너스 ===
회원 레벨을 입력하세요 (1-5): 잘못된 입력입니다.
레벨을 묻는 곳에서는 아무것도 치지 않고 Ctrl + D 를 눌렀습니다. 1주차 2절에서 “입력 끝 알리기”라고 소개한 키입니다. 터미널은 Ctrl + D 를 화면에 찍지 않으므로, 오류 문구가 프롬프트 바로 뒤에 붙어 나옵니다. 입력이 없으면 scanf 가 1이 아닌 값을 돌려주고, 50~53번째 줄의 검사가 그것을 잡아서 프로그램을 끝냅니다. 이 검사가 없으면 level 에는 아무 값도 들어가지 않은 채(2주차 1절의 “초기화하지 않은 변수”) switch 로 들어가 엉뚱한 혜택을 출력할 수 있습니다.
실험: switch 에 쓸 수 없는 것
double d = 1.0;
switch (d) {
swd.c:5:13: error: switch quantity not an integer
5 | switch (d) {
| ^
“switch 에 넣은 값이 정수가 아니다.” 실수는 쓸 수 없습니다. 4절에서 봤듯 실수는 == 로 비교하는 것 자체가 위험하니, 애초에 막아 둔 것입니다.
int n = 2, limit = 2;
switch (n) {
case limit:
swv.c:6:9: error: case label does not reduce to an integer constant
6 | case limit:
| ^~~~
“case 표지판이 정수 상수가 아니다.” limit 은 변수라서 쓸 수 없습니다. 표지판의 값은 컴파일할 때 정해져 있어야 합니다. 그래야 컴파일러가 미리 점프 표를 만들 수 있기 때문입니다.
switch 와 if-else, 언제 무엇을 쓸까
| 상황 | 알맞은 것 | 예 |
|---|---|---|
| 한 변수를 여러 상수와 비교 | switch |
메뉴 번호, 요일, 명령 문자 |
범위 비교 (>=, <) |
if-else if |
점수로 학점 매기기 |
| 여러 변수를 함께 판단 | if-else if |
나이와 학생 여부 |
| 실수 비교, 문자열 비교 | if-else if |
switch 로는 불가능 |
16절의 메뉴 프로그램은 모두 “번호를 입력받아 switch” 구조입니다. 메뉴야말로 switch 가 가장 빛나는 곳입니다.
11. 반복문 — for
1주차 12절의 연습 과제에서 별 삼각형을 printf 다섯 줄로 그렸습니다. 100줄짜리 삼각형이라면 printf 를 100번 쓸까요? 1부터 100까지 더하려면 + 를 99번 쓸까요? 반복문은 “같은 일을 N번”, 또는 “조건이 참인 동안” 되풀이하게 해 줍니다. 컴퓨터가 사람보다 압도적으로 잘하는 일이 바로 지치지 않고 되풀이하는 것입니다.
C의 반복문은 for, while, do-while 세 가지입니다. 먼저 가장 많이 쓰는 for 부터 봅시다.
for 의 생김새
for (초기화; 조건; 증감) {
// 반복할 코드 (몸통)
}
괄호 안이 세미콜론 두 개로 나뉜 세 칸입니다. 이 세 칸이 언제 실행되는지가 for 의 전부입니다.
| 칸 | 언제 실행되나 | 보통 하는 일 |
|---|---|---|
| ① 초기화 | 맨 처음 딱 한 번 | 반복 횟수를 셀 변수(보통 i) 만들기 |
| ② 조건 | 몸통을 실행하기 전마다 | 참이면 몸통 실행, 거짓이면 반복 끝 |
| ③ 증감 | 몸통을 실행한 뒤마다 | i++ 로 한 칸 전진 |
for (int i = 1; i <= 3; i++) 가 실제로 어떤 순서로 도는지, 한 단계씩 적어 보면 이렇습니다.
| 단계 | 하는 일 | i | 결과 |
|---|---|---|---|
| 1 | ① int i = 1 |
1 | |
| 2 | ② i <= 3 ? |
1 | 참 → 몸통 실행 |
| 3 | 몸통 | 1 | (1회째) |
| 4 | ③ i++ |
2 | |
| 5 | ② i <= 3 ? |
2 | 참 → 몸통 실행 |
| 6 | 몸통 | 2 | (2회째) |
| 7 | ③ i++ |
3 | |
| 8 | ② i <= 3 ? |
3 | 참 → 몸통 실행 |
| 9 | 몸통 | 3 | (3회째) |
| 10 | ③ i++ |
4 | |
| 11 | ② i <= 3 ? |
4 | 거짓 → 반복 끝 |
두 가지를 기억하세요. 조건은 몸통보다 먼저 검사됩니다. 그래서 처음부터 조건이 거짓이면 몸통은 한 번도 실행되지 않습니다. 그리고 반복이 끝났을 때 i 는 3이 아니라 4 입니다. 조건이 거짓이 되어야 멈추기 때문입니다.
예제: for_examples.c
/*
* for_examples.c - for 반복문 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
// 기본 for 문: 1부터 10까지 출력
printf("=== 1부터 10까지 ===\n");
for (int i = 1; i <= 10; i++) {
printf("%d ", i);
}
printf("\n");
// 역순 반복
printf("\n=== 10부터 1까지 (역순) ===\n");
for (int i = 10; i >= 1; i--) {
printf("%d ", i);
}
printf("\n");
// 2씩 증가
printf("\n=== 짝수만 출력 (2씩 증가) ===\n");
for (int i = 2; i <= 20; i += 2) {
printf("%d ", i);
}
printf("\n");
// 홀수만 출력
printf("\n=== 홀수만 출력 ===\n");
for (int i = 1; i <= 20; i += 2) {
printf("%d ", i);
}
printf("\n");
// 합계 계산
printf("\n=== 1부터 100까지 합 ===\n");
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
printf("합계: %d\n", sum); // 5050
// 구구단 (2단)
printf("\n=== 구구단 2단 ===\n");
for (int i = 1; i <= 9; i++) {
printf("2 x %d = %2d\n", i, 2 * i);
}
// 배열과 for
printf("\n=== 배열과 for ===\n");
int numbers[] = {10, 20, 30, 40, 50};
int size = sizeof(numbers) / sizeof(numbers[0]);
for (int i = 0; i < size; i++) {
printf("numbers[%d] = %d\n", i, numbers[i]);
}
// 여러 변수 사용
printf("\n=== 여러 변수 사용 ===\n");
for (int i = 0, j = 10; i < j; i++, j--) {
printf("i=%d, j=%d\n", i, j);
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g for_examples.c -o for_examples && ./for_examples
=== 1부터 10까지 ===
1 2 3 4 5 6 7 8 9 10
=== 10부터 1까지 (역순) ===
10 9 8 7 6 5 4 3 2 1
=== 짝수만 출력 (2씩 증가) ===
2 4 6 8 10 12 14 16 18 20
=== 홀수만 출력 ===
1 3 5 7 9 11 13 15 17 19
=== 1부터 100까지 합 ===
합계: 5050
=== 구구단 2단 ===
2 x 1 = 2
2 x 2 = 4
2 x 3 = 6
2 x 4 = 8
2 x 5 = 10
2 x 6 = 12
2 x 7 = 14
2 x 8 = 16
2 x 9 = 18
=== 배열과 for ===
numbers[0] = 10
numbers[1] = 20
numbers[2] = 30
numbers[3] = 40
numbers[4] = 50
=== 여러 변수 사용 ===
i=0, j=10
i=1, j=9
i=2, j=8
i=3, j=7
i=4, j=6
각 부분을 세 칸의 관점으로 읽어 봅시다.
| 줄 | 초기화 | 조건 | 증감 | 결과 |
|---|---|---|---|---|
| 11 | i = 1 |
i <= 10 |
i++ |
1부터 10까지 |
| 18 | i = 10 |
i >= 1 |
i-- |
거꾸로 10부터 1까지 |
| 25 | i = 2 |
i <= 20 |
i += 2 |
2씩 건너뛰어 짝수 |
| 32 | i = 1 |
i <= 20 |
i += 2 |
1부터 2씩, 홀수 |
| 62 | i = 0, j = 10 |
i < j |
i++, j-- |
두 변수가 양쪽에서 좁혀 옴 |
세 칸의 내용만 바꾸면 방향도, 간격도, 변수 개수도 자유롭게 바꿀 수 있습니다.
- 합계(39~42번째 줄):
sum을 0으로 반복 전에 만들어 두고, 몸통에서sum += i로 하나씩 쌓습니다. “0에서 시작해 차곡차곡 더하기”는 반복문의 가장 기본적인 모양이라, 앞으로 셀 수 없이 쓰게 됩니다. 1부터 100까지의 합이 5050인 것은 가우스가 어릴 때 (1 + 100) × 50 으로 풀었다는 그 문제입니다. - 구구단 (48번째 줄):
%2d는 2주차 6절의 폭 지정입니다. 숫자를 2칸 너비에 오른쪽 정렬해서 찍으므로2 x 1 = 2처럼 한 자리 수 앞에 공백이 들어가 줄이 맞습니다. - 배열과 for (53~58번째 줄): 7절에서 미리 본 배열입니다. 배열의 칸 번호가 0부터 시작하기 때문에, 배열을 도는
for는i = 0; i < size모양이 됩니다. 4주차에 수없이 쓰게 될 모양이니 눈에 익혀 두세요. - 여러 변수 (62번째 줄): 8절 우선순위 표 맨 아래에 있던 쉼표 연산자가 여기서 쓰입니다.
i = 0, j = 10은 “두 가지를 차례로 하라”는 뜻입니다.
실험: for 에서 자주 하는 실수 세 가지
① 괄호 뒤에 세미콜론
semi.c:
#include <stdio.h>
int main(void) {
int i;
for (i = 0; i < 5; i++);
printf("i = %d\n", i);
return 0;
}
0부터 4까지 다섯 줄이 찍힐까요?
$ gcc -Wall -Wextra -std=c11 semi.c -o semi
semi.c: In function ‘main’:
semi.c:5:5: warning: this ‘for’ clause does not guard... [-Wmisleading-indentation]
5 | for (i = 0; i < 5; i++);
| ^~~
semi.c:6:9: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘for’
6 | printf("i = %d\n", i);
| ^~~~~~
$ ./semi
i = 5
한 줄만 찍혔고, 그것도 5입니다. for (...) 바로 뒤의 ; 가 “아무것도 안 하는 빈 문장”이라서, for 는 빈 문장을 다섯 번 반복하고 끝났습니다. 그다음 printf 가 반복과 상관없이 한 번 실행되고, 그때 i 는 앞의 추적 표에서 본 대로 조건을 깨뜨린 값 5입니다. 9절의 if 에서 말한 세미콜론 함정의 for 판입니다. GCC가 9절의 brace.c 와 똑같은 경고로 잡아 줍니다.
② 반복이 끝난 뒤 반복 변수 쓰기
scope.c:
#include <stdio.h>
int main(void) {
for (int i = 0; i < 3; i++) {
printf("%d ", i);
}
printf("\n마지막 i = %d\n", i);
return 0;
}
$ gcc -Wall -Wextra -std=c11 scope.c -o scope
scope.c: In function ‘main’:
scope.c:7:33: error: ‘i’ undeclared (first use in this function)
7 | printf("\n마지막 i = %d\n", i);
| ^
scope.c:7:33: note: each undeclared identifier is reported only once for each function it appears in
“i 가 선언되지 않았다(undeclared).” 분명 4번째 줄에서 만들었는데요. for 의 초기화 칸에서 만든 변수는 그 for 안에서만 살아 있습니다. 반복이 끝나면 사라집니다. 변수가 살아 있는 범위를 유효 범위(scope) 라고 하고, 5주차 함수에서 본격적으로 다룹니다. 반복이 끝난 뒤에도 값이 필요하면 ①의 semi.c 처럼 for 밖에서 미리 int i; 로 만들어 두면 됩니다.
for (int i = ...) 처럼 초기화 칸에서 변수를 만드는 것은 1주차 5절의 표에서 본 대로 C99부터 생긴 문법입니다. 그래서 -std=c89 로 컴파일하면 이 모양 자체가 오류가 됩니다.
③ unsigned 로 거꾸로 세기
uns.c: 3부터 0까지 거꾸로 세려고 합니다. 음수가 필요 없으니 unsigned int 를 썼습니다. 혹시 몰라 여섯 번 돌면 멈추는 안전장치(if (++count == 6) break;)를 넣어 두었습니다(break 는 14절).
#include <stdio.h>
int main(void) {
int count = 0;
for (unsigned int i = 3; i >= 0; i--) {
printf("%u ", i);
if (++count == 6) break;
}
printf("\n");
return 0;
}
$ gcc -Wall -Wextra -std=c11 uns.c -o uns
uns.c: In function ‘main’:
uns.c:5:32: warning: comparison of unsigned expression in ‘>= 0’ is always true [-Wtype-limits]
5 | for (unsigned int i = 3; i >= 0; i--) {
| ^~
$ ./uns
3 2 1 0 4294967295 4294967294
0 다음에 4294967295 가 나왔습니다. unsigned int 에는 음수가 없어서, 0에서 1을 빼면 1절의 오버플로처럼 가장 큰 값으로 넘어갑니다. 그러니 i >= 0 은 영원히 참이고, 안전장치가 없었다면 이 반복은 끝나지 않습니다. GCC가 -Wextra 로 “부호 없는 값이 0 이상인지 비교하는 것은 항상 참”이라고 정확히 알려 줍니다. 거꾸로 셀 때는 int 를 쓰세요.
중첩 for: 반복 안의 반복
for 몸통 안에 또 for 를 넣을 수 있습니다. 시계를 떠올리면 쉽습니다. 분침(안쪽 반복)이 한 바퀴 다 돌아야 시침(바깥 반복)이 한 칸 움직입니다.
/*
* nested_for.c - 중첩 for 반복문 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
// 구구단 전체 출력 (세로)
printf("=== 구구단 (세로) ===\n\n");
for (int dan = 2; dan <= 4; dan++) { // 예시로 2~4단만
printf("[ %d단 ]\n", dan);
for (int i = 1; i <= 9; i++) {
printf("%d x %d = %2d\n", dan, i, dan * i);
}
printf("\n");
}
// 구구단 가로 출력
printf("=== 구구단 (가로) ===\n\n");
for (int i = 1; i <= 9; i++) {
for (int dan = 2; dan <= 9; dan++) {
printf("%d x %d = %2d ", dan, i, dan * i);
}
printf("\n");
}
// 별 찍기: 직각삼각형
printf("\n=== 별 찍기 (직각삼각형) ===\n");
for (int i = 1; i <= 5; i++) {
for (int j = 0; j < i; j++) {
printf("*");
}
printf("\n");
}
// 별 찍기: 역삼각형
printf("\n=== 별 찍기 (역삼각형) ===\n");
for (int i = 5; i >= 1; i--) {
for (int j = 0; j < i; j++) {
printf("*");
}
printf("\n");
}
// 별 찍기: 우측 정렬 직각삼각형
printf("\n=== 별 찍기 (우측 정렬) ===\n");
int n = 5;
for (int i = 1; i <= n; i++) {
// 공백 출력
for (int j = 0; j < n - i; j++) {
printf(" ");
}
// 별 출력
for (int j = 0; j < i; j++) {
printf("*");
}
printf("\n");
}
// 숫자 피라미드
printf("\n=== 숫자 피라미드 ===\n");
for (int i = 1; i <= 5; i++) {
for (int j = 1; j <= i; j++) {
printf("%d ", j);
}
printf("\n");
}
// 2차원 좌표 출력
printf("\n=== 2차원 좌표 ===\n");
for (int y = 0; y < 3; y++) {
for (int x = 0; x < 4; x++) {
printf("(%d,%d) ", x, y);
}
printf("\n");
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g nested_for.c -o nested_for && ./nested_for
=== 구구단 (세로) ===
[ 2단 ]
2 x 1 = 2
2 x 2 = 4
2 x 3 = 6
2 x 4 = 8
2 x 5 = 10
2 x 6 = 12
2 x 7 = 14
2 x 8 = 16
2 x 9 = 18
...
=== 구구단 (가로) ===
2 x 1 = 2 3 x 1 = 3 4 x 1 = 4 5 x 1 = 5 6 x 1 = 6 7 x 1 = 7 8 x 1 = 8 9 x 1 = 9
2 x 2 = 4 3 x 2 = 6 4 x 2 = 8 5 x 2 = 10 6 x 2 = 12 7 x 2 = 14 8 x 2 = 16 9 x 2 = 18
2 x 3 = 6 3 x 3 = 9 4 x 3 = 12 5 x 3 = 15 6 x 3 = 18 7 x 3 = 21 8 x 3 = 24 9 x 3 = 27
2 x 4 = 8 3 x 4 = 12 4 x 4 = 16 5 x 4 = 20 6 x 4 = 24 7 x 4 = 28 8 x 4 = 32 9 x 4 = 36
2 x 5 = 10 3 x 5 = 15 4 x 5 = 20 5 x 5 = 25 6 x 5 = 30 7 x 5 = 35 8 x 5 = 40 9 x 5 = 45
2 x 6 = 12 3 x 6 = 18 4 x 6 = 24 5 x 6 = 30 6 x 6 = 36 7 x 6 = 42 8 x 6 = 48 9 x 6 = 54
2 x 7 = 14 3 x 7 = 21 4 x 7 = 28 5 x 7 = 35 6 x 7 = 42 7 x 7 = 49 8 x 7 = 56 9 x 7 = 63
2 x 8 = 16 3 x 8 = 24 4 x 8 = 32 5 x 8 = 40 6 x 8 = 48 7 x 8 = 56 8 x 8 = 64 9 x 8 = 72
2 x 9 = 18 3 x 9 = 27 4 x 9 = 36 5 x 9 = 45 6 x 9 = 54 7 x 9 = 63 8 x 9 = 72 9 x 9 = 81
=== 별 찍기 (직각삼각형) ===
*
**
***
****
*****
=== 별 찍기 (역삼각형) ===
*****
****
***
**
*
=== 별 찍기 (우측 정렬) ===
*
**
***
****
*****
=== 숫자 피라미드 ===
1
1 2
1 2 3
1 2 3 4
1 2 3 4 5
=== 2차원 좌표 ===
(0,0) (1,0) (2,0) (3,0)
(0,1) (1,1) (2,1) (3,1)
(0,2) (1,2) (2,2) (3,2)
(3단, 4단의 세로 출력은 2단과 같은 모양이라 줄였습니다.)
- 구구단 세로 (12~18번째 줄): 바깥
dan이 2일 때 안쪽i가 1부터 9까지 다 돌고, 그다음dan이 3이 되어 다시 안쪽이 1부터 9까지 돕니다. 안쪽printf는 모두 3 × 9 = 27번 실행됩니다. 중첩 반복의 총 횟수는 바깥 횟수 × 안쪽 횟수입니다. - 구구단 가로 (23~28번째 줄): 바깥과 안쪽의 역할을 바꿨습니다. 바깥이
i(곱하는 수), 안쪽이dan(단)이라서 한 줄에 “2단의 i, 3단의 i, …, 9단의 i”가 나란히 찍힙니다. 어느 쪽을 바깥에 두느냐로 출력 모양이 바뀝니다. 화면은 위에서 아래로, 왼쪽에서 오른쪽으로 찍히니, “한 줄에 무엇이 오는가”를 안쪽 반복에 맡기면 됩니다. - 별 찍기 (32~37번째 줄): 안쪽 반복의 횟수가 바깥 변수
i에 따라 바뀝니다. 1번째 줄에 별 1개, 2번째 줄에 2개…j < i가 그 규칙입니다. 한 줄을 다 찍으면 36번째 줄의printf("\n")로 줄을 바꿉니다. 1주차 12절에서 손으로 그렸던 삼각형이 이 여섯 줄로 크기와 상관없이 그려집니다. - 우측 정렬 (51~61번째 줄): 한 줄 안에 반복이 두 개입니다. 먼저 공백을
n - i개 찍고, 이어서 별을i개 찍습니다. 공백이 줄어드는 만큼 별이 늘어나 오른쪽이 맞춰집니다. - 좌표 (74~79번째 줄): 바깥이
y(행), 안쪽이x(열)입니다. 출력을 보면(x,y)가 격자처럼 놓입니다. 화면이나 표, 게임 판처럼 2차원을 다루는 코드는 거의 항상 이 모양입니다. 4주차의 2차원 배열에서 다시 만납니다.
별 찍기의 요령: 모양을 보고 곧장 코드를 짜려 하지 말고, 줄 번호별로 공백 몇 개, 별 몇 개인지 표를 먼저 만드세요. 그 표에서
i와의 관계(n - i,2 * i - 1…)를 찾으면 코드는 저절로 나옵니다. 16절 프로젝트 3에서 이 방법을 그대로 씁니다.
12. 반복문 — while
몇 번 돌지 모를 때
for 는 “1부터 10까지”처럼 횟수가 정해진 반복에 잘 맞습니다. 그런데 “숫자가 0이 될 때까지 10으로 나눈다”, “합이 목표를 넘을 때까지 더한다”처럼 몇 번 돌지 미리 알 수 없는 반복도 있습니다. 이럴 때는 while 이 자연스럽습니다.
while (조건) {
// 조건이 참인 동안 반복
}
while 은 “~하는 동안”이라는 뜻입니다. for 의 세 칸 중 조건 칸만 남은 것이라고 보면 됩니다. 실제로 둘은 서로 바꿔 쓸 수 있습니다.
for (int i = 1; i <= 10; i++) { int i = 1;
printf("%d ", i); ⇔ while (i <= 10) {
} printf("%d ", i);
i++;
}
오른쪽을 보면 for 의 초기화는 while 앞으로, 증감은 몸통의 끝으로 옮겨졌습니다. 이 증감을 몸통 안에 직접 써야 한다는 점이 while 의 함정입니다. 빼먹으면 조건이 영원히 참으로 남아 15절의 무한 루프가 됩니다.
예제: while_examples.c
/*
* while_examples.c - while 반복문 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
// 기본 while: 1부터 10까지
printf("=== 1부터 10까지 ===\n");
int i = 1;
while (i <= 10) {
printf("%d ", i);
i++;
}
printf("\n");
// 합계 계산
printf("\n=== 1부터 100까지 합 ===\n");
int sum = 0;
int num = 1;
while (num <= 100) {
sum += num;
num++;
}
printf("합계: %d\n", sum);
// 자릿수 계산
printf("\n=== 자릿수 계산 ===\n");
int number = 12345;
int original = number;
int digits = 0;
while (number > 0) {
digits++;
number /= 10;
}
printf("%d는 %d자리 수입니다.\n", original, digits);
// 자릿수 합
printf("\n=== 자릿수 합 ===\n");
number = 12345;
original = number;
int digit_sum = 0;
while (number > 0) {
digit_sum += number % 10; // 마지막 자릿수 더하기
number /= 10; // 마지막 자릿수 제거
}
printf("%d의 각 자릿수 합: %d\n", original, digit_sum);
// 2의 거듭제곱 (1000 이하)
printf("\n=== 2의 거듭제곱 (1000 이하) ===\n");
int power = 1;
while (power <= 1000) {
printf("%d ", power);
power *= 2;
}
printf("\n");
// 피보나치 수열 (100 이하)
printf("\n=== 피보나치 수열 (100 이하) ===\n");
int a = 1, b = 1;
printf("%d %d ", a, b);
while (a + b <= 100) {
int temp = a + b;
a = b;
b = temp;
printf("%d ", b);
}
printf("\n");
// 조건 확인 while (실제 사용 패턴)
printf("\n=== 간단한 카운트다운 ===\n");
int countdown = 5;
while (countdown > 0) {
printf("%d...\n", countdown);
countdown--;
}
printf("발사!\n");
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g while_examples.c -o while_examples && ./while_examples
=== 1부터 10까지 ===
1 2 3 4 5 6 7 8 9 10
=== 1부터 100까지 합 ===
합계: 5050
=== 자릿수 계산 ===
12345는 5자리 수입니다.
=== 자릿수 합 ===
12345의 각 자릿수 합: 15
=== 2의 거듭제곱 (1000 이하) ===
1 2 4 8 16 32 64 128 256 512
=== 피보나치 수열 (100 이하) ===
1 1 2 3 5 8 13 21 34 55 89
=== 간단한 카운트다운 ===
5...
4...
3...
2...
1...
발사!
처음 두 부분(1부터 10, 합계)은 for 로 쓰는 게 더 자연스러운 예를 일부러 while 로 쓴 것입니다. 나머지는 모두 “조건이 깨질 때까지 값을 바꿔 나간다” 는 while 다운 예입니다.
- 자릿수 계산: 12345를 10으로 계속 나눕니다. 1234, 123, 12, 1, 0. 0이 되면 멈추고, 나눈 횟수가 자릿수입니다. 몇 자리 수가 들어올지 모르니 횟수를 미리 정할 수 없습니다. 1절의 자릿수 분리와 같은 원리입니다.
- 2의 거듭제곱:
power를 두 배씩 키우다가 1000을 넘으면 멈춥니다. 512 다음은 1024라서 512까지만 찍힙니다. - 피보나치 수열: 앞의 두 수를 더해 다음 수를 만드는 수열입니다(1, 1, 2, 3, 5, 8, …). 변수
a,b가 “바로 앞의 두 수”를 들고 다니고,temp에 합을 잠시 담았다가 한 칸씩 밀어 줍니다. 6절 XOR 스왑에서 “임시 변수를 쓰는 평범한 방법”이라고 했던temp가 여기서도 쓰였습니다. - 카운트다운: 5부터 1까지 거꾸로 세고 “발사!”.
countdown--가 없으면 영원히 “5…”만 찍힙니다.
for 와 while, 무엇을 쓸까
정해진 규칙은 없지만, 대부분의 C 프로그래머는 이렇게 고릅니다.
| 상황 | 고르는 것 | 이유 |
|---|---|---|
| 횟수가 정해짐 (N번, 배열의 모든 칸) | for |
초기화·조건·증감이 한 줄에 모여 한눈에 보임 |
| 조건에 따라 끝남 (0이 될 때까지, 입력이 끝날 때까지) | while |
반복 변수가 따로 없거나, 바뀌는 방식이 복잡함 |
| 최소 한 번은 실행해야 함 | do-while |
다음 절 |
13. 반복문 — do-while
일단 한 번은 한다
while 은 조건을 먼저 검사합니다. 처음부터 거짓이면 몸통을 한 번도 실행하지 않습니다. 그런데 “일단 한 번 해 보고, 결과를 보고 계속할지 정한다”가 자연스러운 경우가 있습니다. 대표적인 것이 입력 받기입니다. 입력을 받아 봐야 그 입력이 올바른지 알 수 있으니까요.
do {
// 최소 한 번은 실행
} while (조건); // ← 세미콜론 필수!
do 는 “해라”입니다. 몸통을 먼저 실행하고, 끝에서 조건을 검사해 참이면 다시 올라갑니다. while 이 “들어가기 전에 확인”, do-while 이 “나오면서 확인”입니다.
마지막 줄 끝의 세미콜론을 빼먹기 쉽습니다. while 반복문은 while (조건) { 로 끝나지만, do-while 은 } while (조건); 로 끝나는 하나의 문장이라 ; 가 필요합니다. 빼먹으면 이렇게 됩니다. dws.c:
do {
i++;
} while (i < 3)
printf("%d\n", i);
dws.c: In function ‘main’:
dws.c:7:20: error: expected ‘;’ before ‘printf’
7 | } while (i < 3)
| ^
| ;
8 | printf("%d\n", i);
| ~~~~~~
1주차 10절의 에러 1과 똑같은 메시지입니다. 컴파일러가 ; 를 넣을 자리까지 그려 줍니다.
예제: do_while_examples.c
이 예제에는 처음 보는 함수 두 개(exit_if_eof, flush_line)가 위쪽에 들어 있습니다. 16절의 메뉴 프로그램들도 똑같이 쓰는 부품이라, 여기서 한 번 제대로 보고 가겠습니다.
/*
* do_while_examples.c - do-while 반복문 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
#include <stdlib.h>
/*
* 입력이 끝났으면 프로그램을 정상 종료합니다.
*
* 메뉴형 프로그램은 사람이 앞에 있다고 가정합니다. 그런데 파이프나
* 파일로 돌리면 입력이 끝나고, 그 뒤로 scanf 는 계속 EOF 를 돌려줍니다.
* 그걸 '잘못된 입력' 으로만 처리하면 메뉴를 다시 찍고 또 읽으려 하면서
* 영원히 돕니다. 사람이 없으니 아무도 멈춰 주지 않죠.
*
* 이 저장소에서 실제로 그랬습니다. 스크린샷을 자동으로 만들려고
* 입력 없이 돌렸더니 **초당 0.7 GB** 를 쏟아냈고, 그 출력을 받아
* 모으던 쪽이 메모리 25 GB 를 먹고 OOM 킬러에 죽었습니다.
*
* scanf 의 반환값만으로는 '잘못된 글자' 와 '입력이 아예 없음' 을
* 구별할 수 없습니다. feof() 를 같이 봐야 합니다.
*/
static void exit_if_eof(void)
{
if (feof(stdin)) {
printf("\n입력이 끝났습니다. 종료합니다.\n");
exit(0);
}
}
/*
* 입력 버퍼를 줄 끝까지 비웁니다.
*
* EOF 를 반드시 같이 봐야 합니다. 이렇게 쓰면
*
* while (getchar() != '\n');
*
* 파이프로 입력을 주거나 파일에서 읽을 때 여기서 영원히 돕니다.
* 입력이 끝나면 getchar() 는 '\n' 이 아니라 EOF 를 계속 돌려주니까요.
* 터미널에서 직접 치면 잘 돌아가서 더 찾기 어려운 버그입니다.
*/
static void flush_line(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
int main(void) {
// while vs do-while 비교
printf("=== while vs do-while 비교 ===\n");
// while: 조건이 처음부터 거짓이면 한 번도 실행 안 함
printf("\nwhile (조건이 거짓):\n");
int x = 0;
while (x > 0) {
printf("while: 실행됨 (x=%d)\n", x);
x--;
}
printf("while 루프가 실행되지 않음\n");
// do-while: 최소 한 번은 실행
printf("\ndo-while (조건이 거짓):\n");
int y = 0;
do {
printf("do-while: 실행됨 (y=%d)\n", y);
y--;
} while (y > 0);
printf("do-while은 한 번 실행됨\n");
// 기본 do-while: 1부터 5까지
printf("\n=== 기본 do-while: 1부터 5까지 ===\n");
int i = 1;
do {
printf("%d ", i);
i++;
} while (i <= 5);
printf("\n");
// 입력 유효성 검사 (do-while의 대표적 사용 예)
printf("\n=== 입력 유효성 검사 ===\n");
int age;
int valid;
do {
printf("나이를 입력하세요 (1-150): ");
if (scanf("%d", &age) != 1) {
exit_if_eof();
// 숫자가 아닌 글자가 들어왔다: 그 줄을 버리고 다시 묻는다
flush_line(); // 버퍼 비우기
printf("숫자로 입력해주세요.\n");
valid = 0;
} else if (age < 1 || age > 150) {
printf("유효하지 않은 나이입니다. 다시 입력해주세요.\n");
valid = 0;
} else {
valid = 1;
}
} while (!valid);
printf("입력된 나이: %d세\n", age);
// 합계가 목표에 도달할 때까지
printf("\n=== 합계가 100 이상이 될 때까지 ===\n");
int sum = 0;
int num = 0;
do {
num++;
sum += num;
printf("num=%d, sum=%d\n", num, sum);
} while (sum < 100);
printf("합계가 100 이상이 되려면 1부터 %d까지 더해야 합니다. (합: %d)\n", num, sum);
// 자릿수 세기 (0도 처리)
printf("\n=== 자릿수 세기 (0 포함) ===\n");
int number = 0;
int digits = 0;
int original = number;
do {
digits++;
number /= 10;
} while (number > 0);
printf("%d는 %d자리 수입니다.\n", original, digits);
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g do_while_examples.c -o do_while_examples
$ ./do_while_examples
=== while vs do-while 비교 ===
while (조건이 거짓):
while 루프가 실행되지 않음
do-while (조건이 거짓):
do-while: 실행됨 (y=0)
do-while은 한 번 실행됨
=== 기본 do-while: 1부터 5까지 ===
1 2 3 4 5
=== 입력 유효성 검사 ===
나이를 입력하세요 (1-150): abc
숫자로 입력해주세요.
나이를 입력하세요 (1-150): 200
유효하지 않은 나이입니다. 다시 입력해주세요.
나이를 입력하세요 (1-150): 30
입력된 나이: 30세
=== 합계가 100 이상이 될 때까지 ===
num=1, sum=1
num=2, sum=3
num=3, sum=6
num=4, sum=10
num=5, sum=15
num=6, sum=21
num=7, sum=28
num=8, sum=36
num=9, sum=45
num=10, sum=55
num=11, sum=66
num=12, sum=78
num=13, sum=91
num=14, sum=105
합계가 100 이상이 되려면 1부터 14까지 더해야 합니다. (합: 105)
=== 자릿수 세기 (0 포함) ===
0는 1자리 수입니다.
while 과 do-while 비교 (55~71번째 줄): 조건은 둘 다 > 0 이고 변수는 둘 다 0입니다. while 은 들어가기 전에 확인해서 한 번도 돌지 않았고, do-while 은 일단 한 번 돌고 나서 확인했습니다. 조건이 같아도 검사 시점이 다르면 결과가 다릅니다.
입력 유효성 검사 (82~101번째 줄): 이것이 do-while 의 대표적인 쓰임입니다. 흐름을 따라가 봅시다.
abc를 넣었습니다.scanf가 숫자를 읽지 못해 0을 돌려줍니다. 입력이 끝난 게 아니니exit_if_eof()는 그냥 지나가고,flush_line()이 남은abc와 줄바꿈을 버린 뒤, “숫자로 입력해주세요”를 찍고valid = 0. 조건!valid가 참이라 다시 묻습니다.200을 넣었습니다. 숫자는 맞지만 1~150 범위 밖이라valid = 0. 다시 묻습니다.30을 넣었습니다.valid = 1, 조건!valid가 거짓이 되어 반복을 빠져나옵니다.
flush_line() 이 왜 필요할까요? 2주차 7절에서 봤듯, scanf 가 abc 를 숫자로 읽지 못하면 abc 를 입력 버퍼에 그대로 남겨 둡니다. 버리지 않으면 다음 바퀴의 scanf 가 또 abc 를 만나 또 실패하고, 이것이 영원히 반복됩니다. flush_line 은 2주차에서 본 while ((c = getchar()) != '\n' && c != EOF); 를 이름 붙여 묶은 것입니다.
exit_if_eof() 는 왜 필요할까요? 입력이 아예 끝나 버린 경우(Ctrl + D, 또는 파일로 입력을 주다가 파일이 끝난 경우) scanf 는 몇 번을 다시 불러도 계속 실패합니다. 사람이 다시 칠 일이 없으니까요. 이 상황을 “잘못된 입력”으로만 처리하면 프로그램은 “다시 입력하세요”를 영원히 반복합니다. feof(stdin) 은 “표준 입력(키보드)이 끝났는가”를 알려 주는 함수이고, 끝났으면 exit(0) 으로 프로그램 전체를 끝냅니다. exit 는 <stdlib.h> 에 들어 있어서 7번째 줄에서 그 헤더를 포함했습니다. 이 함수가 없을 때 실제로 무슨 일이 벌어지는지는 15절에서 보여 드리겠습니다.
합계가 100 이상이 될 때까지 (105~116번째 줄): 몇 번 더해야 100을 넘을지 모르니 조건으로 멈춥니다. 14번 더하면 105로 넘습니다.
자릿수 세기 (118~129번째 줄): 12절의 while 버전은 0을 넣으면 “0자리”라고 답합니다. while (number > 0) 이 처음부터 거짓이라 한 번도 세지 않기 때문입니다. do-while 은 일단 한 번 세므로 0을 “1자리”로 올바르게 셉니다. “최소 한 번”이 정답을 바꾸는 좋은 예입니다.
14. break 와 continue
break: 반복을 당장 끝낸다
10절에서 switch 를 빠져나갈 때 쓴 break 는 반복문에서도 똑같이 동작합니다. 가장 가까운 반복문 하나를 즉시 빠져나갑니다. 조건이 아직 참이어도 상관없습니다.
continue: 이번 바퀴만 건너뛴다
continue 는 반복을 끝내지 않고, 이번 바퀴의 나머지를 건너뛰고 다음 바퀴로 갑니다. for 에서는 곧바로 ③ 증감 칸으로, while 에서는 곧바로 조건 검사로 갑니다.
for (초기화; 조건; 증감) {
...
if (...) break; ──→ 반복문 밖으로 탈출
...
if (...) continue; ──→ 증감 칸으로 (다음 바퀴)
...
}
예제: break_continue.c
/*
* break_continue.c - break와 continue 예제
* 3주차: 연산자와 제어문
*/
#include <stdio.h>
int main(void) {
// break 예제: 특정 값 찾기
printf("=== break: 특정 값 찾기 ===\n");
int numbers[] = {3, 7, 2, 9, 5, 1, 8};
int size = sizeof(numbers) / sizeof(numbers[0]);
int target = 9;
int found = 0;
int found_index = -1;
for (int i = 0; i < size; i++) {
printf("검사 중: numbers[%d] = %d\n", i, numbers[i]);
if (numbers[i] == target) {
printf("찾았습니다! 인덱스: %d\n", i);
found = 1;
found_index = i;
break; // 찾으면 즉시 종료
}
}
if (found) {
printf("=> %d는 인덱스 %d에서 발견되었습니다.\n", target, found_index);
} else {
printf("%d를 찾지 못했습니다.\n", target);
}
// continue 예제: 홀수만 출력
printf("\n=== continue: 홀수만 출력 ===\n");
for (int i = 1; i <= 10; i++) {
if (i % 2 == 0) {
continue; // 짝수면 건너뜀
}
printf("%d ", i);
}
printf("\n");
// continue 예제: 3의 배수 건너뛰기
printf("\n=== continue: 3의 배수 건너뛰기 ===\n");
for (int i = 1; i <= 15; i++) {
if (i % 3 == 0) {
printf("[%d 건너뜀] ", i);
continue;
}
printf("%d ", i);
}
printf("\n");
// 실용적 예제: 음수 제외하고 합계
printf("\n=== 음수 제외하고 합계 ===\n");
int values[] = {5, -3, 8, -1, 10, -7, 3};
int len = sizeof(values) / sizeof(values[0]);
int sum = 0;
for (int i = 0; i < len; i++) {
if (values[i] < 0) {
printf("%d (음수, 건너뜀)\n", values[i]);
continue;
}
printf("%d (더함)\n", values[i]);
sum += values[i];
}
printf("양수 합계: %d\n", sum);
// 중첩 반복문에서 break
printf("\n=== 중첩 반복문에서 break ===\n");
printf("내부 반복문에서 j=3일 때 break\n\n");
for (int i = 0; i < 3; i++) {
printf("외부 반복 i=%d: ", i);
for (int j = 0; j < 5; j++) {
if (j == 3) {
break; // 내부 반복문만 종료
}
printf("j=%d ", j);
}
printf("(내부 루프 종료)\n");
}
// 중첩 반복문 전체 탈출 (플래그 사용)
printf("\n=== 중첩 반복문 전체 탈출 (플래그 사용) ===\n");
int escape = 0;
for (int i = 0; i < 5 && !escape; i++) {
for (int j = 0; j < 5 && !escape; j++) {
printf("(%d,%d) ", i, j);
if (i == 2 && j == 2) {
printf("<- 여기서 탈출!");
escape = 1;
}
}
printf("\n");
}
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g break_continue.c -o break_continue && ./break_continue
=== break: 특정 값 찾기 ===
검사 중: numbers[0] = 3
검사 중: numbers[1] = 7
검사 중: numbers[2] = 2
검사 중: numbers[3] = 9
찾았습니다! 인덱스: 3
=> 9는 인덱스 3에서 발견되었습니다.
=== continue: 홀수만 출력 ===
1 3 5 7 9
=== continue: 3의 배수 건너뛰기 ===
1 2 [3 건너뜀] 4 5 [6 건너뜀] 7 8 [9 건너뜀] 10 11 [12 건너뜀] 13 14 [15 건너뜀]
=== 음수 제외하고 합계 ===
5 (더함)
-3 (음수, 건너뜀)
8 (더함)
-1 (음수, 건너뜀)
10 (더함)
-7 (음수, 건너뜀)
3 (더함)
양수 합계: 26
=== 중첩 반복문에서 break ===
내부 반복문에서 j=3일 때 break
외부 반복 i=0: j=0 j=1 j=2 (내부 루프 종료)
외부 반복 i=1: j=0 j=1 j=2 (내부 루프 종료)
외부 반복 i=2: j=0 j=1 j=2 (내부 루프 종료)
=== 중첩 반복문 전체 탈출 (플래그 사용) ===
(0,0) (0,1) (0,2) (0,3) (0,4)
(1,0) (1,1) (1,2) (1,3) (1,4)
(2,0) (2,1) (2,2) <- 여기서 탈출!
- 특정 값 찾기 (17~25번째 줄): 9를 찾자마자
break로 나왔습니다. 뒤의 5, 1, 8은 검사조차 하지 않았습니다. 출력의 “검사 중” 줄이 네 개뿐인 것으로 확인할 수 있습니다. 찾는 일은 찾으면 끝이니, 나머지를 더 볼 이유가 없습니다. 이 모양은 4주차에 선형 탐색이라는 이름으로 다시 나옵니다.found와found_index를 반복 밖에 만들어 둔 것도 보세요. 11절에서 본 대로 반복 안의 변수는 반복이 끝나면 사라지니, 결과를 밖으로 들고 나오려면 밖에 만들어 둬야 합니다. - 3의 배수 건너뛰기 (45~51번째 줄):
continue앞에printf("[%d 건너뜀] ")이 있어서 건너뛴 것까지 눈에 보입니다.continue뒤의printf("%d ", i)는 3의 배수일 때 실행되지 않습니다. - 음수 제외 합계 (60~67번째 줄):
continue의 전형적인 쓰임입니다. “이런 경우는 처리하지 않는다”를 반복 맨 앞에서 걸러 내면, 나머지 코드는 “정상적인 경우”만 생각하며 쓸 수 있습니다. 5 + 8 + 10 + 3 = 26. - 중첩 반복의 break (74~83번째 줄): 안쪽
break는 안쪽 반복 하나만 끝냅니다. 그래서j는 매번 3에서 멈추지만 바깥i는 0, 1, 2 끝까지 돕니다. - 중첩 반복 전체 탈출 (87~98번째 줄): 안쪽에서 바깥까지 한 번에 나가려면
break하나로는 안 됩니다. 이 예제는escape라는 플래그 변수를 두 반복의 조건에 모두 넣었습니다.escape = 1이 되면 안쪽 조건j < 5 && !escape가 거짓이 되어 안쪽이 끝나고, 바깥도 다음 검사에서 거짓이 되어 끝납니다. 5절의&&와!가 여기서 쓰입니다.
goto: 쓸 수는 있지만
중첩 반복을 한 번에 빠져나가는 또 다른 방법으로 goto 가 있습니다.
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
if (i * j == 42) {
goto done; // 아래의 done: 표지판으로 곧장 점프
}
}
}
done:
printf("찾았습니다\n");
goto 이름; 은 같은 함수 안의 이름: 표지판(레이블)으로 곧장 점프합니다. 강력하지만, 여기저기로 점프하는 코드는 흐름을 따라가기 매우 어렵습니다(이런 코드를 “스파게티 코드”라고 부릅니다). 1968년 에츠허르 데이크스트라가 쓴 「goto 문은 해롭다」는 글 이후, goto 는 대부분의 경우 피하는 것이 원칙이 됐습니다. 다만 리눅스 커널은 “오류가 나면 정리 코드로 점프”하는 용도로 goto 를 적극적으로 씁니다. 그 쓰임은 뒤의 주차에서 동적 메모리를 정리할 때 보게 됩니다. 이번 주에는 플래그 변수로 충분합니다.
15. 무한 루프
일부러 만드는 무한 루프
조건이 영원히 참인 반복을 무한 루프(infinite loop) 라고 합니다. 실수로 만들면 버그지만, 일부러 만들 때도 많습니다. 게임은 “사용자가 종료를 누를 때까지” 화면을 그리고, 웹 서버는 “꺼질 때까지” 요청을 처리합니다. 이런 프로그램은 끝나는 조건이 반복의 머리가 아니라 몸통 한가운데 있어서, 무한 루프를 만들고 안에서 break 로 나오는 모양이 자연스럽습니다.
for (;;) { ... } // 세 칸을 모두 비움. 조건 칸이 비면 항상 참
while (1) { ... } // 1 은 항상 참 (5절)
do { ... } while (1);
for (;;) 는 “for-ever”라고 읽는 사람도 있습니다. 두 모양 모두 널리 쓰이고 기계어도 똑같이 나옵니다.
/*
* infinite_loop.c - 무한 루프 예제
* 3주차: 연산자와 제어문
*
* 참고: 실제로 무한히 실행되는 예제는 주석 처리되어 있습니다.
*/
#include <stdio.h>
#include <string.h>
int main(void) {
// 무한 루프의 형태들
printf("=== 무한 루프의 형태들 ===\n");
printf("1. for (;;) { ... }\n");
printf("2. while (1) { ... }\n");
printf("3. do { ... } while (1);\n\n");
// 무한 루프 + break 예제 (실제로 실행됨)
printf("=== 무한 루프 + break 예제 ===\n");
printf("1부터 시작하여 합이 50을 초과하면 종료\n\n");
int sum = 0;
int num = 0;
while (1) { // 무한 루프
num++;
sum += num;
printf("num=%d, sum=%d\n", num, sum);
if (sum > 50) {
printf("합이 50을 초과하여 종료합니다.\n");
break;
}
}
// for 무한 루프 + break 예제
printf("\n=== for 무한 루프 + break 예제 ===\n");
printf("2의 거듭제곱 중 1000을 초과하는 첫 번째 수 찾기\n\n");
int power = 1;
int count = 0;
for (;;) { // 무한 루프
power *= 2;
count++;
printf("2^%d = %d\n", count, power);
if (power > 1000) {
printf("1000을 초과하는 첫 번째 2의 거듭제곱: %d\n", power);
break;
}
}
// 간단한 에코 프로그램 (제한된 반복)
printf("\n=== 간단한 명령어 시뮬레이션 ===\n");
printf("명령어를 입력하세요 (quit으로 종료):\n");
// 실제 입력 대신 시뮬레이션
const char *commands[] = {"hello", "world", "test", "quit"};
int cmd_count = sizeof(commands) / sizeof(commands[0]);
for (int i = 0; i < cmd_count; i++) {
printf("> %s\n", commands[i]);
if (strcmp(commands[i], "quit") == 0) {
printf("프로그램을 종료합니다.\n");
break;
}
printf("입력: %s\n", commands[i]);
}
// 무한 루프 주의사항
printf("\n=== 무한 루프 주의사항 ===\n");
printf("1. 반드시 탈출 조건(break)이 있어야 합니다.\n");
printf("2. 탈출 조건이 실제로 도달 가능해야 합니다.\n");
printf("3. 실수로 무한 루프가 되지 않도록 주의하세요.\n");
printf("4. Ctrl+C로 강제 종료할 수 있습니다.\n");
// 흔한 무한 루프 버그 예시 (주석)
printf("\n=== 흔한 무한 루프 버그 ===\n");
printf("// 버그 1: 증감 연산 누락\n");
printf("int i = 0;\n");
printf("while (i < 10) {\n");
printf(" printf(\"%%d \", i);\n");
printf(" // i++; 누락!\n");
printf("}\n\n");
printf("// 버그 2: 잘못된 조건\n");
printf("for (int i = 10; i >= 0; i++) { // i++가 아니라 i--여야 함\n");
printf(" printf(\"%%d \", i);\n");
printf("}\n");
return 0;
}
$ gcc -Wall -Wextra -std=c11 -g infinite_loop.c -o infinite_loop && ./infinite_loop
=== 무한 루프의 형태들 ===
1. for (;;) { ... }
2. while (1) { ... }
3. do { ... } while (1);
=== 무한 루프 + break 예제 ===
1부터 시작하여 합이 50을 초과하면 종료
num=1, sum=1
num=2, sum=3
num=3, sum=6
num=4, sum=10
num=5, sum=15
num=6, sum=21
num=7, sum=28
num=8, sum=36
num=9, sum=45
num=10, sum=55
합이 50을 초과하여 종료합니다.
=== for 무한 루프 + break 예제 ===
2의 거듭제곱 중 1000을 초과하는 첫 번째 수 찾기
2^1 = 2
2^2 = 4
2^3 = 8
...
2^7 = 128
2^8 = 256
2^9 = 512
2^10 = 1024
1000을 초과하는 첫 번째 2의 거듭제곱: 1024
=== 간단한 명령어 시뮬레이션 ===
명령어를 입력하세요 (quit으로 종료):
> hello
입력: hello
> world
입력: world
> test
입력: test
> quit
프로그램을 종료합니다.
=== 무한 루프 주의사항 ===
1. 반드시 탈출 조건(break)이 있어야 합니다.
2. 탈출 조건이 실제로 도달 가능해야 합니다.
3. 실수로 무한 루프가 되지 않도록 주의하세요.
4. Ctrl+C로 강제 종료할 수 있습니다.
=== 흔한 무한 루프 버그 ===
// 버그 1: 증감 연산 누락
int i = 0;
while (i < 10) {
printf("%d ", i);
// i++; 누락!
}
// 버그 2: 잘못된 조건
for (int i = 10; i >= 0; i++) { // i++가 아니라 i--여야 함
printf("%d ", i);
}
(2^4부터 2^6까지는 줄였습니다.)
두 반복 모두 머리에는 조건이 없고, 몸통 안의 if (sum > 50), if (power > 1000) 이 참이 되는 순간 break 로 나옵니다. 무한 루프의 안전은 조건이 아니라 break 가 책임집니다. 그러니 break 에 반드시 도달할 수 있는지를 확인하는 것이 무한 루프를 쓸 때의 핵심입니다.
“명령어 시뮬레이션”은 진짜 입력 대신 미리 준비한 단어 네 개를 돌리면서 quit 이 나오면 멈춥니다. 문자열이 같은지 비교하는 strcmp 는 4주차에 배우는 <string.h> 의 함수입니다. 진짜 입력을 받는 버전은 16절의 메뉴 프로그램들입니다.
마지막의 “흔한 무한 루프 버그” 둘은 printf 로 코드를 글자로 찍기만 한 것이지 실제로 실행되지는 않습니다. %%d 로 %d 를 찍은 것도 눈여겨보세요. 두 번째 버그 for (int i = 10; i >= 0; i++) 는 i 가 10, 11, 12… 로 커지기만 하니 i >= 0 이 계속 참입니다. 엄밀히는 int 의 끝(21억)에 닿으면 1절의 오버플로가 일어나는데, 그것은 정의되지 않은 동작이라 어떻게 될지 모릅니다.
실수로 만든 무한 루프: 얼마나 빨리 돌까?
증감을 빼먹은 while 을 정말로 돌려 보겠습니다. forever.c:
#include <stdio.h>
int main(void) {
int i = 0;
while (i < 10) {
printf("%d\n", i);
}
return 0;
}
컴파일러는 아무 경고도 하지 않습니다. i 가 몸통에서 바뀌지 않는다는 걸 알아채지 못하기 때문입니다. 실행하면 화면이 0 으로 가득 차며 멈추지 않습니다. 이때 Ctrl + C 를 누르세요(1주차 2절).
$ ./forever
0
0
0
...
^C
$ echo $?
130
^C 는 터미널이 Ctrl + C 를 표시한 것입니다. 종료 코드 130은 1절의 136과 같은 규칙, 128 + 신호 번호입니다. Ctrl + C 는 프로그램에 2번 신호(SIGINT, “interrupt, 중단하라”)를 보냅니다.
얼마나 빨리 도는지 재 볼 수도 있습니다. timeout 1 은 “1초만 실행하고 멈춰라”라는 명령이고, 출력을 화면 대신 파일에 담아 크기를 봅니다.
$ timeout 1 ./forever > big.txt
$ wc -c big.txt
56938496 big.txt
$ wc -l big.txt
28469248 big.txt
$ rm big.txt
wc -c 는 바이트 수, wc -l 은 줄 수를 셉니다(1주차에서 본 wc).
1초에 2,800만 줄, 57MB. 실수 하나가 1분이면 3GB, 한 시간이면 디스크 하나를 채웁니다. 컴퓨터는 이렇게 빠르고, 그래서 멈추는 조건을 잘못 쓴 반복은 순식간에 큰 사고가 됩니다.
입력이 끝났는데 계속 묻는 프로그램
사람이 앞에 있을 때는 무한 루프에 빠지면 Ctrl + C 로 끄면 됩니다. 그런데 사람이 없을 때는 어떨까요? 프로그램에 입력을 파일이나 파이프로 주면(2주차), 입력이 끝난 뒤에는 아무도 멈춰 주지 않습니다.
이 강좌의 저장소에서 실제로 있었던 일입니다. 16절의 구구단 출력기는 원래 메뉴 번호를 읽다 실패하면 “잘못된 입력입니다”를 찍고 메뉴를 다시 보여 주기만 했습니다. 사람이 abc 를 쳤을 때는 올바른 동작입니다. 그런데 입력이 끝난 상태에서는 scanf 가 영원히 실패하므로, 메뉴와 “잘못된 입력입니다”를 끝없이 찍습니다. 입력 7 한 줄만 주고 3초 동안 돌려 보았더니 5MB를 넘게 쏟아냈습니다. 스크린샷을 자동으로 만드는 도구가 이 출력을 받다가 메모리가 바닥나서 강제 종료된 적도 있습니다.
13절의 exit_if_eof() 가 바로 이 문제를 막는 부품입니다. scanf 가 실패하면 먼저 “입력이 아예 끝났는지”를 feof(stdin) 으로 확인하고, 끝났으면 프로그램을 정상 종료합니다. 같은 실패라도 “잘못 친 것”과 “더 칠 게 없는 것”은 다르게 다뤄야 한다는 교훈입니다.
무한 루프를 피하는 체크리스트
반복문을 쓸 때마다 이 세 가지를 확인하세요.
- 조건에 쓰인 변수가 몸통에서 바뀌는가?
while (i < 10)이면 몸통 어딘가에i++이 있어야 합니다. - 바뀌는 방향이 조건을 거짓으로 만드는 쪽인가?
i >= 0을 조건으로 두고i++을 하면 영원히 참입니다. 11절의unsigned처럼 자료형 때문에 거짓이 될 수 없는 경우도 있습니다. - 입력에 의존한다면, 입력이 끝났을 때도 멈추는가?
scanf의 반환값과feof를 확인합니다.
의심스러우면 1주차에서 배운 대로 반복 안에 printf 를 넣어 변수 값을 찍어 보세요. 6주차에는 gdb 로 반복을 한 바퀴씩 멈춰 가며 보는 방법을 배웁니다.
16. 실습 프로젝트
이번 주에 배운 연산자, 조건문, 반복문을 모두 엮어 프로그램 네 개를 만듭니다. 네 프로젝트는 모두 같은 뼈대를 가지고 있습니다.
do {
메뉴를 보여 준다
번호를 입력받는다 (실패하면: 입력이 끝났으면 종료, 아니면 버리고 다시)
switch (번호) {
case 1: 기능 1
case 2: 기능 2
...
case 0: 종료 인사
}
} while (번호 != 0);
do-while(13절)로 “최소 한 번은 메뉴를 보여 주고”, switch(10절)로 번호에 따라 갈라지고, 0을 고르면 반복이 끝납니다. 이 모양 하나를 제대로 이해하면 네 프로젝트를 모두 읽을 수 있습니다.
그리고 네 프로젝트 모두 함수를 씁니다. print_header, is_prime 처럼 이름 붙은 부품들입니다. 함수는 5주차 주제지만, 프로그램이 길어지면 main 하나에 다 넣기가 어려워서 미리 썼습니다. 지금은 이렇게만 읽으면 충분합니다.
void print_header(const char *title); // ① 선언: "이런 부품이 있다" (1주차 5절의 stdio.h 속 printf 선언과 같은 것)
print_header("구구단 출력기"); // ② 호출: 부품을 쓴다. 괄호 안의 값이 부품에 전달된다
void print_header(const char *title) { // ③ 정의: 부품의 실제 내용
printf(" %s\n", title); // 전달받은 값은 title 이라는 이름으로 쓴다
}
void 는 1주차에서 본 대로 “없음”입니다. 앞에 오면 “돌려주는 값이 없다”, int is_prime(int n) 처럼 int 가 오면 “정수를 돌려준다”는 뜻입니다.
프로젝트 1: 구구단 출력기
gugudan.c:
/*
* gugudan.c - 구구단 출력기
* 3주차 프로젝트: 연산자와 제어문
*
* 기능:
* - 특정 단 출력
* - 전체 구구단 출력
* - 범위 지정 출력
*/
#include <stdio.h>
#include <stdlib.h>
/*
* 입력이 끝났으면 프로그램을 정상 종료합니다.
*
* 메뉴형 프로그램은 사람이 앞에 있다고 가정합니다. 그런데 파이프나
* 파일로 돌리면 입력이 끝나고, 그 뒤로 scanf 는 계속 EOF 를 돌려줍니다.
* 그걸 '잘못된 입력' 으로만 처리하면 메뉴를 다시 찍고 또 읽으려 하면서
* 영원히 돕니다. 사람이 없으니 아무도 멈춰 주지 않죠.
*
* scanf 의 반환값만으로는 '잘못된 글자' 와 '입력이 아예 없음' 을
* 구별할 수 없습니다. feof() 를 같이 봐야 합니다.
*/
static void exit_if_eof(void)
{
if (feof(stdin)) {
printf("\n입력이 끝났습니다. 종료합니다.\n");
exit(0);
}
}
/*
* 입력 버퍼를 줄 끝까지 비웁니다.
*
* EOF 를 반드시 같이 봐야 합니다. 이렇게 쓰면
*
* while (getchar() != '\n');
*
* 파이프로 입력을 주거나 파일에서 읽을 때 여기서 영원히 돕니다.
* 입력이 끝나면 getchar() 는 '\n' 이 아니라 EOF 를 계속 돌려주니까요.
* 터미널에서 직접 치면 잘 돌아가서 더 찾기 어려운 버그입니다.
*/
static void flush_line(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
// 함수 선언
void print_single_dan(int dan);
void print_all_dan(void);
void print_range_dan(int start, int end);
void print_header(const char *title);
int main(void) {
int choice;
int dan, start, end;
do {
printf("\n");
print_header("구구단 출력기");
printf("1. 특정 단 출력\n");
printf("2. 전체 구구단 출력 (가로)\n");
printf("3. 전체 구구단 출력 (세로)\n");
printf("4. 범위 지정 출력\n");
printf("0. 종료\n");
printf("-----------------------------\n");
printf("선택: ");
if (scanf("%d", &choice) != 1) {
exit_if_eof();
printf("잘못된 입력입니다.\n");
flush_line();
continue;
}
switch (choice) {
case 1:
printf("몇 단을 출력할까요? (2-9): ");
if (scanf("%d", &dan) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (dan >= 2 && dan <= 9) {
print_single_dan(dan);
} else {
printf("잘못된 입력입니다. 2-9 사이의 숫자를 입력하세요.\n");
}
break;
case 2:
printf("\n===== 구구단 전체 (가로) =====\n\n");
for (int i = 1; i <= 9; i++) {
for (int d = 2; d <= 9; d++) {
printf("%d x %d = %2d ", d, i, d * i);
}
printf("\n");
}
break;
case 3:
print_all_dan();
break;
case 4:
printf("시작 단 (2-9): ");
if (scanf("%d", &start) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
printf("끝 단 (2-9): ");
if (scanf("%d", &end) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (start >= 2 && end <= 9 && start <= end) {
print_range_dan(start, end);
} else {
printf("잘못된 입력입니다.\n");
}
break;
case 0:
printf("프로그램을 종료합니다.\n");
break;
default:
printf("잘못된 선택입니다. 다시 선택해주세요.\n");
}
} while (choice != 0);
return 0;
}
void print_header(const char *title) {
printf("=============================\n");
printf(" %s\n", title);
printf("=============================\n");
}
void print_single_dan(int dan) {
printf("\n========== %d단 ==========\n\n", dan);
for (int i = 1; i <= 9; i++) {
printf(" %d x %d = %2d\n", dan, i, dan * i);
}
printf("\n==========================\n");
}
void print_all_dan(void) {
printf("\n===== 구구단 전체 (세로) =====\n");
for (int dan = 2; dan <= 9; dan++) {
printf("\n[ %d단 ]\n", dan);
for (int i = 1; i <= 9; i++) {
printf("%d x %d = %2d\n", dan, i, dan * i);
}
}
}
void print_range_dan(int start, int end) {
printf("\n===== %d단 ~ %d단 =====\n\n", start, end);
for (int i = 1; i <= 9; i++) {
for (int dan = start; dan <= end; dan++) {
printf("%d x %d = %2d ", dan, i, dan * i);
}
printf("\n");
}
}

구구단
$ gcc -Wall -Wextra -std=c11 -g gugudan.c -o gugudan
$ ./gugudan
=============================
구구단 출력기
=============================
1. 특정 단 출력
2. 전체 구구단 출력 (가로)
3. 전체 구구단 출력 (세로)
4. 범위 지정 출력
0. 종료
-----------------------------
선택: 1
몇 단을 출력할까요? (2-9): 7
========== 7단 ==========
7 x 1 = 7
7 x 2 = 14
7 x 3 = 21
7 x 4 = 28
7 x 5 = 35
7 x 6 = 42
7 x 7 = 49
7 x 8 = 56
7 x 9 = 63
==========================
=============================
구구단 출력기
=============================
(메뉴 생략)
선택: 4
시작 단 (2-9): 3
끝 단 (2-9): 5
===== 3단 ~ 5단 =====
3 x 1 = 3 4 x 1 = 4 5 x 1 = 5
3 x 2 = 6 4 x 2 = 8 5 x 2 = 10
3 x 3 = 9 4 x 3 = 12 5 x 3 = 15
3 x 4 = 12 4 x 4 = 16 5 x 4 = 20
3 x 5 = 15 4 x 5 = 20 5 x 5 = 25
3 x 6 = 18 4 x 6 = 24 5 x 6 = 30
3 x 7 = 21 4 x 7 = 28 5 x 7 = 35
3 x 8 = 24 4 x 8 = 32 5 x 8 = 40
3 x 9 = 27 4 x 9 = 36 5 x 9 = 45
=============================
구구단 출력기
=============================
(메뉴 생략)
선택: 0
프로그램을 종료합니다.
코드 읽기 포인트
- 메뉴 입력 검사 (73~78번째 줄): 13절에서 본 두 부품이 순서대로 쓰였습니다.
scanf가 실패하면 먼저exit_if_eof()로 “입력이 끝났는지” 보고, 아니면flush_line()으로 잘못 친 줄을 버리고continue합니다.do-while안의continue는 곧장 맨 아래 조건choice != 0으로 갑니다. 15절에서 말한 “입력 7 한 줄에 5MB를 쏟아낸” 버그가 바로 이 자리에exit_if_eof()가 없어서 생겼습니다. - 하위 입력 검사: “몇 단을 출력할까요?”처럼 메뉴 안에서 한 번 더 묻는 곳도 똑같이 검사합니다. 검사 블록 끝의
break는switch를 빠져나가는break입니다(14절의 반복문break가 아닙니다).switch안에서는break가 가장 가까운switch를 끝내므로, 메뉴로 돌아가 다시 묻게 됩니다. - 범위 검사
start >= 2 && end <= 9 && start <= end: 세 조건을&&로 묶었습니다. 시작이 끝보다 크면(예: 7단부터 3단까지) 거절합니다. - 2번(가로)과 4번(범위): 11절
nested_for.c의 가로 구구단과 같은 모양입니다. 바깥이 곱하는 수i, 안쪽이 단dan입니다. 4번은 안쪽 반복의 범위만start ~ end로 바꾼 것입니다.
직접 해 보기: 메뉴에 “5. 제곱표 출력(1×1 부터 9×9 까지 대각선만)”을 추가해 보세요. case 5: 를 하나 늘리고, printf 메뉴 줄도 하나 늘리면 됩니다. 중첩 반복 없이 반복 하나로 됩니다.
프로젝트 2: 소수 판별기
소수(prime number) 는 1과 자기 자신으로만 나누어떨어지는 2 이상의 자연수입니다. 2, 3, 5, 7, 11, 13, … 암호 기술이 소수의 성질 위에 서 있어서, 인터넷 쇼핑의 카드 결제도 결국 커다란 소수 덕분에 안전합니다.
prime_checker.c:
/*
* prime_checker.c - 소수 판별기
* 3주차 프로젝트: 연산자와 제어문
*
* 기능:
* - 단일 숫자 소수 판별
* - 범위 내 소수 찾기
* - N까지의 소수 개수 세기
*/
#include <stdio.h>
#include <stdlib.h>
/*
* 입력이 끝났으면 프로그램을 정상 종료합니다.
*
* 메뉴형 프로그램은 사람이 앞에 있다고 가정합니다. 그런데 파이프나
* 파일로 돌리면 입력이 끝나고, 그 뒤로 scanf 는 계속 EOF 를 돌려줍니다.
* 그걸 '잘못된 입력' 으로만 처리하면 메뉴를 다시 찍고 또 읽으려 하면서
* 영원히 돕니다. 사람이 없으니 아무도 멈춰 주지 않죠.
*
* scanf 의 반환값만으로는 '잘못된 글자' 와 '입력이 아예 없음' 을
* 구별할 수 없습니다. feof() 를 같이 봐야 합니다.
*/
static void exit_if_eof(void)
{
if (feof(stdin)) {
printf("\n입력이 끝났습니다. 종료합니다.\n");
exit(0);
}
}
/*
* 입력 버퍼를 줄 끝까지 비웁니다.
*
* EOF 를 반드시 같이 봐야 합니다. 이렇게 쓰면
*
* while (getchar() != '\n');
*
* 파이프로 입력을 주거나 파일에서 읽을 때 여기서 영원히 돕니다.
* 입력이 끝나면 getchar() 는 '\n' 이 아니라 EOF 를 계속 돌려주니까요.
* 터미널에서 직접 치면 잘 돌아가서 더 찾기 어려운 버그입니다.
*/
static void flush_line(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
// 함수 선언
int is_prime(int n);
void find_primes_in_range(int start, int end);
int count_primes(int limit);
void print_divisors(int n);
void print_header(const char *title);
int main(void) {
int choice;
int num, start, end;
do {
printf("\n");
print_header("소수 판별기");
printf("1. 단일 숫자 판별\n");
printf("2. 범위 내 소수 찾기\n");
printf("3. N까지의 소수 개수\n");
printf("4. 약수 구하기\n");
printf("0. 종료\n");
printf("-----------------------------\n");
printf("선택: ");
if (scanf("%d", &choice) != 1) {
exit_if_eof();
printf("잘못된 입력입니다.\n");
flush_line();
continue;
}
switch (choice) {
case 1:
printf("숫자를 입력하세요: ");
if (scanf("%d", &num) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (num < 2) {
printf("\n%d는 소수가 아닙니다.\n", num);
printf("(소수는 2 이상의 자연수 중 1과 자기 자신만을 약수로 갖는 수입니다)\n");
} else if (is_prime(num)) {
printf("\n%d는 소수입니다!\n", num);
} else {
printf("\n%d는 소수가 아닙니다.\n", num);
print_divisors(num);
}
break;
case 2:
printf("시작 숫자: ");
if (scanf("%d", &start) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
printf("끝 숫자: ");
if (scanf("%d", &end) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (start > end) {
int temp = start;
start = end;
end = temp;
}
find_primes_in_range(start, end);
break;
case 3:
printf("상한값을 입력하세요: ");
if (scanf("%d", &num) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (num < 2) {
printf("2 이상의 숫자를 입력하세요.\n");
} else {
int count = count_primes(num);
printf("\n2부터 %d까지 소수의 개수: %d개\n", num, count);
}
break;
case 4:
printf("숫자를 입력하세요: ");
if (scanf("%d", &num) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (num > 0) {
printf("\n");
print_divisors(num);
} else {
printf("양의 정수를 입력하세요.\n");
}
break;
case 0:
printf("프로그램을 종료합니다.\n");
break;
default:
printf("잘못된 선택입니다.\n");
}
} while (choice != 0);
return 0;
}
void print_header(const char *title) {
printf("=============================\n");
printf(" %s\n", title);
printf("=============================\n");
}
int is_prime(int n) {
if (n < 2) return 0;
if (n == 2) return 1;
if (n % 2 == 0) return 0;
// 홀수만 검사, 제곱근까지만
for (int i = 3; i * i <= n; i += 2) {
if (n % i == 0) {
return 0;
}
}
return 1;
}
void find_primes_in_range(int start, int end) {
printf("\n===== %d ~ %d 사이의 소수 =====\n\n", start, end);
int count = 0;
for (int i = start; i <= end; i++) {
if (is_prime(i)) {
printf("%5d ", i);
count++;
if (count % 10 == 0) {
printf("\n");
}
}
}
if (count % 10 != 0) {
printf("\n");
}
printf("\n총 %d개의 소수가 있습니다.\n", count);
}
int count_primes(int limit) {
int count = 0;
for (int i = 2; i <= limit; i++) {
if (is_prime(i)) {
count++;
}
}
return count;
}
void print_divisors(int n) {
printf("%d의 약수: ", n);
int count = 0;
for (int i = 1; i <= n; i++) {
if (n % i == 0) {
printf("%d ", i);
count++;
}
}
printf("\n(약수의 개수: %d개)\n", count);
}
$ gcc -Wall -Wextra -std=c11 -g prime_checker.c -o prime_checker
$ ./prime_checker
(메뉴 생략)
선택: 1
숫자를 입력하세요: 97
97는 소수입니다!
(메뉴 생략)
선택: 1
숫자를 입력하세요: 91
91는 소수가 아닙니다.
91의 약수: 1 7 13 91
(약수의 개수: 4개)
(메뉴 생략)
선택: 2
시작 숫자: 1
끝 숫자: 50
===== 1 ~ 50 사이의 소수 =====
2 3 5 7 11 13 17 19 23 29
31 37 41 43 47
총 15개의 소수가 있습니다.
(메뉴 생략)
선택: 3
상한값을 입력하세요: 1000
2부터 1000까지 소수의 개수: 168개
(메뉴 생략)
선택: 4
숫자를 입력하세요: 36
36의 약수: 1 2 3 4 6 9 12 18 36
(약수의 개수: 9개)
(메뉴 생략)
선택: 0
프로그램을 종료합니다.
91은 얼핏 소수처럼 보이지만 7 × 13입니다. 이 프로그램의 핵심은 is_prime 함수입니다. 한 줄씩 봅시다.
int is_prime(int n) {
if (n < 2) return 0; // ① 0, 1, 음수는 소수가 아니다
if (n == 2) return 1; // ② 2는 유일한 짝수 소수
if (n % 2 == 0) return 0; // ③ 2가 아닌 짝수는 소수가 아니다
for (int i = 3; i * i <= n; i += 2) { // ④ 3, 5, 7, ... 홀수로만
if (n % i == 0) {
return 0; // ⑤ 나누어떨어지면 소수 아님, 바로 끝
}
}
return 1; // ⑥ 아무것도 나누지 못했으면 소수
}
return은 함수를 즉시 끝냅니다. ①~③에서 답이 나오면 나머지 줄은 실행되지 않습니다. 반복 안의 ⑤return도 반복과 함수를 한꺼번에 끝냅니다. 14절의break보다 더 강력한 탈출입니다. 이렇게 “답이 정해지는 경우를 앞에서 먼저 돌려보내는” 모양을 조기 반환(early return) 이라고 하고, 9절에서 말한 “중첩을 줄이는” 좋은 방법입니다.%가 “나누어떨어지는지”를 판단합니다. 1절의 배수 확인 그대로입니다.- ④
i += 2: ③에서 짝수를 이미 걸러 냈으니, 짝수로 나눠 볼 필요가 없습니다. 검사 횟수가 절반으로 줄어듭니다. - ④
i * i <= n: 이 조건이 이 함수의 가장 영리한 부분입니다. 왜 n 까지가 아니라 i 의 제곱이 n 을 넘을 때까지만 확인할까요? 91 = 7 × 13 처럼 n 이 두 수의 곱이라면, 둘 중 작은 쪽은 반드시 √n 이하입니다(둘 다 √n 보다 크면 곱이 n 보다 커지니까요). 그러니 √n 까지 나눠 봐서 나누어떨어지지 않으면, 그보다 큰 약수도 없습니다. 97을 판별할 때i는 3, 5, 7, 9 까지만 봅니다. 11 × 11 = 121 이 97보다 크기 때문입니다. 96번 나눠 볼 일을 4번으로 줄였습니다.
수학 함수 sqrt 를 쓰지 않고 i * i 로 비교한 것도 이유가 있습니다. sqrt 는 실수를 돌려주니 4절의 실수 비교 문제가 끼어들 수 있지만, i * i 는 정수 계산이라 정확합니다.
find_primes_in_range 의 줄바꿈: %5d 로 5칸씩 맞춰 찍다가, count % 10 == 0 일 때마다 줄을 바꿉니다. 1절의 % 가 “10개마다”를 표현합니다. 마지막의 if (count % 10 != 0) 은 “마지막 줄이 10개로 딱 떨어지지 않았으면 줄을 바꿔 준다”는 뒷정리입니다.
직접 해 보기: 3번 메뉴에 1000000을 넣어 보세요. 1초 안에 78498개라는 답이 나올 겁니다. 그다음 is_prime 의 조건을 i * i <= n 에서 i < n 으로 바꿔 다시 컴파일하고 같은 값을 넣어 보세요. 얼마나 느려지는지가 “√n 까지만”의 가치입니다. 기다리다 지치면 Ctrl + C 입니다.
프로젝트 3: 별 찍기 패턴
11절의 “별 찍기의 요령”을 그대로 적용하는 프로젝트입니다. 여덟 가지 모양을 그립니다.
star_patterns.c:
/*
* star_patterns.c - 별 찍기 패턴 모음
* 3주차 프로젝트: 연산자와 제어문
*
* 기능:
* - 직각삼각형
* - 역삼각형
* - 피라미드
* - 다이아몬드
* - 속이 빈 사각형
* - 모래시계
*/
#include <stdio.h>
#include <stdlib.h>
/*
* 입력이 끝났으면 프로그램을 정상 종료합니다.
*
* 메뉴형 프로그램은 사람이 앞에 있다고 가정합니다. 그런데 파이프나
* 파일로 돌리면 입력이 끝나고, 그 뒤로 scanf 는 계속 EOF 를 돌려줍니다.
* 그걸 '잘못된 입력' 으로만 처리하면 메뉴를 다시 찍고 또 읽으려 하면서
* 영원히 돕니다. 사람이 없으니 아무도 멈춰 주지 않죠.
*
* 이 저장소에서 실제로 그랬습니다. 스크린샷을 자동으로 만들려고
* 입력 없이 돌렸더니 **초당 0.7 GB** 를 쏟아냈고, 그 출력을 받아
* 모으던 쪽이 메모리 25 GB 를 먹고 OOM 킬러에 죽었습니다.
*
* scanf 의 반환값만으로는 '잘못된 글자' 와 '입력이 아예 없음' 을
* 구별할 수 없습니다. feof() 를 같이 봐야 합니다.
*/
static void exit_if_eof(void)
{
if (feof(stdin)) {
printf("\n입력이 끝났습니다. 종료합니다.\n");
exit(0);
}
}
/*
* 입력 버퍼를 줄 끝까지 비웁니다.
*
* EOF 를 반드시 같이 봐야 합니다. 이렇게 쓰면
*
* while (getchar() != '\n');
*
* 파이프로 입력을 주거나 파일에서 읽을 때 여기서 영원히 돕니다.
* 입력이 끝나면 getchar() 는 '\n' 이 아니라 EOF 를 계속 돌려주니까요.
* 터미널에서 직접 치면 잘 돌아가서 더 찾기 어려운 버그입니다.
*/
static void flush_line(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
// 함수 선언
void pattern_right_triangle(int n);
void pattern_inverted_triangle(int n);
void pattern_right_aligned(int n);
void pattern_pyramid(int n);
void pattern_inverted_pyramid(int n);
void pattern_diamond(int n);
void pattern_hollow_rectangle(int width, int height);
void pattern_hourglass(int n);
void print_header(const char *title);
int main(void) {
int choice;
int size, width, height;
do {
printf("\n");
print_header("별 찍기 패턴");
printf("1. 직각삼각형 (좌측 정렬)\n");
printf("2. 직각삼각형 (우측 정렬)\n");
printf("3. 역삼각형\n");
printf("4. 피라미드\n");
printf("5. 역피라미드\n");
printf("6. 다이아몬드\n");
printf("7. 속이 빈 사각형\n");
printf("8. 모래시계\n");
printf("0. 종료\n");
printf("-----------------------------\n");
printf("선택: ");
if (scanf("%d", &choice) != 1) {
exit_if_eof();
printf("잘못된 입력입니다.\n");
flush_line();
continue;
}
switch (choice) {
case 1:
printf("크기 (1-20): ");
if (scanf("%d", &size) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (size >= 1 && size <= 20) {
pattern_right_triangle(size);
} else {
printf("1-20 사이의 숫자를 입력하세요.\n");
}
break;
case 2:
printf("크기 (1-20): ");
if (scanf("%d", &size) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (size >= 1 && size <= 20) {
pattern_right_aligned(size);
} else {
printf("1-20 사이의 숫자를 입력하세요.\n");
}
break;
case 3:
printf("크기 (1-20): ");
if (scanf("%d", &size) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (size >= 1 && size <= 20) {
pattern_inverted_triangle(size);
} else {
printf("1-20 사이의 숫자를 입력하세요.\n");
}
break;
case 4:
printf("크기 (1-15): ");
if (scanf("%d", &size) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (size >= 1 && size <= 15) {
pattern_pyramid(size);
} else {
printf("1-15 사이의 숫자를 입력하세요.\n");
}
break;
case 5:
printf("크기 (1-15): ");
if (scanf("%d", &size) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (size >= 1 && size <= 15) {
pattern_inverted_pyramid(size);
} else {
printf("1-15 사이의 숫자를 입력하세요.\n");
}
break;
case 6:
printf("크기 (1-15): ");
if (scanf("%d", &size) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (size >= 1 && size <= 15) {
pattern_diamond(size);
} else {
printf("1-15 사이의 숫자를 입력하세요.\n");
}
break;
case 7:
printf("가로 (3-30): ");
if (scanf("%d", &width) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
printf("세로 (3-20): ");
if (scanf("%d", &height) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (width >= 3 && width <= 30 && height >= 3 && height <= 20) {
pattern_hollow_rectangle(width, height);
} else {
printf("유효한 범위를 입력하세요.\n");
}
break;
case 8:
printf("크기 (1-15): ");
if (scanf("%d", &size) != 1) {
exit_if_eof();
flush_line();
printf("숫자를 입력하세요.\n");
break;
}
if (size >= 1 && size <= 15) {
pattern_hourglass(size);
} else {
printf("1-15 사이의 숫자를 입력하세요.\n");
}
break;
case 0:
printf("프로그램을 종료합니다.\n");
break;
default:
printf("잘못된 선택입니다.\n");
}
} while (choice != 0);
return 0;
}
void print_header(const char *title) {
printf("=============================\n");
printf(" %s\n", title);
printf("=============================\n");
}
// 1. 직각삼각형 (좌측 정렬)
void pattern_right_triangle(int n) {
printf("\n직각삼각형 (좌측 정렬, 크기: %d)\n\n", n);
for (int i = 1; i <= n; i++) {
for (int j = 0; j < i; j++) {
printf("*");
}
printf("\n");
}
}
// 2. 직각삼각형 (우측 정렬)
void pattern_right_aligned(int n) {
printf("\n직각삼각형 (우측 정렬, 크기: %d)\n\n", n);
for (int i = 1; i <= n; i++) {
// 공백 출력
for (int j = 0; j < n - i; j++) {
printf(" ");
}
// 별 출력
for (int j = 0; j < i; j++) {
printf("*");
}
printf("\n");
}
}
// 3. 역삼각형
void pattern_inverted_triangle(int n) {
printf("\n역삼각형 (크기: %d)\n\n", n);
for (int i = n; i >= 1; i--) {
for (int j = 0; j < i; j++) {
printf("*");
}
printf("\n");
}
}
// 4. 피라미드
void pattern_pyramid(int n) {
printf("\n피라미드 (크기: %d)\n\n", n);
for (int i = 1; i <= n; i++) {
// 공백 출력
for (int j = 0; j < n - i; j++) {
printf(" ");
}
// 별 출력 (2*i - 1개)
for (int j = 0; j < 2 * i - 1; j++) {
printf("*");
}
printf("\n");
}
}
// 5. 역피라미드
void pattern_inverted_pyramid(int n) {
printf("\n역피라미드 (크기: %d)\n\n", n);
for (int i = n; i >= 1; i--) {
// 공백 출력
for (int j = 0; j < n - i; j++) {
printf(" ");
}
// 별 출력 (2*i - 1개)
for (int j = 0; j < 2 * i - 1; j++) {
printf("*");
}
printf("\n");
}
}
// 6. 다이아몬드
void pattern_diamond(int n) {
printf("\n다이아몬드 (크기: %d)\n\n", n);
// 상단 절반 (피라미드)
for (int i = 1; i <= n; i++) {
for (int j = 0; j < n - i; j++) {
printf(" ");
}
for (int j = 0; j < 2 * i - 1; j++) {
printf("*");
}
printf("\n");
}
// 하단 절반 (역피라미드, 가운데 줄 제외)
for (int i = n - 1; i >= 1; i--) {
for (int j = 0; j < n - i; j++) {
printf(" ");
}
for (int j = 0; j < 2 * i - 1; j++) {
printf("*");
}
printf("\n");
}
}
// 7. 속이 빈 사각형
void pattern_hollow_rectangle(int width, int height) {
printf("\n속이 빈 사각형 (%dx%d)\n\n", width, height);
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
// 첫 행, 마지막 행, 첫 열, 마지막 열만 별
if (i == 0 || i == height - 1 || j == 0 || j == width - 1) {
printf("*");
} else {
printf(" ");
}
}
printf("\n");
}
}
// 8. 모래시계
void pattern_hourglass(int n) {
printf("\n모래시계 (크기: %d)\n\n", n);
// 상단 (역피라미드)
for (int i = n; i >= 1; i--) {
for (int j = 0; j < n - i; j++) {
printf(" ");
}
for (int j = 0; j < 2 * i - 1; j++) {
printf("*");
}
printf("\n");
}
// 하단 (피라미드, 꼭지점 제외)
for (int i = 2; i <= n; i++) {
for (int j = 0; j < n - i; j++) {
printf(" ");
}
for (int j = 0; j < 2 * i - 1; j++) {
printf("*");
}
printf("\n");
}
}

별 찍기
$ gcc -Wall -Wextra -std=c11 -g star_patterns.c -o star_patterns
$ ./star_patterns
(메뉴 생략)
선택: 4
크기 (1-15): 5
피라미드 (크기: 5)
*
***
*****
*******
*********
(메뉴 생략)
선택: 6
크기 (1-15): 4
다이아몬드 (크기: 4)
*
***
*****
*******
*****
***
*
(메뉴 생략)
선택: 7
가로 (3-30): 10
세로 (3-20): 4
속이 빈 사각형 (10x4)
**********
* *
* *
**********
(메뉴 생략)
선택: 8
크기 (1-15): 4
모래시계 (크기: 4)
*******
*****
***
*
***
*****
*******
(메뉴 생략)
선택: 0
프로그램을 종료합니다.
피라미드를 표로 분석하기. 크기 5짜리 피라미드를 줄마다 세어 봅시다.
| 줄 (i) | 공백 수 | 별 수 |
|---|---|---|
| 1 | 4 | 1 |
| 2 | 3 | 3 |
| 3 | 2 | 5 |
| 4 | 1 | 7 |
| 5 | 0 | 9 |
공백은 줄마다 하나씩 줄어드니 n - i, 별은 1, 3, 5, 7, 9 로 2씩 늘어나는 홀수이니 2 * i - 1 입니다. pattern_pyramid 의 두 안쪽 반복 조건이 정확히 이 두 식입니다. 나머지 모양도 같은 방법으로 읽을 수 있습니다.
- 역피라미드: 같은 두 식에
i만n부터 1까지 거꾸로 돌립니다. - 다이아몬드: 피라미드(1 ~ n)에 역피라미드를 붙이되, 가운데 줄이 두 번 찍히지 않도록 역피라미드를
n - 1부터 시작합니다. - 모래시계: 다이아몬드를 뒤집은 것입니다. 역피라미드 다음에 피라미드를 2부터 붙여 꼭짓점이 겹치지 않게 합니다.
- 속이 빈 사각형: 모든 칸을 돌면서 “첫 행, 마지막 행, 첫 열, 마지막 열 중 하나냐?”를
||로 이어 네 조건을 한 번에 묻습니다. 테두리면 별, 아니면 공백. 5절 논리 연산자의 좋은 예입니다.
직접 해 보기: “9. 숫자 피라미드”를 추가해서 크기 4일 때 이렇게 찍어 보세요. 표부터 만드세요.
1
121
12321
1234321
프로젝트 4: 메뉴 시스템
지금까지 배운 것을 모두 모은 가장 긴 프로그램입니다. 계산기, 숫자 맞추기 게임, 통계 계산기 세 가지가 들어 있습니다.
menu_system.c:
/*
* menu_system.c - 메뉴 기반 미니 프로그램 모음
* 3주차 프로젝트: 연산자와 제어문
*
* 기능:
* - 간단 계산기
* - 숫자 맞추기 게임
* - 통계 계산기
*/
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/*
* 입력이 끝났으면 프로그램을 정상 종료합니다.
*
* 메뉴형 프로그램은 사람이 앞에 있다고 가정합니다. 그런데 파이프나
* 파일로 돌리면 입력이 끝나고, 그 뒤로 scanf 는 계속 EOF 를 돌려줍니다.
* 그걸 '잘못된 입력' 으로만 처리하면 메뉴를 다시 찍고 또 읽으려 하면서
* 영원히 돕니다. 사람이 없으니 아무도 멈춰 주지 않죠.
*
* 이 저장소에서 실제로 그랬습니다. 스크린샷을 자동으로 만들려고
* 입력 없이 돌렸더니 **초당 0.7 GB** 를 쏟아냈고, 그 출력을 받아
* 모으던 쪽이 메모리 25 GB 를 먹고 OOM 킬러에 죽었습니다.
*
* scanf 의 반환값만으로는 '잘못된 글자' 와 '입력이 아예 없음' 을
* 구별할 수 없습니다. feof() 를 같이 봐야 합니다.
*/
static void exit_if_eof(void)
{
if (feof(stdin)) {
printf("\n입력이 끝났습니다. 종료합니다.\n");
exit(0);
}
}
/*
* 입력 버퍼를 줄 끝까지 비웁니다.
*
* EOF 를 반드시 같이 봐야 합니다. 이렇게 쓰면
*
* while (getchar() != '\n');
*
* 파이프로 입력을 주거나 파일에서 읽을 때 여기서 영원히 돕니다.
* 입력이 끝나면 getchar() 는 '\n' 이 아니라 EOF 를 계속 돌려주니까요.
* 터미널에서 직접 치면 잘 돌아가서 더 찾기 어려운 버그입니다.
*/
static void flush_line(void)
{
int c;
while ((c = getchar()) != '\n' && c != EOF)
;
}
// 함수 선언
void clear_screen(void);
void wait_for_enter(void);
void show_main_menu(void);
void calculator(void);
void number_game(void);
void statistics(void);
int main(void) {
int choice;
do {
clear_screen();
show_main_menu();
printf("\n선택: ");
if (scanf("%d", &choice) != 1) {
exit_if_eof();
printf("잘못된 입력입니다.\n");
flush_line();
wait_for_enter();
continue;
}
switch (choice) {
case 1:
calculator();
break;
case 2:
number_game();
break;
case 3:
statistics();
break;
case 0:
printf("\n프로그램을 종료합니다. 안녕히 가세요!\n");
break;
default:
printf("\n잘못된 선택입니다. 다시 선택해주세요.\n");
wait_for_enter();
}
} while (choice != 0);
return 0;
}
void clear_screen(void) {
// ANSI 이스케이프 코드로 화면 지우기
printf("\033[2J\033[H");
}
void wait_for_enter(void) {
int c;
printf("\n계속하려면 Enter를 누르세요...");
// 버퍼에 남은 입력과 Enter를 한 번에 소비 (두 번 눌러야 하는 문제 방지)
while ((c = getchar()) != '\n' && c != EOF);
}
void show_main_menu(void) {
printf("======================================\n");
printf(" 미니 프로그램 모음 v1.0\n");
printf("======================================\n");
printf("\n");
printf(" 1. 계산기\n");
printf(" 2. 숫자 맞추기 게임\n");
printf(" 3. 통계 계산기\n");
printf(" 0. 종료\n");
printf("\n");
printf("--------------------------------------\n");
}
void calculator(void) {
double num1, num2, result;
char op;
char again;
do {
clear_screen();
printf("===================================\n");
printf(" 간단 계산기\n");
printf("===================================\n\n");
printf("첫 번째 숫자: ");
if (scanf("%lf", &num1) != 1) {
exit_if_eof();
printf("잘못된 입력입니다.\n");
flush_line();
wait_for_enter();
continue;
}
printf("연산자 (+, -, *, /, %%): ");
if (scanf(" %c", &op) != 1) {
exit_if_eof();
}
printf("두 번째 숫자: ");
if (scanf("%lf", &num2) != 1) {
exit_if_eof();
printf("잘못된 입력입니다.\n");
flush_line();
wait_for_enter();
continue;
}
int valid = 1;
switch (op) {
case '+':
result = num1 + num2;
break;
case '-':
result = num1 - num2;
break;
case '*':
result = num1 * num2;
break;
case '/':
if (num2 == 0) {
printf("\n[오류] 0으로 나눌 수 없습니다!\n");
valid = 0;
} else {
result = num1 / num2;
}
break;
case '%':
if ((int)num2 == 0) {
printf("\n[오류] 0으로 나눌 수 없습니다!\n");
valid = 0;
} else {
result = (int)num1 % (int)num2;
}
break;
default:
printf("\n[오류] 지원하지 않는 연산자입니다: %c\n", op);
valid = 0;
}
if (valid) {
printf("\n-----------------------------------\n");
printf("결과: %.2f %c %.2f = %.2f\n", num1, op, num2, result);
printf("-----------------------------------\n");
}
printf("\n계속하시겠습니까? (y/n): ");
if (scanf(" %c", &again) != 1) {
exit_if_eof();
}
} while (again == 'y' || again == 'Y');
}
void number_game(void) {
int secret, guess, attempts;
char again;
int max_num, min_num;
// 간단한 난수 생성 (시드 기반)
static unsigned int seed = 12345;
do {
clear_screen();
printf("===================================\n");
printf(" 숫자 맞추기 게임\n");
printf("===================================\n\n");
// 난이도 선택
printf("난이도를 선택하세요:\n");
printf("1. 쉬움 (1-50)\n");
printf("2. 보통 (1-100)\n");
printf("3. 어려움 (1-200)\n");
printf("선택: ");
int difficulty;
if (scanf("%d", &difficulty) != 1) {
exit_if_eof();
flush_line();
difficulty = 2; // 잘못 입력하면 '보통' 으로
}
switch (difficulty) {
case 1:
min_num = 1;
max_num = 50;
break;
case 2:
min_num = 1;
max_num = 100;
break;
case 3:
min_num = 1;
max_num = 200;
break;
default:
min_num = 1;
max_num = 100;
}
// 난수 생성
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
secret = (seed % (max_num - min_num + 1)) + min_num;
printf("\n%d ~ %d 사이의 숫자를 맞춰보세요!\n\n", min_num, max_num);
attempts = 0;
do {
printf("추측: ");
if (scanf("%d", &guess) != 1) {
exit_if_eof();
printf("숫자를 입력해주세요.\n");
flush_line();
continue;
}
attempts++;
if (guess < min_num || guess > max_num) {
printf("[범위 밖] %d ~ %d 사이의 숫자를 입력하세요.\n", min_num, max_num);
} else if (guess < secret) {
printf("더 큰 숫자입니다. ↑\n");
} else if (guess > secret) {
printf("더 작은 숫자입니다. ↓\n");
} else {
printf("\n===================================\n");
printf(" 정답입니다! %d번 만에 맞추셨습니다!\n", attempts);
printf("===================================\n");
if (attempts <= 3) {
printf(" 대단해요! 당신은 천재입니다!\n");
} else if (attempts <= 5) {
printf(" 훌륭합니다! 센스가 좋으시네요!\n");
} else if (attempts <= 7) {
printf(" 잘 하셨어요!\n");
} else {
printf(" 다음엔 더 잘 할 수 있을 거예요!\n");
}
}
} while (guess != secret);
printf("\n다시 하시겠습니까? (y/n): ");
if (scanf(" %c", &again) != 1) {
exit_if_eof();
}
} while (again == 'y' || again == 'Y');
}
void statistics(void) {
double numbers[100];
int count = 0;
double sum = 0, min = 0, max = 0, input;
char again;
do {
clear_screen();
printf("===================================\n");
printf(" 통계 계산기\n");
printf("===================================\n\n");
printf("숫자를 입력하세요 (최대 100개)\n");
printf("입력을 마치려면 0을 입력하세요.\n\n");
count = 0;
sum = 0;
while (count < 100) {
printf("숫자 %d: ", count + 1);
if (scanf("%lf", &input) != 1) {
exit_if_eof();
printf("잘못된 입력입니다. 다시 입력하세요.\n");
flush_line();
continue;
}
if (input == 0 && count > 0) {
break;
}
if (input == 0 && count == 0) {
printf("최소 하나의 숫자를 입력하세요.\n");
continue;
}
numbers[count] = input;
sum += input;
if (count == 0) {
min = max = input;
} else {
if (input < min) min = input;
if (input > max) max = input;
}
count++;
}
if (count >= 100) {
printf("\n최대 입력 개수(100개)에 도달했습니다.\n");
}
// 결과 출력
printf("\n===================================\n");
printf(" 통계 결과\n");
printf("===================================\n");
printf("입력 개수: %d개\n", count);
printf("합계: %.2f\n", sum);
printf("평균: %.2f\n", sum / count);
printf("최솟값: %.2f\n", min);
printf("최댓값: %.2f\n", max);
printf("범위: %.2f\n", max - min);
printf("===================================\n");
// 입력값 목록
printf("\n입력한 값들: ");
for (int i = 0; i < count; i++) {
printf("%.1f", numbers[i]);
if (i < count - 1) printf(", ");
}
printf("\n");
printf("\n다시 하시겠습니까? (y/n): ");
if (scanf(" %c", &again) != 1) {
exit_if_eof();
}
} while (again == 'y' || again == 'Y');
}
$ gcc -Wall -Wextra -std=c11 -g menu_system.c -o menu_system
$ ./menu_system
이 프로그램은 화면을 지우면서 진행하므로, 화면 단위로 나눠서 보여 드리겠습니다.
======================================
미니 프로그램 모음 v1.0
======================================
1. 계산기
2. 숫자 맞추기 게임
3. 통계 계산기
0. 종료
--------------------------------------
선택: 1
===================================
간단 계산기
===================================
첫 번째 숫자: 7
연산자 (+, -, *, /, %): /
두 번째 숫자: 2
-----------------------------------
결과: 7.00 / 2.00 = 3.50
-----------------------------------
계속하시겠습니까? (y/n): n
===================================
숫자 맞추기 게임
===================================
난이도를 선택하세요:
1. 쉬움 (1-50)
2. 보통 (1-100)
3. 어려움 (1-200)
선택: 2
1 ~ 100 사이의 숫자를 맞춰보세요!
추측: 50
더 작은 숫자입니다. ↓
추측: 25
더 작은 숫자입니다. ↓
추측: 12
더 작은 숫자입니다. ↓
추측: 6
더 큰 숫자입니다. ↑
추측: 9
더 작은 숫자입니다. ↓
추측: 7
===================================
정답입니다! 6번 만에 맞추셨습니다!
===================================
잘 하셨어요!
다시 하시겠습니까? (y/n): n
===================================
통계 계산기
===================================
숫자를 입력하세요 (최대 100개)
입력을 마치려면 0을 입력하세요.
숫자 1: 4
숫자 2: 8
숫자 3: 15
숫자 4: 16
숫자 5: 23
숫자 6: 42
숫자 7: 0
===================================
통계 결과
===================================
입력 개수: 6개
합계: 108.00
평균: 18.00
최솟값: 4.00
최댓값: 42.00
범위: 38.00
===================================
입력한 값들: 4.0, 8.0, 15.0, 16.0, 23.0, 42.0
다시 하시겠습니까? (y/n): n
코드 읽기 포인트
- 화면 지우기
printf("\033[2J\033[H")(105번째 줄): 1주차 6절의 이스케이프 시퀀스가 한 단계 발전한 것입니다.\033은 8진수 33, 즉 ESC(Escape) 문자입니다. 터미널은 ESC 뒤에[로 시작하는 글자가 오면 출력이 아니라 명령으로 받아들입니다.[2J는 “화면 전체 지우기”,[H는 “커서를 맨 왼쪽 위로”. 1주차에서\r이 “커서를 줄 맨 앞으로” 보내는 신호였던 것처럼, 이것도 터미널에 보내는 신호입니다. 26주차 TUI 프로그래밍에서 이 명령들로 화면을 자유롭게 그립니다. - 계산기의
switch (op): 문자'+','-'로 갈라집니다(10절의 문자switch)./에서는 0으로 나누기를,%에서는(int)num2 == 0을 먼저 검사합니다. 1절에서 정수를 0으로 나누면 프로그램이 죽는 것을 봤으니까요.%는 정수에만 쓸 수 있어서(1절 실험),(int)num1 % (int)num2처럼 형 변환해서 계산합니다. - 숫자 맞추기의 “난수”: 256번째 줄의
seed = (seed * 1103515245 + 12345) & 0x7fffffff;가 난수를 만드는 공식입니다. 선형 합동 생성기(LCG) 라는 오래된 방법인데, 공식이 정해져 있으니 시작 값(seed)이 같으면 나오는 수도 항상 같습니다. 이 프로그램은seed가 12345에서 시작하므로, 프로그램을 새로 실행하면 첫 게임의 정답은 언제나 7입니다(위 실행이 7이었던 이유). 진짜로 매번 다른 수가 필요하면<stdlib.h>의rand와<time.h>의time으로 시작 값을 현재 시각으로 정하는 방법을 쓰는데, 뒤의 주차에서 다룹니다.& 0x7fffffff는 6절의 비트 AND 로, 맨 앞 비트를 0으로 만들어 결과를 양수로 유지합니다. - 최적의 전략: 위 실행처럼 매번 범위의 한가운데를 부르면(50 → 25 → 12 → 6 → 9 → 7) 1~100 사이 어떤 수든 최대 7번 안에 맞힐 수 있습니다. 한 번에 후보가 절반씩 줄어들기 때문입니다(100 → 50 → 25 → … → 1). 이것이 이진 탐색(binary search) 이고, 4주차 이후 정렬된 배열에서 값을 찾을 때 다시 만납니다.
- 통계 계산기의
min,max: 첫 입력(count == 0)일 때min = max = input으로 첫 값을 기준으로 삼고, 그 뒤로 더 작거나 큰 값이 오면 바꿉니다. 3절의a = b = c연쇄 대입이 여기서 쓰였습니다. 입력한 값들은 4주차에서 배울 배열numbers[100]에 저장해 두었다가 마지막에 다시 찍습니다. wait_for_enter(): 잘못된 선택을 했을 때 “계속하려면 Enter” 로 잠시 멈춥니다. 화면을 바로 지워 버리면 오류 메시지를 읽을 틈이 없기 때문입니다.
직접 해 보기:
- 계산기에 거듭제곱 연산자
^를 추가해 보세요. (6절에서 봤듯 C의^는 XOR 이지 거듭제곱이 아닙니다! 여기서는 입력 문자로만 쓰고, 계산은 반복문으로num2번 곱하면 됩니다.) - 숫자 맞추기에서 7번 안에 못 맞히면 “정답은 N 이었습니다”를 보여 주고 끝내 보세요.
do-while의 조건에&& attempts < 7을 더하면 됩니다.
17. 연습 문제
코드를 보고 이해하는 것과 직접 짜는 것은 다른 능력입니다. 아래 문제는 모두 이번 주에 배운 것만으로 풀 수 있습니다. 막히면 힌트를 보되, 먼저 10분은 혼자 고민해 보세요.
기본 문제
- FizzBuzz: 1부터 100까지 출력하되, 3의 배수는 “Fizz”, 5의 배수는 “Buzz”, 15의 배수는 “FizzBuzz”를 대신 출력하세요.
힌트: 9절의 “순서 실험”을 떠올리세요. 15의 배수는 3의 배수이기도 합니다. 어떤 조건을 먼저 검사해야 할까요?
- 팩토리얼: 입력받은 n 에 대해 n! = n × (n−1) × … × 1 을 계산하세요. 13!을 넣으면 어떻게 되나요?
힌트: 합계는 0에서 시작하지만 곱은 1에서 시작합니다. 13! 은 약 62억으로
int의 끝을 넘습니다(1절의 오버플로).long long과%lld로 바꿔 보세요. - 피보나치: n 번째 피보나치 수를 출력하세요(1, 1, 2, 3, 5, 8, 13, …).
힌트: 12절
while_examples.c의a,b,temp를for로 n 번 돌리세요. - 약수 구하기: 입력받은 수의 모든 약수를 출력하세요.
힌트: 프로젝트 2의
print_divisors. 그런데 √n 까지만 돌면서 약수를 짝으로 찾을 수도 있을까요? - 완전수 판별: 자기 자신을 뺀 약수의 합이 자기 자신과 같은 수를 완전수라고 합니다(6 = 1 + 2 + 3). 10000 이하의 완전수를 모두 찾으세요.
힌트: 중첩 반복. 답은 네 개입니다.
심화 문제
- 이진수 변환: 10진수를 입력받아 2진수로 출력하세요.
힌트: 1절의 자릿수 분리에서 10 대신 2로 나누면 됩니다. 거꾸로 나오는 문제는 6절
print_binary처럼 시프트로 윗자리부터 꺼내면 해결됩니다. - 최대공약수와 최소공배수: 두 수의 GCD 와 LCM 을 구하세요.
힌트: 유클리드 호제법. “a 를 b 로 나눈 나머지로 바꾸기”를 나머지가 0이 될 때까지
while로 반복합니다. LCM = a × b ÷ GCD. - 숫자 뒤집기: 12345 를 입력하면 54321 을 출력하세요.
힌트:
% 10으로 떼어 낸 자리를result = result * 10 + 자리로 쌓습니다. - 팰린드롬 수: 12321 처럼 뒤집어도 같은 수인지 판별하세요.
힌트: 8번의 결과를 원래 수와 비교합니다. 원래 수는 미리 복사해 두세요(1절
original). - 별 찍기 응용: 프로젝트 3의 “숫자 피라미드”, 그리고 나비 모양을 만들어 보세요.
힌트: 표를 먼저. 한 줄이 “별, 공백, 별” 세 덩어리입니다.
* *
** **
*** ***
********
*** ***
** **
* *
18. 마무리
이번 주에 배운 것
연산자
| 종류 | 연산자 | 꼭 기억할 것 |
|---|---|---|
| 산술 | + - * / % |
정수끼리 나누면 몫만. 0 쪽으로 버림. % 는 정수 전용. 정수를 0으로 나누면 프로그램이 죽음 |
| 증감 | ++ -- |
앞이면 먼저 바꾸고, 뒤면 나중에 바꿈. 독립된 문장으로만 쓰기 |
| 대입 | = += -= … |
“같다”가 아니라 “넣어라”. 대입도 값을 가진 식 |
| 관계 | == != < > <= >= |
결과는 1 또는 0. 실수는 == 로 비교하지 않기. 1 < x < 5 는 수학이 아님 |
| 논리 | && \|\| ! |
0만 거짓. 단락 평가로 “안전 확인 후 실행” |
| 비트 | & \| ^ ~ << >> |
한 자리씩. 플래그 켜기/끄기/확인/뒤집기. unsigned 에 쓰기 |
| 3항 | ? : |
한눈에 읽히면 3항, 아니면 if |
| 우선순위 | 비트 연산자는 비교보다 낮다. 헷갈리면 괄호 |
제어문
| 제어문 | 쓰는 곳 | 꼭 기억할 것 |
|---|---|---|
if / else if / else |
조건에 따라 갈림길 | 처음 참인 것 하나만. 순서가 중요. 한 줄이라도 { } |
switch |
한 값을 여러 상수와 비교 | break 없으면 아래로 떨어짐. 정수 상수만 |
for |
횟수가 정해진 반복 | 초기화 한 번, 조건은 매번 먼저, 증감은 매번 나중 |
while |
조건에 따라 끝나는 반복 | 몸통에서 조건 변수를 바꿔야 끝남 |
do-while |
최소 한 번 실행 | 입력 검증. 끝에 ; |
break / continue |
반복 탈출 / 건너뛰기 | 가장 가까운 반복(또는 switch) 하나에만 |
GCC 가 잡아 준 실수들
이번 주에 -Wall -Wextra 가 알려 준 경고를 모아 보면, 그대로 초보자 실수 목록이 됩니다.
| 경고 이름 | 무엇을 잡았나 | 절 |
|---|---|---|
-Wdiv-by-zero |
상수 0으로 나누기 | 1 |
-Wsequence-point |
i = i++ |
2 |
-Wparentheses |
if (a = 5), 1 < x < 5, 5 & 3 == 1 |
4, 8 |
-Wbool-compare |
참/거짓 값을 상수와 비교 | 4 |
-Wmisleading-indentation |
중괄호 없는 if, for 뒤 세미콜론 |
9, 11 |
-Wdangling-else |
누구 짝인지 모호한 else |
9 |
-Wimplicit-fallthrough |
switch 의 break 누락 |
10 |
-Wtype-limits |
unsigned 가 0 이상인지 비교 |
11 |
그리고 컴파일러가 잡지 못한 것도 기억하세요. 변수로 0을 나누기, else if 의 순서, 증감을 빼먹은 무한 루프, 입력이 끝났을 때의 무한 반복은 모두 경고 없이 컴파일됐습니다. 경고가 0개라고 버그가 0개인 것은 아닙니다. 그래서 직접 실행해 보고, 이상한 입력(abc, 음수, Ctrl + D)도 넣어 보는 습관이 필요합니다.
다음 주 예고
이번 주 예제에 여러 번 “4주차에서 배운다”며 넘어간 것이 있습니다. int numbers[] = {3, 7, 2, 9}; 같은 배열, 그리고 sizeof(numbers) / sizeof(numbers[0]) 라는 수수께끼 같은 식입니다. 다음 주에는 이것들의 정체를 밝힙니다.
- 배열은 메모리에 어떻게 놓일까? 왜 번호가 1이 아니라 0부터 시작할까?
- 배열의 끝을 넘어서 쓰면 무슨 일이 생길까? (C에서 가장 위험한 버그의 시작입니다)
- 문자열은 사실
char배열이었다. 1주차의\0은 거기서 어떤 역할을 할까? "안녕"의 길이는 왜 2가 아니라 6일까?
이번 주의 반복문은 다음 주 내내 배열과 짝을 이뤄 쓰입니다. 특히 for (int i = 0; i < size; i++) 모양은 손에 익을 때까지 연습해 두세요.
19. 체크리스트
각 항목을 설명할 수 있으면 체크하세요.
- [ ]
17 / 5와17 % 5의 값, 그리고-7 / 2가 -3인 이유를 안다 - [ ] 평균을 구할 때 정수 나눗셈을 피하는 방법(형 변환)을 안다
- [ ] 정수를 0으로 나누면 무슨 일이 생기는지, 종료 코드 136의 뜻을 안다
- [ ]
b = ++a와b = a++의 차이를 설명할 수 있다 - [ ]
=와==를 헷갈렸을 때 무슨 일이 생기는지 안다 - [ ]
0.1 + 0.2 == 0.3이 거짓인 이유와, 실수를 비교하는 방법을 안다 - [ ] C에서 참과 거짓이 무엇인지 안다 (0만 거짓)
- [ ] 단락 평가로 0 나누기를 막는 코드를 쓸 수 있다
- [ ] 12 & 10, 12 | 10, 12 ^ 10 을 손으로 계산할 수 있다
- [ ] 플래그를 켜고, 끄고, 확인하고, 뒤집는 비트 연산을 쓸 수 있다
- [ ]
5 & 3 == 1이 0인 이유를 우선순위로 설명할 수 있다 - [ ]
else if사슬에서 조건의 순서가 왜 중요한지 안다 - [ ] 중괄호를 생략하면 생기는 두 가지 함정을 안다
- [ ]
switch에서break를 빼면 무슨 일이 생기는지, 일부러 빼는 경우는 언제인지 안다 - [ ]
for의 세 칸이 각각 언제 실행되는지 추적할 수 있다 - [ ]
for,while,do-while을 상황에 맞게 고를 수 있다 - [ ] 중첩 반복으로 구구단과 별 피라미드를 그릴 수 있다
- [ ]
break와continue의 차이를 안다 - [ ] 무한 루프를
Ctrl + C로 멈출 수 있고, 반복문을 쓸 때 무한 루프를 피하는 세 가지 확인을 안다 - [ ] 입력을 받는 반복에서
scanf의 반환값과feof를 왜 확인해야 하는지 안다
모두 체크했다면 4주차로 넘어갈 준비가 되었습니다!