Функции в языке СИ

advertisement
Функции в языке СИ

Всякая программа на языке Си представляет собой совокупность
функций, выполняющих основную работу по реализации некоторого
алгоритма

Функции это независимый набор описаний и
заключенных между заголовком функции и ее концом

Все объекты, определенные в теле функции, ограниченном
открывающей и закрывающей фигурными скобками, являются
локальными для этой функции в смысле области видимости и
времени существования

В составе общей программы любая функция идентифицируется своим
собственным уникальным именем, которым может быть любое
правильное имя в смысле грамматики языка Си
операторов,

Функция, с которой начинается выполнение программы, называется
главной функцией и должна иметь предопределенное имя
void main()
{

}
Все остальные функции, входящие в программу, запускаются в работу
путем их прямого или опосредованного (через другие функции)
вызова из главной функции

Используя функции, исходную задачу можно представить в виде
последовательности более простых задач, каждая из которых
реализует некоторую часть общего алгоритма

Оперируя функциями, состоящими из сравнительно небольшого числа
операторов,
значительно
легче
удовлетворить
требованиям
структурного программирования и снизить трудозатраты на отладку
программного обеспечения

С другой стороны, в виде функций могут быть реализованы
отдельные часто используемые алгоритмы. Включение таких функций
в состав стандартных библиотек дает принципиальную возможность
не перепрограммировать всякий раз наиболее ходовые методы,
алгоритмы и операции

Для организации связи между независимыми функциями в языке Си
используется либо аппарат формальных/фактических параметров,
либо набор глобальных или внешних переменных

Формальными параметрами мы будем называть аргументы функции,
стоящие в ее заголовке и имена которых используются для построения
тела функции при ее определении

Фактическими же параметрами являются произвольные выражения,
значения которых передаются формальным параметрам при
обращении к функции. Таким образом реализуется возможность
передачи необходимой информации от вызывающей функции к
вызываемой непосредственно в момент ее вызова

Имя функции может быть использовано для передачи значения

Поэтому необходимо связать с этим именем конкретный тип данных
из числа поддерживаемых компилятором языка Си

Эта связь устанавливается при определении самой функции или при
составлении ее предварительного описания

Определяя функцию в соответствующей программной компоненте,
нужно, прежде всего,



указать ее имя и тип возвращаемого значения
задать список формальных параметров
определить тип каждого из них

Такую совокупность описаний принято называть заголовком функции

Вслед за ним должно размещаться тело функции, представляющее
собой правильный блок, т.е. набор описаний и операторов,
заключенных в фигурные скобки

В языке Си определение функции имеет следующий формат:
<sc-specifier> <type-specifier> declarator (<parameter-list>)
<parameter-declarations>
function-body

Например,
void work(n, beta)
int n; float beta;
{ .................
.................
.................
}



void work(int n, float beta)
{ .................
.................
.................
}
sc-specifier - описатель класса памяти (static или extern)
type-specifier - тип возвращаемого функцией значения
declarator – имя (идентификатор) функции, перед которым может стоять
символ звездочка (*), если эта функция возвращает указатель на элемент
данных соответствующего типа

Никакая функция не должна возвращать массивы или другие
функции, но допустима передача указателей на эти объекты

В тех случаях, когда функция не вырабатывает никакого значения или
возвращает указатель неопределенного типа, ее описание должно
начинаться с ключевого слова void, стоящего на месте имени типа
возвращаемого значения

Те параметры, имена которых не объявлены явным образом в одной из
инструкций описания, по умолчанию получают тип int, назначаемый
компилятором

Память под размещение формальных параметров выделяется
динамически из ресурса программного стека в момент обращения к
соответствующей функции. Это означает, что параметры всегда
должны иметь класс памяти auto или register, причем первый из
них назначается компилятором по умолчанию

Переменные, которые описаны в теле функции будут локальными
для этой функции, ибо область их видимости ограничена
соответствующим блоком. Они создаются в момент обращения к
данной функции и исчезают по окончании ее работы (класс памяти
auto), а их имена не должны совпадать с именами формальных
параметров

Поскольку память под размещение локальных переменных
выделяется исполняющей системой динамически из программного
стека, последний должен иметь достаточную для этого длину

Инициализация локальных объектов допустима лишь в случае
простых переменных и невозможна для массивов и других
структурированных данных

Выполнение инструкций в теле функции начинается с самого первого
оператора и продолжается до тех пор, пока не встретится оператор
возврата return, либо пока не будет достигнут конец внешнего блока
(})

Возвращаемое функцией значение равно значению выражения в
операторе return, а при его отсутствии считается неопределенным.
В случае необходимости тип результата преобразуется к типу
функции стандартным образом

Инструкция вызова функции в общем случае имеет следующий
формат:
expression (<expression-list>)
Например:
void work(int n, float beta)
{ .................
.................
.................
}

void main()
{
...
int n; float b;
work(n,b);
}
Вызов функции может представлять собой выражение, которое может
играть роль операнда в составе более сложного выражения

Таким образом, в языке Си реализован механизм передачи параметров
по значению

Всякая функция работает лишь с копиями значений своих
аргументов, поэтому никакие изменения значений формальных
параметров в теле функции не могут отразиться на значениях
фактических параметров

Это в свою очередь означает, что аргументы функции являются
носителями лишь входной информации и не могут быть использованы
для передачи результатов ее работы вызвавшей функции

Для преодоления этого ограничения необходимо использовать
указатели в качестве аргументов функций, передавая тем самым
числовые значения соответствующих адресов. Таким же образом
решается проблема передачи массивов, функций и некоторых других
структурированных данных

Для иллюстрации приведем пример программы, вычисляющей
квадратный корень из числа, введенного с клавиатуры консольного
терминала.

Основная работа по нахождению корня выполняется функцией
sqrt(),
реализующей
итерационный
метод
Ньютона
с
фиксированным числом итераций
#include <stdio.h>
void main()
{ float dat;
float sqrt(float); /* Описание функции */
printf("\nЗадайте положительное вещественное число... ");
scanf("%f", &dat);
printf("\nКорень из числа %.3f равен %.3f", dat,sqrt(dat));
}
float sqrt(float arg)
{ int count;
float root = arg/2.0;
for (count = 1; count <= 5; count++) root = 0.5*(root +
arg/root);
return (root);
}

Если теперь в ответ на запрос программы ввести, например, число 25,
то по окончании ее работы будет напечатан следующий результат:
Корень из числа 25.000 равен 5.000

Заметим, что значение переменной dat в главной функции ни при
каких обстоятельствах не может быть изменено при выполнении
итерационного алгоритма в теле функции sqrt() , поскольку вся
работа здесь ведется с копией значения, переданного через параметр
arg

При решении различных задач с помощью компьютера бывает необходимо
вычислить логарифм или модуль числа, синус угла и т.д.


Вычисления часто употребляемых функций осуществляются посредством
подпрограмм, называемых стандартными функциями, которые заранее
запрограммированы и встроены в транслятор языка.
Название и математическое обозначение функции
Абсолютная величина (модуль)
Стандартная Функция на
функция в псевдоязыке
Си++
|х|
abs(x)
abs(x)
x
sqrt(x)
sqrt(x)
Натуральный логарифм
ln x
log(x)
ln(x)
Десятичный логарифм
lg x
log10(x)
lg(x)
Экспонента (степень числа е ~ 2.72)
ex
exp(x)
exp(x)
Корень квадратный
Знак числа x ( - 1, если х<0; 0, если x = 0;
1, если x > 0)
sign(x)
Целая часть х (т.е. округление в меньшую
сторону)
floor(x)
Int(x)
Округление в большую сторону
ceil(x)
Минимум из чисел х и y
min(x,y)
Максимум из чисел х и y
max(x,y)
max(x,y)
Остаток от деления целого х на целое y
x%y
mod(x,y)
Случайное число в диапазоне от 0 до х – 1
random(x)
Rnd(x)
min(x,y)
Синус (угол в радианах)
sin x
sin(x)
Sin(x)
Косинус (угол в радианах)
cos x
cos(x)
cos(x)
Тангенс (угол в радианах)
tg x
tan(x)
tg(x)
Арксинус (главное значение в радианах)
arcsin x
asin(x)
arcsin(x)
Арккосинус (главное значение в радианах)
arccos x
acos(x)
arccos(x)
Арктангенс (главное значение в радианах)
arctg x
atan(x)
arctg(x)
Предварительное описание функций

Отдельные функции, входящие в состав какой-либо программы на
языке Си, являются абсолютно независимыми одна от другой

Каждая из них может быть определен




в произвольном месте исходного файла
в отдельном файле
находиться в одной из внешних библиотек
Такая организация Си-программы, обладает большой гибкостью и
полностью отвечает требованиям структурного программирования,
однако затрудняет осуществление контроля со стороны компилятора
за взаимным соответствием типов и количества формальных и
фактических параметров, а также за правильностью типа
возвращаемого той или иной функцией значения

Причина этого заключается в том, что любая функция может быть
вызвана раньше, чем она определена в текущем файле, быть внешней
или библиотечной функцией

В такой ситуации компилятор не имеет возможности следить за
правильностью вызова

В подобных случаях компилятор пытается интерпретировать вызов
некоторым стандартным образом, присваивая возвращаемому
функцией значению тип int и не выполняя проверки правильности
списка аргументов

В то же время, возможное несоответствие типов и количества
формальных и фактических параметров или отличие типа реально
возвращаемого функцией значения от int могут привести к трудно
обнаруживаемым ошибкам в программе

Простой выход из этого положения заключается в составлении
предварительного описания, или прототипа, вызываемой функции, в
котором будут определены основные ее атрибуты

В самом общем случае такое предварительное описание имеет
следующий формат:
<sc-specifier> <type-specifier> declarator (<arg-list>)
<, declarator(<arg-list>) ... >;

Здесь sc-specifier задает класс памяти (static или extern), который
имеет вызываемая функция, type-specifier устанавливает тип
возвращаемого ей значения, а arg-list определяет количество и тип
аргументов,
declarator
в
приведенной
схеме
является
идентификатором функции, возможно модифицированным при
помощи круглых скобок и символа звездочка для указателей на
функции и функций, возвращающих указатели
double sin(float a);
double cos(float);

Введенное таким образом понятие предварительного описания
функции дает возможность компилятору построить некоторый шаблон
этой функции до ее фактического определения в текущем или
внешнем файле

Этот шаблон может быть использован для контроля правильности
типа возвращаемого функцией значения и соответствия формальных и
фактических параметров

Ниже приведены несколько характерных примеров построения
предварительных описаний

1. В этом примере описана функция с именем add , возвращающая
значение типа double и оба аргумента которой являются указателями
на тип float:
double add(float*, float*);

2. Если функция с именем sum имеет два аргумента типа double и
возвращает указатель на массив трех элементов типа double, то ее
предварительное описание должно иметь следующий вид:
double (*sum(double, double))[3];

3. В том случае, когда функция не имеет аргументов и возврашает
указатель неопределенного типа, в ее предварительном описании
необходимо использовать ключевое слово void на месте имени типа
возвращаемого значения и списка аргументов:
void *draw(void);
Использование указателей в качестве аргументов функций

Принятое в языке Си соглашение о передаче параметров по значению
является достаточно естественным и обеспечивает полную
независимость отдельных функций, однако в ряде практически
важных случаев оно оказывается излишне ограничительным

Например, передача по значению структурированных данных,
например массивов, оказывается мало эффективной из-за большой
потери времени на копирование элементов передаваемой структуры и
неоправданного перерасхода ресурса памяти на создание такой копии

Возможность преодоления всех отмеченных выше трудностей
открывается в связи с применением мощного аппарата указателей,
значения которых могут передаваться из одной функции в другую

Такая передача значений указателей по существу реализует механизм
доступа к переменным, определенным вне тела функции, по адресной
ссылке. Получив же необходимый адрес, функция, в свою очередь,
может использовать его для доступа к соответствующему значению

Рассмотрим простой случай передачи адресов скалярных переменных

Напишем функцию, заменяющую значение своего аргумента на его
абсолютную величину
void abs(int *arg)
{*arg = (*arg >= 0) ? (*arg) : -(*arg);}

Теперь в случае обращения
abs(&value);
адрес переменной value будет передан в тело функции abs(),
которая заменит числовое значение, размещенное по этому адресу, на
его абсолютную величину


Другим уже знакомым нам примером передачи адресов через аппарат
формальных/фактических параметров может служить использование
стандартной функции scanf() для форматированного ввода
значений с клавиатуры консольного терминала
Действительно, обращение вида
scanf("%d", &alpha);

делает переменную alpha доступной в теле этой функции
непосредственно через ее адрес, в результате чего введенное числовое
значение будет известно и в точке вызова

Наиболее важную роль при разработке программ на языке Си играет
возможность передачи между отдельными функциями массивов
переменных и, в частности, символьных строк

Здесь вновь оказывается полезным аппарат указателей, ибо для
обеспечения доступа к массиву в теле всякой функции ей достаточно
передать адрес его нулевого элемента, причем носителем последнего
является само имя этого массива

В следующем примере функция summa(), выполняющая
суммирование элементов числового массива, получает от
вызывающей ее функции main() адрес начала массива vector и
общее количество его элементов:
void main()
{ float s, vector[100], float summa();
/* Описание функции*/
......................
......................
s = summa(vector, 100);
......................
......................
}
float summa(mas, n)
float mas[]; int n;
{ int i;
float sum = 0;
for (i = 0; i < n; i++) sum += mas[i];
return (sum);
}

Обратим внимание на то, что при описании формального массива mas
в заголовке функции summa() его размерность явно не указана. В
этом нет необходимости, поскольку фактическая работа будет
выполняться здесь над массивом vector, адрес начала и длина
которого передаются при обращении к этой функции

Вместо описания массива неопределенной длины можно иметь
эквивалентное ему описание
float *mas;
определяющее указатель на начало обрабатываемого массива

Примером использования параметров для передачи символьных строк
могут служить функции strcmp() , strcpy() , strlen() и другие
подпрограммы обработки символьных строк. Аргументами каждой из
них являются массивы элементов типа char , идентифицируемые
своими начальными адресами. В этом случае уже не нужно
дополнительно передавать количество обрабатываемых символов,
конец всякой строки легко находится по завершающему ее нульсимволу
Использование указателей для передачи одних функций в
другие

Определим понятие указателя на функцию. Указатель на функцию
связывается с адресом первого исполняемого оператора функции,
задающем ее входную точку

Указатель на функцию определяется в программе подобно тому, как и
сами функции, с той лишь разницей, что в этом случае заголовок
выглядит следующим образом:
<sc-specifier> <type-specifier> (*identifier) (<parameter-list>)
<parameter-declarations>
float (*calc)(float alpha, beta)
{ .........................
.........................
}
float (*calc)(float alpha, beta)
{ .........................
.........................
}

Обращение же к этой функции будут выглядеть так
(*calc)(x, y);
где переменные x и y имеют тип float и играют роль фактических
параметров

Рассмотрим фрагмент программы, вычисляющей проекцию отрезка
длины len , составляющего угол alpha с осью абсцисс, на одно из
двух координатных направлений в зависимости от значения ключа
direct :
double len;
/* Длина отрезка */
double alpha; /* Угол с осью x */
void main()
{ char direct;
/* Ключ направления */
double px, py; /* Длины проекций */
double cos(double), sin(double);
/* Описания вызываемых */
double proect(double (*)(double)); /* функций */
................................
................................
switch (direct)
{ case 'x': px = proect(cos); /* Проекция на ось x */
break;
case 'y': py = proect(sin); /* Проекция на ось y */
break;
}
................................
................................
}
double proect(double (*func)(double))
{ return (len*(*func)(alpha)); }
/* Указатель на функцию */

В этом примере формальным параметром функции proect является
указатель func на функцию, возвращающую значение типа double
double proect(double (*func)(double))
{ return (len*(*func)(alpha)); }

Фактически ми же параметрами при ее вызове становятся имена
функций cos() и sin() , задающие соответствующие входные
точки
px = proect(cos);
py = proect(sin);
Рекурсивные вызовы функций

Всякая функция в языке Си имеет реентерабельный (повторно
входимый) код, что позволяет ей обращаться к самой себе
непосредственно или через другие функции

Такие обращения
рекурсией

При каждом очередном рекурсивном вызове создается новая копия
параметров функции, а также определенных в ее теле автоматических
и регистровых переменных. Внешние и статические переменные,
имеющие глобальное время существования, сохраняют при этом свои
прежние значения и размещение памяти
называются
рекурсивными
вызовами
или

Несмотря на то, что ни стандарт языка Си, ни компилятор формально
не налагают никакого ограничения на количество рекурсивных
обращений, тем не менее оно практически всегда существует для
любых типов компьютеров, ибо каждый новый вызов требует
дополнительной памяти из ресурса программного стека

Если количество вызовов излишне велико, возникает переполнение
сегмента стека и операционная система уже не может создать
очередного экземпляра локальных объектов функции, что ведет, как
правило, к аварийному завершению программы

В качестве примера реализации рекурсивного алгоритма рассмотрим
функцию printd() , печатающую целое число в виде
последовательности символов ASCII (т.е. цифр, образующих запись
этого числа):
void printd(int num)
{ int i;
if (num < 0) { putchar('-'); num = -num; }
if ((i = num/10) != 0) printd(i);
putchar(num % 10 + '0') ;
}

Если значение переменной value равно 123, то в случае вызова
void printd(value);

эта функция дважды обратится сама к себе для печати цифр заданного
числа

Классическим примером написания рекурсивной функции является
вычисление факториала целого числа. Разумеется, эту задачу легко
решить при помощи обычного цикла, но на этом простом примере
наглядно видна идея рекурсивного алгоритма

Текст такой функции достаточно прост:
/* Рекурсивное вычисление n! */
int fact(int n)
{ if (n==0) return (1);
else
return(n*fact(n-1));
}

Если обратиться к этой функции, например, так:
int m;
...
m = fact(5);

то, прежде чем получить значение 5!, функция должна вызвать самое себя как
fact(4) , та, в свою очередь, вызывает fact(3) . Так будет продолжаться
до вызова fact(0) . Лишь после этого вызова будет получено конкретное
число (единица). Затем все пойдет в обратном порядке, и в конце концов мы
получим результат от обращения fact(5) : 120
Порядок вызовов ¦
¦
fact(5)
¦
fact(4)
¦
fact(3)
¦
fact(2)
¦
fact(1)
¦
fact(0)
¦
Порядок возвратов |
|
return(1)
|
return(1*0!)
|
return(2*1!)
|
return(3*2!)
|
return(4*3!)
|
return(5*4!)
|
Возвращаемое
значение
1
1
2
6
24
120

Последнее возвращаемое значение будет присвоено переменной m. Обратите
внимание на то, что при каждом новом вызове предыдущий еще не закончил
работу, поэтому параметры, переданные функции при прежнем обращении,
еще хранятся в стеке

При очередном вызове стек наращивается, т.к. в него загружаются копии
других параметров. Очистка стека будет происходить постепенно, и он
полностью будет очищен лишь после возврата окончательного результата
Аргументы командной строки


Те, кому хоть раз приходилось работать с командной строкой
операционной среды, видимо обратили внимание на то, что
большинство команд пользовательского интерфейса могут иметь один
или более параметров, называемых аргументами командной строки
Так, например, обращение к команде copy, выполняющей
копирование файлов, обычно выглядит следующим образом:
copy oldfile.txt newfile.txt

где параметры oldfile.txt и newfile.txt определяют имена
файла-источника и файла-приемника соответственно. Эти параметры
обрабатываются командным процессором и передаются в тело
программы copy, в результате чего последняя узнает о файлах, над
которыми должна быть выполнена операция копирования



Поскольку язык Си часто применяется при разработке системного
программного обеспечения, он имеет встроенные средства для
получения аргументов команды непосредственно от командного
процессора
Любая функция, входящая в состав Си-программы, может иметь
параметры, через которые она получает необходимую информацию от
вызывающей ее функции
Совершенно аналогично, главная функция main(), с которой
начинается выполнение всякой программы, могла бы в момент вызова
получать исходные данные через аргументы командной строки. Для
этого достаточно снабдить функцию main() набором параметров,
которые обычно имеют имена argc и argv :
main(argc, argv) {
}


Параметр argc (ARGument Count) является переменной типа int ,
получающей от командного процессора информацию о количестве
аргументов, набранных в командной строке, включая и имя самой
команды
Второй параметр argv (ARGument Vector) обычно определяется как
массив указателей типа char , каждый из которых хранит адрес
начала отдельного слова командной строки. Их описание в программе
может выглядеть следующим образом:
int argc;
char *argv[];

В соответствии с принятым соглашением, нулевой элемент argv[0]
массива указателей ссылается на строку символов, содержащую имя
самой команды и поэтому параметр argc всегда имеет значение
большее или равное единице. Следующий элемент argv[1] задает
адрес размещения в памяти первого аргумента команды, также
представленного последовательностью символов, и т.д. Обращаясь к
очередному элементу массива argv нетрудно получить доступ ко всем
аргументам командной строки

В качестве примера, иллюстрирующего работу с параметрами
функции main() , рассмотрим программу, выводящую на экран
терминала свои собственные аргументы и реализующую команду
echo:
#include <stdio.h>
main(argc, argv)
int argc; char *argv[];
{ int i;
for (i = 1; i <= argc; i++)
printf("%s%c", argv[i], (i < argc-1) ? ' ' : '\n');
}

Поместив теперь загрузочный модуль этой программы в файл
echo.exe и обратившись к ней при помощи команды
C:\> echo first second third

получим на экране терминала такое сообщение
first second third
C:\>

Поскольку массив указателей в определенном смысле эквивалентен
"указателю на указатель", мы могли бы определить переменную argv
в заголовке функции main() как косвенный указатель типа char :
char **argv;

что полностью равносильно предыдущему описанию. В этих
терминах наша программа echo могла бы выглядеть, например,
следующим образом:
#include <stdio.h>
main(int argc, char **argv)
{ while (--argc > 0)
printf((argc > 1) ? "%s " : "%s\n", *++argv);
}
где выражение ++argv увеличивает на единицу значение указателя,
заставляя его ссылаться на очередную строку, полученную от
командного процессора
Download