Итераторы выходных потоков ведут себя аналогично итераторам потоков ввода. В примере 7.11 я копирую значения из своего vector в cout, создав для этого ostream_iterator, который указывает на cout, следующим образом.
copy(v.begin(), v.end(), ostream_iterator<string>(cout, ", "));
Аргумент шаблона ostream_iterator говорит, что записываемые элементы будут иметь тип string. Первый аргумент конструктора ostream_iterator — это поток, в который будет производиться запись (и который может быть любым потоком вывода, включая ofstream и ostringstream), а второй это используемый разделитель. Это дает удобный способ выводить диапазон значений на стандартный вывод, что я часто делаю при отладке.
Если требуется дополнительное управление внешним видом вывода, например вывод последовательности в квадратных или фигурных скобках или отсутствие последнего разделителя в конце последовательности, то это потребует всего нескольких дополнительных строк кода. Пример 7.12 показывает тело printContainer и printRange, первая из которых используется в примерах этой главы.
Пример 7.12. Написание собственной функции печати
#include <iostream>
#include <string>
#include <algorithm>
#include <iterator>
#include <vector>
using namespace std;
template<typename C>
void printContainer(const C& c, char delim = ',', ostream& out = cout) {
printRange(c.begin(), c.end(), delim, out);
}
template<typename Fwd>
void printRange(Fwd first, Fwd last, char delim = ',', ostream& out = cout) {
out << "{";
while (first != last) {
out << *first;
if (++first != last)
out << delim << ' ';
}
out << "}" << endl;
}
int main() {
cout << "Введите набор строк: ";
istream_iterator<string> start(cin);
istream_iterator<string> end;
vector<string> v(start, end);
printContainer(v);
printRange(v.begin(), v.end(), ';', cout);
}
Функция printRange представляет собой более общий подход, так как оперирует с диапазонами (более подробно это объясняется в рецепте 7.10), но printContainer более удобна для печати целого контейнера. Имеется множество других способов сделать это. В голову также приходит определение версии operator<<, которая бы работала с выходным потоком и контейнером, и использование стандартного алгоритма for_each с собственным функтором для записи элементов в поток.
Глава 8
Классы
8.0. Введение
Эта глава содержит решения проблем, часто возникающих при работе с классами С++. Рецепты по большей части независимы, но разбиты на две части, каждая из которых занимает примерно по половине главы. Первая половина главы содержит решения проблем, которые могут возникнуть при создании объектов классов, таких как использование функции для создания объекта (которая часто называется шаблоном фабрики) или использование конструкторов и деструкторов для управления ресурсами. Вторая половина содержит решения проблем, возникающих после создания объектов, таких как определение типа объекта во время выполнения, а также некоторые методики реализации наподобие создания интерфейса с помощью абстрактного базового класса.
Конечно, классы — это главная особенность С++, которая обеспечивает возможность объектно-ориентированного программирования, и с ними можно выполнять очень много разных действий. Эта глава не содержит рецептов, объясняющих основы классов: виртуальные функции (полиморфизм), наследование и инкапсуляцию. Я полагаю, что вы уже знакомы с этими основными принципами объектно-ориентированного проектирования независимо от используемого языка программирования. Напротив, целью этой главы является описание принципов некоторых механических сложностей, с которыми можно столкнуться при реализации объектно-ориентированного дизайна на С++.
Объектно-ориентированное проектирование и связанные с ним шаблоны проектирования — это обширный вопрос, и имеется большое количество различной литературы на эту тему. В этой главе я упоминаю названия только некоторых шаблонов проектирования, и это шаблоны, для которых возможности 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-членов) требует обязательного использования списка инициализации. В соответствии со стандартом ссылка всегда должна ссылаться на одну переменную и никогда не может измениться и ссылаться на другую переменную. Переменная-ссылка никогда не может не ссылаться на какой-либо объект. Следовательно, чтобы присвоить что-то осмысленное переменной-члену, являющейся ссылкой, это должно происходить при инициализации, т.е. в списке инициализации.