Язык программирования Си для персонального компьютера

Бочков C. О.

Субботин Д. М.

ВЫРАЖЕНИЯ

 

 

Введение

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

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

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

Значение, представляемое операндом в выражении, имеет тип. Этот тип может быть в ряде случаев преобразован (явно или неявно) к другому типу по некоторым правилам. Преобразования типов описаны в разделе 4.7.

 

Операнды

 

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

Каждый операнд имеет тип. В разделе 4.3 "Операции" рассматриваются допустимые типы операндов для каждого вида операций. Следует помнить, что перечислимый тип является подмножеством целого типа, и его значения участвуют в выражениях как значения целого типа. Тип операнда может быть явно преобразован к другому типу посредством операции приведения типа (см. раздел 4.7.2). Выражение приведения типа само рассматривается как операнд содержащего его выражения.

 

Идентификаторы

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

1) Идентификаторы переменных целого и плавающего типа представляют значения соответствующего типа.

2) Идентификатор переменной перечислимого типа представляет значение одной константы из соответствующего этому типу списка перечисления. Тип этого значения—int.

3) Идентификатор структуры или объединения представляет совокупность значений, специфицированных этой структурой или объединением.

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

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

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

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

 

Константы

Операнду-константе соответствует значение и тип представляющей его константы. Типы констант подробно описаны в разделе 1.2. Символьная константа имеет тип int. Целая константа имеет один из следующих типов: int, long, unsigned int или unsigned long, в зависимости от размера целого на данном компьютере и от того, как специфицировано ее значение. Константы с плавающей точкой имеют тип double (в версии 2.0 СП ТС допустимы также константы типа float). Символьные строки имеют тип массив символов; они обсуждаются в разделе 4.2.3.

 

Символьные строки

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

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

 

Вызовы функций

Синтаксис:

<выражение> (<список-выражений>)

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

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

 

Индексные выражения

Синтаксис:

<выражение1>[<выражение2>]

Здесь квадратные скобки являются символами языка Си, а не элементами описания.

Значение индексного выражения находится по адресу, который вычисляется как сумма значений <выражения1> и <выражения2>. Выражение1 должно иметь тип указателя на некоторый тип, например быть идентификатором массива, а выражение2, заключенное в квадратные скобки, должно иметь целый тип. Однако требование синтаксиса состоит лишь в том, чтобы одно из выражений было указателем, а другое имело целый тип; порядок же следования выражений безразличен.

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

Индексное выражение вычисляется путем сложения целого значения со значением указателя (или с адресом массива) и последующим применением к результату операции косвенной адресации. Операция косвенной адресации описана в разделе 4.3.2. Например, для одномерного массива следующие четыре выражения эквивалентны, если а — массив или указатель, а b — целое.

а[b]

*(а + b)

*(b + а)

b[а]

В соответствии с правилами преобразования типов для операции сложения (смотри раздел 4.3.4) целочисленное значение при сложении с указателем (адресом) должно умножаться на размер типа, адресуемого указателем. Предположим, например, что идентификатор line определен как массив типа int. При вычислении выражения line[i], целое значение i умножается на размер типа int. Полученное значение представляет i ячеек типа int. Это значение складывается со значением указателя line, что дает адрес объекта, смещенного на i ячеек типа int относительно line, т.е. адрес i-го элемента line.

Заключительным шагом вычисления индексного выражения является применение к полученному адресу операции косвенной адресации. Результатом является значение i-го элемента массива line.

Следует помнить, что индексное выражение line[0] представляет значение первого элемента массива, так как индексация элементов массива начинается с нуля. Следовательно, выражение line[5] ссылается на шестой по порядку следования в памяти элемент массива.

Доступ к многомерному массиву

Индексное выражение может иметь более одного индекса. Синтаксис такого выражения следующий:

<выражение1>[<выражение2>][<выражение3>]…

Индексное выражение интерпретируется слева направо. Сначала вычисляется самое левое индексное выражение — <выражение1>[<выражение2>]. С адресом, полученным в результате сложения <выражения1> и <выражения2>, складывается (по правилам сложения указателя и целого) <выражение3> и т. д. <ВыражениеЗ> и последующие <выражения> имеют целый тип. Операция косвенной адресации осуществляется после вычисления последнего индексного выражения. Однако, если значение последнего указателя адресует значение типа массив, операция косвенной адресации не применяется (смотри третий и четвертый примеры ниже).

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

Примеры:

int рrор[3][4][6];

int i, *ip, (*ipp)[6];

i = prop[0][0][1]; /* пример 1 */

i = prop[2][1][3]; /* пример 2 */

ip = prop[2][1]; /* пример 3 */

ipp = prop[2]; /* пример 4 */

Массив с именем prop содержит 3 элемента, каждый из которых является двумерным массивом значений типа int. В примере 1 показано, каким образом получить доступ ко второму элементу (типа int) массива prop. Поскольку массив заполняется построчно, последний индекс меняется наиболее быстро. Выражение prop[0][0][2] ссылается на следующий (третий) элемент массива и т. д.

Во втором примере выражение вычисляется следующим образом:

1) Первый индекс 2 умножается на размер двумерного массива (4 на 6), затем на размер типа int и прибавляется к значению указателя prop. Результат будет указывать на третий двумерный массив (размером 4 на 6 элементов) в трехмерном массиве prop.

2) Второй индекс 1 умножается на размер 6-элементного массива типа int и прибавляется к адресу, представляемому выражением prop[2].

3) Каждый элемент 6-элементного массива имеет тип int, поэтому индекс 3 умножается на размер типа int и прибавляется к адресу, представляемому выражением prop[2][1]. Результирующий указатель адресует четвертый элемент массива из шести элементов.

4) На последнем шаге вычисления выражения рrор[2][1][3] выполняется косвенная адресация по указателю. Результатом является элемент типа int, расположенный по вычисленному адресу.

В примерах 3 и 4 представлены случаи, когда косвенная адресация не применяется. В примере 3 выражение prop[2][1] представляет указатель на массив из шести элементов в трехмерном массиве prop. Поскольку значение указателя адресует массив, операция косвенной адресации не применяется. Аналогично, результатом вычисления выражения prop[2] в примере 4 является значение указателя, адресующего двумерный массив.

 

Выбор элемента

Синтаксис:

<выражение>.<идентификатор>

<выражение> -> <идентификатор>

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

В первой синтаксической форме <выражение> представляет значение типа struct или union, а идентификатор именует элемент специфицированной структуры или объединения. Во второй синтаксической форме <выражение> представляет указатель на структуру или объединение, а идентификатор именует элемент специфицированной структуры.

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

<выражение> -> <идентификатор>

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

(*<выражение>).<идентификатор>

однако более наглядна.

Примеры:

struct pair {

int a;

inl b;

struct pair *sp;

} item, list[10];

item.sp = &item; /* пример 1 */

(item.sp)->a = 24; /* пример 2 */

list[8].b = 12; /* пример 3 */

В первом примере адрес структуры Нет присваивается элементу sp этой же структуры. В результате структура item содержит указатель на себя.

Во втором примере используется адресное выражение item.sp с операцией выбора элемента ->, присваивающее значение элементу а. Учитывая результат примера 1, пример 2 эквивалентен записи

item.a = 24;

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

 

Операции и L-выражения

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

Первичные выражения рассмотрены в разделах 4.2.4, 4.2.5, 4.2.6.

Унарное выражение состоит из операнда с предшествующей ему унарной операцией.

Синтаксис:

<унарная-операция> <операнд>

Унарные операции рассмотрены в разделе 4.3.2.

Бинарное выражение состоит из двух операндов, разделенных бинарной операцией.

Синтаксис:

<операнд1> <бинарная-операция> <операнд2>

Бинарные операции рассмотрены в разделах 4.3.3 — 4.3.9.

Тернарное выражение состоит из трех операндов, разделенных знаками условной операции "?:".

Синтаксис:

<операнд1> ? <операнд2> : <операнд3>

Условная операция рассмотрена в разделе 4.3.10.

Выражения присваивания используют унарные или бинарные операции присваивания. Унарными операциями присваивания являются инкремент "++" и декремент "--". Бинарные операции присваивания — это простое присваивание "=" и составные операции присваивания. Каждая составная операция присваивания представляет собой комбинацию какой-либо бинарной операции с простой операцией присваивания.

Синтаксис выражений присваивания:

Унарные операции присваивания:

<операнд> ++

<операнд> --

++ <операнд>

--<операнд>

Бинарные операции присваивания:

<операнд1> = <операнд2>

<операнд1> <составное-присваивание> <операнд2>

Операция присваивания рассмотрена в разделе 4.4.

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

Синтаксис:

(<абстрактное-имя-типа>) <операнд>

Операция приведения типа подробно рассматривается в разделе 4.7.2. Абстрактные имена типов описаны в разделе 3.8.3.

Операнды некоторых операций в языке Си должны представлять собой так называемые L-выражения (Lvalue expressions). L-выражением является выражение, которое ссылается на ячейку памяти и потому имеет смысл в левой части бинарной операции присваивания. Простейшим примером L-выражения является идентификатор переменной: он ссылается на ячейку памяти, которая хранит значение этой переменной.

Поскольку L-выражение ссылается на ячейку памяти, адрес этой ячейки может быть получен с помощью операции адресации (&). Имеются, однако, исключения: не может быть получен адрес битового поля и адрес переменной класса памяти register, хотя значение им может быть присвоено.

К L-выражениям относятся:

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

— индексные выражения, исключая те из них, значение которых имеет тип массив;

— выражение выбора элемента, если выбранный элемент сам является одним из допустимых L-выражений;

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

— L-выражение в скобках;

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

char *р;

int i;

long n;

(long *)p = &n; /* допустимое приведение типа */

(long)i = n; /* недопустимое приведение типа */

Перечисленные L-выражения называются также модифицируемыми L-выражениями. Кроме того, существуют немодифицируемые L-выражения; их адрес может быть получен, но использоваться в левой части бинарной операции присваивания они не могут. К ним относятся идентификаторы массивов, функций, а также переменных, объявленных с модификатором const.

 

Скобочные выражения

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

(10+5)/5

скобки означают, что выражение 10+5 является левым операндом операции деления. Результат выражения равен 3. В отсутствие скобок значение выражения равнялось бы 11. Хотя скобки влияют на то, каким путем группируются операнды в выражении, они не гарантируют определенный порядок вычисления операндов для операций, обладающих свойством коммутативности (мультипликативные, аддитивные, поразрядные операции). Например, выражение (а+b)+с компилятор может вычислить как а+(b+с) или даже как (а+с)+b.

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

 

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

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

Константные выражения, используемые в директивах препроцессора, имеют дополнительные ограничения, поэтому они называются ограниченными константными выражениями. Ограниченные константные выражения не могут содержать операцию sizeof (в СП ТС — могут), констант перечисления и выражений приведения типа и плавающих констант. Однако ограниченные константные выражения, используемые в директивах препроцессора, могут содержать специальные константные выражения defined (<идентификатор>), описанные в разделе 7.2.1 "Директива #define". . Только выражения инициализации допускают применение плавающих констант, выражений приведения типа к неарифметическим типам и операции адресации. Операция адресации может быть применена к переменной внешнего уровня базового или структурного типа, к объединению, а также к элементу массива. В этих выражениях допускается сложение или вычитание адресного выражения с константным выражением, не содержащим операции адресации.

 

Операции

 

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

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

b << 2 << 2

выполняется как (b << 2) << 2, а не как b << (2 << 2). Ассоциативность "справа налево" означает, что первой будет выполняться операция, знак которой записан правее остальных.

В языке Си реализованы следующие унарные операции:

Знак операции Наименование
- унарный минус
+ унарный плюс
~ обратный код
! логическое отрицание
& адресация
* косвенная адресация
sizeof определение размера

Примечание. Операция унарного плюса реализована полностью только в СП ТС. В СП MSC версии 4 она отсутствует, а в версии 5 реализована только синтаксически.

Унарные операции предшествуют своему операнду и ассоциируются справа налево.

В языке Си реализованы следующие бинарные операции:

Знак Наименование
* / % мультипликативные операции
+ - аддитивные операции
<< >> операции сдвига
< > <= >= == != операции отношения
& | ^ поразрядные операции
&& || логические операции
, операция последовательного вычисления

Бинарные операции ассоциируются слева направо. В языке Си имеется одна тернарная операция — условная, обозначаемая ?:. Она ассоциируется справа налево.

 

Преобразования по умолчанию

Большинство операций языка Си выполняют преобразование типов для приведения своих операндов к общему типу либо для того, чтобы расширить значения коротких по размеру типов до размера, используемого в машинных операциях. Преобразования, зависящие от конкретной операции и от типа операнда (или операндов), рассмотрены в разделе 4.7. Тем не менее, многие операции выполняют одинаковые преобразования целых и плавающих типов. Эти преобразования называются далее преобразованиями по умолчанию.

Преобразования по умолчанию осуществляются следующим образом:

1) Все операнды типа float преобразуются к типу double.

2) Только для СП ТС: если один операнд имеет тип long double, то второй операнд также преобразуется к типу long double.

3) Если один операнд имеет тип double, то второй операнд преобразуется к типу double.

4) Если один операнд имеет тип unsigned long, то второй операнд преобразуется к типу unsigned long.

5) Если один операнд имеет тип long, то второй операнд преобразуется к типу long.

6) Если один операнд имеет тип unsigned int, то второй операнд преобразуется к типу unsigned int.

7) Все операнды типов char или short преобразуются к типу int.

8) Все операнды типов unsigned char или unsigned short преобразуются к типу unsigned int.

9) Иначе оба операнда имеют тип int.

 

Унарные операции

Унарный минус (-)

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

Унарный плюс (+)

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

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

Обычно компиляторы языка Си осуществляют перегруппировку выражений, переупорядочивая операции, обладающие свойством коммутативности (умножение, сложение, поразрядные операции), пытаясь сгенерировать как можно более эффективный код. При этом скобки, ограничивающие операции, не принимаются в расчет. Однако СП ТС не будет реорганизовывать выражения в скобках, если перед скобками записана операция унарного плюса. Это позволяет, в частности, контролировать точность вычислений с плавающей точкой. Например, если а, b, с и f имеют тип float, выражение

f = а *+ (b * с)

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

В СП MSC для гарантии порядка вычислений следует пользоваться вспомогательной переменной, например

t = b * с;

f = а * t

Обратный код (~)

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

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

Логическое отрицание (!)

Операция логического отрицания вырабатывает значение 0, если операнд есть ИСТИНА, и значение 1, если операнд есть ЛОЖЬ. Результат имеет тип int. Операнд должен иметь целый или плавающий тип либо быть указателем.

Примеры:

/* пример 1 */

short х = 987;

х = ~х;

/* пример 2 */

unsigned short у = 0xAAAA;

y = ~y;

/* пример 3 */

if(!(x

В первом примере новое значение х равно -987.

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

В третьем примере, если х больше или равен у, то результат условного выражения в операторе if равен 1 (ИСТИНА). Если х меньше у, то результат равен 0 (ЛОЖЬ).

Адресация "&"

Операция адресации вырабатывает адрес своего операнда. Операндом может быть L-выражение, в т. ч. немодифицируемое (см. раздел 4.2.7). Результат операции адресации является указателем на операнд. Тип результата — указатель на тип операнда.

Операция адресации не может применяться к битовым полям, а также к идентификаторам, объявленным с классом памяти register.

См. примеры после описания операции косвенной адресации.

Косвенная адресация "*"

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

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

Примеры:

int *ра, х;

int a[20];

double d;

pa = &а[5]; /* пример 1 */

x = *ра; /* пример 2 */

if ( х == *&x ) /* пример 3 */

printf("BEPHO\n");

d = *(double *)(&x); /* пример 4 */

В первом примере операция адресации вырабатывает адрес шестого (по порядку следования) элемента массива а. Результат записывается в адресную переменную (указатель) ра.

Во втором примере используется операция косвенной адресации для доступа к значению типа int, адрес которого хранится в указателе ра. Результат присваивается целой переменной х.

В третьем примере будет печататься слово ВЕРНО. Пример демонстрирует симметричность операций адресации и косвенной адресации: *&х эквивалентно х.

Четвертый пример показывает полезное приложение этого свойства. Адрес х преобразуется операцией приведения типа к типу указатель на double. К полученному указателю применяется операция косвенной адресации. Результатом выражения является значение типа double.

Операция sizeof

Операция sizeof определяет размер памяти, который соответствует объекту или типу. Операция sizeof имеет следующий вид:

sizeof <выражение>

sizeof (<абстрактное имя типа>)

Операндом является либо <выражение>, либо абстрактное имя типа в скобках. Результатом операции sizeof является размер памяти в байтах, соответствующий заданному объекту или типу. Тип результата — unsigned int. Если размер объекта не может быть представлен значением типа unsigned int (например, в СП MSC допустимы массивы типа huge размером более 64 Кбайтов), то следует использовать приведение типа:

(long) sizeof <выражение>

В СП MSC версии 4 допустимым выражением является L-выражение, а в версии 5 и в СП ТС — произвольное выражение. Следует учитывать, что само <выражение> не вычисляется, т. к. операция sizeof выполняется на этапе компиляции программы. Для нее существен только тип результата <выражения>, а не его значение. Недопустим тип void. Применение операции sizeof к идентификатору функции в СП ТС считается ошибкой, а в СП MSC эквивалентно определению размера указателя на функцию.

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

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

struct {

char m[3][3];

} s;

то значение sizeof(s.m) будет равно 9, а значение sizeof(s) будет равно 10.

Используя операцию sizeof для ссылок на размеры типов данных (которые могут различаться для разных компьютеров), можно повысить переносимость программы. В следующем примере операция sizeof используется для спецификации размера типа int в качестве аргумента стандартной функции распределения памяти calloc. Значение, возвращаемое функцией (адрес выделенного блока памяти), присваивается переменной buffer.

buffer = calloc(100, sizeof(int));

 

Мультипликативные операции

К мультипликативным операциям относятся операции умножения *, деления / и получения остатка от деления %. Операндами операции % должны быть целые значения. Операции умножения * и деления / выполняются над целыми и плавающими операндами. Типы первого и второго операндов могут отличаться, при этом выполняются преобразования операндов по умолчанию. Типом результата является тип операндов после преобразования.

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

Умножение (*)

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

Деление (/)

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

Остаток от деления (%)

Результатом операции является остаток от деления первого операнда на второй. Знак результата совпадает со знаком делимого.

Примеры:

int i = 10, j = 3, n;

double x = 2.0, у,

у = х*i; /* пример 1 */

n = i/j; /* пример 2 */

n = i%j; /* пример 3 */

В первом примере х умножается на i. Результат равен 20.0 и имеет тип double.

Во втором примере 10 делится на 3. Результат округляется до 3 и имеет тип int.

В третьем примере п присваивается остаток от деления 10 на 3, т.е. 1.

 

Аддитивные операции

 

К аддитивным операциям относятся сложение (+) и вычитание (-). Их операндами могут быть целые и плавающие значения. В некоторых случаях аддитивные операции могут также выполняться над адресными значениями. Над операндами выполняются преобразования по умолчанию. Типом результата является тип операндов после преобразования. В процессе выполнения аддитивных операций ситуация переполнения или потери значимости не контролируется. Если результат аддитивной операции не может быть представлен типом операндов после преобразования, то информация теряется.

Сложение (+)

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

Вычитание (-)

Операция вычитания вычитает второй операнд из первого. Операнды могут иметь целый или плавающий тип. Типы первого и второго операндов могут различаться. Допускается вычитание целого из указателя и вычитание двух указателей.

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

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

Тип, который имеет разность указателей, зависит от компьютера, поэтому он определен посредством typedef в стандартном включаемом файле stddef.h. Имя этого типа — ptrdiff.t. Если разность указателей не может быть представлена этим типом, следует явно приводить ее к типу long.

 

Адресная арифметика

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

На компьютерах с сегментной архитектурой памяти (в частности, с микропроцессором типа 8086/8088) аддитивные операции над адресным и целым значениями могут не всегда выполняться правильно. Это вызвано тем, что указатели, используемые в программе, могут иметь различные размеры в зависимости от используемой модели памяти. Например, при компиляции программы в некоторой стандартной модели памяти адресные модификаторы (near, huge, far) могут специфицировать для какого-либо указателя другой размер, чем определяемый по умолчанию выбранной моделью памяти. Более подробная информация о работе с указателями в различных моделях памяти приведена в разделе 8 "Модели памяти".

Примеры:

int i = 4, j;

float x[10];

float *px;

px = &x[4] + 1; /* пример 1 */

j = &x[i] — &x[i-2]; /* пример 2*/

В первом примере целочисленный операнд i складывается с адресом пятого (по порядку следования) элемента массива х. Значение i умножается на длину типа float и складывается с адресом x[4]. Значение результирующего указателя представляет собой адрес девятого элемента массива.

Во втором примере адрес третьего элемента массива х (заданный как &х[i-2]) вычитается из адреса пятого элемента (заданного как &x[i]). Полученная разность делится на размер типа float. В результате получается целое значение 2.

 

Операции сдвига

Операции сдвига сдвигают свой первый операнд влево (<<) или вправо (>>) на число разрядов машинного слова, специфицированное вторым операндом. Оба операнда должны быть целыми значениями. Выполняются преобразования по умолчанию, причем в СП MSC над обоими операндами совместно, а в СП ТС независимо над каждым операндом. Например, если переменная b имеет тип int, а переменная и тип unsigned long, то перед выполнением операции b<

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

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

Если второй операнд отрицателен, то результат операции сдвига не определен.

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

Пример:

unsigned int х, у, z;

х = 0х00АА;

у = 0х5500;

z= (х<<8) + (у>>8);

В примере х сдвигается влево на 8 позиций, а у сдвигается вправо на 8 позиций. Результаты сдвигов складываются, давая значение ОхАА5а, которое присваивается z.

 

Операции отношения

Операции отношения сравнивают первый операнд со вторым и вырабатывают значение 1 (ИСТИНА) или 0 (ЛОЖЬ). Результат имеет тип int. Имеются следующие операции отношения:

Операция Проверяемое отношение
< Первый операнд меньше, чем второй операнд
> Первый операнд больше, чем второй операнд
<= Первый операнд меньше или равен второму операнду
>= Первый операнд больше или равен второму операнду
== Первый операнд равен второму операнду
!= Первый операнд не равен второму операнду

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

Операндами любой операции отношения могут быть два указателя на один и тот же тип. Для операции проверки на равенство или неравенство результат сравнения означает, указывают ли оба указателя на одну и ту же ячейку памяти или нет. Результат сравнения указателей для других операций ( <, >, <=, >=) отражает относительное положение двух адресов памяти.

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

Сравнение между собой указателей типа far не всегда имеет смысл, поскольку один и тот же адрес может быть представлен различными комбинациями значений сегмента и смещения и, следовательно, различными указателями типа far. Указатели типа huge в СП ТС хранятся в нормализованном формате, поэтому их сравнение всегда корректно.

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

Из-за специфики машинной арифметики не рекомендуется проверять плавающие значения на равенство, поскольку 1.0/3.0*3.0 не будет равно 1.0.

Примеры:

int х, у;

х < у /* выражение 1 */

у > х /* выражение 2 */

x <= У /* выражение 3 */

x >= У /* выражение 4 */

x == У /* выражение 5 */

x != у /* выражение 6 */

Если х и у равны, то выражения 3, 4, 5 имеют значение 1, а выражения 1, 2, 6 имеют значение 0.

 

Поразрядные операции

Поразрядные операции выполняют над разрядами своих операндов логические функции И (&), включающее ИЛИ (|) и исключающее ИЛИ (^). Операнды поразрядных операций должны иметь целый тип, но бит знака, если он есть, также участвует в операции. Над операндами выполняются преобразования по умолчанию. Тип результата определяется типом операндов посте преобразования.

Таблица значений для поразрядных операций:

х 0 0 1 1
у 0 1 0 1
х|у 0 1 1 1
х&у 0 0 0 1
x^y 0 1 1 0

Примеры:

short i = 0хAВ00;

short j = 0xABCD;

short n;

n = i & j; /* пример 1 */

n = i | j; /* пример 2 */

n = i ^ j; /* пример 3 */

В первом примере n присваивается шестнадцатеричное значение АВ00.

Во втором примере результатом операции включающего ИЛИ будет шестнадцатеричное значение ABCD, а в третьем примере результатом операции исключающего ИЛИ будет шестнадцатеричное значение CD.

 

Логические операции

Логические операции выполняют над своими операндами логические функции И (&&) и ИЛИ (||). Операнды логических операций могут иметь целый, плавающий тип, либо быть указателями. Типы первого и второго операндов могут различаться. Сначала всегда вычисляется первый операнд; если его значения достаточно для определения результата операции, то второй операнд не вычисляется.

Логические операции не выполняют преобразования по умолчанию. Вместо этого они вычисляют операнды и сравнивают их с нулем. Результатом логической операции является либо 0 (ЛОЖЬ), либо 1 (ИСТИНА). Тип результата — int.

Логическое И (&&)

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

Логическое ИЛИ (||)

Логическая операция ИЛИ выполняет над своими операндами операцию включающее ИЛИ. Она вырабатывает значение 0, если оба операнда имеют значение 0; если какой-либо из операндов имеет ненулевое значение, то результат операции равен 1. Если первый операнд не равен нулю, то значение второго операнда не вычисляется.

Примеры:

int х, у;

if(х<у && у

if(х==у !! х==z) printf("x равен у или z\n"); /* пример 2 */

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

Во втором примере сообщение печатается в том случае, если х равен у или z. Если х равен у, то значение второго операнда (х==z) не вычисляется.

 

Операция последовательного вычисления

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

<выражение1>, <выражение2>

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

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

Примеры:

/* пример 1 */

for(i=j=1; i+j<20; i+=i, j--)…

/* пример 2 */

func_one( x, у + 2, z);

func_two((x--, y + 2), z);

В первом примере каждый операнд третьего выражения оператора цикла for вычисляется независимо. Сначала вычисляется i+=i, затем j--.

Во втором примере символ "запятая" используется как разделитель в двух различных контекстах. В первом вызове функции func_onc передаются три аргумента, разделенных запятыми: х, у+2, 2. Здесь символ "запятая" используется просто как разделитель

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

 

Условная операция

В языке Си имеется одна тернарная операция — уловная. Она имеет следующий синтаксис:

<операнд1> ? <операнд2>: <операнд3>

Выражение <операнд1> вычисляется и сравнивается с нулем. Выражение может иметь целый, плавающий тип, либо быть указателем. Если <операнд1> имеет ненулевое значение, то вычисляется <операнд2> и результатом условной операции является его значение. Если же <операнд1> равен нулю, то вычисляется <операнд3> и результатом является его значение. В любом случае вычисляется только один из операндов, <операнд2> или <операнд3>, но не оба.

Тип результата зависит от типов второго и третьего операндов (они могут различаться) следующим образом:

1) Если второй и третий операнды имеют целый или плавающий тип, то выполняются преобразования по умолчанию. Типом результата является тип операндов после преобразования.

2) Второй и третий операнды могут быть структурами, объединениями или указателями одного и того же типа. Типом результата будет тот же самый тип структуры, объединения или указателя.

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

4) Если либо второй, либо третий операнд является указателем на какой-либо тип, а другой является указателем на void, то результат имеет тип указатель на void.

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

Пример:

j = (i < 0 )?(-i) : (i);

В примере j присваивается абсолютное значение i. Если i меньше нуля, то j присваивается -i. Если i больше или равно нулю, то j присваивается i.

 

Операции присваивания

 

В языке Си имеются следующие операции присваивания:

Операция Действие
++ Унарный инкремент
-- Унарный декремент
= Простое присваивание
*= Умножение с присваиванием
/= Деление с присваиванием
%= Остаток от деления с присваиванием
+= Сложение с присваиванием
-= Вычитание с присваиванием
<<= Сдвиг влево с присваиванием
>>= Сдвиг вправо с присваиванием
&= Поразрядное И с присваиванием
|= Поразрядное включающее ИЛИ с присваиванием
^= Поразрядное исключающее ИЛИ с присваиванием

При присваивании тип правого операнда преобразуется к типу левого операнда. Специфика этого преобразования зависит от обоих типов и подробно описана в разделе 4.7.1. Левый (или единственный) операнд операции присваивания должен быть модифицируемым L-выражением (см. раздел 4.2.7).

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

 

Операции инкремента и декремента

Операции ++ и -- инкрементируют (увеличивают на единицу) и декрементируют (уменьшают на единицу) свой операнд. Операнд должен иметь целый, плавающий тип или быть указателем. В качестве операнда допустимо только модифицируемое L-выражение.

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

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

Примеры:

/* пример 1 */

if(pos++ > 0) *p++ = *q++;

/* пример 2 */

if(line[--i] != '\n') return;

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

Во втором примере переменная i декрементируется перед ее использованием в качестве индекса массива line.

 

Простое присваивание

Операция простого присваивания обозначается знаком =. Значение правого операнда присваивается левому операнду. Левый операнд должен быть модифицируемым L-выражением. При присваивании выполняются правила преобразования типов, описанные в разделе 4.7.1.

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

Пример 1:

double х;

int у;

х = у; Значение у преобразуется к типу double и присваивается х.

Пример 2:

int а, b, с; b = 2; a = b + (с = 5);

Переменной с присваивается значение 5, переменной а — значение b + 5, равное 7.

 

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

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

<выражение1> += <выражение2>

Оно может быть записано и таким образом:

<выражение1> = <выражение1> + <выражение2>

Значение операции вырабатывается по тем же правилам, что и для операции простого присваивания. Однако выражение составного присваивания не эквивалентно обычной записи, поскольку в выражении составного присваивания <выражение1> вычисляется только один раз, в то время как в обычной записи оно вычисляется дважды: в операции сложения и в операции присваивания. Например, оператор

*str1.str2.ptr += 5;

легче для понимания и выполняется быстрее, чем оператор

*str1.str2.ptr = *str1.str2.ptr + 5;

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

Пример:

n &= 0xFFFE;

В этом примере операция поразрядное И выполняется над n и шестнадцатеричным значением FFFE, и результат присваивается n.

 

Приоритет и порядок выполнения

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

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

Таблица 4.1.

Приоритет и ассоциативность операций в языке Си

Знак операции Наименование Ассоциативность
() [] . -> Первичные Слева направо
+ - ~ ! * & ++ -- sizeof приведение типа Унарные Справа налево
* / % Мультипликативные Слева направо
+ - Аддитивные Слева направо
>> << Сдвиг Слева направо
< > <= >= Отношение Слева направо
== != Отношение Слева направо
& Поразрядное И Слева направо
^ Поразрядное исключающее ИЛИ Слева направо
| Поразрядное включающее ИЛИ Слева направо
&& Логическое И Слева направо
|| Логическое ИЛИ Слева направо
?: Условная Справа налево
= *= /= %= += -= <<= >>= &= |= ^= Простое и составное присваивание Справа налево
, Последовательное вычисление Слева направо

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

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

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

а = b & 0xFF + 5

вычисляется как

а = b & (0xFF + 5),

а выражение

а +с >> 1

вычисляется как

(а + с) >> 1

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

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

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

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

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

Пример:

int х, у, z, f();

z = х > у || f(x, у);

Сначала вычисляется выражение х>у. Если оно истинно, то переменной z присваивается значение 1, а функция f не вызывается. Если же значение х не больше у, то вычисляется выражение f(x,y). Если функция f возвращает ненулевое значение, то переменной z присваивается 1, иначе 0. Отметим также, что при вызове функции f гарантируется, что значение ее первого аргумента больше второго.

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

if(!feof(pf)) && (с = getc(pf)) …

Здесь feof — функция проверки на конец файла, getc — функция чтения символа из файла (см. раздел 12).

В-третьих, можно гарантировать, что в выражении f(x)&&g(y) функция f будет вызвана раньше, чем функция g. Для выражения f(x)+g(y) этого утверждать нельзя.

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

Выражение Группирование операндов
a & b || c (a & b) || c
a = b || c a = (b || c)
q && r || s-- (q && r) || (s--)
p == 0 ? p += 1 : p += 2 (p == 0 ? p += 1 : p) += 2

В первом примере поразрядная операция И (&) имеет больший приоритет, чем -логическая операция ИЛИ (||), поэтому выражение а&b является первым операндом логической операции ИЛИ.

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

В третьем примере показано синтаксически корректное выражение, которое может выработать неожиданный результат. Логическая операция И (&&) имеет более высокий приоритет, чем логическая операция ИЛИ (||), поэтому запись q&&r образует операнд. Поскольку логические операции сначала вычисляют свой левый операнд, то выражение q&&r вычисляется раньше, чем s--. Однако если q&&r дает ненулевое значение, то s-- не будет вычисляться и s не декрементируется. Более надежно было бы поместить s-- на место первого операнда выражения либо декрементировать s отдельной операцией.

В четвертом примере показано неверное выражение, которое приведет к ошибке при компиляции. Операция равенства (==) имеет наибольший приоритет, поэтому p==0 группируется в операнд. Тернарная операция ?: имеет следующий приоритет. Ее первым операндом является выражение p==0, вторым операндом — выражение p+=1. Однако последним операндом тернарной операции будет считаться p, а не p+=2. так как в данном случае идентификатор p по приоритету операций связан более тесно с тернарной операцией, чем с составной операцией сложения с присваиванием. В результате возникает синтаксическая ошибка, поскольку левый операнд составной операции присваивания не является L-выражением.

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

(р == 0) ? (р += 1) : (р += 2)

 

Побочные эффекты

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

Порядок вычисления выражения зависит от реализации компилятора, за исключением случаев, в которых явно гарантируется определенный порядок вычислений (см. раздел 4.5). При вычислении выражения в языке Си существуют так называемые контрольные точки. По достижении контрольной точки все предшествующие вычисления, в том числе все побочные эффекты, гарантированно произведены. Контрольными точками являются операция последовательного вычисления, условная операция, логические операции И и ИЛИ, вызов функции. Другие контрольные точки:

—конец полного выражения (т.е. выражения, которое не является частью другого выражения);

—конец инициализирующего выражения для переменной класса памяти auto;

—конец выражений, управляющих выполнением операторов if, switch, for, do, while и выражения в операторе return. Приведем примеры побочных эффектов:

add(i + 1, i = j + 2);

Аргументы вызова функции add могут быть вычислены в любом порядке. Выражение i+1 может быть вычислено перед выражением i=j+2, или после него, с различным результатом в каждом случае.

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

int i, а [10];

i = 0;

a[i++] = i;

Неизвестно, какое значение будет присвоено элементу а[0] — нуль или единица, поскольку для операции присваивания порядок вычисления аргументов не оговаривается.

 

Преобразования типов

 

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

 

Преобразования типов при присваивании

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

Тип long double ведет себя в преобразованиях аналогично типу double.

Преобразования знаковых целых типов Знаковое целое значение преобразуется к короткому знаковому целому значению (short signed int) посредством усечения старших битов. Знаковое целое значение преобразуется к длинному знаковому целому значению (long signed int) путем расширения знака влево. Преобразование знаковых целых значений к плавающим значениям происходит путем преобразования к типу long, а затем преобразования к плавающему типу. При этом возможна некоторая потеря точности. При преобразовании знакового целого значения к беззнаковому целому значению (unsigned int) производится лишь преобразование к размеру беззнакового целого типа, и результат интерпретируется как беззнаковое целое значение.

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

Таблица 4.2.

Преобразование знаковых целых типов

От типа К типу Метод
char short дополнение знаком
char long дополнение знаком
char unsigned char сохранение битового представления;
char unsigned short старший бит теряет функцию знакового бита дополнение знаком до short; преобразование short в unsigned short
char unsigned long дополнение знаком до long; преобразование long в unsigned long
char float дополнение знаком до long; преобразование long к float
char double дополнение знаком до long; преобразование long к double
short char сохранение младшего байта
short long дополнение знаком
short unsigned char сохранение младшего байта
short unsigned short сохранение битового представления; старший бит теряет функцию знакового бита
short unsigned long дополнение знаком до long; преобразование long в unsigned long
short float дополнение знаком до long; преобразование long к float
short double дополнение знаком до long; преобразование long к double
long char сохранение младшего байта
long short сохранение младшего слова
long unsigned char сохранение младшего байта
long unsigned short сохранение младшего слова
long unsigned long сохранение битового представления; старший бит теряет функцию знакового бита
long float представляется как float; возможна некоторая потеря точности
long double представляется как double; возможна некоторая потеря точности

Примечание. В СП MSC и СП ТС тип int эквивалентен типу short и преобразование для типа int производится как для типа short. В некоторых реализациях языка Си тип int эквивалентен типу long и преобразование для типа int производится как для типа long.

Преобразование беззнаковых целых типов

Беззнаковое целое значение преобразуется к короткому беззнаковому целому значению или короткому знаковому целому значению путем усечения старших битов. Беззнаковое целое значение преобразуется к длинному беззнаковому целому значению или длинному знаковому целому значению путем дополнения нулями слева. Беззнаковое целое значение преобразуется к значению с плавающей точкой путем преобразования к типу long, а затем преобразования значения типа long к значению с плавающей точкой.

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

Правила преобразования беззнаковых целых типов приведены в таблице 4.3.

Таблица 4.3.

Преобразование беззнаковых целых типов

От типа К типу Метод
unsigned char char сохранение битового представления; старший бит становится знаковым
unsigned char short дополнение нулевыми битами
unsigned char long дополнение нулевыми битами
unsigned char unsigned short дополнение нулевыми битами
unsigned char unsigned long дополнение нулевыми битами
unsigned char float дополнение нулевыми битами до long; преобразование long к float
unsigned char double дополнение нулевыми битами до long; преобразование long к double
unsigned short char сохранение младшего байта
unsigned short short сохранение битового представления; старший бит становится знаковым
unsigned short long дополнение нулевыми битами
unsigned short unsigned char сохранение младшего байта
unsigned short unsigned long дополнение нулевыми битами
unsigned short float дополнение нулевыми битами до long; преобразование long к float
unsigned short double дополнение нулевыми битами до long; преобразование long к double
unsigned long char сохранение младшего байта
unsigned long short сохранение младшего слова
unsigned long long сохранение битового представления; старший бит становится знаковым
unsigned long unsigned char сохранение младшего байта
unsigned long unsigned short сохранение младшего слова
unsigned long float преобразование к long; преобразование long к float
unsigned long double преобразование к long; преобразование long к double (в версии 5 СП MSC это преобразование производится напрямую, без промежуточного типа long)

Примечание. В СП MSC и СП ТС тип unsigned int эквивалентен типу unsigned short и преобразование для типа unsigned int производится как для типа unsigned short. В некоторых реализациях языка Си тип unsigned int эквивалентен типу unsigned long и преобразование для типа int производится как для типа unsigned long.

Преобразование плавающих типов Значения типа float преобразуются к типу double без потери точности. Значения типа double при преобразовании к типу float представляются с некоторой потерей точности. Однако если порядок значения типа double слишком велик для представления экспонентой значения типа float, то происходит потеря значимости, о чем сообщается во время выполнения.

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

Таблица 4.4.

От типа К типу Метод
float char преобразование к long; преобразование long к char
float short преобразование к long; преобразование long к short
float long усечение дробной части; результат не определен, если он слишком велик для представления типом long
float unsigned short преобразование к long; преобразование long к unsigned short
float unsigned long преобразование к long; преобразование long к unsigned long
float double дополнение мантиссы нулевыми битами справа
double char преобразование к float; преобразование float к char
double short преобразование к float; преобразование float к short
double long усечение дробной части; результат не определен, если он слишком велик для представления типом long
double unsigned short преобразование к long; преобразование long к unsigned short
double unsigned long преобразование к long; преобразование long к unsigned long
double float усечение младших битов мантиссы; возможна потеря точности; если значение слишком велико для представления типом float, то результат преобразования не определен

Преобразование указателей

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

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

Указатели на любые типы данных могут быть преобразованы к указателям на функции, и обратно. Однако в СП MSC для того, чтобы присвоить указатель на данные указателю на функцию (или наоборот), необходимо выполнить явное приведение его типа.

Специальные ключевые слова near, far, huge позволяют модифицировать формат и размер указателей в программе. Компилятор учитывает принятый в выбранной модели памяти размер указателей и может в некоторых случаях неявно производить соответствующие преобразования адресных значений. Так, передача указателя в качестве аргумента функции может вызвать неявное преобразование его размера к большему из следующих двух значений:

—принятому по умолчанию размеру указателя для действующей модели памяти (например, в средней модели указатель на данные имеет тип near);

—размеру типа аргумента.

Если задано предварительное объявление функции, в котором указан явно тип аргумента-указателя, в т.ч. с модификаторами near, far, huge, то будет преобразование именно к этому типу.

Указатель может быть преобразован к значению целого типа. Метод преобразования зависит от размера указателя и размера целого типа следующим образом:

—если указатель имеет тот же самый или меньший размер, чем целый тип, то указатель преобразуется по тем же правилам, что и беззнаковое целое;

—если размер указателя больше, чем размер целого типа, то указатель сначала преобразуется к указателю того же размера, что и целый тип, а затем преобразуется к целому типу.

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

Преобразования других типов

Из определения перечислимого типа следует, что его значения имеют тип int. Поэтому преобразования к перечислимому типу и из него осуществляются так же, как для типа int.

Недопустимы преобразования объектов типа структура или объединение.

Тип void не имеет значения по определению. Поэтому он не может быть преобразован к другому типу, и никакое значение не может быть преобразовано к типу void путем присваивания. Тем не менее, значение может быть явно преобразовано операцией приведения типа к типу void (см. раздел 4.7.2).

 

Явные преобразования типов

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

(<абстрактное-имя-типа>) <операнд>

<абстрактное-имя-типа> — специфицирует некоторый тип; <операнд> — выражение, значение которого должно быть преобразовано к специфицированному типу (абстрактные имена типов рассмотрены в разделе 3.8.3).

Преобразование операнда осуществляется так, как если бы он присваивался переменной типа <имя-типа>. Правила преобразования для операции присваивания, приведенные в разделе 4.7.1, полностью действуют для операции приведения типа. Однако, преобразование к типу char или short выполняется как преобразование к int. а преобразование к типу float — как преобразование к double.

Имя типа void может быть использовано в операции приведения типа, но результирующее выражение не может быть присвоено никакому объекту, и ему также нельзя ничего присвоить. Значение типа void не может быть приведено ни к какому типу; например, результат функции, возвращающей void, не может быть присвоен.

Результат операции приведения типа L-выражения сам является L-выражением и может представлять левый (или единственный) операнд операции присваивания, если приведенный тип не превышает по размеру исходный тип.

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

int (*р)(long); /* объявление указателя на функцию */

(*(int(*)(int))р)(0); /*вызов функции по указателю */

В операции приведения типа можно также задавать объявление структурного типа (тега), например:

(struct {int a; int b;} *)р->а = 5;

Область действия этого тега распространяется в СП MSC на остаток блока, а в СП ТС — на остаток тела функции.

 

Преобразования типов при вызовах функций

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

Если предварительное объявление имеется, и оно содержит список типов аргументов, то компилятор осуществляет контроль типов. Процесс контроля типов подробно описан в разделе 6.4.1 "Фактические аргументы".

Если предварительное объявление отсутствует, или в нем опущен список типов аргументов, то над аргументами вызываемой функции выполняются только преобразования по умолчанию. Преобразования выполняются отдельно для каждого аргумента вызова. Смысл этих преобразований сводится к тому, что значения типа float преобразуются к типу double, значения типов char и short преобразуются к типу int, значения типов unsigned char и unsigned short преобразуются к типу unsigned int.

Если в вызываемой функции в объявлениях формальных параметров - указателей используются модификаторы near, far, huge, то могут также происходить неявные преобразования формата указателей, передаваемых в функцию. Процесс преобразования можно контролировать путем задания соответствующих модификаторов в предварительном объявлении функции в списке типов аргументов - указателей. В этом случае компилятор выполнит преобразования в соответствии со списком типов аргументов.