문제

저는 C++로 실험 중이었고 아래 코드가 매우 이상하다는 것을 발견했습니다.

class Foo{
public:
    virtual void say_virtual_hi(){
        std::cout << "Virtual Hi";
    }

    void say_hi()
    {
        std::cout << "Hi";
    }
};

int main(int argc, char** argv)
{
    Foo* foo = 0;
    foo->say_hi(); // works well
    foo->say_virtual_hi(); // will crash the app
    return 0;
}

나는 vtable 조회가 필요하고 유효한 개체에 대해서만 작동할 수 있기 때문에 가상 메서드 호출이 충돌한다는 것을 알고 있습니다.

나는 다음과 같은 질문이 있습니다

  1. 가상이 아닌 방법은 어떻게 작동합니까? say_hi NULL 포인터 작업을 하시나요?
  2. 물체는 어디에 있습니까? foo 배정받나요?

이견있는 사람?

도움이 되었습니까?

해결책

그 물체 foo 유형이있는 로컬 변수입니다 Foo*. 그 변수는 스택에 할당 될 것입니다. main 다른 로컬 변수와 마찬가지로 기능. 하지만 저장 foo 널 포인터입니다. 그것은 아무데도 가리키지 않습니다. 유형 인스턴스가 없습니다 Foo 어디서나 대표됩니다.

가상 함수를 호출하려면 발신자는 기능이 호출되는 객체를 알아야합니다. 객체 자체가 실제로 어떤 함수를 호출 해야하는지 알려주기 때문입니다. (이는 객체에 Vtable, 함수 포인터 목록에 대한 포인터를 제공함으로써 자주 구현되며 발신자는 포인터가 어디에 포인트를 미리 알지 못하고 목록에서 첫 번째 함수를 호출해야한다는 것을 알고 있습니다.)

그러나 비 약동적 기능을 호출하기 위해 발신자는 모든 것을 알 필요가 없습니다. 컴파일러는 어떤 함수가 호출 될지 정확히 알고 있으므로 CALL 머신 코드 명령어 원하는 기능으로 직접 이동합니다. 그것은 단순히 함수가 함수에 숨겨진 매개 변수로 호출 된 객체에 대한 포인터를 전달합니다. 다시 말해, 컴파일러는 기능 호출을 다음과 같이 변환합니다.

void Foo_say_hi(Foo* this);

Foo_say_hi(foo);

이제 해당 기능의 구현은 그 객체의 멤버를 결코 언급하지 않기 때문에 this 논쟁, 당신은 널 포인터를 불러 일으키는 총알을 효과적으로 피합니다.

공식적으로 전화 어느 널 포인터에서 기능 (비가 적은 것조차도)은 정의되지 않은 동작입니다. 정의되지 않은 동작의 허용 결과 중 하나는 코드가 의도 한대로 정확하게 실행되는 것으로 보입니다. 컴파일러 공급 업체에서 라이브러리를 찾을 수도 있지만 이에 의존해서는 안됩니다. 하다 그것에 의존합니다. 그러나 컴파일러 공급 업체는 정의되지 않은 동작에 대한 추가 정의를 추가 할 수 있다는 이점이 있습니다. 직접하지 마십시오.

다른 팁

그만큼 say_hi() 멤버 기능은 일반적으로 컴파일러 AS에서 구현됩니다

void say_hi(Foo *this);

회원에 액세스하지 않으므로 전화가 성공합니다 (표준에 따라 정의되지 않은 동작을 입력하더라도).

Foo 전혀 할당되지 않습니다.

널리 포인터를 사용하지 않으면 "정의되지 않은 동작"이 발생합니다. 이는 모든 일이 발생할 수 있음을 의미합니다. 코드가 올바르게 작동하는 것처럼 보일 수 있습니다. 그러나 이에 의존해서는 안됩니다. 다른 플랫폼 (또는 같은 플랫폼에서 동일한 코드)에서 동일한 코드를 실행하면 충돌이 발생할 수 있습니다.

코드에는 foo 객체가 없으며 값 NULL과 비교되는 포인터 만 있습니다.

정의되지 않은 행동입니다. 그러나 대부분의 컴파일러는 멤버 변수 및 가상 테이블에 액세스하지 않으면이 상황을 올바르게 처리 할 수있는 지침을 작성했습니다.

Visual Studio에서 분해를 보려면 무슨 일이 일어나는지 이해하십시오.

   Foo* foo = 0;
004114BE  mov         dword ptr [foo],0 
    foo->say_hi(); // works well
004114C5  mov         ecx,dword ptr [foo] 
004114C8  call        Foo::say_hi (411091h) 
    foo->say_virtual_hi(); // will crash the app
004114CD  mov         eax,dword ptr [foo] 
004114D0  mov         edx,dword ptr [eax] 
004114D2  mov         esi,esp 
004114D4  mov         ecx,dword ptr [foo] 
004114D7  mov         eax,dword ptr [edx] 
004114D9  call        eax  

볼 수 있듯이 Foo : Say_hi는 일반적인 기능으로 불려지지만 이것 ECX 레지스터에서. 단순화하기 위해 당신은 그것을 가정 할 수 있습니다 이것 우리가 당신의 예에서는 결코 사용하지 않는 암시 적 매개 변수로 전달되었습니다.
그러나 두 번째 경우 우리는 가상 테이블에 대한 기능의 접착제를 계산하는 것을 계산합니다.

a) 암시적 "this" 포인터를 통해 어떤 것도 역참조하지 않기 때문에 작동합니다.그렇게하자마자 붐.100% 확신할 수는 없지만 널 포인터 역참조는 메모리 공간의 처음 1K를 보호하는 RW에 의해 수행된다고 생각하므로 1K 라인을 지나서만 역참조하면 널 참조가 포착되지 않을 가능성이 적습니다(예:다음과 같이 매우 멀리 할당되는 일부 인스턴스 변수:

 class A {
     char foo[2048];
     int i;
 }

그러면 A->i는 A가 null일 때 포착되지 않을 수 있습니다.

b) 어디에도 main():s 스택에 할당되는 포인터만 선언했습니다.

say_hi에 대한 호출은 정적으로 바인딩됩니다. 따라서 컴퓨터는 실제로 단순히 함수에 대한 표준 호출을 수행합니다. 함수는 필드를 사용하지 않으므로 문제가 없습니다.

Virtual_Say_Hi 로의 호출은 동적으로 바인딩되므로 프로세서는 가상 테이블로 이동하고 가상 테이블이 없으므로 무작위로 점프하여 프로그램이 충돌합니다.

그것을 실현하는 것이 중요합니다 둘 다 전화는 정의되지 않은 행동을 일으키고 그 행동은 예기치 않은 방식으로 나타날 수 있습니다. 전화하더라도 나타납니다 일하기 위해서는 지뢰밭을 내려 놓을 수 있습니다.

이 작은 변화를 예를 들어 고려하십시오.

Foo* foo = 0;
foo->say_hi(); // appears to work
if (foo != 0)
    foo->say_virtual_hi(); // why does it still crash?

첫 번째 전화 이후 foo 정의되지 않은 동작을 가능하게합니다 foo NULL, 컴파일러는 이제 자유롭게 가정 할 수 있습니다. foo ~이다 ~ 아니다 없는. 그것은 그것을 만듭니다 if (foo != 0) 중복 및 컴파일러는 최적화 할 수 있습니다! 이것이 매우 무의미한 최적화라고 생각할 수도 있지만, 컴파일러 작가는 매우 공격적이며 이와 같은 일은 실제 코드에서 발생했습니다.

C ++의 원래 일에서 C ++ 코드는 C로 변환되었습니다. 객체 방법은 이와 같은 비 객체 방법으로 변환됩니다 (귀하의 경우).

foo_say_hi(Foo* thisPtr, /* other args */) 
{
}

물론 FOO_SAY_HI라는 이름은 단순화됩니다. 자세한 내용은 C ++ 이름 Mangling을 살펴보십시오.

보시다시피, thisptr이 절대 설득되지 않으면 코드는 괜찮고 성공합니다. 귀하의 경우, 인스턴스 변수 나 thisptr에 의존하는 모든 것이 사용되지 않았습니다.

그러나 가상 기능은 다릅니다. 올바른 개체 포인터가 함수의 매개 변수로 전달되도록하기 위해 많은 객체 조회가 있습니다. 이것은 thisptr을 피하고 예외를 유발합니다.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top