Как передать файловый поток в функцию

Обновлено: 05.07.2024

В статье на примерах объясняется, как реализовать поддержку потокового ввода-вывода из стандартной библиотеки (<iostream>) для своих классов.

В тексте статьи будет часто встречаться слово «поток», что означает именно поток ввода-вывода ((i/o)stream), но не поток выполнения (thread). Потоки выполнения в статье не рассматриваются.

Потоки из стандартной библиотеки — мощный инструмент. Аргументом функции можно указать поток, и это обеспечивает ее универсальность: она может работать как со стандартными файлами (fstream) и консолью (cin/cout), так и с сокетами и COM-портами, если найти соответствующую библиотеку.

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

Используемое окружение


При написании статьи для теста примеров использовался компилятор g++ (Ubuntu 5.4.0-6ubuntu1

16.04.4), а также стандарт c++11. Для наглядности я использовал из него ключевое слово override, чтобы пометить переопределяемые методы базового класса, однако если его убрать (а еще nullptr на NULL заменить), то должно собраться и на более старых стандартах.

Все примеры также доступны на github: streambuf_examples.

Каждый класс, поддерживающий потоковый ввод-вывод, наследует классы std::istream (ввод), std::ostream (вывод) или std::iostream (ввод и вывод). Именно они обеспечивают возможность использования перегруженных операторов '<<' и '>>', форматирования вывода, преобразование чисел в строки и наоборот и т.д.

Однако непосредственное чтение или запись данных происходят не в нем, а в классе, наследующем std::streambuf. Сам по себе streambuf является лишь интерфейсом с набором виртуальных функций, которые надо переопределить в классе-наследнике и уже в них реализовать свою логику чтения/записи данных (именно так и сделано в классах std::filebuf и std::stringbuf для fstream и stringstream соответственно).

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

При разработке собственных потоков наиболее сложной частью является реализация наследника std::streambuf. Производные классы от istream, ostream или iostream в простых случаях могут и вовсе отсутствовать.

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

    int overflow(int c) — вызывается при переполнении буфера. Аргументом является символ, который «не влез» в буфер.

Возвращаемое значение: в случае успеха, код записанного сивола, приведенный к типу int, иначе EOF.

Поведение по-умолчанию: всегда возвращает EOF.

Возвращаемое значение: в случае успеха, код считанного символа, приведенный к типу int, иначе EOF.

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

Возвращаемое значение: как в underflow.

Пример 1 — фильтруем цифры

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

Результат работы программы:


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

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

Как я уже ранее говорил, streambuf уже реализует в себе часть логики работы с буфером и предоставляет доступ к 6-и указателям, по 3 указателя на входной и выходной буферы. Однако streambuf не реализует выделение памяти под буферы. Эта задача возлагается на программиста вместе с инициализацией буферных указателей.

Для входного буфера указатели следующие:

  • eback()(end back pointer) — указатель на первый элемент буфера
  • gptr()(get pointer) — указатель на на элемент буфера, который будет считан следующим
  • egptr()(end get pointer) — указатель на элемент, следующий за последним элементом буфера. Когда gptr достигает его, это означает, что буфер исчерпан и его нужно снова заполнить


Также для управления указателями входного буфера служат следующие фукнции:

  • setg(eback, gptr, egptr) — устанавливает значения соответствующих указателей
  • gbump(offset) — сдвинуть указатель gptr на offset позиций. Фактически, после выполнения функции gptr примет значение gptr + offset
  • pbase()(put base pointer) — указатель на первый элемент буфера
  • pptr()(put pointer) — указатель на на элемент буфера, который будет записан следующим
  • epptr()(end put pointer) — указатель на элемент, следующий за последним элементом буфера.


Управляющие функции выходного буфера также схожи:

  • setp(pbase, epptr) — устанавливает значения соответствующих указателей. Обратите внимание, что у setp только два аргумента, в отличие от setg . При инициализации указателей выходного буфера pptr автоматически приравнивается pbase (т.е. устанавливается на начало буфера)
  • pbump(offset) — сдвинуть указатель pptr на offset позиций. Фактически, после выполнения функции pptr примет значение pptr + offset

Пример 2 — блочный вывод

В одном проекте мне понадобилось прозрачно делить поток на небольшие части, каждая из которых при выводе сопровождалась некоторым заголовком. Реализовал я это с помощью нового наследника streambuf . Мне показалось, что этот класс достаточно просто и понятно показывает простую работу с выходным буфером. Поэтому в следующем примере мы будем делить вывод на части и обрамлять каждую тегами <start> и <end> :

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

Для этого и служит еще одна виртуальная функция int sync(). Обычно она вызывается как раз по требованию программиста, однако в примере выше мы также вызываем ее сами при переполнении буфера. Возвращаемое ею значение говорит об успешной синхронизации (0) или неудачной (-1), при неудаче поток приобретает невалидное состояние. Реализация по-умолчанию ничего не делает и просто возвращает 0 (успех).

Кстати о переполнении буфера. В примере для упрощения реализации overflow() применен небольшой трюк: фактический размер буфера всегда на 1 элемент больше, чем «думает» streambuf. Это позволяет поместить переданный функции overflow «не влезший» символ в буфер и не усложнять код его специфичной обработкой.

Вывод программы для блоков размером в 10 символов следующий:

Пример 3 — буферизированный ввод из файла

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


Так как пример простой, он обладает рядом недостатков, основные из которых мы разберем далее.


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

seekoff и seekpos для перемещения по файлу

    streampos seekpos(streampos sp, openmode which) — вызывается при попытке перемещения в позицию, заданную абсоолютной величиной, т.е. позицией от начала последовательности.

Возвращаемое значение: в случае успеха, новая установленная позиция, иначе -1.

Поведение по-умолчанию: ничего не делает и возвращает -1.

Возвращаемое значение: в случае успеха, новая установленная абсолютная позиция, иначе -1.

    openmode — тип указателя, который необходимо передвинуть: ios_base::in (позиция чтения) и ios_base::out (позиция записи). Заметьте что аргумент является битовой маской: т.е. может содержать как одно из значений, так и сразу оба.


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

Выглядит довольно просто, но на самом деле всю сложность на себя берет функция fill_buffer_from . Ее реализация следующая:


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

Ну а мы идем далее.

pbackfail — возврат прочитанных символов назад

Бывают алгоритмы, которые не требуют свободного перемещения в произвольное место потока, однако в процессе чтения и обработки они могут попросить вернуть несколько символов (обычно 1-3) назад в поток. Для этого у istream предусмотрены методы unget() и putback(character) . В классе streambuf при совпадении возвращенного в поток символа с предыдущим в буфере никаких дополнительных вызовов не происходит. Однако если символы не совпали или указатель буфера в самом его начале, то вызывается функция, позволяющая обработать эту ситуацию:

    int pbackfail(int c) — вызывается при несовпадении возвращаемого в поток символа c и символа, находящегося в буфере на предыдущей позиции (или она не существует).

Возвращаемое значение: код возвращенного в поток символа, приведенный к типу int, в случае неудачи — EOF.


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

Пример 4 — чтение файла с позиционированием и возвратом символов

Здесь просто представлен код, в котором добавлены правки, реализованные в предыдущих разделах, а также примеры использования этого функционала, с пояснениями:

Другие возможности

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

Другие доступные для переопределения методы:

    imbue() — переопределение этой функции позволяет работать с различными локалями для преобразования читаемых и записываемых символов.

Работа файлового ввода/вывода в языке C++ почти аналогична работе обычных потоков ввода/вывода (но с небольшими нюансами).

Классы файлового ввода/вывода

Есть три основных класса файлового ввода/вывода в языке C++:

ofstream (является дочерним классу ostream);

fstream (является дочерним классу iostream ).

С помощью этих классов можно выполнить однонаправленный файловый ввод, однонаправленный файловый вывод и двунаправленный файловый ввод/вывод. Для их использования нужно всего лишь подключить заголовочный файл fstream.

В отличие от потоков cout, cin, cerr и clog, которые сразу же можно использовать, файловые потоки должны быть явно установлены программистом. То есть, чтобы открыть файл для чтения и/или записи, нужно создать объект соответствующего класса файлового ввода/вывода, указав имя файла в качестве параметра. Затем, с помощью оператора вставки ( << ) или оператора извлечения ( >> ), можно записывать данные в файл или считывать содержимое файла. После проделывания данных действий нужно закрыть файл — явно вызвать метод close() или просто позволить файловой переменной ввода/вывода выйти из области видимости (деструктор файлового класса ввода/вывода закроет этот файл автоматически вместо нас).

Файловый вывод

Для записи в файл используется класс ofstream . Например:

// Класс ofstream используется для записи данных в файл. // Если мы не можем открыть этот файл для записи данных, cerr << "Uh oh, SomeText.txt could not be opened for writing!" << endl ; // Когда outf выйдет из области видимости, то деструктор класса ofstream автоматически закроет наш файл

Обратите внимание, мы также можем использовать метод put() для записи одного символа в файл.

Файловый ввод

Теперь мы попытаемся прочитать содержимое файла, который создали в предыдущем примере. Обратите внимание, ifstream возвратит 0 , если мы достигли конца файла (это удобно для определения «длины» содержимого файла). Например:

// ifstream используется для чтения содержимого файла. // Если мы не можем открыть этот файл для чтения его содержимого, cerr << "Uh oh, SomeText.txt could not be opened for reading!" << endl ; // то перемещаем эти данные в строку, которую затем выводим на экран // Когда inf выйдет из области видимости, то деструктор класса ifstream автоматически закроет наш файл

Результат выполнения программы:

Хм, это не совсем то, что мы хотели. Как мы уже узнали на предыдущих уроках, оператор извлечения работает с «отформатированными данными», т.е. он игнорирует все пробелы, символы табуляции и символ новой строки. Чтобы прочитать всё содержимое как есть, без его разбивки на части (как в примере, приведенном выше), нам нужно использовать метод getline():

// ifstream используется для чтения содержимого файлов. // Мы попытаемся прочитать содержимое файла SomeText.txt // Если мы не можем открыть файл для чтения его содержимого, cerr << "Uh oh, SomeText.txt could not be opened for reading!" << endl ; // то перемещаем то, что можем прочитать, в строку, а затем выводим эту строку на экран // Когда inf выйдет из области видимости, то деструктор класса ifstream автоматически закроет наш файл

Результат выполнения программы:

Буферизованный вывод

Вывод в языке C++ может быть буферизован. Это означает, что всё, что выводится в файловый поток, не может сразу же быть записанным на диск (в конкретный файл). Это сделано, в первую очередь, по соображениям производительности. Когда данные буфера записываются на диск, то это называется очисткой буфера. Одним из способов очистки буфера является закрытие файла. В таком случае всё содержимое буфера будет перемещено на диск, а затем файл будет закрыт.

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

Также буфер можно очистить вручную, используя метод ostream::flush() или отправив std::flush в выходной поток. Любой из этих способов может быть полезен для обеспечения немедленной записи содержимого буфера на диск в случае сбоя программы.

Интересный нюанс: Поскольку std::endl; также очищает выходной поток, то его чрезмерное использование (приводящее к ненужным очисткам буфера) может повлиять на производительность программы (так как очистка буфера в некоторых случаях может быть затратной операцией). По этой причине программисты, которые заботятся о производительности своего кода, часто используют \n вместо std::endl для вставки символа новой строки в выходной поток, дабы избежать ненужной очистки буфера.

Режимы открытия файлов

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

app — открывает файл в режиме добавления;

ate — переходит в конец файла перед чтением/записью;

binary — открывает файл в бинарном режиме (вместо текстового режима);

in — открывает файл в режиме чтения (по умолчанию для ifstream );

out — открывает файл в режиме записи (по умолчанию для ofstream );

trunc — удаляет файл, если он уже существует.

Можно указать сразу несколько флагов путем использования побитового ИЛИ (|).

ifstream по умолчанию работает в режиме ios::in;

ofstream по умолчанию работает в режиме ios::out;

fstream по умолчанию работает в режиме ios::in ИЛИ ios::out, что означает, что вы можете выполнять как чтение содержимого файла, так и запись данных в файл.

Для вывода данных используеся оператор << . Этот опрератор определен для всех встроенных типов C++ и некоторых классов, входящих в стандартную библиотеку. Для вывода перевода строки можно использовать специальный объект endl .

Примеры использования потока вывода:

Также оператор << можно использовать и со своими классами, определив для них перегруженный оператор. Например, для структуры Book перегруженный оператор может выглядеть так:

Некоторые методы класса ostream:

  • put(char c) - записать символ с в поток
  • write(const char* s, streamsize n) - записать первые n элементов массива s в поток (streamsize представляет целое число со знаком, например, int)
  • flush() - записать значение из буфера
  • close() - закрытие потока

Потоки ввода (istream)

Для ввода используется оператор >> . Он также определен для всех встроенных типов и некоторых классов стандартной библиотеки.

Оператор >> также можно определить для своих классов:

Некоторые методы класса istream:

  • get() - считать следующий символ
  • get(char *buf, streamsize n) - считать максимум n-1 символ и поместить в массив buf
  • get(char *buf, streamsize n, char delim) - считывание символов до символа-разделителя delim (разделитель не считывается и остается в потоке)
  • getline(char *buf, streamsize n)
  • getline(char *buf, streamsize n, char delim)
  • peek() - считывает следующий символ, но оставляет его в потоке
  • ignore(streamsize n = 1, int delim = EOF) - извлекает символы из потока до тех пор, пока их число меньше n или пока не встретился символ delim
  • putback(char c) - добавляет символ с в текущую позицию потока
  • unget() - возвращает последний считанный символ в поток

Форматирование

Для управления форматом вывода можно устанавливать специальные флаги потока методом setf(ios_base::fmtflags f) . Но удобнее пользоваться манипуляторами - специальными функциями, реализованными в заголочных файлах <istream> , <ostream> (они по умолчанию включены в <iostream> ) и <iomanip> .

Основные манипуляторы ввода/вывода:

  • boolalpha - стороковое представление логических значений
  • noboolalpha - числовое представление логических значений
  • showbase - включает вывод 0 перед восьмеричными и 0x перед шестнадцатеричными числами
  • noshowbase - выключает вывод 0 и 0x
  • dec - вывод чисел в десятичной системе счисления
  • oct - в восьмеричной
  • hex - в шестнадцатеричной
  • uppercase - заглавные буквы в записи шестнадцатеричных чисел и чисел с плавающей запятой в научной записи
  • nouppercase - строчные буквы в записи чисел
  • skipws - пропуск символов-разделителей ( ' ', '\t', '\n', и т.п. )
  • noskipws - выключение пропуска разделителей
  • setw(int n) - определяет минимальное количество символов, которые выведутся следующей операцией вывода
  • setfill(char c) - символ-заполнитель
  • left - выравнивание поля по левому краю
  • right - выравнивание поля по правому краю
  • internal - выравнивание поля по ширине
  • scientific - научная запись для чисел с плавающей запятой
  • fixed - фиксированная точность для чисел с плавающей запятой
  • setprecision - точность вывода чисел (по умолчанию равна 6)
  • endl - запись \n и очистка буфера
  • ends - запись \0
  • flush - очистка буфера потока
  • ws - прочитать и проигнорировать символы-разделители

Сотояние потока

Каждый поток istream или ostream имеет связанное с ним состояние.

Методы проверки состояния:

  • good() - можно выполнить следующую операцию
  • eof() - конец потока
  • fail() - следующая операция не выполнится
  • bad() - поток испорчен

Стандартные потоки - iostream

Для реализации стандартного ввода/вывода в библиотеку C++ включен заголовочный файл iostream , содержащий следующие предопределенные объекты потоков:

  • cin - стандартный поток ввода (соответствует потоку C stdin)
  • cout - стандартный поток вывода (соответствует stdout)
  • cerr - стандартный поток вывода ошибок (соответствует stderr)
  • clog - стандартный поток вывода журнала (соответствует stderr)

Файловые потоки - fstream

Файловые потоки расположены в заголовочном файле <fstream> . ifstream - поток ввода, ofstream - поток вывода.

Имя файла передается потоку либо в конструкторе, либо через вызов метода open .

Под вводом-выводом в программировании понимается процесс обмена информацией между оперативной памятью и внешними устройствами: клавиатурой, дисплеем, магнитными накопителя­ми и т. п. Ввод — это занесение информации с внешних устройств в оперативную память, а вывод — вынос информации из опера­тивной памяти на внешние устройства. Такие устройства, как дис­плей и принтер, предназначены только для вывода; клавиатура — устройство ввода. Магнитные накопители (диски, ленты) исполь­зуются как для ввода, так и для вывода.

Основным понятием, связанным с информацией на внешних устройствах ЭВМ, является понятие файла. Всякая операция вво­да-вывода трактуется как операция обмена с файлами: ввод — это чтение из файла в оперативную память; вывод — запись инфор­мации из оперативной памяти в файл. Поэтому вопрос об органи­зации в языке программирования ввода-вывода сводится к вопро­су об организации работы с файлами.

Внутренний файл — это переменная файлового типа, являющаяся структурированной величиной. Элементы файловой переменной могут иметь разный тип и, со­ответственно, разную длину и форму внутреннего представле­ния. Внутренний файл связывается с внешним (физическим) файлом с помощью стандартной процедуры Assign. Один эле­мент файловой переменной становится отдельной записью во внешнем файле и может быть прочитан или записан с помощью одной команды. Попытка записать в файл или прочитать из него величину, не совпадающую по типу с типом элементов файла, приводит к ошибке.

Аналогом понятия внутреннего файла в языках Си/Си++ яв­ляется понятие потока. Поток — это байтовая последовательность, передаваемая в про­цессе ввода-вывода.

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

Основные отличия файлов в Си состоят в следующем: здесь отсутствует понятие типа файла и, следовательно, фиксирован­ной структуры записи файла. Любой файл рассматривается как байтовая последовательность:


Стрелочкой обозначен указатель файла, определяющий теку­щий байт файла. EOF является стандартной константой — призна­ком конца файла.

Существуют стандартные потоки и потоки, объявляемые в про­грамме. Последние обычно связываются с файлами на диске, со­здаваемыми программистом. Стандартные потоки назначаются и открываются системой автоматически. С началом работы любой программы открываются 5 стандартных потоков, из которых ос­новными являются следующие:

o stdin — поток стандартного ввода (обычно связан с клавиатурой);

o stdout — поток стандартного вывода (обычно связан с дисплеем);

Кроме этого, открывается поток для стандартной печати и до­полнительный поток для последовательного порта.

Работа с файлами на диске. Работа с дисковым файлом начи­нается с объявления указателя на поток. Формат такого объяв­ления:

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

Следующий шаг — открытие потока, которое производится с помощью стандартной функции fopen (). Эта функция возвраща­ет конкретное значение для указателя на поток и поэтому ее зна­чение присваивается объявленному ранее указателю. Соответству­ющий оператор имеет формат:

Имя_указателя=fореn (“имя_файла”, “режим_открытия”) ;

Параметры функции fopen () являются строками, которые мо­гут быть как константами, так и указателями на символьные мас­сивы. Например:

Здесь test .dat — это имя физического файла в текущем ката­логе диска, с которым теперь будет связан поток с указателем fр. Параметр режима r означает, что файл открыт для чтения. Что касается терминологии, то допустимо употреблять как выражение «открытие потока», так и выражение «открытие файла».

Существуют следующие режимы открытия потока и соответ­ствующие им параметры:

r открыть для чтения

w создать для записи

а открыть для добавления

r+ открыть для чтения и записи

w+ создать для чтения и записи

а+ открыть для добавления или

создать для чтения и записи

Поток может быть открыт либо для текстового, либо для дво­ичного (бинарного) режима обмена.

Понятие текстового файла: это последовательность символов, которая делится на строки специальными кодами — возврат ка­ретки (код 13) и перевод строки (код 10). Если файл открыт в текстовом режиме, то при чтении из такого файла комбинация символов «возврат каретки — перевод строки» преобразуется в один символ \n — переход к новой строке.

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

Указанные выше параметры режимов открывают текстовые файлы. Если требуется указать на двоичный файл, то к параметру добавляется буква b. Например: rb, или « b », или r +b. В некоторых компиляторах текстовый режим обмена обозначается буквой t, т.е. записывается a+t или rt.

Если при открытии потока по какой-либо причине возникла ошибка, то функция fopen() возвращает значение константы null. Эта константа также определена в файле stdio.h. Ошибка может возникнуть из-за отсутствия открываемого файла на диске, нехватки места в динамической памяти и т.п. Поэтому желатель­но контролировать правильность прохождения процедуры откры­тия файла. Рекомендуется следующий способ открытия:

if (fp=fopen("test.dat", "r")==NULL)

В случае ошибки программа завершит выполнение с закрыти­ем всех ранее открытых файлов.

Закрытие потока (файла) осуществляет функция fclose(), прототип которой имеет вид:

int fclose(FILE *fptr);

Здесь fptr обозначает формальное имя указателя на закрыва­емый поток. Функция возвращает ноль, если операция закрытия прошла успешно. Другая величина означает ошибку.

Запись и чтение символов. Запись символов в поток произво­дится функцией putc() с прототипом

int putc (int ch, FILE *fptr);

Если операция прошла успешно, то возвращается записанный символ. В случае ошибки возвращается константа EOF.

Считывание символа из потока, открытого для чтения, произ­водится функцией gets () с прототипом

int gets (FILE *fptr);

Функция возвращает значение считываемого из файла сим­вола. Если достигнут конец файла, то возвращается значение EOF. Заметим, что это происходит лишь в результате чтения кода EOF.

Исторически сложилось так, что gets() возвращает значение типа int. To же можно сказать и про аргумент ch в описании функции puts(). Используется же в обоих случаях только млад­ший байт. Поэтому обмен при обращении может происходить и с переменными типа char.

Пример 1. Составим программу записи в файл символьной пос­ледовательности, вводимой с клавиатуры. Пусть признаком завер­шения ввода будет символ *.

Читайте также: