Массивы объектов класса
Чтобы можно было описать массив объектов класса с конструктором, этот класс должен иметь стандартный конструктор, т.е. конструктор, вызываемый без параметров. Например, в соответствии с определением 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[] освобождает функцию размещения от обязанности хранить счетчики числа элементов для каждого массива. Исполнение такой обязанности в реализациях С++ вызывало бы существенные потери времени и памяти и нарушило совместимость с С.Небольшие объекты
Если в вашей программе много небольших объектов, размещаемых в свободной памяти, то может оказаться, что много времени тратится на размещение и удаление таких объектов. Для выхода из этой ситуации можно определить более оптимальный распределитель памяти общего назначения, а можно передать обязанность распределения свободной памяти создателю класса, который должен будет определить соответствующие функции размещения и удаления. Вернемся к классу 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.Упражнения
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
Не плоди объекты без нужды. - В. Оккам Эта глава посвящена понятию производного класса. Производные классы - это простое, гибкое и эффективное средство определения класса. Новые возможности добавляются к уже существующему классу, не требуя его перепрограммирования или перетрансляции. С помощью производных классов можно организовать общий интерфейс с несколькими различными классами так, что в других частях программы можно будет единообразно работать с объектами этих классов. Вводится понятие виртуальной функции, которое позволяет использовать объекты надлежащим образом даже в тех случаях, когда их тип на стадии трансляции неизвестен. Основное назначение производных классов - упростить программисту задачу выражения общности классов.