문제

#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.

(I ... 0x00000014 명령어가 일종의 컴파일러 최적화라고 가정합니까?)

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

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

및 6.5.16 과제 연산자, §4 :

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

행동은 실제로 두 가지를 호출하기 때문에 설명 할 수 없습니다. 지정되지 않은 행동 그리고 정의되지 않은 행동, 우리는이 코드에 대한 일반적인 예측을 할 수 없지만 읽는 경우 Olve Maudal 's 다음과 같은 작업 깊은 c 그리고 지정되지 않고 정의되지 않았습니다 때로는 특정 컴파일러와 환경을 가진 매우 구체적인 경우에서 좋은 추측을 할 수 있지만 생산 근처에서는 그렇게하지 않아도됩니다.

그래서 계속 이동합니다 지정되지 않은 행동, 안에 초안 C99 표준 부분6.53 (강조 광산):

연산자와 오페라의 그룹화는 나중에 지정된대로 (함수-콜 (), &&, ||,? : 및 쉼표 연산자)를 제외하고 구문으로 표시됩니다. 하위 표현의 평가 순서와 부작용이 발생하는 순서는 모두 지정되지 않습니다.

그래서 우리가 다음과 같은 줄이있을 때 :

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 처럼:

행동,이 국제 표준이 요구 사항을 부과하지 않는 잘못된 데이터 구성 또는 잘못된 데이터를 사용하여 행동

그리고 그 점에 주목합니다 :

정의되지 않은 행동은 예측할 수없는 결과로 상황을 완전히 무시하고, 환경의 문서화 된 방식으로 문서화 된 방식으로 번역 또는 프로그램 실행 중에 행동에 이르기까지 (진단 메시지의 발행 유무에 관계없이) 번역 또는 실행 (발행 또는 실행을 종료하는 것) 진단 메시지).

C 표준에서 인용 된 대부분의 답변은 이러한 구성의 동작이 정의되지 않았 음을 강조합니다. 이해하다 이러한 구성의 동작이 정의되지 않은 이유, C11 표준에 비추어이 용어를 먼저 이해합시다.

시퀀싱 : (5.1.2.3)

두 가지 평가가 주어졌습니다 A 그리고 B, 만약에 A 전에 시퀀싱됩니다 B, 그런 다음 실행 A 실행보다 우선합니다 B.

시합되지 않은 :

만약에 A 전후에 시퀀싱되지 않습니다 B, 그 다음에 A 그리고 B 시합되지 않습니다.

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

  • 가치 계산, 표현의 결과를 해결하는 것; 그리고
  • 부작용, 객체의 수정입니다.

시퀀스 지점 :

표현의 평가 사이의 시퀀스 점의 존재 A 그리고 B 모든 것을 암시합니다 가치 계산 그리고 부작용 와 관련된 A 매번 전에 시퀀싱됩니다 가치 계산 그리고 부작용 와 관련된 B.

이제 질문에옵니다.

int i = 1;
i = i++;

Standard는 다음과 같이 말합니다.

6.5 표현 :

스칼라 물체의 부작용이 다음과 비교하여 시합되지 않은 경우 어느 하나 동일한 스칼라 물체에서 다른 부작용 또는 동일한 스칼라 객체의 값을 사용한 값 계산, 행동은 정의되지 않습니다. [...]

따라서 위의 표현식은 동일한 물체에 두 가지 부작용으로 인해 UB를 호출합니다. i 서로에 대해 시합되지 않습니다. 즉 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++ AS를 평가할 수 있습니다

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를 호출합니다.

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

첫 번째 조각이 물었고 i = i++ + ++i, 내 책에서는 분명히 미쳤다. 아무도 실제 프로그램에 글을 쓰지 않을 것입니다. 그것이 무엇을하는지는 분명하지 않습니다. 누군가 가이 특별한 일련의 작업 순서를 초래할 수있는 코드를 만들려고 할 수있는 상상할 수있는 알고리즘이 없습니다. 그리고 그것이 당신과 나에게 무엇을 해야하는지 분명하지 않기 때문에 컴파일러가 무엇을 해야하는지 알 수 없다면 내 책에서는 괜찮습니다.

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

i = i + 1

물론 C에는 편리한 바로 가기가 있습니다.

i++

즉, "1에 1을 추가하고 결과를 i에 다시 할당하십시오"를 의미합니다. 그래서 우리가 두 사람의 Hodgepodge를 건설한다면,

i = i++

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

현실적으로,이 미친 표현이 쓰여진 유일한 시간은 사람들이 그것들을 ++가 어떻게 작동 해야하는지에 대한 인공적인 예로 사용하는 것입니다. 물론 ++가 어떻게 작동하는지 이해하는 것이 중요합니다. 그러나 ++를 사용하기위한 실질적인 규칙 중 하나는 "++를 사용하는 표현이 무엇을 의미하는지 명확하지 않다면 쓰지 마십시오."

우리는 Comp.Lang.c에서 이와 같은 표현을 논의하는 데 수많은 시간을 보냈습니다. 그들은 정의되지 않았습니다. 왜 이유를 설명하려고 노력하는 두 가지 더 긴 답변은 웹에 보관됩니다.

또한보십시오 질문 3.8 그리고 나머지 질문 섹션 3C FAQ 목록.

종종이 질문은 코드와 관련된 질문의 복제본으로 연결됩니다.

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++ 지정되지 않습니다. 그러나 그것은 완벽하게 합법적이고 유효한 진술입니다. 거기 있습니다 아니요 이 진술에서 정의되지 않은 행동. 수정이기 때문에++x 그리고 y++) 완료되었습니다 별개의 사물.

다음 진술을 렌더링합니다

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 증분 i 에게 6 그리고 i++ 오래된 가치를 산출합니다 i (6)에 할당됩니다 j. 그 다음에 i becomes 7 증가 후.

그래서 반점 함수 호출에서 쉼표 연산자가되었습니다.

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

문제가되지 않습니다. 그러나 그것은 호출합니다 정의되지 않은 행동 때문에 반점 여기에 있습니다 분리 기호.


새로운 사람들을 위해 정의되지 않은 행동 독서의 혜택을 누릴 수 있습니다 모든 C 프로그래머가 정의되지 않은 행동에 대해 알아야 할 것 C의 개념과 다른 많은 변형을 이해하려면 C에서 정의되지 않은 행동의 변형을 이해합니다.

이 게시물 : 정의되지 않은, 지정되지 않은 및 구현 정의 된 동작 또한 관련이 있습니다.

컴파일러와 프로세서가 실제로 그렇게 할 가능성은 낮지 만, 컴파일러가 시퀀스와 함께 "i ++"를 구현하는 것은 C 표준에 따라 합법적 일 것입니다.

In a single operation, read `i` and lock it to prevent access until further notice
Compute (1+read_value)
In a single operation, unlock `i` and store the computed value

어떤 프로세서가 하드웨어가 효율적으로 수행 할 수 있도록 하드웨어를 지원한다고 생각하지는 않지만, 그러한 동작이 다중 스레드 코드를 더 쉽게 만들 수있는 상황을 쉽게 상상할 수 있습니다 (예 : 두 스레드가 위의 스레드를 수행하려고 시도하면 보장 할 수 있습니다. 시퀀스는 동시에 i 2)에 의해 증가 할 것이며 일부 미래의 프로세서가 기능을 제공 할 수 있다는 것은 완전히 상상할 수없는 것은 아닙니다.

컴파일러가 작성 해야하는 경우 i++ 위에서 지적한 바와 같이 (표준에 따라 합법적) 전체 표현식 평가를 통해 위의 지시 사항을 산재해야하며 다른 지침 중 하나가 액세스하기 위해 발생하지 않은 경우 i, 컴파일러가 교착 상태에 대한 일련의 지침을 생성하는 것이 가능하고 합법적입니다. 확실히, 컴파일러는 동일한 변수가있는 경우에 문제를 거의 확실히 감지 할 것입니다. i 두 곳 모두에서 사용되지만 일상이 두 포인터에 대한 참조를 수락하는 경우 p 그리고 q, 그리고 사용 (*p) 그리고 (*q) 위의 표현에서 (사용하기보다는 i 두 번) 컴파일러는 동일한 오브젝트의 주소가 둘 다에 대해 전달 된 경우 발생하는 교착 상태를 인식하거나 피할 필요가 없습니다. p 그리고 q.

C 표준에 따르면 변수는 두 시퀀스 지점 사이에서 최대 한 번만 할당되어야한다고 말합니다. 예를 들어 세미콜론은 시퀀스 지점입니다.
그래서 양식의 모든 진술 :

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

그리고 그 규칙을 위반합니다. 표준은 또한 행동이 정의되지 않았으며 지정되지 않았다고 말합니다. 일부 컴파일러는이를 감지하고 일부 결과를 생성하지만 표준 당은 아닙니다.

그러나 두 시퀀스 지점 사이에서 두 가지 다른 변수를 증가시킬 수 있습니다.

while(*src++ = *dst++);

위는 문자열을 복사/분석하는 동안 일반적인 코딩 실습입니다.

동안 통사론 같은 표현의 a = a++ 또는 a++ + a++ 합법적입니다 행동 이 구성 중 하나입니다 한정되지 않은 a ~일 것이다 C 표준에서는 순종하지 않습니다. C99 6.5p2:

  1. 이전과 다음 시퀀스 지점 사이에서 객체는 표현의 평가에 의해 최대 한 번에 저장된 값을 수정해야합니다. [72] 또한, 이전 값은 저장 될 값을 결정하기 위해서만 읽어야한다 [73

와 함께 각주 73 그것을 더 명확하게합니다

  1. 이 단락은 다음과 같은 정의되지 않은 진술 표현을 렌더링합니다

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

    허용하는 동안

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

다양한 시퀀스 포인트는 부록 C에 나열되어 있습니다. C11 (그리고 C99):

  1. 다음은 5.1.2.3에 설명 된 시퀀스 포인트입니다.

    • 기능 지정자의 평가와 기능 호출과 실제 호출의 실제 인수 사이. (6.5.2.2).
    • 다음 연산자의 첫 번째 및 두 번째 피연산자의 평가 사이 : 논리 및 && (6.5.13); 논리적 또는 || (6.5.14); 쉼표, (6.5.17).
    • 조건부의 첫 번째 피연산자 평가 사이에? : 운영자와 두 번째 및 세 번째 피연산자 중 어느 것이 평가되는지 (6.5.15).
    • 전체 선언자의 끝 : 선언자 (6.7.6);
    • 완전한 발현의 평가와 평가할 다음 전체 표현 사이에. 다음은 전체 표현입니다 : 화합물 문자 (6.7.9)의 일부가 아닌 이니셜 라이저; 표현식 진술 (6.8.3)의 표현; 선택 문의 제어 표현 (IF 또는 스위치) (6.8.4); 시간 또는 DO 문 (6.8.5)의 제어 표현; 명령문 (6.8.5.3)의 각각의 (선택적) 표현; 반환 문 (6.8.6.4)의 (선택적) 표현.
    • 라이브러리 기능이 돌아 오기 직전 (7.1.4).
    • 각 형식의 입력/출력 함수 변환 지정자 (7.21.6, 7.29.2)와 관련된 조치 후.
    • 비교 함수에 대한 각 호출 직전 및 직후, 비교 함수에 대한 호출과 해당 호출 (7.22.5)에 대한 인수로 전달 된 객체의 움직임 사이.

같은 문구 C11의 단락 이다:

  1. 스칼라 객체의 부작용이 동일한 스칼라 객체에 대한 다른 부작용 또는 동일한 스칼라 객체의 값을 사용하여 값 계산에 비해 시합되지 않으면 동작이 정의되지 않습니다. 표현의 하위 표현의 여러 허용 순서가있는 경우, 그러한 시퀀스 부작용이 순서 중 어느 쪽에서 발생하는 경우 동작이 정의되지 않습니다 .84)

예를 들어 최근 버전의 GCC를 사용하여 프로그램에서 이러한 오류를 감지 할 수 있습니다. -Wall 그리고 -Werror, 그런 다음 GCC는 프로그램 컴파일을 거부합니다. 다음은 GCC의 출력입니다 (Ubuntu 6.2.0-5ubuntu12) 6.2.0 20161005 :

% gcc plusplus.c -Wall -Werror -pedantic
plusplus.c: In function ‘main’:
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = i++ + ++i;
    ~~^~~~~~~~~~~
plusplus.c:6:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
plusplus.c:10:6: error: operation on ‘i’ may be undefined [-Werror=sequence-point]
    i = (i++);
    ~~^~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = u++ + ++u;
    ~~^~~~~~~~~~~
plusplus.c:14:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
plusplus.c:18:6: error: operation on ‘u’ may be undefined [-Werror=sequence-point]
    u = (u++);
    ~~^~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
    v = v++ + ++v;
    ~~^~~~~~~~~~~
plusplus.c:22:6: error: operation on ‘v’ may be undefined [-Werror=sequence-point]
cc1: all warnings being treated as errors

중요한 부분은 아는 것입니다 시퀀스 포인트는 무엇입니까? 무엇인가요 시퀀스 지점과 무엇을 그렇지 않습니다. 예를 들어 쉼표 운영자 시퀀스 지점입니다

j = (i ++, ++ i);

잘 정의되어 있으며 증가합니다 i 하나씩, 오래된 가치를 산출하고, 그 값을 버립니다. 그런 다음 쉼표 운영자에서 부작용을 해결하십시오. 그런 다음 증가합니다 i 하나씩, 결과 값은 표현의 가치가된다 - 즉 이것은 단지 쓰기하는 방법 일뿐입니다. j = (i += 2) 다시는 "영리한"방법입니다.

i += 2;
j = i;

그러나, 그 , 기능 인수 목록은 다음과 같습니다 ~ 아니다 쉼표 연산자이며, 뚜렷한 인수의 평가 사이에는 시퀀스 지점이 없습니다. 대신에 그들의 평가는 서로와 관련하여 우표되지 않습니다. 그래서 함수 호출

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

가지다 정의되지 않은 행동 왜냐하면 평가 사이에는 시퀀스 점이 없습니다 i++ 그리고 ++i 기능 인수, 및 가치 i 따라서 둘 다에 의해 두 번 수정됩니다 i++ 그리고 ++i, 이전과 다음 시퀀스 지점 사이.

~ 안에 https://stackoverflow.com/questions/29505280/incrementing-array-index-in-c 누군가가 다음과 같은 성명서에 대해 물었습니다.

int k[] = {0,1,2,3,4,5,6,7,8,9,10};
int i = 0;
int num;
num = k[++i+k[++i]] + k[++i];
printf("%d", num);

7 인쇄 7 ... OP는 6을 인쇄 할 것으로 예상했다.

그만큼 ++i 나머지 계산 전에 모든 완료를 보장하는 것은 아닙니다. 실제로, 다른 컴파일러는 여기서 다른 결과를 얻을 것입니다. 제공 한 예에서 처음 2 ++i 실행 된 다음 값 k[] 읽었고 마지막으로 읽었습니다 ++i 그 다음에 k[].

num = k[i+1]+k[i+2] + k[i+3];
i += 3

최신 컴파일러는 이것을 매우 잘 최적화합니다. 사실, 원래 쓴 코드보다 더 나은 것일 수 있습니다 (원하는 방식으로 작동했다고 가정 함).

당신의 질문은 아마도 "왜 이러한 구성이 C에서 정의되지 않은 행동입니까?" 당신의 질문은 아마도 "왜이 코드를 사용 했습니까? ++) 내가 기대했던 가치를주지 않습니까? ", 누군가가 당신의 질문을 중복으로 표시하고 여기로 보냈습니다.

이것 답변은 그 질문에 대답하려고 시도합니다. 코드가 왜 당신이 기대했던 대답을 제공하지 않았으며, 예상대로 작동하지 않는 표현을 인식하고 피하는 법을 배울 수있는 방법을 어떻게 배울 수 있습니까?

나는 당신이 C의 기본 정의를 들었다고 생각합니다. ++ 그리고 -- 지금까지 연산자와 접두사가 어떻게 형성되는지 ++x Postfix 양식과 다릅니다 x++. 그러나이 운영자들은 생각하기가 어렵 기 때문에 당신이 이해하기 위해, 아마도 당신은 아마

int x = 5;
printf("%d %d %d\n", x, ++x, x++);

그러나 놀랍게도이 프로그램은 그랬습니다 ~ 아니다 이해하는 데 도움이 ++ 당신이 생각한 것과 전혀 다른 것이 아니라 완전히 다른 것을 수행합니다.

아니면 아마도 당신은 이해하기 어려운 표현을보고 있습니다.

int x = 5;
x = x++ + ++x;
printf("%d\n", x);

아마도 누군가가 당신에게 그 코드를 퍼즐로 주었을 것입니다. 이 코드는 또한 이해가되지 않습니다. 특히 실행하는 경우 두 가지 다른 컴파일러로 컴파일하고 실행하면 두 가지 다른 답변을 얻을 수 있습니다! 그게 무슨 일이야? 어떤 대답이 맞습니까? (그리고 대답은 둘 다 또는 그들 중 어느 것도 없다는 것입니다.)

지금까지 들었 듯이이 모든 표현은 한정되지 않은, 이는 C 언어가 자신이 할 일에 대해 보장하지 않음을 의미합니다. 이것은 이상하고 놀라운 결과입니다. 아마도 당신이 편집하고 실행되는 한 당신이 쓸 수있는 모든 프로그램이 독특하고 잘 정의 된 출력을 생성 할 것이라고 생각했기 때문입니다. 그러나 정의되지 않은 행동의 경우 그렇지 않습니다.

표현이 정의되지 않은 이유는 무엇입니까? 관련된 표현입니다 ++ 그리고 -- 항상 정의되지 않았습니까? 물론 : 이것들은 유용한 연산자이며, 제대로 사용하면 완벽하게 정의되어 있습니다.

표현에 대해 우리는 그들이 정의되지 않은 것에 대해 이야기하는 것에 대해 이야기하고 있습니다. 한 번에 너무 많은 일이 일어나고있을 때, 어떤 순서가 일어날 지 확실하지 않을 때, 결과에 대한 순서가 중요한 시점은 우리가 얻는 것입니다.

이 답에 사용한 두 가지 예로 돌아가 봅시다. 내가 썼을 때

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

문제는 전화하기 전에입니다 printf, 컴파일러는 값을 계산합니까? x 첫째, 또는 x++, 또는 아마도 ++x? 그러나 그것은 밝혀졌습니다 우리는 모른다. C에는 함수에 대한 인수가 왼쪽에서 오른쪽으로 또는 오른쪽에서 왼쪽에서 또는 다른 순서로 평가되는 규칙이 없습니다. 따라서 컴파일러가 할 것인지는 말할 수 없습니다 x 먼저 ++x, 그 다음에 x++, 또는 x++ 그 다음에 ++x 그 다음에 x, 또는 다른 주문. 그러나 컴파일러가 사용하는 순서에 따라 다른 결과가 인쇄 될 수 있기 때문에 순서는 분명히 중요합니다. printf.

이 미친 표현은 어떻습니까?

x = x++ + ++x;

이 표현의 문제는 x의 값을 수정하려는 세 가지 다른 시도를 포함한다는 것입니다. (1) x++ Part는 1에 1을 추가하고 새 값을 x, 그리고 오래된 가치를 반환합니다 x; (2) ++x Part는 1에 1을 추가하고 새 값을 x, 새로운 가치를 반환합니다 x; 그리고 (3) x = Part는 다른 두 개의 합을 x에 다시 할당하려고합니다. 이 세 가지 시도 중 어느 것이 "승리"합니까? 세 값 중 어느 것이 실제로 할당 될 것인지 x? 다시, 아마도 놀랍게도 C에는 우리에게 말할 규칙이 없습니다.

우선 순위 나 연관성 또는 왼쪽에서 오른쪽으로 평가하면 어떤 순서가 발생하는지 알려주지 만 그렇지 않습니다. 당신은 나를 믿지 않을 수도 있지만, 내 말을 받아들이십시오. 다시 말할 것입니다. 우선 순위와 연관성은 C에서 표현의 평가 순서의 모든 측면을 결정하지는 않습니다. 우리가 새로운 가치를 다음과 같은 것에 할당하려고하는 다른 지점 x, 우선 순위와 연관성 ~ 아니다 어떤 시도가 먼저, 마지막으로, 또는 어떤 일이 발생하는지 알려주세요.


따라서 모든 배경과 소개를 통해 모든 프로그램이 잘 정의되어 있는지 확인하고 어떤 표현을 작성할 수 있는지, 어떤 프로그램을 작성할 수 있는지, 어떤 프로그램을 쓸 수 없는지 확인하려면?

이 표현은 모두 괜찮습니다.

y = x++;
z = x++ + y++;
x = x + 1;
x = a[i++];
x = a[i++] + b[j++];
x[i++] = a[j++] + b[k++];
x = *p++;
x = *p++ + *q++;

이러한 표현은 모두 정의되지 않았습니다.

x = x++;
x = x++ + ++x;
y = x + x++;
a[i] = i++;
a[i++] = i;
printf("%d %d %d\n", x, ++x, x++);

마지막 질문은 어떤 표현이 잘 정의되어 있고 어떤 표현이 정의되지 않은지 알 수 있습니까?

앞서 말했듯이, 정의되지 않은 표현식은 한 번에 너무 많은 표현이있는 곳, 어떤 순서가 어떤 일이 일어날 지 확신 할 수없고 주문이 중요한 곳입니다.

  1. 둘 이상의 다른 장소에서 수정 된 변수가 하나있는 경우, 어떤 수정이 먼저 발생하는지 어떻게 알 수 있습니까?
  2. 한 곳에서 수정되고 다른 장소에서 값을 사용하는 변수가 있다면 이전 값을 사용하는지 또는 새 값을 사용하는지 어떻게 알 수 있습니까?

#1의 예로서 표현식에서

x = x++ + ++x;

`x를 수정하려는 세 가지 시도가 있습니다.

#2의 예로서 표현식에서

y = x + x++;

우리는 둘 다의 가치를 사용합니다 x, 수정하십시오.

그래서 이것이 답입니다. 쓰는 모든 표현식에서 각 변수가 최대 한 번에 수정되고 변수가 수정되면 해당 변수의 값을 다른 곳에서 사용하려고 시도하지 않도록하십시오.

이런 종류의 계산에서 일어나는 일에 대한 좋은 설명은 문서에 제공됩니다. N1188 ~에서 ISO W14 사이트.

나는 아이디어를 설명한다.

이 상황에 적용되는 표준 ISO 9899의 주요 규칙은 6.5p2입니다.

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

시퀀스는 표현과 같은 표현을 가리 킵니다 i=i++ 전입니다 i= 그리고 후 i++.

내가 위에서 인용 한 논문에서는 작은 상자에 의해 형성되는 것으로 프로그램을 파악할 수 있으며, 각 상자에는 2 개의 연속 시퀀스 지점 사이의 지침이 포함되어 있습니다. 시퀀스 지점은 표준의 부록 C로 정의됩니다. i=i++ 완전 발현을 구분하는 2 개의 시퀀스 포인트가 있습니다. 이러한 표현은 expression-statement 뒷모야-나우르 형태의 문법 (문법은 표준의 부록 A에 제공됨).

따라서 상자 내부의 지침 순서에는 명확한 순서가 없습니다.

i=i++

로 해석 될 수 있습니다

tmp = i
i=i+1
i = tmp

또는 AS

tmp = i
i = tmp
i=i+1

코드를 해석하기 위해이 모든 양식이 있기 때문입니다 i=i++ 유효하고 둘 다 다른 답변을 생성하기 때문에 동작은 정의되지 않습니다.

따라서 프로그램을 구성하는 각 상자의 시작과 끝에 시퀀스 포인트를 볼 수 있습니다. 순서를 변경하면 때때로 결과가 변경 될 수 있습니다.

편집하다:

그러한 모호성을 설명하기위한 다른 좋은 출처는 항목입니다. C-FAQ 사이트 (또한 게시 책으로), 즉 여기 그리고 여기 그리고 여기 .

그 이유는 프로그램이 정의되지 않은 동작을 실행하고 있기 때문입니다. C ++ 98 표준에 따라 필요한 시퀀스 포인트가 없기 때문에 문제는 평가 순서에 있습니다 (C ++ 11 용어에 따라 작업이 전후에 시퀀싱되지 않음).

그러나 하나의 컴파일러를 고수하면 기능 호출이나 포인터를 추가하지 않는 한 동작이 더 지저분 해지는 한 동작이 지속적입니다.

  • 먼저 GCC : 사용 누 웬 밍프 15 GCC 7.1 당신은 다음을 얻을 것입니다 :

    #include<stdio.h>
    int main(int argc, char ** argv)
    {
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 2
    
    i = 1;
    i = (i++);
    printf("%d\n", i); //1
    
    volatile int u = 0;
    u = u++ + ++u;
    printf("%d\n", u); // 2
    
    u = 1;
    u = (u++);
    printf("%d\n", u); //1
    
    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); //2
    

    }

GCC는 어떻게 작동합니까? 오른쪽 (RHS)의 왼쪽에서 오른쪽 순서에서 하위 표현식을 평가 한 다음 왼쪽 (LHS)에 값을 할당합니다. 이것이 바로 Java와 C#이 표준을 행동하고 정의하는 방식입니다. (예, Java 및 C#의 동등한 소프트웨어는 동작을 정의했습니다). RHS 문서에서 각 하위 표현식을 왼쪽에서 오른쪽 순서로 하나씩 평가합니다. 각각의 서브 표현에 대해 : ++ C (사전 증가)를 먼저 평가 한 다음 값 C가 작동에 사용 된 다음 사후 증분 C ++).

~에 따르면 GCC C ++ : 연산자

GCC C ++에서 운영자의 우선 순위는 개별 운영자가 평가되는 순서를 제어합니다.

정의 된 동작 C ++의 동등한 코드는 다음을 이해합니다.

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    //i = i++ + ++i;
    int r;
    r=i;
    i++;
    ++i;
    r+=i;
    i=r;
    printf("%d\n", i); // 2

    i = 1;
    //i = (i++);
    r=i;
    i++;
    i=r;
    printf("%d\n", i); // 1

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

    u = 1;
    //u = (u++);
    r=u;
    u++;
    u=r;
    printf("%d\n", u); // 1

    register int v = 0;
    //v = v++ + ++v;
    r=v;
    v++;
    ++v;
    r+=v;
    v=r;
    printf("%d\n", v); //2
}

그런 다음 우리는갑니다 비주얼 스튜디오. Visual Studio 2015, 당신은 얻을 수 있습니다 :

#include<stdio.h>
int main(int argc, char ** argv)
{
    int i = 0;
    i = i++ + ++i;
    printf("%d\n", i); // 3

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

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

    u = 1;
    u = (u++);
    printf("%d\n", u); // 2 

    register int v = 0;
    v = v++ + ++v;
    printf("%d\n", v); // 3 
}

Visual Studio가 어떻게 작동하고, 또 다른 접근 방식을 취하고, 첫 번째 패스의 모든 사전 점점 표현을 평가 한 다음, 두 번째 패스의 작업에서 변수 값을 사용하고, 세 번째 패스에서 RHS에서 LHS로 할당 한 다음 마지막 패스에서 모든 평가를 평가합니다. 한 번의 패스의 점유 후 표현.

따라서 Visual C ++로 정의 된 동작 C ++의 동등성은 다음을 이해합니다.

#include<stdio.h>
int main(int argc, char ** argv)
{
    int r;
    int i = 0;
    //i = i++ + ++i;
    ++i;
    r = i + i;
    i = r;
    i++;
    printf("%d\n", i); // 3

    i = 1;
    //i = (i++);
    r = i;
    i = r;
    i++;
    printf("%d\n", i); // 2 

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

    u = 1;
    //u = (u++);
    r = u;
    u = r;
    u++;
    printf("%d\n", u); // 2 

    register int v = 0;
    //v = v++ + ++v;
    ++v;
    r = v + v;
    v = r;
    v++;
    printf("%d\n", v); // 3 
}

Visual Studio 문서화가 우선 순위 및 평가 순서:

여러 운영자가 함께 나타나는 경우, 그들은 동등한 우선 순위를 가지며 그들의 연관성에 따라 평가됩니다. 테이블의 연산자는 PostFix 연산자로 시작하는 섹션에 설명되어 있습니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top