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

Читать, слущать книги онлайн бесплатно!

Электронная Литература.

Бесплатная онлайн библиотека.

Читать: Встраиваемые системы. Проектирование приложений на микроконтроллерах семейства 68HC12/HCS12 с применением языка С - Стивен Ф. Барретт на бесплатной онлайн библиотеке Э-Лит


Помоги проекту - поделись книгой:

Обсудим действие следующего выражения:

address = (int *) 0x1000;

Это выражение назначает ячейку памяти с адресом 0x1000 как указатель с именем address. Для того, чтобы извлечь содержимое ячеек памяти следует поместить оператор * перед именем address.

3.4. Функции

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

3.4.1. Что такое функция?

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

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

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

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

3.4.2. Основная программа

Основная программа — это особый тип функции, ее отличие от других функций заключается в том, что она исполняется, когда запускают программу с определенным именем. Корректно написанная основная программа работает как программа «управленец» (менеджер высшего звена). Текст основной функции main.c отражает структуру всей прикладной программы, при этом не затрагивая специфических особенностей отдельных задач по управлению объектом. Мы можем представить основную программу в роли управляющего, который контролирует выполнение отдельных «команд» управления путем запуска программ функций. Предположим, что мы хотим выполнить задачи с первой по n-ую. Для этого оформлены n функций. Тогда мы запишем основную программу, в которой будут последовательно вызываться эти функции:

1  void main(void)

2  {

3   function_one();

4   function_two();

5   function_three();

6   :

7   :

8   :

9   function_n();

10 }

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

3.4.3. Прототипы функций

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

тип возвращаемой переменной имя функции

 (<тип переменной1> <имя переменной1>,

 <тип переменной2> <имя переменной2>,

 :

 <тип переменной> <имя переменнойN>);

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

Пример 1: int compute(int, int);

Пример 2: float change(char name, float number, int a);

Пример 3: double find(unsigned int, float, double);

В примере 1 функция compute использует два аргумента. Аргументы функции — это те переменные, которые необходимы для ее корректного исполнения. Спецификация типа аргументов функции приведена в круглых скобках. В данном случае указано, что функция compute будет использовать два целочисленных аргумента, т.е. при вызове функции ей должны быть указаны для целочисленных значения. Результатом действия функции compute будет вычисление значения некоторой переменной. Спецификация типа возвращаемой переменной приведена перед именем функции. В данном случае это тоже целочисленный 16 разрядный формат.

В примере 2 записан прототип функции change. Эта функция предполагает наличие трех аргументов: однобайтового целочисленного name, числа с именем number в формате с плавающей запятой и двухбайтового целого числа с именем a. Функция change должна возвратить значение переменной в формате с плавающей запятой. В примере 3 объявляется функция find с тремя аргументами, для которых указан тип данных, но не указаны имена.

При знакомстве с программами на Си Вы можете встретить прототип функции, в котором на первом месте указано слово extern:

extern not_here(int a, int b, int c)

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

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

compute(23, 12);

change('b', 7.825, 2);

find(25, 5.1524, 23.54721);

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

3.4.4. Описание функций

Каждая объявленная в начале некоторого программного модуля функция должна быть определена в этом модуле или в тексте программы другого модуля, который в процессе генерации исполняемого кода программы будет присоединен к текущему модулю. Функция может быть также определена в подключаемом файле стандартной библиотеки. Текст определения функции может быть записан в любом месте программного модуля, однако принято определения всех используемых функций располагать сразу за текстом основной программы main.c. Например, предположим, что объявленная в предыдущем параграфе функция compute вычисляет модуль вектора двух ортогональных составляющих a и b и возвращает его в переменной с именем result. Прототип функции:

int compute(int a, int b);

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

/*Функция compute: вычисляет модуль вектора по двум его ортогональным */

/*составляющим                                                        */

1 int compute(int a, int b)

2 {

3  int sum, result;

4  sum = a*a + b*b;

5  result=(int) (sqrt(sum));

6  return(result);

7 }

В приведенном примере строка 1 открывает определение функции, информируя компилятор о том, что имя функции compute, она использует для своей работы две целочисленных переменных и возвращает одну целочисленную переменную. Фигурная скобка в строке 2 открывает область операторов определяемой функции. В строке 3 объявляются локальные переменные, т.е. те переменные, которые используются только внутри функции. Это переменные sum и result. Операторы, расположенные в строках 4 и 5 выполняют заявленные в описании функции вычисления. Причем в строке 5 используется функция извлечения квадратного корня sqrt, которая определена в библиотеке математических вычислений. При компиляции эта библиотека должна быть обязательно присоединена к файлу с рассматриваемой функцией посредством специальных директив компилятора, которые мы рассмотрим в следующем параграфе. В строке 5 следует обратить внимание на оператор (int) перед вызовом функции извлечения квадратного корня. Этот оператор осуществляет преобразование типа данных к заявленному в прототипе функции compute целочисленному формату int, поскольку функция извлечения квадратного корня возвращает данные в другом формате. В строке 6 применен оператор возврата return, которые позволяет использовать значение переменной result другими операторами основной программы. Фигурная скобка в строке 7 завершает определение функции. Любые операторы, записанные после скобки, уже не будут ассоциироваться с функцией compute.

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

3.4.5. Вызов функций, передача параметров, возврат полученных значений

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

magnitude = compute(12, 24)

В этом примере мы предполагаем, что переменная magnitude ранее была объявлена как целочисленная. После того, как функция была вызвана и выполнена, переменной magnitude будет присвоено значение 26. Истинный результат вычисления равен 26,832816. Именно это значение будет вычислено функцией извлечения квадратного корня sqrt. Однако перед присвоением переменной result этого значения производится смена формата представления данных, и дробная часть результата отбрасывается.

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

3.5. Файлы заголовков

В этом параграфе мы расширим наши знания о технике программирования на Си посредством знакомства с заголовочными файлами (header file). Заголовочный файл — это внешний файл, помещаемый в начало программы с помощью директивы #include, обычно содержащий определения типов и переменных, используемых в программе. Язык Си предоставляет программисту некоторый набор стандартных функций, определения которых находятся в нескольких заголовочных файлах. Например, в созданной нами функции compute мы использовали функцию извлечения квадратного корня sqrt, которая определена в файле математических функций math.h.

Выражения языка С для включения файлов заголовков в модуль разрабатываемой программы обычно располагаются в начале программы. Заголовочные файлы содержат определения переменных, макросы, объявления функций, позволяя программисту возможность вызывать эти функции, использовать переменные и макросы без дополнительного определения их в тексте создаваемой программы. В процессе компиляции значения постоянных переменных замещают их символьные значения, упомянутые в основной программе. Далее в примерах программ мы будем использовать заголовочный файл stdio.h, в котором определены функции библиотеки стандартного ввода/вывода. Эти функции позволяют отобразить результаты преобразования данных в МК на экране дисплея. А также передать в МК код нажатой клавиши на клавиатуре. Поставляемые фирмами производителями программного обеспечения компиляторы уже содержат библиотеки и соответствующие им заголовочные файлы. Например, нами будет использована библиотека математических функций, и соответствующий ей файл math.h. Во многих случаях у пользователя возникает необходимость создания своего собственного заголовочного файла, в котором будут содержаться определения констант. Для того чтобы включить файл заголовка в разрабатываемый программный модуль, следует воспользоваться директивой #include. Приведем три примера:

#include <stdio>

#include <math.h>

#include "myheader.h"

В первых двух записях имя подключаемого файла заключено в «<>», что информирует компилятор о том, что названные файлы располагаются в определенной директории (папке). Обычно это папка с именем include, которая располагается в основном каталоге компилятора Си. В третьей записи имя подключаемого файла заключено в двойные кавычки. Для компилятора это означает, что данный файл располагается в той же папке, что и создаваемый программный модуль.

3.6. Директивы компилятора

Директивы компилятора — это инструкции для программы компилятора, которые указывают ему каким образом следует обрабатывать исходный текст программы. Достаточно часто эти инструкции называют директивами препроцессора компилятора, акцентируя внимание пользователей на том, что эти директивы выполняют обработку исходного текста программы перед тем, как компилятор начнет генерацию ассемблерного текста программы. Известно 11 директив компилятора Си: #if, #ifdef, #ifndef, #else, #elif, #include, #define, #undef, #line, #error, #pragma. Из приведенного списка понятно, что директивы отмечаются символом # в первом знаке имени. Далее мы рассмотрим наиболее часто используемые директивы.

3.6.1. Директивы условной компиляции

Директивы #if, #ifdef, #ifndef, #else, #elif и #endif относятся к группе директив условной компиляции. Эти директивы используются для того, чтобы обозначенный фрагмент исходного текста программы можно было бы включать или не включать в компилируемый код в зависимости от выполнения некоторого наперед заданного условия. Такое действие может быть полезным, например, в процессе отладки программы. Тогда в отладочной версии программы промежуточные результаты вычислений будут выводиться на экран дисплея, в рабочей версии эти действия выполняться не будут.

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

1   #include <stdio.h>

2   #define DEBUG 1

3   void main(void)

4.  {

     :

     :

m   #if DEBUG

m+l  printf{"The program reached this point in the program\n"};

m+2 #endif

     :

     :

n   }

Для этого в строке 2 мы присвоили переменной DEBUG значение 1, используя директиву препроцессора #define. Эту директиву мы обсудим несколько позже, а пока констатируем, что единичное значение переменной DEBUG соответствует условию «истина» в строке m с директорией #if. Поэтому вызов функции prinf, записанный в строке m+1, будет включен в компилируемый текст программы. При ее исполнении мы увидим строку «The program reached this point in the program» на экране монитора. Обратите внимание, что точка с запятой в конце строки с директивой не ставится.

В рассмотренном выше примере предполагалось только две возможности: включать или не включать в исполняемый код программы определенный фрагмент. Директивы #else и #elif расширяют возможности условной компиляции и позволяют выбрать для компиляции один из нескольких фрагментов текста. Например:

1  #define М68НС11 0

2  #define М68НС12 1

3  #define М8051 2

4  #define Processor 1

5  void main(void)

6  {

7  #if Processor == М68НС11

8   Instruction(s) А

9  #elif processor == М68НС12

10  Instruction(s) В



Поделиться книгой:

На главную
Назад