ВВЕДЕНИЕ

1 Эволюция технологий программирования

### 1.1 Эволюция технологий программирования

###

Технология программирования - это совокупность методов и средств разработки (написания) программ и порядок применения этих методов и средств.

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

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

В 1958 году были разработаны первые языки программирования, Фортран и Алгол-58. Программа на Фортране состояла из главной программы и некоторого количества процедур - подпрограмм и функций. Программа на Алголе-58 и его последующей версии Алголе-60 представляла собой единое целое, но имела блочную структуру, включающую главный блок и вложенные блоки подпрограмм и функций. Компиляторы для Фортрана обеспечивали раздельную трансляцию процедур и последующее их объединение в рабочую программу, первые компиляторы для Алгола предполагали, что транслируется сразу вся программа, раздельная трансляция процедур не обеспечивалась.

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

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

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

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

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

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

- Построением языка программирования, содержащего как можно больше типов данных, и выбором для каждого класса задач некоторого подмножества этого языка. Такой язык иногда называют языком-оболочкой. На роль языка-оболочки претендовал язык ПЛ/1, оказавшийся настолько сложным, что так и не удалось построить его формализованное описание. Отсутствие формализованного описания, однако, не помешало широкому применению ПЛ/1 как в Западной Европе, так и в СССР.

- Построением расширяемого языка, содержащего небольшое ядро и допускающего расширение, дополняющее язык типами данных и операторами, отражающими концептуальную сущность конкретного класса задач. Такой язык называют языком-ядром. Как язык-ядро были разработаны языки Симула и Алгол-68, не получившие широкого распространения, но оказавшие большое влияние на разработку других языков программирования.

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

1.2 Средства объектно-ориентированного программирования

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

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

Объектно-ориентированное программирование (ООП) в настоящее время является мощным средством создания программ. Но ООП является эффективным инструментом лишь в рамках целостного объектно-ориентированного подхода на всех этапах жизненного цикла программных систем.

Термин ООП (Object-oriented programming) появился в начале 80-х годов вместе с языком SmallTalk. Развитие ООП определялось потребностями развития программных систем – необходимо было создавать мобильные, легко расширяемые и стыкуемые программы с унифицированным дружественным интерфейсом. В среду Turbo-Pascal объекты были введены уже с версии 5.5, самое известное DOS-приложение ООП – пакет Turbo-Vision.

Наиболее мощно как языковое средство ООП представлено в языке С++. Операционная система MS Windows (как и другие многозадачные ОС) в своей основе содержит объектно-ориентированные подходы, и игнорировать эти подходы, мягко говоря, неперспективно. Развитие средств создания программ в настоящее время находится на стадии совершенствования сред визуального проектирования, базирующихся на популярных языках программирования. Для профессиональных программистов это системы типа C++ Builder, для пользователей – среды типа Delphi и Visual Basic.

1.3 Принцип объектно-ориентированного программирования

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

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

###

### 1.4 Сущность объектно-ориентированного подхода к программированию

###

Основные идеи объектно-ориентированного подхода опираются на следующие положения:

- Программа представляет собой модель некоторого реального процесса, части реального мира.

- Модель реального мира или его части может быть описана как совокупность взаимодействующих между собой объектов.

- Объект описывается набором параметров, значения которых определяют состояние объекта, и набором операций (действий), которые может выполнять объект.

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

- Объекты, описанные одним и тем же набором параметров и способные выполнять один и тот же набор действий представляют собой класс однотипных объектов.

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

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

Классы объектов часто удобно строить так, чтобы они образовывали иерархическую структуру. Например, класс “Студент”, описывающий абстрактного студента, может служить основой для построения классов “Студент 1 курса”, “Студент 2 курса” и т.д., которые обладают всеми свойствами студента вообще и некоторыми дополнительными свойствами, характеризующими студента конкретного курса. При разработке интерфейса с пользователем программы могут использовать объекты общего класса “Окно” и объекты классов специальных окон, например, окон информационных сообщений, окон ввода данных и т.п. В таких иерархических структурах один класс может рассматриваться как базовый для других, производных от него классов. Объект производного класса обладает всеми свойствами базового класса и некоторыми собственными свойствами, он может реагировать на те же типы сообщений от других объектов, что и объект базового класса и на сообщения, имеющие смысл только для производного класса. Обычно говорят, что объект производного класса наследует все свойства своего базового класса.

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

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

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

Наиболее используемыми в настоящее время объектно-ориентированными языками являются Паскаль с объектами и Си++, причем наиболее развитые средства для работы с объектами содержатся в Си++.

Практически все объектно-ориентированные языки программирования являются развивающимися языками, их стандарты регулярно уточняются и расширяются. Следствием этого развития являются неизбежные различия во входных языках компиляторов различных систем программирования. Наиболее распространенными в настоящее время являются системы программирования Microsoft C++, Microsoft Visual C++ и системы программирования фирмы Borland International. Дальнейший материал в данном пособии излагается применительно к системе программирования Borland C++. Это связано прежде всего наличием в этой системе программирования развитой интегрированной среды, объединяющей текстовый редактор, компилятор, редактор связей (компоновщик) и отладочные средства.

1.5 Описание терминов и методологий программирования

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

Понятию “объект” сопоставляют ряд дополняющих друг друга определений. Ниже приведены некоторые из них.

#### - Объект - это осязаемая реальность, характеризующаяся четко определяемым поведением.

#### - Объект - особый опознаваемый предмет, блок или сущность (реальная или абстрактная), имеющая важное функциональное назначение в данной предметной области.

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

Состояние объекта определяется перечнем всех возможных (обычно статических) свойств и текущими значениями (обычно динамическими) каждого из этих свойств. Свойства объекта характеризуются значениями его параметров.

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

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

#### - конструктор, создание и инициализация объекта;

#### - деструктор, разрушающий объект;

#### - модификатор, изменяющий состояние объекта;

#### - селектор для доступа к переменным объекта без их изменения;

#### - итератор для доступа к содержанию объекта по частям в определенной последовательности.

Известна и другая классификация методов объекта, когда выделяют функции управления, реализации, доступа и вспомогательные функции.

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

Объекты могут находиться в определенных отношениях друг к другу. Эти отношения могут быть иерархическими. Основные иерархические отношения - это отношения использования и включения.

Отношение использования реализуется посылкой сообщений от объекта A к объекту B. При этом объект A может выступать в роли:

- активного или воздействующего объекта, когда он воздействует на другие объекты, но сам воздействию не подвергается;

- пассивного или исполняющего, когда объект подвергается воздействию, но сам на другие объекты не воздействует;

- посредника, если объект и воздействует и сам подвергается воздействию.

Отношение включения имеет место, когда составной объект содержит другие объекты.

Структура и поведение сходных объектов определяют класс объектов.

Между классами также могут быть установлены отношения:

#### - отношение разновидности (кошка - вид определенного биологического семейства или кошка - домашнее животное);

#### - включения или составной части (лапа - часть кошки);

#### - ассоциативности, когда между классами есть чисто смысловая связь (кошки и собаки - домашние животные).

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

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

Приведенная выше характеристика объектного подхода соответствует применению его для построения и программирования имитационных моделей реальных процессов и систем.

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

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

Объектно-ориентированный подход к проектированию программных изделий предполагает:

#### - проведение объектно-ориентированного анализа предметной области;

#### - объектно-ориентированное проектирование;

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

1.6 Понятия абстракции, инкапсуляции, наследования и полиморфизма

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

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

В терминах ООП, оператор “слон есть млекопитающее” описывает отношение. Выражение “цирк содержит слона” описывает отношение подразделения.

С++ поддерживает virtual функции-члены, которые объявлены в основном классе и переопределены в порожденном классе. Иерархия классов, определенная общим наследованием, создает связанный набор типов пользователя, на которые можно ссылаться с помощью указателя базового класса. При обращении к виртуальной функции через этот указатель в С++ выбирается соответствующее функциональное определение во время выполнения. Объект, на который указывается, должен содержать в себе информацию о типе, поскольку различия между ними может быть сделано динамически. Это особенность типична для ООП кода. Каждый объект “знает” как на него должны воздействовать. Эта форма полиморфизма называется чистым полиморфизмом.

Способность к наследованию должна быть встроена в программное обеспечение для того, чтобы максимизировать многократное использование кода и позволить естественное моделирование предметной области. С использованием наследования ключевыми элементами методологии ООП становятся:

· разработка соответствующего набора типов

· проектирование из возможных связей и применение механизма наследования для совместного использования кода

· использование виртуальных функций для полиморфической обработки связанных объектов.

Наследование - один из самых мощных механизмов языка С++. С его помощью можно разрабатывать очень сложные классы, наращивая уже созданные классы. Таким образом строится целая иерархия классов. Имеющиеся классы называются базовыми или порождающими, а вновь создаваемые классы - производными или потомками. Классы-потомки наследуют данные и методы своих базовых классов и пополняются собственными данными и методами. Любой производный класс, в свою очередь, может становиться базовым для других классов. Прежде чем строить новый класс, надо выяснить, нет ли класса с подобными свойствами. В большинстве случаев возможно использование уже существующих классов, при этом можно переопределить методы, дающие нежелательные действия, ввести новые данные и методы.

При наследовании важную роль играет статус доступа к компонентам базового класса. При наследовании базового класса возможно использование одного из следующих спецификаторов доступа:

· private (спецификатор по умолчанию), при котором все компоненты базового класса становятся приватными в производном классе.

· public, при котором все общедоступные компоненты в базовом классе остаются такими же в производном, а приватные в базовом остаются приватными в производном.

Но в производном классе, независимо от спецификатора базового класса , доступны только те компоненты базового класса, которые имеют доступ public или protected. Ниже приводится пример использования доступа к членам базового класса:

class First {

int value;

protected:
void SetValue( int v ) { value = v; }

};
class Second: public First {

int total;

public:
void SetTotal( int t )

{ total = t;

// использование защищенного в базовом классе метода

// Но нельзя value = t;

SetValue(t);
}};
class Third: public Second {

int count;

public:
void SetCount( int c )

{ count = c;

// использование защищенного в базовом классе метода

SetValue(c);
// использование доступного в базовом классе метода

SetTotal(c);
}};

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

class D : X,Y,Z { };

Здесь X, Y, Z - базовые классы. По умолчанию все базовые классы имеют спецификатор доступа private, при необходимости изменения спецификатора перед именем базового класса следует указывать public.

class D : public X, public Y, public Z { };

Обычно спецификатор protected перед именем базового класса при определении производного класса не используется. Следует заметить, что наследование не допускается для объединений union.

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

Есть некоторые свойства и методы, которые не наследуются. К таким методам относятся:

· конструкторы;

· деструкторы;

· операторы new, определенные пользователем;

· операторы присваивания, определенные пользователем.

Не наследуются отношения дружественности, объявленные в базовом классе.

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

class First {

int a,b,c;

public:
First(int x, int y, int z) { a = x; b = y; c = z; }

};
class Second: public First {

int value;

public:
Second(int d ):

First(d, d+2, d+5 ) { value = d + 66; }

Second(int d, int e);

};

Second::Second(int d, int e ):First(d,e,33)

{ value = d + e; };

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

Деструкторы вызываются в обратном, по отношению к вызовам конструкторов, порядке: сначала деструкторы производных классов, а затем - базовых.

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

<имя класса>::<идентификатор>

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

class A {

public:
int foo(){ return 1;}

};

class B: public A {

public:
int foo(){ return 2;}

int bf() { retun 33;}

};

class C: public B {

public:
int foo(){ return 3;}

};

class D: public C{

public:
int foo(){ return 4;}

int f1() { return A::foo();}

int f2() { return C::foo();}

};
void main() {

D d;

int k = d.B:: foo(); // вызывается функция, принадлежащая классу B

k = d.bf(); // bf - идентификатор встречается один раз

}

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

class A {

int height;

public:
void SetHeight(int h ) { height = h ; }

};
class B : public A {

int width

public:
void SetWidth(int w ) { width = w; }

};
void main() {

B b;

b.SetHeight(55);
b.SetWidth(66);
}

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

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

class A { };

class B: public A { };

void foo ( A * obj) { };

void main() {

A a;

B b;

A * pa = new A;

B * pb = new B;

a = b; // так можно

pa = pb; // так можно

b = a; // так нельзя

pb = pa; // и так нельзя

foo ( pa );// так можно

foo ( pb );// так можно

}

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

При множественном наследовании очень быстро нарастает сложность. Рассмотрим пример наследования, показанный ниже. Здесь для каждого класса А создается своя копия value (см. рисунок 1).

Рисунок 1 – Наследование классов

class A{

public:
int value;

};
class B : public A{};

class C : public A{};

class D : public B, public C{

public:
int Value() { return value;}

};

На этапе компиляции будет выдано сообщение об ошибке, так как не известно, к какому value идет обращение в функции Value(). Можно явно указывать, к какому value идет обращение:

return C:: val;

В программе возможно использование следующих конструкций:

void main(){

D d;

int k = d.B::value;

D * pd = new D;

pd->C::value = 66;

}

Наличие нескольких копий одного и того же базового класса не только вносит путаницу, но и ведет к лишним затратам памяти. Объявление виртуального базового класса заставляет компилятор создавать только одну копию данного базового класса. Тогда класс D можно определить следующим образом (см. рисунок 2):

Рисунок 2 – Связь классов

class A{

public:
int value;

};

class B : public virtual A{};

class C : public virtual A{ };

class D : public B, public C{

public:
int Value() { return value;}

};

### Полиморфизм.

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

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

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

class A {
public:
virtual void Display() { cout << "Класс А" }
};
class B : public A {
public:
virtual void Display() { cout << "Класс B" }
};
void Show( A * pa ) { pa -> Display(); }
void main() {
A * pa = new A;
B * pb = new B;
pa -> Display(); // вывод Класс А
pb -> Display(); // вывод Класс B
Show(pa); // вывод Класс А
Show(pb); // вывод Класс B
}

Если бы функция Display() не была бы объявлена виртуальной, то в последнем случае выполнялся бы вызов функции, принадлежащей классу A, и вывод был бы выполнен в виде Класс А.

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

class A {
public:
virtual int f(int x)
{ return x*x;}
};
class B: public A {
public:
virtual int f(int x)
{ return A::f(x * 33);}
};

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

virtual тип имя_функции ( список формальных параметров ) = 0;

Чистая виртуальная функция недоступна для вызовов. Такие вызовы отлавливаются во время выполнения программы и приводят к выводу сообщения об ошибке. Абстрактные классы - это классы, которые содержат хотя бы одну чистую виртуальную функцию. Абстрактные классы используются исключительно как базовые для других классов. Нельзя создать объекты абстрактных классов.

Вопросы для самопроверки.

1. В чем разница между двумя функциями:

virtual void func() = 0;

virtual void func1(){};

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

3. Создайте производный класс, используя класс из предыдущего вопроса. Не переопределяя чистую функцию, попробуйте создать и использовать объект производного класса.

4. Можно ли сделать виртуальную функцию дружественной другому классу? Приведите пример.

5. Могут ли быть объявлены виртуальными операторы? Приведите примеры.

6. В чем разница между виртуальными и невиртуальными функциями?

7. Почему при вызове конструкторов производного класса соблюдается такой порядок, при котором сначала вызываются конструкторы базовых классов?

8. Почему вызов деструкторов производного класса выполняется в порядке, обратном вызову конструкторов?

9. Возможно ли при наследовании указать спецификатор базового класса protected?

10. Как используется наследование при построении библиотек классов?

11. Постройте иерархию классов, показанную на рисунке. Предусмотрите конструкторы и деструкторы, данные в каждом классе. Постройте демонстрационную программу для данной иерархии (см. рисунок 3).

### Рисунок 3 – Иерархия классов

12. Может ли быть виртуальная функция приватной? Приведите пример использования.

13. Может ли быть виртуальная функция типа inline? Приведите пример.

14. Можно ли использовать виртуальные функции в конструкторе?

15. Можно ли использовать виртуальные функции в деструкторе?