문제

우리 모두는 C++에 가상 함수가 무엇인지 알고 있지만 심층적인 수준에서 어떻게 구현됩니까?

런타임에 vtable을 수정하거나 직접 액세스할 수 있나요?

모든 클래스에 대해 vtable이 존재합니까, 아니면 적어도 하나의 가상 기능이 있는 클래스에만 존재합니까?

추상 클래스에는 최소한 하나의 항목에 대한 함수 포인터에 대한 NULL이 있습니까?

단일 가상 기능을 사용하면 전체 수업 속도가 느려지나요?아니면 가상 함수에 대한 호출만 해당됩니까?그리고 가상 기능을 실제로 덮어쓰는지 여부에 관계없이 속도에 영향을 미치나요? 아니면 가상 기능인 한 효과가 없나요?

도움이 되었습니까?

해결책

가상 기능은 심층적인 수준에서 어떻게 구현되나요?

에서 "C++의 가상 함수":

프로그램에 가상 함수가 선언될 때마다 해당 클래스에 대해 v 테이블이 구성됩니다.v-table은 하나 이상의 가상 함수를 포함하는 클래스의 가상 함수에 대한 주소로 구성됩니다.가상 함수를 포함하는 클래스의 객체에는 메모리에 있는 가상 테이블의 기본 주소를 가리키는 가상 포인터가 포함되어 있습니다.가상 함수 호출이 있을 때마다 v-table을 사용하여 함수 주소를 확인합니다.하나 이상의 가상 함수를 포함하는 클래스의 객체에는 메모리의 객체 맨 처음 부분에 vptr이라는 가상 포인터가 포함되어 있습니다.따라서 이 경우 개체의 크기는 포인터의 크기만큼 증가합니다.이 vptr에는 메모리에 있는 가상 테이블의 기본 주소가 포함되어 있습니다.가상 테이블은 클래스별로 다릅니다. 즉, 포함된 가상 기능의 수에 관계없이 클래스에 대해 가상 테이블은 하나만 있습니다.이 가상 테이블에는 클래스의 하나 이상의 가상 함수에 대한 기본 주소가 포함됩니다.객체에 대해 가상 함수가 호출될 때 해당 객체의 vptr은 메모리에 있는 해당 클래스에 대한 가상 테이블의 기본 주소를 제공합니다.이 테이블은 해당 클래스의 모든 가상 함수 주소를 포함하므로 함수 호출을 해결하는 데 사용됩니다.이것이 가상 함수 호출 중에 동적 바인딩이 해결되는 방법입니다.

런타임에 vtable을 수정하거나 직접 액세스할 수 있나요?

보편적으로 나는 대답이 "아니요"라고 믿습니다.vtable을 찾기 위해 메모리 맹글링을 수행할 수 있지만 이를 호출하는 함수 서명이 어떻게 생겼는지 여전히 알 수 없습니다.이 기능(언어가 지원하는)으로 달성하려는 모든 것은 vtable에 직접 액세스하거나 런타임에 수정하지 않고도 가능해야 합니다.또한 C++ 언어 사양에 유의하세요. 하지 않습니다 vtable이 필요하다고 지정합니다. 그러나 이것이 대부분의 컴파일러가 가상 함수를 구현하는 방식입니다.

모든 객체에 대해 vtable이 존재합니까, 아니면 최소한 하나의 가상 기능이 있는 객체에만 존재합니까?

믿다 여기서 대답은 "구현에 따라 다릅니다"입니다. 왜냐하면 사양에는 처음에 vtable이 필요하지 않기 때문입니다.그러나 실제로 모든 최신 컴파일러는 클래스에 가상 함수가 하나 이상 있는 경우에만 vtable을 생성한다고 생각합니다.vtable과 관련된 공간 오버헤드와 가상 함수 호출과 가상이 아닌 함수 호출과 관련된 시간 오버헤드가 있습니다.

추상 클래스에는 최소한 하나의 항목에 대한 함수 포인터에 대한 NULL이 있습니까?

대답은 언어 사양에 지정되지 않았으므로 구현에 따라 다릅니다.순수 가상 함수를 호출하면 정의되지 않은 경우(일반적으로 그렇지 않음) 정의되지 않은 동작이 발생합니다(ISO/IEC 14882:2003 10.4-2).실제로는 함수에 대해 vtable에 슬롯을 할당하지만 주소를 할당하지는 않습니다.이로 인해 파생 클래스가 함수를 구현하고 vtable을 완료해야 하는 vtable이 불완전해집니다.일부 구현에서는 단순히 vtable 항목에 NULL 포인터를 배치합니다.다른 구현에서는 어설션과 유사한 작업을 수행하는 더미 메서드에 대한 포인터를 배치합니다.

추상 클래스는 순수 가상 함수에 대한 구현을 정의할 수 있지만 해당 함수는 자격을 갖춘 ID 구문(예: 메서드 이름에 클래스를 완전히 지정하는 방식)으로만 호출할 수 있습니다. 이는 기본 클래스 메서드를 호출하는 것과 유사합니다. 파생 클래스).이는 사용하기 쉬운 기본 구현을 제공하는 동시에 파생 클래스가 재정의를 제공하도록 요구하기 위해 수행됩니다.

단일 가상 함수를 사용하면 전체 클래스 속도가 느려지나요, 아니면 가상 함수에 대한 호출만 느려지나요?

이것은 내 지식의 한계에 도달하고 있으므로 내가 틀렸다면 누군가 나를 도와주세요!

믿다 클래스에서 가상인 함수만이 가상 함수 호출과 관련된 시간 성능 저하를 경험합니다.비가상 기능.수업을 위한 공간 오버헤드는 어느 쪽이든 존재합니다.vtable이 있는 경우 vtable당 1개만 있습니다. 수업, 하나당 하나가 아님 물체.

가상 기능이 실제로 재정의되거나 그렇지 않으면 속도에 영향을 미치나요, 아니면 가상인 한 효과가 없나요?

나는 기본 가상 함수를 호출하는 것에 비해 재정의된 가상 함수의 실행 시간이 감소한다고 믿지 않습니다.그러나 파생 클래스와 기본 클래스에 대해 다른 vtable을 정의하는 것과 관련된 클래스에 대한 추가 공간 오버헤드가 있습니다.

추가 자료:

http://www.codersource.net/published/view/325/virtual_functions_in.aspx (웨이백 머신을 통해)
http://en.wikipedia.org/wiki/Virtual_table
http://www.codesourcery.com/public/cxx-abi/abi.html#vtable

다른 팁

  • 런타임에 vtable을 수정하거나 직접 액세스할 수 있나요?

이식성은 없지만 더러운 속임수에 신경 쓰지 않는다면 당연합니다!

경고:이 기술은 어린이, 3세 미만의 성인에게는 사용하지 않는 것이 좋습니다. 969, 또는 Alpha Centauri의 작은 털복숭이 생물.부작용은 다음과 같습니다. 코에서 날아가는 악마, 갑작스런 등장 요그소토스 모든 후속 코드 검토에 대한 필수 승인자 또는 소급 추가 IHuman::PlayPiano() 모든 기존 인스턴스에]

내가 본 대부분의 컴파일러에서 vtbl *은 객체의 처음 4바이트이고 vtbl 내용은 단순히 거기에 있는 멤버 포인터의 배열입니다(일반적으로 기본 클래스의 첫 번째와 함께 선언된 순서대로).물론 다른 가능한 레이아웃도 있지만 이것이 제가 일반적으로 관찰한 것입니다.

class A {
  public:
  virtual int f1() = 0;
};
class B : public A {
  public:
  virtual int f1() { return 1; }
  virtual int f2() { return 2; }
};
class C : public A {
  public:
  virtual int f1() { return -1; }
  virtual int f2() { return -2; }
};

A *x = new B;
A *y = new C;
A *z = new C;

이제 장난을 좀 쳐볼까...

런타임 시 클래스 변경:

std::swap(*(void **)x, *(void **)y);
// Now x is a C, and y is a B! Hope they used the same layout of members!

모든 인스턴스에 대한 메서드 교체(클래스 몽키패칭)

vtbl 자체가 아마도 읽기 전용 메모리에 있기 때문에 이것은 조금 더 까다롭습니다.

int f3(A*) { return 0; }

mprotect(*(void **)x,8,PROT_READ|PROT_WRITE|PROT_EXEC);
// Or VirtualProtect on win32; this part's very OS-specific
(*(int (***)(A *)x)[0] = f3;
// Now C::f1() returns 0 (remember we made x into a C above)
// so x->f1() and z->f1() both return 0

후자는 mprotect 조작으로 인해 바이러스 검사기와 링크가 깨어나 주의를 끌 가능성이 높습니다.NX 비트를 사용하는 프로세스에서는 실패할 수도 있습니다.

단일 가상 기능을 사용하면 전체 수업 속도가 느려지나요?

아니면 가상 함수에 대한 호출만 해당됩니까?그리고 가상 기능을 실제로 덮어쓰는지 여부에 관계없이 속도에 영향을 미치나요? 아니면 가상 기능인 한 효과가 없나요?

가상 함수를 사용하면 해당 클래스의 객체를 처리할 때 하나 이상의 데이터 항목을 초기화하고 복사해야 하는 한 전체 클래스의 속도가 느려집니다.6명 정도의 구성원이 있는 클래스의 경우 그 차이는 미미할 것입니다.단일 항목만 포함하는 클래스의 경우 char 회원이 있거나 회원이 전혀 없는 경우에는 차이가 눈에 띌 수 있습니다.

그 외에도 가상 함수에 대한 모든 호출이 가상 함수 호출인 것은 아니라는 점에 유의하는 것이 중요합니다.알려진 유형의 객체가 있는 경우 컴파일러는 일반적인 함수 호출을 위한 코드를 생성할 수 있으며, 필요하다고 판단되면 해당 함수를 인라인할 수도 있습니다.기본 클래스의 개체나 일부 파생 클래스의 개체를 가리킬 수 있는 포인터나 참조를 통해 다형성 호출을 수행하는 경우에만 vtable 간접 참조가 필요하고 성능 측면에서 이에 대한 비용을 지불합니다.

struct Foo { virtual ~Foo(); virtual int a() { return 1; } };
struct Bar: public Foo { int a() { return 2; } };
void f(Foo& arg) {
  Foo x; x.a(); // non-virtual: always calls Foo::a()
  Bar y; y.a(); // non-virtual: always calls Bar::a()
  arg.a();      // virtual: must dispatch via vtable
  Foo z = arg;  // copy constructor Foo::Foo(const Foo&) will convert to Foo
  z.a();        // non-virtual Foo::a, since z is a Foo, even if arg was not
}

기능을 덮어쓰는지 여부에 관계없이 하드웨어가 수행해야 하는 단계는 본질적으로 동일합니다.vtable의 주소는 객체에서 읽혀지고, 해당 슬롯에서 검색된 함수 포인터, 포인터에 의해 호출되는 함수입니다.실제 성능 측면에서 분기 예측은 어느 정도 영향을 미칠 수 있습니다.예를 들어, 대부분의 객체가 주어진 가상 함수의 동일한 구현을 참조하는 경우 포인터가 검색되기 전에도 분기 예측기가 호출할 함수를 올바르게 예측할 가능성이 있습니다.그러나 어떤 기능이 일반적인 기능인지는 중요하지 않습니다.덮어쓰지 않은 기본 사례에 위임하는 대부분의 객체일 수도 있고, 동일한 하위 클래스에 속하여 동일한 덮어쓰기된 사례에 위임하는 대부분의 객체일 수도 있습니다.

깊은 수준에서 어떻게 구현됩니까?

나는 모의 구현을 사용하여 이를 시연하는 jheriko의 아이디어를 좋아합니다.하지만 저는 C를 사용하여 위의 코드와 유사한 것을 구현하여 낮은 수준을 더 쉽게 볼 수 있습니다.

부모 클래스 푸

typedef struct Foo_t Foo;   // forward declaration
struct slotsFoo {           // list all virtual functions of Foo
  const void *parentVtable; // (single) inheritance
  void (*destructor)(Foo*); // virtual destructor Foo::~Foo
  int (*a)(Foo*);           // virtual function Foo::a
};
struct Foo_t {                      // class Foo
  const struct slotsFoo* vtable;    // each instance points to vtable
};
void destructFoo(Foo* self) { }     // Foo::~Foo
int aFoo(Foo* self) { return 1; }   // Foo::a()
const struct slotsFoo vtableFoo = { // only one constant table
  0,                                // no parent class
  destructFoo,
  aFoo
};
void constructFoo(Foo* self) {      // Foo::Foo()
  self->vtable = &vtableFoo;        // object points to class vtable
}
void copyConstructFoo(Foo* self,
                      Foo* other) { // Foo::Foo(const Foo&)
  self->vtable = &vtableFoo;        // don't copy from other!
}

파생 클래스 Bar

typedef struct Bar_t {              // class Bar
  Foo base;                         // inherit all members of Foo
} Bar;
void destructBar(Bar* self) { }     // Bar::~Bar
int aBar(Bar* self) { return 2; }   // Bar::a()
const struct slotsFoo vtableBar = { // one more constant table
  &vtableFoo,                       // can dynamic_cast to Foo
  (void(*)(Foo*)) destructBar,      // must cast type to avoid errors
  (int(*)(Foo*)) aBar
};
void constructBar(Bar* self) {      // Bar::Bar()
  self->base.vtable = &vtableBar;   // point to Bar vtable
}

가상 함수 호출을 수행하는 함수 f

void f(Foo* arg) {                  // same functionality as above
  Foo x; constructFoo(&x); aFoo(&x);
  Bar y; constructBar(&y); aBar(&y);
  arg->vtable->a(arg);              // virtual function call
  Foo z; copyConstructFoo(&z, arg);
  aFoo(&z);
  destructFoo(&z);
  destructBar(&y);
  destructFoo(&x);
}

보시다시피 vtable은 대부분 함수 포인터를 포함하는 메모리의 정적 블록일 뿐입니다.다형성 클래스의 모든 객체는 동적 유형에 해당하는 vtable을 가리킵니다.이는 또한 RTTI와 가상 기능 간의 연결을 더욱 명확하게 만듭니다.클래스가 가리키는 vtable을 보면 클래스가 어떤 유형인지 확인할 수 있습니다.위의 내용은 다음과 같이 여러 가지 방법으로 단순화되었습니다.다중 상속이지만 일반적인 개념은 건전합니다.

만약에 arg 유형이다 Foo* 그리고 당신은 가져 arg->vtable, 이지만 실제로는 유형의 객체입니다. Bar, 그러면 여전히 정확한 주소를 얻을 수 있습니다. vtable.그 이유는 vtable 호출 여부에 관계없이 항상 객체 주소의 첫 번째 요소입니다. vtable 또는 base.vtable 올바른 유형의 표현식에서.

일반적으로 함수에 대한 포인터 배열인 VTable을 사용합니다.

이 답변은 커뮤니티 위키 답변

  • 추상 클래스에는 최소한 하나의 항목에 대한 함수 포인터에 대한 NULL이 있습니까?

이에 대한 대답은 지정되지 않았다는 것입니다. 순수 가상 함수를 호출하면 정의되지 않은 경우(일반적으로 그렇지 않음) 정의되지 않은 동작이 발생합니다(ISO/IEC 14882:2003 10.4-2).일부 구현에서는 단순히 vtable 항목에 NULL 포인터를 배치합니다.다른 구현에서는 어설션과 유사한 작업을 수행하는 더미 메서드에 대한 포인터를 배치합니다.

추상 클래스는 순수 가상 함수에 대한 구현을 정의할 수 있지만 해당 함수는 자격을 갖춘 ID 구문(예: 메서드 이름에 클래스를 완전히 지정하는 방식)으로만 호출할 수 있습니다. 이는 기본 클래스 메서드를 호출하는 것과 유사합니다. 파생 클래스).이는 사용하기 쉬운 기본 구현을 제공하는 동시에 파생 클래스가 재정의를 제공하도록 요구하기 위해 수행됩니다.

함수 포인터를 클래스 멤버로 사용하고 정적 함수를 구현으로 사용하거나 구현을 위해 멤버 함수 및 멤버 함수에 대한 포인터를 사용하여 C++에서 가상 함수의 기능을 다시 만들 수 있습니다.두 방법 사이에는 표기상의 장점만 있습니다.실제로 가상 함수 호출은 그 자체로 표기상의 편의를 위한 것일 뿐입니다.사실 상속은 단지 표기상의 편의일 뿐입니다...상속을 위한 언어 기능을 사용하지 않고도 모두 구현할 수 있습니다.:)

아래는 테스트되지 않은 쓰레기이고 버그가 있는 코드일 수 있지만 아이디어를 보여주기를 바랍니다.

예를 들어

class Foo
{
protected:
 void(*)(Foo*) MyFunc;
public:
 Foo() { MyFunc = 0; }
 void ReplciatedVirtualFunctionCall()
 {
  MyFunc(*this);
 }
...
};

class Bar : public Foo
{
private:
 static void impl1(Foo* f)
 {
  ...
 }
public:
 Bar() { MyFunc = impl1; }
...
};

class Baz : public Foo
{
private:
 static void impl2(Foo* f)
 {
  ...
 }
public:
 Baz() { MyFunc = impl2; }
...
};

간단하게 정리해보도록 할게요 :)

우리 모두는 C++에 가상 함수가 무엇인지 알고 있지만 심층적인 수준에서 어떻게 구현됩니까?

이는 특정 가상 함수의 구현인 함수에 대한 포인터가 있는 배열입니다.이 배열의 인덱스는 클래스에 대해 정의된 가상 함수의 특정 인덱스를 나타냅니다.여기에는 순수 가상 기능이 포함됩니다.

다형성 클래스가 다른 다형성 클래스에서 파생되면 다음과 같은 상황이 발생할 수 있습니다.

  • 파생 클래스는 새로운 가상 함수를 추가하거나 재정의하지 않습니다.이 경우 이 클래스는 기본 클래스와 vtable을 공유합니다.
  • 파생 클래스는 가상 메서드를 추가하고 재정의합니다.이 경우 추가된 가상 함수는 마지막 파생 함수 이후부터 시작하는 인덱스를 갖는 자체 vtable을 얻습니다.
  • 상속의 다중 다형성 클래스.이 경우 두 번째 베이스와 다음 베이스 사이의 인덱스 이동과 파생 클래스의 인덱스가 있습니다.

런타임에 vtable을 수정하거나 직접 액세스할 수 있나요?

표준 방식이 아닙니다. 액세스할 수 있는 API가 없습니다.컴파일러에는 액세스하기 위한 일부 확장이나 비공개 API가 있을 수 있지만 이는 확장일 뿐일 수 있습니다.

모든 클래스에 대해 vtable이 존재합니까, 아니면 적어도 하나의 가상 기능이 있는 클래스에만 존재합니까?

최소한 하나의 가상 함수(소멸자라도)를 갖거나 vtable을 갖는 최소한 하나의 클래스를 파생시키는 것("다형성")만 해당됩니다.

추상 클래스에는 최소한 하나의 항목에 대한 함수 포인터에 대한 NULL이 있습니까?

이는 가능한 구현이지만 실제로 실행되지는 않습니다.대신 일반적으로 "순수 가상 함수 호출"과 같은 내용을 인쇄하고 다음을 수행하는 함수가 있습니다. abort().생성자나 소멸자에서 추상 메서드를 호출하려고 하면 해당 호출이 발생할 수 있습니다.

단일 가상 기능을 사용하면 전체 수업 속도가 느려지나요?아니면 가상 함수에 대한 호출만 해당됩니까?그리고 가상 기능을 실제로 덮어쓰는지 여부에 관계없이 속도에 영향을 미치나요? 아니면 가상 기능인 한 효과가 없나요?

속도 저하는 통화가 직접 통화로 해결되는지 아니면 가상 통화로 해결되는지에 따라서만 달라집니다.그리고 다른 건.:)

객체에 대한 포인터나 참조를 통해 가상 함수를 호출하면 항상 가상 호출로 구현됩니다. 왜냐하면 컴파일러는 런타임에 이 포인터에 어떤 종류의 객체가 할당될지, 그리고 그것이 객체인지 여부를 알 수 없기 때문입니다. 이 메소드가 재정의되는지 여부를 나타내는 클래스입니다.두 가지 경우에만 컴파일러가 가상 함수에 대한 호출을 직접 호출로 확인할 수 있습니다.

  • 값(값을 반환하는 변수 또는 함수의 결과)을 통해 메서드를 호출하는 경우 - 이 경우 컴파일러는 객체의 실제 클래스가 무엇인지 의심하지 않으며 컴파일 타임에 클래스를 "하드 해결"할 수 있습니다. .
  • 가상 메소드가 선언된 경우 final 호출하는 데 사용되는 포인터나 참조가 있는 클래스에서(C++11에서만).이 경우 컴파일러는 이 메서드가 더 이상 재정의될 수 없으며 이 클래스의 메서드만 될 수 있다는 것을 알고 있습니다.

가상 호출에는 두 포인터를 역참조하는 오버헤드만 있다는 점에 유의하세요.RTTI를 사용하는 것은 (다형성 클래스에만 사용 가능하지만) 가상 메소드를 호출하는 것보다 느립니다. 두 가지 방법으로 동일한 것을 구현하는 경우를 찾으면 됩니다.예를 들어, 정의 virtual bool HasHoof() { return false; } 그런 다음 다음과 같이 재정의하십시오. bool Horse::HasHoof() { return true; } 전화할 수 있는 기능을 제공할 것입니다. if (anim->HasHoof()) 그게 노력하는 것보다 더 빠를 거야 if(dynamic_cast<Horse*>(anim)).이 때문입니다 dynamic_cast 어떤 경우에는 실제 포인터 유형과 원하는 클래스 유형에서 경로를 구축할 수 있는지 확인하기 위해 클래스 계층 구조를 재귀적으로 탐색해야 합니다.가상 호출은 항상 동일하지만 두 포인터를 역참조합니다.

여기에는 실행 가능 최신 C++에서 가상 테이블을 수동으로 구현합니다.의미가 잘 정의되어 있고 해킹이나 해킹이 없습니다. void*.

메모: .* 그리고 ->* 와는 다른 연산자입니다 * 그리고 ->.멤버 함수 포인터는 다르게 작동합니다.

#include <iostream>
#include <vector>
#include <memory>

struct vtable; // forward declare, we need just name

class animal
{
public:
    const std::string& get_name() const { return name; }

    // these will be abstract
    bool has_tail() const;
    bool has_wings() const;
    void sound() const;

protected: // we do not want animals to be created directly
    animal(const vtable* vtable_ptr, std::string name)
    : vtable_ptr(vtable_ptr), name(std::move(name)) { }

private:
    friend vtable; // just in case for non-public methods

    const vtable* const vtable_ptr;
    std::string name;
};

class cat : public animal
{
public:
    cat(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does meow\n"; 
    }
};

class dog : public animal
{
public:
    dog(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return true; }
    bool has_wings() const { return false; }
    void sound() const
    {
        std::cout << get_name() << " does whoof\n"; 
    }
};

class parrot : public animal
{
public:
    parrot(std::string name);

    // functions to bind dynamically
    bool has_tail() const { return false; }
    bool has_wings() const { return true; }
    void sound() const
    {
        std::cout << get_name() << " does crrra\n"; 
    }
};

// now the magic - pointers to member functions!
struct vtable
{
    bool (animal::* const has_tail)() const;
    bool (animal::* const has_wings)() const;
    void (animal::* const sound)() const;

    // constructor
    vtable (
        bool (animal::* const has_tail)() const,
        bool (animal::* const has_wings)() const,
        void (animal::* const sound)() const
    ) : has_tail(has_tail), has_wings(has_wings), sound(sound) { }
};

// global vtable objects
const vtable vtable_cat(
    static_cast<bool (animal::*)() const>(&cat::has_tail),
    static_cast<bool (animal::*)() const>(&cat::has_wings),
    static_cast<void (animal::*)() const>(&cat::sound));
const vtable vtable_dog(
    static_cast<bool (animal::*)() const>(&dog::has_tail),
    static_cast<bool (animal::*)() const>(&dog::has_wings),
    static_cast<void (animal::*)() const>(&dog::sound));
const vtable vtable_parrot(
    static_cast<bool (animal::*)() const>(&parrot::has_tail),
    static_cast<bool (animal::*)() const>(&parrot::has_wings),
    static_cast<void (animal::*)() const>(&parrot::sound));

// set vtable pointers in constructors
cat::cat(std::string name) : animal(&vtable_cat, std::move(name)) { }
dog::dog(std::string name) : animal(&vtable_dog, std::move(name)) { }
parrot::parrot(std::string name) : animal(&vtable_parrot, std::move(name)) { }

// implement dynamic dispatch
bool animal::has_tail() const
{
    return (this->*(vtable_ptr->has_tail))();
}

bool animal::has_wings() const
{
    return (this->*(vtable_ptr->has_wings))();
}

void animal::sound() const
{
    (this->*(vtable_ptr->sound))();
}

int main()
{
    std::vector<std::unique_ptr<animal>> animals;
    animals.push_back(std::make_unique<cat>("grumpy"));
    animals.push_back(std::make_unique<cat>("nyan"));
    animals.push_back(std::make_unique<dog>("doge"));
    animals.push_back(std::make_unique<parrot>("party"));

    for (const auto& a : animals)
        a->sound();

    // note: destructors are not dispatched virtually
}

각 개체에는 멤버 함수 배열을 가리키는 vtable 포인터가 있습니다.

이 모든 답변에서 여기에 언급되지 않은 점은 다중 상속의 경우 기본 클래스에 모두 가상 메서드가 있다는 것입니다.상속 클래스에는 vmt에 대한 여러 포인터가 있습니다.결과적으로 해당 개체의 각 인스턴스 크기가 더 커집니다.가상 메서드가 있는 클래스에는 vmt에 대한 추가 바이트가 4바이트 있다는 것을 누구나 알고 있지만 다중 상속의 경우 가상 메서드가 4배인 각 기본 클래스에 대한 것입니다.4는 포인터의 크기입니다.

Burly의 답변은 다음 질문을 제외하고는 정확합니다.

추상 클래스에는 최소한 하나의 항목에 대한 함수 포인터에 대한 NULL이 있습니까?

대답은 추상 클래스에 대해 가상 테이블이 전혀 생성되지 않는다는 것입니다.이 클래스의 객체를 생성할 수 없으므로 필요하지 않습니다!

즉, 다음과 같은 경우가 있습니다.

class B { ~B() = 0; }; // Abstract Base class
class D : public B { ~D() {} }; // Concrete Derived class

D* pD = new D();
B* pB = pD;

pB를 통해 액세스되는 vtbl 포인터는 클래스 D의 vtbl이 됩니다.이것이 바로 다형성이 구현되는 방식입니다.즉, pB를 통해 D 메소드에 액세스하는 방법입니다.클래스 B에는 vtbl이 필요하지 않습니다.

아래 Mike의 의견에 대한 응답으로 ...

내 설명의 B 클래스에 가상 메서드가 있는 경우 푸() D와 가상 메서드에 의해 재정의되지 않습니다. 술집() 이것이 재정의되면 D의 vtbl은 B에 대한 포인터를 갖게 됩니다. 푸() 그리고 그 자체로 술집().B에 대해서는 아직 생성된 vtbl이 없습니다.

아주 귀여운 개념 증명을 조금 더 일찍 만들었습니다(상속 순서가 중요한지 확인하기 위해).귀하의 C++ 구현이 실제로 이를 거부하는지 알려주세요(제 gcc 버전은 익명 구조체 할당에 대한 경고만 제공하지만 이는 버그입니다). 궁금합니다.

CCPolite.h:

#ifndef CCPOLITE_H
#define CCPOLITE_H

/* the vtable or interface */
typedef struct {
    void (*Greet)(void *);
    void (*Thank)(void *);
} ICCPolite;

/**
 * the actual "object" literal as C++ sees it; public variables be here too 
 * all CPolite objects use(are instances of) this struct's structure.
 */
typedef struct {
    ICCPolite *vtbl;
} CPolite;

#endif /* CCPOLITE_H */

CCPolite_constructor.h:

/** 
 * unconventionally include me after defining OBJECT_NAME to automate
 * static(allocation-less) construction.
 *
 * note: I assume CPOLITE_H is included; since if I use anonymous structs
 *     for each object, they become incompatible and cause compile time errors
 *     when trying to do stuff like assign, or pass functions.
 *     this is similar to how you can't pass void * to windows functions that
 *         take handles; these handles use anonymous structs to make 
 *         HWND/HANDLE/HINSTANCE/void*/etc not automatically convertible, and
 *         require a cast.
 */
#ifndef OBJECT_NAME
    #error CCPolite> constructor requires object name.
#endif

CPolite OBJECT_NAME = {
    &CCPolite_Vtbl
};

/* ensure no global scope pollution */
#undef OBJECT_NAME

main.c:

#include <stdio.h>
#include "CCPolite.h"

// | A Greeter is capable of greeting; nothing else.
struct IGreeter
{
    virtual void Greet() = 0;
};

// | A Thanker is capable of thanking; nothing else.
struct IThanker
{
    virtual void Thank() = 0;
};

// | A Polite is something that implements both IGreeter and IThanker
// | Note that order of implementation DOES MATTER.
struct IPolite1 : public IGreeter, public IThanker{};
struct IPolite2 : public IThanker, public IGreeter{};

// | implementation if IPolite1; implements IGreeter BEFORE IThanker
struct CPolite1 : public IPolite1
{
    void Greet()
    {
        puts("hello!");
    }

    void Thank()
    {
        puts("thank you!");
    }
};

// | implementation if IPolite1; implements IThanker BEFORE IGreeter
struct CPolite2 : public IPolite2
{
    void Greet()
    {
        puts("hi!");
    }

    void Thank()
    {
        puts("ty!");
    }
};

// | imposter Polite's Greet implementation.
static void CCPolite_Greet(void *)
{
    puts("HI I AM C!!!!");
}

// | imposter Polite's Thank implementation.
static void CCPolite_Thank(void *)
{
    puts("THANK YOU, I AM C!!");
}

// | vtable of the imposter Polite.
ICCPolite CCPolite_Vtbl = {
    CCPolite_Thank,
    CCPolite_Greet    
};

CPolite CCPoliteObj = {
    &CCPolite_Vtbl
};

int main(int argc, char **argv)
{
    puts("\npart 1");
    CPolite1 o1;
    o1.Greet();
    o1.Thank();

    puts("\npart 2");    
    CPolite2 o2;    
    o2.Greet();
    o2.Thank();    

    puts("\npart 3");    
    CPolite1 *not1 = (CPolite1 *)&o2;
    CPolite2 *not2 = (CPolite2 *)&o1;
    not1->Greet();
    not1->Thank();
    not2->Greet();
    not2->Thank();

    puts("\npart 4");        
    CPolite1 *fake = (CPolite1 *)&CCPoliteObj;
    fake->Thank();
    fake->Greet();

    puts("\npart 5");        
    CPolite2 *fake2 = (CPolite2 *)fake;
    fake2->Thank();
    fake2->Greet();

    puts("\npart 6");        
    #define OBJECT_NAME fake3
    #include "CCPolite_constructor.h"
    fake = (CPolite1 *)&fake3;
    fake->Thank();
    fake->Greet();

    puts("\npart 7");        
    #define OBJECT_NAME fake4
    #include "CCPolite_constructor.h"
    fake2 = (CPolite2 *)&fake4;
    fake2->Thank();
    fake2->Greet();    

    return 0;
}

산출:

part 1
hello!
thank you!

part 2
hi!
ty!

part 3
ty!
hi!
thank you!
hello!

part 4
HI I AM C!!!!
THANK YOU, I AM C!!

part 5
THANK YOU, I AM C!!
HI I AM C!!!!

part 6
HI I AM C!!!!
THANK YOU, I AM C!!

part 7
THANK YOU, I AM C!!
HI I AM C!!!!

참고로 나는 가짜 개체를 할당하지 않으므로 어떤 파괴 작업도 수행할 필요가 없습니다.소멸자는 자동으로 동적으로 할당된 개체의 범위 끝에 배치되어 개체 리터럴 자체와 vtable 포인터의 메모리를 회수합니다.

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