ฟังก์ชันเสมือนจริงและ vtable ใช้งานอย่างไร


110

เราทุกคนรู้ว่าฟังก์ชันเสมือนจริงคืออะไรใน C ++ แต่จะใช้งานในระดับลึกได้อย่างไร

vtable สามารถแก้ไขหรือเข้าถึงได้โดยตรงที่รันไทม์?

vtable มีอยู่สำหรับทุกคลาสหรือเฉพาะที่มีฟังก์ชันเสมือนอย่างน้อยหนึ่งฟังก์ชัน?

คลาสนามธรรมมีค่า NULL สำหรับตัวชี้ฟังก์ชันอย่างน้อยหนึ่งรายการหรือไม่?

การมีฟังก์ชันเสมือนเดียวทำให้ทั้งชั้นเรียนช้าลงหรือไม่? หรือเฉพาะการเรียกใช้ฟังก์ชันที่เสมือนจริง? และความเร็วจะได้รับผลกระทบหรือไม่หากฟังก์ชันเสมือนถูกเขียนทับจริงหรือไม่หรือจะไม่มีผลตราบใดที่เป็นเสมือน


2
แนะนำให้อ่านผลงานชิ้นเอกInside the C++ Object ModelโดยStanley B. Lippman. (ข้อ 4.2 หน้า 124-131)
smwikipedia

คำตอบ:


123

ฟังก์ชันเสมือนถูกนำไปใช้ในระดับลึกอย่างไร?

จาก"ฟังก์ชันเสมือนใน C ++" :

เมื่อใดก็ตามที่โปรแกรมมีการประกาศฟังก์ชันเสมือน av - table จะถูกสร้างขึ้นสำหรับคลาส v-table ประกอบด้วยแอดเดรสของฟังก์ชันเสมือนสำหรับคลาสที่มีฟังก์ชันเสมือนอย่างน้อยหนึ่งฟังก์ชัน ออบเจ็กต์ของคลาสที่มีฟังก์ชันเสมือนมีตัวชี้เสมือนที่ชี้ไปยังที่อยู่ฐานของตารางเสมือนในหน่วยความจำ เมื่อใดก็ตามที่มีการเรียกใช้ฟังก์ชันเสมือน v-table จะถูกใช้เพื่อแก้ไขเป็นที่อยู่ของฟังก์ชัน อ็อบเจ็กต์ของคลาสที่มีฟังก์ชันเสมือนตั้งแต่หนึ่งฟังก์ชันขึ้นไปมีตัวชี้เสมือนที่เรียกว่า vptr ที่จุดเริ่มต้นของอ็อบเจ็กต์ในหน่วยความจำ ดังนั้นขนาดของวัตถุในกรณีนี้จะเพิ่มขึ้นตามขนาดของตัวชี้ vptr นี้มีแอดเดรสฐานของตารางเสมือนในหน่วยความจำ โปรดสังเกตว่าตารางเสมือนเป็นคลาสเฉพาะกล่าวคือ มีตารางเสมือนเพียงตารางเดียวสำหรับคลาสโดยไม่คำนึงถึงจำนวนฟังก์ชันเสมือนที่มีอยู่ ในทางกลับกันตารางเสมือนนี้มีที่อยู่ฐานของฟังก์ชันเสมือนหนึ่งหรือหลายฟังก์ชันของคลาส ในเวลาที่ฟังก์ชันเสมือนถูกเรียกใช้บนอ็อบเจ็กต์ vptr ของอ็อบเจ็กต์นั้นจัดเตรียมแอดเดรสฐานของตารางเสมือนสำหรับคลาสนั้นในหน่วยความจำ ตารางนี้ใช้เพื่อแก้ไขการเรียกฟังก์ชันเนื่องจากมีที่อยู่ของฟังก์ชันเสมือนทั้งหมดของคลาสนั้น นี่คือวิธีแก้ไขการเชื่อมโยงแบบไดนามิกระหว่างการเรียกใช้ฟังก์ชันเสมือน vptr ของอ็อบเจ็กต์นั้นจัดเตรียมแอดเดรสฐานของตารางเสมือนสำหรับคลาสนั้นในหน่วยความจำ ตารางนี้ใช้เพื่อแก้ไขการเรียกฟังก์ชันเนื่องจากมีที่อยู่ของฟังก์ชันเสมือนทั้งหมดของคลาสนั้น นี่คือวิธีแก้ไขการเชื่อมโยงแบบไดนามิกระหว่างการเรียกใช้ฟังก์ชันเสมือน vptr ของอ็อบเจ็กต์นั้นจัดเตรียมแอดเดรสฐานของตารางเสมือนสำหรับคลาสนั้นในหน่วยความจำ ตารางนี้ใช้เพื่อแก้ไขการเรียกฟังก์ชันเนื่องจากมีที่อยู่ของฟังก์ชันเสมือนทั้งหมดของคลาสนั้น นี่คือวิธีแก้ไขการเชื่อมโยงแบบไดนามิกระหว่างการเรียกใช้ฟังก์ชันเสมือน

vtable สามารถแก้ไขหรือเข้าถึงได้โดยตรงที่รันไทม์?

โดยทั่วไปฉันเชื่อว่าคำตอบคือ "ไม่" คุณสามารถใช้หน่วยความจำเพื่อค้นหา vtable ได้ แต่คุณยังไม่รู้ว่าลายเซ็นของฟังก์ชันจะเรียกมันว่าอะไร สิ่งใดก็ตามที่คุณต้องการบรรลุด้วยความสามารถนี้ (ที่ภาษานั้นรองรับ) ควรเป็นไปได้โดยไม่ต้องเข้าถึง vtable โดยตรงหรือแก้ไขในขณะรันไทม์ โปรดทราบว่าข้อมูลจำเพาะภาษา C ++ ไม่ได้ระบุว่าจำเป็นต้องใช้ vtables - อย่างไรก็ตามนั่นคือวิธีที่คอมไพเลอร์ส่วนใหญ่ใช้ฟังก์ชันเสมือน

vtable มีอยู่สำหรับอ็อบเจ็กต์ทั้งหมดหรือเฉพาะอ็อบเจ็กต์ที่มีฟังก์ชันเสมือนอย่างน้อยหนึ่งฟังก์ชัน?

ฉันเชื่อว่าคำตอบที่นี่คือ "ขึ้นอยู่กับการนำไปใช้งาน" เนื่องจากข้อมูลจำเพาะไม่จำเป็นต้องใช้ vtables ตั้งแต่แรก อย่างไรก็ตามในทางปฏิบัติฉันเชื่อว่าคอมไพเลอร์สมัยใหม่ทั้งหมดจะสร้าง vtable ก็ต่อเมื่อคลาสมีฟังก์ชันเสมือนอย่างน้อย 1 ฟังก์ชัน มีพื้นที่เหนือศีรษะที่เกี่ยวข้องกับ vtable และค่าใช้จ่ายด้านเวลาที่เกี่ยวข้องกับการเรียกฟังก์ชันเสมือนกับฟังก์ชันที่ไม่ใช่ฟังก์ชันเสมือน

คลาสนามธรรมมีค่า NULL สำหรับตัวชี้ฟังก์ชันอย่างน้อยหนึ่งรายการหรือไม่?

คำตอบคือมันไม่ได้ระบุโดยสเป็คภาษาดังนั้นจึงขึ้นอยู่กับการนำไปใช้งาน การเรียกใช้ฟังก์ชันเสมือนจริงจะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดไว้หากไม่ได้กำหนดไว้ (ซึ่งโดยปกติจะไม่ใช่) (ISO / IEC 14882: 2003 10.4-2) ในทางปฏิบัติจะจัดสรรสล็อตใน vtable สำหรับฟังก์ชัน แต่ไม่ได้กำหนดแอดเดรสให้ สิ่งนี้ทำให้ vtable ไม่สมบูรณ์ซึ่งต้องใช้คลาสที่ได้รับเพื่อใช้ฟังก์ชันและทำให้ vtable สมบูรณ์ การใช้งานบางอย่างทำได้เพียงแค่วางตัวชี้ NULL ในรายการ vtable การใช้งานอื่น ๆ จะชี้ไปที่วิธีการจำลองที่ทำสิ่งที่คล้ายกับการยืนยัน

โปรดสังเกตว่าคลาสนามธรรมสามารถกำหนดการใช้งานสำหรับฟังก์ชันเสมือนจริงได้ แต่ฟังก์ชันนั้นสามารถเรียกใช้ด้วยไวยากรณ์ id ที่มีคุณสมบัติเหมาะสมเท่านั้น (เช่นการระบุคลาสในชื่อเมธอดโดยสมบูรณ์คล้ายกับการเรียกเมธอดคลาสฐานจาก a คลาสที่ได้รับ) สิ่งนี้ทำขึ้นเพื่อให้การใช้งานเริ่มต้นที่ง่ายต่อการใช้งานในขณะที่ยังต้องการให้คลาสที่ได้รับมาจัดเตรียมการแทนที่

การมีฟังก์ชันเสมือนเดียวทำให้ทั้งคลาสช้าลงหรือเฉพาะการเรียกใช้ฟังก์ชันที่เสมือนจริงหรือไม่?

นี่คือความรู้ของฉันดังนั้นใครบางคนโปรดช่วยฉันที่นี่ถ้าฉันผิด!

ฉันเชื่อว่าเฉพาะฟังก์ชันที่เป็นเสมือนในชั้นเรียนเท่านั้นที่สัมผัสกับประสิทธิภาพของเวลาที่เกี่ยวข้องกับการเรียกฟังก์ชันเสมือนกับฟังก์ชันที่ไม่ใช่ฟังก์ชันเสมือน พื้นที่เหนือศีรษะสำหรับชั้นเรียนมีทางใดทางหนึ่ง โปรดทราบว่าถ้ามี 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


2
มันจะไม่สอดคล้องกับปรัชญาของ C ++ ของ Stroustrup สำหรับคอมไพเลอร์ที่จะใส่ตัวชี้ vtable ที่ไม่จำเป็นในวัตถุที่ไม่ต้องการ กฎคือคุณจะไม่ได้รับค่าโสหุ้ยที่ไม่ได้อยู่ใน C เว้นแต่คุณจะขอและมันก็หยาบคายสำหรับคอมไพเลอร์ที่จะทำลายสิ่งนั้น
Steve Jessop

3
ฉันยอมรับว่ามันจะโง่สำหรับคอมไพเลอร์ใด ๆ ที่ใช้ตัวเองอย่างจริงจังในการใช้ vtable เมื่อไม่มีฟังก์ชันเสมือน อย่างไรก็ตามฉันรู้สึกว่ามันสำคัญที่จะต้องชี้ให้เห็นว่าสำหรับความรู้ของฉันมาตรฐาน C ++ ไม่ / ต้องการ / มันดังนั้นโปรดเตือนไว้ก่อนขึ้นอยู่กับมัน
Zach Burlingame

8
แม้แต่ฟังก์ชันเสมือนก็สามารถเรียกได้ว่าไม่ใช่แทบ ในความเป็นจริงนี่เป็นเรื่องธรรมดา: ถ้าวัตถุอยู่บนสแต็กภายในขอบเขตคอมไพเลอร์จะทราบประเภทที่แน่นอนและปรับให้เหมาะสมกับการค้นหา vtable โดยเฉพาะอย่างยิ่งสำหรับ dtor ซึ่งต้องถูกเรียกในขอบเขตสแต็กเดียวกัน
MSalters

1
ฉันเชื่อว่าเมื่อคลาสที่มีฟังก์ชันเสมือนอย่างน้อยหนึ่งฟังก์ชันทุกอ็อบเจ็กต์มี vtable ไม่ใช่หนึ่งสำหรับคลาสทั้งหมด
Asaf R

3
การใช้งานทั่วไป: แต่ละวัตถุมีตัวชี้ไปที่ vtable ชั้นเรียนเป็นเจ้าของโต๊ะ เวทมนตร์การก่อสร้างประกอบด้วยการอัปเดตตัวชี้ vtable ใน ctor ที่ได้รับหลังจาก ctor ฐานเสร็จสิ้น
MSalters

31
  • vtable สามารถแก้ไขหรือเข้าถึงได้โดยตรงที่รันไทม์?

ไม่พกพา แต่ถ้าคุณไม่รังเกียจเทคนิคสกปรกแน่นอน!

คำเตือน : เทคนิคนี้ไม่แนะนำให้ใช้กับเด็กผู้ใหญ่ที่มีอายุต่ำกว่า969ปีหรือสิ่งมีชีวิตขนยาวตัวเล็กจาก Alpha Centauri ผลข้างเคียงอาจรวมถึงปีศาจที่บินออกจากจมูกของคุณการปรากฏตัวทันทีของYog-Sothothในฐานะผู้อนุมัติที่จำเป็นสำหรับการตรวจสอบโค้ดที่ตามมาทั้งหมดหรือการเพิ่มย้อนหลังของ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
อืม. รู้สึกเป็นลางไม่ดีที่สิ่งนี้ได้รับค่าหัว ฉันหวังว่านั่นไม่ได้หมายความว่า @Mobilewits คิดว่าเชนานีแกนเป็นความคิดที่ดีจริงๆ ...
puetzk

1
โปรดพิจารณาการใช้เทคนิคนี้อย่างไม่ย่อท้อและชัดเจนแทนที่จะ "ขยิบตา"
einpoklum

" เนื้อหา vtbl เป็นเพียงอาร์เรย์ของตัวชี้ของสมาชิก " ที่จริงมันเป็นบันทึก (เป็น struct) กับรายการที่แตกต่างกันที่เกิดขึ้นที่จะเว้นระยะเท่ากัน
curiousguy

1
คุณสามารถมองไปทางใดก็ได้ พอยน์เตอร์ของฟังก์ชันมีลายเซ็นที่แตกต่างกันดังนั้นตัวชี้ประเภทต่างๆ ในแง่นั้นมันเหมือนโครงสร้างจริงๆ แต่ในบริบทอื่น ๆ แต่แนวคิดของดัชนี vtbl นั้นมีประโยชน์ (เช่น ActiveX ใช้มันในลักษณะที่อธิบายอินเทอร์เฟซแบบคู่ในตัวพิมพ์) ซึ่งเป็นมุมมองที่เหมือนอาร์เรย์มากกว่า
puetzk

17

การมีฟังก์ชันเสมือนเดียวทำให้ทั้งชั้นเรียนช้าลงหรือไม่?

หรือเฉพาะการเรียกใช้ฟังก์ชันที่เสมือนจริง? และความเร็วจะได้รับผลกระทบหรือไม่หากฟังก์ชันเสมือนถูกเขียนทับจริงหรือไม่หรือจะไม่มีผลตราบใดที่เป็นเสมือน

การมีฟังก์ชันเสมือนจะทำให้ทั้งคลาสทำงานช้าลงเนื่องจากต้องมีการเตรียมข้อมูลเบื้องต้นคัดลอก ... เมื่อจัดการกับอ็อบเจ็กต์ของคลาสดังกล่าว สำหรับชั้นเรียนที่มีสมาชิกประมาณครึ่งโหลความแตกต่างควรจะลบล้างได้ สำหรับชั้นเรียนที่มี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 เพื่อใช้งานบางอย่างที่คล้ายกับโค้ดด้านบนเพื่อให้มองเห็นระดับต่ำได้ง่ายขึ้น

ระดับผู้ปกครอง Foo

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!
}

คลาสบาร์ที่ได้รับ

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 เป็นเพียงบล็อกแบบคงที่ในหน่วยความจำซึ่งส่วนใหญ่มีตัวชี้ฟังก์ชัน ทุกออบเจ็กต์ของคลาส polymorphic จะชี้ไปที่ vtable ที่สอดคล้องกับประเภทไดนามิก นอกจากนี้ยังทำให้การเชื่อมต่อระหว่าง RTTI และฟังก์ชันเสมือนชัดเจนขึ้น: คุณสามารถตรวจสอบประเภทของคลาสได้ง่ายๆเพียงแค่ดูว่า vtable นั้นชี้ไปที่อะไร ข้างต้นนั้นง่ายขึ้นในหลาย ๆ วิธีเช่นการสืบทอดหลาย ๆ อย่าง แต่แนวคิดทั่วไปนั้นฟังดูดี

หากargเป็นประเภทFoo*และคุณใช้arg->vtableแต่จริงๆแล้วเป็นวัตถุประเภทBarคุณจะยังคงได้รับที่อยู่ที่ถูกต้องของไฟล์vtable. นั่นเป็นเพราะvtableองค์ประกอบแรกเป็นที่อยู่ของวัตถุเสมอไม่ว่าจะเรียกvtableหรือbase.vtableอยู่ในนิพจน์ที่พิมพ์ถูกต้องก็ตาม


"ทุกออบเจ็กต์ของคลาส polymorphic จะชี้ไปที่ vtable ของมันเอง" คุณกำลังบอกว่าทุกวัตถุมี vtable ของตัวเองหรือไม่? AFAIK vtable ใช้ร่วมกันระหว่างออบเจ็กต์ทั้งหมดในคลาสเดียวกัน แจ้งให้เราทราบหากฉันผิด
ภุวรรณ

1
@ ภูวัน: ไม่คุณพูดถูก: มีเพียงหนึ่ง vtable ต่อประเภท (ซึ่งอาจเป็นต่อเทมเพลตอินสแตนซ์ในกรณีของเทมเพลต) ฉันตั้งใจจะบอกว่าแต่ละออบเจ็กต์ของคลาสโพลีมอร์ฟิกที่ชี้ไปที่ vtable ที่ใช้กับมันดังนั้นแต่ละอ็อบเจกต์จึงมีพอยน์เตอร์ แต่สำหรับอ็อบเจ็กต์ประเภทเดียวกันมันจะชี้ไปที่ตารางเดียวกัน ฉันควรจะ reword นี้
MvG

1
@MvG " วัตถุประเภทเดียวกันจะชี้ไปที่ตารางเดียวกัน " ไม่ใช่ในระหว่างการสร้างคลาสพื้นฐานกับคลาสฐานเสมือน! (เป็นกรณีพิเศษ)
ซอกแซก

1
@curiousguy: ฉันจะยื่นเรื่องภายใต้“ ข้างต้นนั้นง่ายขึ้นในหลาย ๆ ด้าน” โดยเฉพาะอย่างยิ่งเนื่องจากการประยุกต์ใช้ฐานเสมือนหลักคือการสืบทอดหลายรายการซึ่งฉันไม่ได้จำลองแบบด้วย แต่ขอบคุณสำหรับความคิดเห็นการมีสิ่งนี้ไว้ที่นี่มีประโยชน์สำหรับผู้ที่อาจต้องการข้อมูลเชิงลึกมากกว่านี้
MvG


2

คำตอบนี้ได้รวมอยู่ในคำตอบของCommunity Wiki

  • คลาสนามธรรมมีค่า NULL สำหรับตัวชี้ฟังก์ชันอย่างน้อยหนึ่งรายการหรือไม่?

คำตอบคือไม่ระบุ - การเรียกฟังก์ชันเสมือนจริงจะส่งผลให้เกิดพฤติกรรมที่ไม่ได้กำหนดหากไม่ได้กำหนดไว้ (ซึ่งโดยปกติจะไม่ใช่) (ISO / IEC 14882: 2003 10.4-2) การใช้งานบางอย่างทำได้เพียงแค่วางตัวชี้ NULL ในรายการ vtable การใช้งานอื่น ๆ จะชี้ไปที่วิธีการจำลองที่ทำสิ่งที่คล้ายกับการยืนยัน

โปรดสังเกตว่าคลาสนามธรรมสามารถกำหนดการใช้งานสำหรับฟังก์ชันเสมือนจริงได้ แต่ฟังก์ชันนั้นสามารถเรียกใช้ด้วยไวยากรณ์ id ที่มีคุณสมบัติเหมาะสมเท่านั้น (เช่นการระบุคลาสในชื่อเมธอดโดยสมบูรณ์คล้ายกับการเรียกเมธอดคลาสฐานจาก a คลาสที่ได้รับ) สิ่งนี้ทำขึ้นเพื่อให้การใช้งานเริ่มต้นที่ง่ายต่อการใช้งานในขณะที่ยังต้องการให้คลาสที่ได้รับมาจัดเตรียมการแทนที่


นอกจากนี้ฉันไม่คิดว่าคลาสนามธรรมสามารถกำหนดการใช้งานสำหรับฟังก์ชันเสมือนจริงได้ ตามนิยามฟังก์ชันเสมือนจริงไม่มีเนื้อความ (เช่น bool my_func () = 0;) อย่างไรก็ตามคุณสามารถจัดเตรียมการใช้งานสำหรับฟังก์ชันเสมือนปกติได้
Zach Burlingame

ฟังก์ชันเสมือนจริงสามารถมีคำจำกัดความได้ ดู "Effective C ++, 3rd Ed" ของ Scott Meyers รายการ # 34, ISO 14882-2003 10.4-2 หรือbytes.com/forum/thread572745.html
Michael Burr

2

คุณสามารถสร้างการทำงานของฟังก์ชันเสมือนใน 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; }
...
};

void(*)(Foo*) MyFunc;นี่คือไวยากรณ์ของ Java หรือไม่
ซอกแซก

ไม่ใช่ไวยากรณ์ C / C ++ สำหรับตัวชี้ฟังก์ชัน เพื่ออ้างถึงตัวเอง "คุณสามารถสร้างการทำงานของฟังก์ชันเสมือนใน C ++ ขึ้นใหม่โดยใช้ตัวชี้ฟังก์ชัน" มันเป็นไวยากรณ์ที่น่ารังเกียจ แต่เป็นสิ่งที่ต้องทำความคุ้นเคยหากคุณคิดว่าตัวเองเป็นโปรแกรมเมอร์ C
jheriko

ตัวชี้ฟังก์ชัน ac จะมีลักษณะดังนี้: int ( PROC) (); และตัวชี้ไปยังฟังก์ชันสมาชิกคลาสจะมีลักษณะดังนี้: int (ClassName :: MPROC) ();
อันตราย

1
@menace คุณลืมไวยากรณ์ที่นั่น ... คุณกำลังคิดว่า typedef อาจจะ? typedef int (* PROC) (); ดังนั้นคุณสามารถทำ PROC foo ในภายหลังแทน int (* foo) ()?
jheriko

2

ฉันจะพยายามทำให้มันง่าย :)

เราทุกคนรู้ว่าฟังก์ชันเสมือนจริงคืออะไรใน C ++ แต่จะใช้งานในระดับลึกได้อย่างไร

นี่คืออาร์เรย์ที่มีตัวชี้ไปยังฟังก์ชันซึ่งเป็นการนำฟังก์ชันเสมือนไปใช้งาน ดัชนีในอาร์เรย์นี้แสดงถึงดัชนีเฉพาะของฟังก์ชันเสมือนที่กำหนดไว้สำหรับคลาส ซึ่งรวมถึงฟังก์ชันเสมือนจริง

เมื่อคลาส polymorphic มาจากคลาส polymorphic อื่นเราอาจมีสถานการณ์ต่อไปนี้:

  • คลาสที่ได้มาไม่ได้เพิ่มฟังก์ชันเสมือนใหม่หรือลบล้างใด ๆ ในกรณีนี้คลาสนี้จะแชร์ vtable กับคลาสพื้นฐาน
  • คลาสที่ได้มาจะเพิ่มและลบล้างวิธีการเสมือน ในกรณีนี้จะได้รับ vtable ของตัวเองโดยที่ฟังก์ชันเสมือนที่เพิ่มเข้ามาจะมีดัชนีเริ่มต้นจากค่าที่ได้รับล่าสุด
  • หลายชั้นเรียนหลายรูปแบบในการสืบทอด ในกรณีนี้เรามีการเลื่อนดัชนีระหว่างฐานที่สองและฐานถัดไปและดัชนีของมันในคลาสที่ได้รับ

vtable สามารถแก้ไขหรือเข้าถึงได้โดยตรงที่รันไทม์?

ไม่ใช่วิธีมาตรฐาน - ไม่มี API ในการเข้าถึง คอมไพเลอร์อาจมีส่วนขยายหรือ API ส่วนตัวเพื่อเข้าถึง แต่อาจเป็นเพียงส่วนขยายเท่านั้น

vtable มีอยู่สำหรับทุกคลาสหรือเฉพาะที่มีฟังก์ชันเสมือนอย่างน้อยหนึ่งฟังก์ชัน?

เฉพาะฟังก์ชันที่มีฟังก์ชันเสมือนอย่างน้อยหนึ่งฟังก์ชัน (ไม่ว่าจะเป็นตัวทำลาย) หรือได้มาอย่างน้อยหนึ่งคลาสที่มี vtable ("คือความหลากหลาย")

คลาสนามธรรมมีค่า NULL สำหรับตัวชี้ฟังก์ชันอย่างน้อยหนึ่งรายการหรือไม่?

นั่นเป็นการนำไปใช้งานได้ แต่ไม่ได้รับการฝึกฝน แต่มักจะมีฟังก์ชั่นที่พิมพ์บางอย่างเช่น "ฟังก์ชั่นเสมือนบริสุทธิ์เรียกว่า" abort()และไม่ได้ การเรียกที่อาจเกิดขึ้นหากคุณพยายามเรียกวิธีนามธรรมในตัวสร้างหรือตัวทำลาย

การมีฟังก์ชันเสมือนเดียวทำให้ทั้งชั้นเรียนช้าลงหรือไม่? หรือเฉพาะการเรียกใช้ฟังก์ชันที่เสมือนจริง? และความเร็วจะได้รับผลกระทบหรือไม่หากฟังก์ชันเสมือนถูกเขียนทับจริงหรือไม่หรือจะไม่มีผลตราบใดที่เป็นเสมือน

การชะลอตัวขึ้นอยู่กับว่าการโทรได้รับการแก้ไขเป็นการโทรโดยตรงหรือเป็นการโทรเสมือน และไม่มีอะไรอื่นที่สำคัญ. :)

หากคุณเรียกใช้ฟังก์ชันเสมือนผ่านตัวชี้หรือการอ้างอิงถึงอ็อบเจ็กต์ฟังก์ชันนั้นจะถูกนำไปใช้เป็นการโทรเสมือนเสมอเนื่องจากคอมไพลเลอร์ไม่สามารถทราบได้ว่าจะกำหนดอ็อบเจ็กต์ประเภทใดให้กับพอยน์เตอร์นี้ในรันไทม์และไม่ว่าจะเป็นของ คลาสที่วิธีนี้ถูกแทนที่หรือไม่ มีเพียงสองกรณีเท่านั้นที่คอมไพลเลอร์สามารถแก้ไขการเรียกใช้ฟังก์ชันเสมือนเป็นการโทรโดยตรง:

  • หากคุณเรียกใช้เมธอดผ่านค่า (ตัวแปรหรือผลลัพธ์ของฟังก์ชันที่ส่งคืนค่า) - ในกรณีนี้คอมไพเลอร์ไม่สงสัยว่าคลาสที่แท้จริงของอ็อบเจ็กต์คืออะไรและสามารถ "แก้ไข" ได้ในเวลาคอมไพล์ .
  • หากวิธีการเสมือนถูกประกาศfinalในคลาสที่คุณมีตัวชี้หรือการอ้างอิงที่คุณเรียกใช้ ( เฉพาะใน C ++ 11 ) ในกรณีนี้คอมไพลเลอร์รู้ว่าเมธอดนี้ไม่สามารถทำการลบล้างใด ๆ ได้อีกและเป็นวิธีการจากคลาสนี้เท่านั้น

โปรดทราบว่าการโทรเสมือนมีค่าใช้จ่ายในการยกเลิกการอ้างถึงตัวชี้สองตัวเท่านั้น การใช้ RTTI (แม้ว่าจะใช้ได้เฉพาะกับคลาส polymorphic) จะช้ากว่าการเรียกใช้วิธีการเสมือนจริงคุณควรหากรณีที่จะใช้สิ่งเดียวกันสองวิธีดังกล่าว ยกตัวอย่างเช่นการกำหนดvirtual bool HasHoof() { return false; }แล้วแทนที่เป็นเพียงbool Horse::HasHoof() { return true; }จะช่วยให้คุณมีความสามารถในการเรียกร้องที่จะเร็วขึ้นกว่าการพยายามif (anim->HasHoof()) if(dynamic_cast<Horse*>(anim))เนื่องจากdynamic_castต้องเดินผ่านลำดับชั้นของคลาสในบางกรณีถึงกับวนซ้ำเพื่อดูว่าสามารถสร้างเส้นทางจากประเภทตัวชี้จริงและประเภทคลาสที่ต้องการได้หรือไม่ แม้ว่าการโทรเสมือนจะเหมือนกันเสมอ - การอ้างถึงตัวชี้สองตัว


2

นี่คือการใช้งานตารางเสมือนด้วยตนเองที่รันได้ใน 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
}

1

แต่ละวัตถุมีตัวชี้ vtable ที่ชี้ไปยังอาร์เรย์ของฟังก์ชันสมาชิก


1

สิ่งที่ไม่ได้กล่าวถึงในที่นี้ในคำตอบทั้งหมดนี้คือในกรณีของการสืบทอดหลายรายการโดยที่คลาสพื้นฐานทั้งหมดมีวิธีการเสมือน คลาสที่สืบทอดมีพอยน์เตอร์หลายตัวไปยัง vmt ผลลัพธ์ก็คือขนาดของแต่ละอินสแตนซ์ของวัตถุนั้นใหญ่ขึ้น ทุกคนรู้ว่าคลาสที่มีเมธอดเสมือนมี 4 ไบต์พิเศษสำหรับ vmt แต่ในกรณีที่มีการสืบทอดหลายรายการสำหรับคลาสพื้นฐานแต่ละคลาสที่มีเมธอดเสมือนคูณ 4 4 เป็นขนาดของตัวชี้


0

คำตอบของ Burly ถูกต้องที่นี่ยกเว้นคำถาม:

คลาสนามธรรมมีค่า NULL สำหรับตัวชี้ฟังก์ชันอย่างน้อยหนึ่งรายการหรือไม่?

คำตอบคือไม่มีการสร้างตารางเสมือนสำหรับคลาสนามธรรมเลย ไม่จำเป็นเพราะไม่สามารถสร้างวัตถุของคลาสเหล่านี้ได้!

กล่าวอีกนัยหนึ่งถ้าเรามี:

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

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

ตัวชี้ vtbl ที่เข้าถึงผ่าน pB จะเป็น vtbl ของคลาส D นี่คือวิธีการใช้ความหลากหลาย นั่นคือวิธีการเข้าถึงวิธี D ผ่าน pB ไม่จำเป็นต้องมี vtbl สำหรับคลาส B

ตามหลังไมค์คอมเม้นด้านล่าง ...

ถ้าคลาส B ในคำอธิบายของฉันมีเมธอดเสมือนfoo ()ที่ไม่ถูกแทนที่โดย D และแถบวิธีการเสมือน()ที่ถูกแทนที่ D's vtbl จะมีตัวชี้ไปที่ B ของfoo ()และไปยังแถบของมันเอง() . ยังไม่มี vtbl สร้างให้บี


สิ่งนี้ไม่ถูกต้องด้วยเหตุผล 2 ประการ: 1) คลาสนามธรรมอาจมีเมธอดเสมือนปกตินอกเหนือจากเมธอดเสมือนจริงและ 2) เมธอดเสมือนจริงอาจเลือกนิยามที่สามารถเรียกได้ด้วยชื่อที่มีคุณสมบัติครบถ้วน
Michael Burr

ถูกต้อง - ในความคิดที่สองฉันจินตนาการว่าหากวิธีการเสมือนจริงทั้งหมดเป็นเสมือนจริงคอมไพเลอร์อาจปรับแต่ง vtable ให้เหมาะสมที่สุด (จะต้องมีความช่วยเหลือในรูปแบบตัวเชื่อมโยงเพื่อให้แน่ใจว่าไม่มีคำจำกัดความเช่นกัน)
Michael Burr

1
" คำตอบคือไม่มีการสร้างตารางเสมือนสำหรับคลาสนามธรรม " ผิด " ไม่จำเป็นเพราะไม่สามารถสร้างวัตถุของคลาสเหล่านี้ได้! " ผิด
ซอกแซก

ผมสามารถทำตามเหตุผลของคุณที่ไม่มี vtable B ควรมีความจำเป็น เพียงเพราะวิธีการบางอย่างมีการใช้งาน (ค่าเริ่มต้น) ไม่ได้หมายความว่าจะต้องเก็บไว้ใน vtable แต่ฉันเพิ่งรันโค้ดของคุณ (โมดูโลแก้ไขบางอย่างเพื่อให้คอมไพล์) gcc -Sตามด้วยc++filtและมี vtable สำหรับBรวมอยู่ในนั้นอย่างชัดเจน ฉันเดาว่าอาจเป็นเพราะ vtable ยังเก็บข้อมูล RTTI เช่นชื่อคลาสและการสืบทอด อาจจำเป็นสำหรับไฟล์dynamic_cast<B*>. แม้-fno-rttiจะไม่ทำให้ vtable หายไป ด้วยclang -O3แทนgccมันหายไปอย่างกระทันหัน
MvG

@MvG " เพียงเพราะวิธีการบางอย่างมีการใช้งาน (ค่าเริ่มต้น) ไม่ได้หมายความว่าจะต้องเก็บไว้ใน vtable " ใช่มันหมายความแค่นั้น
ซอกแซก

0

การพิสูจน์แนวคิดที่น่ารักมากที่ฉันทำไว้ก่อนหน้านี้ (เพื่อดูว่าลำดับการสืบทอดมีความสำคัญหรือไม่); แจ้งให้เราทราบหากการใช้งาน 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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.