Абстрактные базовые классы и полиморфизм



Наследование классов объединяется с абстрактными базовыми классами (abstract base classes) для создания важного инструмента разработки структуры данных. Эти абстрактные базовые классы определяют открытый интерфейс класса, независимый от внутренней реализации данных класса и операций. Открытый интерфейс класса определяет методы доступа к данным. Клиент хочет, чтобы открытый интерфейс оставался постоянным, несмотря на изменения во внутренней реализации. Объектно-ориентированные языки подходят к этой проблеме, используя абстрактный базовый класс, который объявляет имена и параметры для каждого из public-методов. Абстрактный базовый класс предоставляет ограниченные детали реализации и сосредотачивает внимание на объявлении public-методов. Это объявление форсирует реализацию в производном классе. Абстрактный базовый класс C++ объявляет некоторые методы как чистые виртуальные функции (pure virtual functions). Следующее объявление определяет абстрактный базовый класс List, который задает операции списка. Слово "virtual" и присвоение нуля операции определяют чистую виртуальную функцию.

template <class T>

class List

{

protected:

// число элементов в списке.

// обновляется производным классом

int size size;

public:

// конструктор

List(void);

// методы доступа

virtual int ListSize(void) const;

virtual int ListEmpty(void) const;

virtual int Find(T& item) = 0;

// методы модификации списка

virtual void Insert(const T& item) = 0;

virtual void Delete(const T& item) = 0;

virtual void ClearList(void) = 0;

};

Этот абстрактный базовый класс предназначен для описания очень общих списков. Он используется как база для серии классов наборов (структур списков) в последующих главах. Использование абстрактного класса в качестве базы требует, чтобы наборы реализовывали общие методы класса List. Для иллюстрации этого процесса класс SeqList снова рассматривается в главе 12, где он порождается от List.

1.9.1. Полиморфизм и динамическое связывание

Концепция наследования поддерживается в языке C++ рядом мощных конструкций. Мы уже рассмотрели чистые виртуальные функции в абстрактном базовом классе. Общая концепция виртуальных функций поддерживает наследование в том, что позволяет двум или более объектам в иерархии наследования иметь операции с одним и тем же объявлением, выполняющие различные задачи. Это объектно-ориентированное свойство, называемое полиморфизм (polymorhism), позволяет объектам из множества классов отвечать на одно и то же сообщение. Получатель этого сообщения определяется динамически во время выполнения. Например, системный администратор может использовать полиморфизм для обработки резервных файлов в многосистемной среде. Предположим, что администратор имеет подсистему с магнитной лентой 1/2 дюйма и компактный магнитофон с лентой 1/4 дюйма. Классы HalfTape и Quarter-Tape являются производными от общего класса Таре и управляют соответствующими лентопротяжными устройствами. Класс Таре имеет виртуальную операцию Backup, содержащую действия, общие для всех лентопротяжных устройств. Производные классы имеют (виртуальную) операцию Backup, использующую специфическую внутреннюю информацию о лентопротяжных механизмах. Когда администратор дает указание выполнить системное резервное копирование, каждый лентопротяжный механизм принимает сообщение Backup и выполняет особую операцию, определенную для его аппаратных средств. Объект типа HalfTape выполняет резервное копирование на 1/2-дюймовую ленту, а объект типа QuarterTape – на 1/4-дюймовую ленту.

Концепция полиморфизма является фундаментальной для объектно-ориентированного программирования. Профессионалы часто говорят об объектно-ориентированном программировании как о наследовании с полиморфизмом времени исполнения. C++ поддерживает эту конструкцию, используя динамическое связывание (dynamic binding) и виртуальные функции-члены (virtual member functions). Эти понятия описываются в главе 12. Сейчас же мы концентрируем внимание на этих понятиях, не давая технической информации о языковой реализации.

 

 

 


Динамическое связывание позволяет многим различным объектам в системе отвечать на одно и то же сообщение. Каждый объект отвечает на это сообщение определенным для его типа способом. Рассмотрим работу профессионального маляра, когда он (или она) выполняет малярную работу с различными типами домов. Определенные общие задачи должны быть выполнены при покраске дома. Допустим, что они описываются в классе House. Кроме общих задач требуются особые методы работы для различных типов домов. Покраска деревянного дома отличается от покраски дома с наружной штукатуркой или дома с виниловой облицовкой стен и так далее. В контексте объектно-ориентированного программирования особые малярные задачи для каждого вида дома задаются в производном классе, который наследует базовый класс House. Допустим, что каждый производный класс имеет операцию Paint. Класс House имеет операцию Paint, которая задается как виртуальная функция. Предположим, что Big Woody – это объект типа Wood-Frame. Мы можем указать Big Woody покрасить дом, вызывая явно операцию Paint. Это называется статическим связыванием (static binding).

BigWoody. Paint ( );// static binding

Допустим, однако, что подрядчик имеет список адресов домов, которые нуждаются в покраске, и что он передает сообщение своей команде маляров пойти по адресам в списке и покрасить эти дома. В данном случае каждое сообщение привязано не к определенному дому, а скорее – к адресу дома-объекта в списке. Команда маляров приходит к дому и выбирает правильную малярную операцию Paint, после того, как она увидит тип дома. Этот процесс известен как динамическое связывание (dynamic binding).

(House at address 414).Paint ( ); // dynamic binding

Процесс вызывает операцию Paint, соответствующую дому по данному адресу. Если дом по адресу 414 является деревянным, то выполняется операция Paint( ) из класса WoodFrame.

При использовании структур наследования в C++ операции, которые динамически связываются с их объектом, объявляются как виртуальные функции-члены (virtual member functions). Генерируется код для создания таблицы, указывающей местоположения виртуальных функций какого-либо объекта и устанавливается связь между объектом и этой таблицей. Во время выполнения, когда на положение объекта осуществляется ссылка, система использует это положение для получения доступа к таблице и выполнения правильной функции.

Понятие полиморфизма является фундаментальным в объектно-ориентированном программировании. Мы используем его с более совершенными структурами данных.

 

 

2. БАЗОВЫЕ КОНЦЕПЦИИ КЛАССОВ В С++

Выше были даны абстрактные типы данных (ADT) и их представление в качестве классов C++. Это введение описывает структуру класса, которая обеспечивает инкапсуляцию данных и скрытие информации. В этой главе содержится более полное описание базовых концепций класса. Здесь рассматриваются разработка и использование конструкторов класса, реализация методов класса и использование классов с другими структурами данных. Для обеспечения хорошего понимания классов читателем приводится широкий диапазон примеров, которые используются в законченных программах. Выбранные соответствующим образом ADT иллюстрируют связь между абстрактной структурой и объявлением класса.

Пользовательский тип — КЛАСС

private: данные операторы
Класс — это определяемый пользователем тип с данными и функциями (методами), называемыми членами (members) класса. Переменная типа класс называется объект (object). Класс создает различные уровни доступа к его членам, разделяя объявление на части: private, protected и public. Часть private (закрытая) объекта может быть доступна только для функций-членов в этом классе. Часть public (открытая) объекта может быть доступна для внешних элементов программы, в области действия которых находится этот объект (рис. 3.1). Protected (защищенные) члены используются с производными классами и описываются в главе, посвященной наследованию.

 

 

 

 


Рис. 2.1. Доступ к методам класса

Объявление класса

Объявление класса начинается с заголовка класса (class head), состоящего из зарезервированного слова class, за которым следует имя класса. Члены класса определяются в теле класса (class body), которое заключается в фигурные скобки и заканчивается точкой с запятой. Зарезервированные слова public и private разделяют члены класса, и эти спецификаторы доступа заканчиваются двоеточием. Члены класса объявляются как переменные C++, а методы задаются как объявления функций C++. Общая форма объявления класса такова:

class Class_Name

{

 private: //Закрытые данные

 //Объявление закрытых методов

 //

 public: //Открытые данные

 //Объявление открытых методов

 //

};

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

Пример 1.

Класс Rectangle

При геометрических измерениях прямоугольник определяется его длиной и шириной. Это позволяет нам вычислять периметр и площадь фигуры. Параметры длины и ширины и операции объединяются для образования абстрактного типа данных прямоугольной фигуры. Вы должны самостоятельно подготовить спецификацию ADT в качестве упражнения, освоив самостоятельно главу 1, а здесь будет разработан класс Rectangle C++, который реализует этот ADT. Класс содержит конструктор и набор методов — GetLength, PutLength, GetWidth и PutWidth, имеющих доступ к закрытым членам класса. Объявление класса Rectangle следующее:

class Rectangle

{

 private:

 //длина и ширина прямоугольного объекта

 float length, width

 public:

 //конструктор

 Rectangle(float l=0, float w=0);

 //методы для нахождения и изменения закрытых данных

 float GetLength(void) const;

 void PutLength(float 1);

 float GetWidth(void) const;

 void PutWidth(float w);

 //вычислять и возвращать измерения прямоугольника

 float Perimeter(void) const;

 float Area(void) const; I

}

Обратите внимание, что методы GetLength, GetWidth, Perimeter и Area имеют ключевое слово const после списка параметров. Это объявляет каждый метод как константный. В определении константного метода никакой элемент данных не может быть изменен. Иначе, выполнение метода, объявленного как const, не изменяет состояния объекта Rectangle.

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

Конструктор

Функция, называемая конструктором (constructor) класса, имеет то же имя, что и класс. Подобно другим функциям C++, конструктору могут передаваться параметры, используемые для инициализации одного или более данных-членов класса. В классе Rectangle конструктору дается имя Rectangle, и он принимает параметры l и w, используемые для инициализации длины и ширины объекта, соответственно. Заметьте, что эти параметры имеют значения по умолчанию, которые указывают, что используется значение 0, когда параметр l или w не передается явно.

Пример 1 иллюстрирует объявление класса (class definition), так как методы описываются только объявлениями функций. Код C++ для определения отдельных функций создает реализацию класса (class implementation).

Объявление объекта

Объявление класса описывает новый тип данных. Объявление объекта типа класс создает экземпляр (instance) класса. Это делает реальным объект типа класс и автоматически вызывает конструктор для инициализации некоторых или всех данных-членов класса. Параметры для объекта передаются конструктору заключением их в скобки после имени объекта. Заметьте, что конструктор не имеет возвращаемого типа, поскольку вызывается только во время создания объекта:

ClassName object(<parameters>);//список параметров может быть

// пустым

Например, следующие объявления создают два объекта типа Rectangle:

Rectangle room(12, 10);

Rectangle t; //использование параметров по умолчанию (0, 0).

Каждый объект имеет полный диапазон данных-членов и методов, объявляемых в классе. Открытые члены доступны с использованием имени объекта и имени члена, разделяемых «.» (точкой). Например:

х = room.Area(); //присваивает х площадь 12*10=120

t.PutLength(20); //присваивает 20 как длину объекта Rectangle

// Текущая длина равна 0 так как используются параметры

// по умолчанию,

cout<<t.GetWidth(); // выводит текущую ширину, которая = 0

// по умолчанию

В объявлении объекта Room конструктор первоначально устанавливает значение длины, равным 12, а ширины — 10. Клиент может изменять размеры, используя методы доступа PutLength и PutWidth:

room.PutLength(15); //изменение длины и ширины на 15 и 12

room.PutWidth(12);

Объявление класса не обязательно должно включать конструктор. Это действие, которое не рекомендуется, оставляет объект с неинициализированными данными в точке его объявления. Например, класс Rectangle может не иметь конструктора, а клиент мог бы задать длину и ширину с помощью открытых методов доступа. Включение в класс конструктора, обеспечивает правильную инициализацию важных данных. Конструктор позволяет объекту инициализировать его собственные данные-члены класса.

Класс Rectangle содержит члены класса типа float. В общем, класс может содержать элементы любого допустимого типа C++, даже других классов. Однако, класс не может содержать объект его собственного типа в качестве члена.

Реализация класса

Каждый метод в объявлении класса должен быть определен. Определения функций могут быть заданы в теле класса (встроенный код) или вне его. При помещений функций вне тела имя класса, за которым следует два двоеточия, должно предшествовать имени этой функции. Символ "::" называется операцией разрешения области действия (scope resolution operator) и указывает на то, что функция принадлежит области действия класса. Это позволяет всем операторам в определении функции иметь доступ к закрытым членам класса. В случае с классом Rectangle идентификатор "Rectangle::" предшествует именам методов.

Далее следует определение GetLength(), когда она записана вне тела класса Rectangle:

float Regtangle::GetLength(void) const

{

 return length; //доступ к закрытому члену length

}

Заметьте, что при определении константного метода может быть также использован квалификатор const.

Функция-член класса может быть записана внутри тела класса. В этом случае код является расширенным встраиваемым (expanded inline), а операция разрешения области действия не используется, так как код находится в области действия тела класса. Встраиваемое определение операции GetLength имеет вид:

class Rectangle

{

 private:

 float length;

 float width;

 public:

 float GetLength(void) const //код задается как inline

 {

return(length);

 }

};

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

Реализация конструктора

Конструктор может быть определен как inline или вне тела класса. Например, следующий код определяет конструктор Rectangle:

Rectangle::Rectangle(float 1, float w)

{

 length = l;

 width = w;

}

C++ предоставляет специальный синтаксис для инициализации членов класса. Список инициализации членов (member initialization list) — это список имен данных-членов класса, разделенных запятыми, за каждым из которых следует начальное его значение, заключенное в скобки. Начальные значения обычно являются параметрами конструктора, которые присваиваются соответствующим данным-членам класса в списке. Список инициализации членов помещается после заголовка функции и отделяется от списка параметров двоеточием:

 

ClassName::ClassName(parm list):data1(parm1),..., datan(parmn)

Например, параметры конструктора l и w могут быть присвоены данным-членам класса length и width:

Rectangle::Rectangle(float l, float w): length(l), width(w)

{}

Создание объектов

Один объект может использоваться для инициализации другого в каком-либо объявлении. Например, следующий оператор является правильным:

Rectangle square(10, 10), yard = square, S;

Объект square создается с length и width, равными 10. Второй объект yard создается с начальными данными, копируемыми из объекта square. Объект S имеет length и width, по умолчанию равными 0.

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

S = yard;

копирует все данные из объекта yard в объект S. В этом случае length и width объекта yard копируются в length и width объекта S.

Объект может быть создан ссылкой на его конструктор. Например, объявление Rectangle(10,5) создает временный объект с lengh = 10 и width = 5. В следующем операторе операция присваивания копирует данные из временного объекта в rectangle S:

S = Rectangle(10,5);

Пример 2.

1.Операторы

S = Rectangle(10,5);

cout << S.Area() << endl;

приводят к выводу в поток cout числа 50.

2.Оператор

cout << Rectangle(10,5).GetWidth() << endl;

Выводит число 5.

 

Программа 1. Использование класса Rectangle

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

Предположим, что опалубка проходит по периметру передней стороны и периметру проема двери. Мы запрашиваем у пользователя размер передней стороны гаража и затем вводим цикл, позволяющий выбрать размер двери. Цикл заканчивается, когда пользователем выбирается опция "Quit". Для каждого выбора двери программа определяет стоимость отделки передней стороны гаража и выводит это значение. Мы задаем константами стоимость деревянной обшивки $2 за кв. фут и стоимость опалубки на $0,50 за погонный фут.

 


Опалубка

 


Дверь

 

Обшивка

 

Длина опалубки равна сумме периметров передней стороны гаража и двери. Стоимость обшивки равна площади передней стороны гаража минус площадь двери.

//Example1.срр

#include <iostream.h>

class Rectangle

{

 private:

 // длина и ширина прямоугольного объекта

 float length, width;

 public:

 // конструктор

 Rectangle(float l=0, float w = 0);

 // методы для получения и модификации закрытых данных

 float GetLength(void) const;

 void PutLength(float 1);

 float GetWidth (void) const;

 void PutWidthffloat w) ;

 //вычисление характеристик прямоугольника

 float Perimeter (void) const;

 float Area(void) const;

}

//конструктор, выполняет присваивания: length=l,

Rectangle:: Rectangle(float l, float w): length(l), width(w)

{}

// возвратить длину прямоугольника

float Rectangle:: GetLength(void) const

{

 return length;

}

//изменить длину прямоугольника

void Rectangle:: PutLength(float l)

{

 length = l;

}

// возвратить ширину прямоугольника

float Rectangle:: GetWidth(void) const

{

 return width;

}

// изменить ширину прямоугольника

void Rectangle:: PutWidth(float w)

{

 width = w;

}

// вычислить и возвратить периметр прямоугольника

float Rectangle:: Perimeter(void) const

{

 return 2.0*(length + width) ;

}

//вычислить и возвратить площадь прямоугольника

float Rectangle:: Area(void) const

{

 return length*width;

}

void main(void)

{

 //стоимости обшивки и опалубки - постоянные

 const float sidingCost = 2.00, moldingCost = 0.50;

 int completedSelections = 0;

 //опция из меню, выделенная пользователем

 char doorOption;

 //длина/ширина и стоимость двери

 float glength, gwidth, doorCost;

 //общая стоимость, включая дверь, обшивку и опалубку

 float totalCost;

 cout << "Введите длину и ширину гаража: ";

 cin >> glength >> gwidth;

 //создать объект garage(гараж) с размерами по умолчанию

 // создать объект door(дверь) с размерами по умолчанию

 Rectangle garage(glength, gwidth);

 Rectangle door;

 while (!completedSelections)

 {

cout << "Введите 1-4 или 'q' для выхода" << endl << endl;

cout << "Дверь 1 (12х8; $380) "

cout << "Дверь 2 (12x10; $420) "<<endl;

cout << "Дверь 3 (16x8; $450) "

cout << "Дверь 4 (16 x10; $480)" << endl;

cout << endl;

cin >> doorOption;

if(doorOption == 'q')

completedSelections = 1;

else

switch (doorOption)

{

case '1':door.PutLength(12); // 12 x 8 ($380)

door.PutWidth(8) ;

doorCost = 380;

break;

case '2':door.PutLength (12) ; // 12 x 10 ($420)

door.PutWidth(10);

doorCost = 420;

break;

case '3':door.PutLength (16) ; // 16 x 8 ($450)

door.PutWidth(8);

doorCost = 450;

break;

case '4':door.PutLength (12) ; //16x10 ($480)

door.PutWidth(10);

doorCost = 480;

break;

}

totalCost=doorCost+moldingCost*(garage.Perimeter()+

door.Perimeter())+sidingCost* (garage.Area()-door.Area());

cout <<"Общая стоимость двери, обшивки и опалубки:$" <<

totalCost << endl << endl;

 }

}

}

/*

<Запуск программы 3.1>

Введите длину и ширину гаража: Введите 1-4 или 'q' для выхода

Дверь 1 (12х8; $380) Дверь 2 (12х10; $420)

Дверь 3 (16х8; $450) Дверь 4 (16х10; $480)

Общая стоимость двери, обшивки и опалубки: $720

Введите 1-4 или ' q' для выхода

Дверь 1 (12х8; $380) Дверь 2 (12х10; $420)

Дверь 3 (16x8; $450) Дверь 4 (16x10; $480)

q

*/

Примеры классов

Следующие два примера классов иллюстрируют конструкторы класса в С++. Класс Temperature поддерживает записи значений высокой и низкой температуры. В качестве приложения объект мог бы иметь высокую (точка кипения) и низкую (точка замерзания) температуры воды. ADT RandomNumber определяет тип для создания последовательности целых или с плавающей точкой случайных чисел. В реализации C++ конструктор позволяет клиенту самому инициализировать последовательность случайных чисел или использовать программный способ получения последовательности с системно-зависимой функцией времени.

Класс Temperature

Класс Temperature содержит информацию о значениях высокой и низкой температуры. Конструктор присваивает начальные значения двум закрытым данным-членам highTemp и lowTemp, которые являются числами с плавающей точкой. Метод UpdateTemp принимает новое значение данных и определяет, должно ли обновляться одно из значений температуры в объекте. Если отмечается новое самое низкое значение, то обновляется lowTemp. Аналогично, новое самое высокое значение изменит highTemp. Этот класс имеет два метода доступа к данным: GetHighTemp возвращает самую высокую температуру, a GetLowTemp возвращает самую низкую температуру.

Спецификация класса Temperature

ОБЪЯВЛЕНИЕ

class Temperature

{

private:

float highTemp, lowTemp; // закрытые данные-члены

public:

Temperature(float h, float l);

void UpdateTemp(float temp);

float GetHighTemp(void) const;

float GetLowTemp(void) const;

};

ОБСУЖДЕНИЕ

Конструктору должны быть переданы начальные высокая и низкая температуры для объекта. Эти значения могут быть изменены методом UpdateTemp. Методы GetLowTemp и GetHighTemp являются константными функциями, так как они не изменяют никакие данные-члены в классе.

ПРИМЕР

//точка кипения/замерзания воды по Фаренгейту

Temperature fwater(212,32);

//точка кипения/замерзания воды по Цельсию

Temperature cwater(100, 0) ;

cout << Вода замерзает при<<cwater.GetLowtemp << " С"<< endl;

cout << Вода кипит при << fwater.GetHighTemp << " F" << endl;

Выход:

Вода замерзает при 0 С

Вода кипит при 212 F


Дата добавления: 2018-10-25; просмотров: 224; Мы поможем в написании вашей работы!

Поделиться с друзьями:






Мы поможем в написании ваших работ!