Виртуальные функции и полиморфизм - PullRequest
1 голос
/ 14 марта 2010

Предположим, у меня есть это:

class A
{
    public:
    virtual int hello(A a);
};

class B : public A
{
   public:
   int hello(B b){ bla bla };
};

Итак, A это абстрактный класс.

1) В классе B я определяю метод, который, как предполагается, переопределяет Aучебный класс.Но этот параметр немного отличается.Я не уверен в этом, это правильно?Может быть, из-за полиморфизма это нормально, но это довольно запутанно.2) Если я это сделаю: A a = new B; затем a.hello (смеется);если "lol" не относится к типу B, то это приведет к ошибке компиляции?, а если это тип A из другого класса C (класс C: public A), что произойдет?

Я запуталсяо переопределении и виртуальности ... все примеры, которые я нашел, работают с методами без параметров.

Любой ответ, ссылка или что-то еще, что ценится.

спасибо

pd: извините за мой английский

Ответы [ 6 ]

7 голосов
/ 14 марта 2010

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

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

Когда виртуальный вызов выполняется для указателя или ссылки, которая имеет тип базового класса, он будет «учитывать» только переопределения в производном классе, но не перегрузки. Это очень важно - для того, чтобы вызывающий объект обрабатывал экземпляр B так, как будто он делает все, что может сделать A (что является точкой динамического полиморфизма и виртуальных функций), его функция hello должна иметь возможность принимать любой тип A. Функция A hello, которая принимает только объекты типа B, а не A, является более строгой. Он не может играть роль функции hello, поэтому он не переопределен.

Если вы немного поэкспериментируете с вызовом hello для A и B, передавая объекты типа A или B, вы сможете увидеть разницу. A имеет функцию, принимающую A (которую вы не определили, поэтому, если вы ее вызовите, ваша программа не сможет связать, но вы можете это исправить). В B есть функция, принимающая B. Они имеют одно и то же имя, и, конечно, поскольку B наследуется от A, вы можете передать B функции, принимающей A. Но функция B не действует как переопределение в виртуальных вызовах. .

Можно вызывать функцию A для объекта B, но только через ссылку или указатель на A. Особенностью C ++ является то, что определение hello в B скрывает определение в A. Если перегрузка - это то, что вам нужно хотите, можно скрыть функцию базового класса, добавив using A::hello; к классу B. Если вы хотите переопределить то, что вам нужно, вы должны определить функцию, принимающую те же параметры. Например:

#include <iostream>

class A
{
    public:
    virtual int hello(A a) {std::cout << "A\n"; }
    virtual int foo(int i) { std::cout << "A::Foo " << i << "\n"; }
};

class B : public A
{
   public:
   using A::hello;
   // here's an overload
   int hello(B b){ std::cout << "B\n"; };
   // here's an override:
   virtual int foo(int i) { std::cout << "B::Foo " << i << "\n"; }
};

int main() {
    A a;
    B b;
    a.hello(a);  // calls the function exactly as defined in A
    a.hello(b);  // B "is an" A, so this is allowed and slices the parameter
    b.hello(a);  // OK, but only because of `using`
    b.hello(b);  // calls the function exactly as defined in B
    A &ab = b;   // a reference to a B object, but as an A
    ab.hello(a); // calls the function in A
    ab.hello(b); // *also* calls the function in A, proving B has not overridden it
    a.foo(1);    // calls the function in A
    b.foo(2);    // calls the function in B
    ab.foo(3);   // calls the function in B, because it is overridden
}

Выход:

A
A
A
B
A
A
A::Foo 1
B::Foo 2
B::Foo 3

Если вы удалите строку using A::hello; из B, то вызов b.hello(a); не будет скомпилирован:

error: no matching function for call to `B::hello(A&)'
note: candidates are: int B::hello(B)
4 голосов
/ 14 марта 2010

Куча хороших ответов, рассказывающих, ЧТО происходит, я подумала, что я пойду с ПОЧЕМУ.

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

Ваша попытка переопределить усиливает предварительное условие, оно принимает Bs, а не все как.

Обратите внимание, что ковариация IS допускается для типов возврата. Если ваш базовый класс возвратил A, то он гарантирует, что возвращаемое значение равно -A. Базовый класс мог бы затем вернуть B, потому что каждый B является -A A.

Но для входных параметров только контравариантность соответствует теоретическим требованиям LSP, а входные / выходные параметры являются инвариантами. В частности, в C ++ все типы параметров инвариантны для целей перегрузки.

3 голосов
/ 14 марта 2010

Во-первых, A не является абстрактным классом в вашем коде. Он должен иметь хотя бы одну чисто виртуальную функцию, чтобы быть абстрактным.

  1. разные параметры означают совершенно разные методы, даже если имя одно и то же. Думайте об этом как о другом имени. Вот почему это называется «подпись». Если A будет абстрактным классом, этот код не будет компилироваться вообще.

  2. A :: hello () будет вызван. Нет проблем с этим, и параметр должен иметь тип A, как если бы не было наследования.

1 голос
/ 14 марта 2010

Спасибо за ответы, но я должен уточнить некоторые вещи, чтобы получить окончательный ответ.

Предположим, у меня есть класс A именно так, как я его определил в исходном вопросе. И я добавляю еще один метод:

class A {
    ...
    int yeah();
}

Затем я определяю класс B следующим образом:

class B : public A {
    int hello(A a);
};

И еще один класс С, аналогичный В.

Что я знаю, потому что я программист, так это то, что hello методы B и C будут иметь явно объекты типа A в качестве параметров, но экземпляры одного и того же класса. Например:

B b; 
b.hello(some_other_b_instance);

или

C c; 
c.hello(some_other_c_instance);

Проблема в том, что в каждой привет-функции классов B и C я хочу делать определенные вещи с атрибутами определенного класса B или C. И поскольку параметр имеет тип A, я не могу их использовать.

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

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

1 голос
/ 14 марта 2010

То, что вы делаете, это перегрузка не переопределение, то есть как будто класс B:

class B
{
public:
  int hello(A a) {...}
  int hello(B b) {...}
};

У вас есть две функции с одинаковыми именами, но с разными сигнатурами, что делает их разными функциями (точно так же, как стандартная библиотека имеет разные функции для abs(float) и abs(double) и т. Д.)

Если вы хотите переопределить, тогда вам нужно иметь ту же самую сигнатуру, то есть, привет классу B, должен принять параметр типа A. Таким образом, когда вы вызываете hello для объекта класса B, он будет использовать класс B hello, а не класс A.

Если вы действительно хотите, чтобы класс B приветствовал только объекты типа B, то у вас все в порядке, хотя вы, вероятно, хотите сделать hello класса A не виртуальным, поскольку вы на самом деле не хотите его переопределять - вы определяют новую функцию с новыми параметрами и новым поведением.

1 голос
/ 14 марта 2010

Когда вы переопределяете метод, он переопределяет, что будет делать метод. Вы можете переопределить только виртуальные элементы, которые уже определены (с их набором параметров). Если тип A, метод на A будет вызван. Если тип имеет тип B, метод на B будет вызываться, даже если переменная имеет тип A, но содержит экземпляр типа B.

Нельзя изменить определения параметров для переопределенного метода, иначе он перестанет быть переопределенным.

...