Глава 9 Язык программирования С-51

We use cookies. Read the Privacy and Cookie Policy

С — это язык программирования общего назначения, предназначенный для написания программ, эффективных по исполняемому коду, с элементами структурного программирования и богатым набором операторов. Это позволяет использовать его для эффективного решения широкого круга задач. Однако при написании программ для микроконтроллеров, принадлежащих к семейству MCS-51, необходимо учитывать особенности построения аппаратуры этих микросхем, поэтому создан диалект этого языка, С-51.

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

Язык программирования С-51 удовлетворяет стандарту ANSI–C и предназначен для получения компактных быстродействующих программ для микроконтроллеров семейства MCS-51. Язык С-51 обеспечивает гибкость программирования на широко известном языке С, при скорости работы и компактности, сравнимой с программами, написанными на ассемблере.

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

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

Графическое изображение процесса разработки и отладки программы на языке программирования С-51 приведено на рис. 9.1. Необходимо отметить, что в состав языков программирования высокого уровня, предназначенных для написания программ для микроконтроллеров, обязательно входит язык программирования ассемблер. Это, как уже говорилось ранее, позволяет писать эффективные программы как с точки зрения скорости работы, так и с точки зрения наглядности программы и скорости ее написания.

Рис. 9.1. Разработка и отладка программы на языке программирования С-51

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

— постановка задачи (полное определение решаемой проблемы);

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

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

— написание текста программы и подготовка к трансляции при помощи любого текстового редактора;

— компиляция программы;

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

— создание и сохранение библиотек часто используемых объектных модулей при помощи программы Iib51.exe;

— связывание полученных перемещаемых объектных модулей в абсолютный модуль и размещение переменных в памяти микроконтроллера при помощи редактора связей bl51.exe;

— создание программы, записываемой в ПЗУ микроконтроллера (загружаемый модуль) в hex формате, при помощи программы oh.exe;

— проверка полученной программы при помощи символьного отладчика или других программных или аппаратных средств.

Файл, в котором хранится программа, написанная на языке С-51 (исходный текст программы), называется исходным модулем. Для исходного текста программы, написанной на языке программирования С-51, принято использовать расширение имени файла «*.с». Исходный текст программы можно написать, используя любой текстовый редактор, однако намного удобнее воспользоваться интегрированной средой программирования, подобной Keil-C. Кроме текстового редактора, в интегрированную среду программирования обычно входят отладчик программ, менеджер проектов и средства запуска программ-трансляторов.

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

Рис. 9.2. Кнопка трансляции исходного текста файла в интегрированной среде программирования Keil-C

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

c51.exe modul.c

В этом примере в результате трансляции исходного текста программы, содержащегося в файле modul.c, будет получен объектный модуль, который будет записан в файл с именем modul.obj. Как показано на рис. 9.1, объектный модуль не может быть загружен в память программ микроконтроллера. В память микроконтроллера загружается исполняемый модуль.

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

bl51. exe main.obj, modul1.obj, modul2.obj

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

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

Рис. 9.3. Кнопка получения исполняемого и загрузочного модулей в интегрированной среде программирования Keil-C

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

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

oh.exe main

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

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

— внутрисхемным эмулятором;

— встроенным программным отладчиком;

— внешним программным отладчиком;

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

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

Рис. 9.4. Пример системы отладки программного обеспечения для микроконтроллеров

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

— включение информации о типе переменных для проверки типов при связывании модулей. Эта же информация используется внутрисхемным эмулятором. Исключение информации о переменных пользователя может использоваться для создания прототипов или для уменьшения размера объектного модуля;

— включение или исключение таблиц символьной информации;

— конфигурация вызовов функций для обеспечения связывания с модулями, написанными на языке программирования ASM-51;

— определение желаемого содержания и формата выходного листинга программы. Распечатка промежуточных кодов на языке ассемблер после компилирования программ, написанных на языке программирования PLM-51. Включение или исключение листингов отдельных блоков исходного текста.

Структура программ С-51

Язык программирования С-51 является структурно-модульным языком. Каждая программа, написанная на языке программирования С-51, состоит из одного или более модулей. Каждый модуль записывается в отдельном файле и компилируется отдельно.

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

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

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

При разработке программы для микроконтроллеров всегда необходимо иметь перед глазами принципиальную схему устройства, для которого пишется программа, т. к. схема и программа тесно связаны между собой и дополняют друг друга. Для иллюстрации простейшей программы, написанной на языке программирования С-51, воспользуемся схемой, приведенной на рис. 9.5.

Рис. 9.5. Пример простейшей схемы устройства, построенного с использованием микроконтроллера

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

#include<reg51.h>

void main (void)

{P0«=0;         //Зажигание светодиода

while(1);      //Бесконечный цикл

Программа начинается с оператора присваивания P0 = 0. Следующий оператор, while (1), обеспечивает зацикливание программы. Это сделано для того, чтобы микроконтроллер не выполнял больше никаких действий. В противном случае он перейдет к следующей ячейке памяти программ и будет выполнять команды, которые мы не записывали.

Обратите внимание на то, что язык программирования «знает», где находится порт Р0. Эту информацию он получает из команды включения файла, содержащейся в операторе #inciude<reg5i.h>.

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

#include<reg51.h>

void svGorit(void)

{P0=0;          //Зажигание светодиода

void main(void)

{svGorit();   //Вызов функции с именем svGorit

while(1);     //Бесконечный цикл

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

Элементы языка С-51

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

Используемые символы алфавита

В исходном тексте программы, написанной на языке программирования С-51, используется часть ASCII- или ANSI-символов. Множество символов, используемых в языке программирования С, можно разделить на пять групп:

1. Символы, используемые для образования ключевых слов и идентификаторов, приведены в табл. 9.1. В эту группу входят прописные и строчные буквы английского алфавита, а также символ подчеркивания. Следует отметить, что в языке программирования С-51 различаются прописные и строчные буквы. Например, идентификаторы start и Start будут считаться различными. Цифры, кроме применения в ключевых словах и идентификаторах, могут быть использованы для записи числовых констант, хотя могут быть использованы и в идентификаторах констант и переменных.

2. Группа прописных и строчных букв русского алфавита приведена в табл. 9.2. Эти буквы могут быть использованы в комментариях к исходному тексту программы и строковых константах.

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

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

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

Управляющие последовательности 00 и хHHH (здесь о обозначает восьмеричную цифру; н обозначает шестнадцатеричную цифру) позволяют представить символ из кодовой таблицы ASCII или ANSI как последовательность восьмеричных или шестнадцатеричных цифр соответственно.

Например, символ возврата каретки может быть представлен следующими способами:

— управляющая последовательность;

15 — восьмеричный код символа возврата каретки;

x00D — шестнадцатеричный код символа возврата каретки.

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

"ABCDEx009FGH"

Данная строковая команда будет напечатана с использованием определенных функций языка С, как два отдельных слова: ABCDE и FGH, — разделенные табуляцией. Если указать неполную управляющую строку ABCDEx09FGH, то при печати появится строка ABCDE?GH, т. к. компилятор воспримет последовательность x09F как символ?.

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

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

printf("Это очень длинная

строка")

Если в тексте исходной программы встречается символ, отличающийся от символов, перечисленных выше, то компилятор С-51 выдает сообщение об ошибке.

Лексические единицы, разделители и использование пробелов

Наименьшей единицей операторов С-51 является лексическая единица.

Каждая из лексических единиц относится к одному из классов:

— идентификаторы;

— ключевые слова;

— простые ограничители (все специальные символы, кроме «_», являются простыми ограничителями);

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

!=, +=, -=, *=, «, >>, <=, >=, /*, */, //);

— числовые константы;

— текстовые строковые константы.

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

Х=АР*(FT-3)/А

X, AP, FT, A — являются идентификаторами переменных; 3 — числовой константой; все прочие символы — простыми ограничителями.

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

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

Идентификаторы в языке программирования С-51 используются для определения имени переменной, функции, символической константы или метки оператора. Длина идентификатора может достигать 255 символов, но транслятор различает идентификаторы только по первым 31 символам.

Возникает вопрос: а зачем тогда нужен такой длинный идентификатор?

Ответ: для создания «говорящего» имени функции или переменной, которое может состоять из нескольких слов. Например:

ProchitatPort();        //Прочитать порт

Vklychitlndikator();   //Включить индикатор

В приведенном примере функция ProchitatPort выполняет действия, необходимые для чтения порта, а функция Vklychitlndikator выполняет действия, необходимые для зажигания индикатора. Естественно, что намного легче понять, какое действие выполняет функция непосредственно из ее имени, чем заглядывать каждый раз в алгоритм программы или искать исходный текст функции, для того чтобы в очередной раз разобраться: что же она делает? Для этого при объявлении имени функции можно потратить количество символов и большее, чем 31!

То же самое можно сказать и про имена переменных. Например:

sbit ReleVklPitanija = 0x80;   //К нулевому выводу порта Р0 подключено реле включения питания

sbit svDiod = 0x81;              //К первому выводу порта Р0 подключен светодиод

sbit DatTemperat = 0x82;     //Ко второму выводу порта Р0 подключен датчик температуры

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

В качестве идентификатора может быть использована любая последовательность строчных или прописных букв латинского алфавита и цифр, а также символов подчеркивания (_). Идентификатор может начинаться только с буквы или символа «_», но ни в коем случае не с цифры. Это позволяет программе-транслятору различать идентификаторы и числовые константы. Строчные и прописные буквы в идентификаторе различаются. Например: идентификаторы abc и ABC, А128B и а128b воспринимаются как разные.

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

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

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

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

Примеры правильных идентификаторов:

А

XYR_56

OpredKonfigPriem

Byte_Prinjat

SvdiodGorit

Ключевые слова

Ключевые слова — это зарезервированные слова, которые используются для построения операторов языка.

Список ключевых слов:

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

Константы

Константы предназначены для введения чисел и символов в состав выражений. В языке программирования С-51 разделяют четыре типа констант:

— целые знаковые и беззнаковые константы;

— константы с плавающей запятой;

— символьные константы;

— литеральные строки.

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

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

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

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

Шестнадцатеричная константа начинается с обязательной последовательности символов 0х или 0Х и содержит одну или несколько шестнадцатеричных цифр: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, А, В, С, D, E, F.

Примеры целых констант:

Если требуется сформировать отрицательную целую константу, то используют знак «-» перед записью константы (который будет называться унарным минусом). Например: -0х2а, — 088, -16.

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

— десятичные константы рассматриваются как числа со знаком, и им присваивается тип int (целая) или long (длинное целая) в соответствии со значением константы. Если константа меньше 32768, то ей присваивается тип int в противном случае — long.

— восьмеричным и шестнадцатеричным константам присваивается тип int, unsigned int (беззнаковое целое), long или unsigned long в зависимости от значения константы согласно табл. 9.5.

Иногда требуется с самого начала интерпретировать константу как длинное целое число. Для того чтобы любую целую константу определить типом long, достаточно в конце константы поставить букву l или L. Пример записи целой константы типа long:

51, 61, 128L, 0105L, 0x2A11L.

Примеры синтаксически недопустимых целочисленных констант:

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

0x2ADG — символ G недопустим при записи шестнадцатеричных чисел.

Кроме целых констант в языке программирования С-51 используются числовые константы с плавающей запятой.

Константа с плавающей запятой — это десятичное число, представленное в виде действительной величины с десятичной точкой и (или) той. Формат записи константы имеет вид:

[Цифры].[Цифры] ['Е'|'е' ['+'|'-'] Цифры]

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

115.75, 1.5Е-2, -0.025, 075, -0.85Е2

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

Примеры записи символьных констант:

' ' — пробел;

'Q' —буква Q;

' ' — символ новой строки;

'' — обратная дробная черта;

'v' — вертикальная табуляция.

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

if (NajKn=='p') VklUstr();

В этом примере если в переменной NajKn содержится код, соответствующий букве 'p', то будет выполнена функция VkiUstr.

Строковая константа (литерал или литеральная строка) — это последовательность символов, включая строковые и прописные буквы русского и латинского алфавита, а также цифры, знаки пунктуации и разделители, заключенные в кавычки ("). Например: «школа n 35", «город Тамбов", «YZPT КОД".

Отметим, что все управляющие символы, кавычка ("), обратная дробная черта () и символ новой строки в литеральной строке и в символьной константе представляются соответствующими управляющими последовательностями. Каждая управляющая последовательность представляется как один символ. Например, при печати литеральной константы «школа N 35» ее часть «школа» будет напечатана на одной строке, а вторая часть «n 35» на следующей строке.

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

Литеральная строка рассматривается как массив символов (char []). Отметим важную особенность: число элементов массива равно числу символов в строке плюс 1, т. к. нулевой символ (символ конца строки) также является элементом массива. Все литеральные строки рассматриваются компилятором как различные объекты. Одна литеральная строка может выводиться на дисплей как несколько строк. Такие строки разделяются при помощи обратной дробной черты и символа новой строки . На одной строке исходного текста программы можно записать только одну литеральную строку. Если необходимо продолжить написание одной и той же литеральной строки на следующей строке исходного текста, то в конце строки исходного текста можно поставить обратную строку. Например, исходный текст:

"строка неопределенной

длины"

полностью идентичен литеральной строке:

"строка неопределенной длины".

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

Использование комментариев в тексте программы

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

В языке программирования С-51, в отличие от ASM-51, возможно использование двух типов комментариев:

1. Комментарий, который может быть использован внутри строки.

2. Комментарий, который игнорирует символы до конца строки.

Первый вид комментария начинается парой символов (/*) и завершается парой символов (*/). Данная особенность позволяет использовать этот вид комментария внутри операторов языка программирования. Кроме того, комментарий может занимать несколько строк. Например:

/* комментарий к программе */

/* начало алгоритма */

или

/*комментарий можно записать в следующем виде, однако надо быть осторожным, чтобы внутри строк, которые игнорируются компилятором, не попались операторы программы, которые также будут игнорироваться */

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

/* комментарии к алгоритму /* решение краевой задачи */ */

или

/* комментарии к алгоритму решения */ краевой задачи */

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

PrmDem(); //Получить, отфильтровать и демодулировать сигнал

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

//PrmDem(); //Получить, отфильтровать и демодулировать сигнал

Типы данных языка программирования С-51 и их объявление

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

В языке программирования С-51 любая переменная должна быть объявлена до первого использования этой переменной в программе. Как уже говорилось ранее, этот язык программирования предназначен для написания программ для микроконтроллеров семейства MCS-51, поэтому в составе языка должна отображаться внутренняя структура этого семейства микроконтроллеров. Особенности микроконтроллеров отражены во введении в состав языка программирования новых типов данных. В остальном язык программирования С-51 не отличается от стандартного ANSI С.[3]

Начиная с этого момента для описания синтаксических правил языка программирования С-51 применяются синтаксические диаграммы. Поясним основные понятия и обозначения, которые в них используются.

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

Терминальными называются имена, входящие в текст программы так, как они написаны. Например, терминальными именами являются символьные или строковые константы. В синтаксических правилах такие терминальные имена заключаются в апострофы и кавычки соответственно.

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

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

Итак, вооружившись этим набором несложных понятий и договоренностей, приступим к описанию синтаксиса языка программирования С-51.

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

[Спецификатор класса памяти] Спецификатор типа

[Спецификатор типа памяти] Описатель ['=' Инициатор]

[, Описатель ['=' Инициатор]]…

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

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

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

Спецификатор класса памяти — определяется одним из ключевых слов языка С-51: auto, bit, extern, register, sbit, sfr, sfrl6 static, и указывает, каким образом и в какой области памяти микроконтроллера будет распределяться память под объявляемую переменную, с одной стороны, а с другой — область видимости этой переменной, т. е. из каких программных модулей можно будет к ней обратиться.

Спецификатор типа памяти — определяется одним из шести ключевых слов языка С-51: code, data, idata, bdata, xdata, pdata, и указывает, в какой области памяти микроконтроллера будет размещена переменная.

Компилятор С51 обеспечивает следующие расширения ANSI-стандарта языка программирования С, необходимые для программирования микроконтроллеров семейства MCS-51:

— области памяти;

— типы памяти;

— модели памяти;

— описатели типа памяти;

— описатели изменяемых типов данных;

— битовые переменные и данные с битовой адресацией;

— регистры специальных функций;

— указатели;

— атрибуты функций.

Типы данных bit, sbit, sfr и sfri6 являются расширением языка программирования С-51 для поддержки процессора 8051. Они не описаны стандартом ANSI, поэтому к ним нельзя обращаться при помощи переменных-указателей.

Категории типов данных

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

Для целых типов данных: bit, sbit, char, int, short, long, signed, unsigned, sfr, sfrl6.

Для типов данных с плавающей запятой: float.

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

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

Примеры использования ключевого слова const:

const float A=2.128E-2;

const В=286;     //подразумевается const int В=286

Отметим, что переменные со спецификатором класса памяти размещаются во внутреннем ОЗУ. Неизменяемость контролируется только на этапе трансляции. Для размещения переменной в ПЗУ лучше воспользоваться спецификатором типа памяти code.

Целочисленный тип данных

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

Отметим, что ключевые слова signed и unsigned необязательны. Они указывают, как интерпретируется старший бит объявляемой переменной, т. е. если указано ключевое слово unsigned, то нулевой бит интерпретируется как часть числа, в противном случае нулевой бит интерпретируется как знаковый.

При отсутствии ключевого слова unsigned целочисленная переменная считается знаковой. В том случае, если спецификатор типа состоит из ключевого типа signed или unsigned и далее следует идентификатор переменной, то она будет рассматриваться как переменная типа int. Например: 

unsigned int n;      //Беззнаковое шестнадцатиразрядное число n

unsigned int b;

int с;                    /*подразумевается signed int с */

unsigned d;           /*подразумевается unsigned int d */

signed f;               /*подразумевается signed int f */ 

Отметим, что модификатор типа char используется для представления одиночного символа или для объявления литеральных строк. Численное значение объекта типа char соответствует ANSI-коду записанного символа (размером 1 байт).

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

Например:

0хА8С    //int signed;

017861   //long signed;

0xF7u    //int unsigned;

Числа с плавающей запятой

Для переменных, представляющих число с плавающей запятой? используется модификатор типа float. Спецификатор double тоже допустим в языке программирования С-51, но он не приводит к увеличению точности результата.

Величина со спецификатором типа float занимает 4 байта. Из них 1 бит отводится для знака, 8 битов для избыточной экспоненты и 23 бита для мантиссы. Отметим, что старший бит мантиссы всегда равен 1, поэтому он явным образом в битовом представлении числа не указывается, в связи с этим диапазон значений переменной с плавающей точкой равен от ±1.175494Е-38 до ±3.402823Е+38.

Пример объявления переменной:

float f, a, b;

Переменные перечислимого типа

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

Например, вместо использования чисел 1, 2, 3, 4, 5, 6, 7 можно использовать Названия Дней Недели: Poned, Vtorn, Sreda, Chetv, Pjatn, Subb, Voskr. При этом каждой константе будет соответствовать конкретное число.

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

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

Например:

If(rab_ned == SUB) dejstvie = rabota [rab_ned];

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

"enum" [Имя типа перечисления] '{' Список констант'}' Имя1 [',' Имя2…];

"enum" Имя типа перечисления Описатель [',' Описатель..];

В первом формате имена и значения констант задаются в Списке констант. Необязательное Имя типа перечисления — это идентификатор, который представляет собой тип переменной, соответствующий списку констант. За списком констант записывается Имя одной или нескольких переменных.

Список констант содержит одну или несколько конструкций вида:

Идентификатор ['=' Константное выражение]

Каждый Идентификатор — это имя константы. Все идентификаторы в списке констант оператора enum должны быть уникальными. Если константе явным образом не присваивается Константное выражение (чаще всего это число), то первому идентификатору присваивается значение 0, следующему — значение 1 и т. д.

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

enum week {SUB = 0,     /* константе SUB присвоено значение 0 */

                  VOS = 0,     /* константе VOS присвоено значение 0 */

                  POND,        /* константе POND присвоено значение 1 */

                  VTOR,        /* константе VTOR присвоено значение 2 */

                  SRED,        /* константе SRED присвоено значение 3 */

                  HETV,        /* константе HETV присвоено значение 4 */

                  PJAT         /* константе PJAT присвоено значение 5 */

                 }  rab ned; 

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

— объявляемая переменная может содержать повторяющиеся значения констант;

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

— Имена типов перечислений должны быть отличны от других имен типов перечислений, структур и объединений в этой же области видимости;

— значение может следовать за последним элементом списка констант перечисления.

Во втором формате для объявления переменной перечислимого типа используется готовый тип переменной, уже объявленный ранее. Например: enum week rabl;

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

typedef enum {SUB = 0,     /* константе SUB присвоено значение 0 */

                      VOS = 0,     /* константе VOS присвоено значение 0 */

                      POND,        /* константе POND присвоено значение 1 */

                      VTOR,        /* константе VTOR присвоено значение 2 */

                      SRED,         /* константе SRED присвоено значение 3 */

                      HETV,        /* константе HETV присвоено значение 4 */

                      PJAT          /* константе PJAT присвоено значение 5 */

                    }  week; 

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

Указатели

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

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

Спецификатор типа [Модификатор] '*' Описатель.

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

unsigned int * а;     /* переменная а представляет собой указатель на целочисленную беззнаковую переменную */

float * х;               /* переменная х может указывать на переменную с плавающей точкой */

char * fuffer;         /* объявляется указатель с именем fuffer, который может указывать на символьную переменную */

float *nomer;

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

void *addres;          /* Переменная addres объявлена как указатель на объект любого типа. Поэтому ей можно присвоить адрес любого объекта */

addres = &nomer;    /* (& — операция вычисления адреса). */

(float *)address ++; /* Однако, как было отмечено выше, ни одна арифметическая операция не может быть выполнена над указателем, пока не будет явно определен тип данных, на которые он указывает. В данном примере используется операция приведения типа (float *) для преобразования типа указателя address к типу float. Затем оператор ++ отдает приказ перейти к следующему адресу переменной с таким же типом. */

В качестве модификаторов при объявлении указателя могут выступать ключевые слова const, data, idata, xdata, code. Ключевое слово const указывает, что указатель не может быть изменен в программе.

Вследствие уникальности архитектуры контроллера 8051 и его производных компилятор С-51 поддерживает 2 вида указателей: специализированные (memory-specific pointers) и общего вида (generic pointers).

Указатели общего вида

Указатели общего вида объявляются точно так же, как указатели в стандартном языке программирования С. Для того чтобы не зависеть от типа памяти, в которой может быть размещена переменная, для указателей общего вида выделяется 3 байта. В первом байте указывается вид памяти переменной, во втором байте — старший байт адреса, в третьем — младший байт адреса переменной. Указатели общего вида могут быть использованы для обращения к любым переменным независимо от типа памяти микроконтроллера. Многие библиотечные функции языка программирования С-51 используют указатели этого типа, поскольку в этом случае совершенно неважно, в какой именно области памяти размещаются переменные. Приведем листинг 9.1, в котором отображаются особенности трансляции указателей общего вида.

Специализированные указатели

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

char data *str;          /* указатель на строку во внутренней памяти данных data */

int xdata *numtab;    /* указатель на целое во внешней памяти данных xdata */

long code *powtab;  /* указатель на длинное целое в памяти программ code */

Поскольку модель памяти определяется во время компиляции, специализированным указателям не нужен байт, в котором указывается тип памяти микроконтроллера. Поэтому программа с использованием типизированных указателей короче и будет выполняться быстрее по сравнению с программой, использующей указатели общего вида. Специализированные указатели могут иметь размер в 1 байт (указатели на память idata, data, bdata и pdata) или в 2 байта (указатели на память code и xdata).

Массивы

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

Массивы — это группа элементов одинакового типа (char, float, int и т. п.).

Из объявления массива компилятор должен получить информацию о типе элементов массива и их количестве. Объявление массива имеет два формата:

Спецификатор типа Имя [Константное выражение];

Спецификатор типа Имя [];

Имя — это идентификатор массива.

Спецификатор типа задает тип элементов объявляемого массива. Элементами массива не могут быть функции и элементы типа void.

Константное выражение в квадратных скобках задает количество элементов массива — размерность массива. При объявлении массива размерность может быть опущена в следующих случаях:

— массив инициализируется при объявлении;

— массив объявлен как формальный параметр функции;

— массив объявлен как внешняя переменная, явно определенная в другом файле.

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

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

К каждому конкретному элементу массива можно обратиться при помощи его индекса (порядкового номера). Отметим, что в языке С первый элемент массива всегда имеет индекс, равный 0. Например:

В последнем примере определен и инициализирован двумерный массив w[3] [3], состоящий из трех трехэлементных строк. Списки, выделенные в фигурные скобки, соответствуют строкам массива и используются для инициализации (присваивания начальных значений) элементов массива.

В случае отсутствия скобок инициализация будет выполнена неправильно.

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

Примеры:

int s[2][3];

Если при обращении к некоторой функции написать s [0], то будет передаваться нулевая строка массива s.

int b[2][3][4];

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

Для работы с литеральными строками в языке программирования С используются массивы символов, например:

char str[] = «объявление символьного массива";

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

char code str[] = «объявление массива символов";

Структуры

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

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

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

В простейшем случае тип структуры определяется записью вида:

"struct" [Идентификатор] '{' Список описаний '}'

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

Тип данных Описатель';'

где Тип данных указывает тип поля структуры для объектов, определяемых в Описателях. В простейшей форме описатели представляют собой идентификаторы переменных или массивы.

Пример объявления структур:

Переменные tochka1, tochka2 объявляются как структуры, каждая из которых состоит из трех полей: tzvet, x и у. Переменная simv объявляется как двумерный массив, состоящий из 63 элементов, описывающих рисунок символа. Во втором объявлении каждая из двух переменных — date1, date2 — состоит из трех полей: year, moth, day.

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

Этот способ основан на использовании заранее объявленного типа структуры. Объявление структурного типа аналогично объявлению перечислимого типа. Структурный тип объявляется следующим образом:

"struct" Тип '{' Список описаний '}';

где Тип — это идентификатор.

В приведенном ниже примере идентификатор student объявляется как тип структуры, список описаний полей которой приводится далее в фигурных скобках:

struct student

{char name[25];    //Имя и фамилия студента

int id;                  //Номер в журнале

int age;                //Возраст

char usp;              //успеваемость

};

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

struct Тип Список идентификаторов';'

Пример:

struct student st[23];

Поле структуры может быть указателем на структуру того же самого типа, в которой оно объявлено. Ниже рассматривается пример такой ссылки.

struct node { int data; struct node * next; } st1_node;

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

st2.id=5;                     //Присвоить значение полю id структуры st2

st1.name="Иванов";     //Присвоить значение полю name структуры st1

st1_node.data=1985;    //Присвоить значение полю data структуры st1

st[1].name="Иванов";   //Занести студента Иванова в журнал

st[1].id=2;                   //поместить его в журнал под вторым номером

st[1].age=23;               //Занести в журнал его возраст

Битовые поля

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

"struct"  '{' «unsigned» Идентификатор1 ':' Длина поля1'

"unsigned" Идентификатор2 ':' Длина поля2'  '}' 

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

Пример:

struct { unsigned a1 : 1;

           unsigned a2 : 2;

           unsigned a3 : 5;

           unsigned a4 : 2; } prim; 

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

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

Cntr.Cmd=30;

Объединения (смеси)

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

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

Тип объединения объявляется в следующем виде:

"union" '{' Описание элемента1';'

Описание элементаn';' '}';

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

Доступ к элементам объединения осуществляется тем же способом, что и к полям структур. Тип объединения может быть объявлен точно так же, как и тип структуры.

Объединение применяется для следующих целей:

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

— интерпретации основного представления объекта одного типа, как если бы этому объекту был присвоен другой тип.

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

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

union {float Koeff;    //Интерпретация объединения как переменной плавающего типа

char byte[4];           //Интерпретация объединения как массива

} bufer;                  //Объявление переменной bufer

Объединение bufer позволяет последовательному порту получить доступ к отдельным байтам числа bufer.Koeff, начиная от младшего байта bufer.byte[0] и заканчивая старшим байтом bufer.byte[3]. В программе затем можно пользоваться загруженным числом как числом с плавающей запятой.

Определение типов

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

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

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

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

Примеры объявления и использования новых типов:

При объявлении переменных и типов здесь были использованы имена типов (MATH и FIO). Помимо определения переменных, имена типов могут еще использоваться в трех случаях: в списке формальных параметров при определении функций, в операциях приведения типов и в операции sizeof.

Инициализация данных

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

'=' Инициатор';' (формат 1)

'=' '{' Список инициаторов '}'';' (формат 2)

Формат 1 используется при инициализации переменных основных типов и указателей, а формат 2 — при инициализации составных объектов.

Примеры:

char to1 = 'N';

Переменная to1 инициализируется символом 'N'.

const long megabute = (1024 * 1024);

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

static int b[2][2] = {1,2,3,4};

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

static int b[2][2] = { { 1,2 }, { 3,4 } };

При инициализации массива можно опустить одну или несколько размерностей

static int b[3][] = { { 1,2 }, { 3,4 } };

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

static int b[2][2] = { { 1,2 }, { 3 } };

элементы первой строки получат значения 1 и 2, а второй — 3 и 0.

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

Примеры:

struct complex { float real;

                        float imag; } comp [2] [3] =

       { { {1,1}, {2,3}, {4,5} },

         { {6,7}, {8,9}, {10,11} } }; 

В данном примере инициализируется массив структур comp из двух строк.

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

struct complex comp2 [2][3] = { {1,1}, {2,3}, {4,5}, {6,7}, {8,9}, {10,11} };

В этом примере компилятор интерпретирует рассматриваемые фигурные скобки следующим образом:

— первая левая фигурная скобка — начало составного инициатора для массива соmр2;

— вторая левая фигурная скобка — начало инициализации первой строки массива соmр2 [0]. Значения 1, 1 присваиваются двум полям первой структуры;

— первая правая скобка (после 1) указывает компилятору, что список инициаторов для строки массива окончен, и элементы оставшихся структур в строке соmр2 [0] автоматически инициализируются нулем;

— аналогично список {2,3} инициализирует первую структуру в строке соmр2 [1], а оставшиеся структуры массива обращаются в нули;

— на следующий список инициализаторов {4,5} компилятор будет сообщать о возможной ошибке, т. к. строка 3 в массиве, соmр2 [2], отсутствует.

При инициализации объединения задается значение первого элемента объединения в соответствии с его типом.

Пример:

union tab { unsigned char name[10);

                 int tabl;

                }               pers = {'A', 'H', 'T', 'O', 'H'}; 

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

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

char stroka[] = «привет";

Инициализируется массив символов из 7 элементов, последним элементом (седьмым) будет символ '', которым завершаются все литеральные строки.

В том случае, если задается размер массива, а литеральная строка длиннее, чем размер массива, то лишние символы отбрасываются.

Следующее определение инициализирует переменную stroka литеральной строкой, состоящей из семи элементов.

char stroka[5] = «привет";

В переменную stroka попадают первые пять элементов литерала, а символы 'T' и '' отбрасываются.

Если строка короче, чем размер массива, то оставшиеся элементы массива заполняются нулями.

Отметим, что инициализация объединения типа tab может иметь следующий вид:

union tab pers1 = «Антон";

и, таким образом, в символьный массив попадут символы:

'А', 'н' 'т', 'о', 'н', '',

а остальные элементы будут инициализированы нулем.

Выражения

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

А+В

A*(B+C)-(D-E)/F

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

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

а=(int)b+(int)с;        //Переменные а и b могут быть восьмиразрядными. Преобразование типов нужно, чтобы избежать переполнения

s=sin((float)a/15));  //Если не преобразовать тип переменной а, то деление будет целочисленным и результат деления может быть равен нулю (если а<15).

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

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

Операнды и операции

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

Если в качестве операнда используется константа, то ему соответствует значение и тип представляющей его константы. Целочисленная константа может быть типа char, int, long, unsigned int, unsigned long, в зависимости от ее значения и от формы записи. Символьная константа имеет тип char. Константа с плавающей точкой всегда имеет тип float.

Литеральная строка состоит из последовательности символов, заключенных в кавычки, и представляется в памяти как массив элементов типа char, инициализируемый указанной последовательностью символов.

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

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

Идентификаторы переменных и функций. Каждый идентификатор имеет тип, который устанавливается при его объявлении или определении.

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

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

— идентификатор переменной типа enum представлен значением одной константы из множества значений констант, указанных в перечислении. Значением идентификатора является константное значение. Тип значения — int, что следует из определения перечисления;

— идентификатор переменной типа struct или union представляет значение, определенное структурой или объединением;

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

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

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

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

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

Выражение1 '('[Список выражений]')'

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

Индексное выражение задает элемент массива и имеет вид:

Выражение 1 '[' Выражение2 ']'

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

Значений Выражение 1 и Выражение2.

Обычно Выражение1 — это указатель, например, идентификатор массива, а Выражение2 — это целочисленная величина. Однако требуется только, чтобы одно из выражений было указателем, а второе целочисленной величиной. Поэтому Выражение1 может быть целочисленной величиной, а Выражение2 — указателем. В любом случае Выражение2 должно быть заключено в квадратные скобки. Хотя индексное выражение обычно используется для ссылок на элементы массива, тем не менее, индекс может появляться с любым указателем.

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

Пусть, например, объявлен массив элементов типа float.

float arr[10];

Чтобы получить доступ к 5-му элементу массива аrr, нужно написать аrr[5]. При этом индекс 5 умножается на размер переменной float-типа (четыре байта) для того, чтобы вычислить смещение 5-го элемента массива аrr относительно его начала. Затем полученное значение складывается с начальным адресом массива аrr, что в свою очередь дает адрес 5-го элемента массива.

Таким образом, результатом индексного выражения arr[i] является значение i-го элемента массива.

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

Для ссылки на элемент многомерного массива индексное выражение должно иметь несколько индексов, заключенных в квадратные скобки:

Идентификатор массива '[' Индексное выражение1 ']' '[' Индексное выражение2 ']'…

Такое индексное выражение интерпретируется слева направо, т. е. вначале рассматривается первое индексное выражение:

Идентификатор массива '[' Индексное выражение1 ']'

Результат вычисления этого выражения — это адрес первого элемента вложенного массива, с которым складывается индексное Выражение 2, и т. д. Считывание элемента массива осуществляется после вычисления последнего индексного выражения. Отметим, что если к двумерному массиву применить только один индекс, то результатом будет не значение первого элемента вложенного массива, а его адрес.

Например, пусть объявлен трехмерный массив mass:

int mass [2] [5] [3];

Рассмотрим процесс вычисления индексного выражения mass [1] [2] [2].

1. Вычисляется выражения mass [1]. Индекс 1 умножается на размер элемента этого массива, которым является двухмерный массив, содержащий 5x3 элементов типа int. Получаемое значение складывается с начальным адресом массива mass. Результатом является адрес вложенного двухмерного массива размером (5x3) в трехмерном массиве mass.

2. Второй индекс 2 умножается на размер массива из трех элементов типа int и складывается с адресом mass [1].

3. Так как каждый элемент трехмерного массива — это величина типа int, то третий индекс 2 умножается на размер int-типа (в С-51 это два байта) перед сложением с адресом mass [1] [2].

4. Наконец, выполняется считывание значения полученного элемента массива int-типа.

Если было бы указано mass [1] [2], то результатом был бы адрес массива из трех элементов типа int. Соответственно значением индексного выражения mass [1] является адрес двухмерного массива.

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

Имя'.' Поле

Имя"->"Поле

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

Имя "->" Поле

эквивалентно записи

"(*" Имя')'.Поле

Пример:

struct tree {float                num;

                 int                   spisoc[5);

                 struct tree       *left;

                } tr[5], elem;

             elem.left = & elem; 

В приведенном примере используется операция выбора ('.') для доступа к полю left структурной переменной elem. Таким образом, элементу left структурной переменной elem присваивается адрес самой переменной elem, т. е. переменная elem хранит указатель на себя саму.

Приведение типов — это изменение (преобразование) типа объекта. Приведение типов используется для преобразования объектов одного скалярного типа в другой скалярный тип. Для выполнения преобразования необходимо перед объектом записать в скобках нужный тип:

'(' Имя типа ')' Операнд.

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

int i;

float x;

х = (float)i+2.0;

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

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

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

2. Операция «адрес» (&) может быть использована только при инициализации.

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

Унарное выражение состоит из операнда и предшествующего ему знака унарной операции и имеет следующий формат:

Знак унарной операции Операнд

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

Операнд1 Знак бинарной операции Операнд2

Тернарное выражение состоит из трех операндов, разделенных знаками тернарной операции ('?' и ':')> и имеет формат:

Операнд1 '?' Операнд2 ':' ОперандЗ.

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

В языке С-51 имеются следующие унарные операции, приведенные в табл. 9.8.

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

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

В отличие от унарных, бинарные операции, список которых приведен в табл. 9.9, выполняются слева направо.

Левый операнд операции присваивания должен быть выражением, ссылающимся на область памяти (но не идентификатором, объявленным с ключевым словом const). Левый операнд не может также быть массивом.

При записи выражений следует помнить, что символы '*', '&', '-' '+' могут обозначать как унарную, так и бинарную операцию.

Преобразования типов при вычислении выражений

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

Рассмотрим общие арифметические преобразования.

1. Если один операнд имеет тип float, то второй также преобразуется к типу float.

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

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

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

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

float                 ft,sd;

unsigned  char   ch;

unsigned long    in;

int                   i;

....

   sd=ft*(i+ch/in); 

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

Операции унарного минуса, логического и поразрядного отрицания

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

Пример:

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

Пример:

int t, z=0;

t=!z;

Переменная t получит значение, равное 1, т. к. переменная z имела значение, равное 0.

Операция поразрядного отрицания (-) инвертирует каждый бит операнда.

Операнд должен быть целого типа. Пример:

char b = 9;

char f;

f = ~b;

Двоичное значение 9 равно 00001001. В результате операции ~b будет получено двоичное значение 11110110.

Операции разадресации и вычисления адреса

Эти операции используются для работы с переменными типа указатель.

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

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

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

— указатель является нулевым;

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

— указатель определяет адрес, который не выровнен до типа объекта, на который он указывает;

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

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

Операция & не может применяться к элементам структуры, являющимся полями битов, т. к. эти элементы не выровнены по байтам. Кроме того, эта операция не может быть применена к объектам с классом памяти register.

Примеры:

int t,             //Объявляется переменная целого типа t

     f=0,         //Объявляется переменная f и ей присваивается 0

     *adress;   //Объявляется указатель на переменные целого типа

 adress = &t  // указателю adress присваивается адрес переменной t

*adress =f;    /* переменной, находящейся по адресу, содержащемуся в переменной adress, т.е. переменной t, присваивается значение переменной f, т.е. 0, что эквивалентно оператору t=f; */

Операция sizeof

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

"sizeof ("Выражение') '

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

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

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

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

Операция умножения (*) выполняет умножение операндов. Тип выполняемой операции умножения зависит от типа операндов. Перед операцией операнды приводятся к одному типу. Например:

int     i=5;

float  f=0.2;

float  g,z;

     g=f*i; 

Тип переменной i преобразуется к типу float, затем выполняется умножение а результат умножения присваивается переменной g.

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

int i=49, j=10, n. m;

n = i/j;                     /* результат 4 */

m = i/(-j);                /* результат -4 */ 

Операция вычисления остатка от деления (%) дает остаток от деления первого операнда на второй. Знак результата зависит от конкретной реализации транслятора с языка программирования. В языке программирования С-51 знак результата совпадает со знаком делимого. Примеры выражений с использованием операции определения остатка:

int n = 49, m = 10, i, j, k, l;

i = n % m;                /* 9 */

j = n % (-m);            /* 9 */

k = (—n) % m;          /* -9 */

l = (-n) % (-m);        /* -9 */

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

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

Пример переполнения результата:

int   i=30000, j=30000, k;

        k=i+j; 

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

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

Когда производится суммирование указателя с переменной целого типа, то к содержимому указателя прибавляется произведение количества ячеек памяти, зависящее от типа указателя, на значение целочисленной переменной. Например:

char *a=5;   //Объявить указатель и настроить на ячейку памяти с адресом 5

int *b=5;     //Объявить указатель и настроить на ячейку памяти с адресом 5

long *c=5;   //Объявить указатель и настроить на ячейку памяти с адресом 5

а=а+5;        //Увеличить значение указателя на 5 (адрес 10)

t>=b+5;      //Увеличить значение указателя на 5 (адрес 15)

с=с+5;       //Увеличить значение указателя на 5 (адрес 25)

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

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

1. Оба операнда — целого или плавающего типа.

2. Оба операнда являются указателями на один и тот же тип.

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

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

Пример работы с указателем:

double d[10],* u;

int i;

u = d+2;    /* u указывает на третий элемент массива */

i = u-d;     /* i принимает значение равное 2              */

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

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

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

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

Отметим, что сдвиг влево соответствует умножению первого операнда на 2 в степени, равной второму операнду, а сдвиг вправо соответствует делению первого операнда на 2 в степени, равной второму операнду.

Примеры использования операции сдвига:

int    i=0x1234, j, к ;

k=i«4;          /* k="0x2340» */

j=i«8;          /* j="0x3400» */

i=j»8;          /* i = 0x0034  */

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

К поразрядным (побитовым) операциям относятся: операция поразрядного «И» (&), операция поразрядного «ИЛИ» (|) и операция поразрядного «исключающего ИЛИ» (^).

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

Операция поразрядного «И» (&) сравнивает каждый бит первого операнда с соответствующим битом второго операнда. Если оба сравниваемых бита единицы, то соответствующий бит результата устанавливается в 1, в противном случае в 0.

Операция поразрядного «ИЛИ» (|) сравнивает каждый бит первого операнда с соответствующим битом второго операнда. Если любой (или оба) из сравниваемых битов равен 1, то соответствующий бит результата устанавливается в 1, в противном случае результирующий бит равен 0.

Операция поразрядного «исключающего ИЛИ» (^) сравнивает каждый бит первого операнда с соответствующим битом второго операнда. Если один из сравниваемых битов равен 0, а второй бит равен 1, то соответствующий бит результата устанавливается в 1, в противном случае, т. е. когда оба бита равны 1 или 0, бит результата устанавливается в 0.

Пример:

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

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

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

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

Операция логического «И» (&&) вырабатывает значение 1, если оба операнда имеют ненулевые значения. Если хотя бы один из операндов равен 0, то результат также равен 0. Если значение первого операнда равно 0, то второй операнд не вычисляется.

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

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

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

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

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

Операнд1 '?' Операнд2 ':' ОперандЗ

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

Если Операнд1 не равен 0, то вычисляется Операнд2, и его значение является результатом операции. Если Операнд1 равен 0, то вычисляется Операнд3 и его значение является результатом операции. Следует отметить, что при выполнении этой операции вычисляется либо Операнд2, либо Операнд3, но ни в коем случае не оба сразу. Тип результата зависит от типов Операнд2 и Операнд3 следующим образом:

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

2. Если оба операнда имеют один и тот же тип структуры, объединения или указателя, то тип результата будет тем же самым типом структуры, объединения или указателя.

3. Если оба операнда имеют тип void, то результат имеет тип void.

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

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

Пример:

max = (d<=b)? b: d;

Переменной max присваивается максимальное значение переменных d и b.

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

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

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

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

Примеры использования операции инкремента:

int t=1, s=2, z, f;

      z=(t++)*5; 

Вначале происходит умножение t*5, а затем увеличение t. В результате получится z=5, t=2.

f=(++s)/3;

Вначале значение s увеличивается, а затем используется в операции деления. В результате получим s=3, f=1.

В случае, если операции увеличения и уменьшения используются как самостоятельные операторы, префиксная и постфиксная формы записи становятся эквивалентными.

z++; /* эквивалентно */ ++z;

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

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

Пример:

int t;

char f;

long z;

t=f+z;

Значение переменной f преобразуется к типу long, вычисляется f+z, результат преобразуется к типу int и затем присваивается переменной t.

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

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

Операнд1 Бинарная операция '=' Операнд2.

Составное присваивание эквивалентно следующему простому присваиванию:

Операнд1 '=' Операнд1 Бинарная операция Операнд2.

Каждая операция составного присваивания выполняет преобразования, которые осуществляются соответствующей бинарной операцией. Левым операндом операций «+=» и «-=» может быть указатель, в то время как правый операнд должен быть целым числом.

Примеры:

double arr[4]={ 2.0, 3.3, 5.2, 7.5 } ;

double b=3.0;

b+=arr[2];      /* эквивалентно оператору b=b+arr(2)             */

arr[3]/=b+1;  /* эквивалентно оператору  arr[3]=arr[3]/(b+1) */ 

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

Приоритеты операций и порядок вычислений

В языке С-51 операции с высшими приоритетами вычисляются первыми. Наивысшим является приоритет, равный 1. Приоритеты и порядок операций приведены в табл. 9.10.

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

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

prog (a, a=k*2);

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

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

Например, выражение i*j + (j++) + (-i) может принимать различные значения при обработке разными компиляторами. Чтобы избежать недоразумений при выполнении программ из-за побочных эффектов, необходимо придерживаться следующих правил:

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

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

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

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

— при выполнении операций осуществляются обычные арифметические преобразования (которые были рассмотрены выше);

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

— при передаче аргументов функции.

Кроме того, в языке программирования С-51 есть возможность явного приведения значения одного типа к другому.

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

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

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

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

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

Преобразование из float, double или long double к типу unsigned long производится с потерей точности, если преобразуемое значение больше, чем максимально возможное положительное значение, представленное типом long.

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

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

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

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

Целый тип может быть преобразован к адресному типу по следующим правилам:

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

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

Преобразования при вызове функции. Преобразования, выполняемые над аргументами при вызове функции, зависят от того, был ли задан прототип функции (объявление «вперед») со списком объявлений типов аргументов.

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

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

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

'(' Имя типа ')' Операнд

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

Пример:

int       i=2;

long     l=2;

double   d;

float      f;

d=(double)i * (double)l;

f=(float)d; 

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

Операторы

Все операторы языка С-51 могут быть условно разделены на следующие категории:

— условные операторы, к которым относятся оператор условия if и оператор выбора switch;

— операторы цикла (for, while, do-while);

— операторы Перехода (break, continue, return, goto);

— другие операторы (оператор «выражение», пустой оператор).

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

Все операторы языка С-51, кроме составных, заканчиваются точкой с запятой (;).

Оператор-выражение

Любое выражение, которое заканчивается точкой с запятой, является оператором.

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

Примеры:

++ i;

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

a=cos(b*5);

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

а(х, у);

Этот оператор представляет выражение, состоящее из вызова функции.

Пустой оператор

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

— в операторах do, for, while, if в строках, когда не требуется выполнение каких-либо действий, но по синтаксису требуется хотя бы один оператор;

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

Пример:

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

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

'{'    [Список объявлений)

      [Список операторов)

 '}' 

Заметим, что в конце составного оператора точка с запятой не ставится.

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

Пример:

Переменные е, g, f, q будут уничтожены после выполнения составного оператора. Отметим, что переменная q является локальной в составном операторе, т. е. она никоим образом не связана с переменной q объявленной в начале функции main с типом int.

Оператор if

Формат оператора:

"if (" Выражение')' Оператор 1';' ["else" Оператор2';']

Выполнение оператора if начинается с вычисления выражения.

Далее выполнение происходит по следующей схеме:

1) если Выражение истинно (т. е. отлично от 0), то выполняется Оператор1;

2) если Выражение ложно (т. е. равно 0), то выполняется Оператор2;

3) если Выражение ложно и отсутствует Оператор2, то выполняется следующий за if оператор.

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

Пример:

if (i < j)

   i++;

  else

{j=i-3;

 i++;

Этот пример иллюстрирует также и тот факт, что на месте Оператора1, так же как и на месте Оператора2, могут находиться сложные конструкции.

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

Примеры:

int main ( )

  {int t=2, b=7, r=3;

       if(t>b)

        {if(b<r) r=b;

        }

      else

        r=t;

    return (0);

  } 

В результате выполнения этой программы переменная r станет равной 2. Если же в программе опустить фигурные скобки, стоящие после оператора if, то программа будет иметь следующий вид:

int main( )

  {int t=2, b=7, r=3;

    if(a>b)

      if (b<c)

          t=b;

       else

        r=t ;

   return (0);

 } 

В этом случае r получит значение, равное 3, т. к. ключевое слово else относится ко второму оператору if, который не выполняется, поскольку не выполняется условие, проверяемое в первом операторе if.

Следующий фрагмент иллюстрирует вложенные операторы if:

char ZNAC;

int x,y,z;

   :

if (ZNAC == '-') x = у - z;

else if (ZNAC == '+') x = у + z;

else if (ZNAC == '*') x = у * z;

else if (ZNAC == '/') x = у / z;

else ... 

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

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

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

Оператор switch

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

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

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

Все константные выражения в операторе switch должны быть различными. Кроме операторов, начинающихся с ключевого слова case, в составе оператора switch может быть один фрагмент, помеченный ключевым словом default. Он будет выполняться, если не выполнится ни одно из условий.

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

Отметим также, что в операторе switch можно использовать свои локальные переменные, объявления которых находятся перед первым ключевым словом case, однако в объявлениях не должна использоваться инициализация.

Схема выполнения оператора switch следующая:

1) вычисляется выражение в круглых скобках;

2) вычисленное значение последовательно сравнивается с константными выражениями, следующими за ключевыми словами case;

3) если одно из константных выражений совпадает со значением выражения, то управление передается на оператор, помеченный соответствующим ключевым словом case;

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

Отметим интересную особенность использования оператора switch: конструкция со словом default может быть не последней в теле оператора switch.

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

Например:

int i=2;

switch (i)

   {case 1: i += 2;

    case 2: i *= 3;

    case 0: i /= 2;

    case 4: i -= 5;

   default:        ;  

  } 

Выполнение оператора switch начинается со строки, помеченной case 2. Таким образом, переменная i получает значение, равное 6. Далее выполняется оператор, помеченный ключевым словом case 0, а затем — case 4, переменная i примет значение 3, а затем значение — 2. Пустой оператор, помеченный ключевым словом default, не изменяет значения переменной.

Рассмотрим, как выглядит ранее приведенный пример, в котором иллюстрировалось использование вложенных операторов if, если его переписать с использованием оператора switch.

char ZNAC;

int x,y,z;

switch (ZNAC)

{case '+' : x = у + z;  break;

 case '-' : x = у - z;   break;

 case '*' : x = у * z;   break;

 case '/' : x= у / z;  break;

default  : ;

Оператор break позволяет в необходимый момент прервать последовательность выполняемых операторов в теле оператора switch путем передачи управления оператору, следующему за switch.

Отметим, что в теле оператора switch можно использовать вложенные операторы switch, при этом в ключевых словах case можно использовать одинаковые константные выражения.

Пример использования вложенного оператора выбора switch:

    ...

switch (a)

  {case 1: b=c; break;

   case 2:

   switch (d)

     {case 0: f=s;  break;

      case 1: f=9;  break;

      case 2: f-=9; break;

    }

  case 3: b-=c; break;

     ...

  }

Оператор break

Оператор break обеспечивает прекращение выполнения самого внутреннего из объемлющих его операторов: switch, do, for или while. После выполнения оператора break управление передается оператору, следующему за прерванным оператором.

Оператор цикла for

Оператор for — это наиболее общий способ организации цикла. Он имеет следующий формат:

"for (" Выражение1';' Выражение2';' Выражение3 ')' Тело цикла';'

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

Последовательность выполнения оператора цикла for:

1) Вычисялется Выражение 1;

2) Вычисялется Выражение2;

3) если значение Выражение2 отлично от нуля, то выполняется тело цикла, вычисляется Выражение3 и осуществляется переход к пункту 2, если Выражение2 равно нулю, то управление передается на оператор, следующий за оператором for.

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

Пример использования оператора цикла for:

int main ()

{int i,b;

 for (i=1; i<10; i++)

   b=i*i;

  return 0;

}

В этом примере вычисляются квадраты чисел от 1 до 9.

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

Пример использования нескольких переменных в операторе цикла for:

int main()

{int top,bot;

 char string (100), temp;

 for(top=0, bot=100; top<bot; top++,bot--) 

    {temp=string[top];

     string[bot]=temp;

    }

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

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

В этом же примере можно наглядно проследить за тем, как влияет выбор типа переменных на размер загрузочного файла. В приведенном примере для организации цикла использованы переменные типа int. В результате получился машинный код размером 59 байт. При замене типа этих же переменных на unsigned char размер кода сокращается до 41 байта. Эти же действия увеличивают быстродействие программы в полтора раза!

В микроконтроллерах оператор цикла for используется^для реализации бесконечного цикла, который необходим для непрерывной работы устройства. Организация такого цикла возможна при использовании пустого Выражения2. Иногда для реализации алгоритма работы устройства требуется при выполнении какого-либо условия выйти из бесконечного цикла. Для этого можно воспользоваться оператором break.

Пример реализации бесконечного цикла с возможностью выхода из него при помощи оператора for:

for (;;)

{ ...

  ...  break;

  ...

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

Пример использования пустого оператора для поиска:

for(i=0;t[i]<10;i++);

В данном примере при завершении цикла переменная цикла i принимает значение номера первого элемента массива t, значение которого больше 10.

Оператор цикла while

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

"while ("Выражение')' Тело ';'

Этот оператор обычно приводит к более коротким и эффективным программам по сравнению с предыдущим оператором цикла, т. к. элементарно накладывается на машинные инструкции микроконтроллера.

В качестве выражения допускается использовать любое выражение языка С-51, а в качестве тела — любой оператор, в том числе пустой или составной операторы. Последовательность выполнения оператора while:

1) вычисляется Выражение;

2) если Выражение ложно, то выполнение оператора while заканчивается и выполняется следующий по порядку оператор. Если выражение истинно, то выполняется тело оператора while;

3) переход к пункту 1.

Оператор цикла вида

"for ("Выражение 1';' Выражение2';' Выражение3')' Тело ';'

может быть заменен оператором while следующим образом:

Выражение1;

"whilе ("Выражение2')'

'{'    Тело

Выражение3;

'}'  

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

Внутри операторов for и while можно использовать локальные переменные, которые должны быть объявлены с определением соответствующих типов.

Оператор цикла do-while

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

"do» Тело «while» '('Выражение");"

Схема выполнения оператора do-whiie:

1) выполняется тело цикла (которое может быть составным оператором);

2) вычисляется Выражение;

3) если Выражение ложно, то выполнение оператора do-whiie заканчивается и выполняется следующий по порядку оператор. Если Выражение истинно, то выполняется переход к пункту 1.

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

Операторы while и do-while могут быть вложенными.

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

int i,j,k;

...

i=0; j=0; k=0;

do {i++;

      j--;

      while(a[k]<i)k++;

}while(i<30&&j<-30); 

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

char i=0, j=0, k=0;

...

do{i++; j--;

     while(a[k]<i)k++;

     if(j>=-30)break;

}while(i<30); 

Оператор continue

Оператор continue используется только внутри операторов цикла, но в отличие от break, осуществляется не выход из цикла, а переход к следующему циклу. Формат записи оператора continue:

"continue;"

Пример использования оператора continue:

int main()

{int a,b;

 for (a=1,b=0; a<100; b+=a,a++)

   {if (b%2) continue;

    ... /* обработка четных сумм */

   }

 return 0;

Когда сумма чисел от 1 до а становится нечетной, оператор continue передает управление на очередную итерацию цикла for, не выполняя операторы обработки четных сумм.

Оператор continue, как и оператор break, прерывает самый внутренний из объемлющих его циклов.

Оператор возврата из функции return

Оператор return завершает выполнение функции, в которой он задан, и возвращает управление в вызывающую функцию, в точку, непосредственно следующую за вызовом данной функции. Формат оператора возврата из функции:

"return" [Выражение]';'

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

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

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

Пример использования оператора return для возвращения результата работы функции суммирования двух переменных:

int sum (int a, int b)

{return (a+b);}

Функция sum объявлена с двумя формальными параметрами, а и b, int-типа и возвращает значение такого же типа, о чем говорит описатель, стоящий перед именем функции. Возвращаемое оператором return значение равно сумме фактических параметров.

Пример использования оператора return для выхода из подпрограммы-процедуры:

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

Оператор безусловного перехода goto

Использование оператора безусловного перехода goto в практике программирования на языке С-51 настоятельно не рекомендуется, т. к. затрудняет понимание программ и возможность их модификаций. В то же самое время алгоритм любой степени сложности может быть построен при использовании оператора условного перехода if и операторов циклов while и do-while. Оператор безусловного перехода goto может быть использован только в случае крайней необходимости.

Формат этого оператора записывается в следующем виде:

"goto" Имя метки';'

                  ...

Имя метки':' Оператор';' 

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

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

Использование функций в языке программирования С-51

Определение и вызов функций

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

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

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

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

С использованием функций в языке программирования С-51 связаны три понятия: определение функции (описание действий, выполняемых функцией в виде операторов), объявление функции (задание формы обращения к функции) и вызов функции.

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

Пример определения подпрограммы-функции:

int rus (unsigned char r)

{if (r>='A' && c<=' ')

      return 1;

    else

      return 0;

В данном примере определена функция с именем rus, имеющая один параметр с именем r и типом unsigned char. Функция возвращает целое значение, равное 1, если параметр функции является буквой русского алфавита, или 0 в противном случае.

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

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

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

int rus (unsigned char r);

или

int rus (unsigned char);

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

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

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

[Спецификатор класса памяти] [Спецификатор типа] Имя функции

'(' [Список формальных параметров]')'

'{ 'Тело функции' }'

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

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

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

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

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

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

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

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

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

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

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

Тело функции — это составной оператор, содержащий операторы, определяющие действие функции.

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

Иногда при написании программы требуется вызов функции самой из себя или функция может вызываться из основной программы и подпрограммы обслуживания прерывания. В стандартном языке программирования С это не создает проблем, ведь там локальные переменные хранятся в стеке. В языке программирования С-51 для таких функций следует применять атрибут reentrant. При его использовании локальные переменные будут располагаться в стеке. При этом стек будет размещаться в зависимости от вида принятой для компиляции модели памяти (small, compact, large) в области памяти data, pdata, xdata соответственно. Пример использования атрибута reentrant:

int calc (char i, int reentrant {

int x;

x = table [i] ;

return (x * b);

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

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

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

/* Неправильное использование параметров */

void change (int х, int у)

{ int k=х;

       х=у;

       y=k;

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

/* Правильное использование параметров */

  void change (int *х, int *у)

{ int k=*х;

      *х=*у;

      *y=k;

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

change (&а,&b);

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

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

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

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

— Функция возвращает значение типа, отличного от int.

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

Объявление (запись прототипа) функции производится в следующем формате:

[Спецификатор класса памяти] [Спецификатор типа] Имя функции '(' [Список формальных параметров]')' [',' Список имен функций]';'

Если прототип не задан, а встретился вызов функции, то строится неявный прототип из анализа формы вызова функции. Тип возвращаемого значения создаваемого прототипа — int, а список типов и числа параметров функции формируется на основании типов и числа фактических параметров, используемых при данном вызове.

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

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

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

Адресное выражение '(' [Список выражений]')'

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

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

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

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

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

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

3. Управление передается на первый оператор функции.

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

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

Пример объявления переменной указателя на функцию:

int (*fun)(int x, int *y);

Здесь объявлена переменная fun как указатель на функцию с двумя параметрами типа int и указателем на int. Сама функция должна возвращать значение типа int. Круглые скобки, содержащие имя указателя fun и признак указателя *, обязательны, иначе запись

int *fun (intx, int *y);

будет интерпретироваться как объявление функции fun, возвращающей указатель на int.

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

(*fun)(i,&j);

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

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

Пример:

double (*fun1)(int x, int y);

double fun2(int k, int l);

     fun1=fun2;        /* инициализация указателя на функцию */

     (*fun1) (2,7);    /* обращение к функции                         */ 

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

Рассмотрим пример использования указателя на функцию в качестве параметра функции, вычисляющей производную от cos(x).

Итак, подведем итоги

В данной главе было приведено краткое описание языка программирования С-51. Использование этого языка позволяет сократить время разработки программ для микроконтроллеров. В большинстве случаев ресурсов выбранного микроконтроллера более чем достаточно для реализации требуемого алгоритма. Это позволяет использовать для создания программы язык С-51. В главе показаны примеры использования С-51 для управления микроконтроллером. Этот язык позволяет создавать достаточно сложные программы при минимальных затратах времени.

Однако следует помнить, что ничего не бывает бесплатно. Избавляя от одних проблем, язык программирования С-51 приводит к другим. Как это неоднократно подчеркивалось в тексте главы, при программировании на языке С-51 необходимо чрезвычайно тщательно выбирать типы используемых переменных и следить за их правильным использованием.

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

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