Proven C BookEnglish GitHub

20 수식과 상수 — 값이 되는 것

먼저 알아야 할 것

7장 정수의 표현 · 정수의 표현
19장 프로그램의 구조 · 문장과 수식의 자리

돌아보기

7장과 8장에서 수를 기계에 담는 법(2의 보수, IEEE 754)을 배웠다. 그러면 소스 코드에 수를 적으면 — 이를테면 12라고 쓰면 — 그것은 언제 기계의 비트가 되는가?

답. 컴파일 때다. 소스의 12는 그냥 글자 두 개(9장의 표현으로 문자 12)이고, 컴파일러가 그것을 읽어 2의 보수 비트 패턴으로 번역해 실행 파일에 심는다. 소스에 적힌 값 표기를 리터럴(literal)이라 부른다 — “글자 그대로”라는 뜻이다. 글자의 세계와 비트의 세계를 잇는 다리가 컴파일러라는 것 — 16장의 릴레이가 여기서도 일하고 있다.

이 장의 필요성과 맥락

뼈대 안으로 들어가는 첫 걸음이라 값을 적는 법이 먼저다. 이 장을 상수 표기의 집합소로 삼은 것은 의도적이다 — 정수·문자·부동소수점·문자열 표기를 흩어 두면 나중에 찾을 곳이 없어진다. 7장에서 배운 정수의 표현이 여기서 처음 소스에 적는 글자로 바뀐다.

이 장이 끝나면

문장의 안쪽으로 들어간다. 소스 코드에 값을 적는 법이 절반이고 — 정수·문자· 부동소수점·문자열 상수의 표기를 이 장에 한자리로 모아 두었으니, 나중에 「이 표기가 뭐였더라」 싶을 때 돌아올 자리다 — 나머지 절반이 값을 계산으로 엮는 법 (연산자와 수식(expression))과 계산의 순서(우선순위와 괄호)다. 단, 이 책의 원칙대로 연산자는 최소 집합만: 덧셈, 뺄셈, 곱셈, 괄호. 이것으로도 놀랄 만큼 멀리 간다.

이 장에서 답할 질문

  1. 그러면 소스에 0.1이라고 적으면, 기계에는 정확히 0.1이 담기는가?
  2. l 소문자를 쓰지 말라는 말을 자주 듣는데?
  3. 16진 부동소수점은 왜 지수부 p 가 필수인가? 그리고 왜 쓰는가?
  4. 나눗셈은 왜 미룬 것인가? 사칙연산에서 하나만 빠지니 오히려 어색한데.
  5. printf("안녕")도 값이 되는가? 수식의 자리에 함수 부르기가 적혀 있는데.

20.1 리터럴 — 소스에 적는 값

리터럴은 이미 여러 번 만났다. 헬로 월드의 0이 정수 리터럴이고, "안녕, 세상!\n"문자열 리터럴이다 — 따옴표 안의 글자들이 “글자 그대로” 값이 된다. 정수 리터럴에 소수점을 붙이면(3.5) 8장의 부동소수점 값이 된다.

문. 그러면 소스에 0.1이라고 적으면, 기계에는 정확히 0.1이 담기는가?

답. 아니다 — 8장에서 배운 그대로다. 컴파일러는 0.1이라는 리터럴을 가장 가까운 이웃(3FB9 9999 9999 999A)으로 번역한다. 리터럴은 “값을 그대로 적는 표기”이지만, 그 값이 표현 가능한 눈금 위에 없으면 번역 단계에서 이미 근사가 일어난다. 소스에 적었다고 정확한 것이 아니다 — 8장의 교훈이 소스 코드의 세계에서도 그대로 유효하다.

20.2 상수를 적는 법 — 한눈에 보기

앞 절에서 리터럴을 만났으니, 이 자리에서 소스에 값을 적는 표기를 한 번에 모아 둔다. 표준은 이것을 어휘 요소 조항(§6.4.4, §6.4.5)에 흩어 두었고, 실무에서 헷갈리는 것은 거의 언제나 접두어·접미어·이스케이프 셋이다.

이 절은 참조용이다. 지금 전부 외울 필요는 없고, 「그런 표기가 있다」는 지도만 얻어 두었다가 필요할 때 돌아오면 된다. 시연도 그런 뜻으로 읽는다 — 출력을 훑어 보며 표기와 결과를 눈으로 잇는 정도면 충분하다.

examples/ch20/constants.c

/* 상수를 적는 방법들 — 표기가 값과 타입을 어떻게 정하는가. */
#include <inttypes.h>
#include <stdio.h>
#include <string.h>
#include <uchar.h>

int main(void)
{
    puts("[integer constants - four bases and digit separators (C23)]");
    printf("  1234        = %d\n", 1234);          /* 10진 */
    printf("  0755        = %d   <- a leading 0 means octal\n", 0755);
    printf("  0xFF        = %d\n", 0xFF);
    printf("  0b1010      = %d   <- binary, new in C23\n", 0b1010);
    printf("  1'000'000   = %d   <- digit separators, new in C23\n", 1'000'000);
    printf("  0b11'10'11'01 = %d\n", 0b11'10'11'01);

    puts("\n[접미어가 타입을 정한다]");
    printf("  sizeof 1    = %zu, sizeof 1L  = %zu, sizeof 1LL = %zu\n",
           sizeof 1, sizeof 1L, sizeof 1LL);
    printf("  sizeof 1U   = %zu, sizeof 1wb = %zu  ← wb 는 C23 의 _BitInt\n",
           sizeof 1U, sizeof 1wb);

    puts("\n[문자 상수 — 접두어가 타입을 정한다]");
    printf("  'a'   크기 %zu, 값 %d      ← C 에서 문자 상수는 int 다\n",
           sizeof 'a', 'a');
    printf("  u8'a' 크기 %zu (char8_t)\n",  sizeof u8'a');
    printf("  u'a'  크기 %zu (char16_t)\n", sizeof u'a');
    printf("  U'a'  크기 %zu (char32_t)\n", sizeof U'a');
    printf("  L'a'  크기 %zu (wchar_t)\n",  sizeof L'a');
    /* 'ab' 같은 다중 문자 상수는 값이 구현 정의라 -Wmultichar 가 경고한다.
       여기서는 경고를 켠 채 두고, 값은 본문의 실측 표로만 보인다. */

    puts("\n[이스케이프 — 8진과 16진]");
    printf("  '\\101' = %d, '\\x41' = %d   ← 둘 다 'A'\n", '\101', '\x41');
    printf("  \"\\x41\" \"1\" = \"%s\"        ← 16진은 가장 긴 열을 먹는다.\n",
           "\x41" "1");
    puts("    그래서 \"\\x411\" 이 아니라 문자열을 쪼개 이어 붙인다.");

    puts("\n[부동소수점 상수]");
    printf("  3.14  1e3=%g  1.=%g  .5=%g\n", 1e3, 1., .5);
    printf("  0x1p-3 = %g            ← 16진 부동소수점. 지수부 p 는 필수다\n", 0x1p-3);
    printf("  sizeof 1.0 = %zu, 1.0f = %zu, 1.0L = %zu\n",
           sizeof 1.0, sizeof 1.0f, sizeof 1.0L);
    printf("  0.1 == 0.1f ? %s   ← 접미어가 다르면 값도 다르다\n",
           (double)0.1f == 0.1 ? "예" : "아니오");

    puts("\n[문자열 리터럴]");
    printf("  sizeof \"abc\" = %zu        ← NUL 이 한 칸 더 붙는다\n", sizeof "abc");
    printf("  \"hello, \" \"world\" = \"%s\"  ← 인접한 것은 하나로 이어진다\n",
           "hello, " "world");
    printf("  \"a\\0b\": strlen = %zu, sizeof = %zu  ← 안에 NUL 을 넣어도 배열은 남는다\n",
           strlen("a\0b"), sizeof "a\0b");
    printf("  sizeof u8\"\" = %zu, sizeof u\"\" = %zu, sizeof U\"\" = %zu\n",
           sizeof u8"\uAC00", sizeof u"\uAC00", sizeof U"\uAC00");
    return 0;
}

실행 결과

[integer constants - four bases and digit separators (C23)]
  1234        = 1234
  0755        = 493   <- a leading 0 means octal
  0xFF        = 255
  0b1010      = 10   <- binary, new in C23
  1'000'000   = 1000000   <- digit separators, new in C23
  0b11'10'11'01 = 237

[접미어가 타입을 정한다]
  sizeof 1    = 4, sizeof 1L  = 8, sizeof 1LL = 8
  sizeof 1U   = 4, sizeof 1wb = 1  ← wb 는 C23 의 _BitInt

[문자 상수 — 접두어가 타입을 정한다]
  'a'   크기 4, 값 97      ← C 에서 문자 상수는 int 다
  u8'a' 크기 1 (char8_t)
  u'a'  크기 2 (char16_t)
  U'a'  크기 4 (char32_t)
  L'a'  크기 4 (wchar_t)

[이스케이프 — 8진과 16진]
  '\101' = 65, '\x41' = 65   ← 둘 다 'A'
  "\x41" "1" = "A1"        ← 16진은 가장 긴 열을 먹는다.
    그래서 "\x411" 이 아니라 문자열을 쪼개 이어 붙인다.

[부동소수점 상수]
  3.14  1e3=1000  1.=1  .5=0.5
  0x1p-3 = 0.125            ← 16진 부동소수점. 지수부 p 는 필수다
  sizeof 1.0 = 8, 1.0f = 4, 1.0L = 16
  0.1 == 0.1f ? 아니오   ← 접미어가 다르면 값도 다르다

[문자열 리터럴]
  sizeof "abc" = 4        ← NUL 이 한 칸 더 붙는다
  "hello, " "world" = "hello, world"  ← 인접한 것은 하나로 이어진다
  "a\0b": strlen = 1, sizeof = 4  ← 안에 NUL 을 넣어도 배열은 남는다
  sizeof u8"가" = 4, sizeof u"가" = 4, sizeof U"가" = 8

20.3 정수 상수 (§6.4.4.1)

진법표기비고
10진1 ~ 9 로 시작1234
8진0 으로 시작0755 = 493가장 흔한 함정08 은 오류다
16진0x 또는 0X0xFF = 255
2진0b 또는 0B0b1010 = 10C23 에서 추가

표 20.1

뒤에 붙는 글자 — 접미어 — 도 있다. 10L, 1U, 3ULL 처럼 쓰는데, 이것은 값이 아니라 타입을 바꾸는 표시다.

접미어무슨 표시인가
u U부호 없는 타입으로
l Llong 이상으로
ll LLlong long 이상으로
wb WB_BitInt 로 (C23)

표 20.2

지금은 「접미어가 타입을 바꾼다」는 사실만 알아 두면 된다. 여기 나온 타입 이름들과, 접미어가 없을 때 컴파일러가 어떤 타입을 고르는지의 정확한 규칙은 27장에서 정수를 정식으로 배운 뒤 「정수 상수의 타입」 절에서 다룬다. 미리 한 가지만 — 같은 값을 십진으로 적느냐 16진으로 적느냐에 따라 타입이 달라질 수 있다.

문. l 소문자를 쓰지 말라는 말을 자주 듣는데?

답. 글꼴에 따라 1(숫자 1)과 l(엘)이 거의 같아 보이기 때문이다. 10l101 로 읽히는 사고가 실제로 있었다. 접미어는 대문자로10L, 1UL — 가 오랜 관행이고, 이 책도 그렇게 쓴다. 접두어 0x 는 소문자가 관행이라 방향이 반대인데, 이유는 같다: 눈에 잘 갈리는 쪽을 고른 것이다.

20.4 C23 의 자리 구분자 '

긴 수를 읽기 좋게 끊는 표기가 C23 에 들어왔다. 값에는 아무 영향이 없다 — 표준의 말대로 상수의 값을 정할 때 무시된다.

표준이 직접 든 예가 함정까지 함께 보여 준다. 실측으로 확인하면 이렇다.

적은 것결과
12'341234자리 구분자는 숫자 사이에만
0b11'10'11'012372진에도 쓸 수 있다
0x1'2'3'4AB'C'D30544174116진에도
0x'FF오류 — “digit separator after base indicator”0x 바로 뒤에는 못 온다
'1'2오류문자 상수 '1' 뒤에 2 가 온 것으로 읽힌다

표 20.3

마지막 줄이 이 표기의 유일한 위험이다. 구분자는 숫자와 숫자 사이에서만 구분자이고, 맨 앞에 오면 작은따옴표 — 즉 문자 상수의 시작 — 으로 읽힌다.

20.5 문자 상수 (§6.4.4.4)

타입 이름들은 9장에서 문자 집합을, 27장에서 정수를 배운 뒤라야 온전히 읽힌다 — 지금은 접두어가 타입을 정한다는 것만 보면 된다.

표기타입실측 크기비고
'a'int4★C 에서 문자 상수는 char 가 아니다
u8'a'char8_t1C23. UTF-8 코드 단위 하나여야 한다
u'a'char16_t2UTF-16 코드 단위 하나
U'a'char32_t4UTF-32 코드 단위 하나
L'a'wchar_t4 (리눅스), 2 (윈도)와이드 리터럴 인코딩(9장)
'ab'int4값이 구현 정의. GCC 는 -Wmultichar 로 경고한다

표 20.4

흔한 오해. 'a'char 니까 크기가 1이다”

C++ 에서는 맞고 C 에서는 틀리다. 실측하면 갈린다 — 같은 sizeof('a') 가 C 에서 4, C++ 에서 1이다.

표준의 문장은 「정수 문자 상수는 int 타입을 가진다」(§6.4.4.4p11)이다. 값은 「그 문자를 char 객체에 담았다가 int 로 변환했을 때의 값」이라, 값은 우리가 기대하는 그대로이고 타입만 넓다.

이 차이가 드러나는 자리는 대개 sizeof_Generic, 그리고 함수 오버로딩이 있는 C++ 쪽이다. C 만 쓴다면 실질적 피해는 거의 없지만, 두 언어에 함께 쓰이는 헤더에서는 알고 있어야 한다.

이스케이프는 다음이 전부다(§6.4.4.4).

갈래표기주의
반드시 이스케이프\' \\작은따옴표와 역슬래시는 반드시 이 형태로
해도 되고 안 해도 되고\" \?문자열 안에서는 \" 가 필요하다
비그래픽 문자\a \b \f \n \r \t \v의미는 §5.2.3 에 정의되어 있다
8진\ + 8진 숫자최대 세 자리
16진\x + 16진 숫자자릿수 제한이 없다
유니버설 문자 이름\uXXXX \UXXXXXXXX기본 문자 집합 밖의 글자를 이름으로

표 20.5

반례. "\x411" — 16진 이스케이프가 뒷글자까지 먹는다

표준이 못박는다 — 8진이나 16진 이스케이프는 그것을 이룰 수 있는 가장 긴 문자 열이다(§6.4.4.4p7). 8진은 세 자리에서 멈추지만 16진은 멈추지 않는다.

"\x411"      /* 'A'(0x41) 뒤에 '1' 이 아니라, 0x411 을 요구한 것 */

실측하면 GCC 가 hex escape sequence out of range 라고 경고한다. 고치는 법은 문자열을 쪼개어 잇는 것이다 — 인접한 문자열 리터럴은 하나로 이어지므로 (아래), "\x41" "1" 은 정확히 "A1" 이 된다.

20.6 부동소수점 상수 (§6.4.4.3)

십진 형태는 소수점이 있거나 지수부가 있거나 둘 중 하나를 갖춰야 한다. 그래서 1., .5, 1e3 이 모두 유효하고 1 은 정수 상수다.

부동소수점에도 접미어가 있다(타입의 정체는 8장과 50장에서 본다).

접미어타입실측 크기
(없음)double8
f Ffloat4
l Llong double16 (x86-64 리눅스)
df dd dl_Decimal32 / _Decimal64 / _Decimal128 (C23)4 / 8 / 16

표 20.6

16진 부동소수점(C99)도 있다 — 0x1p-3 은 정확히 0.125 다.

문. 16진 부동소수점은 왜 지수부 p 가 필수인가? 그리고 왜 쓰는가?

답. e 를 쓸 수 없기 때문이다. 16진에서 e숫자 14라 지수의 시작으로 읽을 수 없다. 그래서 이진 지수를 뜻하는 p 를 따로 두었고, p 는 생략할 수 없다 — 생략하면 어디까지가 가수인지 정할 수 없다. 0x1p-3 은 「1 × 2−3」이다.

쓰는 이유는 정확성이다. 8장에서 본 대로 십진 0.1 은 이진에 정확히 담기지 않지만, 16진 표기는 이진 표현을 그대로 옮겨 적는 것이라 번역에서 반올림이 일어나지 않는다. 그래서 부동소수점 시험의 기대값, 표준 라이브러리의 상수 표, 부동소수점을 다루는 논문의 예제가 이 표기를 쓴다. printf%a 가 같은 표기로 찍어 준다 (부록 B).

접미어가 값까지 바꾼다는 것도 짚어 둔다. 실측에서 (double)0.1f == 0.1거짓이다 — 0.1ffloat 눈금 위의 가장 가까운 값이고, 0.1double 눈금 위의 가장 가까운 값이라 서로 다른 수다(8·50장).

20.7 문자열 리터럴 (§6.4.5)

표기원소 타입인코딩실측 sizeof
"가"char리터럴 인코딩(9장)4 (UTF-8 3바이트 + NUL)
u8"가"char8_t언제나 UTF-84
u"가"char16_tUTF-164 (코드 단위 1 + NUL)
U"가"char32_tUTF-328
L"가"wchar_t와이드 리터럴 인코딩8 (리눅스)

표 20.7

네 가지 성질을 함께 기억한다.

  1. 끝에 NUL 이 붙는다. sizeof "abc" 는 3이 아니라 4다.
  2. 인접한 것은 하나로 이어진다(번역 단계 6). "hello, " "world" 는 한 문자열이다. 긴 문자열을 줄로 나눠 적는 정석이고, 위의 \x 함정을 푸는 방법이기도 하다. 단 접두어가 섞이면 안 된다u"a" U"b" 는 컴파일 오류다(제약 위반).
  3. 안에 NUL 을 넣어도 배열은 잘리지 않는다. "a\0b"strlen 이 1, sizeof 는 4다 — 문자열 함수가 멈출 뿐 데이터는 다 있다.
  4. 수정하면 정의되지 않은 동작이다. 실측하면 대개 실행 중에 죽는다(읽기 전용 구역에 놓이기 때문이다). 그래서 문자열 리터럴은 const char * 로 받는다.

반례. char *s = "abc"; s[0] = 'X';

컴파일은 통과하고(C 에서 문자열 리터럴의 타입은 const 가 아닌 char[N] 이다), 실행하면 죽는다 — 실측에서 SIGSEGV 였다.

C++ 는 이 자리를 아예 막았다(문자열 리터럴이 const char[N] 이라 대입이 오류다). C 는 옛 코드 호환 때문에 열어 두었고, 그래서 습관으로 막아야 한다const char *s = "abc"; 로 받으면 컴파일 시점에 잡힌다. -Wwrite-strings 로 경고를 켜 두는 방법도 있다.

20.8 상수처럼 보이는 것들

표기정체자세히
RED (열거 상수)정수 상수다. 타입은 int55장 — 보통 식별자 마당에 산다
nullptrnullptr_t 타입의 예약어 (C23)36장
true falsebool 값을 주는 예약어 (C23)30장
(int[]){1,2,3} 복합 리터럴상수가 아니라 객체다 — 주소를 얻을 수 있다47장
#define N 100상수가 아니라 토큰 치환이다57장
constexpr int n = 10;C23 의 진짜 상수 — 상수 수식에 쓸 수 있다23장

표 20.8

마지막 두 줄의 구별이 실무에서 값을 한다. 매크로는 타입도 스코프도 없고(57장), const int 는 C 에서 상수 수식이 아니다 — C++ 와 갈리는 자리다. 다만 「못 쓴다」가 아니라 어디서 못 쓰는가가 정확한 말이라, 실측으로 갈라 둔다.

const int n = 10; 을 이렇게 쓰면결과(GCC, C23)
블록 안에서 int a[n];통과한다 — 다만 상수 배열이 아니라 가변 길이 배열(VLA)이 된다
파일 스코프에서 int a[n];오류 — variably modified 'a' at file scope
블록 안에서 static int a[n];오류 — storage size of 'a' isn't constant
case n:오류 — case label does not reduce to an integer constant

표 20.9

진짜 상수 수식이 필요한 자리에서 걸린다. C23 의 constexpr 이 그 자리를 메우려고 들어왔다.

20.9 수식 — 계산되어 값이 되는 것

리터럴을 연산자로 엮으면 수식(expression)이 된다. 2 + 3 * 4 처럼. 수식의 정의는 한 문장이면 된다 — 계산되어(평가되어) 값이 되는 것. 이 “값이 된다”는 성질이 수식의 전부이자, 앞으로 C를 읽는 눈의 절반이다. 코드의 어느 자리에 값이 필요하든, 그 자리에는 수식이 올 수 있다 — 리터럴 하나도 가장 단순한 수식이다.

이 장의 연산자는 넷뿐이다 — 더하기 +, 빼기 -, 곱하기 *, 그리고 괄호 ( ). 이 책의 나선형 원칙대로, 나머지 연산자들은 필요해지는 장에서 하나씩 합류한다(비교는 30장, 나눗셈은 28장 — 나눗셈이 뒤로 미뤄진 이유는 잠시 뒤에).

시연이다. 아래 예제의 %d는 “이 자리에 정수 값을 십진법으로 찍어라” 라는 표시로, 정식 설명은 22장에서 한다 — 지금은 계산 결과를 눈으로 확인하는 창으로만 쓴다.

examples/ch20/expr.c

#include <stdio.h>

int main(void)
{
    printf("%d\n", 2 + 3 * 4);    /* 곱셈이 먼저 계산된다 */
    printf("%d\n", (2 + 3) * 4);  /* 괄호가 순서를 명시한다 */
    return 0;
}

실행 결과

14
20

20.10 순서 — 우선순위, 그리고 괄호라는 관행

위 시연의 첫 줄이 14인 것은 곱셈이 덧셈보다 먼저 계산됐기 때문이다. 수학 시간의 관례 그대로이고, C는 이런 “누가 먼저인가”의 서열 — 우선순위(precedence) — 을 모든 연산자에 정해 두었다.

여기서 이 책의 권고를 미리 밝힌다. 우선순위 표를 외우지 말고, 괄호를 써라. C의 연산자는 수십 개이고 서열표는 15줄이 넘는다 — 그것을 다 외운 사람들끼리도 헷갈려서 사고가 난다. 둘째 줄처럼 괄호로 순서를 명시하면 외울 것도, 오해할 것도 없다. 전체 서열표는 부록에 참고 자료로 두고, 본문은 “산술은 수학 관례대로, 그 밖에는 괄호”로 간다.

문. 나눗셈은 왜 미룬 것인가? 사칙연산에서 하나만 빠지니 오히려 어색한데.

답. 정수의 나눗셈이 수학의 나눗셈과 다르기 때문이다. C에서 7 / 2는 3.5가 아니라 3이다 — 정수끼리의 나눗셈은 소수점 아래를 버린 몫이고, 짝꿍 연산자 %(나머지)와 세트로 이해해야 뜻이 선다. 그 “버림”의 정확한 규칙(음수에서는 어느 쪽으로 버리나)까지 포함해서, 7장의 정수 세계 위에서 제대로 다룰 가치가 있는 주제라 28장에 자리를 잡아 두었다. 어중간하게 지금 소개해서 “7/2=3.5인 줄 알았는데”라는 오개념을 만드는 것보다, 정색하고 한 번에 다루는 것이 낫다는 판단이다.

문. printf("안녕")도 값이 되는가? 수식의 자리에 함수 부르기가 적혀 있는데.

답. 좋은 눈이다 — 된다. 함수를 부르는 것 자체가 수식이고, 따라서 값이 된다. 그 값이 무엇이고 어디로 가는지가 바로 다음 장의 주제다. 이 질문 하나로 다음 장의 문이 이미 반쯤 열렸다.

20.11 미리 심는 주의 하나 — 계산의 시간 순서는 다를 수 있다

우선순위에 관해, 지금은 씨앗만 심어 둘 주의가 하나 있다. 우선순위는 “누가 누구의 재료인가”라는 묶기의 규칙이지, 기계가 시간상 무엇을 먼저 계산하는가가 아니다 — 이를테면 덧셈의 왼쪽 재료와 오른쪽 재료 중 어느 쪽을 먼저 계산할지는 표준이 정해 두지 않았고, 컴파일러마다 다를 수 있다. 2 + 3 같은 수식에서야 아무래도 좋지만, 재료의 계산이 출력 같은 흔적을 남기는 경우에는 그 흔적의 순서가 달라질 수 있다는 뜻이다.

다만 안심할 것 — 문장과 문장 사이의 순서는 철석같이 보장된다. 19장의 “위에서 아래로 한 문장씩”은 계약이다. 흔들릴 수 있는 것은 한 문장 안쪽뿐이고, 그 안쪽의 정확한 규칙(부수효과, 시퀀스 포인트라는 개념)은 재료가 더 갖춰진 뒤 33장에서 정면으로 다룬다. 지금의 실전 수칙은 하나면 된다: 순서가 중요한 일들은 한 문장에 욱여넣지 말고 문장을 나눈다.

정리하면 — 소스에 값을 적는 표기가 리터럴이고, 계산되어 값이 되는 것이 수식이며, 묶는 순서는 산술 관례와 괄호로 다스리되 한 문장 안의 계산 시간 순서는 달라질 수 있다. 다음 장에서는 값을 소비하고 생산하는 가장 중요한 장치 — 함수를 부르는 법 — 를 익힌다. 헬로 월드의 심장 printf(...)가 드디어 정면으로 다뤄진다.