Смекни!
smekni.com

Правила правой руки 17 Замечания для программистов на c 17 Глава 1 (стр. 30 из 43)

глобальных статических объектов в файле выполняются в том порядке,

в котором встречаются описания; деструкторы вызываются в обратном

порядке. Неопределено, вызывается ли конструктор для локального

статического объекта, если функция, в которой этот объект описан,

не вызывается. Если конструктор для локального статического объекта

вызывается, то он вызывается после того, как вызваны конструкторы

для лексически предшествующих ему глобальных статических объектов.

Параметры конструкторов для статических объектов должны быть

константными выражениями:

void g(int a)

{

static table t(a); // ошибка

}

Традиционно выполнение main() считалось выполнением программы.

Так никогда не было, даже в C, но только размещение статических

объектов класса с конструктором и/или деструктором дают

программисту простой и очевидный способ задания того, что будет

выполняться до и/или после вызова main().

Вызов конструкторов и деструкторов для статических объектов

играет в C++ чрезвычайно важную роль. Это способ обеспечить

надлежащую инициализацию и очистку структур данных в библиотеках.

Рассмотрим . Откуда берутся cin, cout и cerr? Где они

получают инициализацию? И, что самое главное, поскольку потоки

вывода имеют внутренние буферы символов, как же эти буфуры

становятся заполненными? Простой и очевидный ответ, что эта работа

осуществляется соответствующими конструкторами и деструкторами до и

после выполнения main(). Для инициализации и очистки библиотечных

средств есть возможности, альтернативные использованию

конструкторов и деструкторов. Все они или очень специальные, или

очень уродливые.

Если программа завершается с помощью функции exit(), то

деструкторы для статических объектов будут вызваны, а если она

завершается с помощью abort(), то не будут. Заметьте, что это

подразумевает, что exit() не завершает программу мгновенно. Вызов

exit() в деструкторе может привести к бесконечной рекурсии.

- стр 167 -

Иногда, когда вы разрабатываете библиотеку, необходимо или просто

удобно создать тип с конструктором и деструктором, предназначенными

только для одного: инициализировать и очистить. Такой тип обычно

используется только с одной целью, для размещения статического

объекта так, чтобы вызывались конструктор и деструктор.

5.5.3 Свободная Память

Рассмотрим:

main() {

table* p = new table(100);

table* q = new table(200);

delete p;

delete p; // возможно, ошибка

}

Конструктор table::table() будет вызван дважды, как и деструктор

table::~table(). То, что C++ не дает никаких гарантий, что для

объекта, созданного с помощью new, когда-либо будет вызван

деструктор, ничего не значит. В предыдущей программе q не

уничтожается, а p уничтожается дважды! Программист может счесть это

ошибкой, а может и не счесть, в зависимости от типа p и q. Обычно

то, что объект не уничтожается, является не ошибкой, а просто

лишней тратой памяти. Уничтожение p дважды будет , как правило,

серьезной ошибкой. Обычно результатом применения delete дважды к

одному указателю приводит к бесконечному циклу в подпрограмме

управления свободной памятью, но определение языка не задает

поведение в таком случае, и оно зависит от реализации.

Пользователь может определить новую реализацию операций new и

delete (см. #3.2.6). Можно также определить способ взаимодействия

конструктора или деструктора с операциями new и delete (см. #5.5.6)

5.5.4 Объекты Класса и Члены

Рассмотрим

class classdef {

table members;

int no_of_members;

// ...

classdef(int size);

~classdef();

};

Очевидное намерение состоит в том, что classdef должен содержать

таблицу длиной size из членов member, а сложность - в том, как

сделать так, чтобы конструктор table::table() вызывался с

параметром size. Это делается примерно так:

- стр 168 -

classdef::classdef(int size)

: members(size)

{

no_of_members = size;

// ...

}

Параметры для конструктора члена member (здесь это table::table())

помещаются в определение (не в описание) конструктора класса,

вмещающего его (здесь это classdef::classdef()). После этого

конструктор члена вызывается перед телом конструктора, задающего

его список параметров.

Если есть еще члены, которым нужны списки параметров для

конструкторов, их можно задать аналогично. Например:

class classdef {

table members;

table friends;

int no_of_members;

// ...

classdef(int size);

~classdef();

};

Список параметров для членов разделяется запятыми (а не

двоеточиями), и список инициализаторов для членов может

представляться в произвольном порядке:

classdef::classdef(int size)

: friends(size), members(size)

{

no_of_members = size;

// ...

}

Порядок, в котором вызываются конструкторы, неопределен, поэтому

не рекомендуется делать списоки параметров с побочными эффектами:

classdef::classdef(int size)

: friends(size=size/2), members(size); // дурной стиль

{

no_of_members = size;

// ...

}

Если конструктору для члена не нужно ниодного параметра, то

никакого списка параметров задавать не надо. Например, поскольку

table::table был определен с параметром по умолчанию 15, следующая

запись является правильной:

classdef::classdef(int size)

: members(size)

{

no_of_members = size;

// ...

}

- стр 169 -

и размер size таблицы friend'ов будет равен 15.

Когда объект класса, содержащий объект класса, (например,

classdef) уничтожается, первым выполняется тело собственного

деструктора объекта, а затем выполняются деструкторы членов.

Рассмотрим традиционную альтернативу тому, чтобы иметь объекты

класса как члены, - иметь члены указатели и инициализировать их в

конструкторе:

class classdef {

table* members;

table* friends;

int no_of_members;

// ...

classdef(int size);

~classdef();

};

classdef::classdef(int size)

{

members = new table(size);

friends = new table; // размер таблицы по умолчанию

no_of_members = size;

// ...

}

Так как таблицы создавались с помощью new, они должны уничтожаться

с помощью delete:

classdef::~classdef()

{

// ...

delete members;

delete friends;

}

Раздельно создаваемые объекты вроде этих могут оказаться полезными,

но учтите, что members и friends указывают на отдельные объекты,

что требует для каждого из них действие по выделению памяти и ее

освобожению. Кроме того, указатель плюс объект в свободной памяти

занимают больше места, чем объект член.

5.5.5 Вектора Объектов Класса

Чтобы описать вектор объектов класса, имеющего конструктор, этот

класс должен иметь конструктор, который может вызываться без списка

параметров. Нельзя использовать даже параметры по умолчанию.

Например:

table tblvec[10];

будет ошибкой, так как для table::table() требуется целый параметр.

Нет способа задать параметры конструктора в описании вектора. Чтобы

можно было описывать вектор таблиц table, можно модифицировать

описание table (#5.3.1) например так:

- стр 170 -

class table {

// ...

void init(int sz); // как старый конструктор

public:

table(int sz) // как раньше, но без по умолчанию

{ init(sz); }

table() // по умолчанию

{ init(15); }

}

Когда вектор уничтожается, деструктор должен вызываться для

каждого элемента этого вектора. Для векторов, которые не были

размещены с помощью new, это делается неявно. Однако для векторов в

свободной памяти это не может быть сделано неявно, поскольку

компилятор не может отличить указатель на один объект от указателя

на первый элемент вектора объектов. Например:

void f()

{

table* t1 = new table;

table* t2 = new table[10];

delete t1; // одна таблица

delete t2; // неприятность: 10 таблиц

}

В этом случае длину вектора должен задавать программист:

void g(int sz)

{

table* t1 = new table;

table* t2 = new table[sz];

delete t1;

delete[] t2;

}

Но почему же компилятор не может найти число элементов вектора из

объема выделенной памяти? Потому, что распределитель свободной

памяти не является частью языка и может быть задан программистом.

5.5.6 Небольшие Объекты

Когда вы используете много небольших объектов, размещаемых в

свободной памяти, то вы можете обнаружить, что ваша программа

тратит много времени выделяя и освобождая память под эти объекты.

Первое решение - это обеспечить более хороший распределитель памяти

общего назначения, второе для разработчика классов состоит в том,

чтобы взять под контроль управление свободной памятью для объектов

некоторого класса с помощью подходящих конструкторов и

деструкторов.

Рассмотрим класс name, который использовался в примерах table.

Его можно было бы определить так:

- стр 171 -

struct name {

char* string;

name* next;

double value;

name(char*, double, name*);

~name();

};

Программист может воспользоваться тем, что размещение и

освобождение объектов заранее известного размера может

обрабатываться гораздо эффективнее (и по памяти, и по времени), чем

с помощью общей реализации new и delete. Общая идея состоит в том,

чтобы предварительно разместить "куски" из объектов name, а затем

сцеплять их, чтобы свести выделение и освобождение к простым

операциям над связанным списком. Переменная nfree является вершиной

списка неиспользованных name:

const NALL = 128;

name* nfree;

Распределитель, используемый операцией new, хранит размер объекта

вместе с объектом, чтобы обеспечить правильную работу операции

delete. С помощью распределителя, специализированного для типа,

можно избежать этих накладных расходов. Например, на моей машине

следующий распределитель использует для хранения name 16 байт,

тогда как для стандартного распределителя свободной памяти нужно 20

байт. Вот как это можно сделать:

name::name(char* s, double v, name* n)

{

register name* p = nfree; // сначала выделить

if (p)

nfree = p->next;

else { // выделить и сцепить

name* q = (name*)new char[ NALL*sizeof(name) ];

for (p=nfree=&q[NALL-1]; qnext = p-1;

(p+1)->next = 0;

}

this = p; // затем инициализировать

string = s;

value = v;

next = n;

}

Присвоение указателю this информирует компилятор о том, что

программист взял себе управление, и что не надо использовать