Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ
Шрифт:
Таким образом, чтобы избежать использования объектов до их инициализации, вам следует сделать три вещи. Первое: вручную инициализировать не являющиеся членами объекты встроенных типов. Второе: использовать списки инициализации членов для всех частей объекта. И наконец, третье: обойти за счет правильного проектирования проблему негарантированного порядка инициализации нелокальных статических объектов, определенных в разных единицах трансляции.
• Всегда вручную инициализировать объекты встроенных типов, поскольку C++ делает это, только не всегда.
• В конструкторе отдавать
• Избегайте проблем с порядком инициализации в разных единицах трансляции, заменяя нелокальные статические объекты локальными статическими объектами.
Глава 2
Конструкторы, деструкторы и операторы присваивания
Почти во всех ваших классах будут определены один или несколько конструкторов, деструктор и оператор присваивания. Это функции, которые отвечают за операции создания и инициализации объекта, его уничтожения, а также присваивания ему нового значения. Ошибки в этих функциях приводят к далеко идущим и неприятным последствиям и отражаются на всех ваших классах, поэтому они должны быть написаны правильно. В настоящей главе изложены правила по программированию функций, составляющих основу классов.
Правило 5: Какие функции C++ создает и вызывает молча
Когда пустой класс перестает быть пустым? Когда за него берется C++. Если вы не объявите конструктор копирования, оператор присваивания или деструктор самостоятельно, то компилятор сделает это за вас. Более того, если вы не объявите вообще никакого конструктора, то компилятор автоматически создаст конструктор по умолчанию. Все эти функции будут открытыми и встроенными (см. правило 30). Например, такое объявление:
эквиваленто следующему:
Эти функции генерируются, только если они нужны, но мало найдется случаев, когда без них можно обойтись. Так, следующий код приведет к их автоматической генерации компилятором:
Итак, компилятор пишет эти функции для вас, но что они делают? Конструктор по умолчанию и деструктор – это места, в которые компилятор помещает служебный код, например вызов конструкторов и деструкторов базовых классов и нестатических данных-членов. Отметим, что сгенерированный деструктор не является виртуальным (см. правило 7), если только речь не идет о классе, наследующем классу, у которого есть виртуальный деструктор (в этом случае виртуальность наследуется от базового класса).
Что касается конструктора
копирования и оператора присваивания, то сгенерированные компилятором версии просто копируют каждый нестатический член данных исходного объекта в целевой. Например, рассмотрим шаблон NamedObject, который позволяет ассоциировать имена с объектами типа T:Поскольку в классе NamedObject объявлен конструктор, компилятор не станет генерировать конструктор по умолчанию. Это важно. Значит, если вы спроектировали класс так, что его конструктору обязательно должны быть переданы какие-то аргументы, то вам не нужно беспокоиться, что компилятор проигнорирует ваше решение и по собственной инициативе добавит еще и конструктор без аргументов.
В классе NamedObject нет ни конструктора копирования, ни оператора присваивания, поэтому компилятор сгенерирует их (при необходимости). Посмотрите на следующее употребление конструктора копирования:
Конструктор копирования, сгенерированный компилятором, должен инициализировать no2.nameValue и no2.objectValue, используя nol.nameValue и nol.objectValue соответственно. Член nameValue имеет тип string, а в стандартном классе string объявлен конструктор копирования, поэтому no2. nameValue будет инициализирован вызовом конструктора копирования string с аргументов nol.nameValue. С другой стороны, член NameObject<int>::objectValue имеет тип int (поскольку T есть int в данной конкретизации шаблона), а int – встроенный тип, поэтому no2.objectValue будет инициализирован побитовым копированием nol.objectValue.
Сгенерированный компилятором оператор присваивания для класса Named-Object<int> будет вести себя аналогичным образом, но, вообще говоря, сгенерированная компилятором версия оператора присваивания ведет себя так, как я описал, только в том случае, когда в результате получается корректный и осмысленный код. В противном случае компилятор не сгенерирует operator=.
Например, предположим, что класс NamedObject определен, как показано ниже. Обратите внимание, что nameValue – ссылка на string, а objectValue имеет тип const T: