Урок 6 - Ввод и вывод. Семейство функций printf Печать
Автор: Андрей   
11.08.2009 21:37

Предисловие
Функция вывода языка C - printf
Функция ввода языка C - scanf

Предисловие
Функции вывода и ввода языка C - это printf, scanf и другие. Еще раз напомню, что если вы пишете программы на языке C++, вам скорее всего гораздо чаще следует обращаться к потокам (смотрите предыдущий урок), нежели к этим функциям. Однако в ряде задач знакомство с этими функциями будет вам полезно, и, увы, не в последнюю очередь - когда вы будете иметь дело со старыми компиляторами, реализующих ранние версии языка C++, во многом еще имевших значительно большее сходство со своим прародителем, нежели современная версия языка. Впрочем, не все так плохо, и функции ввода/вывода языка C могут иметь применение и в случаях, имеющих гораздо большую практическую пользу.
Как и любое достаточно низкоуровневое средство, функции семейства printf имеют не самый простой для понимания и достаточно запутанный интерфейс, однако являются очень гибкими и предоставляют возможности для решения очень широкого класса задач. И это понятно - чем больше настроек, тем труднее в них разобраться, но тем больше они предоставляют (в идеале) возможностей. Рассмотрим же теперь наши функции подробнее.

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

int printf(const char* format, ...);

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

  • d - знаковое целое число в десятичной форме записи;
  • u - беззнаковое десятичное целое;
  • o - восьмеричное целое;
  • x, X - шестнадцатеричное целое. x означает, что будут использоваться символы abcdef, X - что ABCDEF (напр., aa32ff, FFFFFF);
  • f - десятичное число с плавающей точкой (напр., 12.345);
  • e, E - десятичное число с плавающей точкой в экспоненциальной форме (напр., 6.02e+23, 1.60E-19);
  • c - символ;
  • s - строка, ограниченная нулем.
Для вывода знака % используется комбинация %%.
Т.е. минимальная управляющая последовательность имеет, например, такой вид: %d, %c, %s...
Рассмотрим, например, такой код:

void f(int i, char* s, float f)
 {
 printf("Value of variable i is equal %d\n", i);
 printf("Pushkin has told: \"%s\"\n", s);
 printf("The cost of usual brain today is %f rubles\n", f);
 }

Если значения i, s и f, соответственно, равны 5, "On seashore far a green oak towers,\nAnd to it with a gold chain bound", 23.54, то функция выведет:

Value of variable i is equal 5
Pushkin has told: "On seashore far a green oak towers,
And to it with a gold chain bound"
The cost of usual brain today is 23.540001 rubles

Как видим, там, где в строке форматирования стояло %d, мы теперь имеем значение целой переменной; где стояло %s - значение строковой переменной (специальный символ \" представляет двойную кавычку); там же, где стояло %f - значение переменной типа float (странное значение 23.540001 или другое сходное обуславливается спецификой хранения в памяти чисел с плавающей точкой).
Однако, обратите внимание на тот факт, что функции семейства printf не производят проверку типов, а интерпретируют последовательности битов, представляющих переменную, так как будто они относятся к переменной типа, указанного в управляющей последовательности. Это можно использовать, например, для установления соответствия между символом и его численным представлением:

int f(char ch)
 {
 printf("Numerical value of simbol %c is %d", ch, ch);
 }

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

%[флаги][ширина][.точность][размер]тип

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

  • - (минус) - выравнивание выводимого значения происходит по левому кряю поля, а не по правому как обычно;
  • + (плюс) - перед значением знакового типа будет обязательно стоять знак + или -;
  • 0 (ноль) - если ширина поля указана большей, чем необходимо, то свободное место заполняется нулями, а не пробелами как обычно. Если присутствует флаг - (минус) или установлена точность, данный флаг игнорируется;
  •   (пробел) - перед положительным значением знакового типа оставляется один пробел. Если присутствует флаг + (плюс), данный флаг игнорируется;
  • # ("решетка") - числа с плавающей точкой выводится с десятичной точкой даже если после нее идут одни нули; выводятся незначащие нули; перед восьмеричными числами выводится 0, а перед шестнадцатиричными - 0x или 0X.

В одной управляющей последовательности может присутствовать одновременно несколько флагов.
Строка цифр, не предваряемая точкой, определяет ширину поля. Если указанное значение недостаточно, поле расширяется до необходимого размера. Если указанная ширина поля превышает необходимую для вывода, то поле будет дополнено пробелами (по умолчанию - справа, при использовании флага - (минус) - справа); при наличии флага 0 (ноль) (число начинается с нуля) заполнение будет происходить нулями.
Строка цифр, начинающаяся с точки, определяет точность. Для целых типов это будет минимальное количество выводимых символов; для чисел с плавающей точкой - минимальное число символов после запятой; для строк - максимальное число символов.
Вместо числовых строк в управляющей последовательности может быть указана звездочка (*). Это означает, что ширина или точность указывается в целочисленном аргументе функции printf - этот аргумент(ы) будет распологаться перед аргументом, относящимся к этой управляющей последовательности.
Перед спецификатором типа также может располагаться спецификатор размера. В C++ их всего два:

  • h - последующий d, o, u, x относится к целочисленному аргументу типа short
  • l - последующий d, o, u, x относится к целочисленному аргументу типа long

Для того, чтобы лучше усвоить всю эту информацию справочного толка, рассмотрим пример вывода данных с помощью функции printf. Одним из самых очевидных примеров может служить информация о сотрудниках (Employee). Но, разумеется, мы не ищем легких путей и потому вместо всей этой канцелярщины обратимся к чему-то более романтичному, а именно - к парусным судам. Предположим, что у нас есть структура (подробнее о структурах вы сможете прочитать в соответствующей моей статье, когда я ее напишу) Ship, описывающая парусное судно, и содержащяя такие поля, как Name (название корабля, тип char[]), Type (тип корабля, тип char[]), Country (родина корабля, тип char[]), Year (год спуска на воду, тип int), Displaycement (водоизмещение в тоннах, тип int) и Length (длина в метрах, тип float). Также считаем, что мы определили три объекта типа Ship - Ship1, Ship2 и Ship3 со следующими значениями соответствующих полей:

"Standart", "frigate", "Russian Empire", 1703, 220, 34.5
"Golden Hind", "galleon", "England", 1577, 300, 37.0
"Ariel", "clipper", "United Kingdom", 1865, 853, 10.3

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

printf("Name of ship |  Type  |    Country    | Launched | Displaycement(t) | Length(m)\n");
printf("-------------+--------+---------------+----------+------------------+----------\n");
printf("%12.12s |%8.8s| %14.14s|%9.4d |%17.3d |%10.1f\n", Ship1.Name, Ship1.Type, Ship1.Country, Ship1.Year, Ship1.Displaycement, Ship1.Length);
printf("%12.12s |%8.8s| %14.14s|%9.4d |%17.3d |%10.1f\n", Ship2.Name, Ship2.Type, Ship2.Country, Ship2.Year, Ship2.Displaycement, Ship2.Length);
printf("%12.12s |%8.8s| %14.14s|%9.4d |%17.3d |%10.1f\n", Ship3.Name, Ship3.Type, Ship3.Country, Ship3.Year, Ship3.Displaycement, Ship3.Length);

В этом примере на выходе мы получим:

Name of ship |  Type  |    Country    | Launched | Displaycement(t) | Length(m)
-------------+--------+---------------+----------+------------------+----------
    Standart | frigate| Russian Empire|     1703 |              220 |      34.5
 Golden Hind | galleon|        England|     1577 |              300 |      37.0
       Ariel | clipper| United Kingdom|     1865 |              853 |      10.3

Функция scanf
Теперь, когда мы разобрались, как выводить результаты работы программы с помощью функции printf, давайте посмотрим, как же передать программе исходные данные, воспользовавшись функцией scanf. Ее синтаксис определяется так:

int scanf(const char *format, ...);

Первый аргумент представляет собой ожидаемую строку ввода. За ней следуют адреса переменных, которые надо считать. (для получения адреса переменной aaa необходимо поставить перед ней знак & (амперсанд): &aaa) Не забывайте об этом! Если вы вместо адреса переменной укажете саму ее, то компилятор посмотрит на это сквозь пальцы, но при попытке выполнения функции случится некий коллапс и мы получим ошибку на этапе исполнения.
Итак, как же работает функция scanf? В начале ее работы выполнение программы приостанавливается и пользователю предоставляется ввести строку (вплоть до нажатия клавиши Enter строку можно без проблем редактировать). Когда нажатием Enter пользователь сообщает программе, что строка введена, функция начинает разбирать введенное. Она проводит соответствие между ожидаемой строкой и реально введенной. Если, например, ожидаемая строка (строка формата) состоит только из управляющих последовательностей (возможно, разделенных пробелами или группами пробелов), то введенные пользователем символы интерпретируются в соответствии с указаниями управляющих последовательностей, которые для функции scanf имеют следующий общий вид:

%[ширина][размер]тип

Элементы в квадратных скобкая являются необязательными. Первая управляющая последовательность соответствует адресу первой переменной в списке аргументов, вторая - адресу второй, и т.д. Если адресов переменных окажется больше, чем управляющих последовательностей, то часть из них окажется несчитанной, если же меньше - то последствия будут непредсказуемы.
Ширина, задаваемая числом, означает максимальное количество символов, которое разрешено ввести пользователю для задания значения данной переменной. Если введено меньшее число символов - никаких проблем не будет, если же больше, то переменной присвоится преобразованное значение из последовательности символов максимальной ширины, излишние же символы будут интепретироваться как часть строки для следующей управляющей последовательности.
Размер и тип соответствуют аналогичным для функции printf за исключением того, что в функции scanf есть особый "тип" [.
Точнее, в данном случае используется пара квадратных скобок [], между которыми заключена последовательность символов. Функция считывает из введенной пользователем строки символы до тех пор, пока не встретится символ, отсутствующий между квадратных скобок, либо же, если после [ сразу же стоит знак ^, то наоборот - пока не встретится символ, присутствующий между скобок. Если нужно поместить между скобок символ ], то он должен идти сразу же после открывающей [, либо, если он есть - после ^. В противном случае ] будет означать окончание конструкции. Считанная последовательность символов преобразуется к строковому типу.
Границами последовательностей символов, соответствующих различным управляющим последовательностям являются пробельные символы (например, пробел, знак табуляции и т.п.) и их последовательности. Функция scanf пропускает их и переходит к следующей значащей последовательности символов. Также она поступает и при превышении ширины последовательности символов.
В строке форматирования, однако, могут присутствовать не только управляющие последовательности, но и обычные символы (комбинация %% будет означать символ знака процента %). В этом случае пользователь обязан ввести строку того же вида, что и строка форматирования, т.е. обычные символы должны совпадать там и там, а всё прочее должно правильно преобразовываться в формат, определяемый управляющими последовательностями. Обычные символы также могут быть ограничителями преобразуемых последовательностей символов.
Перейдем теперь наконец от теории к практике. Здесь я первым делом хочу посоветовать вам: не плодите лишних сущностей без достаточной на то необходимости. В большинстве случаев для ввода данных вам потребуются самые простые конструкции навроде вот таких:

void f()
 {
 int a, b;
 char name[20];
 printf("Enter your name: ");
 scanf("%s", name);
 printf("Enter 1st value: ");
 scanf("%d", &a);
 printf("Enter 2nd value: ");
 scanf("%d", &a);
 printf("Thank you, %s! The sum of %d and %d is equal %d!\n", name, a, b, a+b);
 }

Каждая scanf просто и без изысков считает положенное ей значение и затем будет выведен итог. Заметьте, кстати, что при считывании строки указано не &name, а name. Это является следствием тесной связи массивов и указателей в языке C++: по сути name является указателем на первый элемент массива, т.е. содержит его адрес. Поэтому использование символа & здесь не нужно.
Вот, однако, чуть более замысловатый пример:

void g()
 {
 int a, b, c;
 printf("Enter a+b=c: ");
 scanf("%d+%d=%d", &a, &b, &c);
 if (a + b == c)
  printf("You are absolutely right! %d+%d=%d", a, b, c);
 else
  printf("Oh, no! %d+%d!=%d", a, b, c);
 }

Здесь мы обязываем пользователя ввести выражение вида 12+34=56, и если он вместо этого введет, например, 12+34 = 56, то переменная c не считается как положено, т.к. программа ожидает ввода трех десятичных чисел, разделенных знаками плюс и равно, и не ожидает никаких пробелов. Чуть большую вольность предоставляет нам следующий вариант функции scanf:

scanf("%d + %d = %d", &a, &b, &c);

В этом случае нам простятся всяческие хитросплетения с пробелами, пусть даже мы вовсе забудем про них, введя 12+34=56. Пробелы в строке форматирования дозволяют нам использовать их (пробелы) при вводе, однако не обязывают нас к этому. Знаки плюс и равно, не являющиеся цифрами, вполне позволяют отделить друг от друга все три введенных числа.
И, наконец, посмотрим, как же мы можем с помощью функции scanf выполнить экзотическую задачу: считать из введенной пользователем строки римское число, содержащее не более 8 знаков:

void h()
 {
 char num[8];
 scanf("%8[IVXLCDM]", num);
 printf("%s", num);
 }

В C-строку (массив символов) будут считываться символы до тех пор, пока не будет считано 8 штук (ширина), либо пока не встретиться символ, не являющийся римской цифрой. Конечно же, никто не гарантирует, что считанное будет римским числом, записанным по всем правилам, но это, уже, разумеется, совсем другой вопрос. Такая вот управляющая последовательность гарантирует нам, что считаны будут только последовательность римских цифр.
И в заключение несколько слов о поведении функции scanf в неожиданных ситуациях. Видно, что на пользователя налагаются весьма жесткие ограничения, и вводить что попало ему настоятельно не рекомендуется. Так, например, функция завершит свою работу, если введенное пользователем не сможет быть преобразовано должным образом в значение определенного типа; либо же если символы в строке форматирования не совпадут с введенными пользователем. При этом несчитанные и последующие за ними символы останутся в потоке ввода (куда и попадают по нажатию пользователем клавиши Enter, и откуда впоследствии и читаются). Поэтому если впоследствии вновь будет вызвана функция scanf, то она вновь будет пытаться считать то, от чего отступилась предыдущая, и не факт, что программист ожидал от нее именно этого...
Потому будьте крайне осторожны при использовании этих функций, равно как и вообще будьте осторожны в программировании, если только вы целенаправленно не экспериментируете, а хотите добиться четкой и ясной работы программы!

Обновлено 27.10.2009 15:57