Занятие 2.5: Ветвления в программе, условный оператор, оператор множественного выбора

Условный оператор if

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

Если условие истинно, то инструкция выполняется. Общая схема оператора такова:

if (условие) инструкция;

Пример:

int a;
cin >> a;
if (a > 0) cout << "Chislo polozhitelnoe";

Если в переменную a пользователь введёт положительное значение, то на экран выведется соответствующая фраза, иначе — не выведется ничего.

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

int a;
cin >> a;
if (a > 0) {
	cout << "Chislo polozhitelnoe" << endl;
	cout << "Kvadrat chisla raven " << a << endl;
}
cout << "Do svidaniya";

В конце работы программа «попрощается» с пользователем в любом случае, но сообщение о положительности и квадрат числа выведутся только в том случае, если в переменную a было введено положительное значение.

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

У оператора if существует вариация с дополнительной (и, как мы видели выше, необязательной) частью else:

if (условие) инструкция1;
else инструкция2;

В случае истинности условия выполняется простая или составная инструкция1, а в случае ложности — простая или составная инструкция2.

Пример:

if (a != 0) cout << 100 / a;
else cout << "Na 0 delit nelzya";

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

if (a != 0) {
	cout << 100 / a;
} else {
	cout << "Na 0 delit nelzya";
}

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

Форматирование кода и стандарты кодирования

По традиции, внутренности каждого блока смещают вправо относительно строк его начала и конца (закрывающей фигурной скобки). Смещение делается с помощью двух пробелов или же с помощью табуляции (нажатием клавиши Tab). В C++ такое форматирование имеет исключительно оформительский характер (не требуется для корректной компиляции программы), но настоятельно рекомендуется правильно форматировать код: это позволяет без затруднений читать исходный код программы, где одни блоки вложены внутрь других. Благодаря отступам в таких программах становится ясно, где и какой блок заканчивается, к какому из операторов if относится очередная инструкция else и т.д.

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

if (a != 0)
{
  cout << 100 / a;
}
else
{
  cout << "Na 0 delit nelzya";
}

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

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

Последовательные и вложенные проверки условий

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

Первый вариант решения:

#include 
using namespace std;
int main(void) {
	double n;
	cout << "Vvedite chislo: ";
	cin >> n;
	if(n<0) {
		cout << "Chislo otritsatelnoe" << endl;
	}
	if(n==0) {
		cout << "Chislo nulevoe" << endl;
	}	
	if(n>0) {
		cout << "Chislo polozhitelnoe" << endl;
	}		
}

Второй пример:

#include 
using namespace std;
int main(void) {
	double n;
	cout << "Vvedite chislo: ";
	cin >> n;
	if(n<0) {
		cout << "Chislo otritsatelnoe" << endl;
	} else {
		if(n==0) {
			cout << "Chislo nulevoe" << endl;
		} else {
			cout << "Chislo polozhitelnoe" << endl;
		}
	}		
}

Оба варианта программы будут давать правильный результат при всех возможных испытаниях. Но второй вариант реализации программы оптимален. В чём же его превосходство над первым?

Давайте рассмотрим, сколько проверок будет выполнено каждым вариантом, если пользователь введёт отрицательное число. В первом случае всегда будут выполняться три проверки: первая из них окажется приведёт к срабатыванию инструкции и вывода фразы на экран, но после этого всё равно произойдут две другие проверки (нам понятно, что три перечисленных условия взаимоисключающие, но программа будет честно исполнять написанные человеком инструкции и проверит не оказалось ли число нулевым или положительным). Зато во втором примере, при истинности первого условия, выполнится заключенная в блок первого if инструкция вывода и далее программа завершится. Если число окажется не положительным, то тогда будет выполнена вторая проверка (заключенная внутрь else). Дойдя до второй проверки (вложенной в else) мы заведомо знаем, что число уже не положительное, а, значит, либо отрицательное, либо нуль. Чтобы из двух вариантов выбрать один верный — достаточно одной проверки.

Итого получается, что первый пример будет всегда выполнять три проверки, а во втором примере в лучшем случае случится одна проверка, а в худшем — две. Таким образом, мы сокращаем количество выполняемых команд и ускоряем работу всей программы.

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

Конструкция else…if

Когда требуется последовательно перепроверять несколько взаимоисключающих условий, для частей else не открывают отдельного блока (записывают следующий if как простую, а не как составную инструкцию), чтобы получить более компактную запись. Такие конструкции в коде называют else…if.

Рассмотрим сначала вариант записи со вложенными блоками. Для примера рассмотрим следующую задачу.

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

  1. Если указан возраст от 18 и до 27 лет, то сообщать, что заполняющий подлежит призыву на срочную службу или может служить по контракту.
  2. Если указан возраст от 28 до 59 лет, то сообщать, что заполняющий может служить по контракту.
  3. Если указан возраст менее 18 или более 59 лет, то сообщать о том, что заполняющий находится в непризывном возрасте.
  4. Если указан неположительный возраст или возраст более 100 лет, то сообщить об ошибке.

Предположим, что возраст сохранён в переменной n, тогда для вывода подсказки потребуется следующий код:

if (n>=18 && n<27) {
    cout << "Вы можете пройти срочную или контрактную службу";
} else {
    if (n>=27 && n<60) {
        cout << "Вы можете служить только по контракту";
    } else {
        if (n>0 && n<=100) {
            cout << "Вы находитесь в непризывном возрасте";
        } else {
            cout << "Скорее всего, вы допустили ошибку и неверно указали возраст";
        }
    }
}

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

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

n>0 && n<18 || n>=60 && n<=100

Но оно было бы избыточным, ведь ранее (за счёт выше стоящих проверок) мы уже убедились, что переменная n не попала в промежуток [18;60). Если бы переменная попадала в промежуток, то вывелось бы сообщение «Вы можете пройти срочную или контрактную службу» или «Вы можете служить только по контракту».

Но для случая, когда проверяется последовательность условий можно (но не необходимо) использовать более компактную запись, где содержание каждого else кроме последнего не оформляется в виде отдельного блока. Тот же фрагмент кода с учётом этой возможности может записываться так:

if (n>=18 && n<27) {
    cout << "Вы можете пройти срочную или контрактнуню службу";
} else if (n>=27 && n<60) {
    cout << "Вы можете служить только по контракту";
} else if (n>0 && n<=100) {
    cout << "Вы находитесь в непризывном возрасте";
} else {
    cout << "Скорее всего, вы допустили ошибку и неверно указали возраст";
}

Оператор множественного выбора

Инструкция множественного выбора switch позволяет выполнять различные части программы в зависимости от того, какое значение будет иметь некоторая целочисленная переменной (её называют «переменной-переключателем», а «switch» с английского переводится как раз как «переключатель»).

Схема инструкции такова:

switch (переключатель)  {
  case значение1:
    инструкция1;
    break;
  case значение2:
    инструкция2;
    break;
  …
  default:
    инструкция_по_умолчанию;
}

Рассмотрим все элементы оператора:

  • переключатель — это целочисленная переменная или выражение дающее целочисленный результат;
  • значение1, значение2, … — это целочисленные литералы, с которыми будет сравниваться значение переключателя. Если переключатель равен значениюN, то программа будет выполняться со строки, следующей за case значениеN: и до ближайшего встреченного break, либо до конца блока switch (если break не встретится);
  • default: — это метка инструкции после которой будут выполняться в том случае, если выше ни одно из значенийN не совпало с переключателем. Метка default — необязательная: можно её не включать в блок switch меток или не выполнять после неё никаких команд;
  • инструкцияN — простая или составная инструкция. Притом в случае составной несколько команд не обязательно объединять в блок, можно их просто написать друг за другом разделяя с помощью «;» (и начиная новые строки для удобства).

Рассмотрим такую ситуацию: в какой-то момент требуется просить у пользователя, надо ли продолжать программу или можно её завершить. Предположим, что ответ пользователя принимается в виде символа, вводимого с клавиатуры и сохраняемого в переменной ans: Д — да, продолжать, Н — нет, остановить.

Тогда получаем примерно такой программный код:

switch(ans) {
    case 'Д':
        cout << "Продолжаем программу";
        break;
    case 'Н':
        cout << "Останавливаем программу";
        break;
}

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

switch(ans) {
    case 'Д':
        cout << "Продолжаем программу";
        break;
    case 'Н':
        cout << "Останавливаем программу";
        break;
    default:
        cout << "Вы ввели неподходящий символ";
}

После инструкций причастных к этой метке break обычно не ставят, потому что default располагается в конце всего блока switch и после неё всё равно завершится оператор множественного выбора. Но в остальных ветках — break необходим. Рассмотрим пример, в котором по ошибке пропущен первый break в ситуации, когда в переменной-переключателе будет находится символ «Д»:

char ans = 'Д';
switch(ans) {
    case 'Д':
        cout << "Продолжаем программу";
    case 'Н':
        cout << "Останавливаем программу";
        break;
    default:
       cout << "Вы ввели неподходящий символ";
}

На экран будут выведены сразу две фразы: «Продолжаем программу» и «Останавливаем программу». Это случилось потому, что после первой ветки с литералом «Д» программа выполнялась до первого встреченного break, т.е. выполнилась и часть, относящаяся к ветке с литералом «Н».

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

switch(ans) {
    case 'Д':
    case 'д':
    case 'Y':
    case 'y':                
       cout << "Продолжаем программу";
    case 'Н':
    case 'н':                
    case 'N':                
    case 'n':                                
        cout << "Останавливаем программу";
        break;
    default:
        cout << "Вы ввели неподходящий символ";
}

Теперь пользователь для продолжения программы сможет ввести не только символ «Д», но и символы «д», «Y», «y» (от английского «yes»).

Любой оператор switch можно заменить конструкцией if…else:

if (ans=='Д' || ans=='д' || ans=='Y' || ans=='y') {                
    cout << "Продолжаем программу";
} else if (ans=='Н' || ans=='н' || ans=='N' || ans=='n') {                                
    cout << "Останавливаем программу";
} else {
    cout << "Вы ввели неподходящий символ";
}

Обратное — неверно, ведь switch позволяет только сравнивать переключатель с конкретными значениями, но не позволяет для какой-то из веток задать условие в виде целого диапазона значений с использованием операторов сравнения (например, с использованием строгих неравенств и логических операторов «и» или «или»). Также напомним, что в качестве переключателя могут выступать только целочисленные переменные или выражения.

if…else более универсальный оператор, чем switch.