Чем определяется мобильность языка си

Чем определяется мобильность языка си

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

Особенности мобильного программирования на языке Си

Особая роль языка программирования Си состоит в том, что он, с одной стороны, позволяет писать для UNIX-систем практически столь же эффективный код, что и языки ассемблера, а с другой, является основным средством переноса программ между UNIX-системами. Можно сказать, что Си является машинно-независимым языком ассемблера для UNIX-систем. Это делает его основным средством написания эффективных и переносимых программ для этого класса вычислительных систем. Стандартизация языка сначала Американским национальным институтом стандартов (ANSI), а затем и Международной организацией по стандартам (ISO) закрепила эту роль, распространив ее и на персональные компьютеры. Будем ссылаться на версию языка Си, определенную стандартом, как на язык ANSI C.

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

Программа на ANSI C переносима из исходной ВС в целевую, если она успешно компилируется в целевой ВС и ее работа функционально эквивалентна работе в исходной ВС.

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

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

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

Наконец, семантика многих стандартных библиотечных функций (например, функций ввода/вывода) зависит от особенностей операционной системы.

Все перечисленные факторы учтены в определении ANSI C путем фиксирования неуточняемого (стандартом) поведения программ, неопределенного поведения программ и поведения программ, определяемого реализацией.

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

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

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

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

можно заменить на последовательность операторов

в зависимости от нужного порядка вызова функций f() и g().

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

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

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

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

Поведение, зависящее от реализации

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

Семантика фактических параметров функции main.

Для облегчения переноса программы полезно локализовать обработку внешних аргументов.

Число значащих начальных символов (сверх 31) в идентификаторе без внешней связи.

В переносимой программе не используется свыше 31 значащего символа в идентификаторах без внешней связи.

Число значащих начальных символов (сверх 6) в идентификаторе с внешней связью.

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

Имеет ли значение регистр символов, входящих в идентификаторы с внешней связью.

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

Символы входного алфавита, кроме явно определенных в стандарте.

Это касается, в основном, символов, используемых в символьных и строковых константах (например, русские буквы).

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

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

Соответствие символов входного алфавита (в символьных и строковых константах) символам алфавита времени выполнения.

В основном это касается управляющих символов. Например, символ «конец строки» (\n) в разных реализациях может быть представлен в потоках ввода-вывода различными последовательностями кодов. Надо стараться писать программу так, чтобы ее поведение не зависело от конкретного представления управляющих символов в окружении выполнения.

Число и порядок символов в целом.

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

Число и порядок следования разрядов в символах из набора символов времени выполнения.

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

Переносимой программе не следует использовать информацию этих двух пунктов.

Значение символьной константы, состоящей более, чем из одного символа.

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

Следует ли трактовать «простые» символьные объекты как знаковые или беззнаковые.

Переносимая программа не должна зависеть от того, является ли тип char знаковым или беззнаковым.

Представление и наборы значений различных целочисленных типов.

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

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

Переносимая программа не использует эту информацию.

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

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

Знак остатка целочисленного деления.

Переносимая программа не использует эту информацию.

Является ли сдвиг вправо значения знакового целочисленного типа логическим или арифметическим.

Переносимая программа не должна зависеть от вида сдвига вправо знаковых целых.

Представление и наборы значений различных типов вещественных чисел.

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

Способ округления, когда вещественное число преобразуется к более узкому вещественному числу.

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

Результат преобразования указателя в целое и наоборот.

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

Элемент смеси union используется как элемент другого типа.

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

Дополнение пустот и выравнивание элементов записей.

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

Считается ли «простое» целое битовое поле знаковым или беззнаковым.

Переходит ли битовое поле, не умещающееся в одном целом, в следующее.

Порядок расположения битовых полей в целом.

Может ли битовое поле пересекать физические границы ячеек памяти.

Переносимая программа не должна использовать всю эту информацию.

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

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

Максимальное число вариантов в переключателе.

Переносимая программа должна исходить из того, что число вариантов в переключателе не должно превышать 255.

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

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

Обработка имен в кавычках, относящихся к включаемым файлам.

Поведение каждой директивы #pragma.

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

Константа, получающаяся при подстановке макроопределения NULL, обозначающая пустой указатель.

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

Диагностическое сообщение и способ завершения программы, применяемый в функции assert.

Наборы символов, проверяемые в функциях isalnum, isalpha, iscntrl, islower, isprint и isupper.

Значения, выдаваемые математическими функциями при возникновении ошибок области определения.

Устанавливают ли математические функции целое выражение errno в положение ERANGE при возникновении потери значимости.

Набор сигналов для функции signal.

Семантика каждого сигнала, распознаваемого библиотечной функцией signal.

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

Восстанавливается ли стандартная обработка, если при обработке сигнала функцией, указанной при вызове функции signal, возникает сигнал SIGILL.

Нужно ли заканчивать последнюю строку текстового потока символом «конец строки».

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

Количество символов NULL, которые дописываются к двоичному потоку.

Характеристики буферизации файлов.

Существует ли файл нулевой длины.

Правила образования правильных имен файлов.

Может ли один файл открываться много раз.

Результат выполнения функции remove над открытым файлом.

Эффект работы функции rename, если файл с новым именем существовал ранее.

Выходная строка, получающаяся при работе преобразования %p в функции fprintf.

Входная строка, поступающая для преобразования %p в функции fscanf.

Интерпретация символа ^, который есть ни первый, ни последний символ в списке сканирования в преобразовании %[ в функции fscanf.

Значение, которое получает errno от функций fgetpos и ftell в случае неудачи.

Сообщения, выдаваемые функцией perror.

Поведение функций calloc, malloc и realloc в случае, если размер запрошенной памяти равен нулю.

Поведение функции abort по отношению к открытым и временным файлам.

Статус, возвращаемый функцией exit, если значение фактического параметра не равно нулю, или значениям макроимен EXIT_SUCCESS и EXIT_FAILURE.

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

Содержание и режим выполнения командной строки функцией system.

Знак значения, возвращаемого функцией сравнения (memcmp, strcmp или strncmp), если первая пара различающихся символов разнится в старшем разряде.

Содержание строк сообщений об ошибках, возвращаемых функцией strerror.

Местный временной пояс и летнее время.

Точка отсчета для функции clock.

Метрические ограничения переносимой программы

Обеспечение независимости от особенностей версии ОС UNIX

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

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

Источник

Введение в Си. Послание из прошлого столетия

Предисловие

Я несколько раз в своих комментариях ссылался на книгу Эндрю Таненбаума «Operating Systems Design and Implementation» на ее первое издание и на то, как в ней представлен язык Си. И эти комментарии всегда вызывали интерес. Я решил, что пришло время опубликовать перевод этого введения в язык Си. Оно по-прежнему актуально. Хотя наверняка найдутся и те, кто не слышал о языке программировании PL/1, а может даже и об операционной системе Minix.

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

Хочу сразу оговориться, что мой второй язык французский:

Чем определяется мобильность языка си. Смотреть фото Чем определяется мобильность языка си. Смотреть картинку Чем определяется мобильность языка си. Картинка про Чем определяется мобильность языка си. Фото Чем определяется мобильность языка си

Но это компенсируется 46-летним программистским стажем.
Итак, приступим, наступила очередь Эндрю Таненбаума.

Введение в язык Си (стр. 350 — 362)

Чем определяется мобильность языка си. Смотреть фото Чем определяется мобильность языка си. Смотреть картинку Чем определяется мобильность языка си. Картинка про Чем определяется мобильность языка си. Фото Чем определяется мобильность языка си

Язык программирования Cи был создан Деннисом Ритчи из AT&T Bell Laboratories как язык программирования высокого уровня для разработки операционной системы UNIX. В настоящее время язык широко используется в различных областях. C особенно популярен у системных программистов, потому что позволяет писать программы просто и кратко.

Основной книгой, описывающая язык Cи, является книга Брайана Кернигана и Денниса Ритчи « Язык программирования Cи» (1978). Книги по языку Си писали Bolon (1986), Gehani (1984), Hancock and Krieger (1986), Harbison и Steele (1984) и многие другие.

В этом приложении мы попытаемся дать достаточно полное введение в Cи, так что те кто знаком с языками высокого уровня, такими как Pascal, PL/1 или Modula 2, смогут понять большую часть кода MINIX, приведенного в этой книге. Особенности Cи, которые не используются в MINIX, здесь не обсуждаются. Многочисленные тонкие моменты опущены. Акцент делается на чтении программ на Си, а не на написании кода.

А.1. Основы языка Си

Программа на Cи состоит из набора процедур (часто называемых функциями, даже если они не возвращают значений). Эти процедуры содержат объявления, операторы и другие элементы, которые вместе говорят компьютеру что надо делать. На рисунке A-1 показана небольшая процедура, в которой объявляются три целочисленные переменные и присваиваются им значения. Имя процедуры — main (главная). Процедура не имеет формальных параметров, на что указывает отсутствие каких-либо идентификаторов между скобками за именем процедуры. Тело процедуры заключено в фигурные скобки ( < >). Этот пример показывает, что Cи имеет переменные, и что эти переменные должны быть объявлены до использования. Cи также имеет операторы, в этом примере это операторы присваивания. Все операторы должны заканчиваться точкой с запятой (в отличие от Паскаля, который использует двоеточия между операторами, а не после них).

Комментарии начинаются с символов « / *» и заканчивается символами «* /» и могут занимать несколько строк.

Процедура содержит три константы. Константа 10 в первом присваивании
это обычная десятичная константа. Константа 015 является восьмеричной константой
(равно 13 в десятичной системе счисления). Восьмеричные константы всегда начинаются с начального нуля. Константа 0xFF является шестнадцатеричной константой (равной 255 десятичной). Шестнадцатеричный константы всегда начинаются с 0x. Все три типа используются в Cи.

А.2. Основные типы данных

Cи имеет два основных типа данных (переменных): целое и символ, объявляемые как int и char, соответственно. Нет отдельной булевой переменной. В качестве булевой переменной используется переменная int. Если эта переменная содержит 0, то это означает ложь/false, а любое другое значение означает истина/true. Cи также имеет и типы с плавающей точкой, но MINIX не использует их.

Спецификатор register также допускается как для int, так и для char, и является подсказкой для компилятору, что объявленную переменную стоит поместить в регистр, чтобы программа работала быстрее.

Некоторые объявления показаны на рис. А — 2.

Преобразование между типами разрешено. Например, оператор

разрешен, даже если i имеет тип int, а flag_pole — long. Во многих случаях
необходимо или полезно принудительно проводить преобразования между типами данных. Для принудительного преобразования достаточно поставить целевой тип в скобках перед выражением для преобразования. Например:

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

При преобразовании между типами следует обратить внимание на знак.
При преобразовании символа в целое число некоторые компиляторы обрабатывают символы как знаковые, то есть от — 128 до +127, тогда как другие рассматривают их как
без знака, то есть от 0 до 255. В MINIX часто встречаются такие выражения, как

которые преобразует с (символ) в целое число, а затем выполняет логическое И
(амперсанд) с восьмеричной константой 0377. В результате получается, что старшие 8 бит
устанавливаются в ноль, фактически заставляя рассматривать c как 8-битное число без знака, в диапазоне от 0 до 255.

А.3. Составные типы и указатели

В этом разделе мы рассмотрим четыре способа построения более сложных типов данных: массивы, структуры, объединения и указатели (arrays, structures, unions, and pointers). Массив — это коллекция/множество элементов одного типа. Все массивы в Cи начинаются с элемента 0.

объявляет массив a с 10 целыми числами, которые будут хранится в элементах массива от [0] до a [9]. Второе, массивы могут быть трех и более измерений, но они не используются в MINIX.
Структура — это набор переменных, обычно разных типов. Структура в Cи похож на record в Паскале. Оператор

объявляет s как структуру, содержащую два члена, целое число i и символ c.

Чтобы присвоить члену i структуры s значение 6, нужно записать следующее выражение:

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

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

Указатели используются для хранения машинных адресов в Cи. Они используются очень и очень часто. Символ звездочка (*) используется для обозначения указателя в объявлениях. Объявление

объявляет целое число i, указатель на целое число pi, массив a из 10 элементов, массив b из 10 указателей на целые числа и указатель на указатель ppi на целое число.

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

На рисунке A-3 показано объявление массива z структур struct table, каждая из которых имеет
три члена, целое число i, указатель cp на символ и символ с.

Массивы структур распространены в MINIX. Далее, имя table можно объявить как структуру struct table, которую можно использовать в последующих объявлениях. Например,

объявляет p указателем на структуру struct table и предлагает сохранить ее
в register. Во время выполнения программы p может указывать, например, на z [4] или
на любой другой элемент в z, все 20 элементов которой являются структурами типа struct table.

Чтобы сделать p указателем на z [4], достаточно написать

где амперсанд в качестве унарного (монадического) оператора означает «взять адрес того, что за ним следует ». Скопировать в целочисленную переменную n значение члена i
структуры, на которую указывает указатель р, можно следующим образом:

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

Разница в том, что z [4] является структурой, и оператор точки выбирает элементы
из составных типов (структуры, массивы) напрямую. С помощью указателей мы не выбираем участника напрямую. Указатель предписывает сначала выбрать структуру и только потом выбрать члена этой структуры.

Иногда удобно дать имя составному типу. Например:

определяет unshort как unsigned short (короткое целое число без знака). Теперь unshort может быть использован в программе как основной тип. Например,

объявляет короткое целое число без знака, указатель на короткое целое число без знака и
массив коротких целых без знака.

А.4. Операторы

Процедуры в Cи содержат объявления и операторы. Мы уже видели объявления, так что теперь мы будем рассматривать операторы. Назначение условного оператора и операторов цикла по существу такие же, как и в других языках. Рисунок А – 4 показывает несколько примеров из них. Единственное, на что стоит обратить внимание, это то, что фигурные скобки используются для группировки операторов, а оператор while имеет две формы, вторая из которых похожа на оператор repeat Паскаля.

Cи также имеет оператор for, но он не похож на оператор for в любом другом языке. Оператор for имеет следующий вид:

Тоже самое можно выразить через опертор while:

В качестве примера рассмотрим следующий оператор:

Этот оператор устанавливает первые n элементов массива a равными нулю. Выполнение оператора начинается с установки i в ноль (это делается вне цикла). Затем оператор повторяется до тех пор, пока i похожи на своих аналогов в других языках. Оператор %
используется по модулю. Стоит отметить, что оператор равенства это ==, а оператор неравенства это! =. Чтобы проверить равны ли a и b, можно написать так:

Си также позволяет объединять оператор присваивания с другими операторами, поэтому

Другие операторы также могут быть объединены таким образом.

Си имеет операторы для манипулирования битами слова. Разрешены как сдвиги, так и побитовые логические операции. Операторы сдвига влево и вправо являются > соответственно. Побитовые логические операторы &, | и ^, которые являются логическим И (AND), включающим ИЛИ (OR) и исключающим ИЛИ (XOP) соответственно. Если i имеет значение 035 (восьмеричное), тогда выражение i & 06 имеет значение 04 (восьмеричное). Еще один пример, если i = 7, то

и получим 074 для j.
Другой важной группой операторов являются унарные операторы, каждый из которых принимает только один операнд. Как унарный оператор, амперсанд & получает адрес переменной.

Если p является указателем на целое число, а i является целым числом, оператор

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

Другими словами, в качестве унарного оператора за звездочкой следует указатель (или
выражение, дающее указатель), и возвращает значение элемента, на который указывает. Если i имеет значение 6, то оператор

присвоит j число 6.
Оператор! (восклицательный знак – оператор отрицания) возвращает 0, если его операнд отличен от нуля, и 1, если его оператор равен 0.

Он в основном используется в операторах if, например

проверяет значение х. Если x равен нулю (false), то k присваивается значение 0. В действительности, оператор! отменяет условие, следующее за ним, так же, как оператор not в Паскаль.

является побитовым оператором дополнения. Каждый 0 в своем операнде
становится 1, а каждый 1 становится 0.

Оператор sizeof сообщает размер его операнда в байтах. Применительно к
массиву из 20 целых чисел a на компьютере с 2-байтовыми целыми числами, например sizeof a будет иметь значение 40.

Последняя группа операторов — это операторы увеличения и уменьшения.

означает увеличение р. На сколько увеличится p, зависит от его типа.
Целые числа или символы увеличиваются на 1, но указатели увеличиваются на
размер объекта, на который указывает Таким образом, если а является массивом структур, а р указатель на одну из этих структур, и мы пишем

чтобы заставить p указать на одну из структур в массиве, то после увеличения p
будет указывать на a[4] независимо от того, насколько велики структуры. Оператор

аналогичен оператору p++, за исключением того, что он уменьшает, а не увеличивает значение операнда.

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

сначала увеличивается k, затем его новое значение сохраняется в n.

Последний оператор – это? (знак вопроса), который выбирает одну из двух альтернатив
разделеных двоеточием. Например, оператор,

сравнивает х с у. Если x меньше y, тогда i получает значение 6; в противном случае переменная i получает значение k + 1. Скобки не обязательны.

А.6. Структура программы

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

Допускается объявлять переменные вне процедур, например, в начале файла перед первым объявлением процедуры. Эти переменные являются глобальными, и могут использоваться в любой процедуре во всей программе, если только ключевое слово static не предшествует объявлению. В этом случае эти переменные нельзя использовать в другом файле. Те же правила применяются к процедурам. Переменные, объявленные внутри процедуры, являются локальными для процедуры.
Процедура может обращаться к целочисленной переменной v, объявленной в другом файле (при условии, что переменная не является статической), объявляя ее у себя внешней:

Каждая глобальная переменная должна быть объявленным ровно один раз без атрибута extern, чтобы выделить память под нее.

Переменные могут быть инициализированы при объявлении:

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

А.7. Препроцессор Cи

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

1. Включение файлов.
2. Определение и замена макросов.
3. Условная компиляция.

Все директивы препроцессора начинаются со знака числа (#) в 1-ом столбце.
Когда директива вида

встречается препроцессором, он включает файл prog.h, строка за строкой, в
программу, которая будет передана компилятору. Когда директива #include написана как

определяет макрос BLOCK_SIZE и присваивает ему значение 1024. С этого момента
каждое вхождение строки из 10 символов «BLOCK_SIZE» в файле будет
заменяться 4-символьной строкой «1024» до того, как компилятор увидит файл с программой. По соглашению имена макросов пишутся в верхнем регистре. Макросы могут иметь параметры, но на практике немногие это делают.

Третья особенность препроцессора — условная компиляция. В MINIX есть несколько
мест, где код написан специально для процессора 8088, и этот код не должен включаться при компиляции для другого процессора. Эти разделы выглядят как так:

Если символ i8088 определен, то операторы между двумя директивами препроцессора #ifdef i8088 и #endif включаются в выходные данные препроцессора; в противном случае они пропускаются. Вызывая компилятор с командой

или включив в программу заявление

мы определяем символ i8088, поэтому весь зависимый код для 8088 быть включен. По мере развития MINIX он может приобрести специальный код для 68000s и других процессоров, которые будут обрабатываться также.

В качестве примера того, как работает препроцессор, рассмотрим программу рис. A-7 (a). Она включает в себя один файл prog.h, содержимое которого выглядит следующим образом:

Представьте, что компилятор был вызван командой

После того, как файл прошел через препроцессор, вывод будет таким, как показано на Рис. A-7 (b).

Именно этот вывод, а не исходный файл, дается как вход в Cи компилятор.

Обратите внимание, что препроцессор выполнил свою работу и удалил все строки, начинающиеся со знаком #. Если компилятор был бы вызван так

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

А.8. Идиомы

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

Переменные p и q обычно являются символьными указателями, а n является счетчиком. Цикл копирует n-символьную строку из места, на которое указывает q, в место, на которое указывает р. На каждой итерации цикла счетчик уменьшается, пока он не доходит до 0, и каждый из указателей увеличивается, поэтому они последовательно указывают на ячейки памяти с более высоким номером.

Еще одна распространенная конструкция:

которая устанавливает первые N элементов а в 0. Альтернативный способ написания этого цикла выглядит так:

В этой формулировке целочисленный указатель p инициализируется так, чтобы указывать на нулевой элемент массива. Цикл продолжается до тех пор, пока p не достиг адреса N-ого элемента массива. Конструкция указателя гораздо эффективнее, чем конструкция массива, и поэтому обычно используют ее.

Операторы присвоения могут появляться в неожиданных местах. Например,

сначала вызывает функцию f, затем присваивает результат вызова функции a и
наконец, проверяет, является ли оно истинным (ненулевым) или ложным (нулевым). Если а не равно нулю, то условие выполнено. Оператор

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

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

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *