До сих пор мы использовали в программе данные, которые генерировались сходу. Но в большинстве программ (и конечно же в компьютерных играх) используются дополнительные данные (текст) и ресурсы (картинки). Эти дополнительные данные неудобно хранить в исходном коде программы - они хранятся в отдельных файлах. Именно для получения доступа к этим данным и используются возможности ввода/вывода языка C++.

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

В C++ есть два способа получить доступ к файлам: потоки (streams) и доступ к файлам унаследованный от C. Мы будем пользоваться только потоками. На мой взгляд они проще того способа, который использовался в C.

Прежде чем мы приступим к обсуждению ввода/вывода на языке C++ стоит разобрать две небольшие, но исключительно полезные функции.

Функции sizeof и memcpy

Мы уже использовали данные функции: и sizeof, и memcpy, но пока что нам не довелось рассмотреть их более подробно.

Функция sizeof (size of - размер чего-либо) принимает имя типа в качестве аргумента и возвращает его размер в байтах:

код на языке c++
sizeof(int);   // 4
sizeof(float); // 4
sizeof(short); // 2
sizeof(char);  // 1

Функцию sizeof необходимо использовать где только возможно - это очень полезная функция. Приведу небольшой примерчик:

код на языке c++
int simple_function(void* p, long size); // прототип

short var = 3;
simple_function(static_cast<void*>(&var),sizeof(short));

int a[5];
simple_function(static_cast<void*>(a),sizeof(a));

class warrior { /* определение класса*/ };
warrior* w1 = new warrior;
simple_function(static_cast<void*>(w1),sizeof(warrior));

Здесь мы объявляем функцию, которая принимает указатель на void и размер входных данных - size. Функция может принимать любые данные: простые переменные, массивы, пользовательские типы.

Сначала в функцию передаётся переменная типа short. Во-первых, так как функция принимает указатель, то мы передаём адрес var. Кроме того, мы используем оператор static_cast для явного приведения к типу void* типа short*. Второй аргумент - размер переменной var. Мы могли бы использовать константу 2. В данном случае никакой разницы нет: вычислять размер типа с помощью sizeof или использовать константное значение. Но если использовать функцию sizeof, то сразу становится понятно что именно значит этот аргумент.

Затем мы передаём массив. Функция sizeof умеет вычислять размер массива, для этого необходимо передать в функцию идентификатор массива.

В последнем случае мы передаём в функцию тип warrior. Размер типа warrior может быть как очень большим, так и очень маленьким - в зависимости от количества членов-данных этого класса. Функция sizeof без проблем сама вычисляет размер класса.

Важное замечание: даже если вам известен размер типа, всё равно воспользуйтесь функцией sizeof. Это сделае программу более читабельной.

Теперь разберёмся с функцией memcpy. Данная функция копирует данные из одного участка памяти в другой:

код на языке c++
void* memcpy ( void* dest,
               const void* src,
               size_t count);

dest (от destination - место назначения):
Указатель на буфер, в который будет осуществляться копирование данных.

src (от source - исходный):
Указатель на буфер, из которого будет осуществляться копирование данных.

count (количество):
Количество копируемых байт.

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

Думаю, объяснять больше ничего не надо. Посмотрим на пример.

код на языке c++
int b1 = 3;
int b2 = 4;
memcpy(&b1,&b2,4); // b1 = 4

int b3[5];
int b4[5];
memcpy(b3,b4,sizeof(b3));

Помните, что первые два аргумента memcpy - указатели.

Обратите внимание - здесь мы не используем static_cast. Функция memcpy, как и функция simple_function (пример выше), принимает указатель на void. В данном случае и для memcpy и для simple_function не обязательно использовать явное приведение типов (через static_cast), компилятор без проблем сам справится с этой задачей.

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

код на языке c++
int b1 = 3;
int b2 = 4;
memcpy(static_cast<void*>(&b1),static_cast<void*>(&b2),4); // b1 = 4

int b3[5];
int b4[5];
memcpy(static_cast<void*>(b3),static_cast<void*>(b4),sizeof(b1));

Классы ввода/вывода в C++. Потоки данных (Streams).

В C++ для ввода и вывода данных используются объекты потоковых классов.

Мы уже использовали потоковые объекты для ввода/вывода данных в консольных приложениях. Ключевые слова cout и cin - это обычные объекты. А операторы извлечения из потока >> и вставки в поток << - перегруженные операции классов этих объектов.

Система классов ввода/вывода довольна сложна. Базовым классом является класс ios (от Input/Output Stream - потоковый ввод/вывод). У класса ios довольно много производных классов. На данный момент нас интересует лишь несколько.

cin и cout - объекты классов istream (от Input Stream - поток ввода) и ostream (от Output Stream - поток вывода) соответственно. Именно для этих классов перегружены операторы извлечения и вставки.

Как я уже писал выше, нас интересует файловый ввод/вывод. Наследниками istream и ostream являются ifstream (от Input File Stream) и ofstream (от Output File Stream).

Кроме того, есть ещё один класс - fstream (от File Stream - фаловый поток), в котором объединены возможности ifstream и ofstream. fstream наследует одновременно и от istream, и от ostream.

На самом деле классов ввода/вывода больше и зависимости между ними немного сложнее чем я описал. Но на первое время нам будет достаточно такой упрощённой модели.

Текстовые (ASCII) и бинарные (двоичные) файлы

Если вы откроете любой текстовый файл, то вы сможете увидеть, сюрприз, текст! Как вы возможно помните, текст закодирован с помощью какой-нибудь кодировки - ASCII или Unicode, например. В памяти компьютера текст представлен всё теми же числами.

Например, если используется кодировка ASCII, то текстовый редактор прочитает следующую последовательность цифр как слово "Hello": 72 101 108 108 111.

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

При создании текстового файла и картинки используются два разных режима: текстовый (или просто ASCII) и бинарный (двоичный). В чём отличия? В текстовом режиме все данные хранятся в виде символьных строк (char). Рассмотрим пример, чтобы было понятней.

Допустим, у нас есть число 1492. Если мы будем сохранять это число в текстовый файл, то число будет представлено строкой символов.: 49 52 57 50. То есть число займёт в памяти четыре байта. В двоичном режиме это число займёт два байта (если использовать тип short), и сохранится в том же виде, в каком оно хранилось в памяти - 1492.

Пример, который я привёл выше - не совсем корректен (я его привёл для наглядности): и в текстовом и в бинарном режиме можно вводить любые данные. Единственное отличие - в текстовом режиме при вводе значения 10, перед ним автоматически будет добавляться значение 13. Дело в том, что в кодировке ASCII 10 - это символ новой строки. 13 - это возврат каретки (возвращение на первый символ строки). Почему оно работает именно так? Это так сказать дань традициям. Дело в том, что первый стандрат ASCII был принят в 1963 году. Тогда вместо клавиатур использовали электрические печатные машинки. В печатной машинке при окончании строки нужно вернуть каретку (печатающая головка) в начало строки, а затем перейти на новую строку. То есть эти две операции происходят раздельно. Вот и в стандарте ASCII это разделение сохранилось, хотя для современных компьютеров оно не имеет никакого смысла. В общем, почти всегда мы будем пользоваться бинарным режимом.

Создание текстового файла

Начнём с простого: запись в файл и последующее чтение из него. Нам понадобятся объекты классов ifstream и ofstream. Для использования возможностей работы с файлами, необходимо включить заголовочный файл fstream. Также нужно подключить заголовочный файл iostream (операторы вставки и извлечения из потоков). Для демонстрации потоков я создал консольный проект file. Код:

код на языке c++
ofstream os("text.txt");
os << "Hello";
os.close();

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

Затем потоковый объект используется точно также как и cout. Только вывод будет осуществляться не в консоль, а в файл text.txt.

Последний оператор - вызов метода close. Данный метод закрывает файл. Когда поток открывает файл, то этот файл блокируется, и никто не сможет получить к нему доступ. Для этого и необходим вызов метода close - файл закрывается и разблокируется.

Теперь прочитаем этот файл и выведем его содержимое на экран:

код на языке c++
char a[6];
ifstream is("text.txt");
is >> a;
cout << a;
is.close();

На экран будет выведено слово: Hello

is использует оператор извлечения из потока, точно также как и объект cin. Обратите внимание на размер массива a. Мы выделили на один байт больше количества букв в слове, так как в этот массив считается ещё и символ конца файла.

Найдите file.exe (так программа называется у меня) в папке решение/debug и запустите его. В консоли будет выведено слово, а в папке где находится file.exe будет создан файл text.txt.

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

Поставьте в коде несколько точек прерывания и в отладчике посмотрите содержимое переменных is и os во время выполнения программы.

Двоичный (бинарный) режим записи и чтения файлов

Конструкторы классов ifstream и ofstream могут принимать второй параметр - набор флагов. Одним из флагов указывается двоичный режим:

код на языке c++
ofstream os("text.txt", ios::binary);
ifstream is("text.txt", ios::binary);

Замечание: хотя здесь и создаётся файл с расширением txt, этот файл уже не является текстовым!

Для записи в файлы и чтения из файлов объектами is и os могут использоваться функции write (писать) и read (читать). Первый аргумент этих функций - адрес буфера (указатель на char), второй аргумент - количество байт в буфере. Прототипы read и write довольно сложны, но упрощённо они выглядят примерно так:

код на языке c++
void ofstream::write (char*,long);
void ifstream::read (char*,long);

Давайте обсудим первый параметр - укатель на тип char. Как мы знаем тип char представляет собой не столько символы, сколько байты. То есть данные в файлах хранятся в виде последовательности байтов. Поэтому когда мы вводим или выводим данные из файлов, мы должны преобразовывать байты, хранящиеся в файлах, в нужные нам типы. Поместим число типа int в файл:

код на языке c++
ofstream os("text.txt", ios::binary);
int a = 1492;
os.write(reinterpret_cast<char*>(&a),sizeof(int));

Оператор reinterpret_cast изменяет тип данных в памяти не обращая внимания, имеет это смысл или нет. Допустим этому оператору нужно привести тип int* к типу char*. Оператор reinterpret_cast ничего не будет делать с данными содержащимися в переменной типа int, он просто будет считать этот участок памяти (4 байта занятые переменой типа int) как массив из четырёх байт. Использовать reinterpret_cast нужно тольк в случаях, подобных этому: когда, например, необходимо привести тип int* к типу char* или совершить преобразования между пользовательскими типами.

Обратите внимание, что аргумент оператора reinterpret_cast - адрес переменной a. Мы передаём адрес, так как в методах write и read происходит приведение к типу char*.

Как вы конечно же помните, имя массива - это указатель. Передадим теперь в метод write массив переменных.

код на языке c++
ofstream os("text.txt", ios::binary);
int a[] = { 1492, 31562, 290893,382 };
os.write(reinterpret_cast<char*>(a),sizeof(a);

Здесь нам уже не нужно брать адрес a, так как имя массива - это и есть адрес.

А теперь код в котором происходит и чтение и запись:

код на языке c++
ofstream os("text.txt", ios::binary);
int a[] = { 1492, 31562, 290893,382 };
os.write(reinterpret_cast<char*>(a),sizeof(a));
os.close();

ifstream is("text.txt", ios::binary);
int b[4];
is.read(reinterpret_cast<char*>(b),sizeof(b));
is.close();

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

Пока что с вводом/выводом всё. В будущем мы ещё вернёмся к этой теме. Ну а на данный момент у нас достаточно знаний, чтобы рассмотреть конкретные примеры работы с файлами.