Глава 10 Потоки и файлы

10.0. Введение

Потоки (streams) являются одной из самых мощных (и сложных) компонент стандартной библиотеки С++. Их применение при простом, неформатированном вводе-выводе в целом не представляет трудностей, однако ситуация усложняется, если необходимо изменить формат с помощью стандартных манипуляторов или приходится писать свои собственные манипуляторы. Поэтому первые несколько рецептов описывают различные способы форматирования вывода потока данных. Следующие два рецепта показывают, как можно записывать объекты класса в поток и считывать их оттуда.

Затем рецепты переходят с темы чтения и записи содержимого файлов на работу с самими файлами (и каталогами). Если в вашей программе используются файлы особенно если такая программа является демоном или процессом на стороне сервера, вам, вероятно, потребуется создавать файлы и каталоги, удалять их, переименовывать и выполнять другие операции над ними. Существует ряд рецептов, которые показывают, как следует решать эти непривлекательные, но необходимые задачи в С++.

Последняя треть рецептов показывает, как можно манипулировать именами файлов и путями доступа к ним, используя многие стандартные строковые функции-члены. Стандартные строки содержат массу функций, предназначенных для анализа и манипулирования их содержимым, и если вам придется анализировать пути доступа к файлам и имена файлов, эти функции окажутся полезными. Если в этих рецептах нет того, что вам требуется, вернитесь к главе 7: возможно, там описано то, что вы ищете.

Манипулирование файлами требует прямого взаимодействия с операционной системой (ОС), но между различными ОС часто имеются тонкие отличия (а иногда вопиющие несовместимости). Многие типичные операции над файлами и каталогами выполняются с помощью вызовов системных функций стандартной библиотеки С. которые работают одинаково или аналогично в различных системах. В рецептах я отмечаю отличия версий библиотек различных ОС там, где они имеются.

Как я отмечал в предыдущих главах, Boost — это проект открытого исходного кода, результатом которого стал ряд высококачественных и переносимых библиотек. Однако поскольку данная книга посвящена C++, а не проекту Boost, во всех возможных случаях я предпочитаю пользоваться стандартными решениями С++. Однако во многих случаях (наиболее примечательный — рецепт 10.12) нельзя получить решения, используя стандартную библиотеку С++, поэтому я пользуюсь библиотекой Boost Filesystem, написанной Биманом Дейвисом (Beman Dawes); она обеспечивает переносимый интерфейс для файловой системы, позволяя получать переносимые решения. Используйте библиотеку Boost Filesystem, если требуется обеспечить переносимость взаимодействия с файловой системой, и это позволит вам сэкономить много времени и многих усилий. Дополнительную информацию по проекту Boost вы найдете на сайте www.boost.org.

10.1. Вывод выровненного текста

Проблема

Требуется вывести текст, выровненный по вертикали. Например, если ваши данные представлены в табличном виде, вам захочется, чтобы они выглядели следующим образом.

Jim   Willсох  Mesa     AZ

Bill   Johnson  San Mateo   CA

Robert Robertson Fort Collins CO

Кроме того, вам, вероятно, захочется иметь возможность выравнивать текст вправо или влево.

Решение

Используйте определенные в

типы
ostream
или
wostream
для узких или широких символов и стандартные манипуляторы потоков для установки размера полей и выравнивания текста. Пример 10.1 показывает, как это можно сделать.

Пример 10.1. Вывод выровненного текста

#include 

#include 

#include 


using namespace std;


int main() {

 ios_base::fmtflags flags = cout.flags();

 string first, last, citystate;

 int width = 20;

 first = "Richard";

 last = "Stevens";

 citystate = "Tucson, AZ";

 cout << left       // Каждое поле выравнивается влево.

  << setw(width) << first // Затем для каждого поля

  << setw(width) << last  // устанавливается его ширина и

              // записываются некоторые данные

  << setw(width) << citystate << endl;

 cout.flags(flags);

}

Вывод выглядит следующим образом.

Richard       Stevens       Tucson, AZ

Обсуждение

Манипулятор — это функция, которая выполняет некоторую операцию над потоком. Применяемые к потоку манипуляторы задаются в операторе

operator<<
. Формат потока (ввода и вывода) задается набором флагов и установочных параметров конечного базового класса потока,
ios_base
. Манипуляторы обеспечивают удобный способ настройки этих флагов и установочных параметров без явного использования для этой цели функций
setf
или
flags
, которые громоздки и воспринимаются с трудом. Для форматирования потока вывода лучше всего использовать манипуляторы.

В примере 10.1 используется два манипулятора для вывода текста в две колонки. Манипулятор

setw
задает размер поля, a
left
обеспечивает выравнивание влево находящегося в поле значения (дополнением манипулятора
left
, что неудивительно, является
right
). Когда вы используете слово «поле», вы просто говорите, что хотите дополнить заполнителем выдаваемое в поле значение с одной или с другой стороны, чтобы только ваше значение выводилось в этом поле. Если, как в примере 10.1, вы выравниваете значение влево и затем задаете размер поля, следующее записываемое в поток значение будет начинаться с первой позиции этого поля. Если переданные в поток данные имеют недостаточный размер и не могут заполнить все пространство поля, то правая часть поля будет дополнена символом заполнителя потока, которым по умолчанию является одиночный пробел. Вы можете изменить символ заполнителя с помощью манипулятора
setfill
.

myostr << setfill('.') << "foo";

Если помещаемое в поле значение превышает его размер, будет напечатано все значение и никаких дополнительных символов выводиться не будет.

Табл. 10.1 содержит краткое описание манипуляторов, работающих с любыми типами значений (текстом, числами с плавающей точкой, целыми числами и т.д.). Имеется ряд манипуляторов, которые применяются только при выводе чисел с плавающей точкой — они описываются в рецепте 10.2.


Табл. 10.1. Текстовые манипуляторы

Манипулятор Описание Пример вывода
left right
Выровнять значения в текущем поле вправо или влево, заполняя незанятое пространство символом-заполнителем Выравнивание влево
apple    banana   cherry   
Выравнивание вправо (ширина поля 10)
   apple    banana    cherry
setw(int n)
Установить размер поля на n символов См. предыдущий пример
setfill(int с)
Использовать символ с для заполнения незанятого пространства поля
cout << setfill('.') << setw(10) << right << "foo" 
Выдает:
.......foo
boolalpha noboolalpha
Отобразить булевы значения в текущем локализованном представлении слов
true
и
false
, а не 1 и 0
cout << boolalpha << true
Выдает:
true
endl
Записать в поток символ новой строки (newline) и очистить буфер вывода Нет
ends
Записать в поток null-символ ('\0') Нет
flush
Очистить буфер вывода Нет

Некоторые представленные в табл. 10.1 (и в табл. 10.2 в следующем рецепте) манипуляторы переключают бинарные флаги потоков и в действительности реализуются как два манипулятора, которые включают и отключают флаг. Например, возьмем манипулятор

boolalpha
. Если вы хотите, чтобы булевы значения отображались в соответствии с текущей локализацией (например, «true» и «false»), используйте манипулятор
boolalpha
. Для отключения этого режима, чтобы вместо слов печатались 0 и 1, используйте манипулятор
noboolalpha
(он используется по умолчанию).

Действие всех манипуляторов сохраняется до тех пор, пока оно не будет явно изменено, исключая манипулятор

setw
. Из примера 10.1 видно, что он вызывается перед каждой записью, однако
left
используется только один раз. Это объясняется тем, что ширина поля устанавливается в нуль после записи каждого значения в поток при помощи оператора
operator<<
; чтобы обеспечить одинаковую ширину всех полей, мне пришлось каждый раз вызывать
setw
.

Стандартные манипуляторы позволяют делать многое, но не все. Если у вас возникает потребность в написании собственного манипулятора, см. рецепт 10.2.

Как и все другие классы стандартной библиотеки, работающие с символами, манипуляторы работают с потоками узких или широких символов. Поэтому вы можете использовать их в шаблонах для написания утилит форматирования, обрабатывающих потоки символов любого вида. В примере 10.2 приводится шаблон класса

TableFormatter
, который форматирует данные в колонки одинаковой ширины и выдает их в поток.

Пример 10.2. Параметрический класс для табличного представления данных

#include 

#include 

#include 

#include 


using namespace std;


// TableFormatter выдает в поток вывода символы типа T в форматированном

// виде.

template

class TableFormatter {

public:

 TableFormatter(basic_ostream& os) : out_(os) {}

 ~TableFormatter() {out_ << flush;}

 template

 void writeTableRow(const vector& v, int width);

 //...

private:

 basic_ostream& out_;

};


template

 typename valT>    // ссылается на список параметров функции-члена

void TableFormatter::writeTableRow(const std::vector& v,

 int width) {

 ios_base::fmtflags flags = out_.flags();

 out_.flush();

 out_ << setprecision(2) << fixed; // Задать точность в случае применения

                  // чисел с плавающей точкой

 for (vector::const_iterator p = v.begin(); p != v.end(); ++p)

  out_ << setw(width) << left << *p; // Установить ширину поля, его

                   // выравнивание и записать элемент

  out_ << endl; // Очистить буфер

 out setf(flags); // Восстановить стандартное состояние флагов

}


int main() {

 TableFormatter fmt(cout);

 vector vs;

 vs.push_back("Sunday");

 vs.push_back("Monday");

 vs.push_back("Tuesday");

 fmt.writeTableRow(vs, 12);

 fmt.writeTableRow(vs, 12);

 fmt.writeTableRow(vs, 12);

 vector vd;

 vd.push_back(4.0);

 vd.push_back(3.0);

 vd.push_back(2.0);

 vd.push_back(1.0);

 fmt.writeTableRow(vd, 5);

}

Вывод представленной в примере 10.2 программы выглядит следующим образом.

Sunday   Monday    Tuesday

4.00 3.00 2.00 1.00

Смотри также

Таблица 10.1, рецепт 10.2.

10.2. Форматирование вывода чисел с плавающей точкой

Проблема

Требуется выдать числа с плавающей точкой в удобном формате либо ради обеспечения необходимой точности (применяя нотацию, которая используется в науке, а не в виде числа с фиксированной точкой), либо просто выравнивая значения по десятичной точке для лучшего восприятия.

Решение

Используйте стандартные манипуляторы, определенные в

и
, для управления форматом значений чисел с плавающей точкой при их записи в поток. Это можно делать очень многими способами, и в примере 10.3 предлагается несколько способов отображения значения числа «пи».

Пример 10.3. Форматирование числа «пи»

#include 

#include 

#include 


using namespace std;


int main() {

 ios_base::fmtflags flags = // Сохранить старые флаги

 cout.flags();

 double pi = 3.14285714;

 cout << "pi = " << setprecision(5) // Обычный (стандартный) режим;

  << pi << '\n';           // показать только 5 цифр в сумме

                   // по обе стороны от точки.

 cout << "pi = " << fixed // Режим чисел с фиксированной точкой;

  << showpos        // выдать "+" для положительных чисел.

  << setprecision(3)   // показать 3 цифры *справа* от

 << pi << '\n';      // десятичной точки.

 cout << "pi = " << scientific // Режим научного представления;

  << noshowpos         // знак плюс больше не выдается

  << pi * 1000 << '\n';

 cout.flags(flags); // Восстановить значения флагов

}

Это приведет к получению следующего результата.

pi = 3.1429

pi = +3.143

pi = 3.143е+003

Обсуждение

Манипуляторы, работающие с числами с плавающей точкой, делятся на две категории. Одни из них задают формат и в данном рецепте устанавливают общий вид целых значений и значений чисел с плавающей точкой, а другие используются для тонкой настройки каждого формата. Предусмотрены следующие форматы.

Обычный (стандартный)

В этом формате фиксировано количество отображаемых цифр (по умолчанию это количество равно шести), а десятичная точка отображается в соответствующем месте. Поэтому число «пи» по умолчанию будет иметь вид

3.14286
, а умноженное на 100 будет отображаться как
314.286
.

Фиксированный

В этом формате фиксировано количество цифр, отображаемое справа от десятичной точки, а количество цифр слева не фиксировано. В этом случае при стандартной точности, равной шести, число «пи» будет отображаться в виде

3.142857
, а умноженное на 100 —
314.285714
. В обоих случаях количество цифр, отображаемое справа от десятичной точки, равно шести, а общее количество цифр может быть любым.

Научный

Значение начинается с цифры, затем идет десятичная точка и несколько цифр, количество которых определяется заданной точностью; затем идет буква «е» и степень 10, в которую надо возвести предыдущее значение. В этом случае число «пи», умноженное на 1000, будет отображаться как

3.142857е+003
.

В табл. 10.2 приводятся все манипуляторы, которые воздействуют на вывод чисел с плавающей точкой (а иногда и на вывод любых чисел). См. табл. 10.1, где приводятся манипуляторы общего типа, которые можно использовать совместно с манипуляторами чисел с плавающей точкой.


Табл. 10.2. Манипуляторы, работающие с любыми числами и числами с плавающей точкой

Манипулятор Описание Пример вывода
fixed
Показать значение чисел с плавающей точкой с фиксированным количеством цифр справа от десятичной точки При стандартной точности, равной шести цифрам:
pi = 3.142857
scientific
Показать значение чисел с плавающей точкой, применяя научную нотацию, в которой используется значение с десятичной точкой и экспонентный множитель pi * 1000 при стандартной точности, равной шести цифрам:
pi = 3.142857е+003
setprecision
Установить количество цифр, отображаемых в выводе (см. последующие объяснения) Число «пи» в стандартном формате при точности, равной трем цифрам:
pi = 3.14
В фиксированном формате:
pi = 3.143
В научном формате:
pi = 3.143е+000
showpos noshowpos
Показать знак «плюс» перед положительными числами. Это действует для чисел любого типа, с десятичной точкой или целых
+3.14
showpoint noshowpoint
Показать десятичную точку, даже если после нее идут одни нули. Это действует только для чисел с плавающей точкой и не распространяется на целые числа Следующая строка при точности, равной двум цифрам:
cout << showpoint << 2.0
выдаст такой результат:
2.00
showbase noshowbase
Показать основание числа, представленного в десятичном виде (основание отсутствует), в восьмеричном виде (ведущий нуль) или в шестнадцатеричном виде (префикс 0x). См. следующую строку таблицы Десятичное представление:
32
Восьмеричное:
040
Шестнадцатеричное:
0x20
dec oct hex
Установить основание для отображения числа в десятичном, восьмеричном или шестнадцатеричном виде. Само основание по умолчанию не отображается; для его отображения используйте showbase См предыдущую строку таблицы
uppercase nouppercase
Отображать значения, используя верхний регистр Устанавливает регистр вывода чисел, например для префикса
0X
шестнадцатеричных чисел или буквы
E
для чисел, представленных в научной нотации

Все манипуляторы, кроме

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 

#include 

#include 


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& operator<<

 (basic_ostream& (* pf)basic_ostream&))

Здесь

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 

#include 


using namespace std;


// ManipInfra - это небольшой промежуточный класс, который помогает

// создавать специальные манипуляторы с аргументами. Вызывайте его

// конструктор с предоставлением указателя функции и значения из основной

// функции манипулятора

// Указатель функции должен ссылаться на вспомогательную функцию, которая

// делает реальную работу. См. примеры ниже

template

class ManipInfra {

public:

 ManipInfra(basic_ostream& (*pFun) (basic_ostream&, T), T val) :

  manipFun_(pFun), val_(val) {}

 void operator()(basic_ostream& os) const {

  manipFun_(os, val_);

 }    // Вызовите функцию, задавая ее указатель и

private: // передавая ей поток и значение

 T val_;

 basic_ostream<С>& (*manipFun_) (basic_ostream&, T);

};


template

basic_ostream& operator<<(basic_ostream& os,

 const ManipInfra& manip) {

 manip(os);

 return(os);

}


// Вспомогательная функция, которая вызывается в итоге в классе ManipInfra

ostream& setTheWidth(ostream& os, int n) {

 os.width(n);

 return(os);

}


// Собственно функция манипулятора. Именно она используется в клиентском

// программном коде

ManipInfra setWidth(int n) {

 return(ManipInfra(setTheWidth, n));

}


// Ещё одна вспомогательная функция, которая принимает аргумент типа char

ostream& setTheFillChar(ostream& os, char с) {

 os.fill(c);

 return(os);

}


ManipInfra setFill(char c) {

 return(ManipInfra(setTheFillChar, c));

}


int main() {

 cout << setFill('-')

  << setWidth(10) << right << "Proust\n";

}

Если последовательность событий при работе этого класса все же остается неясной, я советую прогнать пример 10.5 через отладчик. Увидев его реальную работу, вы все поймете.

10.4. Создание класса, записываемого в поток

Проблема

Требуется записать класс в поток для последующего его чтения человеком или с целью его хранения в постоянной памяти, т.е. для его сериализации.

Решение

Перегрузите

operator<<
для записи в поток соответствующих данных-членов. В примере 10.6 показано, как это можно сделать.

Пример 10.6. Запись объектов в поток

#include 

#include 


using namespace std;


class Employer {

 friend ostream& operator<<       // Он должен быть другом для

  (ostream& out, const Employer& empr); // получения доступа к неоткрытым

public:                 // членам

 Employer() {}

 ~Employer() {}

 void setName(const string& name) {name_ = name;}

private:

 string name_;

};


class Employee {

friend ostream& operator<< (ostream& out, const Employee& obj);

public:

 Employee() : empr_(NULL) {}

 ~Employee() {if (empr_) delete empr_;}


 void setFirstName(const string& name) {firstName_ = name;}

 void setLasttName(const string& name) {lastName_ = name;}

 void setEmployer(Employer& empr) {empr_ = &empr;}

 const Employer* getEmployer() const {return(empr_);}

private:

 string firstName_;

 string lastName_;

 Employer* empr_;

};


// Обеспечить передачу в поток объектов

Employer... ostream& operator<<(ostream& out, const Employer& empr) {

 out << empr.name_ << endl; return(out);

}


// Обеспечить передачу в поток объектов Employee...

ostream& operator<<(ostream& out, const Employee& emp) {

 out << emp.firstName_ << endl;

 out << emp.lastName_ << endl;

 if (emp.empr_) out << *emp.empr_ << endl;

  return(out);

}


int main() {

 Employee emp;

 string first = "William";

 string last = "Shatner";

 Employer empr;

 string name = "Enterprise";

 empr.setName(name);

 emp.setFirstName(first);

 emp.setLastName(last);

 emp.setEmployer(empr);

 cout << emp; // Запись в поток

}

Обсуждение

Прежде всего, необходимо объявить оператор

operator<<
другом (
friend
) класса, который вы хотите записывать в поток. Вы должны использовать
operator<<
, а не функцию-член типа
writeToStream(ostream& os)
, потому что этот оператор принято использовать в стандартной библиотеке для записи любых объектов в поток. Вам придется объявить его другом, потому что в большинстве случаев потребуется записывать в поток закрытые члены, а не являющиеся друзьями функции не смогут получить доступ к ним.

После этого определите версию

operator<<
, которая работает с
ostream
или
wostream
(которые определены в
) и вашим классом, который вы уже объявили с ключевым словом
friend
. Здесь вы должны решить, какие данные-члены должны записываться в поток. Обычно потребуется записывать в поток все данные, как это я делал в примере 10.6.

out << emp.firstName_ << endl;

out << emp.lastName_ << endl;

В примере 10.6 я записал в поток объект, на который ссылается указатель

empr_
, вызывая для него оператор
operator<<
.

if (emp.empr_)

 out << *emp.empr << endl;

Я могу так делать, потому что

empr_
указывает на объект класса
Employer
, а для него, как и для
Employee
, я определил оператор
operator<<
.

После записи в поток членов вашего класса ваш оператор

operator<<
должен возвратить переданный ему поток. Это необходимо делать в любой перегрузке
operator<<
, тогда она может успешно использоваться, как в следующем примере.

cout << "Here's my object. " << myObj << '\n';

Описанный мною подход достаточно прост, и если вы собираетесь записывать класс с целью его дальнейшего восприятия человеком, он будет хорошо работать, но это только частичное решение проблемы. Если вы записываете объект в поток, это обычно делается по одной из двух причин. Либо этот поток направляется куда-то, где он будет прочитан человеком (

cout
, окно терминала, файл журнала и т.п.), либо поток записывается на носитель временной или постоянной памяти (
stringstream
, сетевое соединение, файл и т.д.) и вы планируете восстановить в будущем объект из потока. Если вам требуется воссоздать объект из потока (тема рецепта 10.5), необходимо тщательно продумать взаимосвязи вашего класса.

Сериализация трудно реализуется для любых классов, не считая тривиальных. Если в вашем классе имеются ссылки или указатели на другие классы, что характерно для большинства нетривиальных классов, вам придется учесть потенциальную возможность наличия циклических ссылок, обработать их должным образом при записи в поток объектов и правильно реконструировать ссылки при считывании объектов. Если вам приходится строить что-то «с чистого листа», необходимо учесть эти особенности проектирования, однако если вы можете использовать внешнюю библиотеку, вам следует воспользоваться библиотекой Boost Serialization, которая обеспечивает переносимый фреймворк сериализации объектов.

Смотри также

Рецепт 10.5.

10.5. Создание класса, считываемого из потока

Проблема

В поток записан объект некоторого класса и теперь требуется считать эти данные из потока и использовать их для инициализации объекта того же самого класса.

Решение

Используйте

operator>>
для чтения данных из потока в ваш класс для заполнения значений данных-членов; это просто является обратной задачей по отношению к тому, что сделано в примере 10.6. Реализация приводится в примере 10.7.

Пример 10.7. Чтение данных из потока в объект

#include 

#include 

#include 

#include 


using namespace std;


class Employee {

 friend ostream& operator<<       // Они должны быть друзьями,

  (ostream& out, const Employee& emp); // чтобы получить доступ к

 friend istream& operator>>       // неоткрытым членам

  (istream& in, Employee& emp);

public:

 Employee() {}

 ~Employee() {}


 void setFirstName(const string& name) {firstName_ = name;}

 void setLastName(const string& name) {lastName_ = name;}

private:

 string firstName_;

 string lastName_;

};


// Передать в поток объект Employee...

ostream& operator<<(ostream& out, const Employee& emp) {

 out << emp.firstName_ << endl;

 out << emp.lastName_ << endl;

 return(out);

}


// Считать из потока объект Employee

istream& operator>>(istream& in, Employee& emp) {

 in >> emp.firstName_;

 in >> emp.lastName_;

 return(in);

}


int main() {

 Employee emp;

 string first = "William";

 string last = "Shatner";

 emp.setFirstName(first);

 emp.setLastName(last);

 ofstream out("tmp\\emp.txt");

 if (!out) {

  cerr << "Unable to open output file.\n";

  exit(EXIT_FAILURE);

 }

 out << emp; // Записать Emp в файл

 out.close();

 ifstream in("tmp\\emp.txt");

 if (!in) {

  cerr << "Unable to open input file.\n";

  exit(EXIT_FAILURE);

 }

 Employee emp2;

 in >> emp2; // Считать файл в пустой объект

 in.close();

 cout << emp2;

}

Обсуждение

При создании класса, считываемого из потока, выполняемые этапы почта совпадают с этапами записи объекта в поток (только они имеют обратный характер) Если вы еще не прочитали рецепт 10.4, это необходимо сделать сейчас, чтобы был понятен пример 10.7.

Во-первых, вам необходимо объявить

operator>>
как дружественный для вашего целевого класса, однако в данном случае вам нужно, чтобы он использовал поток
istream
, а не
ostream
. Затем определите оператор
operator>>
(вместо
operator<<
) для прямого чтения значений из потока в каждую переменную-член вашего класса. Завершив чтение данных, возвратите входной поток.

Смотри также

Рецепт 10.4.

10.6. Получение информации о файле

Проблема

Требуется получить информацию о файле, например о его размере, номере устройства, времени последнего изменения и т.п.

Решение

Используйте вызов системной C-функции

stat
, определенной в
. См. Пример 10.8, где показано типичное применение stat для вывода на печать некоторых атрибутов файла.

Пример 10.8. Получение информации о файле

#include 

#include 

#include 

#include 

#include 

#include 


int main(int argc, char** argv) {

 struct stat fileInfo;

 if (argc < 2) {

  std::cout << "Usage: fileinfo \n";

  return(EXIT_FAILURE);

 }

 if (stat(argv[1], &fileInfo) != 0) { // Используйте stat() для получения

                    // информации

  std::cerr << "Error: " << strerror(errno) << '\n';

  return(EXIT_FAILURE);

 }

 std::cout << "Type::";

 if ((fileInfo.st_mode & S_IFMT) == S_IFDIR) { // Из sys/types.h

  std::cout << "Directory\n";

 } else {

  std::cout << "File\n";

 }

 std::cout << "Size : " <<

 fileInfo.st_size << '\n';        // Размер в байтах

 std::cout << "Device : " <<

  (char)(fileInfo.st_dev + 'A') >> '\n'; // Номер устройства

 std::cout << "Created : " <<

  std::ctime(&fileInfo.st_ctime);     // Время создания

 std::cout << "Modified : " <<

  std.:ctime(&fileInfo.st_mtime);     // Время последней модификации

}

Обсуждение

Стандартная библиотека C++ обеспечивает операции с содержимым файловых потоков, но она не имеет встроенных средств по чтению и изменению поддерживаемых ОС метаданных файла, например размера файла, его владельца, прав доступа, различных времен и другой информации. Однако стандартный С содержит несколько стандартных библиотек системных вызовов, которые можно использовать для получения различной информации о файле, что сделано в примере 10.8.

Существует два средства, обеспечивающие получение информации о файле. Во-первых, это структура

struct
с именем
stat
, которая содержит члены с информацией о файле, и, во-вторых, системный вызов (функция) с тем же самым именем, который обеспечивает получение любой запрошенной информации о файле, помещая ее в структуру
stat.
Системный вызов — это функция, обеспечивающая некоторую системную службу ОС. Ряд системных вызовов является частью стандартного С, и многие из них стандартизованы и входят в состав различных версий Unix. Структура
stat
имеет следующий вид (из книги Кернигана (Kernigan) и Ричи (Richie) «The С Programming Language», [издательство «Prentice Hall»]).

struct stat {

 dev_t  st_dev;  /* устройство */

 ino_t  st_ino;  /* номер inode */

 short  st_mode;  /* вид */

 short  st_nlink  /* число ссылок на файл */

 short  st_uid;  /* пользовательский идентификатор владельца */

 short  st_gid;  /* групповой идентификатор владельца */

 dev_t  st_rdev;  /* для особых файлов */

 off_t  st_size;  /* размер файла в символах */

 time_t st_atime; /* время последнего доступа */

 time_t st_mtime; /* время последней модификации */

 time_t st_ctime; /* время последнего изменения inode */

};

Смысл каждого члена

stat
зависит от ОС. Например,
st_uid
и
st_gid
не используются в системах Windows, в то время как в системах Unix они фактически содержат идентификаторы пользователя и группы владельца файла. Воспользуйтесь документацией ОС, чтобы узнать, какие значения поддерживаются и как они интерпретируются.

В примере 10.8 показано, как можно отображать на экране некоторые переносимые члены

stat
.
st_mode
содержит битовую маску, описывающую тип файла. Она позволяет узнать, является ли файл каталогом или нет.
st_ size
задает размер файла в байтах. Три члена типа
size_t
определяют время последнего доступа, модификации и создания файлов.

Остальные члены содержат информацию, зависящую от операционной системы. Рассмотрим

st_dev
: в системах Windows этот член содержит номер устройства (дисковода) в виде смещения от буквы А, представленной в коде ASCII (именно поэтому в примере я добавляю
'A'
, чтобы получить буквенное обозначение дисковода). Но в системе Unix это будет означать нечто другое; значение этого члена передайте в системный вызов
ustat
, и вы получите имя файловой системы.

Если вам требуется получить дополнительную информацию о файле, лучше всего обратиться к документации вашей ОС. Стандартные системные вызовы C-функций ориентированы на Unix, поэтому они обычно приносят больше пользы в системах Unix (и совместно с ними может использоваться ряд других системных вызовов). Если вы не используете Unix, вполне возможно, что в вашей ОС имеются поставляемые со средой разработки собственные библиотеки, которые позволяют получать более детальную информацию.

10.7. Копирование файла

Проблема

Требуется скопировать файл, причем так, чтобы эта операция была переносимой, т.е. без использования зависящего от ОС программного интерфейса.

Решение

Используйте файловые потоки С++, определенные в

, для копирования одного потока в другой. Пример 10.9 показывает, как можно скопировать поток с помощью буфера

Пример 10.9. Копирование файла

#include 

#include 


const static int BUF_SIZE = 4096;


using std::ios_base;


int main(int argc, char** argv) {

 std::ifstream in(argv[1], 

  ios_base::in | ios_base::binary);  // Задается двоичный режим, чтобы

 std::ofstream out(argv[2],      // можно было обрабатывать файлы с

  ios_base::out | ios_base::binary), // любым содержимым

 // Убедитесь, что потоки открылись нормально...

 char buf[BUF_SIZE];

 do {

  in.read(&buf[0], BUF_SIZE);    // Считать максимум n байт в буфер,

  out.write(&buf[0], in.gcount()); // затем записать содержимое буфера

 } while (in.gcount() > 0);     // в поток вывода.

 // Проверить наличие проблем в потоках...

 in.close();

 out.close();

}

Обсуждение

Можно посчитать, что копирование файла — это простая операция чтения из одного потока и записи в другой поток. Однако библиотека потоков C++ достаточно большая, и существует несколько различных способов чтения и записи потоков, поэтому надо обладать некоторыми знаниями об этой библиотеке, чтобы избежать ошибок, снижающих производительность этой операции.

Пример 10.9 работает быстро, потому что используется буферизация ввода-вывода. Функции

read
и
write
оперируют сразу всем содержимым буфера вместо посимвольного копирования, когда в цикле считывается символ из потока ввода в буфер и затем записывается в поток вывода. При их выполнении не делается никакого форматирования, подобного тому, которое выполняется операторами сдвига влево и вправо, что ускоряет выполнение операции. Кроме того, поскольку потоки работают в двоичном режиме, не надо специально обрабатывать символы EOF. В зависимости от используемого вами оборудования, ОС и т.д. вы получите различный результат при различных размерах буфера. Экспериментально вы можете найти наилучшие параметры для вашей системы

Однако можно добиться большего. Все потоки C++ уже буферизуют данные при их чтении и записи, поэтому в примере 10.9 фактически выполняется двойная буферизация. Поток ввода имеет свой собственный внутренний буфер потока, который содержит символы, прочитанные из исходного файла, но еще не обработанные с помощью

read
,
operator<<
,
getc
или любых других функций-членов, а поток вывода имеет буфер, который содержит вывод, записанный в поток, но не в «пункт назначения» (в случае применения
ofstream
это файл, но могла бы быть строка, сетевое соединение и кто знает, что еще). Поэтому лучше всего обеспечить непосредственный обмен данных буферов. Вы это можете сделать с помощью оператора
operator<<
, который работает иначе с буферами потоков. Например, вместо цикла
do/while
приведенного в примере 10.9, используйте следующий оператор.

out << in.rdbuf();

Не следует размещать этот оператор в теле цикла, замените весь цикл одной строкой. Это выглядит немного странно, поскольку обычно оператор

operator<<
говорит, «возьмите правую часть и передайте ее в поток левой части», однако, поверьте мне, эта запись имеет смысл,
rdbuf
возвращает буфер потока ввода, а реализация
operator<<
, принимающая буфер потока справа, считывает каждый символ буфера ввода и записывает его в буфер вывода. Когда буфер ввода заканчивается, он «знает», что должен заново заполнить себя данными из реального источника, a
operator<<
ведет себя не лучше.

Пример 10.9 показывает, как можно скопировать содержимое файла, но ваша ОС отвечает за управление файловой системой, которая осуществляет копирование, так почему бы не предоставить право ОС сделать эту работу? В большинстве случаев на это можно ответить, что прямой вызов программного интерфейса ОС, конечно, не является переносимым решением. Библиотека Boost Filesystem скрывает от вас множество зависящих от ОС программных интерфейсов, предоставляя функцию

copy_file
, которая выполняет системные вызовы ОС для той платформы, для которой она компилируется. Пример 10.10 содержит короткую программу, которая копирует файл из одного места в другое.

Пример 10.10. Копирование файла при помощи Boost

#include 

#include 

#include 

#include 


using namespace std;

using namespace boost::filesystem;


int main(int argc, char** argv) {

 // Проверка параметров...

 try {

  // Преобразовать аргументы в абсолютные пути, используя «родное»

  // форматирование

  path src = complete(path(argv[1], native));

  path dst = complete(path(argv[2], native));

  copy_file(src, dst);

 } catch (exception& e) {

  cerr << e.what() << endl;

 }

 return(EXIT_SUCCESS);

}

В этой небольшой программе все же имеется несколько ключевых вопросов, которые необходимо пояснить, поскольку другие рецепты данной главы используют библиотеку Boost Filesystem. Во первых, центральным компонентом библиотеки Boost Filesystem является класс

path
, описывающий независимым от ОС способом путь к файлу или каталогу. Вы можете создать
path
, используя как переносимый тип строки, так и специфичный для конкретной ОС. В примере 10.10 я создаю путь
path
из аргументов программы (этот путь я затем передаю функции
complete
, которую мы вскоре рассмотрим).

path src = complete(path(argv[1], native));

Первый аргумент — это текстовая строка, представляющая путь, например «

tmp\\foo.txt
», а второй аргумент — имя функции, которая принимает аргумент типа
string
и возвращает значение типа
Boolean
, которое показывает, удовлетворяет или нет путь определенным правилам. Функция
native
говорит о том, что проверяется родной формат ОС. Я его использовал в примере 10.10, потому что аргументы берутся из командной строки, где они, вероятно, вводятся человеком, который, по-видимому, использует родной формат ОС при задании имен файлов. Существует несколько функций, предназначенных для проверки имен файлов и каталогов и названия которых не требует пояснений:
portable_posix_name
,
windows_name
,
portable_name
,
portable_directory_name
,
portable_file_name
и
no_check
. Особенности работы этих функций вы найдете в документации.

Функция

complete
формирует абсолютный путь, используя текущий рабочий каталог и переданный ее относительный путь. Так, я могу следующим образом создать абсолютный путь к исходному файлу.

path src = complete(path("tmp\\foo.txt", native));

В том случае, если первый аргумент уже имеет абсолютное имя файла, функция

complete
выдает заданное значение и не будет пытаться его присоединить к текущему рабочему каталогу. Другими словами, в следующем операторе, выполняемом при текущем каталоге «
c:\myprograms
», последний будет проигнорирован, потому что уже задан полный путь.

path src = complete(path("c:\\windows\\garbage.txt", native));

Многие функции из библиотеки Boost Filesystem будут выбрасывать исключения, если не удовлетворяется некоторое предусловие. Это подробно описано в документации, но хорошим примером является сама функция

copy_file
. Файл должен существовать перед копированием, поэтому если исходного файла нет, операция не будет завершена успешно и
copy_file
выбросит исключение. Перехватите исключение, как я это сделал в примере 10.10, и вы получите сообщение об ошибке, объясняющее, что произошло.

10.8. Удаление или переименование файла

Проблема

Требуется удалить или переименовать файл и сделать эту операцию переносимой, те. без использования специфичного для конкретной ОС программного интерфейса.

Решение

Это сделают стандартные C-функции

remove
и
rename
, определенные в
. Пример 10.11 кратко демонстрирует, как это делается.

Пример 10.11. Удаление файла

#include 

include 

#include 


using namespace std;


int main(int argc, char** argv) {

 if (argc != 2) {

  cerr << "You must supply a file name to remove." << endl;

  return(EXIT_FAILURE);

 }

 if (remove(argv[1]) == -1) { // remove() возвращает при ошибке -1

  cerr << "Error: " << strerror(errno) << endl;

  return(EXIT_FAILURE);

 } else {

  cout << "File '" << argv[1] << "' removed." << endl;

 }

}

Обсуждение

Эти системные вызовы легко использовать: просто вызовите любую из двух функций, передав ей имя файла, который требуется удалить или переименовать. Если что-то не получится, будет возвращено ненулевое значение, и

errno
будет иметь номер соответствующей ошибки. Вы можете использовать
strerror
или
perror
(обе функции определены в
) для вывода на печать сообщения об ошибке, зависящего от реализации.

Для переименования файла следует поменять в примере 10.11 вызов функции

remove
следующим программным кодом.

if (rename(argv[1], argv[2])) {

 cerr << "Error: " << strerror(errno) << endl;

 return(EXIT_FAILURE);

}

Библиотека Boost Filesystem также предоставляет средства для удаления и переименования файла. В примере 10.12 показана короткая программа по удалению файла (или каталога, однако см. обсуждение, приводимое после этою примера).

Пример 10.12. Удаления файла средствами Boost

#include 

#include 

#include 

#include 


using namespace std;

using namespace boost::filesystem;


int main(int argc, char** argv) {

 // Проверить параметры...

 try {

  path p = complete(path(argv[1], native));

  remove(p);

 } catch (exception& e) {

  cerr << e.what() << endl;

 }

 return(EXIT_SUCCESS);

}

Важную часть примера 10.12 составляет функция

remove
. При ее вызове следует задавать достоверный путь в аргументе
path
, который ссылается на файл или пустой каталог, и они будут удалены. Пояснения по классу
path
и функции
complete
(оба они входят в библиотеку Boost Filesystem) приводятся при обсуждении рецепта 10.7. См. рецепт 10.11, где показан пример удаления каталога и всех содержащихся в нем файлов.

Переименование файла и каталога выполняется аналогично. Замените программный код в блоке

try
примера 10.12 следующим кодом.

path src = complete(path(argv[1], native));

path dst = complete(path(argv[2], native));

rename(src, dst);

В результате

src
будет переименован в
dst
при условии, что оба они содержат достоверные пути,
src
и
dst
не обязаны иметь общий каталог, и в этом смысле функция переименования фактически перемещает файл или каталог в новый базовый каталог при условии, что путь
dst
существует.

Смотри также

Рецепт 10.7.

10.9. Создание временного имени файла и временного файла

Проблема

Требуется временно сохранить на диске некоторые данные, и вам не хочется писать самому программу, которая генерирует уникальные имена.

Решение

Используйте функцию

tmpfile
или
tmpnam
, которые объявлены в
.
tmpfile
возвращает
FILE*
, который уже открыт на запись, a
tmpnam
генерирует уникальное имя файла, которое вы можете сами открыть. Пример 10.13 показывает, как можно использовать функцию
tmpfile
.

Пример 10.13. Создание временного файла

#include 

#include 


int main() {

 FILE* pf = NULL;

 char buf[256];

 pf = tmpfile(); // Создать и открыть временный файл

 if (pf) {

 fputs("This is a temp file", pf); // Записать в него некоторые данные

 }

 fseek(pf, 5, SEEK_SET); // Восстановить позицию в файле

 fgets(buf, 255, pf);   // Считать оттуда строку

 fclose(pf);

 std:cout << buf << '\n';

}

Обсуждение

Создать временный файл можно двумя способами; в примере 10.13 показан один из них. Функция

tmpfile
объявляется в
; она не имеет параметров и возвращает
FILE*
при успешном завершении и
NULL
в противном случае.
FILE*
— это тот же самый тип, который может использоваться функциями С, обеспечивающими ввод-вывод;
fread
,
fwrite
,
fgets
,
puts
и т.д.
tmpfile
открывает временный файл в режиме «wb+» — это означает, что вы можете записывать в него или считывать его в двоичном режиме (т.е. при чтении символы никак специально не интерпретируются) После нормального завершения работы программы временный файл, созданный функцией
tmpfile
, автоматически удаляется.

Такой подход может как подойти, так и не подойти для вас — все зависит от того, что вы хотите делать. Заметив, что

tmpfile
не предоставляет имени файла, вы спросите, как можно передать его другой программе? В этом случае никак; вам потребуется вместо этой функции использовать аналогичную с именем
tmpnam
.

tmpnam
на самом деле не создает временный файл, она просто создает уникальное имя файла, которое вы можете использовать при открытии файла,
tmpnam
принимает единственный параметр типа
char*
и возвращает значение типа
char*
. Вы можете передать указатель на буфер символов
char
(он должен быть, по крайней мере, не меньше значения макропеременной
L_tmpnam
, также определенной в
), куда
tmpnam
скопирует имя временного файла и возвратит указатель на тот же самый буфер. Если вы передадите
NULL
,
tmpfile
возвратит указатель на статический буфер, содержащий это имя файла, что означает его перезапись последующими вызовами
tmpnam
. (См. пример 10.14.)

Пример 10.14. Создание имени временного файла

#include 

#include 

#include 

#include 


int main() {

 char* pFileName = NULL;

 pFileName = tmpnam(NULL);

 // Здесь другая программа может получить то же самое имя временного

 // файла.

 if (!pFileName) {

  std::cerr << "Couldn't create temp file name.\n";

  return(EXIT_FAILURE);

 }

 std::cout << "The temp file name is: " << pFileName << '\n';

 std::ofstream of(pFileName);

 if (of) {

  of << "Here is some temp data.";

  of.close();

 }

 std::ifstream ifs(pFileName);

 std::string s;

 if (ifs) {

  ifs >> s;

  std::cout << "Just read in \"" << s << "\"\n";

  ifs.close();

 }

}

Однако одну важную особенность необходимо знать о функции

tmpnam
. Может возникнуть условие состязания, когда несколько процессов могут генерировать одинаковое имя файла, если один процесс вызывает
tmpname
, а другой вызывает
tmpname
до того, как первый процесс откроет этот файл. Это плохо по двум причинам. Во-первых, написанная злоумышленником программа может делать это для перехвата данных временного файла, и, во-вторых, ни о чем не подозревающая программа может получить то же самое имя файла и просто испортить или удалить данные.

10.10. Создание каталога

Проблема

Требуется создать каталог, причем эта операция должна быть переносимой, т.е. в ней не должен использоваться специфичный для конкретной ОС программный интерфейс.

Решение

На большинстве платформ вы сможете использовать системный вызов

mkdir
, который входит в состав большинства компиляторов и содержится в заголовочных файлах C-функций. Он имеет разный вид в различных ОС, но тем не менее вы можете его использовать для создания нового каталога. Стандартными средствами C++ нельзя обеспечить переносимый способ создания каталога. В этом вы можете убедиться на примере 10.15.

Пример 10.15. Создание каталога

#include 

#include 


int main(int argc, char** argv) {

 if (argc < 2) {

  std::cerr << "Usage: " << argv[0] << " [new dir name]\n";

  return(EXIT_FAILURE);

 }

 if (mkdir(argv[1]) == -1) { // Созвать каталог

  std::cerr << "Error: " << strerror(errno);

  return(EXIT_FAILURE);

 }

}

Обсуждение

Системные вызовы по созданию каталогов слегка отличаются в различных ОС, но пусть это вас не останавливает — их все же следует использовать. В большинстве систем поддерживаются различные варианты вызова

mkdir
, поэтому для создания каталога достаточно просто знать, какой включать заголовочный файл и как выглядит сигнатура функции.

Пример 10.15 работает в системах Windows, но не в Unix. В Windows

mkdir
объявляется в
. Эта функция принимает один параметр (имя каталога) и возвращает -1, если возникла ошибка, устанавливая в errno соответствующий номер ошибки. Вы можете получить зависящую от реализации текстовую строку ошибки, вызывая strerror или perror.

В Unix

mkdir
объявляется в
, и сигнатура этой функции немного отличается. Семантика ошибки такая же, как в Windows, но существует второй параметр, определяющий права доступа нового каталога. Вы должны указать права доступа, используя традиционный формат
chmod
(см. дополнительную информацию на man-странице
chmod
); например, 0777 означает, что владелец, групповой пользователь и прочие пользователи имеют право на чтение, запись и выполнение. Таким образом, вы могли бы вызвать эту функцию следующим образом.

#include 

#include  

#include 


int main(int argc, char** argv) {

 if (argc < 2) {

  std::cerr << "Usage: " << argv[0] << " [new dir name]\n";

  return(EXIT_FAILURE);

 }

 if (mkdir(argv[1], 0777) == -1) { // Создать каталог

  std::cerr << "Error: << strerror(errno);

  return(EXIT_FAILURE);

 }

}

Если вам требуется обеспечить переносимость, не следует самому писать операторы

#ifdef
, лучше воспользоваться библиотекой Boost Filesystem. Вы можете создать каталог, используя функцию
сreate_directory
, как показано в примере 10.16, который содержит короткую программу, создающую каталог.

Пример 10.16. Создание каталога средствами Boost

#include 

#include 

#include 

#include 

#include 


using namespace std;

using namespace boost::filesystem;


int main(int argc, char** argv) {

 // Проверка параметров...

 try {

  path p = complete(path(argv[1], native));

  create_directory(p);

 } catch (exception& e) {

  cerr << e.what() << endl;

 }

 return(EXIT_SUCCESS);

}

Функция

create_directory
создает каталог, имя которого вы задаете в аргументе
path
. Если этот каталог уже существует, выбрасывается исключение
filesystem_error
(которое является производным от стандартного класса исключения). Пояснения по классу
path
и функции
complete
(оба они входят в библиотеку Boost Filesystem) приводятся в обсуждении рецепта 10.7. См. рецепт 10.11, где показан пример удаления каталога и всех содержащихся в нем файлов. С другой стороны, если переносимость вас не волнует, используйте программный интерфейс файловой системы вашей ОС, который, вероятно, обладает большей гибкостью.

Смотри также

Рецепт 10.12.

10.11. Удаление каталога

Проблема

Требуется удалить каталог, причем эта операция должна быть переносимой, т.е. в ней не должен использоваться специфичный для конкретной ОС программный интерфейс.

Решение

На большинстве платформ вы сможете воспользоваться системным вызовом

rmdir
, который входит в состав большинства компиляторов и содержится в заголовочных файлах C-функций. Стандартными средствами C++ нельзя обеспечить переносимый способ удаления каталога. Вызов
rmdir
имеет разный вид в различных ОС, но тем не менее вы можете его использовать для удаления каталога. См. Пример 10.17, в котором приводится короткая программа по удалению каталога.

Пример 10.17. Удаление каталога

#include 

#include 


using namespace std;


int main(int argc, char** argv) {

 if (argc < 2) {

  cerr << "Usage: " << argv[0] << " [dir name]" << endl;

  return(EXIT_FAILURE);

 }

 if (rmdir(argv[1]) == -1) { // Удалить каталог

  cerr << "Error: " << strerror(errno) << endl;

  return(EXIT_FAILURE);

 }

}

Обсуждение

Сигнатура

rmdir
совпадает в большинстве ОС, однако объявляется эта функция в разных заголовочных файлах. В Windows она объявляется в
, а в Unix — в
. Она принимает один параметр (имя каталога) и возвращает -1, если возникла ошибка, устанавливая в
errno
соответствующий номер ошибки. Вы можете получить зависящую от реализации текстовую строку ошибки, вызывая
strerror
или
perror
.

Если целевой каталог не пустой,

rmdir
завершится с ошибкой. Для просмотра списка содержимого каталога, перечисления его элементов для их удаления см. рецепт 10.12.

Если вам требуется обеспечить переносимость, не следует самому писать операторы

#ifdef
, заключая в них специфичные для конкретной ОС функции, — лучше воспользоваться библиотекой Boost Filesystem. В библиотеке Boost Filesystem используется концепция пути для ссылки на файл или каталог, а пути можно удалять с помощью одной функции —
remove
.

Функция

removeRecurse
из примера 10.18 рекурсивно удаляет каталог и все его содержимое. Наиболее важной ее частью является функция
remove
(которая на самом деле является функцией
boost::filesystem::remove
, а не стандартной библиотечной функцией). Она принимает путь
path
в качестве аргумента и удаляет его, если это файл или пустой каталог, но она не удаляет каталог, если он содержит файлы.

Пример 10.18. Удаление каталога средствами Boost

#include 

#include 

#include 

#include 

#include 


using namespace std;

using namespace boost::filesystem;


void removeRecurse(const path& p) {

 // Сначала удалить содержимое каталога

 directory_iterator end;

 for (directory_iterator it(p); it != end; ++it) {

  if (is_directory(*it)) {

  removeRecurse(*it);

  } else {

  remove(*it);

  }

 }

 // Затем удалить сам каталог

 remove(p);

}


int main(int argc, char** argv) {

 if (argc != 2) {

  cerr << "Usage: " << argv[0] << " [dir name]\n";

  return(EXIT_FAILURE);

 }

 path thePath = system_complete(path(argv[1], native));

 if (!exists(thePath)) {

  cerr << "Error: the directory " << thePath.string()

  << " does not exist.\n";

  return(EXIT_FAILURE);

 }

 try {

  removeRecurse(thePath);

 } catch (exception& e) {

  cerr << e.what() << endl;

  return(EXIT_FAILURE);

 }

 return(EXIT_SUCCESS);

}

Программный код, обеспечивающий просмотр содержимого каталога, требует некоторых пояснений, и это является темой рецепта 10.12.

Библиотека Boost Filesystem достаточно удобна, однако следует помнить, что формально она не является стандартом, и поэтому нет гарантии, что она будет работать в любой среде. Если вы посмотрите на исходный код библиотеки Boost Filesystem, вы увидите, что фактически она компилирует системные вызовы, специфичные для целевой платформы. Если вас не волнует переносимость, используйте программный интерфейс файловой системы вашей ОС, который, вполне вероятно, обладает большей гибкостью.

Смотри также

Рецепт 10.12.

10.12. Чтение содержимого каталога

Проблема

Требуется прочитать содержимое каталога, вероятно, для того, чтобы сделать что-то с каждым его файлом или подкаталогом.

Решение

Для получения переносимого решения воспользуйтесь классами и функциями библиотеки Boost Filesystem. Она содержит ряд удобных функций по работе с файлами, обеспечивая переносимое представление путей, итераторы каталога и различные функции по переименованию, удалению и копированию файлов и т.п. Пример 10.19 показывает, как можно использовать некоторые из этих средств.

Пример 10.19. Чтение каталога

#include 

#include 

#include 


using namespace boost::filesystem;


int main(int argc, char** argv) {

 if (argc < 2) {

  std::cerr << "Usage: " << argv[0] << " [dir name]\n";

  return(EXIT_FAILURE);

 }

 path fullPath = // Создать полный, абсолютный путь

  system_complete(path(argv[1], native));

 if (!exists(fullPath)) {

  std::cerr << "Error: the directory " << fullPath.string()

  << " does not exist.\n";

  return(EXIT_FAILURE);

 }

 if (!is_directory(fullPath)) {

  std::cout << fullPath.string() << " is not a directory!\n";

  return(EXIT_SUCCESS);

 }

 directory_iterator end;

 for (directory_iterator it(fullPath);

  it != end; ++it) {     // Просматривать в цикле каждый

              // элемент каталога почти

 std::cout << it->leaf(); // так же, как это делалось бы для

  if (is_directory(*it))  // STL-контейнера

  std::cout << " (dir)";

  std::cout << '\n';

 }

 return(EXIT_SUCCESS);

}

Обсуждение

Как и при создании и удалении каталогов (см. рецепты 10.10 и 10.11), не существует стандартного, переносимого способа чтения содержимого каталога. Чтобы облегчить жизнь в C++, библиотека Filesystem проекта Boost содержит ряд переносимых функций по работе с файлами и каталогами. Она также содержит много других функций; дополнительную информацию вы найдете при описании других рецептов этой главы или на веб-странице библиотеки Boost Filesystem сайта www.boost.com.

В примере 10.19 приводится простая программа просмотра каталога (наподобие команды

ls
в Unix или
dir
в MS-DOS). Сначала она следующим образом формирует абсолютный путь на основе аргументов, переданных программе.

path fullPath = complete(path(argv[1], native));

Тип данных переменной, содержащей путь, называется, естественно,

path
(путь). С этим типом данных работает файловая система, и он легко преобразуется в строку путем вызова
path::string
. После формирования пути программа проверяет его существование (с помощью функции
exists
), затем с помощью другой функции,
is_directory
, проверяет, задает ли данный путь каталог. Если ответ положителен, то все хорошо и можно перейти к реальной работе по просмотру содержимого каталога.

Файловая система имеет класс с именем

directory_iterator
, который использует стандартную семантику итераторов, подобную применяемой для стандартных контейнеров, чтобы можно было использовать итераторы как указатели на элементы каталога. Однако в отличие от стандартных контейнеров здесь нет функции-члена
end
, представляющей элемент, следующий за последним элементом (т.е.
vector::end
). Вместо этого, если вы создаете итератор каталога
directory_iterator
при помощи стандартного конструктора, он предоставляет конечный маркер, который вы можете использовать в операциях сравнения для определения момента завершения просмотра каталога. Поэтому используйте следующий оператор.

directory_iterator end;

Затем вы можете создать итератор для вашего пути и следующим образом сравнивать его с маркером конца.

for (directory_iterator it(fullPath); it != end; ++it) {

 // выполнить любые действия с *it

 std::cout << it->leaf();

}

Функция-член

leaf
возвращает строку, представляющую конечный элемент пути, а не весь путь, который вы можете получить, вызывая функцию-член
string
.

Если вам требуется обеспечить переносимость, но по каким-то причинам вы не можете использовать библиотеки Boost, обратите внимание на исходный код Boost. Он содержит операторы

#ifdef
, которые учитывают (по большей части) отличия среды Windows и ОС, использующих интерфейс Posix, и в частности отличия в представлении путей, например буквы дисководов и имена устройств.

Смотри также

Рецепты 10.10 и 10.11.

10.13. Извлечение расширения файла из строки

Проблема

Имеется имя файла или полный путь и требуется получить расширение файла, которое является частью имени файла, расположенной за последней точкой. Например, в именах файлов src.cpp, Window.class и Resume.doc расширениями файла являются соответственно .cpp, .class и .doc.

Решение

Преобразуйте имя файла или путь к нему в строку

string
, используйте функцию-член
rfind
для определения позиции последней точки и возвратите то, что находится за ней. Пример 10.20 показывает, как это можно сделать.

Пример 10.20. Получение расширения файла из его имени

#include 

#include 


using std::string;


string getFileExt(const string& s) {

 size_t i = s.rfind('.', s.length());

 if (i ! = string::npos) {

  return(s.substr(i+1, s.length() - i));

 }

 return("");

}


int main(int argc, char** argv) {

 string path = argv[1];

 std::cout << "The extension is \"" << getFileExt(path) << "\"\n";

}

Обсуждение

Для получения расширения из имени файла достаточно лишь найти позицию последней точки «.» и выделить все, что находится справа от нее. Стандартный класс

string
, определенный в
, содержит функции, которые могут выполнить обе эти операции:
rfind
и
substr
.

rfind
выполнит поиск (в обратном направлении) того, что вы передаете ей в первом аргументе (символ типа
char
в данном случае), начиная с позиции, указанной вторым аргументом, и возвращает позицию, в которой найден указанный объект. Если поиск завершился неудачей,
rfind
возвратит
string::npos
. Функция
substr
также имеет два аргумента. Первый аргумент содержит позицию первого копируемого элемента, а второй аргумент — количество копируемых символов.

Стандартный класс строки содержит несколько функций-членов, выполняющих поиск. См. рецепт 4.9, в котором более детально обсуждается поиск строк.

Смотри также

Рецепты 4.9 и 10.12.

10.14. Извлечение имени файла из полного пути

Проблема

Имеется полный путь к файлу, например d:\apps\src\foo.с, и требуется получить имя файлa, foo.с.

Решение

Примените подход, который использовался в предыдущем рецепте, и используйте функции

rfind
и
substr
для поиска и получения из полного пути то, что вам нужно. Пример 10.21 показывает, как это можно сделать.

Пример 10.21. Извлечение имени файла из полного пути

#include 

#include 


using std::string;


string getFileName(const string& s) {

 char sep = '/';

#ifdef _WIN32

 sep = '\\';

#endif

 size_t i = s.rfind(sep.s.length());

 if (i ! = string::npos) {

  return(s.substr(i+1, s.length( ) - i));

 }

 return("");

}


int main(int argc, char** argv) {

 string path = argv[1];

 std::cout << "The file name is \"" << getFileName(path) << "\"\n";

}

Обсуждение

См. предыдущий рецепт, в котором приводится детальное описание функций

rfind
и
substr
. Стоит отметить только то, что вы уже, по-видимому, заметили в примере 10.21: в Windows в качестве разделителя используется обратный слеш вместо прямого, поэтому я добавил оператор
#ifdef
для установки требуемого разделителя элементов пути.

Класс

path
из библиотеки Boost Filesystem позволяет легко получить с помощью функции-члена
path::leaf
последний элемент полного пути, которым может быть имя файла или каталога. В примере 10.22 приводится простая программа, которая использует эту функцию, чтобы показать, к чему относится этот путь: к файлу или к каталогу.

Пример 10.22. Получение имени файла из пути

#include 

#include 

#include 


using namespace std;

using namespace boost::filesystem;


int main(int argc, char** argv) {

 // Проверка параметров

 try {

  path p = complete(path(argv[1], native));

 cout << p.leaf() << " is a "

  << (is_directory(p) ? "directory" : "file") << endl;

 } catch (exception& e) {

  cerr << e.what() << endl;

 }

 return(EXIT_SUCCESS);

}

См. обсуждение рецепта 10.7, где более детально рассматривается класс

path
.

Смотри также

Рецепт 10.15.

10.15. Извлечение пути из полного имени файла

Проблема

Имеется полное имя файла (имя файла и путь доступа к нему), например

d:\apps\src\foo.с
, и требуется получить путь к файлу,
d:\apps\src
.

Решение

Примените подход, который использовался в предыдущих двух рецептах, и используйте функции

rfind
и
substr
для поиска и получения из полного пути то, что вам нужно. В примере 10.23 приводится короткая программа, показывающая, как это можно сделать.

Пример 10.23. Получение пути из полного имени файла

#include 

#include 


using std::string;


string getPathName(const string& s) {

 char sep = '/';

#ifdef _WIN32

 sep = '\\';

#endif

 size_t i = s.rfind(sep, s.length());

 if (i != string::npos) {

  return(s.substr(0, !));

 }

 return("");

}


int main(int argc, char** argv) {

 string path = argv[1];

 std::cout << "The path name is \"" << getPathName(path) << "\"\n";

}

Обсуждение

Пример 10.23 тривиален, особенно если вы уже знакомы с двумя предыдущими рецептами, поэтому дополнительные пояснения не требуются. Однако, как и во многих других рецептах, библиотека Boost Filesystem позволяет извлекать с помощью функции

branch_path
все, что угодно, кроме последней части имени файла. Пример 10.24 показывает, как можно использовать эту функцию.

Пример 10.24. Получение базового пути

#include 

#include 

#include 


using namespace std;

using namespace boost::filesystem;


int main(int argc, char** argv) {

 // Проверка параметров...

 try {

  path p = complete(path(argv[1], native));

  cout << p.branch_path().string() << endl;

 } catch (exception& e) {

  cerr << e.what() << endl;

 }

 return(EXIT_SUCCESS);

}

Результат выполнения примера 10.24 может выглядеть следующим образом.

D:\src\ccb\c10>bin\GetPathBoost.exe с:\windows\system32\1033

с:/windows/system32

Смотри также

Рецепты 10.13 и 10.14.

10.16. Замена расширения файла

Проблема

Имеется имя файла (возможно, с путем доступа к нему) и требуется заменить расширение файла. Например, имя файла

thesis.tex
требуется преобразовать в
thesis.txt
.

Решение

Используйте функции-члены

rfind
и
replace
класса
string
для поиска расширения и его замены. Пример 10.25 показывает, как это можно сделать.

Пример 10.25. Замена расширения файла

#include 

#include 


using std::string;


void replaceExt(string& s, const string& newExt) {

 string::size_type i = s.rfind('.', s.length());

 if (i != string::npos) {

  s.replace(i+1, newExt.length(), newExt);

 }

}


int main(int argc, char** argv) {

 string path = argv[1];

 replaceExt(path, "foobar");

 std::cout << "The new name is \"" << path << "\"\n";

}

Обсуждение

Здесь используется подход, аналогичный тому, который применялся в предыдущих рецептах, однако в данном случае я использовал функцию

replace
для замены части строки новой подстрокой. Функция
replace
имеет три параметра. Первый параметр задает позицию, в которую вставляется новая подстрока, а второй параметр определяет количество символов, которые необходимо удалить в формируемой строке. Третий параметр — это значение, которое будет использовано для замены удаляемой части строки.

Смотри также

Рецепт 4.9.

10.17. Объединение двух путей в один

Проблема

Имеется два пути и требуется их объединить в один путь. Например, вы имеете в качестве первого пути

/usr/home/ryan
и в качестве второго —
utils/compilers
; требуется получить
/usr/home/ryan/utils/compilers
, причем первый путь может как иметь, так и не иметь в конце разделитель элементов пути.

Решение

Рассматривайте пути как строки и используйте оператор добавления в конец строки,

operator+=
, для составления полного пути из составных частей. См. пример 10.26.

Пример 10.26. Объединение путей

#include 

#include 


using std::string;


string pathAppend(const string& p1, const string& p2) {

 char sep = '/';

 string tmp = p1;

#ifdef _WIN32

 sep = '\\';

#endif

 if (p1[p1.length()] != sep) { // Необходимо добавить

  tmp += sep;          // разделитель

  return(tmp + p2);

 } else

  return(p1 + p2);

}


int main(int argc, char** argv) {

 string path = argv[1];

 std::cout << "Appending somedir\\anotherdir is \""

  << pathAppend(path, "somedir\\anotherdir") << "\"\n";

}

Обсуждение

В программе примера 10.26 для представления путей используются строки, но здесь не делается дополнительной проверки достоверности путей и переносимость их полностью зависит от содержащихся в них значений. Например, если эти значения получены от пользователя, то вы не можете заранее знать, имеют ли они правильный формат конкретной ОС или содержат недопустимые символы.

Для многих других рецептов данной главы я включил примеры по использованию библиотеки Boost Filesystem, и при работе с путями такой подход имеет много преимуществ. Как я говорил при обсуждении рецепта 10.7, библиотека Boost Filesystem содержит класс

path
, обеспечивающий переносимое представление пути к файлу или каталогу. Операции в библиотеке Filesystem в основном оперируют объектами
path
, и поэтому с помощью класса
path
можно реализовать объединение относительного пути с абсолютной его базовой частью. (См. пример 10.27.)

Пример 10.27. Объединение путей средствами Boost

#include 

#include 

#include 

#include 

#include 


using namespace std;

using namespace boost::filesystem;


int main(int argc, char** argv) {

 // Проверка параметров...

 try {

  // Составить путь из значений двух аргументов path

  p1 = complete(path(argv[2], native),

  path(argv[1], native));

  cout << p1.string() << endl;

  // Создать путь с базовой частью пути текущего каталога path

  p2 = system_complete(path(argv[2], native));

  cout << p2.string() << endl;

 } catch (exception& e) {

  cerr << e.what() << endl;

 }

 return(EXIT_SUCCESS);

}

Результат выполнения программы примера 10.27 может иметь такой вид.

D:\src\ccb\c10>bin\MakePathBoost.exe d:\temp some\other\dir

d:/temp/some/other/dir

D:/src/ccb/c10/some/other/dir

Или такой.

D:\src\ccb\c10>bin\MakePathBoost.exe d:\temp с:\WINDOWS\system32

c:/WINDOWS/system32

c:/WINDOWS/system32

Из этого примера видно, что функции

complete
и
system_complete
объединяют пути, когда это возможно, и возвращают абсолютный путь, когда объединение путей не имеет смысла. Например, в первом случае первый переданный программе аргумент является абсолютным путем каталога, а второй — относительным путем. Функция
complete
объединяет их и формирует один, абсолютный путь. Первый аргумент
complete
является относительным путем, а второй — абсолютным путем, и если первый аргумент уже является абсолютным путем, второй аргумент игнорируется. Поэтому во втором случае аргумент «
d:\temp
» игнорируется, так как переданный мною второй аргумент уже является абсолютным путем.

system_complete
принимает только один аргумент (в данном случае это относительный путь) и добавляет его в конец текущего рабочего каталога, получая другой абсолютный путь. И в этом случае, если переданный вами путь уже является абсолютным, текущий рабочий каталог игнорируется и просто возвращается переданный вами абсолютный путь.

Однако эти пути не согласуются с требованиями файловой системы. Вам придется самому проверить объекты

path
, чтобы убедиться, что они представляют правильный путь файловой системы. Например, для проверки существования этих путей вы можете использовать функцию
exists
.

path p1 = complete(path(argv[2], native),

path(argv[1], native));

if (exists(p1)) {

 // ...

Существует много других функций, позволяющих получать информацию о пути:

is_directory
,
is_empty
,
file_size
,
last_write_time
и т.д. Дополнительную информацию вы найдете в документации по библиотеке Boost Filesystem на сайте www.boost.org.

Смотри также

Рецепт 10.7.

Загрузка...