0 Про идею файлов в ОС UNIX

0.1 Концепция: все есть файл

Интуитивное определение файла звучит примерно так. Файл -- именованная область на жестком диске. На самом деле с точки зрения ОС UNIX это совсем не так. В ОС UNIX файл -- очень удобная абстракция. С точки зрения UNIX файлом называется "что-нибудь", из чего можно считывать информацию или во что можно записывать информацию. Файлы это:


Файл -- это всё, что предназначено для ввода или вывода информации.
С этой точки зрения файлы бывают разными: принтер может только выводить информацию, а клавиатура -- только вводить. У такого рода файлов есть много особенностей. У файла на жестком диске есть понятие конца файла. Мы можем его считывать до тех пор, пока он не кончится. Тогда как у клавиатуры нет конца.


0.2 Разделение понятий файла и названия

Неправильно думать, что между сущностями "файл" и "название файла" есть взаимно однозначное соответствие.

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

С точки зрения UNIX:


Правильно говорить, что у названия есть файл. И наоборот: неправильно говорить, что у файла есть название. Никакого эффективного способа узнать имя файла не существует (но можно перебрать все файлы файловой системы).


0.3 Функции link и unlink

Пусть есть файл file1.txt. Для его удаления используется функция:

int unlink(const char* filename);

Эта функция не всегда удаляет файл (с жесткого диска), а только удаляет "этикетку" этого файла. Если есть другая "этикетка" этого файла, то файл останется на жестком диске; просто у него уже не будет этой "этикетки". Файл знает, сколько у него таких "этикеток" (есть специальный счетчик). И если этот счетчик стал равен нулю, то функция удаляет файл с жесткого диска. В ОС Windows эта функция всегда удаляет файл.

Парная функция к этой функции:

int link(const char* filename1, const char* filename2);

Эта функция создает еще одну "этикетку" для этого файла и прибавляет к значению счетчика "этикеток" единицу.

1 Ввод и вывод, язык C, структура FILE

1.1 Чтение и запись: printf и scanf

Всем хорошо известная функция printf:

printf("Hello!") -- печать текста на экран;
printf("N = %d", N) -- форматированный вывод на экран: вывести число N в десятичной записи;
printf("N = %x", N) -- форматированный вывод на экран: вывести число N в шестнадцатеричной записи;

Аналогично парная функция scanf:

scanf("%d", &N) -- считывание с клавиатуры значения переменной N в десятичной записи;

char *ptr = new char[10];
scanf("%s", ptr); -- считывание с клавиатуры строки в массив *ptr

Тут могут возникать различные проблемы.
  1. Проблема безопасности:

    char *ptr = new char[10];
    scanf("%s", ptr);

    Тут налицо потенциальная проблема переполнения буфера (в данном примере в буфере всего 10 байт).
    Никогда не следует пользоваться scanf'ом для чтения строк.

    scanf + "%s" -- запрещенная комбинация!
  2. Форматная строка не компилируется: она будет разбираться в момент исполнения программы. Это обозначает проблему быстродействия. scanf -- не предназначен для чтения большого количества информации.
    Аналогично printf -- тоже сравнительно медленный (однако существенно быстрее, чем scanf).
  3. Проблема безопасной работы со стеком:
    printf("%d %d", N);

    Проблема состоит в том, что форматная строка "%d %d" будет проанализирована в момент исполнения. В данном случае произойдет ошибка при работе со стеком: во время исполнения будет взят лишний int.
Перечисленные недостатки означают, что использование функций printf и scanf небезопасно и малоэффективно. Существенным плюсом этих функций является возможность простого форматированного ввода и вывода.


1.2 Чтение и запись файлов: FILE*, fopen, fprintf, fscanf

Есть несколько способов работы с файлами c использованием языков C и C++.

Самый распространенный связан со структурой FILE (это не класс, потому что сущность языка C). Эта структура определена в заголовочном файле стандартной библиотеки <stdio.h>. Размер этой структуры и ее поля зависят от ОС и от версии компилятора. Поэтому никто не пользуется структурой FILE. Обычно пользуются указателем на эту структуру: FILE*. Например:

FILE *f = fopen("file1.txt", "r");

fopen -- функция из стандартной библиотеки. Первый параметр -- имя файла (в текущем каталоге). Второй параметр задает режим открытия файла; в данном случае "r" означает, что файл будет открыт только для чтения. Эта функция возвращает ненулевой указатель, если открытие прошло успешно; и возвращает NULL, если произошла ошибка. Ошибка может возникать в следующих ситуациях:
  1. не существует файла;
  2. у программы недостаточно прав доступа для работы с файлом;


Для дальнейшей корректной работы следует писать примерно такой код:

if (f == NULL) {
  // файл не удалось открыть
}
else {
  // Работа с файлом
}

Допустим, что нам удалось открыть файл, т.е. f != NULL. Тогда для того, чтобы считывать файл, можно использовать функцию:

fscanf(f, "%s", ptr);

Эта функция работает аналогично функции scanf. Поэтому использовать эту функцию небезопасно! Все проблемы, перечисленные для scanf'а, имеют место и при работе с fscanf'ом.


Если мы хотим записать в файл что-то, то мы должны сначала открыть его на запись:

FILE *f = fopen("file2.html", "w");
Тут "w" означает, что мы открываем файл на запись (от write). Если файл не существовал, то он создастся и откроется на запись, а если он существовал, то он сначала будет уничтожен, а затем создан заново, и потом файл будет открыт на запись.
Еще один способ открыть файл -- это открыть его на дозапись. Это можно сделать с помощью параметра "a" (от append). Если файл не существовал, то он создастся и откроется на запись, а если он существовал, то он откроется на запись, и запись будет производится в конец файла.

Затем можно использовать функцию fprintf(f, ...)

1.2.1 Зачем нужно закрывать файлы

1.2.2 Важность буфера при работе с файлами

1.3 Стандартные уже открытые файлы: stdin, stdout, stderr

С точки зрения UNIX клавиатура и экран -- это файлы.

Есть три стандартные константы:
FILE *stdin
FILE *stdout
FILE *stderr

Это три стандартных заранее открытых файла.

stdin -- это стандартный файл (поток) ввода, а stdout -- стандартный файл (поток) вывода. Таким образом:
scanf(...) в точности эквивалентно fscanf(stdin, ...)
printf(...) в точности эквивалентно fprintf(stdout, ...)

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

//FILE *f = fopen(...);
FILE *f = stdin;

При этом код программы будет содержать такие функции: fscanf(f, ...) или fprintf(f, ...). А когда отладка законичится, просто снимаем/ставим соответствующие комментарии в двух строчках программы.

stderr -- это стандартный файл (поток) ошибок. По умолчанию выводит данные на экран.
Но существует заметное отличие этого "файла" от stdin и stdout: stderr -- небуферизованный файл (поток). Поэтому в этот файл (поток) все байты уходят без "задержки", которая могла бы возникнуть при буферизированном подходе. Понятно, что польза от этого подхода заключается в том, что вместо кода:

fprintf(stdout, ...);
fflush(stdout);

мы пишем:

fprintf(stderr, ...);

1.4 Текстовые и бинарные файлы; что меняет опция t/b

Рассмотрим строку:

fopen(f, "file1.txt", "w");

Почему второй параметр "w" является строкой, а не символом?
На самом деле бывает много способов прочитать/записать файл. Например:
fopen("file1.txt", "wt") -- откроет файл как текстовый файл;
fopen("file1.txt", "wb") -- откроет файл как бинарный файл.

Но в чем отличие?

Разница заключается лишь в том, что символы переноса строк запишутся по разному.
Рассмотрим пример в UNIX и Windows:

Исходная строка кода выглядит так: fprintf("Hello\n");

  1. Откроем в Windows файл на запись с параметром "wb" (как бинарный файл). Это означает, что в него запишется в точности то, что мы передали в функции fprintf. Тогда в файл запишутся ровно 6 байт: Hello\10

  2. А теперь мы откроем в Windows файл на запись с параметром "wt" (как текстовый файл). Тогда в файл запишутся ровно 7 байт: Hello\10\13
    Тут \10\13 означает симлов перевода строки в ОС Windows.

  3. Откроем в UNIX файл на запись с параметром "wt" или "wb". Тогда в файл запишутся ровно 6 байт: Hello\10
    Тут \10 означает симлов перевода строки в ОС UNIX.

    В ОС UNIX разницы все-таки нет.
Различие между "wt" и "wb" объясняется тем, что в разных операционных системах символы перевода строки разные. При чтении файла, т.е. при открытии файла с параметрами "rt" или "rb", проблема следующая. Если мы поставим параметр "rb", то при чтении файла символ \10 будет восприниматься как перевод строки. А если поставим параметр "rt", то при чтении файла пара символов \10\13 будет восприниматься как символ перевода строки.


1.5 Как же читать/писать на самом деле: fgets, fread и fwrite

Использование функций ptintf и scanf для записи и для чтения -- это очень плохая идея. Тогда все-таки как лучше читать и записывать?

Хороший способ чтения из файла дает функция fgets() (от "get string"):

char *fgets(char *buffer, size_t length, FILE *file);

Тут Эта функция делает примерно следующее. Она читает из файла file в буфер buffer не больше length-1 символов. Функция может прочитать не все length-1 символов в том случае, если она встретит конец строки, либо конец файла. Функция читает length-1 символ потому, что последний символ функция добавляет сама -- '\0'

Налицо быстрота и безопасность. Главное отличие от scanf'а заключается в том, что функция перестанет читать в тот момент, когда закончится буфер. Быстрота обусловлена тем, что функция scanf должна в момент выполнения разобрать форматную строку, в то время как fgets просто читает строку.

1.5.1 Как доставать числа? Семейство atoi, sscanf

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

В языке C есть семейство функций ~ atoi (a -- ASCII, i -- integer):

N = atoi(string);

Функция принимает единственный параметр строку и пытается ее привести в типу int. Надо заметить, что функция atoi безопасная, но не очень удобная. Безопасная в том смысле, что не сломается: atoi("25a") == 25. "Неудобства" заключаются в том, то если мы передаем в качестве параметра строку, в которой есть не только числа, нужно быть очень внимательным и знать, как работает эта функция. Функция atoi никак не проинформирует нас, если преобразование прошло неудачно.

Например, atoi("abc") == 0, что на самом деле не совсем соостветствует действительности. Использовать функцию atoi нужно лишь в том случае, когда вы уверены, что в строке есть число.

Родственные функции: atol, atoll, atof, strtol.

Им соответствуют функции для преобразования в типы long, long long и float.

Рассмотрим подробнее strtol:

long strtol(char *buffer, char **endPtr, int base);

Тут

Более мощное средство

Есть более мощное средство, чем нежели fgets + atoi. Речь идет о функции sscanf.
Вместо использования функции fscanf(f, "%d", &N) можно использовать связку:

fgets(ptr, 100, f);
sscanf(ptr, "%d", &N);

В чем преимущество и мощность такого подхода?

При таком подходе, осталась невысокая скорость работы, однако надежность есть.

1.5.2 fread и fwrite

На самом деле не все файлы выглядят как текст. Файле могут быть записаны числовые данные.

size_t fread(void *ptr, size_t size, size_t nelts, FILE *f);


Есть парная функция:

size_t fwrite(const void *ptr, size_t size, size_t nelts, FILE *F);

Аналогично fread эта функция возвращает количество элементов, которые удалось записать.
Тут параметр nelts просто показывает, сколько элементов надо вывести.


1.6 Другие полезные опции: fseek и ftell

Для файлов, которые открыты на чтение есть полезные функции. Одна из них это:

int fseek(FILE *f, long offset, int flag);


Еще одна полезная функция может определить текущее положение в файле (который открыт для чтения):

long int ftell(FILE *f);


2 Другие подходы для работы с файлами

2.1 File descriptors. Open, close, read, write

В языке C есть много способов работы с файлами. Помимо структуры FILE можно использовать так называемые дескрипторы файла (file descriptors). Дескриптор файла -- целое неотрицательное число. Оно обозначает номер открытого файла в таблице открытых файлов операционной системы. Использование дескрипторов файла -- более низкий уровень, чем нежели ипользование струкруты FILE. Структура FILE -- сущность языка C и его стандартной библиотеки, тогда как дескриптор файла -- сущность операционной системы. Например, при работе со структурой FILE автоматически создается буфер, и программист работает с более высокоуровневой абстракцией. А при работе с дескрипторами файла программист должен позаботится о буферизации вручную.

Пример работы с дескрипторами файла довольно прост и почти в точности повторяет процесс работы со структурой FILE:

int fd = open("...");

Сходство работы с дескрипторами файла с работой со структурой FILE заключается в том, что в названии функций отсутствует буква "f". Иногда параметры функций незначительно отличаются.

Структуру FILE полезно использовать при работе с настоящими "файлами" (которые находятся на жестком диске). Ипользовать дескрипторы файла полезно в случаях работы со специальными "файлами". В этом подходе есть своя специфика работы, но сейчас просто полезно знать, что такой подход существует.

Аналогами stdin, stdout и stderr в дескрипторах файла являются числа 0, 1 и 2 соответственно. Стандарт POSIX.1 обозначил числа 0, 1, 2 символическими константами STDIN_FILENO, STDOUT_FILENO и STDERR_FILENO соответственно.


2.2 Memory mapping. Что делает mmap

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

В языке C был придуман удобный способ работы в таких ситуациях, который называется memory mapping. Соответствующая функция:

char *ptr = mmap("...");

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

Можно "отобразить" не весь файл целиком, а, например, отдельную часть файла: с 3-его килобайта по 4-ый килобайт.


2.3 Win32 API: FileCreate, FileRead, etc.

При работе с файлами в ОС Windows можно использовать все те функции, которые были описаны выше. В ОС Windows есть своя большая стандартная библиотека Win32 API. В этой библиотеке также есть функции для работы с файлами: например, функции FileCreate(...) или FileOpen(...). Они по своей работе похожи на функции из стандартной библиотеки C, но отличия также присутствуют. Они заключаются в параметрах этих функций и небольших "хитростях", которые мы здесь опустим.

Если вы программируете под ОС Windows и пишите программу для работы в ОС Windows, то стоит пользоваться библиотекой Win32 API для работы с файлами.


3 Ввод и вывод в языке C++, потоки

В языке C++ объекты для работы с файлами называются потоками (streams). В данном случае слово "поток" означает то же самое, что и "файл" в языке C.

Классы для работы с файлами в языке C++ называются std::istream и std::ostream для ввода и вывода соответственно.

3.1 Глобальные переменные std::cout, std::cin, std::cerr

В header'е <iostream> объявлена глобальная переменная std::cout; она используется как стандартный поток вывода на экран. Эта переменная является объектом класса std::ostream.
В этом классе есть перегруженный оператор <<, который выводит на экран:

std::cout << 1;

В данном случае на экран будет выведена единица.

Аналогично можно выводить переменные: std::cout << N;   В данном случае переменная N (целое число) будет выведена на экран в естественной форме. Это можно переписать аналогично через printf():

printf("%d", N);

Еще в header'е <iostream> объявлены переменные std::cin и std::cerr для стандартного потока ввода и потока ошибок соответственно. Они являются объектами классов std::istream и std::ostream соотсветственно.

Аналогично тому, как stderr отличается от stdin, в языке C++ std::cerr отличается от std::cout отсутствием буферизации.

В классе std::istream есть перегруженный оператор >>. Можно считывать информацию из стандартного потока ввода (с клавиатуры).

3.2 Форматированный вывод возможен: std::ios::hex

Возможен ли форматированный вывод, которым мы пользовались в языке C фунцией printf()? Наример, как вывести ту же переменную N в 16-ой записи?

В языке C++ форматированный вывод возможен при помощи вывода на экран специальной управляющей команды:

std::cout << std::ios::hex << N;

В точности то же самое выведет команда printf("%x", N);

Чтобы не писать перед кажой переменной ее формат, можно использовать функцию:

std::cout.setf(std::ios::hex);

Она установит формат вывода в стандартный поток вывода на экран. Этот подход настолько же мощный, как и использование форматной печати с помощью printf.

3.3 Операторы << и >>

Давайте рассмотрим более подробно опрератор <<. В рассмотренном ранее случае синтаксис такой:

std::ostream &operator << (std::ostream &os, int N);
В общем случае вторым параметром может быть любой стандартный тип. Это связано с тем, что этот оператор был перегружен для всех стандартных типов языка C++.

В случае оператора >> все аналогично.

Если хочется написать свой оператор <<, то нужно переопределить этот оператор в своем классе.

Если у нас есть класс комплексных чисел Complex, то вывод этих чисел через оператор << надо написать всего один раз на все случаи (экран, файл, принтер ...)

3.4 Работа с файлами

Классами для работы с файлами в языке C++ являются ifstream, ofstream и fstream.

Код для открытия файла и его чтения выглядит примерно так:

ifstream ifs;
ifs.open("file1.txt");

  // далее с помощью оператора >> можно читать из файла, если он успешно открылся;

Аналогично можно использовать конструктор с параметром: ifs("file1.txt"); после чего создается объект и открывается по возможности файл.

В классе istream есть метод close(), который закрывает файл (на подобие работы с файлами в языке C). Однако вызывать этот метод необязательно. Дело в том, что в деструкторе класса этот метод вызовется автоматически.

Работа с объектами классов ofstream и ofstream и fstream осуществляется по аналогичному сценарию.

3.5 Класс stringstream

Класс stringstream наследуется от iostream.
Используется этот для класс для следующих целей. Если мы хотим выводить комплексные числа не только на экран или в файл, но и в окно какой-нибудь программы (GUI), то как использовать stringstream? При этом мы не хотим писать один и тот же код программы.

Можно просто печатать в строку с помощью stringstream.

3.6 Иерархия классов

4 Общий совет

Общий совет заключается в том, что не надо смешивать техники для работы с файлами.
Например, не надо в одной и той же программе использовать функции из стандартной библиотеки C (fread/fwrite) и классы-потоки из языка C++ (istream/ostream).