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) В
11 #elif processor == М8051
12 Instruction(s) С
13 #else
14 Instruction(s) D
15 #endif
16 }
В этом примере исходный текст программы написан таким образом, что он может быть компилирован для исполнения различными микроконтроллерами: Motorola HC11, Motorola HC12 и Intel 8051. Директивы #if, #elif и #else позволяют для данного сеанса компиляции выбрать конкретный тип МК. Для этого в строках 1…3 программы каждому символьному имени МК присвоено определенное численное значение. Далее в зависимости от выбранного типа МК переменной Processor присваивается желаемое значение. В примере мы собираемся компилировать программу для МК HC12, поэтому присвоили переменной Processor значение 1. В строке 7 компилятор проверяет истинность выражения, записанного в качестве условия директивы #if. Это условие не выполняется, поскольку Processor = 1 ≠ М68НС11 = 0. Поэтому группа инструкций Instruction(s) A не будет включена в программу. Далее в строке 9 компилятор обнаружит выполнение условия директивы #elif, и выражения Instruction(s) B будут присутствовать в конечном варианте программы. Условие строки 11 не выполняется, и группа инструкций Instruction(s) C в исполняемом коде программы присутствовать не будет. Если ни одно из условий для директив #elif не выполнено, то выражения, следующие за директивой #else, будут включены в программу автоматически.