실수하기 쉬운 C의 증감 연산자 예제
2020 년 7 월 30 일 오늘 아래와 같은 코드를 테스트하다가 발견한 이해하기 어려운 C의 증감 연산자 예제를 기록한다.
up[*nup].parent = ++(*nup);
내가 의도한 코드는 다음과 같다.
up[*nup].parent = *nup + 1;
++(*nup);
즉, 왼쪽의 *nup
은 현재 값에 머무르고 오른쪽의 *nup
은 1이 증가한 후 up[*nup].parent
에 할당된다. 정말 이렇게 될까? 다음의 코드를 보자.
#include <stdio.h>
void main()
{
int a;
int b[3];
a = 1; b[0] = b[1] = b[2] = 0;
b[a] = ++a;
printf("a = 1; b[a] = ++a\n");
printf("%d\n", b[0]);
printf("%d\n", b[1]);
printf("%d\n", b[2]);
a = 1; b[0] = b[1] = b[2] = 0;
b[a] = a++;
printf("a = 1; b[a] = a++\n");
printf("%d\n", b[0]);
printf("%d\n", b[1]);
printf("%d\n", b[2]);
a = 1; b[0] = b[1] = b[2] = 0;
b[a-1] = ++a;
printf("a = 1; b[a-1] = ++a\n");
printf("%d\n", b[0]);
printf("%d\n", b[1]);
printf("%d\n", b[2]);
a = 1; b[0] = b[1] = b[2] = 0;
b[a-1] = a++;
printf("a = 1; b[a-1] = a++\n");
printf("%d\n", b[0]);
printf("%d\n", b[1]);
printf("%d\n", b[2]);
}
출력은 다음과 같다.
a = 1; b[a] = ++a
0
0
2
a = 1; b[a] = a++
0
0
1
a = 1; b[a-1] = ++a
2
0
0
a = 1; b[a-1] = a++
1
0
0
b[a] = ++a
의 경우를 보면 a
가 먼저 증가한 다음 좌우변 모두에 적용되었다. 즉, a = a + 1; b[a] = a
가 되었다. 이해할 만하다. b[a] = a++
는 b[a] = a; a = a + 1
이 될 것 같지만 그렇지 않다. b[a+1] = a; a = a + 1
이 되었다. 이 두 경우를 일반화하자면 좌변의 a
값은 우변의 연산이 종료한 뒤의 값이라고 할 수 있다. 이게 옳은지 확인해 보자. b[a-1] = ++a
가 a = a + 1; b[a-2] = a
가 되었다. 즉, 좌변의 a
가 증가하기 전의 값을 가진다. 만약 앞서의 일반화가 옳다면 a = a + 1; b[a-1] = a
가 되어서 b[0]
대신 b[1]
의 값이 2가 되어야 한다. b[a-1] = a++
는 어떤가? b[a-1] = a; a = a + 1
이 되었다. 이 경우는 자연스럽다.
정리하자면, 다음의 두 경우는 자연스러워 보인다. 두 경우 모두 a
가 먼저(++a
) 또는 나중에(a++
) 증가했다.
# a = 1; a = a + 1; b[2] = 2;
# ++a 때문에 a의 값이 먼저 증가한 후 좌우변에 적용된다.
a = 1; b[a] = ++a
0
0
2
# a = 1; b[1-1] = 1; a = a + 1;
# a++ 때문에 a의 값이 증가하기 전에 좌우변에 적용된다.
a = 1; b[a-1] = a++
1
0
0
반면, 다음의 두 경우는 실수하기 쉽다.
# a = 1; b[1+1] = 1; a = a + 1;
# 좌변의 a 값은 우변의 연산이 끝난 다음의 값이다.
# a++ 때문에 좌변의 a 값이 우변의 연산이 끝나기 전의 값이 될 것 같지만 아니다.
a = 1; b[a] = a++
0
0
1
# a = 1; a = a + 1; b[2-1-1] = 2;
# 좌변의 a 값은 우변의 연산이 끝나기 전의 값이다.
# ++a 때문에 좌변의 a 값이 우변의 연산이 끝난 후의 값이 될 것 같지만 아니다.
a = 1; b[a-1] = ++a
2
0
0
다시 정리하자.
# a = 1; a = a + 1; b[2] = 2;
# 좌, 우: 증가 후의 값 (이해 가능)
a = 1; b[a] = ++a
0
0
2
# a = 1; b[1+1] = 1; a = a + 1;
# 좌: 증가 후의 값 (실수하기 쉬음)
# 우: 증가 전의 값 (이해 가능)
a = 1; b[a] = a++
0
0
1
# a = 1; a = a + 1; b[2-1-1] = 2;
# 좌: 증가 전의 값 (실수하기 쉬음)
# 우: 증가 후의 값 (이해 가능)
a = 1; b[a-1] = ++a
2
0
0
# a = 1; b[1-1] = 1; a = a + 1;
# 좌, 우: 증가 전의 값 (이해 가능)
a = 1; b[a-1] = a++
1
0
0
이제까지 살펴 본 예제들로 인한 문제는 GCC의 -Wsequence-point
옵션이 잘 설명하고 있다. 변수의 값이 언제 바뀔지 정의되어 있지 않기 때문에 위와 같은 코드는 쓰지 말아야 한다. 언뜻 보면 원하는 결과가 나올 것 같지만 그렇지 않았던 이유가 여기에 있다. 좌우변에 동일한 변수를 두고서 증감 연산자를 쓰지 말자.
Examples of code with undefined behavior are “a = a++;”, “a[n] = b[n++]” and “a[i++] = i;”. Some more complicated cases are not diagnosed by this option, and it may give an occasional false positive result, but in general it has been found fairly effective at detecting this sort of problem in programs. GCC(1)