Программирование — страница 20 из 57

Вычисления

Если результат не обязательно должен быть точным,

я могу вычислить его сколь угодно быстро”.

Джеральд Вайнберг (Gerald M. Weinberg)


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

vector
, предназначенный для хранения последовательностей значений.

4.1. Вычисления

 Все программы что-нибудь вычисляют; иначе говоря, они получают на вход какие-то данные и выводят какие-то результаты. Кроме того, само устройство, на котором выполняются программы, называется компьютером[5]. Эта точка зрения является правильной и обоснованной, пока мы придерживаемся широкой трактовки ввода и вывода.



Входная информация может поступать с клавиатуры, от мыши, с сенсорного экрана, из файлов, от других устройств ввода и других частей программы. К категории “другие устройства ввода” относятся наиболее интересные источники данных: музыкальные клавишные пульты, устройства видеозаписи, датчики температуры, сенсоры цифровых видеокамер и т.п. Разнообразие этих устройств бесконечно.

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

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

С программистской точки зрения наиболее важными и интересными категориями ввода-вывода являются “в другую программу” и “в другие части программы”. Большая часть настоящей книги посвящена последней категории: как представить программу в виде взаимодействующих частей и как обеспечить взаимный доступ к данным и обмен информацией. Это ключевые вопросы программирования. Проиллюстрируем их графически.



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

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

Вычислением мы называем некое действие, создающее определенные результаты и основанное на определенных входных данных, например порождение результата (вывода), равного 49, на основе аргумента (ввода), равного 7, с помощью вычисления (функции) извлечения квадратного корня (см. раздел 4.5). Как курьезный факт, напомним, что до 1950-х годов компьютером[6] в США назывался человек, выполнявший вычисления, например бухгалтер, навигатор, физик. В настоящее время мы просто перепоручили большинство вычислений компьютерам (машинам), среди которых простейшими являются калькуляторы.

4.2. Цели и средства

 Цель программиста — описать вычисления, причем это должно быть сделано следующим образом:

• правильно;

• просто;

• эффективно.


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

 Забота о правильности, простоте и эффективности программ возлагается на нас с той минуты, когда мы начинаем писать программы для других людей и осознаем ответственность за качество своей работы; иначе говоря, решив стать профессионалами, мы обязаны создавать хорошие программы. С практической точки зрения это значит, что мы не можем просто нагромождать инструкции, пока программа не заработает; мы должны разработать определенную структуру программы. Парадоксально, но забота о структуре и качестве кода часто является самым быстрым способом разработки работоспособных программ. Если программирование выполнено качественно, то хорошая структура программы позволяет сэкономить время на самой неприятной части работы: отладке. Иначе говоря, хорошая структура программы, продуманная на этапе разработки, может минимизировать количество сделанных ошибок и уменьшить объем времени, затрачиваемого на поиск таких ошибок и их исправление.

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

Абстракция. Этот способ предполагает сокрытие деталей, которые не являются необходимыми для работы с программой (детали реализации) за удобным и универсальным интерфейсом. Например, вместо изучения деталей сортировки телефонной книги (о методах сортировки написано множество толстых книг), мы можем просто вызвать алгоритм сортировки из стандартной библиотеки языка С++. Все, что нам нужно для сортировки, — знать, как вызывается этот алгоритм, так что мы можем написать инструкцию

sort(b, e)
, где
b
и
e
— начало и конец телефонной книги соответственно. Другой пример связан с использованием памяти компьютера. Непосредственное использование памяти может быть довольно сложным, поэтому чаще к участкам памяти обращаются через переменные, имеющие тип и имя (раздел 3.2), объекты класса
vector
из стандартной библиотеки (раздел 4.6, главы 17–19), объекты класса
map
(глава 21) и т.п.

• “Разделяй и властвуй”. Этот способ подразумевает разделение большой задачи на несколько меньших задач. Например, если требуется создать словарь, то работу можно разделить на три части: чтение, сортировка и вывод данных. Каждая из новых задач намного меньше исходной.


 Чем это может помочь? Помимо всего прочего, программа, созданная из частей, обычно немного больше, чем программа, в которой все фрагменты оптимально согласованы друг с другом. Причина заключается в том, что мы плохо справляемся в большими задачами. Как правило, как в программировании, так и в жизни, — мы разбиваем их на меньшие части, полученные части разделяем на еще более мелкие, пока не получим достаточно простую задачу, которую легко понять и решить. Возвращаясь к программированию, легко понять, что программа, состоящая из 1000 строк, содержит намного больше ошибок, чем программа, состоящая из 100 строк, поэтому стоит разделить большую программу на части, размер которых меньше 100 строк. Для более крупных программ, скажем, длиной более 10 тыс. строк, применение абстракции и метода “разделяй и властвуй” является даже не пожеланием, а настоятельным требованием.

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

Рассматривая процесс разбиения программ, мы всегда учитываем, какие инструменты помогают выделить эти части и обеспечить взаимодействие между ними. Хорошая библиотека, содержащая полезные средства для выражения идей, может существенно повлиять на распределение функциональных свойств между разными частями программы. Мы не можем просто сидеть и фантазировать, как получше разбить программу на части; мы должны учитывать, какие библиотеки находятся в нашем распоряжении и как их можно использовать. Пока вы находитесь в начале пути, но вскоре увидите, что использование существующих библиотек, таких как стандартная библиотека языка С++, позволяет сэкономить много сил не только на этапе программирования, но и на этапах тестирования и документации. Например, потоки ввода-вывода позволяют нам не вникать в детали устройства аппаратных портов ввода-вывода. Это первый пример разделения программы на части с помощью абстракции. В следующих главах мы приведем новые примеры.

Обратите внимание на то, какое значение мы придаем структуре и организации программы: вы не сможете написать хорошую программу, просто перечислив множество инструкций. Почему мы упоминаем об этом сейчас? На текущем этапе вы (или, по крайней мере, многие читатели) слабо представляете себе, что такое программа, и лишь через несколько месяцев будете готовы написать программу, от которой может зависеть жизнь или благосостояние других людей. Мы упоминаем об этом, чтобы помочь вам правильно спланировать свое обучение. Существует большой соблазн набросать примерный план курса по программированию — похожего на изложенный в оставшейся части книги, — выделив темы, которые имеют очевидное полезное применение и проигнорировав более “тонкие” вопросы разработки программного обеспечения. Однако хорошие программисты и проектировщики систем знают (и это знание часто приобретается тяжелой ценой), что вопросы структуры лежат в основе хорошего программного обеспечения и пренебрежение ими порождает массу проблем. Не обеспечив хорошей структуры программы, вы, образно говоря, лепите ее из глины. Это вполне возможно, но таким образом никогда нельзя построить пятиэтажный дом (глина просто не выдержит). Если хотите построить не времянку, а солидное здание, то следует уделить внимание структуре и правильной организации кода, а не возвращаться к этим вопросам, совершив множество ошибок.

4.3. Выражения

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

a
',
3.14
или
"Norah"
.

Имена переменных также являются выражениями. Переменная — это объект, имеющий имя. Рассмотрим пример.


// вычисление площади:

int length = 20;  // литеральное целое значение

                  // (используется для инициализации переменной)

int width = 40;

int area = length*width; // умножение


Здесь литералы

20
и
40
используются для инициализации переменных, соответствующих длине и ширине. После этого длина и ширина перемножаются; иначе говоря, мы перемножаем значения
length
и
width
. Здесь выражение “значение
length
” представляет собой сокращение выражения “значение, хранящееся в объекте с именем
length
”. Рассмотрим еще один пример.


length = 99; // присваиваем length значение 99


Здесь слово

length
, обозначающее левый операнд оператора присваивания, означает “объект с именем
length
”, поэтому это выражение читается так: “записать число 99 в объект с именем
length
”. Следует различать имя
length
, стоящее в левой части оператора присваивания или инициализации (оно называется “
lvalue
переменной
length
”) и в правой части этих операторов (в этом случае оно называется “
rvalue
переменной
length
”, “значением объекта с именем
length
”, или просто “значением
length
”). В этом контексте полезно представить переменную в виде ящика, помеченного именем.



Иначе говоря,

length
— это имя объекта типа
int
, содержащего значение 99. Иногда (в качестве
lvalue
) имя
length
относится к ящику (объекту), а иногда (в качестве
rvalue
) — к самому значению, хранящемуся в этом ящике.

Комбинируя выражения с помощью операторов, таких как 

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


int perimeter = (length+width)*2; // сложить и умножить


Без скобок это выражение пришлось бы записать следующим образом:


int perimeter = length*2+width*2;


что слишком громоздко и провоцирует ошибки.


int perimeter = length+width*2; // сложить width*2 с length


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

perimeter
, инициализированную корректным выражением. Если результат выражения не имеет смысла, то это ваши проблемы. Вы знаете математическое определение периметра, а компилятор нет.

В программах применяются обычные математические правила, регламентирующие порядок выполнения операторов, поэтому

length+width*2
означает
length+(width*2)
. Аналогично выражение
a*b+c/d
означает
(a*b)+(c/d)
, а не
a*(b+c)/d
. Таблица приоритетов операторов приведена в разделе A.5.

Первое правило использования скобок гласит: “Если сомневаешься, используй скобки”. И все же программист должен научиться правильно формировать выражения, чтобы не сомневаться в значении формулы

a*b+c/d
. Слишком широкое использование операторов, например
(a*b)+(c/d)
, снижает читабельность программы.

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


a*b+c/d*(e–f/g)/h+7 // слишком сложно


и всегда старайтесь выбирать осмысленные имена.

4.3.1. Константные выражения

В программах, как правило, используется множество констант. Например, в программе для геометрических вычислений может использоваться число “пи”, а в программе для пересчета дюймов в сантиметры — множитель 2.54. Очевидно, что этим константам следует приписывать осмысленные имена (например,

pi
, а не
3.14159
). Аналогично, константы не должны изменяться случайным образом. По этой причине в языке С++ предусмотрено понятие символической константы, т.е. именованного объекта, которому после его инициализации невозможно присвоить новое значение. Рассмотрим пример.


const double pi = 3.14159;

pi = 7;  // ошибка: присваивание значения константе

double c = 2*pi/r; // OK: мы просто используем переменную pi,

                   // а не изменяем ее


Такие константы полезны для повышения читабельности программ. Увидев фрагмент кода, вы, конечно, сможете догадаться о том, что константа

3.14159
является приближением числа “пи”, но что вы скажете о числе
299792458
? Кроме того, если вас попросят изменить программу так, чтобы число “пи” было записано с точностью до 12 десятичных знаков, то, возможно, вы станете искать в программе число
3.14
, но если кто-нибудь неожиданно решил аппроксимировать число “пи” дробью
22/7
, то, скорее всего, вы ее не найдете. Намного лучше изменить определение константы
pi
, указав требуемое количество знаков.


const double pi = 3.14159265359;


 Следовательно, в программах предпочтительнее использовать не литералы (за исключением самых очевидных, таких как

0
и
1
). Вместо них следует применять константы с информативными именами. Неочевидные литералы в программе (за рамками определения констант) насмешливо называют “магическими”.

В некоторых местах, например в метках оператора

case
(см. раздел 4.4.1.3), язык С++ требует использовать константные выражения, т.е. выражения, имеющие целочисленные значения и состоящие исключительно из констант. Рассмотрим пример.


const int max = 17; // литерал является константным выражением

int val = 19;

max+2 // константное выражение (константа плюс литерал)

val+2 // неконстантное выражение: используется переменная


 Кстати, число

299792458
— одна из универсальных констант Вселенной, означающая скорость света в вакууме, измеренную в метрах в секунду. Если вы ее сразу не узнали, то вполне возможно, будете испытывать трудности при распознавании остальных констант в программе. Избегайте “магических” констант!

4.3.2. Операторы

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



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

lval
(сокращение фразы “значение, стоящее в левой части оператора присваивания”). Полный список операторов приведен в разделе А.5.

Примеры использования логических операторов

&&
(И),
||
(ИЛИ) и
!
(НЕ) приведены в разделах 5.5.1, 7.7, 7.8.2 и 10.4.

 Обратите внимание на то, что выражение

a
означает
(a
, а значение выражения
a
имеет тип
bool
, т.е. оно может быть либо
true
, либо
false
. Итак, выражение
a
эквивалентно тому, что выполняется либо неравенство
true
, либо неравенство
false
. В частности, выражение
a
не означает “Лежит ли значение
b
между значениями
a
и
c
?”, как многие наивно (и совершенно неправильно) думают. Таким образом, выражение
a
в принципе является бесполезным. Не используйте такие выражения с двумя операциями сравнения и настораживайтесь, когда видите их в чужой программе — скорее всего, это ошибка.

Инкрементацию можно выразить по крайней мере тремя способами:


++a

a+=1

a=a+1


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

++a
, поскольку он точнее остальных отражает идею инкрементации. Он показывает, что мы хотим сделать (добавить к значению переменной
a
единицу и записать результат в переменную). В целом всегда следует выбирать тот способ записи, который точнее выражает вашу идею. Благодаря этому ваша программа станет точнее, а ее читатель быстрее в ней разберется. Если мы запишем
a=a+1
, то читатель может засомневаться, действительно ли мы хотели увеличить значение переменной
a
на единицу. Может быть, мы просто сделали опечатку вместо
a=b+1
,
a=a+2
или даже
a=a–1
; если же в программе будет использован оператор
++a
, то простора для сомнений останется намного меньше. Пожалуйста, обратите внимание на то, что этот аргумент относится к области читабельности и корректности программы, но не к ее эффективности. Вопреки распространенному мнению, если переменная
a
имеет встроенный тип, то современные компиляторы для выражений
a=a+1
и
++a
, как правило, генерируют совершенно одинаковые коды. Аналогично, мы предпочитаем использовать выражение
a *= scale
, а не
a = a*scale

4.3.3. Преобразования

Типы в выражениях можно “смешивать”. Например, выражение

2.5/2
означает деление переменной типа
double
на переменную типа
int
. Что это значит? Какое деление выполняется: целых чисел или с плавающей точкой? Целочисленное деление отбрасывает остаток, например
5/2
равно
2
. Деление чисел с плавающей точкой отличается тем, что остаток в его результате не отбрасывается; например
5.0/2.0
равно
2.5
. Следовательно, ответ на вопрос “Какие числа делятся в выражении
2.5/2
: целые или с плавающей точкой?” совершенно очевиден: “Разумеется, с плавающей точкой; в противном случае мы потеряли бы информацию”. Мы хотели бы получить ответ
1.25
, а не
1
, и именно
1.25
мы и получим. Правило (для рассмотренных нами типов) гласит: если оператор имеет операнд типа
double
, то используется арифметика чисел с плавающей точкой и результат имеет тип
double
; в противном случае используется целочисленная арифметика, и результат имеет тип
int
.

 Рассмотрим пример.


5/2 равно 2 (а не 2.5)

2.5/2 равно 2.5/double(2), т.е. 1.25

'a'+1 означает int('a')+1


Иначе говоря, при необходимости компилятор преобразовывает (“продвигает”) операнд типа

int
в операнд типа
double
, а операнд типа
char
— в операнд типа
int
. Вычислив результат, компилятор может преобразовать его снова для использования при инициализации или в правой части оператора присваивания. Рассмотрим пример.


double d = 2.5;

int i = 2;

double d2 = d/i; // d2 == 1.25

int i2 = d/i;    // i2 == 1

d2 = d/i;        // d2 == 1.25

i2 = d/i;        // i2 == 1


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

f = 9/5*с+32
. Ее можно записать так:


double dc;

cin >> dc;

double df = 9/5*dc+32; // осторожно!


К сожалению, несмотря на вполне логичную запись, это выражение не дает точного преобразования шкалы: значение

9/5
равно
1
, а не
1.8
, как мы рассчитывали. Для того чтобы формула стала правильной, либо
9
, либо
5
(либо оба числа) следует представить в виде константы типа
double
.


double dc;

cin >> dc;

double df = 9.0/5*dc+32; // лучше

4.4. Инструкции

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

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


a = b;

++b;


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

=
— это оператор, поэтому
a=b
— это выражение, и для его завершения необходимо поставить точку с запятой
a=b
; в итоге возникает инструкция. Зачем нужна точка с запятой? Причина носит скорее технический характер.

Рассмотрим пример.


a = b ++ b; // синтаксическая ошибка: пропущена точка с запятой


Без точки с запятой компилятор не знает, что означает это выражение:

a=b++; b;
или
a=b; ++b;
. Проблемы такого рода не ограничиваются языками программирования. Например, рассмотрим выражение “Казнить нельзя помиловать!” Казнить или помиловать?! Для того чтобы устранить неоднозначность, используются знаки пунктуации. Так, поставив запятую, мы полностью решаем проблему: “Казнить нельзя, помиловать!” Когда инструкции следуют одна за другой, компьютер выполняет их в порядке записи. Рассмотрим пример.


int a = 7;

cout << a << '\n';


Здесь объявление с инициализацией выполняется до оператора вывода. В целом мы хотим, чтобы инструкция имела какой-то эффект. Без эффекта инструкции, как правило, бесполезны. Рассмотрим пример.


1+2; // выполняется сложение, но сумму использовать невозможно

a*b; // выполняется умножение, но произведение не используется


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

Упомянем еще об одной разновидности: пустой инструкции. Рассмотрим следующий код:


if (x == 5);

{ y = 3; }


 Это выглядит как ошибка, и это почти правда. Точка с запятой в первой строке вообще-то не должна стоять на этом месте. Но, к сожалению, эта конструкция в языке С++ считается вполне допустимой. Она называется пустой инструкцией, т.е. инструкцией, которая ничего не делает. Пустая инструкция, стоящая перед точкой с запятой, редко бывает полезной. В нашем случае компилятор не выдаст никакого предупреждения об ошибке, и вам будет трудно понять причину неправильной работы программы.

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

x
числу
5
. Если это условие истинно, то будет выполнена следующая инструкция (пустая). Затем программа перейдет к выполнению следующей инструкции, присвоив переменной
y
число
3
. Если же значение переменной
x
не равно
5
, то компилятор не будет выполнять пустую инструкцию (что также не порождает никакого эффекта) и присвоит переменной y число
3
(это не то, чего вы хотели, если значение переменной
x
не равно
5
).

Иначе говоря, эта инструкция

if
присваивает переменной
y
число
3
независимо от значения переменной
x
. Эта ситуация типична для программ, написанных новичкам, причем такие ошибки трудно обнаружить.

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

4.4.1. Инструкции выбора

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

if
и
switch
.

4.4.1.1. Инструкции if

Простейшая форма выбора в языке С++ реализуется с помощью инструкции

if
, позволяющей выбрать одну из двух альтернатив. Рассмотрим пример.


int main()

{

  int a = 0;

  int b = 0;

  cout << "Пожалуйста, введите два целых числа \n";

  cin >> a >> b;

  if (a

           // 1-я альтернатива (выбирается, если условие истинно):

    cout << "max(" << a << "," << b <<") равно " << b <<"\n";

  else

           // 2-я альтернатива (выбирается, когда условие ложно):

    cout << "max(" << a << "," << b <<") равно " << a << "\n";

}


 Инструкция

if
осуществляет выбор из двух альтернатив. Если его условие является истинным, то выполняется первая инструкция; в противном случае выполняется вторая. Это простая конструкция. Она существует в большинстве языков программирования. Фактически большинство основных конструкций в языках программирования представляют собой просто новое обозначение понятий, известных всем еще со школьной скамьи или даже из детского сада. Например, вам, вероятно, говорили в детском саду, что, для того чтобы перейти улицу, вы должны дождаться, пока на светофоре не загорится зеленый свет: “если горит зеленый свет, то можно переходить, а если горит красный свет, то необходимо подождать”. В языке С++ это можно записать как-то так:


if (traffic_light==green) go();

if (traffic_light==red) wait();


Итак, основное понятие является простым, но и это простое понятие можно использовать слишком наивно. Рассмотрим неправильную программу (ошибка в ней заключается не только в отсутствии директивы

#include
).


// преобразование дюймов в сантиметры и наоборот

// суффикс 'i' или 'c' означает единицу измерения на входе

int main()

{

  const double cm_per_inch = 2.54; // количество сантиметров

                                   // в дюйме

  double length = 1;  // длина в дюймах или

                      // сантиметрах

  char unit = 0;

  cout<< "Пожалуйста, введите длину и единицу измерения
 (c или i):\n";

  cin >> length >> unit;

  if (unit == 'i')

    cout << length << "in == " << cm_per_inch*length << "cm\n";

  else

    cout << length << "cm == " << length/cm_per_inch << "in\n";

}


На самом деле эта программа работает примерно так, как предусмотрено: введите

1i
, и вы получите сообщение
1in==2.54cm
введите
2.54c
, и вы получите сообщение
2.54cm==1in
. Поэкспериментируйте — это полезно.

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

unit=='i'
отличает единицу измерения
'i'
от любых других вариантов. Она никогда не проверяет его для единицы измерения
'c'
.

Что произойдет, если пользователь введет

15f
(футов) “просто, чтобы посмотреть, что будет”? Условие (
unit=='i'
) станет ложным, и программа выполнит часть инструкции
else
(вторую альтернативу), преобразовывая сантиметры в дюймы. Вероятно, это не то, чего вы хотели, вводя символ
'f'
.

 Мы должны всегда проверять входные данные программы, поскольку — вольно или невольно — кто-нибудь когда-нибудь введет неверные данные. Программа должна работать разумно, даже если пользователь так не поступает.

Приведем улучшенную версию программы.


// преобразование дюймов в сантиметры и наоборот

// суффикс 'i' или 'c' означает единицу измерения на входе

// любой другой суффикс считается ошибкой

int main()

{

  const double cm_per_inch = 2.54; // количество сантиметров

                                   // в дюйме

  double length = 1;               // длина в дюймах или сантиметрах

  char unit = ' ';                 // пробел - не единица измерения

  cout<< "Пожалуйста, введите длину и единицу измерения (
c или i):\n";

  cin >> length >> unit;

  if (unit == 'i')

    cout << length << "in == " << cm_per_inch*length << "cm\n";

  else if (unit == 'c')

    cout << length << "cm == " << length/cm_per_inch << "in\n";

  else

    cout << "Извините, я не знаю, что такое '" << unit << "'\n";

}


Сначала мы проверяем условие

unit=='i'
, а затем условие
unit=='c'
. Если ни одно из этих условий не выполняется, выводится сообщение "
Извините, ...
". Это выглядит так, будто вы использовали инструкцию "
else-if
", но такой инструкции в языке С++ нет. Вместо этого мы использовали комбинацию двух инструкций
if
. Общий вид инструкции
if
выглядит так:


if (выражение) инструкция else инструкция


Иначе говоря, за ключевым словом

if
следует выражение в скобках, а за ним — инструкция, ключевое слово
else
и следующая инструкция. Вот как можно использовать инструкцию
if
в части
else
инструкции
if
:


if (выражение) инструкция else if (выражение) инструкция else инструкция


В нашей программе этот примем использован так:


if (unit == 'i')

  ... // 1-я альтернатива

else if (unit == 'c')

  ... // 2-я альтернатива

else

  ... // 3-я альтернатива


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


ПОПРОБУЙТЕ

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

4.4.1.2. Инструкции switch

Сравнение единиц измерения с символами

'i'
и
'c'
представляет собой наиболее распространенную форму выбора: выбор, основанный на сравнении значения с несколькими константами. Такой выбор настолько часто встречается на практике, что в языке C++ для него предусмотрена отдельная инструкция:
switch
. Перепишем наш пример в ином виде


int main()

{

  const double cm_per_inch = 2.54; // количество сантиметров

                                   // в дюйме

  double length = 1; // длина в дюймах или сантиметрах

  char unit = 'a';

  cout<< "Пожалуйста, введите длину и единицу измерения 
(c или i):\n";

  cin >> length >> unit;

  switch (unit) {

  case 'i':

    cout << length << " in == " << cm_per_inch*length << " cm\n";

    break;

  case 'c':

    cout << length << " cm == " << length/cm_per_inch << " in\n";

    break;

  default:

    cout << "Извините, я не знаю, что такое '" << unit << "'\n";

    break;

  }

}


 Синтаксис оператора

switch
архаичен, но он намного яснее вложенных инструкций
if
, особенно если необходимо сравнить значение со многими константами. Значение, указанное в скобках после ключевого слова
switch
, сравнивается с набором констант. Каждая константа представлена как часть метки
case
. Если значение равно константе в метке
case
, то выбирается инструкция из данного раздела
case
. Каждый раздел case завершается ключевым словом
break
. Если значение не соответствует ни одной метке
case
, то выбирается оператор, указанный в разделе
default
. Этот раздел не обязателен, но желателен, чтобы гарантировать перебор всех альтернатив. Если вы еще не знали, то знайте, что программирование приучает человека сомневаться практически во всем.

4.4.1.3. Технические подробности инструкции switch

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

switch
.

1. Значение, которое определяет выбор варианта, должно иметь тип

int
,
char
или
enum
(см. раздел 9.5). В частности, переключение по строке произвести невозможно.

2. Значения меток разделов

case
должны быть константными выражениями (см. раздел 4.3.1). В частности, переменная не может быть меткой раздела
case
.

3. Метки двух разделов

case
не должны иметь одинаковые значения.

4. Один раздел

case
может иметь несколько меток.

5. Не забывайте, что каждый раздел

case
должен завершаться ключевым словом
break
. К сожалению, компилятор не предупредит вас, если вы забудете об этом.


Рассмотрим пример.


int main() // переключение можно производить только по целым

           // числам и т.п.

{

  cout << "Вы любите рыбу?\n";

  string s;

  cin >> s;

  switch (s) { // ошибка: значение должно иметь тип int,

               // char или enum

  case " нет ":

    // ...

    break;

  case " да ":

    // ...

    break;

  }

}


Для выбора альтернатив по строке следует использовать инструкцию

if
или ассоциативный массив (подробнее об этом речь пойдет в главе 21). Инструкция
switch
генерирует оптимизированный код для сравнения значения с набором констант. Для крупных наборов констант он обычно создает более эффективный код по сравнению с коллекцией инструкций
if
. Однако это значит, что значения меток разделов
case
должны быть разными константами. Рассмотрим пример.


int main() // метки разделов case должны быть константами

{

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

  int y = 'y'; // это может создать проблемы

  const char n = 'n';

  const char m = '?';

  cout << "Вы любите рыбу ?\n";

  char a;

  cin >> a;

  switch (a) {

  case n:

    // ...

    break;

  case y:  // ошибка: переменная метка раздела case

    // ...

    break;

  case m:

    // ...

    break;

  case 'n': // ошибка: дубликат метки раздела case

            // (значение метки n равно 'n')

    // ...

    break;

  default:

    // ...

    break;

  }

}


Часто для разных значений инструкции

switch
целесообразно выполнить одно и то же действие. Было бы утомительно повторять это действие для каждой метки из этого набора. Рассмотрим пример.


int main() // одна инструкция может иметь несколько меток

{

  cout << "Пожалуйста, введите цифру \n";

  char a;

  cin >> a;

  switch (a) {

  case '0': case '2': case '4': case '6': case '8':

    cout << " четная \n";

    break;

  case '1': case '3': case '5': case '7': case '9':

    cout << " нечетная \n";

    break;

  default:

    cout << " не цифра \n";

    break;

  }

}


 Чаще всего, используя инструкцию

switch
, программисты забывают завершить раздел
case
ключевым словом
break
. Рассмотрим пример.


int main() // пример плохой программы (забыли об инструкции break)

{

  const double cm_per_inch = 2.54; // количество сантиметров

                                   // в дюйме

  double length = 1;  // длина в дюймах или сантиметрах

  char unit = 'a';

  cout << "Пожалуйста, введите длину и единицу 
измерения (c или i):\n";

  cin >> length >> unit;

  switch (unit) {

  case 'i':

    cout << length << "in == " << cm_per_inch*length << "cm\n";

  case 'c':

    cout << length << "cm == " << length/cm_per_inch << "in\n";

  }

}


К сожалению, компилятор примет этот текст, и когда вы закончите выполнение раздела

case
с меткой
'i'
, просто “провалитесь” в раздел case с меткой
'c'
, так что при вводе строки
2i
программа выведет на экран следующие результаты:


2in == 5.08cm

2cm == 0.787402in


Мы вас предупредили!


ПОПРОБУЙТЕ

Перепишите программу преобразования валют из предыдущего раздела, используя инструкцию

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

4.4.2. Итерация

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

4.4.2.1. Инструкции while

В качестве примера итерации рассмотрим первую программу, выполненную на компьютере EDSAC. Она была написана Дэвидом Уилером (David Wheeler) в компьютерной лаборатории Кэмбриджского университета (Cambridge University, England) 6 мая 1949 года. Эта программа вычисляет и распечатывает простой список квадратов.


0 0

1 1

2 4

3 9

4 16

...

98 9604

99 9801


Здесь в каждой строке содержится число, за которым следуют знак табуляции (

'\t'
) и квадрат этого числа. Версия этой программы на языке C++ выглядит так:


// вычисляем и распечатываем таблицу квадратов чисел 0–99

int main()

{

  int i = 0;   // начинаем с нуля

  while (i<100) {

    cout << i << '\t' << square(i) << '\n';

    ++i;       // инкрементация i (т.е. i становится равным i+1)

  }

}


Обозначение

square(i)
означает квадрат числа
i
. Позднее, в разделе 4.5, мы объясним, как это работает.

Нет, на самом деле первая современная программа не была написана на языке С++, но ее логика была такой же.

• Вычисления начинаются с нуля.

• Проверяем, не достигли ли мы числа 100, и если достигли, то завершаем вычисления.

• В противном случае выводим число и его квадрат, разделенные символом табуляции (

'\t'
), увеличиваем число и повторяем вычисления. Очевидно, что для этого необходимо сделать следующее.

• Способ для повторного выполнения инструкции (цикл).

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

int
и называется
i
.

• Начальное значение счетчика цикла (в данном случае — 0).

• Критерий прекращения вычислений (в данном случае мы хотим выполнить возведение в квадрат 100 раз).

• Сущность, содержащая инструкции, находящиеся в цикле (тело цикла).


В данной программе мы использовали инструкцию

while
. Сразу за ключевым словом
while
следует условие и тело цикла.


while (i<100) // условие цикла относительно счетчика i

{

  cout << i << '\t' << square(i) << '\n';

  ++i; // инкрементация счетчика цикла i

}


Тело цикла — это блок (заключенный в фигурные скобки), который распечатывает таблицу и увеличивает счетчик цикла

i
на единицу. Каждое повторение цикла начинается с проверки условия
i<100
. Если это условие истинно, то мы не заканчиваем вычисления и продолжаем выполнять тело цикла. Если же мы достигли конца, т.е. переменная
i
равна 100, выходим из инструкции
while
и выполняем инструкцию, следующую за ней. В этой программе после выхода из цикла программа заканчивает работу, поэтому мы из нее выходим.

Счетчик цикла для инструкции

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


int i = 0; // начинаем вычисления с нуля


и все станет хорошо.

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


ПОПРОБУЙТЕ

Символ

'b'
равен
char('a'+1)
,
'c'
— равен
char('a'+2)
и т.д. Используя цикл, выведите на экран таблицу символов и соответствующих им целых чисел.


a 97

b 98

...

z 122

4.4.2.2. Блоки

Обратите внимание на то, как мы сгруппировали две инструкции, подлежащие выполнению.


while (i<100) {

  cout << i << '\t' << square(i) << '\n';

  ++i; // инкрементация i (т.е. i становится равным i+1)

}


 Последовательность инструкций, заключенных в фигурные скобки (

{
и
}
), называется блоком, или составной инструкцией. Блок — это разновидность инструкции. Пустой блок (
{}
) иногда оказывается полезным для выражения того, что в данном месте программы не следует ничего делать. Рассмотрим пример.


if (a<=b) { // ничего не делаем

}

else {      // меняем местами a и b

  int t = a;

  a = b;

  b = t;

}

4.4.2.3. Инструкции for

 Итерация над последовательностями чисел настолько часто используется в языке С++, как и в других языках программирования, что для этой операции предусмотрена специальная синтаксическая конструкция. Инструкция

for
похожа на инструкцию
while
за исключением того, что управление счетчиком цикла сосредоточено в его начале, где за ним легко следить. Первую программу можно переписать так:


// вычисляем и распечатываем таблицу квадратов чисел 0–99

int main()

{

  for (int i = 0; i<100; ++i)

    cout << i << '\t' << square(i) << '\n';

}


Это значит: “Выполнить тело цикла, начиная с переменной

i
, равной нулю, и увеличивать ее на единицу при каждом выполнении тела цикла, пока переменная
i
не станет равной
100
”. Инструкция
for
всегда эквивалентна некоей инструкции
while
. В данном случае конструкция


for (int i = 0; i<100; ++i)

  cout << i << '\t' << square(i) << '\n';


эквивалентна


{

  int i = 0;      // инициализатор инструкции for

  while (i<100) { // условие инструкции for

    cout << i << '\t' << square(i) << '\n'; // тело инструк
ции for

    ++i;          // инкрементация инструкции for

  }

}


 Некоторые новички предпочитают использовать инструкции

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

 Никогда не изменяйте счетчик цикла в теле инструкции

for
. Это нарушит все разумные предположения читателя программы о содержании цикла. Рассмотрим пример.


int main()

{

  for (int i = 0; i<100; ++i) { // для i из диапазона [0:100)

    cout << i << '\t' << square(i) << '\n';

    ++i; // Что это? Похоже на ошибку!

  }

}


Любой читатель, увидевший этот цикл, разумно предположит, что его тело будет выполнено 100 раз. Однако это не так. Инструкция

++i
в его теле гарантирует, что счетчик каждый раз будет инкрементирован дважды, так что вывод будет осуществлен только для 50 четных чисел. Увидев такой код, вы можете предположить, что это ошибка, вызванная некорректным преобразованием инструкции
for
из инструкции
while
. Если хотите, чтобы счетчик увеличивался на
2
, сделайте следующее:


// вычисляем и выводим на печать таблицу квадратов

// четных чисел из диапазона [0:100]

int main()

{

  for (int i = 0; i<100; i+=2)

    cout << i << '\t' << square(i) << '\n';

}


Пожалуйста, учтите, что ясная и простая программа короче запутанной. Это общее правило.


ПОПРОБУЙТЕ

Перепишите программу, выводящую на печать символы и соответствующие им целые числа с помощью инструкции

for
. Затем модифицируйте программу так, чтобы таблица содержала прописные символы и цифры.

4.5. Функции

В приведенной выше программе осталось невыясненной роль выражения

square(i)
. Это вызов функции. Конкретнее, это вызов функции, вычисляющей квадрат аргумента
i
. Функция — это именованная последовательность инструкций. Она может возвращать результат, который также называется возвращаемым значением.

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

sqrt()
, использованная в разделе 3.4. Однако многие функции мы пишем самостоятельно. Рассмотрим возможное определение функции
square
.


int square(int x) // возвращает квадрат числа x

{

  return x*x;

}


Первая строка этого определения утверждает, что это функция (об этом говорят скобки), которая называется

square
, принимающая аргумент типа
int
(с именем) и возвращающая значение типа
int
(тип результата всегда предшествует объявлению функции); иначе говоря, ее можно использовать примерно так:


int main()

{

  cout << square(2) << '\n';  // выводим 4

  cout << square(10) << '\n'; // выводим 100

}


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


square(2);              // возвращаемое значение не используется

int v1 = square();      // ошибка: пропущен аргумент

int v2 = square;        // ошибка: пропущены скобки

int v3 = square(1,2);   // ошибка: слишком много аргументов

int v4 = square("two"); // ошибка: неверный тип аргумента —

                        // ожидается int


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

two
", вы на самом деле имели в виду число
2
. Однако компилятор языка С++ совсем не так умен. Компьютер просто проверяет, соответствуют ли ваши инструкции синтаксическим правилам языка С++, и точно их выполняет. Если компилятор станет угадывать, что вы имели в виду, то он может ошибиться и вы — или пользователи вашей программы — будете огорчены. Достаточно сложно предсказать, что будет делать ваша программа, если компилятор будет пытаться угадывать ваши намерения.

Тело функции является блоком (см. раздел 4.4.2.2), который выполняет реальную работу.


{

  return x*x; // возвращаем квадрат числа x

}


Для функции

square
эта работа тривиальна: мы вычисляем квадрат аргумента и возвращаем его в качестве результата. Выразить это на языке С++ проще, чем на естественном языке. Это типично для простых идей. Помимо всего прочего, язык программирования предназначен именно для простого и точного выражения таких простых идей.

Синтаксис определения функции можно описать так:


тип идентификатора (список параметров) тело функции


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

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

void
(означающее “ничего”). Рассмотрим пример.


void write_sorry() // не принимает никаких аргументов;

                   // ничего не возвращает

{

  cout << "Извините \n";

}


Специфические аспекты, связанные с языком программирования, будут описаны в главе 8.

4.5.1. Зачем нужны функции

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

• Эти вычисления логически отделены от других.

• Отделение вычислений делает программу яснее (с помощью присваивания имен функциям).

• Функцию можно использовать в разных местах программы.

• Использование функций упрощает отладку программы.


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

x*x
, или
7*7
, или
(x+7)*(x+7)
, а не
square(x)
,
square(7)
или
square(x+7)
. Однако функция square сильно упрощает такие вычисления. Рассмотрим теперь извлечение квадратного корня (в языке С++ эта функция называется
sqrt
): можете написать выражение
sqrt(x)
, или
sqrt(7)
, или
sqrt(x+7)
, а не повторять код, вычисляющий квадратный корень, запутывая программу. И еще один аргумент: можете даже не интересоваться, как именно вычисляется квадратный корень числа в функции
sqrt(x)
, — достаточно просто передать функции аргумент
x
.

В разделе 8.5 мы рассмотрим множество технических деталей, связанных с функциями, а пока рассмотрим еще один пример. Если мы хотим действительно упростить цикл в функции

main()
, то можно было бы написать такой код:


void print_square(int v)

{

  cout << v << '\t' << v*v << '\n';

}


int main()

{

  for (int i = 0; i<100; ++i) print_square(i);

}


Почему же мы не использовали версию программы на основе функции

print_square()
? Дело в том, что эта программа ненамного проще, чем версия, основанная на функции
square()
, и, кроме того,

• функция

print_square()
является слишком специализированной и вряд ли будет использована в другой программе, в то время как функция
square()
, скорее всего, будет полезной для других пользователей;

• функция

square()
не требует подробной документации, а функция
print_square()
очевидно требует пояснений.


Функция

print_square()
выполняет два логически отдельных действия:

• печатает числа;

• вычисляет квадраты.


Программы легче писать и понимать, если каждая функция выполняет отдельное логическое действие. По этой причине функция

square()
является более предпочтительной.

В заключение попробуем ответить, почему мы использовали функцию

square(i)
, а не выражение
i*i
, использованное в первой версии программы? Одной из целей функций является упрощение кода путем распределения сложных вычислений по именованным функциям, а для программы 1949 года еще не было аппаратного обеспечения, которое могло бы непосредственно выполнить операцию “умножить”. По этой причине в первоначальной версии этой программы выражение
i*i
представляло собой действительно сложное вычисление, как если бы вы выполняли его на бумаге. Кроме того, автор исходной версии, Дэвид Уилер, ввел понятие функций (впоследствии названных процедурами) в современном программировании, поэтому было вполне естественно, что он использовал их в своей программе.


ПОПРОБУЙТЕ

Реализуйте функцию

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

4.5.2. Объявления функций

Вы заметили, что вся информация, необходимая для вызова функции, содержится в первой строке ее объявления? Рассмотрим пример.


int square(int x)


Этой строки уже достаточно, чтобы написать инструкцию


int x = square(44);


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

sqrt()
? Мы знаем, что она извлекает квадратный корень из своего аргумента. А зачем нам знать, как устроено тело функции
square()
? Разумеется, в нас может разжечься любопытство. Но в подавляющем большинстве ситуаций достаточно знать, как вызвать функцию, взглянув на ее определение. К счастью, в языке С++ существует способ, позволяющий получить эту информацию, не заглядывая в тело функции. Эта конструкция называется объявлением функции.


int square(int);      // объявление функции square

double sqrt(double);  // объявление функции sqrt


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


int square(int x) // определение функции square

{

  return x*x;

}


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

#include
. Определение функции может быть в любом другом месте. Это “любое другое место” мы укажем в разделах 8.3 и 8.7. В более крупных программах разница между объявлениями и определениями становится существеннее. В этих программах определения позволяют сосредоточиться на локальном фрагменте программы (см. раздел 4.2), не обращая внимания на остальную часть кода.

4.6. Вектор

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

vector
(вектор).

Вектор — это последовательность элементов, к которым можно обращаться по индексу. Например, рассмотрим объект типа

vector
с именем
v
.



Иначе говоря, индекс первого элемента равен 0, индекс второго элемента — 1 и т.д. Мы ссылаемся на элемент, указывая имя вектора и индекс элемента в квадратных скобках, так что значение

v[0]
равно
5
, значение
v[1]
равно
7
и т.д. Индексы вектора всегда начинаются с нуля и увеличиваются на единицу. Это вам должно быть знакомым: вектор из стандартной библиотеки С++ — это просто новый вариант старой и хорошо известной идеи. Я нарисовал вектор так, как показано на рисунке, чтобы подчеркнуть, что вектор “знает свой размер”, т.е. всегда хранит его в одной из ячеек.

Такой вектор можно создать, например, так:


vector v(6); // вектор из 6 целых чисел

v[0] = 5;

v[1] = 7;

v[2] = 9;

v[3] = 4;

v[4] = 6;

v[5] = 8;


Как видим, для того чтобы создать вектор, необходимо указать тип его элементов и их начальные значения. Тип элементов вектора указывается после слова

vector
в угловых скобках (
<>
). Здесь использован тип
, а количество элементов указано после имени в круглых скобках (
(6)
). Рассмотрим еще один пример.


vector philosopher(4); // вектор из 4 строк

philosopher [0] = "Kant";

philosopher [1] = "Plato";

philosopher [2] = "Hume";

philosopher [3] = "Kierkegaard";


Естественно, в векторе можно хранить элементы только одного типа.


philosopher[2] = 99; // ошибка: попытка присвоить целое число строке

v[2] = "Hume";       // ошибка: попытка присвоить строку целому числу


Когда мы объявляем объект типа

vector
с заданным размером, его элементы принимают значения, заданные по умолчанию для указанного типа. Рассмотрим пример.


vector v(6);  // вектор из 6 целых чисел инициализируется нулями

vector philosopher(4); // вектор из 4 строк инициализируется

                               // значениями ""


Если вам не подходят значения, заданные по умолчанию, можете указать другие. Рассмотрим пример.


vector vd(1000,–1.2); // вектор из 1000 действительных

                              // чисел, инициализированных как –1.2


Пожалуйста, обратите внимание на то, что мы не можем просто сослаться на несуществующий элемент вектора.


vd[20000] = 4.7; // ошибка во время выполнения программы


Ошибки, возникающие во время выполнения программы, и работа с индексами описаны в следующей главе.

4.6.1. Увеличение вектора

 Часто мы начинаем работу с пустым вектором и увеличиваем его размер по мере считывания или вычисления данных. Для этого используется функция

push_back()
, добавляющая в вектор новый элемент. Новый элемент становится последним элементом вектора. Рассмотрим пример.


vector v; // начинаем с пустого вектора,

                  // т.е. объект v не содержит ни одного элемента



v.push_back(2.7); // добавляем в конец вектора v элемент

                  // со значением 2.7

                  // теперь вектор v содержит один элемент

                  // и v[0]==2.7



v.push_back(5.6); // добавляем в конец вектора v элемент

                  // со значением 5.6

                  // теперь вектор v содержит два элемента

                  // и v[1]==5.6



v.push_back(7.9); // добавляем в конец вектора v элемент

                  // со значением 7.9

                  // теперь вектор v содержит три элемента

                  // и v[2]==7.9



Обратите внимание на синтаксис вызова функции

push_back()
. Он называется вызовом функции-члена; функция
push_back()
является функцией-членом объекта типа
vector
, и поэтому для ее вызова используется особая форма вызова.


вызов функции-члена:

имя_объекта.имя_функции_члена(список_аргументов)


Размер вектора можно определить, вызвав другую функцию-член объекта типа

vector: size()
. В начальный момент значение
v.size()
равно 0, а после третьего вызова функции
push_back()
значение
v.size()
равно
3
. Зная размер вектора, легко выполнить цикл по всем элементам вектора. Рассмотрим пример.


for(int i=0; i

  cout << "v[" << i << "]==" << v[i] << '\n';


Этот цикл выводит на экран следующие строки:


v[0]==2.7

v[1]==5.6

v[2]==7.9


Если вы имеете опыт программирования, то можете заметить, что тип

vector
похож на массив в языке С и других языках. Однако вам нет необходимости заранее указывать размер (длину) вектора, и вы можете добавлять в него элементы по мере необходимости. В дальнейшем мы убедимся, что тип
vector
из стандартной библиотеки С++ обладает и другими полезными свойствами.

4.6.2. Числовой пример

Рассмотрим более реалистичный пример. Часто нам требуется считать коллекцию данных в программу и что-то с ними сделать. Это “что-то” может означать построение графика, вычисление среднего и медианы, сортировку, смешивание с другими данными, поиск интересующих нас значений, сравнение с другими данными и т.п. Перечислять операции с данными можно бесконечно, но сначала данные необходимо считать в память компьютера. Рассмотрим основной способ ввода неизвестного — возможно, большого — объема данных. В качестве конкретного примера попробуем считать числа с плавающей точкой, представляющие собой значения температуры.


// считываем значения температуры в вектор

int main()

{

  vector temps;    // значения температуры

  double temp;

  while (cin>>temp)        // считываем

    temps.push_back(temp); // записываем в вектор

  // ...что-то делаем...

}


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


vector temps; // значения температуры

double temp;


Вот где указывается тип входных данных. Как видим, мы считываем и храним числа типа

double
.

Теперь выполняется цикл считывания.


while (cin>>temp)        // считываем

  temps.push_back(temp); // записываем в вектор


Инструкция

cin>>temp
считывает число типа
double
, а затем это число “заталкивается” в вектор (записывается в конец вектора). Эти операции уже были продемонстрированы выше. Новизна здесь заключается в том, что в качестве условия выхода из цикла
while
мы используем операцию ввода
cin>>temp
. В основном условие
cin>>temp
является истинным, если значение считано корректно, в противном случае оно является ложным. Таким образом, в цикле
while
считываются все числа типа
double
, пока на вход не поступит нечто иное. Например, если мы подадим на вход следующие данные


1.2 3.4 5.6 7.8 9.0 |


то в вектор

temps
будут занесены пять элементов:
1.2
,
3.4
,
5.6
,
7.8
,
9.0
(именно в таком порядке, т.е.
temps[0]==1.2
). Для прекращения ввода используется символ
|
, т.е. значение, не имеющее тип
double
. В разделе 10.6 мы обсудим способы прекращения ввода и способы обработки ошибок ввода.

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


// вычисляем среднее и медиану значений температур

int main()

{

  vector temps; // значения температур

  double temp;

  while (cin>>temp) // считываем данные

    temps.push_back(temp); // заносим их в вектор

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

  double sum = 0;

  for (int i = 0; i < temps.size(); ++i) sum += temps[i];

  cout << "Average temperature: " << sum/temps.size() << endl;

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

  sort(temps.begin(),temps.end()); // сортируем значения

                                   // температуры

                                   // "от начала до конца"

  cout << "Медиана температуры: " << temps[temps.size()/2] << endl;

}


Мы вычисляем среднее значение, просто суммируя все элементы и деля сумму на количество элементов (т.е. на значение

temps.size()
).


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

double sum = 0;

for (int i = 0; i < temps.size(); ++i) sum += temps[i];

cout << "Средняя температура: " << sum/temps.size() << endl;


Обратите внимание, насколько удобным оказался оператор

+=
. Для вычисления медианы (значения, относительно которого половина всех значений оказывается меньше, в другая половина — больше) элементы следует упорядочить. Для этой цели используется алгоритм
sort()
из стандартной библиотеки.


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

sort(temps.begin(),temps.end()); // сортировка

cout << "Медиана температуры: " << temps[temps.size()/2] << endl;


Стандартная функция

sort()
принимает два аргумента: начало и конец сортируемой последовательности. Этот алгоритм будет рассмотрен позднее (в главе 20), но, к счастью, вектор “знает” свое начало и конец, поэтому нам не следует беспокоиться о деталях: эту работу выполняют функции
temps.begin()
и
temps.end()
. Обратите внимание на то, что функции
begin()
и
end()
являются функциями-членами объекта типа
vector
, как и функция
size()
, поэтому мы вызываем их из вектора с помощью точки. После сортировки значений температуры медиану легко найти: мы просто находим средний элемент, т.е. элемент с индексом
temps.size()/2
. Если проявить определенную придирчивость (характерную для программистов), то можно обнаружить, что найденное нами значение может оказаться не медианой в строгом смысле. Решение этой маленькой проблемы описано в упр. 2.

4.6.3. Текстовый пример

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


// простой словарь : список упорядоченных слов

int main()

{

  vector words;

  string temp;

  while (cin>>temp) // считываем слова, отделенные разделителями

    words.push_back(temp); // заносим в вектор

  cout << "Количество слов: " << words.size() << endl;

  sort(words.begin(),words.end()); // сортируем весь вектор

  for (int i = 0; i < words.size(); ++i)

    if (i==0 || words[i–1]!=words[i]) // это новое слово?

  cout << words[i] << "\n";

}


Если в эту программу ввести несколько слов, то она выведет их в алфавитном порядке без повторов. Например, допустим, что в программу вводятся слова


man a plan panama


В ответ программа выведет на экран следующие слова:


a

man

panama

plan


Как остановить считывание строки? Иначе говоря, как прекратить цикл ввода?


while (cin>>temp) // считываем

  words.push_back(temp); // заносим в вектор


Когда мы считывали числа (см. раздел 4.6.2), для прекращения ввода просто вводили какой-то символ, который не был числом. Однако для строк этот прием не работает, так как в строку может быть считан любой (одинарный) символ. К счастью, существуют символы, которые не являются одинарными. Как указывалось в разделе 3.5.1, в системе Windows поток ввода останавливается нажатием клавиш , а в системе Unix — .

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


if (i==0 || words[i–1]!=words[i]) // это новое слово?


Если удалить эту проверку из программы, то вывод изменится.


a

a

man

panama

plan


Мы не любим повторений, поэтому удаляем их с помощью данной проверки. Что она делает? Она выясняет, отличается ли предыдущее слово от вновь веденного (

words[i-1]!=words[i]
), и если отличается, то слово выводится на экран, а если нет, то не выводится. Очевидно, что у первого слова предшественника нет (
i==0
), поэтому сначала следует проверить первый вариант и объединить эти проверки с помощью оператора
||
(или).


if (i==0 || words[i–1]!=words[i]) // это новое слово?


Обратите внимание на то, что мы можем сравнивать строки. Для этого мы используем операторы

!=
(не равно);
==
(равно),
<
(меньше),
<=
(меньше или равно), 
>
(больше) и
>=
(больше или равно), которые можно применять и к строкам. Операторы, и тому подобные основаны на лексикографическом порядке, так что строка "
Ape
" предшествует строкам "
Apple
" и "
Chimpanzee
".


ПОПРОБУЙТЕ

Напишите программу, заглушающую нежелательные слова; иначе говоря, считайте слова из потока

cin
и выведите их в поток
cout
, заменив нежелательные слова словом
BLEEP
. Начните с одного нежелательного слова, например


string disliked = "Broccoli";


Когда отладите программу, добавьте еще несколько нежелательных слов.

4.7. Свойства языка

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

for
и
while
), выбор (инструкция
if
), простые арифметические инструкции (операторы
++
и
+=
), логические операторы и операторы сравнения (
==
,
!=
и
||
), переменные и функции (например,
main()
,
sort()
и
size()
). Кроме того, мы использовали возможности стандартной библиотеки, например
vector
(контейнер элементов),
cout
(поток вывода) и
sort()
(алгоритм).

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


Задание

Выполните задание шаг за шагом. Не следует торопиться и пропускать этапы. На каждом этапе проверьте программу, введя по крайней мере три пары значений — чем больше, тем лучше.

1. Напишите программу, содержащую цикл

while
, в котором считываются и выводятся на экран два целых числа. Для выхода из программы введите символ.

2. Измените программу так, чтобы она выводила на экран строку "

Наименьшее из двух значений равно:
", а затем — меньшее и большее значения.

3. Настройте программу так, чтобы она выводила только равные числа.

4. Измените программу так, чтобы она работала с числами типа

double
, а не
int
.

5. Измените программу так, чтобы она выводила числа, которые почти равны друг другу. При этом, если числа отличаются меньше, чем на 1.0/100, то сначала следует вывести меньшее число, а затем большее.

6. Теперь измените тело цикла так, чтобы он считывал только одно число типа

double
за один проход. Определите две переменные, чтобы определить, какое из них имеет меньшее значение, а какое — большее среди всех ранее введенных значений. За каждый проход цикла выводите на экран одно введенное число. Если оно окажется наименьшим среди ранее введенных, выведите на экран строку "
Наименьшее среди ранее введенных
". Если оно окажется наибольшим среди ранее введенных, выведите на экран строку "
Наибольшее среди ранее введенных
".

7. Добавьте к каждому введенному числу типа

double
единицу измерения; иначе говоря, введите значения, такие как
10cm
,
2.5in
,
5ft
или
3.33m
. Допустимыми являются четыре единицы измерения:
cm
,
m
,
in
,
ft
. Коэффициенты преобразования равны:
1m==100cm
,
1in==2.54cm
,
1ft==12in
. Индикаторы единиц измерения введите в строку.

8. Если введена неправильная единица измерения, например

yard
,
meter
,
km
и
gallons
, то ее следует отклонить.

9. Вычислите сумму введенных значений (помимо наименьшего и наибольшего) и определите их количество. Когда цикл закончится, выведите на экран наименьшее значение, наибольшее значение, количество значений и их сумму. Обратите внимание на то, что накапливая сумму, вы должны выбрать единицу измерения (используйте метры).

10. Сохраните все введенные значения (преобразованные в метры) в векторе и выведите их на экран.

11. Перед тем как вывести значения из вектора, отсортируйте их в возрастающем порядке.


Контрольные вопросы

1. Что такое вычисления?

2. Что подразумевается под входными данными и результатами вычислений?

Приведите примеры.

3. Какие три требования должен иметь в виду программист при описании вычислений?

4. Для чего предназначены выражения?

5. В чем разница между инструкцией и выражением?

6. Что такое значение

lvalue
? Перечислите операторы, требующие наличия значения
lvalue
. Почему именно эти, а не другие операторы требуют наличия значения
lvalue
?

7. Что такое константное выражение?

8. Что такое литерал?

9. Что такое символическая константа и зачем она нужна?

10. Что такое “магическая” константа? Приведите примеры.

11. Назовите операторы, которые можно применять как к целым числам, так и к числам с плавающей точкой.

12. Какие операторы можно применять только к целым числам, но не к числам с плавающей точкой?

13. Какие операторы можно применять к строкам?

14. Когда оператор

switch
предпочтительнее оператора
if
?

15. Какие проблемы порождает использование оператора

switch
?

16. Объясните, каково предназначение каждой части заголовка цикла

for
и в каком порядке они выполняются?

17. Когда используется оператор

for
, а когда оператор
while
?

18. Как вывести числовой код символа?

19. Опишите смысл выражения

char foo(int x)
в определении функции.

20. Когда часть программы следует оформить в виде функции? Назовите причины.

21. Какие операции можно выполнить над объектом типа

int
, но нельзя применить к объекту типа
string
?

22. Какие операции можно выполнить над объектом типа

string
, но нельзя применить к объекту типа
int
?

23. Чему равен индекс третьего элемента вектора?

24. Напишите цикл

for
, в котором выводятся все элементы вектора?

25. Что делает выражение

vector alphabet(26);
?

26. Что делает с вектором функция

push_back()
?

27. Что делают функции-члены вектора

begin()
,
end()
и
size()
?

28. Чем объясняется полезность и популярность типа

vector
?

29. Как упорядочить элементы вектора?


Термины


Упражнения

1. Выполните задание ПОПРОБУЙТЕ, если еще не сделали этого раньше.

2. Допустим, мы определяем медиану последовательности как “число, относительно которого ровно половина элементов меньше, а другая половина — больше”. Исправьте программу из раздела 4.6.2 так, чтобы она всегда выводила медиану. Подсказка: медиана не обязана быть элементом последовательности.

3. Считайте последовательности чисел типа

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

4. Напишите программу, угадывающую число. Пользователь должен задумать число от 1 до 100, а программа должна задавать вопросы, чтобы выяснить, какое число он задумал (например, “Задуманное число меньше 50”). Ваша программа должна уметь идентифицировать число после не более семи попыток. Подсказка: используйте операторы

<
и
<=
, а также конструкцию
if-else
.

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

double
и символ операции. Если входные аргументы равны
35.6
,
24.1
и
'+'
, то программа должна вывести на экран строку "
Сумма 35.6 и 24.1 равна 59.7
". В главе 6 мы опишем более сложный калькулятор.

6. Создайте вектор, хранящий десять строковых значений "

zero
", "
one
", ..., "
nine
". Введите  их  в  программу,  преобразующую  цифру  в  соответствующее строковое представление; например, при вводе цифры  7 на экран должна быть выведена строка
seven
. С помощью этой же программы, используя тот же самый цикл ввода, преобразуйте строковое представление цифры в числовое; например, при вводе строки
seven
на экран должна быть выведена цифра.

7. Модифицируйте мини-калькулятор, описанный в упр. 5, так, чтобы он принимал на вход цифры, записанные в числовом или строковом формате.

8. Легенда гласит, что некий царь захотел поблагодарить изобретателя шахмат и предложил ему попросить любую награду. Изобретатель попросил положить на первую клетку одно зерно риса, на вторую — 2, на третью — 4 и т.д., удваивая количество зерен на каждой из 64 клеток. На первый взгляд это желание выглядит вполне скромным, но на самом деле в царстве не было такого количества риса! Напишите программу, вычисляющую, сколько клеток надо заполнить, чтобы изобретатель получил хотя бы 1000 зерен риса, хотя бы 1 000 000 зерен риса и хотя бы 1 000 000 000 зерен риса. Вам, разумеется, понадобится цикл и, вероятно, переменная типа

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

9. Попробуйте вычислить число зерен риса, запрошенных изобретателем шахмат в упр. 8. Оказывается, что это число настолько велико, что для его хранения не подходит ни тип

int
, ни
double
. Определите наибольшее количество клеток, на котором еще может поместиться столько зерен риса, чтобы хранить их количество в переменной типа
int
. Определите наибольшее количество клеток, на котором еще может поместиться столько зерен риса, чтобы хранить их примерное количество в переменной типа
double
?

10. Напишите программу для игры “Камень, бумага, ножницы”. Если вы не знаете правил этой игры, попробуйте выяснить их у друзей или с помощью поисковой машины Google. Такие исследования — обычное занятие программистов. Для решения поставленной задачи используйте инструкцию

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

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

primes
, то
primes[0]==2
,
primes[1]==3
,
primes[2]==5
и т.д.). Напишите цикл перебора чисел от 1 до 100, проверьте каждое из них и сохраните найденные простые числа в векторе. Напишите другой цикл, в котором все найденные простые числа выводятся на экран. Сравните полученные результаты с вектором
primes
. Первым простым числом считается число
2
.

12. Измените программу из предыдущего упражнения так, чтобы в нее вводилось число

max
, а затем найдите все простые числа от 1 до
max
.

13. Напишите программу, находящую все простые числа от 1 до 100. Для решения этой задачи существует классический метод “Решето Эратосфена”. Если этот метод вам неизвестен, поищите его описание в веб. Напишите программу на основе этого метода.

14. Измените программу, описанную в предыдущем упражнении, так, чтобы в нее вводилось число

max
, а затем найдите все простые числа от
1
до
max
.

15. Напишите программу, принимающую на вход число

n
и находящую первые
n
простых чисел.

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

17. Напишите программу, определяющую наименьшее и наибольшее числа, а также моду последовательности строк.

18. Напишите программу для решения квадратичных уравнений. Квадратичное уравнение имеет вид

2
. Если вы не знаете формул для решения этого уравнения, проведите дополнительные исследования. Напоминаем, что программисты часто проводят такие исследования, прежде чем приступают к решению задачи. Для ввода чисел
a
,
b
и с используйте переменные типа
double
. Поскольку квадратичное уравнение имеет два решения, выведите оба значения,
x1
и
x2
.

19. Напишите программу, в которую сначала вводится набор пар, состоящих из имени и значения, например

Joe 17
и
Barbara 22
. Для каждой пары занесите имя в вектор
names
, а число — в вектор
scores
(в соответствующие позиции, так что если
names[7]=="Joe"
, то
scores[7]==17
). Прекратите ввод, введя строку
NoName 0
. Убедитесь, что каждое имя уникально, и выведите сообщение об ошибке, если имя введено дважды. Выведите на печать все пары (имя, баллы) по одной в строке.

20. Измените программу из упр. 19 так, чтобы при вводе имени она выводила соответствующее количество баллов или сообщение "

name not found
".

21. Измените программу из упр. 19 так, чтобы при вводе целого числа она выводила все имена студентов, получивших заданное количество баллов или сообщение "

score not found
".


Послесловие

С философской точки зрения вы уже можете делать с помощью компьютера все, что захотите, — остальное детали! Разумеется, важность деталей и практических навыков несомненна, поскольку вы только начинаете программировать. Но мы говорим серьезно. Инструменты, представленные в главе, позволяют описывать любые вычисления: у вас может быть столько переменных, сколько вам нужно (включая векторы и строки), вы можете выполнять арифметические операции, сравнения, а также выбор и итерации. С помощью этих примитивов можно выразить любые вычисления. Вы можете вводить и выводить числа и строки в виде текста (и даже графиков). Можете даже организовать вычисления в виде набора функций. Осталось только научиться писать хорошие программы, т.е. правильные, понятные и эффективные. Не менее важно, чтобы вы смогли научиться этому, затратив разумное количество сил.

Глава 5