Uploaded by Malik Ubaydullaev

ЛЕКЦИЯ-9

advertisement
Программирование
9 неделя
Тема: Файлы и работа с файлами.
9
Тема
Файлы и
работа с
файлами
Вид
занятий
Содержание занятий
Часы
№
9 лекция
2 Файлы и работа с файлами
Lab_9.1
2
Lab_9.2
задания для
самостоятельног
о выполнения
Создание программ с использованием
текстового ввода и вывода.
Создание программ для открытия
файла в разных режимах и с
2
использованием бинарного ввода и
вывода.
Ответить на заданные вопросы,
2
ответить на тесты
4 Создание программ
Лекция 8.
Тема: Файлы и работа с файлами.
План:
9.1
9.2
9.3
9.4
9.5
9.6
9.7
9.8
9.9
Введение;
Текстовый ввод / вывод;
Форматирование вывода;
Функции: getline, get и put;
fstream и режимы открытия файлов;
Тестирование потоковых состояний;
Бинарный ввод / вывод;
Файл с произвольным доступом;
Обновление файлов.
Ключевые слова:
binary file, file open mode, file pointer, input stream, output
stream, text file, stream state, relative file nam
9.1 Введение.
Ключевой момент. Вы можете читать / записывать данные из / в
файл, используя функции классов ifstream, ofstream и fstream.
Данные, хранящиеся в переменных, массивах и объектах, являются
временными; они теряются при завершении программы. Для постоянного
хранения данных, созданных в программе, вы должны сохранить их в файле
на постоянном носителе, например, на диске. Файл может быть перенесен и
может быть прочитан позже другими программами. На уроках 1-го семестра,
«Простой ввод и вывод файла», расмотрено ввод простой текстовый ввод /
вывод, включающий числовые значения. Этот урок подробно описывает ввод
/ вывод.
C++ предоставляет классы ifstream, ofstream и fstream для обработки и
манипулирования файлами. Эти классы определены в заголовочном файле
<fstream>. Класс ifstream предназначен для чтения данных из файла, класс
ofstream предназначен для записи данных в файл, а класс fstream можно
использовать для чтения и записи данных в файл.
C++ использует термин поток для описания потока данных. Если он
поступает в вашу программу, этот поток называется входным потоком. Если
он вытекает из вашей программы, он называется потоком вывода. C++
использует объекты для чтения / записи потока данных. Для удобства
входной объект называется входным потоком, а выходной объект
называется выходным потоком.
Вы уже использовали входной поток и выходной поток в своих
программах. cin (консольный ввод) - это предопределенный объект для
чтения ввода с клавиатуры, а cout (консольный вывод) - это
предопределенный объект для вывода символов на консоль. Эти два объекта
определены в заголовочном файле <iostream>. В этом уроке вы узнаете, как
читать / записывать данные из / в файлы.
9.2 Текстовый ввод / вывод.
Ключевой момент. Данные в текстовом файле могут быть
прочитаны текстовым редактором.
В этом разделе показано, как выполнить простой ввод и вывод текста.
Каждый файл помещается в каталог в файловой системе. Абсолютное
имя файла содержит имя файла с полным путем и буквой диска. Например,
c:\example\scores.txt - это абсолютное имя файла для файла Scores.txt в
операционной системе Windows. Здесь c:\example называется путем к
каталогу для файла. Абсолютные имена файлов зависят от машины. В UNIX
абсолютное имя файла может быть /home/liang/example/scores.txt, где
/home/liang/example - путь к каталогу для файла Scores.txt.
Относительное имя файла относительно его текущего рабочего
каталога. Полный путь к каталогу для относительного имени файла опущен.
Например, Scores.txt - это относительное имя файла. Если текущим рабочим
каталогом является c:\example, абсолютное имя файла будет
c:\example\scores.txt.
Запись данных в файл
Класс ofstream может использоваться для записи примитивных
значений типа данных, массивов, строк и объектов в текстовый файл. В
листинге 8.1 показано, как записывать данные. Программа создает экземпляр
ofstream и записывает две строки в файл Scores.txt. Каждая строка состоит
из имени (string), отчества инициала (char), фамилии (string) и счета (int).
Листинг 8.1 TextFileOutput.cpp
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ofstream output;
// Sozdat fayl John T Smith 90
output.open("scores.txt");
// Napisat dve stroki
output << "John" << " " << "T" << " " << "Smith"
<< " " << 90 << endl;
output << "Eric" << " " << "K" << " " << "Jones"
<< " " << 85 << endl;
output.close();
cout << "Done" << endl;
return 0;
}
Так как класс ofstream определен в файле заголовка fstream, строка 2
включает этот файл заголовка.
Строка 7 создает объект output из класса ofstream, используя его
конструктор без аргументов.
Строка 10 открывает файл с именем Scores.txt для объекта output. Если
файл не существует, создается новый файл. Если файл уже существует, его
содержимое уничтожается без предупреждения.
Вы можете записать данные в объект output с помощью оператора
вставки потока (<<) так же, как вы отправляете данные в объект cout. Строки
13–16 записывают строки и числовые значения для вывода, как показано на
рисунке 8.1.
Рис. 8.1. Выходной поток отправляет данные в файл.
Функция close() (строка 18) должна использоваться для закрытия потока
для объекта. Если эта функция не вызывается, данные могут быть
неправильно сохранены в файле.
Вы можете открыть поток вывода, используя следующий конструктор:
ofstream output("scores.txt");
Это утверждение эквивалентно
ofstream output;
output.open("scores.txt");
Предосторожность
Если файл уже существует, его содержимое будет уничтожено без предупреждения.
предосторожность
Разделитель каталогов для Windows является обратной косой чертой (\). Обратная косая черта
является специальным escape-символом и должна быть записана как \\ в строковом литерале Например,
output.open("c:\\example\\scores.txt");
Заметка
Абсолютное имя файла зависит от платформы. Лучше использовать
относительное имя файла без букв дисков. Если вы используете IDE для
запуска C++, каталог относительного имени файла может быть указан в IDE.
Например, каталог по умолчанию для файлов данных - это тот же каталог,
что и исходный код в QTCreator.
Чтение данных из файла.
Класс ifstream можно использовать для чтения данных из текстового
файла. В листинге 8.2 показано, как читать данные. Программа создает
экземпляр ifstream и считывает данные из файла Scores.txt, который был
создан в предыдущем примере.
Листинг 8.2 TextFileInput.cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
ifstream input("scores.txt");
// Read data
string firstName;
char mi;
string lastName;
int score;
input >> firstName >> mi >> lastName >> score;
cout << firstName << " " << mi << " " << lastName << " "
<< score << endl;
input >> firstName >> mi >> lastName >> score;
cout << firstName << " " << mi << " " << lastName << " "
<< score << endl;
input.close();
cout << "Done" << endl;
return 0;
}
Поскольку класс ifstream определен в заголовочном файле fstream,
строка 2 включает этот заголовочный файл.
В строке 8 создается объект input из класса ifstream для файла
scores.txt.
Вы можете читать данные из объекта input с помощью оператора
извлечения потока (>>) так же, как вы читаете данные из объекта cin. Строки
15 и 19 читают строки и числовые значения из входного файла, как показано
на рисунке 8.2.
Функция close() (строка 23) используется для закрытия потока для
объекта. Нет необходимости закрывать входной файл, но это хорошая
практика, чтобы высвободить ресурсы, занятые файлом.
Вы можете открыть входной поток, используя следующий конструктор:
ifstream input("scores.txt");
Рис. 8.2. Входной поток читает данные из файла.
Это утверждение эквивалентно
ifstream input;
input.open("scores.txt");
предосторожность
Чтобы правильно читать данные, вам нужно точно знать, как хранятся данные. Например, программа
в листинге 8.2 не будет работать, если файл содержит счет в виде значения double с десятичной точкой.
Наличие файла тестирования
Если файл не существует при чтении файла, ваша программа запустится
и выдаст неверные результаты. Может ли ваша программа проверить,
существует ли файл? Да. Вы можете вызвать функцию fail() сразу после
вызова функции open. Если fail() возвращает true, это означает, что файл не
существует.
1 // Open a file
2 input.open("scores.txt");
3
4 if (input.fail())
5 {
6 cout << "File does not exist" << endl;
7 cout << "Exit program" << endl;
8
9 return 0;
10 }
Тестирование конца файла
Листинг 8.2 читает две строки из файла данных. Если вы не знаете,
сколько строк в файле и хотите прочитать их все, как вы узнаете конец
файла? Вы можете вызвать функцию eof() для объекта input, чтобы
обнаружить его. Однако эта программа не будет работать, если после
последнего номера есть дополнительные пустые символы. Чтобы понять это,
давайте посмотрим на файл, который содержит числа, показанные на рисунке
8.3. Обратите внимание, что после последнего номера есть дополнительный
пустой знак.
Рис. 8.3. Файл содержит числа, разделенные пробелами.
Если вы используете следующий код для чтения всех данных и
суммирования, последний номер будет добавлен дважды.
ifstream input("score.txt");
double sum = 0;
double number;
while (!input.eof()) // Continue if not end of file
{
input >> number; // Read data
cout << number << " "; // Display data
sum += number;
}
Причина этого заключается в том, что при считывании последнего
номера 85.6 файловая система не знает, что это последний номер, поскольку
после последнего номера есть пустые символы. Итак, функция eof()
возвращает false. Когда программа снова читает число, функция eof()
возвращает true, но номер переменной не изменяется, поскольку из файла
ничего не читается. Переменная number по-прежнему содержит значение
85.6, которое снова добавляется к sum.
Есть два способа решить эту проблему. Один из них - проверить
функцию eof() сразу после чтения числа. Если функция eof() возвращает true,
выйдите из цикла, как показано в следующем коде:
ifstream input("score.txt");
double sum = 0;
double number;
while (!input.eof()) // Continue if not end of file
{
input >> number; // Read data
if (input.eof()) break;
cout << number << " "; // Display data
sum += number;
}
Другой способ решить эту проблему - написать код следующим
образом:
while (input>>number)//Continue to read data until it fails
{
cout << number << " "; // Display data
sum += number;
}
Оператор input>>number фактически предназначен для вызова
операторной функции. Функции оператора будут представлены на
следующей лекции. Эта функция возвращает объект, если число читается; в
противном случае возвращается NULL. NULL является константным
значением 0. C++ автоматически преобразует его в значение bool false, когда
оно используется в качестве условия в операторе цикла или операторе
выбора. Если из входного потока не читается число, input >> number
возвращает NULL и цикл завершается.
В листинге 8.3 приведена полная программа, которая считывает числа
из файла и отображает их сумму.
Листинг 8.3 TestEndOfFile.cpp
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
// Open a file
ifstream input("score.txt");
if (input.fail())
{
cout << "File does not exist" << endl;
cout << "Exit program" << endl;
return 0;
}
double sum = 0;
double number;
while (input >> number) // Continue if not end of file
{
cout << number << " "; // Display data
sum += number;
}
input.close();
cout << "\nSum is " << sum << endl;
return 0;
}
Программа читает данные в цикле (строки 19–23). Каждая итерация
цикла читает одно число и добавляет его к sum. Цикл завершается, когда вход
достигает конца файла.
Разрешение пользователю вводить имя файла
В предыдущих примерах имена файлов являются строковыми
литералами, жестко закодированными в программе. Во многих случаях
желательно, чтобы пользователь вводил имя файла во время выполнения. В
листинге 8.4 приведен пример, который предлагает пользователю ввести имя
файла и проверяет, существует ли файл.
Листинг 8.4 CheckFile.cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
string filename;
cout << "Enter a file name: ";
cin >> filename;
ifstream input(filename.c_str());
if (input.fail())
cout << filename << " does not exist" << endl;
else
cout << filename << " exists" << endl;
return 0;
}
Программа предлагает пользователю ввести имя файла в виде строки
(строка 10). Однако имя файла, передаваемое конструктору потока ввода и
вывода или функции open, должно быть C-strings в стандартном C++. Итак,
функция c_str() в строковом классе вызывается для возврата C-strings из
объекта string (строка 12).
9.3 Форматирование вывода.
Ключевой момент. Потоковые манипуляторы могут использоваться
для форматирования вывода на консоль, а также на вывод файла.
Вы использовали потоковые манипуляторы для форматирования вывода
на консоль в 1-м семестре, «Форматирование вывода консоли». Вы можете
использовать тот же потоковый манипулятор для форматирования вывода в
файл. В листинге 8.5 приведен пример, который форматирует записи
учеников в файл с именем formattedscores.txt.
Листинг 8.5 WriteFormattedData.cpp
#include <iostream>
#include <iomanip>
#include <fstream>
using namespace std;
int main()
{
ofstream output;
// Create a file
output.open("formattedscores.txt");
// Write two lines
output<<setw(6)<<"John"<<setw(2)<<"T"<<setw(6)<< "Smith"
<< " " << setw(4) << 90 << endl;
output<<setw(6)<<"Eric"<<setw(2)<<"K"<< setw(6)<<"Jones"
<< " " << setw(4) << 85;
output.close();
cout << "Done" << endl;
return 0;
}
Содержимое файла показано ниже:
9.4 Функции: getline, get и put.
Ключевой момент. Функция getline может использоваться для чтения
строки, содержащей пробельные символы, а функция get/put может
использоваться для чтения и записи одного символа.
Существует проблема при чтении данных с использованием оператора
извлечения потока (>>). Данные ограничены пробелами. Что произойдет,
если пробельные символы являются частью строки? В разделе «Чтение
строк» вы узнали, как использовать функцию getline для чтения строки с
пробелами. Вы можете использовать ту же функцию для чтения строк из
файла. Напомним, что синтаксис для функции getline
getline(ifstream& input, int string s, char delimitChar)
Функция прекращает чтение символов, когда встречается символ
разделителя или метка конца файла. Если встречается разделитель, он
читается, но не сохраняется в массиве. Третий аргумент delimitChar имеет
значение по умолчанию ('\n'). Функция getline определена в заголовочном
файле iostream.
Предположим, создан файл с именем state.txt, который содержит имена
состояний, разделенные символом номера (#). Следующий рисунок
показывает содержимое файла:
В листинге 8.6 приведена программа, которая читает состояния из
файла.
Листинг 8.6 ReadCity.cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
// Open a file
ifstream input("state.txt");
if (input.fail())
{
cout << "File does not exist" << endl;
cout << "Exit program" << endl;
return 0;
}
// Read data
string city;
while (!input.eof()) // Continue if not end of file
{
getline(input, city, '#');
cout << city << endl;
}
input.close();
cout << "Done" << endl;
return 0;
}
Вызов getline(input, state, '#') (строка 23) считывает символы в
состояние массива, пока не встретит символ # или конец файла.
Две другие полезные функции - это get и put. Вы можете вызвать
функцию get для входного объекта, чтобы прочитать символ, и вызвать
функцию put для выходного объекта, чтобы написать символ.
Функция get имеет две версии:
char get() // Return a char
ifstream* get(char& ch) // Read a character to ch
Первая версия возвращает символ из ввода. Вторая версия передает
аргумент ссылки на символ, читает символ из ввода и сохраняет его в ch. Эта
функция также возвращает ссылку на используемый объект ввода.
Заголовок для функции put
void put(char ch)
Записывает указанный символ в выходной объект.
В листинге 8.7 приведен пример использования этих двух функций.
Программа предлагает пользователю ввести файл и копирует его в новый
файл.
Листинг 8.7 CopyFile.cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
// Enter a source file
cout << "Enter a source file name: ";
string inputFilename;
cin >> inputFilename;
// Enter a target file
cout << "Enter a target file name: ";
string outputFilename;
cin >> outputFilename;
// Create input and output streams
ifstream input(inputFilename.c_str());
ofstream output(outputFilename.c_str());
if (input.fail())
{
cout << inputFilename << " does not exist" << endl;
cout << "Exit program" << endl;
return 0;
}
char ch = input.get();
while (!input.eof()) // Continue if not end of file
{
output.put(ch);
ch = input.get(); // Read next character
}
input.close();
output.close();
cout << "\nCopy Done" << endl;
return 0;
}
Программа предлагает пользователю ввести имя исходного файла в
строке 11 и имя целевого файла в строке 16. Входной объект для
inputFilename создается в строке 19, а выходной объект для outputFilename
в строке 20. Имена файлов должны быть C-Strings. inputFilename.c_str()
возвращает C-Strings из строки inputFilename.
Строки 22–27 проверяют, существует ли входной файл. Строки 30–34
последовательно читают символы по одному, используя функцию get, и
записывают символ в выходной файл, используя функцию put.
Предположим, строки 29–34 заменены следующим кодом:
while (!input.eof()) // Continue if not end of file
{
output.put(input.get());
}
Что случится? Если вы запустите программу с этим новым кодом, вы
увидите, что новый файл на один байт больше исходного. Новый файл
содержит дополнительный символ мусора в конце. Это связано с тем, что,
когда последний символ читается из входного файла с использованием
input.get(), input.eof() все еще имеет значение false. После этого программа
пытается прочитать другой символ; input.eof() теперь становится истиной.
Однако посторонний символ мусора уже отправлен в выходной файл.
Правильный код в листинге 8.7 читает символ (строка 29) и проверяет
eof() (строка 30). Если eof() имеет значение true, символ не выводится; в
противном случае он копируется (строка 32). Этот процесс продолжается до
тех пор, пока eof() не вернет true.
9.5 fstream и режимы открытия файлов.
Ключевой момент. Вы можете использовать fstream для создания
файлового объекта как для ввода, так и для вывода.
В предыдущих разделах вы использовали ofstream для записи данных и
ifstream для чтения данных. Кроме того, вы можете использовать класс
fstream для создания потока ввода или вывода. Удобно использовать
fstream, если вашей программе нужно использовать один и тот же объект
потока для ввода и вывода. Чтобы открыть файл fstream, вы должны указать
режим открытия файла, чтобы сообщить C++, как этот файл будет
использоваться. Режимы файлов перечислены в таблице 8.1.
Таблица 8.1 Режимы файлов
Режим
Описание
ios::in
Открывает файл для ввода.
ios::out
Открывает файл для вывода.
ios::app
Добавляет весь вывод в конец файла.
ios::ate
Открывает файл для вывода. Если файл уже существует,
перейдите в конец файла. Данные могут быть записаны в
любом месте файла.
ios::truct
Удаляет содержимое файла, если файл уже существует.
(Это действие по умолчанию для ios: out.)
os::binary
Открывает файл для двоичного ввода и вывода.
Заметка
Некоторые из файловых режимов также могут использоваться с объектами ifstream и ofstream для
открытия файла. Например, вы можете использовать режим ios:app, чтобы открыть файл с объектом
ofstream, чтобы вы могли добавлять данные в файл. Однако для согласованности и простоты лучше
использовать файловые режимы с объектами fstream.
Заметка
Несколько режимов могут быть объединены с помощью | оператор. Это побитовый оператор ИЛИ.
Например, чтобы открыть выходной файл с именем city.txt для добавления данных, вы можете использовать
следующую инструкцию:
stream.open("city.txt", ios::out | ios::app);
В листинге 8.8 приведена программа, которая создает новый файл с
именем city.txt (строка 11) и записывает данные в файл. Затем программа
закрывает файл и снова открывает его для добавления новых данных (строка
19), а не перезаписывает его. Наконец, программа читает все данные из
файла.
Листинг 8.8 AppendFile.cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
fstream inout;
// Create a file
inout.open("city.txt", ios::out);
// Write cities
inout << "Dallas" << " "<<"Houston"<<" "<< "Atlanta" << " ";
inout.close();
// Append to the file
inout.open("city.txt", ios::out | ios::app);
// Write cities
inout << "Savannah" << " " << "Austin" << " " << "Chicago";
inout.close();
string city;
// Open the file
inout.open("city.txt", ios::in);
while (!inout.eof()) // Continue if not end of file
{
inout >> city;
cout << city << " ";
}
inout.close();
return 0;
}
Программа создает объект fstream в строке 8 и открывает файл city.txt
для вывода в режиме файла ios::out в строке 11. После записи данных в
строке 14 программа закрывает поток в строке 16.
Программа использует тот же объект потока для повторного открытия
текстового файла с комбинированными режимами ios :: out | ios :: app в
строке 19. Затем программа добавляет новые данные в конец файла в строке
22 и закрывает поток в строке 24.
Наконец, программа использует тот же объект потока для повторного
открытия текстового файла с режимом ввода ios :: in в строке 29. Затем
программа считывает все данные из файла (строки 30–34).
9.6 Тестирование потоковых состояний.
Ключевой момент. Функции eof(), fail(), good() и bad() могут
использоваться для проверки состояний потоковых операций.
Вы использовали функцию eof() и fail() для проверки состояний потока.
C++ предоставляет еще несколько функций в потоке для тестирования
потоковых состояний. Каждый объект потока содержит набор битов,
которые действуют как флаги. Эти битовые значения (0 или 1) указывают на
состояние потока. В таблице 8.2 перечислены эти биты.
Таблица 8.2 Режимы файлов
Бит
Описание
ios::eofbit
Устанавливается
потока.
при
достижении
конца
входного
ios::failbit
Установить, когда операция не удалась.
ios::hardfail
Установите, когда произошла неисправимая ошибка.
ios::badbit
Устанавливается, когда была предпринята попытка
неверной операции.
ios::goodbit
Устанавливается, если ни один из предыдущих битов не
установлен.
Состояния операций ввода / вывода представлены в этих битах. Прямой
доступ к этим битам не удобен. C++ предоставляет функции-члены в объекте
потока ввода-вывода для их проверки. Эти функции перечислены в таблице
8.3.
Таблица 8.3 Функции состояния потока
Функция
Описание
eof()
Возвращает true, если установлен флаг eofbit.
fail()
Возвращает true, если установлен флаг failbit или
hardfail.
bad()
Возвращает true, если badbit установлен.
good()
Возвращает true, если задан goodbit.
clear()
Сбрасывает все флаги.
В листинге 8.9 приведен пример обнаружения состояний потока.
Листинг 8.9 ShowStreamState.cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
void showState(const fstream&);
int main()
{
fstream inout;
// Create an output file
inout.open("temp.txt", ios::out);
inout << "Dallas";
cout << "Normal operation (no errors)" << endl;
showState(inout);
inout.close();
// Create an input file
inout.open("temp.txt", ios::in);
// Read a string
string city;
inout >> city;
cout << "End of file (no errors)" << endl;
showState(inout);
inout.close();
// Attempt to read after file closed
inout >> city;
cout << "Bad operation (errors)" << endl;
showState(inout);
return 0;
}
void
{
cout
cout
cout
cout
cout
}
showState(const fstream& stream)
<<
<<
<<
<<
<<
"Stream status: " << endl;
" eof(): " << stream.eof() << endl;
" fail(): " << stream.fail() << endl;
" bad(): " << stream.bad() << endl;
" good(): " << stream.good() << endl;
Программа создает объект fstream, используя его конструктор без
аргументов в строке 10, открывает файл temp.txt для вывода в строке 13 и
записывает строку Dallas в строку 14. Состояние потока отображается в
строке 15. Нет ошибок. до сих пор.
Затем программа закрывает поток в строке 17, снова открывает файл
temp.txt для ввода в строке 20 и считывает строку Dallas в строке 24.
Состояние потока отображается в строке 26. Пока нет ошибок, но конец
файла достигнута.
Наконец, программа закрывает поток в строке 28 и пытается прочитать
данные после закрытия файла в строке 31, что вызывает ошибку. Состояние
потока отображается в строке 33.
При вызове функции showState в строках 16, 26 и 33 объект потока
передается в функцию по ссылке.
9.7 Бинарный ввод / вывод
Ключевой момент. Режим ios::binary можно использовать для
открытия файла для двоичного ввода и вывода.
До сих пор вы использовали текстовые файлы. Файлы могут быть
классифицированы на текстовые и бинарные. Файл, который можно
обработать (прочитать, создать или изменить) с помощью текстового
редактора, такого как Блокнот в Windows или vi в UNIX, называется
текстовым файлом. Все остальные файлы называются двоичными
(бинарными) файлами. Вы не можете читать двоичные файлы с помощью
текстового редактора. Они предназначены для чтения программами.
Например, исходные программы C++ хранятся в текстовых файлах и могут
быть прочитаны текстовым редактором, но исполняемые файлы C++
хранятся в двоичных файлах и читаются операционной системой.
Хотя это не является технически точным и правильным, вы можете
представить текстовый файл как состоящий из последовательности символов
и двоичный файл как состоящий из последовательности битов. Например,
десятичное целое число 199 сохраняется в виде последовательности из трех
символов: ‘1’, ‘9’, ‘9’ в текстовом файле, и такое же целое число сохраняется
в виде целого числа C7 в двоичном файле, поскольку десятичное число 199
равно шестнадцатеричной C7 (199 = 12 * 161 + 7). Преимущество двоичных
файлов заключается в том, что они более эффективны в обработке, чем
текстовые файлы.
Заметка
Компьютеры не различают двоичные файлы и текстовые файлы. Все файлы хранятся в двоичном
формате, и, таким образом, все файлы по сути являются двоичными файлами. Текстовый ввод / вывод
построен на двоичном вводе / выводе, чтобы обеспечить уровень абстракции для кодирования и
декодирования символов.
Двоичный ввод / вывод не требует преобразований. Если вы
записываете числовое значение в файл, используя двоичный ввод / вывод,
точное значение в памяти копируется в файл. Чтобы выполнить бинарный
ввод / вывод в C++, вы должны открыть файл, используя бинарный режим
ios :: binary. По умолчанию файл открывается в текстовом режиме.
Вы использовали оператор << и функцию put для записи данных в
текстовый файл, а операторы >>, get и getline - для чтения данных из
текстового файла. Для чтения / записи данных из / в двоичный файл вы
должны использовать функции чтения и записи в потоке.
Функция записи
Синтаксис для функции записи
streamObject.write(const char* s, int size)
который записывает массив байтов в типе char*. Каждый символ
является байтом.
В листинге 8.10 показан пример использования функции записи.
Листинг 8.10 BinaryCharOutput.cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
fstream binaryio("city.dat", ios::out | ios::binary);
string s = "Atlanta";
binaryio.write(s.c_str(), s.size()); // Write s to file
binaryio.close();
cout << "Done" << endl;
return 0;
}
Строка 8 открывает двоичный файл city.dat для вывода. Вызов
binaryio.write (s.c_str (), s.size ()) (строка 10) записывает строку s в файл.
Часто вам нужно написать данные, отличные от символов. Как вы
можете сделать это? Вы можете использовать оператор reinterpret_cast.
Оператор reinterpret_cast может привести любой тип указателя к другому
типу указателя несвязанных классов. Он просто выполняет двоичное
копирование значения из одного типа в другой без изменения данных.
Синтаксис использования оператора reinterpret_cast следующий:
reinterpret_cast<dataType*>(address)
Здесь address - это начальный адрес данных (примитива, массива или
объекта), а dataType - это тип данных, к которому вы приводите данные. В
этом случае для двоичного ввода / вывода это char*.
Например, см. код в листинге 8.11.
Листинг 8.11 BinaryCharOutput.cpp
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
fstream binaryio("temp.dat", ios::out | ios::binary);
int value = 199;
binaryio.write(reinterpret_cast<char*>(&value),sizeof(value));
binaryio.close();
cout << "Done" << endl;
return 0;
}
Строка 9 записывает содержимое переменной в файл. reinterpret_cast
<char*> (& value) приводит адрес типа int к типу char*. sizeof(value)
возвращает размер хранилища для переменной value, которая равна 4,
поскольку это переменная типа int.
Заметка
Для согласованности эта книга использует расширение .txt для именования текстовых файлов и .dat
для именования двоичных файлов.
Функция чтения
Синтаксис для функции чтения
streamObject.read(char* address, int size)
Параметр size указывает максимальное количество прочитанных
байтов. Фактическое количество прочитанных байтов можно получить из
функции-члена gcount.
Предположим, файл city.dat был создан в листинге 8.10. Листинг 8.12
читает байты с использованием функции read.
Листинг 8.12 BinaryCharInput.cpp
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
fstream binaryio("city.dat", ios::in | ios::binary);
char s[10]; // Array of 10 bytes. Each character is a byte.
binaryio.read(s, 10);
cout << "Number of chars read: "<<binaryio.gcount() << endl;
s[binaryio.gcount()] = '\0'; // Append a C-string terminator
cout << s << endl;
binaryio.close();
return 0;
}
Строка 7 открывает двоичный файл city.dat для ввода. Вызов
binaryio.read(s, 10) (строка 9) считывает до 10 байтов из файла в массив.
Фактическое количество прочитанных байтов можно определить, вызвав
binaryio.gcount() (строка 11).
Предположим, что файл temp.dat был создан в листинге 8.11. Листинг
8.13 читает целое число, используя функцию read.
Листинг 8.13 BinaryIntInput.cpp
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
fstream binaryio("temp.dat", ios::in | ios::binary);
int value;
binaryio.read(reinterpret_cast<char*>(&value), sizeof(value));
cout << value << endl;
binaryio.close();
return 0;
}
Данные в файле temp.dat были созданы в листинге 8.11. Данные состоят
из целого числа и перед сохранением преобразуются в байты. Эта программа
сначала считывает данные в байтах, а затем использует оператор
reinterpret_cast для преобразования байтов в значение типа int (строка 9).
Пример: бинарный массив ввода / вывода
Вы можете использовать оператор reinterpret_cast для приведения
данных любого типа в байты и наоборот. В этом разделе приведен пример,
приведенный в листинге 8.14, для записи массива двойных значений в
двоичный файл и считывания его из файла.
Листинг 8.14 BinaryArrayIO.cpp
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
const int SIZE = 5; // Array size
fstream binaryio; // Create stream object
// Write array to the file
binaryio.open("array.dat", ios::out | ios::binary);
double array[SIZE] = {3.4, 1.3, 2.5, 5.66, 6.9};
binaryio.write(reinterpret_cast<char*>(&array), sizeof(array));
binaryio.close();
// Read array from the file
binaryio.open("array.dat", ios::in | ios::binary);
double result[SIZE];
binaryio.read(reinterpret_cast<char*>(&result), sizeof(result));
binaryio.close();
// Display array
for (int i = 0; i < SIZE; i++)
cout << result[i] << " ";
return 0;
}
Программа создает объект потока в строке 9, открывает файл array.dat
для двоичного вывода в строке 12, записывает массив двойных значений в
файл в строке 14 и закрывает файл в строке 15.
Затем программа открывает файл array.dat для двоичного ввода в
строке 18, считывает массив двойных значений из файла в строке 20 и
закрывает файл в строке 21.
Наконец, программа отображает содержимое в результате массива
(строки 24–25).
Пример: бинарный объект ввода / вывода
В этом разделе приведен пример записи объектов в двоичный файл и
чтения объектов обратно из файла.
Листинг 8.1 записывает студенческие записи в текстовый файл.
Студенческая запись состоит из имени, отчества, фамилии и оценки. Эти
поля записываются в файл отдельно. Лучший способ обработки - определить
класс для моделирования записей. Каждая запись является объектом класса
Student.
Пусть класс будет называться Student с полями данных firstName, mi,
lastName и score, их вспомогательными аксессорами и мутаторами и двумя
конструкторами. Диаграмма класса UML показана на рисунке 8.4.
Функции get и set для этих полей данных
представлены в классе, но для краткости на
диаграмме UML опущены.
Имя студента
Средний инициал студента
Фамилия студетна
Оценка студента
Конструктор объекта Student по умолчанию
Конструктор с указанием имени, инициала, фамилии
и оценки студента
Рис. 8.4. класс Student описывает студенческую информацию.
В листинге 8.15 определяется класс Student в заголовочном файле, а в
листинге 8.16 - класс. Обратите внимание, что имя и фамилия хранятся в двух
массивах символов с фиксированной длиной 25 внутри (строки 22, 24), так
что каждая запись студента будет иметь одинаковый размер. Это необходимо
для обеспечения правильного чтения студентов из файла. Поскольку
использовать строковый тип проще, чем C-string, строковый тип
используется в функциях get и set для firstName и lastName (строки 12, 14,
16, 18).
Листинг 8.15 Student.h
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
const int SIZE = 5; // Array size
fstream binaryio; // Create stream object
// Write array to the file
binaryio.open("array.dat", ios::out | ios::binary);
double array[SIZE] = {3.4, 1.3, 2.5, 5.66, 6.9};
binaryio.write(reinterpret_cast<char*>(&array), sizeof(array));
binaryio.close();
// Read array from the file
binaryio.open("array.dat", ios::in | ios::binary);
double result[SIZE];
binaryio.read(reinterpret_cast<char*>(&result), sizeof(result));
binaryio.close();
// Display array
for (int i = 0; i < SIZE; i++)
cout << result[i] << " ";
return 0;
}
Листинг 8.16 Student.cpp
#include "Student.h"
#include <cstring>
// Construct a default student
Student::Student()
{
}
// Construct a Student object with specified data
Student::Student(const string& firstName, char mi,
const string& lastName, int score)
{
setFirstName(firstName);
setMi(mi);
setLastName(lastName);
setScore(score);
}
void Student::setFirstName(const string& s)
{
strcpy(firstName, s.c_str());
}
void Student::setMi(char mi)
{
this->mi = mi;
}
void Student::setLastName(const string& s)
{
strcpy(lastName, s.c_str());
}
void Student::setScore(int score)
{
this->score = score;
}
string Student::getFirstName() const
{
return string(firstName);
}
char Student::getMi() const
{
return mi;
}
string Student::getLastName() const
{
return string(lastName);
}
int Student::getScore() const
{
return score;
}
В листинге 8.17 приведена программа, которая создает четыре объекта
Student, записывает их в файл student.dat и считывает их обратно из файла.
Листинг 8.17 BinaryObjectIO.cpp
#include <iostream>
#include <fstream>
#include "Student.h"
using namespace std;
void
{
cout
cout
cout
cout
}
displayStudent(const Student& student)
<<
<<
<<
<<
student.getFirstName() << " ";
student.getMi() << " ";
student.getLastName() << " ";
student.getScore() << endl;
int main()
{
fstream binaryio; // Create stream object
binaryio.open("student.dat", ios::out | ios::binary);
Student
Student
Student
Student
student1("John", 'T', "Smith", 90);
student2("Eric", 'K', "Jones", 85);
student3("Susan", 'T', "King", 67);
student4("Kim", 'K', "Peterson", 95);
binaryio.write(reinterpret_cast<char*>
(&student1), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student2), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student3), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student4), sizeof(Student));
binaryio.close();
// Read student back from the file
binaryio.open("student.dat", ios::in | ios::binary);
Student studentNew;
binaryio.read(reinterpret_cast<char*>
(&studentNew), sizeof(Student));
displayStudent(studentNew);
binaryio.read(reinterpret_cast<char*>
(&studentNew), sizeof(Student));
displayStudent(studentNew);
binaryio.close();
return 0;
}
Программа создает объект потока в строке 16, открывает файл
student.dat для двоичного вывода в строке 17, создает четыре объекта
Student в строках 19–22, записывает их в файл в строках 24–31 и закрывает
файл в строке 33.
Оператор для записи объекта в файл
binaryio.write(reinterpret_cast<char*>
(&student1), sizeof(Student));
Адрес объекта student1 приведен к типу char*. Размер объекта
определяется полями данных в объекте. Каждый студент имеет одинаковый
размер, который является sizeof (Student).
Программа открывает файл student.dat для двоичного ввода в строке 36,
создает объект Student с помощью конструктора no-arg в строке 38,
считывает объект Student из файла в строках 40–41 и отображает данные
объекта в строке 43 Программа продолжает читать другой объект (строки 45–
46) и отображает свои данные в строке 48.
Наконец, программа закрывает файл в строке 50.
9.8 Файл с произвольным доступом
Ключевой момент. Функции seekg() и seekp() могут использоваться для
перемещения файлового указателя в любую позицию в файле произвольного
доступа для ввода и вывода.
Файл состоит из последовательности байтов. Специальный маркер,
называемый указателем файла, располагается на одном из этих байтов.
Операция чтения или записи выполняется в месте расположения указателя
файла. Когда файл открывается, указатель файла устанавливается в начале
файла. Когда вы читаете или записываете данные в файл, указатель файла
перемещается вперед к следующему элементу данных. Например, если вы
читаете байт с помощью функции get(), C++ читает один байт из указателя
файла, и теперь указатель файла на 1 байт опережает предыдущее
местоположение, как показано на рисунке 8.5.
Рис. 8.5. После считывания байта указатель файла перемещается на
один байт вперед.
Все программы, которые вы разработали, до сих пор
считывают/записывают данные последовательно. Это называется файлом с
последовательным доступом. То есть указатель файла всегда движется
вперед. Если файл открыт для ввода, он начинает читать данные от начала до
конца. Если файл открыт для вывода, он записывает данные один за другим
с начала или с конца (в режиме добавления ios :: app).
Проблема с последовательным доступом заключается в том, что для
чтения байта в определенном месте должны быть прочитаны все байты,
предшествующие ему. Это не эффективно. C++ позволяет указателю файла
свободно перемещаться назад или вперед, используя функции-члены seekp и
seekg для объекта потока. Эта возможность называется файлом
произвольного доступа.
Функция seekp («seek put») предназначена для выходного потока, а
функция seekg («seek get») предназначена для входного потока. Каждая
функция имеет две версии с одним или двумя аргументами. С одним
аргументом аргумент является абсолютным местоположением. Например,
input.seekg(0);
output.seekp(0);
перемещает указатель файла в начало файла.
С двумя аргументами первый аргумент является длинным целым
числом, которое указывает смещение, а второй аргумент, известный как база
поиска, определяет, откуда рассчитать смещение. В таблице 8.4 перечислены
три возможных аргумента поиска базы.
Таблица 8.4 Искать базу
Искать базу
Описание
ios::beg
Вычисляет смещение от начала файла
ios::end
Вычисляет смещение от конца файла.
ios::cur
Вычисляет смещение от текущего указателя файла.
В таблице 8.5 приведены некоторые примеры использования функций
seekp и seekg.
Таблица 8.5 Примеры seekp и seekg
Строка кода
seekg(100, ios::beg);
Описание
Перемещает указатель файла на 100-й байт от
начала файла.
seekg(-100, ios::end); Перемещает указатель файла на 100-й байт
назад от конца файла.
seekp(42, ios::cur);
Перемещает указатель файла на 42 байта
вперед от текущего указателя файла.
seekp(-42, ios::cur);
Перемещает указатель файла на 42-й байт
назад от текущего указателя файла.
seekp(100);
Перемещает указатель файла на сотый байт в
файле.
Вы также можете использовать функции Tellp и Tellg для возврата
позиции указателя файла в файле.
В листинге 8.18 показано, как получить произвольный доступ к файлу.
Программа сначала сохраняет 10 объектов ученика в файл, а затем извлекает
третьего ученика из файла.
Листинг 8.18 RandomAccessFile.cpp
#include <iostream>
#include <fstream>
#include "Student.h"
using namespace std;
void
{
cout
cout
cout
cout
}
displayStudent(const Student& student)
<<
<<
<<
<<
student.getFirstName() << " ";
student.getMi() << " ";
student.getLastName() << " ";
student.getScore() << endl;
int main()
{
fstream binaryio; // Create stream object
binaryio.open("student.dat", ios::out | ios::binary);
Student
Student
Student
Student
Student
Student
Student
Student
Student
Student
student1("FirstName1", 'A', "LastName1", 10);
student2("FirstName2", 'B', "LastName2", 20);
student3("FirstName3", 'C', "LastName3", 30);
student4("FirstName4", 'D', "LastName4", 40);
student5("FirstName5", 'E', "LastName5", 50);
student6("FirstName6", 'F', "LastName6", 60);
student7("FirstName7", 'G', "LastName7", 70);
student8("FirstName8", 'H', "LastName8", 80);
student9("FirstName9", 'I', "LastName9", 90);
student10("FirstName10", 'J', "LastName10", 100);
binaryio.write(reinterpret_cast<char*>
(&student1), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student2), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student3), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student4), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student5), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student6), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student7), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student8), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student9), sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student10), sizeof(Student));
binaryio.close();
// Read student back from the file
binaryio.open("student.dat", ios::in | ios::binary);
Student studentNew;
binaryio.seekg(2 * sizeof(Student));
cout << "Current position is " << binaryio.tellg() << endl;
binaryio.read(reinterpret_cast<char*>
(&studentNew), sizeof(Student));
displayStudent(studentNew);
cout << "Current position is " << binaryio.tellg() << endl;
binaryio.close();
return 0;
}
Программа создает объект потока в строке 16, открывает файл
student.dat для двоичного вывода в строке 17, создает десять объектов
Student в строках 19–28, записывает их в файл в строках 30–49 и закрывает
файл в строке 51.
Программа открывает файл student.dat для двоичного ввода в строке 54,
создает объект Student с использованием его конструкции без аргументов в
строке 56 и перемещает указатель файла на адрес третьего студента в файле
в строке 58. Текущий позиция теперь на 112. (Обратите внимание, что sizeof
(Student) равен 56.) После прочтения третьего объекта указатель файла
перемещается на четвертый объект. Итак, текущая позиция становится 168.
9.9 Обновление файлов
Ключевой момент. Вы можете обновить двоичный файл, открыв
файл, используя режим ios::in | ios:out | ios::binary.
Часто вам нужно обновить содержимое файла. Вы можете открыть файл
для ввода и вывода. Например,
binaryio.open("student.dat",ios::in|ios::out| ios::binary);
Этот оператор открывает двоичный файл student.dat для ввода и
вывода.
В листинге 8.19 показано, как обновить файл. Предположим, что файл
student.dat уже был создан с десятью объектами Student из Листинга 8.18.
Программа сначала читает второго ученика из файла, меняет фамилию,
записывает исправленный объект обратно в файл и считывает новый объект
обратно из файла.
Листинг 8.19 UpdateFile.cpp
#include <iostream>
#include <fstream>
#include "Student.h"
using namespace std;
void
{
cout
cout
cout
cout
}
displayStudent(const Student& student)
<<
<<
<<
<<
student.getFirstName() << " ";
student.getMi() << " ";
student.getLastName() << " ";
student.getScore() << endl;
int main()
{
fstream binaryio; // Create stream object
// Open file for input and output
binaryio.open("student.dat", ios::in | ios::out | ios::binary);
Student student1;
binaryio.seekg(sizeof(Student));
binaryio.read(reinterpret_cast<char*>
(&student1), sizeof(Student));
displayStudent(student1);
student1.setLastName("Yao");
binaryio.seekp(sizeof(Student));
binaryio.write(reinterpret_cast<char*>
(&student1), sizeof(Student));
Student student2;
binaryio.seekg(sizeof(Student));
binaryio.read(reinterpret_cast<char*>
(&student2), sizeof(Student));
displayStudent(student2);
binaryio.close();
return 0;
}
Программа создает объект потока в строке 16 и открывает файл
student.dat для двоичного ввода и вывода в строке 19.
Программа переходит ко второму ученику в файле (строка 22) и читает
студента (строки 23–24), отображает его (строка 25), меняет его фамилию
(строка 27) и записывает исправленный объект обратно в файл. (строки 29–
30).
Затем программа снова переходит ко второму студенту в файле (строка
33), читает его (строки 34–35) и отображает его (строка 36). Вы увидите, что
фамилия этого объекта была изменена в примере вывода.
Контрольные вопросы:
1. Как вы объявляете и открываете файл для вывода? Как вы объявляете
и открываете файл для ввода?
2. Почему вы всегда должны закрывать файл после его обработки?
3. Как вы определяете, существует ли файл?
4. Как определить, достигнут ли конец файла?
5. Следует ли передавать имя файла в виде string или C-String для
создания объекта потока ввода и вывода или для функции open?
6. В чем различия между функциями getline и get?
7. Какую функцию вы используете, чтобы написать символ?
8. Как открыть файл, чтобы вы могли добавить данные в файл?
9. Что такое режим открытия файлов ios :: trust?
10.Как вы определяете состояние операций ввода / вывода?
11.Что такое текстовый файл и что такое бинарный файл? Можете ли вы
просмотреть текстовый файл или двоичный файл с помощью
текстового редактора?
12.Как открыть файл для бинарного ввода / вывода?
13.Функция write может записывать только массив байтов. Как записать
значение типа примитива или объект в бинарный файл?
14.Если вы записываете строку «ABC» в текстовый файл ASCII, какие
значения хранятся в файле?
15.Если вы записываете строку «100» в текстовый файл ASCII, какие
значения хранятся в файле? Если вы запишите числовое значение
типа байтов 100, используя двоичный ввод / вывод, какие значения
будут храниться в файле?
16.Что такое указатель файла?
17.Каковы различия между seekp и seekg?
Download