Объектно-ориентированное программирование на С/C++
Объектно-ориентированное программирование как технология программирования
Парадигма программирования. Модульное программирование. Нисходящее программирование. Структурное программирование. Понятия объекта, класса объектов. Основные понятия объектно-ориентированного программирования: инкапсуляция, наследование и полиморфизм.
При изучении объектно-ориентированного программирования (ООП) наибольшей проблемой является использование новой терминологии и понимание нового подхода к решению старых задач - новой технологии программирования. Определения новых терминов и характеристики методов программирования составляют содержание данной темы.
Как в любом виде деятельности в программировании имеется своя технология: - это знания, правила, навыки и инструменты, позволяющие получать гарантированный качественный результат. Но само по себе соблюдение ряда правил не дает гарантию качества результата. Это объясняется спецификой программирования.
- Во-первых, это не наука, где знание какой-либо формулы позволяет однозначно решить задачу, подставив в нее исходные данные и получив результат.
- Во-вторых, эти правила необходимо соблюдать не столько на бумаге, сколько в голове. То есть технология программирования - это скорее способ организации процесса обдумывания программы, нежели ее записи. Из сказанного следует, что если пишущий программу - мыслит, то он уже придерживается какой-то технологии программирования, даже не подозревая об этом. Простейший метод заключается в написании программы сразу от начала до конца, без использования каких-либо общих принципов, то есть "как бог на душу положит".
Рассмотрим наиболее известные из технологий:
- метод "северо-западного" угла (имеется в виду лист бумаги или экран дисплея). Программа пишется сразу от начала до конца, без использования каких-либо общих принципов;
- технология структурного программирования, в ней предполагается придерживаться принципов модульности, нисходящего и пошагового проектирования программ, одновременного проектирования программ и структур данных.
- технология объектного программирования: связана с использованием при проектировании программы понятий объектов и их классов.
Парадигма программирования. Проблема "Что первично - курица или яйцо?" применительно к программированию звучит как парадигма: "Что первично: алгоритм (процедура, функция) или обрабатываемые им данные". В традиционной технологии программирования взаимоотношения процедуры - данные имеют более-менее свободный характер, причем процедуры (функции) являются ведущими в этой связке: как правило, функция вызывает функцию, передавая данные друг - другу по цепочке. Соответственно, технология структурного проектирования программ, прежде всего, уделяет внимание разработке алгоритма.
В технологии ООП взаимоотношения данных и алгоритма имеют более регулярный характер: во-первых, класс (базовое понятие этой технологии) объединяет в себе данные (структурированная переменная) и методы (функции). Во-вторых, схема взаимодействия функций и данных принципиально иная. Метод (функция), вызываемый для одного объекта, как правило, не вызывает другую функцию непосредственно. Для начала он должен иметь доступ к другому объекту (создать, получить указатель, использовать внутренний объект в текущем и т.д.), после чего он уже может вызвать для него один из известных методов. Таким образом, структура программы определяется взаимодействием объектов различных классов между собой. Как правило, имеет место иерархия классов, а технология ООП иначе может быть названа как программирование "от класса к классу".
Модульное программирование. Принцип модульности формулируется как требование разработки программы в виде совокупности модулей (функций). При этом разделение на модули должно носить не механический характер, а исходить из логики программы:
- размер модуля должен быть ограничен;
- модуль должен выполнять логически целостное и завершенное действие;
- модуль должен быть универсальным, то есть по возможности параметризованным: все изменяемые характеристики выполняемого действия должны передаваться через параметры;
- входные параметры и результат модуля желательно передавать не через глобальные переменные, а через формальные параметры и результат функции.
Еще одной, но уже физической единицей программы является текстовый файл, содержащий некоторое количество функций
и
определений типов данных и переменных. Модульное программирование на уровне файлов - это возможность разделить
полный
текст программы на несколько файлов, транслировать их независимо друг от друга.
Принцип модульности распространяется не только на программы, но и на данные: любой набор параметров,
характеризующих
логический или физический объект, должен быть представлен в программе в виде единой структуры данных
(структурированной переменной).
Олицетворением принципа модульности является библиотека стандартных функций. Она, как правило, обеспечивает
полный
набор параметризованных действий, используя общие структуры данных. Библиотеки представляют собой аналогичные
Си-программы, независимо оттранслированные и помещенные в каталог библиотек.
Нисходящее программирование. Нисходящее проектирование программы заключается в том, что разработка идет от общей неформальной формулировки некоторого действия программы на естественном языке, "от общего к частному": к замене ее одной из трех формальных конструкций языка программирования:
- простой последовательности действий;
- конструкции выбора или оператора if;
- конструкции повторения или цикла.
В записи алгоритма это соответствует движению от внешней (объемлющей) конструкции ко внутренней (вложенной). Эти конструкции также могут содержать в своих частях неформальное описание действий, то есть нисходящее проектирование по своей природе является пошаговым. Отметим основные свойства такого подхода:
- первоначально программа формулируется в виде некоторого неформального действия на естественном языке;
- первоначально определяются входные параметры и результат действия;
- очередной шаг детализации не меняет структуру программы, полученную на предыдущих шагах;
- если в процессе проектирования получаются идентичные действия в различных ветвях, то это означает необходимость оформления этого действия отдельной функцией;
- необходимые структуры данных проектируются одновременно с детализацией программы.
В результате проектирования получается программа, в которой принципиально отсутствует оператор перехода goto, поэтому такая технология называется "программирование без goto".
Пошаговое программирование. Нисходящее проектирование по своей природе является
пошаговым, ибо предполагает каждый раз замену одной словесной формулировки на единственную конструкцию языка. Но
в
процессе разработки программы могут быть и другие шаги, связанные с детализацией самой словесной формулировки в
более
подробную.
То, что этот принцип выделен отдельно, говорит о необходимости предотвратить соблазн детализации программы сразу
от
начала до конца и развивать умение выделять и сосредоточивать внимание на главных, а не второстепенных деталях
алгоритма.
Вообще нисходящее пошаговое проектирование программы не дает гарантии получения "правильной"
программы, но
позволяет возвратиться при обнаружении тупиковой ситуации к одному из верхних шагов детализации.
Структурное программирование. При нисходящей пошаговой детализации программы
необходимые для работы структуры данных и переменные появляются по мере перехода от неформальных определений к
конструкциям языка, то есть процессы детализации алгоритма и данных идут параллельно. Однако это касается,
прежде
всего, отдельных локальных переменных и внутренних параметров. С самой же общей точки зрения предмет (в нашем
случае
-
данные) всегда первичен по отношению к выполняемым с ним действиям (в нашем случае - алгоритм). Поэтому на самом
деле
способ организации данных в программе более существенно влияет на ее структуру алгоритма, чем что-либо другое, и
процесс проектирования структур данных должен опережать процесс проектирования алгоритма их обработки.
Структурное программирование - модульное нисходящее пошаговое проектирование алгоритма и структур данных.
Объектно-ориентированный подход к программированию включает в себя 3 основные компоненты:
- объектно-ориентированный анализ (ООА),
- объектно-ориентированное проектирование (ООД),
- объектно-ориентированное программирование (ООП).
В любой инженерной дисциплине под проектированием обычно понимается некий унифицированный подход, с помощью которого мы ищем пути решения определенной проблемы, обеспечивая выполнение поставленной задачи. В контексте инженерного проектирования цель проектирования определяется как создание системы, которая
- удовлетворяет заданным (возможно, неформальным) функциональным спецификациям;
- согласована с ограничениями, накладываемыми оборудованием;
- удовлетворяет явным и неявным требованиям по эксплуатационным качествам и потреблению ресурсов;
- удовлетворяет явным и неявным критериям дизайна продукта;
- удовлетворяет требованиям к самому процессу разработки, таким, например, как продолжительность и стоимость, а также привлечение дополнительных инструментальных средств.
Проектирование подразумевает учет противоречивых требований. Его продуктами являются модели, позволяющие нам понять структуру будущей системы, сбалансировать требования и наметить схему реализации.
Программа – это числовая модель проектируемой системы.
Важность построения модели. Моделирование широко распространено во всех инженерных дисциплинах, в значительной степени из-за того, что оно реализует принципы декомпозиции, абстракции и иерархии. Каждая модель описывает определенную часть рассматриваемой системы, а мы в свою очередь строим новые модели на базе старых, в которых более или менее уверены. Модели позволяют нам контролировать наши неудачи. Мы оцениваем поведение каждой модели в обычных и необычных ситуациях, а затем проводим соответствующие доработки, если нас что-то не удовлетворяет.
Элементы программного проектирования. Ясно, что не существует такого универсального метода, который бы провел инженера-программиста по пути от требований к сложной программной системе до их выполнения. Проектирование сложной программной системы не сводится к слепому следованию некоему набору рецептов. Скорее это постепенный и итеративный процесс. И тем не менее использование методологии проектирования вносит в процесс разработки определенную организованность. Инженеры-программисты разработали десятки различных методов, которые мы можем классифицировать по трем категориям. Несмотря на различия, эти методы имеют что-то общее. Их, в частности, объединяет следующее:
- условные обозначения - язык для описания каждой модели;
- процесс - правила проектирования модели;
- инструменты - средства, которые ускоряют процесс создания моделей, и в которых уже воплощены законы функционирования моделей. Инструменты помогают выявлять ошибки в процессе разработки.
Хороший метод проектирования базируется на прочной теоретической основе и при этом дает программисту известную степень свободы самовыражения.
Объектно-ориентированные модели. Существует ли наилучший метод
проектирования?
На этот вопрос нет однозначного ответа. По сути дела это завуалированный предыдущий вопрос: "Существует ли
лучший
способ декомпозиции сложной системы?" Если и существует, то пока он никому не известен. Этот вопрос можно
поставить следующим образом: "Как наилучшим способом разделить сложную систему на подсистемы?" Еще раз
напомним, что полезнее всего создавать такие модели, которые фокусируют внимание на объектах, найденных в самой
предметной области, и образуют то, что называется объектно-ориентированной декомпозицией.
Объектно-ориентированный анализ и проектирование - это метод, логически приводящий к объектно-ориентированной
декомпозиции. Применяя объектно-ориентированное проектирование, создаются гибкие программы, написанные
экономными
средствами. При разумном разделении пространства состояний мы добиваемся большей уверенности в правильности
нашей
программы. В итоге, мы уменьшаем риск при разработке сложных программных систем.
Так как построение моделей крайне важно при проектировании сложных систем, объектно-ориентированное
проектирование
предлагает богатый выбор моделей, которые представлены на рис. 1. Объектно-ориентированные модели проектирования
отражают иерархию и классов, и объектов системы. Эти модели покрывают весь спектр важнейших конструкторских
решений,
которые необходимо рассматривать при разработке сложной системы, и таким образом вдохновляют нас на создание
проектов,
обладающих всеми пятью атрибутами хорошо организованных сложных систем.

Рис. 1. Объектно-ориентированные модели.
Выводы:
- Программам присуща сложность, которая нередко превосходит возможности человеческого разума.
- Задача разработчиков программных систем - создать у пользователя разрабатываемой системы иллюзию простоты.
- Сложные структуры часто принимают форму иерархий; полезны обе иерархии: и классов, и объектов.
- Сложные системы обычно создаются на основе устойчивых промежуточных форм.
- Познавательные способности человека ограничены; мы можем раздвинуть их рамки, используя декомпозицию, выделение абстракций и создание иерархий.
- Сложные системы можно исследовать, концентрируя основное внимание либо на объектах, либо на процессах; имеются веские основания использовать объектно-ориентированную декомпозицию, при которой мир рассматривается как упорядоченная совокупность объектов, которые в процессе взаимодействия друг с другом определяют поведение системы.
- Объектно-ориентированный анализ и проектирование - метод, использующий объектную декомпозицию; объектно-ориентированный подход имеет свою систему условных обозначений и предлагает богатый набор логических и физических моделей, с помощью которых мы можем получить представление о различных аспектах рассматриваемой системы.
Объектная модель
Объектно-ориентированная технология основывается на так называемой объектной модели. Основными
принципами ее
построения являются: абстрагирование, инкапсуляция, модульность, иерархичность, типизация, параллелизм и
сохраняемость. Каждый из этих принципов сам по себе не нов, но в объектной модели они впервые применены в
совокупности.
Объектно-ориентированный анализ и проектирование принципиально отличаются от традиционных подходов структурного
проектирования: здесь нужно по-другому представлять себе процесс декомпозиции, а архитектура получающегося
программного продукта в значительной степени выходит за рамки представлений, традиционных для структурного
программирования. Отличия обусловлены тем, что структурное проектирование основано на структурном
программировании,
тогда как в основе объектно-ориентированного проектирования лежит методология объектно-ориентированного
программирования.
Основные положения объектной модели
Методы структурного проектирования помогают упростить процесс разработки сложных систем за счет использования алгоритмов как готовых строительных блоков. Аналогично, методы объектно-ориентированного проектирования созданы для того, чтобы помочь разработчикам применять мощные выразительные средства объектного и объектно-ориентированного программирования, использующего в качестве блоков классы и объекты.
Объектно-ориентированный анализ. (object-oriented analysis, ООА) направлен на создание моделей реальной действительности на основе объектно-ориентированного мировоззрения.
Объектно-ориентированный анализ - это методология, при которой требования к системе воспринимаются с точки зрения классов и объектов, выявленных в предметной области.
Объектно-ориентированное проектирование. (object-oriented design, ООД)
Программирование прежде всего подразумевает правильное и эффективное использование механизмов конкретных языков программирования. Проектирование, напротив, основное внимание уделяет правильному и эффективному структурированию сложных систем. Определим объектно-ориентированное проектирование следующим образом:
Объектно-ориентированное проектирование - это методология проектирования, соединяющая в себе процесс объектной декомпозиции и приемы представления логической и физической, а также статической и динамической моделей проектируемой системы.
В данном определении содержатся две важные части: объектно-ориентированное проектирование
1) основывается на объектно-ориентированной декомпозиции;
2) использует многообразие приемов представления моделей, отражающих логическую (классы и объекты) и физическую
(модули и процессы) структуру системы, а также ее статические и динамические аспекты.
Именно объектно-ориентированная декомпозиция отличает объектно-ориентированное проектирование от структурного, в
первом случае логическая структура системы отражается абстракциями в виде классов и объектов, во втором -
алгоритмами.
Объектно-ориентированное программирование. (object-oriented programming, OOП)
Объектно-ориентированное программирование - это методология программирования,
основанная
на представлении программы в виде совокупности объектов, каждый из которых является экземпляром определенного
класса,
а классы образуют иерархию наследования.
В данном определении можно выделить три части:
1) OOП использует в качестве базовых элементов объекты, а не алгоритмы;
2) каждый объект является экземпляром какого-либо определенного класса;
3) классы организованы иерархически.
Программа будет объектно-ориентированной только при соблюдении всех трех указанных требований. В частности,
программирование, не основанное на иерархических отношениях, не относится к OOП, а называется программированием
на
основе абстрактных типов данных.
Выделяют пять основных разновидностей стилей программирования, которые перечислены ниже вместе с присущими им
видами
абстракций:
| • процедурно-ориентированный | алгоритмы |
| • объектно-ориентированный | классы и объекты |
| • логико-ориентированный | цели, часто выраженные в терминах исчисления предикатов |
| • ориентированный на правила | правила "если-то" |
| • ориентированный на ограничения | инвариантные соотношения |
Невозможно признать какой-либо стиль программирования наилучшим во всех областях практического применения.
Например,
для проектирования баз знаний более пригоден стиль, ориентированный на правила, а для вычислительных задач -
процедурно-ориентированный. По накопленному опыту объектно-ориентированный стиль является наиболее приемлемым
для
широчайшего круга приложений; действительно, эта парадигма часто служит архитектурным фундаментом, на котором
основываются другие парадигмы.
Каждый стиль программирования имеет свою концептуальную базу. Каждый стиль требует своего умонастроения и
способа
восприятия решаемой задачи. Для объектно-ориентированного стиля концептуальная база - это объектная
модель.
Она имеет четыре главных элемента:
- абстрагирование;
- инкапсуляция;
- модульность;
- иерархия.
Эти элементы являются главными в том смысле, что без любого из них модель не будет объектно-ориентированной. Кроме главных, имеются еще три дополнительных элемента:
- типизация;
- параллелизм;
- сохраняемость.
Называя их дополнительными, имеется в виду, что они полезны в объектной модели, но не обязательны.
Абстракция выделяет существенные характеристики некоторого объекта, отличающие его
от
всех
других видов объектов и, таким образом, четко определяет его концептуальные границы с точки зрения
наблюдателя.
Абстракция основывается на понятиях клиента и сервера.
Клиентом называется любой объект, использующий ресурсы другого объекта (называемого сервером).
Мы будем характеризовать поведение объекта услугами, которые он оказывает другим объектам, и операциями, которые
он
выполняет над другими объектами. Такой подход концентрирует внимание на внешних проявлениях объекта и приводит к
идее
контрактной модели программирования, когда внешнее проявление объекта рассматривается с точки зрения
его
контракта с другими объектами, в соответствии с этим должно быть выполнено и его внутреннее устройство (часто во
взаимодействии с другими объектами). Контракт фиксирует все обязательства, которые объект-сервер имеет перед
объектом-клиентом. Другими словами, этот контракт определяет ответственность объекта, то есть то
поведение,
за которое он отвечает.
Каждая операция, предусмотренная этим контрактом, однозначно определяется ее формальными параметрами и типом
возвращаемого значения. Полный набор операций, которые клиент может осуществлять над другим объектом, вместе с
правильным порядком, в котором эти операции вызываются, называется протоколом. Протокол отражает все
возможные способы, которыми объект может действовать или подвергаться воздействию. Он полностью определяет, тем
самым,
внешнее поведение абстракции со статической и динамической точек зрения.
Инкапсуляция - это процесс отделения друг от друга элементов объекта, определяющих
его
устройство и поведение. Инкапсуляция служит для того, чтобы изолировать контрактные обязательства абстракции от
их
реализации.
Абстракция и инкапсуляция дополняют друг друга: абстрагирование направлено на наблюдаемое поведение объекта, а
инкапсуляция занимается внутренним устройством. Чаще всего инкапсуляция выполняется посредством скрытия
информации,
то
есть маскировкой всех внутренних деталей, не влияющих на внешнее поведение. Обычно скрываются и внутренняя
структура
объекта, и реализация его методов. Практически это означает наличие двух частей в классе: интерфейса и
реализации.
Интерфейс отражает внешнее поведение объекта, описывая абстракцию поведения всех
объектов
данного класса. Внутренняя реализация описывает представление этой абстракции и
механизмы
достижения желаемого поведения объекта. Принцип разделения интерфейса и реализации соответствует сути вещей: в
интерфейсной части собрано все, что касается взаимодействия данного объекта с любыми другими объектами;
реализация
скрывает от других объектов все детали, не имеющие отношения к процессу взаимодействия объектов.
Модульность - это свойство системы, которая была разложена на внутренне связные, но
слабо
связанные между собой модули.
В процессе разделения системы на модули могут быть полезными два правила. Во-первых, поскольку модули служат в
качестве элементарных и неделимых блоков программы, которые могут использоваться в системе повторно,
распределение
классов и объектов по модулям должно учитывать это. Во-вторых, многие компиляторы создают отдельный сегмент кода
для
каждого модуля. Поэтому могут появиться ограничения на размер модуля. Динамика вызовов подпрограмм и
расположение
описаний внутри модулей может сильно повлиять на локальность ссылок и на управление страницами виртуальной
памяти.
При
плохом разбиении процедур по модулям учащаются взаимные вызовы между сегментами, что приводит к потере
эффективности
кэш-памяти и частой смене страниц.
Свести воедино столь разноречивые требования довольно трудно, но главное – это уяснить, что вычленение классов и
объектов в проекте и организация модульной структуры есть независимые действия. Процесс вычленения
классов
и
объектов составляет часть процесса логического проектирования системы, а деление на модули - этап физического
проектирования. Разумеется, иногда невозможно завершить логическое проектирование системы, не завершив
физическое
проектирование, и наоборот. Два этих процесса выполняются итеративно.
Иерархия - это упорядочение абстракций, расположение их по уровням.
Основными видами иерархических структур применительно к сложным системам являются структура классов (иерархия
"is-a")
и структура объектов (иерархия "part of").
Важным элементом объектно-ориентированных систем и основным видом иерархии "is-a" является
упоминавшаяся
выше концепция наследования. Наследование означает такое отношение между классами (отношение родитель/потомок),
когда
один класс заимствует структурную или функциональную часть одного или нескольких других классов (соответственно,
одиночное
и множественное наследование). Иными словами, наследование создает такую иерархию абстракций, в которой
подклассы наследуют строение от одного или нескольких суперклассов. Часто подкласс достраивает или переписывает
компоненты вышестоящего класса.
Если иерархия "is а" определяет отношение "обобщение/специализация", то отношение "part
of"
(часть) вводит иерархию агрегации. В иерархии "part of" класс находится на более высоком уровне
абстракции,
чем любой из использовавшихся при его реализации.
Типизация - это способ защититься от использования объектов одного класса вместо другого, или по крайней мере управлять таким использованием.
Параллелизм - это свойство, отличающее активные объекты от пассивных.
Сохраняемость - способность объекта существовать во времени, переживая породивший его процесс, и (или) в пространстве, перемещаясь из своего первоначального адресного пространства.
Расширение языка С
Прототипы функций. Перегрузка функций. Значения формальных параметров по умолчанию. Ссылки и
параметры-ссылки. Объявления переменных. Встраиваемые функции. Операции new и delete
Прототипы функций. При обращении к функции, формальные параметры заменяются фактическими,
причем
соблюдается строгое соответствие параметров по типам. В отличие от своего предшественника - языка Си Си++ не
предусматривает автоматического преобразования в тех случаях, когда фактические параметры не совпадают по типам
с
соответствующими им формальными параметрами. Говорят, что язык Си++ обеспечивает «строгий контроль типов». В
связи с
этой особенностью языка Си++ проверка соответствия типов формальных и фактических параметров выполняется на
этапе
компиляции.
Строгое согласование по типам между формальными и фактическими параметрами требует, чтобы в модуле до первого
обращения к функции было помещено либо ее определение, либо ее описание (прототип), содержащее сведения о ее
типе (о
типе результата, т.е. возвращаемого значения) и о типах всех параметров. Именно наличие такого прототипа либо
полного
определения позволяет компилятору выполнять контроль соответствия типов параметров. Прототип (описание) функции
может
внешне почти полностью совпадать с заголовком ее определения:
тип_функции имя_функции (спецификация_формальных_параметров);
Основное различие - точка с запятой в конце описания (прототипа). Второе отличие - необязательность имен
формальных
параметров в прототипе даже тогда, когда они есть в заголовке определения функции.
Перегрузка функций. Цель перегрузки функций состоит в том, чтобы функция с одним именем
по-разному
выполнялась и возвращала разные значения при обращении к ней с разными по типам и количеству фактическими
параметрами.
Например, может потребоваться функция, возвращающая максимальное значение элементов одномерного массива,
передаваемого
ей в качестве параметра. Массивы, использованные как фактические параметры, могут содержать элементы разных
типов,
но
пользователь функции не должен беспокоиться о типе результата. Функция всегда должна возвращать значение того же
типа,
что и тип массива - фактического параметра.
Для обеспечения перегрузки функций необходимо для каждого имени определить, сколько разных функций связано с
ним,
т.е.
сколько вариантов сигнатур допустимы при обращении к ним. Предположим, что функция выбора максимального значения
элемента из массива должна работать для массивов типа int, long, float, double. В этом случае
придется написать четыре разных варианта функции с одним и тем же именем.
Распознавание перегруженных функций при вызове выполняется по их сигнатурам. Поэтому перегруженные функции
должны
иметь одинаковые имена, но спецификации их параметров должны различаться по количеству и (или) по типам, и (или)
по
расположению.
При использовании перегруженных функций нужно с осторожностью задавать начальные значения их параметров.
Значения формальных параметров по умолчанию. Спецификация формальных параметров - это либо
пусто,
либо void, либо список спецификаций отдельных параметров, в конце которого может быть поставлено многоточие.
Спецификация каждого параметра в определении функции имеет вид:
тип имя_параметра
тип имя_параметра = умалчиваемое_значение
Как следует из формата, для параметра может быть задано (а может отсутствовать) умалчиваемое значение. Это
значение
используется в том случае, если при обращении к функции соответствующий параметр опущен. При задании начальных
(умалчиваемых) значений должно соблюдаться следующее соглашение. Если параметр имеет умалчиваемое значение, то
все
параметры, специфицированные справа от него, также должны иметь начальные значения.
Ссылки и параметры-ссылки. В языке Си++ ссылка определена как другое имя уже соответствующего
объекта. Основные достоинства ссылок проявляются при работе с функциями, однако ссылки могут использоваться и
безотносительно к функциям. Для определения ссылки используется символ *, если он
употребляется в
таком контексте:
type &имя_ссылки = инициализатор;
В соответствии с синтаксисом инициализатора, наличие которого обязательно, определение ссылки может быть
таким:
type &имя_ссылки = выражение;
или
type &имя_ссылки (выражение);
Раз ссылка есть «другое имя уже существующего объекта», то в качестве инициализирующего выражения должно
выступать
имеющее значение леводопустимое выражение, т.е. имя некоторого объекта, имеющего место в памяти. Значением
ссылки
после определения с инициализацией становиться адрес этого объекта. Примеры определений ссылок:
int L = 777; // Определена и инициализирована переменная L int &RL = L; // Значением ссылки RL является адрес переменной L int &RI(0); // Опасная инициализация - значением ссылки RI // становится адрес объекта, в котором // временно размещено нулевое целое значение
В определении ссылки символ «&» не является частью типа, т.е. RL или RI имеют
тип int и именно так должны восприниматься в программе.
Итак, имя_ссылки определяет местоположение в памяти инициализирующего выражения, то есть
значением
ссылки является адрес объекта, связанного с инициализирующим выражением.
Функционально ссылка ведет себя подобно обычной переменной, того же, что и ссылка, типа. Для доступа к
содержимому
участка памяти, на который «смотрит» ссылка, нет необходимости явно выполнять разыменование, как это нужно для
указателя. Если рассматривать переменную как пару «имя_переменной - значение_переменной», то
инициализированная этой переменной ссылка может быть представлена парой «имя_ссылки -
значение_переменной». Из этого становится понятной необходимость инициализации ссылок при их
определении.
Тут ссылки схожи с константами языка Си++. Раз ссылка есть имя, связанное со значением (объектом), уже
размещенным в
памяти, то, определяя ссылку, необходимо с помощью начального значения определить тот объект (тот участок
памяти),
на
который указывает ссылка.
После определения с инициализацией имя_ссылки становится еще одним именем (синонимом,
псевдонимом,
алиасом) уже существующего объекта. Таким образом для нашего примера оператор
RL -= 77;
уменьшает на 77 значение переменной L. Связав ссылку (RL) с переменной (L), мы получаем две
возможности изменять значение переменной:
RL = 88;
или
L = 88;
Здесь есть аналогия с указателями, однако отсутствует необходимость в явном разыменовании, что обязательно при
обращении к значению переменной через указатель.
Ссылки не есть полноправные объекты, подобные переменным, либо указателям. После инициализации значение ссылки
изменить нельзя, она всегда (смотрит) на тот участок памяти (на тот объект), с которым она связана
инициализацией.
Ни
одна из операций не действует на ссылку, а относится к тому объекту, с которым она связана. Можно считать, что
это
основное свойство ссылки. Таким образом, ссылка полностью аналогична исходному имени объекта. Конкретизируем и
поясним
сказанное. Пусть определены:
double a[] = { 10.0, 20.0, 30.0, 40.0 } ; // a - массив
double *pa = a; // pa - указатель на массив
double &ra = a[0]; // ra - ссылка на первый элемент массива
double *&rpd = a; // Ссылка на указатель (на имя массива)
Для ссылок и указателей из нашего примера соблюдаются равенства: pa == &ra, *pa == ra == a[0], rpd == a.
Применив к ссылке операцию получения адреса &, определим не адрес ссылки, а адрес того объекта, которым
инициализирована ссылка. Можно рассмотреть и другие операции, но вывод один - каждая операция над ссылкой
является
операцией над тем объектом, с которым она связана.
Так как ссылки не есть настоящие объекты, то существуют ограничения при определении и использовании ссылок.
Во-первых,
ссылка не может иметь тип void, т.е. определение void
имя_ссылки запрещено. Ссылку нельзя создать с помощью операции
new,
т.е.
для ссылки нельзя выделить новый участок памяти. Не определены ссылки на другие ссылки. Нет указателей на ссылки
и
невозможно создать массив ссылок.
Параметры ссылки. В качестве основных причин включения ссылок в язык СИ++ указывают необходимость повысить эффективность обмена с функциями через аппарат параметров и целесообразность возможности использовать вызов функции в качестве леводопустимого значения. При использовании ссылки в качестве формального параметра обеспечивается доступ из тела функции к соответствующему фактическому параметру, т.е. к участку памяти, выделенному для фактического параметра. При этом параметр - ссылка обеспечивает те же самые возможности, что и параметр - указатель. Отличия состоят в том, что в теле функции для параметра - ссылки не нужно применять операцию разыменования *, а фактическим параметром должен быть не адрес (как для параметра - указателя), а обычная переменная.
Ссылки обеспечивают доступ из тела функции к фактическим параметрам, в качестве которых используются обычные переменные, определенные в вызывающей программе.
В спецификации ссылки как формального параметра инициализация необязательна, однако она не запрещена. Сложность состоит в том, что объект, имя которого используется для инициализации параметра - ссылки, должен быть известен при определении функции. Иначе параметр должен быть ссылкой на константу, и с его помощью можно будет передавать значения только внутрь функции, а не из нее.
Подобно указателю на функцию определяется и ссылка на функцию:
тип_функции (&имя_ссылки)(спецификация_параметров) =
инициализирующее_выражение;
Здесь тип_функции - это тип возвращаемого функцией значения,
спецификация_параметров определяет сигнатуру функций, допустимых для ссылки,
инициализирующее_выражение - включает имя уже известной функции, имеющей тот же тип и ту же
сигнатуру, что и определяемая ссылка. Например,
int infunc(float, int);
// Прототип функции
int (&iref)(float, int) = infunc;
// Определение ссылки
iref - ссылка на функцию, возвращающую значение типа int и имеющую два параметра с типами float
и
int. Напомним, что использование имени функции без скобок (и без параметров) воспринимается как адрес
функции.
Ссылка на функцию обладает всеми правами основного имени функции, т.е. является его синонимом (псевдонимом).
Изменить
значение ссылки на функцию невозможно, поэтому указатели на функции имеют гораздо большую сферу применения, чем
ссылки.
Классы
Функции-члены (методы) и данные-члены. Интерфейсы и реализация. Конструкторы и инициализация. Конструктор без параметров (по умолчанию). Деструкторы и очистка. Конструктор копирования. Указатель this. Статические члены: функции и данные. Указатели на члены. Структуры и объединения. Константные члены-функции и константные объекты.
Структуру классического Си можно рассматривать, как предшественницу класса. Объединяя программный код с данными, структура может служить элементарной формой класса.
Рассмотрим реализацию понятия даты с использованием struct для того, чтобы определить представление даты date и множества функций для работы с переменными этого типа:
struct date {
int day; // день
int month; // месяц
int year; // год
} today;
void set_date(date*, int, int, int);
void next_date(date*);
void print_date(date*);
// ...
Функции-члены и данные-члены. Никакой явной связи между функциями и типом данных в этом примере нет. Такую связь можно установить, описав функции как члены структуры. Эти функции могут действовать на данные, содержащие в самой структуре. По умолчанию при объявлении структуры ее данные и функции являются общими, то есть, у объектов типа структура нет ни инкапсуляции, ни защиты данных:
struct date {
int day; // день
int month; // месяц
int year; // год
void set(int, int, int);
void get(int*, int*, int*);
void next();
void print();
};
Функции, описанные таким образом, называются функциями - членами и могут вызываться только для специальной переменной соответствующего типа с использованием стандартного синтаксиса для доступа к данным - членам структуры. Например:
date today; // сегодня
date my_burthday; // мой день рождения
void f() {
my_burthday.set(30,12,1950);
today.set(18,1,1985);
my_burthday.print();
today.next();
}
Поскольку разные структуры могут иметь функции члены с одинаковыми именами, при определении функции члена необходимо указывать имя структуры, связывая их с помощью оператора видимости ::
void date::next() {
if(++day > 28) {
// делает сложную часть работы
}
}
В функции члене имена членов могут использоваться без явной ссылки на объект. В этом случае имя относится к члену того объекта, для которого функция была вызвана.
Интерфейсы и реализация. Описание date в предыдущем примере дает множество функций для работы с date, но не указывает, что эти функции должны быть единственными для доступа к объектам типа date. Это ограничение можно наложить, используя вместо struct class:
class date {
int day;
int month;
int year;
public:
void set(int, int, int);
void get(int*, int*, int*);
void next();
void print();
};
Метка public делит тело класса на две части. Имена в первой, закрытой части (private), могут использоваться только функциями членами. Вторая, открытая часть, составляет интерфейс к объекту класса. Обе эти части составляют реализацию объекта. struct - это просто class, у которого все члены общие (public), поэтому функции члены определяются и используются точно так же, как в предыдущем случае. Описание date в предыдущем примере дает множество функций для работы с date, но не указывает, что эти функции должны быть единственными для доступа к объектам типа date. Это ограничение можно наложить, используя вместо struct class:
Личная часть класса не обязательно должна следовать в начале определения класса. Для обозначения отношения элементов структуры к личной части в произвольном месте определения класса перед ними можно использовать служебное слово private. Стандартным является размещение элементов данных в личной части, а функций-элементов - в общей части класса. Тогда закрытая личная часть определяет данные объекта, а функции-элементы общей части образуют интерфейс объекта "к внешнему миру" (методы).
Конструкторы и инициализация. Использование для обеспечения инициализации объекта класса функций вроде set_date() (установить дату) неэлегантно и чревато ошибками. Поскольку нигде не утверждается, что объект должен быть инициализирован, то программист может забыть это сделать, или (что приводит, как правило, к столь же разрушительным последствиям) сделать это дважды. Есть более хороший подход: дать возможность программисту описать функцию, явно предназначенную для инициализации объектов. Поскольку такая функция конструирует значения данного типа, она называется конструктором. Конструктор распознается по тому, что имеет то же имя, что и сам класс.
class date { // Класс date
date(int, int, int); // Конструктор
};
Когда класс имеет конструктор, все объекты этого класса будут инициализироваться. Если для конструктора нужны параметры, они должны даваться:
date today = date(23,6,1983); date xmas(25,12,0); // сокращенная форма (xmas - рождество) date my_burthday; // недопустимо, опущена инициализация
Часто бывает нужно обеспечить несколько способов инициализации объекта класса. Для этого надо задать несколько конструкторов:
class date {
int day;
int month;
int year;
public:
// ...
date(int, int, int); // день месяц год
date(char*); // дата в виде строки
date(int); // день, месяц и год сегодняшние
date(); // дата по умолчанию: сегодня
};
Конструкторы подчиняются тем же правилам относительно типов параметров, что и перегруженные функции. Если конструкторы существенно различаются по типам своих параметров, то компилятор при каждом использовании может выбрать правильный:
date today(4);
date july4("Июль 4, 1983");
date guy("5 Ноя");
date now; // инициализируется по умолчанию: скобки указывать не обязательно!
Конструктор без параметров (по умолчанию). Размножение конструкторов в примере с date типично. При разработке класса всегда есть соблазн обеспечить "все", поскольку кажется проще обеспечить какое-нибудь средство просто на случай, что оно кому-то понадобится или потому, что оно изящно выглядит, чем решить, что же нужно на самом деле. Последнее требует больших размышлений, но обычно приводит к программам, которые меньше по размеру и более понятны. Один из способов сократить число родственных функций - использовать параметры по умолчанию. В случае date для каждого параметра можно задать значение по умолчанию, интерпретируемое как "по умолчанию принимать: today" (сегодня).
class date {
int day;
int month;
int year;
public:
// ...
date(int d = 0, int m = 0, int y = 0);
date(char*); // дата в виде строки
};
// Реализация конструктора
date::date(int d, int m, int y) {
day = d ? d : today.day;
month = m ? m : today.month;
year = y ? y : today.year;
// проверка, что дата допустимая
// ...
}
Когда используется значение параметра, указывающее "брать по умолчанию", выбранное значение должно лежать вне множества возможных значений параметра. Для дня day и месяца month ясно, что это так, но для года year выбор нуля неочевиден. К счастью, в европейском календаре нет нулевого года . Сразу после 1 г. до н.э. (year=-1) идет 1 г. н.э. (year=1).
Объект класса без конструкторов можно инициализировать путем присваивания ему другого объекта этого класса. Это можно делать и тогда, когда конструкторы описаны.
date d = today; // инициализация посредством присваивания
По существу, имеется конструктор по умолчанию, определенный как побитовая копия объекта того же класса. Если для класса X такой конструктор по умолчанию нежелателен, его можно переопределить конструктором с именем X(X&).
Деструкторы и очистка. Определяемый пользователем тип чаще имеет, чем не имеет, конструктор, который обеспечивает надлежащую инициализацию. Для многих типов также требуется обратное действие, деструктор, чтобы обеспечить соответствующую очистку объектов этого типа. Имя деструктора для класса X есть ~X() ("дополнение конструктора"). В частности, многие типы используют некоторый объем памяти из свободной памяти, который выделяется конструктором и освобождается деструктором. Заметим, что в Си++ для этого используются операторы new и delete. Пример конструктора и деструктора объекта date:
class date {
int *day; // Указатель на int
int *month;
int *year;
public:
// Конструктор
date(int d, int m, int y) {
// Отводим динамическую память
day = new int;
month = new int;
year = new int;
// Присваиваем значения полям
*day = d ? d : 1;
*month = m ? m : 1;
*year = y ? y : 1;
};
...
// Деструктор
~date() {
delete day; // Освобождаем динамическую память
delete month;
delete year;
};
};
Конструктор копирования. Как правило, при создании объекта вызывается конструктор, за исключением случая, когда объект создается как копия другого объекта этого же класса, например: date date2 = date1;
Однако имеются случаи, в которых создание объекта без вызова конструктора осуществляется неявно:
- формальный параметр - объект, передаваемый по значению, создается в стеке в момент вызова функции и инициализируется копией фактического параметра;
- результат функции - объект, передаваемый по значению, в момент выполнения оператора return копируется во временный объект, сохраняющий результат функции.
Во всех этих случаях транслятор не вызывает конструктора для вновь создаваемого объекта:
- date2 в приведенном определении;
- создаваемого в стеке формального параметра;
- временного объекта, сохраняющего значение, возвращаемое функцией.
Вместо этого в них копируется содержимое объекта-источника:
- date1 в приведенном примере;
- фактического параметра;
- объекта - результата в операторе return.
При наличии в объекте указателей на динамические переменные и массивы или идентификаторов связанных ресурсов, такое копирование требует дублирования этих переменных или ресурсов в объекте-приемнике, как это было сделано выше в операции присваивания. С этой целью вводится конструктор копирования, который автоматически вызывается во всех перечисленных случаях. Он имеет единственный параметр - ссылку на объект-источник:
#include <iostream>
#include <string.h> // strlen
class string {
char *Str;
int size;
public:
string(string&); // Конструктор копирования
string(const char* str){
size = strlen(str);
Str = new char[size + 1]; // Добавляем 1,
// потому что в строке символ 0 - окончание строки
strcpy(Str, str);
}
void show(){
std::cout << Str << std::endl;
}
};
// Создает копии динамических переменных и ресурсов
string::string(string& right) {
size = right.size;
Str = new char[size + 1];
strcpy(Str, right.Str); // strcpy - копирование строки
}
int main() {
string a("Test");
string b = a; // Используется конструктор копирования
a.show();
b.show();
return 0;
}
Конструктор копирования обязателен, если в программе используются функции-элементы и переопределенные операции, которые получают формальные параметры и возвращают в качестве результата такой объект не по ссылке, а по значению.
Указатель this. В методе на поля объекта, для которого она была вызвана, можно ссылаться непосредственно.
class X {
int m;
public:
int readm() { return m; };
};
X aa; // Экземпляр класса X
X bb; // Экземпляр класса X
void f() {
int a = aa.readm();
int b = bb.readm();
// ...
};
В первом вызове члена readm() m относится к aa.m, а во втором - к bb.m.
Указатель на объект, для которого вызвана функция-член, является скрытым параметром функции. На этот неявный параметр можно ссылаться явно как на this. В каждой функции класса x указатель this неявно описан как
X *this;
и инициализирован так, что он указывает на объект, для которого была вызвана функция член. this не может быть описан явно, так как это ключевое слово. Класс x можно эквивалентным образом описать так:
class X {
int m;
public:
int readm() { return this->m; };
};
При ссылке на члены использование this излишне. Главным образом this используется при написании функций членов, которые манипулируют непосредственно указателями.
Статические члены: функции и данные. Класс - это тип, а не объект данных, и в каждом объекте класса имеется своя собственная копия данных, членов этого класса. Однако некоторые типы наиболее элегантно реализуются, если все объекты этого типа могут совместно использовать (разделять) некоторые данные. Предпочтительно, чтобы такие разделяемые данные были описаны как часть класса.
Иногда требуется определить данные, которые относятся ко всем объектам класса. Типичные случаи: требуется контроль общего количества объектов класса или одновременный доступ ко всем объектам или части их, разделение объектами общих ресурсов. Тогда в определение класса могут быть введены статические элементы - переменные. Такой элемент сам в объекты класса не входит, зато при обращении к нему формируется обращение к общей статической переменной с именем имя_класса::имя_элемента
Доступность ее определяется стандартным образом в зависимости от размещения в личной или общей части класса. Сама переменная должна быть явно определена в программе и инициализирована:
#include <stdio.h>
class dat {
int day, month, year;
static dat *fst; // Указатель на первый элемент
dat *next; // Указатель на следующий элемент
public:
void show(); // Просмотр всех объектов
dat(); // Конструктор
~dat(); // Деструктор
};
dat *dat::fst = NULL; // Определение статического элемента
void dat::show() {
dat *p;
for (p = fst; p != NULL; p = p->next)
{ /* вывод информации об объекте */ }
};
//------ Конструктор - включение в начало списка -------
dat::dat()
{ /* ... */ next = fst; fst = this; }
//------ Деструктор - поиск и исключение из списка ------
dat::~dat()
{
dat *&p = fst; // Ссылка на указатель на
// текущий элемент списка
for (; p != NULL; p = p->next)
if (p == this) { // Найден - исключить и...
p = p->next;
return; // ...выйти
};
};
В данном примере используется ссылки на указатель текущего элемента списка (неявный указатель на указатель текущего элемента списка).
Статическими могут быть объявлены также и функции-элементы. Их "статичность" определяется тем, что вызов их не связан с конкретным объектом и может быть выполнен по полному имени. Соответственно в них не используются неявный указатель на текущий объект this. Они вводятся, как правило, для выполнения действий, относящихся ко всем объектам класса. Для предыдущего примера функция просмотра всех объектов класса может быть статической:
class list {
...
static void show(); // Стaтическая функция просмотра
}; // списка объектов
void list::show() {
list *p;
for (p = fst; p !=NULL; p=p->next)
{ ...вывод информации об объекте... };
};
int main() {
...
list::show(); // Вызов функции по полному имени
};