널 인스턴스에서 멤버 함수를 호출하면 언제 정의되지 않은 동작이 발생합니까?


120

다음 코드를 고려하십시오.

#include <iostream>

struct foo
{
    // (a):
    void bar() { std::cout << "gman was here" << std::endl; }

    // (b):
    void baz() { x = 5; }

    int x;
};

int main()
{
    foo* f = 0;

    f->bar(); // (a)
    f->baz(); // (b)
}

(b)해당 멤버가 없기 때문에 충돌이 예상 됩니다.xnull 포인터에 . 실제로 포인터가 사용되지 (a)않기 때문에 충돌하지 않습니다 this.

(b)역 참조 하기 때문에this 포인터 ( (*this).x = 5;), 그리고 this널은 널 (null)를 역 참조 항상 정의되지 않은 동작이라고합니다으로,이 프로그램은 정의되지 않은 동작에 들어갑니다.

않습니다 (a)정의되지 않은 동작이 발생할? 두 함수 (및 x)가 모두 정적 이면 어떨까요?


두 함수가 모두 정적 인 경우 어떻게 x가 baz 내부에서 참조 될 수 있습니까? (x는 비 정적 멤버 변수)
legends2k

4
@ legends2k : 척도 x정적으로 만들어졌습니다. :)
GManNickG

물론 (a)의 경우 모든 경우에서 동일하게 작동합니다. 즉, 함수가 호출됩니다. 그러나 포인터 값을 0에서 1로 바꾸면 (예 : reinterpret_cast를 통해) 거의 항상 충돌합니다. a의 경우와 같이 0의 값 할당과 따라서 NULL이 컴파일러에게 특별한 것을 나타 냅니까? 할당 된 다른 값과 함께 항상 충돌하는 이유는 무엇입니까?
Siddharth Shankaran

5
흥미로운 점 : C ++의 다음 개정판이 나오면 더 이상 포인터를 역 참조하지 않을 것입니다. 이제 포인터를 통해 간접 지정수행 합니다. 자세한 내용은 다음 링크를 통해 간접 참조하십시오. N3362
James McNellis 2012-06-05

3
널 포인터에서 멤버 함수를 호출하는 것은 항상 정의되지 않은 동작입니다. 당신의 코드를 보는 것만으로도 정의되지 않은 동작이 천천히 목을 기어 오는 것을 느낄 수 있습니다!
fredoverflow

답변:


113

모두 (a)(b)정의되지 않은 동작이 발생할. 널 포인터를 통해 멤버 함수를 호출하는 것은 항상 정의되지 않은 동작입니다. 함수가 정적 인 경우 기술적으로도 정의되지 않지만 일부 분쟁이 있습니다.


가장 먼저 이해해야 할 것은 널 포인터를 역 참조하는 것이 정의되지 않은 동작 인 이유입니다. C ++ 03에서는 실제로 여기에 약간의 모호성이 있습니다.

비록 "정의되지 않은 동작에 널 포인터 결과를 역 참조" §1.9 / 4, §8.3.2 / 4 모두 노트에 언급, 그것은 명시 적으로 언급 한 적이 없어요. (참고는 비표준입니다.)

그러나 §3.10 / 2에서 추론 할 수 있습니다.

lvalue는 개체 또는 함수를 나타냅니다.

역 참조 할 때 결과는 lvalue입니다. 널 포인터 객체를 참조 lvalue를 사용할 때 정의되지 않은 동작이 있습니다. 문제는 이전 문장이 결코 언급되지 않았다는 것입니다. 그래서 lvalue를 "사용"한다는 것은 무엇을 의미합니까? 아예 생성하거나 lvalue에서 rvalue로 변환하는보다 형식적인 의미에서 사용 하시겠습니까?

어쨌든 rvalue (§4.1 / 1)로 변환 할 수는 없습니다.

lvalue가 참조하는 객체가 T 유형의 객체가 아니고 T에서 파생 된 유형의 객체가 아니거나 객체가 초기화되지 않은 경우이 변환을 필요로하는 프로그램은 정의되지 않은 동작을 갖습니다.

여기에는 확실히 정의되지 않은 동작이 있습니다.

모호성은 정의되지 않은 동작인지 여부에서 비롯 되지만 유효하지 않은 포인터의 값을 사용 하지 않습니다 (즉, lvalue를 가져 오지만 rvalue로 변환하지 않음). 그렇지 않다면 int *i = 0; *i; &(*i);잘 정의되어 있습니다. 이것은 활발한 문제 입니다.

따라서 우리는 엄격한 "널 포인터 역 참조, 정의되지 않은 동작 가져 오기"뷰와 약한 "비 참조 된 널 포인터 사용, 정의되지 않은 동작 얻기"뷰가 있습니다.

이제 우리는 질문을 고려합니다.


예, (a)정의되지 않은 동작이 발생합니다. 사실 this이 null이면 함수내용에 관계없이 결과는 정의되지 않습니다.

이것은 §5.2.5 / 3에서 다음과 같습니다.

E1"클래스 X에 대한 포인터"유형이있는 경우 표현식 E1->E2은 동등한 형식으로 변환됩니다.(*(E1)).E2;

*(E1)엄격한 해석으로 정의되지 않은 동작이 발생 .E2하고 rvalue로 변환되어 약한 해석에 대해 정의되지 않은 동작이됩니다.

또한 (§9.3.1 / 1)에서 직접 정의되지 않은 동작입니다.

X 유형이 아니거나 X에서 파생 된 유형의 객체에 대해 클래스 X의 비 정적 멤버 함수가 호출되면 동작이 정의되지 않습니다.


정적 함수를 사용하면 엄격한 해석과 약한 해석이 차이를 만듭니다. 엄밀히 말하면 정의되지 않았습니다.

정적 멤버는 클래스 멤버 액세스 구문을 사용하여 참조 될 수 있으며,이 경우 object-expression이 평가됩니다.

즉, 정적이 아닌 것처럼 평가되고 다시 한 번 (*(E1)).E2.

그러나 E1정적 멤버 함수 호출에서 사용되지 않기 때문에 약한 해석을 사용하면 호출이 잘 정의됩니다. *(E1)결과가 lvalue이고 정적 함수가 해결되고 *(E1)삭제되고 함수가 호출됩니다. lvalue에서 rvalue 로의 변환이 없으므로 정의되지 않은 동작이 없습니다.

C ++ 0x에서는 n3126부터 모호성이 남아 있습니다. 지금은 안전합니다 : 엄격한 해석을 사용하십시오.


5
+1. 계속해서 "약한 정의"에서 비 정적 멤버 함수는 "X 유형이 아닌 객체에 대해"호출되지 않았습니다. 전혀 객체가 아닌 lvalue에 대해 호출되었습니다. 따라서 제안 된 솔루션은 "또는 lvalue가 빈 lvalue 인 경우"라는 텍스트를 인용하는 절에 추가합니다.
Steve Jessop

좀 더 설명해 주시겠습니까? 특히, "종료 된 문제"및 "활성 문제"링크에서 문제 번호는 무엇입니까? 또한 이것이 닫힌 문제인 경우 정적 함수에 대한 예 / 아니요 대답은 정확히 무엇입니까? 답변을 이해하려는 마지막 단계를 놓친 것 같습니다.
Brooks Moses

4
나는 CWG 결함 315가 "종료 된 문제"페이지에 존재한다는 것을 의미하는 것처럼 "종료"되었다고 생각하지 않습니다. 근거는 " lvalue가 rvalue로 변환되지 않는 한 null 일 *p때 오류가 아니기 때문에 허용되어야한다고 말합니다 p." 그러나 이는 CWG 결함 (232) 에 대한 제안 된 해결책의 일부 이지만 채택되지 않은 "빈 값"의 개념에 의존합니다 . 따라서 C ++ 03 및 C ++ 0x의 언어를 사용하면 lvalue에서 rvalue 로의 변환이 없더라도 널 포인터 역 참조는 여전히 정의되지 않습니다.
James McNellis 2010 년

1
@JamesMcNellis : 내 이해에 따르면, p읽을 때 일부 동작을 트리거하는 하드웨어 주소 였지만 선언 되지 않은 경우, 해당 주소를 실제로 읽는 데 volatile*p;이 필요 하지는 않지만 허용 됩니다. &(*p);그러나이 진술 은 그렇게하는 것이 금지됩니다. 경우 *p였다 volatile읽기가 요구 될 것이다. 두 경우 모두 포인터가 유효하지 않으면 첫 번째 문이 Undefined Behavior가 아닌 방법을 알 수 없지만 두 번째 문이 왜 그런지 알 수 없습니다.
supercat 2014 년

1
".E2는 그것을 rvalue로 변환합니다."-어, 아닙니다
MM

30

물론 그것의 의미 정의되지 않은 정의되지를 하지만, 경우에 따라서는 예측 될 수 있습니다. 내가 제공하려는 정보는 확실히 보장되지 않기 때문에 작업 코드에 의존해서는 안되지만 디버깅 할 때 유용 할 수 있습니다.

객체 포인터에서 함수를 호출하면 포인터가 역 참조되고 UB가 발생한다고 생각할 수 있습니다. 함수가 가상이 아닌 경우 실제로, 컴파일러는 첫 번째 매개 변수로 포인터를 전달하는 일반 함수 호출로 변환 한 것 참조 해제를 우회하고 호출 된 멤버 함수에 대한 시한 폭탄을 만들어. 멤버 함수가 멤버 변수 나 가상 함수를 참조하지 않으면 실제로 오류없이 성공할 수 있습니다. 성공은 "정의되지 않은"우주에 속한다는 것을 기억하십시오!

Microsoft의 MFC 함수 GetSafeHwnd는 실제로이 동작에 의존합니다. 나는 그들이 무엇을 피우고 있었는지 모릅니다.

가상 함수를 호출하는 경우 vtable에 도달하기 위해 포인터를 역 참조해야하며 확실히 UB를 얻을 것입니다 (아마도 충돌이 발생하지만 보장 할 수 없음을 기억하십시오).


1
GetSafeHwnd는 먼저! this 검사를 수행하고 참이면 NULL을 리턴합니다. 그런 다음 SEH 프레임을 시작하고 포인터를 역 참조합니다. 메모리 액세스 위반 (0xc0000005)이있는 경우이를 포착하고 호출자에게 NULL을 반환합니다. :) 그렇지 않으면 HWND가 반환됩니다.
Петър Петров

@ ПетърПетров에 대한 코드를 살펴본 지 꽤 몇 년이 지났으며 그 이후로 코드를 GetSafeHwnd향상 시켰을 가능성이 있습니다. 그리고 그들이 컴파일러 작동에 대한 내부 지식을 가지고 있다는 것을 잊지 마십시오!
Mark Ransom 2014 년

동일한 효과를 가진 가능한 구현 샘플을 언급하고 있습니다. 실제로하는 일은 디버거를 사용하여 리버스 엔지니어링되는 것입니다. :)
Петър Петров

1
"그들은 컴파일러 작동에 대한 내부 지식을 가지고 있습니다!" -g ++에서 Windows API를 호출하는 코드를 컴파일하도록 허용하려는 MinGW와 같은 프로젝트의 영원한 문제의 원인
MM

@MM 나는 이것이 불공평하다는 데 모두 동의 할 것이라고 생각합니다. 그 때문에 호환성에 관한 법률이있어이를 유지하는 것이 조금 불법이라고 생각합니다.
v.oddou
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.