C ++ 프로그래머가 알아야 할 일반적인 정의되지 않은 동작은 무엇입니까? [닫은]


201

C ++ 프로그래머가 알아야 할 일반적인 정의되지 않은 동작은 무엇입니까?

다음과 같이 말합니다.

a[i] = i++;


3
확실합니까. 잘 정의 된 것 같습니다.
Martin York

17
6.2.2 C ++ 프로그래밍 언어의 평가 순서 [expr.evaluation] 다른 말이 없습니다
yesraaj

4
그는 바로 .. 단지 = 내가 ++ 정의되지 않는다 [I] 언어 프로그래밍의 C 6.2.2 ++ 보았다 그것은 V 말한다 것
dancavallaro

4
컴파일러가 v [i]의 메모리 위치를 계산하기 전이나 후에 i ++를 실행하기 때문에 상상할 수 있습니다. 물론, 나는 항상 그곳에 배정 될 것입니다. 그러나 어느 V 쓸 수 [I] V [내가 + 1] ... 작업 순서에 따라 또는
에반 테란

2
C ++ 프로그래밍 언어는 "표현식 내 하위 표현식의 연산 순서는 정의되어 있지 않습니다. 특히 표현식이 왼쪽에서 오른쪽으로 평가된다고 가정 할 수 없습니다"라고 말합니다.
dancavallaro

답변:


233

바늘

  • NULL포인터 역 참조
  • 크기가 0 인 "새"할당에 의해 반환 된 포인터 역 참조
  • 수명이 종료 된 객체에 대한 포인터 사용 (예 : 스택 할당 객체 또는 삭제 된 객체)
  • 아직 초기화되지 않은 포인터 역 참조
  • 배열의 경계 (위 또는 아래)를 벗어난 결과를 생성하는 포인터 산술을 수행합니다.
  • 배열 끝을 넘는 위치에서 포인터를 역 참조합니다.
  • 호환되지 않는 유형의 객체로 포인터 변환
  • memcpy겹치는 버퍼 복사에 사용 .

버퍼 오버 플로우

  • 음의 오프셋 또는 해당 객체의 크기를 초과하는 오프셋에서 객체 또는 배열에 대한 읽기 또는 쓰기 (스택 / 힙 오버플로)

정수 오버플로

  • 부호있는 정수 오버플로
  • 수학적으로 정의되지 않은 표현식 평가
  • 음수로 왼쪽 이동 값 (음수로 오른쪽 이동은 구현이 정의 됨)
  • 숫자의 비트 수보다 크거나 같은 양만큼 값 이동 (예 : int64_t i = 1; i <<= 72정의되지 않음)

유형, 캐스트 및 콘 스트

  • 대상 유형으로 표시 할 수없는 값으로 숫자 값 캐스트 (직접 또는 static_cast를 통해)
  • 자동 변수를 지정하기 전에 자동 변수 사용 (예 : int i; i++; cout << i; )
  • volatile또는 이외의 유형의 객체 값 사용sig_atomic_t신호를 수신 수신
  • 수명 동안 문자열 리터럴 또는 다른 const 객체를 수정하려고 시도
  • 전처리 과정에서 폭이 넓은 문자열 리터럴로 좁은 연결

기능 및 템플릿

  • 값 반환 함수에서 값을 반환하지 않음 (직접 또는 try-block에서 흘러 나옴)
  • 동일한 엔터티에 대한 여러 가지 다른 정의 (클래스, 템플릿, 열거, 인라인 함수, 정적 멤버 함수 등)
  • 템플릿 인스턴스화에서 무한 재귀
  • 다른 매개 변수를 사용하여 함수 호출 또는 함수가 사용하는 것으로 정의 된 매개 변수 및 링크에 대한 링크.

죄송합니다

  • 정적 저장 시간을 갖는 객체의 계단식 파괴
  • 부분적으로 겹치는 객체에 할당 한 결과
  • 정적 객체를 초기화하는 동안 재귀 적으로 함수를 다시 입력
  • 생성자 또는 소멸자에서 객체의 순수한 가상 함수에 대한 가상 함수 호출
  • 구성되지 않았거나 이미 파괴 된 객체의 비 정적 멤버 참조

소스 파일 및 전처리

  • 개행으로 끝나지 않거나 백 슬래시로 끝나는 비어 있지 않은 소스 파일 (C ++ 11 이전)
  • 문자 또는 문자열 상수에서 지정된 이스케이프 코드의 일부가 아닌 문자 뒤에 오는 백 슬래시 (C ++ 11에서 구현 정의 됨).
  • 구현 제한 초과 (중첩 된 블록 수, 프로그램의 함수 수, 사용 가능한 스택 공간 ...)
  • 로 표현할 수없는 전 처리기 숫자 값 long int
  • 함수형 매크로 정의의 왼쪽에있는 전처리 지시문
  • #if표현식 에서 정의 된 토큰을 동적으로 생성

분류되기

  • 정적 저장 기간을 가진 프로그램을 파괴하는 동안 호출 종료

흠 ... NaN (x / 0) 및 무한대 (0 / 0)는 IEE 754에 의해 다루어졌습니다. 만약 C ++가 나중에 디자인 되었다면 왜 x / 0을 정의되지 않은 것으로 기록합니까?
새로운 123456

Re : "백 슬래시 뒤에 문자 나 문자열 상수에서 지정된 이스케이프 코드의 일부가 아닌 문자가옵니다." C89 (§3.1.3.4) 및 C ++ 03 (C89 포함)의 UB이지만 C99에는 없습니다. C99는 "결과는 토큰이 아니며 진단이 필요합니다"(§6.4.4.4)라고 말합니다. 아마도 C ++ 0x (C89를 통합)는 동일 할 것입니다.
Adam Rosenfield 2018 년

1
C99 표준에는 부록 J.2에 정의되지 않은 동작 목록이 있습니다. 이 목록을 C ++에 적용하려면 약간의 작업이 필요합니다. C99 절이 아닌 올바른 C ++ 절에 대한 참조를 변경하고 관련이없는 것을 제거하고 C뿐만 아니라 C ++에서도 실제로 정의되지 않은 모든 항목을 확인해야합니다. 그러나 시작을 제공합니다.
Steve Jessop

1
@ new123456-모든 부동 소수점 단위가 IEE754와 호환되는 것은 아닙니다. C ++에 IEE754 준수가 필요한 경우 컴파일러는 명시 적 검사를 통해 RHS가 0 인 경우를 테스트하고 처리해야합니다. 동작을 정의하지 않으면 컴파일러는 "IEEE754가 아닌 FPU를 사용하지 않으면 IEEE754 FPU 동작을 얻지 못한다"고 말함으로써 오버 헤드를 피할 수 있습니다.
SecurityMatt

1
"결과가 해당 유형의 범위에없는 표현식 평가".... 정수 오버 플로우는 부호없는 정수 유형이 아닌 UNSIGNED 정수 유형에 대해 잘 정의되어 있습니다.
nacitar sevaht

31

함수 매개 변수가 평가되는 순서는 지정되지 않은 동작 입니다. (이것은 정의되지 않은 동작 과 달리 프로그램이 충돌하거나 폭발하거나 피자를 주문하지 않습니다. .)

유일한 요구 사항은 함수를 호출하기 전에 모든 매개 변수를 완전히 평가해야한다는 것입니다.


이:

// The simple obvious one.
callFunc(getA(),getB());

이것과 동등 할 수 있습니다 :

int a = getA();
int b = getB();
callFunc(a,b);

아니면 이거:

int b = getB();
int a = getA();
callFunc(a,b);

둘 중 하나 일 수 있습니다. 컴파일러에 달려 있습니다. 부작용에 따라 결과가 중요 할 수 있습니다.


23
순서는 지정되지 않았으며 정의되지 않았습니다.
Rob Kennedy

1
나는이 하나 :) 내가 ... 이러한 경우 중 하나 추적하면 일의 하루를 잃어 어쨌든 내 교훈을 싫어 다행히 다시 하락하지 않은
로버트 굴드를

2
@Rob : 여기서 의미의 변화에 ​​대해 여러분과 논쟁 할 것이지만, 표준위원회가이 두 단어의 정확한 정의에 매우 까다 롭다는 것을 알고 있습니다. 난 그냥 그것을 바꿀거야 그래서 :-)
마틴 뉴욕

2
나는 이것에 운이 좋았다. 나는 대학에있을 때 물 렸고 교수님이 한 번 보며 약 5 초 만에 제 문제를 말해주었습니다. 그렇지 않으면 디버깅에 얼마나 많은 시간을 낭비했는지 알 수 없습니다.
Bill the Lizard

27

컴파일러는 표현식의 평가 부분을 자유롭게 재정렬 할 수 있습니다 (의미가 변경되지 않은 경우).

원래 질문에서 :

a[i] = i++;

// This expression has three parts:
(a) a[i]
(b) i++
(c) Assign (b) to (a)

// (c) is guaranteed to happen after (a) and (b)
// But (a) and (b) can be done in either order.
// See n2521 Section 5.17
// (b) increments i but returns the original value.
// See n2521 Section 5.2.6
// Thus this expression can be written as:

int rhs  = i++;
int lhs& = a[i];
lhs = rhs;

// or
int lhs& = a[i];
int rhs  = i++;
lhs = rhs;

이중 점검 잠금. 그리고 하나의 쉬운 실수가 있습니다.

A* a = new A("plop");

// Looks simple enough.
// But this can be split into three parts.
(a) allocate Memory
(b) Call constructor
(c) Assign value to 'a'

// No problem here:
// The compiler is allowed to do this:
(a) allocate Memory
(c) Assign value to 'a'
(b) Call constructor.
// This is because the whole thing is between two sequence points.

// So what is the big deal.
// Simple Double checked lock. (I know there are many other problems with this).
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        a = new A("Plop");  // (Point A).
    }
}
a->doStuff();

// Think of this situation.
// Thread 1: Reaches point A. Executes (a)(c)
// Thread 1: Is about to do (b) and gets unscheduled.
// Thread 2: Reaches point B. It can now skip the if block
//           Remember (c) has been done thus 'a' is not NULL.
//           But the memory has not been initialized.
//           Thread 2 now executes doStuff() on an uninitialized variable.

// The solution to this problem is to move the assignment of 'a'
// To the other side of the sequence point.
if (a == null) // (Point B)
{
    Lock   lock(mutex);
    if (a == null)
    {
        A* tmp = new A("Plop");  // (Point A).
        a = tmp;
    }
}
a->doStuff();

// Of course there are still other problems because of C++ support for
// threads. But hopefully these are addresses in the next standard.

시퀀스 포인트는 무엇을 의미합니까?
yesraaj


1
Ooh .. 특히 자바에서 권장하는 정확한 구조를 본 이후로 불쾌합니다.
Tom

일부 컴파일러는이 상황에서 동작을 정의합니다. 예를 들어, VC ++ 2005+에서 a가 휘발성 인 경우 필요한 순서가 변경되어 명령 순서 변경을 방지하여 이중 검사 잠금이 작동합니다.
Eclipse

마틴 요크 : <i> // (c)는 (a)와 (b) </ i> 이후에 발생할 수 있습니다. 이 특정 예에서 'i'가 하드웨어 레지스터에 매핑 된 휘발성 변수이고 a [i] (이전 값 'i')가 별명 인 경우에만 문제가 될 수 있습니다. 시퀀스 포인트 전에 증분이 발생한다고 보장합니까?
supercat

5

필자가 가장 좋아하는 것은 "템플릿 인스턴스화의 무한 재귀"입니다. 컴파일 타임에 정의되지 않은 동작이 발생하는 유일한 이유라고 생각하기 때문입니다.


전에이 작업을 수행했지만 어떻게 정의되지 않았는지 알 수 없습니다. 나중에 당신이 무한 재귀를하는 것이 분명합니다.
로버트 굴드

문제는 컴파일러가 코드를 검사 할 수없고 무한 재귀로 고통 받을지 여부를 정확하게 결정할 수 없다는 것입니다. 중지 문제의 예입니다. 다음을 참조하십시오 : stackoverflow.com/questions/235984/…
Daniel Earwicker 08.

그래, 그야말로 그만두는 문제
Robert Gould

너무 적은 메모리로 인한 스와핑으로 인해 시스템이 다운되었습니다.
Johannes Schaub-litb

2
int에 맞지 않는 전 처리기 상수도 컴파일 시간입니다.
Joshua

5

다음을 const사용하여 벗겨짐 후 상수에 할당 const_cast<>:

const int i = 10; 
int *p =  const_cast<int*>( &i );
*p = 1234; //Undefined

5

undefined behavior 이외에도 구현이 정의 되지 않은 동작있습니다. .

정의되지 않은 동작은 프로그램이 결과가 표준에 의해 지정되지 않은 작업을 수행 할 때 발생합니다.

구현-정의 된 행동은 결과가 표준에 의해 정의되지는 않지만 구현이 문서화되어야하는 프로그램에 의한 행동이다. 예를 들어, 스택 오버플로 질문의 "멀티 바이트 문자 리터럴"입니다. 컴파일에 실패한 C 컴파일러가 있습니까? .

구현 정의 동작은 이식을 시작할 때만 물지 만 (새로운 버전의 컴파일러로 업그레이드하는 것도 이식 중입니다!)


4

변수는 표현식에서 한 번만 (기술적으로 시퀀스 포인트 사이에서 한 번) 업데이트 될 수 있습니다.

int i =1;
i = ++i;

// Undefined. Assignment to 'i' twice in the same expression.

사실 두 시퀀스 포인트 사이 에 적어도 한 번은 있습니다.
Prasoon Saurav

2
@Prasoon : 나는 당신이 의미한다고 생각합니다. 최대 두 번의 시퀀스 포인트 사이에 있습니다. :-)
Nawaz 2016 년

3

다양한 환경 한계에 대한 기본 이해. 전체 목록은 C 사양의 5.2.4.1 섹션에 있습니다. 여기 몇 가지가 있습니다.

  • 하나의 기능 정의에서 127 개의 파라미터
  • 한 함수 호출에서 127 개의 인수
  • 하나의 매크로 정의에서 127 개의 매개 변수
  • 한 번의 매크로 호출에서 127 개의 인수
  • 논리 소스 라인의 4095 자
  • 문자열 리터럴 또는 와이드 문자열 리터럴의 4095 자 (연결 후)
  • 객체의 65535 바이트 (호스트 환경에서만)
  • #includedfiles의 15nesting 레벨
  • 스위치 명령문에 대한 1023 케이스 레이블 (중첩 스위치 명령문에 대한 케이스 레이블 제외)

실제로 switch 문에 대한 1023 케이스 레이블의 한계에 약간 놀랐습니다. 생성 된 코드 / 렉스 / 파서가 상당히 쉽게 초과되는 것을 알 수 있습니다.

이러한 제한을 초과하면 정의되지 않은 동작 (크래쉬, 보안 결함 등)이 있습니다.

맞습니다. 이것이 C 사양에 의한 것임을 알고 있지만 C ++은 이러한 기본 지원을 공유합니다.


9
이 한계에 도달하면 정의되지 않은 동작보다 더 많은 문제가 있습니다.
new123456

STD :: vector와 같은 객체에서 65535 바이트를 쉽게 초과 할 수 있습니다.
Demi

2

memcpy겹치는 메모리 영역간에 복사하는 데 사용 합니다. 예를 들면 다음과 같습니다.

char a[256] = {};
memcpy(a, a, sizeof(a));

C ++ 03 표준에 따라 C 표준에 따라 동작이 정의되지 않습니다.

7.21.2.1 memcpy 함수

개요

1 / #include void * memcpy (void * 제한 s1, const void * 제한 s2, size_t n);

기술

2 / memcpy 함수는 s2가 가리키는 오브젝트에서 s1이 가리키는 오브젝트로 n 개의 문자를 복사합니다. 겹치는 객체간에 복사가 수행되면 동작이 정의되지 않습니다. 반환 값 3 memcpy 함수는 s1의 값을 반환합니다.

7.21.2.2 memmove 기능

개요

1 #include void * memmove (void * s1, const void * s2, size_t n);

기술

2 memmove 함수는 s2가 가리키는 객체에서 s1이 가리키는 객체로 n 개의 문자를 복사합니다. s2가 가리키는 객체의 n 문자가 s1 및 s2가 가리키는 객체와 겹치지 않는 n 문자의 임시 배열로 먼저 복사 된 다음 임시 배열의 n 문자가 복사되는 것처럼 복사가 수행됩니다. s1이 가리키는 객체 보고

3 memmove 함수는 s1의 값을 반환합니다.


2

C ++에서 크기를 보장하는 유일한 유형은 char입니다. 크기는 1입니다. 다른 모든 유형의 크기는 플랫폼에 따라 다릅니다.


<cstdint>의 목적이 아닙니까? uint16_6 등의 유형을 정의합니다.
Jasper Bekkers

예, 그러나 대부분의 유형의 크기, 예를 들어 long은 잘 정의되어 있지 않습니다.
JaredPar

또한 cstdint는 현재 C ++ 표준의 일부가 아닙니다. 현재 휴대용 솔루션에 대해서는 boost / stdint.hpp를 참조하십시오.
Evan Teran

그것은 정의되지 않은 행동이 아닙니다. 표준에 따르면 적합한 플랫폼은 크기를 정의하는 표준이 아니라 크기를 정의합니다.
Daniel Earwicker 8

1
@JaredPar : 많은 대화 스레드가있는 복잡한 게시물이므로 여기에 요약했습니다 . 결론은 다음과 같습니다. "5. -2147483647 및 +2147483647을 이진수로 나타내려면 32 비트가 필요합니다."
John Dibling

2

다른 컴파일 단위의 네임 스페이스 수준 개체는 초기화 순서가 정의되어 있지 않으므로 초기화에 서로 의존해서는 안됩니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.