Главная > Программирование > Языки C/C++/Builder > |
Краткий FAQ по C++ |
Секция 3 из 3 - Предыдущая - Следующая
Все секции
- 1
- 2
- 3
[10.11] Что такое ошибка в порядке статической инициализации ("static initialization order fiasco")?
Незаметный и коварный способ убить ваш проект.
Ошибка порядка статической инициализации - это очень тонкий и часто неверно воспринимаемый аспект С++. К сожалению, подобную ошибку очень сложно отловить, поскольку она происходит до вхождения в функцию main().
Представьте себе, что у вас есть два статических объекта x и y, которые находятся в двух разных исходных файлах, скажем x.cpp и y.cpp. И путь конструктор объекта y вызывает какой-либо метод объекта x.
Вот и все. Так просто.
Проблема в том, что у вас ровно пятидесятипроцентная возможность катастрофы. Если случится, что единица трансляции с x.cpp будет проинициализирована первой, то все в порядке. Если же первой будет проинициализирована единица трансляции файла y.cpp, тогда конструктор объекта y будет запущен до конструктора x, и вам крышка. Т.е., конструктор y вызовет метод объекта x, когда сам x еще не создан.
Идите работать в МакДональдс. Делайте Биг-Маки, забудьте про классы.
Если вам нравится играть в русскую рулетку с барабаном, на половину заполненным пулями, то вы можете дальше не читать. Если же вы хотите увеличить свои шансы на выживание, систематически устраняя проблемы в зародыше, вы, вероятно, захотите прочесть ответ на следующий вопрос [10.12].
Примечание: ошибки статической инициализации не распространяются на базовые/встроенные типы, такие как int или char*. Например, если вы создаете статическую переменную типа float, у вас не будет проблем с порядком инициализации. Проблема возникает только тогда, когда у вашего статического или глобального объекта есть конструктор.
[10.12] Как предотвратить ошибку в порядке статической инициализации?
Используйте "создание при первом использовании", то есть, поместите ваш статический объект в функцию.
Представьте себе, что у нас есть два класса Fred и Barney. Есть глобальный объект типа Fred, с именем x, и глобальный объект типа Barney, с именем y. Конструктор Barney вызывает метод goBowling() объекта x. Файл x.cpp содержит определение объекта x:
// File x.cpp #include "Fred.hpp" Fred x;
Файл y.cpp содержит определение объекта y:
// File y.cpp #include "Barney.hpp" Barney y;
Для полноты представим, что конструктор Barney::Barney() выглядит следующим образом:
// File Barney.cpp #include "Barney.hpp" Barney::Barney() { // ... x.goBowling(); // ... }
Как описано выше [10.11], проблема случается, если y создается раньше, чем x, что происходит в 50% случаев, поскольку x и y находятся в разных исходных файлах.
Есть много решений для этой проблемы, но одно очень простое и переносимое - заменить глобальный объект Fred x, глобальной функцией x(), которая возвращает объект типа Fred по ссылке.
// File x.cpp #include "Fred.hpp" Fred& x() { static Fred* ans = new Fred(); return *ans; }
Поскольку локальные статические объекты создаются в момент, когда программа в процессе работы в первый раз проходит через точку их объявления, инструкция new Fred() в примере выше будет выполнена только один раз: во время первого вызова функции x(). Каждый последующий вызов возвратит тот же самый объект Fred (тот, на который указывает ans). И далее все случаи использования объекта x замените на вызовы функции x():
// File Barney.cpp #include "Barney.hpp" Barney::Barney() { // ... x().goBowling(); // ... }
Это и называется "создание при первом использовании", глобальный объект Fred создается при первом обращении к нему.
Отрицательным моментом этой техники является тот факт, что объект Fred нигде не уничтожается. Книга C++ FAQ Book описывает дополнительную технику, которая позволяет решить и эту проблему (правда, ценой появления возможный ошибок порядка статической деинициализации).
Примечание: ошибки статической инициализации не распространяются на базовые/встроенные типы, такие как int или char*. Например, если вы создаете статическую переменную типа float, у вас не будет проблем с порядком инициализации. Проблема возникает только тогда, когда у вашего статического или глобального объекта есть конструктор.
[10.13] Как бороться с ошибками порядка статической инициализации объектов - членов класса?
Используйте ту же самую технику, которая описана в [10.12], но вместо глобальной функции используйте статическую функцию-член.
Предположим, у вас есть класс X, в котором есть статический объект Fred:
// File X.hpp class X { public: // ... private: static Fred x_; };
Естественно, этот статический член инициализируется отдельно:
// File X.cpp #include "X.hpp" Fred X::x_;
Опять же естественно, объект Fred будет использован в одном или нескольких методах класса X:
void X::someMethod() { x_.goBowling(); }
Проблема проявится, если кто-то где-то каким-либо образом вызовет этот метод, до того как объект Fred будет создан. Например, если кто-то создает статический объект X и вызывает его someMethod() во время статической инициализации, то ваша судьба всецело находится в руках компилятора, который либо создаст X::x_, до того как будет вызван someMethod(), либо же только после.
(Должен заметить, что ANSI/ISO комитет по C++ работает над этой проблемой, но компиляторы, которые работают в соответствии с последними изменениями, пока недоступны; возможно, в будущем в этом разделе будут сделаны дополнения в связи с изменившейся ситуацией.)
В любом случае, всегда можно сохранить переносимость (и это абсолютно безопасный метод), заменив статический член X::x_ на статическую функцию-член:
// File X.hpp class X { public: // ... private: static Fred& x(); };
Естественно, этот статический член инициализируется отдельно:
// File X.cpp #include "X.hpp" Fred& X::x() { static Fred* ans = new Fred(); return *ans; }
После чего вы просто меняете все x_ на x():
void X::someMethod() { x().goBowling(); }
Если для вас крайне важна скорость работы программы и вас беспокоит необходимость дополнительного вызова функции для каждого вызова X::someMethod(), то вы можете сделать static Fred&. Как вы помните, статические локальные переменные инициализируются только один раз (при первом прохождении программы через их объявление), так что X::x() теперь будет вызвана только один раз: во время первого вызова X::someMethod():
void X::someMethod() { static Fred& x = X::x(); x.goBowling(); }
Примечание: ошибки статической инициализации не распространяются на базовые/встроенные типы, такие как int или char*. Например, если вы создаете статическую переменную типа float, у вас не будет проблем с порядком инициализации. Проблема возникает только тогда, когда у вашего статического или глобального объекта есть конструктор.
[10.14] Как мне обработать ошибку, которая произошла в конструкторе?
Сгенерируйте исключение. Смотрите подробности в [17.1].
Раздел [11]: Деструкторы
Деструктор - это исполнение последней воли объекта.
Деструкторы используются для высвобождения занятых объектом ресурсов. Например, класс Lock может заблокировать ресурс для эксклюзивного использования, а его деструктор этот ресурс освободить. Но самый частый случай - это когда в конструкторе используется new, а в деструкторе - delete.
Деструктор это функция "готовься к смерти". Часто слово деструктор сокращается до dtor.
[11.2] В каком порядке вызываются деструкторы для локальных объектов?
В порядке обратном тому, в каком эти объекты создавались: первым создан - последним будет уничтожен.
В следующем примере деструктор для объекта b будет вызван первым, а только затем деструктор для объекта a:
void userCode() { Fred a; Fred b; // ... }
[11.3] В каком порядке вызываются деструкторы для массивов объектов?
В порядке обратном созданию: первым создан - последним будет уничтожен.
В следующем примере порядок вызова деструкторов будет таким: a[9], a[8], ..., a[1], a[0]:
void userCode() { Fred a[10]; // ... }
[11.4] Могу ли я перегрузить деструктор для своего класса?
Нет.
У каждого класса может быть только один деструктор. Для класса Fred он всегда будет называться Fred::~Fred(). В деструктор никогда не передаётся никаких параметров, и сам деструктор никогда ничего не возвращает.
Всё равно вы не смогли бы указать параметры для деструктора, потому что вы никогда на вызываете деструктор напрямую [11.5] (точнее, почти никогда [11.10]).
[11.5] Могу ли я явно вызвать деструктор для локальной переменной?
Нет!
Деструктор всё равно будет вызван еще раз при достижении закрывающей фигурной скобки } конца блока, в котором была создана локальная переменная. Этот вызов гарантируется языком, и он происходит автоматически; нет способа этот вызов предотвратить. Но последствия повторного вызова деструктора для одного и того же объекта могут быть плачевными. Бах! И вы покойник...
[11.6] А что если я хочу, чтобы локальная переменная "умерла" раньше закрывающей фигурной скобки? Могу ли я при крайней необходимости вызвать деструктор для локальной переменной?
Нет! [Смотрите ответ на предыдущий вопрос [11.5]].
Предположим, что (желаемый) побочный эффект от вызова деструктора для локального объекта File заключается в закрытии файла. И предположим, что у нас есть экземпляр f класса File и мы хотим, чтобы файл f был закрыт раньше конца своей области видимости (т.е., раньше }):
void someCode() { File f; // ... [Этот код выполняется при открытом f] ... // <-- Нам нужен эффект деструктора f здесь
// ... [Этот код выполняется после закрытия f] ...
}
Для этой проблемы есть простое решение, которое мы покажем в [11.7]. Но пока запомните только следующее: нельзя явно вызывать деструктор [11.5].
[11.7] Хорошо, я не буду явно вызывать деструктор. Но как мне справиться с этой проблемой?
[Также смотрите ответ на предыдущий вопрос [11.6]].
Просто поместите вашу локальную переменную в отдельный блок {...}, соответствующий необходимому времени жизни этой переменной:
void someCode() { { File f; // ... [В этом месте f еще открыт] ... } // ^-- деструктор f будет автомагически вызван здесь! // ... [В этом месте f уже будет закрыт] ... }
[11.8] А что делать, если я не могу поместить переменную в отдельный блок?
В большинстве случаев вы можете воспользоваться дополнительным блоком {...} для ограничения времени жизни вашей переменной [11.7]. Но если по какой-то причине вы не можете добавить блок, добавьте функцию-член, которая будет выполнять те же действия, что и деструктор. Но помните: вы не можете сами вызывать деструктор!
Например, в случае с классом File, вы можете добавить метод close(). Обычный деструктор будет вызывать close(). Обратите внимание, что метод close() должен будет как-то отмечать объект File, с тем чтобы последующие вызовы не пытались закрыть уже закрытый файл. Например, можно устанавливать переменную-член fileHandle_ в какое-нибудь неиспользуемое значение, типа -1, и проверять в начале, не содержит ли fileHandle_ значение -1.
class File { public: void close(); ~File(); // ... private: int fileHandle_; // fileHandle_ >= 0 если/только если файл открыт }; File::~File() { close(); } void File::close() { if (fileHandle_ >= 0) { // ... [Вызвать системную функцию для закрытия файла] ... fileHandle_ = -1; } }
Обратите внимание, что другим методам класса File тоже может понадобиться проверять, не установлен ли fileHandle_ в -1 (т.е., не закрыт ли файл).
Также обратите внимание, что все конструкторы, которые не открывают файл, должны устанавливать fileHandle_ в -1.
[11.9] А могу ли я явно вызывать деструктор для объекта, созданного при помощи new?
Скорее всего, нет.
За исключением того случая, когда вы использовали синтаксис размещения для оператора new [11.10], вам следует просто удалять объекты при помощи delete, а не вызывать явно деструктор. Предположим, что вы создали объект при помощи обычного new:
Fred* p = new Fred();
В таком случае деструктор Fred::~Fred() будет автомагически вызван, когда вы удаляете объект:
delete p; // Вызывает p->~Fred()
Вам не следует явно вызывать деструктор, поскольку этим вы не освобождаете память, выделенную для объекта Fred. Помните: delete p делает сразу две вещи [16.8]: вызывает деструктор и освобождает память.
[11.10] Что такое "синтаксис размещения" new ("placement new") и зачем он нужен?
Есть много случаев для использования синтаксиса размещения для new. Самое простое - вы можете использовать синтаксис размещения для помещения объекта в определенное место в памяти. Для этого вы указываете место, передавая указатель на него в оператор new:
#include <new> // Необходимо для использования синтаксиса размещения #include "Fred.h" // Определение класса Fred void someCode() { char memory[sizeof(Fred)]; // #1 void* place = memory; // #2 Fred* f = new(place) Fred(); // #3 (смотрите "ОПАСНОСТЬ" ниже) // Указатели f и place будут равны // ... }
В строчке #1 создаётся массив из sizeof(Fred) байт, размер которого достаточен для хранения объекта Fred. В строчке #2 создаётся указатель place, который указывает на первый байт массива (опытные программисты на С наверняка заметят, что можно было и не создавать этот указатель; мы это сделали лишь чтобы код был более понятным [As if - :) YM]). В строчке #3 фактически происходит только вызов конструктора Fred::Fred(). Указатель this в конструкторе Fred будет равен указателю place. Таким образом, возвращаемый указатель тоже будет равен place.
СОВЕТ: Не используйте синтаксис размещения new, за исключением тех случаев, когда вам действительно нужно, чтобы объект был размещён в определённом месте в памяти. Например, если у вас есть аппаратный таймер, отображённый на определённый участок памяти, то вам может понадобиться поместить объект Clock по этому адресу.
ОПАСНО: Используя синтаксис размещения new вы берёте на себя всю ответственность за то, что передаваемый вами указатель указывает на достаточный для хранения объекта участок памяти с тем выравниванием (alignment), которое необходимо для вашего объекта. Ни компилятор, ни библиотека не будут проверять корректность ваших действий в этом случае. Если ваш класс Fred должен быть выровнен четырёхбайтовой границе, но вы передали в new указатель на не выровненный участок памяти, у вас могут быть большие неприятности (если вы не знаете, что такое "выравнивание" (alignment), пожалуйста, не используйте синтаксис размещения new). Мы вас предупредили.
Также на вас ложится вся ответственность по уничтожения размещённого объекта. Для этого вам необходимо явно вызвать деструктор:
void someCode() { char memory[sizeof(Fred)]; void* p = memory; Fred* f = new(p) Fred(); // ... f->~Fred(); // Явный вызов деструктора для размещённого объекта }
Это практически единственный случай, когда вам нужно явно вызывать деструктор.
[11.11] Когда я пишу деструктор, должен ли я явно вызывать деструкторы для объектов-членов моего класса?
Нет. Никогда не надо явно вызывать деструктор (за исключением случая с синтаксисом размещения new [11.10]).
Деструктор класса (неявный, созданный компилятором, или явно описанный вами) автоматически вызывает деструкторы объектов-членов класса. Эти объекты уничтожаются в порядке обратном порядку их объявления в теле класса:
class Member { public: ~Member(); // ... }; class Fred { public: ~Fred(); // ... private: Member x_; Member y_; Member z_; }; Fred::~Fred() { // Компилятор автоматически вызывает z_.~Member() // Компилятор автоматически вызывает y_.~Member() // Компилятор автоматически вызывает x_.~Member() }
[11.12] Когда я пишу деструктор производного класса, нужно ли мне явно вызывать деструктор предка?
Нет. Никогда не надо явно вызывать деструктор (за исключением случая с синтаксисом размещения new [11.10]).
Деструктор производного класса (неявный, созданный компилятором, или явно описанный вами) автоматически вызывает деструкторы предков. Предки уничтожаются после уничтожения объектов-членов производного класса. В случае множественного наследования непосредственные предки класса уничтожаются в порядке обратном порядку их появления в списке наследования.
class Member { public: ~Member(); // ... }; class Base { public: virtual ~Base(); // Виртуальный деструктор[20.4] // ... }; class Derived : public Base { public: ~Derived(); // ... private: Member x_; }; Derived::~Derived() { // Компилятор автоматически вызывает x_.~Member() // Компилятор автоматически вызывает Base::~Base() }
Примечание: в случае виртуального наследования порядок уничтожения классов сложнее. Если вы полагаетесь на порядок уничтожения классов в случае виртуального наследования, вам понадобится больше информации, чем содержит этот FAQ.
Секция 3 из 3 - Предыдущая - Следующая
Вернуться в раздел "Языки C/C++/Builder" - Обсудить эту статью на Форуме |
Главная - Поиск по сайту - О проекте - Форум - Обратная связь |