ГОСУДАРСТВЕННЫЙ КОМИТЕТ РОССИЙСКОЙ ФЕДЕРАЦИИ ПО ВЫСШЕМУ ОБРАЗОВАНИЮ ────────────── ПЕНЗЕНСКИЙ ГОСУДАРСТВЕННЫЙ ТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ ──────────────────────────────────────────────────
А.В.С и в о х и н
ОБЪЕКТНО-ОРИЕНТИРОВАННЫЙ ЯЗЫК ПРОГРАММИРОВАНИЯ С++ В ПРИМЕРАХ
Учебное пособие
ПЕНЗА 1994
УДК 681.3.06 С 34 Сивохин А.В. Объектно-ориентированный язык программирования С++ в примерах: Учеб. пособие/Под ред.профессора Б.Г.Хмелевского. - Пенза: Пенз. гос. техн. ун-т, 1994. -80 с.:3 ил., 3 табл., библиогр. 14 назв. Рассмотрены принципы объектно-ориентированного программирования и средства их реализации в языке С++.Дано обоснование иерархического подхода при разработке сложных программ, обеспечивающего адекватное и целостное представление в ЭВМ предметной области; на примерах показаны механизмы наследования и инкапсуляции данных вместе с обрабатывающими функциями, а также их использование для построения гибкого и надежного программного обеспечения; определены виды полиморфизма операций, функций и объектов, являющегося фундаментальным принципом технологии объектно-ориентированного программирования и представляющего наибольшую сложность в освоении технологии. Для серьезного использования на практике указанных возможностей языка С++ необходимо иметь полномасштабную справочную литературу,хорошие методические пособия и солидную программную поддержку для автоматизированного изучения языка на персональных ЭВМ,оснащенных компилятором С++.Недостаток или отсутствие всех этих материалов в настоящее время не позволяет эффективно организовать учебный процесс. Учебное пособие разработано на кафедре "Математическое обеспечение и применение ЭВМ" по дисциплине "Языки и системы представления знаний",ориентировано на использование пакета разработанных автором автоматизированных обучающих курсов и предназначено для студентов всех специальностей,которым в том или ином объме читаются основы технологии объектно-ориентированного программирования. Под редакцией профессора Б.Г.Хмелевского Р е ц е н з е н т ы : технический совет НПО "Рубин",г.Пенза; В.В.Пикулин,канд. техн. наук, доц. втуза. 2404010000 С------------------ Без объявл. 7К1(03)-94 С
Издательство Пензенского государственного университета.1994
В В Е Д Е Н И Е Язык программирования С++ является новой версией широко используемого на практике языка С,которая представляет собой существенное и вместе с тем почти строгое его расширение. Несмотря на свою молодость(первое полное описание С++ дано автором Бьярном Строуструпом в 1985 году[1]),он приобрел к настоящему времени значительное число сторонников как за рубежом,так и у нас в стране. Одно из принципиальных новшеств языка - это увеличение степени статического контроля типов используемых данных,производимого на стадии компиляции программы,что связано с требованиями создания надежного программного обеспечения на базе технологии абстракции данных. Именно слабость статического контроля языка С постоянно вызывала критику его оппонентов. Другое еще более важное новшество - это включение непосредственно в язык мощного аппарата для поддержания наиболее перспективной технологии - технологии объектно-ориентированного программирования: классы и объекты,иерархия структур,объединений и классов с различными механизмами наследования,инкапсуляция данных и обрабатывающих функций с несколькими уровнями доступа,полиморфизм операций,функций и объектов,обеспечивающий как статический,так и виртуальный методы доступа. Встроенный аппарат объектно-ориентированного программирования позволяет описывать сложные предметные области и создавать гибкие,легко адаптируемые к нуждам пользователя программные пакеты как системного,так и прикладного назначения. Наконец,третье новшество языка С++ - многочисленные улучшения языка С,порой даже вызывающие восторг у его пользователей: ссылки на типы,инициализация параметров значениями по умолчанию,параметризованные типы(шаблоны) составных объектов и функций,обработка исключительных ситуаций,новые директивы для препроцессирования. Поэтому не случайно многие сторонники языка используют при разработке программ только эти его возможности, т.е. работают всего лишь на "улучшенном" С,и лишь незначительная их часть пишут на действительно новом языке С++. 3
Для серьезного использования на практике новых по сравнению с С возможностей С++ необходимо,во-первых,технически освоить язык,а,во-вторых, глубоко разобраться в принципах объектно-ориентированного программирования и способах их реализации на языке С++. Все это требует и наличия полномасштабной справочной литературы,и хороших методических пособий,и наконец,солидной программной поддержки для автоматизированного обучения на персональных ЭВМ,оснащенных компилятором С++. Можно, конечно,достигнуть указанной цели,имея компьютер и компилятор С++,но это слишком дорогое удовольствие и доступно не каждому пользователю. Данное учебное пособие разрабатывалось с учетом указанных требований. Оно содержит разделы,где рассматриваются способы реализации принципов объектно-ориентированного программирования на языке С++:структуры с механизмами наследования,инкапсуляция данных и функций доступа к ним,полиморфизм операций,функций и объектов,а также дается фрагмент дидактического контекстно-свободного описания синтаксиса языка С++ с многочисленными примерами и краткими пояснениями. В приложении приведен текст обучающей программы для изучения аспектов объектноориентированного программирования. Пособие ориентируется на использование интеллектуальной обучающей системы,реализованной на персональных IBM-совместимых ЭВМ и имеющей большую и легко доступную информационную базу по языку С++,синтаксическую базу знаний,а также набор примеров для изучения семантики языка и контроля качества знаний. Заключение содержит перечень автоматизированных курсов,имеющихся в системе, и указания по их применению. Учебное пособие предназначено для самостоятельного изучения языка программирования С++ в учебных центрах и лабораториях, оснащенных персональными ЭВМ,и пригодно для различных категорий пользователей - от школьников и до преподавателей высших учебных заведений. 4
1.ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ И ЕГО ПОДДЕРЖКА В С++ 1.1.Реальный мир и объекты Цель науки - выработка понятий и категорий о реальном мире,а также их теоретическая систематизация в виде генеалогических деревьев по тем или иным областям знаний.При этом единственная общая категория предметной области составляет корень дерева,от которого веером расходятся ветви к подчиненным категориям различного уровня вплоть до простых неделимых понятий.Такие деревья существуют для насекомых,рыб,млекопитающих,пресмыкающихся, растений,химических элементов,внутриатомных частиц,звездных галактик и т.д.На рис.1 приведен фрагмент генеалогического дерева для злаков. ┌───────┐ │ Злаки │ └───┬───┘ │ ┌────────────────────┬─┴──────────────────────┐ ┌────┴─────┐
┌────┴─────┐
┌─────────┴─────────┐
│ Зерновые │
│ Кормовые │
│ Сахарный тростник │
└────┬─────┘
└────┬─────┘
└───────────────────┘
│
└───────────────────┐
┌─┴──────────┬───────────┐
┌─────┴──────┐
┌──┴───┐
┌────┴────┐
┌──┴───┐
┌───┴────┐
┌───┴────┐
│ Рожь │
│ Пшеница │
│ Овес │
│ Мятлик │
│ Житняк │
└──────┘
└─────────┘
└──────┘
└────────┘
└────────┘
Рис.1.Фрагмент генеалогического дерева Категория "злаки" включает три подкатегории:"зерновые злаки","кормовые злаки" и "сахарный тростник".В свою очередь,две первые подкатегории имеют множество своих подкатегорий. Такой процесс классификации называется систематикой.Вопросы, какие задают себе ученые в этом процессе,таковы: 5
а)насколько новый объект похож на объекты какого-либо друго класса? б)чем он отличается от объектов этого класса? Процесс идентификации нового объекта начинается с корня генеалогического дерева и идет по ветвям до определения местоположения этого объекта в рассматриваемой классификации. Важно отметить,что как только свойство некоторой подкатегории задано,все подчиненные подкатегории обладают этим свойством.Это так называемый механизм наследования.Каждая отдельная подкатегория - это совокупность свойств и поведенческих функций,унаследованных от более общих подкатегорий, вместе с видовыми свойствами и поведенческими функциями, присущими только этой подкатегории и подкатегориям,ей подчиненным,причем в унаследованных поведенческих функциях возможны некоторые вариации. Иерархическое строение реального мира также объективно, как и сам реальный мир.Поэтому его любая модель,претендующая на адекватное описание,должна удовлетворять следующим необходимым условиям: а)целостное описание
свойств объектов и их поведенчес-
ких функций; б)иерархическая структура описания системы объектов с встроенным механизмом наследования свойств и функций; в)полиморфизм описательных средств для функций,обеспечивающий передачу действий вверх и вниз по иерархии объектов с реализацией этого действия способом,соответствующим каждому объекту в иерархии. Осмысление этих фактов в области программирования произошло в середине 80-х годов и привело к созданию нового стиля проектирования сложных программных систем,получившего название объектно-ориентированного программирования.Это - естественная эволюция более ранних подходов в проектировании языков программирования:оно более структурировано,чем предыдущие попытки в структурном программировании;оно более модульно и абстрактно,чем предыдущие попытки в абстрагировании данных и сокрытии деталей.К настоящему времени разработано несколько языков программирования,которые поддерживают этот 6
стиль.К их числу относится также язык С++.Объекты реального мира в этом языке представляются в зависимости от их сложности структурами,объединениями и классами. Эта концепция достаточно сложна,и даже опытным программистам требуется трехмесячная переподготовка для овладения новым стилем мышления и программирования.Итак,начнем с рассмотрения простых примеров. 1.2.Структуры
с механизмами наследования
Объект реального мира,будь то материальный предмет или физическое явление, характеризуется прежде всего некоторой совокупностью данных.Это напоминает структурный тип языка С++,который служит "оболочкой" или "капсулой" для соединения нескольких связанных между собой элементов данных под одним именем. Так, для позиции на экране можно задать следующий структурный тип: struct Location /*-позиция:
*/
{ int x,y;/*-координаты точки экрана;
*/
};/*-конец определения позиции;
*/
Здесь Location является типом структура;это - шаблон,который используется компилятором
для создания переменных типа струк-
тура.Переменная типа Location является отдельным экземпляром этого типа.В программе может быть определено любое число переменных типа Location,или,как принято называть в языке C++,любое число объектов типа Location: Location Loc0_0={0,0}; Location Loc9_9={9,9}; Location Loc1,Loc2,LocCent; Тип Location можно рассматривать двумя способами.Когда бывают нужны отдельные координаты х или у,то используют отдельные поля структуры Location и записывают,например,так: Loc0_0.x и Loc0_0.y.Если необходимо указать точку с координатами х и у, 7
которая определяет положение объекта на экране,то ссылаются на всю структуру Location. Предположим,что нам необходимо
высветить точку в позиции
на экране,описанной структурой Location.Определим тип typedef enum{false,true} Boolean;/*-булевский тип;
*/
и добавим к структуре Location поле,указывающее,светится ли точка в данной позиции.Новую структуру назовем Point: struct Point /*-точка на экране в графическом режиме: { int x,y
/*-координаты точки на экране;
Boolean Visible;/*-видимость точки на экране; };/*-конец определения точки;
*/ */ */ */
Язык С++ позволяет это записать и в другой форме: struct Point /*-точка на экране в графическом режиме: { Location Position;/*-позиция точки на экране; Boolean
*/ */
Visible ;/*-видимость точки на экране;
};/*-конец определения точки;
*/ */
Написанные структуры допустимы и в языке Турбо Си, и мы здесь не добавили ничего нового.Такой способ манипулирования данными затушевывает сущность рассматриваемых вещей.Дадим следующее определение точки:точка - это позиция на экране,работающем в графическом режиме,которая может светиться.А теперь воспользуемся возможностями языка C++ и построим структуры в соответствии с данным определением. В определении точки используется позиция этой точки на экране.В объектно-ориентированном программировании используется именно эта взаимосвязь.Поскольку все точки должны содержать позицию на экране,говорят,что тип Point - это тип,порожденный типом Location.Point наследует все,как в генеалогическом дереве,что имеет Location,и добавляет нечто новое к Point,чтобы сделать Point тем,чем оно должно быть-светящейся позицией. Процесс,посредством которого один тип наследует свойства другого типа,называется наследованием,а средства,с помощью 8
которых это достигается,механизмом наследования.Наследник называется порожденным типом;тип,от которого наследует порожденный тип,называется родительским,или базовым типом. В нашем примере два типа объектов следует теперь определить следующем образом: struct Location /*-позиция:
*/
{ int x,y; /*-координаты точки экрана;
*/
};/*-конец определения позиции;
*/
typedef enum{false,true} Boolean;/*-булевский тип;
*/
struct Point:/-точка на экране в графическом режиме;
*/
Location /*-позиция;
*/
{ Boolean Visible;/*-видимость точки на экране;
*/
};/*-конец определения точки на экране;
*/
Здесь двоеточие используется для указания наследования.При этом Location является родительским,или базовым типом,а Point - порожденным,или производным типом.Рассмотренный процесс может продолжаться бесконечно:можно задать наследников типа Point,наследников типа,порожденного от Point и т.д.Следует отметить,что большая часть применений объектно-ориентированного программирования состоит в построении этой иерархии объектов,выражающей семейное дерево объектов. Все типы,порожденные от Location,называются Location-порожденными типами,а тип Point - один из непосредственных наследников Location.В свою очередь,Location - непосредственный родитель для Point.Любой тип может иметь любое число непосредственных наследников,но только одного непосредственного предка,если не используется множественное наследование.При множественном наследовании производный тип имеет несколько базовых типов,имена которых задаются в его определении с помощью списка.Конечно,корневой тип не имеет предка. Производный тип Point используется в языке С++ так же,как и любой другой тип: 9
Point VarPoint={0,0,true};/*-переменная с инициализацией; */ Point& RefPoint=VarPoint; /*-ссылка на Point;
*/
Point* PtrPoint=new Point;/*-указатель на Point;
*/
tipedef struct Point* PointPtr;/*-производный от Point тип*/ PointPtr DynPoint;/*-требуется использование операторов
*/
/* new для размещения и delete для уда- */ /* ления структуры Point;
*/
Доступ к полям данных объектов производится обычным способом: cout<<"\n"<<"x="<
Visible="<Visible= RefPoint.Visible; Следует особо подчеркнуть,что унаследованные поля доступны так же, как и поля,объявленные внутри данного типа объекта.Например,хотя х и у не являются частью объявления Point,но наследуются из типа Location,и поэтому их можно задавать,как будто они объявлены внутри Point. Хотя к полям рассматриваемых объектов имеется непосредственный доступ,поступать так,значит иметь потенциальную возможность их порчи в непредсказуемые моменты времени со всеми вытекающими отсюда последствиями.Основополагающая идея объектно-ориентированного программирования состоит в том, чтобы использовать для этих целей только специальные функции, включаемые в определение структуры и называемые методами объекта,а поля объекта защищать от воздействия каких-либо других функций или операций. 1.3.Инкапсуляция данных и функций доступа к ним Сначала рассмотрим,каким образом встроить в структуру функцию инициализации ее полей.При этом заметим,что использование 10
инициализатора структуры или операторов присваивания неприемлемо,так как здесь участвует лишь один экземпляр структуры.Для преоделения этого недостатка напишем функцию инициализации, которой любой экземпляр структуры передается как параметр: void InitPoint(Point& Target,int NewX,int NewY,/*-инициа- */ Boolean NewVisiable)/* лизация полей Point;*/ { Target.x
=NewX;
Target.y
=NewY;
Target.Visiable=NewVisiable; } /*-конец определения функции инициализации Point;
*/
Данная функция построена для обслуживания структуры Point, чтобы произвести инициализацию любого экземпляра этой структуры.Учитывая эти обстоятельства и стремясь к компактности,авторы языка С++ предложили следующие усовершенствования: 1)для функций инициализации объектов некоторого типа использовать имя этого типа ; 2)параметр для передачи имени экземпляра типа исключить из заголовка функции инициализации; 3)заменить в теле функции ссылку на экземпляр указателем this, который при использовании функции всегда устанавливается на нужный экземпляр типа; 4)не указывать тип возвращаемого значения. Функция инициализации экземпляров структуры Point теперь выглядит следующим образом: Point(int NewX,int NewY,Boolean NewVisible) { this->x
=NewX;
this->y
=NewY;
this->Visible=NewVisible; } /*-конец определения функции инициализации Point;
*/
Для большей компактности указатель на текущий экземпляр типа можно опускать.В языке С++ требуется перед именем функции, определяемой вне структуры,указывать имя той структуры,в тело которой она включается.Так что окончательно получаем: 11
Point::Point(int NewX,int NewY,Boolean NewVisible) { x
=NewX;
y
=NewY;
Visible=NewVisible; } /*-конец определения функции инициализации; */ Чтобы можно было пользоваться этой функцией для инициализации объектов типа Point,необходимо включить описание этой функции в само определение Point: struct Point: /*-точка на экране в графическом режиме: Location /*-позиция:
*/ */
{ Boolean Visible;/*-видимость точки на экране;
*/
Point(int NewX,int NewY,Boolean NewVisible); };/*-конец определения точки на экране;
*/
Приведем несколько примеров определения объектов типа Point с одновременной их инициализацией: Point VarPoint=Point(0,0,true);/*-полная запись;
*/
Point VarPoint Point(0,0,true);/*-сокращенная запись;
*/
Point VarPoint(0,0,true);/*-компактное определение объек-*/ /* та и его инициализация;такая */ /* форма наиболее употребительна*/ Point* PtrPoint=new Point(0,0,true);/*-выделяется место
*/
/* для объекта типа Point и происходит инициализа-*/ /* ция его полей;
*/
typedef struct Point* PointPtr; PointPtr DynPoint=new Point(0,0,true); Функцию инициализации можно определить и внутри структуры Point следующим образом: struct Point: /*-точка на экране в графическом режиме: Location /*-позиция;
*/ */
{ Boolean Visible;/*-видимость точки на экране;
*/
Point(int NewX,int NewY,Boolean NewVisible) { x=NewX;y=NewY;Visible=NewVisible; };/*-конец определения структуры Point; 12
} */
И в том,и в другом случае функция Point называется функцией-членом структуры Point. Она реализует определенный метод доступа к полям этой структуры.В общем случае, определение структуры в соответствии с принципами объектно-ориентированного программирования должно содержать описания или определения всех функций,которые так или иначе используют поля этой структуры.Таким образом,и функции,и данные должны быть вместе во время проектирования программ.Когда функции и данные существуют отдельно,всегда есть опасность вызова правильной функции с неверными данными или неверной функции с правильными данными. Интеграция определения данных и функций,обеспечивающих доступ к этим данным,в одной структуре-капсуле называется инкапсуляцией данных.Инкапсуляция данных-это дисциплина,которой должен следовать программист,проектирующий надежную программную систему. Функция Point имеет специальное название - конструктор объекта.Для нее не надо задавать тип возвращаемого значения (в данном случае слово void было бы излишним).Расширим набор функций-членов для структуры Point: struct Point: /*-точка на экране в графическом режиме: Location /*-позиция; { Boolean Visible;/*-видимость точки на экране;
*/ */ */
Point(int x,int y, Boolean Visible) { this->x=x; this->y=y; this->Visible=Visible; } int GetX() { return x; } /*-возвращает абсциссу; */ int GetY() { return y; } /*-возвращает ординату; */ void PrintPoint() { cout<<"\n"<<"x="<<x<<" "<<"y="<
структуры Point;
*/
Как и функцию Point,новые функции можно определить вне структуры либо до ее определения,либо после.В первом случае необходимо определениям функций предпослать описание структуры: struct Point; Разделенные физически в исходной программе тела функцийчленов(тела методов) и поля данных объекта на самом деле используют одну и ту же область действия.Этим объясняется тот факт,что в операторе return x функции GetX() переменная x записана без указателя объекта this.Это происходит потому, что x принадлежит тому же объекту, что и эта функция: struct Point VarPoint(0,0,true);/*-объект VarPoint;
*/
cout<<"\n"<
полей данных и объединения их с фун-
кциями доступа в язык C++ введен новый тип данных - класс,в котором,как правило,присутствуют две части:защищенная часть и открытая часть,причем как в той,так и в другой части могут быть определены как поля данных,так и функции,использующие 14
эти поля.Открытая часть класса определяется после защищенной (закрытой) части и начинается с ключевого слова public,за которым следует двоеточие.Обычно в открытой части записываются только функции доступа.Порожденный класс получает доступ к открытой части базового класса только после указания в его определении слова public перед именем базового класса.Другими словами,если это слово не задано,то порожденный класс лишается какой бы то ни было возможности обращаться к полям данных или использовать функции доступа базового класса.В этом случае,доступ к полям и функциям базового класса обеспечивается специальной идентификацией,когда к основному имени добавляется префикс - имя базового класса и два двоеточия. В рассмотренном примере был еще один недостаток,нарушающий дисциплину объектно-ориентированного программирования:конструктор порожденного класса Point производил инициализацию полей базового класса.В языке C++ имеется для этих целей специальный вызов конструктора базового класса в определении конструктора производного класса.Это обеспечивает гибкость программы,так как изменения в способе инициализации полей базового класса никак не отразятся на производных классах,которых может быть несколько. С учетом сделанных замечаний рассматриваемый пример приобретает следующий вид: class Location /*-позиция:
*/
{ int x,y;/*-координаты точки экрана; public: /*-открытая часть класса: Location(int x,int y) /*-конструктор класса;
*/ */ */
{ this->x=x;
/*-инициализация абсциссы;
*/
this->y=y;
/*-инициализация ординаты;
*/
} ~Location() {} /*-пустой деструктор класса;
*/
int GetX() { return x; } /*-возвращает абсциссу;
*/
int GetY() { return y; } /*-возвращает ординату;
*/
void Print()/*-печать координат точек экрана; { cout<<"\nx="<<x;/*-печать абсциссы; cout<<"
y="<
*/ */ */
} };/*-конец определения класса позиция; 15
*/
typedef enum{false,true} Boolean;/*-булевский тип;
*/
class Point: /*-точка на экране в графическом режиме:
*/
public Location /*-базовый класс позиция;
*/
{ Boolean Visible;/*-видимость точки на экране;
*/
public: /*-открытая часть класса;
*/
Point(int x,int y,Boolean Visible):/*конструктор; */ Location(x,y)/*-вызов базового конструктора; */ { this->Visible=Visible;/*-инициализация поля*/ }
/* класса Point;
~Point() { } /*-пустой деструктор класса;
*/ */
Boolean /*-состояние видимости точки на экране;
*/
GetVisible() { return Visible; } void Print() /*-печать координат и состояния точки:
*/
{ Location::Print();/*-печать координат; cout<<"
*/
Visible="<
} };/*-конец определения класса точка;
*/
В производном классе Point функция Print переопределяется. Для доступа к функции Print базового класса используется префикс Location :: . Вернемся опять к структурам.Оказывается,что и в структурах можно получить тот же самый результат,используя спецификатор private(личный).По определению структура - это просто класс, все члены которого открыты,т.е. struct Location { ... }; есть просто сокращенная запись для struct Location { public:... }; Структуры используются в тех случаях,когда не требуется защита данных.Посмотрим,тем не менее,как выглядит рассматриваемый пример,когда в нем используются структуры и спецификатор доступа private: 16
struct Location /*-позиция:
*/
{private:int x,y;/*-координаты точки экрана; public: /*-открытая часть структуры:
*/ */
Location(int x,int y) /*-конструктор структуры;
*/
{this->x=x;
/*-инициализация абсциссы;
*/
this->y=y;
/*-инициализация ординаты;
*/
} ~Location() {} /*-пустой деструктор структуры;
*/
int GetX() { return x; } /*-возвращает абсциссу;
*/
int GetY() { return y; } /*-возвращает ординату;
*/
void Print()/*-печать координат точек экрана: { cout<<"\nx="<<x;/*-печать абсциссы; cout<<"
y="<
*/ */ */
} };/*-конец определения структуры Location;
*/
typedef enum{false,true} Boolean;/*-булевский тип;
*/
struct Point: /*-точка на экране в графическом режиме:
*/
Location /*-базовая структура позиции;
*/
{ private:Boolean Visible;/*-видимость точки;
*/
public: /*-открытая часть структуры:
*/
Point(int x,int y,Boolean Visible):/*-конструктор;*/ Location(x,y)/*-вызов базового конструктора; */ { this->Visible=Visble;/*-инициализация по-
*/
}
*/
/* ля структуры;
~Point() {} /*-пустой деструктор структуры; Boolean /*-состояние видимости точки на экране:
*/ */
GetVisible() { return Visible; } void Print() /*-печать координат и состояния точки: { Location::Print();/*-печать координат; cout<<"
*/ */
Visible="<
}
/* стояние;*/
};/*-конец определения структуры Point; В примере сделаны следующие преобразования: а)ключевое слово class заменено на ключевое слово struct; 17
*/
б)в определении производного класса Point удалено ключевое слово public,так как для структуры такой спецификатор предполагается по умолчанию; в)для защиты полей структур включен спецификатор private. Именованное объединение union определяется как структура, в которой все члены имеют один и тот же адрес,т.е.налагаются друг на друга,что в ряде случаев ведет к экономии памяти. Как и в структуре,все члены объединения открыты,однако они не могут быть переопределены спецификатором private или каким-либо другим способом.Таким образом,возможности объединений по организации иерархии наследуемых типов весьма ограничены. Помимо спецификаторов доступа private и public,в реализации языка С++ имеется еще спецификатор protected(защищенный),который по сравнению с private разрешает членам производного класса использовать соответствующие члены базового класса.Однако ни объектам базового класса,ни объектам производного класса доступ к указанным членам невозможен.Таким образом, спецификатор protected выполняет служебную функцию при создании производных классов,расширяя их права доступа к членам базового класса. Систематизируем и дополним сведения по организации защиты и доступа к тем или иным компонентам иерархического представления предметной области,получаемого
на языке
С++.
Как было показано на примерах,основными компонентами такого представления являются составные типы:класс,структура и объединение,а также множество объектов указанных типов.Определение составного типа включает ряд описаний или определений своих непосредственных составляющих или членов.Каждый объект такого типа располагает всеми его членами,но не все члены объекта доступны программисту.Как известно,этот доступ регулируется спецификаторами доступа private,protected и public,задаваемыми по умолчанию или явно.В табл. 1 приведены способы задания спецификаторов доступа для составных типов языка С++. 18
Таблица 1 Способы задания спецификаторов доступа ┌─────────────┬────────────────────┬────────────────────────┐ │Составной тип│Задание по умолчанию│
Явное задание
│
├─────────────┼────────────────────┼────────────────────────┤ │ Класс
│
Private
│Private,protected,public│
├─────────────┼────────────────────┼────────────────────────┤ │ Структура
│
Public
│Private,protected,public│
├─────────────┼────────────────────┼────────────────────────┤ │ Объединение │
Public
│Public
│
└─────────────┴────────────────────┴────────────────────────┘ Имеется два случая использования спецификаторов доступа: 1)в определении составного типа перед одним из его членов: тогда он отделяется от члена двоеточием и его действие распространяется на все последующие члены,начиная с члена,перед которым он указан,и кончая последним членом типа или членом, перед которым задан новый спецификатор;спецификатор по умолчанию действует до первого явного задания нового спецификатора; 2)в определении производного типа:тогда ему предшествует двоеточие,а за ним следует имя базового типа;если в этом месте спецификатор не задан ,то действует спецификатор по умолчанию;когда производный тип определяется через список базовых типов(множественное наследование),то элементы списка разделяются запятой и перед именем соответствующего базового типа,если необходимо,задается требуемый спецификатор доступа. В табл. 2 дано уточнение смысла спецификаторов доступа для различных случаев их использования,причем для первого случая использования употреблен термин спецификатор доступа члена,а для второго случая - спецификатор доступа типа. 19
Таблица 2 Доступность членов составного типа ┌──────────────┬──────────────────────┬──────────────────────┐ │
\Специфи- │
│
│ Спе- \ катор │
Private
│
│
Public
│
│ цифика-\ ти- │
│
│
│ тор члена\па │
│
│
├──────────────┼──────────────────────┴──────────────────────┤ │
│
1.Доступен всем членам базового типа
│
│
│
2.Недоступен в объектах базового типа
│
│
3.Недоступен членам производного типа
│
│
│
4.Недоступен в объектах производного
│
│
│
│
Private
типа
│
├──────────────┼──────────────────────┬──────────────────────┤ │
│1.Доступен всем чле-
│1.Доступен всем чле-
│
│
│
│
│
│
│2.Недоступен в объек- │2.Недоступен в объек- │
│
│
│
│3.Доступен всем чле-
│
Protected
нам базового типа тах базового типа
│
нам базового типа тах базового типа
│3.Доступен всем чле-
│ │
│
нам производного ти-│
нам производного ти-│
│
│
па,но уже со специ- │
па с тем же специфи-│
│
│
фикатором private
катором protected
│
│
│4.Недоступен в объ-
│4.Недоступен в объ-
│
│
│
ектах производного
│
ектах производного
│
│
│
типа
│
типа
│
│
├──────────────┼──────────────────────┼──────────────────────┤ │
│1.Доступен всем чле-
│1.Доступен всем чле-
│
│
│
│
│
│
│2.Доступен в объектах │2.Недоступен в объек- │
│
│
│
│3.Доступен всем чле-
│
Public
нам базового типа базового типа
│
нам базового типа тах базового типа
│3.Доступен всем чле-
│ │
│
нам производного ти-│
нам производного ти-│
│
│
па,но уже со специ- │
па с тем же специфи-│
│
│
фикатором private
катором public
│
│4.Недоступен в объ-
│4.Доступен в объектах │
│
│
ектах производного
│
│
│
типа
│
│
производного типа
│ │ │
└──────────────┴──────────────────────┴──────────────────────┘ 20
Из табл. 2 видно,что в отношении производных типов спецификатор доступа члена базового типа определяет лишь потенциальный статус этого члена.Его фактический статус в производном типе зависит от спецификатора доступа типа,указываемого в заголовке производного типа,и определяется следующим образом.Если тип объявляется базовым для некоторого другого со спецификатором типа public,то личные члены базового типа становятся личными членами производного типа,а защищенные члены базового типа - защищенными членами производного типа.Если же тип объявляется для некоторого другого типа базовым со спецификатором доступа private,то личные и защищенные члены базового типа становятся личными членами производного типа. Расмотренная система разграничения доступа к членам составных типов легко может быть нарушена путем включения в определение этого типа нового члена,являющегося описанием(или даже определением) некоторой внешней функции с ключевым словом friend(друг).Функция-друг не имеет никаких особенностей, помимо права доступа к любой,в том числе закрытой части типа.В частности,функция-друг не имеет указателя this(если только она не является полноправным членом типа).Описание функции-друга вводит имя функции в самой внешней области видимости программы и сопоставляется с другими описаниями или определением этого имени.Описание друга может располагаться или в закрытой,или в открытой части описания класса:где именно,значения не имеет. Необходимость использования таких функций определяется тем,что в языке С++ одна и та же функция не может быть одновременно членом двух и более классов,как это требуется,например,в следующем варианте умножения классов вектор и матрица: class matrix;/*-предварительное описание класса матрицы;
*/
class vector /*-определение класса вектор:
*/
{ float v[4]; float operator[](int i) { return v[i]; } /* Необходимые функции члена класса вектор;
*/
friend vector multiply(matrix&,vector&); };/*-конец определения класса вектор; 21
*/
class matrix /*-определение класса матрица через вектор;
*/
{ vector v[4]; /* Необходимые функции члены класса матрица;
*/
friend vector multiply(matrix&,vector&); };/*-конец определения класса матрица;
*/
В оба класса здесь введена одна и та же функция-друг multiply,выполняющая следующие действия: vector multiply(matrix& m,vector& v) /*-внешняя функция; { vector r; /*-возращаемый вектор результата;
*/ */
for(int i=0;i<=3;i++) {r.v[i]=0;/*-предполагается,что в классе век- */ /* тор определена операция[]; for (int j=0;j<=3;j++) /*-строка на столбец;
*/ */
r.v[i]+=m.v[i][j]*v.v[j]; } /*-конец цикла по элементам вектора;
*/
return r; };/*-конец определения внешней функции умножения /* матрицы на вектор;
*/ */
Если все функции одного класса являются друзьями другого, то используется такая краткая запись: class x { friend class y; ... }; Рассмотренный способ включения внешних функций в определение составного типа позволяет по иному построить иерархию составных типов,когда все функции,необходимые для доступа к полям типов,являются внешними и включаются в соответствующие типы как функции-друзья.Достоинства и недостатки такого подхода рассматриваются в [1]. Поскольку класс является в языке C++ основным средством реализации принципов объектно-ориентированного программирования, перечислим виды членов,используемых для его определения: а)определения констант и переменных основных типов:char,int, float и т.д.; 22
б)описания и определения любых производных типов:указателей, ссылок,векторов,составных типов(структур,объединений и классов),функций,при этом описания или определения таких типов,как класс или функция рассматриваются внешними); в)описания или определения констант и переменных (объектов) типа указатель,ссылка,вектор,структура,объединение и класс; г)определения перечислимых типов и объектов этих типов; д)описания или определения функций-членов данного класса; е)описания или определения функций-друзей составных типов; ж)описания или определения конструкторов данного класса; з)описание или определение деструктора класса. Конструкторы и деструктор классов определяют,каким образом создаются,инициализируются,копируются и уничтожаются объекты этого класса.Пример конструктора для структуры и класса Point был уже рассмотрен;было дано также подробное объяснение принципов его построения.Там же без пояснений приведены примеры других конструкторов,записаны образцы часто встречающихся пустых конструкторов.Имена конструкторов и деструкторов совпадают с именами соответствующих определяемых классов,при этом перед именем деструктора ставится знак тильда(~).Как конструкторы класса,так и его деструктор являются функциями-членами класса.Они могут определяться внутри класса или иметь внешнее определение,тогда внутри класса необходимо дать их описания. Если некоторый класс имеет один или несколько конструкторов,то при определении любого объекта этого класса должен вызываться явно или неявно один из этих конструкторов.Явный вызов конструктора записывается сразу же за именем объекта в определении и отделяется от этого имени либо пробелом,либо знаком равно.Неявный вызов имеет место,когда за именем объекта в определении в круглых скобках задан список аргументов требуемого конструктора.По числу и типу аргументов производится поиск нужного конструктора и осуществляется его вызов.Если при неявном вызове используется конструктор с пустым списком параметров,то список аргументов не указывается.Следует помнить,что в этом случае при определении объекта автоматически вызывается конструктор с пустым списком параметров. 23
Деструкторы удаляют создаваемые конструкторами объекты.Они вызываются программистом или автоматически ,когда рассматриваемый объект прекращает свое существование(выход из области действия). Конструкторы и деструкторы являются членами класса,и на них распространяются многие правила для функций-членов класса.Однако,существует ряд специфических правил: 1.Для них не надо указывать тип возвращаемого значения,в том числе и тип void. 2.Они не могут наследоваться,хотя производный класс может вызывать как конструкторы,так и деструкторы базового класса. 3.Конструкторы,как и большинство функций языка С++,могут иметь аргументы по умолчанию или использовать список инициализации параметров. 4.Конструкторы никогда не бывают виртуальными,а деструкторы могут быть,если необходимо,и виртуальными. 5.В программе не разрешается использовать указатели на конструкторы или деструкторы. 6.Конструкторы и деструкторы могут иногда генерироваться автоматически,если они не определены явно в классе;очень часто генерируются автоматически также и их вызовы без явного указания со стороны программиста.При этом конструктор или деструктор,сгенерированный компилятором,является открытым(public). 7.Нельзя вызывать конструктор как обычную функцию;конструктор вызывается только при определении объекта или при его создании с помощью операции new,аргументом которой является вызов соответствующего конструктора. 8.При вызове деструктора требуется указывать его полное имя,включающее имя класса: class x* p; .......... p->x::~x();/*-правильный вызов деструктора класса x;
*/
x::x();
/*-конструктор нельзя вызывать как обычную
*/
/* функцию;
*/ 24
9.Компилятор автоматически вызывает конструкторы при определении объектов,а деструкторы - при их удалении. 10.Конструкторы и деструкторы могут неявно вызывать соответственно операции new и delete,когда для объекта требуется выделение памяти. 11.Объект с конструктором или деструктором никогда не может быть членом объединения из-за сложностей,возникающих при организации использования общей области памяти,выделенной для членов объединения. 1.4.Полиморфизм операций,функций и составных объектов 1.4.1.Полиморфизм операций В языке С++ имеется специальный аппарат для перегрузки(overload) операций,который позволяет программисту задавать требуемый смысл для имеющихся в языке операций при их применении к объектам вновь определяемых классов.Тем самым обеспечивается полиморфизм операций языка.Кроме арифметических,можно определять еще и логические операции,операции сравнения, вызова () и индексирования [], а также можно переопределять присвоение и инициализацию,определять явное и неявное преобразование между типами,определяемыми программистом и основными. Ранее в примерах использовалась операция вывода << в выходной поток cout, являющаяся переопределением операции сдвига влево.Это переопределение сделано в стандартном заголовочном файле с именем stream.h.В этом же файле перегружена еще одна операция - операция сдвига вправо >> для дополнения ее новым смыслом как вывод в стандартный поток вывода cin.Расширение семантики операций позволяет программисту обеспечить более общепринятую и удобную запись для манипулирования объектами классов,чем та, которую можно достичь,используя лишь основную функциональную запись. Обозначим через @ переопределяемую операцию,которой может быть любая операция языка С++. + - * / % ^ &: ~ ! = < > += -= *= /= %= ^= &=:= << >> >>= <<= == != <= >= &&:: ++ -- [] () new delete 25
Для любой операции @ можно лишь задать новую семантику.Изменить ее приоритет или арность,т.е.синтаксис выражений,невозможно.Невозможно также задать различную семантику для префиксной и постфиксной форм записи операций ++ и --,т.е.не имеет значения,как записывать ++x или x++,--x или x--. Таким образом,при необходимости одна и та же операция @ помимо основной,предопределенной в языке семантики,может иметь в программе или даже в иерархии составных типов несколько дополнительных семантик.Вывод требуемой семантики в каждой конкретной точке программы осуществляется по типу используемых операндов рассматриваемой операции,причем в ряде случаев могут иметь место автоматические преобразования типов операндов.Такое преобразование типов может иметь место,когда компилятор не нашел операции с заданными типами операндов.Автоматически могут выполняться как стандартные преобразования типов,так и преобразования типов,определяемые программистом,причем первым отдается предпочтение - они выполняются в первую очередь,а для вторых разрешен только один уровень.Разумеется,программист может явно использовать любые стандартные и собственные преобразования. Для задания новой семантики операции @ программист должен выполнить следующие действия: 1.Включить в соответствующий составной тип описание функции с именем operator@. 2.Включить в этот же тип описания функций для выполнения необходимых преобразований типов. 3.Дать внешнее определение для функции operator@. 4.Дать внешние определения для всех функций преобразований типов. Если определения функций занимают мало места,то их можно включать непосредственно в составной тип,замещая описания этих функций. Рассмотрим пример простой реализации понятия комплексного числа,в котором число представляется парой чисел с плавающей точкой двойной точности: 26
class complex /*-реализация понятия комплексного числа: { double re, im;
*/
/*-действительная часть числа;
*/
/*-мнимая часть числа;
*/
public: complex(double r) { re=r;im=0; }
/*-конструк- */
complex(double r,double i) { re=r;im=i; } /* торы;*/ complex operator-() { return complex(-re,-im); } complex operator~() { return complex(re,-im); } friend complex operator+(complex a,complex b) { return complex(a.re+b.re,a.im+b.im); } friend complex operator*(complex a,complex b) { return complex(a.re*b.re-a.im*b.im, a.re*b.im+a.im*b.re); } ~complex() { }
/*-пустой деструктор класса;
*/
double GetRe() { return re; } double GetIm() { return im; } };/*-конец определения класса комплексных чисел;
*/
#include /*-стандартные классы ввода-вывода; */ void main(void)
/*-примеры использования операций:
*/
{ complex a(1,1),b(2,2),c(3,3),d(4,4),e(5,5),d(6); a=2*(a+c); b=d+~e+10.5; c=(-a+c)*e; cout<<"\nc.re="<
c.im="<
/*-конец программы работы с комплексными числами; */
В этих примерах целые константы сначала автоматически преобразуются к типу double,а затем вызывается соответствующий конструктор complex.Для константы с плавающей точкой 10.5 автоматически вызывается конструктор complex(double r),который создает временный объект класса complex,удаляемый из памяти сразу же после выполнения операции над этим объектом.Таким образом,конструкторы также выполняют преобразования типов операндов. Бинарная операция в составном типе может быть определена или как функция-член,получающая один параметр(другим парамет27
ром является неявный указатель this),или как функция-друг,получающая два параметра.Таким образом,для любой бинарной операции @ выражение aa@bb интерпретируется в первом случае как aa.operator@(bb),а во втором случае как operator@(aa,bb).Аналогично префиксная или постфиксная унарная операция может быть определена или как функция-член,не получающая явных параметров,или как функция-друг,получающая один параметр.Таким образом,для любой унарной операции @ aa@(или @aa) интерпретируется в первом случае как aa.operator@(),а во втором случае как operator@(aa).При переопределении унарных операций префиксная операция остается префиксной,а постфиксная операция - постфиксной.Заметим,что любой вариант задания новой семантики для бинарных и унарных операций не изменяет общепринятую форму записи выражений.Это является достоинством выбранного в языке С++ подхода по расширению его операционных возможностей. Приведем несколько рекомендаций по выбору вида определяемой функции: 1.Функция для операции,изменяющей состояние объекта(=,+=,++ и т.п.) должна быть функцией-членом,а не функцией-другом. 2.Если нужно иметь неявное преобразование для всех операндов операции,то реализующая ее фукция должна быть другом,а не членом. 3.Если никакие преобразования не определены, то реализующая функция может быть как членом,так и другом.Однако и в этом случае рекомендуется использовать функцию-член,так как в противном случае при включении функции для преобразования типов может быть нарушена исходная семантика операции.Это включение может иметь место при дальнейшей модификации программы пользователем. Заметим,что в языке С++ для составных типов (структура,объединение и класс) имеется только четыре операции с предопределенным смыслом:присваивание =,адрес объекта &, создание объекта new и уничтожение объекта delete.Разумеется,что и этим операциям программист может задать новую семантику.Особенно часто такая необходимость возникает для операции присваивания,поскольку выполняемая при этом побитовая копия объекта того же типа может привести к непредсказуемым разрушениям в 28
программе и к зависанию системы.А если учесть,что эта операция используется и для инициализации объекта при его определении,когда вызывается
конструктор по умолчанияю для такого
же побитового копирования составного объекта,то последствия от неправильного использования операции "=" могут быть еще более разрушительными.Чтобы избежать всего этого,необходимо: 1)для рассматриваемого составного типа Х ввести конструктор вида Х(Х&), осуществляющий построение нового объекта по ссылке на уже созданный;лишь в частном случае,это построение может свестись к побитовому копированию,когда такое копирование допустимо; 2)для операции присваивания в составном типе вводят функцию,которая задает этой операции новую семантику;очень
час-
то определяемое присваивание сначала удаляет объект слева от знака присваивания,а затем заменяет его копией объекта справа от этого знака. Приведем несколько примеров. struct string /*-строка символов с указателем ее длины; { char* p; int
/*-указатель на строку символов;
size;/*-число символов в строке;
*/ */ */
string(int sz) {p=new char[size=sz];}/*-констру-*/ ~string() {delete p;}/* ктор и деструктор; };/*-конец определения структуры строка; void asgerr1(void)/*-ошибка в операции присваивания:
*/ */ */
{ string s1(10);/*-строка из 10 символов;
*/
string s2(20);/*-строка из 20 символов;
*/
s1=s2;/*-побитовое копирование структуры s1 и s2;
*/
}/*-конец определения 1-го примера с ошибкой;
*/
Введем в определение структуры собственнную операцию присваивания: struсt string /*-строка символов с указателем ее длины; { char* p;
/*-указатель на строку символов; 29
*/ */
int
size; /*-число символов в строке;
*/
string(int sz) {p=new char[size=sz];}/*-констру- */ ~string() {delete p; }/* ктор и деструктор;
*/
void operator=(string&)/*-операция присваивания; */ };/*-конец определения структуры с операцией =;
*/
void string::operator=(string& a)/*-присваивание строки; */ { if(this==&a) return;/*-переприсваивание исключается*/ delete p; p=new char[size=a.size]; strcpy(p,a.p); } /*-конец определения операции присваивания;
*/
В примере asgerr1 присваивание копированием s1=s2 изменяло указатель p на строку символов в объекте s1.После присваивания указатели p в обоих объектах s1 и s2 указывают на одну и ту же строку из 20 символов.По завершении asgerr1 автоматически дважды вызывается деструктор ~string() соответственно для объекта s1 и s2 и дважды удаляется одна и та же строка с непредсказуемо разрушительными последствиями. void asgerr2(void) /*-ошибка при инициализации; { string s1(10);/*-правильная структура;
*/ */
string s2=s1 ;/*-побитовое копирование s2 в s1; } /*-конец определения 2-го примера с ошибкой;
*/ */
Здесь создается только одна строка,а уничтожаются две.Введем в структуру конструктор вида: void string::string(string& a)/*-конструктор копирования;*/ { p=new char[size=a.size]; strcpy(p,a.p); }/*-конец конструктора для инициализации по объекту;*/ Теперь инициализация и присваивание выполняются правильно: 30
void init_asq() /*-инициализация и присваивание;
*/
{ string s1(10),s2(s1),s3(30); s3=s2 }/*-конец примера с правильной инициализацией и =;
*/
При перегрузке некоторых операций следует учитывать ряд особенностей,обусловленных спецификой этих операций.Особенность рассмотренной операции присваивания состоит в том,что она не может наследоваться производными типами и не может быть статической операцией для рассматриваемого типа.Особенности операций вызова функции,индексации и доступа к члену класса смотрите в [2]. 1.4.2.Полиморфизм функций. Полиморфизм функций - это задание одного имени действию,которое варьируется для одного объекта или передается вверх и вниз по иерархии объектов с реализацией этого действия способом,соответствующим каждому объекту в иерархии.Полиморфные функции в языке С++,как и полиморфные операции,называются перегруженными(overload).В описаниях или определениях таких функций перед их именем, как правило, ставится спецификатор overload. Полиморфизм функций в языке С++ поддерживается следующими механизмами: а)функции,определяющие сходные по содержанию действия,могут иметь одинаковые имена,если различны их наборы параметров:каждая пара наборов параметров отличается друг от друга либо числом параметров,либо типом хотя бы одного параметра при одинаковом их числе; б)для функций-членов составного типа также можно использовать одно и то же имя,когда эти функции определяют сходные по содержанию действия и имеют различие в наборах параметров; в)функции,определенные в некотором составном типе,наследуются вверх по иерархии производными типами и при необходимости переопределяются в производных типах с теми же или новыми наборами параметров,причем после переопределения наследуется уже функция с новым содержанием вверх по иерархии 31
типов,начиная от места переопределения этой функции;такой механизм называется статическим, или статическим методом доступа,так как поиск вызываемых в программе функций в иерархии типов производится статически, т.е. на стадии компиляции; г)функции,определяемые в иерархии составных типов со словом virtual(виртуальный),имеют одно и то же имя и один и тот же набор параметров,а вариация действия,специфицируемого каждой функцией,зависит от места ее определения в иерархии; этот механизм называется динамическим,или виртуальным методом доступа,так как поиск вызываемых в программе функций в иерархии производных типов производится динамически,т.е. на стадии выполнения программы. Различные функции обычно имеют разные имена,но функциям,выполняющим сходные действия и имеющим различные наборы параметров,иногда лучше дать возможность иметь одинаковые имена. Используя различия в наборах параметров,компилятор всегда может их идентифицировать и выбирать для вызова нужную функцию.Можно,например,написать одну функцию возведения в степень для различных сочетаний типов основания и показателя степени: overload pow;/*-е-возводимое в степень число:р-показатель:*/ int pow(int e,int p); double pow(double e,int p); double pow(double e,double p); x=pow(11,5);y=pow(12.6,7);y=pow(12.3,4.5);/*-вызовы pow;
*/
Описание overload pow сообщает компилятору,что использование имени pow более,чем для одной функции,является умышленным. Такой полиморфизм функций улучшает выразительные возможности языка С++,но непосредственно не связан с объектно-ориентированным программированием,где используется полиморфизм методов доступа для объектов составного типа:структур,объединений и классов. Хорошим примером полиморфных функций для составного объекта является набор конструкторов составного типа,когда зада32
ются способы конструирования объектов по различным исходным данным.Вот довольно реалистичный класс строк string.В нем производится учет ссылок на строку с целью минимизировать копирование полей данных.Класс имеет три конструктора и две операции присваивания,учитывающие разнообразие типов данных: class string /*-класс объектов из строк и счетчиков: { struct srep /*-представление строки данных: { char* s;/*-указатель строки данных; int
n;/*-счетчик числа ссылок строки;
};/*-конец определения типа структуры;
*/ */ */ */ */
srep* p;/*-указатель на представляющую структуру; */ public: string();
/*-конструктор с пустыми данными; */
string(char*);
/*-конструктор по строке данных;
*/
string(string&);/*-конструктор по объекту string; */ string& operator=(char*);
/*-присваивание данных;*/
string& operator=(string&);/*-присваивание объекта*/ ~string();
/*-деструктор класса;
}; /*-конец определения класса объектов string; string::string() /*-конструктор с пустыми данными;
*/ */ */
{ p=new srep;/*-выделение места для s и n;
*/
p->s=0;/*-нулевой указатель(нет данных);
*/
p->n=1;/*-данный объект ссылается на s;
*/
} /*-конец определения пустого конструктора;*/ string::string(char*s)/*-конструктор по строке данных; { p=new srep;/*-выделение места для s и n;
*/ */
p->s=new char[strlen(s)+1];/*-для данных; */ strcpy(p->s,s);/*-копирование данных;
*/
p->n=1;/*-данный объект ссылается на s;
*/
} /*-конец определения конструктора char*; string::string(string& x) /*-конструктор-копия объекта; { x.p->n++;/*-учет ссылок в объекте x;
*/ */ */
p=x.p;/*-новый объект - это ссылка на па- */ /* ру: данные - счетчик объекта x;
*/
} /*-конец определения конструктора копии;
*/
33
string::~string() /*-деструктор класса для всех вариантов */ /*-конструирования объектов;
*/
{ if(--p->n==0)/*-когда была одна ссылка:
*/
{ delete p->s;/*-удаление поля данных; */ delete p;/*-удаление пары s и n;
*/
} } /*-конец определения деструктора string;*/ Заметим,что здесь p->s может быть равен нулю,тогда операцией delete ничего не удаляется,что как раз и необходимо для пустого объекта. string& string::operator=(char*s)/*-присваивается строка /* символов;
*/ */
{ if(p->n>1)/*-на объект ссылаются другие объекты;*/ { p->n--;
/*-снятие с учета данного объекта*/
p=new srep;/*-конструирование объекта зано- */ }
/* во;
*/
else /*-на объект других ссылок нет,кроме своей;*/ if(p->n==1)/*-своя единственная ссылка:
*/
delete p->s;/*-удаление старого поля данных;*/ p->s=new char[strlen(s)+1];/*-новое поле данных */ strcpy(p->s,s);/*-копирование правой части;
*/
p->n=1;/*-ссылка самого объекта на поле данных; */ return *this;/*-возврат ссылки на объект слева; */ } /*-конец присваивания объекту строки символов;
*/
string& string::operator=(string& x)/*-присваивание одно- */ /* го объекта другому */ { x,p->n++;/*-учет новой ссылки в объекте справа; */ if(--p->n==0)/*-уменьшение счетчика ссылок слева*/ { delete p->s;/*-удаление строки данных и уда-*/ delete p;/* ление объекта слева,если ссылок */ /* на строку больше нет;
*/
} p=x.p;/*-объект слева ссылается на объект спра-*/ /* ва;
*/
return *this;/*-возврат ссылки на объект слева;*/ } /*-конец присваивания объекту другого объекта; 34
*/
Когда полиморфные функции определены статически по иерархии составных типов,поиск нужной функции при вызове производится компилятором в следующем порядке: 1.При вызове функции компилятор устанавливает тип объекта, для которого вызывается эта функция. 2.Установив тип,компилятор ищет функцию в пределах этого типа и, найдя ее здесь,компилятор назначает вызов этой функции. 3.Если указанная функция не найдена,то компилятор начинает рассматривать его базовый тип и ищет в нем функцию,имя которой указано в вызове.В случае,если функция с таким именем найдена и типы аргументов и параметров отождествляются,то найденная функция вызывается. 4.Если же искомая функция отсутствует в базовом типе,то компилятор переходит к следующему по иерархии базовому типу, где и осуществляет дальнейший поиск.Процесс продолжается до тех пор,пока вызванная функция не будет найдена,иначе компилятор выдает сообщение об ошибке. 5.Если функция,найденная в рассматриваемой иерархии типов, вызывает другие функции иерархии,то поиск этих функций будет начинаться с места определения указанной функции в направлении к родительским типам,даже если потомки имеют собственные функции с такими же именами и параметрами. Приведем примеры использования статического полиморфизма функций: class Location /*-базовый класс позиция: { int x,y;/*-координаты точки экрана; public: /*-открытая часть класса:
*/ */ */
Location(int InitX,int InitY) /*-конструктор точки*/ { x=InitX; /*-инициализация абсциссы;
*/
y=InitY; /*-инициализация ординаты;
*/
} /*-конец определения конструктора точки;*/ ~Location() { } /*-пустой деструктор класса позиция;*/ int GetX() { return x; } /*-возвращает абсциссу;
*/
int GetY() { return y; } /*-возвращает ординату;
*/
void Print(){ cout<<"\nLocation";} /*-печать имени класса; };/*-конец определения базового класса позиция; 35
*/ */
class SymbolLocation:/*-производный класс символ на экране*/ public Location /*-доступ к открытой /* части Location; { char Symbol;/*-поле со значением символа экрана; public:
*/ */ */
/*-открытая часть производного класса: */
SymbolLocation(int InitX,int InitY,char InitSymbol): Location(InitX,InitY){ Symbol=InitSymbol; } ~SymbolLocation() { }
/*-пустой деструктор символа;*/
void Print() /*-выдача символа в заданную точку экрана: */ { cout<<"\n"<<Symbol; } };/*-конец определения класса символ на текстовом /* экране в заданной позиции;
*/ */
class StringLocation:/*-производный класс строка на экране;*/ public Location /*-доступ к базовому;
*/
{ char* String;/*-указатель поля для строки символов;*/ public: /*-открытая часть класса:
*/
StringLocation(int InitX,int InitY, /*-конструктор;*/ char* InitString): Location(InitX,InitY)/*-вызов базы;
*/
{ String=InitString;}/*-инициализа-
*/
/* ция строки;
*/
~StringLocation() { } /*-пустой деструктор класса;
*/
void Print() /*-выдача строки с установкой ее начала:
*/
{ cout<<"\n"<<String; } };/*-конец определения класса устанавливаемой строки;*/ void Print(Location* ObjPtr) /*-вывод полиморфного объ-
*/
/* екта(символа или строки): */ { ObjPtr->Print(); } Здесь определены три класса:базовый класс Location,а также два производных от него класса SymbolLocation и StringLocation. Во всех трех классах имеются функции печати с одним и тем же именем Print и с пустыми наборами параметров.Эти функции выполняют выдачу на экран символьной информации в текстовом режиме. 36
Приведена также функция печати полиморфного объекта для случая виртуального полиморфизма,рассматриваемого ниже. Вот примеры использования всех этих функций: void main(void) /*-главная программа,использующая раз-
*/
/* личные объекты типов,производных от
*/
/* базового типа Location:
*/
{
Location Loc(1,1); SymbolLocation SymLoc(1,1,'A'); StringLocation StrLoc(1,1,"ABC"); Loc.Print();/*-на экран выдается слово Location;*/ SymLoc.Print();/*-с новой позиции выдается символ; */ StrLoc.Print();/*-с новой позиции выдается строка; */ SymbolLocation* SymLocPtr=&SymLoc; StringLocation* StrLocPtr=&StrLoc; Print(SymLocPtr);/*-на экран выдается не символ А, */ /* а слово Location;
*/
Print(StrLocPtr);/*-на экран выдается не строка /* АВС,а слово Location;
*/ */
} /*-конец программы выдачи полиморфного объекта;
*/
Когда полиморфные функции с одним и тем же именем и одним и тем же набором параметров определены по иерархии составных типов со словом virtual,то при вызове какой-либо функции из набора ее идентификация может быть произведена только во время исполнения программы,так как в таких случаях при компиляции неизвестен тип объекта,использующего эту функцию.Происходит это из-за полиморфности объектов составного типа,которая является обязательным атрибутом виртуальных методов. При использовании виртуальных полиморфных функций необходимо выполнять следующие условия: 1.Если базовый тип описывает функцию как виртуальную,то все его производные типы,кoтoрые реализуют функцию с тем же именем,должны описать эту функцию тоже виртуальной.Другими словами,нельзя заменять виртуальную функцию статической. 2.Если переопределяется реализация виртуальной функции,то заголовок заново определяемой виртуальной функции в производном типе не может быть изменен. 37
3.В определении типов,составляющих иерархию,должны обязательно определяться конструкторы типов, производящих цию соответствующих объектов и обеспечивающих
инициализа-
взаимосвязь
с
таблицей виртуальных функций,которую строит компилятор в этом случае.В итоге все объекты,сконструированные этими конструкторами,имеют связь с таблицей виртуальных функций соответствующего типа. Важно отметить,что таблица виртуальных функций - одна для каждого типа,а не у каждой переменной того или иного типа.Переменная,или объект,лишь держит связь с таблицей своего типа, которую установил конструктор.Когда в программе встречается полиморфный объект,имеющий виртуальные функции,производится идентификация его типа,после чего по указанной связи находятся реализации виртуальных функций,соответствующие установленному типу пoлиморфного объекта.Все это делается во время исполнения программы.Более того,если тип Z наследует от типа Y виртуальную функцию,вызывающую другие функции,то последние вызовы будут относиться к функциям типа Z,а не Y.В случае статических методов все было бы наоборот:вызовы не "вернулись" бы в Z. Напишем программу печати полиморфного объекта с использованием виртуальных полиморфных функций: class Location /*-базовый класс позиция; { int x,y;/*-координаты точки экрана: public: /*-открытая часть класса;
*/ */ */
Location(int InitX,int InitY)/*-конструктор точки;*/ { x=InitX;/*-инициализация абсциссы;
*/
y=InitY;/*-инициализация ординаты;
*/
}/*-конец определения конструктора точки; */ ~Location() { } /*-пустой деструктор класса; virtual void Print() const=0;/*-чисто виртуаль/* ная функция; };/*-конец определения базового класса позиция; 38
*/ */ */ */
class SymbolLocation:/*-символ и его позиция:
*/
public Location { char Symbol;/*-поле для записи символа; public: /*-открытая часть производного класса;
*/ */
SymbolLocation(int X,int Y,char Sym): Location(X,Y) { Symbol=Sym;} ~SymbolLocation() { }
/*-пустой деструктор;
*/
virtual void Print() /*-реализация функции;
*/
const { cout<<"\n"<<Symbol; } };/*-конец определения 1-го производного класса;
*/
class StringLocation:/*-строка и ее начальная позиция:
*/
public Location { char* String;/*-указатель поля для строки; public: /*-открытая часть класса:
*/ */
StringLocation(int X,int Y,char* Str): Location(X,Y) { String=Str; }/*-конструктор;*/ ~StringLocation() { } /*-пустой деструктор;
*/
virtual void Print() /*другая реализация функции; */ const { cout<<"\n"<<String; } };/*-конец определения 2-го производного класса; void Print(Location* ObjPtr) /*-вывод полиморфного объ-
*/ */
/* екта(символа или строки): */ { ObjPtr->Print(); } void main(void) /*-главная программа,использующая раз-
*/
/* личные объекты типов,производных от
*/
/* базового типа Location:
*/
{ SymbolLocation SymLoc(1,1,'A'); StringLocation StrLoc(1,1,"ABC"); SymbolLocation* SymLocPtr=&SymLoc; StringLocation* StrLocPtr=&StrLoc; Print(SymLocPtr);/*-выдача символа на экран;
*/
Print(StrLocPtr);/*-выдача строки на экран;
*/
} /*-конец программы выдачи полиморфного объекта; 39
*/
1.4.3.Полиморфизм составных объектов Помимо возможности описывать иерархическую структуру предметной области с помощью аппарата производных типов,обеспечивающих прозрачный,полупрозрачный и непрозрачный доступ к их базовому типу,необходимо также иметь гибкие средства обработки объектов,находящихся на различных уровнях этой иерархии - полиморфных объектов.Для создания полиморфного объекта используются операторы присваивания или вызовы функций, работающие с указателями или ссылками на составные типы:объект производного типа при использовании его указателя или ссылки,можно рассматривать как объект его базового типа.Другими словами, если производный тип SymbolLocation имеет открытый базовый тип Location,то указатель или ссылку на SymbolLocation можно присваивать переменной соответственно типа указатель или ссылка на Location,не используя явное преобразование типа.Аналогично,если производный тип SymbolLocation имеет открытый базовый тип Location,то указатель или ссылка на SymbolLocation можно передавать в качестве аргумента параметру соответственно типа указатель или ссылка на Location,точно так же не используя явное преобразование типа.Обратное преобразование указателя или ссылки на Location соответственно в указатель или ссылку на SymbolLocation должно быть явным.Такие присваивания и вызовы функций,как правило,имеют место в связи с использованием виртуальных функций. Приведем несколько примеров использования полиморфных объектов,реализуемых указателями и ссылками в операторах присваивания,а также в вызовах внешних функций Print(),параметрами которых являются соответственно указатель и ссылка на базовый тип: class Location /*-базовый класс позиция; { int x,y;/*-координаты точки экрана: public: /*-открытая часть класса;
*/ */ */
Location(int InitX,int InitY)/*-конструктор точки;*/ { x=InitX;/*-инициализация абсциссы;
*/
y=InitY;/*-инициализация ординаты;
*/
}/*-конец определения конструктора точки; */ 40
~Location() { } /*-пустой деструктор класса; virtual void Print() const=0;/*-чисто виртуаль/* ная функция; };/*-конец определения базового класса позиция; class SymbolLocation:/*-символ и его позиция:
*/ */ */ */ */
public Location { char Symbol;/*-поле для записи символа; public: /*-открытая часть производного класса;
*/ */
SymbolLocation(int X,int Y,char Sym): Location(X,Y) { Symbol=Sym;} ~SymbolLocation() { }
/*-пустой деструктор;
*/
virtual void Print() /*-реализация функции;
*/
const { cout<<"\n"<<Symbol; } };/*-конец определения 1-го производного класса;
*/
class StringLocation:/*-строка и ее начальная позиция:
*/
public Location { char* String;/*-указатель поля для строки; public: /*-открытая часть класса:
*/ */
StringLocation(int X,int Y,char* Str): Location(X,Y) { String=Str; }/*-конструктор;*/ ~StringLocation() { } /*-пустой деструктор;
*/
virtual void Print() /*другая реализация функции; */ const { cout<<"\n"<<String; } };/*-конец определения 2-го производного класса; void Print(Location* ObjPtr) /*-вывод полиморфного объ-
*/ */
/* екта(символа или строки): */ { ObjPtr->Print(); } void Print(Location& ObjRef) /*-вывод полиморфного объ-
*/
/* екта(символа или строки): */ { ObjRef.Print(); } void main(void) /*-главная программа,использующая раз-
*/
/* личные объекты типов,производных от
*/
/* базового типа Location:
*/
41
{ SymbolLocation SymLoc(1,1,'A'); StringLocation StrLoc(1,1,"ABC"); Location* LocPtr; SymbolLocation* SymLocPtr=&SymLoc; StringLocation* StrLocPtr=&StrLoc; SymbolLocation& SymLocRef=SymLoc; StringLocation& StrLocRef=StrLoc; Print(SymLocPtr);/*-выдача символа на экран;
*/
Print(StrLocPtr);/*-выдача строки на экран;
*/
LocPtr=SymLocPtr; Print(LocPtr);
/*-выдача символа на экран;
*/
/*-выдача строки на экран;
*/
LocPtr=StrLocPtr; Print(LocPtr);
Print(SymLocRef);/*-выдача символа на экран;
*/
Print(StrLocRef);/*-выдача строки на экран;
*/
} /*-конец программы выдачи полиморфного объекта;
*/
Поскольку базовый класс может быть закрыт для производного,когда не используется спецификатор доступа public в определении этого производного класса,то для классов в этом случае неявное преобразование указателя производного класса в указатель базового класса не имеет места. Модифицированный оператор присваивания для указателей и ссылок составных типов является необходимым,но не достаточным средством обработки полиморфных объектов.Теперь необходимо выяснить,каким образом установить по указателю базового типа тип обрабатываемого объекта с тем,чтобы вызвать требуемую функцию обработки.Существуют два основных способа решения этой проблемы в языке С++: 1)использовать виртуальные функции,когда объекты автоматически получают связь со своим типом; 2)поместить в базовый класс специальное поле типа - дискриминант,который смогут просматривать функции. И в том,и в другом случае используются указатели или ссылки на базовые типы,которые конструируются как контейнерные или вмещающие типы в виде множеств,векторов или списков.Получаемые при этом неоднородные списки объектов облегчают програм42
мисту производить формулировку общности.Наибольшей гибкостью обладает первый подход,так как добавление к пакету новых производных типов и виртуальных функций не требует даже перекомпиляции исходного пакета.Во втором случае нобходимо использовать оператор switch с вариантами обработки на каждый производный тип,что приводит к необходимости перепрограммирования исходного пакета,когда включается новый производный тип.
2.ОБЛАСТИ ДЕЙСТВИЯ И ВРЕМЯ ЖИЗНИ ОБЪЕКТОВ
2.1.Области действия Имя обозначает объект,функцию,множество функций,константу перечисления,тип,член составного типа,шаблон,значение или метку. Имя вводится в программу объявлением.Имя может использоваться только внутри области текста программы,называемой его областью действия.Каждое имя имеет тип,определяющий способ его употребления. Есть четыре вида областей действия:локальная,функция,файл и класс. 1.Локальная.Имя,объявленное в блоке,локализовано в этом блоке и может использоваться только в нем после места объявления и в охватываемых блоках. Имена формальных параметров функции рассматриваются так, как если бы они были объявлены в самом внешнем блоке этой функции,т.е. в ее теле. 2.Функция.Метки,объявленные в функции,могут использоваться в любом месте этой функции.Лишь метки имеют своей областью действия функцию без учета ее блочной структуры. 3.Файл.Имя,объявленное вне любого блока или составного типа, может использоваться в файле,где оно описано,после места объявления.Имена,имеющие файл областью своего действия,называются глобальными. 4.Класс или вообще составной тип.Имя члена составного типа локализуется в этом типе.Правила доступа к ним были подробно изложены в предыдущих разделах и могут быть сформулированы следующим образом: 43
а)функция-член составного типа может использовать имя любого члена,заданного в объявлении этого типа,а также имена членов тех базовых типов,в которых эти члены определены со спецификаторами public или protected; б)если спецификатором доступа для некоторого члена составного типа является public,то имя этого члена может быть использовано после операции ".",примененной к объекту или ссылке на объект этого типа,а также тех производных от него типов, которые образованы от базового со спецификатором public; в)если спецификатором доступа для некоторого члена составного типа является public,то имя этого члена может быть использовано после операции "->",примененной к указателю на объект этого типа,а также тех производных от него типов,которые образованы от базового со спецификатором public; г)если спецификатором доступа для некоторого члена составного типа является public,то имя этого члена может быть использовано после операции "::",примененной к имени этого типа, а также тех производных от него типов,которые образованы от базового со спецификатором public. В связи с указанными правилами для классов область действия имен можно называть областью их доступности,в то время как в остальных случаях ее можно называть областью видимости имен. Часто область действия имени некоторого объекта называют областью действия этого объекта. 2.2.Время жизни Объект - это область памяти.Смысл значения, обнаруженного в объекте, определяется типом выражения,использованного для доступа к нему.Константа-литерал также занимает область памяти, однако ее значение определяется самим изображением константы. Каждый объект имеет класс памяти,определяющий его время жизни, т.е. временной интервал от момента выделения памяти для объекта с возможной его инициализацией и до момента освобождения этой памяти.Для констант место выделяется статически при компиляции программы. 44
2.3.Классы хранения В табл. 3 систематизируются и обобщаются свойства констант и объектов,используемых в различных частях логической структуры программы.Введено понятие класса хранения как интегральной характеристики обрабатываемых данных,размещаемых в оперативной памяти.Выделены следующие классы хранения данных: 1)литеральный для констант-литералов; 2)внешний для определяемых в файле объектов; 3)внешний статический для определяемых в файле объектов с классом памяти static; 4)транзитный для параметров функций; 5)внутренний статический для определяемых в блоке объектов с классом памяти static; 6)автоматический для определяемых в блоке объектов с классом памяти auto или без указания класса памяти; 7)регистровый для определяемых в блоке объектов с классом памяти register; 8)временный для создаваемых компилятором объектов; 9)динамический для объектов,создаваемых с помощью операции new; 10)инкапсулированный для полей составных типов. Для каждого класса определены области действия имен объектов,а также способы создания,размещения,использования и удаления этих объектов.
45
Таблица 3 Классы хранения литералов и объектов ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейст│Создание,размещение,исполь-│
│хранения│
вия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Константа-литерал:сим- │ 1.Создается статически ком│
│
│вольная,целая,с плавающей │пилятором по изображению.
│
│точкой и строковая;опреде-│ 2.Размещается при загрузке│
│
│ляется своим изображением.│программы в сегменте данных│
│
│ 2.Существует одна область│или в сегменте кодов в за- │
│
│действия константы-лите-
│
│рала - это вся программа. │использования,а также реа- │
│висимости от типа и места
│
│
│1.Лите- │
│лизации.
│
│
раль- │
│ 3.Использование константы-│
│
ный
│
│литерала ничем не ограничи-│
│
│
│вается в пределах ее дейст-│
│
│
│вия и времени жизни.
│
│
│ 4.Удаляется вместе со всей│
│
│
│программой после прекраще- │
│
│
│ния или завершения ее рабо-│
│
│
│ты.
│
│
│
│ 5.Время жизни литерала -
│
│
│
│это время работы программы.│
│
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Определяется в файле
│ 1.При отсутствии конструк-│
│
│вне любого блока или сос- │тора объект создается и
│
│тавного типа и при необхо-│инициализируется статически│
│
│димости имеет несколько
│
│описаний со спецификатором│лизация производится нулями│
│
│extern,не противоречащих
│при отсутствии инициализа- │
│
│исходному определению и
│тора;в противном случае,
│
│располагаемых в файлах,
│объект создается и инициа- │
│
│блоках или составных ти-
│лизируется конструктором
│
│пах.
│перед началом работы прог- │
│
│ 2.Число областей дейст-
│раммы.
│
│компилятором,причем инициа-│
│ │ │
└────────┴──────────────────────────┴───────────────────────────┘ 46
Продолжение таблицы 3 ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейст│Создание,размещение,исполь-│
│хранения│
вия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│вия определяется числом
│ 2.Размещение или создание │
│
│объявлений.Начало области │объекта производится в сег-│
│
│совпадает с объявлением,а │менте данных перед началом │
│
│конец области определяется│работы программы,если кон- │
│
│концом файла,блока или со-│структор не использует опе-│
│
│ставного типа в зависимос-│рацию new;в противном слу- │
│
│ти от месторасположения
│чае,статическая часть объ- │
│2.Внеш- │объявления.
│екта создается в сегменте
│
ний
│данных,а динамическая часть│
│
(гло- │
│- в куче.
│
│
баль- │
│ 3.Использование внешнего
│
│
ный)
│
│объекта ничем не ограничено│
│
│
│в пределах его областей дей│
│
│
│ствия и времени жизни.Бо-
│
│
│лее того,использование пре-│
│
│
│фикса :: перед именем объ- │
│
│
│екта позволяет использовать│
│
│
│внешний объект в любой точ-│
│
│
│ке программы.
│
│
│ 4.При наличии деструктора │
│
│
│внешний объект удаляется
│
│
│
│этим деструктором в конце
│
│
│
│работы программы,иначе объ-│
│
│
│ект удаляется вместе со
│
│
│всей программой по прекра- │
│
│
│щению или завершению ее ра-│
│
│
│боты.
│
│
│
│ 5.Время жизни внешнего
│
│
│
│объекта - это время работы │
│
│
│программы.
│
│
│
│
│
│
└────────┴──────────────────────────┴───────────────────────────┘ 47
Продолжение таблицы 3 ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейст│Создание,размещение,исполь-│
│хранения│
вия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Определяется в файле
│ 1.Внешний статический объ-│
│
│вне любого блока или сос- │ект создается точно так же,│
│
│тавного типа.Описания со
│
│спецификатором extern не- │ 2.Размещение внешнего ста-│
│
│допустимы.
│
│ 2.Существует только одна │но внешнему.
│как внешний.
│
│тического объекта аналогич-│
│3.Внеш- │область действия.Ее нача- │ 3.Использование внешнего
│ │
│
ний
│ло совпадает с началом оп-│статического объекта ничем │
│
(гло- │ределения,а конец ограни- │не ограничено в пределах
│
│
баль- │чивается концом файла,где │его области действия и
│
│
ный)
│
стати-│
│префикса :: для имени тако-│
│
ческий│
│го объекта недопустимо.
│
│
│ 4.Удаление внешнего стати-│
│
│
│ческого объекта аналогично │
│
│
│удалению внешнего.
│
│
│
│ 5.Время жизни внешнего
│
│
│
│статического объекта - это │
│
│
│время работы программы.
│встретилось определение.
│времени жизни.Использование│ │
│
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Определяется в списке
│ 1.Если параметр не являет-│
│
│параметров заголовка функ-│ся именем массива,рассмат- │
│
│ции,являющейся независимой│риваемым как указатель на
│
│внешней функцией или функ-│его первый элемент,или ука-│
│
│цией обработки полей сос- │зателем на какой-либо тип, │
│
│тавного типа,включенной в │то при каждом вызове функ- │
│
│этот тип на правах члена
│ции динамически конструкто-│
│
│или друга.
│ром,а при его отсутствии,
│
│ 2.Область действия пред- │специальной программой,
│
│
│ставляет собой блок,явля- │создаваемой компилятором,
│
│
│ющийся телом функции без
│
│строится объект заданного
│
│
└────────┴──────────────────────────┴───────────────────────────┘ 48
Продолжение таблицы 3 ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейс-│Создание,размещение,исполь-│
│хранения│
твия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│вложенных блоков,в которых│в определении типа и иници-│
│
│имя транзитного объекта
│
│(параметра) переопределя- │нным соответствующим аргу- │
│
│ется для других целей.
│ментом в вызове функции или│
│
│
│значением по умолчанию,ука-│
│
│
│занным для параметра в спи-│
│
│
│ске параметров.Когда пара- │
│
│
│метром является имя массива│
│
│
│или указатель на какой-либо│
│
│
│тип,то используется массив │
│
│
│или объект типа,заданные в │
│
│
│качестве аргументов.
│
│
│ 2.Размещение создаваемого │
│
│
│объекта производится в сте-│
│
│
│ке либо полностью,либо час-│
│
│
│тично.Последнее имеет место│
│4.Тран- │
│в случае использования кон-│
│
зитный│
│структором операции new.Та-│
│
│
│ким образом,когда не испо- │
│
│
│льзуется имя массива или
│
│
│какой-либо указатель,то вы-│
│
│
│зываемая функция не может
│
│
│изменять значения соответ- │
│
│
│ствующих аргументов.При ис-│
│
│
│пользовании параметра-ука- │
│
│
│зателя со спецификатором
│
│
│const также удается предот-│
│
│
│вратить такое изменение
│
│
│
│аргумента.
│
│
│
│ 3.Использование транзитно-│
│
│
│го объекта(параметра) ничем│
│ализируется значением,зада-│
│
│ │
│
└────────┴──────────────────────────┴───────────────────────────┘ 49
Продолжение таблицы 3 ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейст│Создание,размещение,исполь-│
│хранения│
вия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│
│не ограничено в пределах
│
│
│
│его области действия и
│
│
│
│времени жизни.
│
│
│
│ 4.При наличии деструктора │
│
│
│транзитный объект удаляется│
│
│
│этим деструктором при воз- │
│
│
│врате из функции,иначе он
│
│
│удаляется специальной под- │
│
│
│программой,созданной компи-│
│
│
│лятором и вызываемой перед │
│
│
│возвратом из функции.
│
│
│ 5.Время жизни транзитного │
│
│
│объекта - это время испол- │
│
│
│нения функции от момента
│
│
│ее вызова до выхода из нее.│
│
│
│
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Определяется в теле фу-│ 1.Создание,размещение,ис- │
│
│нкции или любом ее блоке
│
│со спецификатором static. │мя жизни внутреннего стати-│
│
│ 2.Существует только одна │ческого объекта определяют-│
│
│область действия.Ее нача- │ся точно так же,как и для
│пользование,удаление и вре-│
│
│5.Внут- │ло совпадает с началом оп-│внешнего статического с по-│ │
ренний│ределения объекта,а конец │правкой на новую область
│
│
стати-│совпадает с концом блока, │действия.Из определения
│
│
ческий│где встретилось это опре- │времени жизни объекта сле- │
│
│деление.В область дейст-
│дует,что его значение со-
│
│
│вия не включаются ее внут-│храняется даже в те момен- │
│
│ренние блоки,где то же са-│ты,когда объект невидим из │
│
│мое имя используется для
│
│определения других объек- │раммы.
│
│
│тов.
│
│исполняемого участка прог- │ │
└────────┴──────────────────────────┴───────────────────────────┘ 50
Продолжение таблицы 3 ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейст│Создание,размещение,исполь-│
│хранения│
вия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Определяется в теле фу-│ 1.Автоматический объект
│
│
│нкции или любом ее блоке
│создается и инициализирует-│
│
│со спецификатором класса
│ся при каждом входе в блок.│
│
│памяти auto,который чаще
│При отсутствии конструктора│
│
│всего опускается.
│это выполняется специальной│
│
│ 2.Область действия авто- │подпрограммой,сгенерирован-│
│
│матического объекта опре- │ной компилятором,при нали- │
│
│деляется точно так же,как │чии конструкторов-одним из │
│
│и для внутреннего стати-
│этих конструкторов,выбира- │
│
│ческого.
│емых по типу и числу аргу- │
│
│
│ментов конструктора,задан- │
│
│
│ных в определении объекта. │
│
│
│ 2.Размещение создаваемого │
│
│
│объекта производится в сте-│
│
│
│ке либо полностью,либо час-│
│
│
│тично.Последнее имеет место│
│
│
│в случае использования кон-│
│
│
│структором операции new,с
│
│6.Авто- │
│помощью которой дополнитель│
│ мати-
│ное место выделяется в ку- │
│
│
ческий│
│че.
│
│
│
│ 3.Использование автомати- │
│
│
│ческого объекта ничем не
│
│
│
│ограничено в пределах его
│
│
│
│области действия и времени │
│
│
│жизни.
│
│
│ 4.При наличии деструктора │
│
│
│автоматический объект уда- │
│
│
│ляется этим деструктором,в │
│
│
│противном случае-специаль- │
│
│
│ной подпрограммой,созданной│
│
└────────┴──────────────────────────┴───────────────────────────┘ 51
Продолжение таблицы 3 ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейст│Создание,размещение,исполь-│
│хранения│
вия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│
│компилятором.Это удаление
│
│
│
│производится при каждом вы-│
│
│
│ходе из блока.
│
│
│ 5.Время жизни автоматичес-│
│
│
│кого объекта-это время ак- │
│
│
│тивности блока от момента
│
│
│входа в него до момента вы-│
│
│
│хода из него.
│
│ │
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Определяется в теле фу-│ 1.Создание,размещение,ис- │
│
│нкции или любом ее блоке
│пользование,удаление и вре-│
│
│со спецификатором класса
│мя жизни регистрового объ- │
│
│памяти register.
│екта определяются точно так│
│7.Реги- │ 2.Область действия ре-
│же,как и для автоматическо-│
│
стро- │гистрового объекта опре-
│го объекта,за исключением
│
вый
│
│деляется точно так же,как │того,что будет производить-│
│
│и для автоматического объ-│ся попытка разместить объ- │
│
│екта.
│ект в аппаратных регистрах │
│
│
│для уменьшения времени ис- │
│
│
│полнения блока.
│
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Объект определяется
│ 1.Временный объект созда- │
│
│компилятором при трансля- │ется,размещается,использу- │
│
│ции инициализаторов или
│
│выражений,когда имеют мес-│матический специальными
│
│
│то сложные преобразования │программами,генерируемыми
│
│
│типов,и помещается в ре-
│
│
│альный или фиктивный блок,│ 2.Компилятор минимизирует │
│8.Вре-
│образованный компилятором.│время жизни временных объ- │
│
мен-
│Объект имеет статус авто- │ектов,удаляя их из памяти
│
ный
│матический,недоступен про-│всякий раз,как только в них│
│ется и удаляется как авто- │
│компилятором.
│
└────────┴──────────────────────────┴───────────────────────────┘ 52
Продолжение таблицы 3 ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейст│Создание,размещение,исполь-│
│хранения│
вия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│граммисту и используется
│отпала необходимость.
│
│
│для реализации семантики
│
│
│
│языка.
│
│
│
│ 2.Область действия вре-
│
│
│
│менного объекта определя- │
│
│
│ется точно так же,как и
│
│
│
│для автоматического объ-
│
│
│
│екта.
│
│
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Динамический объект не-│ 1.Динамический объект соз-│
│
│которого типа определяет- │дается операцией new или
│
│
│ся указателем на этот тип.│конструктором составного
│
│
│ 2.Область действия дина- │типа,если он предусмотрен
│
│
│мического объекта- это об-│в определении этого типа.
│
│
│ласть действия определя-
│
│ющего его указателя,кото- │объекта осуществляется в
│
│рый может иметь любой
│
│класс хранения,в том числе│ляемой памяти в момент ис- │
│
│и динамический.Для сложной│полнения операции new или
│
│
│динамической структуры оп-│вызова конструктора.
│
│
│ределяющим указателем яв- │ 3.Доступ к динамическому
│
│
│ляется указатель динамиче-│объекту производится с по- │
│ 2.Размещение динамического│ │
│куче-динамически распреде- │
│9.Дина- │ского объекта,который соз-│мощью указателя или цепоч- │ │
мичес-│дается первым в этой стру-│ки указателей для сложной
│
кий
│
│ктуре,если,конечно,соблю- │динамической структуры.Ис- │
│
│даются правила строгой ти-│пользование динамического
│
│
│пизации при работе с ука- │объекта ничем не ограничи- │
│
│зателями объектов.
│вается в пределах его об-
│
│
│
│ласти действия и времени
│
│
│
│жизни.
│
│
│
│ 4.Удаление динамического
│
└────────┴──────────────────────────┴───────────────────────────┘ 53
Продолжение таблицы 3 ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейст│Создание,размещение,исполь-│
│хранения│
вия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│
│объекта производится опера-│
│
│
│цией delete или деструкто- │
│
│
│ром составного типа.
│
│
│ 5.Время жизни динамическо-│
│
│
│го объекта - это время от
│
│
│момента его создания до мо-│
│
│
│мента его удаления.
│ │ │
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Объект определяется как│ 1.Инкапсулированный объект│
│
│компонента составного объ-│создается как компонента
│
│
│екта,который может иметь
│
│
│любой класс хранения,в том│иметь любой класс хранения,│
│
│числе и инкапсулированный.│кроме инкапсулированного.
│
│ 2.Области действия для
│
│инкапсулированного объекта│ванного объекта,если он не │
│
│определяются по иерархии
│
│составных типов,содержащей│изводится таким же образом,│
│
│тип данного объекта,и раз-│как и составного.Если ин-
│
│личаются в зависимости от │капсулированный объект яв- │
│
│класса используемых прог- │ляется динамическим,то он
│
│
│раммистом функций и опера-│размещается в куче.
│
│
│ций:
│
│ а)функции и операции,яв- │рованных объектов со специ-│
│
│ляющиеся членами или дру- │фикатором public ничем не
│
│зьями типа;
│ограничено в пределах обла-│
│
│ б)функции и операции,не
│сти их действия и времени
│
│
│имеющие такого статуса.
│жизни.Инкапсулированные
│
│
│ Если в некоторой точке
│объекты со спецификаторами │
│
│программы составной объект│private или protected в
│
│является видимым,то для
│
│функций и операций,являю- │вия и времени жизни доступ-│
│составного,который может
│
│ 2.Размещение инкапсулиро- │ │является динамическим,про- │ │
│ 3.Использование инкапсули-│ │
│
│пределах их области дейст- │
└────────┴──────────────────────────┴───────────────────────────┘ 54
Продолжение таблицы 3 ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейс-│Создание,размещение,исполь-│
│хранения│
твия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│щихся членами или друзьями│ны только функциям и опера-│
│
│соответствующего типа,ока-│циям со статусом член или
│10.Ин-
│зываются видимыми все ком-│друг соответствующего сос- │
│
│
кап- │поненты этого типа,а также│тавного типа.
│
│
сули-│унаследованные компоненты │ 4.Если инкапсулированный
│
│ │
рован-│как со спецификатором дос-│объект является динамичес- │ ный
│тупа public,так и со спе- │ким,то после размещения его│
│
│цификатором protected.
│с помощью операции new и
│
│
│
│использования он должен
│
│
│являющиеся друзьями или
│
│членами,могут использовать│В противном случае он уда- │
│
│только компоненты со спе- │ляется вместе с удалением
│
│
│цификатором доступа public│составного объекта.
│
│
│(как основные,так и уна-
│
│
│следованные от прародите- │рованного объекта определя-│
│
│льских типов).
│
│ Составной объект,для ком-│и удаления.
│
│
│поненты которого определя-│
│
│
│ются области действия,мо- │
│
│
│жет иметь любой класс хра-│
│
│
│нения,кроме инкапсулирова-│
│
│
│нного.
│
Функции и операции,не
│удаляться операцией delete.│
│ 5.Время жизни инкапсули-
│ется моментами его создания│
│
└────────┴──────────────────────────┴───────────────────────────┘
55
Продолжение таблицы 3 ┌────────┬──────────────────────────┬───────────────────────────┐ │ Класс
│Объявления и области дейс-│Создание,размещение,исполь-│
│хранения│
твия объекта
│зование и удаление объекта │
├────────┼──────────────────────────┼───────────────────────────┤ │
│ 1.Объект описывается как │ 1.Создается и инициализи- │
│
│компонента составного типа│руется статически компиля- │
│
│с ключевым словом static и│тором при обработке соотве-│
│
│существует в единственном │тствующего определения.
│11.Ста- │числе для всех объектов
│
│ 2.Размещается при загрузке│
│
ти-
│
чес- │состав,причем определение │ных.
│
кий
│такого объекта должно быть│ 3.Используется,когда необ-│
│
ин-
│дано вне класса.Это опре- │ходимо выполнить операцию
│
кап- │деление может задавать яв-│над всеми объектами типа
│
│
сули-│ную инициализацию.
│
│ │
│данного типа,не входя в их│программы в сегменте дан-
│или хранить информацию о
рован-│ 2.Область действия объек-│всех объектах типа,наприный
│ │ │
│
│та - это область действия │мер,счетчик этих объектов. │
│
│содержащего его составного│ 4.Удаляется вместе со всей│
│
│типа,при этом на него мож-│программой после прекраще- │
│
│но ссылаться только с по- │ния или завершения ее рабо-│
│
│мощью выражения,включающе-│ты.
│
│
│го имя составного типа,оп-│ 5.Время жизни объекта -
│
│
│ерацию "::" и имя объекта.│это время работы программы.│
└────────┴──────────────────────────┴───────────────────────────┘ 56
3.БАЗОВЫЕ СТРУКТУРЫ ДАННЫХ И ИХ КЛАССЫ 3.1.Выбор базовых структур для обучающей программы Обучающая программа позволяет уяснить основные идеи объектно-ориентированного программирования на языке С++.Это достигается путем изучения примера разработки несложной программы и параллельного выполнения упражнений на ЭВМ по языковым понятиям и конструкциям,вовлекаемым в очередную фазу разработки. Объектно-ориентированное программирование - это метод программирования,имитирующий способы,какими обычно в повседневной жизни изготавливаются те или иные предметы. Оно является естественной эволюцией более ранних новшеств в проектировании языков программирования:оно более структурировано,чем предыдущие попытки в структурном программировании;оно более модульно и абстрактно,чем предыдущие попытки в абстрагировании данных и сокрытии деталей. Три основных свойства характеризуют язык С++ как язык объектно-ориентированного программирования: 1.Инкапсуляция - объединение структур данных с функциями, что превращает их в новый тип данных-класс. 2.Наследование - задание класса,а затем использование его для построения иерархии порожденных классов,с наследованием доступа каждого из порожденных классов к коду и данным предка. 3.Полиморфизм - задание одного имени или обозначения действию,которое передается вверх или вниз по иерархии классов,с реализацией этого действия способом,соответствующим каждому классу в иерархии. Основополагающая идея структурного программирования на языке С++ состоит в том,что сначала разрабатываются необходимые для предметной области классы,а затем их используют без каких-либо изменений в прикладных программах. Физически классы состоят из одного или более заголовочных файлов,определяющих интерфейс,и одного или более файлов,определяющих реализацию. Затем заголовочные файлы включаются в прикладную 57
программу с помощью директивы #include,а файлы,определяющие реализацию обычно компилируются и помещаются в библиотеку объектных модулей. Разберем процесс создания программы для построения на экране геометрических фигур,основанный на данном подходе. Программа естественным образом разделяется на следующие части: 1)базовые структуры данных:односвязные простые и обобщенные списки,очереди и стеки; 2)обработка ошибок,возникающих в базовых структурах данных; 3)администратор экрана:подпрограммы низкого уровня и структуры данных,определяющих экран;он ведает только точками и прямыми; 4)библиотека фигур:набор определений основных фигур вроде прямоугольника и круга,а также стандартные программы для работы с ними; 5)прикладная программа:множество определений,специализированных для данного приложения,и набор операторов,использующих эти определения. Эти части,по-видимому,будут писать люди разной квалификации и,возможно,в разных организациях и в разное время:первые четыре части - программисты,хорошо владеющие методами объектно-ориентированного программирования,а пятую часть - лица, хорошо знающие предметную область и требования заказчика. В ряде случаев,обработчики ошибок также разрабатываются программистами прикладных программ и заменяют стандартные функции обработки ошибок. 3.2.Базовые структуры данных 3.2.1.Циклический односвязный список Циклический односвязный список состоит из элементов-звеньев,содержащих два поля: поле для ссылки на последующий элемент и поле для ссылки на данные,причем последний элемент списка ссылается на первый,а фиксатор списка указывает на последний его элемент. В зависимости от данных,на которые 58
ссылаются элементы,списки бывают однородные и неоднородные. Определим универсальный указатель: typedef void* ent; Элемент списка определим как класс с двумя дружественными классами:список и оператор списка,которые определим позже: class slink /*-элемент односвязного списка: {
/* ┌───────┐ /* │
┌ ─ ─ ─ ┐
next ├──Ў
/* ├───────┤ /* │
e │
/*
·
*/ ─ ─ Ў
├ ─ ─ ─ ┤
│
e
/* └───┬───┘ /*
next
*/ */ */ */
└ ─ ┬ ─ ┘
*/ */
·
*/
/*
*/
friend class slist;
/*-дружественные
friend class slist_iterator;/* классы;
*/ */
slink* next; /*-на последующий элемент;
*/
ent e;
*/
/*-указатель поля данных;
slink(ent a, slink* p) /*-конструктор звена;
*/
{ e=a; next=p; } };/*-конец определения односвязного списка;
*/
Обратите внимание,что все члены класса,в том числе и его конструктор,являются личными private. Циклический односвязный список с одним фиксатором и четырьмя операциями:включить в начало,добавить в конец,выбрать с удалением и очистить - определим следующим образом: class slist /*-циклический односвязный список:
*/
{ friend class slist_iterator;/*дружественный класс;*/ slink* last;/*-фиксатор списка,определяющий /* его последний элемент;
*/ */
public: int insert(ent a);/*-включить элемент в начало*/ int append(ent a);/*-добавить элемент в конец;*/ 59
ent get();
/*-выбрать и удалить элемент*/
void clear();
/*-очистить список;
/* Конструктор:*/ slist()
*/
{ last=0; }
/* Конструктор:*/ slist(ent a) { last=new slink(a,0); last->next=last; /* Деструктор: */~slist()
}
{ clear(); }
};/*-конец определения циклического односвязного /* списка;
*/ */
Рассмотрим реализации функций-членов класса slist: а)включить элемент в начало списка: int slist::insert(ent a) /*-параметр а определяет указа/* тель на поле данных; { if(last) /*-если список не пуст:
*/ */ */
last->next=new slink(a,last->next);/*-новое звено;*/ else { last=new slink(a,0);/*-новое звено без указателя*/ last->next=last;
/*-указатель на самого себя;*/
} return 0; } б)добавить элемент в конец списка: int slist::append(ent a) /*-указатель а определяет указа- */ /* тель на поле данных; { if(last)
/*-если список не пуст:
*/ */
last=last->next=new slink(a,last->next); else { last=new slink(a,0);/*-новое звено без указателя*/ last->next=last;
/*-указатель на самого себя;*/
} return 0; } в)выбрать и удалить первый элемент: 60
void slist_handlr(char*) {} /*-заглушка для функции;
*/
ent slist::get() { if(last==0) slist_handlr("Список пуст");/*-см. 4.3; */ else /*-продолжение обработки,если список не пуст:
*/
{ slink* g=last->next;/*-указатель 1-го элемента; */ ent ret=g->e;/*-указатель на поле данных для
*/
/* 1-го элемента;
*/
if(g==last)/*-если элемент - один в списке;
*/
last=0;
/*-список пуст после удаления звена; */
else last->next=g->next;/*-исключение звена; delete g;
*/
/*-освобождение места для 1-го звена;*/
return ret;/*-возвращение указателя данных;
*/
} } г)удаление всех элементов из списка(очистка списка): void slist::clear() { slink* nxt=last;/*-выбор 1-го удаляемого элемента; */ if(nxt==0)return;/*-возврат,если список пуст;
*/
do { /*-удаление элементов из непустого списка;
*/
slink* dlt=nxt;/*-указатель удаляемого звена; */ nxt=nxt->next; /*-указатель очередного звена; */ delete dlt; } while(nxt!=last);/*-пока не последнее(уже удален/* ное) звено;
*/ */
} Заметим,что сам класс slist не имеет функций,которые обеспечивали бы перебор элементов в цикле.Обычно для этих целей вводят специальный класс - итератор,который содержит все необходимые для перебора элементов функции и является дружественным к исходному классу slist.Структура итератора такова: 61
class slist_iterator /*-итератор циклического односвязного*/ /* списка:
*/
{ slink* ce;/*-указатель очередного элемента списка;*/ slist* cs;/*-указатель итерируемого списка;
*/
public: slist_iterator(slist& s) /*-конструктор:
*/
{cs=&s;ce=cs->last;} ent operator()()/*-операция итерирования;
*/
{ ent ret=ce?(ce=ce->next)->e:0; if(ce==cs->last)ce=0;/*-конец списка*/ return ret;/*-поле данных или нуль; */ } };/*-конец определения итератора списка элементов;
*/
Из класса slist можно получать списки тех объектов,которые представляют интерес в конкретной программе.Например,для списка строк символов мы можем построить следующий производный класс: struct nlist:slist /*-класс для списка строк знаков; { void insert(char* a)
{slist::insert(a);}
void append(char* a)
{slist::append(a);}
char* get()
{ return "ABCDE"; }
*/
nlist(char* a):slist(a) {} };/*-конец определения призводного класса nlist;
*/
Функции нового класса или наследуются от slist непосредственно(например,функция clear()),или ничего не делают,кроме преобразования типа.Класс nlist - это ничто иное,как альтернативный интерфейс класса slist.Так как на самом деле тип ent есть void*,нет необходимости явно преобразовывать указатели char*,которые используются в качестве фактических параметров.Поиск требуемой строки в списке реализуется следующей функцией: int find(nlist* spisok,char* stroka) { slist_iterator tek_stroka(*(slist*)spisok); 62
ent ptr;/*-указатель на текущее поле данных;
*/
while((ptr=tek_stroka())!=0) /*-пока не исчерпан
*/
/* список элементов; if(ptr==stroka) return 1;/*-строка найдена; return 0;/*-возврат,если строка не найдена;
*/ */ */
} Печать строк списка может быть осуществлена так: #include /*-стандартные классы ввода-вывода; */ void print_spisok(nlist* spisok) { slist_iterator tek_stroka(*(slist*)spisok); ent ptr;/*-указатель на печатаемую строку;
*/
while((ptr=tek_stroka())!=0) /*-пока не все строки;*/ cout<<(char*)ptr<<"\n"; } 3.2.2.Обобщенный класс односвязных списков Используя базовый класс slist,можно определять аналогичным образом списки элементов любых типов.Специальные макрокоманды,расположенные в файле generic.h,позволяют упростить этот процесс.Так,макрокоманда _Paste2_x(z,v) осуществляет конкатенацию аргументов z и v,а макрокоманда declare(z,v) формирует имя новой макрокоманды из аргумента z и слова declare,а затем вызывает эту макрокоманду с аргументом v.Следующие три макрокоманды,написанные с применением указанных стандартных макрокоманд,автоматизирует процесс формирования циклических односвязных списков с элементами данных любых типов,а также обеспечивает получение итераторов,связанных с этими списками. С помощью указанных макрокоманд список указателей на строку nlist и объект этого класса можно определить так: 63
#include #define gslist(type) _Paste2_x(type,gslist) #define gslist_iterator(type) _Paste2_x(type,gslist_iterator) #define gslistdeclare(type)
\ \
struct gslist(type):slist
\
{ int insert(type a)
\
{ return slist::insert(ent(a));} int append(type a)
\ \
{ return slist::append(ent(a));}
\ \
type get()
\
{ return type(slist::get());}
\
gslist(type)(){}
\
gslist(type)(type a):slist(ent(a)){}
\
~gslist(type)(){slist::clear();}
\ \
};
\ \
struct gslist_iterator(type):slist_iterator
\
{ gslist_iterator(type)(gslist(type)& s)\ :slist_iterator((slist&)s){}
\
type operator()()
\
{ return type(slist_iterator:: operator()());} };
\ \ \
typedef char* Pchar; declare(gslist,Pchar); gslist(Pchar) spisok; 3.2.3.Стеки и очереди Наиболее употребительной списочной структурой является стек.Его элемент имеет одно поле для связи и одно поле данных.Направления записи и чтения при работе со стеками противоположны:последний записанный элемент читается первым.Для 64
обеспечения работы стека достаточен один фиксатор,содержащий адрес последнего записанного элемента,а поле связей первого элемента пусто.Каждый следующий элемент стека указывает на предыдущий. Класс stack определим как производный от класса slist с помощью следующих логически разделенных действий: а)ограничение понятия списка: class ogr_slist:slist { public: slist::insert; slist::get; ogr_slist(){} ogr_slist(ent a):slist(a){} }; б)задание специальных функций и типа char*,чтобы создать стек указателей на символы: class stack:ogr_slist { public: void push(char* a)
{ slist::insert(a); }
char* pop() { return (char*)slist::get(); } stack()
{}
stack(char* a):ogr_slist(a)
{}
}; Очередь(бобслей),как и стек,состоит из элементов,имеющих два поля:поле связи и поле данных,но в отличие от стека направления чтения и записи в ней совпадают,т.е.элемент,записанный первым,и читается первым.Определим класс очередь целых,совместив два указанных действия для базового класса slist: class iqueue:slist { public: void put(int a){slist::append((void*)a); } int deq()
{return int(slist::get());}
iqueue()
{}
}; 65
3.3.Обработка ошибок в базовых структурах данных При обнаружении ошибки во время обработки той или иной базовой структуры возможны следующие действия: а)возврат пользователю недопустимого значения; б)возврат пользователю дополнительного значения состояния; в)вызов стандартной функции обработки ошибок для базовых структур; г)вызов функции обработки ошибок,поставляемой пользователем. Для
небольших программ выбор такого действия зависит от
вкусов разработчика,так как все эти варианты эквивалентны. При разработке мощных и гибких универсальных средств обработки структур данных первые два подхода непригодны,так как трудно идентифицировать недопустимые значения или реагировать на возникающие состояния.Последние два подхода приемлемы и их можно сделать почти эквивалентными,если задавать не саму функцию,а указатель на нее.Это позволит разработчику такого средства,как циклический односвязаный список slist предоставить функцию ошибок, действующую по умолчанию,и при этом программистам,которые будут использовать списки,будет легко задавать свои собственные функции ошибок, когда нужно,и там,где нужно.Рассмотрим,как это может быть сделано. #include <stdlib.h> /*-стандартный файл системных функций;*/ typedef void (*PFC)(char*);/*-задание типа PFC-указателя
*/
/* на функцию с параметром ти- */ /* па char*; extern PFC slist_handler;
*/
/*-описание указателя функции; */
extern PFC set_slist_handler(PFC);/*-описание функции с
*/
/* параметром типа PFC, */ /* которая возвращает
*/
/* указатель на функцию */ /* типа PFC. 66
*/
void default_error(char*s)/*-стандартная функция для вы{ cerr<<s<<"\n"; exit(1);
*/
/* дачи текста сообщения об
*/
/* ошибке и прерывания работы;
*/
} PFC slist_handler=default_error;/*-инициализация указателя*/ /* функции
*/
PFC set_slist_handler(PFC handler) /*-определение функции */ /* для переустановки
*/
/* указателя обработ-
*/
/* чика ошибок;
*/
{ PFC rr=slist_handler;/*фиксация текущего обработчика*/ slist_handler=handler;/*установка нового обработчика*/ return rr;
/*-возврат для восстановления;*/
} void reset() { PFC old=set_slist_handler(slist_handlr); /*-установка и запоминание;*/ /*Программа,использующая slist_handlr для ошибок;
*/
set_slist_handler(old);/*-восстановление обработчика*/ }
4.ПРОЕКТИРОВАНИЕ ОБУЧАЮЩЕЙ ПРОГРАММЫ 4.1.Администратор экрана В примере используется простая концепция экрана. Он представляется как двумерный массив символов,имеющих номер колонки x и номер строки y в качестве координат.В памяти имеется образ экрана, где точка представляется знаком '*'. #include /*-стандартный файл ввода-вывода пульта; */ const XMAX=40,YMAX=24;
/*-размер модельного экрана;
*/
char screen[XMAX][YMAX];/*-модельный экран;
*/
char black='*'; /*-для изображения точки на экране;
*/
char wite =' '; /*-для стирания точки на экране;
*/
67
void screen_init() /*-очистка образа экрана-модели экрана;*/ { for(int y=0;y
/*снизу вверх по строчкам*/
for(int x=0;x<XMAX;x++)/*-слева направо; screen[x][y]=wite;
*/
/*-стирание точки экрана;*/
} void screen_clear() /*-альтернативное название функции для*/ /* очистки экрана(точнее,его образа); */ { screen_init();} void screen_refresh()/*-обновление экрана по его образу; { for(int y=YMAX-1;0<=y;y--)
*/
/*-сверху вниз;
*/
/*-слева направо;
*/
cout<<screen[x][y];
/*-выдача строки;
*/
cout<<"\n";
/*-переход на строку;*/
{ for(int x=0;x<XMAX;x++)
} } inline int on_screen(int a,int b) /*-проверка допустимости*/ /* координат точки;
*/
{ return (0<=a&&a<XMAX&&
/*-проверка абсциссы;
*/
0<=b&&b
/*-проверка ординаты;
*/
} Как легко заметить,строки нумеруются снизу вверх,начиная с нуля и кончая YMAX-1,a колонки - слева направо от 0 до XMAX-1.Это не совпадает с привычной нумерацией строк и колонок в текстовом или графическом режимах работы. Строят точки и линии на модельном экране специальные функции put_point() и put_line(),которые используют структуру point: struct point { int x,y;/*-координаты точки:0<=x<XMAX;0<=y
/*пустой конструктор*/
point(int a,int b){ x=a;y=b;}/*-конструктор точки*/ }; 68
void put_point(int a,int b)/*-занести точку в модель;
*/
{ if(on_screen(a,b)) screen[a][b]=black; } void put_point(point p)
/*-занести точку в модель;
*/
{ put_point(p.x,p.y); } void put_line(int x0,int y0,int x1,int y1)/*-занести линию*/ { register dx=1; int a=x1-x0; if(a<0)dx=-1,a=-a;
/*-смотрите литературу по
*/
register dy=1;
/* машинной графике для выяс-
*/
int b=y1-y0;
/* нения алгоритмов построе-
*/
if(b<0)dy=-1,b=-b;
/* ния геометрических фигур;
*/
int two_a=2*a;
/* [10,11,12,13]
*/
int two_b=2*b; int xcrit=-b+two_a; register eps=0; for(;;){ put_point(x0,y0); if(x0==x1&&y0==y1)break; if(eps<=xcrit)x0+=dx,eps+=two_b; if(eps>=a||a<=b)y0+=dy,eps-=two_a; } } void put_line(point a,point b) /*-занести линию в модель; */ { put_line(a.x,a.y,b.x,b.y); }
4.2.Библиотека фигур Определим обобщенный список shape_list,в который будем включать требуемые для построения изображения на экране фигуры.Для этих целей воспользуемся макрокомандами из раздела 3.2.2: struct shape; /*-предварительное определение типа фигуры; */ 69
typedef shape* sp;
/*-определение простого имени типа;
*/
declare(gslist,sp); /*-макрогенерация типов для списка и
*/
/* его итератора;
*/
Сгенерированные типы с точностью до обозначения их имен выглядят следующим образом: struct sp_gslist:slist /*-циклический односвязный список; */ { int insert(sp a) { return slist::insert(ent(a)); } int append(sp a) { return slist::append(ent(a)); } sp
get()
{ return sp(slist::get());
}
sp_gslist(){} sp_gslist(sp a):slist(ent(a)){} }; struct sp_gslist_iter:slist_iterator { sp_gslist_iter(sp_gslist& s): slist_iterator((slist&)s) {} sp operator()() { return sp(slist_iterator::operator()()); } }; sp_gslist shape_list;/*-определение списка фигур с пустым */ /* конструктором;
*/
Теперь необходимо определить общее понятие фигуры shape таким образом,чтобы оно использовалось как базовый класс всеми конкретными фигурами:линиями,прямоугольниками,кругами и т.д.Причем манипулирование фигурой должно осуществляться только через фиксированный интерфейс: struct shape /*-определение понятия фигуры:
*/
{ shape(){ shape_list.append(this);}/*-конструктор,*/ /* осуществляющий включение вновь опреде- */ /* ляемой фигуры в конец списка;
*/
virtual point north() { return point(0,0); } /*-север; 70
*/
virtual point south() { return point(0,0); } /*-юг; virtual point east()
*/
{ return point(0,0); } /*-восток;
*/
virtual point neast() { return point(0,0); } /*-северо-восток;*/ virtual point seast() { return point(0,0); } virtual void draw()
{}
virtual void move(int,int){}
/*-юго-восток;
*/
/*-нарисовать;
*/
/*-переместить;
*/
};/*-конец определения понятия фигуры;
*/
Предполагается,что расположение фигуры задается с помощью функции move(),a построение фигуры осуществляется функцией draw().Фигуры можно ориентировать друг относительно друга, используя понятие точки соприкосновения,указанные в определении типа:север,юг и т.д.Каждая конкретная фигура определяет свой смысл этих точек,и каждая определяет способ,которым она рисуется. Рассмотрим определения только двух фигур - линии и прямоугольника. Линию можно построить либо по двум точкам,либо по точке и целому.В последнем случае рисуется горизонтальная линия, длину которой определяет целое.Знак целого указывает,каким концом является точка - левым или правым.Север определяется как самая северная точка над центром.Определение линии таково: class line:public shape { point w,e;/*-линия из точки w в точку e;
*/
public: point north(){return point((w.x+e.x)/2, e.y<w.y?w.y:e.y);} point south(){return point((w.x+e.x)/2, e.y<w.y?e.y:w.y);} void move(int a,int b) { w.x+=a;w.y+=b; e.x+=a;e.y+=b; } 71
void draw() {put_line(w,e);} /* Конструкторы:
*/
line(point a,point b) { w=a;e=b; } line(point a,int l) { w=point(a.x+l-1,a.y);e=a; } };/*-конец определения линии;
*/
Прямоугольник определяется с помощью двух точек:юго-западной sw и северо-восточной ne: class rectangle:public shape { point sw,ne; /* nw ************* ne
*/
/*
*
*
*/
/*
*
*
*/
/* sw ************* se
*/
public: point north(){return point((sw.x+ne.x)/2, ne.y);} point south(){return point((sw.x+ne.x)/2, sw.y);} point neast(){return ne;} point swest(){return sw;} void move(int a,int b) { sw.x+=a;sw.y+=b; ne.x+=a;ne.y+=b; } void draw(); rectangle(point,point); };/*-конец определения прямоугольника;
*/
Чтобы построить прямоугольник,строят по угловым точкам его четыре стороны: void rectangle::draw()
/*-функция для построения пря- */ /* моугольника;
*/
{ point nw(sw.x,ne.y);/*-левый верхний угол;
*/
point se(ne.x,sw.y);/*-правый нижний угол;
*/
put_line(nw,ne);
*/
/*-верхняя сторона; 72
put_line(ne,se);
/*-правая сторона;
*/
put_line(se,sw);
/*-нижняя сторона;
*/
put_line(sw,nw);
/*-левая сторона;
*/
} /*-конец функции построения прямоугольника;
*/
Прямоугольник определяется двумя точками:юго-западной sw и северо-восточной ne.Функция усложняется из-за необходимости выяснять относительное положение этих точек: rectangle::rectangle(point a,point b) /*-конструктор; { if(a.x<=b.x)
*/
/*-точка a левее точки b;
*/
{ if(a.y<=b.y)/*-точка a ниже точки b;
*/
{ sw=a; ne=b; } else
/*-точка a выше точки b;
*/
{ sw=point(a.x,b.y); ne=point(b.x,a.y); } } else { if(a.y<=b.y)
/*-точка b левее точки a;
*/
/*-точка a ниже точки b;
*/
{ sw=point (b.x,a.y); ne=point(a.x,b.y); } else
/*-точка a выше точки b;
*/
{ sw=b; ne=a; } } } /*-конец определения конструктора прямоугольни- */ /* ка по двум точкам a и b;
*/
Функция shape_refresh() обновляет экран,рисуя все фигуры списка shape_list заново,при этом она не располагает информацией,какие фигуры надо
рисовать.Это знает функция draw(),
задаваемая в элементе списка. 73
void shape_refresh() /*-построение фигур списка на экране;*/ { screen_clear();/*-очистка модельного экрана;
*/
sp_gslist_iter next(shape_list);/*-итератор списка;*/ shape* p; /*-указатель фигуры,заносимой в модель;
*/
while((p=next())!=0) p->draw();/*-занесение фигур; */ screen_refresh(); /*-построение фигур на экране; } /*-конец функции построения фигур на экране;
*/ */
Сервисная функция ставит одну фигуру на другую,задавая, что south() одной должен быть сразу над north()другой: void stack(shape* q,shape* p) /*-функция компоновки фигур;*/ { point n=p->north();/*-север нижней фигуры;
*/
point s=q->south();/*-юг верхней фигуры;
*/
q->move(n.x-s.x,n.y-s.y+1);/*-изменение координат
*/
/* базовых точек переме-*/ /* щаемой фигуры; };/*-конец функции компоновки фигур для построения;
*/ */
4.3.Прикладная программа Перейдем теперь к рассмотрению прикладной программы,разрабатываемой с ориентацией на набор функций для управления экраном и библиотеку построения геометрических фигур.Эти компоненты могут быть представлены в виде заголовочных файлов,содержащих определение типов,и библиотеки,составленной из откомпилированных вариантов определяемых функций. В прикладной программе определяется новая более сложная фигура,немного похожая на лицо человека,а затем в главной функции осуществляется ее построение и одевание на нее шляпы.Наконец,все это изображение перемещается по экрану. class myshape:public rectangle /*-лицо человека; { line* l_eye; /*-левый глаз;
*/ */
line* r_eye; /*-правый глаз;
*/
line* mouth; /*-рот;
*/
74
public: myshape(point,point);/*-конструктор лица; ~myshape();
*/
/*-деструктор лица;
*/
void draw();
/*-построитель лица;
*/
void move(int,int);
/*-изменение коорди-
*/
/* нат базовых точек; */ };/*-конец определения класса лицо человека;
*/
myshape::myshape(point a,point b):rectangle(a,b) /*-конструктор лица*/ { int ll=neast().x-swest().x+1;/*-ширина лица;
*/
int hh=neast().y-swest().y+1;/*-высота лица;
*/
l_eye=new line(point(swest().x+2,/*-левый глаз;*/ swest().y+hh*3/4),2); r_eye=new line(point(swest().x+ll-4,/*-правый; */ swest().y+hh*3/4),2); mouth=new line(point(swest().x+2, /*-рот;
*/
swest().y+hh/4),ll-4); } /*-конец определения конструктора лица; myshape::~myshape()
*/
/*-деструктор лица*/
{ delete l_eye; delete r_eye; delete mouth; } /*-конец определения деструктора лица;
*/
Заметим, что нос не задан в классе myshape,его просто добавляют к изображению функция draw(): void myshape::draw() /*-функция построения лица человека; */ { rectangle::draw(); put_point(point((swest().x+neast().x)/2, (swest().y+neast().y)/2)); } /*-конец функции построения лица;
*/
Изображение лица передвигается посредством перемещения базового прямоугольника и вторичных объектов:левого глаза,правого глаза и рта: 75
void myshape::move(int a,int b) /*-перемещение лица;
*/
{ rectangle::move(a,b);/*-перемещение прямоугольника; */ l_eye->move(a,b); /*-перемещение левого глаза;
*/
r_eye->move(a,b); /*-перемещение правого глаза;
*/
mouth->move(a,b); /*-перемещение рта;
*/
} /*-конец определения функции перемещения лица;
*/
В главной программе строится несколько фигур и производится их перемещение: void main(void) /*-построение прямоугольника,линии и лица:*/ { shape* p1=new rectangle(point(0,0),point(10,10)); /*-цилиндр шляпы;*/ shape* p2=new line(point(0,15),17); /*-поле шляпы;
*/
shape* p3=new myshape(point(15,10),point(27,18)); /*-лицо человека;*/ shape_refresh(); getch();/*-1-я приостановка исполнения программы;
*/
/*-построение лица и шляпы (надевание шляпы):
*/
p3->move(-10,-10);/*-лицо смещается вниз и влево;
*/
stack(p2,p3);/*-поле шляпы ставится над лицом;
*/
stack(p1,p2);/*-шляпа ставится над полем шляпы;
*/
shape_refresh();/*-строится нормальное изображение;*/ getch();/*-2-я приостановка исполнения программы;
*/
delete p3; delete p2; delete p1; } /*-конец определения главной программы;
*/
Изображения на экране после 1-й и 2-й приостановок показаны на рис.2 и 3. 76
^
^
***********
│
│
*
*
20│
│
*
*
│
│
*
*
│
*************
│
*
*
│
*
*
│
*
*
│
* **
** *
│
*
*
*
15│
*
*
*
│
*
* *
15│***************** │
*
│
*
*
│
*
│
* ********* *
│
***********
│
*
*
│
*************
10│
10│***********
*
*****************
│*
*
│
│*
*
│
*************
│*
*
│
*
│*
*
│
* **
5│*
*
5│
*
│*
*
│
*
│*
*
│
*
│*
*
│
* ********* *
│*
*
│
*
0│***********
0│
└───────────────────────────> 0
5
10
15
20
25
Рис.2.Отдельные фигуры
* ** * * *
* * *
*************
└─────────────────────────> 0
5
10
15
20
25
Рис.3.Изображение лица
З А К Л Ю Ч Е Н И Е В пособии рассмотрена лишь та часть языка С++,которая обеспечивает поддержку технологии объектно-ориентированного программирования. При этом многие детали языка опущены и основное внимание уделено раскрытию принципов объектно-ориентированного программирования и рассмотрению многочисленных примеров, показывающих,как тот или иной принцип реализуется на языке С++. 77
Все примеры пособия отлажены,в чем читатель может убедиться сам,набрав и откомпилировав интересующий его пример.Впрочем, этого даже не потребуется делать,если воспользоваться автоматизированными обучающими курсами,специально подготовленными для облегчения усвоения материала пособия и включенными в разработанную на кафедре интеллектуальную обучающую компьютерную систему "ПОИСК".Укажем названия нескольких таких курсов и дадим их краткую характеристику: rotstart - для изучения корневых структур,объединений и классов,в которых наряду с полями данных всех основных типов включены функции доступа к этим полям,при этом все члены открыты, так что к полям можно обращаться как непосредственно операторами присваивания и ввода-вывода,так и с помощью функций доступа; nasstart - для изучения механизмов единичного и множественного наследования данных,когда имеется несколько предков и один или несколько потомков; prvstart - для изучения способов защиты как на уровне отдельных членов,так и на уровне составных типов; plmstart - для изучения полиморфизма операций,функций и объектов; prmstart - набор примеров,приведенных в пособии,с контрольными проверками и оценками по пятибалльной системе; scrstart - набор контрольных примеров по программе администратора экрана,проектирование которой рассмотрено в пособии. Если при изучении пособия встретятся затруднения по другим разделам языка или появится желание глубоко и быстро разобраться в имеющихся библиотечных классах компилятора, а также в пакете Turbo Vision,то упомянутая выше обучающая система "ПОИСК" и IBM-совместимый персональный компьютер помогут решить эти задачи.В зависимости от режима работы система может использоваться как электронный учебник,лабораторный практикум,тренажер или учебная экспертная система по языку С++. Количественная оценка знаний почти по всем видам занятий позволяет обучаемому осуществлять самоконтроль познавательного процесса,а преподавателю систематически и быстро проводить текущий контроль успеваемости всех обучаемых. 78
Л и т е р а т у р а 1.Страуструп Б. Язык программирования С++.-М.:Радио и связь, 1991.-348с. 2.Эллис М.,Страуструп Б. Справочное руководство по языку программирования С++ с комментариями.-М.:Мир,1992.-448с. 3.Поляков Д.Б.,Круглов И.Ю. Программирование в среде Турбо Паскаль(версия 5.5).-М.:Издательство МАИ,А/О "РОСВУЗНАУКА",1992. -576с. 4.Субботин Д.М.,Бочков С.О. Система программирования на языке высокого уровня Си.-М.:Международный центр научной и технической информации,1991.-272с. 5.Язык Си для профессионалов. -М.:ИВК.-СОФТ,1992.-320с. 6.Брюзгин А.А. Программирование в системе Windows.Практическое руководство.-М.:Малип,1992.-280с. 7.Turbo vision для С++.-Киев.:ДИАЛЕКТИКА,1992.-246с. 8.Чип С. Объектно-ориентированное программирование.Состояние и перспективы.-Фирма "ПРИНТ",1991.-60с. 9.Кардышев С.В.,Капкин А.М. Интерфейс программиста Турбо Си. -М.:Радио и связь,1992.-192с. 10.Аммерал Л.Интерактивная трехмерная машинная графика.-М.:"Сол Систем",1992.-320с. 11.Аммерал Л.Программирование графики на Турбо Си.-М.:"Сол Систем",1992.-224с. 12.Аммерал Л.Принципы программирования
в
машинной
графике.-М.:
"Сол Систем",1992.-224с. 13.Аммерал Л.Машинная
графика
на
персональных
компьютерах.-М.:
"Сол Систем",1992.-244с. 14.Сивохин А.В. Представление знаний в интеллектуальных системах обучения: Учеб. пособие/Под ред. Б.Г.Хмелевского. - Пенза: Пенз. политехн. ин-т, 1990. - 88 с.
79
С О Д Е Р Ж А Н И Е
Введение . . . . . . . . . . . . . . . . . . . . . . . .
3
1.Объектно-ориентированное программирование и его поддержка в С++ . . . . . . . . . . . . . . . . .. . . . . . .
5
1.1.Реальный мир и объекты . . . . . . . . . . . . . . .
5
1.2.Структуры с механизмами наследования . . . . . . . .
7
1.3.Инкапсуляция данных и функций доступа к ним . . . .
10
1.4.Полиморфизм операций,функций и составных объектов .
25
1.4.1.Полиморфизм операций . . . . . . . . . . . . . 25 1.4.2.Полиморфизм функций . . . . . . . . . . . . .
31
1.4.3.Полиморфизм составных объектов . . . . . . . . 40 2.Области действия и время жизни объектов . . . . . . . .
43
2.1.Области действия . . . . . . . . . . . . . . . . . . 43 2.2.Время жизни . . . . . . . . . . . . . . . . . . . .
44
2.3.Классы хранения . . . . . . . . . . . . . . . . . .
45
3.Базовые структуры данных и их классы . . . . . . . . . . 57 3.1.Выбор базовых структур для обучающей программы . . . 57 3.2.Базовые структуры данных . . . . . . . . . . . . . . 58 3.2.1.Циклический односвязный список . . . . . . . . 58 3.2.2.Обобщенный класс односвязных списков . . . . . 63 3.2.3.Стеки и очереди . . . . . . . . . . . . . . .
64
3.3.Обработка ошибок в базовых структурах данных . . . . 66 4.Проектирование обучающей программы . . . . . . . . . . . 67 4.1.Администратор экрана . . . . . . . . . . . . . . . . 67 4.2.Библиотека фигур . . . . . . . . . . . . . . . . . . 69 4.3.Прикладная программа . . . . . . . . . . . . . . . . 74 Заключение . . . . . . . . . . . . . . . . . . . . . . . 77 Литература . . . . . . . . . . . . . . . . . . . . . . . 79
80