- •1.2.1 Процедурное программирование
- •1.2.2 Модульное программирование
- •1.2.3 Абстракция данных
- •1.2.4 Пределы абстракции данных
- •1.2.5 Объектно-ориентированное программирование
- •1.3 "Улучшенный с"
- •1.3.1 Программа и стандартный вывод
- •1.3.2 Переменные и арифметические операции
- •1.3.3 Указатели и массивы
- •1.3.4 Условные операторы и циклы
- •1.3.5 Функции
- •1.3.6 Модули
- •1.4 Поддержка абстракции данных
- •1.4.1 Инициализация и удаление
- •1.4.2 Присваивание и инициализация
- •1.4.3 Шаблоны типа
- •1.4.4 Обработка особых ситуаций
- •1.4.5 Преобразования типов
- •1.4.6 Множественные реализации
- •1.5 Поддержка объектно-ориентированного программирования
- •1.5.1 Механизм вызова
- •1.5.2 Проверка типа
- •1.5.3 Множественное наследование
- •1.5.4 Инкапсуляция
- •1.6 Пределы совершенства
- •* Глава 2. Описания и константы
- •2.1 Описания
- •2.1.1 Область видимости
- •2.1.2 Объекты и адреса
- •2.1.3 Время жизни объектов
- •2.2 Имена
- •2.3 Типы
- •2.3.1 Основные типы
- •2.3.2 Неявное преобразование типа
- •2.3.3 Производные типы
- •2.3.5 Указатели
- •2.3.6 Массивы
- •2.3.7 Указатели и массивы
- •2.3.8 Структуры
- •2.3.9 Эквивалентность типов
- •2.3.10 Ссылки
- •2.4 Литералы
- •2.4.1 Целые константы
- •2.4.2 Константы с плавающей точкой
- •2.4.3 Символьные константы
- •2.4.4 Строки
- •2.4.5 Нуль
- •2.5 Поименованные константы
- •2.5.1. Перечисления
- •2.6. Экономия памяти
- •2.6.1 Поля
- •2.6.2. Объединения
- •2.7 Упражнения
- •* Глава 3. Выражения и операторы
- •3.1 Калькулятор
- •3.1.1 Анализатор
- •3.1.2 Функция ввода
- •3.1.3 Таблица имен
- •3.1.4 Обработка ошибок
- •3.1.5 Драйвер
- •3.1.6 Параметры командной строки
- •3.2 Сводка операций
- •3.2.1 Скобки
- •3.2.2 Порядок вычислений
- •3.2.3 Инкремент и декремент
- •3.2.4 Поразрядные логические операции
- •3.2.5 Преобразование типа
- •3.2.6 Свободная память
- •3.3 Сводка операторов
- •3.3.1 Выбирающие операторы
- •3.3.2 Оператор goto
- •3.4 Комментарии и расположение текста
- •3.5 Упражнения
- •* Глава 4
- •4.1 Введение
- •4.2 Связывание
- •4.3 Заголовочные файлы
- •4.3.1 Единственный заголовочный файл
- •4.3.2 Множественные заголовочные файлы
- •4.4 Связывание с программами на других языках
- •4.5 Как создать библиотеку
- •4.6 Функции
- •4.6.1 Описания функций
- •4.6.2 Определения функций
- •4.6.3 Передача параметров
- •4.6.4 Возвращаемое значение
- •4.6.5 Параметр-массив
- •4.6.6 Перегрузка имени функции
- •4.6.7 Стандартные значения параметров
- •4.6.8 Неопределенное число параметров
- •4.6.9 Указатель на функцию
- •4.7 Макросредства
- •4.8 Упражнения
- •* Глава 5. Классы
- •5.1 Введение и краткий обзор
- •5.2 Классы и члены
- •5.2.1 Функции-члены
- •5.2.2 Классы
- •5.2.3 Ссылка на себя
- •5.2.4 Инициализация
- •5.2.5 Удаление
- •5.2.6 Подстановка
- •5.3 Интерфейсы и реализации
- •5.3.1 Альтернативные реализации
- •5.3.2 Законченный пример класса
- •5.4 Еще о классах
- •5.4.1 Друзья
- •5.4.2 Уточнение имени члена
- •5.4.3 Вложенные классы
- •5.4.4 Статические члены
- •5.4.5 Указатели на члены
- •5.4.6 Структуры и объединения
- •5.5 Конструкторы и деструкторы
- •5.5.1 Локальные переменные
- •5.5.2 Статическая память
- •5.5.3 Свободная память
- •5.5.4 Объекты класса как члены
- •5.5.5 Массивы объектов класса
- •5.5.6 Небольшие объекты
- •5.6 Упражнения
- •* Глава 6
- •6.1 Введение и краткий обзор
- •6.2 Производные классы
- •6.2.1 Функции-члены
- •6.2.2 Конструкторы и деструкторы
- •6.2.3 Иерархия классов
- •6.2.4 Поля типа
- •6.2.5 Виртуальные функции
- •6.3 Абстрактные классы
- •6.4 Пример законченной программы
- •6.4.1 Монитор экрана
- •6.4.2 Библиотека фигур
- •6.4.3 Прикладная программа
- •6.5 Множественное наследование
- •6.5.1 Множественное вхождение базового класса
- •6.5.2 Разрешение неоднозначности
- •6.5.3 Виртуальные базовые классы
- •6.6 Контроль доступа
- •6.6.1 Защищенные члены
- •6.6.2 Доступ к базовым классам
- •6.7 Свободная память
- •6.7.1 Виртуальные конструкторы
- •6.7.2 Указание размещения
- •6.8 Упражнения
- •* Глава 7
- •7.1 Введение
- •7.2 Операторные функции
- •7.2.1 Бинарные и унарные операции
- •7.2.2 Предопределенные свойства операций
- •7.2.3 Операторные функции и пользовательские типы
- •7.3 Пользовательские операции преобразования типа
- •7.3.1 Конструкторы
- •7.3.2 Операции преобразования
- •7.3.3 Неоднозначности
- •7.4 Литералы
- •7.5 Большие объекты
- •7.6 Присваивание и инициализация
- •7.7 Индексация
- •7.8 Вызов функции
- •7.9 Косвенное обращение
- •7.10 Инкремент и декремент
- •7.11 Строковый класс
- •7.12 Друзья и члены
- •7.13 Предостережения
- •7.14 Упражнения
- •* Глава 8. Шаблоны типа
- •8.1 Введение
- •8.2 Простой шаблон типа
- •8.3 Шаблоны типа для списка
- •8.3.1 Список с принудительной связью
- •8.3.2 Список без принудительной связи
- •8.3.3 Реализация списка
- •8.3.4 Итерация
- •8.4 Шаблоны типа для функций
- •8.4.1 Простой шаблон типа для глобальной функции
- •8.4.2 Производные классы позволяют ввести новые операции
- •8.4.3 Передача операций как параметров функций
- •8.4.4 Неявная передача операций
- •8.4.5 Введение операций с помощью параметров шаблонного класса
- •8.5 Разрешение перегрузки для шаблонной функции
- •8.6 Параметры шаблона типа
- •8.7 Шаблоны типа и производные классы
- •8.7.1 Задание реализации с помощью параметров шаблона
- •8.8 Ассоциативный массив
- •8.9 Упражнения
- •* Глава 9
- •9.1 Обработка ошибок
- •9.1.1 Особые ситуации и традиционная обработка ошибок
- •9.1.2 Другие точки зрения на особые ситуации
- •9.2 Различение особых ситуаций
- •9.3 Имена особых ситуаций
- •9.3.1 Группирование особых ситуаций
- •9.3.2 Производные особые ситуации
- •9.4 Запросы ресурсов
- •9.4.1 Конструкторы и деструкторы
- •9.4.2 Предостережения
- •9.4.3 Исчерпание ресурса
- •9.4.4 Особые ситуации и конструкторы
- •9.5 Особые ситуации могут не быть ошибками
- •9.6 Задание интерфейса
- •9.6.1 Неожиданные особые ситуации
- •9.7 Неперехваченные особые ситуации
- •9.8 Другие способы обработки ошибок
- •9.9 Упражнения
- •* Глава 10. Потоки
- •10.1 Введение
- •10.2 Вывод
- •10.2.1 Вывод встроенных типов
- •10.2.2 Вывод пользовательских типов
- •10.3 Ввод
- •10.3.1 Ввод встроенных типов
- •10.3.2 Состояния потока
- •10.3.3 Ввод пользовательских типов
- •10.4 Форматирование
- •10.4.1 Класс ios
- •10.4.1.1 Связывание потоков
- •10.4.1.2 Поля вывода
- •10.4.1.3 Состояние формата
- •10.4.1.4 Вывод целых
- •10.4.1.5 Выравнивание полей
- •10.4.1.6 Вывод плавающих чисел.
- •10.4.2 Манипуляторы
- •10.4.2.1 Стандартные манипуляторы ввода-вывода
- •10.4.3 Члены ostream
- •10.4.4 Члены istream
- •10.5 Файлы и потоки
- •10.5.1 Закрытие потоков
- •10.5.2 Строковые потоки
- •10.5.3 Буферизация
- •10.6 Ввод-вывод в с
- •10.7 Упражнения
- •* Проектирование и развитие
- •11.1 Введение
- •11.2 Цели и средства
- •11.3 Процесс развития
- •11.3.1 Цикл развития
- •11.3.2 Цели проектирования
- •11.3.3 Шаги проектирования
- •11.3.3.1 Шаг 1: определение классов
- •11.3.3.2 Шаг 2: определение набора операций
- •11.3.3.3 Шаг 3: указание зависимостей
- •11.3.3.4 Шаг 4: определение интерфейсов
- •11.3.3.5 Перестройка иерархии классов
- •11.3.3.6 Использование моделей
- •11.3.4 Эксперимент и анализ
- •11.3.5 Тестирование
- •11.3.6 Сопровождение
- •11.3.7 Эффективность
- •11.4 Управление проектом
- •11.4.1 Повторное использование
- •11.4.2 Размер
- •11.4.3 Человеческий фактор
- •11.5 Свод правил
- •11.6 Список литературы с комментариями
- •12.1 Проектирование и язык программирования.
- •12.1.1 Игнорирование классов
- •12.1.2 Игнорирование наследования
- •12.1.3 Игнорирование статического контроля типов
- •12.1.4 Гибридный проект
- •12.2 Классы
- •12.2.1 Что представляют классы?
- •12.2.2 Иерархии классов
- •12.2.3 Зависимости в рамках иерархии классов.
- •12.2.4 Отношения принадлежности
- •12.2.5 Принадлежность и наследование
- •12.2.6 Отношения использования
- •12.2.7 Отношения внутри класса
- •12.2.7.1 Инварианты
- •12.2.7.2 Инкапсуляция
- •12.2.8 Программируемые отношения
- •12.3 Компоненты
- •12.4 Интерфейсы и реализации
- •12.5 Свод правил
- •* Проектирование библиотек
- •13.1 Введение
- •13.2 Конкретные типы
- •13.3 Абстрактные типы
- •13.4 Узловые классы
- •13.5 Динамическая информация о типе
- •13.5.1 Информация о типе
- •13.5.2 Класс Type_info
- •13.5.3 Как создать систему динамических запросов о типе
- •13.5.4 Расширенная динамическая информация о типе
- •13.5.5 Правильное и неправильное использование динамической
- •13.6 Обширный интерфейс
- •13.7 Каркас области приложения
- •13.8 Интерфейсные классы
- •13.9 Управляющие классы
- •13.10 Управление памятью
- •13.10.1 Сборщик мусора
- •13.10.2 Контейнеры и удаление
- •13.10.3 Функции размещения и освобождения
- •13.11 Упражнения
13.3 Абстрактные типы
Самый простой способ ослабить связь между пользователем класса и его создателем, а также между программами, в которых объекты создаются, и программами, в которых они используются, состоит в введении понятия абстрактных базовых классов. Эти классы представляют интерфейс со множеством реализаций одного понятия. Рассмотрим класс set, содержащий множество объектов типа T: class set { public: virtual void insert(T*) = 0; virtual void remove(T*) = 0; virtual int is_member(T*) = 0; virtual T* first() = 0; virtual T* next() = 0; virtual ~set() { } }; Этот класс определяет интерфейс с произвольным множеством (set), опираясь на встроенное понятие итерации по элементам множества. Здесь типично отсутствие конструктора и наличие виртуального деструктора, см. также $$6.7. Рассмотрим пример: class slist_set : public set, private slist { slink* current_elem; public: void insert(T*); void remove(T*); int is_member(T*); virtual T* first(); virtual T* next(); slist_set() : slist(), current_elem(0) { } }; class vector_set : public set, private vector { int current_index; public: void insert(T*); void remove(T*); int is_member(T*); T* first() { current_index = 0; return next(); } T* next(); vector_set(int initial_size) : array(initial_size), current_index(0) { } }; Реализация конкретного типа используется как частный базовый класс, а не член класса. Это сделано и для удобства записи, и потому, что некоторые конкретные типы могут иметь защищенный интерфейс с целью предоставить более прямой доступ к своим членам из производных классов. Кроме того, подобным образом в реализации могут использоваться некоторые классы, которые имеют виртуальные функции и не являются конкретными типами. Только с помощью образования производных классов можно в новом классе изящно переопределить (подавить) виртуальную функцию класса реализации. Интерфейс определяется абстрактным классом. Теперь пользователь может записать свои функции из $$13.2 таким образом: void my(set& s) { for (T* p = s.first(); p; p = s.next()) { // мой код } // ... } void your(set& s) { for (T* p = s.first(); p; p = s.next()) { // ваш код } // ... } Стало очевидным сходство между двумя функциями, и теперь достаточно иметь только одну версию для каждой из функций my() или your(), поскольку для общения с slist_set и vector_set обе версии используют интерфейс, определяемый классом set: void user() { slist_set sl; vector_set v(100); my(sl); your(v); my(v); your(sl); } Более того, создатели функций my() и your() не обязаны знать описаний классов slist_set и vector_set, и функции my() и your() никоим образом не зависят от этих описаний. Их не надо перетранслировать или как-то изменять, ни если изменились классы slist_set или vector_set ни даже, если предложена новая реализация этих классов. Изменения отражаются лишь на функциях, которые непосредственно используют эти классы, допустим vector_set. В частности, можно воспользоваться традиционным применением заголовочных файлов и включить в программы с функциями my() или your() файл определений set.h, а не файлы slist_set.h или vector_set.h. В обычной ситуации операции абстрактного класса задаются как чистые виртуальные функции, и такой класс не имеет членов, представляющих данные (не считая скрытого указателя на таблицу виртуальных функций). Это объясняется тем, что добавление невиртуальной функции или члена, представляющего данные, потребует определенных допущений о классе, которые будут ограничивать возможные реализации. Изложенный здесь подход к абстрактным классам близок по духу традиционным методам, основанным на строгом разделении интерфейса и его реализаций. Абстрактный тип служит в качестве интерфейса, а конкретные типы представляют его реализации. Такое разделение интерфейса и его реализаций предполагает недоступность операций, являющихся "естественными" для какой-то одной реализации, но не достаточно общими, чтобы войти в интерфейс. Например, поскольку в произвольном множестве нет упорядоченности, в интерфейс set нельзя включать операцию индексирования, даже если для реализации конкретного множества используется массив. Это приводит к ухудшению характеристик программы из-за отсутствия ручной оптимизации. Далее, становится как правило невозможной реализация функций подстановкой (если не считать каких-то конкретных ситуаций, когда настоящий тип известен транслятору), поэтому все полезные операции интерфейса, задаются как вызовы виртуальных функций. Как и для конкретных типов здесь плата за абстрактные типы иногда приемлема, иногда слишком высока. Подводя итог, перечислим каким целям должен служить абстрактный тип: [1] определять некоторое понятие таким образом, что в программе могут сосуществовать для него несколько реализаций; [2] применяя виртуальные функции, обеспечивать достаточно высокую степень компактности и эффективности выполнения программы; [3] сводить к минимуму зависимость любой реализации от других классов; [4] представлять само по себе осмысленное понятие. Нельзя сказать, что абстрактные типы лучше конкретных типов, это просто другие типы. Какие из них предпочесть - это, как правило, трудный и важный вопрос для пользователя. Создатель библиотеки может уклониться от ответа на него и предоставить варианты с обеими типами, тем самым выбор перекладывается на пользователя. Но здесь важно ясно понимать, с классом какого вида имеешь дело. Обычно неудачей заканчивается попытка ограничить общность абстрактного типа, чтобы скорость программ, работающих с ним, приблизилась к скорости программ, рассчитанных на конкретный тип. В этом случае нельзя использовать взаимозаменяемые реализации без большой перетрансляции программы после внесения изменений. Столь же неудачна бывает попытка дать "общность" в конкретных типах, чтобы они могли по мощности понятий приблизиться к абстрактным типам. Это снижает эффективность и применимость простых классов. Классы этих двух видов могут сосуществовать, и они должны мирно сосуществовать в программе. Конкретный класс воплощает реализацию абстрактного типа, и смешивать его с абстрактным классом не следует. Отметим, что ни конкретные, ни абстрактные типы не создаются изначально как базовые классы для построения в дальнейшем производных классов. Построение производных к абстрактным типам классов скорее нужно для задания реализаций, чем для развития самого понятия интерфейса. Всякий конкретный или абстрактный тип предназначен для четкого и эффективного представления в программе отдельного понятия. Классы, которым это удается, редко бывают хорошими кандидатами для создания на их базе новых, но связанных с ними, классов. Действительно, попытки построить производные, "более развитые" классы на базе конкретных или абстрактных типов, таких как, строки, комплексные числа, списки или ассоциативные массивы приводят обычно к громоздким конструкциям. Как правило эти классы следует использовать как члены или частные базовые классы, тогда их можно эффективно применять, не вызывая путаницы и противоречий в интерфейсах и реализациях этих и новых классов. Когда создается конкретный или абстрактный тип, акцент следует сделать на том, чтобы предложить простой, реализующий хорошо продуманное понятие, интерфейс. Попытки расширить область приложения класса, нагружая его описание всевозможными "полезными" свойствами, приводят только к беспорядку и неэффективности. Этим же кончаются напрасные усилия гарантировать повторное использование класса, когда каждую функцию-член объявляют виртуальной, не подумав зачем и как эти функции будут переопределяться. Почему мы не стали определять классы slist и vector как прямые производные от класса set, обойдясь тем самым без классов slist_set и vector_set? Другими словами зачем нужны конкретные типы, когда уже определены абстрактные типы? Можно предложить три ответа: [1] Эффективность: такие типы, как vector или slist надо создавать без накладных расходов, вызванных отдалением реализаций от интерфейсов (разделения интерфейса и реализации требует концепция абстрактного типа). [2] Множественный интерфейс: часто разные понятия лучше всего реализовать как производные от одного класса. [3] Повторное использование: нужен механизм, который позволит приспособить для нашей библиотеки типы, разработанные "где-то в другом месте". Конечно, все эти ответы связаны. В качестве примера [2] рассмотрим понятие генератора итераций. Требуется определить генератор итераций (в дальнейшем итератор) для любого типа так, чтобы с его помощью можно было порождать последовательность объектов этого типа. Естественно для этого нужно использовать уже упоминавшийся класс slist. Однако, нельзя просто определить общий итератор над slist, или даже над set, поскольку общий итератор должен допускать итерации и более сложных объектов, не являющихся множествами, например, входные потоки или функции, которые при очередном вызове дают следующее значение итерации. Значит нам нужны и множество и итератор, и в тоже время нежелательно дублировать конкретные типы, которые являются очевидными реализациями различных видов множеств и итераторов. Можно графически представить желательную структуру классов так: Здесь классы set и iter предоставляют интерфейсы, а slist и stream являются частными классами и представляют реализации. Очевидно, нельзя перевернуть эту иерархию классов и, предоставляя общие интерфейсы, строить производные конкретные типы от абстрактных классов. В такой иерархии каждая полезная операция над каждым полезным абстрактным понятием должна представляться в общем абстрактном базовом классе. Дальнейшее обсуждение этой темы содержится в $$13.6. Приведем пример простого абстрактного типа, являющегося итератором объектов типа T: class iter { virtual T* first() = 0; virtual T* next() = 0; virtual ~iter() { } }; class slist_iter : public iter, private slist { slink* current_elem; public: T* first(); T* next(); slist_iter() : current_elem(0) { } }; class input_iter : public iter { isstream& is; public: T* first(); T* next(); input_iter(istream& r) : is(r) { } }; Можно таким образом использовать определенные нами типы: void user(const iter& it) { for (T* p = it.first(); p; p = it.next()) { // ... } } void caller() { slist_iter sli; input_iter ii(cin); // заполнение sli user(sli); user(ii); } Мы применили конкретный тип для реализации абстрактного типа, но можно использовать его и независимо от абстрактных типов или просто вводить такие типы для повышения эффективности программы, см. также $$13.5. Кроме того, можно использовать один конкретный тип для реализации нескольких абстрактных типов. В разделе $$13.9 описывается более гибкий итератор. Для него зависимость от реализации, которая поставляет подлежащие итерации объекты, определяется в момент инициализации и может изменяться в ходе выполнения программы.