сообщает потоку, чтобы он ничего не делал
// с байтами
if (!ofs) error("Невозможно открыть файл для ввода ",name);
vector<int> v;
// чтение из бинарного файла:
int i;
while (ifs.read(as_bytes(i),sizeof(int))) // примечание:
// читаем байты
v.push_back(i);
// ...что-то делаем с вектором v...
// записываем в двоичный файл:
for(int i=0; i<v.size(); ++i)
ofs.write(as_bytes(v[i]),sizeof(int)); // примечание:
// запись байтов
return 0;
}
Мы открыли эти файлы с помощью опции ios_base::binary.
ifstream ifs(name.c_str(), ios_base::binary);
ofstream ofs(name.c_str(), ios_base::binary);
В обоих вариантах мы выбрали более сложное, но часто более компактное двоичное представление. Если мы перейдем от символьно-ориентированного ввода-вывода к двоичному, то не сможем использовать обычные операторы ввода и вывода >> и <<. Эти операторы преобразуют значения в последовательности символов, руководствуясь установленными по умолчанию правилами (например, строка "asdf" превращается в символы a, s, d, f, а число 123 превращается в символы 1, 2, 3). Если вы не хотите работать с двоичным представлением чисел, достаточно ничего не делать и использовать режим, заданный по умолчанию. Мы рекомендуем применять опцию binary, только если вы (или кто-нибудь еще) считаете, что так будет лучше. Например, с помощью опции binary можно сообщить потоку, что он ничего не должен делать с байтами.
А что вообще мы могли бы сделать с типом int? Очевидно, записать его в память размером четыре байта; иначе говоря, мы могли бы обратиться к представлению типа int в памяти (последовательность четырех байтов) и записать эти байты в файл. Позднее мы могли бы преобразовать эти байты обратно в целое число.
ifs.read(as_bytes(i),sizeof(int)) // чтение байтов
ofs.write(as_bytes(v[i]),sizeof(int)) // запись байтов
Функция write() потока ostream и функция read() потока istream принимают адрес (с помощью функции as_bytes()) и количество байтов (символов), полученное с помощью оператора sizeof. Этот адрес должен ссылаться на первый байт в памяти, хранящей значение, которое мы хотим прочитать или записать. Например, если у нас есть объект типа int со значением 1234, то мы могли бы получить четыре байта (используя шестнадцатеричную систему обозначений) — 00, 00, 04, d2:
Функция as_bytes() позволяет получить адрес первого байта объекта. Ее определение выглядит так (некоторые особенности языка, использованные здесь, будут рассмотрены в разделах 17.8 и 19.3):
template<class T>
char* as_bytes(T& i) // рассматривает объект T как последовательность
// байтов
{
void* addr = &i; // получаем адрес первого байта
// памяти, использованной для хранения объекта
return static_cast<char*>(addr); // трактуем эту память как байты
}
Небезопасное преобразование типа с помощью оператора static_cast необходимо для того, чтобы получить переменную в виде совокупности байтов. Понятие адреса будет подробно изучено в главах 17 и 18. Здесь мы просто показываем, как представить любой объект, хранящийся в памяти, в виде совокупности байтов, чтобы прочитать или записать его с помощью функций read() и write().
Этот двоичный вывод запутан, сложен и уязвим для ошибок. Однако программисты не всегда должны иметь полную свободу выбора формата файла, поэтому иногда они просто вынуждены использовать двоичный ввод-вывод по воле кого-то другого. Кроме того, отказ от символьного представления иногда можно логично обосновать. Типичные примеры — рисунок или звуковой файл, — не имеющие разумного символьного представления: фотография или фрагмент музыкального произведения по своей природе является совокупностью битов.
Символьный ввод-вывод, по умолчанию предусмотренный в библиотеке, не изменяется при переносе программ из одного компьютера в другой, доступен для человеческого понимания и поддерживается любыми средствами набора текстов. Если есть возможность, рекомендуем использовать именно символьный ввод-вывод, а двоичный ввод-вывод применять только в случае крайней необходимости.
11.3.3. Позиционирование в файлах
При малейшей возможности считывайте и записывайте файлы от начала до конца. Это проще всего и открывает меньше возможностей для совершения ошибок. Каждый раз, когда вы понимаете, что пора изменить файл, лучше создайте новый и запишите в него все изменения. Однако, если вы должны поступить иначе, то можно выполнить позиционирование и указать конкретное место для чтения и записи в файле. В принципе в любом файле, открытом для чтения, существует позиция для считывания/ввода (“read/get position”), а в любом файле, открытом для записи, есть позиция для записи/вывода (“write/put position”).
Эти позиции можно использовать следующим образом.
fstream fs(name.c_str()); // открыть для ввода и вывода
if (!fs) error("Невозможно открыть файл ",name);
fs.seekg(5); // перенести позицию считывания (буква g означает "get")
// на пять ячеек вперед (шестой символ)
char ch;
fs>>ch; // считать и увеличить номер позиции для считывания
cout << " шестой символ — это " << ch << '(' << int(ch) << ")n";
fs.seekp(1); // перенести позицию для записи (буква p означает "put")
// на одну ячейку вперед
fs<<'y'; // записать и увеличить позицию для записи
Будьте осторожны: ошибки позиционирования не распознаются. В частности, если вы попытаетесь выйти за пределы файла (используя функцию seekg() или seekp()), то последствия могут быть непредсказуемыми и состояние операционной системы изменится.
11.4. Потоки строк
В качестве источника ввода для потока istream или цели вывода для потока ostream можно использовать объект класса string. Поток istream, считывающий данные из объекта класса string, называется istringstream, а поток ostream, записывающий символы в объект класса string, называется ostringstream. Например, поток istringstream полезен для извлечения числовых значений из строк.
double str_to_double(string s)
// если это возможно, преобразовывает символы из строки s
// в число с плавающей точкой
{
istringstream is(s); // создаем поток для ввода из строки s
double d;
is >> d;
if (!is) error("Ошибка форматирования типа double: ",s);
return d;
}