Во многих классах есть несколько конструкторов, и каждый конструктор имеет свой собственный список инициализации. Если у класса много данных-членов или базовых классов, то наличие большого числа списков инициализации порождает нежелательное дублирование кода (в списках) и тоску (у программистов). В таких случаях имеет смысл опустить в списках инициализации те данные-члены, для которых присваивание работает так же, как настоящая инициализация, переместив инициализацию в одну (обычно закрытую) функцию, которую вызывают все конструкторы. Этот подход может быть особенно полезен, если начальные значения должны быть загружены из файла или базы данных. Однако, вообще говоря, инициализация членов посредством списков инициализации более предпочтительна, чем псевдоинициализация присваиванием.
Один из аспектов C++, на который можно положиться, – это порядок, в котором инициализируются данные объектов. Этот порядок всегда один и тот же: базовые классы инициализируются раньше производных (см. также правило 12), а внутри класса члены-данные инициализируются в том порядке, в котором объявлены. Например, в классе ABEntry член theName всегда будет инициализирован первым, theAddress – вторым, thePhones – третьим, а numTimesConsulted – последним. Это верно даже в случае, если в списке инициализации членов они перечислены в другом порядке (что, к сожалению, не запрещено). Чтобы не вводить в заблуждение человека, читающего вашу программу, и во избежание ошибок непонятного происхождения, всегда перечисляйте данные-члены в списке инициализации в том порядке, в котором они объявлены в классе.
Позаботившись о явной инициализации объектов встроенных типов, которые не являются членами классов, и обеспечив правильную инициализацию базовых классов и их данных-членов посредством списков инициализации, у вас останется только одна вещь, о чем нужно будет подумать. Речь идет о порядке инициализации нелокальных статических объектов, объявленных в разных единицах трансляции.
Отнесемся к этой фразе со всем вниманием.
Статический объект существует от момента, когда был сконструирован, и до конца работы программы. Объекты, размещенные в стеке и в «куче», к статическим не относятся. Статическими являются глобальные объекты, объекты, объявленные в области действия пространства имен, объекты, объявленные с ключевым словом static внутри классов и функций, а также в области действия отдельного файла с исходным текстом. Статические объекты, объявленные внутри функций, известны как локальные статические объекты (поскольку они локальны по отношению к функции), а все прочие называют нелокальными статическими объектами. Статические объекты автоматически уничтожаются при завершении программы, то есть при выходе из функции main() автоматически вызываются их деструкторы.
Единица трансляции (translation unit) – это исходный код, который порождает отдельный объектный файл. Обычно это один исходный файл плюс все файлы, включенные в него директивой #include.
Проблема возникает, когда есть, по крайней мере, два отдельно компилируемых исходных файла, каждый из которых содержит, по крайней мере, один нелокальный статический объект (то есть глобальный объект либо объявленный в области действия пространства имен, класса или файла). Суть ее в том, что если инициализация нелокального статического объекта происходит в одной единице трансляции, а используется он в другой, то такой объект может оказаться неинициализированным в момент использования, поскольку относительный порядок инициализации нестатических локальных объектов, определенных в разных единицах трансляции, не определен.
Рассмотрим пример. Предположим, у вас есть класс FileSystem, который делает файлы из Internet неотличимыми от локальных. Поскольку ваш класс представляет мир как единую файловую систему, вы могли бы создать в глобальной области действия или в пространстве имен соответствующий ей специальный объект:
class FileSystem { // из вашей библиотеки
public:
...
std::size_t numDisks() const; // одна из многих функций-членов
...
};
extern FileSystem tfs; // объект для использования клиентами
// “tfs” = “the file system”
Класс FileSystem определенно не тривиален, поэтому использование объекта theFileSystem до того, как он будет сконструирован, приведет к катастрофическим последствиям.
Теперь предположим, что некий пользователь создает класс, описывающий каталоги файловой системы. Естественно, его класс будет использовать объект theFileSystem: