C 언어 시리즈 #03 — 제어 흐름: 분기·스위치 테이블·setjmp와 스택
이 글의 핵심
분기문이 어셈블리로 어떻게 깔릴 수 있는지, longjmp가 왜 호출 규약·프레임 체인과 충돌할 수 있는지, “멋진 트릭”이 왜 현대 코드베이스에서 쓰이지 않는지 정리합니다.
시리즈 안내
#03 | 📋 전체 목차 | 이전: #02 타입 · 다음: #04 함수·호출 규약
제어 흐름 글을 교과서 순서(if부터)로 쓰면 이미 아는 내용이 대부분이라 금방 지루해집니다. 그래서 이번 편은 실무에서 논쟁이 되는 것부터—goto, break, continue, switch—다루고, if·for 같은 기본 문법은 뒤로 미뤘습니다. 문법 표 대신 코드 리뷰에서 실제로 걸리는 지점과 예제로 설명합니다.
먼저 goto — 팀에서 한 번은 논쟁이 됩니다
C 프로젝트에서 goto를 쓸지 말지는 거의 반드시 한 번은 논쟁이 됩니다. 한쪽은 “리눅스 커널도 쓰는데”라고 하고, 다른 쪽은 “스파게티 코드만 남는다”고 합니다. 제가 본 합의점은 대부분 비슷했습니다. 정리(cleanup) 블록 하나로 모을 때만 허용하고, 그 밖에는 함수 분리와 에러 코드로 처리하는 것입니다. 이 기준을 코딩 규칙에 적어 두면 리뷰에서 “이 goto는 out: 한 군데로만 가므로 괜찮다”처럼 짧게 판단할 수 있습니다.
goto label;은 같은 함수 안의 label:로만 점프합니다. 다른 함수로는 갈 수 없으며, 함수 경계를 넘는 흐름은 return, 상태 머신, 정말 필요하면 setjmp로 다룹니다. 또 하나의 제약은 가변 길이 배열(VLA)의 유효 범위 안으로 goto로 뛰어드는 것이 금지된다는 점입니다. 컴파일러는 jump into scope of identifier with variably modified type 같은 오류를 냅니다. VLA의 크기 계산과 스택 할당을 건너뛰게 되기 때문입니다.
정리 패턴은 다음과 같습니다. 자원을 여러 개 차례로 여는데 중간에 실패하면, 그때까지 연 것만 역순으로 닫아야 합니다. 실패 지점마다 닫는 코드를 복붙하면 한 곳을 빼먹기 쉽습니다. 대신 실패 지점에서 goto로 정리 구간의 적절한 진입점으로 보내면, 레이블이 역순으로 쌓여 있어서 “여기까지 연 것”만 자연스럽게 닫힙니다. 리눅스 커널 드라이버나 OpenSSL 스타일 코드에서 자주 보이는 이유가 이것입니다.
int setup(void) {
int fd = -1;
void *p = NULL;
if (open_f(&fd) != 0) goto out;
if (alloc_p(&p) != 0) goto out_fd;
return 0;
out_fd: close_f(fd);
out: return -1;
}
위 코드에서 alloc_p가 실패하면 out_fd로 가서 fd만 닫고, out으로 흘러내려 -1을 반환합니다. 레이블 순서가 획득 순서의 역순이라는 점이 핵심입니다. 새 자원을 추가할 때 레이블 순서를 틀리면 이미 닫은 핸들을 또 닫거나 열지 않은 핸들을 닫는 버그가 생기므로, 리뷰할 때는 “획득 순서 ↔ 레이블 역순”을 한 번 대조해 보는 습관이 도움이 됩니다.
남용하면 그물망이 됩니다. 레이블이 서너 개를 넘어가면 함수로 쪼갤 신호로 보면 됩니다. GCC/Clang이라면 __attribute__((cleanup(fn)))으로 스코프 종료 시 자동 정리를 걸 수도 있지만, 표준 C가 아니므로 이식성이 필요한 코드에서는 goto 정리 패턴이 여전히 기본입니다.
break — 한 겹만 끊습니다
break는 가장 안쪽 switch나 while / do / for 딱 한 단계만 빠져나옵니다. 이중 for를 돌다가 “찾았다” 하고 안쪽에서 break를 치면 안쪽 루프만 끝나고 바깥 루프는 계속 돕니다.
그래서 쓰는 전략이 몇 가지 있습니다. 플래그를 두고 바깥 조건에 !found를 거는 방법, find_in_matrix() 같은 함수로 빼서 찾는 즉시 return하는 방법, 팀 규칙이 허용하면 깊은 중첩 한정으로 goto found;를 쓰는 방법입니다. Java의 레이블 붙은 break outer; 같은 문법은 C에 없습니다. 개인적으로는 함수 추출을 가장 먼저 권합니다. 탐색 로직에 이름이 붙고, 반환값으로 “못 찾음”을 표현할 수 있어서 호출하는 쪽도 읽기 쉬워집니다.
for (size_t i = 0; i < n; i++) {
for (size_t j = 0; j < m; j++) {
if (M[i][j] == key) {
/* 여기 break는 j 루프만 종료 */
}
}
}
switch 안에 루프가 있거나 루프 안에 switch가 있을 때도 같은 규칙이 적용됩니다. 루프 안의 switch에서 break를 쓰면 switch만 빠져나오고 루프는 계속됩니다. 루프를 끝내려는 의도로 switch 안에서 break를 쳤다가 무한 루프가 되는 실수가 흔한데, 이때는 return이나 플래그가 필요합니다.
continue — 이번 반복만 건너뜁니다
continue는 가장 안쪽 루프에서 “나머지 본문은 이번에 실행하지 않는다”는 뜻입니다. 여기에 함정이 하나 있습니다. for에서 continue를 만나면 세 번째 절(step)이 실행된 다음에 조건을 검사합니다. 반면 while은 곧바로 조건만 다시 봅니다. 그래서 while 본문 끝에 i++를 두고 중간에 continue를 치면, i가 증가하지 않은 채 같은 값으로 영원히 도는 버그가 납니다. 인덱스 루프는 증가식을 for의 세 번째 칸에 몰아 두는 편이 이런 실수가 적습니다.
for (size_t i = 0; i < n; i++) {
if (skip(a[i]))
continue;
use(a[i]);
}
do-while에서 continue는 조건식으로 갑니다. 즉 본문 맨 아래로 점프한 것처럼 동작하므로, 조건식이 참이면 다음 반복이 이어집니다. 초보자가 “continue하면 루프 맨 위로 간다”고 기억하는 경우가 많은데, 정확히는 “루프 본문의 끝으로 간다”로 기억하는 편이 세 종류 루프 모두에 맞습니다.
switch — 점프 테이블은 “있을 수도” 있습니다
switch는 정수 계열 값 하나를 보고 해당 case 레이블로 들어갑니다. break가 없으면 아래 case로 그대로 떨어집니다(fall-through). 의도한 것이라면 /* fall through */ 주석을 달아 두는 것이 관례이고, C23부터는 [[fallthrough]]; 속성으로 명시할 수 있습니다. GCC의 -Wimplicit-fallthrough(-Wextra에 포함)는 이 표시가 없는 fall-through에 this statement may fall through 경고를 냅니다.
밀집된 정수 케이스면 컴파일러가 간접 점프 테이블을 만들 수도 있습니다. 값이 듬성하거나 범위가 넓으면 if 사슬이나 이진 탐색이 쓰입니다. 케이스가 몇 개 안 되면 테이블 로드와 간접 분기보다 비교 몇 번이 싸기 때문에 오히려 if 사슬을 택하기도 합니다. “우리 switch는 무조건 테이블이다”라고 믿지 말고, 병목이라면 -S로 어셈블리를 보거나 측정하는 것이 맞습니다.
switch (cmd) {
case 'q': quit(); break;
case 'h': help(); break;
default: unknown(); break;
}
enum 값 전체를 switch로 처리할 때는 트레이드오프가 하나 있습니다. default를 두면 이상한 값을 흡수할 수 있지만, 대신 GCC/Clang의 -Wswitch가 “새로 추가된 열거자를 처리하지 않았다”는 경고(enumeration value 'X' not handled in switch)를 내지 못합니다. C에서는 열거형 변수에 아무 int나 들어올 수 있으므로 방어 코드도 필요합니다. 그래서 default를 빼고 모든 열거자를 나열한 뒤, switch 바깥에서 잘못된 값을 처리하는 패턴을 쓰는 팀도 있습니다.
case 레이블 사이에서 변수를 선언하고 초기화하면 jump to case label crosses initialization(C++) 류의 문제가 생깁니다. C에서는 컴파일은 되더라도 다른 case로 들어오면 초기화가 건너뛰어진 변수를 읽게 됩니다. case 안에 지역 변수가 필요하면 { }로 블록을 여는 습관이 안전합니다.
이제 기본기 — if와 else
여기부터는 기본 문법입니다. C에서 조건은 0이면 거짓, 0이 아니면 참입니다. 포인터는 널이면 거짓이므로 if (p)가 if (p != NULL)과 같습니다.
dangling else만 짚고 넘어가겠습니다. else는 문법상 가장 가까운 if에 붙습니다. 들여쓰기와 실제 의미가 다를 수 있어서 다음과 같은 코드가 나옵니다.
if (a)
if (b)
do_something();
else
other(); /* else는 if(b)에 붙음—들여쓰기랑 다를 수 있음 */
의도를 바로잡으려면 중괄호로 범위를 닫으면 됩니다. GCC는 이런 경우 suggest explicit braces to avoid ambiguous 'else'(-Wdangling-else, -Wall에 포함) 경고를 내므로, 경고를 켜 두는 것만으로 상당수를 잡을 수 있습니다.
if (a) {
if (b)
do_something();
} else {
other();
}
else if 사슬은 부호나 구간을 나눌 때 쓰고, 정수 상수 비교가 줄줄이 이어지면 앞에서 본 switch가 읽기 나을 때가 많습니다.
단락 평가도 기억해 둘 만합니다. if (p && p->len > 0)에서 p가 널이면 p->len은 평가되지 않습니다. &&와 ||는 좌변을 먼저 평가하고 그 사이에 시퀀스 포인트가 있어서 이 순서가 보장됩니다. 반면 &, | 비트 연산자는 단락 평가를 하지 않으므로, 둘을 헷갈리면 널 포인터 역참조가 그대로 일어납니다. 조건식에 a++ 같은 부작용을 넣으면 단락 평가 때문에 실행 여부가 조건에 따라 달라지므로, 조건은 가급적 순수하게 유지하는 편이 좋습니다.
if (x = 0)처럼 비교 대신 대입을 쓰는 실수도 여전히 흔합니다. -Wall의 suggest parentheses around assignment used as truth value 경고가 이를 잡아 줍니다. 의도한 대입이라면 if ((x = f()) != 0)처럼 명시적으로 쓰는 것이 관례입니다.
조건 연산자 ?:
condition ? expr1 : expr2는 둘 중 하나만 평가됩니다. 짧은 선택에는 좋고, 중첩되어 읽기 어려워지면 if로 쪼갭니다.
int m = (a > b) ? a : b;
주의할 점은 두 피연산자의 타입이 usual arithmetic conversions로 맞춰진다는 것입니다. 예를 들어 한쪽이 unsigned이고 다른 쪽이 음수 int면 결과가 큰 양수가 될 수 있습니다. 이 규칙은 #02 타입에서 다룬 승격 규칙과 같습니다. 같은 이유로 MAX(a, b) 매크로를 ?:로 만들면 인자가 두 번 평가되어 MAX(i++, j) 같은 호출이 예상과 다르게 동작합니다.
반복 — while, do-while, for
while (condition)은 들어가기 전에 조건을 봅니다. 처음부터 거짓이면 본문은 0번 실행됩니다.
while (condition)
body;
do { } while (condition);은 본문을 한 번은 실행하고 나서 조건을 봅니다. 입력 프롬프트나 재시도처럼 “최소 한 번은 해 봐야 하는” 작업에 잘 맞습니다.
int n;
do {
n = read_int();
} while (n < 1 || n > 10);
for (init; condition; step)은 익숙한 패턴이며, 인덱스 증가를 세 번째 칸에 두면 continue와도 잘 맞습니다. for(;;)와 while(1)은 취향 차이이고, while (true)는 stdbool.h가 있을 때(C23부터는 키워드) 씁니다.
for (init; condition; step)
body;
쉼표 연산자로 i = 0, j = n - 1처럼 몰아넣을 수는 있지만, 지나치게 복잡해지면 함수로 빼는 편이 낫습니다.
부호 없는 인덱스로 역순 루프를 돌 때 가장 많이 틀립니다. for (size_t i = n - 1; i >= 0; i--)는 size_t가 음수가 될 수 없어서 조건이 항상 참이고, i가 0에서 SIZE_MAX로 감싸져 배열 밖을 읽습니다. -Wextra는 comparison of unsigned expression >= 0 is always true 경고를 냅니다. 관용구는 for (size_t i = n; i-- > 0; )입니다. 조건에서 비교 후 감소하므로 본문 안에서는 n-1부터 0까지 정확히 돕니다.
중첩에서 탈출할 때
앞에서 말한 대로 플래그, 함수 추출, 또는 제한된 goto를 씁니다. 플래그 방식의 예는 다음과 같습니다.
int found = 0;
for (i = 0; i < n && !found; i++)
for (j = 0; j < m && !found; j++)
if (a[i][j] == key) found = 1;
이 코드에는 미묘한 함정이 있습니다. found = 1이 된 뒤에도 각 루프의 i++, j++가 한 번씩 더 실행되므로, 루프가 끝난 뒤의 i, j는 찾은 위치보다 1씩 큽니다. 위치가 필요하면 찾은 순간 별도 변수에 저장하거나, 함수로 빼서 그 자리에서 반환하는 편이 정확합니다.
핫 루프·Duff’s device·분기 예측 (심화)
호이스팅: 루프마다 똑같이 계산되는 strlen 같은 것은 밖으로 빼면 불필요한 작업이 줄어듭니다. 컴파일러가 대신해 주는 경우도 많지만, 루프 본문에서 s를 수정하거나 별칭(alias) 가능성이 있으면 컴파일러는 strlen 결과가 바뀌지 않는다고 증명하지 못해 매번 다시 호출합니다. 그러면 O(n) 루프가 O(n²)이 됩니다. 디버그 빌드에서는 최적화 자체가 꺼지므로 소스에 명시하는 편이 확실합니다.
/* 의심: strlen이 매 루프 */
for (i = 0; i < strlen(s); i++) use(s[i]);
size_t L = strlen(s);
for (i = 0; i < L; i++) use(s[i]);
Duff’s device는 switch를 do-while 안에 끼워 넣어 수동으로 루프를 풀던 옛 트릭입니다. 오늘날 컴파일러의 -O2/-O3는 루프 언롤과 벡터화를 스스로 하고, 이런 비정형 제어 흐름은 오히려 벡터화를 방해할 수 있습니다. 신규 프로덕션 코드에서 권하기 어려운 이유는 성능보다 리뷰·검증 비용입니다. count가 0일 때의 동작처럼 경계 조건을 누구나 한눈에 확인하기 어렵습니다(아래 스케치는 count == 0이면 8개를 복사해 버립니다).
CPU 분기 예측은 핫 루프에서 분기 방향이 들쭉날쭉하면 예측 실패가 늘어나 파이프라인을 비웁니다. 소스에서 할 수 있는 일은 데이터를 정렬해 분기 패턴을 규칙적으로 만들기, 분기 자체를 줄이기, 0/1 마스크 연산으로 대체하기(가독성·미정의 동작과의 트레이드오프) 정도입니다. GCC의 __builtin_expect나 C++20 [[likely]] 같은 힌트는 코드 배치에 영향을 줄 뿐 런타임 예측기를 직접 제어하지는 않습니다. 최종 판단은 벤치마크로 합니다.
실전 예제 몇 가지
입력 검증: scanf가 실패하면 읽지 못한 문자가 입력 버퍼에 그대로 남아서, 다음 반복에서도 같은 문자에 걸려 무한 루프가 됩니다. 그래서 실패 시 getchar로 줄 끝까지 비웁니다.
int read_int_in_range(int lo, int hi) {
int x;
for (;;) {
printf("[%d,%d] 정수: ", lo, hi);
if (scanf("%d", &x) == 1 && x >= lo && x <= hi) return x;
int c;
while ((c = getchar()) != '\n' && c != EOF) { }
puts("잘못된 입력/범위.");
}
}
여기서 c를 char가 아니라 int로 선언한 것이 중요합니다. EOF는 unsigned char 범위 밖의 음수 값이라 char에 담으면 구별되지 않거나(부호 없는 char 플랫폼), 0xFF 바이트와 혼동됩니다. 또 이 함수는 입력이 EOF로 끝나면 scanf가 계속 EOF를 반환해 영원히 돌 수 있으므로, 실제 코드에서는 scanf의 반환값이 EOF인 경우를 따로 처리해야 합니다. 더 견고한 방법은 fgets로 한 줄을 읽고 strtol로 파싱하는 것입니다. 오버플로도 errno == ERANGE로 감지할 수 있기 때문입니다.
메뉴 루프: continue로 잘못된 입력만 건너뜁니다.
void menu(void) {
for (;;) {
int ch;
puts("0=종료 1=Foo 2=Bar");
if (scanf("%d", &ch) != 1) { flush_line(); continue; }
switch (ch) {
case 0: return;
case 1: do_foo(); break;
case 2: do_bar(); break;
default: puts("?"); break;
}
}
}
switch 안의 break는 switch만 빠져나오고 for(;;)는 계속됩니다. 그래서 종료는 return으로 처리했습니다. 앞에서 말한 “루프 안의 switch” 함정을 피하는 전형적인 모양입니다.
선형 탐색과 정렬된 배열의 이진 탐색은 다음과 같습니다.
int *linsearch(int *a, size_t n, int k) {
for (size_t i = 0; i < n; i++) if (a[i] == k) return a + i;
return NULL;
}
int *bsearch_eq(int *a, size_t n, int k) {
size_t lo = 0, hi = n;
while (lo < hi) {
size_t mid = lo + (hi - lo) / 2; /* 오버플로 방지 */
if (a[mid] == k) return a + mid;
if (a[mid] < k) lo = mid + 1; else hi = mid;
}
return NULL;
}
mid = (lo+hi)/2는 lo + hi가 타입 최댓값을 넘으면 감싸져 엉뚱한 인덱스가 되므로 lo + (hi-lo)/2 관용구를 씁니다. 이 버그는 Java 표준 라이브러리의 이진 탐색에도 오랫동안 있었던 것으로 유명합니다. hi = n인 반열린 구간 [lo, hi)를 쓰기 때문에 hi = mid(mid 제외)와 lo = mid + 1이 짝을 이룹니다. 구간 표기를 닫힌 구간과 섞으면 무한 루프나 오프바이원이 생깁니다. 타입 관련 내용은 #02를 참고하세요.
패턴 하나씩
센티널은 '\0', EOF, NULL처럼 끝을 알리는 값입니다. 플래그는 여러 조건을 ok 하나로 모을 때 씁니다. 상태 머신은 enum + switch로 다음 상태만 정해 주는 방식이며, goto로 상태 사이를 점프하는 것보다 추론이 쉬운 경우가 많습니다. 현재 상태가 변수 하나에 담겨 있어서 디버거나 로그로 확인하기 쉽기 때문입니다.
문제가 생기면 다음 순서로 의심해 봅니다. switch가 이상하게 실행되면 break가 빠졌는지, fall-through가 의도인지 확인합니다. if-else가 이상하면 else가 어느 if에 붙었는지, 중괄호가 있는지 봅니다. while(1)이 멈추지 않으면 break/return 경로가 있는지 봅니다. for에서 i가 늘지 않으면 while로 바꾸면서 증가식을 continue 아래에 두지 않았는지 봅니다. scanf가 반복해서 실패하면 입력 버퍼를 비웠는지 확인합니다. longjmp 이후에 값이 이상하거나 크래시가 나면 아래 setjmp 절과 #04의 스택 프레임 설명을 함께 보면 됩니다.
setjmp / longjmp (심화)
setjmp는 jmp_buf에 현재 실행 환경(스택 포인터, 프로그램 카운터, 호출자 보존 레지스터 등)을 저장하고 0을 반환합니다. 나중에 longjmp(buf, v)가 호출되면 그 지점으로 비국소 점프해 setjmp가 다시 v(0이면 1)를 반환한 것처럼 실행이 이어집니다. return과 달리 중간 프레임의 정리 코드는 실행되지 않습니다. 중간 함수가 malloc한 메모리나 연 파일은 그대로 새고, C++ 코드가 섞여 있으면 소멸자가 호출되지 않는 것은 물론 미정의 동작이 될 수 있습니다.
표준이 거는 제약도 알아 두어야 합니다.
setjmp는if (setjmp(buf) == 0)이나switch (setjmp(buf))처럼 제한된 문맥에서만 호출할 수 있습니다.int r = setjmp(buf);처럼 결과를 대입하는 형태는 표준상 허용 목록에 없습니다.setjmp를 호출한 함수가 이미 반환한 뒤에 그jmp_buf로longjmp하면 미정의 동작입니다. 저장된 스택 프레임이 이미 사라졌기 때문이며, 증상은 보통 엉뚱한 곳에서의 세그폴트입니다.setjmp를 호출한 함수의 지역 변수 중volatile이 아니고setjmp이후에 값이 바뀐 변수는longjmp뒤 값이 불확정입니다.
세 번째 항목이 가장 자주 사람을 괴롭힙니다. 최적화를 켜면 지역 변수가 레지스터에 올라가는데, longjmp가 레지스터를 setjmp 시점 값으로 되돌리기 때문에 “분명 증가시켰는데 0으로 돌아왔다”는 현상이 생깁니다. 디버그 빌드(-O0)에서는 변수가 스택 메모리에 있어서 멀쩡하다가 릴리스 빌드에서만 재현되므로 원인을 찾기가 특히 어렵습니다. GCC는 variable 'x' might be clobbered by 'longjmp' or 'vfork'(-Wclobbered, -Wextra에 포함) 경고로 힌트를 줍니다.
#include <setjmp.h>
static jmp_buf env;
int parse_all(void) {
volatile int count = 0; /* volatile이 없으면 longjmp 뒤 값이 불확정 */
if (setjmp(env) != 0) {
return count; /* 에러 경로: 여기까지 처리한 개수 */
}
while (next_token()) {
count++; /* 파싱 중 오류 시 깊은 곳에서 longjmp(env, 1) */
}
return count;
}
VLA와 섞이면 문제가 더 커집니다. VLA를 할당한 뒤 longjmp로 그 스코프 바깥으로 빠져나가면 해당 메모리가 회수되지 않을 수 있다고 표준이 명시합니다. 그래서 setjmp/longjmp를 C++ 예외 대체로 쓰라는 뜻은 아닙니다. 실제로 쓰이는 곳은 Lua 인터프리터의 에러 처리, libpng/libjpeg의 에러 복구 콜백처럼 “깊은 재귀 파서에서 한 번에 최상위로 탈출”해야 하고, 그 사이에서 자원 소유권을 엄격히 관리하는 좁은 영역입니다. 그런 설계가 아니라면 에러 코드 전파, 콜백, 상태 머신을 먼저 고려하는 편이 안전합니다.
흐름을 한 장에
flowchart TD A[입력·이벤트] --> B[검증·파싱] B --> C[연산·상태] C --> D[I/O·저장] D --> E[로그·결과]
각 단계 사이에 지켜져야 할 불변식(예: “검증을 통과한 값은 범위 안이다”)을 문장으로 적어 두면, 디버깅할 때 “어느 단계에서 불변식이 깨졌는가”로 범위를 좁힐 수 있습니다.
부록으로 남기는 디테일
널문장: if (x);처럼 조건 뒤에 세미콜론을 붙이면 세미콜론 하나가 본문(널문장)이 되고, 다음 줄은 조건과 무관하게 실행됩니다. 그래서 if에는 항상 중괄호를 쓰는 규칙을 둔 팀이 많습니다. Clang은 if statement has empty body(-Wempty-body) 경고를 냅니다.
if (ptr == NULL); /* 실수: 널문장이 if 본문 */
panic("ptr"); /* 조건과 무관하게 실행될 수 있음 */
switch의 제어식은 정수 계열이어야 합니다. 부동소수나 문자열이면 if 사슬을 씁니다. 여러 문장으로 된 매크로가 if 뒤에 오면 첫 문장만 조건에 걸리므로 do { } while(0) 래퍼로 감싸거나 static inline 함수로 바꿉니다.
반열린 구간 [0, n)으로 인덱스 표기를 통일해 두면 빈 배열 처리와 이진 탐색 while (lo < hi)가 자연스럽게 맞습니다. 닫힌 구간과 섞어 쓰면 오프바이원이 나기 쉽습니다.
2차원 배열은 행 우선으로 저장되므로 M[i][j]에서 j를 안쪽 루프로 돌면 메모리를 연속으로 읽어 캐시에 유리합니다. 루프 순서를 바꾸면 결과는 같아도 캐시 미스가 크게 늘 수 있습니다.
임베디드에서 하드웨어 레지스터나 인터럽트 핸들러가 바꾸는 변수는 volatile로 선언해야 합니다. 그렇지 않으면 while (!flag) { } 같은 대기 루프에서 컴파일러가 flag를 한 번만 읽고 무한 루프로 최적화해 버립니다. 무한 for(;;)는 main 루프, idle 루프, 감시 루프에 흔히 쓰이며, 빈 바쁜 대기(busy wait)는 전력 소비와 응답 속도 사이의 트레이드오프입니다.
Duff’s device 개념 스케치는 역사적 참고로만 남깁니다. 실제 코드에 복사하지 마세요.
/* 개념 스케치: 실제로 복붙하지 말 것 */
void duff_idea(int *to, int *from, int count) {
int n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to++ = *from++;
case 7: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
} while (--n > 0);
}
}
break·continue·return·exit의 범위를 정리하면 다음과 같습니다. break/continue는 루프·스위치 한 겹, return은 함수, exit는 프로세스 전체입니다(atexit 핸들러와 stdio 버퍼 flush는 실행됩니다). C에는 try가 없으므로 에러는 반환 코드와 정리 패턴으로 다루고, 드물게 longjmp를 씁니다.
다음: #04 함수·스택·호출 규약에서는 longjmp와 goto 정리 패턴이 스택 프레임과 어떻게 맞물리는지 더 자세히 다룹니다.