Порядок выполнения лабораторной работы
1. Изучить теоретические сведения. Разобрать представленные примеры. Запустить первый, второй и пятый примеры, продемонстрировать их преподавателю, если примеры не были показаны на занятии, результаты их работы должны быть включены в отчет.
2. Получить вариант индивидуального задания у преподавателя. Предполается реализация двух задач: первая задача представлена в пункте лабораторное задание, вторая повторяется из предыдущей работы (вторая группа заданий), только теперь необходима реализация данной задачи при помощи двоичных файлов (объявить структуру и записывать/читать ее из двоичного файла). При необходимости перегрузить операторы << и >> для работы со стандартными потоковыми классами.
3. Реализовать полученные задания. Для выбора решения задачи пользователем реализовать меню. Любое общение с пользователем должно сопровождаться поясняющимся текстом. Также в программе должно быть реализовано минимум 2 исключительных ситуации. Исключительные ситуации должны обрабатываться в отдельном классе. Обязательна оконная (формочная) реализация программы с использованием элементов управления.
|
|
4. Показать результат работы программы преподавателю.
5. Защитить лабораторную работу.
Требования к отчету
Отчет должен содержать:
1. Цель работы.
2. Задание.
3. Словесное описание исключительных ситуаций.
4. UML диаграмма классов.
5. Блок-схема программы.
6. Текст программы на языке C++ с комментариями.
7. Тесты (копии выходных форм программы/сформированных файлов).
8. Выводы.
Теоретические сведения
Cохранение данных в двоичных файлах
Сохранение в двоичных файлах данных стандартных типов.
Для того, чтобы открыть двоичный файл, необходимо задать режим доступа ios::binary (в некоторых компиляторах С++ - ios::bin).
Двоичные файлы более компактны и в некоторых случаях более удобны для обработки.
Для создания выходного файла создают объект
ofstream out_fil (”Outfil.dat”,ios::out | ios::binary);
if (! out_fil) { cerr<<”Error: Outfil.dat”<<endl;
exit(1);
}
Для того, чтобы открыть существующий двоичный файл для чтения, нужно создать объект
ifstream in_fil (”Infil.dat”, ios::in | ios::binary);
if (! in_fil) { cerr<<”Error: Infil.dat”<<endl;
exit(2);
}
К сожалению, созданные объекты in_fil и out_fil не слишком приспособлены для работы с двоичными файлами и требуют некоторых дополнительных действий, необходимых для корректной работы.
Пример 1. Запись значения типа double в двоичный файл.
# include <fstream>
# include <iostream>
# include <stdlib.h>
using namespace std;
class bin_outstream:public ofstream
{
public:
bin_outstream(const char *fn): ofstream(fn, ios::out | ios::binary){}
|
|
void writeOurDate(const void*, int);
ofstream &operator<<(double d) {
writeOurDate(&d, sizeof(d));
return *this;
}
};
int main()
{
bin_outstream bin_out("B_out.dat");
if (!bin_out)
{
cerr << "Unable to write to B_out.dat" << endl;
system("pause");
exit(1);
}
double d = 5.252;
bin_out << d;
bin_out << d*d;
d = 5.2E-5;
bin_out << d;
system("pause");
return 0;
}
void bin_outstream::writeOurDate(const void *Ptr, int len)
{
if (!Ptr) return;
if (len <= 0) return;
write((char*)Ptr, len);
}
Пример 2. Чтение значений типа double из двоичного файла.
# include <fstream>
# include <iostream>
# include <stdlib.h>
using namespace std;
class bin_instream: public ifstream
{
public:
bin_instream(const char *fn): ifstream(fn, ios::in | ios::binary){}
void readOurDate(void*, int);
bin_instream &operator>>(double &d) {
readOurDate(&d, sizeof(d));
return *this;
}
};
int main()
{
bin_instream bin_in("B_in.dat");
if (!bin_in)
{
cerr << "Unable to open B_in.dat" << endl;
system("pause");
exit(1);
}
double d;
long count = 0;
bin_in >> d;
while (!bin_in.eof())
{
cout << ++count << ':' << d << endl;
bin_in >> d;
}
system("pause");
return 0;
}
void bin_instream::readOurDate(void *p, int len)
{
if (!p) return;
if (len <= 0) return;
read((char*)p, len);
}
Для работы с файловыми потоками любого из стандартных типов, нужно перегрузить операторы ввода и вывода под требуемый тип данных или воспользоваться шаблоном класса, задаваемым с помощью ключевого слова template.
Сохранение в двоичных файлах данных, имеющих тип, создаваемый пользователем.
Иногда возникает необходимость сохранить в файле данные, структура которых задается программистом. В этих случаях задают класс, содержащий данные и функции, перегруженные операторы ввода и вывода под эти данные.
Пример 3.
Объявим структуру
struct mountine {
char name[20]; //название горы
int altitude; //высота над уровнем моря
int complicate; //сложность
};
mountine mount;
Для сохранения информации в двоичном файле выполняют следующее:
ofstream fil_out(”mountines.txt”, ios_base::app);
fil_out << mount.name << ” ” << mount.altitude << ’ ’ << mount.complicate <<”\n”;
Для сохранения той же информации в двоичном файле выполняют следующее:
ofstrem fil_out(”mountines.dat”, ios_base::app | ios_base::bynary);
fil_out.write((char *) &mount, sizeof(mountine));
Метод write копирует указанное число байтов (в данном случае – sizeof(mountine)) в файл из памяти ЭВМ. Несмотря на то, что сохранение данных происходит в двоичном файле, адрес переменной преобразуется к указателю на тип char.
Для чтения данных из двоичного файла используют метод read:
ifstream fil_in(”mountines.dat”, ios_base::binary);
fil_in.read((char *) &mount, sizeof(mountine));
При записи или чтении классов, не содержащих виртуальных функций, можно использовать тот же самый подход. Чтобы сделать класс потоковым, нужно перегрузить операторы << и >>:
friend ostream &operator<<(ostream &, AnyClass &);
friend istream &operator>>(istream &, AnyClass &);
Пример 4. Чтение и запись структуры данных в файл.
//перечисляемый тип с оценками
enum mark {very_bad = 1, bad = 2, satisfactory = 3, good = 4, excellent = 5};
struct rec //структура данных
{ char name[10];
char subname[15];
int clas;
mark marks[4];
};
//открытие "типизированного" файла для записи
void WriteFile(int n, rec &wrec)
{ FILE * fo;
fo = fopen("type.txt","wb"); //открываем (создаем) файл для записи
if (fo == NULL)
{ cout << "Ошибка открытия файла для записи" << endl;
exit(EXIT_FAILURE);
}
for(int i = 0; i < n; i++)
{ int temp=0; //временная переменная для оценок
cout << "Введите имя ученика: "; cin >> wrec.name;
cout << "Введите фамилию ученика: "; cin >> wrec.subname;
cout << "Введите класс, где обучается ученик: "; cin >> wrec.clas;
cout << "Введите оценки ученика (4 штуки от 1 до 5)" << endl;
for(int j = 0; j < 4; j++)
{ cout << "Введите " << j+1 << " оценку: "; cin >> temp;
wrec.marks[j] = (mark)temp;
}
WriteElement(wrec, fo); //запись одной структуры в файл
|
|
cout << endl;
}
fclose(fo); //закрываем файл
cout << endl;
}
//запись структуры в файл
void WriteElement(rec &wrec, FILE * f1)
{
fwrite(&wrec,sizeof(rec),1,f1); //записываем одну структуру
}
//чтение структуры из файла
void ReadElement(rec & rrec, FILE * f1)
{
fread(&rrec, sizeof(rec), 1, f1); //читаем одну структуру
}
Произвольный доступ к элементам файлов
Файловый указатель.
Каждый файл имеет два связанных с ним значения: указатель чтения и указатель записи, по-другому называемые файловым указателем или текущей позицией.
При последовательном доступе к элементам файлов перемещение файлового указателя происходит автоматически. Но иногда бывает нужно контролировать его состояние. Для этого используются следующие функции:
- seekg() – установить текущий указатель чтения;
- tellg() – проверить текущий указатель чтения;
- seekp() – установить текущий указатель записи;
- tellp() – проверить текущий указатель записи.
Организация доступа к элементам двоичных файлов.
Благодаря наличию файлового указателя, в двоичных файлах допустим произвольный доступ к их элементам, который можно реализовать с помощью перегруженных функций – элементов, унаследованных из класса istream:
istream &seekg(streampos) или
istream &seekg(streamoff, ios::seek_dir);
Типы данных streampos и streamoff эквивалентны значениям типа long, но использовать long в явном виде не рекомендуется из-за неоднозначности работы различных компиляторов. Поэтому их определяют как
typedef long streampos;
typedef long streamoff;
Первая из перегруженных форм функции seekg позиционирует входной поток на заданном байте, вторая – на смещении относительно одной из трех позиций, определенных значением константы ios::seek_dir (табл. 1)
Таблица 1
Константа | Значение | Описание |
beg | 0 | поиск от начала файла |
cur | 1 | поиск от текущей позиции файла |
end | 2 | поиск от конца файла |
Для позиционирования внутреннего указателя файла для выходных потоков используют перегруженные функции выходных файловых потоков, унаследованных из класса ostream:
|
|
ostream &seekp(streampos);
ostream &seekp(streamoff, ios::seek_dir);
Пример 5. В двоичном файле, содержащем целые числа, заменить максимальное значение файла суммой его четных элементов.
#include <fstream>
#include <iostream>
using namespace std;
class bin_stream:public fstream {
public:
bin_stream(const char *fn): fstream(fn, ios::out | ios::in | ios::binary) {}
void doneOurDate(const void*, int, int);
bin_stream &operator<<(int d) {
doneOurDate(&d, sizeof(d), 0);
return *this;
}
bin_stream &operator>>(int &d) {
doneOurDate(&d, sizeof(d), 1);
return *this;
}
};
int main()
{
int i, d, max, i_max, sum_even = 0;
bin_stream bin_out("Bin.dat");
if (!bin_out) {
cerr << "Unable to write to Bin.dat" << endl;
system("pause");
exit(1);
}
for (i = 0; i < 10; i++) {
d = rand() % 100;
bin_out << d;
if (d % 2 == 0) sum_even += d;
}
bin_out.seekp(0, ios::beg);
bin_out >> max;
i_max = 0;
for (i = 1; i < 10; i++) {
bin_out >> d;
if (d > max) { max = d; i_max = i; }
}
bin_out.seekp(sizeof(int)* i_max, ios::beg);
bin_out << sum_even;
bin_out.seekp(0, ios::beg);
for (i = 0; i < 10; i++) {
bin_out >> d;
cout << d << ' ';
}
bin_out.close();
cout << endl;
system("pause");
return 0;
}
void bin_stream::doneOurDate(const void *Ptr, int len, int sign)
{
if (!Ptr)
return;
if (len <= 0)
return;
if (sign == 0)
write((char*)Ptr, len);
else
read((char*)Ptr, len);
}
Контрольные вопросы
- Что такое поток?
- Особенности работы с двоичными файлами.
- Что представляет собой файловый указатель?
- Как организовать доступ к произвольному месту двоичного файла?