C 언어 디자인 목표 중의 하나는 효과성을 강조합니다 -- 즉 C 컴파일러가 상대적으로 작고 만들기 쉽게 하자는 것과, (기계어) 코드를 쉽게 생성할 수 있도록 하자는 것입니다. 이 두가지 목표는 C 언어 specification에 큰 영향을 미쳤습니다. 비록 C 언어가 좀 더 tight하게 정의되었으면 하는 사용자들과 C 언어가 지원하는 것보다 좀 더 많은 것을 (예를 들어 사용자의 실수를 미리 방지하는 기능) 요구하는 사용자들에게는 반가운 내용은 아니었지만 말입니다.
a[i] = i++;
int i = 7; printf("%d\n", i++ * i++);
`49'를 출력합니다. 평가 순서(order of evaluation)에 상관없이, `56'을 출력해야 하지 않을까요?
++
), 감소(--
) 연산자가 뒤에 쓰일 때에는
먼저 기존의 값을 계산한 다음, 증가/감소하게 됩니다. “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.)
Although the postincrement and postdecrement operators++
and--
perform their operations after yielding the former value, the implication of “after” is often misunderstood.
int i = 3; i = i++;
어떤 컴파일러는 i가 3이라고 하며, 또 4를 출력하는 컴파일러도 있었습니다. 어떤 컴파일러가 맞는 것인가요?
i++
나 ++i
는 둘 다 i + 1
과 같지 않습니다.
원하는 것이 단순히 i 값을 증가시키는 것이라면 i=i+1
,
i+=1
, i++
, ++i
중 하나를 쓰시기 바랍니다.
질문
3.12를 참고하기 바랍니다)
a ^= b ^= a ^= b
이 코드가 올바른가요?
예를 들어 다음과 같은 코드를 SCO 최적화 C 컴파일러(icc)에서 돌렸을 경우, b를 123으로 설정하고 a를 0으로 설정한다고 보고되었습니다:
int a = 123, b = 7654; a ^= b ^= a ^= b;
연산자 우선 순위와 괄호는 수식 평가의 일부분만을 변경할 수 있습니다. 다음과 같은 수식에서:
f() + g() * h()
우리는 곱셈이 덧셈보다 먼저 일어난다는 것을 알고 있습니다. 그러나, 세개의 함수 중 어떤 함수가 먼저 호출될지는 알 수 없습니다. 즉, 우선 순위는, 평가에서 일부분 영향을 미치는 것이며, 각 피연산자(operand)의 평가 순서에 영향을 주지는 못합니다.
괄호로 둘러 싸는 것은 어떤 피연산자(operand)가 어떤 연산자(operator)와 연결될 것인지를 결정하지만, 마찬가지로, 평가의 모든 부분에 영향을 줄 수 없습니다. 다음과 같이 괄호를 쓰더라도:
f() + (g() + h())함수 실행 순서에 영향을 주지 못합니다. 비슷하게, 질문 3.2에서 나온 식을 괄호로 둘러 싸는 것도 아무 영향을 주지 못합니다. 왜냐하면 ++는 원래 *보다 우선 순위가 높기 때문입니다:
(i++) * (i++) /* WRONG */위 수식은 괄호가 있던 없던 상관없이 `undefined behavior'에 해당합니다.
부분식(subexpression)의 평가 순서가 중요할 때에는, 임시 변수를 만들고 각각 다른 문장(statement)으로 나눠 쓰는 것이 좋습니다.
&&
, ||
연산자에서는 어떤가요?
다음과 같은 코드를 본 기억이 있거든요.
while ((c = getchar()) != EOF && c != '\n') ....
||
연산자에서 왼쪽이 참이거나, &&
연산자에서
왼쪽이 거짓인 경우). 그러므로, 콤마(comma) 연산자와 마찬가지로
이 연산자들은 왼쪽에서 오른쪽으로 평가된다는 것을 보장할 수 있습니다.
게다가 이 연산자들은 모두 (?:
연산자 포함) 추가로
내부적인 `sequence point'를 가지고 있습니다. (질문
3.6,
3.8 참고)
&&
, ||
연산자의 오른쪽이 평가되지 않는다고
보장할 수 있나요?
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가 널 포인터일 경우, 존재하지 않는 메모리 공간을
참조하는 에러가 발생할 것입니다.
printf("%d %d", f1(), f2());
||
. &&
, ?:
, 또는 콤마(comma)
연산자, 또는 함수 호출 바로 이전)의 위치를 의미하는 것으로,
모든 부작용이 일어나지 않는다고 보장하는 시점입니다. 표준에서 `sequence
point'라고 하는 것은 다음과 같은 상황을 말합니다:
||
, &&
, ?:
, and comma
operators; and
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.
i
는 단 한번만 증가되는 것으로 생각할 수 있나요?
a[i] = i++;
a[i]
또는 a[i + 1]
에 값이
들어갈지 모를 뿐더러, 전혀 다른 요소에 (또는 아예 이 배열과는 전혀 상관없는
곳에) 쓸 수 있습니다. 그리고 이 문장 실행 뒤에, i가 어떤 값을 갖고 있는지
전혀 예측할 수 없습니다.
질문
3.2,
3.3,
11.33,
11.35를 참고하기 바랍니다.
i = i++
가 `undefined behavior'를 낳는다고 계속 말하지만,
제가 ANSI 표준 호환 컴파일러에서 실험한 결과, 예상했던 값을 얻었습니다.
a[i] = i++
나 i = i++
이 여러가지 방식으로 해석될 수 있다고
생각하는 사람이면, 문제없을 것 같습니다.)
좀 더 자세히 알아보면, 다음과 같은 규칙을 지키면, 컴파일러나 동료 개발자들에게 확실한 의미를 전달하는데 도움이 됩니다. (표준보다 좀 더 보수적인 규칙일 수 있습니다.)
*p
).
여기서 modification이란, =
연산자를 쓴 간단한 대입이나,
+=, -=, *=와 같은 연산자를 (compound assignment)
쓴 대입, ++나 -를 쓴 증가 또는 감소를 쓴 것을 뜻합니다.
&&
.
(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.
?:
(또는 “ternary”라고도 하는) operator에 대한 것도 다룹니다.
i++
을 써야 하나요, ++i
를 써야 하나요?
또한 full expression이라는 전제 아래에서는 (i++
이
++i
와 같은 것과 비슷하게) i += 1
과
i = i + 1
이 완전히 같습니다.
(그러나, C++에서는 ++i
의 형식을 더 선호합니다.)
덧붙여 질문
3.3도 참고하시기 바랍니다.
if (a < b < c) ...
<
'와 같은 관계 연산자는 (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)
int a = 1000, b = 1000; long int c = a * b;
long int c = (long int)a * b;
또는 다음과 같이 합니다:
long int c = (long int)a * (long int)b;
`(long int)(a * b)
'와 같이 하는 것은 질문의 코드와 똑같은 결과를
만드므로 바람직하지 않습니다. 이런 식으로 캐스팅을 하는 것은 (즉, 곱셈이 끝난 결과를
캐스팅하는 것) 결과 값을 long int 타입에 대입할 때, 어차피 자동적으로
변환되는 것이기 때문에 (implicit conversion), 쓰나마나 한 것이 되어 버립니다.
결과값이 실수 타입인 경우, 나눗셈을 할 경우에도 비슷한 문제가 발생할 수 있습니다. 해결 방법은 위와 같습니다.
double degC, degF; degC = 5 / 9 * (degF - 32);
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);
((condition) ? a : b) = complicated_expression;
?:
연산자는 대부분 연산자들과 같이
`값(value)'을 만들어 내고, 따라서 이 값에 다른 값을
대입할 수 없습니다. (다른 말로, ?:
는 `lvalue'를 만들어내지 않습니다.) 정말
이런 식의 코드를 써야 한다면, 다음과 같이 할 수 있습니다:
*((condition) ? &a : &b) = complicated_expression;
(그러나 일반적으로 이런 식의 코드는 지저분해 보이기 때문에 잘 쓰이지 않습니다.)
a ? b = c : d어떤 컴파일러에서는 위 문장이 동작하는데, 어떤 컴파일러에서는 컴파일되지 않습니다. 왜 그런 것인가요?
(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 호환 컴파일러에서 당연히 옳은 문장입니다. 그러나 만약 아주 오래된 컴파일러를 쓰고 있다면, 적당히 괄호를 써 주어야 합니다.
>
' change in ANSI C”라는 문장을 봤는데,
이게 무슨 뜻이죠?
약간 혼동스러울 수 있는데, 이 메시지는 >
연산자 때문에 발생한 것이 아닙니다.
(사실 이러한 메시지는 거의 모든 C 연산자에서 발생할 수 있습니다.) 이 메시지가
발생한 까닭은 두 개의 서로 다른 타입이 binary operator의 양쪽에 쓰였거나,
작은 타입의 정수 타입이 promote되어야 할 경우에 발생합니다.
(만약에 여러분이 생각할 때, 코드에서 unsigned 타입을 쓴 적이 없다고 생각되면,
대부분은 strlen 때문에 이 메시지가 나왔을 것입니다. 표준 C에 따르면,
strlen은 size_t
타입을 리턴하며, 이 것은 unsigned 타입입니다.
“unsigned preserving” (또는 signed preserving이라고도 합니다) 규칙에서는, 항상 unsigned 타입으로 promote됩니다. 이 규칙은 매우 간단하다는 장점이 있지만, 가끔 예상치 못한 결과를 내기도 합니다. (아래 예를 보기 바랍니다.)
“value preserving” 규칙에서는, promote되기 전의 타입과 후의 타입이 실제로 크기가 다르냐에 따라 달라집니다--다시 말하면, promote된 후의 타입이 그 전의 타입의 모든 표현 가능한 unsigned 값을, signed 값으로 다 표현할 수 있느냐에 달려 있습니다--실제로 크다면, promote된 후의 타입은 signed입니다. 만약 두 타입이 실제로 크기가 같다면 promote된 후의 타입은 unsigned입니다. (후자의 경우 “unsigned preserving”과 똑같이 동작합니다.)
실제 타입의 크기가 이 결정에 중요한 역할을 하므로, 이 결과는 시스템에 따라 달라질 수 있습니다. 어떤 시스템에서는 short int가 int보다 작지만, 어떤 시스템에서는 두 타입이 실제로 크기가 같습니다. 또 어떤 시스템에서는 int가 long int보다 작지만, 어떤 시스템에서는 두 타입이 실제로 크기가 같습니다.
실제로 이 규칙이 적용되는 경우는, binary operator의 한 operand가 int이고 다른 한 쪽이 (규칙에 따라 달라질 수 있지만) int이거나 unsigned int일 경우입니다. 만약에 한 operand가 unsigned int인 경우, 다른 한 쪽이 unsigned로 변경됩니다--당연하게 한 쪽의 값이 음수일 경우에는 예상치 못한 결과가 발생합니다. (뒤따르는 코드를 보기 바랍니다.) ANSI C 위원회가 처음 만들어졌을 때, 예상치 못한 변환을 최소화하기 위해서 “value preserving” 규칙이 채택되었습니다.
Seong-Kook Shin