стандартный механизм распределения памяти. Конструктор name::name()
обрабатывает только тот случай, когда name размещается посредством
new, но для большей части типов это всегда так. В #5.5.8
объясняется, как написать конструктор для обработки как размещения
в свободной памяти, так и других видов размещения.
Заметьте, что просто как
- стр 172 -
name* q = new name[NALL];
память выделять нельзя, поскольку это приведет к бесконечной
рекурсии, когда new вызовет name::name().
Освобождение памяти обычно тривиально:
name::~name()
{
next = nfree;
nfree = this;
this = 0;
}
Присваивание указателю this 0 в деструкторе обеспечивает, что
стандартный распределитель памяти не используется.
5.5.7 Предостережение
Когда в конструкторе производится присваивание указателю this,
значение this до этого присваивания неопределено. Таким образом,
ссылка на член до этого присваивания неопределена и скорее всего
приведет к катастрофе. Имеющийся компилятор не пытается убедиться в
том, что присваивание указателю this происходит на всех траекториях
выполнения:
mytype::mytype(int i)
{
if (i) this = mytype_alloc();
// присваивание членам
};
откомпилируется, и при i==0 никакой объект размещен не будет.
Конструктор может определить, был ли он вызван операцией new, или
нет. Если он вызван new, то указатель this на входе имеет нулевое
значение, в противном случае this указывает на пространство, уже
выделенное для объекта (например, на стек). Поэтому можно просто
написать конструктор, который выделяет память, если (и только если)
он был вызван через new. Например:
mytype::mytype(int i)
{
if (this == 0) this = mytype_alloc();
// присваивание членам
};
Эквивалентного средства, которое позволяет деструктору решить
вопрос, был ли его объект создан с помощью new, не имеется, как нет
и средства, позволяющего ему узнать, вызвала ли его delete, или он
вызван объектом, выходящим из области видимости. Если для
пользователя это существенно, то он может сохранить где-то
соответствующую информацию для деструктора. Другой способ,- когда
пользователь обеспечивает, что объекты этого класса размещаются
только соответствующим образом. Если удается справиться с первой
проблемой, то второй способ интереса не представляет.
- стр 173 -
Если тот, кто реализует класс, является одновременно и его
едиственным пользователем, то имеет смысл упростить, исходя из
предположений о его использовании. Когда класс разрабатывается для
более широкого использования, таких допущений, как правило, лучше
избегать.
5.5.8 Объекты Переменного Размера
Когда пользователь берет управление распределением и
освобождением памяти, он может конструировать объекты, размер
которых во время компиляции недетерминирован. В предыдущих примерах
вмещающие (или контейнерные - перев.) классы vector, stack, intset
и table реализовывалиь как структуры доступа фиксированного
размера, содержацие указатели на реальную память. Это
подразумевает, что для создания таких объектов в свободной памяти
необходимо две операции по выделению памяти, и что любое обращение
к хранимой информации будет содержать дополнительную косвенную
адресацию. Например:
class char_stack {
int size;
char* top;
char* s;
public:
char_stack(int sz) { top=s=new char[size=sz]; }
~char_stack() { delete s; } // деструктор
void push(char c) { *top++ = c; }
char pop() { return *--top; }
};
Если каждый объект класса размещается в свободной памяти, это
делать не нужно. Вот другой вариант:
class char_stack {
int size;
char* top;
char s[1];
public:
char_stack(int sz);
void push(char c) { *top++ = c; }
char pop() { return *--top; }
};
char_stack::char_stack(int sz)
{
if (this) error("стек не в свободной памяти");
if (sz < 1) error("размер стека < 1");
this = (char_stack*) new char[sizeof(char_stack)+sz-1];
size = sz;
top = s;
}
- стр 174 -
Заметьте, что деструктор больше не нужен, поскольку память, которую
использует char_stack, может освободить delete без всякого
содействия со стороны программиста.
5.6 Упражнения
1. (*1) Модифицируйте настольный калькулятор из Главы 3, чтобы
использовать класс table.
2. (*1) Разработайте tnode (#с.8.5) как класс с конструкторами,
деструкторами и т.п. Определите дерево из tnode'ов как класс с
конструкторами, деструкторами и т.п.
3. (*1) Преобразуйте класс intset (#5.3.2) в множество строк.
4. (*1) Преобразуйте класс intset в множество узлов node, где
node - определяемая вами структура.
5. (*3) Определите класс для анализа. хранения, вычисления и
печати простых арифметических выражений, состоящих из целых
констант и операций +, -, * и /. Открытый интерфейс должен
выгляедть примерно так:
class expr {
// ...
public:
expr(char*);
int eval();
void print();
}
Параметр строка конструктора expr::expr() является выражением.
Функция expr::eval() возвращает значение выражение, а
expr::print() печатает представление выражения в cout.
Программа может выглядеть, например, так:
expr x("123/4+123*4-3");
cout << "x = " << x.eval() << "\n";
x.print();
Определите класс expr два раза: один раз используя в качестве
представления связанный список узлов, а другой раз -
символьную строку. Поэкспериментируйте с разными способами
печати выражения: с полностью расставленными скобками, в
постфиксной записи, в ассемблерном коде и т.д.
6. (*1) Определите класс char_queue (символьная очередь) таким
образом, чтобы открытый интерфейс не зависел от представления.
Реализуйте char_queue как (1) связанный список и как (2)
вектор. О согласованности не заботьтесь.
7. (*2) Определите класс histogram (гистограмма), в котором
ведется подсчет чисел в определенных интервалах, которые
задаются как параметры конструктора histogram. Обеспечьте
функцию вывода гистограммы на печать. Сделайте обработку
значений, выходящих за границы. Подсказка: .
8. (*2) Определите несколько классов, предоставляющих случайные
числа с определенными распределениями. Каждый класс имеет
конструктор, задающий параметры распределения, и функцию draw,
которая возвращает "следующее" значение. Подсказка: .
Посмотрите также класс intset.
- стр 175 -
9. (*2) Перенишите пример date (#5.8.2), пример char_stack
(#5.2.5) и пример intset (#5.3.2) не используя функций членов
(даже конструкторов и деструкторов). Используйте только class
и friend. Сравните с версиями, в которых использовались
функции члены.
10. (*3) Для кокого-нибудь языка спроектируйте класс таблица имен
и класс вхождение в таблицу имен. Чтобы посмотреть, как на
самом деле выглядит таблица имен, посмотрите на компилятор
этого языка.
11. (*2) Модифицируйте класс выражение из Упражнения 5 так, чтобы
обрабатывать переменные и операцию присваивания =. Используйте
класс таблица имен из Упражнения 10.
12. (*1) Дана программа:
#include
main()
{
cout << "Hello, world\n";
}
модифицируйте ее, чтобы получить выдачу
Initialize
Hello, world
Clean up
Не делайте никаких изменений в main().
* Глава 6 *
Перегрузка Операций
Здесь водятся Драконы!
- старинная карта
В этой главе описывается аппарат, предоставляемый в C++ для
перегрузки операций. Программист может определять смысл операций
при их применении к объектам определенного класса. Кроме
арифметических, можно определять еще и логические операции,
операции сравнения, вызова () и индексирования [], а также можно
переопределять присваивание и инициализацию. Можно определить явное
и неявное преобразование между определяемыми пользователем и
основными типами. Показано, как определить класс, объект которого
не может быть никак иначе скопирован или уничтожен кроме как
специальными определенными пользователем функциями.
6.1 Введение
Часто программы работают с объектами, которые фвляются
конкретными представлениями абстрактных понятий. Например, тип
данных int в C++ вместе с операциями +, -, *, / и т.д.
предоставляет реализацию (ограниченную) математического понятия
целых чисел. Такие понятия обычно включают в себя множество
операций, которые кратко, удобно и привычно представляют основные
действия над объектами. К сожалению, язык программирования может
непосредственно поддерживать лишь очень малое число таких понятий.
Например, такие понятия, как комплексная арифметика, матричная
алгебра, логические сигналы и строки не получили прямой поддержки в
C++. Классы дают средство спецификации в C++ представления
неэлементарных объектов вместе с множеством действий, которые могут
над этими объектами выполняться. Иногда определение того, как
действуют операции на объекты классов, позволяет программисту
обеспечить более общепринятую и удобную запись для манипуляции
объектами классов, чем та, которую можно достичь используя лишь
основную функциональную запись. Например:
class complex {
double re, im;
public:
complex(double r, double i) { re=r; im=i; }
friend complex operator+(complex, complex);
friend complex operator*(complex, complex);
};
определяет простую реализацию понятия комплексного числа, в которой
число представляется парой чисел с плавающей точкой двойной
точности, работа с которыми осуществляется посредством операций + и
* (и только). Программист задает смысл операций + и * с помощью
определения функций с именами operator+ и operator*. Если,
например, даны b и c типа complex, то b+c означает (по определению)
operator+(b,c). Теперь есть возможность приблизить общепринятую
интерпретацию комплексных выражений. Например:
- стр 177 -
void f()
{
complex a = complex(1, 3.1);
complex b = complex(1.2, 2);
complex c = b;
a = b+c;
b = b+c*a;
c = a*b+complex(1,2);
}
Выполняются обычные правила приоритетов, поэтому второй оператор
означает b=b+(c*a), а не b=(b+c)*a.
6.2 Функции Операции
Можно описывать функции, определяющие значения следующих
операций:
+ - * / % ^ & | ~ !
= < > += -= *= /= %= ^= &=