Страницы

Поиск по вопросам

суббота, 21 декабря 2019 г.

В каких случаях использовать указатель на базовый класс, а в каких на наследник?

#cpp #полиморфизм


Не могли бы вы прокомментировать этот момент: 


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


В каких случаях необходимо использовать указатель на класс наследник, а в каких на
базовый класс?

Просто недавно наткнулся на такой пример, он корректно реализован?

#include 
using namespace std;

class Base { //Базовый класс
public:
   int base_data;
   Base(int base_data);//Конструктор класса
   virtual void Virtual_method();   // Виртуальный метод
   void Nonvirtual_method();   // Не виртуальный метод
   virtual~Base(); //Виртуальный деструктор
};

Base::Base(int base_data){
    this->base_data = base_data;
    cout << "Конструктор базового" << endl;
}

void Base::Virtual_method() {
    cout << "Виртуальный метод базового класса\n";
}

void Base::Nonvirtual_method() {
    cout << "Не виртуальный метод базового класса\n";
}

Base::~Base(){
    cout << "Деструктор базового класса\n";
}

class Derived : public Base {//Класс наследник
public:
    double derived_data;
    Derived(double derived_data, int base_data);
    void Virtual_method();   // Переопределённый виртуальный метод базового класса
    void Nonvirtual_method();   //Не виртуальна функция
    ~Derived();
};

Derived::Derived(double derived_data, int base_data):Base(base_data){
    this->derived_data = derived_data;
    cout << "Конструктор наследника" << endl;
}


void Derived::Virtual_method() {
    cout << "Переопределённый виртуальный метод класса наследника\n";
}

void Derived::Nonvirtual_method() {
    cout << "Не виртуальный метод класса наследника\n";
}

Derived::~Derived(){
    cout << "Деструтор класса наследника\n";
}

int main() {
    setlocale(LC_ALL, "RUS");
    Derived derived(2.0, 1);//Объявили объект класса наследника

    Derived *pDerived = &derived;//Объявили указатель на класс наследник, присвоив
ему ссылку на объект класса наследника
    Base    *pBase    = &derived;//Объявили указатель на базовый класс, присвоив
ему ссылку на объект класса наследника

    pBase->Virtual_method(); // Вызов виртуального метода
    pBase->Nonvirtual_method(); // Вызов не виртуального метода
    pDerived->Virtual_method(); // Вызов виртуального метода
    pDerived->Nonvirtual_method(); // Вызов не виртуального метода
    cout << "base_data = " << derived.base_data << "\nderived_data = " << derived.derived_data
<< "\n";
    return 0;
}

    


Ответы

Ответ 1



Обычно, когда говорят о полиморфизме в программировании, то пытаются дать определение этого слова в терминах языков программирования. Однако на мой взгляд более удачное определение полиморфизма дается в биологии. Только слово организм в этом определении следует заменить словом объект в терминах программирования.:) Полиморфи́зм в биологии (от др.-греч. πολύμορφος — многообразный) — способность некоторых организмов существовать в состояниях с различной внутренней структурой или в разных внешних формах В вашей демонстрационной программе определяется указатель типа Base *, то есть статический тип объектов, адресуемых этим указателем является тип Base. Base *pBase; Когда такому указателю присваивается адрес объекта производного класса, то объект, к которому происходит обращение через этот указатель, рассматривается как объект типа Base. Фактически, вся информация о том, что этот объект на самом деле является производного класса, а не базового, теряется. так как статический тип указателя Base *. Однако благодаря наличию такого средства, как виртуальные функции, позволяет получать многообразие поведения адресуемого указателем объекта в зависимости от того, какой тип на самом деле имеет адресуемый объект. То есть через виртуальные функции имеется возможность различать реальный тип адресуемых объектов и обращаться к их уникальным свойствам. И это демонстрируется ваша программа на примере данных предложения Base *pBase = &derived;//Объявили указатель на базовый класс, присвоив ему ссылку на объект класса наследника pBase->Virtual_method(); // Вызов виртуального метода pBase->Nonvirtual_method(); // Вызов не виртуального метода Как видно указатель типа Base * на самом деле адресует объект производного класса. Но если вызывать не виртуальный метод pBase->Nonvirtual_method(); // Вызов не виртуального метода то вся информация о производном классе недоступна, так как компилятор вызывает функции в соответствии со статическим типом объекта, адресуемого указателем, то есть в соответствии с типом Base. Компилятор при поиске имени функции, которую следует вызвать смотрит определение базового класса. Он ничего не знает о производных классах. Ежели вы вызываете виртуальную функцию pBase->Virtual_method(); // Вызов виртуального метода то компилятор также при поиске имени вызываемой функции смотрит определение базового класса. Однако тут происходит некоторый фокус. Производный класс подменил определение виртуальной функции в базовом классе своим определением этой функции. Технически это делается следующим образом. Если класс объявляет виртуальную функцию, то он создает таблицу указателей на виртуальные функции, объявленные в своем определении. Производные же классы, которые переопределяют виртуальные функции подставляют в эту таблицу базового класса адреса своих определений функций. Поэтому динамически во время выполнения программы вызывается та функция, чей адрес находится в таблице адресов виртуальных функций. Что касается вашего вопроса В каких случаях необходимо использовать указатель на класс наследник, а в каких на базовый класс? То когда вы объявляете указатель на класс наследник, то вы теряете полиморфизм, так как вы можете работать только с объектами класса наследника (я предполагаю, что наследник в свою очередь не наследуется другими классами). А когда вы хотите достичь поведение, соответствующее полиморфизму, то 1) базовый класс должен содержать виртуальные функции т 2) следует объявить указатель или ссылку, имеющую статический тип указателя или ссылки на объекты базового класса, а инициализировать их объектами производных классов. И тогда вы получите способность некоторых объектов (организмов) существовать в состояниях с различной внутренней структурой или в разных внешних формах Примечание. Если при вызове виртуальной функции вы указываете ее квалифицированное имя, то "виртуальность" исчезает. Вызывается функция класса, определенная в этом же классе. Рассмотрите пример #include struct Base { virtual ~Base() { } virtual void virtual_function() const { std::cout << "Base::virtual_function() is called" << std::endl; } }; struct Derived : Base { void virtual_function() const override { std::cout << "Derived::virtual_function() is called" << std::endl; } }; int main() { Derived d; Base &rBase = d;; rBase.virtual_function(); rBase.Base::virtual_function(); return 0; } Ее вывод на консоль будет следующим Derived::virtual_function() is called Base::virtual_function() is called В первом случае вступает в игру полиморфизм. Вызывается переопределенная виртуальная функция производного класса Derived, хотя базовый тип ссылки - это класс Base. Во втором случае, когда используется квалифицированное имя, "виртуальность" фукнции пропадает, и вызывается реализация функции базового класса Base.

Ответ 2



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

Комментариев нет:

Отправить комментарий