Вообще, в классах могут объявляться элементы типа public, private и protected (общие, приватные, и защищенные - детальные разъяснения смотрите в следующем разделе). Эти элементы могут быть другими объектами из других классов и функциями элементами:
Ключевое слово сlass Имя класса (как правило, осмысленное) v v class shape { private: // Ключевое слово private Элемент > int init_flag; // Приватный элемент данных protected: // Ключевое слово protected Элементы > double xo, yo; // Защищенные элементы данных public: // Ключевое слово public Функции -> void init(double x, double y); // Общие элементы -> double area(void); // элементы };
Имя класса shape можно использовать точно так же, как и имя обычного типа данных в Cи. Например, можно сделать следующее объявле- ние для массивов:
shape Times[10]; // объявление массива shape из 10 элементов
Понятие инкапсуляция означает, что функции элементы и структуры данных, определяющие некоторые свойства данного класса, рассматриваются в качестве единого целого. Это подразумевает "защиту" данных в пределах класса таким образом, что только элементы класса получают к ним доступ. То есть, для того чтобы получить значение одного из элементов данных класса, нужно вызвать функцию элемент этого класса, ко- торый возвращает необходимое значение. Для присвоения элементу значения, вызывается соответствующая функция элемент данного класса. Вообще, в объектном программировании считается хорошим тоном закрывать все данные и функции элементы описываемого класса для доступа "извне".
Borland C++ предоставляет программистам три уровня доступа к элементам объектов:
- public (общий), - private (приватный), - protected (защищенный).
Элементы, объявленные общими, будут доступны любому внешнему элементу класса, любой функции элементу или выражению в программе, когда объект является видимым.
Приватные элементы доступны только другим элементам своего же класса. Они не доступны извне, за исключением специальных функций, называемых "дружественными".
К защищенным элементам имеют доступ лишь некоторые из объектов. Они доступны только элементам своего класса и любым его потомкам. Поэтому защищенные элементы занимают промежуточное положение между общими и приватными.
Примечание: приватные элементы недоступны потомкам своего класса. Поэтому и понадобились защищенные элементы.
Уровень доступности к элементам класса проиллюстрирован ниже:
Общие элементы (public) Защищенные элементы (protect) Приватные элементы (private)
Умелое использование уровней доступа повышает надежность программ и их способность к изменениям, ослабляя взаимозависимость между объектами. Правильно описанными функциями элементами типа public можно изменять приватные элементы, не затрагивая программный код других объектов. В реальной жизни мы подобным образом взаимодействуем с различными предметами: телевизором, будильником, радиоприемником, автомобилем - знание внутреннего устройства которых нам не обязательно. Однако, нам важны внешние кнопки и ручки управления.
При порождении потомка класса у вас есть выбор в определении типа элементов. По умолчанию элементы базового класса автоматически получают приватный тип, если только вы не захотите иначе. Использование же ключевого слова public, при порождении потомка класса, делает все элементы базового класса общими в порожденном классе. Приватные элементы базового класса остаются приватными, и не будут доступны из потомков. Вот где требуются защищенные элементы (protect).
Замечание. Если вы хотите использовать в потомке класса элементы базового класса, то они должны быть объявлены как защищенные или общие. Доступ к приватным элементам базового класса невозможен никак иначе, как из их приватных функций элементов или с помощью дружественных функций, объявленных как friend.
В приведенной ниже таблице показана доступность элементов базового класса для его потомка (в зависимости от определенного типа доступа в базовом классе и потомке):
Тип доступа в Тип доступа Доступность элеме- базовом классе в потомке нта базового класса приватный приватный недоступен защищенный приватный приватный общий приватный приватный приватный общий недоступен защищенный общий защищенный общий общий общий
По мере изучения материала вы познакомитесь с многочисленными примерами использования управления доступом.
Наследование - это способность брать существующий - базовый класс и порождать из него новый класс - потомок, с наследованием всех его атрибутов и поведения. Это пожалуй самая впечатляющая возможность объектно-ориентированного программирования и, возможно, единственное коренное отличие С++ от Си.
Рассмотрим отвлеченный пример из реальной жизни - классификационную схему живых организмов. По этой схеме растительные и живые царства делятся на группы, так называемые типы. Каждый тип, в свою очередь, делится на классы, отряды, семейства и далее. Группы более низкого уровня наследуют характеристики групп более высоких уровней. Так, из утверждения о том, что волк относится к семейству псовых, вытекает сразу несколько положений. Из него следует, что у волков хорошо развиты слух и обоняние, поскольку таковы характеристики псовых. Так как псовые входят в отряд хищных, это утверждение говорит еще о том, что волки питаются мясом. Поскольку хищные относятся к млекопитающим, это утверждение говорит и о том, что волки имеют волосяной покров и регулируемую температуру тела. Наконец, так как млекопитающие являются позвоночными, мы узнаем и то, что у волков есть позвоночник.
Волк -> Псовые -> Хищники -> Млекопитающие -> Позвоночные
Подобные схемы наследования можно проследить в классификации языков программирования, классификации типов компьютеров и других. Рассмотрим существующие в объектно-ориентированных языках иерархии порождаемых объектов.
Наследование может использоваться множество раз при порождении объектов. Порожденные классы могут наследовать готовые функции элементы своих классов-предков, набирая при каждом порождении все больше и больше функций элементов.
Окно ^ Наследование Панель Порождение классов v МенюВы можете также порождать несколько классов из базового класса:
^ Меню Насле- Порождение дование > < классов v Вертикальное Горизонтальное
Вертикальное и Горизонтальное меню имеют одного предка - Меню. Общее свойство, унаследованное от Меню - это список элементов, расположение. Порождение нескольких классов из одного корня позволяет использовать текст программ для многих классов.
Используя наследование, можно наращивать иерархию классов, создавая как-бы древовидные формы. Еще более сложные иерархии образуются через множественное наследование.
Под множественным наследованием понимается способность наследования свойств и поведения не от одного базового класса, а от нескольких. Такое наследование легко проследить по классификации языков программирования. У каждого из языков, как правило несколько предков. Так язык программирования С++ является потомком Си и Симулы, язык Ада потомком целого спектра языков и так далее.
Си Симула ^ < > Насле- Порождение дование классов С++ v
Как используется наследование в реальных программах? На примере библиотеки Turbo Vision попробуем проследить его использование в иерархии классов TObject и TView. Внимательное ее изучение будет вам чрезвычайно полезно. Так, вы узнаете, что класс TDialog (диалог) является наследником класса TWindow (окно), который, в свою очередь, является наследником класса TGroup (группа), а последний - класса TView (отображаемый объект). Имеется несколько примеров множественного наследования в иерархии классов Turbo Vision. Вот один из них: TProgram получается из TProgInit и TGroup.
opstream TObject TStreamable ^ ^ ipstream TView ^ TGroup ^ ^ ^ TDeskInit ^ TWindowInit ^ TProgInit TDeskTop ^ >TWindow< THistInit< TProgram THistoryWindow ^ TApplication TDialog
Примечание: по установившейся практике стрелки показывают от порожденного класса на базовый.
Выбрав любой из классов, вы можете найти в его определении унаследованные и новые свойства.
Наследование это ключевая часть объектно-ориентированного программирования. Наследование экономит массу времени на изучение свойств всех классов. Каждый новый порожденный класс обладает уже известными унаследованными свойствами, оставляя для изучения только дополнительные поля и свойства. Наследование позволяет на хорошем уровне разделять код и данные, а также разрешает полиморфизм (см. далее).
Другая выгода от наследования заключается в модульности классов. Так, вы можете распространять ваши классы в объектном виде среди других программистов. На их базе они смогут порождать новые, специализированные классы. При этом без знания вашего исходного текста. Уже появилось большое количество библиотек классов различных фирм (см. приложение 2). Возможно, они будут вам весьма полезны и обеспечат значительный успех в программных проектах.
Наследование позволяет повторно использовать существующие исходные тексты программ, подправлять их и перекомпилировать. Эта способность готового к компиляции исходного текста названа расширяемостью.
Наследование > Новый класс shape.h + circle.h = circle.h shape.obj circle.c circle.obj
Для дополнения класса shape (фигура) классом circle (круг), достаточно лишь объявления его в классе shape (без изменения функций элементов класса) в модуле shape.h и скомпилировать исходный текст в shape.obj. Таким образом нет необходимости изменять исходный текст shape.c.
Успешное создание потомков позволяет увеличивать программу за счет накопления уже готовых текстов программ. Так, круг наследуют все свойства фигуры, а цилиндры наследуют все свойства круга и фигуры.
Цилиндр: cylinder() ht area() vol() Круг: circle() radius area() Фигура: shape() xo yo area()
Возможно вы заметили по картинке, что класс цилиндр имеет три функции area(). Действительно, унаследована одна функция area() у окружности, и одна у фигуры. Заимствование этих двух функций, для цилиндра оставляет возможность доступа к ним.
Примечание: порожденный класс может только наращивать базовый класс, но не способен удалять какие-либо элементы.
Программист, без исходного текста программы, располагая лишь описанием интерфейса с некоторым классом, может определить новый класс, наследующий все свойства базового. После этого он может выборочно переопределить поведение некоторых функций элементов базового класса так, как ему нужно.
Чтобы задать отношения наследования между классами, надо при
описании нового класса после имени класса поставить двоеточие и далее
перечислить через запятую имена потомков.
В этом примере из базового класса shape порождается класс
circle:
class shape { < Объявление класса public: double xo, yo; < Данные (координаты) shape(double x, double y); < Конструктор virtual double area(void); < Виртуальная функция элемент }; Связывание порожденного класса с базовым v class circle : public shape { < Объявление порожденного класса public: double radius; < Дополнительные данные double area(void); < Заимствованная функция элемент circle(double x, double y, double r); < Конструктор };
class shape { public: double xo, yo; // Расположение фигуры shape(double x, double y); // Создание конструктором фигуры virtual double area(void); // Функция, вычисляет поверхность };
shape::shape(double x, double y) // Устанавливает координаты фигуры { xo = x; yo = y; }
Поверхность фигуры shape в качестве исходного значения имеет 0. Порожденные классы вероятно подменят эту функцию.
double shape::area() { return 0; }
class circle : public shape { public: double radius; // Дополнительные данные: радиус круга // конструктор circle circle(double x, double y, double r); double area(void); // Подменяет в классе shape функцию area() };
circle::circle(double x, double y, double r) : shape(x,y) < Обратите внимание как конструктор circle вызывает конструктор shape, с начальной установкой. { radius = r; < Далее попросту устанавливается радиус круга }Функция площади круга подменяет определение, сделанное в базовом классе, и вычисляет знаменитую формулу pi*r2.
double circle::area(void) { return 3.14159 * radius * radius; }
Ниже показано как можно произвести класс cylinder из circle:
class cylinder : public circle { public: double ht; // Дополнительные данные: высота цилиндра // Конструктор для цилиндра cylinder(double x, double y, double r, double h); double area(void); // Подмена в классе circle функции area() double vol(void); // Дополнительная функция вычисления объема };
cylinder::cylinder(double x, double y, double r, double h) : circle(x,y,r) < Обратите внимание как конструктор cylinder вызывает конструктор circle, для дальнейшей работы. { ht = h; }
Функция vol() вычисляет объем цилиндра vol = pi*rЩh. Обратите внимание на изменения функции area() по сравнению с той, что была у окружности.
double cylinder::vol() { v return circle::area() * ht; ^ } оператор разделения
double cylinder::area() { return 6.283 * radius * ht; }
class rect : public shape { public: double wd, ht; // Дополнительные данные: размер прямоугольника // Конструктор для создания объекта rect (прямоугольник) rect(double x, double y, double w, double h); double area(void); // Подмена функции area() в классе shape };
recte::rect(double x, double y, double w, double h) : shape(x,y) < Обратите внимание как конструктор rect вызывает конструктор shape, с начальной установкой. { wd = w; < Затем попросту устанавливаются размеры ht = h; }Область функции area() замещает определение базового класса.
double rect::area(void) { return wd * ht; }
class box3d : public rect { public: double depth; // Добавлена глубина // Конструктор для создания трехмерного прямоугольного объекта box3d(double x, double y, double w, double h, double d); double area(void); // Подмена area() в классе rect double vol(void); // Дополнение функцией вычисления объема };
box3d::box3d(double x, double y, double w, double h, double d) : rect(x,y,w,h) < Обратите внимание как конструктор box3d вызывает конструктор rect, для дальнейшей { работы. depth = d; }
Функция vol() для box3d вычисляет занимаемый им объем, который равен - * ht * depth. Обратите внимание на изменения функции area() по сравнению с той, что работала у прямоугольника.
double box3d::vol() { v return rect::area() * depth; ^ } Оператор селектора
Заимствование кода является нормой для порожденных классов, поэтому и большая часть созданного программного кода будет использоваться повторно или по крайней мере будет входить в повторный цикл.
Функция area() вычисляет поверхность для box3d по формуле - 2 x поверхность основания + 4 x поверхность стороны.
double box3d::area() { Функция area возвращает размер v поверхности основания прямоугольника return 2.0 * rect::area() + 4.0 * rect(0, 0, ht, depth).area(); ^ ^ } Вызывает временную ??? функцию размера поверхности Делает временным объект размер прямоугольника по одной из сторон
Сейчас мы на самом деле достигли наследования текста программы!
Обратите внимание на забавные вызовы конструктора базового класса - использование операции ':' после заголовка функции, и перед телом функции. Конструктор порожденного класса вызывает конструктор базового класса, используя синтаксис вызова функции.
circle::circle(double x, double y, double r) : shape(x,y) < Заметьте как конструктор circle вызывает конструктор shape с начальной установкой { radius = r; }
Важно отметить, что программисты, создавая классы, создают, по сути, абстрактную основу - шаблоны для создания объектов. Из шаблона, когда нужно, создается объект, который и используется. Но, прежде чем вы напишете хотя бы одну строку программы на С++, необходимо хорошо продумать необходимые вам классы и уровни их использования.
Не существует "идеальной" иерархии классов для каждой конкретной программы. По мере продвижения разработки может оказаться, что вам потребуется ввести новые классы, которые коренным образом изменят всю иерархию классов. Каждая иерархия классов представляет собой сплав экспериментальных исследований и интуиции, основанной на практике. Так, потребовались сотни лет для создания классификации животных, и тем не менее, вокруг нее, до сих пор, ведутся горячие споры и дела- ются попытки ее изменения.
Умелое использование наследования позволяет с небольшими усилиями модифицировать огромные по объему программы. К тому же необходимо помнить, что постоянно растущее число поставщиков предоставляют пользователям объектно-ориентированные библиотеки, совместимые с Турбо и Borland С++. Так что не следует "высасывать их из пальцев".
Библиотека классов container, поставляемая в пакете Турбо и Borland C++, содержит классы для часто используемых структур данных (списки, стеки, очереди и т.д.). Классы организованы в соответствии с иерархией классов, что позволяет иметь высокую степень модульности благодаря свойствам наследования и полиморфизма. Вы можете использовать эти классы в том виде, как они есть, либо расширять и дополнять их, получая объектно-ориентированные программные продукты, подходящие для ваших задач.
В вершине иерархии классов conteiner находится абстрактный класс Object. Он почти не содержит данных-элементов, а его функции элементы являются "чистыми" виртуальными функциями (pure virtual - т.е. функциями, никогда не вызываемыми непосредственно, и служащими для задания местоположения функций с тем же именем, определяемых в производных классах). Неабстрактные классы, предназначенные для реализации объектов, называются реализуемыми классами (абстрактные классы заключены в кавычки.)
Чтобы лучше освоить понятие классов, посмотрите их исходные тексты в каталоге CLASSLIB дистрибутивной поставки компилятора.
"Object" Error "Sortable" String "BaseDate" Date "BaseDate" Time Association "Container" "Collection" "AbstractArray" Array SortedArray HashTable Bad Set Dictionary List DoubleList Stack Queue Deque "ContainerIterator" HashTableIterator ListIterator DoubleListIterator ArrayIterator DoubleListElement ListElement Рис. Иерархия классов в CLASSLIB
В переводе с греческого polymorphos - многообразный. Если один и тот же объект может по-разному использоваться, в зависимости от обстоятельств, то он обладает полиморфизмом. В какой-то степени свойствами полиморфизма обладают, например, автомобили-амфибии - используются для передвижения и по суше, и по воде. Чтобы достичь таких "чудес" в ООП используют "ранее" и "позднее связывание".
В результате получаются функции элементы, обладающие полиморфизмом, которые ведут себя по-разному, за счет своих различных свойств.
Например, как функция элемент draw():
rectangle.draw(); jigsaw.draw();
В случае раннего связывания адреса всех функций и процедур определяются на этапе компиляции и компоновки программы, т.е. до выполнения программы.
Вызовы функции: Описания функций: func_down(); > my_first_metod(); my_metod(); > func_down(); my_first_metod(); > my_metod();Так, в языке Cи компилятор прежде всего должен найти описание функции по заданному имени.
> описание функции int my_first_metod(int a, int b) { return a + 2*b; } ....................... k = my_first_metod(12, 24); < вызов функции
В противоположность этому, в случае позднего связывания адрес функции элемента не связывается с обращением к нему до того момента, пока обращение не произойдет фактически, то есть во время выполнения программы.
Так в библиотеке Turbo Vision любой объект, порожденный от TView, должен быть способен к самоизображению в любой момент времени. Объект TView определяет виртуальную функцию элемент draw, и каждый порожденный от него объект должен также иметь эту функцию элемент. Это имеет большое значение, т.к. часто отображаемый объект может быть закрыт другим отображаемым объектом (или на него может быть наложен другой отображаемый объект) и когда другой отображаемый объект убирается или смещается, то отображаемый объект должен быть способен к изображению своей части, которая была скрыта.
File Window Next F6 Demo Window 3 Zoom F5 Demo Window 7 Demo Window 8 Demo Window 1 Demo Window 4 Demo Window 6 Window 2 [*] Demo Window 9 [+] Alt-X Exit F4 New Alt-F3 Close
В такой динамической системе нельзя заранее предсказать, сколько отображаемых объектов будет на экране, каких они будут типов (окно, меню, диалоговая панель и т.д.) и в какой последовательности пользователь будет с ними работать. В программе, где используется только ранее связывание, вся информация о количестве, координатах и типах отображаемых объектов хранится в основной программе. Все возможные действия над ними тоже должны быть предусмотрены в этой программе. Таким образом, программе приходится отслеживать очень многое, она усложняется и теряет гибкость. Стоит добавить один новый тип отображаемого объекта или изменить поведения существующего, и придется скорректировать программу во всех тех местах, где определяется, какие подпрограммы подлежат вызову.
Каким же образом можно улучшить положение с помощью позднего связывания? Рассмотрим случай, когда один отображаемый объект, например, - окно, частично перекрывает другое. Если "верхнее" окно будет передвинуто или закрыто, то нижнее следует перерисовать для восстановления ранее перекрытой части.
Так как меню окно перерисовывается иначе, чем диалоговая панель или окно, то каждый объект в отображаемой иерархии должен знать, как перерисовать себя. В библиотеке Turbo Vision этим занимается функция элемент draw, имеющийся в каждом объекте-потомке класса TView. Следовательно, если требуется перерисовать объект, то программе не нужно анализировать, к какому типу отображаемого объекта он относится (как это требовалось бы при раннем связывании). Она просто вызывает функцию элемент данного объекта draw. Функция исполняется и корректно перерисовывает свой объект на экране. Такая множественность действий, которую может выполнять функция элемент с одним и тем же именем, называется полиморфизмом.
Не смотря на то, что программисты, для достижения полиморфизма предпочитают использовать позднее связывание, но его можно достичь и ранним связыванием с помощью переопределяемых функций.
Действие механизма переопределяемых функций несколько сходно с механизмом функции переключателя switch, используемого в Паскале. Однако, если switch, в зависимости от значения, выполняет тот или иной блок процедур, то переопределяемые функции, в зависимости от заданных аргументов вызывают и выполняют, тот или иной код, используя одно и то же имя функции. Дело в том, что компиляторы Турбо и Borland С++, различают функции не только по их именам, но и по типу их аргументов.
Достичь полиморфизма через "игру" с аргументами можно: потерей одного из аргументов (осторожно, так чтобы компилятор не спутал отсутствие аргумента с особой "потерей" аргументов, принимаемыми "по умолчанию", описываемыми далее), изменить тип аргумента, или использовать различное число аргументов. Например, функцию, вычисляющую площадь прямоугольника
double sqr (double x, double y){return x*y;}можно дополнить одноименной, но вычисляющей площадь квадрата, имеющую отличный тип у аргумента x и меньшее число параметров:
int sqr (int x){return x*x;}
Компилятор предварительно просматривает содержащиеся в функции типы и по отличию в типе аргумента определяет ту или иную функцию.
Переопределяемые функции v v v double sqr (double x, double y); int sqr(int x); { { ^ return x*y; return x * x; } } int i, k; Проверка типов v v k = add(i);
Как вы видите наблюдается некоторая дополнительная работа (компилятора языка С++ по отношению к Си), при раннем связывании функций объектов - проверка типов.
Однако нужно иметь в виду, что компилятор не отличит эти же функции, если тип аргумента сделать одинаковым, а изменить лишь тип возвращаемого значения, например:
double sqr (double x){return x*x;}; и int sqr (double x){return x*x;}
Поскольку в С++ операции, рассматриваются как функции, то очевидно, что и их можно переопределять так, что они будут работать не только с числами, а даже с графическими объектами, строками и вообще с чем угодно. В С++ число операций ограничено стандартным набором:
[] () . -> ++ -- & * + - ~ ! sizeof / % << >> < > <= >= == != ^ | && || ?: = *= /= %= += -= <<= >>= &= ^= |= , # ##
Чтобы определить операцию, необходимо определить функцию, именем которой является специальное ключевое слово oрerator, за которым сле- дует символ операции.
Допустим нам удалось определить тип переменных time - время, так что появилась возможность записывать операции со временем в виде, не разрушающем привычный формат (например: 20.04 + 05.57 = 02.01).
Переопределяемые операции v v time operator+(time a, time b) < { ^ time c; операция + return c = a.val + b.val; } time work_time, busy_time, birth_time; > вызывает переопределенную операцию work_time = busy_time + goof_off_time;
Тип операнда определяет какую из переопределенных операций необ- ходимо использовать.
При переопределении операции действуют следующие ограничения:
. .* :: ?:
a.val.(операция + (b.val)).