- •Глава 6 посвящена понятию производных классов, которое позволяет строить
- •Раздел 3.4 главы 2. Для обозначения справочного руководства применяется
- •1991 Г.Г. (такие как множественное наследование, статические функции-члены
- •1.1 Введение
- •1.2 Парадигмы программирования
- •1.2.1 Процедурное программирование
- •1.2.5 Объектно-ориентированное программирование
- •1.5 Поддержка объектно-ориентированного программирования
- •1.5.1 Механизм вызова
- •1.5.2 Проверка типа
- •1.5.3 Множественное наследование
- •1.6 Пределы совершенства
- •2.2 Имена
- •2.3.2 Неявное преобразование типа
- •2.4 Литералы
- •2.4.4 Строки
- •2.6. Экономия памяти
- •2.6.1 Поля
- •3.1.1 Анализатор
- •3.1.2 Функция ввода
- •3.2 Сводка операций
- •3.2.3 Инкремент и декремент
- •3.2.5 Преобразование типа
- •3.2.6 Свободная память
- •3.3.2 Оператор goto
- •4.1 Введение
- •4.3.1 Единственный заголовочный файл
- •4.3.2 Множественные заголовочные файлы
- •4.4 Связывание с программами на других языках
- •4.6.3 Передача параметров
- •5.1 Введение и краткий обзор
- •5.3.1 Альтернативные реализации
- •5.3.2 Законченный пример класса
- •Vector и matrix, мы могли бы обойтись без контроля индекса при
- •5.4.5 Указатели на члены
- •5.4.6 Структуры и объединения
- •5.5.3 Свободная память
- •5.5.5 Массивы объектов класса
- •6.1 Введение и краткий обзор
- •6.2.3 Иерархия классов
- •6.2.4 Поля типа
- •6.4.1 Монитор экрана
- •6.5 Множественное наследование
- •7.1 Введение
- •7.3 Пользовательские операции преобразования типа
- •7.3.2 Операции преобразования
- •7.3.3 Неоднозначности
- •7.5 Большие объекты
- •Void f2(t a) // вариант с контролем
- •Void f3(t a) // вариант с контролем
- •Inv() обращает саму матрицу m, а не возвращает новую, обратную m,
- •7.13 Предостережения
- •8.1 Введение
- •8.4.4 Неявная передача операций
- •8.4.5 Введение операций с помощью параметров шаблонного класса
- •8.7.1 Задание реализации с помощью параметров шаблона
- •9.1 Обработка ошибок
- •9.1.2 Другие точки зрения на особые ситуации
- •9.3.2 Производные особые ситуации
- •9.4.2 Предостережения
- •9.4.3 Исчерпание ресурса
- •9.4.4 Особые ситуации и конструкторы
- •9.5 Особые ситуации могут не быть ошибками
- •10.1 Введение
- •10.2 Вывод
- •10.2.1 Вывод встроенных типов
- •10.4.1.2 Поля вывода
- •10.4.1.4 Вывод целых
- •Istream - шаблон типа smanip, а smanip - двойник для ioss.
- •10.5.1 Закрытие потоков
- •10.5.2 Строковые потоки
- •X Целый параметр выдается в шестнадцатеричной записи;
- •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 Зависимости в рамках иерархии классов.
- •Vertical_scrollbar или с помощью одного типа scrollbar, который
- •12.2.6 Отношения использования
- •12.2.7 Отношения внутри класса
- •12.3 Компоненты
- •12.4 Интерфейсы и реализации
- •12.5 Свод правил
- •13.1 Введение
- •13.2 Конкретные типы
- •13.4 Узловые классы
- •1, 2, 6 И 7. Класс, который не удовлетворяет условию 6, походит
- •13.5.1 Информация о типе
- •13.6 Обширный интерфейс
- •13.7 Каркас области приложения
- •13.8 Интерфейсные классы
- •13.10 Управление памятью
5.5.5 Массивы объектов класса
Чтобы можно было описать массив объектов класса с конструктором,
этот класс должен иметь стандартный конструктор, т.е. конструктор,
вызываемый без параметров. Например, в соответствии с определением
table tbl[10];
будет создан массив из 10 таблиц, каждая из которых инициализируется
вызовом table::table(15), поскольку вызов table::table() будет
происходить с фактическим параметром 15.
В описании массива объектов не предусмотрено возможности указать
параметры для конструктора. Если члены массива обязательно надо
инициализировать разными значениями, то начинаются трюки с
глобальными или статическими членами.
Когда уничтожается массив, деструктор должен вызываться для
каждого элемента массива. Для массивов, которые размещаются не
с помощью new, это делается неявно. Однако для размещенных в свободной
памяти массивов неявно вызывать деструктор нельзя, поскольку транслятор
не отличит указатель на отдельный объект массива от указателя на начало
массива, например:
void f()
{
table* t1 = new table;
table* t2 = new table[10];
delete t1; // удаляется одна таблица
delete t2; // неприятность:
// на самом деле удаляется 10 таблиц
}
В данном случае программист должен указать, что t2 - указатель
на массив:
void g(int sz)
{
table* t1 = new table;
table* t2 = new table[sz];
delete t1;
delete[] t2;
}
Функция размещения хранит число элементов для каждого размещаемого
массива. Требование использовать для удаления массивов только операцию
delete[] освобождает функцию размещения от обязанности хранить счетчики
числа элементов для каждого массива. Исполнение такой обязанности в
реализациях С++ вызывало бы существенные потери времени и памяти
и нарушило совместимость с С.
5.5.6 Небольшие объекты
Если в вашей программе много небольших объектов, размещаемых в
свободной памяти, то может оказаться, что много времени тратится
на размещение и удаление таких объектов. Для выхода из этой
ситуации можно определить более оптимальный распределитель памяти
общего назначения, а можно передать обязанность распределения
свободной памяти создателю класса, который должен будет
определить соответствующие функции размещения и удаления.
Вернемся к классу name, который использовался в примерах с
table. Он мог бы определяться так:
struct name {
char* string;
name* next;
double value;
name(char*, double, name*);
~name();
void* operator new(size_t);
void operator delete(void*, size_t);
private:
enum { NALL = 128 };
static name* nfree;
};
Функции name::operator new() и name::operator delete() будут
использоваться (неявно) вместо глобальных функций operator new()
и operator delete(). Программист может для конкретного типа написать
более эффективные по времени и памяти функции размещения и
удаления, чем универсальные функции operator new() и
operator delete(). Можно, например, разместить заранее "куски"
памяти, достаточной для объектов типа name, и связать их в список;
тогда операции размещения и удаления сводятся к простым операциям
со списком. Переменная nfree используется как начало списка
неиспользованных кусков памяти:
void* name::operator new(size_t)
{
register name* p = nfree; // сначала выделить
if (p)
nfree = p->next;
else { // выделить и связать в список
name* q = (name*) new char[NALL*sizeof(name) ];
for (p=nfree=&q[NALL-1]; q<p; p--) p->next = p-1;
(p+1)->next = 0;
}
return p;
}
Распределитель памяти, вызываемый new, хранит вместе с объектом его
размер, чтобы операция delete выполнялась правильно. Этого
дополнительного расхода памяти можно легко избежать, если
использовать распределитель, рассчитанный на конкретный тип. Так,
на машине автора функция name::operator new() для хранения объекта
name использует 16 байтов, тогда как стандартная глобальная
функция operator new() использует 20 байтов.
Отметим, что в самой функции name::operator new() память нельзя
выделять таким простым способом:
name* q= new name[NALL];
Это вызовет бесконечную рекурсию, т.к. new будет вызывать
name::name().
Освобождение памяти обычно тривиально:
void name::operator delete(void* p, size_t)
{
((name*)p)->next = nfree;
nfree = (name*) p;
}
Приведение параметра типа void* к типу name* необходимо, поскольку
функция освобождения вызывается после уничтожения объекта, так что
больше нет реального объекта типа name, а есть только кусок
памяти размером sizeof(name). Параметры типа size_t в приведенных
функциях name::operator new() и name::operator delete() не
использовались. Как можно их использовать, будет показано в $$6.7.
Отметим, что наши функции размещения и удаления используются
только для объектов типа name, но не для массивов names.
5.6 Упражнения
1. (*1) Измените программу калькулятора из главы 3 так, чтобы
можно было воспользоваться классом table.
2. (*1) Определите tnode ($$R.9) как класс с конструкторами и
деструкторами и т.п., определите дерево из объектов типа
tnode как класс с конструкторами и деструкторами и т.п.
3. (*1) Определите класс intset ($$5.3.2) как множество строк.
4. (*1) Определите класс intset как множество узлов типа tnode.
Структуру tnode придумайте сами.
5. (*3) Определите класс для разбора, хранения, вычисления и печати
простых арифметических выражений, состоящих из целых констант и
операций +, -, * и /. Общий интерфейс класса должен выглядеть
примерно так:
class expr {
// ...
public:
expr(char*);
int eval();
void print();
};
Конструктор expr::expr() имеет параметр-строку, задающую выражение.
Функция expr::eval() возвращает значение выражения, а expr::print()
выдает представление выражения в cout. Использовать эти функции
можно так:
expr("123/4+123*4-3");
cout << "x = " << x.eval() << "\n";
x.print();
Дайте два определения класса expr: пусть в первом для представления
используется связанный список узлов, а во втором - строка
символов. Поэкспериментируйте с разными форматами печати
выражения, а именно: с полностью расставленными скобками,
в постфиксной записи, в ассемблерном коде и т.д.
6. (*1) Определите класс char_queue (очередь символов) так, чтобы
его общий интерфейс не зависел от представления. Реализуйте
класс как: (1) связанный список и (2) вектор. О параллельности
не думайте.
7. (*2) Определите класс histogram (гистограмма), в котором ведется
подсчет чисел в определенных интервалах, задаваемых в виде
параметров конструктору этого класса. Определите функцию
выдачи гистограммы. Сделайте обработку значений, выходящих за
интервал. Подсказка: обратитесь к <task.h>.
8. (*2) Определите несколько классов, порождающих случайные числа
с определенными распределениями. Каждый класс должен иметь
конструктор, задающий параметры распределения и функцию draw,
возвращающую "следующее" значение. Подсказка: обратитесь к
<task.h> и классу intset.
9. (*2) Перепишите примеры date ($$5.2.2 и $$5.2.4), char_stack
($$5.2.5) и intset ($$5.3.2), не используя никаких функций-членов
(даже конструкторов и деструкторов). Используйте только class
и friend. Проверьте каждую из новых версий и сравните их
с версиями, в которых используются функции-члены.
10.(*3) Для некоторого языка составьте определения класса для таблицы
имен и класса, представляющего запись в этой таблице. Исследуйте
транслятор для этого языка, чтобы узнать, какой должна быть настоящая
таблица имен.
11.(*2) Измените класс expr из упражнения 5 так, чтобы в выражении
можно было использовать переменные и операцию присваивания =.
Используйте класс для таблицы имен из упражнения 10.
12.(*1) Пусть есть программа:
#include <iostream.h>
main()
{
cout << "Всем привет\n";
}
Измените ее так, чтобы она выдавала:
Инициализация
Всем привет
Удаление
Саму функцию main() менять нельзя.
* ГЛАВА 6
Не плоди объекты без нужды.
- В. Оккам
Эта глава посвящена понятию производного класса. Производные
классы - это простое, гибкое и эффективное средство определения
класса. Новые возможности добавляются к уже существующему
классу, не требуя его перепрограммирования или перетрансляции.
С помощью производных классов можно организовать общий
интерфейс с несколькими различными классами так, что в других
частях программы можно будет единообразно работать с объектами
этих классов. Вводится понятие виртуальной функции, которое
позволяет использовать объекты надлежащим образом даже
в тех случаях, когда их тип на стадии трансляции неизвестен.
Основное назначение производных классов - упростить
программисту задачу выражения общности классов.
