31 결정 — if와 switch
먼저 알아야 할 것
돌아보기
12장에서 “if 는 공짜가 아니다”라는 감각을 예고했다 — 분기 예측이 틀리면 파이프라인을 비우는 벌금을 문다고. 그러면 프로그래머는 분기를 피해야 하는가?
답. 아니다 — 분기는 프로그램의 뼈대이고, 예측기는 일상 코드에서 90% 넘게 맞힌다. 그 감각의 쓸모는 “분기를 쓰지 말라”가 아니라 분기의 비용이 일정하지 않음을 아는 것이다 — 규칙적인 갈림길은 싸고, 무작위한 갈림길은 비싸다(정렬된 배열 사건). 성능이 문제 되는 극히 일부 코드에서만 꺼내는 지식이고, 오늘 배우는 분기는 마음껏 쓰면 된다.
이 장의 필요성과 맥락
이 장이 끝나면
if와, 여러 갈래를 한 번에 벌이는 switch — 두 분기 장치를 익힌다. 4장의 계산 모델(“결과가 0이면 건너뛰어라”)이 드디어 C 문법의 얼굴로 나타나는 장이다.이 장에서 답할 질문
- 갈래가 하나뿐이면 중괄호를 생략해도 된다고 들었다 — 써도 되는가?
if의 사다리로도 다 되는데,switch가 굳이 필요한 이유는 무엇인가?
31.1 if — 조건이 참이면
문법은 읽는 그대로다:
if (조건) {
조건이 참일 때의 문장들
} else {
거짓일 때의 문장들 /* else 부분은 없어도 된다 */
}조건 자리에는 bool이 되는 수식(30장)이 오고, 갈래마다 블록(19장)이 온다. 갈래가 여럿이면 else if로 사다리를 만든다 — 시연이 그 모습이다. 25장의 입력 관행(fgets+sscanf)이 다시 나오는 것도 눈여겨보면 된다.
examples/ch31/grade.c
#include <stdio.h>
int main(void)
{
char line[100];
int score = 0;
fgets(line, sizeof line, stdin);
sscanf(line, "%d", &score);
if (score >= 90) {
printf("%d: excellent\n", score);
} else if (score >= 80) {
printf("%d: good\n", score);
} else if (score >= 70) {
printf("%d: fair\n", score);
} else {
printf("%d: needs work\n", score);
}
return 0;
}
표준 입력으로 준 것
87
실행 결과
87: good
사다리는 위에서부터 차례로 검사되고, 처음 참이 되는 갈래 하나만 실행된다 — 87점이 “우”에서 멈추고 아래로 내려가지 않는 것이 그 보장이다. 그래서 사다리의 순서가 곧 논리다: 좁은 조건을 위에, 넓은 조건을 아래에 두는 것이 정석이다.
문. 갈래가 하나뿐이면 중괄호를 생략해도 된다고 들었다 — 써도 되는가?
답. 문법상 허용된다 — 그리고 이 책은 쓰지 않기를 권한다. 중괄호 없는 분기는 “다음 문장 하나만” 갈래에 속하는데, 나중에 문장을 한 줄 보태며 중괄호 없음을 잊는 실수가 고전적 사고 경로다. 실제로 애플의 SSL 라이브러리에서 이 무늬로 검증 코드가 분기 밖으로 밀려나 보안 검사가 통째로 건너뛰어진 사건(“goto fail” 사고, 2014)이 있었다1 — 들여쓰기는 사람 눈에만 보이고 컴파일러에게는 안 보인다는 것(19장)이 값비싸게 증명된 날이다. 갈래에는 언제나 중괄호 — 이 책의 전 예제가 따르는 수칙이다.
31.2 switch — 값으로 갈라서는 판
갈림길이 “어느 범위인가”가 아니라 “어느 값인가”로 갈릴 때는 전용 장치가 따로 있다 — switch다.
examples/ch31/season.c
#include <stdio.h>
int main(void)
{
char line[100];
int month = 0;
fgets(line, sizeof line, stdin);
sscanf(line, "%d", &month);
switch (month) {
case 12:
case 1:
case 2: /* 세 case가 하나의 답을 공유한다(폴스루) */
printf("month %d: winter\n", month);
break;
case 3:
case 4:
case 5:
printf("month %d: spring\n", month);
break;
case 6:
case 7:
case 8:
printf("month %d: summer\n", month);
break;
case 9:
case 10:
case 11:
printf("month %d: autumn\n", month);
break;
default:
printf("month %d: no such month\n", month);
break;
}
return 0;
}
표준 입력으로 준 것
12
실행 결과
month 12: winter
읽는 법 — switch (month)가 값을 하나 들고 판에 들어서면, 실행은 그 값과 일치하는 case 이름표로 건너뛴다. 그리고 여기가 switch의 가장 중요한 성질이다: case는 담장이 아니라 이름표라서, 일단 건너뛴 뒤에는 다음 case를 지나쳐 아래로 계속 흐른다. 이 흐름을 폴스루(fall-through)라 부르고, 멈추고 싶은 자리에 break(판을 나가라)를 명시한다.
폴스루는 양날이다. 시연처럼 여러 값이 한 답을 공유하게 묶는 데는 제격이지만(12·1·2월이 겨울 하나로), break를 잊으면 의도치 않은 갈래까지 흘러 들어가는 고전적 사고가 된다 — 그래서 컴파일러 경고와 C23의 명시 표기([[fallthrough]] — “의도된 흐름임”을 적는 문서)가 이 자리를 지킨다. default는 어느 이름표에도 안 맞을 때의 갈래다 — “그런 달은 없다”처럼, 예상 밖의 값을 잡는 그물로 항상 두는 것이 관행이다.
실제 사례. 표준이 든 기묘한 예 — 도달하지 않는 선언
case 이름표가 담장이 아니라는 성질을 극단까지 밀면 이상한 코드가 가능해진다. 다음은 C 표준이 switch 절의 예제로 직접 든 것이다.
switch (expr) {
int i = 4; /* ← 여기로는 아무도 오지 않는다 */
f(i); /* ← 여기도 */
case 0:
i = 17;
/* 아래로 흘러 들어간다 */
default:
printf("%d\n", i);
}switch에 들어서면 실행은 일치하는 이름표로 곧장 건너뛴다. 그래서 블록 맨 앞의 두 줄은 어떤 값에서도 실행되지 않는다. 그런데 i라는 변수는 존재한다 — 블록에 들어선 이상 그 블록의 지역 변수는 자리를 얻기 때문이다(44장의 자동 저장 기간).
결과가 기묘하다. expr이 0이면 case 0:으로 뛰어 i = 17이 실행되고 17이 찍히지만, 0이 아니면 default:로 곧장 뛰므로 i는 한 번도 초기화되지 않은 채 읽힌다 — 52장에서 정식으로 다룰 미정값 읽기다. 초기화하는 줄이 눈앞에 버젓이 적혀 있는데도 그렇다.
이 예제가 표준에 있는 이유는 “이렇게 쓰라”가 아니라 case가 이름표일 뿐이라는 사실의 귀결을 못박기 위해서다. 실무의 규칙은 둘이다 — switch 블록 맨 앞에 선언을 두지 않는다, 그리고 case 안에서 변수를 선언해야 하면 중괄호로 블록을 만든다(그러지 않으면 컴파일러가 “case 이름표를 건너뛰는 초기화”라고 거절하기도 한다).
문. if의 사다리로도 다 되는데, switch가 굳이 필요한 이유는 무엇인가?
답. 둘의 차이는 표현력이 아니라 의도의 전달이다. switch는 “하나의 값을 여러 상수와 맞춰 본다”는 의도를 문법으로 못박아서, 읽는 사람과 컴파일러 모두에게 구조가 보인다 — 컴파일러는 그 구조를 이용해 점프 표 같은 빠른 코드로 바꾸기도 하고(13장의 편집자가 좋아하는 모양이다), 경고 기능은 “열거된 값 중 빠진 case”를 짚어 줄 수 있다. 값 하나로 갈라서면 switch, 범위·복합 조건이면 if 사다리 — 도구를 의도에 맞추는 것이다. 그리고 다음 장에서, 이 폴스루라는 성질을 극한까지 악용(?)한 전설의 코드를 드디어 만난다.
갈림길을 배웠다 — 그러나 프로그램의 진짜 힘은 갈라서는 것이 아니라 되풀이하는 것에서 나온다. 4장에서 “단순한 걸음도 수십억 번이면”이라 했던 그 반복을, 다음 장에서 손에 넣는다.
주
- CVE-2014-1266. 2014. National Vulnerability Database, NIST.
nvd.nist.gov/vuln/detail/CVE-2014-1266↩