Главная страница
Навигация по странице:

  • Правила доступа

  • 2.2. Открытое наследование

  • Пособие по ооп. С++ ООП УчебноеПособие. Объектноориентированное


    Скачать 1.49 Mb.
    НазваниеОбъектноориентированное
    АнкорПособие по ооп
    Дата23.11.2021
    Размер1.49 Mb.
    Формат файлаpdf
    Имя файлаС++ ООП УчебноеПособие.pdf
    ТипУчебное пособие
    #279608
    страница4 из 7
    1   2   3   4   5   6   7

    ГЛАВА 2.
    ОТНОШЕНИЯ МЕЖДУ КЛАССАМИ
    2.1.
    Наследование классов
    В языке С++ абстракция данных осуществляется с помощью классов, инкапсулирующих типы данных и операции над ними. Однако объектно- ориентированный подход выходит за рамки простой инкапсуляции. При- меняя наследование и полиморфизм, в языке С++ можно на основе сущест- вующих классов создавать новые.

    Наследование (inheritance) – это способность класса приобретать свой- ства ранее определенного класса. Один класс может наследовать структуру и поведение другого класса.
    В языке С++ производный класс наследует все члены базового клас- са, за исключением конструкторов, деструктора, перегруженной операции присваивания и определения друзей класса. Таким образом, производный класс содержит в себе все данные-члены и функции-члены базового класса, добавляя к ним новые члены, определенные в нём самом. Кроме того, про- изводный класс может изменять (переопределять) любую наследуемую функцию-член.
    При использовании механизма наследования в описании класса по- является новый раздел – защищённый (protected). Члены, помещённые в защищённый раздел, доступны из функций классов-наследников, но скрыты вне определения класса. Общие правила доступа относительно раз- делов класса в языке С++ представлены в табл. 7.
    Определение производного класса начинается с указания типа насле- дования – public, protected или private и имени базового класса: class <имя производного класса>:
    {public|protected|private} <имя базового класса>

    67
    Таблица 7
    Правила доступа
    Раздел базового класса
    Открытый
    (public)
    Защищенный
    (protected)
    Закрытый
    (private)
    Доступность из функций базового класса, функций дружественных классов и из дружественных функций
    Да
    Да
    Да
    Доступность из функций классов- наследников базового
    Да
    Да
    Нет
    Доступность из других классов и функций
    Да
    Нет
    Нет
    Открытое наследование. Открытые и защищенные члены базового класса остаются, соответственно, открытыми и защищенными членами производного класса.
    Защищенное наследование. Открытые и защищенные члены базового класса становятся защищенными членами производного класса.
    Закрытое наследование. Открытые и защищенные члены базового класса становятся закрытыми членами производного класса.
    Среди указанных видов наследования открытое наследование являет- ся наиболее важным и часто используемым.
    2.2.
    Открытое наследование
    Открытое наследование устанавливает между классами отношение
    «является», или в английской нотации «is-a».
    При открытом наследовании все, что характеризует объекты класса- предка, является справедливым и для объектов класса-наследника. Это свойство называется совместимостью типов объектов. Благодаря этому объект производного класса можно применять вместо объекта базового класса, но не наоборот.
    Например, объекту базового типа можно присвоить объект производ- ного типа, указателю на объект базового типа – указатель на объект произ-

    68 водного. Объект производного типа также может быть передан в качестве параметра в функцию, вместо объекта базового типа.
    Пример 9. Реализовать иерархию классов Sphere («сфера») – Ball
    («мяч»), которая представлена на UML диаграмме (рис. 2.1).
    Рис. 2.1. UML диаграмма иерархии классов Sphere – Ball
    Определение класса «сфера» – файл sphere.h.
    #ifndef SPHERE_H
    #define SPHERE_H class Sphere { public:
    //Конструкторы
    Sphere();

    69
    Sphere(double iniRadius);
    // Операции void resetRadius(double newRadius); double getRadius(); double getVolume(); void showInform(); protected: double radius; //радиус сферы
    };
    #endif
    Реализация класса «сфера» – файл sphere.cpp
    #include "sphere.h"
    #include
    #include using namespace std;
    Sphere::Sphere(): radius(1.0){ }
    Sphere::Sphere(double iniRadius){ if (iniRadius>0) radius=iniRadius; else radius=1.0;
    } void Sphere::resetRadius(double newRadius){ if (newRadius>0) radius=newRadius; else radius=1.0;
    } double Sphere::getRadius(){ return radius;
    } double Sphere::getVolume(){ double rad3=radius*radius*radius; return (4.0*3.14*rad3)/3.0;
    } void Sphere::showInform(){ cout<<"\n Radius = "< <<"\n Volume = "<}
    Определение класса «мяч» – файл ball.h
    #ifndef BALL_H
    #define BALL_H
    #include "sphere.h"

    70
    #include using namespace std; class Ball: public Sphere{ public:
    //конструкторы
    Ball();
    Ball(double iniRadius, const string iniName);
    //дополнительные или измененные операции string getName(); void reName (const string newName); void resetBall(double newRadius, const string newName); void showInform(); private: string name;
    };
    #endif
    Реализация класса «мяч» – файл ball.cpp
    #include "ball.h"
    #include using namespace std;
    Ball::Ball(): Sphere(){ setName("NoName");
    }
    Ball::Ball(double iniRadius, const string iniName):
    Sphere(iniRadius), name(iniName){} void Ball:: reName(const string newName){ name = newName;
    } string Ball::getName(){ return name;
    } void Ball::resetBall(double newRadius, const string newName) { radius = newRadius; name = newName;
    } void Ball::showInform(){ cout<<" Ball type - "< Sphere::showInform();
    }

    71

    Конструктор производного класса выполняется после конструктора ба- зового класса. Это правило действует и в цепочках наследования любой длины.
    Например, конструктор класса Ball выполняется после конструкто- ра класса Sphere.
    Ball::Ball(): Sphere(){setName("NoName");}
    Ball::Ball(double iniRadius, const string iniName):
    Sphere(iniRadius), name(iniName){}
    Конструкторы класса Ball вызывают соответствующие конструкто- ры класса Sphere. Для указания, какой именно из конструкторов базового типа следует вызвать в каждом конкретном конструкторе класса-потомка, используется синтаксис списка инициализаторов. Если конструктор базо- вого класса отсутствует в списке инициализации, используется конструк- тор базового класса по умолчанию. В этом случае конструктор без пара- метров для класса Ball может выглядеть следующим образом:
    Ball::Ball(){ setName("NoName");
    }
    Если в классе-потомке не определен ни один конструктор, то будет создан конструктор по умолчанию, который вызовет конструкторы по умолчанию всех предков.

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

    72

    Деструктор производного класса выполняется перед деструктором ба- зового класса. В цепочках наследования произвольной длины деструкторы вызываются в порядке обратном порядку вызова конструкторов.
    Соответственно, в нашем примере вначале выполнится деструктор класса Ball, затем — деструктор класса Sphere. Поскольку они явно не определены, то используются автоматические деструкторы.
    Наследование не открывает доступ к закрытым членам. Если бы пе- ременная radius была объявлена как private, она была бы недоступна в классе Ball. Тогда внутри класса Ball для доступа к ней пришлось бы использовать открытые функции класса Sphere — resetRadius и getRadius
    . Но всегда следует помнить, что каждый вызов функции при- водит к увеличению накладных расходов. Поэтому если наследникам ну- жен прямой доступ к полям предка, рекомендуют использовать специфика- тор доступа protected. class Ball{ protected: double radius; //радиус сферы
    }; void Ball::resetBall(double newRadius, const string newName) { radius = newRadius; name = newName;
    }
    В реализации класса Ball можно вызывать функции, наследуемые от класса Sphere. Если бы поле radius было объявлено как private, функция resetBall должна была бы вызывать наследуемую функцию resetRadius. void Ball::resetBall(double newRadius, const string newName) { resetRadius(newRadius); name = newName;
    }

    73
    Функция showInform переопределяется в классе Ball. При этом она вызывает унаследованную версию функции класса предка
    Sphere::showInform. Для разрешения коллизии имён используется операция разрешения области видимости ::. void Ball::showInform(){ cout<<" Ball type - "< Sphere::showInform();
    }
    При переопределении функции базового класса в производном классе списки параметров могут не совпадать. При этом замещающая функция пе- реопределяет исходную функцию, но с другим списком параметров.

    Перегрузка при этом не происходит, так как она возможна только в од- ном пространстве имен. Каждый класс имеет свое пространство имен. Сле- довательно, производный класс вводит новое пространство имен.
    Объекты производного класса могут вызывать открытые функции- члены базового класса. В этом выражается связь между производным и ба- зовым классом, например:
    Ball myBall(5.0, "Volleyball"); cout<Две другие важные связи заключаются в том, что указатель базового класса может указывать на производный объект без явного преобразования типов. Ссылка на базовый класс тоже может иметь значением адрес объек- та производного класса.
    Ball myBall(5.0, "Volleyball");
    Sphere &pb = myBall; Sphere *pt = &myBall;
    Sphere *p = new Ball(9.0, "basketball"); pb.showInform(); cout << pb.getRadius() << endl; pt->showInform(); cout << pt->getVolume() << endl; p->showInform(); cout << p->getVolume() << endl;

    74
    Однако указатель или ссылка базового типа позволяет вызывать только функции базового класса, поэтому воспользоваться pb, pt или p для вызова функции getName() производного класса нельзя.
    Следует обратить внимание и на то, что вызов функции showInform() через указатели p, pt или ссылку pb на базовый класс обратится к реализации этой функции в классе Sphere, игнорируя ее пе- реопределение в классе Ball.
    Пример 10. Описать иерархию классов Person – Student. Класс
    Student не должен иметь доступ к личным данным, содержащимся в классе Person. Методы класса заданы на UML-диаграмме (рис. 2.2).
    Рис. 2.2. UML диаграмма иерархии классов Person – Student

    75
    #pragma once
    #include using namespace std; class Person { private: string name; int age; public:
    Person(const string& name = "noname", int age = 18) : name(name), age(age) {} friend ostream & operator<< (ostream & os, const Person & p)
    { os << p.name << " " << p.age << endl; return os;
    }
    }; class Student: public Person
    { private: string univ; // Университет int m; int* marks; // Оценки public:
    Student() : univ("МГУ"), m(3)
    { marks = new int[m]; for (int i = 0; i }
    Student(const string& name, int age, const string& u, int m):
    Person(name, age), univ(u), m(m)
    { marks = new int[m]; for (int i = 0; i }
    Student(const Student& s): Person(s), univ(s.univ), m(s.m)
    { marks = new int[s.m]; for (int i =0; i }

    Student() { delete[] marks; }

    76
    Student& operator=(const Student& s)
    { if (&s != this)
    { delete[] marks;
    Person::operator=(s); univ = s.univ; m = s.m; marks = new int[m]; for (int i = 0; i } return *this;
    }; void setMark(int i, int mark)
    { if (i < 0 || i >= m) throw - 1; marks[i] = mark;
    } friend ostream & operator<< (ostream & os, const Student & s)
    { os<<(Person)s; os << s.univ << endl; for (int i = 0; i < s.m; ++i) os << s.marks[i] << " "; os << endl; return os;
    }
    };
    #include
    #include
    #include "PS.h" using namespace std; int main() {
    Student s("Pinokkio", 18, "ЮФУ", 3); s.setMark(0, 5); s.setMark(1, 4); cout << s;
    Student s1(s); cout << s1;
    Student s2; cout << s2; s2 = s1;

    77 cout << s2; system("PAUSE"); return 0;
    }
    Наличие динамически выделяемой памяти int* marks в классе
    Student создаёт дополнительные проблемы. В этом случае нельзя ис- пользовать создаваемые по умолчанию функции: конструктор без парамет- ров, конструктор копии, деструктор и операцию присваивания. Необходи- мо предусмотреть их реализацию в классе.
    Конструктор без параметров использует список инициализаторов для членов-данных univ, m. Выделение памяти для массива оценок marks и его инициализация происходит в теле конструктора.
    Student(): univ("МГУ"), m(3){ marks = new int[m]; for (int i = 0; i}
    Инициализация членов-данных, унаследованных от класса Person, может выполниться только внутри конструктора класса Person. Вызов конструктора предка возможен только в списке инициализаторов конструк- тора наследника. В данном примере его нет, поэтому будет вызван конст- руктор по умолчанию класса предка. Если у класса предка нет конструкто- ра по умолчанию, то произойдёт ошибка.

    Если предполагается иерархия наследования, рекомендуется для каждо- го класса в иерархии определять конструктор по умолчанию.
    Конструктор с параметрами требует явного указания конструктора предка в списке инициализаторов.
    Student(const string& name, int age, const string& u, int m):
    Person(name, age), univ(u), m(m){ marks = new int[m]; for (int i = 0; i}

    78
    Здесь Person(name, age) — это вызов конструктора предка. Ес- ли не написать вызов Person(name, age), то произойдет вызов конст- руктора без параметров, который проинициализирует поля name и age значениями по умолчанию.
    В деструкторе необходимо только освободить память, занимаемую массивом marks , а память, выделенная для univ и Person, будет осво- бождена автоматически при вызове соответствующих деструкторов в эпи- логе деструктора Student().
    Student() { delete[] marks;
    // эпилог
    }
    Рассмотрим конструктор копии класса Student.
    Student(const Student& s): Person(s), univ(s.univ), m(s.m) { marks = new int[s.m]; for (int i =0; i}
    Заметим, что Person(s) будет работать корректно благодаря тому, что ссылке на объект класса предка можно присвоить ссылку на объект класса потомка. При этом происходит приведение типа-наследника к базо- вому типу. Такое приведение называется upcast.
    Рассмотрим преобразование типов в конструкторе копии класса
    Student
    , которое возникает при вызове конструктора копии Person(s) в списке инициализации.
    Мы фактически параметру типа const Person & присваиваем переменную типа const Student &.
    Аналогичные преобразования также могут выполняться как для указателей, так и для самих объектов. Преобразования типов для параметров при вызо- ве функций встречаются достаточно часто, в том числе и для параметра this
    . Например, если вызывается для потомка унаследованная функция предка.

    79
    Поскольку передача параметров сводится к выполнению операции присваивания, то рассмотрим преобразование в иерархии «предок- потомок» на примере операции присваивания.
    Для преобразования типов в иерархии «предок-потомок» работает правило: переменной типа предок можно присвоить переменную типа по- томок, но не наоборот.
    Person p("Иванов", 20);
    Student s("Петров", 19, "ЮФУ", 3); p = s;
    При присваивании объекта производного класса переменной базово- го класса происходит копирование только полей базового класса, остальная часть информации объекта производного класса будет утеряна.
    Попытка присвоить объекту класса наследника объект класса предка приведёт к ошибке компиляции. s = p; // ошибка компиляции
    При работе с указателями и ссылками на объекты предка и наследни- ка действует аналогичное правило преобразования типов. Указателю или ссылке на объект базового класс можно присвоить адрес объекта или ссыл- ку на объект, соответственно, производного класса, но не наоборот.
    Person* pp = &p;
    Student* ss = &s; pp = ss; ss = pp; // ошибка компиляции
    Person& rp = s;
    Student& rs = p; // ошибка компиляции
    Таким образом, если мы напишем Person& rp = s;, тогда rp бу- дет давать доступ только к двум полям объекта s, унаследованным от
    Person
    . Именно поэтому в конструкторе копии класса Student не воз- никало проблем с вызовом конструктора копии Person(s) в списке ини- циализации.

    80
    Операция присваивания будет реализована несколько сложнее:
    Student& operator=(const Student& s){ if (&s != this){ delete[] marks;
    Person::operator=(s); univ = s.univ; m = s.m; marks = new int[m]; for (int i = 0; i } return *this;
    };
    Операция присваивания не наследуется. Поэтому, чтобы выполнить присваивание для private полей, унаследованных от базового класса
    Person
    , необходимо выполнить операцию присваивания, определённую в классе Person.
    Person::operator=(s);
    Операция вывода в поток демонстрирует ещё одну особенность об- ращения к функции, определённой для предка. friend ostream & operator<< (ostream & os, const Student & s) { os<<(Person)s; os << s.univ << endl; for (int i = 0; i < s.m; ++i) os << s.marks[i] << " "; os << endl; return os;
    }
    Поскольку операция вывода в поток является внешней, то к ней не- возможно применить разрешение области видимости. Поэтому использует- ся явное приведение типа. os<<(Person)s;
    Обратное действие, когда происходит приведение базового типа к типу-наследнику, называется downcast. В этом случае необходимо исполь- зовать явное приведение типов с помощью шаблонной функции static_cast<тип_наследника>.

    81
    Добавим в класс Student функцию get_univ(), которая возвра- щает название учебного заведения, где учится студент. Такой функции нет, и не может быть в Person. class Student : public Person{ public: string get_univ() const{ return univ;
    }
    };
    Теперь рассмотрим ситуацию, когда нам может понадобиться приве- дение базового типа к типу наследника:
    Person *p = new Student("Петров", 19, "ЮФУ", 3); p->get_univ(); // ошибка компиляции
    При вызове p->get_univ()произойдёт ошибка компиляции, так как в Person не определена функция get_univ(). Для корректного вы- зова указатель p нужно привести к типу *Student.
    // Совеременный стиль: static_cast(pp)->get_univ();
    // Старый стиль:
    ((Student*)pp)->get
    _
    univ();
    Аналогичная ситуация возникает и при использовании ссылок:
    Person & rp = *new Student("Петров", 20, "ЮФУ", 3); static_cast(rp).get_univ(); delete &rp;

    Преобразование downcast в C++ возможно только для указателей или ссылок на объекты.
    Корректное преобразование downcast возможно только как обратное преобразование к upcast.

    82
    1   2   3   4   5   6   7


    написать администратору сайта