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


814
#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?
}

12
@Jarett, 아닙니다. "시퀀스 포인트"에 대한 포인터가 필요했습니다. 작업하는 동안 i = i ++로 코드를 찾았습니다. "i의 값을 수정하지 않습니다". 나는 테스트했고 왜 그런지 궁금했다. 그 이후로이 문구를 제거하고 i ++로 대체했습니다.
PiX 2016 년

198
나는 모든 사람들이 질문자가 구조를 사용하기를 원하기 때문에 항상 이와 같은 질문을한다고 가정하는 것이 흥미 롭습니다. 필자의 첫 번째 가정은 PiX는 이것이 좋지 않다는 것을 알고 있지만 왜 그들이 컴파일러를 사용하고 있는지에 대해 어떻게 행동하는지 궁금하다는 것입니다. 그렇습니다. .. JCF (점프 앤 캐치 파이어) 포함
Brian Postow

32
궁금합니다 : 컴파일러가 "u = u ++ + ++ u;"와 같은 구문에 대해 경고하지 않는 이유는 무엇입니까? 결과가 정의되지 않은 경우?
OpenGL을 ES 알아

5
(i++)괄호에 관계없이 여전히 1로 평가됩니다
Drew McGowen

2
i = (i++);의도 한 것이 무엇이든 , 그것을 작성하는 분명한 방법이 있습니다. 잘 정의 된 경우에도 마찬가지입니다. 의 동작을 정의하는 Java에서도 i = (i++);여전히 잘못된 코드입니다. 그냥 쓰십시오i++;
Keith Thompson

답변:


566

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

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

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

가장 흥미로운 예,

u = (u++);

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


8
@PiX : 여러 가지 이유로 정의되지 않았습니다. 여기에는 다음과 같은 것들이 포함됩니다 : 명확한 "올바른 결과"가없고, 다른 머신 아키텍처는 다른 결과를 강력하게 선호합니다. 기존 관행이 일관성이 없거나 표준 범위를 벗어납니다 (예 : 유효한 파일 이름).
Richard

모든 사람을 혼동시키기 위해 C11에서는 이러한 예제가 잘 정의되어 있습니다 (예 :) i = ++i + 1;.
MM

2
UB의 개념이 존재하는 이유는 분명합니다. 이 표준은 C 구현이 특정 목적에 적합하기 위해 수행해야하는 모든 것을 완전하게 설명하기위한 것이 아니며 ( "하나의 프로그램"규칙에 대한 논의 참조) 대신 구현 자의 판단과 유용한 품질 구현을 생성하려는 욕구에 의존합니다. 저수준 시스템 프로그래밍에 적합한 품질 구현은 하이 엔드 번호 크 런칭 애플리케이션에 필요하지 않은 동작의 동작을 정의해야합니다. 표준을 복잡하게하기보다는 ...
supercat

3
... 모퉁이 사건이 정의되거나 정의되지 않은 것에 대해 극도의 세부 사항을 파악함으로써 표준 작성자는 구현자가 지원해야 할 프로그램의 종류에 따라 어떤 행동이 필요한지 판단 할 수 있도록 더 잘 진행해야 함을 인식했습니다. . 하이퍼 모더니스트 컴파일러는 UB가 특정 프로그램을 만드는 것은 양질의 프로그램이 필요하지 않다는 것을 암시하기위한 것이라고 주장하지만 표준과 이론적 근거는 그러한 의도와 일치하지 않습니다.
supercat

1
@ jrh : 나는 초 현대주의 철학이 어떻게 손에 닿지 않았는지 깨닫기 전에 그 대답을 썼다. "필요한 플랫폼은 어쨌든이를 지원할 수 있기 때문에 공식적으로이 동작을 인식 할 필요가 없습니다"에서 "이 동작은 인식되지 않았으므로 코드를 인식하지 못했기 때문에이 동작을 제거 할 수 있습니다." 그것을 필요로했다 " 모든 행동이 더 나아졌지 만 그들의 합법성을 인정해야하는 교체를 위해 많은 행동들이 오래 전에 더 이상 사용되지 않아야 했다.
supercat

78

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

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

$ 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 명령이 일종의 컴파일러 최적화라고 가정합니다.)


기계 코드는 어떻게 얻습니까? 나는 ++ 데브 C 사용하고 난 컴파일러 설정에서 '코드 생성'옵션으로 주위 재생,하지만 별도의 파일 출력 또는 콘솔 출력 갈
bad_keypoints

5
@ronnieaka gcc evil.c -c -o evil.bingdb evil.bindisassemble evil또는 Windows와 동등한 항목은 다음과 같습니다.)
badp

21
이 답변은의 문제를 실제로 다루지는 않습니다 Why are these constructs undefined behavior?.
Shafik Yaghmour

9
제쳐두고, 어셈블리로 컴파일하는 것이 더 쉬울 gcc -S evil.c것입니다. 조립 및 분해는 원형 교차로 방식입니다.
Kat

50
기록을 위해 어떤 이유에서든 주어진 구조가 무엇을하는지 궁금해하는 경우 , 특히 정의되지 않은 동작 일 수 있다는 의심이있는 경우 "컴파일러로 시도하고 참조하십시오"라는 오래된 조언은 다음과 같습니다. 잠재적으로 매우 위험합니다. 최선을 다해 학습합니다.이러한 상황에서 오늘날이 컴파일러 버전 . 당신은 할 수 없습니다 그것이 보장 기능에 대한 아무것도 할 경우 많은 것을 배운다. 일반적으로 "컴파일러로 시도해보십시오"는 컴파일러에서만 작동하는 이식 불가능한 프로그램으로 이어집니다.
Steve Summit

64

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

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

6.5.16 할당 연산자, §4 :

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


2
위의 내용은 'i = i = 5; "가 정의되지 않은 행동이라는 것을 의미합니까?
supercat

1
내가 아는 한 i=i=5
@supercat

2
@Zaibis : 대부분의 장소에서 사용하고 싶은 이론적 근거는 이론적으로 다중 프로세서 플랫폼이 A=B=5;"쓰기 잠금 A; 쓰기 잠금 B; 저장소 5-A; 저장소 5-B; 잠금 해제 B" 와 같은 것을 구현할 수 있음을 적용합니다 . ; Unock A; "및 C=A+B;"읽기 잠금 A; 읽기 잠금 B; 컴퓨팅 A + B; 잠금 해제 A 및 B; 쓰기 잠금 C; 저장 결과; 잠금 해제 C; " 와 같은 문장 . 그러면 한 스레드가 A=B=5;다른 스레드를 수행 한 경우 다른 스레드가 수행 C=A+B;한 경우 두 쓰기가 모두 발생한 것으로 보거나 둘 다 보지 못하게됩니다. 잠재적으로 유용한 보증입니다. 그러나 하나의 스레드가 I=I=5;...
supercat

1
... 그리고 컴파일러는 두 쓰기가 모두 같은 위치에 있다는 것을 알지 못했습니다 (하나 또는 두 개의 lvalue에 포인터가 포함되어 결정하기 어려울 수 있음). 생성 된 코드가 교착 상태에 빠질 수 있습니다. 실제 구현에서는 일반적인 동작의 일부로 이러한 잠금을 구현한다고 생각하지 않지만 표준에서는 허용되며 하드웨어가 그러한 동작을 저렴하게 구현할 수 있다면 유용 할 수 있습니다. 오늘날의 하드웨어에서 이러한 동작은 기본값으로 구현하기에는 너무 비싸지 만 항상 그렇게되는 것은 아닙니다.
supercat

1
@ supercat이지만 c99의 시퀀스 포인트 액세스 규칙만으로는 정의되지 않은 동작으로 선언하기에 충분하지 않습니까? 하드웨어가 기술적으로 구현할 수있는 것은 중요하지 않습니까?
dhein

55

여기에 나오는 대부분의 답변은 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를 호출한다.


52

지정되지 않은 동작정의되지 않은 동작을 모두 호출하기 때문에 동작을 설명 할 수 없으므로 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로서 :

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

그리고 메모 :

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


33

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

에 대한 첫 번째 조각 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 에있는 나머지 질문 도 참조하십시오 .


1
Undefined Behavior와 관련하여 다소 불쾌한 점 은 컴파일러의 99.9 % 가 더 이상 그렇지 않다는 것을 의미 하는 데 사용 하는 것이 안전 *p=(*q)++;하다는 if (p!=q) *p=(*q)++; else *p= __ARBITRARY_VALUE;것입니다. 하이퍼 현대 C (투입 무슨 상관하지 않는 코드를 나타내는의 표준 방법이 없지만 후자의 공식 같은 것을 쓰고 필요 *p) 이전합니다 (함께 제공하는 데 사용 효율 컴파일러의 수준을 달성 할 else수 있도록하기 위해 필요한 조항을 if컴파일러는 일부 최신 컴파일러에 필요한 것을 최적화 합니다).
supercat

@supercat 나는 이제 그런 종류의 최적화를 수행하기에 충분히 "똑똑한"컴파일러도 assert명령문 을 들여다 볼 수있을만큼 똑똑해야 프로그래머가 문제의 행을 간단한 것으로 선행 할 수 있다고 믿는다 assert(p != q). (물론,이 과정을 수강 <assert.h>하려면 디버그 버전이 아닌 버전에서 어설 션을 완전히 삭제하지 않고 __builtin_assert_disabled()컴파일러에서 알 수있는 코드로 변환 한 다음 코드를 생성하지 않는 것으로 바꾸어야합니다.)
Steve Summit

25

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

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 에서 정의되지 않은 행동의 다른 많은 변형을 이해하면 도움이 될 것입니다

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


이 시퀀스 int a = 10, b = 20, c = 30; printf("a=%d b=%d c=%d\n", (a = a + b + c), (b = b + b), (c = c + c));는 안정적인 동작을 제공하는 것으로 보입니다 (gcc v7.3.0에서 오른쪽에서 왼쪽으로의 인수 평가; 결과 "a = 110 b = 40 c = 60"). 과제가 '완전한 진술'로 간주되어 시퀀스 포인트가 도입 되었기 때문입니까? 그 결과로 왼쪽에서 오른쪽으로의 논쟁 / 문구 평가가 이루어져서는 안됩니까? 아니면 정의되지 않은 행동의 징후일까요?
kavadias

@kavadias printf 문은 위에서 설명한 것과 같은 이유로 정의되지 않은 동작을 포함합니다. 당신이 작성 b하고 c제 3 및 제 4 인수에 각각, 2 인수에 읽기. 그러나이 식 (2, 3, 4 번째 인수) 사이에는 시퀀스가 ​​없습니다. gcc / clang에는 -Wsequence-point이것들을 찾는데 도움 이되는 옵션 이 있습니다.
PP

23

컴파일러와 프로세서가 실제로 그렇게 할 가능성은 낮지 만 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

프로세서가 그러한 일을 효율적으로 수행 할 수 있도록 하드웨어를 지원한다고 생각하지는 않지만 그러한 행동으로 인해 멀티 스레드 코드를 더 쉽게 만드는 상황을 쉽게 상상할 수 있습니다 (예 : 두 스레드가 위의 작업을 수행하려고하면 보장 할 것입니다) 순서는 동시에 i2 씩 증가합니다) 그리고 미래의 일부 프로세서가 이와 같은 기능을 제공 할 수는 없습니다.

컴파일러가 i++위에 표시된대로 (표준에 따라 합법적으로) 작성하고 전체 표현의 평가를 통해 위의 지침을 산재하고 (법적) 다른 지침 중 하나가 발생했음을 알 수없는 경우 액세스 i하려면 컴파일러가 교착 상태에 빠질 수있는 일련의 명령어를 생성 할 수 있습니다. 확인하려면, 컴파일러는 거의 확실 같은 변수가 경우에 문제를 감지 할 i두 곳 모두에 사용됩니다,하지만 루틴은 두 개의 포인터 참조를 허용하는 경우 pq, 그리고 사용 (*p)(*q) 위의 식 (보다는 사용의를i두 번) 컴파일러는 동일한 객체의 주소가 둘 다에 전달되면 교착 상태를 인식하거나 피할 필요가 없습니다 .pq


16

그동안 구문 표현의 좋아하는 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;

다양한 시퀀스 포인트가 C11 (및 C99 ) 의 부록 C에 나열되어 있습니다 .

  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); while 또는 do 문의 제어 표현 (6.8.5); for 문 (6.8.5.3)의 각각의 (선택적) 표현; 리턴 명령문의 (선택 사항) 표현식 (6.8.6.4)
    • 라이브러리 함수가 (7.1.4)를 반환하기 직전에.
    • 형식화 된 각 입력 / 출력 기능 변환 지정자와 연관된 조치 후 (7.21.6, 7.29.2)
    • 비교 함수에 대한 각 호출 직전과 직후, 그리고 비교 함수에 대한 호출과 해당 호출에 대한 인수로 전달 된 객체의 이동 사이 (7.22.5).

C11 에서 동일한 단락 의 문구는 다음과 같습니다.

  1. 스칼라 오브젝트의 부작용이 동일한 스칼라 오브젝트의 다른 부작용 또는 동일한 스칼라 오브젝트의 값을 사용한 값 계산과 관련하여 순서가 지정되지 않으면 동작이 정의되지 않습니다. 표현식의 하위 표현식에 허용되는 순서가 여러 개인 경우 순서에 상관없이 이러한 부작용이 발생하면 동작이 정의되지 않습니다 .84)

예를 들어 -Walland 와 함께 최신 버전의 GCC를 사용하여 프로그램에서 이러한 오류를 감지 -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그러므로 모두 두번 수정 i++++i이전 및 다음 시퀀스 포인트 간의.


14

C 표준에 따르면 변수는 두 시퀀스 포인트 사이에 최대 한 번만 할당해야합니다. 예를 들어 세미콜론은 시퀀스 포인트입니다.
따라서 양식의 모든 진술 :

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

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

그러나 두 시퀀스 포인트간에 서로 다른 두 변수를 증분시킬 수 있습니다.

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

위의 내용은 문자열을 복사 / 분석하는 동안 일반적인 코딩 방법입니다.


물론 하나의 표현식 내에서 다른 변수에는 적용되지 않습니다. 만약 그렇다면 전체 설계 실패 일 것입니다! 두 번째 예제에서 필요한 것은 명령문 끝과 다음 시작 사이에 증가하는 것입니다.이 모든 것은 중심의 시퀀스 포인트 개념으로 인해 보장됩니다.
underscore_d

11

에서 /programming/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[]지난 다음, 읽은 ++ik[].

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

현대의 컴파일러는 이것을 잘 최적화합니다. 실제로, 원래 작성한 코드보다 더 좋을 수 있습니다 (원하는 방식으로 작동했다고 가정).


5

계산의이 종류에서 일어나는 일에 대해 좋은 설명은 문서에서 제공되는 n1188 로부터 는 ISO W14 사이트 .

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

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

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

식의 시퀀스 포인트 i=i++는 이전 i=과 이후 i++입니다.

위에서 인용 한 논문에서는 프로그램이 작은 상자로 구성되는 것으로 파악할 수 있다고 설명합니다. 각 상자에는 두 개의 연속적인 시퀀스 지점 사이의 명령이 들어 있습니다. 서열 점은 표준의 부속서 C에 정의되어 i=i++있으며, 완전한 발현을 한정하는 2 개의 서열 점이 있는 경우 . 이러한 표현은 문법 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 사이트 ( 도서로 도 게시 )의 항목, 즉 여기여기여기 입니다.


이 답변이 기존 답변에 어떻게 새로 추가 되었습니까? 또한 이에 대한 설명 i=i++이 답변 과 매우 유사합니다 .
haccks

@ haccks 나는 다른 답변을 읽지 못했습니다. 나는 ISO 9899의 공식 사이트에서 언급 한 문서에서 배운 것을 내 자신의 언어로 설명하고 싶었 open-std.org/jtc1/sc22/wg14/www/docs/n1188.pdf
alinsoar

5

귀하의 질문은 아마도 "왜 이러한 구조가 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++부분의 시도는, 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. 둘 이상의 다른 위치에서 수정 (할당)되는 변수가 하나 있으면 어떤 수정이 먼저 발생하는지 어떻게 알 수 있습니까?
  2. 한 곳에서 수정되고 다른 곳에서 값을 사용하는 변수가있는 경우 이전 값을 사용하는지 새 값을 사용하는지 어떻게 알 수 있습니까?

식에서 # 1의 예로서

x = x++ + ++x;

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

# 2의 예로서, 표현에서

y = x + x++;

우리는 모두의 값을 사용 x하고 수정합니다.

이것이 바로 답입니다. 작성한 표현식에서 각 변수는 최대 한 번 수정되고 변수가 수정 된 경우 해당 변수의 값을 다른 곳에서도 사용하려고 시도하지 마십시오.


3

그 이유는 프로그램이 정의되지 않은 동작을 실행하고 있기 때문입니다. 문제는 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로 할당 한 다음 마지막으로 모든 단계를 평가합니다. 한 번에 증가 후 표현식.

따라서 정의 된 동작 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 설명서가 명시되어 있습니다 .

여러 연산자가 함께 나타나는 경우 우선 순위가 같고 연관성에 따라 평가됩니다. 표의 연산자는 접미사 연산자로 시작하는 섹션에 설명되어 있습니다.


1
함수 인수를 평가할 때 UB를 추가하기 위해 질문을 편집했습니다.이 질문은 종종 복제본으로 사용되기 때문입니다. (마지막 예)
Antti Haapala

1
또한 질문은 C ++가 아니라 c 에 관한 것입니다
Antti Haapala
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.