C++. Сборник рецептов - Д. Стефенс
Шрифт:
Интервал:
Закладка:
Все манипуляторы, кроме setprecision, одинаково воздействуют на все три формата. В стандартном режиме «точность» определяет суммарное количество цифр по обе стороны от десятичной точки. Например, для отображения числа «пи» в стандартном формате с точностью, равной 2, выполните следующие действия.
cout << "pi = " << setprecision(2) << pi << 'n';
В результате вы получите
pi = 3.1
Для сравнения представим, что вам требуется отобразить число «пи» в формате чисел с плавающей точкой.
cout << "pi = " << fixed << setprecision(2) << pi << 'n';
Теперь результат будет таким.
pi = 3.14
Отличие объясняется тем, что здесь точность определяет количество цифр, расположенных справа от десятичной точки. Если мы умножим число «пи» на 1000 и отобразим в том же формате, количество цифр справа от десятичной точки не изменится.
cout << "pi = " << fixed << setprecision(2) << pi * 1000 << 'n';
выдает в результате:
pi = 3142.86
Это хорошо, потому что вы можете задать точность, установить ширину своего поля при помощи setw, выровнять вправо отображаемое значение при помощи right (см. рецепт 10.1), и ваши числа будут выровнены вертикально по десятичной точке.
Поскольку манипуляторы — это просто удобный способ установки флагов формата для потока, следует помнить, что заданные установки работают до тех пор, пока вы их не уберете или пока поток не будет уничтожен. Сохраните флаги формата (см. пример 10.3) до того, как вы начнете его изменять, и восстановите их в конце.
Смотри такжеРецепт 10.3.
10.3. Написание своих собственных манипуляторов потока
ПроблемаТребуется иметь манипулятор потока, который делает что-нибудь такое, что не могут делать стандартные манипуляторы. Или вам нужен такой один манипулятор, который устанавливает несколько флагов потока, и вам не приходится вызывать несколько манипуляторов всякий раз, когда необходимо установить конкретный формат вывода.
РешениеЧтобы создать манипулятор, который не имеет аргументов (типа left), напишите функцию, которая принимает в качестве параметра ios_base и устанавливает для него флаги потока. Если вам нужен манипулятор с аргументом, см. приводимое ниже обсуждение. Пример 10.4 показывает возможный вид манипулятора без аргументов.
Пример 10.4. Простой манипулятор потока
#include <iostream>
#include <iomanip>
#include <string>
using namespace std;
// вывести числа с плавающей точкой в обычном виде
inline ios_base& floatnormal(ios_base& io) {
io.setf(0, ios_base::floatfield);
return(io);
}
int main() {
ios_base::fmtflags flags = // Сохранить старые флаги
cout.flags();
double pi = 22.0/7.0;
cout << pi = " << scientific // Научный режим
<< pi * 1000 << 'n';
cout << "pi = " << floatnormal << pi << 'n';
cout.flags(flags);
}
ОбсуждениеСуществует два вида манипуляторов: с аргументами и без аргументов. Манипуляторы без аргументов пишутся просто. Вам требуется только написать функцию, которая принимает в качестве параметра поток, выполнить с ним какие-то действия (установить флаги или изменить установочные параметры) и возвратить его. Сложнее написать манипулятор, который имеет один или несколько аргументов, потому что потребуется создавать дополнительные классы и функции, которые работают «за кулисами». Поскольку манипуляторы без аргументов более простые, начнем с них.
Прочитав рецепт 10.1, вероятно, вы поняли, что существует три формата вывода чисел с плавающей точкой и только два манипулятора для выбора формата. Для используемого по умолчанию формата не предусмотрен манипулятор; вам придется соответствующим образом установить флаг для потока, чтобы вернуться к стандартному формату:
myiostr.setf(0, ios_base::float field);
Но для удобства вы можете добавить свой собственный манипулятор, делающий то же самое. Именно это сделано в примере 10.4. Манипулятор floatnormal устанавливает соответствующий флаг потока для вывода чисел с плавающей точкой в стандартном формате.
Компилятор знает, что делать с вашей новой функцией, потому что в стандартной библиотеке определен следующий оператор для basic_ostream (basic_ostream — имя шаблона класса, инстанцируемого в классах ostream и wostream).
basic_ostream<charT, traits>& operator<<
(basic_ostream<charT, traits>& (* pf)basic_ostream<charT, traits>&))
Здесь pf — это указатель функции, которая принимает в аргументе ссылку на basic_ostream и возвращает ссылку на basic_ostream. Этот оператор просто обеспечивает вызов вашей функции, которая принимает в качестве аргумента текущий поток.
Манипуляторы с аргументами более сложные. Чтобы понять причину этого, рассмотрим работу манипулятора без аргументов. Пусть используется, например, следующий манипулятор
myostream << myManip << "foo";
Вы задаете его без скобок, поэтому его имя в действительности заменяется адресом функции вашего манипулятора. В действительности operator<< вызывает функцию манипулятора и передает ей поток, чтобы манипулятор мог выполнить свою работу.
Для сравнения представим, что у вас имеется манипулятор, который принимает числовой аргумент, так что в идеале вы могли бы его использовать следующим образом.
myostream << myFancyManip(17) << "apple";
Как это будет работать? Если вы считаете, что myFancyManip является функцией, принимающей целочисленный аргумент, то возникает проблема: как передать поток в функцию без включения его в параметры и явного его использования? Вы могли бы написать так.
myostream << myFancyManip(17, myostream) << "apple";
Но это выглядит непривлекательно и избыточно. Одним из удобств манипуляторов является то, что их можно просто добавлять в строку с группой операторов operator<<, и они хорошо воспринимаются и используются.
Решение состоит в том, чтобы заставить компилятор пойти окольным путем. Вместо того чтобы operator<< вызывал функцию вашего манипулятора для потока, вам надо просто создать временный объект, который возвращает нечто такое, что может использовать operator<<.
Во-первых, вам необходимо определить временный класс, который делал бы всю работу. Для простоты предположим, что вам требуется написать манипулятор с именем setWidth, который делает то же самое, что и setw. Временная структура, которую вам необходимо построить, будет выглядеть примерно так.
class WidthSetter {
public:
WidthSetter(int n) : width_(n) {}
void operator()(ostream& os) const {os.width(width_);}
private:
int width_;
};
Этот класс содержит простую функцию. Предусмотрите в ней целочисленный аргумент и, когда operator() вызывается с аргументом потока, установите ширину для потока в значение, в какое она была установлена при инициализации этого объекта. В результате мы получим WidthSetter, сконструированный одной функцией и используемый другой. Ваш манипулятор конструирует эту функцию, и это будет выглядеть следующим образом.
WidthSetter setWidth(int n) {
return(WidthSetter(n)); // Возвращает инициализированный объект
}
Эта функция всего лишь возвращает объект WidthSetter, инициализированный целым значением. Этот манипулятор вы будете использовать в строке операторов operator<< следующим образом.
myostream << setWidth(20) << "banana";
Но этого недостаточно, потому что setWidth просто возвращает объект WidthSetter; operator<< не будет знать, что с ним делать. Вам придется перегрузить operator<<, чтобы он знал, как управлять объектом WidthSetter:
ostream& operator<<(ostream& os, const WidthSetter& ws) {
ws(os); // Передать поток объекту ws
return(os); // для выполнения реальной работы
}
Это решает проблему, но не в общем виде. Вам не захочется писать класс типа WidthSetter для каждого вашего манипулятора, принимающего аргумент (возможно, вы это и делаете, но были бы не против поступить по-другому), поэтому лучше использовать шаблоны и указатели функций для получения привлекательной, обобщенной инфраструктуры, на базе которой вы можете создавать любое количество манипуляторов. Пример 10.5 содержит класс ManipInfra и версию operator<<, использующую аргументы шаблона для работы с различными типами символов, которые может содержать поток, и с различными типами аргументов, которые могут быть использованы манипулятором потока.
Пример 10.5. Инфраструктура манипуляторов
#include <iostream>