почему у нас на самом деле есть виртуальные функции? - PullRequest
11 голосов
/ 10 февраля 2010

Я новичок в C ++.

Может кто-нибудь сказать мне разницу между переопределением методов и концепциями виртуальных функций в c ++.

Функциональность виртуальных функций может быть переопределена в ее производных классах. Переопределение функции в производном классе называется переопределением функции.

почему у нас фактически есть виртуальные функции?

Ответы [ 10 ]

12 голосов
/ 10 февраля 2010

Виртуальная функция / метод - это просто функция, поведение которой можно переопределить в подклассе (или в терминах C ++ производного класса) путем переопределения работы функции (с использованием той же сигнатуры).

Подумайте омлекопитающее базового класса с функцией речи.Функция не имеет смысла и просто описывает, как говорит млекопитающее.Когда вы унаследуете от этого класса, вы можете переопределить метод говорения, так что собаки идут "Arf Arf!"и коты говорят: «Мяу-мяу».

Похоже, ваш вопрос состоит в том, в чем заключаются различия, ну, в общем, их нет, потому что с помощью виртуальных функций можно переопределить поведение этих функций.Возможно, вы почувствуете разницу между переопределением функций и их перегрузкой.

Перегрузка функций означает создание функции с тем же именем, но с разными аргументами, т.е. с разными аргументами числа и типа.Вот объяснение перегрузки в C ++ с сайта IBM :

Перегрузка (только C ++) Если вы задаете более одного определения для имени функции или оператора в одной области действияВы перегрузили это имя функции или оператора.Перегруженные функции и операторы описаны в Перегрузочных функциях (только C ++) и Операторах перегрузки (только C ++), соответственно.

Перегруженное объявление - это объявление, которое было объявлено с тем же именем, что и ранее объявленное объявление вта же область действия, за исключением того, что оба объявления имеют разные типы.

Если вы вызываете перегруженное имя функции или оператор, компилятор определяет наиболее подходящее определение для использования, сравнивая типы аргументов, которые вы использовали для вызова функции или оператора с помощьютипы параметров, указанные в определениях.Процесс выбора наиболее подходящей перегруженной функции или оператора называется разрешением перегрузки, как описано в разделе Разрешение перегрузки (только C ++).

Что касается полной рациональной причины ситуаций, когда требуются виртуальные функции, этохорошая запись в блоге: http://nrecursions.blogspot.in/2015/06/so-why-do-we-need-virtual-functions.html

9 голосов
/ 10 февраля 2010

Разница между переопределением функции и virtual функцией становится важной при полиморфизме . В частности, при использовании ссылок или указателей на базовый класс.

Базовая настройка

В C ++ любой производный класс может быть передан в функцию, для которой требуется объект базового класса. (См. Также Нарезка и LSP ). Дано:

struct Base_Virtual
{
  virtual void some_virtual_function();
};

struct Base_Nonvirtual
{
  void some_function();
};

void Function_A(Base_Virtual * p_virtual_base);
void Function_B(Base_Nonvirtual * p_non_virtual_base);

В приведенном выше коде есть два базовых класса, один объявляет виртуальный метод, другой объявляет не виртуальную функцию.

Объявлены две функции, требующие указателей на соответствующие базовые предложения.

Производные классы

Давайте теперь проверим полиморфизм, особенно virtual по сравнению с не виртуальным (переопределяющие методы). Структуры:

struct Derived_From_Virtual
: public Base_Virtual
{
  void some_virtual_function(); // overrides Base_Virtual::some_virtual_function()
};

struct Derived_From_Nonvirtual : public Base_Nonvirtual { void some_function (); }

В соответствии с языком C ++ я могу передать указатель на Derived_From_Virtual на Function_A, поскольку Derived_From_Virtual наследуется от Base_Virtual. Я также могу передать указатель на Derived_From_Nonvirtual на Function_B.

Разница между virtual и переопределением

Модификатор virtual в Base_Virtual сообщает компилятору, что Function_A будет использовать Derived_From_Virtual::some_virtual_function() вместо метода в Base_Virtual. Это потому, что метод virtual , окончательное определение может находиться в future или производном классе. Фактическое определение говорит об использовании метода в наиболее производном классе, содержащем определение.

При передаче указателя на Derived_From_Nonvirtual на Function_B компилятор даст указание функции использовать метод базового класса, Base_Nonvirtual::some_function(). Метод some_function() в производном классе - это отдельный, не связанный метод с базовым классом.

Основная разница между virtual и переопределением возникает при полиморфизме.

8 голосов
/ 10 февраля 2010

Ознакомьтесь с C ++ FAQ lite, http://www.parashift.com/c++-faq-lite/., вероятно, один из лучших ресурсов C ++ для начинающих. он содержит подробные сведения о виртуальных функциях и переопределении.

Я лично нашел, что C ++ FAQ - отличный источник, когда я изучаю C ++. Другие люди имеют другое мнение, ваш пробег может отличаться

4 голосов
/ 11 февраля 2010

Это скорее продолжение комментариев к этому ответу , чем сам по себе ответ.

virtual - это ключевое слово, которое запрашивает диспетчеризацию во время выполнения для объявленного метода и в то же время объявляет метод как одно из переопределений (кроме реализованных чисто виртуальных методов). Объявленный метод и любой метод, который разделяет точную сигнатуру и имя в производной иерархии из этого класса, являются переопределениями . Когда вы вызываете виртуальный метод через родительский указатель или ссылку, среда выполнения вызывает наиболее производное override в иерархии вызываемого объекта.

Если метод не является виртуальным, и этот же метод определен позже в иерархии, вы скрываете родительский метод. Разница здесь в том, что когда метод вызывается через базовый указатель или ссылку, он будет вызывать базовую реализацию, а если он вызывается в производном объекте, он будет вызывать производную реализацию. Это, в других случаях, называется скрытием, потому что базовая и производная функции не связаны, и определение ее в производном классе скрыт базовая версия от вызова:

struct base {
   virtual void override() { std::cout << "base::override" << std::endl; }
   void not_override() { std::cout << "base::not_override" << std::endl; }
};
struct derived : base {
   void override() { std::cout << "derived::override" << std::endl; }
   void not_override() { std::cout << "derived::not_override" << std::endl; }
};
int main() {
   derived d;
   base & b = d;

   b.override();     // derived::override
   b.not_override(); // base::not_override
   d.not_override(); // derived::not_override
}

Разница и то, что неправильно в ответе @ erik2red, заключается в том, что переопределения тесно связаны с виртуальными функциями и подразумевают, что существует механизм диспетчеризации во время выполнения, который определяет наиболее производные переопределить для вызова. Поведение, показанное в ответе и связанное с переопределением , фактически является поведением, когда нет переопределений, а скорее скрытие метода.

Другие вопросы

Язык допускает чисто виртуальные методы с реализацией. В нем ничего не говорится о том, какую терминологию следует использовать с ними, но чисто виртуальный метод никогда не будет рассматриваться для выполнения во время выполнения. Причина в том, что когда классы с чисто виртуальными методами (даже если они реализованы) считаются абстрактными классами, и вы не можете создать экземпляр объекта этого класса. Если у вас есть производный класс, который обеспечивает реализацию для этого метода, эта реализация становится окончательным переопределением в иерархии. Теперь можно создать экземпляр класса, но чистый виртуальный метод не будет вызываться через механизм диспетчеризации во время выполнения.

Виртуальные методы, которые не являются окончательным переопределением 1040 *, а также скрытые методы могут быть вызваны при использовании полного имени. В случае виртуальных методов использование полного имени отключает механизм полиморфной отправки для вызова: d.base::override() вызовет базовую реализацию, даже если в производных классах есть другие переопределения .

Метод может скрывать другие методы в базовых классах, даже если подписи не совпадают.

struct base {
   void f() {}
};
struct derived : base {
   void f(int) {}
};
int main() {
   derived d;
   // d.f() // error, derived::f requires an argument, base::f is hidden in this context
}

Как и в случае переопределений , d.base::f() будет вызывать базовую версию не потому, что она отключает полиморфизм - это не так, поскольку метод не объявлен виртуальным, он никогда не будет иметь полиморфное поведение - а потому полная квалификация сообщает компилятору, где находится метод, даже если он был скрыт другим методом в производном классе.

2 голосов
/ 11 февраля 2010

РЕФЕРАТ

В этой статье мы обсуждаем виртуальные функции в C ++. Часть ноль объясняет, как виртуальные функции объявляются и переопределяются. Первая часть пытается (и, возможно, не удается) объяснить, как реализованы виртуальные функции. Вторая часть - это пример программы, в которой используются примеры классов, определенных в нулевой и первой частях. Третья часть - классический пример животного, приведенный в каждой виртуальной функции - учебник по полиморфизму.

ЧАСТЬ НОЛЬ

Метод класса называется виртуальным тогда и только тогда, когда он объявлен таковым.

class my_base
{
public:
            void non_virtual_test() { cout << 4 << endl; } // non-virtual
    virtual void virtual_test()     { cout << 5 << endl; } // virtual
};

(Конечно, я предполагаю, что программист ранее не делал ничего подобного #define virtual.)

Класс, который повторно объявляет и повторно реализует не виртуальный метод одной из своих баз, называется перегрузкой этого метода. Класс, который повторно объявляет и повторно реализует виртуальный метод одной из его баз, называется переопределить этот метод.

class my_derived : public my_base
{
public:
    void non_virtual_test() { cout << 6 << endl; } // overloaded
    void virtual_test()     { cout << 7 << endl; } // overriden
};

ЧАСТЬ ПЕРВАЯ

Когда компилятор обнаруживает, что класс имеет виртуальные методы, он автоматически добавляет таблицу виртуальных методов (также известную как vtable ) к макету памяти класса. Результат похож на то, что было бы сгенерировано при компиляции этого кода:

class my_base
{
//<vtable>
// The vtable is actually a bunch of member function pointers
protected:
    void (my_base::*virtual_test_ptr)();
//</vtable>

// The actual implementation of the virtual function
// is hidden from the rest of the program.
private:
    void virtual_test_impl() { cout << 5 << endl; }

// Initializing the real_virtual_test pointer in the vtable.
public:
    my_base() : virtual_test_ptr(&my_base::virtual_test_impl) {}

public:
    void non_virtual_test() { cout << 4 << endl; }
    // The interface of the virtual function is a wrapper
    // around the member function pointer.
    inline void virtual_test() { *virtual_test_ptr(); }
};

Когда компилятор обнаруживает, что класс переопределил виртуальный метод, он заменяет связанную с ним запись в vtable. Результат похож на то, что было бы сгенерировано при компиляции этого кода:

class my_derived : public my_base
{
// The actual implementation of the virtual function
// is hidden from the rest of the program.
private:
    void virtual_test_impl() { cout << 7 << endl; }

// Initializing the real_virtual_test pointer in the vtable.
public:
    my_derived() : virtual_test_ptr(&my_derived::virtual_test_impl) {}

public:
    void non_virtual_test() { cout << 6 << endl; }
};

ЧАСТЬ ВТОРАЯ

Теперь, когда ясно, что виртуальные функции реализованы с использованием vtables, которые являются ничем иным, как набором указателей на функции, должно быть понятно, что делает этот код:

#include <iostream>

using namespace std;

class my_base
{
    public:
            void non_virtual_test() { cout << 4 << endl; }
    virtual void virtual_test()     { cout << 5 << endl; }
};

class my_derived : public my_base
{
public:
    void non_virtual_test() { cout << 6 << endl; }
    void virtual_test()     { cout << 7 << endl; }
}

int main()
{
    my_base* base_obj = new my_derived();

    // This outputs 4, since my_base::non_virtual_test() gets called,
    // not my_derived::non_virtual_test().
    base_obj->non_virtual_test();

    // This outputs 7, since the vtable pointer points to
    // my_derived::virtual_test(), not to my_base::virtual_test().
    base_obj->virtual_test();

    // We shall not forget
    // there was an object that was pointed by base_obj
    // who happily lived in the heap
    // until we killed it.
    delete base_obj;

    return 0;
}

ЧАСТЬ ТРЕТЬЯ

Поскольку ни один пример виртуальной функции не обходится без примера с животными ...

#include <iostream>

using namespace std;

class animal
{
public:
    virtual void say_something()
    { cout << "I don't know what to say." << endl
           << "Let's assume I can growl." << endl; }

    /* A more sophisticated version would use pure virtual functions:
     *
     * virtual void say_something() = 0;
     */
};

class dog : public animal
{
public:
    void say_something() { cout << "Barf, barf..." << endl; }
};

class cat : public animal
{
public:
    void say_something() { cout << "Meow, meow..." << endl; }
};

int main()
{
    animal *a1 = new dog();
    animal *a2 = new cat();
    a1->say_something();
    a2->say_something();
}
1 голос
/ 10 февраля 2010

Приходя из Java, можно было бы запутать концепцию виртуальных и не виртуальных функций-членов. Следует помнить, что методы Java соответствуют виртуальным функциям-членам в C ++.

Вопрос не столько в том, почему у нас фактически есть виртуальные функции, а в том, почему у нас есть не виртуальные? То, как я их оправдываю (поправьте меня, если я ошибаюсь), заключается в том, что их дешевле реализовать, поскольку вызовы к ним могут быть разрешены во время компиляции.

1 голос
/ 10 февраля 2010

Виртуальные функции существуют, чтобы помочь спроектировать поведение базового класса. Базовый класс чисто виртуальных функций не может быть создан и называется абстрактным классом.

Производные классы должны реализовывать методы, описанные виртуальными функциями в базовом классе. Производные классы могут быть созданы (они существуют и занимают память).

Из производных классов можно переопределить функцию, уже определенную в родительском объекте. Эта техника уже известна как переопределение и позволяет настраивать поведение этого дочернего объекта.

Когда вы узнаете больше о C ++, вы обнаружите, что наследование - это еще не все, чем нужно. Композиция и часто является лучшей альтернативой. Веселитесь.

0 голосов
/ 11 февраля 2010

Классическим примером является программа рисования, в которой базовый класс Shape создается с помощью виртуальной функции draw (). Затем каждая из фигур (круг, прямоугольник, треугольник и т. Д.) Может быть создана как подкласс, каждый из которых реализует свою функцию draw () соответствующим образом, и основная программа рисования может хранить список фигур, каждый из которых будет выполнять соответствующее рисование ( ), хотя хранится только указатель на базовый класс Shape.

0 голосов
/ 10 февраля 2010

Вертолеты и самолеты оба летают, но они делают это по-разному - оба они являются экземплярами какого-то гипотетического объекта Flyer. Вы можете попросить объект Flyer «летать», но Flyer - это просто интерфейс. Он ничего не знает о полете, кроме того, что он должен летать.

Однако если и вертолет, и самолет следуют интерфейсу флаера, то если у вас есть аэродромный объект, и вы дали ему флаер, все, что нужно сделать аэродрому, - это попросить летчиков летать.

Например:

Airplace X=Airplane X("boeing 747");
Airfield::takeoff(&X);

Helicopter Y= Helicopter("Comache");
Airfield::takeof(&Y);

void Airfield::takeOff(Flyer * f)
{
     f->fly();
}

C ++ является строгим типобезопасным языком, и такого рода функциональные возможности (косвенные вызовы функций для производных классов через базовый класс) возможны только тогда, когда RTTI включен для иерархии объектов, и квалификация виртуальной функции-члена позволяет это .

0 голосов
/ 10 февраля 2010

Разница используется только при вызове метода производного класса через указатель на объект базового класса. В тот момент, если вы вызываете переопределенный метод в производном классе, вы получите исключение базового класса, а если он был виртуальным, вы получили выполнение метода производного класса.

#include <iostream>

class A{
    public:
    virtual void getA() { std::cout << "A in base" << std::endl;};
};

class B : public A {
    public:
    void getA() { std::cout << "A in derived class" << std::endl;}
};

int main(int argc, char** argv)
{
    A a;
    B b;
    a.getA();
    b.getA();

    A* t = new B;
    t->getA();
}

Например: в этой программе t->getA() напечатать "A in derived class", но если в базовом классе А не было виртуального модификатора, он вывел бы "A in base".

Надеюсь, это поможет.

...