IT이야기

왜 이러한 구성들은 사전 및 사후 증가 미정의 행동을 사용하는가?

cyworld 2022. 4. 19. 22:29
반응형

왜 이러한 구성들은 사전 및 사후 증가 미정의 행동을 사용하는가?

#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++);

정의되지 않은 행동의 텍스트북 예시(순서에 대한 위키백과 항목 참조).

여기서 인용한 대부분의 답은 이러한 구성의 동작이 정의되지 않았다는 것을 강조하는 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++;

표준은 다음과 같이 말한다.

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++될 수 .

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를 발동시킨다.

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

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

및 6.5.16 배정 운영자, §4:

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

행동은 지정되지 않은 행동정의되지 않은 행동을 모두 불러일으키기 때문에 실제로 설명될 수 없기 때문에 이 코드에 대한 일반적인 예측은 할 수 없지만, 만약 당신이 Deep C와 불특정하고 정의되지 않은 Olve Maudal의 작품을 읽으면, 당신은 때때로 특정한 컴파일러와 환경으로 매우 구체적인 사례에서 좋은 추측을 할 수 있다.하지만 생산 가까운 곳에서는 그렇게 하지 말아줘.

따라서 초안 c99 표준 섹션의 지정되지 않은 동작으로 이동하십시오.6.5제3항은 다음과 같다.

연산자와 피연산자의 그룹화는 구문.74)로 표시된다. 단, 나중(함수 호출)(&&), |||, ?:, 콤마 연산자에 대해 지정되는 것을 제외하고, 하위 표현에 대한 평가 순서와 부작용이 발생하는 순서는 모두 불특정하다.

그래서 이런 선이 있을 때:

i = i++ + ++i;

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

우리는 또한 프로그램이 변수를 수정하기 때문에 여기서도 정의되지 않은 행동을 가지고 있다.iu, 등) 시퀀스 포인트 사이에 두 번 이상.초안 표준 섹션부터6.5제2항(광산):

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

정의되지 않은 것으로 다음과 같은 코드 예를 인용한다.

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다음과 같이.

이 국제 표준이 요구하는 어떤 요건도 부과하지 않는, 휴대할 수 없거나 잘못된 프로그램 구성 또는 잘못된 데이터 사용 시 동작

다음 사항에 유의하십시오.

정의되지 않은 동작은 예측 불가능한 결과를 가지고 상황을 완전히 무시하는 것에서부터, 환경의 문서화된 방식으로 변환 또는 프로그램을 실행하는 동안 동작하는 것(진단 메시지 발행 여부와 무관), 변환 또는 실행을 종료하는 것(진단 메시지의 발행과 함께)까지 다양하다.e).

이 질문에 대답하는 또 다른 방법은, 순서와 정의되지 않은 행동의 불가사의한 세부사항들에 사로잡히기 보다는, 단순히, 그들이 무엇을 의미해야 하는지를 묻는 것이다.프로그래머는 무엇을 하려고 했는가?

첫 번째 파편은 다음과 같이 물었다.i = i++ + ++i내 책에는 분명히 미친 것 같다.아무도 그것을 실제 프로그램에 쓰지 않을 것이고, 그것이 무엇을 하는지는 분명하지 않다. 누군가가 이런 특정한 일련의 조작을 야기할 수 있는 상상할 수 있는 알고리즘은 없다.그리고 너와 나에겐 그것이 무엇을 해야 하는지가 분명하지 않기 때문에, 컴파일러도 그것이 무엇을 해야 하는지를 알아내지 못하는 것은 내 책에서도 괜찮다.

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

i = i + 1

물론 C는 편리한 지름길을 가지고 있다.

i++

즉, "i에 1을 추가하고 결과를 i에 다시 할당"을 의미한다.그래서 만약 우리가 둘의 잡동사니를 만든다면, 글로써

i = i++

우리가 정말로 말하고 있는 것은 "1을 i에 더하고, 결과를 i에 다시 할당하고, 그 결과를 다시 i에 할당하라"는 것이다.우리는 혼란스러워서 컴파일러도 헷갈려도 별로 신경 안 써.

현실적으로, 이러한 미친 표현들이 쓰여질 수 있는 유일한 때는 사람들이 그것을 어떻게 ++가 작동해야 하는지에 대한 인위적인 예로 사용하고 있을 때다.그리고 물론 ++가 어떻게 작동하는지 이해하는 것이 중요하다.그러나 ++를 사용하는 한 가지 실천적 규칙은 "++를 사용하는 표현이 무엇을 의미하는지 분명하지 않다면 쓰지 말라"는 것이다.

우리는 comp.lang.c에 이런 표현과 그것들이 왜 정의되지 않는지에 대해 토론하는 데 수 많은 시간을 보내곤 했다.그 이유를 설명하기 위해 두 개의 긴 답변이 웹에 기록되어 있다.

또한 질문 3.8 및 C 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 표준에는 다음과 같은 몇 가지 관련 설명이 있다.

Annex 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순서가 방해되지 않는 지점이야


또 다른 세부사항은 프린트f() 호출에 포함된 쉼표쉼표 연산자가 아닌 구분자라는 점이다.

이는 콤마 연산자가 피연산자의 평가 사이에 시퀀스 포인트를 도입하기 때문에 중요한 구별이며, 이는 다음과 같다.

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증분하다i6그리고i++의 옛 가치를 낳다i(6은 에 ~에 되어 있다.j. 그러면.i7사후의 이유로

함수 호출의 쉼표가 쉼표 연산자라면

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

문제가 되지 않을 것이다.그러나 여기 쉼표구분자이기 때문에 정의되지 않은 행동을 유발한다.


정의되지 않은 행동을 처음 접하는 사람들은 C에서 정의되지 않은 행동의 개념과 다른 많은 변형을 이해하기 위해 모든 C 프로그래머가 정의되지 않은 행동에 대해 알아야을 읽는 것이 유익할 것이다.

이 게시물:정의되지 않은, 지정되지 않은, 구현 정의한 행동도 관련이 있다.

당신의 질문은 아마도 "C에서 왜 이런 구성들이 정의되지 않은 행동인가?"가 아니었을 것이다.당신의 질문은 아마도 "왜 이 코드를 사용했는가?++은 가 기대한 라고 물었고, 내가 기대했던 값을 내게 주지 않소?" 그리고 누군가가 당신의 질문을 중복으로 표시하여 당신을 이곳으로 보냈다.

대답은 그 질문에 답하려고 한다: 왜 당신의 코드가 당신이 기대한 답을 주지 않았는가, 그리고 어떻게 당신이 예상한 대로 작동하지 않을 표현을 인식하는 것을 배울 수 있는가.

C의 기본적인 정의를 들어보셨을 겁니다.++그리고--지금 연산자 및 접두사 형식 방법++x사후 처리 양식과 다르다.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또는 다른 순서.하지만 순서는 분명히 중요해, 왜냐하면 컴파일러가 어떤 순서를 사용하느냐에 따라, 우리는 분명히 다른 일련의 숫자들을 인쇄할 수 있을 테니까.

이 미친 표정은 어때?

x = x++ + ++x;

이 표현식의 문제는 이 표현에 세 가지의 다른 가치 수정 시도가 들어 있다는 것이다.x (1) : (1)x++일부분이 가져가려고 하다x1 가치 , 1번 가, 1번 가 장에 장x, 이전 값을 반환한다. (2)++x일부분이 가져가려고 하다x1 가치 , 1번 가, 1번 가 장에 장x, 그리고 새로운 값을 반환한다. 그리고 (3)x =일부는 나머지 두 개의 합을 다시 에 할당하려고 한다.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, 그리고 그것을 수정한다.

즉, 여러분이 쓰는 어떤 표현에서든, 각 변수는 한 번에 수정되도록 하고, 변수가 수정되면, 그 변수의 값도 다른 곳에서 사용하려고 하지 않도록 하는 겁니다.


한가지 더요.내가 이 답을 제시하면서 시작했던 정의되지 않은 표현을 어떻게 '수정'해야 할지 궁금할 것이다.

printf("%d %d %d\n", x, ++x, x++);, 그것은 쉽다 — 그냥 세 개의 별개로 쓰세요.printf전화:

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

이제 그 행동은 완벽하게 정의되어 있고, 당신은 합리적인 결과를 얻을 수 있을 것이다.

x = x++ + ++x다른 한편으로는 고칠 방법이 없어여러분의 기대에 부합하는 행동을 보장하도록 그것을 쓸 방법은 없지만, 괜찮다. 왜냐하면 여러분은 결코 이런 표현을 쓰지 않을 것이기 때문이다.x = x++ + ++x어쨌든 실제 프로그램에서는 말이야

그 표현들의 구문은 다음과 같다.a = a++또는a++ + a++C 표준의 a가 준수되지 않기 때문에 이러한 구성물의 행동정의되지 않는다.C99 6.5p2:

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

각주 73으로 다음 사항을 더욱 명확히 했다.

  1. 이 단락은 다음과 같은 정의되지 않은 문장 식을 렌더링한다.

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

    허락하는 동안에

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

다양한 시퀀스 포인트는 C11의 부속서 C(및 C99):

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

    • 함수 지정자의 평가와 함수 호출에서의 실제 인수와 실제 통화 사이에. (6.5.2.2)
    • 다음 연산자의 첫 번째 피연산자와 두 번째 피연산자의 평가 사이에 논리 AND&&(6.5.13); 논리 OR||(6.5.14); 쉼표 , (6.5.17).
    • 조건부 ? : 연산자와 두 번째 및 세 번째 연산자의 평가 사이에 (6.5.15) 평가된다.
    • 전체 선언기의 끝: 선언기(6.7.6);
    • 전체 표현식의 평가와 평가될 다음 전체 표현식의 평가 사이에.다음은 복합 리터럴에 속하지 않는 이니셜라이저(6.7.9), 표현 문장의 표현(6.8.3), 선택 문(if 또는 switch)의 제어 표현(6.8.4), 잠시 또는 실행 문(do)의 제어 표현(6.8.5), 문(옵션)에 대한 a의 각각(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);

잘 정의되어 있으며 증가함i1로 이전 값을 산출하고 해당 값을 삭제한 다음 콤마 연산자에서 부작용을 해결한 다음 증가시킨다.i1에 의해, 그리고 그 결과 값은 표현식의 값이 된다. 즉, 이것은 단지 의도된 쓰기 방법일 뿐이다.j = (i += 2)그것은 다시 한번 "신비한" 글쓰기 방법이다.

i += 2;
j = i;

하지만, 그,함수 인수 목록에는 쉼표 연산자가 없으며, 구별된 인수의 평가 사이에 시퀀스 포인트가 없다. 대신, 함수 호출은 서로에 대해 서열화되지 않는다.

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

함수 인수의 평가와 사이에 시퀀스 포인트가 없기 때문에 정의되지 않은 동작이 있음i따라서 양자에 의해 두 번 수정된다.i++그리고++i이전 시퀀스 지점과 다음 시퀀스 지점 사이.

이러한 종류의 계산에서 어떤 일이 일어나는지에 대한 좋은 설명은 ISO W14 사이트에서 문서 n1188에 제공된다.

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

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

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

순서는 다음과 같은 표현으로 가리킨다.i=i++이전이다i=그 다음에i++.

위에서 인용한 논문에서는 프로그램을 작은 박스에 의해 형성되는 것으로 파악할 수 있다고 설명되어 있는데, 각 박스는 연속 2개의 시퀀스 포인트 사이의 지시사항을 담고 있다.시퀀스 포인트는 다음의 경우 표준의 부속서 C에 정의되어 있다.i=i++전체 범위를 구분하는 두 개의 시퀀스 포인트가 있다.그러한 표현은 다음 항목과 구문론적으로 동일하다.expression-statement문법의 백커스-나우르 형식(문법은 표준의 부속서 A에 제공된다).

그래서 상자 안의 지시 순서는 명확한 순서가 없다.

i=i++

라고 해석할 수 있다.

tmp = i
i=i+1
i = tmp

또는 로서

tmp = i
i = tmp
i=i+1

왜냐하면 이 모든 양식이 코드를 해석하기 때문이다.i=i++타당하며, 두 가지 모두 다른 답변을 생성하기 때문에, 행동은 정의되지 않는다.

따라서 프로그램을 구성하는 각 박스의 시작과 끝에서 순서 지점을 볼 수 있으며 [상자는 C의 원자 단위] 상자 안에서 지시 순서는 모든 경우에 정의되지 않는다.순서를 바꾸는 것은 때때로 결과를 바꿀 수 있다.

편집:

그러한 모호함을 설명하기 위한 다른 좋은 출처는 c-faq 사이트 (또한 책으로 출판됨)로부터의 출품, 즉 여기와 여기 그리고 여기이다.

코드 라인을 컴파일하고 분해하십시오. 정확히 어떻게 얻는지 알고 싶다면.

이것이 내가 생각하는 것과 함께 내 기계에 걸리는 것이다.

$ 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 명령이 컴파일러 최적화라고 가정하시겠습니까?)

C 표준은 변수가 두 시퀀스 포인트 사이에 최대 한 번만 할당되어야 한다고 말한다.예를 들어 세미콜론은 시퀀스 포인트다.
따라서 양식의 모든 문구는 다음과 같다.

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

그런 식으로 규칙을 어기는 겁니다이 표준은 또한 행동이 정의되지 않고 불특정하지 않다고 말한다.어떤 컴파일러들은 이것을 감지하고 어떤 결과를 만들어내지만 이것은 표준에 부합하지 않는다.

그러나 두 개의 시퀀스 포인트 사이에서 두 개의 다른 변수를 증가시킬 수 있다.

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

위의 내용은 문자열을 복사/분석하는 동안 흔히 볼 수 있는 코딩 실습이다.

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번 인쇄된...OP는 그것이 6을 인쇄할 것으로 기대했다.

++i계산이 끝나기 전에 증분이 모두 보장되지는 않는다.사실, 다른 컴파일러들은 여기서 다른 결과를 얻을 것이다.제공한 예에서 처음 2개++i실행된 다음 다음k[]읽혔고, 그 다음에 마지막이 되었다.++i, 그러면.k[].

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

현대 컴파일러들은 이것을 매우 잘 최적화할 것이다.사실, 아마도 당신이 원래 썼던 암호보다 더 나을 것이다(당신이 바랐던 대로 작용했다고 가정한다).

어떤 컴파일러와 프로세서가 실제로 그렇게 할 것 같지는 않지만, C 표준에 따르면 컴파일러가 다음과 같은 순서로 "i++"를 구현하는 것은 합법적일 것이다.

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++98 표준에 따라 필요한 시퀀스 포인트가 없기 때문에 평가 순서에 있다(C++11 용어에 따라 다른 작업의 앞이나 뒤에 서열화되지 않는다).

그러나 컴파일러 하나를 고수하면 함수 호출이나 포인터를 추가하지 않는 한 그 행동이 지속된다는 것을 알게 될 것이고, 그 행동이 더욱 지저분해질 것이다.

Nuwen MinGW 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)에 값을 할당한다. 이것이 바로 자바와 C#가 어떻게 행동하고 표준을 정의하는지이다. (예, 자바와 C#의 동등한 소프트웨어는 행동을 정의했다.)왼쪽에서 오른쪽 순서로 RHS 문에서 각 하위 식을 하나씩 평가한다. 각 하위 식에 대해 ++c(사전 증가)를 먼저 평가한 다음 c 값을 연산에 사용하고, 이후 증분 c++)를 사용한다.

GCC C++에 따라: 연산자

GCC C++에서 연산자의 우선 순위는 개별 연산자를 평가하는 순서를 제어한다.

정의된 동작 C++에서 GCC가 이해하는 것과 동등한 코드:

#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는 어떻게 작동하는지, 다른 접근방식을 취하고, 1차 통과에서 모든 사전 증가 표현을 평가한 다음, 2차 통과에서 운영의 변수 값을 사용하고, 3차 통과에서 RHS에서 LHS로 할당하고, 마지막으로 한 번의 통과에서 모든 증가 후 표현을 평가한다.

따라서 정의된 동작 C++에서 Visual 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 연산자로 시작하는 절에 설명되어 있다.

참조URL: https://stackoverflow.com/questions/949433/why-are-these-constructs-using-pre-and-post-increment-undefined-behavior

반응형