Ch19: Polymorphism¶
TL;DR: Dynamic (Late) Binding: ฟังก์ชันเสมือน (
virtual) บังคับให้ Compiler ชะลอการเลือกลิงก์เมธอดไปประเมินผลในช่วง Runtime เพื่อให้มั่นใจว่าประมวลผลตามชนิดข้อมูล Dynamic ของออบเจกต์จริง ณ ขณะนั้น
⚡ Quick Reference¶
#include <iostream>
#include <memory>
#include <typeinfo>
class Base {
public:
virtual void draw() const { std::cout << "Base draw" << std::endl; }
virtual ~Base() = default;
};
class Derived : public Base {
public:
void draw() const override { std::cout << "Derived draw" << std::endl; }
~Derived() override final = default;
};
class AbstractInterface {
public:
virtual void execute() = 0;
virtual ~AbstractInterface() = default;
};
int main() {
Derived d;
Base* ptr = &d;
ptr->draw();
Derived* p_derived = dynamic_cast<Derived*>(ptr);
std::cout << typeid(*ptr).name() << std::endl;
return 0;
}
🧠 Core Concepts¶
- Dynamic (Late) Binding: ฟังก์ชันเสมือน (
virtual) บังคับให้ Compiler ชะลอการเลือกลิงก์เมธอดไปประเมินผลในช่วง Runtime เพื่อให้มั่นใจว่าประมวลผลตามชนิดข้อมูล Dynamic ของออบเจกต์จริง ณ ขณะนั้น - Object Slicing: การกำหนดค่าออบเจกต์คลาสลูกให้กับตัวแปรประเภทคลาสแม่ตรงๆ จะตัดส่วนข้อมูลเฉพาะของคลาสลูกทิ้ง (Object Slicing) ระบบ Polymorphism จะทำงานได้เฉพาะผ่าน Pointer หรือ Reference เท่านั้น
- Abstract Interface Hierarchies: Pure Virtual Function (
= 0) ใช้สร้างคลาส Abstract ที่ทำหน้าที่เสมือน Interface สัญญารูปแบบคำสั่งโดยไม่สามารถนำมาสร้างออบเจกต์ตรงๆ ได้ บังคับให้คลาสลูกต้องนำไปเขียนเนื้อหาต่อเอง
⚠️ Pitfalls (Quick Scan)¶
| ข้อผิดพลาด | วิธีแก้ |
|---|---|
| ไม่ระบุ Destructor คลาสฐานให้เป็นแบบ Virtual ในคลาสพหุสัณฐาน | ประกาศ Destructor คลาสแม่เป็นแบบ virtual เสมอ |
| การโคลนคลาสลูกใส่ตัวแปรชนิดคลาสแม่ตรงๆ (Object Slicing) | เรียกใช้งานผ่านตัวแปรแบบ Pointer (Shape*) หรือ Reference (Shape&) เท่านั้น |
เรียกใช้คำสั่ง dynamic_cast กับคลาสที่ไม่มีระบบเสมือน (Non-polymorphic) |
ตรวจสอบให้มั่นใจว่าคลาสฐานมี Virtual Destructor หรือ Virtual Method |
เรียกใช้คำสั่ง typeid บนตัวแปรประเภท Pointer โดยตรง |
ทำการ Dereference พอยน์เตอร์ก่อนเรียกใช้งาน: typeid(*ptr) |
| กำหนดค่าเริ่มต้นพารามิเตอร์ (Default Argument) ให้กับฟังก์ชัน Overrides | หลีกเลี่ยงการกำหนดค่า Default ให้กับฟังก์ชันเสมือน |
เข้าใจผิดว่า final และ override เป็นคีย์เวิร์ดสงวนถาวรในทุกสภาวะ |
ใช้สองคำนี้เฉพาะจุดประสงค์เปรียบเทียบในคลาส เลี่ยงนำไปตั้งชื่อตัวแปร |
| สร้างตัวแปร Static ซ้ำกันระหว่างคลาสฐานและคลาสสืบทอด | หลีกเลี่ยงการเรียกใช้งาน Static Member ผ่านพอยน์เตอร์พหุสัณฐาน |
| พยายามประกาศใช้งานสร้างวัตถุจาก Abstract Class ตรงๆ | ใช้งาน Abstract Class เฉพาะเป็นคลาสฐานสำหรับการอ้างอิงพอยน์เตอร์และเรฟเฟอเรนซ์ |
| ประกาศ Destructor เสมือนเป็นแบบ private ในคลาสแม่ | ตรวจสอบให้มั่นใจว่า Destructor เสมือนอยู่ในระดับสิทธิ์ public |
📖 Full Details¶
Cause → Effect → Fix พร้อม timestamp (คลิกเพื่อดู)
* **ไม่ระบุ Destructor คลาสฐานให้เป็นแบบ Virtual ในคลาสพหุสัณฐาน** -> **สั่งทำลายออบเจกต์ผ่านพอยน์เตอร์คลาสแม่จะข้ามเนื้อหาทำลายคลาสลูก ทำให้เกิดหน่วยความจำรั่วไหล** -> **ประกาศ Destructor คลาสแม่เป็นแบบ `virtual` เสมอ (37:15)** * **การโคลนคลาสลูกใส่ตัวแปรชนิดคลาสแม่ตรงๆ (Object Slicing)** -> **ข้อมูลตัวแปรสมาชิกและพฤติกรรมเสมือนเฉพาะของคลาสลูกจะถูกเฉือนทิ้ง** -> **เรียกใช้งานผ่านตัวแปรแบบ Pointer (`Shape*`) หรือ Reference (`Shape&`) เท่านั้น (37:4)** * **เรียกใช้คำสั่ง `dynamic_cast` กับคลาสที่ไม่มีระบบเสมือน (Non-polymorphic)** -> **เกิด Compile-time error เนื่องจาก dynamic cast จำเป็นต้องมีเมธอด virtual อย่างน้อยหนึ่งตัวในคลาสฐาน** -> **ตรวจสอบให้มั่นใจว่าคลาสฐานมี Virtual Destructor หรือ Virtual Method (37:16)** * **เรียกใช้คำสั่ง `typeid` บนตัวแปรประเภท Pointer โดยตรง** -> **ได้ชนิดข้อมูลของตัวแปรพอยน์เตอร์เอง ไม่ใช่ชนิดข้อมูลจริงของวัตถุปลายทาง** -> **ทำการ Dereference พอยน์เตอร์ก่อนเรียกใช้งาน: `typeid(*ptr)` (37:18)** * **กำหนดค่าเริ่มต้นพารามิเตอร์ (Default Argument) ให้กับฟังก์ชัน Overrides** -> **อาร์กิวเมนต์เริ่มต้นจะถูกยึดโยงตามชนิดข้อมูลตัวแปรของพอยน์เตอร์ (Static type) ไม่ใช่วัตถุจริง** -> **หลีกเลี่ยงการกำหนดค่า Default ให้กับฟังก์ชันเสมือน (37:14)** * **เข้าใจผิดว่า `final` และ `override` เป็นคีย์เวิร์ดสงวนถาวรในทุกสภาวะ** -> **สามารถนำมาใช้ตั้งชื่อตัวแปรทั่วไปได้ในบางสโคป แต่อาจทำให้โค้ดอ่านยากและสับสน** -> **ใช้สองคำนี้เฉพาะจุดประสงค์เปรียบเทียบในคลาส เลี่ยงนำไปตั้งชื่อตัวแปร (37:11)** * **สร้างตัวแปร Static ซ้ำกันระหว่างคลาสฐานและคลาสสืบทอด** -> **ตัวแปรจะแยกสิทธิ์และบดบังกัน ไม่ได้แชร์ร่วมกันแบบพหุสัณฐาน** -> **หลีกเลี่ยงการเรียกใช้งาน Static Member ผ่านพอยน์เตอร์พหุสัณฐาน (37:9)** * **พยายามประกาศใช้งานสร้างวัตถุจาก Abstract Class ตรงๆ** -> **เกิด Compile-time error เนื่องจากคลาสมีฟังก์ชันเสมือนบริสุทธิ์ที่ยังไม่มีนิยามครบถ้วน** -> **ใช้งาน Abstract Class เฉพาะเป็นคลาสฐานสำหรับการอ้างอิงพอยน์เตอร์และเรฟเฟอเรนซ์ (37:19)** * **ประกาศ Destructor เสมือนเป็นแบบ private ในคลาสแม่** -> **คำสั่งภายนอกจะไม่สามารถ delete พอยน์เตอร์คลาสแม่ได้** -> **ตรวจสอบให้มั่นใจว่า Destructor เสมือนอยู่ในระดับสิทธิ์ public (37:19)**📎 Repo Files¶
37.2StaticBindingWithInheritance/main.cpp37.3PolymorphismWithVirtualFunctions/main.cpp37.4SizeOfPolymorphicObjectsAndSlicing/main.cpp37.5PolymorphicObjectsStoredInCollections/main.cpp37.6Override/main.cpp37.7OverloadingOverridingAndHiding/main.cpp37.8PolymorphismAtDifferentLevels/main.cpp37.9InheritanceAndPolymorphismWithStaticMembers/main.cpp37.10Final/main.cpp37.11FinalAndOverrideAreNotKeywords/main.cpp37.12PolymorphicFunctionsAndAccessSpecifiers/main.cpp37.13NonPolymorphicFunctionsAndAccessSpecifiers/main.cpp37.14VirtualFunctionsWithDefaultArguments/main.cpp37.15VirtualDestructors/main.cpp37.16DynamicCasts/main.cpp37.17PolymorphicFunctionsAndDestructors/main.cpp37.18TypeIdOperator/main.cpp37.19PureVirtualFunctionsAndAbstractClasses/main.cpp37.20AbstractClassesAsInterfaces/main.cpp