Tại sao chúng ta cần các chức năng ảo trong C ++?


1312

Tôi đang học C ++ và tôi chỉ tham gia vào các chức năng ảo.

Từ những gì tôi đã đọc (trong sách và trực tuyến), các hàm ảo là các hàm trong lớp cơ sở mà bạn có thể ghi đè trong các lớp dẫn xuất.

Nhưng trước đó trong cuốn sách, khi tìm hiểu về kế thừa cơ bản, tôi đã có thể ghi đè các hàm cơ sở trong các lớp dẫn xuất mà không cần sử dụng virtual.

Vậy tôi còn thiếu gì ở đây? Tôi biết có nhiều chức năng ảo hơn và nó có vẻ quan trọng vì vậy tôi muốn rõ ràng về chính xác nó là gì. Tôi chỉ không thể tìm thấy một câu trả lời trực tuyến.


13
Tôi đã tạo một lời giải thích thực tế cho các chức năng ảo tại đây: nrecacht.blogspot.in/2015/06/ trên
Nav

4
Đây có lẽ là lợi ích lớn nhất của các hàm ảo - khả năng cấu trúc mã của bạn theo cách mà các lớp mới xuất phát sẽ tự động làm việc với mã cũ mà không cần sửa đổi!
dùng3530616

tbh, các chức năng ảo là tính năng chính của OOP, để xóa kiểu. Tôi nghĩ, đó là các phương thức không ảo là điều làm cho Object Pascal và C ++ trở nên đặc biệt, được tối ưu hóa các vtable lớn không cần thiết và cho phép các lớp tương thích với POD. Nhiều ngôn ngữ OOP hy vọng rằng mọi phương pháp đều có thể được ghi đè.
Swift - Thứ Sáu Pie

Đây là một câu hỏi hay. Thật vậy, điều ảo này trong C ++ bị trừu tượng hóa trong các ngôn ngữ khác như Java hoặc PHP. Trong C ++, bạn chỉ cần kiểm soát nhiều hơn một chút đối với một số trường hợp hiếm gặp (Hãy lưu ý đến việc thừa kế nhiều lần hoặc trường hợp đặc biệt của DDOD ). Nhưng tại sao câu hỏi này được đăng trên stackoverflow.com?
Edgar Alloro

Tôi nghĩ rằng nếu bạn xem xét ràng buộc sớm - ràng buộc muộn và VTABLE thì sẽ hợp lý và hợp lý hơn. Vì vậy, có một lời giải thích tốt ( learncpp.com/cpp-tutorial/125-the-virtual-table ) ở đây.
ceyun

Câu trả lời:


2729

Đây là cách tôi hiểu không chỉ các virtualchức năng là gì , mà tại sao chúng được yêu cầu:

Giả sử bạn có hai lớp này:

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."; }
};

Trong chức năng chính của bạn:

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

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

Cho đến nay rất tốt, phải không? Động vật ăn thức ăn chung, mèo ăn chuột, tất cả đều không có virtual.

Bây giờ chúng ta hãy thay đổi nó một chút để eat()được gọi thông qua một hàm trung gian (một hàm tầm thường chỉ cho ví dụ này):

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

Bây giờ chức năng chính của chúng tôi là:

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

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

Uh oh ... chúng tôi đã đưa một con mèo vào func(), nhưng nó sẽ không ăn chuột. Bạn có nên quá tải func()để mất một Cat*? Nếu bạn phải lấy được nhiều động vật hơn từ Động vật, tất cả chúng sẽ cần riêng chúng func().

Giải pháp là biến eat()từ Animallớp thành một hàm ảo:

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."; }
};

Chủ yếu:

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

Làm xong.


165
Vì vậy, nếu tôi hiểu chính xác điều này, ảo cho phép gọi phương thức lớp con, ngay cả khi đối tượng đang được coi là siêu lớp của nó?
Kenny Worden

147
Thay vì giải thích ràng buộc muộn thông qua ví dụ về chức năng trung gian "func", đây là một minh chứng đơn giản hơn - Animal * Animal = new Animal; // Mèo * mèo = Mèo mới; Động vật * mèo = Mèo mới; động vật-> ăn (); // đầu ra: "Tôi đang ăn thức ăn chung." mèo-> ăn (); // đầu ra: "Tôi đang ăn thức ăn chung." Mặc dù bạn đang gán đối tượng được phân lớp (Cat), phương thức được gọi dựa trên loại con trỏ (Animal) chứ không phải loại đối tượng mà nó trỏ tới. Đây là lý do tại sao bạn cần "ảo".
rexbelia

37
Tôi có phải là người duy nhất tìm thấy hành vi mặc định này trong C ++ không? Tôi đã mong đợi mã mà không có "ảo" để làm việc.
David Wong 7/07/2016

20
@David 天宇 Wong Tôi nghĩ rằng virtualgiới thiệu một số ràng buộc động so với tĩnh và vâng, thật lạ nếu bạn đến từ các ngôn ngữ như Java.
peterchaula

32
Trước hết, các cuộc gọi ảo đắt hơn nhiều so với các cuộc gọi chức năng thông thường. Triết lý C ++ là nhanh theo mặc định, vì vậy các cuộc gọi ảo theo mặc định là một điều không nên. Lý do thứ hai là các cuộc gọi ảo có thể dẫn đến việc phá mã của bạn nếu bạn kế thừa một lớp từ thư viện và nó thay đổi triển khai bên trong của phương thức công khai hoặc riêng tư (gọi phương thức ảo bên trong) mà không thay đổi hành vi của lớp cơ sở.
saolof

672

Không có "ảo" bạn sẽ có được "ràng buộc sớm". Việc thực hiện phương thức nào được sử dụng sẽ được quyết định tại thời điểm biên dịch dựa trên loại con trỏ mà bạn gọi qua.

Với "ảo", bạn nhận được "ràng buộc muộn". Việc triển khai phương thức nào được sử dụng sẽ được quyết định trong thời gian chạy dựa trên loại đối tượng được trỏ - cái mà nó được xây dựng ban đầu là. Đây không nhất thiết là những gì bạn nghĩ dựa trên loại con trỏ trỏ đến đối tượng đó.

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"

EDIT - xem câu hỏi này .

Ngoài ra - hướng dẫn này bao gồm ràng buộc sớm và muộn trong C ++.


11
Tuyệt vời, và về nhà nhanh chóng và với việc sử dụng các ví dụ tốt hơn. Tuy nhiên, điều này là đơn giản và người hỏi thực sự chỉ nên đọc trang parashift.com/c++-faq-lite/virtual-fifts.html . Những người khác đã chỉ ra tài nguyên này trong các bài viết SO được liên kết từ chủ đề này, nhưng tôi tin rằng điều này đáng được đề cập lại.
Sonny

36
Tôi không biết ràng buộc sớmmuộn là các thuật ngữ được sử dụng cụ thể trong cộng đồng c ++, nhưng các thuật ngữ chính xác là tĩnh (tại thời gian biên dịch) và ràng buộc động (tại thời gian chạy).
chước

31
@mike - "Thuật ngữ" ràng buộc muộn "có từ ít nhất là những năm 1960, nơi nó có thể được tìm thấy trong Truyền thông của ACM." . Sẽ không tốt sao nếu có một từ đúng cho mỗi khái niệm? Thật không may, nó không phải như vậy. Các thuật ngữ "liên kết sớm" và "liên kết muộn" có trước C ++ và thậm chí lập trình hướng đối tượng, và cũng chính xác như các thuật ngữ bạn sử dụng.
Steve314

4
@BJovke - câu trả lời này đã được viết trước khi C ++ 11 được xuất bản. Mặc dù vậy, tôi đã biên dịch nó trong GCC 6.3.0 (sử dụng C ++ 14 theo mặc định) không có vấn đề - rõ ràng là gói phần khai báo biến và các cuộc gọi trong một mainchức năng, vv Pointer-to-có nguồn gốc ngầm phôi để con trỏ-to-base (chuyên sâu hơn ngầm định để tổng quát hơn). Visa-Versa bạn cần một diễn viên rõ ràng, thường là a dynamic_cast. Bất cứ điều gì khác - rất dễ xảy ra hành vi không xác định, vì vậy hãy chắc chắn rằng bạn biết những gì bạn đang làm. Theo hiểu biết tốt nhất của tôi, điều này đã không thay đổi kể từ trước cả C ++ 98.
Steve314

10
Lưu ý rằng trình biên dịch C ++ ngày nay thường có thể tối ưu hóa muộn thành ràng buộc sớm - khi chúng có thể chắc chắn về những gì ràng buộc sẽ xảy ra. Điều này cũng được gọi là "khử ảo hóa".
einpoklum

83

Bạn cần ít nhất 1 cấp độ kế thừa và hạ thấp để chứng minh điều đó. Đây là một ví dụ rất đơn giản:

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
Ví dụ của bạn nói rằng chuỗi trả về phụ thuộc vào hàm có ảo hay không, nhưng nó không cho biết kết quả nào tương ứng với ảo và tương ứng với không ảo. Ngoài ra, có một chút khó hiểu khi bạn không sử dụng chuỗi được trả lại.
Ross

7
Với từ khóa ảo: Wagger . Không có từ khóa ảo : ? .
Hesham Eraqi

@HeshamEraqi không có ảo nó bị ràng buộc sớm và nó sẽ hiển thị "?" của lớp cơ sở
Ahmad

46

Bạn cần các phương thức ảo để giảm bớt sự an toàn , đơn giảnngắn gọn .

Đó là những gì các phương thức ảo thực hiện: chúng downcast an toàn, với mã rõ ràng đơn giản và ngắn gọn, tránh các phôi thủ công không an toàn trong mã phức tạp và dài dòng hơn mà bạn có.


Phương pháp không ảo binding liên kết tĩnh

Các mã sau đây là cố ý không chính xác. Nó không khai báo valuephương thức này virtualvà do đó tạo ra kết quả sai không mong muốn, cụ thể là 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;
}

Trong dòng được nhận xét là, bad bad, Expression::valuephương thức được gọi, bởi vì kiểu được biết tĩnh (loại được biết tại thời điểm biên dịch) Expressionvaluephương thức này không phải là ảo.


Phương pháp ảo binding ràng buộc động.

Khai báo valuenhư virtualtrong kiểu được biết tĩnh Expressionđảm bảo rằng mỗi cuộc gọi sẽ kiểm tra loại đối tượng thực tế này là gì và gọi việc thực hiện có liên quan valuecho loại động đó :

#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;
}

Ở đây, đầu ra là 6.86như vậy, vì phương thức ảo được gọi là hầu như . Điều này cũng được gọi là ràng buộc động của các cuộc gọi. Một kiểm tra nhỏ được thực hiện, tìm loại đối tượng động thực tế và triển khai phương thức có liên quan cho loại động đó, được gọi.

Việc thực hiện có liên quan là một trong lớp cụ thể nhất (có nguồn gốc nhất).

Lưu ý rằng việc triển khai phương thức trong các lớp dẫn xuất ở đây không được đánh dấu virtualmà thay vào đó được đánh dấu override. Chúng có thể được đánh dấu virtualnhưng chúng tự động ảo. Các overrideĐảm bảo từ khóa đó nếu có không một phương pháp ảo như trong một số lớp cơ sở, sau đó bạn sẽ nhận được một lỗi (mà là mong muốn).


Sự xấu xí khi làm điều này mà không có phương pháp ảo

Nếu không có virtualai sẽ phải thực hiện một số phiên bản Do It Yourself của ràng buộc động. Đó là điều này thường liên quan đến việc hạ thấp thủ công không an toàn, phức tạp và dài dòng.

Đối với trường hợp của một hàm duy nhất, như ở đây, nó đủ để lưu trữ một con trỏ hàm trong đối tượng và gọi qua con trỏ hàm đó, nhưng ngay cả như vậy nó cũng liên quan đến một số chương trình truyền phát không an toàn, phức tạp và dài dòng, để dí dỏm:

#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;
}

Một cách tích cực để xem xét điều này là, nếu bạn gặp phải tình trạng downcasting không an toàn, phức tạp và dài dòng như trên, thì thường một phương pháp hoặc phương thức ảo thực sự có thể giúp ích.


40

Hàm ảo được sử dụng để hỗ trợ Đa hình thời gian chạy .

Đó là, từ khóa ảo yêu cầu trình biên dịch không đưa ra quyết định (ràng buộc chức năng) tại thời điểm biên dịch, thay vào đó hoãn lại nó để chạy " .

  • Bạn có thể tạo một hàm ảo bằng cách đặt trước từ khóa virtualtrong khai báo lớp cơ sở của nó. Ví dụ,

     class Base
     {
        virtual void func();
     }
    
  • Khi Lớp cơ sở có chức năng thành viên ảo, bất kỳ lớp nào kế thừa từ Lớp cơ sở đều có thể xác định lại chức năng với cùng một nguyên mẫu, nghĩa là chỉ có thể xác định lại chức năng, không phải giao diện của chức năng.

     class Derive : public Base
     {
        void func();
     }
    
  • Một con trỏ lớp Base có thể được sử dụng để trỏ đến đối tượng lớp Base cũng như đối tượng lớp Derogen.

  • Khi hàm ảo được gọi bằng cách sử dụng con trỏ lớp Cơ sở, trình biên dịch sẽ quyết định vào thời gian chạy phiên bản nào của hàm - tức là phiên bản lớp Cơ sở hoặc phiên bản lớp Derogen bị ghi đè - sẽ được gọi. Điều này được gọi là đa hình thời gian chạy .

34

Nếu lớp cơ sở là Basevà lớp dẫn xuất là Der, bạn có thể có một Base *pcon trỏ thực sự trỏ đến một thể hiện của Der. Khi bạn gọi p->foo();, nếu không phảifoo là ảo, thì phiên bản của nó sẽ thực thi, bỏ qua thực tế là thực sự trỏ đến a . Nếu foo ảo, thực thi ghi đè "tối đa" , hoàn toàn tính đến lớp thực tế của mục được trỏ. Vì vậy, sự khác biệt giữa ảo và không ảo thực sự rất quan trọng: cái trước cho phép đa hình thời gian chạy , khái niệm cốt lõi của lập trình OO, trong khi cái sau thì không.BasepDerp->foo()foo


8
Tôi ghét mâu thuẫn với bạn, nhưng đa hình thời gian biên dịch vẫn là đa hình. Ngay cả quá tải các chức năng không phải là thành viên là một dạng đa hình - đa hình ad-hoc sử dụng thuật ngữ trong liên kết của bạn. Sự khác biệt ở đây là giữa ràng buộc sớm và muộn.
Steve314

7
@ Steve314, bạn đúng về mặt giáo dục (với tư cách là một giáo viên đồng nghiệp, tôi chấp nhận điều đó ;-) - chỉnh sửa câu trả lời để thêm tính từ còn thiếu ;-).
Alex Martelli

26

Cần cho chức năng ảo giải thích [Dễ hiểu]

#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();

}

Đầu ra sẽ là:

Hello from Class A.

Nhưng với chức năng ảo:

#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();

}

Đầu ra sẽ là:

Hello from Class B.

Do đó với chức năng ảo, bạn có thể đạt được đa hình thời gian chạy.


25

Tôi muốn thêm một cách sử dụng chức năng ảo khác mặc dù nó sử dụng khái niệm tương tự như các câu trả lời đã nêu ở trên nhưng tôi đoán nó đáng được đề cập.

NHÀ PHÂN PHỐI VIRTUAL

Hãy xem xét chương trình này bên dưới, mà không khai báo hàm hủy lớp cơ sở là ảo; bộ nhớ cho Cat có thể không được dọn sạch.

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

Đầu ra:

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

Đầu ra:

Deleting an Animal name Cat
Deleting an Animal

11
without declaring Base class destructor as virtual; memory for Cat may not be cleaned up.Tệ hơn thế. Xóa một đối tượng dẫn xuất thông qua một con trỏ / tham chiếu cơ sở là hành vi không xác định thuần túy. Vì vậy, nó không chỉ là một số bộ nhớ có thể bị rò rỉ. Thay vào đó, chương trình là vô hình thành, vì vậy trình biên dịch có thể biến nó thành bất cứ điều gì: Mã máy mà xảy ra để làm việc tốt, hoặc không có gì, hoặc giấy triệu tập quỷ từ mũi của bạn, hoặc vv Đó lý do tại sao, nếu một chương trình được thiết kế theo như vậy một cách mà một số người dùng có thể xóa một thể hiện dẫn xuất thông qua một tham chiếu cơ sở, cơ sở phải có một hàm hủy ảo
underscore_d

21

Bạn phải phân biệt giữa ghi đè và quá tải. Không có virtualtừ khóa, bạn chỉ quá tải một phương thức của lớp cơ sở. Điều này có nghĩa là không có gì ngoài việc che giấu. Giả sử bạn có một lớp cơ sở Basevà một lớp dẫn xuất Specializedmà cả hai đều thực hiện void foo(). Bây giờ bạn có một con trỏ để Basetrỏ đến một thể hiện của Specialized. Khi bạn gọi foo()nó, bạn có thể quan sát sự khác biệt virtualtạo ra: Nếu phương thức là ảo, việc triển khai Specializedsẽ được sử dụng, nếu nó bị thiếu, phiên bản từ Basesẽ được chọn. Đó là cách tốt nhất để không bao giờ quá tải các phương thức từ một lớp cơ sở. Làm cho một phương thức không ảo là cách tác giả của nó nói với bạn rằng phần mở rộng của nó trong các lớp con không nhằm mục đích.


3
Không có virtualbạn là không quá tải. Bạn đang bị bóng đè . Nếu một lớp cơ sở Bcó một hoặc nhiều hàm foovà lớp dẫn xuất Dđịnh nghĩa một footên, nó foo sẽ ẩn tất cả các foo-s trong đó B. Họ đạt được như B::foosử dụng độ phân giải phạm vi. Để thúc đẩy các B::foochức năng vào Dquá tải, bạn phải sử dụng using B::foo.
Kaz

20

Tại sao chúng ta cần Phương thức ảo trong C ++?

Câu trả lời nhanh:

  1. Nó cung cấp cho chúng ta một trong những "nguyên liệu" cần 1 cho lập trình hướng đối tượng .

Trong lập trình C ++ của Bjarne Stroustrup: Nguyên tắc và thực hành, (14.3):

Hàm ảo cung cấp khả năng định nghĩa một hàm trong một lớp cơ sở và có một hàm cùng tên và gõ trong một lớp dẫn xuất được gọi khi người dùng gọi hàm lớp cơ sở. Điều đó thường được gọi là đa hình thời gian chạy , công văn động hoặc công văn thời gian chạy vì hàm được gọi được xác định tại thời gian chạy dựa trên loại đối tượng được sử dụng.

  1. Đây là cách thực hiện hiệu quả nhanh nhất nếu bạn cần một hàm gọi 2 ảo .

Để xử lý một cuộc gọi ảo, người ta cần một hoặc nhiều phần dữ liệu liên quan đến đối tượng dẫn xuất 3 . Cách thường được thực hiện là thêm địa chỉ của bảng hàm. Bảng này thường được gọi là bảng ảo hoặc bảng chức năng ảo và địa chỉ của nó thường được gọi là con trỏ ảo . Mỗi chức năng ảo có một vị trí trong bảng ảo. Tùy thuộc vào loại đối tượng của người gọi (dẫn xuất), lần lượt, hàm ảo gọi ra ghi đè tương ứng.


1. Việc sử dụng kế thừa, đa hình thời gian chạy và đóng gói là định nghĩa phổ biến nhất của lập trình hướng đối tượng .

2. Bạn không thể mã hóa chức năng để nhanh hơn hoặc sử dụng ít bộ nhớ hơn bằng các tính năng ngôn ngữ khác để chọn trong số các lựa chọn thay thế trong thời gian chạy. Bjarne Stroustrup Lập trình C ++: Nguyên tắc và thực hành. (14.3.1) .

3. Một cái gì đó để cho biết hàm nào thực sự được gọi khi chúng ta gọi lớp cơ sở chứa hàm ảo.


15

Tôi đã có câu trả lời của mình dưới dạng một cuộc trò chuyện để được đọc tốt hơn:


Tại sao chúng ta cần các chức năng ảo?

Vì đa hình.

Đa hình là gì?

Thực tế là một con trỏ cơ sở cũng có thể trỏ đến các đối tượng loại dẫn xuất.

Làm thế nào để định nghĩa về đa hình này dẫn đến sự cần thiết của các chức năng ảo?

Vâng, thông qua ràng buộc sớm .

Liên kết sớm là gì?

Liên kết sớm (liên kết thời gian biên dịch) trong C ++ có nghĩa là một lệnh gọi hàm được cố định trước khi chương trình được thực thi.

Vì thế...?

Vì vậy, nếu bạn sử dụng một loại cơ sở làm tham số của hàm, trình biên dịch sẽ chỉ nhận ra giao diện cơ sở và nếu bạn gọi hàm đó với bất kỳ đối số nào từ các lớp dẫn xuất, nó sẽ bị cắt, đó không phải là điều bạn muốn xảy ra.

Nếu đó không phải là những gì chúng ta muốn xảy ra, tại sao điều này được cho phép?

Bởi vì chúng ta cần đa hình!

Lợi ích của đa hình là gì?

Bạn có thể sử dụng một con trỏ kiểu cơ sở làm tham số của một hàm duy nhất và sau đó trong thời gian chạy chương trình của bạn, bạn có thể truy cập từng giao diện loại dẫn xuất (ví dụ: các hàm thành viên của chúng) mà không gặp vấn đề gì, sử dụng chức năng hủy bỏ hội nghị của đơn đó con trỏ cơ sở.

Tôi vẫn không biết chức năng ảo nào tốt cho ...! Và đây là câu hỏi đầu tiên của tôi!

tốt, điều này là do bạn đã hỏi câu hỏi của bạn quá sớm!

Tại sao chúng ta cần các chức năng ảo?

Giả sử rằng bạn đã gọi một hàm với một con trỏ cơ sở, có địa chỉ của một đối tượng từ một trong các lớp dẫn xuất của nó. Như chúng ta đã nói về nó ở trên, trong thời gian chạy, con trỏ này bị hủy đăng ký, tuy nhiên, rất tốt, chúng tôi hy vọng một phương thức (== một hàm thành viên) "từ lớp dẫn xuất của chúng ta" sẽ được thực thi! Tuy nhiên, một phương thức tương tự (một phương thức có cùng tiêu đề) đã được xác định trong lớp cơ sở, vậy tại sao chương trình của bạn phải bận tâm chọn phương thức khác? Nói cách khác, ý tôi là, làm thế nào bạn có thể nói ra kịch bản này từ những gì chúng ta thường thấy trước đây thường xảy ra?

Câu trả lời ngắn gọn là "một hàm thành viên ảo trong cơ sở" và câu trả lời dài hơn một chút là, "ở bước này, nếu chương trình thấy một hàm ảo trong lớp cơ sở, nó biết (nhận ra) rằng bạn đang cố gắng sử dụng Đa hình "và do đó đi đến các lớp dẫn xuất (sử dụng bảng v , một dạng liên kết muộn) để tìm ra một phương thức khác có cùng tiêu đề, nhưng với một cách bất ngờ - một cách thực hiện khác.

Tại sao thực hiện khác nhau?

Bạn gõ đầu! Đi đọc một cuốn sách hay !

OK, chờ đợi, chờ đợi, tại sao người ta lại bận tâm sử dụng các con trỏ cơ sở, khi anh ta / cô ta chỉ có thể sử dụng các con trỏ loại dẫn xuất? Bạn là thẩm phán, tất cả đau đầu này có đáng không? Nhìn vào hai đoạn này:

// 1:

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

// 2:

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

OK, mặc dù tôi nghĩ rằng 1 vẫn tốt hơn 2 , bạn cũng có thể viết 1 như thế này:

// 1:

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

và hơn nữa, bạn nên lưu ý rằng đây vẫn chỉ là một cách sử dụng giả tạo cho tất cả những điều tôi đã giải thích cho bạn cho đến nay. Thay vì điều này, giả sử ví dụ một tình huống trong đó bạn có một hàm trong chương trình đã sử dụng các phương thức từ mỗi lớp dẫn xuất tương ứng (getMonthBenefit ()):

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

Bây giờ, hãy cố gắng viết lại này, mà không có bất kỳ đau đầu!

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();

Và trên thực tế, đây cũng có thể là một ví dụ giả định!


2
khái niệm lặp lại trên các loại đối tượng (phụ) khác nhau bằng cách sử dụng một loại đối tượng (siêu) duy nhất cần được nêu bật, đó là một điểm tốt mà bạn đã đưa ra, cảm ơn
khắc nghiệt

14

Khi bạn có một hàm trong lớp cơ sở, bạn có thể Redefinehoặc Overridenó trong lớp dẫn xuất.

Xác định lại một phương thức : Một triển khai mới cho phương thức của lớp cơ sở được đưa ra trong lớp dẫn xuất. Không tạo điều kiệnDynamic binding.

Ghi đè một phương thức : Redefiningavirtual methodcủa lớp cơ sở trong lớp dẫn xuất. Phương pháp ảo tạo điều kiện cho Binding động .

Vì vậy, khi bạn nói:

Nhưng trước đó trong cuốn sách, khi tìm hiểu về kế thừa cơ bản, tôi đã có thể ghi đè các phương thức cơ bản trong các lớp dẫn xuất mà không cần sử dụng 'ảo'.

bạn không ghi đè nó vì phương thức trong lớp cơ sở không phải là ảo, thay vào đó bạn đang định nghĩa lại nó


11

Nó giúp nếu bạn biết các cơ chế cơ bản. C ++ chính thức hóa một số kỹ thuật mã hóa được sử dụng bởi các lập trình viên C, "các lớp" được thay thế bằng cách sử dụng "lớp phủ" - các cấu trúc với các phần tiêu đề chung sẽ được sử dụng để xử lý các đối tượng thuộc các loại khác nhau nhưng với một số dữ liệu hoặc hoạt động chung. Thông thường, cấu trúc cơ sở của lớp phủ (phần chung) có một con trỏ tới bảng chức năng trỏ đến một tập các thói quen khác nhau cho từng loại đối tượng. C ++ cũng làm điều tương tự nhưng ẩn các cơ chế, tức là C ++ ptr->func(...)trong đó func là ảo như C (*ptr->func_table[func_num])(ptr,...), trong đó những thay đổi giữa các lớp dẫn xuất là nội dung func_table. [Một phương thức không ảo ptr-> func () chỉ dịch sang mangled_func (ptr, ..).]

Kết quả cuối cùng là bạn chỉ cần hiểu lớp cơ sở để gọi các phương thức của lớp dẫn xuất, tức là nếu một thường trình hiểu lớp A, bạn có thể truyền cho nó một con trỏ lớp B dẫn xuất thì các phương thức ảo được gọi sẽ là của B chứ không phải A vì bạn đi qua bảng chức năng B điểm tại.


8

Từ khóa ảo cho trình biên dịch biết nó không nên thực hiện ràng buộc sớm. Thay vào đó, nó sẽ tự động cài đặt tất cả các cơ chế cần thiết để thực hiện ràng buộc muộn. Để thực hiện điều này, trình biên dịch điển hình1 tạo một bảng duy nhất (được gọi là VTABLE) cho mỗi lớp có chứa các hàm ảo. Trình biên dịch đặt địa chỉ của các hàm ảo cho lớp cụ thể đó trong VTABLE. Trong mỗi lớp có các hàm ảo, nó bí mật đặt một con trỏ, được gọi là vpulum (viết tắt là VPTR), trỏ đến VTABLE cho đối tượng đó. Khi bạn thực hiện một cuộc gọi hàm ảo thông qua một con trỏ lớp cơ sở, trình biên dịch sẽ lặng lẽ chèn mã để tìm nạp VPTR và tìm địa chỉ hàm trong VTABLE, do đó gọi hàm chính xác và gây ra ràng buộc muộn.

Thêm chi tiết trong liên kết này http://cplusplusinterviews.blogspot.sg/2015/04/virtual-mechanism.html


7

Các ảo lực lượng từ khóa trình biên dịch để chọn phương pháp thực hiện quy định tại các đối tượng lớp chứ không phải trong của con trỏ lớp.

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

Trong ví dụ trên, Shape :: getName sẽ được gọi theo mặc định, trừ khi getName () được định nghĩa là ảo trong Hình dạng lớp cơ sở. Điều này buộc trình biên dịch tìm kiếm triển khai getName () trong lớp Triangle chứ không phải trong lớp Shape.

Bảng ảo là cơ chế trong đó trình biên dịch theo dõi các triển khai phương thức ảo khác nhau của các lớp con. Điều này cũng được gọi là công văn năng động, và có một số chi phí liên kết với nó.

Cuối cùng, tại sao ảo thậm chí cần thiết trong C ++, tại sao không biến nó thành hành vi mặc định như trong Java?

  1. C ++ dựa trên các nguyên tắc của "Zero Overhead" và "Trả tiền cho những gì bạn sử dụng". Vì vậy, nó không cố gắng thực hiện công văn động cho bạn, trừ khi bạn cần nó.
  2. Để cung cấp thêm quyền kiểm soát giao diện. Bằng cách làm cho một hàm không ảo, lớp giao diện / trừu tượng có thể kiểm soát hành vi trong tất cả các cài đặt của nó.

4

Tại sao chúng ta cần các chức năng ảo?

Các hàm ảo tránh được vấn đề typecasting không cần thiết và một số người trong chúng ta có thể tranh luận rằng tại sao chúng ta cần các hàm ảo khi chúng ta có thể sử dụng con trỏ lớp dẫn xuất để gọi hàm cụ thể trong lớp dẫn xuất! Câu trả lời là - nó vô hiệu hóa toàn bộ ý tưởng thừa kế trong hệ thống lớn phát triển, trong đó có đối tượng lớp cơ sở con trỏ duy nhất là nhiều mong muốn.

Hãy so sánh hai chương trình đơn giản dưới đây để hiểu tầm quan trọng của các chức năng ảo:

Chương trình không có chức năng ảo:

#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;
}

ĐẦU RA:

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

Chương trình có chức năng ảo:

#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;
}

ĐẦU RA:

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

Bằng cách phân tích chặt chẽ cả hai đầu ra, người ta có thể hiểu tầm quan trọng của các chức năng ảo.


4

Trả lời OOP: Đa hình phụ

Trong C ++, các phương thức ảo là cần thiết để nhận ra đa hình , chính xác hơn là phân nhóm hoặc đa hình phụ nếu bạn áp dụng định nghĩa từ wikipedia.

Wikipedia, Subtyping, 2019-01-09: Trong lý thuyết ngôn ngữ lập trình, subtyping (cũng là đa hình phụ hoặc đa hình bao gồm) là một dạng đa hình loại trong đó một kiểu con là một kiểu dữ liệu có liên quan đến một kiểu dữ liệu khác (siêu kiểu) về khả năng thay thế, nghĩa là các phần tử chương trình, điển hình là chương trình con hoặc hàm, được viết để hoạt động trên các phần tử của siêu kiểu cũng có thể hoạt động trên các phần tử của kiểu con.

LƯU Ý: Subtype có nghĩa là lớp cơ sở và subtyp có nghĩa là lớp kế thừa.

Đọc thêm về đa hình Subtype

Trả lời kỹ thuật: Công văn động

Nếu bạn có một con trỏ tới một lớp cơ sở, thì lệnh gọi của phương thức (được khai báo là ảo) sẽ được gửi đến phương thức của lớp thực tế của đối tượng được tạo. Đây là cách Subtype Polymorphism được nhận ra là C ++.

Đọc thêm Đa hình trong C ++ và Công văn động

Trả lời thực hiện: Tạo mục nhập vtable

Đối với mỗi công cụ sửa đổi "ảo" trên các phương thức, trình biên dịch C ++ thường tạo một mục trong vtable của lớp trong đó phương thức được khai báo. Đây là cách trình biên dịch C ++ phổ biến nhận ra Dynamic Dispatch .

Đọc thêm vtables


Mã ví dụ

#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;
}

Đầu ra của mã ví dụ

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

Sơ đồ lớp UML của ví dụ mã

Sơ đồ lớp UML của ví dụ mã


1
Hãy ủng hộ tôi vì bạn cho thấy việc sử dụng đa hình có lẽ quan trọng nhất: Đó là một lớp cơ sở với các hàm thành viên ảo chỉ định một giao diện , hay nói cách khác là API. Mã sử ​​dụng công việc khung lớp như vậy (ở đây: hàm chính của bạn) có thể xử lý tất cả các mục trong một bộ sưu tập (ở đây: mảng của bạn) một cách đồng nhất và không cần, không muốn và thực sự thường không thể biết việc triển khai cụ thể nào sẽ được gọi tại thời gian chạy, ví dụ vì nó chưa tồn tại. Đây là một trong những nền tảng của việc khắc sâu các mối quan hệ trừu tượng giữa các đối tượng và người xử lý.
Peter - Tái lập Monica

2

Dưới đây là ví dụ đầy đủ minh họa tại sao phương thức ảo được sử dụng.

#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

Về hiệu quả, các chức năng ảo kém hiệu quả hơn một chút so với các chức năng ràng buộc sớm.

"Cơ chế gọi ảo này có thể được thực hiện gần như hiệu quả như cơ chế" gọi hàm thông thường "(trong phạm vi 25%). Chi phí không gian của nó là một con trỏ trong mỗi đối tượng của một lớp có các hàm ảo cộng với một vtbl cho mỗi lớp như vậy" [ A chuyến tham quan C ++ của Bjarne Stroustrup]


2
Liên kết trễ không chỉ làm cho chức năng gọi chậm hơn, nó làm cho chức năng được gọi không xác định cho đến khi chạy, do đó tối ưu hóa trong chức năng gọi không thể được áp dụng. Điều này có thể thay đổi mọi thứ f.ex. trong trường hợp lan truyền giá trị loại bỏ rất nhiều mã (nghĩ rằng if(param1>param2) return cst;trình biên dịch có thể giảm toàn bộ lệnh gọi hàm thành hằng số trong một số trường hợp).
tò mò

1

Phương pháp ảo được sử dụng trong thiết kế giao diện. Ví dụ: trong Windows có một giao diện gọi là IUn Unknown như bên dưới:

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

Các phương thức này được để lại cho người dùng giao diện để thực hiện. Chúng rất cần thiết cho việc tạo và phá hủy một số đối tượng nhất định phải kế thừa IUn Unknown. Trong trường hợp này, thời gian chạy nhận thức được ba phương thức và hy vọng chúng sẽ được thực hiện khi nó gọi chúng. Vì vậy, theo một nghĩa nào đó, chúng hoạt động như một hợp đồng giữa chính đối tượng và bất cứ điều gì sử dụng đối tượng đó.


the run-time is aware of the three methods and expects them to be implementedVì chúng là ảo thuần túy, không có cách nào để tạo một thể hiện IUnknownvà vì vậy tất cả các lớp con phải thực hiện tất cả các phương thức như vậy để chỉ biên dịch. Không có nguy cơ không thực hiện chúng và chỉ phát hiện ra điều đó trong thời gian chạy (nhưng rõ ràng người ta có thể thực hiện sai chúng, tất nhiên!). Và wow, hôm nay tôi đã học Windows #definesa macro với từ này interface, có lẽ vì người dùng của họ không thể (A) nhìn thấy tiền tố Itrong tên hoặc (B) nhìn vào lớp để xem đó là giao diện. Ugh
gạch dưới

1

Tôi nghĩ rằng bạn đang đề cập đến thực tế một khi phương thức được khai báo là ảo, bạn không cần sử dụng từ khóa 'ảo' trong phần ghi đè.

class Base { virtual void foo(); };

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

Nếu bạn không sử dụng 'ảo' trong khai báo foo của Base thì foo của Derive sẽ chỉ che giấu nó.


1

Đây là một phiên bản hợp nhất của mã C ++ cho hai câu trả lời đầu tiên.

#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;
}

Hai kết quả khác nhau là:

Không có #define ảo , nó liên kết tại thời gian biên dịch. Animal * ad và func (Animal *) đều hướng đến phương thức say () của Animal.

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

Với #define ảo , nó liên kết trong thời gian chạy. Dog * d, Animal * ad và func (Animal *) point / tham khảo phương thức Dog's say () vì Dog là loại đối tượng của chúng. Trừ khi phương thức [Dog's said () "wagger"] không được xác định, nó sẽ là phương thức được tìm kiếm đầu tiên trong cây lớp, tức là các lớp dẫn xuất có thể ghi đè các phương thức của các lớp cơ sở của chúng [Animal's say ()].

$ 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

Thật thú vị khi lưu ý rằng tất cả các thuộc tính lớp (dữ liệu và phương thức) trong Python là ảo thực sự . Vì tất cả các đối tượng được tạo động khi chạy, không có khai báo kiểu hoặc nhu cầu từ khóa ảo. Dưới đây là phiên bản mã của 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)))

Đầu ra là:

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

trùng với định nghĩa ảo của C ++. Lưu ý rằng dquảng cáo là hai biến con trỏ khác nhau tham chiếu / trỏ đến cùng một thể hiện Dog. Biểu thức (quảng cáo là d) trả về True và các giá trị của chúng là cùng một đối tượng < main .Dog tại 0xb79f72cc>.


1

Bạn có quen thuộc với con trỏ chức năng? Các hàm ảo là một ý tưởng tương tự, ngoại trừ bạn có thể dễ dàng liên kết dữ liệu với các hàm ảo (với tư cách là thành viên lớp). Nó không phải là dễ dàng để liên kết dữ liệu với con trỏ chức năng. Đối với tôi, đây là sự phân biệt khái niệm chính. Rất nhiều câu trả lời khác ở đây chỉ nói "bởi vì ... đa hình!"


0

Chúng ta cần các phương thức ảo để hỗ trợ "Chạy đa hình thời gian". Khi bạn tham chiếu đến một đối tượng lớp dẫn xuất bằng cách sử dụng một con trỏ hoặc một tham chiếu đến lớp cơ sở, bạn có thể gọi một hàm ảo cho đối tượng đó và thực hiện phiên bản của lớp dẫn xuất của hàm.


-1

Điểm mấu chốt là các chức năng ảo làm cho cuộc sống dễ dàng hơn. Chúng ta hãy sử dụng một số ý tưởng của M Perry và mô tả những gì sẽ xảy ra nếu chúng ta không có các chức năng ảo và thay vào đó chỉ có thể sử dụng các con trỏ chức năng thành viên. Chúng tôi có, trong ước tính bình thường không có chức năng ảo:

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

Ok, đó là những gì chúng ta biết. Bây giờ chúng ta hãy thử làm điều đó với các con trỏ hàm thành viên:

 #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;
 }

Mặc dù chúng ta có thể thực hiện một số điều với các con trỏ hàm thành viên, chúng không linh hoạt như các hàm ảo. Thật khó để sử dụng một con trỏ hàm thành viên trong một lớp; con trỏ hàm thành viên hầu như, ít nhất là trong thực tế của tôi, luôn phải được gọi trong hàm chính hoặc từ bên trong một hàm thành viên như trong ví dụ trên.

Mặt khác, các hàm ảo, trong khi chúng có thể có một số chi tiết con trỏ hàm, thực hiện đơn giản hóa mọi thứ một cách đáng kể.

EDIT: Có một phương thức khác tương tự bởi eddietree: hàm ảo c ++ so với con trỏ hàm thành viên (so sánh hiệu suất) .

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.