книги / Практикум по программированию на языке Си
..pdfЭКСПЕРИМЕНТ. Введите строку символов вместо целого числа. Возможный результат:
z=gjytrv.oi866 <ENTER> x=sum=3630652
Результаты экспериментов просто удручающие. Как же защитить верную программу от неверных исходных данных? Вопрос чрезвычайно важный, а ответ на него далеко не прост и не однозначен.
ЭКСПЕРИМЕНТ. Что будет, если программист использовал не адрес переменной, а ее имя в качестве аргумента функции scanf()?
В следующей программе сделана эта ошибка:
/* 04_23_1.c - ошибка в аргументе функции scanf() */ #include <stdio.h>
int main ()
{
int z, x, sum; printf("z="); scanf("%d",z); printf("x="); scanf("%d",x); sum=z+x; printf("sum=%d",sum); return 0;
}
Программа корректно транслируется. Результат выполнения программы:
z=3 <ENTER> x=4 <ENTER> sum=3630652
Очень плохо! Нет сообщений при компиляции или на этапе сборки исполнимой программы. Программа выполняется, но ошибочно.
121
ЭКСПЕРИМЕНТ. В обращении к функции scanf() укажите спецификацию преобразования, не соответствующую типу объекта, в который выполняется ввод.
/* 04_23_2.c - функция scanf() - ошибка в спецификации */
#include <stdio.h> int main ()
{
int z, x, sum; printf("z="); scanf("%f",&z); printf("x="); scanf("%f",&x); sum=z+x; printf("sum=%d",sum); return 0;
}
Корректная трансляция!!!
Результат выполнения программы:
z=4 <ENTER> x=66 <ENTER> sum=-2096889856
Очень плохо! Успешная компиляция, но ошибочное исполнение.
Наиболее традиционный тип представления вещественных данных это double. Вещественные константы, в записи которых отсутствует суффикс (f или l), по умолчанию имеют тип double. При форматном выводе значения типа double обычно используют спецификации преобразования %e и %f. Но при вводе их применение в форматной строке функции scanf() недопустимо.
ЗАДАЧА 04-24. Определите в программе переменную типа double, введите с клавиатуры ее значение и выведите (напечатайте) значение переменной.
122
/* 04_24.c - функция scanf() для типа double */ #include <stdio.h>
int main ()
{
double real; printf("real="); scanf("%le",&real); printf("result=%e\n",real); printf("result=%f\n",real); return 0;
}
Корректная трансляция. Результат выполнения программы:
real=1234.4321 <ENTER> result=1.234432e+03 result=1234.432100
ЭКСПЕРИМЕНТ. В обращении к scanf() предыдущей программы замените спецификацию преобразования %le на %e.
/* 04_24_1.c - функция scanf(). Неверен формат для double */
#include <stdio.h> int main ()
{double real; printf("real="); scanf("%e",&real); printf("result=%e\n",real);
printf("result=%f\n",real); return 0;
}
Корректная трансляция. Результат выполнения программы:
real=1234.4321 <ENTER> result=2.951329e-310 result=0.000000
Результат неверен.
123
Дальнейшие эксперименты (ошибки в спецификациях, отсутствие знака операции & у аргумента, неверные исходные данные и т.п.) читатель может поставить самостоятельно. Но вывод ясен: функция форматного ввода – потенциальный источник ошибок и ее использовать в программах нужно очень аккуратно.
Есть особенности ввода последовательности символов с клавиатуры, когда после каждого символа нужно нажать клавишу ENTER.
ЗАДАЧА 04-25. Введите с помощью трех обращений к функции форматного ввода значения трех переменных типа char, а затем выведите полученные значения и напечатайте их коды.
При чтении символов из стандартного входного потока необходимо обращать внимание на тот факт, что каждое нажатие клавиши ENTER помещает во входной поток два кода – CR (возврат каретки, значение 15) и LF (переход к следующей строке, значение 10). Считывание очередного символа из входного потока функцией scanf() со спецификацией %c "оставляет" в буфере входного потока один из этих управляющих символов. Следующее обращение к функции scanf() приводит к тому, что из входного потока прочитывается только оставшийся управляющий символ LF и не вводится никакая информация. Для иллюстрации сказанного проведем перед решением поставленной задачи такой эксперимент.
ЗАДАНИЕ. Введите с помощью функции форматного ввода значение одной переменной типа char, а затем, повторно обратившись к той же функции, выведите десятичное значение полученного кода.
Заданию соответствует программа:
/* 04_25_1.c - коды от клавиши ENTER */ #include <stdio.h>
int main ()
{
char x, code; printf("x="); scanf("%c",&x); scanf("%c",&code);
124
printf("\nresult: x=%c, code=%d",x,code); return 0;
}
Результат выполнения программы:
x=f<ENTER>
result: x=f, code=10
Как показывают результаты, пара обращений к функции scanf() выполнилась при однократном нажатии на клавишу ENTER. При втором чтении из буфера входного потока прочитан код LF.
Одно из решений задачи 04-25:
/* 04_25.c - функция scanf() при чтении символов с клавиатуры */
#include <stdio.h> int main ()
{
char x1,x2,x3; printf("x1="); scanf("%c",&x1); printf("x2=");
scanf("%c",&x2); scanf("%c",&x2); printf("x3=");
scanf("%c",&x3); scanf("%c",&x3); printf("\nresult: x1=%c x2=%c x3=%c",x1,x2,x3); return 0;
}
Результат выполнения программы:
x1=a <ENTER> x2=d <ENTER> x3=j <ENTER>
result: x1=a x2=d x3=j
Чтобы получить значения переменных x2 и x3, пришлось дважды обращаться к функции scanf(). В первом чтении вводится код LF, во втором – код символа, набранного пользователем.
125
ЭКСПЕРИМЕНТ. Удалите из программы 04_25.с "удвоение" обращений к функции scanf() для ввода значений x2 и x3. Оттранслируйте и выполните такой вариант программы.
Возможный результат:
x1=a <ENTER> x2=x3=b <ENTER>
result: x1=a x2= x3=b
Никак не удается ввести осмысленное значение для переменной
x2.
Другое более обоснованное и технически более корректное решение обеспечивает функция из стандартной библиотеки:
int fflush(FILE * stream);
Функция очищает буфер потока, указатель на который использован в качестве ее аргумента. В нашем случае после обращения к scanf() необходимо очищать буфер стандартного входного потока stdin. Решение задачи в этом случае будет таким:
/* 04_25_2.c - функция scanf() + функция fflush() */ #include <stdio.h>
int main ()
{
char x1,x2,x3; printf("x1=");
scanf("%c",&x1); fflush(stdin); printf("x2=");
scanf("%c",&x2); fflush(stdin); printf("x3=");
scanf("%c",&x3); fflush(stdin); printf("\nresult: x1=%c x2=%c x3=%c",x1,x2,x3); return 0;
}
Результат выполнения программы:
126
x1=1<ENTER>
x2=2<ENTER>
x3=3<ENTER>
result: x1=1 x2=2 x3=3
К сожалению, компилятор DJGPP неверно выполняет эту программу. При использовании компиляторов Turbo C и Visual C программа работает правильно. Именно с их помощью получен приведенный результат.
Другое решение задачи – ввод трех (или более) символов в одном обращении к функции scanf().
/* 04_26.c - функция scanf() при чтении символов с клавиатуры */
#include <stdio.h> int main ()
{
char x1,x2,x3;
printf("Input x1, x2, x3:\n"); scanf("%c %c %c",&x1,&x2,&x3);
printf("\nresult: x1=%c x2=%c x3=%c",x1,x2,x3); return 0;
}
Результат выполнения программы:
Input x1, x2, x3: q g I <ENTER>
result: x1=q x2=g x3=i
Результат верен, но общение с программой не так удобно. Кроме того, во входном потоке после ввода сохраняется служебный код от клавиши ENTER, что нужно учитывать в дальнейшем (если этот текст только начало большой программы).
Коротко о важном
По материалам темы сформулированы следующие утверждения. В конце большинства утверждений приведены номера программ, подтверждающих их справедливость.
127
!Идентификаторы, выбираемые в программе в качестве имен, не должны совпадать со служебными словами (04_01.с).
!Использование идентификатора библиотечной функции в качестве имени объекта в программе "экранирует" доступ к библиотечной функции (04_02.с).
!Применение препроцессорного идентификатора из заголовочного файла в качестве имени объекта программы приводит к "загадочным" сообщениям об ошибках (04_02.с).
!Компилятор не "с первого раза" распознает все ошибки, присутствующие в программе (04_01.с, 04_02.с).
!Одна ошибка в тексте программы может привести к появлению многочисленных синтаксических ошибок, о которых информирует компилятор.
!typedef вводит не новый тип, а новое обозначение (название)
типа (04_03.с).
!Требования к инициализирующим выражениям внешних и внутренних объектов функции различны (04_04.с).
!Внешние объекты доступны в функции, если их имена не использованы в ее теле для обозначения внутренних объектов
(04_05.с).
!Унарные операции декремента и инкремента имеют приоритет над бинарными операциями суммирования и вычитания
(04_06.с, 04_08.с, 04_08_1.с).
!Операции декремента и инкремента применимы только к псевдодопустимым выражениям (04_06.с, 04_08.с).
!Присваивание является операцией (04_07.с).
!Следует понимать различия между постфиксными и префиксными операциями декремента и инкремента (04_08.с, 04_08_1.с).
!При делении целочисленных операндов учитывайте округление результата (04_09.с, 04_10.с).
!При умножении целочисленных операндов не забывайте о возможном переполнении (04_09.с, 04_10.с).
!Значение выражения с операцией "запятая" определяет правый операнд (04_11.с, 04_15.с).
!Положительное целое трактуется как "истинное" значение
(04_12.с, 04_13.с).
!Цепочка одноранговых логических операций выполняется слева направо только до тех пор, пока не станет очевиден результат
(04_14.с, 04_15.с).
128
!В ряде случаев условное (тернарное) выражение может заменить условный оператор (04_16.с, 04_17.с).
!В условном (тернарном) выражении никогда не вычисляются значения всех трех операндов (04_18.с).
!Битовые представления позволяют иллюстрировать различие между знаковыми и беззнаковыми целыми, между операциями "~" и "!" (04-19.с), между логическими и поразрядными логическими операциями (04_21.с).
!Сдвиг на N разрядов влево эквивалентен умножению целого чис-
ла на 2N (04_20.с).
!Поразрядный сдвиг вправо знакового целого сохраняет значение знакового разряда и делает его нулевым для беззнакового значе-
ния (04-22.с, 04-22_1.с).
!Функция scanf() не "сообщает" о несоответствии вводимых данных спецификациям преобразования (04_23.с).
!Компилятор не распознает несоответствие спецификаций из форматной строки последующим аргументам (04_23_1.с, 04_23_2.с, 04_24_1.с).
!При вводе значения типа double нужна спецификация преобра-
зования %le (04_24.с, 04_24_1.с).
!Нажатие клавиши ENTER приводит к появлению во входном потоке двух служебных символов, только один из которых "извлекает" функция scanf() со спецификацией %с (04_25_1.с).
!Для корректного чтения из входного потока символов, код каждого из которых завершается нажатием клавиши ENTER, следует либо использовать пары вызовов функции scanf(), либо после каждого обращения к scanf() вызывать функцию fflush(stdin) для очистки буфера входного потока (04_25.с, 04_25_2.с).
Тема 5
Управление
последовательностью
вычислений
Программа должна быть записана именно так, как мы ее понимаем и как хотели бы объяснять ее другим людям.
Э. Дейкстра. Заметки по структурному программированию
Основные вопросы темы
!Формы условных операторов.
!Условное выражение как альтернатива условного оператора.
!Использование операторов return совместно с условными операторами.
!Переключатели и роль операторов перехода при ветвлениях.
!Особенности меток и управляющих выражений переключателей.
!Применимость и возможности операторов циклов.
!Ввод-вывод символов средствами форматного обмена.
!Специализированные функции для ввода-вывода символьных данных.
!Переназначение стандартных потоков ввода-вывода.
!Символьные данные в переключателях.
!Признак EOF.
Текст программы – это статический, вневременной объект. Программа, как писал Э.Дейкстра [16], приобретает смысл только во время ее выполнения. Линейные участки текста программы содержат операторы преобразования данных в том порядке, в котором они должны исполняться. Для изменения естественной последовательности исполнения в языке Си используются:
!операторы выбора (if, switch);
!операторы циклов (do, while, for);
!операторы переходов (goto, break, continue, return).
130