ООП на C++
С++ — гибридный язык. Ядро языка развилось из С и используется в качестве классического языка для системного программирования. Поэтому C++ весьма подходит для написания эффективного кода. Дополнения языка на основе классов соответствуют набору требований ООП. В этом качестве C++ подходит для написания многократно используемых библиотек и поддерживает полиморфный стиль программирования.
Объектно-ориентированное программирование (ООП) и C++ были очень быстро приняты индустрией программного обеспечения. C++ является гибридным языком объектно-ориентированного программирования. Он предоставляет многоцелевой подход к программированию. Сохранены традиционные преимущества С как эффективного мощного языка программирования. Новыми ключевыми ингредиентами являются наследование и полиморфизм, то есть способность принимать множество форм.
Требования к языку ООП
Характеристики языка ООП
• Инкапсуляция с сокрытием данных: возможность отделять внутреннее состояние и поведение объекта от его внешнего состояния и поведения.
• Расширяемость типов: возможность добавлять определяемые пользователем типы для расширения набора собственных типов.
• Наследование: возможность создавать новые типы, импортируя или используя повторно описания существующих типов.
• Полиморфизм и динамическое связывание: способность объектов отвечать за интерпретацию вызовов функций.
Эти особенности не могут заменить ни дисциплину программирования1, ни соглашения, принятые сообществом разработчиков, но могут способствовать их развитию.
Типичные процедурные языки программирования, такие как Pascal и С, обладают ограниченными формами расширяемости типов и инкапсуляции. Оба языка имеют типы указателей и записей, предоставляющие данные свойства. Дополнительно в С имеется специальная ориентированная на файлы схема "закрытости" (privacy) на основе объявлений static в области видимости файла. Такие языки, как Modula-2 и Ada, имеют более законченные формы инкапсуляции, а именно, модули и пакеты соответственно. Эти языки позволяют пользователям легко строить АТД и предоставляют значительную библиотечную поддержку для множества прикладных областей. Такой язык, как чистый LISP, поддерживает динамическое связывание. Элементы ООП были доступны в разных языках на протяжении по крайней мере двадцати пяти лет.
LISP, Simula и Smalltalk долгое время широко использовались и в академических, и в исследовательских кругах. Эти языки во многих случаях более элегантны, чем С и C++. Однако до тех пор пока элементы ООП не были добавлены в С, не делалось никаких существенных шагов к использованию ООП в индустрии программного обеспечения. В конце восьмидесятых с триумфом был принят C++, и в этом принятии нового языка были едины компании, целые направления и прикладные области. Мы полагаем, что сфера производства программного обеспечения нуждалась в объединении ООП и возможности эффективного программирования на низком уровне.
Существенной была также легкость перехода от С к C++. В отличие от PL/1, который служил истоком для FORTRAN и COBOL, и Ada, из которого вышел Pascal, для C++ язык С является почти собственным подмножеством. А раз так, от основного установленного кода на С не надо отказываться. Для остальных языков необходим нетривиальный процесс преобразования для изменения существующего кода на языке-предке.
Традиционная академическая мудрость состоит в том, что чрезмерная забота об эффективности вредит хорошему программированию. Такая установка упускает из виду тот очевидный факт, что конкурентоспособность продукта основана на производительности. Соответственно, индустрия ценит технологии низкого уровня. Так что C++ — весьма эффективный инструмент.
АТД в не-ООП языках
Существующие языки и методики поддерживают большую часть методологии ООП с помощью комбинирования свойств языка и дисциплины программирования. Дисциплина программирования и общепринятые в сообществе разработчиков соглашения действительно работают. В не-ООП языке возможно создание и использование АТД. Тремя примерами на С служат псевдотипы: строковый, булевский и файловый. Они являются псевдотипами (а не типами) в том смысле, что не обладают теми же привилегиями, что и собственные типы. Глядя на эти примеры можно лучше понять ограничения расширяемости типов в не-ООП контексте.
Булевский тип в С представлен неявно. А именно, логические выражения принимают нулевые значения за false, а ненулевые — за true. Поскольку ноль является универсальным значением, которое доступно для всех типов, ноль по соглашению используется в качестве «охранного» (sentinel) значения. Ноль, используемый для представления конца списка, является идиомой при обработке данных, основанной на указателях.
while (р) { //р == 0 нулевой указатель (NULL)
• • • • • //обработка списка
р = р -> next; //переход
}
Часто для обеспечения лучшей документированности явно используются перечислимые типы:
enum boolean { FALSE, TRUE };
boolean search (int table[], int x, int & where)
{
where = -1;
for (int i = 0; i < N; ++i)
if (x = = table[i]) {
where = i ;
break;
}
return boolean (where != -1);
}
Строковый тип — это комбинация дисциплины программирования и общепринятых соглашений, представленная в библиотеке string.h. Эта библиотека применима к типу указателя на символ. Конец строки — опять нулевое значение. Конкатенация, копирование, выяснение длины и другие операции представлены функциями из string.h. Мерой успеха библиотеки является частота фактического использования С для создания приложений, в которых применяется обработка строк.
Файловый тип основан на использовании stdio.h. Тип структуры (зависящий от системы) определен с именем FILE. В stdio.h представлены такие функции, как открытие, закрытие и поиск файла. Эти процедуры ожидают в качестве параметров указатели на файл. Конкретные члены структуры не подвергаются непосредственным манипуляциям, если программист придерживается данных соглашений. Опять-таки, С весьма успешно применялся для написания операционных систем и кода, манипулирующего файлами.
Подобные удачи не утверждают статус-кво. Напротив, они «выступают» за встраивание ООП в язык таким образом, чтобы гарантировалось, что библиотечные соглашения не нарушатся.
Заметьте, что в C++ имеется булевский тип, а именно bool; стандартный строковый тип string, определенный в файле стандартной библиотеки string, и более удачная обработка файлов, определенная с помощью fstream.
Клиенты и производители
Чтобы полностью понять парадигму ООП, мы должны рассмотреть весь процесс программирования (кодирования) как занятие, ответственность за которое разделяется и распределяется. Мы используем термин клиент (client) для обозначения пользователя класса и термин производитель (manufacturer) — для поставщика класса.
Клиент класса предполагает некое приближенное соответствие какой-то
абстракции. Стек, чтобы его можно было использовать, должен быть приемлемого размера. Комплексное число должно быть представлено с разумной точностью. Колода должна тасоваться так, чтобы при раздаче карты распределялись случайным образом. Внутренние особенности вычисления поведения всех этих объектов не являются непосредственной заботой клиента. Клиент имеет дело с такими вещами, как стоимость, эффективность и легкость использования, но не с реализацией. Это — принцип черного ящика.
Черный ящик в понимании клиента
• простой в использовании, легко понимаемый и привычный
• дешевый, эффективный и мощный
• может быть составной частью системы (может восприниматься системой как компонент)
Черный ящик в понимании производителя
• легко повторно использовать и изменять, но трудно использовать неправильно и сложно воспроизвести
• дешевый, эффективный и мощный
• выгодный для производства с большой базой клиентов
Производитель сражается за клиента, реализуя АТД-продукт, имеющий приемлемую цену и эффективность. Производитель заинтересован в сокрытии деталей разработки. Такое сокрытие значительно упрощает все то, что производитель должен «объяснять»» клиенту. Это дает ему свободу для дальнейших внутренних усовершенствований продукта, которые не влияют на особенности использования продукта клиентом. Оно защищает клиента от опасного, пусть даже непреднамеренного искажения продукта.1
Структуры и обычные функции в С позволяют строить полезные АТД, но они не поддерживают разграничения между клиентом и производителем. Клиент имеет доступ к внутренним деталям и может модифицировать их нежелательным образом. Рассмотрим стек, представленный в качестве массива с целой переменной top. В С клиент такого стека может извлечь внутренний член массива, используемого для представления стека. Это нарушает абстракцию LIFO (Last-In-First-Out, первым вошел — последним вышел), которую реализует стек.
Инкапсуляция объектов предотвращает подобные нарушения. Схема сокрытия данных, которая ограничивает доступ к деталям реализации для всех, кроме производителя, гарантирует клиенту соответствие абстракции АТД. Закрытые члены скрыты от клиентской программы, а открытые доступны ей. Можно изменить представление скрытых данных, но не доступ к открытым членам и их функциональность. Если сокрытие данных произведено правильно, клиентская программа не нуждается в изменениях, когда модифицируется представление скрытых данных.
Двумя ключами к выполнению условий черного ящика служат наследование и полиморфизм.
Повторное использование кода и наследование
Создание библиотек и их повторное использование являются решающими показателями успеха стратегии языка. Наследование, или производство нового класса от существующего, применяется как для совместного и повторного использования кода, так и для разработки иерархий типов. Посредством наследования может быть создана иерархия родственных АТД, которые разделяют код и общий интерфейс. Это свойство исключительно важно для обеспечения возможности повторно использовать код.
Наследование влияет на разработку программного обеспечения в целом. Оно предоставляет каркас, в котором фиксируются концептуальные элементы, ставшие предметом пристального внимания припостроении и использовании систем. Например, InterView представляет собой библиотеку C++, которая поддерживает построение графического интерфейса пользователя. Главные категории объектов включают интерактивные объекты, текстовые объекты и графические объекты. Эти категории легко комбинируются, позволяя создавать различные приложения, например, системы автоматизированного проектирования — САПР (computer-aided design — CAD), броузеры или редакторы WYSIWYG1.
Методология объектно-ориентированного проектирования
1. Выбери надлежащую совокупность АТД.
2. Спроектируй взаимосвязи между АТД, применяя наследование для использования общего кода и интерфейса.
3. Применяй виртуальные функции для обработки родственных объектов динамически.
Наследование также способствует принципу черного ящика и является важным механизмом для сокрытия деталей. Будучи иерархическим, каждый предыдущий уровень предоставляет встроенную в него функциональность следующему уровню. Ретроспективно можно отметить, что методология структурного программирования с ее процедурно-направленным взглядом опиралась на метод пошагового уточнения для организации вложенных процедур, но не придавала достаточного значения необходимости соответствующего взгляда на данные.
Полиморфизм
Полиморфизм — это джин ООП, получающий от клиента приказы и верно интерпретирующий его желания. Полиморфная функция имеет множество форм. По Карделли и Вегнеру (Cardelli and Wegner, 1985) мы различаем:
Типы полиморфизма
1. Принудительное приведение (coercion) (ad hoc полиморфизм): функция или оператор работает с несколькими различными типами, преобразуя их значения к требуемому типу. Примером в ANSI С служит преобразование при присваивании арифметических типов во время вызова функции.
а / b //тип частного обуславливается
//собственными (встроенными в ядро) //принудительными приведениями
2. Перегрузка (ad hoc полиморфизм): функция вызывается на основе ее сигнатуры, то есть списка типов аргументов. В С оператор целого деления и оператор деления с плавающей точкой различаются. Выбор варианта оператора деления основан на типах параметров.
cout << а //перегрузка функции
3. Включение (inclusion) (чистый полиморфизм): тип является подтипом другого типа. Функция, пригодная для базового типа, будет работать и с подтипом. Такая функция может иметь различные реализации, которые вызываются с учетом выяснения подтипа на этапе выполнения. .
р -> draw ( ) //вызов виртуальной функции
4. Параметрический полиморфизм (чистый полиморфизм): тип остается неопределенным, а позже инстанцируется. В C++ это обеспечивается обобщенными указателями и шаблонами.
stack <window*> win[40]
Полиморфизм локализует ответственность за поведение. Часто клиентский код не нуждается в пересмотре, когда в систему включается дополнительная функциональность с помощью кода, предоставляемого производителем.
Полиморфизм непосредственно содействует принципу черного
ящика. Виртуальные функции, определенные для базового класса, являются интерфейсом, который повсеместно используется клиентом. Клиент знает, что замещенная функция-член несет ответственность за конкретную реализацию данного действия, относящегося к объекту. Клиенту не надо знать о разных процедурах, существующих для каждого вычисления или о различных вариантах спецификации. Эти детали скрыты.
Сложность языка
За все свои достоинства C++ платит существенную цену: сложность языка весьма ощутима. Это приводит к дополнительным затратам на обучение и едва уловимым ошибкам. Стремительное развитие C++ при столь широком его распространении практически беспрецедентно. Язык С — небольшой и элегантный. Синтаксис C++ похож на С, но семантика первого сложнее. Чтобы оценить эти трудности, в следующей таблице приведены для сравнения некоторые характеристики языков Pascal, Modula-2, Modula-3, C++ и Ada.
Сложность языка |
Язык Ключевые слова Инструкции Операторы Страницы |
Pascal 35 9 16 28 Modula-2 40 10 19 25 Modula-3 53 22 25 50 C 29 13 44 40 C++v1.0 42 14 47 66 C++v3.0 1990 48 14 52 155 Ada 1980 63 17 21 241 C++ ANSI 1995 62 15 54 650 |