Вопрос о полиморфизме / наследовании в C ++: переопределение базовых функций против виртуальных функций - PullRequest
10 голосов
/ 03 февраля 2011

Я знаю, что производные классы могут просто "переопределить" функции-члены базового класса, и что при вызове этой функции объекта производного класса используется функция, определенная в производном классе, но ...«виртуальное» ключевое слово избыточно?Я читал о некоторых очевидных существенных различиях между этими двумя случаями (т. Е. Если у вас есть указатель базового класса, указывающий на производный класс, и вы вызываете функцию, если она является виртуальной, будет вызвана функция производного класса, но если нет, тобудет вызываться функция базового класса).

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

ЛичноМне кажется, что это очень запутанно.

Спасибо!

Ответы [ 8 ]

7 голосов
/ 03 февраля 2011

Наиболее распространенный подход на наиболее распространенных языках ООП (Java, SmallTalk, Python и т. Д.) Состоит в том, чтобы по умолчанию каждая функция-член имела вид virtual.

Недостатком является то, что существуетнебольшое снижение производительности за каждый виртуальный вызов.По этой причине C ++ позволяет вам выбрать, хотите ли вы, чтобы ваши методы были определены как виртуальные или нет.

Однако между виртуальным и не виртуальным методом есть очень важное различие.Например:

class SomeClass { ... };
class SomeSubclassOfSomeClass : public SomeClass { ... };
class AnotherSubclassOfSomeClass : public SomeClass { ... };

SomeClass* p = ...;

p->someVirtualMethod();

p->someNonVirtualMethod();

Фактический код, выполняемый при вызове someVirtualMethod, зависит от конкретного типа ссылочного указателя p и полностью зависит от переопределения подклассов SomeClass.

Но код, выполняемый при вызове someNonVirtualMethod, понятен: всегда тот, который находится на SomeClass, поскольку тип переменной p равен SomeClass.

2 голосов
/ 03 февраля 2011

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

В некоторых случаях вам не нужны накладные расходы на включение указателя vtable в каждый объект, поэтому вы стараетесьчтобы убедиться, что в классе нет виртуальных методов.Возьмем, к примеру, класс, который представляет точку и имеет два члена x и y - у вас может быть очень большая коллекция этих точек, а указатель vtable увеличит размер объекта как минимум на 50%.

1 голос
/ 03 февраля 2011

Адресация: «Другими словами, какова цель возможности переопределять функции-члены как не виртуальные функции, и является ли это широко используемой практикой?»

Ну, ты не можешь. Если метод базового класса является виртуальным, то же относится и к соответствующему методу производного класса, если он существует, независимо от того, используется ли ключевое слово «virtual».

Итак: «Разве это не делает« виртуальное »ключевое слово избыточным?». Да, является избыточным в методе производного класса, но не в базовом классе.

Однако обратите внимание, что необычно (будучи вежливым) хотеть иметь не виртуальный метод и затем переопределить его как производный класс.

1 голос
/ 03 февраля 2011

Это будет работать для экземпляра производного класса и указателя на производный класс.Однако, если вы передадите свой производный класс в функцию, которая получает указатель на Base, будет вызвана базовая версия функции.Это, вероятно, не желательно.Например, следующее вернет 5

#include "stdafx.h"
#include <conio.h>
#include <iostream>

class Base
{
public:
    int Foo(){return 5;}
};

class Derived:public Base
{
    int Foo(){return 6;}
};

int Func(Base* base)
{
    return base->Foo();
}

int _tmain(int argc, _TCHAR* argv[])
{
    Derived asdf;

    std::cout << Func(&asdf);
    getch();

    return 0;
}

Это из-за того, как работает виртуальный.Когда объект имеет виртуальный вызов, при вызове виртуальной функции ищется правильная функция в v-таблице.В противном случае у вас нет наследования, у вас есть указатель Base, действующий как базовый класс, а не производный класс.

0 голосов
/ 09 апреля 2012

Простейший способ объяснить это, вероятно, так:

Виртуальный поиск для вас, добавив виртуальную таблицу поиска.

Другими словами, еслиу вас не было виртуального ключевого слова, и вы переопределили метод, вам все равно пришлось бы вызывать этот метод вручную [простите, если моя память для синтаксиса C ++ немного ржавая]:

class A { void doSomething() { cout << "1"; } }
class B: public A { void doSomething() { cout << "2"; } }
class C: public A { void doSomething() { cout << "3"; } }

void someOtherFunc(A* thing) {
    if (typeid(thing) == typeid(B)) {
        static_cast<B*>(thing)->doSomething();
    } else if (typeid(thing) == typeid(C)) {
        static_cast<C*>(thing)->doSomething();
    } else {
        // not a derived class -- just call A's method
        thing->doSomething();
    }
}

ВыМожно немного оптимизировать это (для удобства чтения и производительности, скорее всего), используя таблицу поиска:

typedef doSomethingWithAnA(A::*doSomethingPtr)();
map<type_info, doSomethingWithAnA> A_doSomethingVTable;

void someOtherFuncA* thing) {
   doSomethingWithAnA methodToCall = A_doSomethingVTable[typeid(thing)];
   thing->(*methodToCall)();
}

Теперь это скорее высокоуровневый подход.Компилятор C ++, очевидно, может оптимизировать это немного больше, точно зная, что такое «type_info» и так далее.Так что, вероятно, вместо этой «карты» и поиска «methodToCall = aDoSomethingVTable [typeid (thing)]», затем вызов », компилятор вставляет что-то намного меньше и быстрее, например« doSomethingWithAnA * A_doSomethingVTable; », за которым следует« A_doSomethingTablething »-> type_number ".

Итак, вы правы, что C ++ на самом деле НЕ НУЖДАЕТСЯ в виртуальном, но он добавляет много синтаксического сахара, чтобы облегчить вашу жизнь, и может оптимизировать его тоже.

Тем не менее, я все еще думаю, что C ++ является ужасно устаревшим языком, с большим количеством ненужных сложностей. Например, виртуальный может (и, вероятно, должен ) быть принятым по умолчанию и оптимизироваться там, где это не нужно.Похожее на Scala ключевое слово "override" было бы гораздо полезнее, чем "virtual".

0 голосов
/ 03 февраля 2011

Правила области видимости в C ++ и правила поиска по именам допускают очень странные вещи, и методы здесь не одиноки.Действительно, Сокрытие (или Затенение ) может происходить во многих различных ситуациях:

int i = 3;
for (int i = 0; i != 5; ++i) { ... } // the `i` in `for` hides the `i` out of it

struct Base
{
  void foo();
  int member;
};

struct Derived: Base
{
  void foo(); // hides Base::foo
  int member; // hides Base::member
};

Почему тогда?Для устойчивости.

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

  • Мой вызов всегда вызовет Baseметод, независимо от того, переопределил ли его какой-нибудь дочерний элемент или нет
  • На вызов дочернего элемента не повлияет, поэтому я не буду внезапно завершать работу кода другого программиста

Очевидно, что если вы посмотритедля программы как конечной работы это не имеет смысла, но программы развиваются, а правило сокрытия облегчает эволюцию.

0 голосов
/ 03 февраля 2011

кажется, что вы не обнаружили силу полиморфизма. Полиморфизм работает следующим образом:

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

без этого механизма вы не сможете достичь этого. И этот механизм нуждается в "виртуальном"

0 голосов
/ 03 февраля 2011

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...