Переопределение против Виртуального - PullRequest
39 голосов
/ 29 мая 2010

Какова цель использования зарезервированного слова virtual перед функциями? Если я хочу, чтобы дочерний класс переопределил родительскую функцию, я просто объявляю ту же функцию, например void draw(){}.

class Parent { 
public:
    void say() {
        std::cout << "1";
    }
};

class Child : public Parent {
public:
    void say()
    {
        std::cout << "2";
    }
};

int main()
{
    Child* a = new Child();
    a->say();
    return 0;
}

Выход составляет 2.

Итак, еще раз, зачем зарезервированное слово virtual необходимо в заголовке say()?

Спасибо большое.

Ответы [ 7 ]

37 голосов
/ 29 мая 2010

Если бы функция была виртуальной, то вы могли бы сделать это и получить вывод «2»:

Parent* a = new Child();
a->say();

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

25 голосов
/ 29 мая 2010

Попробуйте:

Parent *a = new Child();
Parent *b = new Parent();

a->say();
b->say();

Без virtual, оба с печатью '1'. Добавьте виртуальный, и ребенок будет действовать как ребенок, даже если на него ссылается указатель на Parent.

18 голосов
/ 29 мая 2010

Я думаю, это классический вопрос о том, как работает полиморфизм. Основная идея заключается в том, что вы хотите абстрагировать конкретный тип для каждого объекта. Другими словами: вы хотите иметь возможность вызывать экземпляры Child, не зная, что это ребенок!

Вот пример: Предполагая, что у вас есть класс «Child» и «Child2» и «Child3», вы хотите иметь возможность ссылаться на них через их базовый класс (Parent).

Parent* parents[3];
parents[0] = new Child();
parents[1] = new Child2();
parents[2] = new Child3();

for (int i=0; i<3; ++i)
    parents[i]->say();

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

16 голосов
/ 29 мая 2010

Если вы не используете ключевое слово virtual, вы не переопределяете, а определяете несвязанный метод в производном классе, который скрывает метод базового класса. То есть без virtual, Base::say и Derived::say не связаны - кроме совпадения имен.

Когда вы используете виртуальное ключевое слово (требуется в базе, необязательно в производном классе), вы сообщаете компилятору, что классы, производные от этой базы, смогут переопределить метод. В этом случае Base::say и Derived::say считаются переопределениями одного и того же метода.

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

12 голосов
/ 20 декабря 2010

Ну, я проверил это для себя, потому что есть много вещей, о которых мы можем думать:

#include <iostream>
using namespace std;
class A
{
public:
    virtual void v() { cout << "A virtual" << endl; }
    void f() { cout << "A plain" << endl; }
};

class B : public A
{
public:
    virtual void v() { cout << "B virtual" << endl; }
    void f() { cout << "B plain" << endl; }
};

class C : public B
{
public:
    virtual void v() { cout << "C virtual" << endl; }
    void f() { cout << "C plain" << endl; }
};

int main()
{
    A * a = new C;
    a->f();
    a->v();

    ((B*)a)->f();
    ((B*)a)->v();
}

выход:

A plain
C virtual
B plain
C virtual

Я думаю, что хороший, простой и короткий ответ может выглядеть следующим образом (потому что я думаю, что люди, которые могут понять больше, могут запомнить меньше, поэтому нуждаются в коротком и простом объяснении):

Виртуальные методы проверяют DATA экземпляра, на который указывает указатель, в то время как классические методы, таким образом, не вызывают метод, соответствующий указанному типу.

Смысл этой функции заключается в следующем: предположим, у вас есть массив А. Массив может содержать B, C (или даже производные типы). если вы хотите последовательно вызывать один и тот же метод для всех этих экземпляров, вы должны вызывать каждый из них, который вы перегружены.

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

Особенность VFtables в том, что мне никогда не объясняли, какой код он добавляет, и именно здесь C ++ требует гораздо большего опыта, чем C, и это может быть основной причиной того, что C ++ был помечен как «медленный» в своем коде. Первые дни: на самом деле, это мощно, но, как и все, оно мощно, если вы знаете, как его использовать, или вы просто «отрываете себе ногу».

2 голосов
/ 29 мая 2010

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

0 голосов
/ 29 мая 2010

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

Что произойдет, если вы измените свой основной на:

int main() { Parent* a = new Child(); a->say(); return 0; }

Также стоит понять, что такое vtable.

...