เหตุใดเราจึงต้องการฟังก์ชันเสมือนใน C ++


1312

ฉันกำลังเรียนรู้ C ++ และฉันเพิ่งเข้าสู่ฟังก์ชั่นเสมือนจริง

จากสิ่งที่ฉันได้อ่าน (ในหนังสือและออนไลน์) ฟังก์ชันเสมือนเป็นฟังก์ชันในคลาสพื้นฐานที่คุณสามารถแทนที่ในคลาสที่ได้รับ

virtualแต่ก่อนหน้านี้ในหนังสือเล่มนี้เมื่อการเรียนรู้เกี่ยวกับมรดกพื้นฐานก็สามารถที่จะแทนที่ฟังก์ชั่นฐานในชั้นเรียนมาโดยไม่ต้องใช้

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


13
ฉันได้สร้างคำอธิบายที่ใช้งานได้จริงสำหรับฟังก์ชั่นเสมือนจริงที่นี่: nrecursions.blogspot.in/2015/06/…
Nav

4
นี่อาจเป็นประโยชน์ที่ใหญ่ที่สุดของฟังก์ชั่นเสมือนจริง - ความสามารถในการจัดโครงสร้างโค้ดของคุณในลักษณะที่คลาสที่ได้รับใหม่จะทำงานกับโค้ดเก่าโดยอัตโนมัติโดยไม่ต้องดัดแปลง!
user3530616

tbh, ฟังก์ชั่นเสมือนเป็นคุณสมบัติหลักของ OOP สำหรับการลบประเภท ฉันคิดว่ามันเป็นวิธีการที่ไม่เสมือนเป็นสิ่งที่ทำให้ Object Pascal และ C ++ พิเศษเป็นการเพิ่มประสิทธิภาพของ vtable ขนาดใหญ่ที่ไม่จำเป็นและช่วยให้เรียน POD เข้ากันได้ ภาษา OOP หลายคนคาดว่าทุกวิธีสามารถเอาชนะได้
Swift - Friday Pie

นี่เป็นคำถามที่ดี อันที่จริงสิ่งนี้ใน C ++ ได้รับการแยกออกเป็นภาษาอื่น ๆ เช่น Java หรือ PHP ใน C ++ คุณจะได้รับการควบคุมเพิ่มขึ้นเล็กน้อยสำหรับบางกรณีที่หายาก (โปรดระวังการสืบทอดหลายรายการหรือกรณีพิเศษของDDOD ) แต่ทำไมคำถามนี้ถึงถูกโพสต์บน stackoverflow.com
Edgar Alloro

ฉันคิดว่าถ้าคุณดูที่การผูกมัดก่อนกำหนดและการโยง VTABLE มันจะสมเหตุสมผลและสมเหตุสมผลมากกว่า ดังนั้นจึงมีคำอธิบายที่ดี ( learncpp.com/cpp-tutorial/125-the-virtual-table ) ที่นี่
ceyun

คำตอบ:


2729

นี่คือวิธีที่ฉันเข้าใจไม่ใช่แค่ว่าvirtualฟังก์ชั่นคืออะไรแต่ทำไมมันถึงต้องการ:

สมมติว่าคุณมีสองคลาสเหล่านี้:

class Animal
{
    public:
        void eat() { std::cout << "I'm eating generic food."; }
};

class Cat : public Animal
{
    public:
        void eat() { std::cout << "I'm eating a rat."; }
};

ในหน้าที่หลักของคุณ:

Animal *animal = new Animal;
Cat *cat = new Cat;

animal->eat(); // Outputs: "I'm eating generic food."
cat->eat();    // Outputs: "I'm eating a rat."

จนถึงตอนนี้ดีใช่มั้ย virtualสัตว์กินอาหารทั่วไปแมวกินหนูทั้งหมดโดยไม่ต้อง

ตอนนี้เปลี่ยนมันนิดหน่อยเพื่อให้eat()ถูกเรียกผ่านฟังก์ชั่นระดับกลาง (ฟังก์ชั่นเล็กน้อยสำหรับตัวอย่างนี้):

// This can go at the top of the main.cpp file
void func(Animal *xyz) { xyz->eat(); }

ตอนนี้หน้าที่หลักของเราคือ:

Animal *animal = new Animal;
Cat *cat = new Cat;

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating generic food."

เอ่อโอ้ ... พวกเราผ่านแคทเข้าไปfunc()แต่มันจะไม่กินหนู คุณควรเกินfunc()จึงใช้เวลาCat*? func()ถ้าคุณมีที่จะได้รับเพิ่มเติมจากสัตว์สัตว์พวกเขาทั้งหมดจะต้องเป็นของตัวเอง

การแก้ปัญหาคือการสร้างฟังก์ชั่นเสมือนจริงeat()จากAnimalคลาส:

class Animal
{
    public:
        virtual void eat() { std::cout << "I'm eating generic food."; }
};

class Cat : public Animal
{
    public:
        void eat() { std::cout << "I'm eating a rat."; }
};

หลัก:

func(animal); // Outputs: "I'm eating generic food."
func(cat);    // Outputs: "I'm eating a rat."

เสร็จสิ้น


165
ดังนั้นหากฉันเข้าใจสิ่งนี้อย่างถูกต้องเสมือนอนุญาตให้เรียกใช้คลาสย่อยวิธีแม้ว่าวัตถุจะถูกถือว่าเป็น superclass ของมัน
Kenny Worden

147
แทนที่จะอธิบายการผูกปลายผ่านตัวอย่างของฟังก์ชั่นตัวกลาง "func" นี่คือการสาธิตที่ตรงไปตรงมามากขึ้น - สัตว์ * สัตว์ = สัตว์ใหม่ // Cat * cat = new cat; สัตว์ * cat = new cat; สัตว์> กิน (); // ผลลัพธ์: "ฉันกำลังกินอาหารทั่วไป" cat-> กิน (); // ผลลัพธ์: "ฉันกำลังกินอาหารทั่วไป" แม้ว่าคุณจะกำหนดวัตถุ subclassed (Cat) แต่วิธีการที่เรียกใช้นั้นขึ้นอยู่กับประเภทของตัวชี้ (สัตว์) ไม่ใช่ประเภทของวัตถุที่ชี้ไป นี่คือเหตุผลที่คุณต้องการ "เสมือน"
rexbelia

37
ฉันเป็นคนเดียวที่พบพฤติกรรมเริ่มต้นนี้ใน C ++ เพียงแปลกหรือไม่ ฉันคาดว่าจะใช้รหัสโดยไม่มี "virtual" ในการทำงาน
เดวิด天宇วงศ์

20
@ David 天宇วงศ์ฉันคิดว่าvirtualมีการเชื่อมโยงแบบไดนามิกและคงที่ใช่ใช่มันแปลกถ้าคุณมาจากภาษาเช่น Java
peterchaula

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

672

หากไม่มี "เสมือน" คุณจะได้รับ "การรวมก่อนหน้า" การใช้วิธีการแบบใดที่ได้รับการตัดสินใจ ณ เวลารวบรวมตามประเภทของตัวชี้ที่คุณโทรหา

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

class Base
{
  public:
            void Method1 ()  {  std::cout << "Base::Method1" << std::endl;  }
    virtual void Method2 ()  {  std::cout << "Base::Method2" << std::endl;  }
};

class Derived : public Base
{
  public:
    void Method1 ()  {  std::cout << "Derived::Method1" << std::endl;  }
    void Method2 ()  {  std::cout << "Derived::Method2" << std::endl;  }
};

Base* obj = new Derived ();
  //  Note - constructed as Derived, but pointer stored as Base*

obj->Method1 ();  //  Prints "Base::Method1"
obj->Method2 ();  //  Prints "Derived::Method2"

แก้ไข - ดูคำถามนี้

นอกจากนี้ - บทช่วยสอนนี้ครอบคลุมการผูกต้นและล่าช้าใน C ++


11
ยอดเยี่ยมและกลับถึงบ้านอย่างรวดเร็วพร้อมกับตัวอย่างที่ดีกว่า นี้เป็นอย่างไรง่ายและถามจริงๆก็ควรอ่านหน้าparashift.com/c++-faq-lite/virtual-functions.html คนอื่น ๆ ได้ชี้ไปที่แหล่งข้อมูลในบทความ SO ที่เชื่อมโยงจากหัวข้อนี้แล้ว แต่ฉันเชื่อว่านี่เป็นสิ่งที่ควรพูดถึง
Sonny

36
ฉันไม่รู้ว่าการผูกพันแต่เนิ่นๆหรือช้ากว่าเป็นคำที่ใช้เฉพาะในชุมชน c ++ แต่เงื่อนไขที่ถูกต้องนั้นเป็นแบบสแตติก (ในเวลารวบรวม) และการเชื่อมโยงแบบไดนามิก
ไมค์

31
@ ไมค์ - "คำว่า" ล่าช้าผูกพัน "วันที่กลับไปอย่างน้อยยุค 60 ซึ่งสามารถพบได้ใน Communications of the ACM" . มันจะไม่ดีถ้ามีหนึ่งคำที่ถูกต้องสำหรับแต่ละแนวคิด? น่าเสียดายที่มันไม่เป็นเช่นนั้น คำว่า "Early binding" และ "Late binding" ลงวันที่ล่วงหน้า C ++ และแม้กระทั่งการเขียนโปรแกรมเชิงวัตถุและถูกต้องตามเงื่อนไขที่คุณใช้
Steve314

4
@BJovke - คำตอบนี้ถูกเขียนก่อนที่จะเผยแพร่ C ++ 11 ถึงกระนั้นฉันก็รวบรวมมันใน GCC 6.3.0 (ใช้ C ++ 14 โดยค่าเริ่มต้น) โดยไม่มีปัญหา - การห่อประกาศตัวแปรและการเรียกใช้ในmainฟังก์ชั่น ฯลฯเห็นได้ชัดว่าตัวชี้ไปยังที่ได้มาโดยนัยชี้ไปที่ฐาน (เฉพาะเจาะจงมากขึ้นมันปลดเปลื้องไปทั่วไปมากขึ้น) dynamic_castวีซ่าในทางกลับกันคุณจำเป็นต้องมีนักแสดงอย่างชัดเจนมักจะเป็น สิ่งอื่นใด - มีแนวโน้มที่จะมีพฤติกรรมที่ไม่ได้กำหนดดังนั้นให้แน่ใจว่าคุณรู้ว่าคุณกำลังทำอะไร เพื่อความรู้ที่ดีที่สุดของฉันสิ่งนี้ไม่ได้เปลี่ยนแปลงไปตั้งแต่ก่อนแม้แต่ C ++ 98
Steve314

10
โปรดทราบว่าในปัจจุบันคอมไพเลอร์ C ++ สามารถเพิ่มประสิทธิภาพในช่วงท้ายของการรวมก่อนหน้าได้เมื่อพวกเขาสามารถแน่ใจได้ว่าการรวมจะเป็นอย่างไร สิ่งนี้เรียกอีกอย่างว่า "de-virtualization"
einpoklum

83

คุณต้องการการสืบทอดอย่างน้อย 1 ระดับและการดาวน์สตรีมเพื่อแสดงให้เห็น นี่เป็นตัวอย่างง่ายๆ:

class Animal
{        
    public: 
      // turn the following virtual modifier on/off to see what happens
      //virtual   
      std::string Says() { return "?"; }  
};

class Dog: public Animal
{
    public: std::string Says() { return "Woof"; }
};

void test()
{
    Dog* d = new Dog();
    Animal* a = d;       // refer to Dog instance with Animal pointer

    std::cout << d->Says();   // always Woof
    std::cout << a->Says();   // Woof or ?, depends on virtual
}

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

7
กับคำเสมือน: โฮ่ง ไม่มีคำหลักเสมือนจริง: ? .
Hesham Eraqi

@HeshamEraqi โดยไม่ต้องเสมือนมันเป็นช่วงต้นที่มีผลผูกพันและมันจะแสดง "?" ของชั้นฐาน
Ahmad

46

คุณจำเป็นต้องมีวิธีการที่เสมือนจริงสำหรับdowncasting ปลอดภัย , ความเรียบง่ายและกระชับ

นั่นคือสิ่งที่วิธีการเสมือนทำ: พวกมันซ่อนเร้นอย่างปลอดภัยด้วยรหัสที่เรียบง่ายและรัดกุมและหลีกเลี่ยงการใช้งานที่ไม่ปลอดภัยในโค้ดที่ซับซ้อนและ verbose มากกว่าที่คุณจะมี


วิธีการที่ไม่ใช่เสมือน binding การรวมคงที่

รหัสต่อไปนี้ตั้งใจ“ ไม่ถูกต้อง” มันไม่ได้ประกาศvalueวิธีการเป็นvirtualและดังนั้นจึงก่อให้เกิดผลลัพธ์ "ผิด" ที่ไม่ได้ตั้งใจคือ 0:

#include <iostream>
using namespace std;

class Expression
{
public:
    auto value() const
        -> double
    { return 0.0; }         // This should never be invoked, really.
};

class Number
    : public Expression
{
private:
    double  number_;

public:
    auto value() const
        -> double
    { return number_; }     // This is OK.

    Number( double const number )
        : Expression()
        , number_( number )
    {}
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;

public:
    auto value() const
        -> double
    { return a_->value() + b_->value(); }       // Uhm, bad! Very bad!

    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    {}
};

auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );

    cout << sum.value() << endl;
}

ในบรรทัดที่แสดงความคิดเห็นว่า“ ไม่ดี” Expression::valueวิธีการนั้นถูกเรียกใช้เนื่องจากชนิดที่รู้จักแบบสแตติก (ชนิดที่รู้จักในเวลาคอมไพล์) คือExpressionและvalueวิธีการนั้นไม่เสมือน


วิธีเสมือน⇒การเชื่อมแบบไดนามิก

ประกาศvalueเป็นvirtualในแบบคงที่รู้จักกันชนิดExpressionเพื่อให้แน่ใจว่าการเรียกแต่ละคนจะตรวจสอบสิ่งที่เกิดขึ้นจริงชนิดของวัตถุนี้และเรียกใช้งานที่เกี่ยวข้องของvalueการที่ประเภทแบบไดนามิก :

#include <iostream>
using namespace std;

class Expression
{
public:
    virtual
    auto value() const -> double
        = 0;
};

class Number
    : public Expression
{
private:
    double  number_;

public:
    auto value() const -> double
        override
    { return number_; }

    Number( double const number )
        : Expression()
        , number_( number )
    {}
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;

public:
    auto value() const -> double
        override
    { return a_->value() + b_->value(); }    // Dynamic binding, OK!

    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    {}
};

auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );

    cout << sum.value() << endl;
}

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

การใช้งานที่เกี่ยวข้องเป็นหนึ่งในชั้นเรียนที่เฉพาะเจาะจงที่สุด (มามากที่สุด)

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


ความอัปลักษณ์ในการทำเช่นนี้โดยไม่มีวิธีเสมือน

หากไม่มีvirtualใครจะต้องใช้เวอร์ชันของDo It Yourselfของการเชื่อมแบบไดนามิก นี่คือสิ่งที่โดยทั่วไปเกี่ยวข้องกับการ downcasting ด้วยตนเองที่ไม่ปลอดภัยความซับซ้อนและการใช้คำฟุ่มเฟือย

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

#include <iostream>
using namespace std;

class Expression
{
protected:
    typedef auto Value_func( Expression const* ) -> double;

    Value_func* value_func_;

public:
    auto value() const
        -> double
    { return value_func_( this ); }

    Expression(): value_func_( nullptr ) {}     // Like a pure virtual.
};

class Number
    : public Expression
{
private:
    double  number_;

    static
    auto specific_value_func( Expression const* expr )
        -> double
    { return static_cast<Number const*>( expr )->number_; }

public:
    Number( double const number )
        : Expression()
        , number_( number )
    { value_func_ = &Number::specific_value_func; }
};

class Sum
    : public Expression
{
private:
    Expression const*   a_;
    Expression const*   b_;

    static
    auto specific_value_func( Expression const* expr )
        -> double
    {
        auto const p_self  = static_cast<Sum const*>( expr );
        return p_self->a_->value() + p_self->b_->value();
    }

public:
    Sum( Expression const* const a, Expression const* const b )
        : Expression()
        , a_( a )
        , b_( b )
    { value_func_ = &Sum::specific_value_func; }
};


auto main() -> int
{
    Number const    a( 3.14 );
    Number const    b( 2.72 );
    Number const    c( 1.0 );

    Sum const       sum_ab( &a, &b );
    Sum const       sum( &sum_ab, &c );

    cout << sum.value() << endl;
}

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


40

ฟังก์ชั่นเสมือนจริงที่ใช้ในการสนับสนุนRuntime Polymorphism

นั่นคือเสมือนคำหลักบอกคอมไพเลอร์จะไม่ทำให้การตัดสินใจ (จากฟังก์ชั่นที่มีผลผูกพัน) ที่รวบรวมเวลาค่อนข้างเลื่อนมันสำหรับรันไทม์"

  • คุณสามารถสร้างฟังก์ชั่นเสมือนจริงโดยนำหน้าคีย์เวิร์ดvirtualในการประกาศคลาสพื้นฐาน ตัวอย่างเช่น,

     class Base
     {
        virtual void func();
     }
  • เมื่อคลาสพื้นฐานมีฟังก์ชันสมาชิกเสมือนคลาสใดก็ตามที่สืบทอดจากคลาสพื้นฐานสามารถกำหนดฟังก์ชันใหม่ได้ด้วยต้นแบบที่เหมือนกันทุกประการเช่นฟังก์ชันการทำงานเท่านั้นที่สามารถกำหนดได้ใหม่ไม่ใช่อินเทอร์เฟซของฟังก์ชัน

     class Derive : public Base
     {
        void func();
     }
  • ตัวชี้คลาสพื้นฐานสามารถใช้เพื่อชี้ไปที่วัตถุคลาสพื้นฐานเช่นเดียวกับวัตถุคลาสที่ได้รับ

  • เมื่อมีการเรียกใช้ฟังก์ชันเสมือนโดยใช้ตัวชี้คลาสพื้นฐานคอมไพเลอร์จะตัดสินใจในขณะใช้งานซึ่งเวอร์ชันของฟังก์ชัน - เช่นเวอร์ชันคลาสฐานหรือเวอร์ชันคลาสที่มาแทนที่ที่ถูกแทนที่ - จะถูกเรียกใช้ นี้เรียกว่าRuntime Polymorphism

34

ถ้าคลาสพื้นฐานคือBaseและคลาสที่ได้รับคือDerคุณสามารถมีBase *pตัวชี้ที่ชี้ไปยังอินสแตนซ์ของDerจริง เมื่อคุณโทรp->foo();ถ้าfooเป็นไม่ได้เสมือนจริงแล้วBase's รุ่นของมันรันไม่สนใจความจริงที่ว่าpจริง ๆ Derแล้วชี้ไปยัง หาก foo เป็นเสมือนจริงให้p->foo()เรียกใช้งานการแทนที่ "leafmost" fooโดยคำนึงถึงคลาสที่แท้จริงของไอเท็มชี้ไปยัง ดังนั้นความแตกต่างระหว่างเวอร์ชวลและไม่ใช่เวอร์ชวลจึงเป็นเรื่องสำคัญมาก: ในอดีตนั้นอนุญาตให้มีความหลากหลายในการทำงานซึ่งเป็นแนวคิดหลักของการเขียนโปรแกรม OO ในขณะที่หลังไม่ได้


8
ฉันเกลียดที่จะแย้งกับคุณ แต่ความแตกต่างในเวลาคอมไพล์ยังคงเป็นความหลากหลาย แม้การใช้งานที่ไม่ใช่สมาชิกมากเกินไปก็เป็นรูปแบบหนึ่งของความหลากหลาย - ad-hoc polymorphism โดยใช้คำศัพท์ในลิงค์ของคุณ ความแตกต่างที่นี่อยู่ระหว่างการรวมก่อนและหลัง
Steve314

7
@ Steve314 คุณถูกอวดรู้ (ในฐานะเพื่อนคนอวดรู้ฉันยอมรับว่า ;-) - แก้ไขคำตอบเพื่อเพิ่มคำคุณศัพท์ที่หายไป ;-)
Alex Martelli

26

อธิบายความต้องการฟังก์ชั่นเสมือนจริง [เข้าใจง่าย]

#include<iostream>

using namespace std;

class A{
public: 
        void show(){
        cout << " Hello from Class A";
    }
};

class B :public A{
public:
     void show(){
        cout << " Hello from Class B";
    }
};


int main(){

    A *a1 = new B; // Create a base class pointer and assign address of derived object.
    a1->show();

}

ผลลัพธ์จะเป็น:

Hello from Class A.

แต่ด้วยฟังก์ชั่นเสมือนจริง:

#include<iostream>

using namespace std;

class A{
public:
    virtual void show(){
        cout << " Hello from Class A";
    }
};

class B :public A{
public:
    virtual void show(){
        cout << " Hello from Class B";
    }
};


int main(){

    A *a1 = new B;
    a1->show();

}

ผลลัพธ์จะเป็น:

Hello from Class B.

ดังนั้นด้วยฟังก์ชั่นเสมือนคุณสามารถบรรลุความแตกต่างรันไทม์


25

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

VIRTUAL DESTRUCTOR

พิจารณาโปรแกรมด้านล่างนี้โดยไม่ต้องประกาศตัวทำลายคลาสพื้นฐานเป็นเสมือน หน่วยความจำสำหรับ Cat อาจไม่ได้รับการทำความสะอาด

class Animal {
    public:
    ~Animal() {
        cout << "Deleting an Animal" << endl;
    }
};
class Cat:public Animal {
    public:
    ~Cat() {
        cout << "Deleting an Animal name Cat" << endl;
    }
};

int main() {
    Animal *a = new Cat();
    delete a;
    return 0;
}

เอาท์พุท:

Deleting an Animal
class Animal {
    public:
    virtual ~Animal() {
        cout << "Deleting an Animal" << endl;
    }
};
class Cat:public Animal {
    public:
    ~Cat(){
        cout << "Deleting an Animal name Cat" << endl;
    }
};

int main() {
    Animal *a = new Cat();
    delete a;
    return 0;
}

เอาท์พุท:

Deleting an Animal name Cat
Deleting an Animal

11
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.มันแย่กว่านั้น การลบวัตถุที่ได้รับผ่านตัวชี้ / การอ้างอิงพื้นฐานนั้นเป็นพฤติกรรมที่ไม่ได้กำหนดอย่างแท้จริง ดังนั้นไม่ใช่ว่าหน่วยความจำบางส่วนอาจรั่ว แต่โปรแกรมนั้นมีรูปแบบไม่ดีดังนั้นคอมไพเลอร์อาจแปลงมันเป็นอะไรก็ได้: รหัสเครื่องที่เกิดขึ้นทำงานได้ดีหรือไม่ทำอะไรเลยหรือเรียกปีศาจจากจมูกของคุณหรืออื่น ๆ นั่นเป็นเหตุผลว่าทำไมโปรแกรมจึงถูกออกแบบด้วย วิธีที่ผู้ใช้บางคนอาจลบอินสแตนซ์ที่ได้รับผ่านการอ้างอิงพื้นฐานฐานต้องมี destructor เสมือนจริง
underscore_d

21

คุณต้องแยกแยะระหว่างการเอาชนะและการบรรทุกเกินพิกัด หากไม่มีvirtualคีย์เวิร์ดคุณจะโอเวอร์โหลดเมท็อดของคลาสพื้นฐานเท่านั้น มันไม่มีความหมายอะไรนอกจากซ่อน สมมติว่าคุณมีฐานชั้นBaseและชั้นเรียนมาซึ่งทั้งสองดำเนินการSpecialized void foo()ตอนนี้คุณมีตัวชี้ไปชี้ไปที่ตัวอย่างของBase Specializedเมื่อคุณเรียกfoo()ใช้คุณสามารถสังเกตเห็นความแตกต่างที่virtualเกิดขึ้น: หากวิธีนั้นเป็นเสมือนจริงการใช้งานSpecializedจะถูกนำมาใช้หากไม่พบBaseจะมีการเลือกเวอร์ชันจาก เป็นแนวปฏิบัติที่ดีที่สุดที่จะไม่โอเวอร์โหลดเมท็อดของคลาสพื้นฐาน การสร้างวิธีการที่ไม่ใช่เสมือนเป็นวิธีการของผู้เขียนที่จะบอกคุณว่าส่วนขยายในคลาสย่อยนั้นไม่ได้มีจุดประสงค์


3
หากไม่มีvirtualคุณจะไม่บรรทุกเกินพิกัด คุณกำลังแชโดว์ ถ้าคลาสพื้นฐานBมีหนึ่งฟังก์ชันขึ้นfooไปและคลาสที่ได้รับจะDกำหนดfooชื่อซึ่งfoo จะซ่อนfoo -s เหล่านั้นBทั้งหมด พวกเขามาถึงว่าB::fooใช้ความละเอียดขอบเขต เพื่อส่งเสริมB::fooการทำงานออกเป็นสำหรับการบรรทุกเกินพิกัดที่คุณต้องใช้D using B::foo
Kaz

20

ทำไมเราต้องใช้วิธีการเสมือนใน C ++

คำตอบที่รวดเร็ว:

  1. มันทำให้เรามีหนึ่งที่จำเป็น "ส่วนผสม" 1สำหรับการเขียนโปรแกรมเชิงวัตถุ

ในการเขียนโปรแกรม Bjarne Stroustrup C ++: หลักการและการปฏิบัติ (14.3):

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

  1. มันเป็นเรื่องที่ดำเนินการที่เร็วที่สุดที่มีประสิทธิภาพมากขึ้นถ้าคุณจำเป็นต้องมีการเรียกใช้ฟังก์ชันเสมือน 2

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


ใช้มรดกหลายรูปแบบเวลาทำงานและห่อหุ้ม 1. เป็นคำนิยามที่พบบ่อยที่สุดของการเขียนโปรแกรมเชิงวัตถุ

2. คุณไม่สามารถเขียนรหัสฟังก์ชันการทำงานให้เร็วขึ้นหรือใช้หน่วยความจำน้อยลงโดยใช้คุณสมบัติภาษาอื่นเพื่อเลือกระหว่างทางเลือกในขณะใช้งาน Bjarne Stroustrup c ++ เขียนโปรแกรม: หลักการและการปฏิบัติ (14.3.1).

3. สิ่งที่จะบอกได้ว่าฟังก์ชั่นใดถูกเรียกใช้จริง ๆ เมื่อเราเรียกคลาสฐานที่มีฟังก์ชันเสมือน


15

ฉันได้รับคำตอบในรูปแบบของบทสนทนาเพื่อการอ่านที่ดีขึ้น:


ทำไมเราถึงต้องการฟังก์ชั่นเสมือนจริง?

เนื่องจากความแตกต่าง

ความแตกต่างคืออะไร?

ความจริงที่ว่าตัวชี้ฐานยังสามารถชี้ไปยังวัตถุชนิดที่ได้รับ

ความหมายของความหลากหลายนี้นำไปสู่ความต้องการฟังก์ชั่นเสมือนจริงอย่างไร?

ผ่านการผูกไว้ล่วงหน้า

การรวมก่อนหน้าคืออะไร

การรวมก่อนหน้า (การคอมไพล์เวลาคอมไพล์) ใน C ++ หมายความว่าการเรียกใช้ฟังก์ชันได้รับการแก้ไขก่อนที่จะเรียกใช้งานโปรแกรม

ดังนั้น...?

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

หากไม่ใช่สิ่งที่เราต้องการจะเกิดขึ้นทำไมจึงได้รับอนุญาต

เพราะเราต้องการความแตกต่าง!

แล้ว Polymorphism นั้นมีประโยชน์อย่างไร?

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

ฉันยังไม่รู้ว่าฟังก์ชั่นเสมือนอะไรที่ดีสำหรับ ... ! และนี่เป็นคำถามแรกของฉัน!

นี่เป็นเพราะคุณถามคำถามของคุณเร็วเกินไป!

ทำไมเราถึงต้องการฟังก์ชั่นเสมือนจริง?

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

คำตอบสั้น ๆ คือ "ฟังก์ชั่นสมาชิกเสมือนในฐาน" และคำตอบอีกต่อไปก็คือ "ในขั้นตอนนี้ถ้าโปรแกรมเห็นฟังก์ชั่นเสมือนจริงในคลาสฐานจะรู้ (รู้ตัว) ว่าคุณกำลังพยายามใช้ polymorphism "และอื่น ๆ ไปที่คลาสที่ได้รับ (โดยใช้v-table , รูปแบบของการรวมภายหลัง) เพื่อค้นหาว่าวิธีอื่นที่มีส่วนหัวเดียวกัน

ทำไมการใช้งานที่แตกต่างกัน?

คุณสนับมือ! ไปอ่านหนังสือดี ๆ !

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

// 1:

Parent* p1 = &boy;
p1 -> task();
Parent* p2 = &girl;
p2 -> task();

// 2:

Boy* p1 = &boy;
p1 -> task();
Girl* p2 = &girl;
p2 -> task();

ตกลงแม้ว่าฉันคิดว่า1ยังดีกว่า2คุณสามารถเขียน1เช่นนี้:

// 1:

Parent* p1 = &boy;
p1 -> task();
p1 = &girl;
p1 -> task();

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

double totalMonthBenefit = 0;    
std::vector<CentralShop*> mainShop = { &shop1, &shop2, &shop3, &shop4, &shop5, &shop6};
for(CentralShop* x : mainShop){
     totalMonthBenefit += x -> getMonthBenefit();
}

ทีนี้ลองเขียนใหม่โดยไม่ปวดหัว!

double totalMonthBenefit=0;
Shop1* branch1 = &shop1;
Shop2* branch2 = &shop2;
Shop3* branch3 = &shop3;
Shop4* branch4 = &shop4;
Shop5* branch5 = &shop5;
Shop6* branch6 = &shop6;
totalMonthBenefit += branch1 -> getMonthBenefit();
totalMonthBenefit += branch2 -> getMonthBenefit();
totalMonthBenefit += branch3 -> getMonthBenefit();
totalMonthBenefit += branch4 -> getMonthBenefit();
totalMonthBenefit += branch5 -> getMonthBenefit();
totalMonthBenefit += branch6 -> getMonthBenefit();

และอันที่จริงนี่อาจเป็นตัวอย่างที่ถูกวางแผนไว้เช่นกัน!


2
แนวคิดของการวนซ้ำบนวัตถุ (sub-) ชนิดต่าง ๆ ที่ใช้ประเภทวัตถุ (super-) เดียวควรถูกเน้นซึ่งเป็นจุดที่ดีที่คุณให้ขอบคุณ
harshvchawla

14

เมื่อคุณมีฟังก์ชันในคลาสพื้นฐานคุณสามารถRedefineหรือOverrideในคลาสที่ได้รับมา

การกำหนดวิธีการใหม่: การใช้งานใหม่สำหรับวิธีการของคลาสฐานจะได้รับในระดับที่ได้รับ ไม่Dynamic bindingอำนวยความสะดวก

การเอาชนะเมธอด : Redefiningavirtual methodของคลาสฐานในคลาสที่ได้รับ วิธีเสมือนอำนวยความสะดวกในการผูกแบบไดนามิก

ดังนั้นเมื่อคุณพูดว่า:

แต่ก่อนหน้านี้ในหนังสือเมื่อเรียนรู้เกี่ยวกับการสืบทอดขั้นพื้นฐานฉันสามารถแทนที่วิธีการพื้นฐานในชั้นเรียนที่ได้รับโดยไม่ต้องใช้ 'เสมือน'

คุณไม่ได้เอาชนะมันในขณะที่วิธีการในคลาสพื้นฐานนั้นไม่ได้เป็นเสมือน แต่คุณกำลังกำหนดมันใหม่


11

มันจะช่วยถ้าคุณรู้กลไกพื้นฐาน C ++ ทำเทคนิคการเข้ารหัสบางอย่างที่ใช้โดยโปรแกรมเมอร์ C "คลาส" แทนที่ด้วย "ภาพซ้อนทับ" - โครงสร้างที่มีส่วนหัวทั่วไปจะถูกใช้เพื่อจัดการวัตถุประเภทต่าง ๆ แต่ด้วยข้อมูลหรือการดำเนินการทั่วไปบางอย่าง โดยปกติโครงสร้างพื้นฐานของโอเวอร์เลย์ (ส่วนทั่วไป) มีตัวชี้ไปยังตารางฟังก์ชันซึ่งชี้ไปที่ชุดคำสั่งที่แตกต่างกันสำหรับแต่ละประเภทของวัตถุ C ++ ทำสิ่งเดียวกัน แต่ซ่อนกลไกนั่นคือ C ++ ptr->func(...)โดยที่ func เป็นเสมือนอย่าง C ซึ่งจะมี(*ptr->func_table[func_num])(ptr,...)การเปลี่ยนแปลงระหว่างคลาสที่ได้รับมาคือเนื้อหา func_table [วิธีที่ไม่ใช่แบบเสมือน ptr-> func () เพียงแปลเป็น mangled_func (ptr, .. ).]

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


8

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

รายละเอียดเพิ่มเติมในลิงค์นี้ http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html


7

เสมือนคำหลักบังคับให้คอมไพเลอร์ที่จะเลือกดำเนินการวิธีการที่กำหนดไว้ในวัตถุชั้นมากกว่าในตัวชี้ระดับ

Shape *shape = new Triangle(); 
cout << shape->getName();

ในตัวอย่างข้างต้น Shape :: getName จะถูกเรียกใช้โดยค่าเริ่มต้นเว้นแต่ว่า getName () จะถูกกำหนดเป็นเสมือนในรูปร่างคลาสฐาน สิ่งนี้บังคับให้คอมไพเลอร์มองหาการใช้งาน getName () ในคลาส Triangle มากกว่าในคลาส Shape

ตารางเสมือนเป็นกลไกในการที่เรียบเรียงติดตามการใช้งานต่างๆเสมือนวิธีการคลาสย่อย นี้จะเรียกว่าการจัดส่งแบบไดนามิกและมีเป็นค่าใช้จ่ายบางอย่างที่เกี่ยวข้องกับมัน

ในที่สุดทำไมถึงต้องใช้เวอร์ชวลในซีพลัสพลัสทำไมจึงไม่ทำให้มันเป็นพฤติกรรมเริ่มต้นเช่นในจาวา

  1. C ++ ขึ้นอยู่กับหลักการของ "Zero Overhead" และ "Pay for what you use" ดังนั้นจึงไม่พยายามทำการจัดส่งแบบไดนามิกสำหรับคุณยกเว้นว่าคุณต้องการ
  2. เพื่อให้การควบคุมอินเทอร์เฟซมากขึ้น โดยการทำให้ฟังก์ชั่นที่ไม่ใช่เสมือนชั้น interface / นามธรรมสามารถควบคุมพฤติกรรมในการใช้งานทั้งหมด

4

ทำไมเราถึงต้องการฟังก์ชั่นเสมือนจริง?

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

ลองเปรียบเทียบโปรแกรมง่ายๆสองโปรแกรมเพื่อทำความเข้าใจถึงความสำคัญของฟังก์ชั่นเสมือนจริง:

โปรแกรมที่ไม่มีฟังก์ชั่นเสมือนจริง:

#include <iostream>
using namespace std;

class father
{
    public: void get_age() {cout << "Fathers age is 50 years" << endl;}
};

class son: public father
{
    public : void get_age() { cout << "son`s age is 26 years" << endl;}
};

int main(){
    father *p_father = new father;
    son *p_son = new son;

    p_father->get_age();
    p_father = p_son;
    p_father->get_age();
    p_son->get_age();
    return 0;
}

เอาท์พุท:

Fathers age is 50 years
Fathers age is 50 years
son`s age is 26 years

โปรแกรมที่มีฟังก์ชั่นเสมือนจริง:

#include <iostream>
using namespace std;

class father
{
    public:
        virtual void get_age() {cout << "Fathers age is 50 years" << endl;}
};

class son: public father
{
    public : void get_age() { cout << "son`s age is 26 years" << endl;}
};

int main(){
    father *p_father = new father;
    son *p_son = new son;

    p_father->get_age();
    p_father = p_son;
    p_father->get_age();
    p_son->get_age();
    return 0;
}

เอาท์พุท:

Fathers age is 50 years
son`s age is 26 years
son`s age is 26 years

โดยการวิเคราะห์อย่างใกล้ชิดทั้งผลลัพธ์ที่หนึ่งสามารถเข้าใจความสำคัญของฟังก์ชั่นเสมือนจริง


4

OOP คำตอบ: ความหลากหลายย่อย

ใน C ++ วิธีเสมือนจริงที่มีความจำเป็นที่จะตระหนักถึงความแตกต่างอย่างแม่นยำมากขึ้นSubtypingหรือชนิดย่อย polymorphismถ้าคุณใช้คำนิยามจากวิกิพีเดีย

Wikipedia, Subtyping, 2019-01-09: ในทฤษฎีภาษาโปรแกรมการพิมพ์ย่อย (เช่น polymorphism ชนิดย่อยหรือ polymorphism ที่รวม) เป็นรูปแบบหนึ่งของประเภท polymorphism ซึ่ง subtype นั้นเป็นประเภทข้อมูลที่เกี่ยวข้องกับประเภทข้อมูลอื่น (supertype) ของความสามารถในการแทนที่, หมายถึงองค์ประกอบของโปรแกรม, โดยปกติแล้วรูทีนย่อยหรือฟังก์ชั่น, เขียนขึ้นเพื่อใช้งานบนองค์ประกอบของ supertypeยังสามารถทำงานกับองค์ประกอบของ subtype ได้

หมายเหตุ: ชนิดย่อยหมายถึงคลาสฐานและชนิดย่อยหมายถึงคลาสที่สืบทอด

อ่านเพิ่มเติมเกี่ยวกับSubtype Polymorphism

คำตอบทางเทคนิค: การจัดส่งแบบไดนามิก

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

การอ่านความแตกต่างเพิ่มเติมใน C ++ และDynamic Dispatch

คำตอบในการนำไปใช้: สร้างรายการ vtable

สำหรับแต่ละโมเดอเรเตอร์ "เสมือน" บนเมธอดคอมไพเลอร์ C ++ มักจะสร้างรายการใน vtable ของคลาสที่มีการประกาศเมธอด นี่คือความที่คอมไพเลอร์ C ++ ทั่วไปรับรู้ถึงDynamic Dispatchไดนามิกส่ง

อ่านเพิ่มเติม vtables


รหัสตัวอย่าง

#include <iostream>

using namespace std;

class Animal {
public:
    virtual void MakeTypicalNoise() = 0; // no implementation needed, for abstract classes
    virtual ~Animal(){};
};

class Cat : public Animal {
public:
    virtual void MakeTypicalNoise()
    {
        cout << "Meow!" << endl;
    }
};

class Dog : public Animal {
public:
    virtual void MakeTypicalNoise() { // needs to be virtual, if subtype polymorphism is also needed for Dogs
        cout << "Woof!" << endl;
    }
};

class Doberman : public Dog {
public:
    virtual void MakeTypicalNoise() {
        cout << "Woo, woo, woow!";
        cout << " ... ";
        Dog::MakeTypicalNoise();
    }
};

int main() {

    Animal* apObject[] = { new Cat(), new Dog(), new Doberman() };

    const   int cnAnimals = sizeof(apObject)/sizeof(Animal*);
    for ( int i = 0; i < cnAnimals; i++ ) {
        apObject[i]->MakeTypicalNoise();
    }
    for ( int i = 0; i < cnAnimals; i++ ) {
        delete apObject[i];
    }
    return 0;
}

ผลลัพธ์ของรหัสตัวอย่าง

Meow!
Woof!
Woo, woo, woow! ... Woof!

แผนภาพคลาส UML ของตัวอย่างโค้ด

แผนภาพคลาส UML ของตัวอย่างโค้ด


1
ใช้ upvote ของฉันเพราะคุณแสดงการใช้ polymorphism ที่สำคัญที่สุด: คลาสฐานที่มีฟังก์ชันสมาชิกเสมือนระบุอินเทอร์เฟซหรือกล่าวอีกนัยหนึ่งคือAPI รหัสโดยใช้คลาสเฟรมดังกล่าว (ที่นี่: ฟังก์ชั่นหลักของคุณ) สามารถปฏิบัติต่อรายการทั้งหมดในคอลเลกชัน (ที่นี่: อาเรย์ของคุณ) อย่างสม่ำเสมอและไม่จำเป็นต้องไม่ต้องการและแน่นอนมักจะไม่ทราบว่า ณ รันไทม์ตัวอย่างเช่นเนื่องจากยังไม่มีอยู่ นี่เป็นหนึ่งในรากฐานของการแกะสลักความสัมพันธ์เชิงนามธรรมระหว่างวัตถุและตัวจัดการ
ปีเตอร์ - Reinstate Monica

2

นี่คือตัวอย่างที่สมบูรณ์ที่แสดงให้เห็นว่าเหตุใดจึงใช้วิธีเสมือน

#include <iostream>

using namespace std;

class Basic
{
    public:
    virtual void Test1()
    {
        cout << "Test1 from Basic." << endl;
    }
    virtual ~Basic(){};
};
class VariantA : public Basic
{
    public:
    void Test1()
    {
        cout << "Test1 from VariantA." << endl;
    }
};
class VariantB : public Basic
{
    public:
    void Test1()
    {
        cout << "Test1 from VariantB." << endl;
    }
};

int main()
{
    Basic *object;
    VariantA *vobjectA = new VariantA();
    VariantB *vobjectB = new VariantB();

    object=(Basic *) vobjectA;
    object->Test1();

    object=(Basic *) vobjectB;
    object->Test1();

    delete vobjectA;
    delete vobjectB;
    return 0;
}

1

เกี่ยวกับประสิทธิภาพฟังก์ชั่นเสมือนจะมีประสิทธิภาพน้อยกว่าเมื่อเทียบกับฟังก์ชั่นก่อนหน้า

"กลไกสายนี้เสมือนสามารถทำเกือบเป็นที่มีประสิทธิภาพในขณะที่ 'ฟังก์ชั่นการโทรปกติ' กลไก (ภายใน 25%). ค่าใช้จ่ายในพื้นที่ของตนเป็นหนึ่งในตัวชี้ในวัตถุของคลาสที่มีฟังก์ชั่นเสมือนแต่ละบวกหนึ่ง vtbl สำหรับแต่ละชั้นเรียนดังกล่าว" [ ทัวร์ C ++โดย Bjarne Stroustrup]


2
การรวมล่าช้าไม่เพียง แต่ทำให้การเรียกใช้ฟังก์ชันช้าลง แต่ยังทำให้การเรียกใช้ฟังก์ชันที่ไม่รู้จักจนกระทั่งเวลาใช้งานดังนั้นจึงไม่สามารถใช้การปรับให้เหมาะสมกับการเรียกฟังก์ชันได้ สิ่งนี้สามารถเปลี่ยนทุกอย่าง f.ex ในกรณีที่การถ่ายทอดค่าลบโค้ดจำนวนมาก (คิดว่าif(param1>param2) return cst;คอมไพเลอร์สามารถลดการเรียกใช้ฟังก์ชันทั้งหมดเป็นค่าคงที่ในบางกรณี)
curiousguy

1

วิธีการเสมือนถูกใช้ในการออกแบบส่วนต่อประสาน ตัวอย่างเช่นใน Windows มีส่วนต่อประสานที่เรียกว่า IUnknown เหมือนด้านล่าง:

interface IUnknown {
  virtual HRESULT QueryInterface (REFIID riid, void **ppvObject) = 0;
  virtual ULONG   AddRef () = 0;
  virtual ULONG   Release () = 0;
};

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


the run-time is aware of the three methods and expects them to be implementedเนื่องจากเป็นเสมือนจริงจึงไม่มีวิธีสร้างอินสแตนซ์ของIUnknownดังนั้นคลาสย่อยทั้งหมดจึงต้องใช้วิธีดังกล่าวทั้งหมดเพื่อรวบรวมเพียงอย่างเดียว ไม่มีอันตรายใด ๆ ที่จะไม่นำมาใช้และพบว่าเกิดขึ้นที่รันไทม์ (แต่แน่นอนว่ามีใครสามารถนำไปใช้ได้อย่างผิดพลาดแน่นอน!) และว้าววันนี้ฉันเรียนรู้#defineแมโครWindows ด้วยคำว่าinterfaceน่าจะเป็นเพราะผู้ใช้ของพวกเขาไม่เพียงแค่ (A) ดูคำนำหน้าIในชื่อหรือ (B) ดูคลาสเพื่อดูว่าเป็นอินเทอร์เฟซ อึ
underscore_d

1

ฉันคิดว่าคุณหมายถึงความจริงเมื่อมีการประกาศวิธีการเสมือนคุณไม่จำเป็นต้องใช้คำหลัก 'เสมือน' ในการแทนที่

class Base { virtual void foo(); };

class Derived : Base 
{ 
  void foo(); // this is overriding Base::foo
};

หากคุณไม่ได้ใช้ 'เสมือน' ในการประกาศของ foo จากนั้น foo ของ Derived ก็จะเป็นเพียงเงา


1

นี่คือรหัส C ++ รุ่นผสานสำหรับคำตอบสองข้อแรก

#include        <iostream>
#include        <string>

using   namespace       std;

class   Animal
{
        public:
#ifdef  VIRTUAL
                virtual string  says()  {       return  "??";   }
#else
                string  says()  {       return  "??";   }
#endif
};

class   Dog:    public Animal
{
        public:
                string  says()  {       return  "woof"; }
};

string  func(Animal *a)
{
        return  a->says();
}

int     main()
{
        Animal  *a = new Animal();
        Dog     *d = new Dog();
        Animal  *ad = d;

        cout << "Animal a says\t\t" << a->says() << endl;
        cout << "Dog d says\t\t" << d->says() << endl;
        cout << "Animal dog ad says\t" << ad->says() << endl;

        cout << "func(a) :\t\t" <<      func(a) <<      endl;
        cout << "func(d) :\t\t" <<      func(d) <<      endl;
        cout << "func(ad):\t\t" <<      func(ad)<<      endl;
}

สองผลลัพธ์ที่แตกต่างคือ:

หากไม่มี #define virtualมันจะทำการรวมเวลาคอมไพล์ สัตว์ * โฆษณาและ func (สัตว์ *) ชี้ไปที่วิธีพูดของสัตว์ ()

$ g++ virtual.cpp -o virtual
$ ./virtual 
Animal a says       ??
Dog d says      woof
Animal dog ad says  ??
func(a) :       ??
func(d) :       ??
func(ad):       ??

ด้วย #define virtualมันจะทำการรวมในเวลาทำงาน Dog * d, Animal * ad และ func (Animal *) point / อ้างถึง Dog's says () วิธีการเนื่องจาก Dog เป็นวัตถุประเภท เว้นแต่จะไม่ได้กำหนดวิธีการของ [Dog's says () "woof"] มันจะเป็นวิธีการค้นหาอันดับแรกในคลาสของชั้นเรียนนั่นคือคลาสที่ได้รับอาจแทนที่วิธีของคลาสพื้นฐานของพวกมัน [Animal's says ()]

$ g++ virtual.cpp -D VIRTUAL -o virtual
$ ./virtual 
Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

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

class   Animal:
        def     says(self):
                return  "??"

class   Dog(Animal):
        def     says(self):
                return  "woof"

def     func(a):
        return  a.says()

if      __name__ == "__main__":

        a = Animal()
        d = Dog()
        ad = d  #       dynamic typing by assignment

        print("Animal a says\t\t{}".format(a.says()))
        print("Dog d says\t\t{}".format(d.says()))
        print("Animal dog ad says\t{}".format(ad.says()))

        print("func(a) :\t\t{}".format(func(a)))
        print("func(d) :\t\t{}".format(func(d)))
        print("func(ad):\t\t{}".format(func(ad)))

ผลลัพธ์คือ:

Animal a says       ??
Dog d says      woof
Animal dog ad says  woof
func(a) :       ??
func(d) :       woof
func(ad):       woof

ซึ่งเหมือนกับการกำหนดเสมือนของ C ++ โปรดทราบว่าdและโฆษณาเป็นตัวแปรตัวชี้ที่แตกต่างกันสองตัวที่อ้างอิง / ชี้ไปที่อินสแตนซ์ Dog เดียวกัน นิพจน์ (โฆษณาคือ d) จะคืนค่าเป็น True และค่าจะเป็น < main .Dog object เดียวกันที่ 0xb79f72cc>


1

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


0

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


-1

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

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
  };

 class derived: public base {
 public:
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main () {
      base hwOne;
      derived hwTwo = new derived();
      base->helloWorld(); //prints "Hello World!"
      derived->helloWorld(); //prints "Hello World!"

ตกลงนั่นคือสิ่งที่เรารู้ ตอนนี้ลองทำกับพอยน์เตอร์ฟังก์ชั่นสมาชิก:

 #include <iostream>
 using namespace std;

 class base {
 public:
 void helloWorld() { std::cout << "Hello World!"; }
 };

 class derived : public base {
 public:
 void displayHWDerived(void(derived::*hwbase)()) { (this->*hwbase)(); }
 void(derived::*hwBase)();
 void helloWorld() { std::cout << "Greetings World!"; }
 };

 int main()
 {
 base* b = new base(); //Create base object
 b->helloWorld(); // Hello World!
 void(derived::*hwBase)() = &derived::helloWorld; //create derived member 
 function pointer to base function
 derived* d = new derived(); //Create derived object. 
 d->displayHWDerived(hwBase); //Greetings World!

 char ch;
 cin >> ch;
 }

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

ในทางกลับกันฟังก์ชั่นเสมือนจริงในขณะที่พวกเขาอาจมีค่าใช้จ่ายตัวชี้ฟังก์ชั่นบางอย่างทำสิ่งที่ง่ายขึ้นอย่างมาก

แก้ไข: มีวิธีอื่นซึ่งเป็นที่คล้ายกันโดย eddietree คือฟังก์ชัน c ++ เสมือน VS ชี้ฟังก์ชันสมาชิก (เปรียบเทียบประสิทธิภาพ)

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