[c] 사전 및 사후 증가 정의되지 않은 동작을 사용하는 이러한 구조는 왜됩니까?

#include <stdio.h>

int main(void)
{
   int i = 0;
   i = i++ + ++i;
   printf("%d\n", i); // 3

   i = 1;
   i = (i++);
   printf("%d\n", i); // 2 Should be 1, no ?

   volatile int u = 0;
   u = u++ + ++u;
   printf("%d\n", u); // 1

   u = 1;
   u = (u++);
   printf("%d\n", u); // 2 Should also be one, no ?

   register int v = 0;
   v = v++ + ++v;
   printf("%d\n", v); // 3 (Should be the same as u ?)

   int w = 0;
   printf("%d %d\n", ++w, w); // shouldn't this print 1 1

   int x[2] = { 5, 8 }, y = 0;
   x[y] = y ++;
   printf("%d %d\n", x[0], x[1]); // shouldn't this print 0 8? or 5 0?
}



답변

C에는 정의되지 않은 동작의 개념이 있습니다. 즉, 일부 언어 구문은 구문 상 유효하지만 코드가 실행될 때 동작을 예측할 수 없습니다.

내가 아는 한, 표준은 정의되지 않은 행동의 개념이 존재 하는지 명시하지 않습니다 . 제 생각에는 언어 디자이너가 의미론에 약간의 여유가 있기를 원했기 때문입니다. 즉, 모든 구현이 정확히 동일한 방식으로 정수 오버플로를 처리하도록 요구하기 때문에 심각한 성능 비용을 부과 할 수 있습니다. 정수 오버플로를 일으키는 코드를 작성하면 어떤 일이 발생할 수 있습니다.

그래서, 그 점을 염두에두고 왜 이러한 “문제”가 있습니까? 언어는 특정 사물이 정의되지 않은 행동으로 이어진다 고 분명히 말합니다 . 아무런 문제가 없으며 관련된 “해야한다”는 없습니다. 관련 변수 중 하나가 선언 될 때 정의되지 않은 동작이 변경되면 volatile아무 것도 증명하거나 변경하지 않습니다. 그것은 인 정의 ; 당신은 그 행동에 대해 추론 할 수 없습니다.

가장 흥미로운 예,

u = (u++);

정의되지 않은 동작의 교과서 예제입니다 ( 시퀀스 포인트 에 대한 Wikipedia의 항목 참조 ).


답변

당신이 얻는 것을 얼마나 정확하게 얻는 지 알고 싶다면 코드 라인을 컴파일하고 분해하십시오.

이것이 내가 생각하는 것과 함께 내 컴퓨터에 얻는 것입니다.

$ cat evil.c
void evil(){
  int i = 0;
  i+= i++ + ++i;
}
$ gcc evil.c -c -o evil.bin
$ gdb evil.bin
(gdb) disassemble evil
Dump of assembler code for function evil:
   0x00000000 <+0>:   push   %ebp
   0x00000001 <+1>:   mov    %esp,%ebp
   0x00000003 <+3>:   sub    $0x10,%esp
   0x00000006 <+6>:   movl   $0x0,-0x4(%ebp)  // i = 0   i = 0
   0x0000000d <+13>:  addl   $0x1,-0x4(%ebp)  // i++     i = 1
   0x00000011 <+17>:  mov    -0x4(%ebp),%eax  // j = i   i = 1  j = 1
   0x00000014 <+20>:  add    %eax,%eax        // j += j  i = 1  j = 2
   0x00000016 <+22>:  add    %eax,-0x4(%ebp)  // i += j  i = 3
   0x00000019 <+25>:  addl   $0x1,-0x4(%ebp)  // i++     i = 4
   0x0000001d <+29>:  leave
   0x0000001e <+30>:  ret
End of assembler dump.

(저는 0x00000014 명령이 일종의 컴파일러 최적화라고 가정합니다.)


답변

C99 표준의 관련 부분은 6.5 식, §2라고 생각합니다.

이전과 다음 시퀀스 포인트 사이에서 객체는 표현식의 평가에 의해 저장된 값을 최대 한 번 수정해야합니다. 또한, 저장 될 값을 결정하기 위해 이전 값은 읽기 전용이어야한다.

6.5.16 할당 연산자, §4 :

피연산자의 평가 순서는 지정되어 있지 않습니다. 할당 연산자의 결과를 수정하거나 다음 시퀀스 포인트 이후에 액세스하려고하면 동작이 정의되지 않습니다.


답변

여기에 나오는 대부분의 답변은 C 표준에서 인용 한 것으로, 이러한 구성의 동작이 정의되어 있지 않음을 강조합니다. 이러한 구성의 동작이 정의되지 않은 이유 를 이해하려면 C11 표준에 비추어 먼저이 용어를 이해하십시오.

순서 : (5.1.2.3)

두 가지 평가가 주어 A지고 이전 에 시퀀싱 된 B경우 의 실행이 의 실행보다 우선한다 . ABAB

순차 :

A전후에 시퀀싱 되지 않은 B경우 A및 순서가 지정되지 않습니다 B.

평가는 다음 두 가지 중 하나 일 수 있습니다.

  • 식의 결과를 산출 하는 값 계산 ; 과
  • 부작용 은 객체의 수정입니다.

시퀀스 포인트 :

식의 계산 사이의 시퀀스 지점이 존재 A하고 B암시하는 모든 값을 계산 하고 부작용 과 관련된 A모든 이전 순서화되는 값 계산부작용 과 연관된 B.

이제 질문에옵니다.

int i = 1;
i = i++;

표준은 말합니다 :

6.5 표현 :

스칼라 객체의 부작용에 대하여 unsequenced 경우동일한 스칼라 객체에서 다른 부작용 또는 동일한 객체의 스칼라 값을 이용하여 값의 계산은, 동작이 정의되지 않는다 . […]

따라서 동일한 오브젝트에 대한 두 개의 부작용 i이 서로에 대해 순서가 다르기 때문에 위의 표현식은 UB를 호출합니다 . 즉, 할당에 의한 부작용이 부작용의 i전후 인지에 따라 순서가 결정되지 않습니다 ++.
할당이 증분 이전 또는 이후에 발생하는지에 따라 다른 결과가 생성되며 이는 정의되지 않은 동작 의 경우 중 하나입니다. .

i대입 왼쪽의 이름 을 대입 il오른쪽 (식에서 i++)으로 바꾸고 ir식은 다음과 같습니다.

il = ir++     // Note that suffix l and r are used for the sake of clarity.
              // Both il and ir represents the same object.  

Postfix ++연산자 와 관련하여 중요한 점 은 다음과 같습니다.

바로이 때문에 ++변수가 증가 늦게 일어나는 것을 의미하지 않는다 후에 온다 . 컴파일러가 원래 값을 사용 하는 한 컴파일러가 원하는 만큼 증가 할 수 있습니다 .

이는 다음과 같이 표현식 il = ir++을 평가할 수 있음을 의미합니다

temp = ir;      // i = 1
ir = ir + 1;    // i = 2   side effect by ++ before assignment
il = temp;      // i = 1   result is 1  

또는

temp = ir;      // i = 1
il = temp;      // i = 1   side effect by assignment before ++
ir = ir + 1;    // i = 2   result is 2  

두 개의 서로 다른 결과를 초래 1하고 2있는 것이 과제로 부작용의 순서에 따라 다르며 ++, 따라서 UB를 호출한다.


답변

지정되지 않은 동작정의되지 않은 동작을 모두 호출하기 때문에 동작을 설명 할 수 없으므로 Deep CUnspecified 및 Undefined 와 같은 Olve Maudal의 작업 을 읽으면 때로는 좋은 결과를 얻을 수 있지만이 코드에 대한 일반적인 예측을 할 수는 없습니다. 특정 컴파일러와 환경에서 매우 구체적인 경우를 추측하지만 프로덕션 근처에서는 그렇게하지 마십시오.

로 이동 그래서 수없는 동작 에서, 표준 C99 초안 섹션 6.53은 말한다 ( 강조 광산 ) :

연산자 및 피연산자들의 그룹핑은 (함수 호출 (용) 나중에 지정된 제외)로 표시된다 syntax.74, &&, ||, :, 및 콤마 연산자) 표현식의 평가 순서 및 순서 어떤 부작용이 발생하는지는 명시되어 있지 않습니다.

따라서 다음과 같은 줄이있을 때 :

i = i++ + ++i;

우리는 여부를 알 수없는 i++또는 ++i먼저 평가됩니다. 이것은 주로 컴파일러에게 최적화를위한 더 나은 옵션을 제공하기위한 것 입니다.

우리는 또한이 정의되지 않은 동작을 프로그램 변수를 수정 (때문에 여기뿐만 아니라 i, u사이에 한 번 이상, 등) 순서 포인트 . 초안 표준 섹션에서 6.52 ( 강조 광산 ) :

이전과 다음 시퀀스 포인트 사이에서 객체는 표현식의 평가에 의해 저장된 값을 최대 한 번 수정해야 합니다. 또한, 저장 될 값을 결정하기 위해 이전 값은 읽기 전용이어야한다 .

다음 코드 예제는 정의되지 않은 것으로 인용합니다.

i = ++i + 1;
a[i++] = i; 

이 모든 예제에서 코드는 동일한 시퀀스 포인트에서 객체를 두 번 이상 수정하려고 시도하는데, ;이 경우 각각의 경우에서 끝납니다 .

i = i++ + ++i;
^   ^       ^

i = (i++);
^    ^

u = u++ + ++u;
^   ^       ^

u = (u++);
^    ^

v = v++ + ++v;
^   ^       ^

지정되지 않은 동작c99 표준 초안 에 섹션 3.4.4으로 정의되어 있습니다.

불특정 한 가치의 사용, 또는이 국제 표준이 둘 이상의 가능성을 제공하고 어떠한 경우에도 선택되는 추가 요구 사항을 부과하지 않는 기타 행동

정의되지 않은 동작이 섹션에서 정의 3.4.3로서 :

휴대 할 수 없거나 잘못된 프로그램 구조 또는 잘못된 데이터를 사용할 때의 행동,이 표준에 요구 사항이없는 경우

그리고 메모 :

정의되지 않은 동작은 예측할 수없는 결과로 상황을 완전히 무시하는 것, 환경의 특징적인 문서화 된 방식으로 진단 또는 프로그램 실행 중 (진단 메시지 발행 여부에 관계없이), 번역 또는 실행 종료 (발급 포함)에 이르기까지 다양합니다. 진단 메시지).


답변

시퀀스 포인트와 정의되지 않은 동작의 세부적인 세부 사항에 얽매이지 않고 이것을 대답하는 또 다른 방법은 단순히 무엇을 의미해야하는지 묻는 것입니다. 프로그래머는 무엇을하려고 했습니까?

에 대한 첫 번째 조각 i = i++ + ++i은 내 책에서 분명히 명백히 미쳤다. 아무도 실제 프로그램에서 그것을 작성하지 않았으며, 그것이 무엇을하는지 명확하지 않으며, 누군가가이 특별한 계획된 조작 순서를 초래할 수있는 코딩을 시도했을 수도있는 알고리즘도 없습니다. 그리고 당신과 나에게해야 할 일이 분명하지 않기 때문에 컴파일러가해야 할 일을 이해할 수 없다면 내 책에서 괜찮습니다.

두 번째 조각 i = i++은 이해하기 조금 더 쉽습니다. 누군가가 분명히 i를 증가시키고 결과를 i에 다시 할당하려고합니다. 그러나 C에서 이것을 수행하는 몇 가지 방법이 있습니다. 1을 i에 더하고 결과를 다시 i에 할당하는 가장 기본적인 방법은 거의 모든 프로그래밍 언어에서 동일합니다.

i = i + 1

물론 C에는 편리한 단축키가 있습니다.

i++

이는 “i에 1을 추가하고 결과를 i에 다시 할당”을 의미합니다. 그래서 우리가이 두 가지를 만들어서

i = i++

우리가 실제로 말하는 것은 “i에 1을 더하고 결과를 다시 i에 할당하고 결과를 다시 i에 할당”입니다. 우리는 혼란스러워서 컴파일러가 혼란스러워도 너무 귀찮게하지 않습니다.

현실적으로,이 미친 표현은 사람들이 ++가 어떻게 작동해야하는지에 대한 인공적인 예로 사용하는 경우에만 가능합니다. 물론 ++ 작동 방식을 이해하는 것이 중요합니다. 그러나 ++를 사용하는 실질적인 규칙 중 하나는 “++를 사용한 표현의 의미가 확실하지 않으면 쓰지 마십시오”입니다.

우리는 comp.lang.c에서 이와 같은 표현과 정의되지 않은지에 대해 많은 시간을 보냈습니다 . 이유를 설명하려는 더 긴 두 가지 답변이 웹에 보관됩니다.

질문 3.8C FAQ 목록섹션 3 에있는 나머지 질문 도 참조하십시오 .


답변

종종이 질문은 다음과 같은 코드와 관련된 질문의 중복으로 연결됩니다

printf("%d %d\n", i, i++);

또는

printf("%d %d\n", ++i, i++);

또는 유사한 변형.

이것은 이미 언급 한 바와 같이 정의되지 않은 동작 이지만 , 다음 printf()과 같은 명령문과 비교할 때 미묘한 차이 가 있습니다.

x = i++ + i++;

다음 진술에서 :

printf("%d %d\n", ++i, i++);

평가의 순서 에 인수가 printf()있습니다 지정 . 그 말, 표현 i++과는 ++i어떤 순서로 평가 될 수있다. C11 표준 에는 다음과 같은 관련 설명이 있습니다.

부록 J, 불특정 행동

인수 내의 함수 지정자, 인수 및 부속 식의 순서는 함수 호출 (6.5.2.2)에서 평가됩니다.

3.4.4, 불특정 행동

지정되지 않은 값 또는이 국제 표준이 두 가지 이상의 가능성을 제공하고 어떤 경우에도 선택되는 추가 요구 사항을 부과하지 않는 기타 행동의 사용.

예 지정되지 않은 동작의 예는 함수의 인수가 평가되는 순서입니다.

수없는 동작 자체는 문제가되지 않습니다. 이 예제를 고려하십시오.

printf("%d %d\n", ++x, y++);

이 역시이 지정되지 않은 동작을 의 평가 순서 때문에 ++x과가 y++지정되지 않습니다. 그러나 그것은 완벽하게 합법적이고 유효한 진술입니다. 없습니다 에는 이 문장에서 정의되지 않은 동작이. 수정 ( ++xy++)은 개체 를 구별 하기 위해 수행 되므로

다음 진술을하는 것

printf("%d %d\n", ++i, i++);

정의되지 않은 동작 이 두 표현은 수정하는 것이 사실이다 같은 객체를 i개입하지 않고 순서 포인트 .


또 다른 세부 사항이 있다는 점이다 콤마 의 printf () 호출에 관련이있다 세퍼레이터 아닌 콤마 연산자 .

이것은 쉼표 연산자 가 피연산자 평가 사이에 시퀀스 포인트 를 도입 하므로 다음과 같은 합법성이 있기 때문에 중요한 차이점입니다 .

int i = 5;
int j;

j = (++i, i++);  // No undefined behaviour here because the comma operator 
                 // introduces a sequence point between '++i' and 'i++'

printf("i=%d j=%d\n",i, j); // prints: i=7 j=6

쉼표 연산자는 피연산자를 왼쪽에서 오른쪽으로 평가하고 마지막 피연산자 값만 산출합니다. 그래서에서 j = (++i, i++);, ++i단위 i6i++수율의 이전 값 i( 6할당한다) j. 그런 다음 i이된다 7사후 증가에 기인.

그래서 만약 쉼표 함수 호출은 쉼표 연산자로했다

printf("%d %d\n", ++i, i++);

문제가되지 않습니다. 그러나 여기서 쉼표구분 기호 이므로 정의되지 않은 동작을 호출합니다 .


정의되지 않은 행동을 처음 접하는 사람들에게는 모든 C 프로그래머가 정의되지 않은 행동 에 대해 알아야 할 내용 을 읽고 C 에서 정의되지 않은 행동의 다른 많은 변형을 이해하면 도움이 될 것입니다

이 게시물 : 정의되지 않은 지정되지 않은 구현 정의 동작 도 관련이 있습니다.