Операторы C++

Автор: manager Воскресенье, Март 23rd, 2008 Нет комментариев

Рубрика: C++. Бархатный путь

Согласно принятой нами терминологии, любое законченное предложение на языке C++ называется оператором. Рассмотрим множество БНФ, определяющих синтаксис операторов.
Оператор ::= ОператорВыражение
::= Объявление
::= СоставнойОператор
::= ПомеченныйОператор
::= ОператорПерехода
::= ВыбирающийОператор
::= ОператорЦикла
ОператорВыражение ::= [Выражение];

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

Мы уже второй раз сталкиваемся с пустым оператором (достаточно внимательно посмотреть на последнюю форму Бэкуса-Наура). Первый раз мы встретили пустой оператор при анализе объявления. Ничего удивительного.

Объявление — это тоже оператор.

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

Оператор объявления мы уже рассмотрели ранее. На очереди — составной оператор.
СоставнойОператор ::= {[СписокОператоров]}

Что такое список операторов — также уже известно. Судя по последней БНФ, составной оператор (даже пустоой) всегда начинается разделителем { и завершается разделителем }. Кроме того, составной оператор может быть абсолютно пустым (между двумя фигурными скобками может вообще ничего не стоять).

Так что конструкция
{};
однозначно воспринимается транслятором как последовательность, состоящая из двух операторов — пустого составного оператора и простого пустого.

Оператор return. Точка вызова и точка возврата

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

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

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

Если выражение является выражением вызова функции, точка завершения выполнения выражения называется точкой возврата из функции.

Так вот оператор return немедленно прекращает выполнение операторов в теле функции и передаёт управление в точку возврата. Поскольку вызов функции является выражением, точка возврата имеет значение. Это значение определяется значением выражения, которое обычно располагается непосредственно за оператором возврата return. Тип возвращаемого значения должен соответствовать типу, который указывается спецификатором определения в объявлении и определении функции.

Если в качестве спецификатора объявления в определении и объявлении функции используется ключевое слово void, оператор return в теле этой функции используется без выражения. В этом случае выражение вызова функции оказывается выражением типа void, а значение выражения вызова в точке возврата оказывается неопределённым. Такое выражение не может входить в состав выражений более сложной структуры в качестве операнда выражения, поскольку значение всего выражения оказывается неопределённым.

Выражение с неопределённым значением (выражение вызова функции типа void) может выступать лишь в качестве выражения-оператора. Главное — это не забыть поставить в конце этого выражения разделитель ‘;’, который и превращает это выражение в оператор.

Выбирающий оператор

ВыбирающийОператор ::= if (Выражение) Оператор [else Оператор]
::= switch (Выражение) Оператор
Определение понятия оператора выбора начнём с важного ограничения. Выражение в скобках после ключевых слов if и switch являются обязательными выражениями. От их значения зависит выполнение тела оператора выбора. Так что в этом месте нельзя использовать выражения с неопределённым значением — выражения вызова функции, возвращающей неопределённое значение.

Операторы выбора определяют один из возможных путей выполнения программы.

Выбирающий оператор if имеет собственное название. Его называют условным оператором.

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

if (i)
{int k = 1;}
else
{int l = 10;}

Этот пример условного оператора интересен тем, что операторы, выполняемые после проверки условия (значение переменной i), являются операторами объявления. В ходе выполнения одного из этих операторов объявления в памяти создаётся объект типа int с именем k и значением 1, либо объект типа int с именем l и значением 10. Областью действия этих имён являются блоки операторов, заключающих данные операторы объявления. Эти объекты имеют очень короткое время жизни. Сразу после передачи управления за пределы блока эти объекты уничтожаются. Ситуация не меняется, если условный оператор переписывается следующим образом:

if (i)
int k = 1;
else
int l = 10;

При этом область действия имён и время жизни объектов остаются прежними. Это позволяет несколько расширить первоначальное определение блока: операторы, входящие в выбирающий оператор также считаются блоком операторов.

Подобное обстоятельство являлось причиной стремления запретить использование операторов объявлений в теле условного оператора. В справочном руководстве по C++ Б.Строуструпа по этому поводу сказано, что в случае, если объявление является единственным оператором, то в случае его выполнения возникает имя «с непонятной областью действия».
Однако запрещение испол
ьзования оператора объявления в условном операторе влечёт за собой очень много ещё более непонятных последствий. Именно по этой причине в последних реализациях C++ это ограничение не выполняется. Проблема области действия является проблемой из области семантики языка и не должна оказывать влияния на синтаксис оператора.

Выбирающий оператор switch или оператор выбора предназначается для организации выбора из множества различных вариантов.

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

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

switch (i) ; // Синтаксически правильный оператор выбора┘
switch (j) {} // Ещё один┘ Такой же бесполезный и правильный┘
switch (r) i++;// Этот правильный оператор также не работает.
В теле условного оператора в качестве оператора может быть использовано определение:
switch (k) {
int q, w, e;
}

Этот оператор выбора содержит определения объектов с именами q, w, e.
Туда могут также входить операторы произвольной сложности и конфигурации:
switch (k) {
int q, w, e;
q = 10; e = 15;
w = q + e;
}

Входить-то они могут, а вот выполняться в процессе выполнения условного оператора не будут!
А вот включение в оператор выбора операторов определений с одновременной инициализацией создаваемого объекта недопустимо. И об этом мы уже говорили. Оно вызывает сообщение об ошибке независимо от того, в каком месте оператора выбора оно располагается:
switch (k) {
int q = 100, w = 255, e = 1024; // Ошибка┘
default: int r = 100; // Опять ошибка┘
}

Дело в том, что в ходе выполнения оператора объявления с одновременной инициализацией создаваемого объекта происходят два события:

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

int q; int w; int e;

во-вторых, выполняется дополнительное днйствие — нечто эквивалентное оператору присвоения:
q = 100; w = 255; e = 1024;

а вот этого в данном контексте и не разрешается делать! Просто так операторы в теле условного оператора не выполняются.

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

Казалось, логичнее было бы не делать никаких различий между операторами объявления и прочими операторами. Но дело в том, что оператор выбора состоит из одного единственного блока. И нет иного пути создания объекта с именем, область действия которого распространялась бы на всё тело оператора выбора, как разрешение объявления переменных в любой точке оператора выбора. Судя по всему, переменная создаётся до того момента, как начинают выполняться операторы в блоке. Объявление превыше всего!

И всё же, какие операторы выполняются в теле оператора выбора (разумеется, за исключением объявления без инициализации)? Ответ: все подряд, начиная с одного из помеченных.

Возможно, что помеченного меткой «default:». При этом в теле оператора выбора может быть не более одной такой метки.

switch (val1) default: x++;
А возможно, помеченного меткой «case КонстантноеВыражение :». В теле оператора выбора таких операторов может быть сколь угодно много. Главное, чтобы они различались значениями константных выражений.
Нам уже известно, что является константным выражением и как вычисляется его значение.
Небольшой тест подтверждает факт вычисления значения константного выражения транслятором:
switch (x)
{
int t;// Об этом операторе уже говорили┘
case 1+2: y = 10;
case 3: y = 4; t = 100; // На этом месте транслятор
//сообщит об ошибке. А откуда он узнал, что 1+2 == 3 ?
// Сам сосчитал┘
default: cout << y << endl;
}
А вот пример, который показывает, каким оразом вычисляется выражение, содержащее операцию запятая:
int XXX = 2;
switch (XXX)
{
case 1,2: cout << "1,2"; break;
case 2,1: cout << "2,1"; break;
}

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

Рассмотрим, наконец, схему выполнения оператора switch:

вычисляется выражение в круглых скобках после оператора switch (предварительная стадия);

это значение последовательно сравнивается со значениями константных выражений за метками case (стадия определения начальной точки выполнения оператора);

если значения совпадают, управление передаётся соответствующему помеченному оператору (стадия выполнения);
если ни одно значение не совпадает и в теле оператора case есть оператор, помеченный меткой default, управление передаётся этому оператору (но даже в этом случае сочетание объявления с инициализацией недопустимо!) (стадия выполнения);

если ни одно значение не совпадает, и в теле оператора case нет оператора, помеченного меткой default, управление передаётся оператору, следующему за оператором switch (стадия выполнения).

Метки case и default в теле оператора switch используются лишь при начальной проверке, на стадии определения начальной точки выполнения тела оператора. На стадии выполнения все операторы от точки выполнения и до конца тела оператора выполняются независимо от меток, если только какой-нибудь из операторов не передаст управление за пределы оператора выбора. Таким образом, программист сам должен заботиться о выходе из оператора выбора, если это необходимо. Чаще всего для этой цели используется оператор break.

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

Рассмотрим в качестве примера следующий оператор выбора:
int intXXX;
:::::
switch (intXXX)
{
case 1:
int intYYY;
/* Здесь инициализация переменной запрещена, однако определение
переменной должно выполняться. */
break;
case 2:
case 3:
intYYY = 0;
break;
}
Казалось бы, этот оператор выбора может быть переписан в виде условного оператора:
int intXXX;
:::::
if (intXXX == 1)
{
int intYYY = 0; // Здесь допускается инициализация!
}
else if (intXXX == 2 || intXXX == 3)
{
intYYY = 0;
/*
Здесь ошибка! Переменная intYYY не объявлялась в этом блоке операторов.
*/
}

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

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

int intXXX;
:::::
if (1)
/*
Этот условный оператор определяет внешний блок операторов, в котором
располагается объявление переменной intYYY.
*/
{
int intYYY = 0;
if (intXXX == 1)
{
intYYY = 0;
}
else if (intXXX == 2 || intXXX == 3)
{
intYYY = 0;
}
}

Нам удалось преодолеть проблемы, связанные с областями действия, пространствами и областями видимости имён путём построения сложной системы вложенных блоков операторов. Простой одноблочный оператор выбора, содержащий N помеченных операторов, моделируется с помощью N+1 блока условных операторов.
Однако каждый оператор хорош на своём месте.
Операторы цикла

Операторы цикла задают многократное исполнение.
ОператорЦикла ::= while (Выражение) Оператор
::=
for (ОператорИнициализацииFor [Выражение] ; [Выражение] )Оператор
::= do Оператор while (Выражение);
ОператорИнициализацииFor ::= ОператорВыражение
::= Объявление
Прежде всего, отметим эквивалентные формы операторов цикла.
Оператор
for (ОператорИнициализацииFor [ВыражениеA] ;[ВыражениеB]) Оператор
эквивалентен оператору
ОператорИнициализацииFor while (ВыражениеA)
{
Оператор
ВыражениеB ;
}

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

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

Следует также обратить внимание на точку с запятой между двумя выражениями цикла for. В последнем примере они представлены символами ВыражениеA и ВыражениеB. Перед нами классический пример разделителя.
ОператорИнициализацииFor является обязательным элементом заголовка цикла. Обязательный оператор вполне может быть пустым.

Рассмотрим пример оператора цикла for:
for ( ; ; ) ;
Его заголовок состоит из пустого оператора (ему соответствует первая точка с запятой) и разделителя, который разделяет два пустых выражения. Тело цикла — пустой оператор.

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

Синтаксис C++ накладывает на структуру нетерминального символа ОператорИнициализацииFor жёсткие ограничения:
это всегда единственный оператор,
он не может быть блоком операторов,
единственным средством усложнения его структуры служит операция запятая.

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

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

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

Если значение этого выражения отлично от нуля (т.е. истинно), выполняется оператор цикла. Этот этап можно назвать этапом выполнения тела цикла.

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

На последних двух этапах могут измениться значения ранее определённых переменных. А потому следующий цикл повторяется с этапа определения условий выполнимости.

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

int qwe;
for (qwe < 10; ; ) {}
// Оператор инициализатор построен на основе выражения сравнения.
for (this; ; ) {}
// Оператор инициализатор образован первичным выражением this.
for (qwe; ; ) {}
// Оператор инициализатор образован первичным выражением qwe.
Ещё пример:
int i = 0;
int j;
int val1 = 0;
int val2;
:::::
i = 25;
j = i*2;
:::::
for ( ; i < 100; i++, j—)
{
val1 = i;
val2 — j;
}

Мы имеем оператор цикла for, оператор инициализации которого пуст, а условие выполнения цикла основывается на значении переменной, которая была ранее объявлена и проинициализирована. Заголовок цикла является центром управления цикла. Управление циклом основывается на внешней по отношению к телу цикла информации.
Ещё пример:

for ( int i = 25, int j = i*2; i < 100; i++, j—)
{
val1 = i;
val2 — j;
}

Заголовок нового оператора содержит пару выражений, связанных операцией запятая. Тело оператора представляет всё тот же блок операторов. Что может содержать тело оператора? Любые операторы. Всё, что может называться операторами. От самого простого пустого оператора, до блоков операторов произвольной сложности! Этот блок живёт по своим законам. В нём можно объявлять переменные и константы, а поскольку в нём определена собственная область действия имён, то объявленные в блоке переменные и константы могут скрывать одноимённые объекты с более широкой областью действия имён.

А вот использование блока в операторе инициализации привело бы к дополнительным трудноразрешимым проблемам с новыми областями действия и видимости имён, вводимых в операторе инициализации. Часть переменных могла бы оказаться невидимой в теле оператора цикла.

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

Рассмотрим несколько примеров. Так, в ходе выполнения оператора цикла
int i;
for (i = 0; i < 10; i++)
{
int j = 0; j += i;
}

десять раз будет выполняться оператор определения переменной j. Каждый раз это будут новые объекты. Каждый раз новой переменной заново будет присваиваться новое значение одной и той же переменной i, объявленной непосредственно перед оператором цикла for.

Объявление переменной i можно расположить непосредственно в теле оператора-инициализатора цикла:
for (int i = 0; i < 10; i++)
{
int j = 0; j += i;
}

И здесь возникает одна проблема. Дело в том, что тело оператора цикла for (оператор или блок операторов) имеет ограниченную область действия имён. А область действия имени, объявленного в операторе-инициализаторе, оказывается шире этой области.

Заголовок цикла for в C++ — центр управления циклом. Здесь следят за внешним миром, за тем, что происходит вне цикла. И потому все обращения к переменным и даже их новые объявления в заголовке цикла относятся к "внешней" области видимости. Следствием такого допущения (его преимущества далеко не очевидны) является правило соотнесения имени, объявленного в заголовке и области его действия.
По отношению к объявлению переменной в заголовке оператора цикла for, правило соотнесения гласит, что область действия имени, объявленного в операторе инициализации цикла for, располагается в блоке, содержащем данный оператор цикла for.

А вот область действия имени переменной j при этом остаётся прежней.
В теле оператора for может быть определена одноимённая переменная:
for (int i = 0; i < 10; i++)
{
int i = 0; i += i;
}

Пространство имени переменной в операторе цикла ограничено блоком из двух операторов. В этом пространстве переменная, объявленная в заголовке, оказывается скрытой одноимённой переменной.
Десять раз переменная i из оператора-инициализатора цикла будет заслоняться одноимённой переменной из оператора тела цикла. И всякий раз к нулю будет прибавляться нуль.

Ещё один пример. Два расположенных друг за другом оператора цикла for содержат ошибку
for (int i = 0, int j = 0; i < 100; i++, j—)
{
// Операторы первого цикла.
}
for (int i = 0, int k = 250; i < 100; i++, k—)
{
// Операторы второго цикла.
}

Всё дело в том, что, согласно правилу соотнесения имён и областей действия имён в операторе цикла for, объявления переменных в заголовке цикла оказываются в общем пространстве имён. А почему, собственно, не приписать переменные, объявленные в заголовке цикла блоку, составляющему тело цикла? У каждого из альтернативных вариантов соотнесения имеются свои достоинства и недостатки. Однако выбор сделан, что неизбежно ведёт к конфликту имён и воспринимается как попытка переобъявления ранее объявленной переменной.
Эту самую пару операторов for можно переписать, например, следующим образом:

for (int i = 0, int j = 0; i < 100; i++, j—)
{
// Здесь располагаются операторы первого цикла.
}
for (i = 0, int k = 250; i < 100; i++, k—)
{
// Здесь располагаются операторы второго цикла.
}

Здесь нет ошибок, но при чтении программы может потребоваться дополнительное время для того, чтобы понять, откуда берётся имя для выражения присвоения i = 0 во втором операторе цикла. Кроме того, если предположить, что операторы цикла в данном контексте реализуют независимые шаги какого-либо алгоритма, то почему попытка перемены мест пары абсолютно независимых операторов сопровождается сообщением об ошибке:

for (i = 0, int k = 250; i < 100; i++, k—)
{
// Здесь располагаются операторы второго цикла.
}
for (int i = 0, int j = 0; i < 100; i++, j—)
{
// Здесь располагаются операторы первого цикла.
}

Очевидно, что в первом операторе оказывается необъявленной переменная i. Возможно, что не очень удобно, однако, в противном случае, в центре управления циклом трудно буден следить за внешними событиями. В конце концов, никто не заставляет программиста располагать в операторе инициализации объявления переменных. Исходная пара операторов может быть с успехом переписана следующим образом:
int i, j, k;
:::::
for (i = 0, k = 250; i < 100; i++, k—)
{
// Здесь располагаются операторы второго цикла.
}
for (i = 0, j = 0; i < 100; i++, j—)
{
// Здесь располагаются операторы первого цикла.
}

А вот ещё один довольно странный оператор цикла, в котором, тем не менее, абсолютно корректно соблюдены принципы областей действия имён, областей видимости имён, а также соглашения о соотнесении имён и областей их действия:
for (int x; x < 10; x++) {int x = 0; x++;}

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

Оператор цикла do ┘ while называется оператором цикла с постусловием. От циклов с предусловием он отличается тем, что сначала выполняется оператор (возможно, составной), а затем проверяется условие выполнения цикла, представленное выражением, которое располагается в скобках после ключевого слова while. В зависимости от значения этого выражения возобновляется выполнение оператора. Таким образом, всегда, по крайней мере один раз, гарантируется выполнение оператора цикла.

int XXX = 0;
do {cout << XXX << endl; XXX++;} while (XXX < 0);

Оставить комментарий

Чтобы оставлять комментарии Вы должны быть авторизованы.

Похожие посты