Б. Страуструп - Язык программирования С++ (1119446), страница 42
Текст из файла (страница 42)
$$2.3.8). Такая интерпретация присваивания может привести к неожиданному (и обычнонежелательному) результату, если присваиваются объекты класса, в котором определен конструктор:void h(){142Бьерн Страуструп.table t1(100);table t2 = t1;table t3(200);t3 = t2;Язык программирования С++// неприятность// неприятность}В этом примере конструктор table вызывается дважды: для t1 и t3. Он не вызывается для t2, посколькуэтот объект инициализируется присваиванием.
Тем не менее, деструктор для table вызывается трираза: для t1, t2 и t3! Далее, стандартная интерпретация присваивания - это копирование по членам,поэтому перед выходом из h() t1, t2 и t3 будут содержать указатель на массив имен, память длякоторого была выделена в свободной памяти при создании t1. Указатель на память, выделенную длямассива имен при создании t3, будет потерян. Этих неприятностей можно избежать (см. $$1.4.2 и$$7.6).5.5.2 Статическая памятьРассмотрим такой пример:table tbl(100);void f(int i){static table tbl2(i);}int main(){f(200);// ...}Здесь конструктор, определенный в $$5.3.1, будет вызываться дважды: один раз для tbl и один раз дляtbl2. Деструктор table::~table() также будет вызван дважды: для уничтожения tbl и tbl2 по выходе изmain().
Конструкторы глобальных статических объектов в файле вызываются в том же порядке, в какомвстречаются в файле описания объектов, а деструкторы для них вызываются в обратном порядке.Конструктор локального статического объекта вызывается, когда при выполнении программы первыйраз встречается определение объекта.Традиционно выполнение main() рассматривалось как выполнение всей программы.
На самом деле, этоне так даже для С. Уже размещение статического объекта класса с конструктором и (или) деструкторомпозволяет программисту задать действия, которые будут выполняться до вызова main() и (или) повыходе из main().Вызов конструкторов и деструкторов для статических объектов играет в С++ чрезвычайно важную роль.С их помощью можно обеспечить соответствующую инициализацию и удаление структур данных,используемых в библиотеках. Рассмотрим <iostream.h>.
Откуда берутся cin, cout и cerr? Когда ониинициализируются? Более существенный вопрос: поскольку для выходных потоков используютсявнутренние буфера символов, то происходит выталкивание этих буферов, но когда? Есть простой иочевидный ответ: все действия выполняются соответствующими конструкторами и деструкторами дозапуска main() и по выходе из нее (см. $$10.5.1). Существуют альтернативы использованиюконструкторов и деструкторов для инициализации и уничтожения библиотечных структур данных, но всеони или очень специализированы, или неуклюжи, или и то и другое вместе.Если программа завершается обращение к функции exit(), то вызываются деструкторы для всехпостроенных статических объектов.
Однако, если программа завершается обращением к abort(), этогоне происходит. Заметим, что exit() не завершает программу немедленно. Вызов exit() в деструктореможет привести к бесконечной рекурсии. Если нужна гарантия, что будут уничтожены как статические,так и автоматические объекты, можно воспользоваться особыми ситуациями ($$9).Иногда при разработке библиотеки бывает необходимо или просто удобно создать тип с конструктороми деструктором только для одной цели: инициализации и уничтожения объектов.
Такой типиспользуется только один раз для размещения статического объекта, чтобы вызвать конструкторы идеструкторы.143Бьерн Страуструп.Язык программирования С++5.5.3 Свободная памятьРассмотрим пример:main(){table*table*deletedelete}p = new table(100);q = new table(200);p;p; // вероятно, вызовет ошибку при выполненииКонструктор table::table() будет вызываться дважды, как и деструктор table::~table(). Но это ничего незначит, т.к. в С++ не гарантируется, что деструктор будет вызываться только для объекта, созданногооперацией new. В этом примере q не уничтожается вообще, зато p уничтожается дважды! Взависимости от типа p и q программист может считать или не считать это ошибкой. То, что объект неудаляется, обычно бывает не ошибкой, а просто потерей памяти.
В то же время повторное удаление p серьезная ошибка. Повторное применение delete к тому же самому указателю может привести кбесконечному циклу в подпрограмме, управляющей свободной памятью. Но в языке результатповторного удаления не определен, и он зависит от реализации.Пользователь может определить свою реализацию операций new и delete (см. $$3.2.6 и $$6.7). Крометого, можно установить взаимодействие конструктора или деструктора с операциями new и delete (см.$$5.5.6 и $$6.7.2). Размещение массивов в свободной памяти обсуждается в $$5.5.5.5.5.4 Объекты класса как членыРассмотрим пример:class classdef {table members;int no_of_members;// ...classdef(int size);~classdef();};Цель этого определения, очевидно, в том, чтобы classdef содержал член, являющийся таблицейразмером size, но есть сложность: надо обеспечить вызов конструктора table::table() с параметром size.Это можно сделать, например, так:classdef::classdef(int size):members(size){no_of_members = size;// ...}Параметр для конструктора члена (т.е.
для table::table()) указывается в определении (но не в описании)конструктора класса, содержащего член (т.е. в определении classdef::classdef()). Конструктор для членабудет вызываться до выполнения тела того конструктора, который задает для него список параметров.Аналогично можно задать параметры для конструкторов других членов (если есть еще другие члены):class classdef {table members;table friends;int no_of_members;// ...classdef(int size);~classdef();};Списки параметров для членов отделяются друг от друга запятыми (а не двоеточиями), а список144Бьерн Страуструп.Язык программирования С++инициализаторов для членов можно задавать в произвольном порядке:classdef::classdef(int size): friends(size), members(size), no_of_members(size){// ...}Конструкторы вызываются в том порядке, в котором они заданы в описании класса.Подобные описания конструкторов существенны для типов, инициализация и присваивание которыхотличны друг от друга, иными словами, для объектов, являющихся членами класса с конструктором,для постоянных членов или для членов типа ссылки.
Однако, как показывает член no_of_members изприведенного примера, такие описания конструкторов можно использовать для членов любого типа.Если конструктору члена не требуется параметров, то и не нужно задавать никаких списков параметров.Так, поскольку конструктор table::table() был определен со стандартным значением параметра, равным15, достаточно такого определения:classdef::classdef(int size): members(size), no_of_members(size){// ...}Тогда размер таблицы friends будет равен 15.Если уничтожается объект класса, который сам содержит объекты класса (например, classdef), товначале выполняется тело деструктора объемлющего класса, а затем деструкторы членов в порядке,обратном их описанию.Рассмотрим вместо вхождения объектов класса в качестве членов традиционное альтернативное емурешение: иметь в классе указатели на члены и инициализировать члены в конструкторе:class classdef {table* members;table* friends;int no_of_members;// ...};classdef::classdef(int size){members = new table(size);friends = new table; // используется стандартный// размер tableno_of_members = size;// ...}Поскольку таблицы создавались с помощью операции new, они должны уничтожаться операцией delete:classdef::~classdef(){// ...delete members;delete friends;}Такие отдельно создаваемые объекты могут оказаться полезными, но учтите, что members и friendsуказывают на независимые от них объекты, каждый из которых надо явно размещать и удалять.
Крометого, указатель и объект в свободной памяти суммарно занимают больше места, чем объект-член.145Бьерн Страуструп.Язык программирования С++5.5.5 Массивы объектов классаЧтобы можно было описать массив объектов класса с конструктором, этот класс должен иметьстандартный конструктор, т.е. конструктор, вызываемый без параметров. Например, в соответствии сопределениемtable tbl[10];будет создан массив из 10 таблиц, каждая из которых инициализируется вызовом table::table(15),поскольку вызов table::table() будет происходить с фактическим параметром 15.В описании массива объектов не предусмотрено возможности указать параметры для конструктора.Если члены массива обязательно надо инициализировать разными значениями, то начинаются трюки сглобальными или статическими членами.Когда уничтожается массив, деструктор должен вызываться для каждого элемента массива.
Длямассивов, которые размещаются не с помощью new, это делается неявно. Однако для размещенных всвободной памяти массивов неявно вызывать деструктор нельзя, поскольку транслятор не отличитуказатель на отдельный объект массива от указателя на начало массива, например:void f(){table*table*deletedeletet1 = new table;t2 = new table[10];t1;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 };146Бьерн Страуструп.Язык программирования С++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 выполнялась правильно. Этого дополнительного расхода памяти можно легко избежать, еслииспользовать распределитель, рассчитанный на конкретный тип.