이러한 구조가 사전 및 사후 증가 정의되지 않은 동작을 사용하는 이유는 무엇입니까?
#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 Expressions, §2라고 생각합니다.
이전 시퀀스 포인트와 다음 시퀀스 포인트 사이에서 객체는 표현식 평가에 의해 최대 한 번 수정 된 저장된 값을 가져야합니다. 또한, 이전 값은 저장 될 값을 결정하기 위해서만 읽혀 져야합니다.
및 6.5.16 할당 연산자, §4 :
피연산자의 평가 순서는 지정되지 않습니다. 할당 연산자의 결과를 수정하거나 다음 시퀀스 포인트 이후에 액세스하려고 시도하면 동작이 정의되지 않습니다.
C 표준에서 인용 한 대부분의 답변은 이러한 구성의 동작이 정의되지 않았 음을 강조합니다. 이러한 구문의 동작이 정의되지 않은 이유 를 이해하려면 먼저 C11 표준에 비추어 이러한 용어를 이해해 보겠습니다.
시퀀스 : (5.1.2.3)
두 개의 평가가 주어
A
지고 이전 에 순서가 지정B
되면 의 실행이 의 실행보다 우선합니다 .A
B
A
B
순서 없음 :
경우
A
이전 또는 이후에 순서가되지 않고B
다음A
과B
unsequenced 있습니다.
평가는 다음 두 가지 중 하나 일 수 있습니다.
- 표현식의 결과를 계산 하는 값 계산 ; 과
- 개체의 수정 인 부작용 .
시퀀스 포인트 :
식의 계산 사이의 시퀀스 지점이 존재
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 C 및 Unspecified 및 Undefined 와 같은 Olve Maudal의 작업 을 읽으면 때때로 좋은 결과를 얻을 수 있습니다. 특정 컴파일러 및 환경에서 매우 특정한 경우에 추측하지만 프로덕션 근처에서는 그렇게하지 마십시오.
로 이동 그래서 수없는 동작 에서, 표준 C99 초안 섹션 6.5
제 3은 말한다 ( 강조 광산 ) :
연산자와 피연산자의 그룹화는 구문으로 표시됩니다 .74) 나중에 지정된 경우를 제외하고 (함수 호출 (), &&, ||,? : 및 쉼표 연산자의 경우), 하위 표현식의 평가 순서 및 순서 어떤 부작용이 발생하는지는 둘 다 명시되지 않았습니다.
따라서 다음과 같은 줄이있을 때 :
i = i++ + ++i;
우리는 여부를 알 수없는 i++
또는 ++i
먼저 평가됩니다. 이것은 주로 컴파일러에게 최적화를위한 더 나은 옵션을 제공하기위한 것 입니다.
우리는 또한이 정의되지 않은 동작을 프로그램 변수를 수정 (때문에 여기뿐만 아니라 i
, u
사이에 한 번 이상, 등) 순서 포인트 . 초안 표준 섹션 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
로서 :
이 국제 표준이 요구 사항을 부과하지 않는 비 이식 적이거나 오류가있는 프로그램 구성 또는 잘못된 데이터를 사용할 때의 행동
다음 사항에 유의하십시오.
가능한 정의되지 않은 동작은 예측할 수없는 결과로 상황을 완전히 무시하는 것, 번역 또는 프로그램 실행 중에 환경 특성 (진단 메시지 발행 여부에 관계없이)의 문서화 된 방식으로 동작하는 것, 번역 또는 실행 종료 (발행 포함)에 이르기까지 다양합니다. 진단 메시지).
이 질문에 답하는 또 다른 방법은 시퀀스 포인트와 정의되지 않은 동작에 대한 난해한 세부 사항에 얽매이기보다는 단순히 무엇을 의미해야하는지 묻는 것입니다. 프로그래머는 무엇을하려고 했습니까?
첫 번째 질문에 대한 질문은 i = i++ + ++i
내 책에서 분명히 미쳤습니다. 아무도 그것을 실제 프로그램으로 작성하지 않을 것이며, 그것이 무엇을하는지 분명하지 않으며, 누군가가이 특별한 인위적인 일련의 작업을 초래할 수있는 코딩을 시도 할 수 있었던 상상할 수있는 알고리즘이 없습니다. 그리고 그것이 무엇을해야하는지 당신과 나에게 분명하지 않기 때문에, 컴파일러가 무엇을해야하는지 알아낼 수 없다면 내 책에서도 괜찮습니다.
두 번째 부분 인 i = i++
은 이해하기가 조금 더 쉽습니다. 누군가 분명히 i를 증가시키고 그 결과를 i에 다시 할당하려고합니다. 그러나 C에서이 작업을 수행하는 몇 가지 방법이 있습니다. i에 1을 더하고 결과를 i에 다시 할당하는 가장 기본적인 방법은 거의 모든 프로그래밍 언어에서 동일합니다.
i = i + 1
물론 C에는 편리한 단축키가 있습니다.
i++
이것은 "i에 1을 더하고 결과를 i에 다시 할당"을 의미합니다. 그래서 우리가 두 가지의 호지 포지를 구성한다면
i = i++
우리가 실제로 말하는 것은 "i에 1을 더하고 결과를 i에 다시 할당하고 결과를 i에 다시 할당"하는 것입니다. 우리는 혼란 스럽기 때문에 컴파일러가 혼란스러워도 너무 귀찮게하지 않습니다.
현실적으로 이런 말도 안되는 표현이 쓰여지는 유일한 경우는 사람들이 ++이 어떻게 작동해야하는지에 대한 인위적인 예제로 사용하는 경우입니다. 물론 ++의 작동 방식을 이해하는 것이 중요합니다. 그러나 ++를 사용하는 한 가지 실용적인 규칙은 "++를 사용하는 표현이 무엇을 의미하는지 확실하지 않으면 쓰지 마십시오."입니다.
우리는 comp.lang.c에서 이와 같은 표현과 그것이 정의되지 않은 이유 를 논의하는 데 수없이 많은 시간을 보냈습니다 . 이유를 실제로 설명하려는 두 가지 긴 답변이 웹에 보관됩니다.
- 표준이 이러한 기능을 정의하지 않는 이유는 무엇입니까?
- 연산자 우선 순위가 평가 순서를 결정하지 않습니까?
C FAQ 목록 의 섹션 3 에있는 질문 3.8 및 나머지 질문 도 참조하십시오 .
종종이 질문은 다음과 같은 코드와 관련된 질문의 중복으로 연결됩니다.
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
이된다 7
사후 증가에 기인.
그래서 만약 쉼표 함수 호출은 쉼표 연산자로했다
printf("%d %d\n", ++i, i++);
문제가되지 않습니다. 그러나 여기서 쉼표 가 구분 기호 이기 때문에 정의되지 않은 동작을 호출합니다 .
정의되지 않은 동작을 처음 접하는 사람들은 모든 C 프로그래머가 정의되지 않은 동작 에 대해 알아야 할 사항 을 읽고 C 에서 정의되지 않은 동작의 다른 많은 변형과 개념을 이해하면 도움이 될 것 입니다.
이 게시물 : 정의되지 않은, 지정되지 않은, 구현 정의 된 동작 도 관련이 있습니다.
컴파일러와 프로세서가 실제로 그렇게 할 가능성은 낮지 만 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 표준 의 shall 이 준수 되지 않기 때문에 이러한 구문 의 동작 은 정의되지 않습니다. C99 6.5p2 :a = a++
a++ + a++
- 이전 시퀀스 포인트와 다음 시퀀스 포인트 사이에서 객체는 표현식 평가에 의해 최대 한 번 수정 된 저장된 값을 가져야합니다. 또한, 이전 값은 저장 될 값을 결정하기 위해서만 읽어야합니다. [73]
함께 각주 (73)가 더 있는지 명확히
이 단락은 다음과 같은 정의되지 않은 문 표현식을 렌더링합니다.
i = ++i + 1; a[i++] = i;
허용하는 동안
i = i + 1; a[i] = i;
다양한 시퀀스 포인트는 C11 (및 C99 ) 의 부록 C에 나열되어 있습니다 .
다음은 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) while 또는 do 문의 제어 표현 (6.8.5); for 문 (6.8.5.3)의 각 (선택적) 표현식 return 문 (6.8.6.4)의 (선택 사항) 표현식.
- 라이브러리 함수가 (7.1.4)를 반환하기 직전.
- 각 형식화 된 입력 / 출력 함수 변환 지정자 (7.21.6, 7.29.2)와 연관된 조치 후.
- 비교 함수에 대한 각 호출 직전과 직후, 그리고 비교 함수에 대한 호출과 해당 호출에 대한 인수로 전달 된 객체 이동 사이 (7.22.5).
C11 에서 같은 단락 의 문구는 다음과 같습니다.
- 스칼라 객체에 대한 부작용이 동일한 스칼라 객체에 대한 다른 부작용이나 동일한 스칼라 객체의 값을 사용하는 값 계산과 관련하여 순서가 지정되지 않은 경우 동작이 정의되지 않습니다. 표현식의 하위 표현식에 허용 가능한 순서가 여러 개있는 경우 순서에 따라 순서가 지정되지 않은 부작용이 발생하면 동작이 정의되지 않습니다 .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
1 씩 증가 하여 이전 값을 생성하고 해당 값을 버립니다. 그런 다음 쉼표 연산자에서 부작용을 해결하십시오. 그런 다음 1 씩 증가 i
하면 결과 값이 표현식의 값이됩니다. 즉, 이것은 j = (i += 2)
다시 "영리한" 쓰기 방법 인 인위적인 쓰기 방법입니다.
i += 2;
j = i;
그러나 ,
in 함수 인수 목록은 쉼표 연산자 가 아니며 개별 인수 평가 사이에 시퀀스 포인트가 없습니다. 대신 그들의 평가는 서로에 대해 순서가 지정되지 않습니다. 그래서 함수 호출
int i = 0;
printf("%d %d\n", i++, ++i, i);
보유 정의되지 않은 동작을 하기 때문에 거기의 평가 사이 시퀀스 지점 없다 i++
및 ++i
함수 인수는 및의 값은 i
그러므로 모두 두번 수정 i++
과 ++i
이전 및 다음 시퀀스 포인트 간의.
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에서 정의되지 않은 동작입니까?"가 아닐 것입니다. 귀하의 질문은 아마도 "이 코드 (사용 ++
)가 내가 기대 한 값을 제공하지 않는 이유는 무엇 입니까?"였으며 누군가 귀하의 질문을 중복으로 표시하여 여기로 보냈습니다.
이 대답은 그 질문에 대답하려고합니다. 왜 코드가 예상 한 대답을 제공하지 않았는지, 그리고 예상대로 작동하지 않는 표현식을 인식하고 피하는 법을 어떻게 배울 수 있습니까?
지금까지 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
first, 또는 x++
또는 어쩌면 값을 계산 ++x
합니까? 그러나 우리가 모른다는 것이 밝혀졌습니다 . C에는 함수에 대한 인수가 왼쪽에서 오른쪽으로, 오른쪽에서 왼쪽으로 또는 다른 순서로 평가된다는 규칙이 없습니다. 우리는 컴파일러가 할 것입니다 여부를 말할 수 없다 그래서 x
첫째, 다음 ++x
, 다음 x++
, 또는 x++
다음 ++x
다음 x
, 또는 다른 순서. 그러나 순서는 명확하게 중요합니다. 컴파일러가 사용하는 순서에 따라 printf
.
이 미친 표정은 어때?
x = x++ + ++x;
이 표현식의 문제점은 x의 값을 수정하려는 세 가지 시도가 포함되어 있다는 것입니다. (1) x++
파트가 x에 1을 더하고에 새 값을 저장하고 ; x
의 이전 값을 반환 하려고합니다 x
. (2) ++x
파트는 x에 1을 더하고에 새 값을 저장하고 새 값을 x
반환 하려고합니다 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의 예로서, 식에서
x = x++ + ++x;
`x를 수정하려면 세 번의 시도가 있습니다.
# 2의 예를 들면 다음과 같습니다.
y = x + x++;
우리는 둘 다의 값을 사용 x
하고 수정합니다.
그게 답입니다. 작성하는 모든 표현식에서 각 변수가 최대 한 번만 수정되고 변수가 수정 된 경우 해당 변수의 값을 다른 곳에서 사용하지 마십시오.
계산의이 종류에서 일어나는 일에 대해 좋은 설명은 문서에서 제공되는 n1188 로부터 는 ISO W14 사이트 .
나는 아이디어를 설명한다.
이 상황에 적용되는 표준 ISO 9899의 주요 규칙은 6.5p2입니다.
이전 시퀀스 포인트와 다음 시퀀스 포인트 사이에서 객체는 표현식 평가에 의해 최대 한 번 수정 된 저장된 값을 가져야합니다. 또한, 이전 값은 저장 될 값을 결정하기 위해서만 읽혀 져야합니다.
식의 시퀀스 포인트 i=i++
는 이전 i=
과 이후 i++
입니다.
위에서 인용 한 논문에서는 프로그램이 2 개의 연속 된 시퀀스 포인트 사이의 명령을 포함하는 작은 상자로 구성된 프로그램을 파악할 수 있다고 설명했습니다. 시퀀스 포인트는 표준의 부속서 C에 정의되어 i=i++
있으며, 전체 표현식을 구분하는 두 개의 시퀀스 포인트가 있는 경우입니다 . 이러한 표현은 문법 expression-statement
의 Backus-Naur 형식의 항목과 구문 적으로 동일 합니다 (문법은 표준의 부록 A에 제공됨).
따라서 상자 안의 지침 순서에는 명확한 순서가 없습니다.
i=i++
다음과 같이 해석 될 수 있습니다.
tmp = i
i=i+1
i = tmp
또는
tmp = i
i = tmp
i=i+1
코드를 해석하는 이러한 모든 형식 i=i++
이 유효하고 둘 다 다른 답변을 생성하기 때문에 동작이 정의되지 않았습니다.
따라서 시퀀스 포인트는 프로그램을 구성하는 각 상자의 시작과 끝에서 볼 수 있습니다 [상자는 C의 원자 단위입니다]. 상자 내부에서 명령 순서가 모든 경우에 정의되어 있지는 않습니다. 그 순서를 변경하면 때때로 결과가 바뀔 수 있습니다.
편집하다:
이러한 모호함을 설명하는 다른 좋은 출처는 c-faq 사이트 ( 책으로 도 출판 됨 )의 항목, 즉 here , here 및 here 입니다.
그 이유는 프로그램이 정의되지 않은 동작을 실행하고 있기 때문입니다. 문제는 평가 순서에 있습니다. 왜냐하면 C ++ 98 표준에 따라 필요한 시퀀스 포인트가 없기 때문입니다 (C ++ 11 용어에 따라 다른 작업이 다른 순서로 순서가 지정되지 않음).
그러나 하나의 컴파일러를 고수하면 함수 호출이나 포인터를 추가하지 않는 한 동작이 지속되므로 동작이 더 복잡해집니다.
따라서 먼저 GCC : 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)에 할당합니다. 이것이 바로 Java 및 C #이 작동하고 표준을 정의하는 방식입니다. (예, Java 및 C #의 동등한 소프트웨어가 동작을 정의했습니다.) RHS 문에서 각 하위 표현식을 왼쪽에서 오른쪽 순서로 하나씩 평가합니다. 각 하위 표현식에 대해 : ++ c (사전 증분)가 먼저 평가 된 다음 값 c가 연산에 사용 된 다음 사후 증분 c ++).
GCC C ++ 에 따르면 : 연산자
GCC C ++에서 연산자의 우선 순위는 개별 연산자가 평가되는 순서를 제어합니다.
GCC가 이해하는대로 정의 된 동작 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 로 이동합니다 . 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 설명서에는 평가의 우선 순위 및 순서가 명시되어 있습니다 .
여러 연산자가 함께 나타나는 경우 우선 순위가 같고 연관성에 따라 평가됩니다. 표의 연산자는 후위 연산자로 시작하는 섹션에 설명되어 있습니다.