Лекция 4. Ввод и вывод

1. «Классический» ввод/вывод

В языке С определены три стандартных потока ввода/вывода:

  1. stdin      – стандартное устройство ввода (клавиатура);
  2. stdout   – стандартное устройство вывода (экран);
  3. stderr    – стандартное устройство вывода сообщения об ошибках (также экран).

Простейший ввод/вывод:

Все эти функции требуют включения заголовочного файла <conio.h>.

Все остальные функции ввода/вывода определены в заголовочном файле <stdio.h>.

1.1. Ввод/вывод с экрана

Для ввода и вывода на экран используются функции scanf и printf соответственно, прототипы которых имеют следующий вид: int scanf (char *format, ...); int printf(char *format, ...);

Вывод осуществляется функцией printf, которая имеет следующий синтаксис: printf(<строка описания форматов> [, <список вывода>]);

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

Обычные символы и управляющие последовательности просто копируются в стандартный выходной поток в порядке их появления.

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

Ввод осуществляется функцией scanf, которая имеет следующий синтаксис: scanf(<строка описания форматов> [, <список ввода>]);

Строка описания форматов состоит из набора спецификаций формата, таких же, как для функции printf. Список ввода состоит из адресов переменных, куда будут заноситься вводимые значения. Адрес переменной вычисляется с помощью унарной операции &. Количество спецификаций формата должно быть равно количеству вводимых значений, которые указываются в списке ввода.

Функция scanf возвращает количество успешно введенных и преобразованных значений. Функция printf возвращает количество символов, записанных в выходной поток.

К управляющим последовательностям относятся следующие последовательности символов.

Последовательность Дейcтвие
\a Звуковой сигнал
\b Удаление предыдущего символа
\n Новая строка
\r Возврат каретки
\t Табуляция
\' Апостроф
\" Кавычки
\\ Обратный слеш
\ooo ASCII символ в восьмеричной нотации
\xooo ASCII символ в шестнадцатеричной нотации

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

%[флаги] [ширина] [.точность] [{h | l | L | I64}] тип

Флаги Значение По умолчанию
Выравнивание по левому краю. Выравнивание по правому краю.
+ Добавление знака + или – перед числами. Знак добавляется только перед отрицательными числами.
0 Добавление нулей перед выводимым значением. Если одновременно используются флаги – и 0, 0 игнорируется. Добавление пробелов.
пробел Добавление пробела перед положительным числом. Если одновременно используются флаги пробел и +, пробел игнорируется. Пробел не добавляется.
# Добавление символов 0, 0х или 0Х перед ненулевым значением, если флаг # используется с форматами о, х или Х соответственно. Символы 0, 0х и 0Х не добавляются.
При использовании с форматами e, E и f флага # выводимое число будет содержать десятичную точку в любом случае. Десятичная точка добавляется, только если за ней следуют цифры.
При использовании с форматами g и G флага # выводимое число будет содержать десятичную точку и хвостовые нули в любом случае. Десятичная точка добавляется, только если за ней следуют цифры. Хвостовые нули не выводятся.
Игнорируется при использовании с форматами c, d, i, u и s.  

Поле ширина содержит минимальное количество выводимых символов – неотрицательное целое число. Если выводимое значение содержит меньше символов, то оно расширяется пробелами (если не задан флаг 0). Если поле ширина содержит звездочку (*), то в качестве значения поля берётся целое число из списка аргументов, предшествующее выводимому значению.

Поле точность также представляет собой неотрицательное целое число. Действие зависит от типа выводимого значения.

Тип Действие По умолчанию
c, C Точность не имеет эффекта. Выводится символ.
d, i, u, o, x, X Точность задаёт минимальное количество символов, которые будут напечатаны. Если число содержит меньше символов, оно расширяется нулями. Точность равна 1.
e, E, f Точность задаёт количество символов после десятичной точки. Число округляется. Точность равна 6. Если точность равна 0 или опущена, десятичная точка не выводится.
g, G Точность задаёт максимальное количество значащих цифр. Печатается 6 значащих цифр.
s, S Точность задаёт максимальное количество выводимых символов. Выводятся все символы строки.

Если поле точность содержит звездочку (*), то в качестве значения поля берётся целое число из списка аргументов, предшествующее выводимому значению.

Дополнительные префиксы h, l, L и I64 задают «размер» аргумента – long или short, однобайтовый символ или расширенный символ, в зависимости от спецификации типа, которую они модифицируют.

Обязательное поле тип задаёт тип выводимого значения.

Символ Тип Формат вывода
c int или wint_t При использовании с функцией printf определяет однобайтовый символ, при использовании с функцией wprintf определяет расширенный символ.
C int или wint_t При использовании с функцией printf определяет расширенный символ, при использовании с функцией wprintf определяет однобайтовый символ.
d int Знаковое десятичное целое.
i int Знаковое десятичное целое.
o int Беззнаковое восьмеричное целое.
u int Беззнаковое десятичное целое.
x int Беззнаковое шестнадцатеричное целое с использованием символов «abcdef».
X int Беззнаковое шестнадцатеричное целое с использованием символов «ABCDEF».
e double Знаковое число в форме [ – ]d.dddd e [знак]ddd, где d есть одна десятичная цифра, dddd – одна или более десятичных цифр, ddd – три десятичные цифры and знак есть + или –.
E double Идентичен формату e, за исключением того, что символ E, а не e вводит экспоненту.
f double Знаковое число в форме [ – ]dddd.dddd, где dddd есть одна или более десятичных цифр. Количество цифр перед десятичной точкой зависит от величины числа, а количество цифр после десятичной точки – от требуемой точности.
g double Знаковое число в формате f или e, в зависимости от того, какой формат более компактен для заданного значения и точности.
G double Идентичен формату g, за исключением того, что символ E, а не e вводит экспоненту.
n pointer to integer Количество символов успешно записанных к данному моменту в выходной поток. Это значение сохраняется в целочисленной переменной, чей адрес задан как аргумент.
p pointer to void Печатает адрес, заданный аргументом.
s string При использовании с функцией printf задаёт строку однобайтовых символов, при использовании с функцией wprintf задаёт строку расширенных символов. Символы печатаются до достижения признака конца строки.
S string При использовании с функцией printf задаёт строку расширенных символов, при использовании с функцией wprintf задаёт строку однобайтовых символов. Символы печатаются до достижения признака конца строки.

int m, n, x; double y; char c = '&'; char str[] = "String";  
scanf("%d%d", &m, &n); // Ввод десятичных целых чисел в переменные m и n
printf("m = %5d\nn = %5d\n", m, n); // Вывод переменных m и n в десятичном целом формате, используются как минимум 5 знаков
scanf("%d", &x); // Ввод десятичного целого числа в переменную x
printf("%#010x\n", x); // Вывод переменной x в шестнадцатеричной системе, используются 10 знаков, // впереди добавляются нули и символы
scanf("%lf", &y); // Ввод вещественного числа в переменную y
printf("y = %7.2lf\n", y); // Вывод вещественной переменной, используются как минимум 7 знаков, из них 2 – после точки
printf("c = %c\n", c); // Вывод одного символа
printf("%.4s\n", str); // Вывод строки (не более 4 символов)

1.2. Чтение из строки/запись в строку

Функции sprintf и sscanf позволяют произвести запись значений переменных в форматированную строку или чтение переменных из строки: int sscanf (char *str, char *format, ...); int sprintf(char *str, char *format, ...);

Эти функции во всём аналогичны функциям printf и scanf, только в качестве первого параметра указывается строка, куда записываются или откуда считываются данные.

1.3. Работа с файлами

  • Объявление файловой переменной
FILE *file;
  • Открытие файла
FILE *fopen(char *name, char *mode);
  • Проверка достижения конца файла
int feof(FILE *file);
  • Закрытие файла
int fclose(FILE *file);

FILE – специальная структура, объявленная в файле <stdio.h>, которая используется при работе с файлами. Для работы с файлом нужно объявить переменную FILE *<имя>.

Функция fopen используется для открытия файла. Первый параметр задаёт имя файла. Второй параметр mode задаёт требуемый тип доступа к файлу.

Mode Действие
"r" Открытие для чтения. Если файл не существует или не может быть найден, функция fopen возвращает признак ошибки.
"w" Открытие для записи. Если файл существует, его содержимое уничтожается. Если файл не существует, он создаётся.
"a" Открытие для добавления. Если файл не существует, он создаётся.
"r+" Открытие для чтения и записи. Файл должен существовать.
"w+" Открытие пустого файла для чтения и записи. Если файл существует, его содержимое уничтожается.
"a+" Открытие для чтения и добавления. Если файл не существует, он создаётся.

Кроме того, к параметру mode могут быть добавлены символы t и b для задания текстового и двоичного режимов соответственно. По умолчанию используется текстовый режим.

В случае ошибки функция fopen возвращает значение NULL.

1.3.1. Текстовый режим

При вводе/выводе в текстовом режиме происходит преобразование между внешним представлением значения и внутренним (машинным) представлением этого значения.

  • Ввод одного символа
int getc (FILE *file);
  • Вывод одного символа
int putc (int c, FILE *file);
  • Ввод
int fscanf (FILE *file, char *format, ...);
  • Вывод
int fprintf(FILE *file, char *format, ...);
  • Ввод строки
char* fgets (char *line, int maxline, FILE *file);
  • Вывод строки
int fputs (char *line, FILE *file);

1.3.2. Двоичный режим

В двоичном режиме никаких преобразований не производится, внутреннее представление значения записывается в файл. Для открытия файла в двоичном режиме необходимо в параметр mode функции fopen добавить символ b.

  • Ввод из двоичного файла
unsigned fread (void *buf, int bytes, int num, FILE *file);
  • Вывод в двоичный файл
unsigned fwrite (void *buf, int bytes, int num, FILE *file);

Функция fread читает из файла file в переменную buf num элементов, каждый размером bytes байт. Функция fwrite записывает в файл file из переменной buf num элементов, каждый размером bytes байт. Функции возвращают количество прочитанных/записанных элементов.

В двоичном режиме возможен прямой доступ к файлу: int fseek(FILE *file, long nbytes, int origin)

Данная функция смещает указатель в файле file на nbytes байт с позиции, определяемой параметром origin. При этом параметр origin может принимать следующие значения:

Функция long ftell(FILE *file) возвращает текущую позицию указателя в файле file.

fseek(file, 0, SEEK_END); n = ftell(file); // Определение размера файла

2. Потоковый ввод/вывод

В языке С++ был разработан другой способ ввода/вывода с использованием так называемых потоков ввода и вывода.

2.1. Использование стандартных потоков cin и cout

Для того чтобы использовать стандартные потоки для ввода и вывода, необходимо включить заголовочный файл <iostream>. Для ввода используется операция >>, для вывода – операция <<. Компилятор определяет тип вводимой/выводимой переменной и соответствующим образом форматирует её.

#include <iostream> using namespace std;
cin >> x; // Ввод значения в переменную x из стандартного потока cin
cout << x; // Вывод значения переменной x в стандартный поток cout
cin >> x >> y; // Ввод двух переменных
cout << "x = " << x << "\ny = " << y << endl; // Функция endl осуществляет перевод строки

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

cin >> x; if (cin.fail()) cout << "Произошла ошибка при вводе\n";

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

Для управления форматом вводимого/выводимого значения используются так называемые манипуляторы. Это функции, которые вставляются между вводимыми/выводимыми значениями и изменяют состояние потока.

Для использования манипуляторов необходимо включить заголовочный файл <iomanip>.

Несколько манипуляторов имеют параметр, который может быть задан литералом или переменной. Изменения, сделанные всеми манипуляторами, кроме setw, остаются в силе до отмены. Действие манипулятор setw распространяется только на одно вводимое/выводимое значение.

Манипулятор Описание Примечание
boolalpha Значения переменных типа bool выводятся как true и false.  
dec Целые значения выводятся в десятичной системе счисления.  Используется по умолчанию
fixed Для вещественных чисел используется фиксированный формат.  
hex Целые значения выводятся в шестнадцатеричной системе счисления.  
internal Знак выравнивается по левому краю, а само число – по правому краю.  
left Выравнивание по левому краю.  
noboolalpha Значения переменных типа bool выводятся как 1 и 0.  Используется по умолчанию
noshowbase Префиксы 0 и , обозначающие систему счисления, не выводятся.  Используется по умолчанию
noshowpoint Вывод только целой части вещественного числа (без точки), если дробная часть равна 0.  Используется по умолчанию
noshowpos Знак перед положительными числами не выводится.  Используется по умолчанию
noskipws Пробел рассматривается как признак завершения ввода.  
nouppercase Шестнадцатеричные цифры и символ экспоненты в научном формате вещественного числа выводятся строчными буквами.  Используется по умолчанию
oct Целые значения выводятся в восьмеричной системе счисления.  
right Выравнивание по правому краю.  Используется по умолчанию
scientific Для вещественных чисел используется научный формат.  
setfill(c) Задаёт символ для заполнения. По умолчанию используется пробел.  
setprecision(n) Задаёт точность для вещественных чисел. По умолчанию точность равна 6. Если не установлен ни фиксированный, ни научный формат вещественного числа, то точность задаёт количество выводимых цифр (всего, до точки и после точки). Если число слишком велико, оно автоматически отображается в научном формате, и тогда точность задаёт количество цифр в мантиссе. Если установлен фиксированный формат вещественного числа, точность задаёт количество цифр после точки. Если установлен научный формат вещественного числа, точность задаёт количество цифр в мантиссе.  
setw(n) Устанавливает минимальное количество символов, используемых для вывода значения. Если значение представляется меньшим количеством символов, остальные позиции заполняются символом, установленным с помощью манипулятора setfill. Выравнивание задаётся манипуляторами left, right и internal. Чтобы установить поведение по умолчанию (столько символов, сколько необходимо), нужно использовать манипулятор setw с параметром 0. Влияет только на одно вводимое/выводимое значение!
showbase Вывод префиксов 0 и для обозначения системы счисления.  
showpoint Вывод и целой, и дробной частей вещественного числа, даже если дробная часть равна 0.  
showpos Вывод знака перед положительным числом.  
skipws Пробелы рассматриваются как разделители между значениями.  Используется по умолчанию
uppercase Шестнадцатеричные цифры и символ экспоненты в научном формате вещественного числа выводятся прописными буквами.  

int m, n, x; double y; cin >> m >> n; cout << "m = " << setw(5) << m << "\nn = " << setw(5) << n << endl; cin >> x; cout << setfill('0') << showbase << hex << setw(10) << internal << x << endl; cin >> y; cout << setfill(' ') << fixed << setw(7) << setprecision(2) << y << endl;

2.3. Связываение потоков

Рассмотрим пример.
char c; cout << "Введите символ: "; cin >> c;

Как нам гарантировать, что слова Введите символ появятся на экране прежде, чем будет выполнена операция считывания? Вывод в стандартный поток буферизуется, так что если потоки cin и cout не зависимы, то выводимый текст не появится на экране, пока не заполнится буфер вывода. Решение этой задачи заключается в том, что потоки связываются с помощью функции tie. Эта функция используется для того, чтобы устанавливать и разрывать связи между потоками ввода и вывода.
char c; cin.tie(&cout); cout << "Введите символ: "; cin >> c;

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

2.4. Файловые потоки

Для ввода/вывода из файла/в файл существуют потоки, которые могут быть связаны с файлом на диске. Для использования файловых потоков необходимо включить заголовочный файл <fstream>. Существует три разновидности файловых потоков: fstream, ifstream и ofstream. Разница между ними состоит в том, что поток fstream по умолчанию открывается для ввода и вывода, поток ifstream по умолчанию открывается для ввода, а поток ofstream по умолчанию открывается для вывода. Изменить поведение по умолчанию, а также задать другие режимы открытия файла можно с помощью следующих констант:

Режимы открытия файла комбинируются с помощью операции поразрядного ИЛИ (|).

Для открытия файла можно задать имя файла непосредственно в конструкторе потока или воспользоваться функцией open.

fstream fs("f1.txt"); // Открытие файла для чтения и записи
ifstream ifs("f2.txt"); // Открытие файла для чтения
ofstream ofs("f3.txt"); // Открытие файла для записи
fstream fs("f1.txt", ios_base::in | ios_base::out | ios_base::trunk); // Открытие файла для чтения и записи с удалением содержимого файла
ifstream ifs("f2.txt", ios_base::in | ios_base::binary); // Открытие двоичного файла для чтения
ofstream ofs; // Создаём поток, не связанный с файлом
ofs.open("f3.txt"); // Открываем файл для записи

Если вы планируете использовать файл только для чтения или только для записи безопаснее воспользоваться соответствующим файловым потоком.

Для проверки открытия файла служит функция is_open.

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

Для работы с текстовыми потоками используются операции << и >>. Также возможно использование манипуляторов для форматирования вводимых/выводимых значений.

int x; fstream f;
f.open("in.txt", ios_base::in); // Открываем файл для чтения
if (!f.is_open()) // Проверяем открытие файла
{ cout << "Невозможно открыть файл 'in.txt'\n"; return; }
f >> x; // Чтение переменной x из файла
if (f.fail()) // Проверка ошибок чтения
{ cout << "Ошибка чтения из файла 'in.txt'\n";  return; }
f.close(); // Закрываем файл
f.open("out.txt", ios_base::out); // Снова открываем файл, теперь для записи
if (!f.is_open()) // Проверяем открытие файла
{ cout << "Невозможно открыть файл 'out.txt'\n"; return; }
f << hex << x << endl; // Выводим значение переменной x в 16-ричной системе
f.close(); // Закрываем файл

Для того чтобы проверить, достигнут ли конец файла, используется функция eof.

int n; ifstream f("in.txt");
if (!f.is_open())
{ cout << "Невозможно открыть файл 'in.txt'\n"; return; }
while (!f.eof()) // Пока не достигнут конец файла
{ f >> n; cout << n << endl; }

Для организации прямого доступа к файлу используются функции seekg/seekp и tellg/tellp. Различие между функциями состоит в том, что функции, с именем, оканчивающимся символом ‘g’, используются для работы с потоками ввода, а функции, с именем, оканчивающимся символом ‘p’, – для работы с потоками вывода.

Функции seekg/seekp перемещают внутренний указатель файла на заданную позицию. Позиции соответствуют байтам, нумерация начинается с 0. Существует две разновидности функций – с одним параметром и с двумя параметрами. Один целочисленный параметр задаёт абсолютную позицию в файле. Два параметра задают смещение (целое число) и точку отсчёта. Этот параметр может принимать следующие значения:

Функции tellg/tellp не имеют параметров. Они возвращают текущую позицию указателя в файле.

Функции seekg/seekp и tellg/tellp работают как с текстовыми, так и с двоичными потоками. В любом случае желательно либо знать структуру файла, либо работать с файлами, все записи в которых имеют одинаковую длину. В противном случае возможно перемещение указателя на позицию, не являющуюся началом записи.

Для работы с двоичными файлами используются функции read и write. В качестве параметров функции получают указатель (типа char* для функции read и типа const char* для функции write), который задаёт адрес начала массива для ввода/вывода, и целое число, задающее количество байт для ввода/вывода.

Пример использования потокового ввода из двоичного файла см. в конце лекции.

3. Примеры

Пример 1. Ввод массива из текстового файла

// Функция ввода одномерного массива. // Если ввод был осуществлен без ошибок, возвращается 1, в противном случае - 0. // x - вводимый массив, // n - указатель на переменную, содержащую количество элементов массива, // fname - имя файла для ввода. int ArrayInput(double x[], int *n, char *fname) { int i; FILE *file; if ((file = fopen(fname, "r")) == NULL) { printf("Невозможно открыть файл '%s'\n", fname); return 0; } if (fscanf(file, "%d", n) < 1) { printf ("Ошибка чтения из файла '%s'\n", fname); fclose(file); return 0; } if (*n < 0 || *n > NMAX) { printf("Кол-во эл-тов масс. должно быть от 1 до %d! (файл '%s')\n", NMAX, fname); fclose(file); return 0; } for (i = 0; i < *n; i++) if (fscanf(file, "%lf", &x[i]) < 1) { printf ("Ошибка чтения из файла '%s'\n", fname); fclose(file); return 0; } fclose(file); return 1; }

Пример 2. Вывод массива в двоичный файл

// Вывод массива в двоичный файл. // Если вывод был осуществлен без ошибок, возвращается 1, в противном случае - 0. // x - выводимый массив, // n - переменная, содержащая количество элементов массива, // fname - имя файла для вывода. int BinOutput(double x[], int n, char *fname) { FILE *file; if ((file = fopen(fname, "wb")) == NULL) { printf("Невозможно открыть файл '%s'\n", fname); return 0; } if (fwrite(x, n * sizeof(double), 1, file) < 1) { printf ("Ошибка записи в файл '%s'\n", fname); fclose(file); return 0; } fclose(file); return 1; }

Пример 3. Ввод массива из двоичного файла

// Ввод массива из двоичного файла. // Если ввод был осуществлен без ошибок, возвращается 1, в противном случае - 0. // x - вводимый массив, // n - указатель на переменную, содержащую количество элементов массива, // fname - имя файла для ввода. int BinInput(double x[], int *n, char *fname) { FILE *file; if ((file = fopen(fname, "rb")) == NULL) { printf("Невозможно открыть файл '%s'\n", fname); return 0; } for (*n = 0; ; (*n)++) if (fread(x + (*n), sizeof(double), 1, file) < 1) if (feof(file)) break; else { printf ("Ошибка чтения из файла '%s'\n", fname); fclose(file); return 0; } fclose(file); return 1; } // Другой способ int BinInput(double x[], int *n, char *fname) { FILE *file; if ((file = fopen(fname, "rb")) == NULL) { printf("Невозможно открыть файл '%s'\n", fname); return 0; } fseek(file, 0, SEEK_END); *n = ftell(file) / sizeof(double); fseek(file, 0, SEEK_SET); if (fread(x, sizeof(double), *n, file) < *n) { printf ("Ошибка чтения из файла '%s'\n", fname); fclose(file); return 0; } fclose(file); return 1; }

Пример 4. Потоковый ввод массива из двоичного файла

// Ввод массива из двоичного файла. // Если ввод был осуществлен без ошибок, возвращается 1, в противном случае - 0. // x - вводимый массив, // n - указатель на переменную, содержащую количество элементов массива, // fname - имя файла для ввода. int BinInput(double x[], int *n, char *fname) { ifstream f(fname, ios_base::in | ios_base::binary); if (!f.is_open()) { cout << "Невозможно открыть файл '" << fname << "'\n"; return 0; } f.seekg(0, ios_base::end); *n = f.tellg() / sizeof(double); f.seekg(0, ios_base::beg); f.read(reinterpret_cast<char *>(x), *n * sizeof(double)); if (f.fail()) { cout << "Ошибка чтения из файла '" << fname << "'\n"; return 0; } return 1; }