Клуб любителей Linux Linux users group of Taraz ANSI C Описание языка программирования ANSI C. ____________________________________________________________ Table of Contents 1. Введение 1.1 Обзор языка программирования С 1.2 О данном Руководстве 2. Элементы С 2.1 Введение 2.2 Набор символов 2.2.1 Буквы, цифра и подчеркивание 2.2.2 Разделительные символы 2.2.3 Знаки пунктуации и специальные символы 2.2.4 Управляющие последовательности 2.2.5 Операторы 2.3 Константы 2.3.1 Целые константы 2.3.2 Константы с плавающей точкой 2.3.3 Символьные константы 2.3.4 Строковые литералы 2.4 Идентификаторы 2.5 Ключевые слова 2.6 Комментарии 2.7 Лексемы 3. Структура программы 3.1 Введение 3.2 Исходная программа 3.3 Исходные файлы 3.4 Выполнение функций и программ 3.5 Время и сфера действия 3.5.1 Блоки 3.5.2 Время действия 3.5.3 Сфера действия 3.5.4 Краткий обзор 3.6 Классы 4. Объявления 4.1 Введение 4.2 Спецификатоpы типа 4.2.1 Память для фундаментальных типов 4.2.2 Диапазоны значений 4.2.3 Категории типов данных 4.3 Деклараторы 4.3.1 Массив, указатель и объявления функции 4.3.2 Kомплексные объявления 4.4 Объявления переменных 4.4.1 Объявления пpостой пеpеменной 4.4.2 Объявления перечислимых типов 4.4.3 Объявления стpуктуp 4.4.4 Объявления об'единений 4.4.5 Объявления массивов 4.4.6 Объявления указателей 4.5 Объявления функций (Прототипы) 4.5.1 Формальные параметры 4.5.2 Возвращаемый тип 4.5.3 Список формальных параметров 4.5.4 Краткий обзор 4.5.5 Объявления переменных на глобальном уровне 4.5.6 Объявления переменных на локальном уровне 4.5.7 Объявления функций на глобальном и локальном уровне 4.6 Инициализация 4.6.1 Фундаментальные типы и типы указателей 4.6.2 Составные типы 4.6.3 Инициализаторы строк 4.7 Объявления типов 4.7.1 Структуры, об'единения и перечислимые типы 4.7.2 Использование объявления typedef 4.8 Имена типов 5. Выражения и Присвоения 5.1 Введение 5.2 Операнды С 5.2.1 Константы 5.2.2 Идентификаторы 5.2.3 Строки 5.2.4 Вызовы функций 5.2.5 Выражения индексов 5.2.6 Выражения выбора компоненты 5.2.7 Выражения с операторами 5.2.8 Выражения в скобках 5.2.9 Выражения преобразования типа 5.2.10 Постоянные выражения 5.2.11 Побочные эффекты 5.2.12 Точки упорядочевания 5.3 Операторы С 5.3.1 Обычные арифметические преобразования 5.3.2 Дополнение и унарный плюс 5.3.3 Операторы адресации и получения адреса 5.3.4 sizeof 5.3.5 Мультипликативные операторы 5.3.6 Аддитивнные операторы 5.3.7 Сдвиг 5.3.8 Отношения 5.3.9 Побитовая обработка 5.3.10 Логические операторы 5.3.11 Последовательные вычисления 5.3.12 Условия 5.4 Операторы Присвоения 5.4.1 Выражения локализации 5.4.2 Унарное увеличение и уменьшение 5.4.3 Простое присвоение 5.4.4 Составные присвоения 5.5 Приоритет и порядок проведения операций 5.6 Преобразования типа 5.6.1 Преобразования в присвоении 5.6.2 Приведения типа 5.6.3 Преобразования операторов 5.6.4 Преобразования вызовов функций 6. Операторы 6.1 Введение 6.2 Оператор break 6.3 Составной оператор 6.4 Оператор continue 6.5 Оператор do 6.6 Оператор-выражение 6.7 Оператор for 6.8 Оператор goto и операторы с метками 6.9 Оператор if 6.10 Пустой оператор 6.11 Оператор return 6.12 Оператор switch 6.13 Оператор while 7. Функции 7.1 Введение 7.2 Определения функций 7.2.1 Класс хранения 7.2.2 Возвращаемый тип и имя функции 7.2.3 Формальные параметры 7.2.4 Тело функции 7.3 Прототипы функций (Объявления) 7.4 Вызовы функций 7.4.1 Действительные аргументы 7.4.2 Вызовы с переменным числом аргументов 7.4.3 Рекурсивные вызовы 8. Директивы препроцессора и Прагмы 8.1 Введение 8.2 Объявленные константы и Макросы 8.2.1 Операторы предпроцессора 8.2.2 Директива #define 8.2.3 Директива #undef 8.3 Включаемые файлы 8.4 Условная компиляция 8.4.1 Директивы #if, #elif, #else и #endif 8.4.2 Директивы #ifdef и #ifndef 8.5 Управление Line 8.6 Прагмы ______________________________________________________________________ 1.1. Введение Обзор языка программирования С Язык программирования общего назначения С широко известен своей эффективностью, компактностью и мобильностью. Благодаря этим своим характеристикам он пригоден практически для любых областей программирования. С показал свою особенную эффективность в системном программировании, т.к. позволяет писать быстрые компактные программы, которые легко адаптируются в других системах. Хорошо написанные программы на С часто работают со скоростью программ на языке ассемблера, но для программиста их обычно легче читать и сопровождать. С был разработан для сочетания эффективных и мощных средств в относительно небольшом языке. С не содержит встроенных функций для выполнения таких задач, как ввод и вывод, размещение в памяти, манипуляции с экраном и управление процессом. Для выполнения таких задач программисты на С используют "исполнительные библиотеки", наборы заранее определенных функций и макросов. Описания функций исполнительных библиотек включены в отдельную книгу. Стиль программирования на С и гибкий и компактный. Этот язык достаточно свободный и он не предполагает и не навязывает конкретной модели программирования. Можно как использовать имеющиеся исполнительные программы, так и разрабатывать собственные вариации для конкретных целей. Разработка позволяет изолировать характеристики языка от особенностей процессора в конкретной реализации С, что позволяет писать мобильные коды. Точные определения языка делают его независимым от любой конкретной системы или машины, и в то же время позволяют использовать программы для конкретных систем для того, чтобы воспользоваться преимуществами самых эффективных характеристик конкретной машины. Среди важных характеристик языка С можно отметить следующие: ╥ Полный набор операторов цикла, условий и перехода для логичного и эффективного управления ходом выполнения программы, способствующий структурному программированию. ╥ Большой набор операторов. Многие из этих операторов соответствуют общим машинным командам и допускают прямую трансляцию в машинные коды. Множество операторов позволяют ясно и с минимальным кодированием создавать другие виды операций. ╥ Несколько размеров целых величин и типы с плавающей точкой обычной и двойной точности. Можно создавать более сложные типы данных, массивы и структуры данных, согласно конкретным требованиям программы. ╥ Объявление указателей на переменные и функции. Пойнтер для элемента соответствует его машинному адресу. Указатели делают программы более эффективными, т.к. позволяют указывать на элемент так, как это делает машина. С обеспечивает арифметику указателей, которая позволяет непосредственно получать доступ и манипулировать адресами памяти. ╥ Предпроцессор С, который обрабатывает текст файла до его компиляции. Можно использовать предпроцессор С для определения констант программы, заменять вызовы функций быстрыми макро-определениями и компилировать части программ в зависимости от заданных условий. С является гибким языком и оставляет многие решения на усмотрение программиста. Придерживаясь этой философии, С налагает незначительные ограничения на такие вопросы, как преобразование типов. Хотя эти особенности языка и делают процесс программирования более легким, нужно хорошо знать язык для понимания принципов работы программ. О данном Руководстве Это руководство организовано следующим образом: Глава 1, "Введение", знакомит с данным руководством и приводит соглашение о способах записи, принятых в этом издании. Глава 2, "Элементы С", описывает буквы, числа и символы, которые можно использовать в программах на языке С, и комбинации символов, которые имеют специальные значения для компилятора С. Глава 3, "Структура программы", рассматривает компоненты и структуру программ на С, и объясняет способ организации исходных файлов С. Глава 4, "Объявления", объясняет, как задавать в С атрибуты переменных, функций и определяемых пользователем типов. В С имеется ряд заранее определенных типов данных и возможность объявления "агрегированных" типов и указателей. Прототипы функций, как относительно новый объект С, рассматриваются в данной Главе и в Главе 7, "Функции". Глава 5, "Выражения и Присвоения", рассматривает операнды и операторы, которые формируют в С выражения и присвоения. Кроме того, в этой Главе также рассмотрено преобразование типов и побочные эффекты, которые могут проявиться при вычислении значений выражений. Глава 6, "Операторы", описывает операторы С, которые управляют процессом выполнения программы. Глава 7, "Функции", рассматривает функции С. А именно, в данной Главе объясняются прототипы функций, формальные параметры и возвращаемые значения. Показывается, как определять, объявлять и вызывать функции. Глава 8, "Директивы процессора и Прагмы", рассматривает распознаваемые предпроцессором С инструкции. Это текстовый процессор, который автоматически вызывается перед компиляцией. Кроме того, в этой Главе рассмотрены так называемые "прагмы", специальные инструкции для компилятора, которые можно помещать в исходный файл. Элементы С Введение В данной Главе рассматриваются элементы языка программирования С, включая имена, числа и символы, которые используются для построения программ на С. В данной Главе рассматриваются следующие темы: ╥ Набор символов ╥ Константы ╥ Идентификаторы ╥ Ключевые слова ╥ Комментарии ╥ Лексемы Набор символов Для использования в программах С определено два набора символов: "набор символов С" и "представительный набор символов". Набор символов С состоит из букв, цифр и символов пунктуации, которые в С имеют специальное значение. Вы строите программу в С, комбинируя символы набора символов С в осмысленные операторы. Набор символов С является подмножеством представительного набора символов, Представительный набор символов содержит все буквы, цифры и символы, которые имеют графическое представление в виде одного символа. Содержание представительного набора зависит от типа используемого терминала, консоли или символьного устройства. Все символы программы на С должны быть из набора символов С. Однако, строковые литералы, символьные константы, комментарии и имена файлов в директиве #include могут содержать любой символ из представительного набора. Каждый символ в наборе символов С имеет в языке конкретное значение, поэтому компилятор выводит сообщение об ошибке при обнаружении несоответствия в использовании символа в программе. Следующий раздел рассматривает символы из набора символов С и объясняет, как и когда ими пользоваться. Буквы, цифра и подчеркивание Набор символов С содержит прописные и строчные буквы латинского алфавита, 10 десятичных цифр арабской системы исчисления и символ подчеркивания (_). ╥ Прописные английские буквы: A B C D E F G H I J K L M N O P Q R S T U V W X Y Z ╥ Строчные английские буквы: a b c d e f g h i j k l m n o p q r s t u v w x y z ╥ Десятичные цифры: 0 1 2 3 4 5 6 7 8 9 ╥ Символ подчеркивания (_) Эти символы используются для формирования констант, идентификаторов и ключевых слов, описанных далее в этой главе. Компилятор С обрабатывает прописные и строчные буквы, как разные символы. Например, если в идентификаторе использована строчная буква a, Вы не можете заменить ее на прописную букву A, а должны использовать строчную букву. Разделительные символы Пробел, смена строки, возврат каретки и символ новой строки называются разделительными символами, т.к. они выполняют функцию пробелов между словами и строками в напечатанной странице. Эти символы отделяют задаваемые пользователем элементы (например, константы и идентификаторы, от других элементов программы. Компилятор С игнорирует разделительные символы, если они не служат для целей разделения или не являются компонентами символьных констант или строковых литералов. Таким образом, можно использовать дополнительные разделительные символы для улучшения восприятия программы. Компилятор рассматривает комментарии, как разделительные символы. (Комментарии рассматриваются в разделе "Комментарии".) Знаки пунктуации и специальные символы Знаки пунктуации и специальные символы из набора символов С имеют самое разное предназначение, от организации текста программы до определения задач, которые будут выполнены компилятором или скомпилированной программой. В Таблице 2.1 приведен список знаков пунктуации и специальных символов из набора символов С. Таблица 2.1. Знаки пунктуации и специальные символы Символ Название , запятая . точка ; точка с запятой : двоеточие ? знак вопроса ' одинарная цитатная скобка " двойная цитатная скобка ( левая круглая скобка ) правая круглая скобка [ левая прямоугольная скобка ] правая прямоугольная скобка { левая фигурная скобка } правая фигурная скобка < левая угловая скобка > правая угловая скобка ! восклицательный знак | вертикальная черта / знак деления \ знак обратного деления ~ тильда + плюс # номер % процент & амперсанд ^ крышечка * звездочка - минус = равно Все эти символы имеют в С специальное значение. В данном руководстве описывается их использование. Любой знак пунктуации из представительной таблицы, не приведенный в Таблице 2.1, может быть использован только в строковых литералах, символьных константах, комментариях и именах файлов в директивах #include. Управляющие последовательности Строки и символьные константы могут содержать "управляющие последовательности". Управляющие последовательности это комбинации символов, состоящие из разделительного символа и неграфических символов. Управляющая последовательность состоит из знака обратного деления (\) за которым следует буква или комбинация цифр. Управляющие последовательности обычно используются для задания каких-либо действий (например, возврат каретки или сдвиг на табуляцию на экране или на принтере) или для литерального представления неграфических символов и символов, имеющих специальное значение (например, двойные цитатные скобки - "). Таблица 2.2 содержит список управляющих последовательностей. Таблица 2.2. Управляющие последовательности Управляющая Название последовательность \n новая строка \t горизонтальная табуляция \v вертикальная табуляция \b забой \r возврат каретки \f смена листа \a зуммер \' одинарная цитатная скобка \" двойная цитатная скобка \\ обратное деление \ddd восьмеричное значение символа ASCII \xddd шестнадцатеричное значение символа ASCII Если знак обратного деления предшествует символу, которого нет в Таблице 2.2, то знак обратного деления игнорируется, а сам символ воспринимается буквально. Например, \c представляет собой символ "с" в строковом литерале или символьной константе. Однако, использование строчных букв в управляющих последовательностях зарезервировано для будущей стандартизации. Т.о. хотя появления неопределенных управляющих последовательностей и не вызывает проблем сейчас, потом могут возникнуть проблемы мобильности программного обеспечения. Последовательность \ddd позволяет задавать любой символ ASCII (Стандартный Код США по Обмену Информацией), указывая его восьмеричный код из трех цифр. Аналогично, последовательность \xddd позволяет задавать любой символ ASCII, указывая его шестнадцатеричный код из трех цифр. Например, пробел можно указать как \b, \010 (восьмеричное значение) или \x008 (шестнадцатеричное значение). В восьмеричных управляющих последовательностях можно использовать только цифры от 0 до 7. Хотя и не нужно использовать все три цифры (как это показано в предыдущем абзаце), нужно указать хотя бы одну. Например, можно задать символ забоя ASCII в восьмеричном представлении, как \10. Аналогично, можно использовать по крайней мере одну цифру в шестнадцатеричной управляющей последовательности, и опустить вторую и третью цифру. Более того, можно задать пробел в шестнадцатеричном представлении, как \x08 или \x8. Примечание При использовании в строках восьмеричного или шестнадцатеричного представления управляющих последовательностей более безопасно задавать все три цифры. Если не задать все три цифры в управляющей последовательности, а за ней непосредственно следует восьмеричное или шестнадцатеричное число, то компилятор интерпретирует этот символ, как часть управляющей последовательности. Например, если напечатать строку "\x07Bell", то результатом будет {ell, т.к. \x07B будет интерпретировано, как левая фигурная скобка ASCII ({). Строка \x007Bell (обратите внимание на два ведущих нуля) является корректным представлением символа зуммера, за которым следует слово Bell. Строка \x7Bell вызовет появление диагностического сообщения компилятора, т.к. шестнадцатеричная величина 7BE слишком велика, чтобы поместиться в один байт. Управляющие последовательности позволяют посылать на экран неграфические управляющие символы. Например, управляющий символ \033 часто используется в качестве первого символа в команде управления терминалом или принтером. Некоторые управляющие последовательности зависят от устройств. Например, вертикальная табуляция и смена листа (\v и \f) никак не скажутся при выводе на экран, но произведут указанное действие на принтере. В программах на С следует всегда представлять неграфические символы в виде управляющих последовательностей, т.к. их непосредственное использование может вызвать появление диагностических сообщений компилятора. Символ обратного деления можно также использовать в качестве символа продолжения. Если символ новой строки непосредственно следует за знаком обратного деления, то компилятор игнорирует знак обратного деления и символ новой строки и рассматривает следующую строку текста, как часть предыдущей. Это особенно удобно для задания определений предпроцессора, которые могут занимать более одной строки. В прошлом эта особенность использовалась для создания строковых объектов, которые занимали более одной строки текста. Однако, теперь более предпочтительно использовать конкатенацию строк (см."Строковые литералы") для создания длинных строковых литералов. Операторы "Операторы" это символы (состоящие из одного символа или комбинации символов), которые задают манипуляции над значениями. Каждый символ интерпретируется как отдельный элемент, называемый "лексемой". (Определение лексем содержится в разделе "Лексемы".) В Таблице 2.3 содержится список унарных операторов С с их именами. В Таблице 2.4 содержится список бинарных и тернарных операторов С с их именами. Операторы нужно задавать так, как они показаны в таблицах, без разделительных знаков в многосимвольных операторах. Обратите внимание на то, что три символа операторов (звездочка, знак минуса и амперсанд) появляются в двух таблицах. Их унарная или бинарная интерпретация зависит от контекста, в котором они появляются. Оператор sizeof не включен в эти таблицы. Это скорее ключевое слово (sizeof), чем символ, и поэтому приводится в разделе "Ключевые слова". Таблица 2.3. "Унарные операторы" Оператор Название ! логическое НЕ ~ битовое дополнение - арифметическое отрицание * индерекция & адресация + унарный плюс Оператор унарного плюса выполняется синтаксически, а не семантически. Таблица 2.4. "Бинарные и тернарные операторы" Оператор Название + сложение - вычитание * умножение / деление % остаток << сдвиг влево >> сдвиг вправо <= меньше или равно > больше >= больше или равно == эквивалентно != неэквивалентность & битовое И | битовое включающее ИЛИ ^ битовое исключающее ИЛИ |= битовое присвоение включающего ИЛИ && логическое И || логическое ИЛИ , последовательное ?: условие ++ инкремент -- декремент = простое присвоение += присвоение со сложением -= присвоение с вычитанием *= присвоение с умножением /= присвоение с делением %= присвоение с остатком >>= присвоение со сдвигом вправо <<= присвоение со сдвигом влево &= присвоение с битовым И ^= присвоение с битовым исключающим ИЛИ Оператор условия является тернарным, а не многосимвольным оператором. Условное выражение имеет вид: выражение?выражение:выражение. Полное описание каждого оператора содержится в главе "Выражения и присвоения". Константы Константа это число, символ или строка символов, которая в программе используется, как значение. Значение константы нельзя изменить. В языке С есть четыре вида констант: целые, с плавающей точкой, символьные и строковые литералы. Целые константы Синтаксис: цифры 0цифры 0xцифры 0Xцифры "Целая константа" это десятичное, восьмеричное или шестнадцатеричное число, которое представляет собой целое значение в одной из следующих форм: ╥ "Десятичная константа" имеет форму одной или нескольких цифр (от 0 до 9), первая из которых не ноль. ╥ "Восьмеричная константа" имеет форму 0цифры, где цифры это одно или нескольких восьмеричных цифр (от 0 до 7), первая из которых ноль. ╥ "Шестнадцатеричная константа" имеет форму 0xцифры или 0Xцифры, где цифры это одно или нескольких шестнадцатеричных цифр (от 0 до 9 и прописные или строчные буквы от a до f), указание 0x или 0X обязательно. Нельзя разделять цифры в целых константах разделительными цифрами. В Таблице 2.5 приведены примеры трех форм целых констант. Таблица 2.5. Примеры целых констант Десятичные Восьмеричные Шестнадцатеричные 10 012 0xa или 0xA 132 0204 0x84 32179 076663 0x7db3 или 0x7DB3 Целые константы всегда имеют положительные значения. Если нужно использовать отрицательное значение, то поместите знак минус (-) перед константой для формирования выражения с отрицательным значением. (В данном случае знак минус интерпретируется как унарный арифметический оператор отрицания.) Каждой целой константе на основании ее значения присваивается соответствующий тип. Тип константы определяет, какие преобразования будут выполнены при использовании константы в выражении или использовании знака минус (-). Можно сформулировать следующие правила: ╥ Десятичные константы рассматриваются как величины со знаками и имеют тип int или long, если этого требует величина значения. ╥ Восьмеричные и шестнадцатеричные константы в зависимости от размера имеют тип int, unsigned int, long или unsigned long. Если константа может быть представлена, как int, то ей присваивается тип int. Если она больше, чем может хранится в int, но она может быть представлена тем же числом бит, что и int, то ей присваивается тип unsigned int. Аналогично, если константа не размещается в unsigned int, то ей присваивается тип long, или unsigned long при необходимости. В Таблице 2.6 показаны диапазоны значений соответствующих типов для восьмеричных и шестнадцатеричных констант для машины, в которой для типа int выделяется 16 бит. Таблица 2.6. Типы, присваиваемые восьмеричным и шестнадцатеричным константам Шестнадцатеричные Восьмеричные Тип 0x0-0x7FFF 0-077777 int 0x8000-0xFFFF 0100000-0177777 unsigned int 0x10000-0x7FFFFFFF 0200000-017777777777 long 0x80000000-0xFFFFFFFF 020000000000-037777777777 unsigned long Последовательность непосредственного ввода значений по Таблице 2.6 состоит в том, что шестнадцатеричные и восьмеричные константы всегда дополняются нулями при их преобразовании в в более емкий тип. (Дополнительная информация по преобразованию типа содержится в главе "Выражения и присвоения". Можно форсировать целую константу в тип long, указав в конце букву l или L. В Таблице 2.7 показаны некоторые формы целых констант long. Таблица 2.7. Примеры целых констант long Десятичные Восьмеричные Шестнадцатеричные 10L 012L 0xaL или 0xAL 79l 0115l 0x4fl или 0x4Fl Типы описаны в главе "Объявления", а преобразования описаны в главе "Выражения и присвоения". Константы с плавающей точкой Синтаксис: [цифры][.цифры][E|e-[+]цифры] "Константа с плавающей точкой" это десятичное число, которое соответствует действительному числу со знаком. Значение действительного числа со знаком состоит из целой части, дробной части и показателя степени. "Цифр" может не быть или их может быть несколько (от 0 до 9), а E (или e) это символ экспоненты. Можно опустить либо цифры до десятичной точки (целая часть числа) либо после десятичной точки (дробная часть числа), но не одновременно. Если используется показатель степени, то только в этом случае можно не вводить десятичную точку. Показатель степени состоит из символа экспоненты (E или e) за которым следует постоянное целое значение. Целое значение может быть отрицательным. Нельзя использовать разделительные символы между цифрами или символами константы. Константы с плавающей точкой всегда имеют положительные значения. Если нужно использовать отрицательное значение, то поместите знак минус (-) перед константой для формирования выражения с отрицательным значением. В данном случае знак минус интерпретируется как арифметический оператор. Все константы с плавающей точкой имеют тип double. Примеры Следующие примеры иллюстрируют некоторые формы констант и выражений с плавающей точкой: 15.75 1.575E1 1575e-2 -0.0025 -2.5e-3 25E-4 Можно опустить целую часть константы с плавающей точкой, как это показано в следующих примерах: -.125 -.175E-2 Символьные константы Синтаксис: 'символ' "Символьная константа" формируется заключением отдельного символа из представительной таблицы символов в одиночные цитатные скобки (' '). Управляющая последовательность рассматривается как отдельный символ и т.о. может быть символьной константой. Заметим, что символы управления должны быть представлены управляющими последовательностями, иначе будет сгенерировано диагностическое сообщение. Значением символьной константы является числовое значение символа. "Символ" в синтаксисе может быть любым символом из представительной таблицы символов (включая любую управляющую последовательность), кроме одиночной цитатной скобки ('), символа обратного деления (\) или символа новой строки (\n). Для использования в качестве символьной константы одиночной цитатной скобки или символа обратного деления нужно поставить перед ними символ обратного деления, как это показано в Таблице 2.8. Таблица 2.8. Примеры символьных констант Константа Значение ' ' одиночный пробел 'a' строчная a '?' знак вопроса '\b' забой '\x1B' управляющий символ ASCII '\'' одиночная цитатная скобка '\\' обратное деление Символьные константы имеют тип int и расширены за счет знака при преобразованиях типов. (Дополнительную информацию можно найти в разделе "Преобразования типов".) Строковые литералы Синтаксис: "символы"["символы"]... Строковый литерал это последовательность символов из представительной таблицы символов, которая заключена в двойные цитатные скобки. Например, строковый литерал может быть таким: "This is f string literal." В строковом литерале "символы" могут не содержать вообще или состоять из нескольких символов представительного набора, включая любые управляющие последовательности. Двойная цитатная скобка ("), обратное деление (\) и символ новой строки должны быть представлены их управляющими последовательностями (\", \\ и \n). Непечатные символы также должны быть представлены соответствующими управляющими последовательностями. Каждая управляющая последовательность рассматривается как управляющий символ. Для форсированного перехода на новую строку в строковом литерале нужно в месте предполагаемого разрыва строки вставить управляющую последовательность перехода на новую строку (\n), следующим образом: "Enter a number between 1 and 100\nOr press Return" Традиционный способ формирования строк литералов, которые не умещаются на одной строке, состоит в том, что вводится знак обратного деления и нажимается клавиша RETURN. Знак обратного деление заставляет компилятор игнорировать следующий за ним символ новой строки. Например, строковый литерал "Long strings can be bro\ ken into two or more pieces." идентичен строке "Long strings can be broken into two or more pieces." Два или несколько строковых литералов, разделенных только символами разделения будут конкатенированы в одну строку. Например, если передать в функцию printf длинные строки как литералы, то они могут быть продолжены с любой колонки следующей строки без возникновения эффекта сдвига при печати: printf ("This is the first half of the string," " this is the second half") ; Поскольку каждая часть строки заключена в цитатные скобки, то произойдет их конкатенация и Мы получим в результате: This is the first half of the string, this is the second half Можно использовать конкатенацию строк везде, где был использован символ обратного деления за которым стоит символ новой строки для ввода нескольких строк. Строки могут начинаться в любой колонке исходного текста без какого-либо эффекта при выводе на экран, поэтому их нужно размещать так, чтобы улучшить восприятие исходного текста. Например, следующий указатель инициализирован, как две отдельные строки литералов, с разделительным символом между ними, хранится как одна строка. При ссылке на него, как это проделано в следующем примере, получим результат, идентичный предыдущему примеру: char *string = ("This is the first half of the string," " this is the second half") ; printf("%s" , string); Для использования в строковом литерале двойной цитатной скобки или обратного деления нужно перед ним поставить знак обратного деления, как это показано в следующем примере: "First\\Second" "\"Yes", I do,\" she said." Обратите внимание на то, что управляющая последовательность (например, \\ или \") в строковом литерале рассматривается как один символ. Символы строки хранятся последовательно в непрерывной области памяти. Пустой символ (представленный управляющей последовательностью \0) автоматически добавляется в конец строки и отмечает конец каждого строкового литерала. Каждая строка программы рассматривается отдельно, однако не гарантируется, что две идентичные строки получат разные места памяти. Кроме того, программы должны быть разработаны таким образом, чтобы не допускать в процессе своего выполнения модификации строковых литералов. Строковые литералы имеют тип массива char (char[]). Это значит, что строка это массив элементов типа char. Число элементов массива равно числу символов в строке плюс один для заканчивающего пустого символа. Идентификаторы Синтаксис: буква|_[буква|цифра|_]... "Идентификаторы" это имена, которые Вы присваиваете переменным, типам, функциям и меткам в вашей программе. Идентификатор это последовательность из одной или нескольких букв, цифр или знаков подчеркивания (_), которая обязательно начинается с буквы или подчеркивания. Идентификатор может содержать любое число символов, но компилятор различает только первые 31 символ. (Другие программы, которые могут читать вывод компилятора (например, компоновщик), могут распознавать еще меньшее число символов.) Идентификатор создается его заданием в объявлении переменной, типа или функции. После этого его можно использовать в операторах программы для ссылки на связанный с ним элемент. Хотя метки и являются специальным видом идентификаторов и имеют собственный класс, их создание аналогично созданию идентификаторов для переменных и функций. (Определения описаны в главе "Определения", а метки описаны в главе "Операторы".) Компилятор С рассматривает прописные и строчные буквы как разные символы, поэтому можно создать разные идентификаторы с одинаковым произношением, но с одной или несколькими буквами иного размера. Идентификаторы не могут иметь произношение и написание, совпадающее с ключевыми словами языка. Ключевые слова описаны в разделе "Ключевые слова". Не следует использовать символ подчеркивания в начале идентификатора: в этом случае идентификаторы могут вызвать конфликт с именами системных программ и переменных, и вызвать ошибки. Не гарантируется мобильность программ, имена которых начинаются с подчеркивания. Примечание Некоторые компоновщики могут ограничить число и тип символов для времени и сферы действия глобальных величин. (Время и сфера действия рассматривается в одноименном разделе.) Кроме того, компоновщик в отличие от компилятора, не делает различий для прописных и строчных букв. Следует найти в документации на компоновщик налагаемые им ограничения на имена. Примеры Приведем ряд примеров для идентификаторов: j cnt temp1 top_of_page skip12 Прописные и строчные буквы считаются разными символами, поэтому все эти идентификаторы разные: add ADD Add aDD Ключевые слова "Ключевые слова" это заранее определенные идентификаторы, которые имеют для компилятора С специальное значение. Их можно использовать только так, как они определены. Имя элемента программы не может совпадать по произношению и написанию с ключевым словом. В языке С имеются следующие ключевые слова: auto double int struct break else long switch case enum register typedef char extern return union const float short unsigned continue for signed void default goto sizeof volatile do if static while Переопределить ключевые слова нельзя. Однако, можно задать текст, который будет заменен ключевыми словами перед компиляцией, используя директивы предпроцессора (см. главу "Функции"). Ключевое слово volatile реализовано синтаксически, но в настоящий момент не имеет какой-либо привязанной к ней семантики. Нельзя использовать volatile в качестве имени переменной в программе. Комментарии Синтаксис: /* символы */ "Комментарий" это последовательность символов, которая рассматривается компилятором как разделительный символ, но игнорируется им. "Символы" в комментарии могут включать в себя любые комбинации символов представительной таблицы, включая символ новой строки, кроме ограничителя "конец комментария" (*/). Комментарии могут занимать более одной строки, но не могут быть вложенными. Комментарии могут появляться везде, где допустимо появление разделительных символов. Комментарий рассматривается компилятором как единичный разделительный символ, поэтому нельзя включать комментарии с лексемами. Однако, из-за того, что компилятор игнорирует символы комментария, в его текст можно включать ключевые слова без появления ошибок. Для подавления компиляции большой части программы или сегмента программы содержащей комментарии, следует заключить нужную часть программы в скобки директив предпроцессора #if и #endif, а не выделять эту часть программы комментариями (см. "Условная компиляция"). Примеры Приведем ряд примеров, иллюстрирующих комментарии: /* Comments can separate and document lines of programm. */ /* Comments can contain keywords such as for and while. */ /***************************************** Comments can occupy several lines. *****************************************/ Комментарии не могут содержать вложенные комментарии. Следующий пример вызовет появление ошибки: /* You cannot /* nest */ comments */ Ошибка происходит из-за того, что компилятор распознает первое появление */ после слова nest как конец комментария. Он пытается обработать остальной текст и не может этого сделать, поэтому выдает сообщение об ошибке. Лексемы В исходном тексте программы на языке С компилятор распознает основные элементы из групп символов, называемые "лексемами". Лексема это текст исходной программы, который компилятор не анализирует на входящие в него компоненты. Например, в следующем фрагменте программы слово elsewhere использовано в качестве имени функции. Хотя else и является ключевым словом в С, конфликта между лексемой имени функции и лексемой ключевого слова С не не происходит. main() { int i = 0; if (i) elsewhere(); } Однако, если ввести elsewhere как else where, с пробелом между словами else и where, то в предыдущем примере компилятор выдаст диагностическое сообщение, суть которого состоит в отсутствии точки с запятой после ключевого слова else. Примерами лексем служат могут служить описанные в данной главе операторы, константы, идентификаторы и ключевые слова. Также являются лексемами такие символы пунктуации, как прямоугольные ([]), фигурные ({}) и угловые скобки (<>), двоеточие и запятая. Лексемы отделяются разделительными символами и другими лексемами, например, операторами и символами пунктуации. Для предотвращения разбивки компилятором элементов на две или несколько лексем, в идентификаторах, многосимвольных операторах и ключевых словах запрещено использование разделительных символов. Когда компилятор интерпретирует лексемы, он пытается включить в отдельную лексему как можно больше символов до перехода к следующей лексеме. Из-за этой особенности компилятор может неверно интерпретировать лексемы, если они не выделяются разделительными символами. Пример Рассмотрим следующее выражение: i+++j В данном примере компилятор сначала пытается выделить максимально длинный оператор (++) из трех знаков плюс. Оставшийся оператор плюс рассматривается как оператор сложения (+). Т.о. выражение интерпретируется как (i++)+(j), а не как (i)+(++j). В данном случае и в аналогичных случаях нужно использовать разделительные символы и скобки для исключения неоднозначности и обеспечения корректного вычисления выражений. Структура программы Введение В данной главе определяются термины, которые позднее используются в данном Руководстве при описании языка С, и рассматривается структура исходного текста программы на языке С. Дается обзор некоторых характеристик языка С, которые находят свое подробное описание в других главах. В главах "Объявления" и "Функции" рассматривается синтаксис и значение объявлений и определений. Предпроцессор С и прагмы рассмотрены в разделе "Директивы предпроцессора и прагмы". Исходная программа Исходная программа С это набор любого числа директив, прагм, объявлений, определений и операторов. Все эти конструкции кратко рассматриваются в последующих разделах. Чтобы эти конструкции имели смысл, они все должны иметь синтаксис, соответствующий приведенному в данном руководстве, хотя и могут следовать в программе в любом порядке (в соответствии со сформулированными в данном Руководстве правилами). Однако, порядок их появления влияет на использование переменных и функций в программе. (Дополнительная информация по данному вопросу содержится в разделе "Время и сфера действия".) Директивы "Директива" содержит инструкции для предпроцессора С по выполнению некоторых действий над текстом программы до его компиляции. Прагмы "Прагма" дает задание компилятору выполнить конкретные действия во время компиляции. Объявления и определения "Объявление" устанавливает связь между именем и атрибутами переменной, функции и типом. В С все переменные должны быть объявлены до своего использования. "Определение" переменной устанавливает те же связи, что и объявление, но кроме этого переменной выделяется память. Т.о. все определения являются объявлениями, но не все объявления являются определениями. Например, объявление переменной, которое начинается со спецификатора класса хранения extern является скорее "ссылочным" объявлением, чем "определяющим". Ссылочные объявления не вызывают выделение памяти и не могут быть инициализированы. (Дополнительная информация по классам хранения содержится в главе "Объявления".) "Объявления функций" (или "прототипы") устанавливают имя функции, ее возвращаемый тип и, возможно, ее формальные параметры. В определении функции присутствуют те же элементы, что и в ее прототипе, плюс тело функции. Если не сделать четкого объявления функции, то компилятор сам построит ее прототип на основании всей имеющейся информации при первой ссылке на функцию, является ли это ее определением или вызовом. Объявления функций и переменных могут появляться как внутри так и вне определения функции. Любое объявление внутри определения функции считается "внутренним" (локальным). Объявление вне всех определений функций считается "внешним" (глобальным). Определения переменных, как и их объявления, могут происходить как на внутреннем, так и на внешнем уровне. Определения функций всегда происходят на внешнем уровне. Обратите внимание на то, что объявления типов (например, структур, об'единений и объявления typedef), которые не включают имена переменных объявленного типа, не вызывают выделения памяти. Пример В следующем примере показан исходный текст простой программы на С. В исходной программе определяется функция main и объявляется функция printf с прототипом. Программа использует определяющие объявления для инициализации глобальных переменных x и y. Локальные переменные z и w объявляются, но не инициализируются. Память выделяется для всех этих переменных, но только x, y, u и v имеют содержательные значения, т.к. они были инициализированы явно или неявно. Значения z и w не имеют смысла до тех пор, пока им не присваиваются значения в выполняемых операторах. int x=1; /* определяющие объявления для */ int y=2; /* внешних переменных */ extern int printf(char *,...); /* "прототип" функции или ее объявление */ main () /* определение функции main */ { int z; /* определения двух инициализированных */ int w; /* локальных переменных */ static int v; /* определение переменной с глобальным действием */ extern int u; /* ссылочное объявление внешней переменной, которая определяется где-то в ином месте */ z=y+x; /* выполняемые операторы */ w=y-x; printf("z=%d w=%d,z,w); printf("v=%d u=%d,v,u); } Исходные файлы Исходную программу можно разделить на один или несколько "исходных файлов". (Например, исходный файл может содержать лишь несколько нужных программе функций.) При компилировании программы для получения общего текста нужно отдельно скомпилировать, а затем скомпоновать ее исходные файлы. Кроме того, можно использовать директиву #include для комбинирования нескольких исходных файлов в большой исходный файл до компиляции. (Информация по "включению" файлов содержится в главе "Директивы предпроцессора и прагмы".) Исходный файл может содержать любую комбинацию полного набора директив, прагм, объявлений и определений. Нельзя разделять элементы (например, определения функций и большие структуры данных) между разными исходными файлами. Последним символом исходного файла должен быть символ новой строки. Исходный файл не обязательно должен содержать выполняемые операторы. Например, иногда бывает полезно поместить определения переменных в один исходный файл, а их объявления - в другой файл, где они используются. Этот метод позволяет легко находить определения и вносить в них изменения. По аналогичной причине объявления констант и макросов также часто помещают в отдельные файлы, на которые при необходимости можно легко организовать ссылку. Директивы исходного файла применяются только к этому файлу и включенным в него файлам. Более того, директива применяется только к той части файла, которая за ней следует. Прагмы обычно воздействуют на указанную часть исходного файла. Реализация определяет заданные в прагме действия компилятора. (Руководство по компилятору подробно описывает эффект применения конкретных прагм.) Пример В следующем примере показана исходная программа С, состоящая из двух исходных файлов. После компиляции этих исходных файлов их можно скомпоновать и использовать, как одну программу. Предполагается, что функции main и max находятся в разных файлах, и выполнение программы начинается с main. /******************************************************* исходный файл 1 - функция main *******************************************************/ #define ONE 1 #define TWO 2 #define THREE 3 extern int max(int a, int b); /* прототип функции */ main () /* определение функции */ { int w=ONE, x=TWO, y=THREE; int z=0; z=max(x,y); w=min(z,w); } В исходном файле 1 (см. выше) объявляется прототип функции max. Этот вид объявления иногда называется "ранним объявлением". Определение функции main содержит вызов max. Строки, которые начинаются с символа номера (#) являются директивами предпроцессора. Эти директивы информируют предпроцессор о том, что во всем исходном файле 1 нужно заменить идентификаторы ONE, TWO и THREE на указанные цифры. Однако, эти директивы никак не относятся к исходному файлу 2 (см. ниже), который будет скомпилирован отдельно и затем скомпонован с исходным файлом 1. /******************************************************* исходный файл 2 - функция max *******************************************************/ int max(int a, int b) /* обратите внимание на то, что формальные параметры включены в заголовок функции */ { if (a>b) return (a); else return (b); } Исходный файл 2 содержит определение функции max. Это определение удовлетворяет вызову max в исходном файле 1. Заметим, что определение max соответствует стандарту C. Дополнительная информация об этой новой форме и прототипе функции содержится в главе "Функции". Выполнение функций и программ Каждая программа на С должна иметь главную функцию, которая должна иметь имя main. Функция main является стартовой точкой выполнения программы. Она обычно управляет выполнением программы, вызывая в нужный момент другие функции. Программа обычно заканчивает свое выполнение в конце функции main, хотя ее выполнение может быть прервано и в других точках программы в зависимости от условий ее выполнения. Исходная программа обычно состоит из нескольких функций, каждая из которых выполняет одну или несколько конкретных задач. Функция main может вызывать эти функции для выполнения соответствующих им задач. Когда main вызывает другую функцию, она передает выполнение этой функции и ее выполнение начинается с выполнения первого оператора функции. Функция возвращает управление после выполнение оператора return или после достижения конца функции. Можно объявить любую функцию, включая main, с параметрами. Когда одна функция вызывает другую, вызванная функция принимает значения своих параметров от вызвавшей ее функции. Эти значения называются "аргументами". Можно объявить формальные параметры в main так, что они могут принимать значения из внешней среды программы. (Чаще всего эти аргументы передаются из командной строки при выполнении программы.) Традиционно первые три параметры функции main объявляются с именами argc, argv и envp. Параметр argc объявляется для хранения общего числа передаваемых в main аргументов. Параметр argv объявляется как массив указателей, каждый элемент которого указывает на строковое представление переданного в main аргумента. Параметр envp это указатель на таблицу строковых значений, которая устанавливает среду выполнения программы. Операционная система присваивает значения параметрам argc, argv и envp, а пользователь вводит действительные аргументы в main. Операционная система, а не язык С, определяет преобразование при передаче аргументов в конкретной системе. Дополнительная информация по данному вопросу содержится в руководстве по компилятору. При объявлении формальных параметров функции они должны быть объявлены при определении функции. Объявление и определение функций подробно рассмотрено в главах "Объявления" и "Функции". Время и сфера действия Для того, чтобы понять, как работает программа на С, нужно сначала понять правила, которые определяют использование переменных и функций в программе. Для осознания этих правил важно понять три основные концепции: "блок", "время действия" и "сфера действия". Блоки Блок это последовательность объявлений, определений и операторов, которая заключена в скобки. В языке С есть два типа блоков. "Составной оператор" (более подробно рассмотренный в главе "Операторы") это один тип блока. Другой тип блока это "определение функции", которое состоит из тела функции и связанного с ней "заголовка" (имя функции, возвращаемый тип и формальные параметры). Блок может содержать в себе другие блоки за тем исключением, что блок не может содержать определения функции. О блоках внутри других блоков говорят, что они "вложены" в них. Заметим, что хотя все составные операторы заключаются в скобки, не все заключенное в скобки есть составной оператор. Например, хотя спецификации массивов, структур и перечислимых элементов могут быть заключены в скобки, они не рассматриваются в качестве составного оператора. Время действия "Время действия" это период выполнения программы, в котором существует переменная или функция. Все функции программы существуют на всем протяжении выполнения программы. Время действия переменной может быть внутренним (локальным) или внешним (глобальным). Элемент с локальным временем действия ("локальный элемент") хранит и определяет значение только в блоке, в котором он был определен или объявлен. Локальному элементу всегда выделяется новое место в памяти каждый раз, когда программа входит в блок, и хранимая величина теряется, когда программа выходит из блока. Если время действия переменной глобально (глобальный элемент), то он хранит и определяет значение на всем протяжении выполнения программы. Следующие правила определяют, имеет переменная глобальное или локальное время действия: - Переменные, которые объявлены на внутреннем уровне (в блоке) обычно имеют локальное время действия. Можно задать для переменной в блоке глобальное время действия, если указать в ее объявлении спецификатор класса хранения static. После объявления static переменная будет хранить в себе значение от одного вызова блока до другого. Однако, к ней можно будет обратиться только в данном блоке или из вложенных в него блоков. (Доступ к элементам рассматривается в следующем разделе. Информацию о спецификаторах класса хранения можно найти в главе "Объявления".) - Переменные, которые объявлены на внешнем уровне (вне всех блоков программы) всегда имеют глобальное время действия. Сфера действия "Сфера действия" элемента определяет ту часть программы, в которой на него можно сослаться по имени. Элемент доступен только в своей сфере действия, которая может быть ограничена (в порядке усиления ограничений) файлом, функцией, блоком или прототипом функции, в котором она появляется. В С только имя метки всегда имеет сферой своего действия функцию. (Дополнительная информация по меткам и их именам содержится в главе "Операторы".) Сфера действия любого другого элемента определяется уровнем, на котором производится его объявление. Объявленный на внешнем уровне элемент имеет сферой своего действия весь файл и доступен из любого места файла. Если его объявление происходит в блоке (включая список формальных параметров в определении функции), то сфера действия элемента ограничивается данным блоком и вложенными в него блоками. Имена формальных параметров в списке параметров прототипа функции имеют сферу действия только от объявления параметра до конца объявления функции. Примечание Хотя элемент с глобальным временем действия и "существует" на всем протяжении выполнения исходной программы (например, внешне объявленная переменная или локальная переменная, объявленная с ключевым словом static), он может и не быть доступен из всех частей программы. Про элемент говорят, что он "глобально доступен", если он доступен или можно сделать соответствующее объявление для доступа к нему, из всех исходных файлов, составляющих программу. (Дополнительная информация по организации доступа между исходными файлами, известной как "компоновка", содержится в главе "Объявления".) Следующие правила определяют доступ к переменным и функциям в программе: ╥ Переменные, которые были объявлены или определены на глобальном уровне (т.е. вне всех блоков программы) доступны от точки их объявления или определения до конца исходного файла. Можно использовать соответствующие объявления для того, чтобы эти переменные стали доступны и из других исходных файлов, как это описано в "Классах хранения". Однако, объявленные на глобальном уровне переменные со спецификатором класса хранения static доступны только в том исходном классе, в котором они были определены. ╥ В общем случае, переменные, которые объявлены или определены на локальном уровне (т.е. внутри блока) доступны всем вложенным блокам. Однако, в этом случае говорят о "вложенности" доступа. Например, блок, который вложен в другой блок, может содержать объявления переменных, идентификаторы (имена) которых могут совпадать с переменными внешнего блока. Такие переопределения доминируют только во внутреннем блоке. После выхода из внутреннего блока будет восстановлено определение внешнего блока. ╥ Функции с классом хранения static доступны только в том исходном файле, где они были определены. Все другие функции доступны глобально. (Дополнительная информация по объявлениям функций содержится в разделе "Определения функций и Прототипы".) Краткий обзор В Таблице 3.1 суммируются основные факторы, определяющие время и сферу действия переменных и функций. Однако, эта таблица не учитывает всех возможных вариантов. Дополнительная информация содержится в Главе "Объявления". Таблица 3.1. Краткий обзор времени и сферы действия Спецификатор Уровень Элемент Класса Время Сфера хранения жизни действия Внешний определение static глобальное ограничено переменной исходным файлом, в котором появляется объявление extern глобальное ограничено остатком переменной исходного файла прототип static глобальное ограничено функции или единственным определение исходным файлом прототип extern глобальное ограничено остатком функции исходного файла Внутренний объявление extern глобальное блок переменной определение static глобальное блок переменной определение auto или локальное блок переменной register Пример Следующий пример программы иллюстрирует блоки, вложение и доступ к переменным. В примере есть четыре уровня доступа: внешний и три уровня блоков. Предположим, что функция printf определена в каком-либо другом месте программы. Значения будут выводиться на экран так, как это показано в предшествующем каждому оператору комментарии. #include /* i определяется на внешнем уровне */ int i=1; /* определение функции main на внешнем уровне */ main () { /* печатается 1 (значение i на внешнем уровне) */ printf("%d\n", i); /* начало первого вложенного блока */ { /* i и j определяются на внутреннем уровне */ int i=2; j=3; /* печатается 2 и 3 */ printf("%d\n%d\n", i, j); /* начало втоpого вложенного блока */ { /* пеpеопpеделение i */ int i=0; /* печатается 0 и 3 */ printf("%d\n%d\n", i, j); /* конец втоpого вложенного блока */ } /* конец пеpвого вложенного блока */ } /*печатается 1(восстановлен внешний уровень определения*/ printf("%d\n", i); } Классы В программе на языке С идентификаторы используются для ссылки на многие виды различных объектов. При написании программы на С идентификаторы используются для функций, переменных, формальных параметров, компонентов об'единений и других используемых в программе объектов. С позволяет использовать один идентификатор для нескольких элементов программы, если придерживаться правил, изложенных в этом разделе. (Определение идентификатора содержится в Главе "Элементы С".) Компилятор устанавливает "классы" для того, чтобы различать идентификаторы, использованные для различных видов элементов. Чтобы не возникало конфликта, имя каждого класса должно быть уникальным, но в нескольких классах может быть использовано идентичное имя. Это значит, что можно использовать идентификатор для нескольких разных объектов, если эти объекты располагаются в разных классах. Компилятор может идентифицировать ссылку исходя из контекста использования идентификатора в программе. Следующий список описывает виды элементов, которым можно присваивать имена в программе на С и привила по присваиванию имен: Переменные Имена переменных и функций находятся в и функции поименованном классе с формальными параметрами, именами typedef перечислимыми константами. Имена переменных и функций должны отличаться от других имен в этом классе с той же самой сферой действия. Однако, можно переопределять имена переменных и функций в блоках программы, как это описано в разделе "Время и сфера действия". Формальные Имена формальных параметров функций группируются с параметры именами переменных функции, поэтому они должны быть разными. Нельзя переобъявить формальные параметры на верхнем уровне функции. Однако, имена формальных параметров могут быть переопределены (т.е. использованы для указания на другие объекты) в блоке, который вложен в тело функции. Перечислимые Перечислимые константы находятся в том же классе, константы что и имена переменных и функций. Это значит, что имена перечислимых констант должны отличаться от имен переменных и функций той же сферы действия и от имен других перечислимых констант этой сферы действия. Однако, аналогично именам переменных, имена перечислимых констант могут быть переопределены во вложенном блоке. (Вложение рассматривается в разделе "Время и сфера действия".) имена typedef Имена типов, определенные с ключевым словом typedef, находятся в одном классе с именами переменных и функций. Т.о. имена typedef должны отличаться от имен всех переменных и функций той же сферы действия, также как и от имен формальных параметров и перечислимых констант. Аналогично именам переменных, типы typedef могут быть переопределены в блоках программы. См. "Время и сфера действия". Признаки Признаки перечислимых типов, имен и об'единений сгруппированы в отдельный класс. Имена одних признаков должны отличаться от других в той же самой сфере действия. Имена признаков не конфликтуют с какими-либо другими именами. Компоненты Компоненты каждой структуры и об'единения формируют класс. Имя компоненты должно быть уникальным в структуре или в об'единении, но не обязательно должно отличаться от других использованных в программе имен, включая имена компонентов в других структурах и об'единениях. Метки Метки операторов формируют отдельный класс. Каждая метка оператора должна отличаться от всех других меток операторов в одной функции. Метки операторов не обязательно должны отличаться от других имен или других имен меток в других функциях. Пример Три элемента с именем student в следующем примере не конфликтуют, т.к. признаки структур, компоненты структур и имена переменных находятся в трех разных классах. Контекст использования каждого элемента позволяет корректно интерпретировать каждое появление student в программе. Например, когда student появляется после ключевого слова struct, компилятор распознает, что это признак структуры. Когда student появляется после оператора выбора компоненты (-> или .), это имя относится к компоненте структуры. В другом случае student относится к структурной переменной. struct student { char student[20]; int class; int id; } student; Объявления Введение В данной Главе pассматpиваются фоpма и содеpжание объяв лений в языке С пеpеменных, функций и типов. Объявление в С имеет вид: [спец-хpан][спец-тип]декл[=иниц][,декл[=иниц]]... где "спец-хpан" это спецификатоp хpанения; "спец-тип" это имя оп pеделяемого типа; "иниц" задает значение или последовательность значений, котоpые будут пpисвоены объявляемой пеpеменной. "декл" это идентификатоp, котоpый может быть модифициpован пpямоугольными ([]), фигуpными (()) скобками или звездочкой (*). До своего использования все пеpеменные в С должны быть явно объявлены. В С можно явно объявить функцию ее пpототипом. Если не задать пpототипа, то он будет создан автоматически по той инфоpмации, котоpая пpисутствует пpи пеpвой ссылке на функцию пpи ее опpеделении или вызове. Язык С имеет стандаpтный набоp типов данных. Пользователь может создавать свои собственные типы данных, объявление котоpых основано на уже имеющихся типах данных. Можно объявлять массивы, стpуктуpы данных и указатели как на пеpеменные, так и на функции. Объявления тpебуют одного или нескольких "деклаpатоpов". Декла pатоp - это идентификатоp, котоpый может быть модифициpован пpямо угольными ([]), фигуpными (()) скобками или звездочкой (*) для объявления соответственно массива, типа функции или пойнтеpа. Пpи опpеделении пpостых пеpеменных (каковыми являются символьные, целые или с плавающей точкой) или стpуктуp и союзов пpостых пеpеменных, деклаpатоp является пpосто идентификатоpом. В С опpеделены четыpе спецификатоpа класса хpанения: auto, extern, register и static. Спецификатоp класса хpанения в объявле нии указывает метод хpанения и инициализации объявляемого объекта и какие части пpогpаммы могут его использовать. Расположение объяв лений в исходном тексте пpогpаммы и наличие и отсутствие дpугих объявлений пеpеменной также являются важными фактоpами для опpеделе ния доступности пеpеменных. Объявления пpототипов функций pассматpиваются в pазделе "Объяв ления функций (пpототипы)" данной Главы и в Главе "Функции" данного Руководства. Инфоpмацию по опpеделениям функций можно найти в Главе "Функции". Спецификатоpы типа В языке С имеются опpеделения для основных типов данных, называ емых "фундаментальными". Их имена пpиведены в Таблице 4.1. Таблица 4.1. Фундаментальные типы Интегpальные Типы с плавающей Пpочие типы точкой char float void int double const short long double volatile long signed unsigned enum Необязательные ключевые слова signed и unsigned могут предшествовать любому интегральному типу, кроме enum, и могут быть использованы отдельно в качестве спецификаторов типа. При этом они понимаются соответственно как signed int и unsigned int. Ключевое слово int, если оно используется отдельно, воспринимается как signed. Ключевые слова long и short, когда они используются отдельно, воспринимаются как long int и short int. Тип long double семантически эквивалентен double, но синтаксически отличается от него. Ключевое слово void можно использовать в трех случаях: как возвращаемый функцией тип, как список типов аргумента для функции без аргументов, и для модификации указателя Ключевое слово volatile реализовано синтаксически, но не семантически. Перечислимые типы рассматриваются как фундаментальные. Примечание Тип long float более не поддерживается, и его появление в тексте старой программы должно быть заменено на double. Типы signed char, signed int, signed short int и signed long int (как и соответствующие unsigned и enum) называются "интегральными" типами. Спецификаторы типов float, double и long double называются "плавающими" типами или типами "с плавающей точкой". В объявлении переменной или функции можно использовать любой спецификатор интегрального типа или типа с плавающей точкой. Можно использовать тип void для объявления функции, которая не возвращает значения, или для объявления указателя на незаданный тип. Когда ключевое слово void появляется отдельно в скобках за именем функции, оно не интерпретируется как спецификатор типа. В данном контексте оно указывает только на то, что у функции нет аргументов. Типы функций рассмотрены в разделе "Объявления функций (прототипы)". Спецификатор типа const объявляет не подлежащий модификации объект. Ключевое слово const может быть модификатором для любого фундаментального или составного типа, или модифицировать указатель на объект любого типа. Спецификатор типа const может модифицировать typedef. Если объявление составного типа содержит модификатор const, то каждый элемент составного типа считается неизменным. Если элемент объявлен только со спецификатором типа const, то считается что он имеет тип const int. Объекты const можно поместить в раздел памяти, предназначенный только для чтения. Спецификатор типа volatile объявляет элемент, значение которого может быть изменено без контроля со стороны программы, в которой он появляется. Ключевое слово volatile может быть использовано в тех же обстоятельствах, что и const (которое только что было рассмотрено). Элемент может быть одновременно и const и volatile, в этом случае элемент не может быть изменен в программе, но может быть изменен некоторым асинхронным процессом. Ключевое слово volatile реализовано синтаксически, но не семантически. С помощью объявлений typedef можно создать дополнительные спецификаторы типов (см. раздел "Объявления типов"). При использовании таких спецификаторов в объявлении их можно изменить только модификаторами const и volatile. Спецификаторы типа обычно записываются сокращенно, как это показано в Таблице 4.2. Интегральные типы по умолчанию имеют знак. Т.о. если в спецификаторе типа не используется ключевое слово unsigned, то интегральный тип имеет знак, даже если не указано ключевое слово signed. В некоторых реализациях можно задать опцию компилятора, по которой тип char сменит установку по умолчанию с signed на unsigned. При использовании этой опции запись char означает unsigned char, и нужно использовать ключевое слово signed для объявления символьного значения со знаком. Опции компилятора описаны в Вашем Руководстве по компилятору. Примечание В данном Руководстве, как правило, используется сокращенная форма написания спецификаторов типа, приведенная в Таблице 4.2, а не полная форма, и предполагается, что тип char по умолчанию имеет знак. Т.о. вместо signed char стоит просто char. Таблица 4.2. Спецификаторы типов и их сокращения Спецификатор типа Сокращенное написание 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 - const int const volatile int volatile const volatile int const volatile Когда вы задаете, что тип char по умолчанию не имеет знака (указывая соответствующую опцию компилятора), нельзя использовать сокращенный тип записи для signed char. Когда вы задаете, что тип char по умолчанию не имеет знака (указывая соответствующую опцию компилятора), можно использовать сокращенный тип записи для unsigned char в виде char. Память для фундаментальных типов В Таблице 4.3 приводятся размеры памяти, которые выделяются для хранения каждого фундаментального типа, и диапазоны значений, которые могут храниться в переменной каждого типа. Спецификатор void не включен в таблицу, т.к. он используется только для указания на функцию без возвращаемого значения и для указателя на незаданный тип. Аналогично, таблица не содержит const и volatile, т.к. размер переменных не модифицируется ими и переменные могут содержать любое значение из диапазона своего фундаментального типа. Таблица 4.3. Память и диапазон значений для фундаментальных типов Тип Память Диапазон значений (Внутренний) char 1 байт от -128 до 127 int зависит от реализации short 2 байта от -32,768 до 32,767 long 4 байта от -2,147,483,648 до 2,147,483,647 unsigned char 1 байт от 0 до 255 unsigned зависит от реализации unsigned short 2 байта от 0 до 65,535 unsigned long 4 байта от 0 до 4,295,967,295 float 4 байта запись по стандарту IEEE, будет рассмотрена далее double 8 байт запись по стандарту IEEE, будет рассмотрена далее long double 8 байт запись по стандарту IEEE, будет рассмотрена далее Тип char хранит целые значения элементов представительного набора символов. Это целое значение есть код ASCII для соответствующего символа. Из-за того, что тип char интерпретируется как целое со знаком, занимающее 1 байт, переменная char может хранить значения от -128 до 127, хотя символьный эквивалент имеют только значения от 0 до 127. Аналогично, unsigned char может хранить значения в диапазоне от 0 до 255. Обратите внимание на то, что язык С не определяет размер памяти и диапазон значений для типов int и unsigned int. Вместо этого используется стандартный размер целого в конкретной машине. Например, в 16-битовой машине тип int обычно занимает 16 бит, или 2 байта. В 32-битовой машине тип int обычно занимает 32 бита, или 4 байта. Т.о. тип int эквивалентен либо типу short int либо long int, а тип unsigned int эквивалентен либо типу unsigned short либо типу unsigned long, в зависимости от реализации. Спецификаторы типа int и unsigned int (или просто unsigned) задают некоторые характеристики языка С (например, тип enum, который будет рассмотрен позднее в разделе "Объявления типов"). В данных случаях определения int и unsigned int для конкретной реализации определяют действительный размер выделяемой памяти. Примечание Спецификаторы типов int и unsigned int широко используются в программах на языке С, т.к. они позволяют конкретной машине обрабатывать целые величины наиболее эффективным именно для данной машины способом. Однако, из-за того, что размеры типов int и unsigned int разные, программы, которые зависят от конкретных размеров int могут не переноситься на другие ЭВМ. Чтобы увеличить мобильность программ, можно вместо жесткого кода размеров данных использовать использовать выражения с оператором sizeof. Реальный размер int и unsigned int следует уточнить по Вашему Руководству по компилятоpу. Числа с плавающей точкой используют фоpмат IEEE(Институт Ин женеpов по Электpике и Электpонике, Инк.). Значение типа float занимают 4 байта, котоpые содеpжат бит знака, 8 бит двоичной экспоненты и 23 бита мантиссы. Мантисса пpедставляет собой число от 1.0 до 2.0. Т.к. стаpший бит мантиссы всегда pавен 1, то он не хpанится в числе. Пpи таком пpедставлении в типе float можно хpа нить величины в диапазоне пpиблизительно от 3.4E-38 до 3.4.E+38. Значения типа double занимают 8 байт. Формат аналогичен формату float, за тем исключением, что на экспоненту выделяется 11 бит, а на мантиссу 52 бита плюс подразумеваемый старший бит 1. С помощью этого формата для типа double можно представлять величины в диапазоне приблизительно от 1.7Е-308 до 1.7Е+308. Дапазоны значений Диапазон значений переменной ограничивается минимальным и максимальным значениями, которые могут быть внутренне представлены заданным числом бит. Однако, в соответствии с правилами преобразования в С (которые подробно излагаются в Главе "Выражения и присвоения") нельзя всегда использовать максимальное и минимальное значение для константы конкретного типа в выражении. Например, постоянное выражение -32768 состоит из оператора арифметического отрицания (-), примененного к постоянной величине 32,768. 32,768 слишком велико, чтобы быть представлено short int, поэтому ему присваивается тип long. Соответственно, постоянное выражение -32768 имеет тип long. Можно представить -32,768 как short int только приведением типа short. При приведении типа не происходит потери информации, т.к. -32,768 будет иметь внутреннее представление из 2 байт. Аналогично, значение 65,000 может быть представлено unsigned short приведением типа или заданием значения в восьмеричной или шестнадцатеричной форме записи. В десятичной форме записи считается, что 65,000 это константа со знаком. Значению присваивается тип long, т.к. 65,000 не умещается в short. Можно привести значение long к типу unsigned short без потери информации, т.к. 65,000 может поместиться в 2 байта, если оно хранится как число без знака. Восьмеричные и шестнадцатеричные константы могут иметь тип signed или unsigned в зависимости от их размера (см. раздел "Целые константы"). Однако, при присваивании типа восьмеричным и шестнадцатеричным константам используется метод, при котором они всегда при преобразовании типа ведут себя как целые без знака. Категории типов данных Среди типов данных языка С можно выделить две категории: скаляры и составные. Скалярные типы включают указатели и арифметические типы. Арифметические типы включают типы с плавающей точкой и целые, как это описано в данном разделе. Составные типы включают массивы и структуры. В Таблице 4.4 показаны категории типов данных С. Таблица 4.4. Категории типов данных С Тип категории Тип данных Интегральные char int short long signed Арифметические unsigned enum Скалярные enum С плавающей точкой float double long double Составные указатели массивы структуры Деклараторы Синтаксис: идентификатор декларатор[[постоянное-выражение]] *декларатор (декларатор) Язык С позволяет объявлять "массивы" значений, "указатели" на значения, "возвращаемые функцией" значения заданного типа. Для объявления этих элементов нужно использовать "декларатор". "Декларатор" это идентификатор, который может модифицироваться прямоугольными ([]) или круглыми (()) скобками и звездочкой (*) для объявления соответственно массива, типа функции или указателя. Деклараторы появляются в описанных в данной Главе объявлениях массивов, указателей и функций. В следующем разделе приводятся правила формирования и интерпретации деклараторов. Массив, указатель и объявления функции Если декларатор состоит из немодифицированного идентификатора, то объявляемый элемент имеет базовый тип. Если за идентификатором следуют прямоугольные скобки ([]), то это объявление массива. Если слева от идентификатора стоит звездочка (*), то это объявление указателя. Если за идентификатором следуют круглые скобки, то это возвращаемое функцией значение. Для того, чтобы декларатор был полным он должен обязательно включать спецификатор типа. Спецификатор типа задает тип элементов массива, тип объектов, на которые указывает указатель, или возвращаемый функцией тип. Детальное описание каждого типа объявлений массива, указателя и функции подробно рассматpивается в последующих pазделах данной Главы. Следующие пpимеpы иллюстpиpуют пpостейшие фоpмы деклаpатоpов: Пpимеp 1 Объявление массива целых значений с именем list: int list[20]; Пpимеp 2 Объявление пойнтеpа с именем сp на значение char: char *cp; Пpимеp 3 Объявление функции с именем func без аpгументов с возвpащаемым значением double: double func(void); Kомплексные объявления Можно заключить любые деклаpатоpы в скобки для задания конкpетной pеализации сложного деклаpатоpа. "Сложный" деклаpатоp это идентификатоp, котоpый задает мо дификатоp для нескольких массивов, или функций. Можно использовать pазные комбинации модификатоpовмассива, и функции с одним иденти фикатоpом. Однако, в деклаpатоpе недопустимо появление следующих некоppектных комбинаций. Массив не может иметь функции в качестве своих элементов. Функция не может возвpащать массив или функцию. Пpи интеpпpетации сложных деклаpатоpов пpямоугольные и кpуглые скобки (модификатоpы, стоящие спpава от идентификатоpа) имеют пpе имущество над звездочкой (модификатоpом котоpый стоит слева от иден тификатоpа). Скобки имеют pавный пpиоpитет и ассоцииpуются слева напpаво. После полной интеpпpетации деклаpатоpа на последнем шаге пpименяется тип спецификатоpа. Используя кpуглые скобки можно изменить пpинятый по умолчанию поpядок ассоциации и назначить конкpетную интеpпpетацию. Пpостой способ интеpпpетации сложных деклаpатоpов состоит в том, что они читаются изнутpи наpужу по следующим четыpем этапам: 1. Hачать с идентификатоpа и пpовеpить, есть ли у него скобки спpава. 2. Интеpпpетиpовать эти скобки и затем пpовеpить, есть ли слева звездочка. 3. Для каждой скобки спpава пpименить пп. 2 и 2 для их содеpжимого. 4. Пpименить спецификатоp типа. Пpимеp 1 В следующем пpимеpе этапы помечены так, в какой последова тельности они интеpпpетиpовались: 1. Идентификатоp var объявлен, как 2. указатель на 3. функцию, возвpащающую 4. указатель на 5. массив из 10 элементов, котоpые есть 6. указатели на 7. значения char char *(*(*var)())[10]; ^ ^ ^ ^ ^ ^ ^ 7 6 4 2 1 3 5 Пpимеpы 2 - 9 являются дополнительной иллюстpацией объявлений и показывают, как скобки могут воздействовать на смысл объявления. Пpимеp 2 В следующем пpимеpе модификатоp массива имеет пpиоpитет над модификатоpом, поэтому var это массив. Модификатоp пpименяется к типу элементов массива. Т.о. элементы массива это значения int. /* массив значения int */ int *var[5]; Пpимеp 3 В следующем пpимеpе скобки дают модификатоpу пpиоpитет над модификатоpом массива, и var это массив пяти значений int. /*массив значений int */ int (*var) [5] Пpимеp 4 Модификатоpы функций имеют пpиоpитет над модификатоpами, поэтому var это функция, возвpащающая значение long. /*функция, возвpащающая значение long */ long *var(long,long); Пpимеp 5 Данный пpимеp аналогичен Пpимеpу 3. Скобки дают модификатоpу пpиоpитет над модификатоpом функции и var будет функция, котоpая возвpащает значение long. Функция снова пpинимает два аpгумента long. /*функция, возвpащающая значение long*/ long (*var)(long,long); Пpимеp 6 Элементы массива не могут быть функциями, но в данном пpимеpе показано, как вместо этого объявить массив указателей на функции. D пpимеpе var это массив из пяти указателей на функции, котоpые возвpа щают стpуктуpы с двумя компонентами. Аpгументами для функций объявле ны две стpуктуpы с тем же самым типом стpуктуpы, both. Обpатите вни мание, что необходимо заключить *nar[5] в скобки. Без этих скобок данное объявление будет некоppектной попыткой объявить массив функций, как это показано: /*некоppектно*/ struct both *var[5](struct both, struct both); /*массив функции, возвpащающие стpуктуpы*/ struct both { int a; char b; } (*var[5])(struct both, struct bith); Пример 7 В данном примере показано, как объявить функцию, возвращающую указатель на массив, т.к. недопустимы функции, возвращающие массив. Здесь var это функция, возвращающая указатель на массив из трех значений double. Тип аргумента задается сложным абстрактным декларатором. Требуются скобки вокруг звездочки в типе аргумента. Без них тип аргумента будет массивом из трех указателей на значения double. Обсуждение и примеры абстрактных деклараторов можно найти в разделе "Имена типов". /* функция, возвращающая указатель на массив из 3 значений double */ double (*var(double(*)[3]))[3]; Пример 8 Как показано в данном примере указатель может указывать на другой указатель, и массив может содержать в качестве элементов другие массивы. Здесь var это массив из пяти элементов. Каждый элемент это массив из пяти указателей, которые указывают на об'единения,каждый из которых имеет две компоненты. /* массив массивов указателей, указывающих на об'единения */ union sign { int x; unsigned y; } **var[5][5]; Пример 9 В данном примере показано, как задание скобок изменяет смысл объявления. В примере var это массив из пяти элементов, которые являются указателямии на массивы из пяти элементов-указателей на об'единения. /* массив указателей на массивы указателей на об'единения */ union sign *(*var[5])[5]; Объявления переменных Синтаксис: [хран-спец]тип-спец декларатор[,декларатор]... В данном разделе описана форма и значение объявления переменной. Объясняется, как объявлять: Простые Отдельные переменные с одним значением переменные интегрального типа или с плавающей точкой Перечислимые Простые переменные интегрального типа, которые переменные хранят одно значение из набора поименованных целых констант Структуры Переменные, составленные из набора значений, которые могут иметь разный тип Об'единения Переменные, составленные из нескольких значений разного типа, которые занимают одну область памяти Массивы Переменные, составленные из набора значений одного типа Пойнтеры Переменные, которые указывают на другие переменные и содержат местоположение переменной (в форме адреса) вместо ее значения В общей форме объявления переменной "тип-спец" задает тип данных переменной, а "декларатор" задает имя переменной, возможно модифицированное для объявления типа массива или пойнтеpа. "Тип -спец" может быть составным, когда тип модифициpует const, volatile или одиним из специальных ключевых слов, описанных в pазделе "Объявления со специальными ключевыми словами". Можно объявить сpазу несколько пеpеменных, если в объявлении исполь зовать чеpез запятую множественные деклаpатоpы. Hапpимеp, int const far *fp объявляет пеpеменную с именем fr как far на неизме няемое значение int. "Хpан-спец" задает класс хpанения пеpеменной. В некотоpых случаях можно инициализиpовать пеpеменную в момент ее объявления. Инфоpмация, касающая классов хpанения и инициализации, содеpжится соответственно в pазделах "Kлассы хpанения" и "Инициализация". Объявления пpостой пеpеменной Синтансис: [хpан-спец]тип-спец идентификатоp[.идентификатоp]... В объявлении пpостой пеpеменной задается ее имя и тип. Kpоме того, можно задать класс хpанения пеpеменной, как это описано в pаз деле "Kлассы хpанения". "Идентификатоp" в объявлении это имя пеpе менной. "Тип-спец" это имя опpеделяемого типа данных. Можно в одном объявлении задать несколько пеpеменных, вводя их идентификатоpы списком чеpез запятую. Kаждый идентификатоp в списке имен задает имя пеpеменной. Все опpеделяемые в объявлении пеpеменные имеют один тип. Пpимеp 1 int x; int const y=1; Пример 2 В этом примере объявляются две переменные с именами reply и flag. Обе переменные имеют тип unsigned long и содержат целые значения без знака. unsigned long reply, flag; Пример 3 В следующем примере объявляется переменная с именем order, которая имеет тип double и может содержать значения с плавающей точкой. double order; Объявления перечислимых типов Синтаксис: enum[признак]{список}[декларатор[,декларатор]...]; enum признак[идентификатор[,декларатор]...]; В "объявлении перечислимого типа" задается имя перечислимой переменной и определяется набор поименованных целых констант ("перечислимый набор"). Переменная перечислимого типа хранит одно из значений перечислимого набора, определенного этим типом. Целые константы перечислимого набора имеют тип int; т.о. объем памяти для размещения перечислимой переменной совпадает с тем, который выделяется отдельному значению int. Переменные типа enum во всех случаях обрабатываются так, как если бы они имели тип int. Они могут использоваться в выражениях индексов и как операнды во всех арифметических операторах и операторах отношений. Объявления перечислимого типа начинаются с ключевого слова enum и имеют две формы, которые показаны в начале данного раздела и рассматриваются ниже: ╥ В первой форме "список" задает имена и значения перечислимого набора. (Сам список будет рассмотрен позднее.) Необязательный элемент "признак" является идентификатором, который задает имя перечислимого типа, задаваемое списком. "Декларатор" задает имя перечислимой переменной. В одном объявлении перечислимого типа можно задать несколько перечислимых переменных, или не не задавать их совсем. ╥ Вторая форма объявления перечислимого типа использует ранее определенный перечислимый признак для ссылки на перечислимый тип, определенный в каком-то другом месте программы.Признак должен указывать на ранее определенный перечислимый тип и этот перечислимый тип должен быть доступен в данный момент. При таком типе объявление "список" в нем не присутствует, т.к. сам перечислимый тип определен в некотором другом месте. Определения указателей на перечислимый тип и определения typedef для перечислимых типов могут использовать перечислимый "признак" еще до определения перечислимого типа. Однако, само определение перечислимого типа должно быть сделано до какого-либо использования объявления typedef или указателя. Если появляется аргумент "признак", но не задан "декларатор", то объявление считается объявлением перечислимого признака. "Список" имеет следующий вид: идентификатор[=постоянное-выражение] [,идентификатор[=постоянное-выражение]...] Каждый "идентификатор" в перечислимом списке есть имя перечислимого набора. По умолчанию, первый идентификатор связывается со значением 0, следующий идентификатор связывается со значением 1, и т.д. до последнего идентификатора в объявлении. Имя перечислимой константы эквивалентно ее значению. Необязательное сочетание "=постоянное-выражение" отменяет установленную по умолчанию последовательность значений. Т.о. если "идентификатор=постоянное-выражение" появляется в списке, то указанный идентификатор связывается с заданным выражением. "Постоянное-выражение" должно иметь тип int и может быть отрицательным. Следующий идентификатор списка связывается со значением "постоянное-выражение"+1, если не связать его явно с другим значением. К компонентам списка применяются следующие правила: ╥ Перечислимый список может содержать дублирующиеся значения констант. Например, можно связать значение 0 с двумя различными идентификаторами с именами null и zero в одном наборе. ╥ Идентификатор перечислимого списка должен отличаться от других идентификаторов той же самой сферы действия, включая имена обычных переменных и идентификаторы других перечислимых списков. ╥ Признак перечислимого списка должен отличаться от признаков других перечислимых списков, структур и об'единений той же самой сферы действия. ╥ За последним элементом перечислимого списка допускается ставить запятую. Пример 1 В примере определяется перечислимый тип с именем day и объявляется переменная с именем workday перечислимого типа. Значение 0 связывается с saturday по умолчанию. Идентификатор sunday явно устанавливается на 0. Остальным идентификаторам по умолчанию присваиваются значения от 1 до 5. enum day { saturday, sunday=0, monday, tuesday wednesday, thursday, friday } worrday; Пример 2 В данном примере значение из определенного в Примере 1 набора присваивается переменной today. Обратите внимание на то, что для присваивания значений используются имена перечислимых констант. Достаточно просто использовать перечислимый признак, т.к. пеpечислимый тип day был опpеделен pанее. enum day today=wednesday; Объявления стpуктуp Синтаксис: struct [пpизнак]{список-объявлений-компонент} [деклаpатоp[,деклаpатоp]...]; struct пpизнак[деклаpатоp[,деклаpатоp]...]; "Объявление стpуктуpы" задает имя стpуктуpной пеpеменной и последовательность значений пеpеменной (называемых "компонентами" стpуктуpы), котоpые могут иметь pазличные типы. Пеpеменная этого типа стpуктуpы содеpжит опpеделенные этим типом последовательности. Объявления стpуктуp начинаются с ключевого слова struct и имеют две фоpмы: ╥ В пеpвой фоpме "список-компонентов-стpуктуpы" задает ти пы и имена компонентов стpуктуpы. Hеобязательный элемент "пpизнак" это идентификатоp, котоpый задает имя типа стpуктуpы, опpеделенное списком компонентов стpуктуpы. ╥ Втоpая фоpма использует пpизнак заpанее опpеделенной стpуктуpы для ссылки на тип стpуктуpы, котоpый был опpеделен в каком либо дpугом месте пpогpаммы. Т.о. если это опpеделение доступно, то нет необходимости в задании списка компонентов стpуктуpы. Объявления стpуктуpы и typedef для типов стpуктуp могут использовать пpизнак стpуктуpы еще до самого опpеделения типа стpуктуpы. Однако опpеделение стpуктуpы должно быть пpоизведено до какого-либо действительного ис пользования typedef. В обоих случаях "деклаpатоp" задает стpуктуpную пеpеменную. Kpоме того, деклаpатоp модифициpует тип пеpеменной на пойнтеp к типу стpуктуpы, массиву стpуктуp или функции, возвpащающей стpуктуpу. Если задан "пpизнак", а деклаpатоpа нет, то это объявление стpуктуpного пpизнака. Стpуктуpный пpизнак должен отличаться от пpизнаков дpугих стpуктуp, пеpечислимых типов той же самой сфеpы действия. "Список-объявлений-компонент" содеpжит объявления одной или нескольких пеpеменных или битовых полей. Kаждая объявленная пеpеменная из списка есть компонент стpук туpного типа. Объявления пеpеменных в списке компонент имеют ту же фоpму, что и дpугие объявления пеpеменных, pассмотpенные в этой Главе. Исключение состоит в том, что и дpугие объявления не могут содеpжать спецификатоpы класса хpанения или инициализатоpы. Kомпоненты стpуктуpы могут быть любого допустимого типа: фундаментальные, массивы или стpук туpы. Kомпонент не может быть объявлен таким обpазом, что он имеет тип стpуктуpы, в котоpой он появляется. Однако, компонент все же может быть объявлен как тип стpуктуpы, в котоpой он появляется, если тип стpуктуpы имеет пpизнак. Это позволяет создавать связанные списки стpуктуp. Объявление битового поля имеет следующий вид: спецификатоp-типа[идентификатоp]:постоянное-выpажение; "Постоянное-выpажение" задает число бит в поле. "Спецификатоp-типа" имеет тип int (signed или unsigned) и "постоянное-выpажение" должно быть неотpицательным целым выpажением. Hе допускается задание мас сивов битовых полей, пойнтеpов на битовые поля и функций, возвpаща ющих битовые поля. Битовые поля без имени могут использоваться как "пустые" для целей выpавнивания. Битовое поле без имени с шиpиной 0 гаpантиpует, что для компонента, котоpый следует за ним в списке объявления компонент, память выделяется начиная с гpаницы int. Kаждый идентификатоp в списке объявлений компонент должен быть уникальным для этого списка. Однако, он не должен обязательно отличаться от имен дpугих обычных пеpеменных или от идентификатоpов дpугого списка объявлений компонент. Память Kомпоненты стpуктуpы хpанятся соответственно поpядку, в котоpом они объявлялись: пеpвый компонент имеет наименьший адpес памяти, а последний компонент - наивысший. Память для каждого компо нента начинается на гpанице памяти, соответствующей его типу. Kpоме того, между компонентами стpуктуpы в памяти могут появляться непоименованные пpобелы. Битовые поля не хранятся на границах их объявленного типа. Например, битовое поле, объявленное типом unsigned int, упаковывается в оставшееся пространство (если оно есть), если предыдущее битовое поле имело тип unsigned int. В противном случае, оно начинает новый объект на границе int. Пример 1 В данном примере определяется структурная переменная с именем complex. Эта структура имеет две компоненты типа float, x и y. У типа структуры нет признака, поэтому она не имеет имени. struct { float x,y; } complex; Пример 2 В этом примере определяется структурная переменная с именем temp. Структура имеет три компоненты: name, id и class. Компонент name это массив из 20 элементов, а id и class это простые компоненты соответственно типа int и long. Идентификатор employee это признак структуры. struct employee { char name[20]; int id; long class; } temp; Пример 3 В данном примере определяются три структурные переменные: student, faculty и staff. Каждая структура имеет аналогичный список из трех компонент. Компоненты объявляются как структуры типа employee, определенного в Примере 2. struct employee student, faculty, staff; Пример 4 В данном примере определяется структурная переменная с именем x. Первые две компоненты структуры это переменная char и указатель на значение float. Третий компонент, next, объявляется как указатель на определяемый тип структуры (sample). struct sample { char c; float *pf; struct sample *next; } x; Пример 5 В этом примере определяется двумерный массив структур с именем screan. Массив содержит 2000 элементов. Каждый элемент это отдельная структура, состоящая из четырех компонентов - битовых полей: icon, color, underline и blink. struct { unsigned icon : 8; unsigned color : 4; unsigned underline : 1; unsigned blink : 1; } screen[25][80]; Объявления об'единений Синтаксис: union [признак]{список-объявлений-компонент} [декларатор[,декларатор.]..]; union признак[декларатор[,декларатор]...]; В "объявлении об'единения" задается имя переменной об'едине ния и указывается набор значений переменной, называемых "компо нентами" об'единения, которые могут иметь различные типы. Пере менная типа union хранит одно из значений, заданных этим типом. Объявление об'динения имеет ту же форму, что и объявление структуры, за исключением того, что оно начинается с ключевого слова union, а не struct. Правила объявления структур распространяются и на об'единения, за исключением того, что в об'единениях не допускается использование битовых полей в качест ве компонент. Память переменной об'единения выделяется такой размер памяти, который необходим для хранения максимального по размеру компонента об'единения. При хранении меньшего компонента переменная об'единения может содержать неиспользуемое пространство памяти. Все компоненты хранятся в одной и той же области памяти и начинаются по одному адресу. Хранимая величина каждый раз перезаписывается при присвоении значения другому компоненту. Пример 1 В данном примере определяется переменная типа sign и объявляется переменная с именем number, которая имеет две компоненты: целую со знаком svar, и целую без знака uvar. Данное объявление позволяет хранить текущее значение number в виде величины со знаком или без знака. sign это признак, который ассоциируется с данным типом об'единения. union sign { int svar; unsigned uvar; } number; Пример 2 В данном примере объявляется переменная jack. Компонентами об'единения являются в порядке своего объявления: указатель на значение char, величина char и массив значений float. Для jack выделяется память, которая требуется для для хранения 20-элементного массива f, т.к. f самый большой компонент об'единения. Тип об'единения не определен, т.к. с ним не связано никакого признака. union { char *a, b; float f[20]; } jack; Пример 3 В данном примере определяется двумерный массив об'единений с именем screen. Массив состоит из 2000 элементов. Каждый элемент массива это об'единение из двух компонент: window1 и screenval. Компонент window1 это структура с двумя компонентами битовых полей, icon и color. Компонент screenval имеет тип int. В любой заданный момент времени каждый элемент об'единения содержит либо int screenval либо представленную window1 структуру. union { struct { unsigned int icon : 8; unsigned color : 4; } window1; int screenval; } screen[25][80]; Объявления массивов Синтаксис: спецификатор-типа декларатор[пост-выражение]; спецификатор-типа декларатор[]; При объявлении массива задается его имя и тип его элементов. Кроме того, можно задать число элементов массива. Переменная типа массив рассматривается как указатель на тип элемента массива, как это описано в разделе "Идентификаторы". Объявления массивов имеют две формы, как это показано в начале данного раздела. Отличия в синтаксисе состоят в следующем: ╥ В первой форме аргумент "пост-выражение" в прямоугольных скобках задает число элементов массива. Каждый элемент имеет тип, который задается "спецификатором-типа". Тип может быть любым, кроме void. Массив элементов не может иметь тип функции. ╥ Во второй форме аргумент "пост-выражение" в прямоугольных скобках опущен. Эта форма может быть использована только при инициализации массива, объявлении его в качестве параметра или объявлении его, как ссылки на массив, который явно определен в каком-либо ином месте программы. В обоих случаях "декларатор" задает имя переменной и может модифицировать ее тип. Прямоугольные скобки ([]), которые следуют за декларатором, модифицируют его на тип массива. Можно объявить массив массивов ("многомерный" массив), если задать за декларатором массива список постоянных выражений, заключенных в прямоугольные скобки, как это показано: спецификатор-типа декларатор[пост-выражение][пост-выражение]...; Каждое "пост-выражение" в прямоугольных скобках задает число элементов в заданном измерении: двумерный массив имеет два выражения в прямоугольных скобках, трехмерный - три и т.д. При определении многомерного массива в функции можно опустить первое постоянное выражение, если массив был проинициализирован, объявлен в качестве формального параметра или объявлен как ссылка на массив, который был определен явно в каком-либо другом месте программы. Можно определять массивы указателей на самые различные типы объектов, используя составные деклараторы, как это описано в разделе "Составные деклараторы". Память Память, которая выделяется для хранения заданного типа массива совпадает по размеру с памятью, которая нужна для хранения всех его элементов. Элементы массива хранятся в непрерывной области памяти по возрастающим адресам от первого до последнего. Для разделения элементов массива в памяти пробел не используется. Массивы хранятся по строкам. Например, следующий массив состоит из двух строк по три колонки в каждой: cahr A[2][3]; Сначала записываются три колонки первой строки, затем три колонки второй строки. Это значит, что последний индекс изменяется более быстро. Для указания отдельного элемента массива используется выражение индекса, как это описано в разделе "Выражения индекса". Пример 1 В данном примере объявляется переменная массива с именем scores для 10 элементов, каждый из которых имеет тип int. Переменная с именем game объявляется как простая переменная типа int. int scores[10], game; Пример 2 В данном примере объявляется двумерный массив с именем matrix. Массив имеет 150 элементов, каждый из которых имеет тип float. float matrix[10][15]; Пример 3 В данном примере объявляется массив структур. В этом массиве 100 элементов; каждый из элементов это структура из двух компонент. struct { float x,y; } complex[100]; Пример 4 В данном примере объявляется тип и имя массива указателей на char. Действительное определение name содержится в каком-либо другом месте программы. extern char *name[]; Объявления указателей Синтаксис: спецификатор-типа *[модифицирующий-спецификатор]декларатор; При объявлении указателя указывается имя переменной указателя и задается тип объекта, на который эта переменная указывает. Объявленная в качестве указателя переменная хранит адрес памяти. "Спецификатор-типа" задает тип объекта, который может быть фундаментальным, структурным или об'единения. Переменные указателя могут указывать на функции, массивы и другие указатели. (Информация по объявлению более сложных типов указателей содержится в разделе "Сложные объявления".) Задав в качестве спецификатора типа void можно на некоторое время отложить спецификацию типа, на который указывает указатель. Такой элемент называют "указателем на void" (void *). Если переменная объявлена как указатель на void, то ее можно использовать для указания на объект любого типа. Однако, для выполнения операций над указателем или над объектом, на который он указывает, должен быть задан тип объекта для каждой операции над ним. Такое преобразование может быть выполнено приведением типов. "Модифицирующий-спецификатор" может быть в виде const или volatile, либо они оба сразу. Этим задается соответственно, что указатель не будет модифицироваться самой программой (const), или что указатель может быть модифицирован некоторым процессом, который протекает независимо от программы (volatile). (Дополнительную информацию по const и volatile можно найти в разделе "Спецификаторы типа".) "Декларатор" задает имя переменной и может включать модификатор типа. Например, если декларатор представляет массив, то тип указателя модифицируется на указатель к массиву. Можно объявить указатель на структуру, об'единение или перечислимый тип до определения структуры, об'единения или перечислимого типа. Однако, определение должно появиться до использования указателя в качестве операнда в выражении. Пойнтер объявляется с использованием признака структуры или об'единения (см. Пример 7 в данном Разделе). Такие объявления допустимы благодаря тому, что компилятору не нужно знать размер структуры или об'единения для выделения памяти для переменной указателя. Память Размер памяти, который требуется для адреса и значение адреса зависят от реализации компьютера. Не гарантируется, что указатели на разные типы имеют ту же самую длину. Пример 1 В данном примере объявляется переменная указатель с именем message. Она указывает на переменную типа char. char *massage; Пример 2 В данном примере объявляется массив указателей с именем pointers. Массив состоит из 10 элементов, каждый элемент это указатель на переменную типа int. int *pointers[10]; Пример 3 В данном примере объявляется переменная указатель с именем pointer. Он указывает на массив из 10 элементов. Каждый элемент этого массива имеет тип int. int (*pointer)[10]; Пример 4 В данном примере объявляется переменная указатель, x, на постоянную величину. Пойнтер может быть изменен для указания на другое значение int, но само значение, на которое он указывает, не может быть изменено. int const *x; Пример 5 Переменная y в Примере 5 объявляется как постоянный указатель на значение int. Значение, на которое он указывает, может быть изменено, но сам указатель должен всегда указывать на то же самое место памяти: адрес fixed_object. Аналогично, z это постоянный указатель, но кроме того, он объявлен для указания на значение int, которое не может быть изменено в программе. Дополнительный спецификатор volatile указывает, что хотя значение const int, на которое указывает z и не может быть изменено в программе, оно может быть изменено некоторым процессом вне программы. Объявлением w задается указываемое значение, которое не будет изменено и сам указатель не будет изменен программой. Однако, некоторый внешний процесс может изменить указатель. const int some_object = 5 ; int other_object = 37; int *const y = &fixed_object; const volatile *const z = &some_object; *const volatile w = &some_object; Пример 6 В данном примере объявляются две переменные указателя, которые указывают на структуру типа list. Это объявление может появиться до определения структуры типа list (см. Пример 7), поскольку определение типа list имеет ту же сферу действия, что и объявление. struct list *next, *previous; Пример 7 В данном примере определяется переменная line, которая имеет тип структуры с именем list. Структура типа list имеет три компоненты: первый компонент это указатель на значение char, второй - это значение int, а третий - это указатель на другую структуру list. struct list { char *token; int count; struct list *next; } line; Пример 8 В данном примере объявляется переменная record, которая имеет тип структуры id. Обратите внимание на то, что pname объявлен как указатель на другой тип структуры с именем name. Это объявление может появиться до определения типа name. struct id { unsigned int id_no; struct name *pname; } record; Пример 9 В данном примере объявляется переменная указатель p, но в объявлении идентификатору p предшествует void *, что означает, что p может позднее быть использован для указания на любой тип объекта. Адрес значения int присваивается p, но над самим указателем нельзя проводить никакие операции до тех пор, пока он не будет явно конвертирован в тип, на который он указывает. Аналогично, недопустимы косвенные операции над объектами, на которые указывает p, пока p не будет явно не будет конвертирован в конкретный тип. И, наконец, использовано приведение типов для преобразования p в указатель на int, и значение p затем увеличивается. int i; /* p определяется как указатель на объект, void *p; тип которого не задан */ p = &1; /* адрес целого i присваивается в p, но сам тип p еще не задан. Операции, подобные p++ все еще нельзя выполнить */ (int *)p++; /* увеличение значения p происходит после преобразования его типа к указателю на int */ Объявления функций (Прототипы) Синтаксис: [спецификатор-класса-хранения] [спецификатор-типа]декларатор ([список-формальных-параметров]) [,список-деклараторов]...; "Объявление функции", часто называемое "прототипом функции", задает имя и возвращаемый тип функции, и может задавать типы и имена формальных параметров и число аргументов функции. Объявление функции не определяет тела функции. Оно просто сообщает некоторую информацию о функции компилятору. Это позволяет компилятору проверить типы действительных аргументов, задаваемые в вызове функции. Если не задать прототип функции, то компилятор сконструирует его из первой обнаруженной ссылки на функцию, есть ли это вызов функции или ее определение. этот прототип будет отражать корректные типы параметров только в том случае, когда определение функции присутствует в том же исходном файле. Если определение происходит в другом модуле, то ошибки несовпадения аргументов могут не быть обнаружены. Детальное описание определений функций содержится в разделе "Прототипы функций (Объявления)". "Спецификатор-класса-хранения" может быть задан либо extern либо static. Спецификаторы класса хранения рассматриваются в разделе "Классы хранения". "Спецификатор-типа" задает возвращаемый функцией тип, а "декларатор" задает имя функции. Если опустить спецификатор типа в объявлении функции, то предполагается, что функция возвращает значение типа int. "Список-формальных-параметров" рассматривается в следующем разделе. Финальный "список-деклараторов" в строке синтаксиса соответствует дополнительным объявлениям, сделанным на той же строке. Это могут быть другие функции, возвращающие значения того же типа, что и первая функция, или объявления некоторых переменных, тип которых совпадает с типом, возвращаемым первой функцией. Каждое такое объявление отделяется от предшествующих и последующих запятой. Формальные параметры "Формальные параметры" описывают действительные аргументы, которые могут быть переданы функции. В объявлении функции объявления параметров задают число и тип действительных аргументов. Кроме того, они могут включать идентификаторы для формальных параметров. Хотя параметры и можно опустить в определении функции, все же рекомендуется их указывать, а в действительном прототипе они вообще обязательны. Увеличение количества информации в объявлении вызывает проведение проверки аргументов функции при ее вызове, которое происходит до обработки компилятором определения функции. Примечание Идентификаторы, которые используются для задания имен формальных параметров в объявлении прототипа, носят лишь описательный характер. Сфера их действия прекращается в конце объявления. Т.о. они не обязательно должны совпадать с идентификаторами, которые используются в разделе объявления определения функции. Использование тех же самых имен улучшает восприятие текста программы, но не имеет какого-либо иного значения. Возвращаемый тип Функции могут возвращать значения любого типа, кроме массивов и функций. Т.о. аргумент "спецификатор-типа" в объявлении функции может задавать любой фундаментальный, структурный или тип об'единения. Можно модифицировать идентификатор функции одной или несколькими звездочками (*) для объявления возвращаемого типа указателя. Хотя функции и не могут возвращать массивы и функции, они могут возвращать указатели на массивы и функции. Можно объявить функцию, которая возвращает указатель на тип массива или функции, модифицировав идентификатор функции звездочкой (*), прямоугольными ([]) или круглыми (()) скобками. Такой идентификатор функции известен, как "сложный декларатор". Правила формирования и интерпретации сложных деклараторов описаны в разделе "Сложные деклараторы". Список формальных параметров Все элементы аргумента "списка-формальных-параметров", которые появляются в скобках за декларатором функции, являются необязательными. Имеются два варианта синтаксиса: [void] [register][спецификатор-типа][декларатор[[,...][,...]]] Если в определении функции опущены формальные параметры, то в скобках должно быть указано ключевое слово void, которое и указывает, что никакие аргументы в функцию не передаются. Если в скобках ничего не указано, то нет никакой информации относительно того, будут ли переданы аргументы в функцию и не проводится никакой проверки типов аргументов. Примечание Пустые скобки в объявлении функции или ее определении не рекомендуются для нового текста программы. Функции без аргументов должны быть объявлены с ключевым словом void, заменяющим список формальных параметров. Такое использование void интерпретируется по контексту и отличается от использования void в качестве спецификатора типа. Объявление списка формальных параметров может содержать спецификатор класса хранения register, отдельно или вместе со спецификатором типа и идентификатором. Если register не задан, то устанавливается класс хранения auto. Единственным явно допустимым классом хранения является register. Если в скобках содержится только ключевое слово register, то считается, что представлен формальный параметр типа int без имени, которому выделен класс хранения register. Если присутствует спецификатор типа, то он может быть именем типа любого фундаментального, структурного или типа об'единения (например, int). "Декларатор" для фундаментального, структурного или типа об'единения просто идентификатор переменной, которая имеет этот тип. Декларатор для указателя, массива или функции может быть сформирован комбинированием спецификатора типа и соответствующего модификатора с идентификатором. В качестве альтернативы может быть использован "абстрактный декларатор" (декларатор без идентификатора). В разделе "Имена типов" объясняется, как формировать и интерпретировать абстрактные деклараторы. Может быть объявлен полный, частичный или пустой список формальных параметров. Если список содержит по крайней мере один декларатор, переменное число параметров может быть задано, если закончить список запятой, за которой следуют три точки (,...). Ожидается, что функция имеет по крайней мере столько аргументов, сколько деклараторов или спецификаторов типа предшествует последней запятой. Допускается использование еще одной специальной конструкции в качестве формального параметра: void* представляет собой указатель на объект незаданного типа. Т.о. в вызове функции указатель может указывать на любой тип объекта после его преобразования (например, приведением типов) к указателю на необходимый тип. Обратите внимание на то, что до проведения операций над указателем или объектом по этому адресу, указатель должен быть явно преобразован. Дополнительная информация по void* содержится в разделе "Объявления указателей". Краткий обзор Задание прототипов необязательно, но крайне желательно. Если оно присутствует, то единственно абсолютно необходимыми элементами являются имя функции, открывающая и закрывающая скобка и точка с запятой. Если не задается возвращаемый тип, как в следующем примере, то предполагается, что функция возвращает тип int: /*** Абсолютная форма объявления функции ***/ minimal_declaration(); /* может иметь или не иметь аргументов */ Полный прототип функции это то же самое, что определение функции, кроме того, что вместо задания собственного тела функции, он заканчивается точкой с запятой (;), сразу же после закрывающей скобки. При объявлении параметров допустима любая комбинация элементов, от полного отсутствия информации (как в приведенном выше примере абсолютной формы) до полного прототипа функции. Если прототип вообще не задается, то прототип будет построен по информации из первой ссылки на функцию, обнаруженной в исходном файле. Пример 1 В этом примере любая включенная в список формальных параметров информация используется для проверки действительных аргументов, которые появляются в вызове функции, который происходит до обработки компилятором определения функции. double func(void); /* возвращает значение double, но * не имеет аргументов */ fun (void*); /* принимает указатель на * незаданный тип, возвращает int */ char *true(long, long); /* принимает два значения long и * возвращает указатель на char */ * указатель на char, а возвращает * int */ new (register a, char *); /* принимает int с запросом на * память register и указатель на * char, а возвращает int */ void go(int *[], char *b); /* принимает массив указателей на * int, используя абстрактный * декларатор и указатель на char; * не возвращает значения */ void *tu(double v,...); /* принимает по крайней мере одно * значение double; могут быть * заданы и другие аргументы; * возвращает указатель на * незаданный тип */ Пример 2 Данный пример это прототип функции с именем add, которая принимает два аргумента int, представленные идентификаторами num1 и num2, и возвращает значение int. int add(int num1, int num2); Пример 3 В этом примере объявляется функция с именем calc, которая возвращает значение double. Простые скобки указывают на неопределенные аргументы функции. double calc(); Пример 4 Это пример прототипа функции с именем strfind, которая возвращает указатель на char. Функция принимает по крайней мере один аргумент, объявленный формальным параметром char *ptr, как указатель на значение char. Список формальных параметров имеет только один элемент и заканчивается запятой, за которой следует три точки, что указывает на то, что у функции могут быть дополнительные аргументы. char *strfind(char *ptr,...); Пример 5 В этом примере объявляется функция с возвращаемым типом void (нет возвращаемого значения). Ключевое слово void замещает список формальных параметров, следовательно, у этой функции нет аргументов. void draw(void); Пример 6 В этом примере объявляется функция sum, которая возвращает указатель на массив из трех значений double. Функция sum принимает в качестве аргументов два значения double. double (*sum(double, double))[3]; Пример 7 В этом примере объявляется функция select, у которой нет аргументов, но которая возвращает указатель на функцию. Возвращаемое значение указателя указывает на функцию, которая принимает один аргумент int, представленный идентификатором number, и возвращает величину int. int (*select(void))(int number); Пример 8 В данном примере объявляется функция prt, которая принимает в качестве аргумента указатель на любой тип и возвращает значение int. Эта операция происходит без появления предупреждающего сообщения о несовпадении типов. int prt(void *); Пример 9 В данном примере показано объявление массива с именем rainbow из незаданного числа постоянных указателей на функции. Каждая из них принимает по крайней мере один параметр типа int, наряду с незаданным числом других параметров. Каждая из указанных функций возвращает значение long. long (*const rainbow[]) (int, ...) ; 4.6 Классы хранения "Классы хранения" переменных определяют, имеет ли данный элемент "локальное" или "глобальное" действие. Переменным с локальным временем действия выделяется новая область памяти каждый раз, когда управление выполнением программы переходит к блоку, в котором они определены. При выходе из этого блока значения переменных теряют смысл. Элемент с глобальным временем действия существует и имеет значение на всем времени выполнения программы. Все функции имеют глобальное время действия. Хотя в языке С определены только два типа классов хранения, имеются следующие четыре спецификатора классов хранения: Таблица 4.5. Спецификаторы классов хранения Элемент, который определен с Имеет auto локальное время действия register локальное время действия static глобальное время действия extern глобальное время действия Четыре спецификатора класса хранения имеют разное предназначение, т.к. они влияют на сферу действия функций и переменных, наряду с их классами хранения. Термин "сфера действия" относится к той части программы, в которой переменная или функция может быть использована по ее имени. Элемент с глобальным временем действия существует на всем протяжении выполнения исходной программы, но он может и не быть доступен из всех частей программы. (Сфера действия и связанная с ней концепция времени действия рассматриваются в Главе "Структура программы".) Местоположение объявлений функции и переменной в исходном файле также влияет на класс хранения и сферу действия. Объявления вне определений всех функций происходят на "внешнем уровне", а объявление внутри определения функции происходит на "внутреннем уровне". Точные значения каждого из спецификаторов класса хранения зависят от двух факторов: ╥ Происходит объявление на внешнем или внутреннем уровне; ╥ Является объявляемый элемент переменной или функцией. Последующие разделы рассматривают значение спецификаторов класса хранения при каждом случая объявления и объясняют назначения по умолчанию, когда спецификатор класса хранения опущен в объявлении переменной или функции. Объявления переменных на глобальном уровне При определении переменной на глобальном уровне (т.е. вне всех функций) можно использовать спецификатор класса хранения static или extern, или вообще не задавать спецификатора класса хранения. На глобальном уровне нельзя использовать спецификаторы класса хранения auto и register. Объявления переменных на глобальном уровне являются либо определениями переменных ("определяющие объявления") либо ссылками на переменные, которые определены в каком-либо ином месте программы ("ссылочные объявления"). Глобальное объявление переменной, в котором осуществляется ее инициализация (явно или неявно) является ее определяющим объявлением. Определение на глобальном уровне имеет несколько форм: ╥ Переменная объявляется со спецификатором класса хранения static. Можно явно инициализировать переменную static с помощью постоянного выражения, как это описано в разделе "Инициализация". Если не сделать инициализацию, то по умолчанию переменная будет проинициализирована значением 0. Например, static int k=16; и static int k; рассматриваются, как определения переменной k. ╥ Можно явно инициализировать переменную на глобальном уровне. Например, int j=3; есть определение переменной j. Если переменная объявлена на глобальном уровне, то она имеет сферой действия всю оставшуюся часть исходного файла, в котором она появилась. В этом исходном файле переменная недоступна до ее определения. Кроме того, она недоступна в других исходных файлах программы, если не открыть к ней доступ с помощью ссылочного объявления, как это будет рассмотрено в данном разделе. Можно определить переменную на глобальном уровне в исходном файле только один раз. Если задать спецификатор класса хранения static, то можно определить другую переменную под тем же именем со спецификатором класса хранения static в другом исходном файле. Определение static имеет сферой действия только собственный исходный файл, поэтому конфликта не будет. Спецификатор класса хранения extern объявляет ссылку на переменную, которая определена в каком-либо другом месте. Можно использовать объявление extern для получения доступа к определению, сделанному в другом исходном файле, или для того, чтобы сделать переменную доступной до ее определения в том же исходном файле. После объявления ссылки на переменную на глобальном уровне переменная будет доступна на всей оставшейся части исходного файла, в котором объявлена эта ссылка. Объявления со спецификатором класса хранения extern не могут содержать инициализаторов, т.к. они ссылаются на переменную, значение которой определено в другом месте. Для того, чтобы ссылка extern имела смысл, переменная, на которую делается ссылка, должна быть определена только один раз на глобальном уровне. Объявление может быть сделано в любом из исходных файлов, из которых формируется программа. В сформулированных выше правилах не рассматривается один специальный случай. В объявлении переменной на глобальном уровне можно опустить и спецификатор класса хранения и инициализатор. Например, int n; является корректным глобальным объявлением. Это объявление в зависимости от контекста может иметь два различных значения: 1. Если в каком-либо другом месте программы уже есть глобальное определяющее объявление переменной с этим именем, то данное объявление рассматривается как ссылка на эту переменную. Т.е. как будто Мы использовали в объявлении спецификатор класса хранения extern. 2. Если нигде в другом месте программы нет глобального определяющего объявления переменной с этим именем, то в процессе компоновки программы переменной выделяется память и она инициализируется значением 0. Этот вид переменной называется "коммунальным". Если в программе появляется несколько таких объявлений, то память выделяется для максимального размера, объявленного для переменной. Например, если программа содержит два объявления i без инициализации на глобальном уровне, int i; и char i; , то в момент компоновки программы для i будет выделена память, как для значения int. Не рекомендуется использовать объявления переменных без инициализации на глобальном уровне для файлов, которые могут быть помещены в библиотеку. Пример В данном примере два исходных файла содержат три глобальных объявления i. Только в одном из них есть инициализация. Это объявление, int i=3; , определяет глобальную переменную i с начальным значением 3. externобъявление i в начале первого исходного файла делает глобальную переменную доступной до ее определения в файле. Без объявления extern в функции main нельзя было бы сделать ссылку на глобальную переменную i. Объявление extern переменной i во втором исходном фале также делает эту переменную доступной в этом исходном файле. Предположим, что функция printf определена где-либо в другом месте программы. Все три функции выполняют одну и ту же задачу: увеличивают значение i печатают его. Будут напечатаны значения 4, 5 и 6. Если бы переменная i не была инициализирована, то в процессе компоновки ей было бы автоматически присвоено значение 0. Тогда были бы напечатаны значения 1, 2 и 3. Первый исходный файл: extern int i; /* ссылка на i, определенную ниже */ main() { i++; printf("%d\n", i); /* i равно 4 */ next(); } int i=3; /* определение i */ next() { i++; printf("%d\n", i); /* i равно 5 */ other(); } Второй исходный файл: extern int i; /* ссылка на i, в первом исходном файле */ other() { i++; printf("%d\n", i); /* i равно 6 */ } Объявления переменных на локальном уровне Для объявления переменной на локальном уровне можно использовать любой из четырех спецификаторов класса хранения. Если в таком объявлении не задать спецификатор класса хранения, то по умолчанию он будет установлен auto. Спецификатор класса хранения объявляет переменную с локальным временем действия. Такая переменная доступна только в том блоке, в котором она объявлена. Объявления переменных auto могут содержать инициализаторы, как это описано в разделе "Инициализация". Переменные с классом хранения auto не инициализируются автоматически, поэтому нужно либо явно инициализировать их при объявлении, либо присвоить им значения в операторах блока. Значения неинициализированных переменных auto не определены. Переменная static auto может быть инициализирована адресом любого глобального или static элемента, но никогда адресом другого элемента auto, т.к. адрес элемента auto не является константой. Спецификатор класса хранения register дает указание компилятору выделить, если это возможно, регистр для хранения переменной. Хранение данных в регистре ускоряет процесс доступа к ним и сокращает размер программы. Объявленные с классом хранения register переменные имеют ту же сферу действия, что и переменные auto. Число регистров, в которых можно хранить переменные, зависит от конкретного компьютера. Если нет свободных регистров в тот момент, когда компилятор встречает объявление register, то переменной присваивается класс хранения auto и она записывается в память. Компилятор выделяет регистры для хранения переменных в той последовательности, в которой и объявления появляются в исходном файле. В отличие от переменных auto, переменные static продолжают сохранять свои значения и после выхода управления программы из блока. Можно инициализировать переменную static постоянным выражением. Переменная static инициализируется только один раз, в начале выполнения программы, ее повторная инициализация не проводится каждый раз при входе управления программы в блок. Если не инициализировать переменную static явно, то по умолчанию она инициализируется значением 0. Объявленные со спецификатором класса хранения extern переменные являются ссылками на переменные с теми же именами, которые были объявлены на глобальном уровне в любом из исходных файлов программы. Локальное объявление extern используется для получения доступа из блока к определению переменной на глобальном уровне. Если объявления переменной на глобальном уровне не сделано, то объявленная с ключевым словом extern переменная имеет сферой своего действия только тот блок, в котором она была объявлена. Пример В данном примере на глобальном уровне определяется переменная i с начальным значением 1. Объявления extern в функции main используется для объявления ссылки на глобальный уровень i. static переменная a инициализируется значением 0 по умолчанию, т.к. инициализатор отсутствует. При вызове printf (предполагается, что функция prinf определена в некотором другом месте исходной программы) печатаются значения 1, 0, 0 и 0. В функции other адрес глобальной переменной i используется для инициализации static переменной указателя external_i. Эта схема работает, т.к. глобальная переменная имеет время действия static, что означает неизменность ее адреса. Далее переменная i переопределяется как локальная переменная с начальным значением 16. Это переопределение i не оказывает какого-либо влияния на i глобального уровня, которое скрыто от использования по имени локальной переменной. К значению глобального i можно получить теперь только косвенный доступ из блока через указатель external_i. Попытка присвоить адрес auto переменной i не будет иметь успеха, т.к. он может быть разным при каждом обращении к блоку. Переменная a объявляется как static и инициализируется значением 2. Это a не конфликтует с a в main, т.к. переменные static на локальном уровне доступны только в том блоке, где они объявлены. Переменная a увеличивается на 2, давая в результате 4. Если в этой программе будет снова вызвана функция other, то начальное значение a будет 4, т.к. локальные переменные static сохраняют свои значения при выходе и повторном входе программы в блок, где они были объявлены. int i=1; main() { /* ссылка на i, определенную выше */ extern int i; /* начальное значение 0, а доступно только в main */ static int a; /* b хранится в регистре, если это возможно */ register int b=0; /* класс хранения по умолчанию auto */ int c=0; /* будут напечатаны значения 1, 0, 0, 0 */ printf("%d\n%d\n%d\n%d\n", i, a, b, c); } other() { /* переменной указателю присваивается адреc глобального i */ static int *external_i=&i; /* переопределение i, глобальное i более недоступно */ int i=16; /* это a доступно только в этой функции */ static int a=2; a+=2 /* будут напечатаны значения 16, 4 и 1 */ printf("%d\n%d\n%d\n", i, a, *external_i); } Объявления функций на глобальном и локальном уровне В объявлениях функций можно использовать спецификатор класса хранения static либо extern. Функции всегда имеют глобальное время действия. Правила, которые регулируют сферу действия функций очень незначительно отличаются от аналогичных правил для переменных. Они состоят в следующем: ╥ Если функция объявлена static, то она доступна только в том исходном файле, где она объявлена. Функции этого исходного файла могут вызывать функцию static, а функции других исходных файлов - нет. Можно объявить другую функцию static под тем же именем в другом исходном файле, и никакого конфликта при этом не возникнет. ╥ Если функция объявлена extern, то она доступна для всех исходных файлов, которые формируют программу (если она позднее не переопределена как static). Любая функция может вызывать функцию extern. ╥ Если в объявлении функции не указан спецификатор класса хранения, то по умолчанию она считается extern. Инициализация Синтаксис: =инициализатор Используя инициализатор в объявлении функции можно присвоить переменной начальное значение. Переменной при этом будет присвоено значение инициализатора. Инициализатору предшествует знак равенства (=). Можно инициализировать переменные любых типов, если придерживаться следующих правил: ╥ Нельзя использовать инициализаторы в объявлениях, которые используют спецификатор класса хранения extern. ╥ Можно инициализировать переменные, объявленные на глобальном уровне. Если на глобальном уровне не инициализировать переменную явно, то по умолчанию она будет инициализирована значением 0. ╥ Для инициализации любой переменной, объявленной со спецификатором класса хранения static, можно использовать постоянное выражение. Переменные static инициализируются при начале выполнения программы. Если не инициализировать переменную static явно, то по умолчанию она будет инициализирована значением 0. ╥ Переменные, которые объявлены со спецификатором класса хранения auto или register, инициализируются каждый раз: когда управление выполнением программы передается на блок, в котором они были объявлены. Если не задать инициализатор при объявлении переменной auto или register, то начальное значение переменной будет не задано. ╥ Составные типы с классом хранения auto (массивы, структуры и об'едеинения) не могут быть инициализированы. Могут быть инициализированы только составные типы static, объявленные на глобальном уровне. ╥ Начальные значения в объявлениях глобальных переменных и для всех переменных static (глобальных и локальных) могут быть только постоянными выражениями. Для инициализации переменных auto и register можно использовать константы или значения переменных. В последующих разделах показано, как инициализировать переменные фундаментальных типов, указатели и составные типы. Фундаментальные типы и типы указателей Синтаксис: =выражение Переменной присваивается значение "выражения". Применяются правила преобразования для присвоений. Локально объявленная переменная static может быть инициализирована только постоянным выражением. Т.к. адрес любой глобально объявленной или static переменной остается неизменным, его можно использовать для инициализации объявленного static указателя. Однако, адрес переменой auto не может быть использован как инициализатор, т.к. он может быть разным при каждом выполнении блока. Пример 1 В данном примере x инициализируется постоянным выражением 10. int x=10; Пример 2 В данном примере указатель px инициализируется значением 0, давая в результате "пустой" указатель. register int *px=0; Пример 3 Данный пример использует постоянное выражение для инициализации c постоянным значением, которое не может быть изменено. const int c=(3*1024); Пример 4 В данном примере указатель b инициализируется адресом другой переменной, x. Пойнтер a инициализируется адресом переменной с именем z. Однако, т.к. ожидается, что значение переменной a будет const, ее можно только инициализировать, но не изменять. Она всегда будет указывать на конкретное место памяти. int *b=&x; int *const a=&z; Пример 5 В Примере 5 на глобальном уровне объявляется глобальная переменная GLOBAL, поэтому она будет иметь глобальное время действия. локальная переменная LOCAL имеет класс хранения auto и имеет адрес только при выполнении функции, в которой она объявлена. Попытка инициализации static указателя lp адресом LOCAL не допускается. static указатель gp может быть инициализирован адресом GLOBAL, т.к. он остается неизменным. Аналогично, *rp может быть инициализирован, т.к. rp это локальная переменная и поэтому может иметь непостоянный инициализатор. Каждый раз при входе в блок, LOCAL будет иметь новый адрес, который будет присвоен rp. int GLOBAL; int function(void) { int LOCAL; static int *lp=&LOCAL; /* некорректное объявление */ static int *gp=&GLOBAL; /* корректное объявление */ register int *rp=&LOCAL; /* корректное объявление */ Составные типы Синтаксис: ={список-инициализаторов} Инициализаторы в "списке-инициализаторов" отделяются друг от друга запятой. Каждый инициализатор списка это либо постоянное выражение либо список инициализаторов. Т.о. заключенный в скобки список инициализаторов может появиться в другом списке инициализаторов. Эта форма удобна для инициализации составных компонент составных типов, как это будет показано в данном разделе. Значения постоянных выражений списка инициализаторов присваиваются соответствующим компонентам составных переменных. При инициализации об'единения список инициализаторов должен быть простым постоянным выражением. Значение постоянного выражения при этом присваивается первому компоненту об'единения. Если список инициализаторов содержит меньше значений, чем составной тип, то остальные компоненты или элементы составного типа инициализируются значением 0. Если в списке инициализаторов больше значений, чем в составном типе, то появляется сообщение об ошибке. Эти правила применимы ко всем вложенным спискам инициализаторов, также как и ко всем составным типам в целом. В следующем примере объявляется массив P размерностью 3 на 4 и элементы его первой строки инициализируются значением 1, второй - 2, третьей - 3 и четвертой - 4: int P[4][3]={ {1, 1, 1 }, {2, 2, 2 }, {3, 3, 3,}, {4, 4, 4,}, }; Обратите внимание на то, что список инициализаторов для третьей и четвертой строки содержит запятую после последнего постоянного выражения. За последним инициализатором ({4, 4, 4,}) также следует запятая. Наличие таких дополнительных запятых допускается, но они не являются обязательными. Обязательными являются только запятые, разделяющие постоянные выражения между собой и один инициализатор от другого. Если для составного компонента нет встроенного списка инициализаторов, то значения просто присваиваются в порядке следования компонент. Т.о. инициализацию предыдущего примера можно было сделать следующим образом: int P[4][3]={ 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4 }; Отдельные инициализаторы в списке также могут заключаться в скобки. При инициализации составной переменной нужно очень аккуратно использовать скобки и список инициализаторов. Следующий пример более подробно показывает интерпретацию компилятором скобок: typedef struct { int n1, n2, n3; } triplet; triplet nlist[2][3] = { {{ 1, 2, 3}, { 4, 5, 6}, { 7, 8, 9}}, /* строка 1 */ {{10,11,12}, {13,14,15}, {16,17,18}}, /* строка 2 */ }; В этом примере объявляется массив структур с именем nlist размерности 2 на 3, каждая структура при этом имеет три компоненты. При инициализации первая строка присваивает значения первой строке nlist следующим образом: 1. Первая открывающая скобка строки 1 сигнализирует компилятору, что идет инициализация первой составной компоненты nlist (т.е. nlist[0]). 2. Вторая открывающая скобка сигнализирует компилятору, что идет инициализация первой составной компоненты nlist[0] (т.е. структуры nlist[0][0]). 3. Первая закрывающая скобка заканчивает инициализацию структуры структуры nlist[0][0]; следующая открывающая скобка начинает инициализацию nlist[0][1]. 4. Процесс продолжается до конца строки, где закрывающая скобка оканчивает инициализацию nlist[0]. Строка 2 присваивает значения второй строке nlist аналогичным образом. Обратите внимание на то, что требуется внешние скобки, которые заключают в себя инициализаторы строк 1 и 2. Следующая конструкция без внешних скобок вызовет появление ошибки: triplet nlist[2][3] = { { 1, 2, 3}, { 4, 5, 6}, { 7, 8, 9}, /* строка 1 */ {10,11,12}, {13,14,15}, {16,17,18} /* строка 2 */ }; В данной конструкции первая открывающая скобка строки 1 инициализирует nlist[0], который представляет собой массив из трех структур. Значения 1, 2 и 3 присваиваются трем компонентам первой структуры. Инициализация nlist[0] заканчивается после закрывающей скобки (которая стоит после значения 3) и две оставшиеся структуры массива из трех структур автоматически инициализируются значением 0. Аналогично, {4,5,6} инициализирует первую структуру из второй строки nlist. Оставшиеся две структуры nlist[1] устанавливаются равными 0. Когда компилятор обнаружит третий список инициализаторов ({7,8,9}), он попытается провести инициализацию nlist[2]. Но nlist имеет только две строки, поэтому появится сообщение об ошибке. Пример 1 В данном примере три int компоненты x инициализируются соответственно значениями 1, 2 и 3. Три элемента первой строки m инициализируются значением 4.0; элементы остальных строк m инициализируются значением 0 по умолчанию. struct list { int i,j,k; float m[2][3]; } x={ 1, 2, 3, {4.0, 4.0, 4.0} }; Пример 2 В данном примере инициализируется переменная y. Первый элемент об'единения это массив, поэтому инициализатор для него будет составным. Список инициализаторов {'1'} присваивает значения первой строке массива. Т.к. в списке указано только одно значение, то значением 1 будет инициализирован только элемент первой колонки, а остальные два элемента этой строки инициализируются значением ноль по умолчанию. Аналогично, первый элемент второй строки x будет инициализирован значением 4, а оставшиеся два элемента строки инициализируются значением 0. union { char x[2][3]; int i,j,k; } y = { { {'1'}, {'4'} } }; Инициализаторы строк Синтаксис: ="символы" Можно инициализировать массив символов литеральной строкой. В следующем примере инициализируется массив из четырех символьных элементов с именем code. Четвертый элемент это пустой символ, который ограничивает все стоковые литералы. char code[ ] = "abc"; Если строка будет длиннее, чем заданный размер массива, то дополнительные символы будут просто игнорироваться. Например, в следующем объявлении массив code инициализируется, как символьный массив из трех элементов: char code[3] = "abcd"; Только первые три символа инициализатора записываются в code. Символ d и заканчивающий строку пустой символ отбрасываются. В результате получается незаконченная строка (т.е. без значения 0 в конце) и появляется диагностическое сообщение, указывающее на возникновение этой ситуации. Если строка короче заданного размера массива, оставшиеся элементы массива инициализируются значениями 0. Объявления типов В объявлении типов задаются имена компоненты структур или об'единений, или имена и перечислимые наборы для перечислимых типов. Можно использовать имя объявленного типа в объявлениях функций или переменных для ссылки на этот тип. Это удобно в том случае, когда несколько переменных или функций имеют один и тот же тип. Спецификатор типа задается объявлением typedef. Объявления typedef можно использовать для построения более коротких или более осмысленных имен для уже определенных в С типов или типов, объявленных пользователем. Структуры, об'единения и перечислимые типы Объявления структур, об'единений и перечислимых типов имеют ту же общую форму, что и объявления переменных этих типов. Однако есть ряд отличий между объявлениями переменных и типов: ╥ При объявлении типа не задается идентификатор переменной, т.к. объявления переменной не происходит. ╥ В объявлении типа требуется задание признака. Он задает имя для структуры, об'единения или перечислимого типа. ╥ В объявлении типа нужно обязательно задавать список объявлений компонент либо перечислимый список. В объявлении типа не допускается использование сокращенных форм объявлений переменных, при котором признак ссылается тип, объявленный в некотором другом месте. Пример 1 В данном примере объявляется перечислимый тип с именем status. Имя этого типа может быть использовано в объявлениях перечислимых переменных. Идентификатор loss явно устанавливается в -1. bye и tie связываются со значением 0, а win задается значение 1. enum ststus { loss = -1, bye, tie = 0, win }; Пример 2 В данном примере объявляется тип структуры с именем student. Объявление struct student employee; может быть использовано для определения структурной переменной типа student. struct student { char name[20]; int id, class; }; Использование объявления typedef Синтаксис: typedef спецификатор-типа декларатор [,декларатор]...; Объявление typedef аналогично объявлению переменной, но вместо спецификатора класса хранения стоит ключевое слово typedef. Объявление typedef интерпретируется аналогично объявлению переменной или функции, но идентификатор становится синонимом типа, вместо заданного объявлением типа. Обратите внимание на то, что объявление typedef не создает новый тип. Оно создает синонимы для уже существующих типов или имена для типов, которые могут быть заданы каким-либо другим способом. Когда имя typedef использовано в качестве спецификатора типа, оно может комбинироваться только с определенными спецификаторами типа, но не со всеми. Допустимыми модификаторами являются const и volatile. В некоторых реализациях есть дополнительные специальные ключевые слова, которые могут быть использованы для модификации typedef. С typedef можно объявить любой тип, включая указатель, функцию и массив. Можно объявить имя typedef для указателя на структуру или об'единение еще до определения структуры или об'единения, если определение имеет ту же сферу действия, что и объявление. Пример 1 Данный пример объявляет WHOLE синонимом для int. Заметим, что теперь WHOLE можно использовать в объявлении переменной как WHOLE i; или const WHOLE i; . Однако, использование long WHOLE i; недопустимо. typedef int WHOLE; Пример 2 В данном примере объявляется тип структуры GROUP с тремя компонентами. Задан признак структуры club, и теперь в объявлениях можно использовать либо имя typedef (GROUP) либо признак структуры. typedef struct club { char name[30]; int size, year; } GROUP; Пример 3 В данном примере использовано имя typedef предыдущего примера для определения типа указателя. Тип PG объявлен как указатель на тип GROUP, который в свою очередь определен как тип структуры. typedef GROUP *PG; Пример 4 В Примере 4 задается тип DRAWF для функции, которая не возвращает значения и принимает два аргумента int. Это значит, например, что объявление DRAWF box; эквивалентно объявлению void box(int, int); . typedef void DRAWF(int, int); Имена типов "Имена типов" задают конкретный тип данных. Кроме обычных объявлений переменных и определенных типов, имена типов можно использовать еще тремя способами: в списке формальных параметров объявления функции, при преобразовании типов и в операциях sizeof. Списки формальных параметров были рассмотрены в разделе "Объявления функций". Преобразования типов и операции sizeof рассмотрены соответственно в Разделах 5.6.2 и 5.3.4 . Имена типов для фундаментальных и перечислимых типов, структур и об'единений есть просто их спецификаторы типов. Имя типа для указателя, массива или функции имеет вид: спецификатор-типа абстрактный-декларатор "Абстрактный-декларатор" это декларатор без идентификатора, и состоит обычно из одного или нескольких модификаторов указателя, массива или функции. Модификатор указателя (*) в деклараторе всегда предшествует декларатору, модификаторы массива ([]) и функции (()) следуют за идентификатором. Зная все это можно определить, где в абстрактном деклараторе должен появиться идентификатор, и интерпретировать декларатор соответствующим образом. Дополнительная информация и примеры по сложным деклараторам содержатся в Разделе 4.3.2 . Абстрактные деклараторы могут быть сложными. Скобки в сложном абстрактном деклараторе задают его конкретную интерпретацию, как это делается в сложных объявлениях. Примечание Абстрактный декларатор, состоящий из пары пустых скобок, (), не допускается, т.к. он не определен. В этом случае невозможно определить, относится ли идентификатор к внутренности скобок (в этом случае это будет немодифицированный тип), либо он действует до скобок (в данном случае это будет тип функции). Установленный объявлением typedef спецификатор типа кроме того, рассматривается как имя типа. Пример 1 В данном примере задается имя типа для "указателя на long". long * Пример 2 В Примерах 2 и 3 показано, как скобки изменяют значения сложных абстрактных деклараторов. В Примере 2 задается имя типа для указателя на массив из пяти значений int. int (*) [5] Пример 3 В Примере 3 задается указатель на функцию, которая не принимает аргументов и возвращает величину int. int (*) (void) Выражения и Присвоения Введение В данной Главе написано, как формировать выражения и проводить приваивание в языке С. "Выражение" это комбинация операнов и операторов, которая в результате имеет ("выражает") единственное значение. "Операнд" это постоянное или переменное значение, которым манипулирует выражение. Каждый операнд в выражении это тоже, в свою очередь, выражение, т.к. он представляет собой отдельное значение. При вычислении значения результирующая величина зависит от порядка следования операторов в выражении, "последовательностей точек" и "побочных эффектов", если они есть. Порядок следования операторов определяет их группирование при вычислении значения выражения. Побочные эффекты это изменения, которые вызваны вычислением выражения. В выражении с побочными эффектами вычисление одного операнда может повлиять на значение другого. Для некоторых операторов порядок вычисления операндов также сказывается на значении всего выражения. Раздел 5.2 содержит описания форматов и правил проведения вычислений операндов языка С, включая случаи рассмотрения побочных эффектов и последовательностей точек. "Операторы" задают способ манипулирования операндом или операндами в выражении. Операторы С рассмотрены в Разделе 5.3 . В языке С присвоения также считаются выражениями, т.к. результатом присвоения будет значение. Значением присвоения будет величина, которую он присваивает. Кроме простого оператора присвоения (=) в языке С имеются сложные операторы присвоения, которые и трансформируют и присваивают их операнды. Операторы присвоения описаны в Разделе 5.4 . Значения, которые представлены каждым операндом в выражении имеют тип, который может быть преобразован в другой тип в определенной ситуации. Преобразования типов происходят в присвоениях, приведении типов, вызовах функций и операциях. (В Разделе 5.5 заданы правила приритета выполнения операций в С; побочные эффекты рассмотрены в Разделе 5.2.11 и преобразование типов в Разделе 5.6.) Операнды С Операнды в С это константы, идентификаторы, строки, вызовы функций, выражения индексов, выражения выбора компонент или более сложные выражения, которые формируются комбинированием операндов с операторами или заключением операндов в скобки. Все операнды, которые представляют собой постоянное значение, называются "постоянными выражениями". Каждый операнд имеет тип. В следующих разделах рассматривается тип значения каждого вида, представляемый операндом. Операнд может быть "приведен" (или временно преобразован) из своего первоначального типа в другой тип, с помощью операции "приведения типа". Выражения приведения типа также формируют операнд выражения. Константы Постоянный операнд имеет то значение и тип, которое совпадает со значением, которое он представляет. Символьная константа имеет тип int. Целая константа имеет тип int, long, unsigned int или unsigned long в зависимости от размера величины и способа ее задания. Константы с плавающей точкой всегда имеют тип double. Строковые литералы считаются массивами символов и рассматриваются в Разделе 5.2.3. Идентификаторы "Идентификатор" задает имя переменной или функции. Каждый идентификатор имеет тип, который устанавливается при его объявлении. Значение идентификатора зависит от его типа следующим образом: ╥ Идентификаторы интегрального типа или типа с плавающей точкой представляют значения соответствующего типа. ╥ Идентификатор типа enum представляет постоянное значение из набора постоянных значений. Значение идентификатора есть постоянное выражение. Его тип есть int, в соответствии с определением типа enum. ╥ Идентификатор типа struct или union представляют значение типа struct или union. ╥ Идентификатор, который объявлен как указатель, есть указатель на значение того типа, который был задан при объявлении указателя. ╥ Объявленный как массив идентификатор представляет указатель, значение которого есть адрес первого элемента массива. Пойнтер указывает на тип элементов массива. Если series объявлен как массив 10 целых элементов, то идентификатор series представляет адрес массива и выражение индекса series[5] ссылается на целое значение, которое есть шестой элемент series. выражение индексов рассматриваются в Разделе 5.2.5. Адрес массива не меняется при выполнении программы, хотя значения отдельных элементов массива могут меняться. Значение указателя, представленное идентификатором массива, это не переменная, поэтому идентификатор массива не может стоять левым операндом в операции присвоения. ╥ Идентификатор, объявленный как функция, представляет указатель, значение которого есть адрес функции. Пойнтер указывает на функцию, возвращающую значение заданного типа. Адрес функции не меняется в процессе выполнения программы, но возвращаемое значение может меняться. Т.о. идентификаторы функций не могут служить левыми операндами в операциях присвоения. Строки Синтаксис: "строка"["строка"] "Строковый литерал" это символ или последовательность символов, заключенная в двойные цитатные скобки. Два или несколько строковых литералов, которые разделены единственным пробелом, сливаются в один строковый литерал. Строковый литерал хранится как массив элементов типа char и инициализируется заключенной в цитатные скобки последовательностью символов. Строковый литерал представляет указатель, значение которого есть адрес первого элемента массива. Адрес первого элемента строки есть константа, поэтому значение, представляемое строковым выражением также есть константа. Строки литералов есть указатели, поэтому их и можно использовать, как указатели, и на них распространяются те же ограничения, что и на указатели. Однако, это не переменные, поэтому ни строковые литералы, ни их элементы не могут быть левыми операндами в операции присвоения. Последний символ строки это всегда пустой символ. Хотя пустой символ и не виден в строковом выражении, он автоматически добавляется как последний элемент при записи строки. Например, строка "abc" в действительности состоит из четырех элементов, а не из трех. Вызовы функций Синтаксис: выражение([список-выражений]) "Вызов функции" состоит из выражения, за которым стоит необязательный элемент - список выражений в скобках, где ╥ выражение задает адрес функции (например, ее идентификатор), и ╥ список выражений (разделенных запятыми), значения которых ("действительные аргументы") передаются в функцию. Аргумент "список выражений" может быть пустым. Выражение вызова функции имеет значение и тип возвращаемого функцией значения. Если функция возвращает тип void (т.е. было объявлено, что она не будет возвращать значений), то выражение вызова функции тоже будет иметь тип void. Если вызванная функция возвращает управление без выполнения оператора return, то значение выражения вызова функции не определено. (Дополнительную информацию о вызовах функций можно найти в Главе "Функции".) Выражения индексов Синтаксис: выражение1 [выражение2] Выражения индексов представляют значение адреса, который на "выражение2" позиций стоит после "выражения1". Обычно значение представленное выражением1 есть значение указателя, например, идентификатор массива, а "выражение2" есть интегральное значение. Однако, все что требуется с точки зрения синтаксиса, это чтобы одно из выражений было типа указателя, а другое - интегрального типа. Т.о. интегральная величина может стоять в позиции выражения1, а значение указателя может стоять в скобках на месте выражения2, или "индекса". Независимо от поррядка следования значений, выражение2 должно быть заключено в скобки ([]). Выражения индексов обычно используются для ссылки на элементы массива, но их можно использовать в качестве любого указателя. Ссылки на одномерный массив Выражение индекса вычисляется добавлением интегрального значения к значению указателя, а затем применением к результату оператора адресации (*). (Оператор адресации рассматривается в Разделе 5.3.6.) Т.о. для одномерного массива следующие четыре выражения эквивалентны (предполагается, что a - указатель, а b целое): a[b] *(a+b) *(b+a) b[a] В соответствии с правилами преобразования для оператора сложения (приведенными в Разделе 5.3.6) интегральное значение преобразуется в адрес сдвига умножением его на длину адресуемого указателем типа. Например, предположим, что идентификатор line ссылается на массив значений int. Для вычисления выражения индекса line[i] используется следующая процедура: 1. Целое значение i умножается на число байт, которое определяется длиной элемента int. Преобразованное значение i теперь соответствует i-ой позиции int. 2. Это преобразованное значение добавляется к начальному значению указателя (line) для получения адреса, который отстоит на i int позиций от line. 3. К новому адресу применяется оператор адресации. результатом будет значение элемента массива в этой позиции (line[i]). Примечание Значению первого элемента line соответствует следующее значение выражения индекса: line[0] Аналогично, для указания пятой позиции от line или шестого элемента массива используется следующая ссылка: line[5] Ссылки на многомерный массив Выражение индекса может повторяться следующим образом: выражение1[выражение2][выражение3]... Выражение индекса ассоциируется слева направо. Самое левое выражение, выражение1[выражение2], вычисляется первым. Адрес является результатом сложения выражения1 и выражения2 и формирует указатель. Затем к этому указателю добавляется выражение3 и формируется новый указатель и т.д. до последнего выражения индекса. Оператор адресации (*) применяется после вычисления последнего выражения индекса, если величина адреса финального указателя не имеет тип массива (см. Пример 3). Выражения с несколькими индексами указывают на элементы "многомерных массивов". Многомерный массив это массив, элементы которого есть массивы. Например, первый элемент трехмерного массива это двумерный массив. Для следующих примеров объявляется массив с именем prop с тремя элементами, каждый из которых это массив 4 на 6 значений int. int prop[3][4][6]; int i, *ip, (*ipp)[6]; Пример 1 В данном примере показано, как указать второй отдельный int элемент prop. Массивы всегда храняться по строкам, поэтому наиболее быстро меняется последний индекс. Выражение prop[0][0][2] указывает следующий (третий) элемент массива, поэтому: i = prop[0][0][1]; Пример 2 Пример показывает более сложную ссылку на отдельный элемент prop. Выражение вычисляется следующим образом: 1. Первый индекс, 2, умножается на размер массива 4 на 6 int и добавляется к значению указателя prop. Результат указывает на третий массив 4 на 6 в prop. 2. Второй индекс, 1, умножается на размер 6-элементного массива int и добавляется к адресу, представленному prop[2]. 3. Каждый элемент 6-элементного массива есть значение int. Следовательно, финальный индекс, 3, умножается на размер int до его добавления к prop[2][1]. Результирующий адрес указывает на четвертый элемент 6-элементного массива. 4. К значению указателя применяется оператор адресации. Результатом является элемент int, расположенный по этому адресу. i = prop[2][1][3]; Пример 3 Примеры 3 и 4 рассматривают случаи, когда оператор адресации не применяется. В Примере 3 выражение prop[2][1] является допустимой ссылкой на трехмерный массив prop. Она указывает на 6-элементный массив (объявленный выше в Примере 1). Т.к. значение указателя указывает на массив, оператор адресации не применяется. ip = prop[2][1]; Пример 4 Как и для случая в Примере 3, результат выражения prop[2] есть указатель, указывающий на двумерный массив. ipp = prop[2]; Выражения выбора компоненты Синтаксис: выражение.идентификатор выражение->идентификатор "Выражение выбора компоненты" указывает компоненту структуры и об'единения. Такие выражения имеют значение и тип выбранный компоненты. Как это показано в строке синтаксиса, выражение выбора компоненты может иметь одну из двух следующих форм: 1. При первой форме, выражение.идентификатор, выражение соответствует значению типа struct или union, а идентификатор задает имя компоненты указанной структуры или об'единения. 2. При второй форме, выражение->идентификатор, выражение соответствует указателю на структуру или об'единение, а идентификатор задает имя компоненты указанной структуры или об'единения. Эти две формы задания выражения выбора компоненты имеют одинаковое действие. Действительно, выражение, которое использует оператор выборки указателя (->) является просто краткой формой записи выражения, в котором используется точка (.), если выражение до точки состоит из оператора адресации (*), прмененному к значению указателя. (Оператор адресации рассматривается в Разделе 5.3.3.) Таким образом, выражение->идентификатор эквивалентно (*выражение).идентификатор где "выражение" есть значение указателя. В Примерах 1, 2 и 3 делается ссылка на следующее объявление структуры: struct pair { int a; int b; struct pair *sp; } item, list[10]; Пример 1 В данном примере адрес структуры item присваивается компоненте sp структуры. Это значит, что item содержит указатель на себя. item.sp = &item; Пример 2 В данном примере указатель item.sp используется с оператором выбора указателя (->) для присвоения значения компоненте a. (item.sp)->a = 24; Пример 3 В данном примере показано, как выбрать отдельную компоненту структуры из массива структур. list[8].b = 12; Выражения с операторами Выражения с операторами могут быть "унарными", "бинарными" и "тернарными" выражениями. Унарное выражение состоит либо из унарного оператора, который применяется к операнду, либо из ключевого слова sizeof, за которым следует выражение. Это выражение может быть либо именем переменной либо выражением преобразования типа. При этом выражение преобразования типа должно быть заключено в скобки. унарный-оператор операнд sizeof выражение Бинарные выражения состоят из двух операндов, объединенных бинарным оператором: операнд бинарный-оператор операнд Тернарные выражения состоят из трех операндов, объединенных тернарным оператором (?:) : операнд ? операнд : операнд Выражения с операторами включают в себя также и выражения присвоения, которые используют унарные или бинарные операторы присвоения. Унарными операторами присвоения являются операторы увеличения (++) и уменьшения (--). Бинарными операторами присвоения являются оператор простого присвоения (=) и составные операторы присвоения. Каждый составной оператор присвоения является комбинацией других бинарных операторов с оператором простого присвоения. Выражения присвоения имеют следующий вид: операнд++ операнд- ++операнд --операнд операнд=операнд операнд составной-оператор-присвоения операнд Операторы присвоения детально описаны в Разделах 5.4.1 - 5.4.4. Выражения в скобках Можно заключать любой операнд в скобки без изменения типа или значения выражения. Например, в соледующем выражении скобки вокруг 10+5 означают, что величина 10+5 является левым операндом для оператора деления (/): (10 + 5) / 5 Результатом (10+5)/5 будет 3. Без скобок, 10+5/5 даст своим результатом 11. Хотя скобки и влияют на способ группирования операндов в выражении, они не могут гарантировать конкретный порядок проведения вычислений для всех случаев. Исключением будут "побочные эффекты", рассмотренные в Разделе 5.2.11. Выражения преобразования типа Преобразование типа дает метод явного пребразования типа объекта в конкретной ситуации. Выражение преобразования типа имеет следующий вид: (имя-типа)операнд Преобразование типа может быть применено к объектам любого скалярного типа для их преобразования в любой другой скалярный тип. К явному преобразованию типа применимы те же самые правила, что определяют эффект неявного преобразования типа, рассмотренного в разделе "Преобразования при присвоении". Дополнительные ограничения могут явиться результатом действительных размеров или представлением конкретных типов в конкретной реализации. Представление типов рассмотрено в Главе "Объявления". Информацию о действительных размерах интегральных типов и указателей можно найти в вашем Руководстве по компилятору. Любой объект может быть преобразован в тип void. Однако, если имя типа в выражении преобразования типа не void, операнд не может быть выражением void. Например, если функция имеет возращаемый тип void, его нельзя преобразовать в отличный от void тип. Обратите внимание на то, что выражение void* имеет тип указателя на void, а не тип void. Если объект преобразован в тип void, результирующее выражение не может быть присвоено никакому элементу. Аналогично, если объект преобразования типа имеет неприемлемое значение, к преобразованному объекту не может быть применено присвоение. Имена типов рассмотрены в Разделе 4.9. Значения рассматриваются в Разделе 5.4.1. Преоразование типа рассматривается в Разделе 5.6. Постоянные выражения Постоянное выражение это выражение, значение которого не меняется. Операндами постоянного выражения могут быть целые константы, символьные константы, константы с плавающей точкой, перечислимые константы, преобразования типа, выражения sizeof и другие постоянные выражения. Можно использовать операторы для комбинирования и модификации операндов, как это рассмотрено в Разделе 5.2.7, со следующими ограничениями: 1. В постоянном выражении нельзя использовать оператор присвоения (см. Раздел 5.4) или бинарный оператор последовательного вычисления (,). 2. Можно использовать унарный оператор получения адреса (&) только при определенных инициализациях (как это описано в последнем параграфе данного раздела). На использованные в директивах предпроцессора постоянные выражения налагаются дополнительные ограничения. Соответственно, такие выражения называются "ограниченными постоянными выражениями". Ограниченные постоянные выражения не могут содержать выражений sizeof, перечислимых констант, преобразований типа в другой тип и констант с плавающей точкой. Однако, они могут содержать специальные постоянные выражения defined(идентификатор). (Дополнительная информация о таких выражениях содержится в Разделе 8.2.2, "Директивы #define".) Постоянные выражения, содержащие константы с плавающей точкой, преобразования в неарифметические типы и выражения получения адреса могут появляться только в инициализаторах. Унарный оператор получения адреса (&) может быть применен только к переменной фундаментального, структурного или типа об'единения, которая объявлена на глобальном уровне или к ссылке на индексированный массив. В таких выражениях постоянное выражение, которое не использует оператор получения адреса может добавлено или отнято от выражения адреса. Побочные эффекты "Побочные эффекты" проявляются, когда при вычислении значения выражения меняется значение переменной. Все операции прсвоения имеют побочные эффекты. Вызовы функций также могут иметь побочные эффекты, если они меняют значение внешного доступного элемента либо прямым присвоением, либо косвенным присвоением через указатель. Порядок вычисления значения выражения зависит от конкретной реализации, за исключением гарантируемого языком программирования конкретного приритета выполнения операций (как это рассматривается в Разделе 5.5). Например, в следующем вызове функции возникает побочный эффект: add(i+1,i=j+2) Аргументы в вызове функции могут быть вычислены в произвольном порядке. Выражение i+1 может быть вычеслено как до выражения i=j+2, так и после него. Результат при этом будет разный. Унарные операции увеличения и уменьшения значения используют присвоение, такие операции могут вызвать побочные эффекты, как это показано в следующем примере: d = 0; a = b++ = c++ = d++; В этом примере значение a непредсказуемо. Значение d (первоначально 0) может быть присвоено с, затем b, а затем а до увеличения значения любой из переменных. В данном случае а будет равно 0. Второй способ вычисления выражения начинается с вычисления операнда c++ = d++. Значение d (первонначально 0) присваивается с, а затем значения c и d увеличиваются. Далее, значение с (теперь 1) присваивается b, и значение b затем увеличивается. Наконец, увеличенное значение b присваивается а. Язык С не задает порядок проведения вычислений при побочном эффекте, поэтому могут быть реализованы оба из описанных выше методов вычисления. Для обеспечения мобильности и ясности Ваших программ следует избегать операторов, которые зависят от конкретной последовательности проведения вычислений и могут вызвать побочные эффекты. Точки упорядочивания Выражения с присвоениями, унарные "увеличения" и "уменьшения" или вызовы функций могут содержать точки, которые задают порядок их вычислений (побочный эффект). При достижении такой "точки" все, что ей предшествует (включая побочные эффекты), будет вычеслено до вычисления оставшейся части выражения. Некоторые операторы могут служить такими точками, включая следующие: ╥ логическое-И (&&) ╥ логическое-ИЛИ (||) ╥ тернарный оператор (?:) ╥ оператор последовательных вычислений (,) ╥ вызов функции (т.е. скобки, которые следуют за именем функции) Другими подобными точками являются: ╥ конец полного выражения (т.е. выражение, которое не является частью другого выражения) ╥ любой инициализатор ╥ выражение в операторе выражения ╥ управляющее выражение в операторах выбора (if или switch) и операторы итерации (do, while или for) ╥ выражение в операторе return Операторы С Операторы языка С обрабатывают один операнд (унарные операторы), два операнда (бинарные операторы) или три операнда (тернарный оператор). Операторы присвоения включают и унарные и бинарные операторы. Операторы присвоения рассматриваются в Разделе 5.4. Унарные операторы ставятся до их операндов и ассоциируются справа налево. В языке С имеются следующие унарные операторы: - ~ ! отрицание и дополнение * & адресация и получение адреса sizeof размер + унарный плюс Бинарные операторы ассоциируются слева направо. В языке С имеются следующие бинарные операторы: * / % мультипликативные + - аддитивные << >> сдвиг < > <= >= == != отношения & | ^ битовые && || логические , последовательные вычисления В языке С есть только один тернарный оператор условия (? :). Он ассоциируется справа налево. Обычные арифметические преобразования Большинство операторов С выполняют преобразование типа, чтобы привести операнды выражения к некоторому общему типу или для расширения коротких значений до размеров целого, который используется в машинных операциях. Выполняемые операторами С преобразования завмсят от конкретных операторов и типов операнда или операндов. Однако, многие операторы выполняют аналогичные преобразования над операндами целого типа и типа с плавающей точкой. Эти преобразования известны, как "арифметические преобразования", т.к. они применяются к типам значений, обычно используемым в арифметике. Арифметические преобразования, собранные в данном разделе, называются "обычными арифмитическими преобразованиями. При рассмотрении каждого оператора в последующих разделах оговаривается, выполняет ли данный оператор обычные арифмитические преобразования. Кроме того, указаны выполняемые оператором дополнительные арифметические преобразования, если они имеют место. Не задан какой-либо порядок следования. Он определяется следующим алгоритмом, который применяется к каждой бинарной операции в выражении: 1. Все опернды типа float преобразуются в тип double. 2. Если один операнд имеет тип long double, то и другой операнд преобразуется в тип long double. 3. Если один операнд имеет тип double, то и другой операнд преобразуется в тип double. 4. Все операнды типа char или short преобразуются в тип int. 5. Все операнды типа unsigned char или unsigned short преобразуются в тип unsigned int. 6. Если один операнд имеет тип unsigned long, то и другой операнд преобразуется в тип unsigned long. 7. Если один операнд имеет тип long, то и другой операнд преобразуется в тип long. 8. Если один операнд имеет тип unsigned int, то и другой операнд преобразуется в тип unsigned int. Следующий пример демонстрирует применение приведенного алгоритма: long l; unsigned char uc; int i; f(l+uc*i); v Преобразование в данном примере будет проделано следующим образом: 1. uc преобразуется в тип unsigned int (шаг 5). 2. i преобразуется в тип unsigned int (шаг 8). Выполняется умножение и результатом будет тип unsigned int. 3. uc*i преобразуется в тип long (шаг 7). Выполняется сложение и результатом будет тип long. Дополнение и унарный плюс Операторы дополнения рассматриваются в следующем списке: - Оператор арифметического отрицания дает своим результатом отрицательное значение (бинарное дополнение) своего операнда. Операнд должен быть целым значением или значением с плавающей точкой. Этот оператор выполняет простые арифметические преобразования. ~ Оператор побитового дополнения дает своим результатом побитовое дополнение для своего операнда. Операнд должен иметь целый тип. Данный операнд выполняет простые арифметические преобразования. Результат имеет тип после преобразования. ! Оператор логического отрицания дает значение 0, если его операнд "истина" (не ноль), и значение 1, если его операнд "ложь" (0). Результат имеет тип int. Операнд может быть целым, значением с плавающей точкой или поцнтером. + Оператор унарного плюса, предшествующий выражению в скобках, форсирует группирование вложенных в скобки операторов. Он используется с выражениями, состоящими из нескольких ассоциативных или коммуникативных бинарных операторов. Примечание Оператор унарного плюса в Microsoft C реализован синтаксически, но не семантически для любого связанного с ним типа. Пример 1 В данном примере новое значение x есть отрицание 987, или -987. short x = 987; x = -x; Пример 2 В данном примере y присваивается новое значение, которое является к величине без знака 0xaaaa, или 0x5555. unsigned short y = 0xaaaa; y = ~y; Пример 3 В данном примере если x больше или равен y, то результат выражения есть 1 ("истина"), а если x меньше y, то результат есть 0 ("ложь"). if ( !(x>) на число позиций, задаваемое вторым операндом. Оба операнда должны быть целыми значениями. Эти операторы выполняют обычные арифметические преобразования. Тип результата совпадает с типом левого операнда после преобразования. Сдвиг влево обращает правые биты в 0. При сдвиге вправо свободные левые биты заполняются в соответствии с типом первого операнда после преобразования. Если этот тип unsigned, то они устанавливаются в 0. В противном случае они заполняются копиями бита знака. Если второй операнд есть отрицательная величина, то результат операции сдвига не определен. Выполняемые операторами сдвига преобразования не делают какой-либо проверки на переполнение, поэтому может произойти потеря информации, если результат операции сдвига не может быть представлен типом первого операнда после преобразования. Пример unsigned int x, y, z; x = 0x00aa; y = 0x5500; z = (x << 8) + (y >> 8); В данном примере x сдвигается влево на восемь позиций, а y сдвигается вправо на восемь позиций. Сдвинутые значения складываются и результат, 0xaa55, присваивается z. Отношения Бинарные операторы отношений сравнивают первый операнд со вторым и проверяют истинность заданного соотношения. Результат проверяемого выражения отношения равен 1, если оно "истина", и равен 0, если оно "ложь". Результат имеет тип int. Имеются следующие операторы отношений: < первый операнд меньше второго > первый операнд больше второго <= первый операнд меньше или равен второму >= первый операнд больше или равен второму == первый операнд равен второму != первый операнд не равен второму Операндами могут быть интегральные типы, типы с плавающей точкой и указатели . Типы сравниваемых операндов могут быть разными. Операторы отношений выполняют обычные арифметические преобразования над операндами целого типа и типа с плавающей точкой. Кроме того, в операторах отношений можно использовать следующие комбинации типов операндов: ╥ Оба операнда любого оператора отношений могут быть указателями на один и тот же тип. Для операторов равенства (==) и неравенства (!=) результат сравнения показывает, задают ли эти два указателя один и тот же адрес места в памяти. Для других операторов отношений (<, >, <= и >=) результат сравнения показывает относительную позицию двух адресов памяти. Адрес заданному значению выделяется случайно, поэтому сравнение адресов двух не связанных величин обычно не имеет смысла. Однако, может быть полезным сравнение адресов различных элементов одного и того же массива, т.к. гарантируется, что элементы массива хранятся в упорядоченной последовательности один за другим, от первого до последнего. Адрес первого элемента массива "меньше" адреса его последнего элемента. ╥ Значение указателя можно сравнить на совпадение (==) или на несовпадение (!=) с постоянной величиной 0. Пойнтер со значением 0 называется "пустым" указателем, и он не указывает ни на какую область памяти. Пример 1 Значения x и y равны, поэтому результатом выражения будет 0. int x = 0, y = 0; x < y Пример 2 Фрагмент данного примера инициализирует каждый элемент массива array пустой символьной константой. char array[10]; char *p; for (p=array; p<&array[10]; p++) *p = '\0'; Пример 3 В данном примере объявляется перечислимая переменная с именем col и признаком color. В любой момент времени переменная может содержать целое значение 0, 1 или 2, которое соответствует одному из элементов перечислимого набора color: red, white или green, соответственно. Если col содержит 0, то выполняется оператор if и все операторы, выполнение которых связано с результатами выполнения if. enum color {red, white, green} col; . . . if (col == red) . . . Побитовая обработка Операторы побитовой обработки выполняют операции побитового-И (&), вкллючающего-ИЛИ (|) и исключающего-ИЛИ (^). Операнды побитовых операторов должны иметь интегральные типы, но эти типы могут быть разными. Данные операторы выполняют обычные арифметические преобразования, тип результата совпадает с типом операндов после преобразования. Операторы языка С побитовой обработки описываются следующим образом: & Оператор побитового-И сравнивает каждый бит своего первого операнда с соответствующим битом его второго операнда. Если оба бита равны 1, то и результирующий бит устанавливается в 1. В противном случае результирующий бит устанавливается в 0. | Оператор побитового включающего-ИЛИ сравнивает каждый бит своего первого операнда с соответствующим битом его второго операнда. Если хотя бы один бит равен 1, то и результирующий бит устанавливается в 1. В противном случае результирующий бит устанавливается в 0. ^ Оператор побитового исключающего-ИЛИ сравнивает каждый бит своего первого операнда с соответствующим битом его второго операнда. Если один бит равен 1, а второй бит равен 0, то и результирующий бит устанавливается в 1. В противном случае результирующий бит устанавливается в 0. В последующих примерах использованы следующие объявления: short i = 0xab00; short j = 0xabcd; short n; Пример 1 Результат, присвоенный n, совпадает с i (шестнадцатеричное 0xab00). n = i & j; Пример 2 Побитовое включающее-ИЛИ дает в результате 0xabcd (шестнадцатеричное). n = i | j; Пример 3 Побитовое исключающее-ИЛИ дает в результате 0xcd (шестнадцатеричное). n = i & j; Логические операторы Логические операторы выполняют операции логического-И (&&) и логического-ИЛИ (||). Операнды логических операций должны иметь интегральный тип, тип плавающей точки или тип указателя. Типы операндов могут быть разными. Выражения операндов логического-И и логического-ИЛИ вычисляются слева направо. Если значение первого операнда достаточно для определения результата операции, то второй операнд не вычисляется. После первого операнда есть точка упорядочевания. Логические операторы не проводят обычных арифметических преобразований. Вместо этого они вычисляют каждый операнд в категории его эквивалентности 0. Результат логической операции 0 или 1. Тип результата int. Логические операторы языка С описываются следующим образом: && Оператор логического-И дает в результате 1, если оба операнда имеют ненулевое значение. Если хотя бы один из операндов равен 0, то результом будет 0. Если первый операнд операции логического-И равен 0, то второй операнд не вычисляется. || Оператор логического-ИЛИ выполняет операцию включающего-ИЛИ над своими операндами. Результатом будет 0, если оба операнда имеют значение 0. Если хотя бы один из операндов имеет ненулевое значение, то результатом будет 1. Если первый операнд операции логического-ИЛИ имеет ненулевое значение, то второй операнд не вычисляется. В последующих примерах использованы данные объявления: int w, x, y, z; Пример 1 В данном примере вызывается функция printf для распечатки сообщения, если x меньше y, и y меньше z. Если x больше y, то второй операнд (y>= присвоение со сдвигом вправо &= присвоение с побитовым-И |= присвоение с побитовым включающим-ИЛИ ^= присвоение с побитовым исключающим-ИЛИ При присвоении тип правого значения преобразуется в тип левого значения. Конкретный способ преобразования, который зависит от двух типов, детально рассматривается в Разделе 5.6. Выражения локализации В операции присвоения значение правого операнда присваивается в область памяти левого операнда. Следовательно, левый операнд в операции присвоения (или единственный операнд унарного выражения присвоения) должен быть выражением, которое указывает на модифицируемую область памяти. Выражения, которые указывают область памяти называются "выражениями локализации". Выражения, которые указывают на изменяемую область памяти, называются "модифицируемыми выражениями локализации". Примером модифицируемого выражения локализации служит имя переменной, которая объявлена без спецификатора const (не постоянная). Имя переменной задает область памяти, а значение переменной это величина, которая хранится в данном месте памяти. В языке С следующие выражения могут быть выражениями локализации: ╥ Идентификатор интегрального типа, типа с плавающей точкой, указателя, структуры или об'единения. ╥ Индекс ([]), который не служит значением массива или функции. ╥ Выражение выбора компоненты (-> или .), если выбранная компонента не является вышеуказанным выражением. ╥ Выражение унарной адресации, которое не указывает массив или функцию. ╥ Выражение локализации в скобках. ╥ Объект const (немодифицируемое выражение локализации). Унарное увеличение и уменьшение Унарные операторы присвоения (++ и --) соответственно увеличивают и уменьшают значения их операндов. Операнды должны быть интегрального типа, типа с плавающей точкой или типа указателя и должны быть модифицируемыми (не постоянными) выражениями локализации. Операнд интегрального типа или типа с плавающей точкой увеличивается или уменьшается на целую величину 1. Тип результата совпадает с типом операнда. Операнд типа указателя увеличивается или уменьшается на величину размера объекта, на который он указывает. Увеличенный указатель указывает на следующий объект, а уменьшенный - на предыдущий. Оператор увеличения (++) или уменьшения (--) может появиться до или после операнда по следующим правилам: ╥ Если оператор появляется до операнда, то операнд увеличивается или уменьшается и результатом выражения будет его новое значение. ╥ Если оператор появляется после операнда, то немедленным значением выражения будет его значение до увеличения или уменьшения. После использования значения операнда по контексту он увеличивается или уменьшается. Пример 1 В данном примере переменная pos сравнивается с 0, а затем увеличивается. Если pos до увеличения была положительной величиной, то выполняется следующий оператор. Сначала значение q присваивается p, а затем значения q и p увеличиваются. if (pos++ > 0) *p++ = *q++; Пример 2 В данном примере переменная i уменьшается до ее использования в качестве индекса line. if (line[--i] != '\n') return; Простое присвоение Оператор простого присвоения присваивает значение своего правого операнда левому операнду. Применимы правила преобразования для присвоений (см. Раздел 5.6.1). Пример В данном примере значение y преобразуется в тип double и присваивается x. double x; int y; x = y; Составные присвоения Операторы составного присвоения комбинируют оператор простого присвоения с другим бинарным оператором. Операторы составного присвоения выполняют операцию, заданную дополнительным оператором, а затем присваивают результат левому операнду. Например составное выражения типа выражение1 += выражение2 следует понимать, как выражение1 = выражение1 + выражение2 Однако, составные выражения присвоения не эквивалентны расширенной версии, т.к. выражение составного присвоения вычисляет выражение1 только один раз, а расширенная версия вычисляет выражение1 дважды: в операции сложения и в операции присвоения. Операнды оператора составного присвоения должны быть интегрального типа или типа с плавающей точкой. Каждый оператор составного присвоения выполняет преобразование так, как соответствующий бинарный оператор обрабатывает и ограничивает типы своих соответствующих операторов. Операторы присвоения со сложением (+=) и присвоения с вычитанием (-=) могут иметь левым операндом тип указателя, в этом случае правый операнд должен быть интегрального типа. Результат операции составного присвоения имеет значение и тип левого операнда. Пример В данном примере операция побитового включающего-ИЛИ выполняется над n и MASK, и результат присваивается n. Объявленная константа MASK определяется с директивой предпроцессора #define (данная директива рассматривается в Разделе 8.2.2.). #define MASK 0xff00 n &= MASK; Приоритет и порядок проведения операций Приоритет и ассоциативность выполнения операторов языка С влияет на группирование и вычисление операндов в выражениях. Приоритет выполнения операторов имеет смысл только в том случае, если имеются другие операторы с большим или меньшим приоритетом. Сначала всегда вычисляются выражения с большим приоритетом операторов. В Таблице 5.1 показаны приоритеты и ассоциативность операторов языка С. Они приводятся в списке в порядке убывания их приоритета. Если на одной строке указано несколько операторов, то они имеют равный приоритет и вычисляются по правилам их ассоциативности. Таблица 5.1. Приоритет и ассоциативность операторов языка С Символ Тип операции Ассоциативность () [] . -> выражения слева направо - ~ ! * & унарные справа налево ++ -- sizeof тип унарные справа налево * / % мультипликативные слева направо + - аддитивные слева направо << >> сдвиг слева направо < > <= >= отношения (неравенство) слева направо == != отношения (равенство) слева направо & битовое-И слева направо ^ битовое включающее-ИЛИ слева направо | битовое исключающее-ИЛИ слева направо && логическое-И слева направо || логическое-ИЛИ слева направо ?: условие справа налево = *= /= %= простые и составные присвоения справа налево += -= <<= >>= простые и составные присвоения справа налево &= |= ^= простые и составные присвоения справа налево , последовательные вычисления слева направо Операторы приведены в списке по убыванию их приоритета. Если на одной строке расположено несколько операторов, то они имеют равный приоритет. Все унарные операторы имеют равный приоритет. Все простые и составные операторы присвоения имеют равный приоритет. Как это показано в Таблице 5.1, операнды, состоящие из константы, идентификатора, строки, вызова функции, выражения индекса, выражения выбора компоненты или выражения в скобках имеют наивысший приоритет и ассоциируются слева направо. Преобразования типа и имеют тот же приоритет и ассоциативность, что и унарные операторы. Выражения могут содержать несколько операторов с одинаковым приоритетом. Если несколько таких операторов появляются в выражении на одном уровне, то их вычисление производится в соответствии с ассоциативностью оператора либо справа налево, либо слева направо. Направление вычислений не влияет на результаты вычисления выражений, которые содержат несколько операторов сложения (+), умножения (*) или обработки бит (& | ^) на одном уровне. Компилятор может вычислять такие выражения в любом порядке, даже если для задания порядка вычислений в выражении заданы скобки. Точками упорядочивания являются только операторы последовательных вычислений (,), логическое-И (&&), логическое-ИЛИ (||), тернарный (?:) и вызовы функций, гарантируя тем самым конкретный порядок вычисления их операндов. Оператор вызова функции это набор скобок, который следует за идентификатором функции. Оператор последовательных вычислений (,) гарантирует вычисление своих операндов слева направо. (Обратите внимание на то, что запятая, которая разделяет аргументы в вызове функции, не является оператором последовательных вычислений и не дает таких гарантий.) Точки упорядочивания рассматриваются в Разделе 5.2.12. Оператор унарного плюса (+) в определенных ситуациях форсирует группировку. Он реализован синтаксически, но не семантически. Дополнительная информация относительно унарных операторов содержится в Разделе 5.3.2, "Операторы дополнения и унарный плюс". Логические операторы также гарантируют выполнение своих операндов слева направо. Однако, они вычисляют минимальное число операндов, которое необходимо для определения результата выражения. Например, в выражении x&&y++ второй операнд y++ вычисляется только тогда, когда x есть "истина" (не ноль). Т.о. если x есть "ложь" (0), то значение y не увеличивается. В следующем списке показаны группировки по умолчанию для некоторых примеров выражений: a&b||c (a&b)||c a=b||c a=(b||c) q&&r||s-- (q&&r)||s- В первом выражении оператор битового-И (&) имеет приоритет выше, чем оператор логического-ИЛИ (||), поэтому a&b формирует первый операнд операции логического-ИЛИ. Во втором выражении оператор логического-ИЛИ (||) имеет приоритет выше, чем оператор простого присвоения (=), поэтому b||c группируется в качестве правого оператора присвоения. Обратите внимание на то, что a будет присвоено значение 0 или 1. В третьем случае показано корректно сформированное выражение, которое может привести к неожиданному результату. Оператор логического-И (&&) имеет приоритет выше, чем у оператора логического-ИЛИ (||), поэтому q&&r группируется в операнд. Логические операторы гарантируют вычисление своих операндов слева направо, поэтому q&&r будет вычислено раньше, чем s--. Однако, если q&&r даст ненулевое значение, s-- не будет вычислено и значение s не уменьшится. Для устранения проблемы следует поставить s-- первым операндом выражения или уменьшить значение s в отдельной операции. Следующее выражение некорректно и в процессе компиляции приведет к появлению диагностического сообщения: p==0?p+=1:p+=2 (p==0?p+=1:p)+=2 В данном выражении оператор равенства (==) имеет высший приоритет, поэтому p==0 группируется в операнд. Следующий приоритет имеет тернарный оператор (?:). Первый его операнд это p==0, а второй - p+=1. Однако, последним операндом тернарного оператора будет p, а не p+=2, т.к. p относится скорее к тернарному оператору, чем к оператору составного присвоения. Возникает синтаксическая ошибка, т.к. +=2 не имеет левого операнда. Для устранения данной ошибки и улучшения восприятия программы следует воспользоваться скобками, например следующим образом: (p==0)?(p+=1):(p+=2) Преобразования типа Преобразование типа производится в следующих случаях: ╥ Если значение одного типа присваивается переменной другого типа. ╥ Если значение одного типа явно приводится к другому типу. ╥ Если оператор преобразует тип своего операнда или операндов до выполнения операции. ╥ Когда значение передается в функцию в качестве аргумента. Преобразования в присвоении В операциях присвоения тип присваиваемого значение преобразуется в тип переменной, которой делается присвоение. Язык С допускает преобразование типов при присвоении между интегральными типами и типами с плавающей точкой, даже если при преобразовании происходит потеря информации. Используемый метод преобразования зависит от типов, которые участвуют в преобразовании, как это описано в Разделе 5.3.1, "Обычное арифметическое преобразование" и Разделах 5.6.1.1 - 5.6.1.5. Преобразования из интегральных типов со знаком Целая со знаком преобразуется в более короткую целую со знаком путем отбрасывания старших битов и в большую целую со знаком за счет расширения знака. Когда целая со знаком преобразуется в целую без знака, значение целой со знаком преобразуется к размеру целого без знака и результат интерпретируется, как значение без знака. Не происходит потери информации при преобразовании целой со знаком в значение с плавающей точкой. Можно потерять некоторую точность при преобразовании значения long int или unsigned long int в значение float. В Таблице 5.2 приводятся преобразования интегральных типов со знаком. Предполагается, что тип char по умолчанию имеет знак. Если при компилировании используется опция, которая отменяет умолчание и тип char будет без знака, то вместо Таблицы 5.2 нужно пользоваться строкой для unsigned char из Таблицы 5.3. Таблица 5.2. Преобразования из интегральных типов со знаком Откуда Куда Метод char short расширение знака char long расширение знака char unsigned char сохранение образца; бит высшего порядка теряется, как бит знака char unsigned short расширение знака до short, преобразование short в unsigned short char unsigned long расширение знака до long, преобразование long в unsigned long char float расширение знака до long, преобразование long в float char double расширение знака до long, преобразование long в double char long double расширение знака до long, преобразование long в double short char сохранение бита младшего порядка short long расширение знака short unsigned char сохранение бита младшего порядка short unsigned short сохранение образца; бит высшего порядка теряется, как бит знака short unsigned long расширение знака до long, преобразование long в unsigned long short float расширение знака до long, преобразование long в float short double расширение знака до long, преобразование long в double short long double расширение знака до long, преобразование long в double long char сохранение бита младшего порядка long short сохранение слова младшего порядка long unsigned char сохранение байта младшего порядка long unsigned short сохранение слова младшего порядка long unsigned long сохранение образца бита; бит высшего порядка теряется, как бит знака long float представляется float; если long не может быть точно представлен, то происходит некоторая потеря точности long double представляется double; если long не может быть точно представлен, то происходит некоторая потеря точности long long double представляется double; если long не может быть точно представлен, то происходит некоторая потеря точности В таблице предполагается, что тип char имеет знак. Примечание Тип int эквивалентен типу short или типу long в зависимости от реализации. Преобразование значения int происходит аналогично short или long, соответственно. Преобразования интегральных типов без знака Целое без знака преобразуется в более короткое целое со знаком или без знака отбрасыванием битов старшего порядка или в большее целое со знаком или без знака дополнением нулей. Когда целое без знака преобразуется в целое со знаком того же размера, битовый состав не меняется. Однако представляемое им значение меняется, если бит знака установлен. При преобразовании целого без знака в величину с плавающей точкой оно сначала преобразуется в значение long со знаком, а затем значение long со знаком преобразуется в величину с плавающей точкой. В Таблице 5.3 содержатся преобразования интегральных типов без знака. Таблица 5.3. Преобразования интегральных типов без знака Откуда Куда Метод unsigned char char битовая схема сохраняется; бит старшего разряда становится битом знака unsigned char short расширяется нулями unsigned char long расширяется нулями unsigned char unsigned short расширяется нулями unsigned char unsigned long расширяется нулями unsigned char float преобразуется в long; long преобразуется в float unsigned char double преобразуется в long; long преобразуется в double unsigned char long double преобразуется в long; long преобразуется в double unsigned short char сохраняется байт младшего порядка unsigned short short битовая схема сохраняется; бит старшего разряда становится битом знака unsigned short long расширяется нулями unsigned short unsigned char сохраняется байт младшего порядка unsigned short unsigned long расширяется нулями unsigned short float преобразуется в long; long преобразуется в float unsigned short double преобразуется в long; long преобразуется в double unsigned short long double преобразуется в long; long преобразуется в double unsigned long char сохраняется байт младшего порядка unsigned long short сохраняется слово младшего порядка unsigned long long битовая схема сохраняется; бит старшего разряда становится битом знака unsigned long unsigned char сохраняется байт младшего порядка unsigned long unsigned long сохраняется слово младшего порядка unsigned long float преобразуется в long; long преобразуется в float unsigned long double преобразуется в long; long преобразуется в double unsigned long long double преобразуется в long; long преобразуется в double Примечание Тип unsigned int в зависимости от реализации эквивалентен типу unsigned short или unsigned long. Преобразование для unsigned int происходит аналогично unsigned short или unsigned long, соответственно. Преобразования из unsigned long в float, double или long double не обладают достаточной точностью, если преобразуемое значение больше, чем максимальная положительная величина long. Преобразования типов с плавающей точкой Преобразование из типа float в тип double не вносит каких-либо изменений в значение. Преобразование значения из double в float происходит явно, если оно возможно. Может произойти потеря точности, если величина не может быть представлена этим типом. При преобразовании величины с плавающей точкой в интегральное значение она сначала преобразуется в тип long, а затем из типа long в конкретный интегральный тип, как это показано в Таблице 5.4. При преобразовании в long десятичная часть величины с плавающей точкой отбрасывается. Если при этом результат все еще слишком велик, чтобы поместиться в long, то результат преобразования будет неопределен. В Таблице 5.4 приводятся преобразования с плавающей точкой Таблица 5.4. Преобразования типов с плавающей точкой Откуда Куда Метод float char преобразуется в long; long преобразуется в char float short преобразуется в long; long преобразуется в short float long отбрасывается десятичная точка. Если результат слишком велик для представления long,то он неопределен float unsigned short преобразуется в long; long преобразуется в unsigned short float unsigned long преобразуется в long; long преобразуется в unsigned long float double сменяется внутреннее представление float long double сменяется внутреннее представление double char преобразуется в float; float преобразуется в char double short преобразуется в float; float преобразуется в short double long отбрасывается десятичная точка. Если результат слишком велик для представления long,то он неопределен double unsigned short преобразуется в long; long преобразуется в unsigned short double unsigned long преобразуется в long; long преобразуется в unsigned long double float представляется как float. Если значение double не может быть представлено, как float, то происходит потеря точности. Если значение слишком велико для float, то результат неопределен. long double char преобразуется в float; float преобразуется в char long double short преобразуется в float; float преобразуется в short long double long отбрасывается десятичная точка. Если результат слишком велик для представления long,то он неопределен long double unsigned short преобразуется в long; long преобразуется в unsigned short long double unsigned long преобразуется в long; long преобразуется в unsigned long long double float представляется как float. Если значение double не может быть представлено, как float, то происходит потеря точности. Если значение слишком велико для float, то результат неопределен. long double double значение long double обрабатывается, как double Примечание Преобразования из float, double или long double в unsigned long не обеспечивают точности, если преобразуемое значение больше, чем максимальное положительное значение long. Преобразование типов указателей Пойнтер на один тип значения может быть преобразован в указатель на другой тип. Однако, при этом можно получить неопределенный результат из-за требований к выравниванию и размерам различных типов в памяти. Пойнтер типа void может быть преобразован в любой тип и в него может быть преобразован любой тип, без каких-либо ограничений. Преобразования других типов Тип enum по определению имеет значение int, поэтому преобразование из этого типа и в этот тип будет таким же, как и для int. Не допускается преобразование между типами структуры и об'единения. Тип void по определению не имеет значения. Т.о. он не может быть преобразован в какой-либо иной тип, и другие типы не могут быть преобразованы в него присвоением. Однако, можно явно привести значение к типу void, как это рассмотрено в разделе 5.6.2. Приведения типа Приведение типов можно использовать для их явного преобразования. Приведение типа имеет вид: (имя-типа)операнд где имя-типа это тип и операнд это значение, которое будет преобразовано в заданный тип. (Имена типов были рассмотрены в Разделе 4.9.) Операнд преобразуется так, как если бы он был присвоен переменной типа имя-типа. Правила преобразования для присвоений (изложенные в Разделе 5.6.1) также применимы и для приведения типов. В операции приведения типа можно использовать можно использовать и имя типа void, но только нельзя присвоить результирующее выражение какому-либо типу. Преобразования операторов Преобразования, выполняемые операторами языка С, зависят от оператора и его операнда или операндов. Многие операторы выполняют обычные арифметические преобразования, рассмотренные в Разделе 5.3.1. Язык С выполняет некоторые арифметические действия с указательами. В арифметике указателей целые значения преобразуются для представления ими позиций памяти. (Дополнительная информация содержится в рассмотрении аддитивных операторов в Разделе 5.3.6, и выражений индексов в Разделе 5.2.5.) Преобразования вызовов функций Тип преобразований, выполняемых над аргументами в вызове функции, зависит от наличия прототипа функции (ее раннего объявления) с объявленными типами аргументов для вызываемой функции. Если имеется прототип функции, который включает объявленные типы аргументов, то компилятор выполняет проверку типов. Процесс проверки типов детально рассмотрен в Главе "Функции". Если прототипа функции нет или в раннем объявлении старого типа опущен список типов аргументов, то проводятся только обычные арифметические преобразования каждого аргумента в вызове функции. Это означает, что величины float преобразуются в double, char или short в int, unsigned char или unsigned short в unsigned int. Если реализованы специальные ключевые слова near, far и huge, то может быть сделано неявное преобразование значений указателей, передаваемых в функцию. Можно отменить эти неявные преобразования, задав прототип функции и дать тем самым компилятору возможность выполнить проверку типов. Информация о преобразованиях указателей содержится в Вашем Руководстве по компилятору. Операторы Введение Операторы программы на языке С управляют процессом ее выполнения. В языке С, как и в других языках программирования, имеется ряд операторов, с помощью которых можно выполнять циклы, указывать другие операторы для выполнения и передавать управление на другой участок программы. Данная Глава рассматривает операторы языка С в алфавитном порядке: оператор break оператор goto и операторы с метками составной оператор оператор if оператор continue пустой оператор оператор do оператор return оператор expression оператор switch оператор for оператор while Операторы языка С состоят из ключевых слов, выражений и других операторов. В операторах языка С появляются следующие ключевые слова: break default for return case do goto switch continue else if while Выражения в операторах языка С это те выражения, которые рассматривались Главе "Выражения и присвоения". Операторы, которые могут появляться в операторах языка С, это могут быть любые рассматриваемые в данной Главе операторы. операторы, которые формируют компоненту другого оператора называются "телом" вложенного оператора. Часто телом оператора является "составной" оператор: отдельный оператор, состоящий из одного или нескольких операторов. Составные операторы ограничиваются фигурными скобками ({}). Все другие операторы языка С заканчиваются точкой с запятой (;). Любой оператор языка С может начинаться с идентифицирующей его метки, которая состоит из имени и двоеточия. Метки операторов распознаются только оператором goto, поэтому рассмотрены вместе с оператором goto в Разделе 6.8. При выполнении программы на языке С ее операторы выполняются в том порядке, в котором они появляются в программе, если нет оператора, который бы явно передавал управление в другое место программы. Оператор break Синтаксис: break; Выполнение Оператор break прекращает выполнение вложенного оператора do, for, switch или while, в котором он появляется. Управление передается тому оператору, который непосредственно следует за прерванным оператором. Оператор break может появиться только в операторах do, for, switch или while. Для вложенных операторов оператор break прекращает выполнение только оператора do, for, switch или while, в котором он появился. Для передачи управления из вложенной структуры можно использовать оператор return или goto. Пример В данном примере обрабатывается массив строк переменной длины, хранимый в lines. Оператор break вызывает выход из внутреннего цикла for после обнаружения ограничительного пустого символа (\0) в каждой строке и его позиция записывается в lengths[i]. Затем управление передается на внешний цикл for. Переменная i увеличивается и процесс продолжается до тех пор, пока i не станет больше или равна LENGTH. for (i=0; i0) { line[i]=x; x++; i--; } Оператор continue Синтаксис: continue; Выполнение Оператор continue передает управление на следующую итерацию оператора do, for или while, в котором он появляется передавая все оставшиеся операторы тела оператора do, for или while. Следующая итерация оператора do, for или while понимается следующим образом: ╥ В операторе do или while следующая итерация начинается вычислением выражения оператора do или while. ╥ В операторе for следующая итерация начинается вычислением выражения цикла оператора for. После вычисления условного выражения в зависимости от его результатов происходит либо прекращение выполнения оператора либо выполнение его тела. (Оператор for рассматривается в Разделе 6.7.) Пример В данном примере тело оператора будет выполнено, если i больше 0. Сначала f(i) присваивается x, затем, если x=1, выполняется оператор continue. Остальные операторы тела игнорируются и выполнение переходит в начало цикла на вычисление i-->0. while (i-->0) { x=f(i); if (x==1) continue; y+=x*x; } Оператор do Синтаксис: do оператор while (выражение); Выполнение Тело оператора do выполняется один или несколько раз до тех пор, пока значением выражения не станет "ложь" (0). Выполнение происходит следующим образом: 1. Выполняется тело оператора. 2. Вычисляется выражение. Если его значение "ложь", то выполнение оператора do заканчивается и управление передается следующему оператору программы. Если его значение "истина" (ненулевое значение), то процесс повторяется, начиная с шага 1. Выполнение оператора do может быть прервано выполнением оператора break, goto или return в теле оператора do. Пример В данном операторе do независимо от значения x выполняются два оператора, y=f(x); и x--;. Затем вычисляется значение x>0. Если x больше 0, то тело цикла выполняется снова и опять проверяется x>0. Тело оператора выполняется повторно до тех пор, пока x остается больше 0. Выполнение оператора do прекращается, когда значение x становится 0 или отрицательным. Тело цикла выполнится по крайней мере один раз. do { y=f(x); x--; } while (x>0); Оператор-выражение Синтаксис: выражение; Выполнение При выполнении оператора-выражения, выражение вычисляется в соответствии с правилами, приведенными в Главе "Выражения и присвоения". В языке С присвоения являются выражениями. Значением выражения в этом случае будет присваиваемое значение (иногда называемое "правым значением"). Вызовы функций также считаются выражениями. В этом случае значением выражения будет возвращаемая функцией величина, если она имеется. Если функция возвращает значение, то оператор-выражение обычно содержит присвоение для записи возвращаемого значения при вызове функции. Возвращаемое функцией значение обычно используется в качестве операнда в другом выражении. Если значение будет использовано несколько раз, его можно присвоить другой переменной. Если значение не будет использовано в качестве операнда и не будет присвоено, то функция вызывается, а возвращаемое значение, если оно есть, не используется. Пример 1 В данном примере x присваивается значение y+3. x=(y+3); Пример 2 В данном примере увеличивается значение x. x++; Пример 3 В данном примере показано выражение вызова функции. Значение выражения, которое включает в себя любую возвращаемую функцией величину, будет присвоено переменной z. z=f(x)+3; Оператор for Синтаксис: for([начальное-выражение]; [условное-выражение]; [выражение-цикла]) оператор Выполнение Тело оператора for может выполниться несколько раз, а может не выполниться ни разу, пока значением необязательного условного-выражения не станет "ложь". Можно использовать необязательные начальное-выражение и выражение-цикла для инициализации и смены значений при выполнении операторов for. Выполнение оператора for происходит следующим образом: 1. Вычисляется начальное-выражение, если оно есть. 2. Вычисляется условное-выражение, если оно есть. Возможны три результата: ╥ Если значение условного-выражения "истина" (ненулевое), то выполняется оператор. Затем вычисляется выражение-цикла, если оно есть. Процесс начинается снова с вычисления условного-выражения. ╥ Если условное-выражение опущено, то считается, что его значение "истина" и процесс выполнения протекает так, как это описано в первом случае. Оператор for без аргумента условного-выражения прекращает свое выполнение только в случае выполнения оператора break или return в теле оператора, или при выполнении оператора goto (который передаст управление на оператор с меткой вне тела оператора for). ╥ Если значение условного-выражения "ложь", то выполнение оператора for прекращается и управление передается к следующему оператору программы. Оператор for также прекращает свое выполнение при выполнении в теле оператора оператора break, goto или return. Пример В данном примере происходит подсчет количества символов пробела ('\x20') и табуляции ('\t') в массиве символов с именем line, и каждый символ табуляции заменяется пробелом. Сначала i, space и tab инициализируются значением 0. Затем i сравнивается с константой MAX; если i меньше MAX, то выполняется тело оператора. В зависимости от значения line[i] тело оператора if будет выполнено один раз или не выполнено совсем. Затем i увеличивается и снова сравнивается с MAX. Тело оператора будет выполняться повторно до тех пор, пока i остается меньше MAX. for (i=space=tab=0; i 0) goto exit; . . . exit: return (errorcode); Оператор if Синтаксис: if (выражение) оператор 1 [else оператор2] Выполнение Тело оператора if выполняется выборочно, в зависимости от значения выражения, по следующей схеме: 1. Вычисляется значение выражения. ╥ Если значение выражения "истина" (не ноль), то выполняется оператор1. ╥ Если значение выражения "ложь", то выполняется оператор2. ╥ Если значение выражения "ложь" и не задана статья else, то оператор1 игнорируется. 2. Управление передается от оператора if на следующий оператор программы. Пример 1 В данном примере если i больше нуля, то выполняется оператор y=x/i. Если i меньше или равно нулю, то значение i присваивается x и y присваивается значение f(x). Обратите внимание на то, что формирующий предложение if оператор заканчивается точкой с запятой. if (i>0) y=x/i; else { x=i; y=f(x);} Примечание В языке С отсутствует оператор "else if", но этого же эффекта можно достигнуть вложенными операторами if. Оператор if может быть вложен либо в предложение if, либо в предложение else другого оператора if. При вложении операторов if и предложений else следует использовать скобки для группировки операторов и предложений в составные операторы, что проясняет Ваши намерения. Если скобки не задать, то компилятор попытается разрешить конфликт сам, группируя каждый else с наиболее близким к нему if, у которого else отсутствует. Пример 2 В данном примере предложение else связывается с внутренним оператором if. Если i меньше или равно 0, то никакое значение x не присваивается. if (i>0) /* без скобок */ if (j>i) x=j; else x=i; Пример 3 В данном примере сковки окружают внутренний оператор if. В результате этого статья else становится частью внешнего оператора if. Если i меньше или равно 0, то значение i присваивается x. if (i>0){ /* со скобками */ if (j>i) x=j;} else x=i; Пустой оператор Синтаксис: ; "Пустой оператор" это оператор, который состоит только из точки с запятой. Он может появиться везде, где ожидается появление оператора. При выполнении пустого оператора ничего не происходит. Операторы подобные do, for, if и while требуют наличия выполняемого оператора в теле оператора. В данном случае пустой оператор удовлетворяет синтаксическим требованиям, если действительно не нужно иметь выполняемое тело оператора. Как и любых других операторах языка С, перед пустым оператором можно ставить метку. Для того, чтобы пометить меткой элемент, который не является оператором, например, закрывающую скобку составного оператора, можно пометить меткой пустой оператор и поместить его непосредственно перед нужным элементом. Пример В данном примере выражение цикла line[i++]=0 в цикле for инициализирует первые 10 элементов line значением 0. Телом оператора является пустой оператор, т.к. не нужно никаких дополнительных операторов. for (i=0; i<10; line[i++]=0) ; Оператор return Синтаксис: return [выражение] Выполнение Оператор return прекращает выполнение функции, в которой он появляется и передает управление на вызов функции. Выполнение программы продолжается непосредственно с той точки, откуда был произведен вызов функции. Значение выражения, если оно есть, передается на вызов функции. Если выражение не задано, то возвращаемое функцией значение не определено. По общему соглашению аргумент "выражение" оператора return заключается в скобки. Однако, для языка С присутствие этих скобок необязательно. Если в определении функции отсутствует оператор return, то управление автоматически передается на вызов функции после выполнения последнего оператора вызванной функции. При этом не определено возвращаемое вызванной функцией значение. Если от функции не требуется возврат какого-либо значения, то функция объявляется с возвращаемым значением типа void. Пример В данном примере функция main вызывает две функции: sq и draw. Функция sq возвращает в функцию main значение x*x, где оно присваивается y. Функция draw не возвращает значение и объявлена типом void. Попытка присвоения возвращаемого функцией draw значения вызовет появление диагностического сообщения. main() { void draw(int,int); long sq(int); . . . y=sq(x); draw(x, y); . . . } long sq(x) int x; { return (x*x); } void draw(x,y) int x, y; { . . . return; } Оператор switch Синтаксис: switch (выражение) { [объявление] . . . [case постоянное-выражение:] . . . [оператор] . . . [default: [оператор]] } Выполнение Оператор switch передает управление на оператор в своем теле. Управление будет передано тому оператору, значение case постоянное-выражение которого совпадает с выражением switch. Оператор switch может содержать любое число элементов case. Выполнение тела оператора начинается в выбранном операторе и заканчивается в конце тела или в тот момент, когда оператор передаст управление вне тела. Оператор default выполняется в том случае, если ни одно постоянное-выражение case которого не совпадет с выражением switch. Если оператор default не задан и ни одно совпадение с case не обнаружено, то ни один из операторов тела switch не будет выполнен. Располагать оператор default в конце не обязательно, он может появиться в произвольном месте тела оператора switch. Выражение switch должно иметь интегральный тип, но результирующее значение будет преобразовано в int. Затем каждое постоянное-выражение case будет преобразовано с использованием обычных арифметических преобразований. Значения всех постоянных-выражений case должны быть разными в теле оператора. если тип выражения switch больше int, то появится диагностическое сообщение. Метки case и default тела оператора switch действуют только при первоначальной проверке, определяющей начало выполнения тела цикла. Все операторы от начала выполнения и до конца тела выполняются независимо от их меток, кроме случая, когда управление передается в часть программы вне тела оператора. Примечание В начале составного оператора, формирующего тело switch, могут появиться объявления, но сделанные в объявлениях инициализации не выполняются. Оператор switch передает управление непосредственно на выполняемый оператор тела, передавая ему строки, содержащие инициализации. Пример 1 В данном примере будут выполнены все три оператора тела switch, если c равно A. При этом управление передается на первый оператор (capa++;) и продолжается до конца тела. Если c равно a, то значения lettera и total увеличиваются. В противном случае будет увеличено только значение total. switch (c) { case 'A': capa++; case 'a': lettera++; default: total++; } Пример 2 В данном примере за каждым оператором тела switch следует оператор break. Оператор break форсирует выход из тела оператора после выполнения одного оператора. Если i равно -1, то увеличивается только n. За оператором n++ следует оператор break, что вызывает передачу управления вне тела оператора, в обход оставшихся операторов. Аналогично, если i равно 0, то увеличивается только z; если i равно 1, то увеличивается только p. Финальный оператор break не является обязательным, т.к. управление выйдет из тела составного оператора автоматически по достижении его конца. Он поставлен здесь для единообразия. switch (i) { case -1: n++; break; case 0: z++; break; case 1: p++; break; } Использование нескольких меток На один оператор может указывать несколько меток case, как это показано в следующем примере: case 'a' : case 'b' : case 'c' : case 'd' : case 'e' : case 'f' : hexcvt(c); Хотя можно поставить метку на любой оператор тела оператора switch, в качестве носителя метки необязательно должен быть оператор. Можно свободно ставить операторы с метками и без меток. Однако, следует помнить о том, что после того, как оператор switch передаст управление на какой-либо оператор тела, будут выполнены все операторы блока, независимо от того, имеют ли они метки или нет. Оператор while Синтаксис: while (выражение) оператор Выполнение Тело оператора while выполнится ноль или более раз до тех пор, пока значением выражения не станет "ложь" (0). Процесс выполнения протекает следующим образом: 1. Вычисляется значение выражения. 2. Если значение выражения есть "ложь", то тело оператора while не выполняется, и управление передается на следующий за оператором while оператор программы. Если значение выражения есть "истина" (не ноль), то выполняется тело оператора и процесс повторяется с шага 1. Выполнение оператора while также может быть закончено выполнением оператора break, goto или return в теле оператора. Пример В данном примере символы string2 копируются в string1. Если i больше или равно 0, то string2[i] присваивается string1[i] и значение i уменьшается. Когда достигнет значения 0 или упадет ниже него, выполнение оператора while заканчивается. while (i>=0) { string1[i]=string2[i]; i--; } Функции Введение Функция это независимый набор объявлений и операторов, который обычно разрабатывается для выполнения конкретной задачи. Программы на языке С имеют по крайней мере одну функцию, main, и могут содержать и другие функции. В данной Главе рассматривается, как можно определять, объявлять и вызывать функции в языке программирования С. В определении функции задается ее имя, тип и число ее формальных параметров и приводятся объявления и операторы, которые задают ее действие. Эти объявления и операторы называются "телом функции". Кроме того, определение функции задает ее тип возвращаемого значения и ее класс хранения. Если возвращаемый тип и класс хранения не заданы явно, то по умолчанию они устанавливаются соответственно int и extern. Прототип функции (или ее объявление) задает имя, возвращаемый тип и класс хранения для функции, полное определение которой содержится в каком-либо другом месте программы. Он также может содержать объявления, задающие тип и число формальных параметров функции. Объявления формальных параметров могут задавать имена для формальных параметров, хотя сфера действия этих имен заканчивается в конце объявления. Для формального параметра может быть задан класс хранения register. Пример В данном примере дается четкое и ясное объявление прототипа и определение форматов. Показано, что прототип функции имеет тот же вид, что и определение функции, за тем исключением, что прототип заканчивается точкой с запятой. Компилятор использует прототип или объявление для сравнения типов действительных аргументов в вызове функции с формальными параметрами функции, даже если явное определение функции отсутствует. Явное задание прототипов и объявлений для функций, возвращающих значение int необязательно. Однако, для обеспечения корректности выполнения нужно обязательно объявить или определить функции с другими возвращаемыми типами до их вызова. (Объявления прототипов функций будут рассмотрены позднее в Разделе "Определения функций (Прототипы)" данной Главы и в Главе "Объявления".) Если не задан прототип или объявление функции, то ее прототип создается по умолчанию на основании той информации, которая имеется при первой же ссылке на функцию. Это может быть вызов или определение функции. Однако, такой прототип по умолчанию может неадекватно представлять определение или вызов функции. При вызове функции управление выполнением программы передается на вызванную функцию. Действительные аргументы, если они есть, передают свои значения вызванной функции. Выполнение оператора return в вызванной функции возвращает управление и возможное возвращаемое значение на вызов функции. Примечание Строго рекомендуется использовать прототипы функций. Иногда они могут дать единственную возможность для компилятора проверить корректность передачи аргументов. Прототипы позволяют компилятору либо диагностировать, либо корректно обработать несоответствие аргументов, которые иначе не могут быть обработаны до выполнения программы. /** Объявление и определение прототипа функции **/ double new_style(int a, double *x); /* прототип функции */ double alt_style(int, double *); /* альтернативная форма прототипа */ double old_style(); /* устаревшая форма объявления функции */ . . . double new_style(int a, double *real); /* стиль */ { /* определения */ return (*real + a); /* функции прототипа */ } double alt_style(int, double *); /* старая форма */ double *real; /* определения функции */ int a; { return (*real + a); } Определения функций Синтаксис: [спецификатор-класса] [спецификатор-типа]декларатор ([список-формальных-параметров]) тело-функции В "определении функции" задается ее имя, формальные параметры и тело функции. Кроме того, может задаваться возвращаемый функцией тип и класс хранения. Необязательный "спецификатор-класса" задает класс хранения функции, который может быть static или extern. Необязательный "спецификатор-типа" и обязательный "декларатор" вместе задают имя и возвращаемый функцией тип. Декларатор является комбинацией идентификатора, который задает имя функции, и скобок, которые следуют за именем. "Список-формальных-параметров" это последовательность объявлений формальных параметров, разделенных запятыми. Объявление каждого формального параметра в списке должно иметь следующий вид: [register] спецификатор-типа [декларатор] [,...] Список формальных параметров содержит объявления параметров функции. Если функции не будут передаваться аргументы, то список должен содержать ключевое слово void. Допустимо использование пустых скобок (()), но в этом случае нет никакой информации о том, будут ли передаваться аргументы. Список формальных параметров может быть полным или частичным. Вторая строка в приведенном выше синтаксисе (запятая, за которой следует многоточие) является частичным списком параметров и показывает, что в функцию могут быть переданы дополнительные параметры, но никакой информации о них не задается. Для таких аргументов не производится проверка их типа. По крайней мере один формальный параметр должен предшествовать многоточию и оно должно стоять последним элементом в списке параметров. Без указания многоточия поведение функции будет непредсказуемым при передаче ей дополнительных параметров, не указанных в списке формальных параметров. Если задан прототип функции, то производится автоматическая проверка и преобразование типов. Если относительно формальных параметров нет никакой информации, то над необъявленными аргументами проводится обычное арифметическое преобразование. "Спецификатор-типа" можно не задавать, только если задан класс хранения register для значения типа int. "Тело-функции" это составной оператор, содержащий локальные объявления переменных, ссылки на внешне объявленные элементы и операторы. Примечание Старая форма объявления и определения функции все еще будет работать, но считается устаревшей. В новых программах рекомендуется использование прототипа. Старая форма определения функции имеет следующий синтаксис: [спецификатор-класса][спецификатор-типа]декларатор ([список-идентификаторов])[объявления-параметров] тело-функции "Список-идентификаторов" это необязательный список идентификаторов, которые функция может использовать в качестве имен своих формальных параметров. Аргументы "объявления-параметров" устанавливают тип формальных параметров. В Разделах 7.2.1 - 7.2.4 содержится детальное описание отдельных частей определения функции. Класс хранения С помощью спецификатора класса хранения в определении функции можно задать класс хранения для функции extern или static. Если определение функции не содержит спецификатора класса хранения, то по умолчанию устанавливается класс хранения extern. Можно явно задать спецификатор класса хранения extern в определении функции, хотя это и не обязательно. Функция с классом хранения static доступна только в том исходном файле, где она появляется. Все другие функции, явно или неявно определенные с классом хранения extern, доступны из всех исходных файлов, составляющих программу. Если желательно задать класс хранения static, то нужно это объявить при первом появлении объявления функции, если оно есть, и в определении функции. Возвращаемый тип и имя функции Синтаксис: [спецификатор-класса] [спецификатор-типа]декларатор ([список-формальных-параметров]) Возвращаемый функцией тип устанавливает размер и тип значения, возвращаемого функцией, и полностью соответствует "спецификатору-типа" в приведенном выше синтаксисе. Спецификатор типа может задать любой фундаментальный, структурный или тип об'единения. Если спецификатор типа не задан, то по умолчанию предполагается, что возвращаемый тип int. Декларатор это идентификатор функции, который может быть модифицирован на тип указателя. Следующие за идентификатором скобки определяют элемент в качестве функции. Функции не могут возвращать массивы или функции, но они могут возвращать указатели на любые типы, включая массивы и функции. Заданный в определении функции возвращаемый тип должен соответствовать возвращаемому типу в объявлении функции, которое находится в каком-либо другом месте программы. Нет необходимости объявлять функции с возвращаемым типом int до их вызова, хотя наличие прототипов рекомендуется для обеспечения проверки корректности аргументов. Однако, функции с другими возвращаемыми типами должны быть определены или объявлены до их вызова. Возвращаемый тип функции используется только тогда, когда функция возвращает какое-либо значение. Функция возвращает значение при выполнении в ней оператора return, содержащего выражение. Это выражение вычисляется, при необходимости преобразуется к типу возвращаемого значения и эта величина передается на место вызова функции. Если оператор return не выполняется или если оператор return не содержит выражения, в этом случае возвращаемое значение не определено. Если при вызове функции предполагается получение от нее возвращаемого значения, а оно не определено, то и поведение программы в целом также не определено. Пример 1 В данном примере функция add по умолчанию возвращает тип int. Функция имеет класс хранения static, это означает, что ее могут вызывать только функции этого исходного файла. Объявленный в заголовке список формальных параметров включает в себя int значение x, для которого запрошен класс хранения register, и int значение y. Определение второй функции subtract дано в старой форме. По умолчанию ее возвращаемый тип int. Формальные параметры объявляются между заголовком и открывающей скобкой. /* определение прототипа */ static add (register x, int y) { return (x+y); } /* определение старой формы */ sutract (x , y) int x, y; { return (x-y); } Пример 2 В данном примере определяется тип STUDENT с объявлением typedef и определяется функция sortstu с возвращаемым типом STUDENT. Функция выбирает и возвращает один из своих двух структурных аргументов. Данное определение прототипа имеет формальные параметры, объявленные в заголовке. При вызове функции компилятор будет проверять соответствие заданных аргументов типу STUDENT. Можно увеличить эффективность, если передавать указатель на структуру, а не саму структуру. typedef struct { char name[20]; int id; long class; } STRUCT; /* возвращаемый тип STUDENT */ STUDENT sortsu (STUDENT a, STUDENT b) { return ( (a.id < b.id) ? a : b); } Пример 3 В данном примере использована старая форма определения функции, возвращающей указатель на массив символов. Функция принимает в качестве аргументов два массива символов (строки) и возвращает указатель на наиболее короткую из двух строк. Пойнтер на массив указывает на тип элементов массива, следовательно, возвращаемый функцией тип это указатель на char. /* возвращаемый тип это указатель на char */ char *smallstr(s1, s2) char s1[], s2[]; { int i; i=0; while ( s1[i] != '\0' && s2[i] != '\0' ) i++; if ( s1[i] == '\0' ) return (s1); else return (s2); } Формальные параметры "Формальные параметры" это переменные, которые принимают передаваемые при вызове функции значения. В определении прототипа функции скобки, которые следуют за именем функции, содержат полные объявления формальных параметров функции. Примечание В старой форме определения функции формальные параметры определялись после закрывающей скобки, непосредственно перед началом составного оператора, составляющего тело функции. В этой форме список идентификаторов в скобках задавал имена всех формальных параметров и порядок, в котором они принимали значения из вызова функции. Список идентификаторов мог содержать несколько идентификаторов, разделенных запятыми, или ни одного идентификатора. Список должен был быть заключен в скобки даже тогда, когда он был пуст. Такая форма записи считается устаревшей и ей не следует пользоваться при написании новых программ. Если в списке формальных параметров есть по крайней мере один формальный параметр, то список может заканчиваться запятой с многоточием (,...). Эта конструкция указывает на переменное число аргументов функции. Однако, ожидается, что в вызове функции будет по крайней мере столько же аргументов, сколько формальных параметров присутствует до последней запятой. В старой форме определения запятая с многоточием может следовать за последним идентификатором в списке идентификаторов. Если в функцию не будут передаваться аргументы, то список формальных параметров заменяется ключевым словом void. Такое использование void отличается от его использования в качестве спецификатора типа. В объявлениях формальных параметров задаются типы, размеры и идентификаторы для значений, хранимых в формальных параметрах. В старой форме определения функции такие объявления имеют вид объявлений других переменных (см. Главу "Объявления"). Однако, в определении прототипа функции каждому идентификатору в списке формальных параметров должен предшествовать его спецификатор типа. Например, в следующем определении функции (старого вида) old, double x, y, z; могут быть объявлены простым отделением запятой идентификаторов: void old(x, y, z) double z, y; double x; { ; } void new(double x, double y, double z) { ; } Функция с именем new определена в формате прототипа со списком формальных параметров в скобках. При таком способе записи спецификатор типа double должен быть повторен для каждого идентификатора. Порядок и тип формальных параметров, включая использование запятой с многоточием, должен быть одинаковым во всех объявлениях функции (если они есть) и в определении функции. Типы действительных аргументов в вызовах функции должны совпадать с типами соответствующих формальных параметров до последней запятой с многоточием. Аргументы, которые следуют за запятой с многоточием не проверяются. Формальный параметр может иметь любой фундаментальный, структурный или тип об'единения, тип указателя или массива. Единственный класс хранения, который можно задать для формального параметра, это register. Предполагается, что необъявленные идентификаторы в скобках, которые следуют за именем функции, имеют тип int. В старой форме определения функции объявления формальных параметров могут следовать в любом порядке. Идентификаторы формальных параметров используются в теле функции для ссылки на переданные в функцию значения. Эти идентификаторы не могут быть переопределены во внешнем по отношению к телу функции блоке, но они могут быть переопределены во внутренних, вложенных блоках. В старой форме только идентификаторы, которые появляются в списке идентификаторов, могли быть объявлены формальными параметрами. Функции с переменной длиной списка аргументов должны использовать новую форму прототипа. Программист отвечает за определение числа передаваемых аргументов и за поиск дополнительных аргументов из стека в теле функции. (В Вашем Руководстве по компилятору содержится информация о макросах, которые позволяют это делать наиболее компактным образом.) Компилятор выполняет обычные арифметические преобразования над каждым формальным параметром и над каждым действительным аргументом, если это является необходимым. После преобразования ни один из формальных параметров не будет короче int, и ни один из формальных параметров не будет иметь тип float. Это значит, например, что объявление формального параметра char имеет тот же эффект, что объявление его int. Если реализованы ключевые слова near, far и huge, то компилятор может преобразовывать аргумент-указатель в функцию. Выполненное преобразование зависит от размера по умолчанию указателя в программе и наличия или отсутствия списка типов аргументов для функции. Конкретная информация о преобразованиях указателей содержится в Вашем Руководстве по компилятору. Преобразованные типы каждого формального параметра определяют интерпретацию аргументов, которые вызов функции помещает в стек. Несоответствие типа действительного и формального параметра может вызвать неверную интерпретацию аргументов в стеке. Например, если в качестве действительного аргумента передан 16-битовый указатель, а затем объявлен формальным параметром long, то первые 32 бита стека интерпретируются как формальный параметр long. Эта ошибка вызывает проблемы не только с формальным параметром long, но и со всеми формальными параметрами, которые за ним следуют. Ошибки такого рода обнаруживаются при объявлении прототипов для всех функций. Пример Данный пример содержит объявление типа структуры, прототипа для функции match, вызова match и определение прототипа match. Обратите внимание на то, что одно и то же имя student может быть использовано без конфликта и для признака структуры, и для имени переменной структуры. Функция match объявляется с двумя аргументами: r - указатель на тип struct student; n - указатель на значение типа char. В определении функции match в списке формальных параметров, который стоит в скобках за именем функции, задаются два формальных параметра с идентификаторами r и n. Параметр r объявлен как указатель на тип struct student, а параметр n объявлен как указатель на значение типа char. Функция вызывается с двумя аргументами, они оба являются компонентами структуры student. У match имеется прототип, поэтому компилятор проводит проверку типа между действительными аргументами, заданными в прототипе типами и формальными параметрами в определении. Если типы совпадают, то нет необходимости в выдаче предупреждающего сообщения или преобразовании. Обратите внимание на то, что заданное в качестве второго аргумента в вызове функции имя массива вычисляется в указатель char. Соответствующий формальный параметр также задан, как указатель char, и используется в выражении индекса так, как если бы он был идентификатором массива. Идентификатор массива вычисляется в выражение указателя, поэтому эффект от объявления формального параметра char *n будет тот же, что и при объявлении char n[]. В функции локальная переменная i определяется и используется для управления текущей позицией в массиве. Функция возвращает компонент структуры id, если компонент name соответствует массиву n, в противном случае возвращается 0. struct student { char name[20]; int id; long class; struct student *nextstu; } student; main() { /* объявление прототипа функции */ int match ( struct student *r, char *n ); . . . if (match (student.nextstu, student.name) > 0) { . . . } } /* определение функции в стиле прототипа */ match ( struct student *r, char *n ) { int i = 0; while ( r->name[i] == n[i] ) if ( r->name[i++] == '\0' ) return (r->id); return (0); } Тело функции "Тело функции" это составной оператор, который состоит из операторов, задающих конкретное действие функции. Тело функции кроме того может содержать объявления переменных, которые используются этими операторами. (Составные операторы рассматриваются в Разделе 6.3.) Все объявленные в теле функции переменные имеют класс хранения auto, если явно не задан другой класс. При вызове функции локальным переменным выделяется память и выполняется их локальная инициализация. Управление выполнением программы передается на первый оператор составного оператора и работа происходит до выполнения оператора return или прохода до конца тела функции. Затем управление возвращается на точку, из которой был произведен вызов функции. Если функция должна возвращать значение, то должен быть выполнен оператор return, содержащий выражение. Если в операторе return нет выражения или оператор return не выполняется, то возвращаемое функцией значение не определено. Прототипы функций (Объявления) Объявление "прототипа функции" задает имя, возвращаемый тип и класс хранения функции. Кроме того, в нем могут быть установлены типы и идентификаторы для некоторых или всех аргументов функции. Прототип имеет тот же формат, что и определение функции, за тем исключением, что у него нет тела и он заканчивается точкой с запятой сразу же после закрывающей скобки. (См. Главу "Объявления" для детального описания синтаксиса объявлений функций. Можно объявить функцию неявно или использовать прототип функции (иногда называемый "ранним объявлением") для ее явного объявления. прототип это объявление, которое предшествует определению функции. В любом случае возвращаемый тип должен совпадать с заданным в определении функции. Если вызов функции происходит до ее объявления или определения, то конструируется прототип функции по умолчанию, а возвращаемый тип устанавливается int. Тип и число действительных аргументов используются как основа для объявления формальных параметров. Т.о. вызов функции является ее неявным объявлением, но генерируемый при этом прототип может неадекватно представлять последующее определение или вызов функции. Прототип устанавливает атрибуты функции таким образом, что вызовы функции, которые предшествуют ее определению (или появлению в других исходных файлах), могут быть проверены на соответствие типов аргументов и возвращаемого значения. Если в прототипе задан спецификатор класса хранения static, то в определении функции также должен быть задан класс хранения static. Если задан спецификатор класса хранения extern или он не указан совсем, то функция имеет класс extern. Прототипы функций имеют следующие важные применения: ╥ Можно установить любой отличный от int возвращаемый тип. При вызове такой функции до ее объявления или определения результат будет неопределен. Хотя для возвращающих значения int функций прототип и не требуется, рекомендуется его использовать. ╥ Если прототип содержит полный список типов параметров, то могут быть проверены типы аргументов, появляющиеся в вызове функции или ее определении. Прототип может содержать как типы, так и идентификаторы для каждого выражения, которое будет передаваться в качестве действительного аргумента. Однако, такие идентификаторы имеют сферу действия только до конца объявления. В прототипе может быть отражен факт переменного числа аргументов функции или их полного отсутствия. Список параметров в прототипе это список имен типов, разделенных запятыми, которые соответствуют действительным аргументам в вызове функции. Этот список используется для проверки соответствия действительных аргументов в вызове функции с формальными параметрами в определении функции. Без такого списка нельзя установить несоответствие, и компилятор не сможет выдать диагностическое сообщение. (Проверка типов рассматривается в Разделе 7.4.1, "Действительные аргументы".) ╥ Прототипы используются для инициализации указателей на функции до определения этих функций. Пример В данном примере неявно определяется функция intadd, возвращающая значение int, т.к. ее вызов происходит до ее определения. Компилятор создает прототип, используя информацию первого вызова. Дойдя до второго вызова intadd, компилятор видит несоответствие float величины val1 и первого аргумента int в созданном им прототипе. float преобразуется в int и передается в функцию. Заметим, что если бы вызовы функции поменять местами, то созданный прототип ожидал бы первым аргументом float в вызове intadd. При втором вызове функции переменная a будет преобразована, но в момент передачи его значения в intadd будет выдано диагностическое сообщение, т.к. заданный в определении тип int не соответствует типу float в созданном компилятором прототипе. Функция realadd возвращает величину double вместо int. Следовательно, прототип realadd в функции main необходим, т.к. функция realadd вызывается до своего определения. Обратите внимание на то, что определение realadd соответствует раннему объявлению по возвращаемому типу double. В раннем объявлении realadd также устанавливаются типы ее двух аргументов. Типы действительных аргументов соответствуют типам, заданным в объявлении, и типам формальных параметров в определении. main() { int a=0, b=1; float val1=2.0, val2=3.0; /* прототип функции */ double realadd(double x, double y); a=intadd(a,b); /* первый вызов intadd */ val1=realadd(val1, val2); a=intadd(val1,b); /* второй вызов intadd */ } /* определение функции с формальными параметрами в заголовке */ intadd(int a, int b) { return (a+b); } double realadd(double x, double y) { return (x+y); } Вызовы функций Синтаксис: выражение([список-выражений]) "Вызов функции" это выражение, которое передает управление и действительные аргументы, если они есть, функции. В вызове функции "выражение" вычисляется для получения адреса функции, а "список-выражений" это список разделенных запятыми выражений. Значения этих выражений являются действительными аргументами, передаваемыми в функцию. Если у функции нет аргументов, то список выражений может быть пустым. При выполнении вызова функции: 1. Вычисляются выражения из списка выражений и преобразуются по правилам обычных арифметических преобразований. Если имеется прототип функции, то результаты этих преобразований могут быть далее преобразованы в соответствии с объявлениями формальных параметров. 2. Выражения из списка выражений передаются в формальные параметры вызванной функции. Первое выражение списка всегда соответствует первому формальному параметру функции, второе выражение соответствует второму формальному параметру, и т.д. по списку. Вызванная функция использует копии действительных аргументов, поэтому все изменения, которые она сделает с аргументами, не повлияют на значения переменных, с которых были сделаны копии. 3. Управление выполнением передается на первый оператор функции. 4. Выполнение оператора return в теле функции возвращает управление и возвращаемое значение, если оно есть, на вызов функции. Если оператор return не выполняется, то управление передается на вызов после выполнения последнего оператора вызванной функции. В этом случае возвращаемое значение не определено. Примечание Выражения в списке аргументов функции могут быть вычислены в любом порядке, поэтому аргументы, значения которых могут быть изменены побочными эффектами из других аргументов, имеют неопределенные значения. Точка упорядочивания, задаваемая оператором вызова функции, гарантирует только то, что все побочные эффекты в списке аргументов вычисляются до передачи управления на вызванную функцию. Дополнительная информация по точкам упорядочивания содержится в Главе "Выражения и Присвоения". Единственное требование к вызову функции состоит в том, что выражение до скобок должно вычисляться в адрес функции. Это означает, что функция может быть вызвана через любое выражение указателя на функцию. Функция вызывается практически аналогично своему объявлению. Например, при объявлении функции задается имя функции, за которым в скобках следует список формальных параметров. Аналогично, при вызове функции нужно задать ее имя, за которым следует список аргументов в скобках. Для вызова функции не требуется оператор адресации (*), т.к. имя функции вычисляется в ее адрес. При вызове функции с использованием указателя применяются те же принципы. Предположим, например, что указатель функции имеет следующий прототип: int (*fpointer) (int num1, int num2); Идентификатор fpointer объявлен для указания на функцию, которая принимает два аргумента int, соответственно num1 и num2, и возвращает значение int. Вызов функции с использованием fpointer может выглядеть так: (*fpointer) (3,4) Оператор адресации (*) используется для получения адреса функции, на которую указывает fpointer. Затем этот адрес используется для вызова функции. Если прототип указателя на функцию предшествует вызову, то будет выполнена та же проверка, что и с любой другой функцией. Пример 1 В данном примере функция realcomp вызывается в операторе rp=realcomp(a,b);. В функцию передаются два аргумента double. Возвращаемое значение, указатель на величину double, присваивается rp. double *realcomp(double value1, double value2); double a, b, *rp; . . . rp=realcomp(a,b); Пример 2 В данном примере вызванная в main функция передает целую переменную и адрес функции lift в функцию work: work (count, lift); Обратите внимание на то, что адрес функции передается просто заданием идентификатора функции, т.к. идентификатор функции вычисляется в выражение указателя. Для использования идентификатора функции таким способом функция должна быть объявлена или определена до использования идентификатора, иначе идентификатор не будет распознан. В этом случае прототип work задается в начале функции main. Формальный параметр function в work объявляется как указатель на функцию, которая принимает один аргумент int и возвращает значение long. Обязательно требуется задать скобки вокруг параметра, без них объявление будет задавать функцию, возвращающую указатель на значение long. Функция work вызывает выбранную функцию следующим образом: (*function)(i); Вызванной функции передается один аргумент, i. main () { /* прототипы функций */ long lift(int), step(int), drop(int); void work (int number, long (*function)(int i); int select, count; . . . select=1; switch ( select ) { case 1: work(count, lift); break; case 2: work(count, step); break; case 3: work(count, drop); default: break; } } /* определение функции с формальными параметрами в заголовке */ void work ( int number, long (*function)(int i) ) { int i; long j; for (i=j=0; i (y)) ? (x) : (y) где действительные значения заменят параметры x и y. Например, появление MAX(1,2) будет заменено на ((1) > (2)) ? (1) : (2) и появление MAX(i,s[i]) будет заменено на ((i) > (s[i])) ? (i) : (s[i]) #define MAX(x,y) ((x) > (y)) ? (x) : (y) Макро легче читается, чем соответствующее выражение, поэтому легче понять исходный текст программы. Обратите внимание на то, что аргументы с побочными эффектами могут дать неожиданные результаты при выполнении макро. Например, появление MAX(i, s[i++]) будет заменено на ((i)>(s[i++]))?(i): (s[i++]). Выражение (s[i++]) может быть вычислено дважды, поэтому после вычисления тернарного оператора i может быть увеличено единожды или дважды, в зависимости от результатов сравнения. Пример 5 В данном примере определяется макро MULT. После определения этого макро появление выражений подобных MULT(3,5) будет заменены на (3)*(5). Скобки вокруг параметров важны, т.к. управление интерпретацией сложных выражений формирует аргументы макро. Например, появление MULT(3+4,5+6) будет заменено на (3+4)*(5+6), что даст в результате 77. Без скобок получаем 3+4*5+6, или 29, т.к. оператор умножения (*) имеет приоритет выше, чем оператор сложения (+). #define MULT(a,b) ((a) * (b)) Пример 6 В данном примере определяются два макро. Объектное макро раскрывается в строковый литерал Hello,Word!, а другое функциональное макро вызывает show, которое берет один аргумент. Однако, определение второго макро включает строковый оператор (#), который непосредственно предшествует формальному параметру x. При передаче аргумента в макро show формальный параметр будет заменен действительным аргументом, заключенным в двойные цитатные скобки. #define GREETING Hello, World! #define show(x) printf(#x) main() { show(x+z); printf("\n"); show(n /* some comment */ + p); printf("\n"); show(GREETING); /* GREETING не раскрыто; */ printf("\n"); /* вместо этого отработал */ show ('\x'); /* строковый оператор */ } По мере обработки предпроцессором исходного файла ссылки на show будут раскрыты так: show (x+z); даст printf("x+z"); show (n/*ccomment*/+p); даст printf("n+p"); show (GREETING); даст printf("GREETING"); show ('\x'); даст printf("'\\x'"); При работе программы на экран будет выведено: x+z n+p GREETING '\x' Пример 7 Данный пример показывает использование строкового оператора и оператора вставки лексем для организации вывода программы. #define paster(n) printf("token" #n " = %d", token##n) Если объявлен token9 и макро вызывается с числовым аргументом, подобно: paster(9); то макро преобразуется в вид: printf("token" "9" " = %d", token9); который станет printf("token9 = %d", token9); Директива #undef Синтаксис: #undef идентификатор Директива #undef удаляет текущее определение идентификатора. Поэтому все встречающиеся появления идентификатора будут игнорироваться предпроцессором. Для удаления определения макро с использованием #undef, нужно задать только идентификатор макро, не задавая список параметров. Можно применить директиву #undef к идентификатору, у которого нет определения. Тем самым пользователь получает дополнительную гарантию того, что данный идентификатор не определен. Директива #undef обычно используется в паре с директивой #define для задания области исходной программы, в которой идентификатор имеет специальное значение. Например, некоторая функция исходной программы может иметь объявленные константы, которые задают значения среды работы, которые не влияют на остальную часть программы. Директива #undef также работает с директивой #if (см. Раздел 8.4.1) для управления условной компиляцией исходной программы. Пример В данном примере директива #undef удаляет определения объявленных констант и макро. Обратите внимание на то, что задается только идентификатор макро. #define WIDTH 80 #define ADD(X,Y) (X) + (Y) . . . #undef WIDTH #undef ADD Включаемые файлы Синтаксис: #include "спецификация-пути" #include <спецификация-пути> v Директива #include добавляет содержимое заданного файла в другой файл. Можно организовать определения констант и макро в отдельном файле, а затем вставить его директивой #include в любой другой файл. Вставка файлов также очень удобна для объединения объявлений внешних переменных и сложных типов данных. Нужно определить и задать имена этих типов только один раз в созданный для этих целей файл. Директива #include информирует предпроцессор о том, что содержание файла с заданным именем следует обрабатывать так, как будто оно присутствует в исходной программе в месте расположения этой директивы. Новый текст также может содержать директивы предпроцессора. Предпроцессор выполняет директивы в новом тексте, а затем продолжает обработку текста исходного файла. "Спецификация пути" это имя файла, которому может предшествовать директория. Это должно быть имя существующего файла. Синтаксис спецификации файла зависит от операционной системы, в которой компилируется программа. При поиске файлов предпроцессор использует концепцию "стандартной" директории. Расположение стандартных директорий для файлов зависит от реализации и операционной системы. Определение стандартной директории можно найти в Вашем Руководстве по компилятору. Предпроцессор останавливает поиск сразу же после обнаружения файла с заданным именем. Если задать полную спецификацию файла, заключенную в двойные цитатные скобки (" "), то предпроцессор использует ее для поиска и игнорирует стандартную директорию. Если заключенная в двойные цитатные скобки спецификация файла является неполной, то предпроцессор сначала ищет директорию "родительского" файла. Родительский файл это файл, содержащий директиву #include. Например, если файл fil2 вставляется в файл fil1, то fil1 будет родительским файлом. Вставка файлов может быть вложенной. Т.е. директива #include может появляться в файле, который сам вставляется директивой #include. Например, в приведенном выше примере файл fil2 может вызывать файл fil3. В этом случае fil1 все еще будет родительским для fil2, но "дедушкой" для fil3. При вложенной вставке файлов поиск директории начинается с родительского файла, затем проходит по дедушкиным файлам. Следовательно, поиск начинается в директории, которая содержит обрабатываемый исходный файл. Если файл не найден, то поиск продолжается в директориях, заданных в командной строке компилятора. И, наконец, производится поиск в стандартной директории. Если спецификация файла заключена в угловые скобки, то предпроцессор не проводит поиска в текущем рабочем каталоге. Поиск файла начинается в директориях, заданный в командной строке компилятора, а затем в стандартной директории. Допускается вложение вставки файлов до 10 уровней. При обработке вложенных #include предпроцессор всегда будет осуществлять вставку в первоначальный исходный файл. Пример 1 В данном примере содержимое файла с именем stdio.h будет вставлено в исходную программу. Угловые скобки заставляют предпроцессор искать stdio.h в стандартной директории, после поиска в директориях, указанных в командной строке. #include Пример 2 Данный пример добавит в исходный файл содержимое файла defs.h. Двойные цитатные скобки означают, что предпроцессор начнет поиск в родительском каталоге. #include "defs.h" Условная компиляция Данный раздел описывает синтаксис и использование директив, которые управляют "условной компиляцией". Эти директивы позволяют подавить компиляцию части исходного файла, проверяя постоянное выражение или идентификатор. Результат проверки определяет, какие блоки текста будут переданы в компилятор и какие блоки текста будут удалены из исходного файла при предпроцессорной обработке. Директивы #if, #elif, #else и #endif Синтаксис: #if граничное-постоянное-выражение [блок-текста] [#elif граничное-постоянное-выражение блок-текста] [#elif граничное-постоянное-выражение блок-текста] . . . [#else блок-текста] #endif Директива #if вместе с директивами #elif, #else и #endif управляют компиляцией части исходного файла. Каждая директива #if в исходном файле должна иметь соответствующую закрывающую директиву #endif. Между директивами #if и #endif может появиться любое число директив #elif, но допускается наличие только одной директивы #else. Если имеется директива #else, то она должна быть последней директивой перед #endif. Предпроцессор выбирает одно из заданных появлений блока текста для дальнейшей обработки. Этот блок может быть любой последовательностью текста. Он может занимать несколько строк. Обычно блок текста это текст программы, который имеет значение для компилятора или предпроцессора. Предпроцессор обрабатывает выбранный блок текста и передает его компилятору. Если блок текста содержит директивы предпроцессора, то предпроцессор выполнит эти директивы. Все блоки текста, не выбранные предпроцессором, удаляются из обрабатываемого файла. Следовательно, эти блоки текста не компилируются. Предпроцессор выбирает отдельный блок текста вычисляя выражение граничной константы, которое следует за каждой директивой #if или #elif, пока результатом выражения граничной константы не будет "истина" (не ноль). Выбирается весь текст (включая начинающиеся с # другие директивы предпроцессора) до соответствующего #elif, #else или #endif. Если значением всех выражений граничных констант будет "ложь", или нет директивы #elif, то предпроцессор выберет блок текста после предложения #else. Если предложения #else нет, то блок вообще не выбирается. Каждое выражение граничной константы соответствует правилам Раздела 5.2.10. Эти выражения не могут содержать выражений sizeof, приведения типа или перечислимых констант. Однако, они могут содержать оператор предпроцессора defined в специальном постоянном выражении, имеющем следующий синтаксис: defined(идентификатор) Это постоянное выражение будет иметь значение "истина" (не ноль), если заданный идентификатор определен, в противном случае - "ложь" (0). Идентификатор, определенный как пустой текст, считается определенным. Директивы #if, #elif, #else и #endif могут быть вложены в другие директивы #if. Каждая вложенная директива #else, #elif или #endif принадлежит к ближайшей к ней директиве #if. Пример 1 В данном примере директивы #if и #endif управляют компиляцией одного из трех вызовов функции. Вызов функции credit компилируется, если определен идентификатор CREDIT. Если определен идентификатор DEBIT, то компилируется вызов функции debit. Если не определен ни один из идентификаторов, то компилируется вызов printerror. Обратите внимание на то, что CREDIT и credit это разные идентификаторы в языке С. #if defined(CREDIT) credit(); #elif defined(DEBIT) debit(); #else printerror(); #end Пример 2 Примеры 2 и 3 предполагают наличие ранее определенной константы с именем DLEVEL. Пример 2 показывает два вложенных набора директив #if, #else и #endif. Первый набор директив будет обработан только если значение DLEVEL>5 "истина". В противном случае будет обработан второй набор директив. #if DLEVEL>5 #define SIGNAL 1 #if STACKUSE == 1 #define STACK 200 #else #define STACK 100 #endif #else #define SIGNAL 0 #if STACKUSE == 1 #define STACK 100 #else #define STACK 50 #endif #endif Пример 3 В примере 3 директивы #elif и #else применятся для того, чтобы сделать выбор одного из четырех вариантов на основании значения DLEVEL. Объявленная константа STACK устанавливается в 0, 100 или 200 в зависимости от определения DLEVEL. Если DLEVEL больше 5, то компилируется display(debugptr); и STACK не определяется. #if DLEVEL == 0 #define STACK 0 #elif DLEVEL == 1 #define STACK 100 #elif DLEVEL > 5 display(debugptr); #else #define STACK 200 #endif Пример 4 В данном примере директивы предпроцессора используются для управления значением объявления register в мобильном исходном файле. Компилятор выделяет регистровую память переменным в той последовательности, в которой объявления register появляются в исходном файле. Если в программе присутствуют больше объявлений register, чем допускает компьютер, то компилятор дает приоритет более ранним объявлениям над более поздними. Программа может стать менее эффективной, если наиболее часто используемые переменные объявлены слишком поздно. #define REG1 register #define REG2 register #if defined(M_86) #define REG3 #define REG4 #define REG5 #else #define REG3 register #if defined(M_68000) #define REG4 register #define REG5 register #else #define REG4 register #define REG5 #endif #endif Приведенные в Примере 4 определения можно использовать для задания высшего приоритета наиболее важным объявлениям регистров. REG1 и REG2 определены с ключевым словом register и для этих двух наиболее важных переменных программы выделяется регистровая память. Например, в следующем фрагменте b и c имеют приоритет выше, чем a и d: func(a) REG3 int a; { REG1 int b; REG2 int c; REG4 int d; . . . } Если определен M_86, то предпроцессор удалит идентификатор REG3 из файла, заменив его пустым текстом. а не будет выделена регистровая память за счет b и с. Если задан M_68000, то всем четырем переменным выделяется регистровая память. Если не задан ни М_86 ни М_68000, то регистровая память выделяется а, b и с. Директивы #ifdef и #ifndef Синтаксис: #ifdef идентификатор #ifndef идентификатор Директивы #ifdef и #ifndef выполняют те же функции, что и директива #if с defined(идентификатор). Директивы #ifdef и #ifndef можно использовать везде, где допустимо использование #if. Эти директивы реализованы только для обеспечения совместимости с предыдущими версиями языка. Предпочтительно использовать постоянное выражение defined(идентификатор) c с директивой #if. Когда предпроцессор обнаруживает директиву #ifdef, он проверяет, определен ли идентификатор. Если это так, то значение условия "истина" (не ноль), и "ложь" (0) в противном случае. Директива #ifndef проверяет условие, противоположное условию #ifdef. Если идентификатор не определен (или его определение удалено с помощью #undef), то условие "истина", и "ложь" (0) в противном случае. Управление Line Синтаксис: #line константа["имя-файла"] Директива #line дает команду компилятору сменить хранимые им номер строки и имя файла на данные. Компилятор использует номер строки и имя файла для указания ошибок, которые он обнаруживает при компиляции. Номер строки обычно соответствует текущей строке, а имя файла - текущему файлу. После обработки каждой строки номер строки увеличивается. Если сменить номер строки и имя файла, то компилятор игнорирует предыдущие значения и продолжит обработку с новыми значениями. Директива #line обычно используется генераторами программ для появления сообщений об ошибках в исходном тексте, а не в генерируемой программе. Константа в директиве #line может быть любой целой константой. Имя файла может быть любой комбинацией символов, заключенной в двойные цитатные скобки. Если имя файла не задано, то предыдущее имя файла остается без изменения. Всегда имеется доступ к текущему номеру строки и имени файла через заранее определенные идентификаторы __LINE__ и __FILE__. Эти идентификаторы можно использовать в самодокументирующихся сообщения об ошибках, помещенных в текст программы. Идентификатор __FILE__ раскрывается в строку, содержимое которой есть имя файла, заключенное в двойные цитатные скобки. Пример 1 В данном примере хранимый номер строки меняется на 151, а имя файла меняется на copy.c . #line 151 "copy.c" Пример 2 В данном примере макро ASSERT использует ранее определенные идентификаторы __LINE__ и __FILE__ для печати сообщения об ошибке в исходном файле, если значение условия "ложь". #define ASSERT(cond) if(!cond)\ {printf("assertion error line %d, file(%s)\n",\ __LINE__, __FILE__ );} else Прагмы Синтаксис: #pragma последовательность-символов #pragma это инструкция компилятору, которая определяется реализацией. Последовательность символов задает конкретную инструкцию компилятору и аргументы, если они есть. Знак номера (#) должен быть первым неразделительным символом на строке, содержащей прагму. Между знаком номера и словом pragma могут стоять разделительные символы. Информация об имеющихся в реализации Вашего компилятора прагмах содержится в Вашем Руководстве по компилятору.