Краткий FAQ по C++
To the complete English version of this C++ FAQ Lite
[Этот документ является переводом сборника ответов на часто задаваемые вопросы группы новостей comp.lang.c++. Разделы будут добавляться по мере их перевода. Последнюю версию перевода можно найти по адресу http://quirks.chat.ru/cpp/faq. Ваши замечания и дополнения вы можете высылать мне по адресу: slava_mironov@mail.ru. Последнюю английскую версию этого сборника можно найти на http://www.parashift.com/c++-faq-lite.]
[Авторские права: Вы можете свободно использовать этот документ в некоммерческих целях и делать копии его или его частей для личного использования, при условии сохранения всей информации об авторских правах, включая это предупреждение. Не допускается цитирование этого документа без ссылки на источник. Для коммерческого использования этого документа требуется получить разрешение, во-первых, автора английской версии, во-вторых, автора этого перевода. Автором английской версии является Marshall Cline (cline@parashift.com), автором перевода - Ярослав Миронов (slava_mironov@mail.ru, tada@mail.wplus.net).]
[Все зарегистрированные торговые марки, упоминающиеся в настоящем документе, являются собственностью своих владельцев.]
[В случае, если какой-либо термин допускает неоднозначный перевод на русский язык, я старался передать его в таком виде, как это сделано в русской версии третьего издания книги Бьерна Страуструпа "Язык программирования С++" (BINOM Publishers/Невский Диалект, 1999), чтобы избежать возможной путаницы. Часто в таких случаях перевод термина сопровождается его английским оригиналом в скобках. — YM]
[8.2] Что происходит в результате присваивания ссылке?
[8.3] Что происходит, когда я возвращаю из функции ссылку?
[8.4] Как можно переустановить ссылку, чтобы она ссылалась на другой объект?
[8.5] В каких случаях мне стоит использовать ссылки, и в каких — указатели?
РАЗДЕЛ [9]: Встроенные (inline) функции
[9.1] Что такое встроенная функция?
[9.2] Как встроенные функции могут влиять на соотношение безопасности и скорости?
[9.3] Зачем мне использовать встроенные функции? Почему не использовать просто #define макросы?
[9.4] Что сделать, чтобы определить функцию — не член класса как встроенную?
[9.5] Как сделать встроенной функцию — член класса?
[9.6] Есть ли другой способ определить встроенную функцию — член класса?
[9.7] Обязательно ли встроенные функции приведут к увеличению производительности?
РАЗДЕЛ [10]: Конструкторы
[10.1] Что такое конструкторы?
[10.2] Есть ли разница между объявлениями List x; и List x();?
[10.3] Как из одного конструктора вызвать другой конструктор для инициализации этого объекта?
[10.4] Всегда ли конструктор по умолчанию для Fred выглядит как Fred::Fred()?
[10.5] Какой конструктор будет вызван, если я создаю массив объектов типа Fred?
[10.6] Должны ли мои конструкторы использовать "списки инициализации" или "присваивания значений"?
[10.7] Можно ли пользоваться указателем this в конструкторе?
[10.8] Что такое "именованный конструктор" ("Named Constructor Idiom")?
[10.10] Почему классы со статическими данными получают ошибки при компоновке?
[10.11] Что такое ошибка в порядке статической инициализации ("static initialization order fiasco")?
[10.12] Как предотвратить ошибку в порядке статической инициализации?
[10.13] Как бороться с ошибками порядка статической инициализации объектов - членов класса?
[10.14] Как мне обработать ошибку, которая произошла в конструкторе?
РАЗДЕЛ [11]: Деструкторы
[11.2] В каком порядке вызываются деструкторы для локальных объектов?
[11.3] В каком порядке вызываются деструкторы для массивов объектов?
[11.4] Могу ли я перегрузить деструктор для своего класса?
[11.5] Могу ли я явно вызвать деструктор для локальной переменной?
[11.7] Хорошо, я не буду явно вызывать деструктор. Но как мне справиться с этой проблемой?
[11.8] А что делать, если я не могу поместить переменную в отдельный блок?
[11.9] А могу ли я явно вызывать деструктор для объекта, созданного при помощи new?
[11.10] Что такое "синтаксис размещения" new ("placement new") и зачем он нужен?
[11.12] Когда я пишу деструктор производного класса, нужно ли мне явно вызывать деструктор предка?
РАЗДЕЛ [8]: Ссылки
Ссылка - это псевдоним (другое имя) для объекта.
Ссылки часто используются для передачи параметра по ссылке:
void swap(int& i, int& j) { int tmp = i; i = j; j = tmp; } int main() { int x, y; // ... swap(x,y); }
В этом примере i и j - псевдонимы для переменных x и y функции main. Другими словами, i — это x. Не указатель на x и не копия x, а сам x. Все, что вы делаете с i, проделывается с x, и наоборот.
Вот таким образом вы как программист должны воспринимать ссылки. Теперь, рискуя дать вам неверное представление, несколько слов о том, каков механизм работы ссылок. В основе ссылки i на объект x - лежит, как правило, просто машинный адрес объекта x. Но когда вы пишете i++, компилятор генерирует код, который инкрементирует x. В частности, сам адрес, который компилятор использует, чтобы найти x, остается неизменным. Программист на С может думать об этом, как если бы использовалась передача параметра по указателю, в духе языка С, но, во-первых, & (взятие адреса) было бы перемещено из вызывающей функции в вызываемую, и, во-вторых, в вызываемой функции были бы убраны * (разыменование). Другими словами, программист на С может думать об i как о макроопределении для (*p), где p — это указатель на x (т.е., компилятор автоматически разыменовывает подлежащий указатель: i++ заменяется на (*p)++, а i = 7 на *p = 7).
Важное замечание: несмотря на то что в качестве ссылки в окончательном машинном коде часто используется адрес, не думайте о ссылке просто как о забавно выглядящем указателе на объект. Ссылка — это объект. Это не указатель на объект и не копия объекта. Это сам объект.
[8.2] Что происходит в результате присваивания ссылке?
Вы меняете состояние ссыльного объекта (того, на который ссылается ссылка).
Помните: ссылка - это сам объект, поэтому, изменяя ссылку, вы меняете состояние объекта, на который она ссылается. На языке производителей компиляторов ссылка — это lvalue (left value - значение, которое может появиться слева от оператора присваивания).
[8.3] Что происходит, когда я возвращаю из функции ссылку?
В этом случае вызов функции может оказаться с левой стороны оператора (операции) присваивания.
На первый взгляд, такая запись может показаться странной. Например, запись f() = 7 выглядит бессмысленной. Однако, если a — это объект класса Array, для большинства людей запись a[i] = 7 является осмысленной, хотя a[i] - это всего лишь замаскированный вызов функции Array::operator[](int), которая является оператором обращения по индексу для класса Array:
class Array { public: int size() const; float& operator[] (int index); // ... }; int main() { Array a; for (int i = 0; i < a.size(); ++i) a[i] = 7; // В этой строке вызывается Array::operator[](int) }
[8.4] Как можно переустановить ссылку, чтобы она ссылалась на другой объект?
Невозможно в принципе.
Невозможно отделить ссылку от ее объекта.
В отличие от указателя, ссылка, как только она привязана к объекту, не может быть "перенаправлена" на другой объект. Ссылка сама по себе ничего не представляет, у нее нет имени, она сама — это другое имя для объекта. Взятие адреса ссылки дает адрес объекта, на который она ссылается. Помните: ссылка — это объект, на который она ссылается.
С этой точки зрения, ссылка похожа на const указатель [18.5], такой как int* const p (в отличие от указателя на const [18.4], такого как const int* p). Несмотря на большую схожесть, не путайте ссылки с указателями - это не одно и то же.
[8.5] В каких случаях мне стоит использовать ссылки, и в каких — указатели?
Используйте ссылки, когда можете, а указатели - когда это необходимо.
Ссылки обычно предпочтительней указателей, когда вам ненужно их "перенаправлять" [8.4]. Это обычно означает, что ссылки особенно полезны в открытой (public) части класса. Ссылки обычно появляются на поверхности объекта, а указатели спрятаны внутри.
Исключением является тот случай, когда параметр или возвращаемый из функции объект требует выделения "охранного" значения для особых случаев. Это обычно реализуется путем взятия/возвращения указателя, и обозначением особого случая при помощи передачи нулевого указателя (NULL). Ссылка же не может ссылаться на разыменованный нулевой указатель.
Примечание: программисты с опытом работы на С часто недолюбливают ссылки, из-за того что передача параметра по ссылке явно никак не обозначается в вызывающем коде. Однако с обретением некоторого опыта работы на С++, они осознают, что это одна из форм сокрытия информации, которая является скорее преимуществом, чем недостатком. Т.е., программисту следует писать код в терминах задачи, а не компьютера (programmers should write code in the language of the problem rather than the language of the machine).
РАЗДЕЛ [9]: Встроенные (inline) функции
[9.1] Что такое встроенная функция?
Встроенная функция — это функция, код которой прямо вставляется в том месте, где она вызвана. Как и макросы, определенные через #define, встроенные функции улучшают производительность за счет стоимости вызова и (особенно!) за счет возможности дополнительной оптимизации ("процедурная интеграция").
[9.2] Как встроенные функции могут влиять на соотношение безопасности и скорости?
В обычном С вы можете получить "инкапсулированные структуры", помещая в них указатель на void, и заставляя его указывать на настоящие данные, тип которых неизвестен пользователям структуры. Таким образом, пользователи не знают, как интерпретировать эти данные, а функции доступа преобразуют указатель на void к нужному скрытому типу. Так достигается некоторый уровень инкапсуляции.
К сожалению, этот метод идет вразрез с безопасностью типов, а также требует вызова функции для доступа к любым полям структуры (если вы позволили бы прямой доступ, то его мог бы получить кто угодно, поскольку будет известно, как интерпретировать данные, на которые указывает void*. Такое поведение со стороны пользователя приведет к сложностям при последующем изменении структуры подлежащих данных).
Стоимость вызова функции невелика, но дает некоторую прибавку. Классы С++ позволяют встраивание функций, что дает вам безопасность инкапсуляции вместе со скоростью прямого доступа. Более того, типы параметры встраиваемых функций проверяются компилятором, что является преимуществом по сравнению с (?)сишными #define макросами.
[9.3] Зачем мне использовать встроенные функции? Почему не использовать просто #define макросы?
Поскольку #define макросы опасны [9.3], опасны [34.1], опасны [34.2], опасны [34.3].
В отличие от #define макросов, встроенные (inline) функции неподвержены известным ошибкам двойного вычисления, поскольку каждый аргумент встроенной функции вычисляется только один раз. Другими словами, вызов встроенной функции — это то же самое что и вызов обычной функции, только быстрее:
// Макрос, возвращающий модуль (абсолютное значение) i #define unsafe(i) \ ( (i) >= 0 ? (i) : -(i) ) // Встроенная функция, возвращающая абсолютное значение i inline int safe(int i) { return i >= 0 ? i : -i; } int f(); void userCode(int x) { int ans; ans = unsafe(x++); // Ошибка! x инкрементируется дважды ans = unsafe(f()); // Опасно! f() вызывается дважды ans = safe(x++); // Верно! x инкрементируется один раз ans = safe(f()); // Верно! f() вызывается один раз }
Также, в отличие от макросов, типы аргументов встроенных функций проверяются, и выполняются все необходимые преобразования.
Макросы вредны для здоровья; не используйте их, если это не необходимо
[9.4] Что сделать, чтобы определить функцию - не член класса как встроенную?
Когда вы объявляете встроенную функцию, это выглядит как обычное объявление функции:
void f(int i, char c);
Но перед определением встроенной функции пишется слово inline, и само определение помещается в заголовочный файл:
inline void f(int i, char c) { // ... }
Примечание: Необходимо, чтобы определение встроенной функции (часть между {...}) была помещена в заголовочный файл, за исключением того случая, когда функция используется только в одном .cpp файле. Если вы помещаете определение встроенной функции в .cpp файл, а вызываете ее из другого .cpp файла, то вы получаете ошибку "unresolved external" ("ненайденный внешний объект") от компоновщика (linker).
(Примечание переводчика: На всякий случай уточню, что само помещение определения функции в заголовочный файл НЕ делает ее встроенной. Это требуется только для того, чтобы тело функции было видно во всех местах, где она вызывается. Иначе невозможно обеспечить встраивание функции. — YM)
[9.5] Как сделать встроенной функцию — член класса?
Когда вы объявляете встроенную функцию - член класса, это выглядит как обычное объявление функции — члена:
class Fred { public: void f(int i, char c); };
Но когда перед определением встроенной функции пишется слово inline, а само определение помещается в заголовочный файл:
inline void Fred::f(int i, char c) { // ... }
Примечание: Необходимо, чтобы определение встроенной функции (часть между {...}) была помещена в заголовочный файл, за исключением того случая, когда функция используется только в одном .cpp файле. Если вы помещаете определение встроенной функции в .cpp файл, а вызываете ее из другого .cpp файла, то вы получаете ошибку "unresolved external" ("ненайденный внешний объект") от компоновщика (linker).
[9.6] Есть ли другой способ определить встроенную функцию — член класса?
Да, определите функцию-член класса в теле самого класса:
class Fred { public: void f(int i, char c) { // ... } };
Хотя такой вид определения проще для создателя класса, но он вызывает определенные трудности для пользователя, поскольку здесь смешивается, что делает класс и как он это делает. Из-за этого неудобства предпочтительно определять функции-члены класса вне тела класса, используя слово inline [9.5]. Причина такого предпочтения проста: как правило, множество людей используют созданный вами класс, но только один человек пишет его (вы); предпочтительно делать вещи, облегчающие жизнь многим
[9.7] Обязательно ли встроенные функции приведут к увеличению производительности?
Нет.
Слишком большое количество встроенных функций может привести к увеличению размера кода, что в свою очередь может оказать негативное влияние на скорость в системах со страничной организацией памяти.
РАЗДЕЛ [10]: Конструкторы
[10.1] Что такое конструкторы?
Конструкторы делают объекты из ничего.
Конструкторы похожи на инициализирующие функции. Они превращают свалку случайных бит в работающий объект. В минимальном случае, они инициализируют используемые переменные класса. Также они могут выделять ресурсы (память, файлы, флажки, сокеты и т. п.).
"ctor" — часто используемое сокращение для слова конструктор.
[10.2] Есть ли разница между объявлениями List x; и List x();?
Огромная!
Предположим, что List - это имя класса. Тогда функция f() объявляет локальный объект типа List с именем x:
void f() { List x; // Локальный объект с именем x (класса List) // ... }
Но функция g() объявляет функцию x(), которая возвращает объект типа List:
void g() { List x(); // Функция с именем x (возвращающая List) // ... }
[10.3] Как из одного конструктора вызвать другой конструктор для инициализации этого объекта?
(Имеются в виду несколько перегруженных конструкторов для одного объекта - примечание переводчика.)
Никак.
Проблема вот в чем: если вы вызовете другой конструктор, компьютер создаст и проинициализирует временный объект, а не объект, из которого вызван конструктор. Вы можете совместить два конструктора, используя значения параметров по умолчанию, или вы можете разместить общий для двух конструкторов код в закрытой (private) функции - члене init().
[10.4] Всегда ли конструктор по умолчанию для Fred выглядит как Fred::Fred()?
Нет. Конструктор по умолчанию — это конструктор, который можно вызывать без аргументов. Таким образом, конструктор без аргументов безусловно является конструктором по умолчанию:
class Fred { public: Fred(); // Конструктор по умолчанию: может вызываться без аргументов // ... };
Однако возможно (и даже вероятно), что конструктор по умолчанию может принимать аргументы, при условии что для всех них заданы значения по умолчанию:
class Fred { public: Fred(int i=3, int j=5); // Конструктор по умолчанию: может вызываться без аргументов // ... };
[10.5] Какой конструктор будет вызван, если я создаю массив объектов типа Fred?
Конструктор по умолчанию [10.4] для класса Fred (за исключением случая, описанного ниже)
Не существует способа заставить компилятор вызвать другой конструктор (за исключением способа, описанного ниже). Если у вашего класса Fred нет конструктора по умолчанию [10.4], то при попытке создания массива объектов типа Fred вы получите ошибку при компиляции.
class Fred { public: Fred(int i, int j); // ... предположим, что для класса Fred нет конструктора по умолчанию [10.4]... }; int main() { Fred a[10]; // ОШИБКА: У Fred нет конструктора по умолчанию Fred* p = new Fred[10]; // ОШИБКА: У Fred нет конструктора по умолчанию }
Однако если вы создаете, пользуясь STL [32.1], vector<Fred> вместо простого массива (что вам скорее всего и следует делать, поскольку массивы опасны [21.5]), вам не нужно иметь конструктор по умолчанию в классе Fred, поскольку вы можете задать объект типа Fred для инициализации элементов вектора:
#include <vector> using namespace std; int main() { vector<Fred> a(10, Fred(5,7)); // Десять объектов типа Fred // будут инициализированы Fred(5,7). // ... }
Хотя вам следует пользоваться векторами, а не массивами, иногда бывают ситуации, когда необходим именно массив. Специально для таких случаев существует способ записи явной инициализации массивов. Вот как это выглядит:
class Fred { public: Fred(int i, int j); // ... предположим, что для класса Fred // нет конструктора по умолчанию [10.4]... }; int main() { Fred a[10] = { Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7), Fred(5,7) }; // Десять объектов массива Fred // будут инициализированы Fred(5,7). // ... }
Конечно, вам не обязательно использовать Fred(5,7) для каждого элемента. Вы можете использовать любые числа или даже параметры и другие переменные. Суть в том, что такая запись (a) возможна, но (б) не так хороша, как запись для вектора. Помните: массивы опасны [21.5]. Если у вы не вынуждены использовать массивы - используйте вектора.
[10.6] Должны ли мои конструкторы использовать "списки инициализации" или "присваивания значений"?
Конструкторы должны инициализировать все члены в списках инициализации.
Например, пусть конструктор инициализирует член x_, используя список инициализации: Fred::Fred() : x_(какое-то-выражение) { }. С точки зрения производительности важно заметить, что какое-то-выражение не приводит к созданию отдельного объекта для копирования его в x_: если типы совпадают, то какое-то-выражение будет создано прямо в x_.
Напротив, следующий конструктор использует присваивание: Fred::Fred() { x_ = какое-то-выражение; }. В этом случае какое-то-выражение приводит к созданию отдельного временного объекта, который потом передается в качестве параметра оператору присваивания объекта x_, а потом уничтожается при достижении точки с запятой. Это неэффективно.
Есть и еще один источник неэффективности: во втором случае (с присваиванием) конструктор по умолчанию для объекта (неявно вызванный до { тела конструктора) мог, например, выделить по умолчанию некоторое количество памяти или открыть файл. Вся эта работа окажется проделанной впустую, если какое-то-выражение и/или оператор присваивания привели к закрытию этого файла и/или освобождению памяти (например, если конструктор по умолчанию выделил недостаточно памяти или открыл не тот файл).
Выводы: при прочих равных условиях ваш код будет более быстрым, если вы используете списки инициализации, а не операторы присваивания.
[10.7] Можно ли пользоваться указателем this в конструкторе?
Некоторые люди не рекомендуют использовать указатель this в конструкторе, потому что объект, на который указывает this еще не полностью создан. Тем не менее, при известной осторожности, вы можете использовать this в конструкторе (в {теле} и даже в списке инициализации [10.6]).
Как только вы попали в {тело} конструктора, легко себе вообразить, что можно использовать указатель this, поскольку все базовые классы и все члены уже полностью созданы. Однако даже здесь нужно быть осторожным. Например, если вы вызываете виртуальную функцию (или какую-нибудь функцию, которая в свою очередь вызывает виртуальную функцию) для этого объекта, мы можете получить не совсем то, что хотели [23.1].
На самом деле вы можете пользоваться указателем this даже в списке инициализации конструктора [10.6], при условии что вы достаточно осторожны, чтобы по ошибке не затронуть каких-либо объектов-членов или базовых классов, которые еще не были созданы. Это требует хорошего знания деталей порядка инициализации в конструкторе, так что не говорите, что вас не предупреждали. Самое безопасное - сохранить где-нибудь значение указателя this и воспользоваться им потом. [Не понял, что они имеют в виду. — YM]
[10.8] Что такое "именованный конструктор" ("Named Constructor Idiom")?
Это техника обеспечивает более безопасный и интуитивно понятный для пользователей процесс создания для вашего класса.
Проблема заключается в том, что конструкторы всегда носят то же имя, что и их класс. Таким образом, единственное различие между конструкторами одного класса — это их список параметров. И существует множество случаев, когда разница между конструкторами становится весьма незначительной, что ведет к ошибкам.
Для использования именованных конструкторов вы объявляете все конструкторы класса в закрытом (private:) или защищенном (protected:) разделе, и пишете несколько открытых (public:) статических методов, которые возвращают объект. Эти статические методы и называются "именованными конструкторами". В общем случае существует по одному такому конструктору на каждый из различных способов создания класса.
Например, допустим, у нас есть класс Point, который представляет точку на плоскости X — Y. Существуют два распространенных способа задания двумерных координат: прямоугольные координаты (X + Y) и полярные координаты (радиус и угол). (Не беспокойтесь, если вы не разбираетесь в таких вещах, суть примера не в этом. Суть в том, что существует несколько способов создания объекта типа Point.) К сожалению, типы параметров для этих двух координатных систем одни и те же: два числа с плавающей точкой. Это привело бы к неоднозначности, если бы мы сделали перегруженные конструкторы:
class Point { public: Point(float x, float y); // Прямоугольные координаты Point(float r, float a); // Полярные координаты (радиус и угол) // ОШИБКА: Неоднозначная перегруженная функция: Point::Point(float,float) }; int main() { Point p = Point(5.7, 1.2); // Неоднозначность: Какая координатная система? }
Одним из путей решения этой проблемы и являются именованные конструкторы:
#include <math.h> // Для sin() и cos() class Point { public: static Point rectangular(float x, float y); // Прямоугольные координаты static Point polar(float radius, float angle); // Полярные координаты // Эти статические члены называются "именованными конструкторами" // ... private: Point(float x, float y); // Прямоугольные координаты float x_, y_; }; inline Point::Point(float x, float y) : x_(x), y_(y) { } inline Point Point::rectangular(float x, float y) { return Point(x, y); } inline Point Point::polar(float radius, float angle) { return Point(radius*cos(angle), radius*sin(angle)); }
Теперь у пользователей класса Point появился способ ясного и недвусмысленного создания точек в обеих системах координат:
int main() { Point p1 = Point::rectangular(5.7, 1.2); // Ясно, что прямоугольные координаты Point p2 = Point::polar(5.7, 1.2); // Ясно, что полярные координаты }
Обязательно помещайте ваши конструкторы в защищенный (protected:) раздел, если вы планируете создавать производные классы от Fred. [Видимо, ошибка. Хотели сказать — Point. - YM]
Именованные конструкторы также можно использовать том в случае, если вы хотите, чтобы ваши объекты всегда создавались динамически (посредством new [16.19]).
[10.9] Почему я не могу проинициализировать статический член класса в списке инициализации конструктора?
Потому что вы должны отдельно определять статические данные классов.
Fred.h:
class Fred { public: Fred(); // ... private: int i_; static int j_; };
Fred.cpp (или Fred.C, или еще как-нибудь):
Fred::Fred() : i_(10), // Верно: вы можете (и вам следует) // инициализировать переменные — члены класса таким образом j_(42) // Ошибка: вы не можете инициализировать // статические данные класса таким образом { // ... } // Вы должны определять статические данные класса вот так: int Fred::j_ = 42;
[10.10] Почему классы со статическими данными получают ошибки при компоновке?
Потому что статические данные класса должны быть определены только в одной единице трансляции [10.9]. Если вы не делаете этого, вы вероятно получите при компоновке ошибку "undefined external" ("внешний объект не определен"). Например:
// Fred.h class Fred { public: // ... private: static int j_; // Объявляет статическую переменную Fred::j_ // ... };
Компоновщик пожалуется ("Fred::j_ is not defined" / "Fred::j_ не определено"), если вы не напишите определение (в отличие от просто объявления) Fred::j_ в одном (и только в одном) из исходных файлов:
// Fred.cpp #include "Fred.h" int Fred::j_ = некоторое_выражение_приводимое_к_int; // По-другому, если вы желаете получить неявную инициализацию нулем для int: // int Fred::j_;
Обычное место для определения статических данных класса Fred — это файл Fred.cpp (или Fred.C, или другое используемое вами расширение).
[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.