Курс на Си. Подбельский. Курс программирования на Си. В., Фомин С. С. Курс программирования на языке Си Учебник
Скачать 1.57 Mb.
|
Глава 5 ФУНКЦИИ 5.1. Общие сведения о функциях Определение функции. В соответствии с синтаксисом в языке Си определены три производных типа: массив, указатель, функция. В этой главе рассмотрим функции. О функциях в языке Си нужно говорить, рассматривая это понятие с двух сторон. Во-первых, функция, как мы только что сказали, - это один из производных типов (наряду с массивом и указателем). С другой стороны, функция - это минимальный исполняемый модуль программы на языке Си. Синонимами этого второго понятия в других языках программирования являются процедуры, подпрограммы, подпрограммы-функции, процедуры-функции. Все функции в языке Си имеют рекомендуемый стандартами языка единый формат определения: тип имя_функции (спецификация_параметров) тело_функции Первая строка - это заголовок функции. Здесь тип - либо void (для функций, не возвращающих значения), либо обозначение типа возвращаемого функцией значения. В предыдущих главах рассмотрены функции, возвращающие значения базовых типов (char, int, double и т. д.). Имя_функции - либо main для основной (главной) функции программы, либо произвольно выбираемое программистом имя (идентификатор), не совпадающее со служебными словами и с именами других объектов (и функций) программы. Спецификация_параметров - это либо пусто, либо список параметров, каждый элемент которого имеет вид: обозначение_типа имя_параметра Примеры спецификаций параметров уже приводились для случаев, когда параметры были базовых типов или массивами. Список параметров функции может заканчиваться запятой с последующим многоточием «...». Многоточие обозначает возможность обращаться к функции с большим количеством параметров, чем явно указано в спецификации параметров. Такая возможность должна быть «подкреплена» специальными средствами в теле функции. В следующих параграфах этой главы особенности подготовки функций с переменным количеством аргументов будут подробно рассмотрены. Сейчас отметим, что мы уже хорошо знакомы с двумя библиотечными функциями, допускающими изменяемое количество аргументов. Вот их заголовки: int printf (char * format, ...) int scanf (char * format, ...) Указанные функции форматированного вывода и форматированного ввода позволяют применять теоретически неограниченное количество аргументов. Обязательным является только параметр char * format - «форматная строка», внутри которой с помощью спецификаций преобразования определяется реальное количество аргументов, участвующих в обменах. Тело_функции - это часть определения функции, ограниченная фигурными скобками и непосредственно размещенная вслед за заголовком функции. Тело функции может быть либо составным оператором, либо блоком. Напоминаем, что, в отличие от составного оператора, блок включает определения объектов (переменных, массивов и т. д.). Особенность языка Си состоит в невозможности определить внутри тела функции иную функцию. Другими словами, определения функций не могут быть вложенными. Обязательным, но не всегда явно используемым оператором тела функции является оператор возврата из функции в точку вызова, имеющий две формы: return; return выражение; Первая форма соответствует завершению функции, не возвращающей никакого значения, то есть функции, перед именем которой в ее определении указан тип void. Выражение во второй форме оператора return должно иметь тип, указанный перед именем функции в ее определении, либо иметь тип, допускающий автоматическое преобразование к типу возвращаемого функцией значения. Мы отметили обязательность оператора возврата из тела функции, однако оператор return программист может явно не использовать в теле функции, возвращающей значение типа void (ничего не возвращающей). В этом случае компилятор автоматически добавляет оператор return в конец тела функции перед закрывающейся фигурной скобкой «}». Итак, в языке Си допустимы функции и с параметрами, и без параметров, функции, возвращающие значения указанного типа и ничего не возвращающие. Описание функции и ее тип. Для корректного обращения к функции сведения о ней должны быть известны компилятору, то есть до вызова функции в том же файле стандартом рекомендуется помещать ее определение или описание. Для функции описанием служит ее прототип: тип имя_функции (спецификация_параметров); В отличие от заголовка функции, в ее прототипе могут не указываться имена параметров. Например, допустимы и эквивалентны следующие прототипы одной и той же функции: double f (int n, float x); double f (int, float); Вызов функции. Для обращения к функции используется выражение с операцией «круглые скобки»: обозначение_функции (список_аргументов) Операндами операции '( )' служат обозначение_функции и список_ аргументов. Наиболее естественное и понятное обозначение_функ- ции - это ее имя. Кроме того, функцию можно обозначить, разыменовав указатель на нее. Так как указатели на функции мы еще не ввели (см. следующие параграфы этой главы), то ограничимся пока для обозначения функций их именами. Список аргументов - это список выражений, количество которых равно числу параметров функции (исключение составляют функции с переменным количеством аргументов). Соответствие между параметрами и аргументами устанавливается по их взаимному расположению в списках. Порядок вычисления значений аргументов (слева направо или справа налево) стандарт языка Си не определяет. Между параметрами и аргументами должно быть соответствие по типам. Лучше всего, когда тип аргумента совпадает с типом параметра. В противном случае компилятор автоматически добавляет команды преобразования типов, что возможно только в том случае, если такое приведение типов допустимо. Например, пусть определена функция с прототипом: int g(int, long); Далее в программе использован вызов: g(3.0+m, 6.4e+2) Оба аргумента в этом вызове имеют тип double. Компилятор, ориентируясь на прототип функции, автоматически предусмотрит такие преобразования: g((int)(3.0+m), (long) 6.4e+2) Так как вызов функции является выражением, то после выполнения операторов тела функции в точку вызова возвращается некоторое значение, тип которого строго соответствует типу, указанному перед именем функции в ее определении (и прототипе). Например, функция float ft(double x, int n) { if (x return n; } всегда возвращает значение типа float. В выражения, помещенные в операторы return, компилятор автоматически добавит средства для приведения типов, то есть получим (невидимые программисту) операторы: return (float) x; return (float) n; Особое внимание нужно уделить правилам передачи параметров при обращении к функциям. Синтаксис языка Си предусматривает только один способ передачи параметров - передачу по значениям. Это означает, что параметры функции локализованы в ней, то есть недоступны вне определения функции, и никакие операции над параметрами в теле функции не изменяют значений аргументов. Передача параметров по значению предусматривает следующие шаги: При компиляции функции (точнее, при подготовке к ее выполнению) выделяются участки памяти для параметров, то есть параметры оказываются внутренними объектами функции. При этом для параметров типа float формируются объекты типа double, а для параметров типов char и short int создаются объекты типа int. Если параметром является массив, то формируется указатель на начало этого массива и он служит представлением массива-параметра в теле функции. Вычисляются значения выражений, использованных в качестве аргументов при вызове функции. Значения выражений-аргументов заносятся в участки памяти, выделенные для параметров функции. При этом float преобразуется в double, а char и short int - в тип int (см. п. 1). В теле функции выполняется обработка с использованием значений внутренних объектов-параметров, и результат передается в точку вызова функции как возвращаемое ею значение. Никакого влияния на аргументы (на их значения) функция не оказывает. После выхода из функции освобождается память, выделенная для ее параметров. Вызов функции всегда является выражением, однако размещение такого выражения в тексте программы зависит от типа возвращаемого функцией значения. Если в качестве типа возвращаемого значения указан тип void, то функция является функцией без возвращаемого результата. Такая функция не может входить ни в какие выражения, требующие значения, а должна вызываться в виде отдельного выражения-оператора: имя_функции (список_аргументов); Например, следующая функция возвращает значение типа void: void print(int gg, int mm, int dd) { printf("\n год: %d",gg); printf(",\t месяц: %d,",mm); printf(",\t день: %d.", dd); } Обращение к ней print(1966, 11, 22); приведет к такому выводу на экран: год: 1966, месяц: 11, день: 22. Может оказаться полезной и функция, которая не только не возвращает никакого значения (имеет возвращаемое значение типа void), но и не имеет параметров. Например, такая: #include void Real_Time (void) { printf("\n Текущее время: %s", _ _TIME_ _ " (час: мин: сек.)"); } При обращении Real_Time ( ); в результате выполнения функции будет выведено на экран дисплея сообщение: Текущее время: 14:16:25 (час: мин: сек.) Указатели в параметрах функций Указатель-параметр. В предыдущем параграфе мы достаточно подробно рассмотрели механизм передачи параметров при вызове функций. Схема передачи параметров по значениям не оставляет никаких надежд на возможность непосредственно изменить аргумент за счет выполнения операторов тела функции. И это действительно так. Объект вызывающей программы, использованный в качестве аргумента, не может быть изменен из тела функции. Однако существует косвенная возможность изменять значения объектов вызывающей программы действиями в вызванной функции. Эту возможность обеспечивает аппарат указателей. С помощью указателя в вызываемую функцию можно передать адрес любого объекта из вызывающей программы. С помощью выполняемого в тексте функции разыменования указателя мы получаем доступ к адресуемому указателем объекту из вызывающей программы. Тем самым, не изменяя самого параметра (указатель-параметр постоянно содержит только адрес одного и того же объекта), можно изменять объект вызывающей программы. Продемонстрируем изложенную схему на простом примере: #include void positive(int * m) /* Определение функции */ { *m = *m > 0 ? *m : -*m; } void main() { int k=-3; positive(&k); printf("\nk=%d", k); } Результат выполнения программы: k=3 Параметр функции positive( ) - указатель типа int *. При обращении к ней из основной программы main( ) в качестве аргумента используется адрес &k переменной типа int. Внутри функции значение аргумента (то есть адрес &k) «записывается» в участок памяти, выделенный для указателя int *m. Разыменование *m обеспечивает доступ к тому участку памяти, на который в этот момент «смотрит» указатель m. Тем самым в выражении *m = *m>0 ? *m :- *m все действия выполняются над значениями той переменной основной программы (int k), адрес которой (&k) использован в качестве аргумента. Рисунок 5.1 графически иллюстрирует взаимосвязь функции positive( ) и основной программы. При обращении к функции positive( ) она присваивает абсолютное значение той переменной, адрес которой использован в качестве ее аргумента. Рис. 5.1. Схема «настройки» параметра-указателя Пояснив основные принципы воздействия с помощью указателей из тела функции на значения объектов вызывающей программы, напомним, что этой возможностью мы уже неоднократно пользовались, обращаясь к функции scanf( ). Функция scanf( ) имеет один обязательный параметр - форматную строку - и некоторое количество необязательных параметров, каждый из которых должен быть адресом того объекта, значение которого вводится с помощью функции scanf( ). Например: long l; double d; scanf("%ld%le", &l, &d); Здесь в форматной строке спецификаторы преобразования %ld и %le обеспечивают ввод (чтение) от клавиатуры соответственно значений типов long int и double. Передача этих значений переменным long l и double d обеспечивается с помощью их адресов &l и &d, которые используются в качестве аргументов функции scanf( ). Имитация подпрограмм. Подпрограммы отсутствуют в языке Си, однако если использовать обращение к функции в виде оператора-выражения, то получим аналог оператора вызова подпрограммы. (Напомним, что выражение приобретает статус оператора, если после него стоит точка с запятой.) Выше были приведены в качестве примеров функции void print(int gg, int mm, int) и void Real_Time(void), каждая из которых очень похожа на подпрограммы других языков программирования. К сожалению, в языке Си имеется еще одно препятствие для непосредственного использования функции в роли подпрограммы - это рассмотренная выше передача параметров только значениями, то есть передаются значения переменных, а не их адреса. Другими словами, в результате выполнения функции нельзя изменить значения ее аргументов. Таким образом, если Z( ) - функция для вычисления периметра и площади треугольника по длинам сторон E, F, G, то невозможно, записав оператор-выражение Z(E, F, G, PP, SS);, получить результаты (периметр и площадь) в виде значений переменных PP и SS. Так как параметры передаются только значениями, то после выполнения функции Z( ) значения переменных PP и SS останутся прежними. Возможно следующее решение задачи. В определении функции параметры, с помощью которых результаты должны передаваться из функции в точку вызова, специфицируются как указатели. Тогда с помощью этих указателей может быть реализован доступ из тела функции к тем объектам вызывающей программы, которые адресуются параметрами-указателями. Для пояснения сказанного рассмотрим следующую программу: #include { float x,y; /* Прототип функции */ void aa(float *, float *); printf("\n Введите: x="); scanf("%f",&x); printf(" Введите: y="); scanf("%f",&y); /* Вызов функции */ aa(&x,&y); printf(" \n Результат: x=%f y=%f", x, y); } /* Функция, меняющая местами значения переменных, на которые указывают фактические параметры: */ void aa(float * b, float * c) /* b и c - указатели */ { float e; /* Вспомогательная переменная */ e=*b; *b=*c; *c=e; } В основной программе описаны переменные x, y, значения которых пользователь вводит с клавиатуры. Параметрами функции aa( ) служат указатели типа float *. Задача функции - поменять местами значения тех переменных, на которые указывают ее параметры-указатели. При обращении к функции aa( ) адреса переменных x, y используются в качестве аргументов. Поэтому после выполнения программы возможен, например, такой результат: Введите: x=33.3 Введите: y=66.6 Результат: x=66.600000 y=33.300000 Имитация подпрограммы (функция) для вычисления периметра и площади треугольника: #include #include { float x,y,z,pp,ss; /* Прототип: */ int triangle(float, float, float, float *, float *); printf("\n Введите: x="); scanf("%f",&x); printf("\t y="); scanf("%f",&y); printf("\t z="); scanf("%f",&z); if (triangle(x,y,z,&pp,&ss) == 1) { printf(" Периметр = %f",pp); printf(", площадь = %f",ss); } else printf("\n Ошибка в данных "); } /* Определение функции: */ int triangle(float a,float b, float c,float * perimeter, float * area) { float e; *perimeter=*area=0.0; if (a+b<=c || a+c<=b || b+c<=a) return 0; *perimeter=a+b+c; e=*perimeter/2; *area=sqrt(e*(e-a)*(e-b)*(e-c)); return 1 ; } Пример результатов выполнения программы:
Периметр = 12.000000, площадь = 6.000000. Массивы и строки как параметры функций |