Объектно-ориентированное проектирование и связанные с ним шаблоны проектирования — это обширный вопрос, и имеется большое количество различной литературы на эту тему. В этой главе я упоминаю названия только некоторых шаблонов проектирования, и это шаблоны, для которых возможности C++ обеспечивают элегантное или, возможно, не совсем очевидное решение. Если вы не знакомы с концепцией шаблонов проектирования, я рекомендую прочесть книгу Design Patterns (Addison Wesley), поскольку это полезная вещь при разработке программного обеспечения. Однако для этой главы знание шаблонов проектирования не требуется.
8.1. Инициализация переменных-членов класса
Требуется инициализировать переменные-члены, которые имеют встроенные типы, являются указателями или ссылками.
Для установки начальных значений переменных членов используйте список инициализации. Пример 8.1 показывает, как это делается для встроенных типов, указателей и ссылок.
Пример 8.1. Инициализация членов класса
#include <string>
using namespace std;
class Foo {
public:
Foo() : counter_(0), str_(NULL) {}
Foo(int c, string* p) : counter_(c), str_(p) {}
private:
int counter_;
string* str_;
};
int main() {
string s = "bar";
Foo(2, &s);
}
Переменные встроенных типов следует всегда инициализировать, особенно если они являются членами класса. С другой стороны, переменные класса должны иметь конструктор, который корректно инициализирует их состояние, так что самостоятельно инициализировать их не требуется. Сохранить неинициализированное состояние переменных встроенных типов, когда они содержат мусор, — значит напрашиваться на проблемы. Но в C++ есть несколько различных способов выполнить инициализацию, и они описываются в этом рецепте.
Простейшими объектами инициализации являются встроенные типы. Работать с int, char и указателями очень просто. Рассмотрим простой класс и его конструктор по умолчанию.
class Foo {
public:
Foo() : counter_(0), str_(NULL) {}
Foo(int c, string* p) : counter_(c), str_(p) {}
private:
int counter_;
string* str_;
};
Для инициализации переменных-членов используется список инициализации, в результате чего тело конструктора освобождается от этой задачи. Тело конструктора может при этом содержать логику, выполняемую при создании объектов, а инициализацию переменных-членов становится легко найти. Это не столь значительное преимущество по сравнению с присвоением начальных значений в теле конструктора, но все его преимущества становятся очевидны при создании переменных-членов типа класса или ссылок или при попытке эффективного использования исключений.
Члены инициализируются в порядке их указания в объявлении класса, а не в порядке объявления их в списке инициализации.
Используя тот же класс Foo, как и в примере 8.1, рассмотрим переменную-член класса.
class Foo {
public:
Foo() : counter_(0), str_(NULL), cls_(0) {}
Foo(int с, string* p) :
counter_(c), str_(p), cls_(0) {}
private:
int counter_;
string* str_;
SomeClass cls_;
};
В конструкторе по умолчанию Foo инициализировать cls_ не требуется, так как будет вызван ее конструктор по умолчанию. Но если требуется создать Foo с аргументами, то следует добавить аргумент в список инициализации, как это сделано выше, а не делать присвоение в теле конструктора. Используя список инициализации, вы избежите дополнительного шага создания cls_ (так как при присвоении cls_ значения в теле конструктора cls_ вначале создается с использованием конструктора по умолчанию, а затем с помощью оператора присвоения выполняется присвоение нового значения), а также получите автоматическую обработку исключений. Если объект создается в списке инициализации и этот объект в процессе его создания выбрасывает исключение, то среда выполнения удаляет все ранее созданные объекты списка и передает исключение в код, вызывавший конструктор. С другой стороны, при присвоении аргумента в теле конструктора такое исключение необходимо обрабатывать с помощью блока try/catch.
Ссылки более сложны: инициализация переменной-ссылки (и const-членов) требует обязательного использования списка инициализации. В соответствии со стандартом ссылка всегда должна ссылаться на одну переменную и никогда не может измениться и ссылаться на другую переменную. Переменная-ссылка никогда не может не ссылаться на какой-либо объект. Следовательно, чтобы присвоить что-то осмысленное переменной-члену, являющейся ссылкой, это должно происходить при инициализации, т.е. в списке инициализации.
Следующая запись в C++ недопустима.
int& x;
Это значит, что невозможно объявить переменную-ссылку без ее инициализации. Вместо этого ее требуется инициализировать каким-либо объектом. Для переменных, не являющихся членами класса, инициализация может выглядеть вот так.
int а;
int& x = a;
Это все замечательно, но приводит к возникновению проблемы при создании классов. Предположим, вам требуется переменная-член класса, являющаяся ссылкой, как здесь.
class HasARef {
public:
int& ref;
};
Большинство компиляторов примет эту запись, но только до тех пор, пока вы не попытаетесь создать экземпляр этого класса, как здесь.
HasARef me;
В этот момент вы получите ошибку. Вот какую ошибку выдаст gcc.
error: structure 'me' with uninitialized reference members
(ошибка: структура 'me' с неинициализированными членами-ссылками)
Вместо этого следует использовать список инициализации.
class HasARef {
public:
int &ref;
HasARef(int &aref) : ref(aref) {}
};
Затем при создании экземпляра класса требуется указать переменную, на которую будет ссылаться переменная ref, как здесь.
int var;
HasARef me(var);
Именно так следует безопасно и эффективно инициализировать переменные-члены. В общем случае всегда, когда это возможно, используйте список инициализации и избегайте инициализации переменных-членов в теле конструктора. Даже если требуется выполнить какие-либо действия с переменными в теле конструктора, список инициализации можно использовать для установки начальных значений, а затем обновить их в теле конструктора.
Рецепт 9.2.
8.2. Использование функции для создания объектов (шаблон фабрики)
Вместо создания объекта в куче с помощью new вам требуется функция (член или самостоятельная), выполняющая создание объекта, тип которого определяется динамически. Такое поведение достигается с помощью шаблона проектирования Abstract Factory (абстрактная фабрика).