Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

OOP_C++ / 11

.htm
Скачиваний:
21
Добавлен:
02.02.2015
Размер:
37.23 Кб
Скачать

11 - Композиция классов. Наследование и полиморфизм Содержание     Предыдущее занятие     Следующее занятие

Занятие 11 Композиция классов. Наследование и полиморфизм 1 Композиция классов Композицией классов называется создание новых классов с использованием объектов других классов в качестве элементов данных. При создании объекта объемлющего класса вначале вызываются конструкторы элементов в порядке их объявления. Деструкторы вызываются в обратном порядке.

Если для конструкторов элементов требуются параметры, то они передаются с помощью списка инициализации, который размещается в определении конструктора после списка формальных параметров и отделяется двоеточием:

class Y { . . . public: Y(int); . . . }; class X { Y y; double d; public: X(int forY, double forX) : y(forY) { d = forX; } . . . }; Инициализация элементов данных базовых типов также может выполняться через список инициализации:

class X { Y y; double d; public: X(int forY, double forX) : y(forY), d(forX) { } . . . }; Когда элемент, являющийся объектом класса, не указан в списке инициализации, то, если он имеет конструктор по умолчанию, он будет проинициализирован неявно с помощью этого конструктора. Если такой конструктор в классе не определен, то будет выдана ошибка.

Элементами данных класса могут быть ссылки. Такие элементы могут быть проинициализированы только через список инициализации.

Класс может содержать константные элементы данных. Такие элементы также должны быть проинициализированы через список инициализации:

class X { const int ci; public: X(int i) : ci(i) { } . . . }; 2 Базовые и производные классы Механизм наследования заключается в порождении производных классов от базовых. Если один класс (производный) является потомком другого (базового), то наследник имеет возможность пользоваться данными и функциями-элементами, определенными в классе-предке. В С++ класс-наследник также называется подклассом базового. Класс и подкласс имеют общий интерфейс, предоставляемый базовым классом (т.к. подкласс имеет все открытые элементы базового класса. Отношения между классами и подклассами называются иерархией наследования классов.

При определении класса список базовых классов указывается после имени через двоеточие:

class X // Базовый класс { . . . }; class Y : public X // Производный класс { . . . }; Функции производного класса имеют доступ только к элементам, описанным в разделах public и protected ( защищенные). Члены класса, объявленные как защищенные, могут использоваться классами-потомками, но никем больше. Закрытые члены класса недоступны даже для его потомков.

Базовый класс может наследоваться как открытый (public), закрытый (private) или защищенный (protected). По умолчанию базовый класс считается закрытым. Например:

class X // Базовый класс { . . . }; // Класс Х наследуется как открытый: class Y : public X { . . . }; // Класс Х наследуется как защищенный: class Z : protected X { . . . }; // Класс Х наследуется как закрытый: class P : X { . . . }; Если базовый класс является открытым, то уровень доступа к наследуемым элементам этого класса сохраняется и в производном классе, то есть защищенные элементы базового класс остаются защищенными и в производном классе, а открытые - остаются открытыми. Наследуемые общие и защищенные элементы закрытого базового класса становятся в производном классе закрытыми. Наследуемые общие и защищенные элементы защищенного базового класса становятся в производном классе защищенными.

Если явно нужно обратиться к элементу базового класса из производного, необходимо использовать операцию разрешения области видимости.

В случае сочетания композиции и наследования конструкторы вызываются в следующем порядке:

конструктор базового класса

конструкторы элементов производного класса

конструктор самого производного класса

Деструкторы вызываются в обратном порядке.

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

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

Объект производного класса может неявно преобразовываться в объект его открытого базового класса. Ссылка на объект производного класса может неявно преобразовываться в ссылку на его открытый базовый класс. Указатель на производный класс может неявно преобразовываться в указатель на его открытый базовый класс.

3 Полиморфизм Полиморфизм - это свойство, при котором поведение объектов класса может определяться не на этапе компиляции, а на этапе выполнения. Классы, предоставляющие идентичный интерфейс, но реализованные под конкретные специфические требования, называются полиморфными классами.

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

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

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

Для задания виртуальной функции нужно начать ее объявление в теле класса ключевым словом virtual. Как виртуальные можно объявлять только функции-элементы класса. Служебное слово virtual можно использовать только в теле класса.

class X { protected: virtual void f(); }; Чисто виртуальная функция - это функция, чье объявление в теле класса инициализируется нулем. У чисто виртуальной функции нет определения.

class X { protected: virtual void f() = 0; }; Класс, в котором есть хотя бы одна чисто виртуальная функция, может использоваться только как базовый для построения производных классов и называется абстрактным базовым классом. Создание объекта такого класса приведет к ошибке. Можно описывать указатели и ссылки на такой класс.

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

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

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

class X { public: virtual void f(int k); }; class Y : public X { public: void f(int k); }; При переопределении виртуальных функций для них можно менять уровень доступа, но при этом, когда такая функция будет вызываться через общий базовый класс, уровень доступа к ней будет определяться ее уровнем доступа в этом базовом классе.

Конструкторы классов не могут быть виртуальными.

Деструктор можно объявлять виртуальным. Если деструктор базового класса виртуальный, то деструктор производного класса тоже будет виртуальным. Описание деструкторов как виртуальных гарантирует, что когда операция delete применяется к указателю на базовый класс, будет вызван деструктор соответствующего класса.

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

Статические функции не могут быть виртуальными. Виртуальная функция может быть inline-функцией:

inline virtual void g(); При этом директива inline выполняется только при вызове виртуальной функции непосредственно для объекта.

5 Множественное наследование У класса может быть несколько прямых базовых классов. В списке базовых классов имена прямых базовых классов разделяются запятой. Спецификацию доступа нужно указывать перед каждым базовым классом:

class X { . . . }; class Y { . . . }; class Z : public X, public Y { . . . }; Множественное наследование применяется в том случае, когда объект представляет собой понятие, сочетающее в себе несколько общих понятий, каждое из которых может быть представлено базовым классом.

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

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

К спецификации базового класса можно добавить служебное слово virtual. Отдельный объект виртуального базового класса V разделяется между всеми базовыми классами, которые указали V при задании своих базовых классов, например:

class A : virtual public V { . . . }; class B : virtual public V { . . . };

class C : public A, public B { . . . }; Здесь объект класса C будет иметь только один вложенный объект класса V (ромбовидное наследование). Конструкторы виртуальных базовых классов вызываются до каких бы то ни было конструкторов невиртуальных базовых классов.

В определении классов можно использовать using-объявления. Объявления using позволяют создавать набор перегруженных функций из базовых и производных классов; using-объявление в определении класса должно относиться к членам базового класса.

Комбинации using-объявления с закрытым или защищенным наследованием можно использовать для предоставления только части интерфейса, предлагаемого классом. Кроме того, в производном классе можно сделать открытыми элементы базового класса, унаследованные как protected:

class Base { private: int i; int j; protected: void setI(int value) { i = value; } public: int getI() const { return i; } int getJ() const { return j; } }; class Descendant : private Base { public: using Base::getI; using Base::setI; }; int main(int argc, char* argv[]) { Descendant d; d.setI(10); cout << d.getI(); cout << d.getJ(); // Ошибка! return 0; } using-объявление не может быть использовано для получения доступа к закрытой информации базового класса.

Объявление функции-элемента в производном классе не перегружает, а скрывает одноименные функции-элементы в базовом, даже если их списки параметров различны. Для того, чтобы сделать одноименные функции перегружаемыми, также может использоваться using-объявление:

class Base { public: void setValue(int); }; class Descendant : private Base { public: using Base::setValue; void setValue(string); }; int main() { Descendant d; d.setValue("a"); d.setValue(2); // Без using-объявления была бы ошибка return 0; } 5 Примеры программ 5.1 Иерархия объектов реального мира Допустим, необходимо разработать иерархию классов "Регион" - "Населенный район" - "Страна". Отдельные классы этой иерархии могут стать базовыми для других классов (например "Необитаемый остров", "Национальный парк", "Административный район", "Автономная республика" и т.д.). Можно предложить следующую иерархию классов:

// Иерархия классов #include <string> using std::string; class Region { double area; public: Region(double initArea) : area(initArea) { } double getArea() const { return area; } void setArea(double value) { area = value; } }; class PopulatedRegion : public Region { int population; public: PopulatedRegion(double initArea) : Region(initArea), population(0) { } int getPopulation() const { return population; } void setPopulation(int value) { population = value;} double density() const { return population / getArea(); } }; class Country : public PopulatedRegion { string capital; public: Country(double initArea) : PopulatedRegion(initArea) { } string getCapital() const { return capital; } void setCapital(string value) { capital = value; } };

У базового класса Region нет конструктора по умолчанию. Это сделано для того, чтобы обязательно задавать площадь при инициализации. Желательно, чтобы она не равнялась нулю.

Конструкторы всех производных классов содержат один параметр - площадь. В менее упрощенном примере целесообразно иметь большее количество конструкторов.

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

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

5.2 Иерархия массивов Ранее был рассмотрен класс, представляющий массив вещественных чисел:

class Array { double *pa; int size; public: Array(int n); ~Array(); void setElem(int index, double elem); double getElem(int index) const; int getSize() const; }; Допустим, необходимо создать набор классов, которые инициализируют элементы массива различными способами. Например, массив может быть проинициализирован единицами, случайными значениями и т.д. Можно создать набор производных классов, в конструкторах которых отразится специфика инициализации:

class ArrayWithOnes : public Array { public: ArrayWithOnes(int n); }; ArrayWithOnes::ArrayWithOnes(int n) : Array(n) { // Memory soon allocated for (int i = 0; i < getSize(); i++) setElem(i, 1); } class ArrayOfRandomValues : public Array { public: ArrayOfRandomValues(int n); }; ArrayOfRandomValues::ArrayOfRandomValues(int n) : Array(n) { for (int i = 0; i < getSize(); i++) setElem(i, rand() * 1.0 / RAND_MAX); } RAND_MAX - максимальное значение, которое возвращает функция rand(). Данная функция генерирует случайные числа в диапазоне от 0 до RAND_MAX. Функция rand() и константа RAND_MAX описаны в заголовочном файле stdlib.h.

Работу производных классов можно протестировать в функции main():

#include <iostream> #include <limits> #include <stdlib.h> ... // Определение классов и их функций-элементов int main() { ArrayWithOnes a1(5); ArrayOfRandomValues a2(6); for (int i = 0; i < a1.getSize(); i++) std::cout << a1.getElem(i) << ' '; std::cout << '\n'; for (int i = 0; i < a2.getSize(); i++) std::cout << a2.getElem(i) << ' '; std::cout << '\n'; a2.setElem(6, 0.5); // Ошибка! } 5.3 Использование полиморфизма для создания класса "Очередь" Допустим, необходимо двумя вариантами реализовать класс для представления очереди целых чисел - через массив переменной длины и через цепочку элементов. Можно выделить базовый абстрактный класс Queue:

class Queue { public: virtual void add(int value) = 0; virtual int get() = 0; }; Реализуем класс через массив переменной длины:

class ArrayQueue : public Queue { int* pa; int size; public: ArrayQueue() : pa(0), size(0)

{ cout << "Array queue:" << endl; } ~ArrayQueue() { if (size) delete [] pa; } virtual void add(int value); virtual int get(); }; void ArrayQueue::add(int value) { int *temp = new int [size + 1]; if (pa) { for (int i = 0; i < size; i++) temp[i] = pa[i]; delete [] pa; } pa = temp; pa[size] = value; size++; } int ArrayQueue::get() { if (size) { int value = pa[0]; int *temp = new int [size - 1]; for (int i = 1; i < size; i++) temp[i - 1] = pa[i]; delete [] pa; pa = temp; size--; return value; } else throw "Queue is empty"; } Реализуем класс через динамическую цепочку:

struct Element // Элемент цепочки { int value; Element* next; }; class ChainQueue : public Queue { Element* first; Element* last; public: ChainQueue() : first(0), last(0)

{ cout << "Chain queue:" << endl; } ~ChainQueue(); virtual void add(int value); virtual int get(); }; void ChainQueue::add(int value) { Element* e = new Element; if (!first) first = e; if (last) last->next = e; last = e; e->value = value; e->next = 0; } int ChainQueue::get() { if (first) { Element* e = first; int value = e->value; first = e->next; delete e; return value; } else throw "Queue is empty"; } ChainQueue::~ChainQueue() { while (first) get(); } Тестируем оба класса:

int main() {

Queue* q[2]; // Массив указателей на

// абстрактный класс

q[0] = new ArrayQueue;

q[0]->add(1);

q[0]->add(2);

try

{

cout << q[0]->get() << ' ';

cout << q[0]->get() << ' ';

cout << q[0]->get() << ' ';

}

catch (char* c)

{

cout << c << endl;

}

q[1] = new ChainQueue;

q[1]->add(2);

q[1]->add(4);

try

{

cout << q[1]->get() << ' ';

cout << q[1]->get() << ' ';

cout << q[1]->get() << ' ';

}

catch (char* c)

{

cout << c << endl;

} return 0; } 5.4 Массив функций Допустим, необходимо создать класс для представления функции с именем. Функция должна быть вычислена и выведена на экран вместе с ее именем. Такой класс должен быть абстрактным, так как он не должен определять ни какой конкретной функции:

#include <iostream> #include <string> #include <cmath> class AFunc { std::string funcName; public: AFunc(char* funcName) { this->funcName = funcName; } virtual ~AFunc() { } virtual double f(double x) = 0; void printFunc(double x) { std::cout << funcName << ": \t" << f(x) << "\n"; } }; Теперь можно создать набор производных классов, представляющих конкретные функции:

class SineFunc : public AFunc { public: SineFunc() : AFunc("Sine") { } virtual double f(double x) { return std::sin(x); } }; class CosineFunc : public AFunc { public: CosineFunc() : AFunc("Cosine") { } virtual double f(double x) { return std::cos(x); } }; class QuadraticFunc : public AFunc { public: QuadraticFunc() : AFunc("Square") { } virtual double f(double x) { return x * x; } }; Указатели на объекты производных классов могут быть помещены в массив указателей на базовый класс:

int main(int argc, char* argv[]) { AFunc *a[3]; a[0] = new SineFunc(); a[1] = new CosineFunc(); a[2] = new QuadraticFunc(); for (int i = 0; i < 3; i++) a[i]->printFunc(3.14159265); for (int i = 0; i < 3; i++) delete a[i]; // Вызов виртуальных деструкторов std::cin.get(); return 0; } 5.5 Уравнение Используя полиморфизм, мы можем создать универсальные классы для реализации полезных математических методов. В следующем примере создается модуль, содержащий класс для решения уравнения методом дихотомии на заданном интервале.

Создаем новый модуль. В заголовочном файле помещаем определение абстрактного класса:

//UnitEquation.h #ifndef UnitEquation_h #define UnitEquation_h class Equation { public: virtual ~Equation() { } virtual double f(double x) = 0; double root(double a, double b, double eps); }; #endif Чисто виртуальная функция f() представляет левую часть уравнения:

f(x) = 0

Функция root() определяется в файле реализации:

//UnitEquation.cpp #include "UnitEquation.h" double Equation::root(double a, double b, double eps) { double x; do { x = (a + b) / 2; if (f(a) * f(x) > 0) a = x; else b = x; } while (b - a > eps); return x; } В любой программе, в которой необходимо решить уравнение, создается производный класс для представления левой части этого уравнения:

#include <iostream> #include <cmath> #include "UnitEquation.h" class MyEquation : public Equation { public: virtual double f(double x) { return std::cos(x / 2); } }; int main() { MyEquation e; std::cout << e.root(0, 6, 0.00001); return 0; } Альтернативное решение заключается в создании абстрактного класса для представления функции - левой части уравнения. В классе Equation в этом случае хранится указатель на объект такого класса. В программе, решающей конкретное уравнение, создается класс, произвозный от абстрактного. В нем реализуется конкретная функция и решается уравнение.

Добавляем новый заголовочный файл:

#ifndef UnitAFunction_h #define UnitAFunction_h class AFunction { public: virtual ~AFunction() { } virtual double f(double x) = 0; }; #endif Заголовочный файл модуля UnitEquation:

//UnitEquation.h #ifndef UnitEquation_h #define UnitEquation_h #include "UnitAFunction.h" class Equation { AFunction *pf; public: Equation(AFunction* pf) { this->pf = pf; } double root(double a, double b, double eps); }; #endif Файл реализации:

//UnitEquation.cpp #include "UnitEquation.h" double Equation::root(double a, double b, double eps) { double x; do { x = (a + b) / 2; if (pf->f(a) * pf->f(x) > 0) a = x; else b = x; } while (b - a > eps); return x; } Тестирующая программа:

#include <iostream> #include <cmath> #include "UnitEquation.h" class MyFunction : public AFunction { public: virtual double f(double x) { return std::cos(x / 2); } }; int main() { MyFunction mf; Equation e(&mf); std::cout << e.root(0, 6, 0.00001); return 0; } Такой подход позволяет организовать многоцелевое использование функций, например, для решения уравнения разными методами.

5.6 Использование множественного наследования При проектировании класса, представляющего очередь целых значений, можно воспользоваться множественным наследованием, определив для такого класса в качестве базовых абстрактный класс Queue и класс IntArray. Реализация класса IntArray аналогична приведенной ранее реализации класса Array для хранения элементов типа double. Добавлена функция deleteElem, удаляющая элемент с указанным индексом. Функция at() используется для доступа к элементам массива.

#include <iostream> #include <limits> using namespace std; class Queue // Определение интерфейса { public: virtual void add(int value) = 0; virtual int get() = 0; }; class IntArray { private: int *pa; int size; public: IntArray() { pa = 0; size = 0; } ~IntArray() { if (pa) delete [] pa; } void addElem(int elem); void deleteElem(int index); int& at(int index); int getSize() const { return size; } }; void IntArray::addElem(int elem) { int *temp = new int [size + 1]; if (pa) { for (int i = 0; i < size; i++) temp[i] = pa[i]; delete [] pa; } pa = temp; pa[size] = elem; size++; } int& IntArray::at(int index) { return pa[index]; } void IntArray::deleteElem(int index) { int *temp = new int [size - 1]; for (int i = 0; i < index; i++) temp[i] = pa[i]; for (int i = index + 1; i < size; i++) temp[i - 1] = pa[i]; delete [] pa; pa = temp; size--; } Класс ArrayQueue наследует интерфейс класса Queue и реализацию класса IntArray:

class ArrayQueue : public Queue, private IntArray { public: ArrayQueue() { } virtual void add(int value); virtual int get(); }; void ArrayQueue::add(int value) { addElem(value); } int ArrayQueue::get() { if (getSize()) { int value = at(0); deleteElem(0); return value; } else return numeric_limits<int>().max(); } int main(int argc, char* argv[]) { ArrayQueue q; q.add(1); q.add(2); q.add(4); cout << q.get() << ' '; cout << q.get() << ' '; cout << q.get() << ' '; cout << q.get() << ' '; cin.get(); return 0; } 6 Задания на самостоятельную работу Задание 1 Описав класс для представления матрицы, создать и протестировать производные классы, которые реализуют различные механизмы инициализации.

Задание 2 Создать класс для представления функции:

y = f(x) + g(x)

где f(x) и g(x) объявлены как чисто виртуальные функции. Написать программу, которая тестирует массив указателей на функции.

Задание 3 Создать универсальный модуль, содержащий класс, реализующий нахождение всех корней уравнения на заданном интервале методом полного перебора. В функции, ищущей корни весь интервал делится на небольшие промежутки; осуществляется поиск промежутков, на которых функция меняет знак.

Реализовать программу, которая ищет все корни конкретного уравнения на заданном интервале.

Задание 4* Спроектировать абстрактный класс, предназначенный для реализации стека для хранения целых чисел.

Стек - это динамическая структура данных, состоящая из переменного числа элементов. Добавление новых и удаление элементов осуществляется с одного конца стека по принципу LIFO (Last In - First Out - последним вошел - первым вышел). Доступ может быть осуществлен с одного конца стека. Со стеком допустимы следующие операции:

добавить элемент в стек - операция push;

удалить элемент из стека - операция pop; получение значения элемента предполагает его удаление из стека.

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

Задание 5* Создать класс для представления стека с использованием множественного наследования. В качестве базовых классов использовать абстрактный класс Stack (интерфейс) и класс IntArray (реализация).

 

Содержание     Предыдущее занятие     Следующее занятие

 

© 2001 - 2006 Иванов Л.В.

Соседние файлы в папке OOP_C++