Proven C BookEnglish GitHub

41 반복의 기법 — 중첩, 탈출, 그리고 블록 만들기

먼저 알아야 할 것

32장 반복 — 루프와 불변식 · while·for·do-while 3형제
39장 다차원 배열 · 행과 열, 그리고 기억 속의 배치

돌아보기

32장에서 루프 3형제를 배웠고, 39장에서 2차원 배열을 배웠다. 그러면 int a[1000][1000]의 모든 칸을 더하는 코드를 두 가지로 적을 수 있다 — 행을 먼저 도는 것과 열을 먼저 도는 것. 두 코드는 같은 답을 낸다. 무엇이 다른가?

답. 속도가 다르다 — 그것도 여덟 배쯤. 39장에서 본 대로 2차원 배열은 기억 속에 행이 먼저 늘어서 있고(행 우선 배치), 11장에서 본 대로 캐시는 이웃한 바이트를 함께 실어 온다. 행을 따라 걸으면 실어 온 줄을 알뜰히 쓰고, 열을 따라 걸으면 매 걸음 새 줄을 부른다.

루프를 배웠다는 것과 루프를 부릴 줄 안다는 것은 다르다. 이 장이 그 차이를 메운다.

이 장의 필요성과 맥락

32장에서 루프의 문법을 배웠지만, 실전 기법은 다차원 배열을 만난 뒤라야 뜻이 있다 — 중첩 루프도 순회 순서도 39장 없이는 예를 들 수 없기 때문이다. 그래서 반복이 두 장으로 갈라져 6부와 7부에 나뉘어 있다. 이 배치가 이 책에서 가장 멀리 떨어진 짝이다.

이 장이 끝나면

루프의 실전 기법을 모은다 — 중첩 루프와 순회 순서(실측으로 확인한다), 다중 루프에서 빠져나오는 네 가지 방법과 그 우열, goto가 정당한 두 자리와 지켜야 할 규율, 매크로를 한 문장으로 만드는 do { } while (0), 그리고 실무에서 반복해 만나는 관용구와 함정들. 32장이 「루프란 무엇인가」였다면 이 장은 「루프를 어떻게 부리는가」다.

이 장에서 답할 질문

  1. 그러면 언제나 안쪽에 마지막 첨자를 두면 되는가?
  2. goto 를 아예 금지한 규약(MISRA 등)에서는 어떻게 하는가?
  3. while (0) 이면 한 번도 안 도는 것 아닌가?
  4. 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 나 다른 방법을 쓴다
Javaouter: for (…) { break outer; }
Go같은 레이블 문법. continue 에도 붙는다
Rust'outer: loop { break 'outer; } — 값까지 반환한다
Perl·PHPlast 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_buflabel1 은 이름이 아니다
변수 초기화를 건너뛰지 않는다초기화를 건너뛴 변수는 불확정 값이고, 가변 길이 배열의 스코프로 뛰어드는 것은 제약 위반이라 컴파일러가 진단한다

표 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

시연에서 SWAPLOGif/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) 가 된다. cint 여야 한다(64장)
루프 조건 속 strlenfor (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-bodyif (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 배열을 훑었다 — 다음 장이 그 배열에 붙은 약속, 곧 문자열이다.