Материалы книги получены с http://www.itlibitum.ru/
Конструкторы
Конструктор можно рассматривать двояко - как функцию, инициализирующую объект, или, с позиций математики, как отображение аргументов конструктора на домен класса. Я предпочитаю второй подход, поскольку он помогает разобраться с некоторыми языковыми средствами (например, операторами преобразования).
С конструкторами связаны очень сложные правила, но каждый программист C++ должен досконально знать их, иначе минимум три ночи в году ему придется проводить за отладкой.
Конструкторы без аргументов
Если в вашем классе имеется конструктор, который вызывается без аргументов, он используется по умолчанию в трех следующих случаях.
class Foo {
public:
Foo();
};
class Bar : public Foo { // 1. Базовый класс
public:
Bar();
};
class BarBar {
private:
Foo f; // 2. Переменная класса
};
Foo f; // 3. Созданный экземпляр Foo
Foo* f1 = new Foo; // 3. То же, что и предыдущая строка
Если в списке инициализации членов (см. следующий раздел) конструктора Bar не указан какой-нибудь другой конструктор Foo, то при каждом создании экземпляра Ваг будет вызываться конструктор Foo без аргументов. Аналогично, если f отсутствует в списке инициализации членов конструктора BarBar, будет использован конструктор Foo без аргументов. Наконец, при каждом создании экземпляра Foo без указания конструктора по умолчанию используется конструктор без аргументов.
Конструкторы с аргументами
Конструкторы, как и все остальные функции, можно перегружать. Вы можете объявить столько сигнатур конструкторов, сколько вам потребуется. Единственное настоящее отличие между сигнатурами конструкторов и обычных функций заключается в том, что конструкторы не имеют возвращаемого значения и не могут объявляться константными. Если вы объявите какие-либо конструкторы с аргументами, но не объявите конструктора без аргументов, то компилятор не позволит конструировать объекты этого класса, даже в качестве базового для другого класса, с использованием конструктора без аргументов.
class Foo {
public:
Foo(char*);
};
Foo f; // Нельзя - нет конструктора без аргументов!
class Bar : public Foo {
public:
Bar();
};
Bar::Bar()
{
// Ошибка! Нет конструктора Foo без аргументов
}
Списки инициализации членов
Чтобы избавиться от этой проблемы, в C++ находится очередное применение символу : - для создания списков инициализации членов. Так называется список спецификаций конструкторов, разделенных занятыми и расположенных между сигнатурой конструктора и его телом.
class Foo {
public:
Foo(char*);
};
class Bar : public Foo {
public:
Bar(char*);
};
class BarBar {
private:
Foo f;
int x;
public:
BarBar();
};
Bar::Bar(char* s) : Foo(s) {...}
BarBar::BarBar : f("Hello"), x(17) {...}
В конструкторе Bar список инициализации членов используется для инициализации базового класса Foo. Компилятор выбирает используемый конструктор на основании сигнатуры, определяемой по фактическим аргументам. При отсутствии списка инициализации членов сконструировать Bar было бы невозможно, поскольку компилятор не мог бы определить, какое значение должно передаваться конструктору базового класса Foo. В конструкторе BarBar список инициализации членов использовался для инициализации (то есть вызова конструкторов) переменных f и х. В следующем варианте конструктор работает не столь эффективно (если только компилятор не отличается
сверхъестественным интеллектом):
BarBar::BarBar() : f("Hello")
{
x = 17;
}
Во втором варианте переменная х сначала инициализируется значением 0 (стандартное требование C++) с использованием по умолчанию конструктора int без аргументов, а затем в теле конструктора ей присваивается значение 17. В первом варианте имеется всего одна инициализация и потому экономится один-два машинных такта. В данном примере это несущественно, поскольку переменная х - целая, но если бы она относилась к более сложному классу с конструктором без аргументов и перегруженным оператором присваивания, то разница была бы вполне ощутима.
Списки инициализации членов нужны там, где у базового класса или переменной нет конструктора без аргументов (точнее, есть один и более конструктор с аргументами, но нет ни одного определенного пользователем конструктора без аргументов). Списки инициализации членов не обязательны в тех ситуациях, когда все базовые классы и переменные класса либо не имеют конструкторов, либо имеют пользовательский конструктор без аргументов.
Порядок вызова конструкторов
Если класс не содержит собственных конструкторов, он инициализируется так, словно компилятор создал конструктор без аргументов за вас. Этот конструктор вызывает конструкторы без аргументов базовых классов и переменных класса. Четко определенный порядок вызова конструкторов не зависит от того, используются конструкторы стандартные или перегруженные, с аргументами или без:
1. Сначала вызываются конструкторы базовых классов в порядке их перечисления в списке наследования (еще один список, в котором после символа : перечисляются базовые классы, разделенные запятыми).
2. Затем вызываются конструкторы переменных класса в порядке их объявления в объявлении класса.
3. После того как будут сконструированы все базовые классы и переменные, выполняется тело вашего конструктора.
Описанный порядок применяется рекурсивно, то есть первым конструируется первый базовый класс первого базового класса... и т. д. Он не зависит от порядка, указанного в списке инициализации членов.
Если бы дело обстояло иначе, для разных перегруженных конструкторов мог бы использоваться разный порядок конструирования. Тогда компилятору было бы трудно гарантировать, что деструкторы будут вызываться в порядке, обратном порядку вызова конструкторов.
Конструкторы копий
Конструктор копий (copy constructor) определяется специальной сигнатурой:
class Foo {
public:
Foo(const Foo&);
};
Foo::Foo(const Foo& f)...
Конструктор копий предназначен для создания копий объектов. Эта задача может возникнуть в самых разных обстоятельствах.
void Fn(Foo f) {...}
void Gn(Foo& f) {...}
Foo f;
Foo f1(f);
Foo f2 = f; // Конструирование, а не присваивание!
Fn(f); // Вызывает конструктор копий для передачи по назначению
const Foo f3;
Gn(f3); // Конструктор копий используется
// для создания неконстантной копии
Давайте внимательно рассмотрим этот фрагмент. Строка Foo f1(f); создает новый экземпляр класса Foo, передавая другой экземпляр класса Foo в качестве аргумента. Это всегда можно сделать, если класс Foo не содержит чисто виртуальных функций. Не важно, объявили ли вы свой собственный конструктор копий; если нет, компилятор построит его за вас. Не важно, есть ли в Foo другие пользовательские конструкторы; в отличие от конструкторов без аргументов, конструктор копий доступен всегда.
Строка Foo f2 = f выглядит как присваивание из-за присутствия оператора =, но на самом деле это альтернативный вариант вызова конструктора копий. Чтобы понять, чем присваивание отличается от инициализации, спросите себя: «Был ли объект сконструирован заранее или же его создание является частью команды?» Если объект уже существует, вы имеете дело с присваиванием. Если он создается на месте, как в пашем примере, используется конструктор копий.
При вызове функции Fn() происходит передача по значению копии Foo. Конструктор копий используется для создания временной копии, существующей лишь во время выполнения Fn(). После этого вызывается деструктор копии, который уничтожает ее.
Вызов функции Gn(), вероятно, ошибочен, и хороший компилятор прочитает вам суровую нотацию о стиле программирования на C++ - что-нибудь вроде: «Создается временная неконстантная копия - поучись программировать, тупица!» По крайней мере, со мной компиляторы обычно поступают именно так. Проблема заключается в том, что аргумент
передается по ссылке, однако фактический аргумент является константным, а формальный - нет. Все изменения аргумента внутри Gn() вносятся в копию, а не в оригинал.
В создаваемом компилятором конструкторе копий по умолчанию используется строго определенная последовательность вызова конструкторов копий базовых классов и переменных класса.
1. Конструкторы копий базовых классов вызываются в том порядке, в котором они объявлены в списке наследования.
2. Конструкторы копий переменных вызываются в том порядке, в котором они объявлены в объявлении класса.
Описанный порядок применяется рекурсивно, то есть первым копируется первый базовый класс первого базового класса... и т. д. Звучит знакомо, не правда ли? Тот же порядок, что и для любого другого конструктора.
С конструкторами копий, в отличие от всех остальных, компилятор ведет себя гордо и ревниво. Если вы перегрузите конструктор копий для некоторого класса, компилятор, фигурально выражаясь, умывает руки и отправляется домой. При отсутствии явного вызова конструкторов копий базовых классов и переменных класса в списке инициализации членов вашего собственного конструктора копий компилятор будет использовать конструктор без аргументов для инициализации базовых классов и переменных.
class Foo {...};
class Bar : public Foo {
private:
Foo f;
public:
Bar(const Bar&);
};
// Вероятно, ошибка
Bar::Bar(const Bar& b)
{
// Стоп! Нет списка инициализации членов
// Будут использованы конструкторы без аргументов
// базового класса и переменной
}
// Вероятно, ошибки нет
Bar::Bar(const Bar& b) : Foo(b), f(b.f) {...}
37
Компилятор очень сильно обидится на первый конструктор копий - так сильно, что он спустит ваше произведение в мусоропровод и даже не сообщит об этом. Для инициализации базового класса и переменной будет использован конструктор Foo без аргументов. В 99 случаях из 100 это совсем не то, чего вы добивались; обычно требуется, чтобы базовые классы и переменные тоже копировались.
Вероятно, второй вариант правилен. Базовый класс и переменная присутствуют в списке
инициализации членов, поэтому будут вызваны их конструкторы копий (компилятор преобразует b к типу Foo в выражении Foo(b)).
В некоторых ситуациях вас интересует именно поведение компилятора по умолчанию. В качестве примера рассмотрим следующий базовый класс, который присваивает уникальный серийный номер каждому производному объекту.
class Serialized {
private:
static int NextSerialNumber;
int serialNumber;
public:
Serialized(const Serialized&);
Serialized();
int SerialNumber();
};
// В Serialized.cpp
int Serialized::NextSerialNumber = 0;
Serialized::Serialized() : serialNumber(NextSerialNumber++)
{
}
Serialized::Serialized(const Serialized&) : serialNumber(NextSerialNumber++)
{
}
int Serialized::SerialNumber()
{
return serialNumber;
}
Нac не интересует, какой конструктор - без аргументов или копий - выберет компилятор во время компиляции производного класса, поскольку мы перегрузили оба конструктора, и они делают одно и то же.
Закрытые и защищенные конструкторы
Конструкторы часто объявляются закрытыми и защищенными, чтобы пользователи не могли создавать экземпляры класса. Если конструктор объявлен закрытым; только обычные и статические функции класса могут создавать стековые экземпляры класса или использовать его в операторе new (по крайней мере, с данным конструктором). Если конструктор объявлен защищенным, пользователь может создавать экземпляры базового класса, поскольку конструктор базового класса может «вызываться» из конструктора производного класса. Пусть для этого потребуется некоторое воображение, но семантика
именно такова. У этой логики есть один недостаток - она не совсем надежна. Если конструктор защищен, любая функция базового или производного класса (включая статические) может создать экземпляр базового класса.
class Foo {
protected:
Foo();
};
38
class Bar : public Foo {
public:
Foo* Fn();
};
Foo Bar::Fn()
{
return new Foo; // Работает вопреки всем вашим усилиям
}
Возможно, вы полагали, что Foo - абстрактный базовый класс и его экземпляры создать невозможно. Оказывается, ничего подобного! В системе защиты открывается зияющая дыра. Друзья классов Foo и Bar тоже могут создавать экземпляры Foo. Единственный «железный» способ, который стопроцентно гарантирует невозможность создания экземпляров класса - включение в него хотя бы одной чисто
виртуальной функции.
Анонимные экземпляры
Анонимным экземпляром (anonymous instance) называется объект, который... впрочем, сейчас увидите.
struct Point {
int X;
int Y;
Point(int x, int y) : X(x), Y(y) {}
};
double distance(Point p)
{
return sqrt(double(p.X) * double(p.X) + double(p.Y) * double(p.Y));
}
double d = distance(Point(17, 29));
Аргумент функции distance() представляет собой анонимный экземпляр. Мы не создали переменной для его хранения. Анонимный экземпляр существует лишь во время вычисления выражения, в котором он встречается.
Анонимные экземпляры обычно связываются с простыми структурами вроде Point, но их можно использовать для любого класса.
Инициализация глобальных объектов
В спецификации языка порядок конструирования глобальных объектов выглядит довольно сложно. Если же учесть причуды коммерческих компиляторов C++, этот порядок становится и вовсе непредсказуемым. В соответствии со спецификацией должны вызываться конструкторы глобальных объектов, включая конструкторы статических переменных классов и структур, однако многие компиляторы этого не делают. Если вам повезло и ваш компилятор считает, что конструкторы важны для глобальных переменных, порядок конструирования глобальных объектов зависит от воображения разработчика компилятора. Ниже перечислены некоторые правила, которые теоретически должны
соблюдаться:
1. Перед выполнением каких-либо операций все глобальные переменные инициализируются значением 0.
2. Объекты, находящиеся в глобальных структурах или массивах, конструируются в порядке их появления в структуре или массиве.
3. Каждый глобальный объект конструируется до его первого использования в программе.
Компилятор сам решает, следует ли выполнить инициализацию до вызова функции main() или отложить ее до первого использования объекта.
4. Глобальные объекты, находящиеся в одном «модуле трансляции» (обычно файле с
расширением .срр), инициализируются в порядке их появления в этом модуле. В сочетании с правилом 3 это означает, что инициализация может выполняться по модулям, при первом использовании каждого модуля.
Вот и все. Внешне простая последовательность глобальных объявлений на самом деле полностью подчиняется всем капризам разработчика компилятора. Она может привести к нужному результату или сгореть синим пламенем.
// В файле file1.cpp
Foo foo;
Foo* f = &foo;
// В файле file2.cpp
extern Foo* f;
Foo f1(*f); // Используется конструктор копий
Если бы все это находилось в одном исходном файле, ситуация была бы нормальной. Со строкой Fоо* f = &foo; проблем не возникает, поскольку глобальные объекты одного исходного файла заведомо (хе-хе) инициализируются в порядке их определения. Другими словами, когда программа доберется до этой строки, объект foo уже будет сконструирован. Тем не менее, никто не гарантирует, что глобальные объекты в файле file1.cpp будут инициализированы раньше глобальных объектов в файле file2.срр. Если file2.cpp будет обрабатываться первым, f оказывается равным 0 (NULL на большинстве
компьютеров), и при попытке получить по нему объект ваша программа героически умрет.
Лучший выход - сделать так, чтобы программа не рассчитывала на конкретный порядок
инициализации файлов .срр. Для этого используется стандартный прием - в заголовочном файле .h определяется глобальный объект со статической переменной, содержащей количество инициализированных файлов .срр. При переходе от 0 к 1 вызывается функция, которая инициализирует все глобальные объекты библиотечного файла .срр. При переходе от 1 к 0 все объекты этого файла уничтожаются.
// В файле Library.h
class Library {
private:
static int count;
static void OpenLibrary();
static void CloseLibrary();
public:
Library();
~Library();
};
static Library LibraryDummy;
inline Library::Library()
{
if (count++ == 0)
OpenLibrary();
}
inline Library::~Library()
{
if (--count == 0)
CloseLibrary();
}
// В Library.cpp
int Library::count = 0; // Делается перед выполнением вычислений
int aGlobal;
40
Foo* aGlobalFoo;
void Library::OpenLibrary()
{
aGlobal = 17;
aGlobalFoo = new Foo;
}
void Library::CloseLibrary()
{
aGlobal = 0;
delete aGlobalFoo;
aGlobalFoo = NULL;
}
К этому нужно привыкнуть. А происходит следующее: файл .h компилируется со множеством других файлов .срр, один из которых - Library.cpp. Порядок инициализации глобальных объектов, встречающихся в этих файлах, предсказать невозможно. Тем не менее, каждый из них будет иметь свою статическую копию LibraryDummy. При каждой инициализации файла .срр, в который включен файл Library.h, конструктор LibraryDummy увеличивает счетчик. При выходе из main() или при вызове exit() файлы .срр уничтожают глобальные объекты и уменьшают счетчик в деструкторе LibraryDummy. Конструктор и деструктор гарантируют, что OpenLibrary() и CloseLibrary() будут вызваны ровно один раз.
Этот прием приписывается многим разным программистам, но самый известный пример его использования встречается в библиотеке iostream. Там он инициализирует большие структуры данных, с которыми работает библиотека, ровно один раз и лишь тогда, когда это требуется.
Назад Содержание Далее
|