41 반복의 기법 — 중첩, 탈출, 그리고 블록 만들기
먼저 알아야 할 것
while·for·do-while 3형제돌아보기
32장에서 루프 3형제를 배웠고, 39장에서 2차원 배열을 배웠다. 그러면 int a[1000][1000]의 모든 칸을 더하는 코드를 두 가지로 적을 수 있다 — 행을 먼저 도는 것과 열을 먼저 도는 것. 두 코드는 같은 답을 낸다. 무엇이 다른가?
답. 속도가 다르다 — 그것도 여덟 배쯤. 39장에서 본 대로 2차원 배열은 기억 속에 행이 먼저 늘어서 있고(행 우선 배치), 11장에서 본 대로 캐시는 이웃한 바이트를 함께 실어 온다. 행을 따라 걸으면 실어 온 줄을 알뜰히 쓰고, 열을 따라 걸으면 매 걸음 새 줄을 부른다.
루프를 배웠다는 것과 루프를 부릴 줄 안다는 것은 다르다. 이 장이 그 차이를 메운다.
이 장의 필요성과 맥락
이 장이 끝나면
goto가 정당한 두 자리와 지켜야 할 규율, 매크로를 한 문장으로 만드는 do { } while (0), 그리고 실무에서 반복해 만나는 관용구와 함정들. 32장이 「루프란 무엇인가」였다면 이 장은 「루프를 어떻게 부리는가」다.이 장에서 답할 질문
- 그러면 언제나 안쪽에 마지막 첨자를 두면 되는가?
goto를 아예 금지한 규약(MISRA 등)에서는 어떻게 하는가?while (0)이면 한 번도 안 도는 것 아닌가?do-while에서continue는 어디로 가는가?
41.1 중첩 루프 — 다차원을 걷는 법
39장의 int a[R][C]는 기억 속에서 한 줄로 늘어서 있고, 그 순서는 행 우선이다 — a[0][0] a[0][1] … a[0][C-1] a[1][0] …. 중첩 루프의 순서는 그 줄 위를 어떤 보폭으로 걷느냐를 정한다.
for (int i = 0; i < R; i++) /* 행 우선 — 이웃 칸으로 한 발씩 */
for (int j = 0; j < C; j++)
sum += a[i][j];
for (int j = 0; j < C; j++) /* 열 우선 — 매번 한 행을 건너뛴다 */
for (int i = 0; i < R; i++)
sum += a[i][j];같은 계산이다. 재어 보면 같지 않다.
| 순회 | 4000×4000 int 합산 | 왜 |
|---|---|---|
행 우선 (i, j) | 11~14 ms | 캐시가 실어 온 줄을 다 쓰고 다음 줄로 간다 |
열 우선 (j, i) | 101~113 ms | 한 걸음마다 16 KB 를 건너뛰어 매번 새 줄을 부른다 |
표 41.1
여덟 배다. 알고리즘도, 연산 횟수도, 결과도 같은데 그렇다. 11장에서 「캐시가 속도를 지배한다」고 한 말이 여기서 눈에 보이는 숫자가 된다.
그래서 실무의 첫 최적화는 대개 루프 순서를 바꾸는 것이다 — 코드를 복잡하게 만들지 않고, 알고리즘도 그대로 두고, 안쪽 첨자가 가장 빨리 변하는 축이 되도록 줄만 맞바꾼다.
문. 그러면 언제나 안쪽에 마지막 첨자를 두면 되는가?
답. 대개는 그렇다. 다만 두 가지를 함께 봐야 한다.
① 무엇이 정답인지는 배치가 정한다. C 는 행 우선이지만 포트란은 열 우선이라, 포트란에서 옮겨 온 수치 코드는 순서가 반대인 것이 옳을 수 있다. 배열이 아니라 구조체의 배열이라면 또 달라진다(46장의 배치 이야기).
② 순서를 바꿀 수 없는 계산도 있다. 앞 칸의 결과가 뒤 칸에 필요한 계산(누적, 점화식)은 순서 자체가 알고리즘의 일부다. 그럴 때 쓰는 것이 타일링(블록으로 잘라 캐시에 얹히는 크기씩 처리하기) 같은 기법인데, 이 책의 범위를 넘는다.
기억할 규칙은 하나다 — 가장 안쪽 루프가 기억 속에서 가장 촘촘히 움직이게 하라. 그것이 공짜로 얻는 몇 배다.
41.2 다중 루프에서 빠져나오기
중첩 루프를 쓰면 곧바로 부딪히는 문제가 있다.
흔한 오해. “break 를 쓰면 루프에서 빠져나온다”
가장 안쪽 한 겹에서만 빠져나온다. 표준이 그렇게 정해 두었다 — break 는 자신을 감싼 가장 가까운 switch 나 반복문 하나를 끝낸다.
시연이 그것을 재어 보였다. 안쪽에서만 break 한 코드는 20칸짜리 표에서 18칸을 훑었다 — 값을 찾고도 바깥 루프가 계속 돌았기 때문이다.
그리고 C 에는 레이블 붙은 break가 없다. 다른 언어들과 갈리는 자리다.
| 언어 | 여러 겹을 한 번에 벗는 장치 |
|---|---|
| C | 없다 — goto 나 다른 방법을 쓴다 |
| Java | outer: for (…) { break outer; } |
| Go | 같은 레이블 문법. continue 에도 붙는다 |
| Rust | 'outer: loop { break 'outer; } — 값까지 반환한다 |
| Perl·PHP | last LABEL / break 2(겹 수를 숫자로) |
표 41.2
C 에서 쓸 수 있는 방법은 넷이고, 하나씩 값이 다르다.
examples/ch41/nested_exit.c
/* 2차원 표에서 값을 찾아 다중 루프를 빠져나오는 세 가지 방식.
셋은 같은 답을 내고, 다른 것은 '읽는 사람이 흐름을 얼마나 쉽게 따라가는가'다. */
#include <stdbool.h>
#include <stdio.h>
#define ROWS 4
#define COLS 5
static const int grid[ROWS][COLS] = {
{ 3, 14, 15, 92, 6 },
{ 53, 58, 9, 79, 3 },
{ 23, 84, 62, 64, 33 },
{ 83, 27, 95, 28, 84 },
};
struct pos { int row, col; bool found; };
/* ── ① 플래그 변수 ──
바깥 루프의 조건에 플래그를 얹는다. 표준 C 만으로 되지만,
조건이 늘고 '어디서 끝났는가'가 두 곳에 흩어진다. */
static struct pos find_flag(int target)
{
struct pos p = { -1, -1, false };
for (int i = 0; i < ROWS && !p.found; i++)
for (int j = 0; j < COLS; j++)
if (grid[i][j] == target) { p.row = i; p.col = j; p.found = true; break; }
return p;
}
/* ── ② goto ──
'두 겹을 한 번에 벗는다'는 뜻이 한 줄로 드러난다.
레이블은 아래에 두고, 이름은 하는 일로 짓는다. */
static struct pos find_goto(int target)
{
struct pos p = { -1, -1, false };
for (int i = 0; i < ROWS; i++)
for (int j = 0; j < COLS; j++)
if (grid[i][j] == target) { p.row = i; p.col = j; p.found = true; goto done; }
done:
return p;
}
/* ── ③ 함수로 빼고 return ──
return 은 몇 겹이든 한 번에 벗는다. 탈출 장치가 아예 필요 없어진다. */
static struct pos find_return(int target)
{
for (int i = 0; i < ROWS; i++)
for (int j = 0; j < COLS; j++)
if (grid[i][j] == target) return (struct pos){ i, j, true };
return (struct pos){ -1, -1, false };
}
static void show(const char *how, struct pos p)
{
if (p.found) printf(" %-16s (%d, %d)\n", how, p.row, p.col);
else printf(" %-16s none\n", how);
}
int main(void)
{
puts("[looking for 62 - three ways, same answer]");
show("① flag", find_flag(62));
show("② goto", find_goto(62));
show("③ function + return", find_return(62));
puts("\n[looking for a value that is not there (100)]");
show("① flag", find_flag(100));
show("② goto", find_goto(100));
show("③ function + return", find_return(100));
puts("\n[break peels off one layer only - hence the devices above]");
int visited = 0;
for (int i = 0; i < ROWS; i++)
for (int j = 0; j < COLS; j++) { visited++; if (grid[i][j] == 62) break; }
printf(" break in the inner loop only: visited %d cells (of %d)\n",
visited, ROWS * COLS);
puts(" -> only the inner loop ended; the outer one kept going.");
return 0;
}
실행 결과
[looking for 62 - three ways, same answer]
① flag (2, 2)
② goto (2, 2)
③ function + return (2, 2)
[looking for a value that is not there (100)]
① flag none
② goto none
③ function + return none
[break peels off one layer only - hence the devices above]
break in the inner loop only: visited 18 cells (of 20)
-> only the inner loop ended; the outer one kept going.
| 방법 | 어떻게 | 좋은 점 | 나쁜 점 |
|---|---|---|---|
| ① 플래그 변수 | 바깥 조건에 && !found 를 얹는다 | goto 가 없다 | 조건이 늘고, 끝나는 자리가 두 곳에 흩어진다. 검사도 매 바퀴 하나 늘어난다 |
② goto | 찾는 즉시 레이블로 | 「두 겹을 한 번에 벗는다」가 한 줄로 드러난다 | goto 를 금지한 규약이 있다 |
③ 함수로 빼고 return | 탐색을 함수로 만든다 | return 은 몇 겹이든 벗는다. 탈출 장치가 아예 필요 없다 | 함수 하나가 늘고, 값을 여럿 돌려주려면 구조체가 필요하다 |
| ④ 조건을 바깥에 얹기 | for (i = 0; i < R && !done; i++) | ①의 변형 | ①과 같은 문제 |
표 41.3
이 책의 권고는 ③ → ② → ① 순서다.
③이 첫째인 이유는 탈출이 문제가 아니라 덩어리가 문제라는 데 있다. 중첩 루프가 길어져 빠져나갈 궁리를 하게 됐다면, 그 안쪽이 이미 「이름 붙일 만한 일」이 된 것이다. 함수로 빼면 이름이 생기고, return 이 탈출을 공짜로 해결하고, 시험도 쉬워진다.
②가 ①보다 나은 이유는 의도가 드러나기 때문이다. 플래그는 「이 변수가 참이 되면 어딘가에서 루프가 끝난다」를 읽는 사람이 재구성해야 하지만, goto done; 은 그 자리에서 끝난다고 말한다. 게다가 플래그는 잊기 쉽다 — 안쪽 break 를 빠뜨리면 조용히 한 바퀴 더 돈다.
41.3 goto 가 정당한 두 자리
goto 를 둘러싼 오해부터 걷어 내자.
실제 사례. 다익스트라가 실제로 반대한 것
1968년 다익스트라가 CACM 에 보낸 편지 「goto 문은 유해하다고 여겨진다」는 프로그래밍 역사에서 가장 많이 인용되고 가장 적게 읽힌 글일 것이다.
그 글의 논지는 「goto 라는 낱말을 쓰지 말라」가 아니다. 프로그램의 텍스트 위 어느 지점과 실행 중의 진행 상태를 대응시킬 수 있어야 한다는 것이고, 무절제한 점프가 그 대응을 무너뜨린다는 것이다. 심지어 제목의 「considered harmful」도 다익스트라가 아니라 편집자 니클라우스 비르트가 붙인 것으로 알려져 있다.
오늘의 관점에서 보면 그 편지가 요구한 것은 대부분 이미 이루어졌다 — while·for·함수·break·return 이 그 시절의 goto 용법을 거의 다 대체했다. 남은 자리가 아래의 둘이고, 그 둘에서는 goto 가 오히려 흐름을 더 읽기 쉽게 만든다. 리눅스 커널의 코딩 스타일 문서가 goto 를 명시적으로 허용하는 것도 그래서다.
41.3.1 자리 ① — 여러 겹의 루프를 한 번에 벗기
앞 절에서 본 무늬다. 규칙은 아래로만, 가까이다.
for (int i = 0; i < R; i++)
for (int j = 0; j < C; j++)
if (grid[i][j] == target) { found = (struct pos){ i, j }; goto done; }
done:
…41.3.2 자리 ② — 오류 처리의 정리 지점 모으기
자원을 여러 개 얻는 함수는 중간에 실패할 때마다 그때까지 얻은 것만 되돌려야 한다. 그것을 if 로 적으면 되돌리는 코드가 겹겹이 복제된다.
examples/ch41/macro_block.c
/* 매크로를 '한 문장'으로 만드는 do { } while (0), 그리고 goto 로 정리하기. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* 중괄호만 쓰면 뒤에 세미콜론이 붙는 순간 문장이 둘이 된다.
그래서 아래처럼 if/else 사이에 놓으면 컴파일 오류가 난다.
#define SWAP_BAD(a, b) { int t = (a); (a) = (b); (b) = t; }
if (x < y) SWAP_BAD(x, y); else puts("...");
→ error: 'else' without a previous 'if'
do { } while (0) 은 '중괄호 블록이면서 동시에 한 문장'이 되게 한다. */
#define SWAP(a, b) do { int t_ = (a); (a) = (b); (b) = t_; } while (0)
#define LOG(fmt, ...) do { \
printf("[log] " fmt "\n", __VA_ARGS__); \
} while (0)
/* goto 로 정리 지점을 한 곳에 모으는 무늬 — 리눅스 커널이 쓰는 그 형태다.
중간에 실패해도 '거기까지 얻은 것만' 되돌린다. */
static bool build(size_t n, bool fail_at_second)
{
char *a = nullptr, *b = nullptr;
bool ok = false;
a = malloc(n);
if (!a) goto out; /* 아직 되돌릴 것이 없다 */
memset(a, 'a', n);
b = fail_at_second ? nullptr : malloc(n);
if (!b) goto free_a; /* a 만 되돌린다 */
memset(b, 'b', n);
ok = true;
free(b);
free_a:
free(a);
out:
return ok;
}
int main(void)
{
int x = 1, y = 2;
puts("[do { } while (0) - a macro becomes one statement]");
if (x < y) SWAP(x, y); else puts(" (we never get here)");
printf(" after the swap: x=%d y=%d <- it does not break between if and else\n", x, y);
LOG("values %d and %d", x, y);
puts("\n[safe as the body of a for, even without braces]");
for (int i = 0; i < 2; i++)
LOG("round %d", i);
puts("\n[gathering cleanup with goto]");
printf(" both succeeded: %s\n", build(16, false) ? "ok" : "failed");
printf(" second one failed: %s <- only the first was undone before leaving\n",
build(16, true) ? "ok" : "failed");
return 0;
}
실행 결과
[do { } while (0) - a macro becomes one statement]
after the swap: x=2 y=1 <- it does not break between if and else
[log] values 2 and 1
[safe as the body of a for, even without braces]
[log] round 0
[log] round 1
[gathering cleanup with goto]
both succeeded: ok
second one failed: failed <- only the first was undone before leaving
시연의 build가 그 무늬다. 레이블을 되돌리는 순서의 역순으로 쌓아 두고, 실패한 지점에 맞는 레이블로 뛴다.
a = malloc(n);
if (!a) goto out; /* 아직 되돌릴 것이 없다 */
b = malloc(n);
if (!b) goto free_a; /* a 만 되돌린다 */
…
free(b);
free_a:
free(a);
out:
return ok;정리 코드가 한 벌만 존재한다는 것이 이 무늬의 값이다. 자원이 셋, 넷으로 늘어도 레이블이 하나씩 늘 뿐 복제가 생기지 않는다. 44장의 malloc/free 짝맞추기가 실무에서 지켜지는 방식이 대개 이것이다.
goto 를 쓸 때의 규율 | 왜 |
|---|---|
| 아래로만 뛴다 | 위로 뛰면 그것은 루프이고, 루프는 루프 문법으로 적어야 읽힌다 |
| 가까이 뛴다 — 같은 함수, 눈에 보이는 거리 | 멀리 뛰면 다익스트라가 말한 그 문제가 실제로 생긴다 |
| 레이블 이름은 하는 일로 | done·cleanup·free_buf — label1 은 이름이 아니다 |
| 변수 초기화를 건너뛰지 않는다 | 초기화를 건너뛴 변수는 불확정 값이고, 가변 길이 배열의 스코프로 뛰어드는 것은 제약 위반이라 컴파일러가 진단한다 |
표 41.4
문. goto 를 아예 금지한 규약(MISRA 등)에서는 어떻게 하는가?
답. 96장에서 보게 될 MISRA 같은 규약은 goto 를 금지하거나 강하게 제한한다. 그런 환경에서 쓰는 대안은 셋이다.
- 함수로 빼기. 위의 ③ 그대로다. 정리 지점 문제도 「얻고-쓰고-되돌리는」 작은 함수로 나누면 상당 부분 사라진다.
- 한 번만 도는
do { … } while (0). 그 안에서break를 쓰면 「정리 지점으로 뛰기」와 같은 효과가 난다 — 아래 절의 무늬를 흐름 제어에 쓰는 것이다. - 상태 변수 하나로 단계 진행.
if (ok) { … }를 이어 붙이는 형태인데, 단계가 많아지면 오히려 읽기 어려워진다.
판단은 12장의 사다리대로 한다 — 규약이 있는 자리에서는 규약을 따르고, 그 대가 (코드가 길어지는 것)를 알고 치른다. 규약이 없는 자리에서 「goto 는 무조건 나쁘다」로 스스로를 묶을 이유는 없다.
41.4 매크로를 한 문장으로 — do { } while (0)
여러 문장을 담은 매크로에는 오래된 함정이 있다. 중괄호로 묶는 것만으로는 부족하다.
#define SWAP_BAD(a, b) { int t = (a); (a) = (b); (b) = t; }
if (x < y) SWAP_BAD(x, y); else puts("...");펼쳐 보면 이유가 드러난다 — if (x < y) { … }; else … 가 되어, 중괄호 뒤의 세미콜론이 빈 문장이 된다. if 는 그 빈 문장으로 끝나 버리고 else 는 짝을 잃는다. 실측하면 GCC 가 이렇게 말한다.
error: 'else' without a previous 'if'do { } while (0) 이 이 문제를 정확히 푼다. 중괄호 블록이면서 동시에, 뒤에 세미콜론을 요구하는 한 문장이 되기 때문이다.
| 매크로 몸통 | if/else 사이 | for 의 몸통(중괄호 없이) | 세미콜론 |
|---|---|---|---|
| 벌거벗은 여러 문장 | 깨진다 | 첫 문장만 반복된다 — 조용한 사고 | 필요 없다(헷갈린다) |
{ … } | 깨진다 | 괜찮다 | 붙이면 빈 문장이 생긴다 |
do { … } while (0) | 괜찮다 | 괜찮다 | 자연스럽게 요구된다 |
표 41.5
시연에서 SWAP 과 LOG 가 if/else 사이에서도, 중괄호 없는 for 몸통으로도 멀쩡히 동작하는 것이 그 확인이다.
문. while (0) 이면 한 번도 안 도는 것 아닌가?
답. do-while 이라서 몸통을 먼저 실행하고 조건을 검사한다(32장). 조건이 거짓이니 딱 한 번 돌고 끝난다 — 그것이 노리는 바다.
컴파일러도 이 무늬를 안다. 최적화 없이 빌드해도 「조건이 항상 거짓인 루프」로 접히므로 실행 비용은 0이다. 그래서 「한 번 도는 루프」라는 형태를 빌렸을 뿐, 실제로 루프가 도는 것은 아니다.
반례. 값을 내는 매크로에 do { } while (0) 을 쓰기
#define MAX(a, b) do { … } while (0) /* 값을 낼 수 없다 */
int m = MAX(x, y); /* 문장이라 대입할 수 없다 */do { } while (0) 은 문장을 만드는 도구다. 값이 필요하면 셋 중 하나를 고른다.
- 수식 매크로 —
#define MAX(a, b) ((a) > (b) ? (a) : (b)). 괄호를 빠짐없이 치고, 인자가 두 번 평가된다는 것을 감수한다(MAX(i++, j)가 사고인 이유다). static inline함수 — 타입이 있고 인자가 한 번만 평가된다. C99 이후 이것이 정공법이다(24장)._Generic으로 타입별 함수 고르기 — 여러 타입을 받아야 할 때(26·57장).
GCC·Clang 에는 ({ … }) 라는 문장 수식 확장이 있어 둘 다 되지만, 표준이 아니다(12장의 회색지대). 이식성이 필요하면 위의 셋으로 푼다.
실제 사례. 표준 라이브러리와 커널의 do { } while (0)
이 무늬는 관행이 아니라 사실상의 표준이다. 리눅스 커널의 헤더를 열면 do { } while (0) 이 수천 번 나오고, 코딩 스타일 문서가 여러 문장 매크로에 이 형태를 요구한다.
재미있는 변형도 있다. 아무 일도 하지 않는 매크로를 정의할 때 — 예컨대 디버그 빌드가 아닐 때의 로그 매크로 — 를 #define LOG(...) 로 비워 두면 세미콜론만 남아 if (x) LOG(…); 같은 자리에서 문제가 생긴다. 그래서 #define LOG(...) do { } while (0) 처럼 빈 do-while 로 정의한다. 아무 일도 하지 않으면서 「문장이다」라는 성질만 유지하는 것이다.
덤으로, 매크로 인자를 쓰지 않으면 「쓰이지 않는 변수」 경고가 날 수 있어 do { (void)(x); } while (0) 처럼 적는 관행도 함께 자리 잡았다.
41.5 그 밖의 관용구와 함정
examples/ch41/loop_idioms.c
/* 루프의 관용구 몇 가지 — 그리고 조용히 무는 자리들. */
#include <stddef.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
int a[] = { 10, 20, 30, 40, 50 };
size_t n = sizeof a / sizeof a[0];
puts("[iterating backwards - with size_t you cannot write i >= 0]");
printf(" the right shape (i-- > 0): ");
for (size_t i = n; i-- > 0; ) printf("%d ", a[i]);
puts("");
puts(" the wrong shape (size_t i = n-1; i >= 0; i--) loops forever -");
puts(" for an unsigned value 0 is followed by SIZE_MAX, so the condition never fails.");
printf(" with a signed index that shape works: ");
for (int i = (int)n - 1; i >= 0; i--) printf("%d ", a[i]);
puts("");
puts("\n[the comma operator - closing in from both ends]");
printf(" reversed: ");
int b[] = { 1, 2, 3, 4, 5, 6 };
size_t m = sizeof b / sizeof b[0];
for (size_t i = 0, j = m - 1; i < j; i++, j--) {
int t = b[i]; b[i] = b[j]; b[j] = t;
}
for (size_t i = 0; i < m; i++) printf("%d ", b[i]);
puts("");
puts("\n[sentinel loop - read and test in one step]");
const char *text = "hi!\n";
const char *p = text;
int c, count = 0;
while ((c = *p++) != '\0') { /* 괄호가 필수다 */
if (c == '\n') printf("\\n ");
else printf("%c ", c);
count++;
}
printf("\n read %d characters\n", count);
puts("\n[do not call strlen in the loop condition]");
const char *s = "measure once";
size_t len = strlen(s); /* 한 번만 잰다 */
size_t vowels = 0;
for (size_t i = 0; i < len; i++)
if (strchr("aeiou", s[i]) && s[i]) vowels++;
printf(" \"%s\" has %zu vowels (length %zu, measured once)\n", s, vowels, len);
puts("\n[write down what an empty body means]");
size_t skip = 0;
const char *q = " value";
while (q[skip] == ' ')
skip++; /* 몸통이 있는 형태로 적는다 */
printf(" skipped %zu leading spaces\n", skip);
puts(" while (q[skip++] == ' ') ; is shorter with an empty body,");
puts(" but the lone semicolon is easy to miss, and that causes accidents.");
return 0;
}
실행 결과
[iterating backwards - with size_t you cannot write i >= 0]
the right shape (i-- > 0): 50 40 30 20 10
the wrong shape (size_t i = n-1; i >= 0; i--) loops forever -
for an unsigned value 0 is followed by SIZE_MAX, so the condition never fails.
with a signed index that shape works: 50 40 30 20 10
[the comma operator - closing in from both ends]
reversed: 6 5 4 3 2 1
[sentinel loop - read and test in one step]
h i ! \n
read 4 characters
[do not call strlen in the loop condition]
"measure once" has 6 vowels (length 12, measured once)
[write down what an empty body means]
skipped 3 leading spaces
while (q[skip++] == ' ') ; is shorter with an empty body,
but the lone semicolon is easy to miss, and that causes accidents.
41.5.1 역순 순회 — size_t 로는 i >= 0 을 쓸 수 없다
가장 자주 무는 자리다.
for (size_t i = n - 1; i >= 0; i--) /* 무한 루프다 */부호 없는 값은 언제나 0 이상이라 조건이 영원히 참이고, 0 다음에 i-- 하면 SIZE_MAX 로 감아 돈다(27장). GCC 는 -Wall -Wextra 에 포함된 -Wtype-limits 로 “comparison of unsigned expression in >= 0 is always true” 라고 말해 준다.
바른 꼴은 둘이다.
for (size_t i = n; i-- > 0; ) /* 관용구 — 조건에서 감소까지 끝낸다 */
for (int i = (int)n - 1; i >= 0; i--) /* 첨자를 부호 있는 타입으로 */첫 꼴이 널리 쓰인다. i-- > 0 은 「지금 값이 0보다 큰지 보고, 그 뒤에 1을 뺀다」 이므로(49장의 후위 증감) 몸통에서는 n-1 부터 0 까지가 차례로 보인다.
41.5.2 나머지 관용구
| 관용구 | 무엇인가 | 주의 |
|---|---|---|
for (;;) / while (1) | 무한 루프. 둘은 완전히 같다 | GCC 는 어느 쪽에도 경고하지 않는다(실측). MSVC 가 while (1) 에 경고를 내던 역사 때문에 for (;;) 를 쓰는 코드베이스가 많다 |
| 쉼표 연산자 | for (i = 0, j = m - 1; i < j; i++, j--) | 두 끝에서 좁혀 오는 무늬. 이 자리 말고는 쉼표 연산자를 쓰지 않는 것이 관행이다 |
| 센티널 루프 | while ((c = getchar()) != EOF) | 괄호가 필수다 — 없으면 c = (getchar() != EOF) 가 된다. c 는 int 여야 한다(64장) |
루프 조건 속 strlen | for (i = 0; i < strlen(s); i++) | 매 바퀴 문자열을 처음부터 센다. 길이를 변수에 받아 둔다(42장) |
| 부동소수점 카운터 | for (double x = 0; x != 1.0; x += 0.1) | 끝나지 않을 수 있다 — 0.1 이 정확하지 않다(50장). 정수로 세고 나눈다 |
빈 몸통 ; | while (*p++) ; | 짧지만 세미콜론이 눈에 안 띈다. 아래 참조 |
표 41.6
반례. 눈에 안 띄는 빈 몸통
for (int i = 0; i < n; i++); /* 세미콜론 하나로 몸통이 사라졌다 */
total += a[i]; /* 루프 밖이다 — 게다가 i 는 여기 없다 */들여쓰기는 반복될 것처럼 보이는데 실제로는 루프가 빈 채로 돌고 끝난다.
★실측해 보면 경고가 도와주지 못한다. -Wempty-body 는 if (x); 는 “suggest braces around empty body in an ‘if’ statement” 로 잡지만, for (…); 와 while (…); 는 잡지 않는다. 일부러 쓰는 관용구이기 때문이다.
그래서 규율로 막는다 — 빈 몸통이 의도라면 그것을 눈에 보이게 적는다.
while (*p++ != '\0')
continue; /* "여기는 일부러 비었다" */문. do-while 에서 continue 는 어디로 가는가?
답. 조건 검사로 간다 — 몸통의 처음이 아니다. while 이나 for 에서와 같은 규칙이다(for 에서는 갱신식을 거쳐 조건으로 간다).
이것이 헷갈리는 이유는 do-while 의 조건이 아래에 적혀 있어서, continue 가 「위로 돌아간다」는 인상을 주기 때문이다. 실제 흐름은 「아래의 조건으로 내려가서, 참이면 다시 위로」다. 실측해 보면 while 로 적은 같은 코드와 출력이 똑같다.
중첩과 탈출, 그리고 매크로를 문장으로 만드는 법까지 갖췄다. 그런데 이 장의 예제가 계속 char 배열을 훑었다 — 다음 장이 그 배열에 붙은 약속, 곧 문자열이다.