Множественное наследование в С++

  • В этой теме 0 ответов, 1 участник, последнее обновление 1 месяц назад сделано Васильев Владимир Сергеевич.
Просмотр 0 веток ответов
  • Автор
    Сообщения
    • #6953
      @admin
      StudLance.ru

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

      Иерархия классов будет такой: Человек, Профессия, Рабочие.

      Человек (Man) имеет закрытые поля для хранения имени и возраста, публичные функции получения их значения и метод преобразования объекта в строку. Задаваться поля будут с помощью конструктора или с помощью функции readMan, считывающий данные с потока istream. Класс имет виртуальный деструктор, так как планируется наследование от него — значит, класс может быть использован полиморфным образом;

      Профессия (Profession) имеет закрытые поля для названия и заработной платы. Как и Man, она имеет функции получения данных, ввода с потока, метод преобразования в строку и виртуальный деструктор.

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

      Такая иерархия имеет ряд проблем — в данном случае они созданы намеренно, но такие ситуации возникают на практике:

      • открытое наследование тут использовать неправильно, так как согласно принципу подстановки Лисков, оно выражает отношение «является». Рабочий является человеком — это верно, но он не является профессией. Рабочий имеет профессию. Отношение «имеет» лучше всего выражать посредством композции, иногда для этого используется закрытое наследование, но оно выражает отношение «реализуется посредством»;
      • Man и Profession имеют поле name, класс Worker наследует их — значит он получает два поля с одинаковым именем;
      • похожая ситуация возникает с функциями name() и toString(), однако, функция toString() является вирутальной.

      Решение проблем

      Ромбовидное наследование

      В нашем примере такой проблемы нет, но это похожая проблема, имеющая стандартное решение.

      Если бы функции name(), toString() и поле name попали в класс через общего предка — имела бы места ромбовидного наследования (показана на рисунке ниже. На самом деле, нередко возникает задача передчи объектов по сети, при этом:

      • они сериализуются (преобразуются в строку или набор байт);
      • все сериализуемые объекты наследуются от интерфейса типа ISerializable, содержащего функцию типа toString(), а значит все наследники получают такую функцию;
      • если некоторый класс наследуюется одновременно от двух сериализуемых классов — то он получит две версии toString() — как в нашем примере.

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

      class Worker : public Man, virtual public Profession {
        //...
      }

      При этом, если у Man и Profession есть общий предок — то он будет включен «реально» всего в одном экземпляре — от Man и виртуально от Profession. При обращении к нему через Profession будет происходить обращению к предку Man.

      Member in multiple base classes of different types

      В ромбовидном наследовании включение содержимого общего предка является недопустимым — это основная проблема, которая и решается с помощью виртуального наследования. В нашем же случае:

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

      То есть основной проблемой является перекрытие имен. Решить проблему можно двумя путями:

      • приводить тип Worker& к Man& или Profession& перед вызовом функции (или обращении к полю) в зависимости от того, какая функция нужна.
      • явно указывать пространство имен вызываемой функции. Такой подход работает, так как каждый класс задает пространство имени (namespace) и вызов функции bar для объекта класса Foo это обращение к функции Foo:bar(). То есть имя класса «кодируется» в имени функции неявно. Ведь реально функции Profession::name() и Man::name() — это разные функции, как и Man::readMan и Profession::readProfession, но во втором случае тип кодируется в имени программистом явно.

      Оба способа показаны в следующей функции:

      string Worker::toString() const {
        string buff;
        stringstream sstr;
      
        // const Profession* profession 
        //    = static_cast<const Profession*>(this);
        const Man* man = static_cast<const Man*>(this);
      
        sstr << Profession::name() << " - " << man->name() 
             << ": " << salary() << "\\" << deal_staff();
      
        std::getline(sstr, buff);
        return buff;
      }

      Тут обращение к полям Man:: происходит через указатель Man*, а в полям Profession — через указание пространство имен Profession::.

      Исходный код

      man.h:

      #ifndef MAN_H
      #define MAN_H
      #include <string>
      #include <istream>
      using namespace std;
      
      class Man {
      public:
        Man(string name, int age);
        virtual ~Man() = default;
      
        string name() const;
        int age() const;
      
        virtual string toStirng() const;
        istream&readMan(istream& ist);
      protected:
        string m_name;
        int m_age;
      };
      
      #endif // MAN_H
      

      profession.h:
      #ifndef PROFESSION_H
      #define PROFESSION_H
      
      #include <string>
      #include <istream>
      using namespace std;
      
      class Profession {
      public:
        Profession(string name, double salary);
        virtual ~Profession() = default;
      
        string name() const;
        int salary() const;
      
        virtual string toStirng() const;
        istream&readProfession(istream& ist);
      protected:
        string m_name;
        double m_salary;
      };
      
      #endif // PROFESSION_H
      

      worker.h:
      #ifndef WORKER_H
      #define WORKER_H
      #include <iostream>
      #include "man.h"
      #include "profession.h"
      
      class Worker : public Man, public Profession {
      public:
        Worker();
        bool operator<(const Worker& rhs) const;
        string toString() const;
        istream& readWorker(istream& ist, ostream& ost, bool silent_mode = false);
        double deal_staff() const;
      protected:
        double m_deal_staff;
      };
      
      #endif // WORKER_H
      

      man.cpp:
      #include "man.h"
      #include <sstream>
      
      Man::Man(string name, int age)
        : m_name(name), m_age(age) {
      }
      
      string Man::name() const {
        return m_name;
      }
      
      int Man::age() const {
        return m_age;
      }
      
      string Man::toStirng() const {
        string buff;
        stringstream sstr;
        sstr << m_name << " : " << m_age;
        std::getline(sstr, buff);
        return buff;
      }
      
      istream& Man::readMan(istream& ist) {
        ist >> m_name >> m_age;
        return ist;
      }
      

      profession.cpp:
      #include "profession.h"
      #include <sstream>
      
      Profession::Profession(string name, double salary)
        : m_name(name), m_salary(salary) {
      }
      
      string Profession::name() const {
        return m_name;
      }
      
      int Profession::salary() const {
        return m_salary;
      }
      
      string Profession::toStirng() const {
        string buff;
        stringstream sstr;
        sstr << m_name << " : " << m_salary;
        std::getline(sstr, buff);
        return buff;
      }
      
      istream& Profession::readProfession(istream &ist) {
        ist >> m_name >> m_salary;
        return ist;
      }
      

      worker.cpp:
      #include "worker.h"
      #include <sstream>
      
      Worker::Worker() : Man("", 0), Profession("", 0.0) {
      }
      
      double Worker::deal_staff() const {
        return m_deal_staff;
      }
      
      bool Worker::operator<(const Worker& rhs) const {
        return (salary() * deal_staff() < rhs.salary() * rhs.deal_staff());
      }
      
      string Worker::toString() const {
        string buff;
        stringstream sstr;
        //const Profession* profession = static_cast<const Profession*>(this);
        const Man* man = static_cast<const Man*>(this);
      
        sstr << Profession::name() << " - " << man->name() << ": " << salary() << "\\" << deal_staff();
        std::getline(sstr, buff);
        return buff;
      }
      
      istream& Worker::readWorker(istream& ist, ostream& ost, bool silent_mode) {
        if (false == silent_mode) {
          ost << "enter profession <name> <salarity>: ";
        }
        readProfession(ist);
        if (false == silent_mode) {
          ost << "enter man <name> <age>: ";
        }
        readMan(ist);
      
        if (false == silent_mode) {
          ost << "deal staff:";
        }
        ist >> m_deal_staff;
      
        return ist;
      }
      

      main.cpp:
      #include <iostream>
      #include <vector>
      #include <algorithm>
      #include "man.h"
      #include "profession.h"
      #include "worker.h"
      using namespace std;
      
      int main() {
        vector<Man> people;
        people.push_back(Man("Vasya", 18));
        people.push_back(Man("Lena", 22));
        people.push_back(Man("Petya", 45));
        people.push_back(Man("Kolya", 68));
      
        for (auto& man : people) {
          cout << man.toStirng() << endl;
        }
        cout << endl;
      
        vector<Profession> professions;
        professions.push_back(Profession("Pekar'", 9000));
        professions.push_back(Profession("Lekar'", 25000));
        professions.push_back(Profession("Aptekar'", 18000));
        professions.push_back(Profession("Teacher", 50000));
      
        for (auto& profession : professions) {
          cout << profession.toStirng() << endl;
        }
        cout << endl;
      
        vector<Worker> workers;
      
        while (true) {
          cout << "0 - exit\n"
                  "1 - add worker\n"
                  "2 - sort workers\n"
                  ": ";
          int menu_point;
          cin >> menu_point;
      
          if (menu_point == 0) {
            break;
          }
      
          if (menu_point < 0 || menu_point > 2) {
            cout << "wrong input, retry..." << endl;
            continue;
          }
      
          if (menu_point == 1) {
            Worker worker;
            worker.readWorker(cin, cout, false);
            workers.push_back(move(worker));
          }
      
          if (menu_point == 2) {
            std::sort(workers.begin(), workers.end());
          }
      
          for (auto& worker : workers) {
            cout << worker.toString() << endl;
          }
          cout << endl;
        }
      }
      

      StudLance.ru

Просмотр 0 веток ответов
  • Для ответа в этой теме необходимо авторизоваться.
×