class Empty {};
эквиваленто следующему:
class Empty {
public:
Empty() {...} // конструктор по умолчанию
Empty(const Empty& rhs) {...} // конструктор копирования
~Empty() {...} // деструктор – см. ниже
// о виртуальных деструкторах
Empty& operator=(const Empty& rhs) {...} // оператор присваивания
};
Эти функции генерируются, только если они нужны, но мало найдется случаев, когда без них можно обойтись. Так, следующий код приведет к их автоматической генерации компилятором:
Empty e1; // конструктор по умолчанию;
// деструктор
Empty e2(e1); // конструктор копирования
e2 = e1; // оператор присваивания
Итак, компилятор пишет эти функции для вас, но что они делают? Конструктор по умолчанию и деструктор – это места, в которые компилятор помещает служебный код, например вызов конструкторов и деструкторов базовых классов и нестатических данных-членов. Отметим, что сгенерированный деструктор не является виртуальным (см. правило 7), если только речь не идет о классе, наследующем классу, у которого есть виртуальный деструктор (в этом случае виртуальность наследуется от базового класса).
Что касается конструктора копирования и оператора присваивания, то сгенерированные компилятором версии просто копируют каждый нестатический член данных исходного объекта в целевой. Например, рассмотрим шаблон NamedObject, который позволяет ассоциировать имена с объектами типа T:
Template
class NamedObject {
public:
NamedObject(const char *name, const T& value);
NamedObject(const std::string& name, const T& value);
...
private:
std:string nameValue;
T objectValue;
};
Поскольку в классе NamedObject объявлен конструктор, компилятор не станет генерировать конструктор по умолчанию. Это важно. Значит, если вы спроектировали класс так, что его конструктору обязательно должны быть переданы какие-то аргументы, то вам не нужно беспокоиться, что компилятор проигнорирует ваше решение и по собственной инициативе добавит еще и конструктор без аргументов.
В классе NamedObject нет ни конструктора копирования, ни оператора присваивания, поэтому компилятор сгенерирует их (при необходимости). Посмотрите на следующее употребление конструктора копирования:
NamedObjectno1(“Smallest Prime Number”, 2);
NamedObjectno2(no1); // вызывается конструктор копирования
Конструктор копирования, сгенерированный компилятором, должен инициализировать no2.nameValue и no2.objectValue, используя nol.nameValue и nol.objectValue соответственно. Член nameValue имеет тип string, а в стандартном классе string объявлен конструктор копирования, поэтому no2. nameValue будет инициализирован вызовом конструктора копирования string с аргументов nol.nameValue. С другой стороны, член NameObject::objectValue имеет тип int (поскольку T есть int в данной конкретизации шаблона), а int – встроенный тип, поэтому no2.objectValue будет инициализирован побитовым копированием nol.objectValue.
Сгенерированный компилятором оператор присваивания для класса Named-Object будет вести себя аналогичным образом, но, вообще говоря, сгенерированная компилятором версия оператора присваивания ведет себя так, как я описал, только в том случае, когда в результате получается корректный и осмысленный код. В противном случае компилятор не сгенерирует operator=.
Например, предположим, что класс NamedObject определен, как показано ниже. Обратите внимание, что nameValue – ссылка на string, а objectValue имеет тип const T:
template
class NamedObject {
public:
// этот конструктор более не принимает const name, поскольку nameValue –
// теперь ссылка на неконстантную строку. Конструктор с аргументом типа
// char* исключен, поскольку нам нужна строка, на которую можно сослаться
NamedObject(std::string& name, const T& value);
... // как и ранее, предполагаем,
// что operator= не объявлен
private:
std::string& nameValue; // теперь это ссылка
const T objectValue; // теперь const
};
Посмотрим, что произойдет в приведенном ниже коде:
std::string newDog(“Persephone”);
std::string oldDog(“Satch”);
NamedObject p(newDog, 2); // Когда я впервые написал это,
// наша собака Персефона собиралась
// встретить свой второй день рождения
NamedObject s(oldDog, 36); // Семейному псу Сатчу (из моего
// детства) было бы теперь 36 лет
p = s; // Что должно произойти
// с данными-членами p?
Перед присваиванием и p.nameValue, и s.nameValue ссылались на объекты string, хотя и на разные. Что должно произойти с членом p.nameValue в результате присваивания? Должен ли он ссылаться на ту же строку, что и s.nameValue, то есть должна ли модифицироваться ссылка? Если да, это подрывает основы, потому что C++ не позволяет изменить объект, на который указывает ссылка. Но, быть может, должна модифицироваться строка, на которую ссылается член p.nameValue, и тогда будут затронуты другие объекты, содержащие указатели или ссылки на эту строку, хотя они и не участвовали непосредственно в присваивании? Это ли должен делать сгенерированный компилятором оператор присваивания?
Читать дальше
Конец ознакомительного отрывка
Купить книгу