Subsections


3. Expressions

C 언어 디자인 목표 중의 하나는 효과성을 강조합니다 -- 즉 C 컴파일러가 상대적으로 작고 만들기 쉽게 하자는 것과, (기계어) 코드를 쉽게 생성할 수 있도록 하자는 것입니다. 이 두가지 목표는 C 언어 specification에 큰 영향을 미쳤습니다. 비록 C 언어가 좀 더 tight하게 정의되었으면 하는 사용자들과 C 언어가 지원하는 것보다 좀 더 많은 것을 (예를 들어 사용자의 실수를 미리 방지하는 기능) 요구하는 사용자들에게는 반가운 내용은 아니었지만 말입니다.

3.1 Evaluation Order

복잡한 expression (수식) 안에서 subexpression을 (부분식) 평가하는 순서는 완전히 컴파일러 마음대로입니다; 이 순서는 여러분이 생각하는 operator precedence와는 (연산자 우선 순위) 별 상관이 없습니다. 여러개의 보이는 부작용이 (multiple visible side effects) 없거나, 한 변수에 여러 개의 side effect가 평행하게 (parallel) 작용하지 않는 한 컴파일러의 평가 순서는 생각할 필요가 없습니다. 그렇지 않다면 이러한 경우, 컴파일러의 행동은 정의되어 있지 않을 수 있습니다. (The behavior may be undefined.)



Q 3.1
이 코드가 왜 동작하지 않을까요?

  a[i] = i++;

Answer
부분식인 `i++'은 부작용을 일으킬 수 있습니다 -- 즉 i의 값이 변경됩니다 -- 수식의 다른 부분에 i가 또 쓰이기 때문에, 이것이 변경되기 이전의 값일지, 변경된 다음의 값일지 알 수가 없습니다. (K&R에서는 이런 수식이 어떠한 행동을 취할 지 명세되지 않았다고(unspecified) 하지만, C 표준에서는 이런 수식의 결과에 대해 정의되지 않았다고(undefined) 강력하게 말하고 있습니다 -- 질문 [*]11.33을 참고하기 바랍니다.)

References
[K&R1] § 2.12
[K&R2] § 2.12
[C89] § 6.3
[H&S] § 7.12 pp. 227-9



Q 3.2
제 컴파일러로 다음과 같은 코드를 실행하면:

  int i = 7;
  printf("%d\n", i++ * i++);

`49'를 출력합니다. 평가 순서(order of evaluation)에 상관없이, `56'을 출력해야 하지 않을까요?

Answer
증가(++), 감소(--) 연산자가 뒤에 쓰일 때에는 먼저 기존의 값을 계산한 다음, 증가/감소하게 됩니다. “after”라는 말이 쓰이긴 하지만 잘못 이해하고 있는 것입니다.

즉 기존의 값을 만든 다음 바로 증가/감소를 할지, 아니면 다른 부분식을 평가하고 난 다음에 할 지는 보장할 수 없습니다. 보장되는 것은 증가/감소 연산이 전체 수식이 끝나기 전에 (ANSI C의 표현을 빌리자면 뒤따르는 “sequence point”로 넘어가기 전에; 질문 [*]3.8 참고) 이루어진다는 것 뿐입니다. 위의 코드에서는 컴파일러가 기존의 값으로 곱한 다음, 증가시키기 때문에 그런 결과가 나오는 것입니다.

부작용이 예상되는 것을 동시에 같은 식에서 쓰면 그 행동양식은 정의되어 있지 않습니다. (대충 말하면 ++, --, =, +=, -=등이 한 수식에서 쓰여서 같은 오브젝트(변수)가 두 번 이상 변경될 경우를 의미합니다; 정확한 정의는 질문 [*]3.8을 참고하기 바라며 “정의되어 있지 않다(undefined)”라는 용어에 대해서는 질문 [*]11.33을 참고하기 바랍니다.) 이러한 상황에서 여러분의 컴파일러가 어떻게 동작할 지 알려고 할 필요가 없습니다 (많은 C 교과서에서 잘못된 설명을 하고 있습니다); K&R에서 언급했던 것처럼, “다양한 컴퓨터에서 어떻게 동작하는 지를 모른다면, 그것을 아예 모르는 것이 낫습니다. (원문: If you don't know how they are done on various machine, that innocence may help to protect you.)

Note
A 란의 “after”란 단어는 원문을 보시면 더 빨리 이해하실 수 있습니다:

Although the postincrement and postdecrement operators ++ and -- perform their operations after yielding the former value, the implication of “after” is often misunderstood.

References
[K&R1] § 2.12 p. 50
[K&R2] § 2.12 p. 54
[C89] § 6.3
[H&S] § 7.12 pp. 227-9
[CT&P] § 3.7 p. 47
[PCS] § 9.5 pp. 120-1



Q 3.3
다음과 같은 코드를 여러 컴파일러에서 실행해 보았습니다:

  int i = 3;
  i = i++;

어떤 컴파일러는 i가 3이라고 하며, 또 4를 출력하는 컴파일러도 있었습니다. 어떤 컴파일러가 맞는 것인가요?

Answer
여기에는 올바른 답이 없습니다; 위와 같은 수식은 행동 양식이 정의되어 있지 않습니다. 질문 [*]3.1, [*]3.8, [*]3.9, [*]11.33을 참고하기 바랍니다. (i++++i는 둘 다 i + 1과 같지 않습니다. 원하는 것이 단순히 i 값을 증가시키는 것이라면 i=i+1, i+=1, i++, ++i 중 하나를 쓰시기 바랍니다. 질문 [*]3.12를 참고하기 바랍니다)



Q 3.3b
다음과 같은 테크닉은 ab를 임시 변수없이 바꾸어 준다고 합니다:

  a ^= b ^= a ^= b

이 코드가 올바른가요?

Answer
이식성(portability)이 없을 뿐만 아니라, 제대로 동작하지도 않는 코드입니다. 위의 코드는 한 `sequence point'에서 변수 a의 값을 두번이나 변경하려 하기 때문에, 행동 양식이 정의되어 있지 않습니다.

예를 들어 다음과 같은 코드를 SCO 최적화 C 컴파일러(icc)에서 돌렸을 경우, b를 123으로 설정하고 a를 0으로 설정한다고 보고되었습니다:

  int a = 123, b = 7654;
  a ^= b ^= a ^= b;

질문 [*]3.1, [*]3.8, [*]10.3, [*]22.15c를 참고하기 바랍니다.



Q 3.4
괄호(parentheses)를 써서 평가 순서를 제가 원하는 대로 바꿀 수 있을까요? 만약에 괄호를 쓰지 않더라도 알아서 되지 않나요?
Answer
항상 그런 것은 아닙니다.

연산자 우선 순위와 괄호는 수식 평가의 일부분만을 변경할 수 있습니다. 다음과 같은 수식에서:

  f() + g() * h()

우리는 곱셈이 덧셈보다 먼저 일어난다는 것을 알고 있습니다. 그러나, 세개의 함수 중 어떤 함수가 먼저 호출될지는 알 수 없습니다. 즉, 우선 순위는, 평가에서 일부분 영향을 미치는 것이며, 각 피연산자(operand)의 평가 순서에 영향을 주지는 못합니다.

괄호로 둘러 싸는 것은 어떤 피연산자(operand)가 어떤 연산자(operator)와 연결될 것인지를 결정하지만, 마찬가지로, 평가의 모든 부분에 영향을 줄 수 없습니다. 다음과 같이 괄호를 쓰더라도:

  f() + (g() + h())
함수 실행 순서에 영향을 주지 못합니다. 비슷하게, 질문 [*]3.2에서 나온 식을 괄호로 둘러 싸는 것도 아무 영향을 주지 못합니다. 왜냐하면 ++는 원래 *보다 우선 순위가 높기 때문입니다:
  (i++) * (i++)     /* WRONG */
위 수식은 괄호가 있던 없던 상관없이 `undefined behavior'에 해당합니다.

부분식(subexpression)의 평가 순서가 중요할 때에는, 임시 변수를 만들고 각각 다른 문장(statement)으로 나눠 쓰는 것이 좋습니다.

References
[K&R1] § 2.12 p. 49, § A.7 p. 185
[K&R2] § 2.12 pp. 52-3, § A.7 p. 200



Q 3.5
그렇다면 &&, || 연산자에서는 어떤가요? 다음과 같은 코드를 본 기억이 있거든요.
  while ((c = getchar()) != EOF && c != '\n')
    ....
Answer
위 코드는 이른바 `short-circuit' 예외(exception)라고 하는 예외 사항입니다. 즉, 연산자의 왼쪽의 결과만 가지고도 전체 결과를 알 수 있을 때에는 오른쪽은 평가되지 않습니다 (즉, || 연산자에서 왼쪽이 참이거나, && 연산자에서 왼쪽이 거짓인 경우). 그러므로, 콤마(comma) 연산자와 마찬가지로 이 연산자들은 왼쪽에서 오른쪽으로 평가된다는 것을 보장할 수 있습니다. 게다가 이 연산자들은 모두 (?: 연산자 포함) 추가로 내부적인 `sequence point'를 가지고 있습니다. (질문 [*]3.6, [*]3.8 참고)

References
[K&R1] § 2.6 p. 38, § A7.11-12 pp. 190-1
[K&R2] § 2.6 p. 41, Secs. A7.14-15 pp. 207-8
[ANSI] § 3.3.13, § 3.3.14, § 3.3.15
[C89] § 6.3.13, § 6.3.14, § 6.3.15
[H&S] § 7.7 pp. 217-8, § 7.8 pp. 218-20, § 7.12.1 p. 229
[CT&P] § 3.7 pp. 46-7



Q 3.6
조건에 따라서 &&, || 연산자의 오른쪽이 평가되지 않는다고 보장할 수 있나요?
Answer
보장합니다.
  if (d != 0 && n / d > 0) {
    /* average is greater than 0 */
  }
이나,
  if (p == NULL || *p == '\0') {
    /* no string */
  }
는 C 코드에서 매우 자주 볼 수 있는 것입니다. 이는 이른바 `short circuit'이라고 합니다. 만약 이 `short circuit'이 없다면, 첫번째 예제의 &&의 오른쪽에서, d가 0일 경우, 0으로 나누는, `divide by 0' 에러가 발생합니다. 두번째 예제에서는, 만약 p가 널 포인터일 경우, 존재하지 않는 메모리 공간을 참조하는 에러가 발생할 것입니다.
References
[ANSI] § 3.3.13, § 3.3.14
[C89] § 6.3.13, § 6.3.14
[H&S] § 7.7 pp. 217-8



Q 3.7
아래 코드에서 왜 f2가 먼저 호출되나요? 제 생각으로는 콤마(,) 연산자는 왼쪽에서 오른쪽으로 평가하는 (evaluation) 것으로 알고 있는데요.
  printf("%d %d", f1(), f2());        
Answer
콤마(,) 연산자는 왼쪽에서 오른쪽으로 평가하는 것이 보장되어 있습니다. 그러나 함수 호출에서 각 인자를 구별하기 위해 사용하는 콤마(,)는 콤마 연산자가 아닙니다.3.1 함수 호출에서 각 인자의 평가 순서는 정해지지 않았습니다. (unspecified) (질문 [*]11.33을 참고하기 바랍니다.)
References
[K&R1] § 3.5 p. 59
[K&R2] § 3.5 p. 63
[ANSI] § 3.3.2.2
[C89] § 6.3.2.2
[H&S] § 7.10 p. 224



Q 3.8
이러한 복잡한 규칙을 다 알아야 하나요? 또 `sequence point'라는 것은 무엇인가요?
Answer
`sequence point'라는 것은 어떤 시간대 (전체 수식의 평가가 끝난 시점, 또는 ||. &&, ?:, 또는 콤마(comma) 연산자, 또는 함수 호출 바로 이전)의 위치를 의미하는 것으로, 모든 부작용이 일어나지 않는다고 보장하는 시점입니다. 표준에서 `sequence point'라고 하는 것은 다음과 같은 상황을 말합니다:

ANSI/ISO C 표준에서는 다음과 같이 정의하고 있습니다:

Between the previous and next sequence point an object shall have its stored value modified at most once by the evaluation of an expression. Furthermore, the prior value shall be accessed only to determine the value to be stored.
위에서 두번째 문장이 어려울 수도 있습니다. 즉, 어떤 오브젝트에 값을 쓰는(write) 경우, 전체 수식은 이 오브젝트에 저장할 값을 계산하기 위한 목적으로 쓰여야 한다는 것을 의미합니다. This rule effectively constrains legal expressions to those in which the accesses demonstrably precede the modification.

질문 [*]3.9를 참고하기 바랍니다.

References
[C89] § 5.1.2.3, § 6.3, § 6.6, Annex C
[ANSI Rationale] § 2.1.2.3
[H&S] § 7.12.1 pp. 228-9



Q 3.9
다음 코드에서 배열의 몇번째 요소에 값을 쓸 지는 모르지만, i는 단 한번만 증가되는 것으로 생각할 수 있나요?

  a[i] = i++;

Answer
아닙니다! 먼저, 몇번째 요소에 값을 쓰는 지 상관없다면, 왜 저런 식으로 코드를 만들었나요? 그리고, 일단 수식이나 프로그램의 행동 양식이 정의되어 있지 않다라고 (undefined) 했으면, 모든 면에서 어떻게 동작할 지 모릅니다. 만약 `undefined expression'이 두 개의 해석이 가능하다고 해도, 컴파일러가 그 두 개의 해석 중 하나를 선택할 것이라고 가정해서는 안됩니다. 표준은 이런 경우에, 컴파일러가 어떤 선택을 해야 한다고 말하지 않습니다. 그리고, 실제로 어떤 컴파일러는 전혀 다른 방식을 쓰기도 합니다. 위 코드의 경우, 우리는 a[i] 또는 a[i + 1]에 값이 들어갈지 모를 뿐더러, 전혀 다른 요소에 (또는 아예 이 배열과는 전혀 상관없는 곳에) 쓸 수 있습니다. 그리고 이 문장 실행 뒤에, i가 어떤 값을 갖고 있는지 전혀 예측할 수 없습니다. 질문 [*]3.2, [*]3.3, [*]11.33, [*]11.35를 참고하기 바랍니다.



Q 3.10
사람들이 i = i++가 `undefined behavior'를 낳는다고 계속 말하지만, 제가 ANSI 표준 호환 컴파일러에서 실험한 결과, 예상했던 값을 얻었습니다.
Answer
질문 [*]11.35를 보기 바랍니다.



Q 3.11
이 모든 복잡한 규칙을 모르고, 평가 순서에서 발생할 지도 모르는 `undefined behavior'를 피할 수 있을까요?
Answer
가장 쉬운 방법은, 여러가지 방식으로 해석되지 않는, 단일한 의미를 가지는 expression을 쓰면, `undefined behavior'를 피할 수 있습니다. (물론 여기에서 `여러가지 방식으로 해석되지 않는'이라는 말이 사람마다 다를 수도 있지만, a[i] = i++i = i++이 여러가지 방식으로 해석될 수 있다고 생각하는 사람이면, 문제없을 것 같습니다.)

좀 더 자세히 알아보면, 다음과 같은 규칙을 지키면, 컴파일러나 동료 개발자들에게 확실한 의미를 전달하는데 도움이 됩니다. (표준보다 좀 더 보수적인 규칙일 수 있습니다.)

  1. 각 expression이 최대 하나의 object만을 변경할 (modification) 수 있게 합니다: 예를 들어, 간단한 변수, 배열의 한 요소, 포인터가 가리키는 대상 (e.g. *p). 여기서 modification이란, = 연산자를 쓴 간단한 대입이나, +=, -=, *=와 같은 연산자를 (compound assignment) 쓴 대입, ++-를 쓴 증가 또는 감소를 쓴 것을 뜻합니다.
  2. 한 expression에서 어떤 object가 한 번 이상 나오며, 이 object의 값이 변경되는 경우에, 저장될 새로운 값을 계산하기 위해, 기존 값을 쓰는 형태가 되도록 합니다. 이 규칙에 따라서 i = i + 1과 같은 수식을 쓸 수 있습니다. 왜냐하면, 비록 i가 두 번 쓰였지만, i의 새 값을 계산하기 위하여, 기존 i 값을 썼기 때문입니다.
  3. 첫번째 규칙을 어긋나는 경우가 꼭 필요하다면, 변경되는 여러 object들이 서로 구별되어 계산되는 것이어야 합니다. 또한, 많아야 두개 또는 세개 정도의 변경이 이루어지도록 하며, 가능하면 지금 소개하는 예의 한 꼴이 되도록 합니다. (이 경우에도 두번째 규칙이 지켜지도록 해야 합니다.)
    이 규칙에 따라서 우리는 c = *p++와 같은 연산을 쓸 수 있습니다. 왜냐하면, 각 object들이 (cp) 서로 다르게 계산되기 때문입니다. *p++ = c도 쓸 수 있습니다, because p and *p (i.e., p itself and what it points to) are both modified but are almost certainly distinct. Similarly, both c = a[i++] and a[i++] = c are allowed, because c, i, and a[i] are presumably all distinct. Finally, expressions in which three or more things are modified--e.g., p, q, and *p in *p++ = *q++, and i, j, and a[i] in a[i++] = b[j++]--are allowed if all three objects are distinct, i.e., only if two different pointers p and q or two different array indices i and j are used.
  4. You may also break the first rules if you interpose a defined sequence point operator between the two modifications or between the modification and the access. This expression (commonly seen in a while loop while reading a line) is legal because the second access of variable c occurs after the sequence point implied by &&.
      (c = getchar()) != EOF && c != '\n'
    
    Without the sequence point, the expression would be illegal because the access of c while comparing it to '\n' on the right does not “determine the value to be stored” on the left.

3.2 Other Expression Questions

C 언어는 expression(수식)에서 각각 다른 타입의 operand(피연산자)를 변경하는 적당한 규칙을 가지고 있습니다. 보통 이런 규칙은 매우 간단합니다. 그러나 예상할 수 없는 결과가 나올 수 있으며, 질문 [*]3.14와 [*]3.15가 그러한 상황에 대해 설명해 줍니다. 덧붙여, 이 section의 질문들은 autoincrement operator와 conditional ?: (또는 “ternary”라고도 하는) operator에 대한 것도 다룹니다.



Q 3.12
만약 수식의 값을 쓰지 않는다면, 변수의 값을 증가시키기 위해, i++을 써야 하나요, ++i를 써야 하나요?
Answer
두 expression 모두, 그 expression의 값이 다른 expression의 내부에 쓰일 경우 (containing expression), 어떻게 값이 해석되느냐에 차이가 있는 것이므로, 간단히 변수의 값을 증가시키기 위한 목적으로 쓴다면, 아무런 차이가 없습니다. (만약 containing expression이 없는 경우에는 full expression이라고 합니다.)

또한 full expression이라는 전제 아래에서는 (i++++i와 같은 것과 비슷하게) i += 1i = i + 1이 완전히 같습니다. (그러나, C++에서는 ++i의 형식을 더 선호합니다.) 덧붙여 질문 [*]3.3도 참고하시기 바랍니다.

References
[K&R1] § 2.8 p. 43
[K&R2] § 2.8 p. 47
[C89] § 6.3.2.4, § 6.3.3.1
[H&S] § 7.4.4 pp. 192-3, § 7.5.8 pp. 199-200



Q 3.13
어떤 숫자 값이 주어진 범위 안에 들어 있는지 검사해야 합니다. 다음과 같이 했는데 왜 제대로 동작하지 않을까요?
  if (a < b < c)
    ...
Answer
`<'와 같은 관계 연산자는 (relational operator) 모두 binary operator입니다; 즉, 두 개의 피연산자를 받아서 처리 결과를 참(1), 또는 거짓(0)으로 알려줍니다. 따라서 a < b < c는 먼저 a < b를 검사하고, 그 결과 0 또는 1을 돌려줍니다. 그래서 결국 평가하는 것은 0 < c 또는 1 < c가 됩니다. (좀 더 확실히 알기 위해서, a < b < c(a < b) < c로 생각하면 쉽습니다. 왜냐하면 컴파일러가 해석하는 순서와 같기 때문입니다.) 한 수치가 어떤 범위에 포함되는지 알고 싶으면, 다음과 같은 코드를 써야 합니다:
  if (a < b && b < c)
References
[K&R1] § 2.6 p. 38
[K&R2] § 2.6 pp. 41-2
[ANSI] § 3.3.8, § 3.3.9
[C89] § 6.3.8, § 6.3.9
[H&S] § 7.6.4, § 7.6.5 pp. 207-10



Q 3.14
이 코드는 왜 동작하지 않을까요?

  int a = 1000, b = 1000;
  long int c = a * b;

Answer
C 언어의 `integral promotion' 규칙에 의해 위의 곱셈은 `int' 타입의 곱셈으로 계산됩니다. 따라서 그 결과가 오버플로우(overflow) 되거나, 또는 `promotion'하기 전에 잘려나갈(truncate) 수 있습니다. 따라서 `long' 타입의 곱셈을 수행하라고 (강제로) 다음과 같이 알려주어야 합니다:

  long int c = (long int)a * b;

또는 다음과 같이 합니다:

  long int c = (long int)a * (long int)b;

`(long int)(a * b)'와 같이 하는 것은 질문의 코드와 똑같은 결과를 만드므로 바람직하지 않습니다. 이런 식으로 캐스팅을 하는 것은 (즉, 곱셈이 끝난 결과를 캐스팅하는 것) 결과 값을 long int 타입에 대입할 때, 어차피 자동적으로 변환되는 것이기 때문에 (implicit conversion), 쓰나마나 한 것이 되어 버립니다.

결과값이 실수 타입인 경우, 나눗셈을 할 경우에도 비슷한 문제가 발생할 수 있습니다. 해결 방법은 위와 같습니다.

덧붙여 질문 [*]3.15도 참고하시기 바랍니다.

References
[K&R1] § 2.7 p. 41
[K&R2] § 2.7 p. 44
[C89] § 6.2.1.5
[H&S] § 6.3.4 p. 176
[CT&P] § 3.9 pp. 49-50



Q 3.15
왜 아래 코드는 계속 0이 나올까요?
  double degC, degF;
  degC = 5 / 9 * (degF - 32);

Answer
어떤 binary operator의 두 operand가 integer인 경우, expression의 나머지 부분이 어떤 타입인지에 상관없이, 정수로 계산합니다. 위 계산에서, 나눗셈 부분은 양쪽이 모두 정수이기 때문에 정수값으로 계산하면, 5 / 9 = 0이 나옵니다. (부분 식에서 예상하지 못한 방법으로 계산되는 것은 꼭 int나 나눗셈에서만 발생하는 것은 아닙니다.) 상수를 int가 아닌 float이나 double로 쓰면, 이 문제는 해결되며, 또는 적절하게 float이나 double로 캐스팅해도 해결됩니다:
  degC = (double)5 / 9 * (degF - 32);
또는,
  degC = 5.0 / 9 * (degF - 32);
캐스팅할 때, 반드시 하나 또는 두 연산자에 캐스팅이 이루어져야 합니다. 아래와 같이 계산이 끝난 다음에 캐스팅하는 것은 아무런 도움이 되지 못합니다:
  degC = (double)(5 / 9) * (degF - 32);

덧붙여 질문 [*]3.14도 참고하시기 바랍니다.

References
[K&R1] § 1.2 p. 10, § 2.7 p. 41
[K&R2] § 1.2 p. 10, § 2.7 p. 44
[ANSI] § 3.2.1.5
[C89] § 6.2.1.5
[H&S] § 6.3.4 p. 176



Q 3.16
어떤 조건에 따라 서로 다른 변수에 복잡한 계산 결과를 대입하려고 합니다. 다음과 같은 코드를 써도 좋습니까?

  ((condition) ? a : b) = complicated_expression;

Answer
안됩니다. ?: 연산자는 대부분 연산자들과 같이 `값(value)'을 만들어 내고, 따라서 이 값에 다른 값을 대입할 수 없습니다. (다른 말로, ?:는 `lvalue'를 만들어내지 않습니다.) 정말 이런 식의 코드를 써야 한다면, 다음과 같이 할 수 있습니다:

  *((condition) ? &a : &b) = complicated_expression;

(그러나 일반적으로 이런 식의 코드는 지저분해 보이기 때문에 잘 쓰이지 않습니다.)

References
[ANSI] § 3.3.15 esp. footnote 50
[C89] § 6.3.15
[H&S] § 7.1 pp. 179-180



Q 3.17
어떤 코드에서 다음과 같은 문장을 봤습니다:
  a ? b = c : d
어떤 컴파일러에서는 위 문장이 동작하는데, 어떤 컴파일러에서는 컴파일되지 않습니다. 왜 그런 것인가요?
Answer
C 언어 정의에 따르면, =?:보다 우선 순위가 낮습니다. 따라서 어떤 오래된 컴파일러에서는 위 문장을 다음과 같이 해석하기도 합니다:
  (a ? b) = (c : d)
위 코드는 아무런 의미가 없는 잘못된 코드이므로, 현대 컴파일러들은 원래 문장을 다음과 같이 해석합니다:
  a ? (b = c) : d
여기에서, =의 왼쪽은 단순히 b입니다. 사실 ANSI/ISO C 표준은 컴파일러가 두번째로 해석해야 한다고 씌여 있습니다. (The grammar in the standard is not precedence based and says that any expression may appear between the ? and : symbols.)

따라서, 물어보신 문장은 ANSI 호환 컴파일러에서 당연히 옳은 문장입니다. 그러나 만약 아주 오래된 컴파일러를 쓰고 있다면, 적당히 괄호를 써 주어야 합니다.

References
[K&R1] § 2.12 p. 49
[ANSI] § 3.3.15
[C89] § 6.3.15
[ANSI Rationale] § 3.3.15

3.3 Preserving Rules

앞 section에서 말한 “expression(수식)에서 각각 다른 타입의 operand(피연산자)를 변경하는 적당한 규칙”의 의미는 classic C와 ANSI/ISO C에서 약간 바뀌었습니다; 이 section에서는 그 차이에 대하여 설명합니다.



Q 3.18
“sematics of `>' change in ANSI C”라는 문장을 봤는데, 이게 무슨 뜻이죠?
Answer
어떤 민감한 컴파일러들은, 여러분이 쓴 어떤 코드가 ANSI 이전의 “unsigned preserving” 규칙과, ANSI의 “value preserving” 규칙에 따라 다른 식으로 해석될 수 있기 때문에 이러한 경고를 보여줍니다.

약간 혼동스러울 수 있는데, 이 메시지는 > 연산자 때문에 발생한 것이 아닙니다. (사실 이러한 메시지는 거의 모든 C 연산자에서 발생할 수 있습니다.) 이 메시지가 발생한 까닭은 두 개의 서로 다른 타입이 binary operator의 양쪽에 쓰였거나, 작은 타입의 정수 타입이 promote되어야 할 경우에 발생합니다. (만약에 여러분이 생각할 때, 코드에서 unsigned 타입을 쓴 적이 없다고 생각되면, 대부분은 strlen 때문에 이 메시지가 나왔을 것입니다. 표준 C에 따르면, strlensize_t 타입을 리턴하며, 이 것은 unsigned 타입입니다.

질문 [*]3.19를 보기 바랍니다.



Q 3.19
“unsigned preserving”과 “value preserving”이란 말이 무슨 뜻이고, 어떤 차이가 있죠?
Answer
이 두 방식은 작은 unsigned 타입이, `큰' 타입으로 promote될 때 어떤 식으로 되느냐에 따라 차이가 있습니다. 간단히 말해서, 부호가 있는, 큰 타입으로 promote될 것인지, 큰 unsigned 타입으로 promote될 것인지가 다릅니다. (여기서 말한 `큰' 타입이 정말로 크냐에 따라 약간 달라질 수 있습니다.)

“unsigned preserving” (또는 signed preserving이라고도 합니다) 규칙에서는, 항상 unsigned 타입으로 promote됩니다. 이 규칙은 매우 간단하다는 장점이 있지만, 가끔 예상치 못한 결과를 내기도 합니다. (아래 예를 보기 바랍니다.)

“value preserving” 규칙에서는, promote되기 전의 타입과 후의 타입이 실제로 크기가 다르냐에 따라 달라집니다--다시 말하면, promote된 후의 타입이 그 전의 타입의 모든 표현 가능한 unsigned 값을, signed 값으로 다 표현할 수 있느냐에 달려 있습니다--실제로 크다면, promote된 후의 타입은 signed입니다. 만약 두 타입이 실제로 크기가 같다면 promote된 후의 타입은 unsigned입니다. (후자의 경우 “unsigned preserving”과 똑같이 동작합니다.)

실제 타입의 크기가 이 결정에 중요한 역할을 하므로, 이 결과는 시스템에 따라 달라질 수 있습니다. 어떤 시스템에서는 short intint보다 작지만, 어떤 시스템에서는 두 타입이 실제로 크기가 같습니다. 또 어떤 시스템에서는 intlong int보다 작지만, 어떤 시스템에서는 두 타입이 실제로 크기가 같습니다.

실제로 이 규칙이 적용되는 경우는, binary operator의 한 operand가 int이고 다른 한 쪽이 (규칙에 따라 달라질 수 있지만) int이거나 unsigned int일 경우입니다. 만약에 한 operand가 unsigned int인 경우, 다른 한 쪽이 unsigned로 변경됩니다--당연하게 한 쪽의 값이 음수일 경우에는 예상치 못한 결과가 발생합니다. (뒤따르는 코드를 보기 바랍니다.) ANSI C 위원회가 처음 만들어졌을 때, 예상치 못한 변환을 최소화하기 위해서 “value preserving” 규칙이 채택되었습니다.

Seong-Kook Shin
2018-05-28