В разделах 3.8.1 и 3.8.2 рассматривались объявления, в которых типам присваиваются идентификаторы для последующего использования. Однако иногда возникает необходимость специфицировать некоторый тип данных без присвоения ему идентификатора и без объявления какого-либо объекта. Такая конструкция, определяющая тип без имени, называется абстрактным именем типа. Абстрактные имена типов используются в трех контекстах: в списках типов аргументов при объявлении функций, в операции приведения типа и в операции sizeof. Списки типов аргументов рассматривались в разделе 3.5 "Объявление функции". Операция приведения типа и операция sizeof обсуждаются в разделах 4.7.2 и 4.3.2, соответственно.
Абстрактными именами для базовых, перечислимых, структурных типов и объединений являются просто соответствующие им спецификации типа. Если в абстрактном имени типа задано определение тега (см. раздел 3.8.1), то область действия этого тега распространяется в СП MSC на остаток блока, а в СП ТС — на остаток тела функции. Абстрактные имена для типов указатель, массив и функция задаются следующей синтаксической конструкцией:
<спецификация типа> <абстрактный описатель>
Абстрактный описатель отличается от обычного Описателя только тем, что он не содержит идентификатора. Как и обычный описатель, он может содержать один или более признаков указателя, массива и функции. Для интерпретации абстрактного описателя следует прежде всего определить в нем место подразумеваемого идентификатора. После этого интерпретация проводится так же, как описано в разделе 3.3.2. Абстрактный описатель, состоящий только из пары пустых круглых скобок, недопустим, поскольку он не позволяет однозначно определить, где подразумевается идентификатор: если внутри скобок, то описан простой тип (заданный только спецификацией типа), а если перед скобками, то тип функция.
Объявления typedef, рассмотренные в разделе 3.8.2, позволяют присваивать короткие, осмысленные идентификаторы абстрактным именам типов и могут использоваться в том же контексте, что и они.
Примеры:
long * /* пример 1 */
int (*)[5] /* пример 2 */
int (*)(void) /* пример 3 */
PG /* пример 4 */
В первом примере задано абстрактное имя типа для указателя на тип long.
Во втором примере задано абстрактное имя типа для указателя на массив из пяти элементов типа int.
В третьем примере задано абстрактное имя типа для указателя на функцию, не требующую аргументов и возвращающую значение типа int.
В четвертом примере с помощью идентификатора PG, объявленного посредством typedef
в разделе 3.8.2, задано абстрактное имя типа "указатель на структуру с тегом club".
К аддитивным операциям относятся сложение (+) и вычитание (-). Их операндами могут быть целые и плавающие значения. В некоторых случаях аддитивные операции могут также выполняться над адресными значениями. Над операндами выполняются преобразования по умолчанию. Типом результата является тип операндов после преобразования. В процессе выполнения аддитивных операций ситуация переполнения или потери значимости не контролируется. Если результат аддитивной операции не может быть представлен типом операндов после преобразования, то информация теряется.
Сложение (+)
Операция сложения складывает два своих операнда. Операнды могут иметь целый или плавающий тип. Типы первого и второго операндов могут различаться. Один из операндов может быть указателем; тогда другой должен быть целым значением. Когда целое значение (назовем его 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.
В программах на языке Си используются два множества символов: множество символов языка Си и множество представимых
символов. Множество символов языка Си содержит буквы, цифры и знаки пунктуации, которые имеют определенный смысл для компилятора языка Си. Программы на языке Си строятся путем комбинирования в осмысленные синтаксические конструкции символов из множества символов языка Си.
Множество символов языка Си является подмножеством множества представимых символов. Множество представимых символов состоит из всех букв, цифр и символов, которые могут быть представлены как отдельный символ на клавиатуре данного персонального компьютера.
Программа на языке Си может содержать только символы из множества символов языка Си, однако внутри символьных строк, символьных констант и комментариев может быть использован любой представимый
символ. Компилятор языка Си выдает сообщение об ошибке при обнаружении неверно использованных символов.
В последующих разделах описываются символы из множества символов языка Си и объясняются правила их использования.
В языке Си реализован набор типов данных, называемых "базовыми" типами. Спецификации этих типов перечислены в таблице 3.1.
Таблица 3.1.
| Базовые типы | Спецификация типов | ||||
| Целые | signed char | знаковый символьный | |||
| signed int | знаковый целый | ||||
| signed short int | знаковый короткий целый | ||||
| signed long int | знаковый длинный целый | ||||
| unsigned char | беззнаковый символьный | ||||
| unsigned int | беззнаковый целый | ||||
| unsigned short int | беззнаковый короткий целый | ||||
| unsigned long int | беззнаковый длинный целый | ||||
| Плавающие | float | плавающий одинарной точности | |||
| double | плавающий двойной точности | ||||
| long float | длинный плавающий одинарной точности | ||||
| long double | длинный плавающий двойной точности | ||||
| Прочие | void | пустой | |||
| enum | перечислимый |
Тип long float реализован только в версии 4.0 СП MSC и эквивалентен типу double. В версии 5.0 СП MSC и в СП ТС реализован тип long double, причем в версии 5.0 СП MSC и версии 1.5 СП ТС он эквивалентен типу double, а в версии 2.0 СП ТС является самостоятельным типом размером 80 битов.
Типы char, int, short
и long имеют две формы — знаковую (signed) и беззнаковую (unsigned). В совокупности они образуют целый тип. Перечислимый тип также служит для представления целых значений, однако, переменная перечислимого типа может принимать значения только из набора, заданного в ее объявлении. Спецификации типов float
и double относятся к плавающему типу.
Целый тип (включая перечислимый тип) и плавающий тип в совокупности образуют арифметический тип.
Тип void (пустой) имеет специальное назначение. Указание спецификации типа void в объявлении функции означает, что функция не возвращает значений. Указание типа void в списке объявлений аргументов в объявлении функции означает, что функция не принимает аргументов. Можно объявить указатель на тип void; он будет указывать на любой, т.е. неспецифицированный тип. Тип void может быть указан в операции приведения типа. Приведение значения выражения к типу void явно указывает на то, что это значение не используется. Нельзя объявить переменную типа void.
|
Спецификация типа |
Сокращение |
|
signed char |
char |
|
signed int |
signed, int |
|
signed short int |
short, signed short |
|
signed long int |
long, signed long |
|
unsigned char |
- |
|
unsigned int |
unsigned |
|
unsigned short int |
unsigned short |
|
unsigned long int |
unsigned long |
|
float |
- |
|
long float |
double |
|
long double |
double (СП MSC 5.0, СП TC 1.5) |
|
long double |
—(СП TC 2.0) |
Синтаксис:
=<выражение>
Значение выражения присваивается переменной. При необходимости выполняются правила преобразования типов.
Примеры:
int х = 10, у = 20; /* пример 1 */
register int *рх = 0; /* пример 2 */
int с = (3*1024); /* пример 3 */
int *b = &x; /* пример 4 */
В первом примере переменная х инициализируется константным выражением 10, переменная у
инициализируется константным выражением 20. Во втором примере указатель рх инициализирован нулевым значением. В третьем примере используется константное выражение для инициализации переменной с. В четвертом примере указатель b инициализируется адресом переменной х.
Множество символов языка Си включает прописные и строчные буквы латинского алфавита и арабские цифры:
прописные латинские буквы: ABCDEFGHIJKLMNOPQRSTUVWXYZ;
строчные латинские буквы: abcdefghijkimnopqrstuvwxyz;
десятичные цифры: 0123456789.
Буквы и цифры используются при формировании констант, идентификаторов и ключевых слов (эти конструкции описаны ниже).
Компилятор языка Си рассматривает одну и ту же прописную и строчную буквы как различные символы.
Целая константа — это десятичное, восьмеричное или шестнадцатеричное число, которое представляет целое значение.
Десятичная константа имеет следующий формат представления:
<цифры>
<цифры> — последовательность из одной или более десятичных цифр от 0 до 9.
Восьмеричная константа имеет следующий формат представления:
0<в-цифры>
<в-цифры>—это одна или более восьмеричных цифр от 0 до 7. Запись нуля впереди обязательна.
Шестнадцатеричная константа имеет следующий формат представления:
0х<ш-цифры>
или 0Х<ш-цифры>
<ш-цифры> —одна или более шестнадцатеричных цифр. Шестнадцатеричная цифра может быть цифрой от 0 до 9 или буквой (большой или малой) от А до F. Допускается "смесь" больших и малых букв. Запись нуля впереди и следующего за ним символа х или Х обязательна.
Между цифрами целой константы пробельные символы недопустимы. В таблице 1.4 приведены примеры целых констант. Константы, записанные в одной строке таблицы, используются для представления одного и того же значения.
Таблица 1.4.
| Десятичные константы | Восьмеричные константы | Шестнадцатеричные константы | |||
| 10 | 012 | 0ха или 0хА | |||
| 132 | 0204 | 0х84 | |||
| 32179 | 076663 | 0x7dB3 или 0x7DB3 |
Целые константы всегда специфицируют положительные значения. Если требуется отрицательное значение, то необходимо сформировать константное выражение из знака минус и следующей за ним константы. Знак минус рассматривается при этом как арифметическая операция.
Каждая целая константа имеет тип, определяющий ее представление в памяти (описание типов приведено в разделе 3.1 "Базовые типы данных"). Десятичные константы могут иметь тип int (целый тип) или long (длинный целый тип).
Восьмеричные и шестнадцатеричные константы в зависимости от размера могут иметь тип int, unsigned int, long или unsigned long. Если константа может быть представлена типом int, то компилятор языка Си присваивает ей тип int. Если ее значение больше, чем максимальное положительное значение, которое может быть представлено типом int, но может быть представлено тем же числом битов, что и int, ей присваивается тип unsigned int. Наконец, константа, значение которой больше, чем максимальное значение, представляемое типом unsigned int, задается типом long
|
Десятичные константы |
Восьмеричные константы |
Шестнадцатеричные константы |
Тип |
|
0—32767 |
0—077777 |
0х0—0x7FFF |
int |
|
0100000—0177777 |
0х8000—0xFFFF |
unsigned int |
|
|
32767—2147483647 |
02000001—017777777777 |
0х10000—0x7FFFFFFF |
long |
|
020000000000—030000000000 |
0х80000000—0xFFFFFFFF |
unsigned long |
|
Десятичные константы |
Восьмеричные константы |
Шестнадцатеричные константы |
|
12L |
012L |
0xaL или 0xAL |
|
0l |
0115l |
0х4fl или 0x4Fl |
Синтаксис:
#define <идентификатор> <текст>
#define <идентификатор> <список параметров> <текст>
Директива #define заменяет все вхождения <идентификатора> в исходном файле на <текст>, следующий в директиве за <идентификатором>. Этот процесс называется макроподстановкой, <идентификатор> заменяется лишь в том случае, если он представляет собой отдельную лексему. Например, если <идентификатор> является частью строки или более длинного идентификатора, он не заменяется. Если за <идентификатором> следует <список параметров>, то директива определяет макроопределение с аргументами.
<Текст> представляет собой набор лексем, таких как ключевые слова, константы, идентификаторы или выражения. Один или более пробельных символов должны отделять <текст> от <идентификатора> (или от заключенных в скобки параметров). Если текст не умещается на строке, то он может быть продолжен на следующей строке; для этого следует набрать в конце строки символ обратный слэш и сразу за ним нажать клавишу ENTER.
<Текст> может быть опущен. В этом случае все экземпляры <идентификатора> будут удалены из исходного текста программы. Тем не менее, сам <идентификатор> рассматривается как определенный и при проверке директивой #if
дает значение 1 (смотри раздел 7.4.1).
<Список параметров>, если он задан, содержит один или более идентификаторов, разделенных запятыми. Идентификаторы в списке должны отличаться друг от друга. Их область действия ограничена макроопределением, в котором они заданы. Список должен быть заключен в круглые скобки. Имена формальных параметров в <тексте> отмечают позиции, в которые должны быть подставлены фактические аргументы макровызова. Каждое имя формального параметра может появиться в <тексте> произвольное число раз.
В макровызове следом за <идентификатором> записывается в круглых скобках список фактических аргументов, соответствующих формальным параметрам из <списка параметров>. <Текст> модифицируется путем замены каждого формального параметра на соответствующий фактический аргумент. Списки фактических аргументов и формальных параметров должны содержать одно и то же число элементов.
В СП ТС
реализована директива #error. Ее формат:
#error <текст>
Обычно эту директиву записывают среди директив условной компиляции для обнаружения некоторой недопустимой ситуации. По директиве #error
препроцессор прерывает компиляцию и выдает следующее сообщение:
Fatal: <имя-файла> <номер-строки> Error directive: <текст>
Fatal — признак фатальной ошибки; <имя-файла> — имя исходного файла; <номер-строки> — текущий номер строки; Error directive — сообщение об ошибке в директиве; <текст> — собственно текст диагностического сообщения.
Например, если именованная константа MYVAL может иметь значение либо 0, либо 1, можно поместить в исходный файл операторы условной компиляции для проверки на некорректное значение MYVAL:
#if (MYVAL != 0 && MYVAL != 1)
#error MYVAL должно иметь значение либо 0, либо 1
#endif
Препроцессор просматривает текст сообщения в директиве #error, и исключает из него комментарии (если они имеются), но именованные константы и макроопределения в тексте не выявляет и макроподстановку не производит.
Синтаксис:
#undef <идентификатор>
Директива #undef отменяет действие текущего определения #define для <идентификатора>. Чтобы отменить макроопределение посредством директивы #undef, достаточно задать его <идентификатор>. Задание списка параметров не требуется.
Не является ошибкой применение директивы #undef
к идентификатору, который ранее не был определен (или действие его определения уже отменено). Это может использоваться для гарантии того, что идентификатор не определен.
Директива #undef обычно используется в паре с директивой #define, чтобы создать область исходной программы, в которой некоторый идентификатор определен.
Пример:
#define WIDTH 80
#define ADD(X, Y) (X)+(Y)
#undef WIDTH
#undef ADD
В этом примере директива #undef отменяет определение именованной константы WIDTH и макроопределения ADD. Обратите внимание на то, что для отмены макроопределения задается только его идентификатор.
Синтаксис:
#if <ограниченное-константное-выражение> [<текст>]
[#elif <ограниченное-константное-выражение> <текст>]
[#elif <ограниченное-константное-выражение> <текст>]
[#else <текст>]
#endif
Директива #if совместно с директивами #elif, #else и #endif управляет компиляцией частей исходного файла. Каждой директиве #if в том же исходном файле должна соответствовать завершающая ее директива #endif. Между директивами #if и #endif
допускается произвольное количество директив #elif
(в том числе ни одной) и не более одной директивы #else. Если директива #else присутствует, то между ней и директивой #endif на данном уровне вложенности не должно быть других директив #elif.
Препроцессор выбирает один из участков <текста> для обработки. <Текст> может занимать более одной строки. Обычно это участок программного текста, однако это не обязательно: препроцессор можно использовать для обработки произвольного текста. Если <текст> содержит директивы препроцессора (в том числе и директивы условной компиляции), то эти директивы выполняются. Обработанный препроцессором текст передается на компиляцию.
Участок текста, не выбранный препроцессором, игнорируется на стадии препроцессорной обработки и не компилируется.
Препроцессор выбирает участок текста для обработки на основе вычисления <ограниченного-константного-выражения>, следующего за каждой директивой #if или #elif. Выбирается <текст>, следующий за <ограниченным-константным-выражением> со значением истина (не нуль), вплоть до ближайшей директивы #elif, #else, или #endif, ассоциированной с данной директивой #if.
Если ни одно ограниченное константное выражение не истинно, то препроцессор выбирает <текст>, следующий за директивой #else. Если же директива #else отсутствует, то никакой текст не выбирается.
Ограниченное константное выражение описано в разделе 4.2.9 "Константные выражения". Такое выражение не может содержать операцию sizeof (в СП ТС — может), операцию приведения типа, константы перечисления и плавающие константы, но может содержать препроцессорную операцию defined(<идентификатор>). Эта операция дает истинное (не равное нулю) значение, если заданный <идентификатор> в данный момент определен; в противном случае выражение ложно (равно нулю). Следует помнить, что идентификатор, определенный без значения, тем не менее рассматривается как определенный. Операция defined
Синтаксис:
#ifdef <идентификатор>
#ifndef <идентификатор>
Аналогично директиве #if, за директивами #ifdef и #ifndef может следовать набор директив #elif и директива #else. Набор должен быть завершен директивой #endif.
Использование директив #ifdef и #ifndef эквивалентно применению директивы #if,
использующей выражение с операцией defined(<идентификатор>). Эти директивы поддерживаются исключительно для совместимости с предыдущими версиями компиляторов языка Си. Для новых программ рекомендуется использовать директиву #if
с операцией defined(<идентификатор>).
Когда препроцессор обрабатывает директиву #ifdef, он проверяет, определен ли в данный момент <идентификатор> директивой #define. Если да, условие считается истинным, если нет — ложным.
Директива #ifndef противоположна по действию директиве #ifdef. Если <идентификатор> не был определен директивой #define, или его определение уже отменено директивой #undef, то условие считается истинным. В противном случае условие ложно.
Фактический аргумент может быть любым значением базового типа, структурой, объединением или указателем. Все фактические аргументы передаются по значению. Массивы и функции не могут быть переданы как параметры, могут передаваться указатели на эти объекты. Поэтому массивы и функции передаются по ссылке. Значения фактических аргументов копируются в соответствующие формальные параметры. Функция использует только эти копии, не изменяя сами переменные, с которых копия была сделана.
Возможность доступа из функции не к копиям значений, а к самим переменным обеспечивают указатели. Указатель на переменную содержит ее адрес, и функция может использовать этот адрес для изменения значения переменной.
Фактические аргументы (выражения в вызове функции) вычисляются и преобразуются следующим образом:
1) Если имеется объявление со списком типов аргументов (прототип), то при вызове функции выполняются преобразования по умолчанию над типом каждого фактического аргумента, заданным в списке типов аргументов. Затем фактический аргумент приводится к полученному преобразованному типу. Независимо от аргумента, тип соответствующего формального параметра в списке параметров функции также подвергается преобразованиям по умолчанию. Затем полученный тип фактического аргумента сравнивается с типом соответствующего формального параметра. В случае несоответствия никакого преобразования не производится, но компилятор выдает такое же предупреждающее сообщение, как для выражения присваивания, когда типы левого и правого операнда не совпадают.
2) Если объявление со списком типов аргументов (прототип) отсутствует, то преобразования по умолчанию производятся отдельно для каждого аргумента, не имеющего соответствующего
имени типа. Если список типов аргументов завершен многоточием и задано больше фактических аргументов, чем имен типов в списке, то лишние фактические аргументы подвергаются только преобразованиям по умолчанию. Соответствующий формальный параметр в списке параметров функции также подвергается преобразованиям по умолчанию.
Формальные параметры — это переменные, которые принимают значения, переданные функции при вызове, в соответствии с порядком следования их имен в списке параметров.
Форма объявления формальных параметров аналогична использованию метода прототипов в объявлении функции. Список объявлений параметров содержит объявления формальных параметров через запятую. После списка сразу начинается тело функции (составной оператор). Список может быть и пустым, но и в этом случае он должен быть ограничен круглыми скобками. Если функция не имеет аргументов, рекомендуется указать это явно, записав в списке объявлений параметров ключевое слово void.
После последнего идентификатора в списке параметров может быть записана запятая с многоточием (,…). Это означает, что число параметров функции переменно, однако не меньше, чем следует идентификаторов до многоточия.
Для доступа к значениям параметров, имена которых не заданы в списке параметров функции, рекомендуется использовать макроопределения va_arg, va_end, va_start, описанные в разделе 12.
Допускается также список параметров, состоящий только из многоточия (…) и не содержащий идентификаторов. Это означает, что число параметров функции переменно и может быть равно нулю.
Примечание. Для совместимости с программами предыдущих версий компилятор допускает запись символа запятой без многоточия в конце списка параметров для обозначения их переменного числа. Запятая может быть использована вместо многоточия и в том случае, когда надо записать список параметров функции, принимающей нуль или более параметров. Использование запятой поддерживается только для совместимости. Для новых программ рекомендуется использовать многоточие.
Объявления параметров имеют тот же самый синтаксис, что и обычные объявления переменных (смотри раздел 3.4). Формальные параметры могут иметь базовый тип, либо быть структурой, объединением, указателем или массивом. Указание первой (или единственной) размерности для массива не обязательно. Массив воспринимается как указатель на тип элементов массива. Для формального параметра, таким образом, эквивалентны объявления
Идентификаторы — это имена переменных, функций и меток, используемых в программе. Идентификатор вводится в объявлении переменной или функции, либо в качестве метки оператора. После этого его можно использовать в последующих операторах программы. Идентификатор — это последовательность из одной или более латинских букв, цифр и символов подчеркивания, которая начинается с буквы или символа подчеркивания. Допускается любое число символов в идентификаторе, однако только первые 32 символа рассматриваются компилятором языка Си как значащие. Если первые 32 символа у двух идентификаторов совпадают, компилятор языка Си рассматривает их как один и тот же идентификатор. Компоновщик также распознает 32 символа в именах глобальных переменных.
В идентификаторах версии 1.5 СП ТС допускается знак $, однако, идентификатор не может с него начинаться.
Компиляторы языка Си в СП MSC и СП ТС имеют опцию, позволяющую изменять число значащих символов в идентификаторах.
При использовании символов подчеркивания в качестве первых символов идентификаторов необходимо соблюдать осторожность, поскольку такие идентификаторы могут совпасть (войти в конфликт) с именами "скрытых" библиотечных функций.
Примеры идентификаторов:
temp1
top_of_page
skip12
Компилятор языка Си рассматривает буквы верхнего и нижнего регистров как различные символы. Поэтому можно создавать идентификаторы, которые совпадают орфографически, но различаются регистром букв. Например, каждый из следующих идентификаторов является уникальным:
add
ADD
Add
aDD
В СП ТС, однако, существует опция компиляции, позволяющая рассматривать в именах внешних переменных буквы верхнего и нижнего регистров как совпадающие.
Компилятор языка Си не допускает использования идентификаторов, совпадающих по написанию с ключевыми словами.
Например, идентификатор while недопустим (однако идентификатор While—допустим).
Идентификаторы именуют переменные и функции. С каждым идентификатором ассоциируется тип, который задается при его объявлении. Значение объекта, именуемого идентификатором, зависит от типа следующим образом:
1) Идентификаторы переменных целого и плавающего типа представляют значения соответствующего типа.
2) Идентификатор переменной перечислимого типа представляет значение одной константы из соответствующего этому типу списка перечисления. Тип этого значения—int.
3) Идентификатор структуры или объединения представляет совокупность значений, специфицированных этой структурой или объединением.
4) Идентификатор указателя представляет адрес некоторого объекта специфицированного типа. Если указателю не присвоено никакого значения, то использование его в выражении может привести к трудно выявляемой ошибке. В языке Си определено, что никакой программный объект не может иметь адрес NULL (ноль), поэтому указатель со значением NULL не указывает ни на какой объект.
5) Идентификатор массива представляет массив, но если в выражении требуется скалярная величина, то представляет адрес первого элемента этого массива. Тип идентификатора — указатель на тип элементов массива. Из этого следует, что использование в выражениях массивов и указателей имеет много общего. Во многих случаях способ организации доступа к объекту — либо по указателю, либо как к элементу массива — не имеет принципиальной разницы и определяется исключительно выбранным стилем программирования. Это родство между массивами и указателями является существенной особенностью языка Си, выделяющей его среди других языков программирования высокого уровня.
Существуют, однако, некоторые различия между массивами и указателями. Во-первых, указатель занимает одну ячейку памяти, предназначенную для хранения машинного адреса (в частности, адреса какого-либо массива). Массив же занимает столько ячеек памяти, сколько элементов определено в нем при его объявлении. Только в выражении массив представляется своим адресом, который эквивалентен указателю. Во-вторых, адрес массива является постоянной величиной, поэтому, в отличие от идентификатора указателя, идентификатор массива не может составлять левую часть операции присваивания.
6) Идентификатор функции представляет функцию, т.е. адрес точки входа в функцию. Тип идентификатора — функция, возвращающая значение специфицированного для нее типа. Однако если в выражении требуется скалярная величина, то типом идентификатора считается указатель на функцию. Адрес функции не изменяется во время выполнения программы и, следовательно, не является переменной величиной. Поэтому идентификатор функции не может составлять левую часть операции присваивания.
Директива #define обычно используется для замены часто используемых в программе констант, ключевых слов, операторов и выражений осмысленными идентификаторами. Идентификаторы, которые заменяют числовые или текстовые константы либо произвольную последовательность символов, называются именованными константами. Идентификаторы, которые представляют некоторую последовательность действий, заданную операторами или выражениями языка Си, называются макроопределениями. Макроопределения могут иметь аргументы. Обращение к макроопределению в программе называется макровызовом.
В языке Си принято записывать идентификаторы именованных констант и макроопределений символами верхнего регистра, чтобы отличать их от имен переменных и функций. Это, однако, не является требованием языка Си.
Директива #undef отменяет текущее определение именованной константы. Только когда определение отменено, именованной константе может быть сопоставлено другое значение. Однако многократное повторение определения с одним и тем же значением не считается ошибкой.
Макроопределение напоминает по синтаксису определение функции. Однако замена вызова функции макровызовом может повысить скорость выполнения программы, поскольку для макроопределения не требуется генерировать вызывающую последовательность, которая занимает относительно большое время (засылка аргументов в стек, передача управления и т.п.). С другой стороны, многократное употребление макроопределения в программе может потребовать значительно большей памяти, чем вызовы функции (код для функции генерируется один раз, а для макроопределения — столько раз, сколько имеется макровызовов в программе).
Имеется возможность задавать определения именованных констант не только в исходном тексте, но и в командной строке компиляции.
Имеется ряд предопределенных идентификаторов, которые нельзя использовать в директивах #define и #undef в качестве идентификаторов. Они рассмотрены в разделе 7.9 "Псевдопеременные".
Синтаксис:
<выражение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] ссылается на шестой по порядку следования в памяти элемент массива.
Переменной в объявлении может быть присвоено начальное значение посредством инициализатора. Записи инициализатора в объявлении предшествует знак равенства
=<инициализатор>
Можно инициализировать переменные любого типа. Функции не инициализируются. Объявления, которые используют спецификацию класса памяти extern, не могут содержать инициализатор.
Переменная, объявленная на внешнем уровне без спецификации класса памяти, может быть инициализирована не более одного раза в каком-либо из исходных файлов, составляющих программу. Если же она явно не инициализирована ни в одном из исходных файлов, то компоновщик инициализирует ее нулевым значением.
Переменная класса памяти static, объявленная как на внешнем, так и на внутреннем уровне, может быть инициализирована константным выражением не более одного раза в исходном
файле. Если ее явная инициализация отсутствует, то компилятор языка Си инициализирует ее нулевым значением.
Инициализация переменных класса памяти auto
и register выполняется каждый раз при входе в блок (за исключением входа в блок по оператору goto), в котором они объявлены. Если инициализатор опущен в объявлении переменной класса памяти auto или register, то ее начальное значение не определено. Инициализация переменных составных типов (массив, структура, объединение), имеющих класс памяти auto, запрещена в СП MSC, но допускается в СП ТС даже для переменных, объявленных с модификатором const. Переменные составного типа, имеющие класс памяти static, могут быть инициализированы на внутреннем уровне.
Инициализирующими значениями для переменных внешнего уровня, а также переменных класса памяти static внутреннего уровня должно быть константное выражение (см. раздел 4.2.9). Переменные классов памяти auto и register могут быть инициализированы не только константными выражениями, но и выражениями, содержащими переменные и вызовы функций.
Модификаторы cdecl, pascal, interrupt воздействуют на идентификатор и должны быть записаны непосредственно перед ним.
Модификаторы const, volatile, near, far, huge
воздействуют либо на идентификатор, либо на звездочку, расположенную непосредственно справа от модификатора. Если справа расположен идентификатор, то модифицируется тип объекта, именуемого этим идентификатором. Если же справа расположена звездочка, то модифицируется тип объекта, на который указывает эта звездочка, т.е. эта звездочка представляет собой указатель на модифицированный тип. Таким образом, конструкция <модификатор>* читается как "указатель на модифицированный тип". Например,
int const *р; - это указатель на const int, а
int * const р; - это const
указатель на int. Модификаторы const
и volatile могут также записываться и перед спецификацией типа.
В СП ТС
использование модификаторов near, far, huge ограничено: они могут быть записаны только перед идентификатором функции или перед признаком указателя (звездочкой).
Допускается более одного модификатора для одного объекта (или элемента описателя). В следующем примере тип функции func
модифицируется одновременно специальными ключевыми словами far
и pascal. Порядок специальных ключевых слов не важен, т. е. комбинации far pascal
и pascal far
имеют один и тот же смысл.
int far * pascal far func();
Тип значения, возвращаемого функцией func, представляет собой указатель на значения типа int. Тип этих значений модифицирован специальным ключевым словом far.
Как обычно, в объявлении могут быть использованы круглые скобки для изменения порядка его интерпретации.
Пример:
char far *(far *getint)(int far *);
7 6 2 1 3 5 4
В примере показано объявление с различными вариантами расположения модификатора far. Учитывая правило, согласно которому модификатор воздействует на элемент описателя, расположенный справа от него, можно проинтерпретировать это объявление следующим образом (шаги интерпретации пронумерованы):
1. Идентификатор getint
объявляется как
2. Указатель на far
3. Функцию, требующую
4. Один аргумент, который является указателем на far
5. Значение типа int
6. И возвращающую указатель на far
7. Значение типа char
Составной описатель — это идентификатор, дополненный более чем одним признаком типа массив, указатель или функция.
С одним идентификатором можно образовать множество различных комбинаций признаков типа массив, указатель или функция. Некоторые комбинации недопустимы. Например, массив не может содержать в качестве элементов функции, а функция не может возвращать массив или функцию.
При интерпретации составных описателей сначала рассматриваются квадратные скобки и круглые скобки, расположенные справа от идентификатора. Квадратные и круглые скобки имеют одинаковый приоритет. Они интерпретируются слева направо. После них справа налево рассматриваются звездочки, расположенные слева от идентификатора. Спецификация типа рассматривается на последнем шаге после того, как описатель уже полностью проинтерпретирован.
Для изменения действующего по умолчанию порядка интерпретации описателя можно использовать внутри него круглые скобки.
Правило интерпретации составных описателей может быть названо чтением "изнутри — наружу". Начать интерпретацию нужно с идентификатора и проверить, есть ли справа от нега открывающие квадратные или круглые скобки. Если они
есть, то проинтерпретировать правую часть описателя. Затем следует проверить, есть ли слева от идентификатора звездочки, и, если они есть, проинтерпретировать левую часть. Если на какой-либо стадии интерпретации справа встретится закрывающая круглая скобка (которая используется для изменения порядка интерпретации описателя), то необходимо сначала полностью провести интерпретацию внутри данной пары круглых скобок, а затем продолжить интерпретацию справа от закрывающей круглой скобки.
На последнем шаге интерпретируется спецификация типа. После этого тип объявленного объекта полностью известен.
Следующий пример иллюстрирует применение правила интерпретации составных описателей. Последовательность шагов интерпретации пронумерована.
char*(*(*var)())[10].
7 6 4 2 1 3 5
1. Идентификатор var объявлен как
2. Указатель на
Исходная программа представляет собой совокупность следующих элементов: директив препроцессора, указаний компилятору, объявлений и определений. Директивы препроцессора специфицируют действия препроцессора по преобразованию текста программы перед компиляцией. Указания компилятору — это специальные инструкции, которым компилятор языка Си следует во время компиляции.
Объявление переменной задает имя и атрибуты переменной. Определение переменной, помимо задания ее имени и атрибутов, приводит к выделению для нее памяти. Кроме того, определение задает начальное значение переменной (явно или неявно).
Объявление функции задает ее имя, тип возвращаемого значения и может задавать атрибуты ее формальных параметров.
Определение функции специфицирует тело функции, которое представляет собой составной оператор (блок), содержащий объявления и операторы. Определение функции также задает имя функции, тип возвращаемого значения и атрибуты ее формальных параметров.
Объявление типа позволяет программисту создать собственный тип данных. Оно состоит в присвоении имени некоторому базовому или составному типу языка Си. Для типа понятия объявления и определения совпадают.
Исходная программа может содержать произвольное число директив, указаний компилятору, объявлений и определений. Их синтаксис описан в последующих разделах. Порядок появления этих элементов в программе весьма существен; в частности, он влияет на возможность использования переменных, функций и типов в различных частях программы (см. раздел 2.4 "Время жизни и область действия").
Для того чтобы программа на языке Си могла быть скомпилирована и выполнена, она должна содержать по крайней мере одно определение — определение функции. Эта функция определяет действия, выполняемые программой. Если же программа содержит несколько функций, то среди них выделяется одна главная функция, которая должна иметь имя main. С нее начинается выполнение программы; она определяет действия, выполняемые программой, и вызывает другие функции. Порядок следования определений функций в исходной программе несуществен.
Если программа содержит только одну функцию, то она и является главной (и должна иметь имя main). В следующем примере приведена простая программа на языке Си:
int х = 1; /* определения переменных. */
inl у = 2;
extern int printf(char *, ..,); /* объявление функции */
main () /* определение главной функции */
{
int z; /* объявления переменных */
int w;
z = у + х; /* выполняемые операторы */
w = y – x;
printf("z = %d\nw = %d\n", z, w);
}
Эта исходная программа определяет функцию с именем main
и объявляет функцию printf. Переменные х и у определяются на внешнем уровне, а переменные z и w объявляются внутри функции.
Текст программы на языке Си может быть разделен на несколько исходных файлов. Исходный файл представляет собой текстовый файл, который содержит либо всю программу, либо ее часть. При компиляции исходной программы каждый из составляющих ее исходных файлов должен быть скомпилирован отдельно, а затем связан с другими файлами компоновщиком. Отдельные исходные файлы можно объединять в один исходный файл, компилируемый как единое целое, посредством директивы препроцессора #include.
Исходный файл может содержать любую целостную комбинацию директив, указаний компилятору, объявлений и определений. Под целостностью подразумевается, что такие объекты, как определения функций, структуры данных либо набор связанных между собой директив условной компиляции, должны целиком располагаться в одном файле, т. е. не могут начинаться в одном файле, а продолжаться в другом.
Исходный файл не обязательно должен содержать выполняемые операторы. Иногда удобно размещать определения переменных в одном файле, а в других файлах использовать эти переменные путем их объявления. В этом случае определения переменных становятся легко доступными для поиска и модификации. Из тех же соображений именованные константы и макроопределения обычно собирают в отдельные файлы и включают их посредством директивы препроцессора #include
в те исходные файлы, в которых они требуются.
Указания компилятору обычно действуют только для отдельных участков исходного файла. Специфические действия компилятора, задаваемые указаниями, определяются конкретной реализацией компилятора языка Си.
В нижеследующем примере исходная программа состоит из двух исходных файлов. Функции main и max
представлены в отдельных файлах. Функция main
использует функцию max в процессе своего выполнения.
/* исходный файл 1 — функция main */
#define ONE 1
#define TWO 2
#define THREE 3
extern int max (int, int); /* объявление функции */
main () /* определение функции */
{
int w = ONE, x = TWO, у = THREE;
int z = 0;
z = max(x, y);
z = max(z, w);
Явное преобразование типа может быть выполнено посредством операции приведения типа. Она имеет следующую синтаксическую форму
(<абстрактное-имя-типа>) <операнд>
<абстрактное-имя-типа> — специфицирует некоторый тип; <операнд> — выражение, значение которого должно быть преобразовано к специфицированному типу (абстрактные имена типов рассмотрены в разделе 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 на остаток блока, а в СП ТС — на остаток тела функции.
Под элементами языка понимаются его базовые конструкции, используемые при написании программ. В этом разделе описываются следующие элементы языка Си:
– алфавит;
– константы;
– идентификаторы;
– ключевые слова;
– комментарии.
Компилятор языка Си воспринимает исходный файл, содержащий программу на языке Си, как последовательность текстовых строк. Каждая строка завершена символом новой строки. Этот символ вставляется текстовым редактором при нажатии клавиши ENTER (ВВОД).
Компилятор языка Си последовательно считывает строки программы и разбивает каждую из считанных строк на группы символов, называемые лексемами. Лексема—это единица текста программы, которая имеет самостоятельный смысл для компилятора языка Си и которая не содержит в себе других лексем. Никакие лексемы, кроме символьных строк, не могут продолжаться на последующих строках текста программы. Знаки операций, константы, идентификаторы и ключевые слова, описанные в этом разделе, являются примерами лексем. Разделители, например квадратные скобки [], фигурные скобки {}, круглые скобки (), угловые скобки < >
и запятые, также являются лексемами. Внутри идентификаторов, ключевых слов, а также знаков операций, состоящих из нескольких символов, пробельные символы недопустимы.
Когда компилятор языка Си выделяет отдельную лексему, он пытается включить в нее последовательно столько символов, сколько возможно, прежде чем перейти к выделению следующей лексемы. Рассмотрим, например, следующее выражение:
i+++j
В этом примере компилятор языка Си вначале сформирует из первых двух знаков "плюс" операцию инкремента (++), а из оставшегося знака плюс — операцию сложения. Выражение проинтерпретируется как (i++)+(j), а не как (i)+(++j). В подобных случаях рекомендуется для ясности разделять лексемы пробельными символами или круглыми скобками.
В определении функции допускается указание спецификации класса памяти static или extern. Классы памяти функций рассматривались в разделе 3.6.
Спецификация класса памяти переменной определяет, какое время жизни она имеет (глобальное или локальное), и влияет на область действия переменной. Объект с глобальным временем жизни существует и имеет значение на протяжении всего времени выполнения программы. Все функции имеют глобальное время жизни.
Переменной с локальным временем жизни выделяется новая ячейка памяти каждый раз, когда управление передается блоку, в котором она определена. Когда управление возвращается из блока, переменная теряет свое значение.
В языке Си имеется четыре спецификации класса памяти:
auto
register
static
extern
Область действия функций, объявленных со спецификацией класса памяти extern, распространяется на все исходные файлы, которые составляют программу; следовательно, такие функции могут быть вызваны из любой функции в любом исходном файле программы.
Переменные классов памяти auto и register имеют локальное время жизни. Спецификации static и extern определяют объекты с глобальным временем жизни.
В совокупности с местоположением объявления объекта спецификация класса памяти определяет область действия переменной или функции. Термин "область действия" определяет часть программы, в которой к функции или переменной возможен доступ. Например, переменная с глобальным временем жизни существует в течение всего времени выполнения исходной программы, но она может быть доступна не во всех частях программы. Область действия и связанное с ней понятие времени жизни рассмотрены в разделе 2.4.
Объявления, расположенные вне тел всех функций, относятся к внешнему уровню, а объявления внутри тел функций относятся к внутреннему уровню. Особый случай представляют объявления формальных параметров функции. В последующих разделах описывается смысл спецификаций класса памяти для каждого варианта объявления, а также поясняются правила умолчания в случае отсутствия в объявлении спецификации класса памяти.
Функции могут быть объявлены со спецификацией класса памяти static или extern либо вообще без спецификации класса памяти. Функции всегда имеют глобальное время жизни.
Объявления функций, в которых опущена спецификация класса памяти, аналогичны объявлениям со спецификацией класса памяти extern.
Если в объявлении функции специфицирован класс памяти static, то и в ее определении должен быть также указан класс памяти static
(это требование не является обязательным для СП ТС).
Объявление функции на внутреннем уровне по смыслу эквивалентно объявлению внешнего уровня, т. е. область действия функции распространяется не до конца блока, а до конца файла.
Область действия функций для различных спецификаций класса памяти рассмотрена подробно в разделе 2.4 "Время жизни и область действия".
Ключевые слова — это предопределенные идентификаторы, которые имеют специальное значение для компилятора языка Си. Их использование строго регламентировано. Имена объектов программы не могут совпадать с ключевыми словами.
список ключевых слов:
| auto | continue | else | for | long | signed | switch | void | ||||||||
| break | default | enum | goto | register | sizeof | typedef | while | ||||||||
| case | do | extern | if | return | static | union | |||||||||
| char | double | float | int | short | struct | unsigned |
При необходимости можно с помощью директив препроцессора определить для ключевых слов другие имена. Например, при наличии в программе макроопределения
#define BOOL int
слово BOOL можно использовать в объявлениях вместо слова int. Смысл объявлений (спецификация целого типа данных) от этого не изменится, однако программа станет более читабельной, если речь идет не просто о целых переменных, а о переменных, предназначенных для хранения значений булевского типа (булевский тип не реализован в языке Си как самостоятельный тип данных).
Имеется также ряд специальных ключевых слов:
| СП MSC: | cdecl | СП ТС: | asm | _cs | _BX | ||||||
| far | cdecl | _ds | _ch | ||||||||
| fortran | far | _es | _cl | ||||||||
| huge | huge | _ss | _Cx | ||||||||
| near | interrupt | _AH | _DH | ||||||||
| pascal | near | _AL | _DI | ||||||||
| const | pascal | _AX | _DL | ||||||||
| volatile | const | _BH | _DX | ||||||||
| interrupt | volatile | _BL | _SI | ||||||||
| _BP | _SP |
В версии 4.0 СП MSC ключевые слова const и volatile зарезервированы, но использовать их невозможно. В версии 5.0 СП MSC ключевое слово volatile реализовано лишь синтаксически, а const — полностью (как синтаксически, так и семантически). В СП ТС и const, и volatile полностью реализованы. В версии 4.0 СП MSC ключевое слово interrupt не реализовано.
Ключевое слово fortran используется для организации связи программ, написанных на языках Си и Фортран. По действию оно аналогично ключевому слову pascal. Ключевое слово asm применяется для записи в программе на языке Си ассемблерных инструкций. Специальные ключевые слова, начинающиеся с подчеркивания, представляют собой имена псевдопеременных, соответствующих регистрам микропроцессора. Ключевые слова cdecl, pascal, interrupt, near, far, huge, const, volatile объясняются подробно в разделе 3.3.3 "Описатели с модификаторами".
Константное выражение — это выражение, результатом вычисления которого является константа. Операндами константного выражения могут быть целые, символьные, плавающие константы, константы перечислимого типа, выражения приведения типа константного выражения, выражения с операцией sizeof
и другие константные выражения. Имеются некоторые ограничения на использование операций в константных выражениях. В константных выражениях нельзя использовать операции присваивания, операцию последовательного вычисления. Кроме того, использование операции адресации, выражений приведения типа и плавающих констант ограничено.
Константные выражения, используемые в директивах препроцессора, имеют дополнительные ограничения, поэтому они называются ограниченными константными выражениями. Ограниченные константные выражения не могут содержать операцию sizeof (в СП ТС — могут), констант перечисления и выражений приведения типа и плавающих констант. Однако ограниченные константные выражения, используемые в директивах препроцессора, могут содержать специальные константные выражения defined (<идентификатор>), описанные в разделе 7.2.1 "Директива #define". . Только выражения инициализации допускают применение плавающих констант, выражений приведения типа к неарифметическим типам и операции адресации. Операция адресации может быть применена к переменной внешнего уровня базового или структурного типа, к объединению, а также к элементу массива. В этих выражениях допускается сложение или вычитание адресного выражения с константным выражением, не содержащим операции адресации.
Константа — это число, символ или строка символов. Константы используются в программе для задания постоянных величин. В языке Си различают четыре типа констант: целые, с плавающей точкой, символьные константы и символьные строки.
Операнду-константе соответствует значение и тип представляющей его константы. Типы констант подробно описаны в разделе 1.2. Символьная константа имеет тип int. Целая константа имеет один из следующих типов: int, long, unsigned int или unsigned long, в зависимости от размера целого на данном компьютере и от того, как специфицировано ее значение. Константы с плавающей точкой имеют тип double (в версии 2.0 СП ТС допустимы также константы типа float). Символьные строки имеют тип массив символов; они обсуждаются в разделе 4.2.3.
Константа с плавающей точкой — это действительное десятичное положительное число. Оно включает целую часть, дробную часть и экспоненту. Константы с плавающей точкой имеют следующий формат представления:
[<цифры>][.<цифры>][<э>[-]<цифры>]
<цифры> —одна или более десятичных цифр (от 0 до 9); <э>—признак экспоненты, задаваемый символом Е или е. Либо целая, либо дробная часть константы может быть опущена, но не обе сразу. Либо десятичная точка с дробной частью, либо экспонента может быть опущена, но не обе сразу.
Экспонента состоит из символа экспоненты, за которым следует целочисленное значение экспоненты, возможно со знаком
плюс или минус.
Между цифрами или символами константы пробельные символы недопустимы.
Примеры констант с плавающей точкой:
15.75
1.575Е1
1575е-2
25.
Примеры констант с плавающей точкой с опущенной целой частью:
.75
.0075е2
Константы с плавающей точкой всегда специфицируют положительные значения. Если требуются отрицательные значения, то необходимо сформировать константное выражение и? знака минус и следующей за ним константы. Знак минус рассматривается при этом как арифметическая операция.
Примеры:
-0.0025
-2.5е-3
-.125
-.175Е-2
Все константы с плавающей точкой имеют тип double. В СП ТС можно явно присвоить константе тип float, добавив к ней суффикс f или F.
Логические операции выполняют над своими операндами логические функции И (&&) и ИЛИ (||). Операнды логических операций могут иметь целый, плавающий тип, либо быть указателями. Типы первого и второго операндов могут различаться. Сначала всегда вычисляется первый операнд; если его значения достаточно для определения результата операции, то второй операнд не вычисляется.
Логические операции не выполняют преобразования по умолчанию. Вместо этого они вычисляют операнды и сравнивают их с нулем. Результатом логической операции является либо 0 (ЛОЖЬ), либо 1 (ИСТИНА). Тип результата — int.
Логическое И (&&)
Логическая операция И вырабатывает значение 1, если оба операнда имеют ненулевое значение. Если один из операндов равен нулю, то результат также равен нулю. Если значение первого операнда равно нулю, то значение второго операнда не вычисляется.
Логическое ИЛИ (||)
Логическая операция ИЛИ выполняет над своими операндами операцию включающее ИЛИ. Она вырабатывает значение 0, если оба операнда имеют значение 0; если какой-либо из операндов имеет ненулевое значение, то результат операции равен 1. Если первый операнд не равен нулю, то значение второго операнда не вычисляется.
Примеры:
int х, у;
if(х<у && у<z) printf("x меньше z\n"); /* пример 1 */
if(х==у !!
х==z) printf("x равен у или z\n"); /* пример 2 */
В первом примере функция printf вызывается для печати сообщения в том случае, если х меньше у и у меньше 2. Если х больше у, то второй операнд (у<z) не вычисляется и печати не происходит.
Во втором примере сообщение печатается в том случае, если х равен у или z. Если х равен у, то значение второго операнда (х==z) не вычисляется.
Реализация моделей памяти в СП MSC и в СП ТС имеет ряд отличий. В разделах 8.1 и 8.2 описаны модели
памяти СП MSC, а в разделе 8.3 приведены отличия моделей памяти СП ТС.
Организация работы с моделями памяти в СП ТС имеет ряд отличий от СП MSC.
В дополнение к описанным выше моделям СП ТС имеет еще одну — tiny (минимальную). В этой модели вся программа — код, данные, стек, динамическая память — размещается в одном сегменте. Таким образом, размер программы ограничен 64 Кбайтами. Все указатели в этой модели имеют тип near. Программы модели памяти tiny могут быть преобразованы в формат выполняемых файлов СОМ операционной системы MSDOS.
В компактной и большой моделях статические данные занимают не несколько сегментов, а один. В этом сегменте также размещается стек. По умолчанию для данных используются указатели типа far, однако для статических данных лучше применять указатели типа near. В компактной, большой и максимальной моделях стек занимает отдельный сегмент.
Только в максимальной модели памяти СП ТС позволяет статическим данным занимать более одного сегмента, т. е. более 64 Кбайтов. Однако в одном исходном файле может быть не более 64 Кбайтов статических данных. Вследствие этого недопустимы массивы, превышающие по размеру 64 Кбайта.
Указатели типа huge в СП ТС всегда хранятся в нормализованном виде, так что значение смещения никогда не превышает 15. Нормализация требует дополнительных временных затрат при работе с указателями тип huge, но зато позволяет применять к ним операции отношения (с ненормализованными 32-битовыми указателями операции отношения работают
некорректно).
Допустимо применение модификатора huge к функциям и указателям на функции.
Имеется также четыре специальных указателя типа near с именами _cs, _ds, _ss, _es. Это 16-битовые указатели, ассоциированные с сегментными регистрами микропроцессора, содержащими адреса сегментов кода, данных, стека и динамической памяти, соответственно. Например, указатель р,
объявленный следующим образом
char _ss *р;
будет содержать 16-битовое значение, хранящееся в сегменте стека.
Объявления с модификаторами near, far, huge отличаются тем, что нельзя модифицировать тип адреса самого указателя. Модификатор в объявлении указателя может стоять только перед звездочкой, тем самым объявляя указатель на модифицируемый тип. А объявление
int * far р;
допустимое в СП MSC, считается ошибкой в СП ТС.
Работая в некоторой стандартной модели памяти, программист может в той или иной мере модифицировать ее, применяя в объявлениях модификаторы near, far и huge. Правила интерпретации объявлений с модификаторами рассмотрены в разделе 3.3.3.4 "Модификаторы near, far, huge".
Модификатор interrupt предназначен для объявления функций, работающих с векторами прерываний процессора 8086/8088. Для функции типа interrupt
при компиляции генерируется дополнительный код в точке входа и выхода из функции, для сохранения и восстановления регистров микропроцессора АХ, ВХ, СХ, DX,
SI, DI, ES и DS. Остальные регистры — ВР, SP, SS, CS и IP сохраняются всегда как часть вызывающей последовательности языка Си или часть самой системы обработки прерывания.
См. пример в разделе 3.3.3.1.
Функции прерываний следует объявлять с типом возвращаемого значения void.
Функции прерываний поддерживаются для всех моделей памяти. В СП MSC, в малой и средней модели в регистр DS заносится при входе в функцию адрес сегмента данных всей программы, а в компактной, большой и максимальной модели в регистр DS заносится адрес сегмента данных программного модуля. В СП ТС только в максимальной модели в регистр DS заносится адрес сегмента данных программного модуля, а в остальных моделях—адрес сегмента данных всей программы.
Модификатор interrupt не может использоваться совместно с модификаторами near, far, huge.
Рассматриваемые системы программирования в языке Си позволяют обращаться из программы на языке Си к программам, написанным на других языках, и обратно. При смешивании языков программирования приходится иметь дело с двумя важными проблемами: написанием внешних имен и передачей параметров.
Результатом работы компилятора языка Си является файл, содержащий объектный код программы. Файлы с объектным кодом, полученные при компиляции всех исходных файлов, составляющих программу, компоновщик объединяет в один выполнимый файл. При этом производится так называемое разрешение ссылок на глобальные объекты из разных исходных файлов программы.
При компиляции все глобальные идентификаторы программы, т. е. имена функций и глобальных переменных, сохраняются в объектном коде и используются компоновщиком в процессе работы. По умолчанию эти идентификаторы сохраняются в своем первоначальном виде (т. е. набранные прописными, строчными буквами либо и теми, и другими). Кроме того, в качестве первого символа каждого идентификатора компилятор языка Си добавляет символ подчеркивания.
Компоновщик по умолчанию различает прописные и строчные буквы, поэтому идентификаторы, используемые в различных исходных файлах программы для именования одного и того
же объекта, должны полностью совпадать с точки зрения, как орфографии, так и регистров клавиатуры. Для обеспечения совпадения идентификаторов, используемых в разноязычных исходных файлах, применяются модификаторы pascal и cdecl.
Модификатор pascal
Применение модификатора pascal к идентификатору приводит к тому, что идентификатор преобразуется к верхнему регистру и к нему не добавляется символ подчеркивания. Этот идентификатор может использоваться для именования в программе на языке Си глобального объекта, который используется также в программе на языке Паскаль. В объектном коде, сгенерированном компилятором языка Си, и в объектном коде, сгенерированном компилятором языка Паскаль, идентификатор будет представлен идентично.
Если модификатор pascal применяется к идентификатору функции, то он оказывает влияние также и на передачу аргументов. Засылка аргументов в стек производится в этом случае не в обратном порядке, как принято в компиляторах языка Си в СП MSC и СП ТС, а в прямом—первым засылается в стек первый аргумент.
Модификатор const не допускает явного присваивания значения переменной либо других косвенных действий по изменению ее значения, таких как выполнение операций инкремента и декремента. Значение указателя, объявленного с модификатором const, не может быть изменено, в отличие от значения объекта, на который он указывает. В СП MSC, в отличие от СП ТС, недопустима также инициализация const
объектов, имеющих класс памяти auto (поскольку их инициализация должна выполняться каждый раз при входе в блок, содержащий их объявления).
Применение модификатора const помогает выявить нежелательные присваивания значений переменным. Переменные, объявленные с модификатором const, могут быть загружены в ячейки постоянной памяти (ПЗУ).
Модификатор volatile противоположен по смыслу модификатору const. Он указывает на то, что значение переменной может быть изменено; но не только непосредственно программой, а также и внешним воздействием, например программой обработки прерываний, либо, если переменная соответствует порту ввода/вывода, обменом с внешним устройством. Объявление объекта с модификатором volatile
предупреждает компилятор языка Си, что не следует делать предположений относительно стабильности значения объекта в момент вычисления содержащего его выражения, т. к. значение может (теоретически) измениться в любой момент. Для выражений, содержащих объекты типа volatile, компилятор языка Си не будет применять методы оптимизации, а сами объекты не будет загружать в машинные регистры.
Возможно одновременное использование в объявлении модификаторов const и volatile. Это означает, что значение объявляемой переменной не может модифицироваться программой, но подвержено внешним воздействиям.
Если с модификатором const или volatile объявляется переменная составного типа, то действие модификатора распространяется
на все ее составляющие элементы. Возможно применение модификаторов const и volatile в составе объявления typedef.
Примечание. При отсутствии в объявлении спецификации типа и наличии модификатора const или volatile
Эти модификаторы оказывают воздействие на работу с адресами объектов.
Компилятор языка Си позволяет использовать при компиляции одну из нескольких моделей памяти. Виды моделей памяти и методы их применения рассмотрены в разделе 8 "Модели памяти".
Модель, которую вы используете, определяет размещение в оперативной памяти вашей программы и данных, а также внутренний формат указателей. Однако при использовании какой-либо модели памяти можно объявить указатель с форматом, отличным от действующего по умолчанию. Это делается с помощью модификаторов near, far и huge.
Указатель типа near — 16-битовый; для определения адреса объекта он использует смещение относительно текущего содержимого сегментного регистра. Для указателя типа near
доступная память ограничена размером текущего 64-килобайтного сегмента данных.
Указатель типа far — 32-битовый; он содержит как адрес сегмента, так и смещение. При использовании указателей типа far допустимы обращения к памяти в пределах 1-мегабайтного адресного пространства процессора Intel 8086/8088, однако значение указателя типа far циклически изменяется в пределах одного 64-килобайтного
сегмента.
Указатель типа huge — 32-битовый; он также содержит адрес сегмента и смещение. Значение указателя типа huge
может быть изменено в пределах всего 1-мегабайтного адресного пространства. В СП ТС указатель типа huge всегда хранится в нормализованном формате. Это имеет следующие следствия:
—операции отношения ==, !=,
<, >, <=, >= выполняются корректно и предсказуемо над указателями типа huge, но не над указателями типа far;
—при использовании указателей типа huge
требуется дополнительное время, т. к. программы нормализации должны вызываться при выполнении любой арифметической операции над этими указателями. Объем кода программы также возрастает.
В СП MSC модификатор huge применяется только к массивам, размер которых превышает 64 К. В СП ТС недопустимы массивы больше 64 К, а модификатор huge применяется к функциям и указателям для спецификации того, что адрес функции или указуемого объекта имеет тип huge.
Компилятор языка Си поддерживает ряд модификаторов типа функций: pascal, cdecl, interrupt, near, far и huge (модификатор interrupt
не реализован в версии 4 СП MSC). Модификаторы рассмотрены в разделе 3.3.3 "Описатели с модификаторами".
К мультипликативным операциям относятся операции умножения *, деления / и получения остатка от деления %. Операндами операции % должны быть целые значения. Операции умножения * и деления / выполняются над целыми и плавающими операндами. Типы первого и второго операндов могут отличаться, при этом выполняются преобразования операндов по умолчанию. Типом результата является тип операндов после преобразования.
В процессе выполнения мультипликативных операций ситуация переполнения или потери значимости не контролируется. Если результат мультипликативной операции не может быть представлен типом операндов после преобразования, то информация теряется.
Умножение (*)
Операция умножения выполняет умножение одного из своих операндов на другой.
Деление (/)
Операция деления выполняет деление первого своего операнда на второй. Если оба операнда являются целыми значениями
не делятся нацело, то результат округляется в сторону нуля. Деление на нуль дает ошибку во время выполнения.
Остаток от деления (%)
Результатом операции является остаток от деления первого операнда на второй. Знак результата совпадает со знаком делимого.
Примеры:
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.
Если непосредственно за ключевым словом near, far или huge следует идентификатор, то это значит, что соответствующий элемент данных будет размещен в стандартном сегменте (для near) или может быть размещен в другом сегменте данных (для far или huge).
Например, объявление
char far х;
сообщает, что адрес объекта х имеет тип far.
Если же непосредственно за ключевым словом near, far или huge следует признак указателя (звездочка), то это значит, что соответствующий указатель будет хранить адрес типа near, типа far или типа huge, соответственно. Например, объявление
char far *р;
сообщает, что указатель р имеет тип far, т. е. может указывать на объект, расположенный в любом сегменте данных (при этом тип адреса этого объекта должен быть far). Объявление
char * far р;
объявляет р как указатель на char, причем сам указатель р может находиться в любом сегменте, и его адрес имеет тип far. Объявление
char far * far р;
сообщает, что указатель р может указывать на объекты с адресом типа far. Адрес самого указателя р также имеет тип far.
Примеры:
char а[3000]; /* пример 1: малая модель */
char far b[30000]; /* пример 2: малая модель */
char a[3000]; /*
пример 3: большая модель */
char near b[3000]; /*
пример 4: большая модель */
char huge a[70000]; /*
пример 5: малая модель */
char huge *pa; /* пример 6: малая модель */
char *pa; /*
пример 7: малая модель */
char far *pb; /*
пример 8: малая модель */
char far **pa; /* пример 9: малая модель */
char far **pa; /*
пример 10: большая модель */
char far *near *pb; /*
пример 11: любая модель */
char far *far *pb; /* пример 12: любая модель */
В примере 1 массиву а выделяется память в стандартном сегменте данных; массиву b во втором примере память может быть выделена в любом из сегментов данных программы. Поскольку оба объявления сделаны в малой модели, то, вероятно, массив а содержит часто используемые данные, которые для ускорения доступа должны располагаться в стандартном сегменте, а массив b содержит редко используемые данные, которые могут выйти за пределы 64-Кбайтного
Объявление функции определяет ее имя, тип возвращаемого значения, класс памяти и может также задавать тип некоторых или всех аргументов функции. Детальное описание синтаксиса объявлений функции дано в разделе 3.5. В разделе 3.6 рассмотрена зависимость области действия функции от ее класса памяти.
Однако, помимо явного объявления, функция может быть объявлена неявно, по контексту ее вызова. Неявное объявление
имеет место всякий раз, когда функция вызывается без предварительного объявления или определения. В этом случае компилятор языка Си считает, что вызываемая функция имеет тип возвращаемого значения int и класс памяти extern. Определение функции, если оно имеется далее в том же самом исходном файле, может переопределить тип возвращаемого значения и класс памяти.
Тип возвращаемого значения функции, указанный в предварительном объявлении, должен соответствовать типу возвращаемого значения в определении функции.
Если функция, тип возвращаемого значения которой не int, вызывается до ее определения или объявления, то компилятор сообщает об ошибке.
Основное назначение предварительного объявления состоит в задании типов и числа аргументов, ожидаемых в вызове функции. Список типов аргументов позволяет компилятору осуществить контроль типов аргументов при вызове функции. Если предварительное объявление отсутствует, то программист сам должен следить за соответствием типов между фактическими аргументами и формальными параметрами. Более детально контроль типов рассмотрен в разделе 6.4.1 "Фактические аргументы".
Пример:
main(void)
{
int а = 0, b = 1;
float х = 2.0, у = 3.0;
double realadd
(double, double);
a = intadd(a, b);
x = realadd(x, y);
}
intadd(int a, int b)
{
return (a + b);
}
double realadd(double x, double y)
{
return (x + y);
}
В примере функция intadd объявлена неявно с типом возвращаемого значения int, так как она вызнана до своего определения. Компилятор не проверит типы аргументов при вызове функции intadd, поскольку список типов аргументов для нее не задан.
Функция realadd возвращает значение типа double. В функции main имеется предварительное объявление функции realadd. Тип возвращаемого значения (double), заданный в определении, соответствует типу возвращаемого значения, заданному в предварительном объявлении. В предварительном объявлении также определены типы двух параметров функции realadd. Типы фактических аргументов соответствуют типам, заданным в предварительном объявлении, и также соответствуют типам формальных параметров в определении функции realadd.
Метод объявления функции, описанный в данном разделе, используется только в версии 4.0 СП MSC. В версии 5.0 СП MSC, а также в СП ТС реализован более современный метод — объявление прототипа функции, а старый метод поддерживается в этих версиях лишь для совместимости программ. В конце данного раздела приведены основные отличия метода объявления прототипа.
Синтаксис:
[<спецификация класса памяти>][<спецификация типа>] <описатель>([<список типов аргументов>]);
Объявление функции специфицирует имя функции, тип возвращаемого значения и, возможно, типы ее аргументов и их число. Эти атрибуты функции необходимы для проверки компилятором языка Си корректности обращения к ней до того, как она определена. Определение функций рассмотрено в разделе 6.2.
Если <описатель> функции представляет собой идентификатор (имя функции), то объявляется функция, тип возвращаемого значения которой задан спецификацией типа. Если же <описатель> представляет собой более сложную конструкцию (см. раздел 3.3.1), то оставшаяся часть описателя в совокупности со <спецификацией типа> задает тип возвращаемого значения. Функция не может возвращать массив или функцию, но может возвращать указатель на эти объекты.
Если спецификация типа в объявлении функции опущена, то предполагается тип int. На внешнем уровне может быть также опущена спецификация класса памяти, а на внутреннем уровне хотя бы одна из спецификаций — класса памяти или типа—должна присутствовать.
В объявлении функции можно задать спецификацию класса памяти extern или static. Классы памяти рассматриваются в разделе 3.6.
Список типов аргументов
Список типов аргументов определяет типы аргументов функции и их число.
Список типов — это список из одного или более имен типов. Каждое имя типа отделяется от другого запятой. Список ограничивается круглыми скобками.
Первое имя типа задает тип первого аргумента, второе имя задает тип второго аргумента и т.д. Концом списка является закрывающая круглая скобка, однако перед ней может быть записана запятая и многоточие (,…).
Правила применения модификаторов near и far в объявлениях функций аналогичны правилам применения их в объявлениях данных. Если непосредственно за модификатором следует имя функции, то данное ключевое слово определяет, в каком сегменте будет размещена функция. Например,
char far fun();
определяет fun как функцию, вызываемую по 32-битовому адресу и возвращающую тип char.
Если же непосредственно за специальным ключевым словом следует признак указателя (звездочка), то данное ключевое слово определяет тип адреса функций, которые могут вызываться через этот указатель. Например,
char (far *pfun)();
определяет pfun как указатель (32-битовый) на far
функцию, возвращающую char.
Модификатор huge к функциям и указателям на функции неприменим.
Объявления функций должны соответствовать их определениям по набору и расположению модификаторов. Рекомендуется всегда использовать предварительные объявления функций со списками типов аргументов, чтобы компилятор мог выявить ситуации некорректного вызова функции.
Примеры:
char far fun(); /*
пример 1: малая модель */
static char far *near fun(); /* пример 2: большая модель */
void far fun(); /*
пример 3: малая модель */
void (far *pfun)() = fun;
double far * far fun(); /*
пример 4: компактная модель */
double far* (far *pfun)() = fun;
В первом примере fun объявляется как функция, возвращающая char. Ключевое слово far
в объявлении означает, что fun вызывается по 32-битовому адресу типа far.
Во втором примере fun объявляется как near
функция класса памяти static, возвращающая указатель на far char. Такая функция в большой модели памяти может быть использована, например, как вспомогательная подпрограмма, которая вызывается часто, но только функциями из своего исходного файла. Поскольку все функции из одного исходного файла помещаются в один и тот же сегмент, они могут обращаться друг к другу по адресам типа near. Будет ошибкой, однако, передать адрес функции fun
в качестве аргумента другой функции, расположенной за пределами сегмента, в котором определена fun, поскольку из другого сегмента функция fun не может быть вызвана.
В третьем примере pfun объявляется как указатель на far функцию, не возвращающую значения, а затем ему присваивается адрес функции fun. Фактически pfun может быть использован для доступа к любой функции, имеющей тип адреса far. Следует понимать, что если функция, вызванная через указатель pfun, не была объявлена с модификатором far, или не получила тип far по умолчанию, то ее вызов приведет к ошибке во время выполнения.
В примере 4 pfun объявляется как указатель на far
функцию, возвращающую указатель на far double, после чего ему присваивается адрес функции fun. Такой вариант может использоваться, например, в компактной модели памяти для функции, которая используется редко, и потому необязательно должна находиться в стандартном сегменте кода. И функция, и указатель должны быть объявлены с модификатором far.
Синтаксис:
[<спецификация типа]> <описатель> [<константное выражение>];
[<спецификация типа]> <описатель> [];
Квадратные скобки, следующие за описателем, являются элементом языка Си, а не признаком необязательности синтаксической конструкции.
Массив позволяет хранить как единое целое последовательность переменных одинакового типа. Объявление массива определяет тип элементов массива и его имя. Оно может определять также число элементов в массиве. Переменная типа массив участвует в выражениях как константа - указатель на значение заданного спецификацией типа. Если спецификация типа опущена, предполагается тип int.
Объявление массива может иметь одну из двух синтаксических форм, указанных выше. Квадратные скобки, следующие за <описателем>, являются признаком типа массив. Если <описатель> представляет собой идентификатор (имя массива), то объявляется массив элементов специфицированного типа. Если же <описатель> представляет собой более сложную конструкцию (см. раздел 3.3.1), то каждый элемент массива имеет тип, заданный совокупностью <спецификации типа> и оставшейся части описателя. Это может быть любой тип, кроме типов void
и функция. Таким образом, элементы массива могут иметь базовый, перечислимый, структурный тип, быть объединением, указателем или, в свою очередь, массивом.
Константное выражение, заключенное в квадратные скобки, определяет число элементов в массиве. Индексация элементов массива начинается с нуля. Таким образом, последний элемент массива имеет индекс на единицу меньше, чем число элементов в массиве.
Во второй синтаксической форме константное выражение в квадратных скобках опущено. Эта форма может быть использована, если в объявлении массива присутствует инициализатор, либо массив объявляется как формальный параметр функции, либо данное объявление является ссылкой на объявление массива где-то в другом месте программы. Однако для многомерного массива может быть опущена только первая размерность.
Объединение позволяет в разные моменты времени хранить в одном объекте значения различного типа. В процессе объявления объединения с ним ассоциируется набор типов значений, которые могут храниться в данном объединении. В каждый момент времени объединение может хранить значение только одного типа из набора. Контроль над тем, какого типа значение хранится в данный момент в объединении, возлагается на программиста. Синтаксис:
union [<тег>] {<список-объявлений-элементов>} <описатель> [,<описатель>…];
union <тег> <описатель> [,<описатель>…];
Объявление объединения специфицирует его имя и совокупность объявлений переменных, называемых элементами объединения, которые могут иметь различные типы.
Объявление объединения имеет тот же синтаксис, что и объявление структуры, за исключением того, что оно начинается с ключевого слова union, а не с ключевого слова struct. Кроме того, СП MSC (в отличие от СП ТС) не допускает в объединении битовые поля.
Память, которая выделяется переменной типа объединение, определяется размером наиболее длинного из элементов объединения. Все элементы объединения размещаются в одной и той же области памяти с одного и того же адреса. Значение текущего элемента объединения теряется, когда другому элементу объединения присваивается значение.
Примеры:
/* пример 1 */
union sign {
int svar;
unsigned uvar;
} number;
/* пример 2 */
union {
char *a, b;
float f[20];
} jack;
В первом примере объявляется переменная типа объединение с именем number. Список объявлений элементов объединения содержит две переменных: svar типа int
и uvar типа unsigned. Это объединение позволяет запоминать целое значение в знаковом или беззнаковом виде. Тип объединения поименован тегом sign.
Во втором примере объявляется переменная типа объединение с именем Jack. Список объявлений элементов содержит три объявления: указателя а на значение типа char, переменной b типа char и массива ж из 20 элементов типа float. Тип объединения не поименован тегом. Память, выделяемая переменной jack, равна памяти, необходимой для хранения массива f, поскольку это самый длинный элемент объединения.
Объявления переменной на внешнем уровне используют спецификации класса памяти static и extern
или вообще опускают их. Спецификации класса памяти auto и register не допускаются на внешнем уровне.
Объявления переменных на внешнем уровне—это либо определения переменных, либо объявления, т.е. ссылки на определения, сделанные в другом месте.
Определение внешней переменной—это объявление, которое вызывает выделение памяти для этой переменной и инициализирует ее (явно или неявно). Определение на внешнем уровне может задаваться в следующих различных формах:
1) Переменная может быть определена путем ее объявления со спецификацией класса памяти static. Такая переменная может быть явно инициализирована константным выражением. Если инициализатор отсутствует, то переменная автоматически инициализируется нулевым значением во время компиляции. Таким образом, каждое из объявлений:
static int k = 16;
и
static int k;
рассматривается как определение.
2) Переменная может быть определена, если спецификация класса памяти в ее объявлении опущена, и переменная явно инициализируется, например,
int j = 3;
Область действия переменной, определенной на внешнем уровне, распространяется от точки, где она определена, до конца исходного файла. Переменная недоступна выше своего определения в том же самом исходном файле. На другие исходные файлы программы область действия переменной распространяется лишь в том случае, если ее определение не содержит спецификации класса памяти static и если в других исходных файлах имеется ее объявление.
Если в объявлении переменной задана спецификация класса памяти static, то в других исходных файлах могут быть определены другие переменные с тем же именем и любым классом памяти. Эти переменные никак не будут связаны между собой, поскольку каждое определение static доступно только в пределах своего исходного файла.
Спецификация класса памяти extern
используется для объявления переменной, определенной где-то в другом месте программы. Такие объявления используются в случае, когда нужно распространить на данный исходный файл область действия переменной, определенной в другом исходном файле, либо сделать переменную доступной в том же исходном файле выше ее определения. Область действия переменной распространяется от места объявления до конца исходного файла.
|
Класс 1 |
Класс 2 |
СП TC |
СП MSC |
|
extern |
static |
static |
static |
|
static |
пусто |
static |
ошибка |
|
static |
extern |
static |
static |
|
пусто |
static |
static |
ошибка |
Любая из четырех спецификаций класса памяти может быть использована для объявления переменной на внутреннем уровне. Если спецификация класса памяти опущена в объявлении переменной на внутреннем уровне, то подразумевается класс памяти auto. Как правило, ключевое слово auto опускается. Понятия объявления и определения для переменных внутреннего уровня совпадают, если только в объявлении не задана спецификация класса памяти extern.
Спецификация класса памяти auto объявляет переменную с локальным временем жизни. Область действия переменной распространяется на блок, в котором она объявлена, (и на все вложенные в него блоки). Переменные класса памяти auto автоматически не инициализируются, поэтому в случае отсутствия инициализации в объявлении значение переменной класса памяти
auto считается неопределенным. Память под переменные класса памяти auto отводится в стеке.
Спецификация класса памяти register
требует, чтобы компилятор языка Си выделил переменной память в регистре микропроцессора, если это возможно. Использование регистровой памяти обычно ускоряет доступ к переменной и уменьшает размер выполняемого кода программы. Переменные, объявленные с классом памяти register, имеют ту же самую область действия, что и переменные auto.
Число регистров, которое может быть использовано для хранения переменных, зависит от компьютера и от реализации компилятора языка Си. Если компилятор языка Си обнаруживает спецификацию класса памяти register
в объявлении переменной, а свободного регистра не имеется, или переменная данного типа не может быть размещена в регистре, то переменной выделяется память класса auto. В СП MSC регистровая память всегда выделяется переменным в том порядке, в котором они объявляются в исходном файле. В СП TC, при наличии нескольких переменных класса памяти register в одном объявлении, регистровая память будет выделяться переменным в обратном порядке. Так, по объявлению register i, j; первой получит регистровую память переменная j.
В регистровой памяти может быть размещен объект размером не больше, чем тип int. К переменной, размещенной в регистре, нельзя применять операцию адресации. При вызове функций из блока, в котором определены регистровые переменные, содержимое регистров будет сохранено в памяти, а по возвращении в блок восстановлено.
Синтаксис:
enum [<тег>]{<список-перечисления>} <описатель>[,<описатель>…];
enum <тег> <идентификатор> [<идентификатор>…];
Объявление переменной перечислимого типа задает имя переменной и определяет список именованных констант, называемый списком перечисления. Каждому элементу списка перечисления ставится в соответствие целое число. Переменная перечислимого типа может принимать только значения из своего списка перечисления. Элементы списка имеют тип int. Поэтому переменной перечислимого типа выделяется ячейка памяти, необходимая для размещения значения типа int. Перечислимый тип, таким образом, представляет собой подмножество целого типа. Над объектами перечислимого типа определены те же операции, что и над объектами целого типа.
<Описатель> специфицирует либо переменную перечислимого типа, либо указатель на значение перечислимого типа, либо массив элементов перечислимого типа, либо функцию, возвращающую значение перечислимого типа, либо более сложный объект, являющийся комбинацией перечисленных типов.
Объявление переменной перечислимого типа начинается с ключевого слова enum и имеет две формы представления.
В первой форме задается список перечисления, содержащий именованные константы. Необязательный <тег> — это идентификатор, который именует перечислимый тип, специфицированный данным списком перечисления, <идентификатор> — это имя переменной перечислимого типа. В одном объявлении может быть описано более одной переменной данного перечислимого типа.
Во второй форме объявления список перечисления отсутствует, однако используется <тег>, который ссылается на перечислимый тип, объявленный в другом месте программы. Если заданный тег ссылается на неизвестный перечислимый тип либо область действия определения этого перечислимого типа не распространяется на текущий блок, то компилятор языка Си сообщает об ошибке. Допускаются объявление указателя на перечислимый тип и объявление typedef
В этом разделе дано последовательное описание синтаксиса и семантики объявлений переменных. Разновидности переменных перечислены в следующей таблице:
Таблица 3.4.
| Вид переменной | Пояснение | ||
| Простая переменная | Скалярная переменная целого или плавающего типа | ||
| Переменная перечислимого типа | Простая переменная целого типа, принимающая значения из предопределенного набора именованных значений | ||
| Структура | Переменная, содержащая совокупность элементов, которые могут иметь различные типы | ||
| Объединение | Переменная, содержащая совокупность элементов, которые могут иметь различные типы, но занимают одну и ту же область памяти | ||
| Массив | Переменная, содержащая совокупность элементов одинакового типа | ||
| Указатель | Переменная, которая указывает на другую переменную (содержит ее адрес) |
Общая синтаксическая форма объявления переменных описана в начале раздела 3. В данном разделе для простоты изложения объявления описываются без спецификаций класса памяти и инициализаторов. Спецификации класса памяти описаны в разделе 3.6, инициализаторы — в разделе 3.7.
В объявлении простой переменной, массива и указателя спецификация типа может быть опущена. Если это объявление записано на внешнем уровне, то спецификация класса памяти тоже может быть опущена. В объявлении внутреннего уровня хотя бы одна из спецификаций — класса памяти или типа — должна присутствовать.
Синтаксис:
<спецификация типа> <идентификатор> [,<идентификатор>…];
Объявление простой переменной определяет имя переменной и ее тип. Имя переменной задается <идентификатором>. <Спецификация типа> задает тип переменной. Тип может быть базовым типом, либо типом структура, либо типом объединение. Если спецификация типа опущена, предполагается тип int.
Можно объявить несколько переменных в одном объявлении, задавая список <идентификаторов>, разделенных запятыми. Каждый <идентификатор> в списке именует отдельную переменную. Все переменные, заданные в таком объявлении, имеют одинаковый тип.
Примеры.
int х; /* пример 1 */
unsigned long
reply, flag; /* пример 2 */
double order; /*
пример 3 */
В первом примере объявляется простая переменная х. Эта переменная может принимать любое значение из области значений типа int.
Во втором примере объявлены две переменные: reply и flag. Обе переменные имеют тип unsigned
long.
В третьем примере
объявлена переменная order, которая имеет тип double.
Этой переменной могут быть присвоены значения с плавающей точкой.
Структура позволяет объединить в одном объекте совокупность значений, которые могут иметь различные типы. Однако в языке Си реализован очень ограниченный набор операций над структурами как единым целым: передача функции в качестве аргумента, возврат в качестве значения функции, получение адреса. Можно присваивать одну структуру другой, если они имеют одинаковый тег.
Синтаксис:
struct [<тег>] {<список-объявлений-элементов>} <описатель> [,<описатель>…];
struct <тег> <описатель> [,<описатель>…];
Объявление структуры может задавать имя структурного типа и/или последовательность объявлений переменных, называемых элементами структуры. Эти элементы могут иметь различные типы.
Объявление структуры начинается с ключевого слова struct и имеет две формы записи, как показано выше. В первой форме типы и имена элементов структуры специфицируются в списке объявлений элементов. Необязательный в данном случае <тег> — это идентификатор, который именует структурный тип, определенный данным списком объявлений элементов.
<Описатель> специфицирует либо переменную структурного типа, либо указатель на структуру данного типа, либо массив структур данного типа, либо функцию, возвращающую структуру данного типа, либо более сложный объект, являющийся комбинацией перечисленных типов.
Вторая синтаксическая форма объявления использует тег структуры для ссылки на структурный тип, определенный где-то в другом месте программы. В этой форме объявления список объявлений элементов отсутствует. Объявление должно находиться в области действия данного тега, т. е. определение структурного типа, именованного тегом, должно предшествовать объявлению, использующему этот тег, за исключением двух случаев: когда тег используется для объявления либо указателя на структуру, либо структурного типа в typedef. Однако при этом определение структурного типа должно предшествовать использованию данного указателя либо типа, объявленного посредством typedef.
Объявление типа структуры, объединения или перечислимого типа имеет такую же синтаксическую форму, как и объявление переменной этих типов, однако в объявлении типа идентификатор переменной (а в общем случае описатель) опущен. Именем типа структуры, объединения или перечислимого типа является тег, который в данном случае обязателен.
Примеры.
/* пример 1 */
enum status {
loss =
-1,
bye,
tie = 0,
win
};
/* пример 2 */
struct student {
char name [20];
int id, class;
}
В первом примере объявляется перечислимый тип с именем status. Имя типа может быть использовано в объявлениях переменных этого перечислимого типа. Идентификатору loss явно присваивается значение -1. Идентификаторы bye и tie
ассоциируются со значением 0, a win - со значением 1.
Во втором примере объявляется структурный тип с именем student. Объявление типа student позволяет записывать впоследствии лаконичные объявления переменных этого типа, например объявление struct student
employee, в котором объявляется структурная переменная employee типа student.
Существует два особых вида объявления, в которых объявляется не переменная или функция, а тип данных. Первый вид позволяет определить тег и элементы структуры, объединения или перечислимого
типа. После такого объявления имя типа (тег) может быть использовано в объявлениях переменных и функций для ссылки на этот тип.
Второй вид объявления типа использует ключевое слово typedef. Это объявление позволяет присвоить осмысленные имена типам, уже существующим в языке или создаваемым пользователем.
Объявление типа имеет такую же блочную область действия, как и объявление переменной. Локальное переобъявление имени типа также возможно. Однако теги занимают отдельное пространство имен, а идентификаторы, объявленные посредством typedef,
разделяют пространство имен с переменными и функциями.
Синтаксис:
typedef <спецификация типа> <описатель> {,<описатель>…];
Объявление typedef синтаксически аналогично объявлению переменной или функции, за исключением того, что вместо спецификации класса памяти записывается ключевое слово typedef
и отсутствует инициализатор.
Объявление typedef интерпретируется таким же образом, как объявление переменной или функции, однако идентификатор, входящий в состав описателя, специфицирует не переменную
или функцию, а тип. Идентификатор становится синонимом для объявленного типа и может употребляться в последующих объявлениях. Другими словами, создаются не новые типы, а имена для специфицированных программистом типов. С помощью typedef может быть объявлено имя для любого типа, как базового, так и составного — указателя, функции, массива.
Объявление typedef для типа указатель на структуру, объединение или значение перечислимого типа, использующее только тег этой структуры, объединения или перечислимого типа, может быть записано раньше, чем данный тег будет определен в программе, однако определение тега должно находиться в пределах области действия этого объявления typedef
и до того, как объявленный тип будет использован.
Принято записывать идентификаторы типов, объявленные посредством typedef, прописными буквами, однако это не является требованием языка.
Примеры:
/*
пример 1 */
typedef int WHOLE;
/* пример 2 */
typedef struct club {
char name [30];
int size, year;
} GROUP;
/* пример 3 */
typedef
GROUP *PG;
/* пример 4 */
typedef void DRAWF
(int, int);
В первом примере объявляется тип WHOLE как синоним для типа int. Во втором примере объявляется тип GROUP для структурного типа, содержащего три элемента. Поскольку специфицирован также тег club, то в последующих объявлениях переменных может быть использован либо тип GROUP, либо тег club. Например, объявления GROUP stgr; и struct
club stgr, эквивалентны по смыслу.
В третьем примере используется имя типа GROUP для объявления типа указатель. Тип PG объявляется как указатель на тип GROUP,
который определен ранее как структурный тип. Например, объявление PG ptr, эквивалентно объявлению struct club *pfr.
В последнем примере объявлен тип DRAWF для функции, не возвращающей значения и требующей два аргумента типа int. Например, объявление DRAWF box; эквивалентно объявлению void box(int, int);.
Указатель — это переменная, предназначенная для хранения адреса объекта некоторого типа. Указатель на функцию содержит адрес точки входа в функцию.
Синтаксис:
[<спецификация типа]> *<описатель>;
Объявление указателя специфицирует имя переменной-указателя и тип объекта, на который может указывать эта переменная. Спецификация типа может задавать базовый, перечислимый,
пустой, структурный тип или тип объединение. Если спецификация типа опущена, предполагается тип int.
Если <описатель> представляет собой идентификатор (имя указателя), то объявляется указатель на значение специфицированного типа. Если же <описатель> представляет собой более сложную конструкцию (см. раздел 3.3.1), то тип объекта, на который указывает указатель, определяется совокупностью оставшейся части описателя и спецификации типа. Указатель может указывать на
значения базового, перечислимого типа, структуры, объединения, массивы, функции, указатели.
Специальное применение имеют указатели на тип void. Указатель на void может указывать на значения любого типа. Однако для выполнения операций над указателем на void
либо над указуемым
объектом, необходимо явно привести тип указателя к типу, отличному от void. Например, если объявлена переменная i
типа int и указатель р на тип void
int i;
void *p;
то можно присвоить указателю р адрес переменной i
p = &i;
но изменить значение указателя нельзя. В СП ТС нельзя также получить значение указуемого объекта по операции косвенной адресации (в СП MSC в этом случае выдается предупреждающее сообщение).
р++; /* недопустимо */
(int *)р++;
/* допустимо */
j = *p; /* недопустимо в СП ТС */
Можно объявить функцию с типом возвращаемого значения указатель на void. Ее значение может быть присвоено указателю на тот тип, который требуется.
Переменная, объявленная как указатель, хранит адрес памяти. Размер памяти, требуемый для адреса, и формат этого адреса зависит от компьютера и реализации компилятора языка Си. Указатели на один и тот же тип данных не обязательно имеют одинаковый размер и формат, поскольку эти параметры зависят от выбранной модели памяти. Кроме того, существуют модификаторы near, far, huge, специфицирующие формат указателя. Объявления, использующие эти модификаторы, рассмотрены в разделе 3.3.3.4.
В этом разделе описываются формат и составные части объявлений переменных, функций и типов. В разделе 2.1 были введены понятия объявления и определения. Далее по тексту будем для краткости называть и объявления, и определения "объявлениями", если явно не конкретизируется то или иное понятие.
Объявления в языке Си имеют следующий синтаксис:
<спецификация КП>
<спецификация типа>
<описатель> [=<инициализатор>] [,<описатель> [= <инициализатор>…]];
где:
<спецификация
КП> — спецификация класса памяти;
<спецификация
типа> — имя типа, присваиваемого объекту;
<описатель> — идентификатор простой переменной либо более сложная конструкция при объявлении переменной составного типа;
<инициализатор> — значение или последовательность значений, присваиваемых переменной при объявлении.
В некоторых случаях спецификация класса памяти и/или спецификация типа может быть опущена.
Все переменные в языке Си должны быть явно объявлены перед их использованием, за исключением формальных параметров, имеющих тип int. Функции могут быть объявлены явно, посредством задания объявления или определения функции, либо неявно, если их вызов следует до определения или объявления.
Спецификация класса памяти влияет на то, в какой области памяти хранится объявляемый объект, производится ли его неявная инициализация и на какие участки программы распространяется его область действия. Местоположение объявления в программе, а также наличие или отсутствие других объявлений этой же переменной также существенны при определении ее области действия. Классы памяти описаны в разделе 3.6.
В языке Си определен набор базовых типов данных. Новые типы данных можно добавлять к этому набору посредством их объявления на основе уже определенных типов данных. Спецификация типа позволяет задавать для объекта либо базовый тип данных (см. раздел 3.1), либо структурный тип (см. раздел 3.4.3), либо тип объединение (см. раздел 3.4.4).