Перед использованием или реализацией функции необходимо описать ее прототип. Прототип функции сообщает информацию об имени функции, типе возвращаемого значения, количестве и типах ее аргументов. Пример:
int gcd(int x, int y);
Описан прототип функции gcd , возвращающей целое значение, с двумя целыми аргументами. Имена аргументов x и y здесь являются лишь комментариями, не несущими никакой информации для компилятора. Их можно опускать, например, описание
int gcd(int, int);
является вполне допустимым.
Описания прототипов функций обычно выносятся в заголовочные файлы, см. раздел 3.1. Для коротких программ, которые помещаются в одном файле, описания прототипов располагают в начале программы. Рассмотрим пример такой короткой программы.
Пример: вычисление наибольшего общего делителя
Программа вводит с клавиатуры терминала два целых числа, затем вычисляет и печатает их наибольший общий делитель. Непосредственно вычисление наибольшего общего делителя реализовано в виде отдельной функции
Язык си с нуля | #6 Функции в си.
int gcd(int x, int y);
( gcd — от слов greatest common divisor ). Основная функция main лишь вводит исходные данные, вызывает функцию gcd и печатает ответ. Описание прототипа функции gcd располагается в начале текста программы, затем следует функция main и в конце — реализация функции gcd . Приведем полный текст программы:
#include // Описания стандартного ввода-вывода int gcd(int x, int y); // Описание прототипа функции int main() < int x, y, d; printf(«Введите два числа:n»); scanf(«%d%d», y); d = gcd(x, y); printf(«НОД = %dn», d); return 0; >int gcd(int x, int y) < // Реализация функции gcd while (y != 0) < // Инвариант: НОД(x, y) не меняется int r = x % y; // Заменяем пару (x, y) на x = y; // пару (y, r), где r — y = r; // остаток от деления x на y >// Утверждение: y == 0 return x; // НОД(x, 0) = x >
Стоит отметить, что реализация функции gcd располагается в конце текста программы.
Можно было бы расположить реализацию функции в начале текста и при этом сэкономить на описании прототипа. Это, однако, дурной стиль! Лучше всегда, не задумываясь, описывать прототипы всех функций в начале текста, ведь функции могут вызывать друг друга, и правильно упорядочить их (чтобы вызываемая функция была реализована раньше вызывающей) во многих случаях невозможно. К тому же предпочтительнее, чтобы основная функция main , с которой начинается выполнение программы, была бы реализована раньше функций, которые из нее вызываются. Это соответствует технологии «сверху вниз» разработки программы: основная задача решается сразу на первом шаге путем сведения ее к одной или нескольким вспомогательным задачам, которые решаются на следующих шагах.
Передача параметров функциям
В языке Си функциям передаются значения фактических параметров. При вызове функции значения параметров копируются в аппаратный стек, см. раздел 2.3. Следует четко понимать, что изменение формальных параметров в теле функции не приводит к изменению переменных вызывающей программы, передаваемых функции при ее вызове, — ведь функция работает не с самими этими переменными, а с копиями их значений! Рассмотрим, например, следующий фрагмент программы:
Язык Си для начинающих / #6 — Функции в Си
void f(int x); // Описание прототипа функции int main() < . . . int x = 5; f(x); // Значение x по-прежнему равно 5 . . . >void f(int x) < . . . x = 0; // Изменение формального параметра . . . // не приводит к изменению фактического // параметра в вызывающей программе >
Здесь в функции main вызывается функция f , которой передается значение переменной x , равное пяти. Несмотря на то, что в теле функции f формальному параметру x присваивается значение 0 , значение переменной x в функции main не меняется.
Если необходимо, чтобы функция могла изменить значения переменных вызывающей программы, надо передавать ей указатели на эти переменные. Тогда функция может записать любую информацию по переданным адресам. В Си таким образом реализуются выходные и входно-выходные параметры функций. Подробно этот прием уже рассматривался в разделе 3.5.4, где был дан короткий обзор функций printf и scanf из стандартной библиотеки ввода-вывода языка Си. Напомним, что функции ввода scanf надо передавать адреса вводимых переменных, а не их значения.
Пример: расширенный алгоритм Евклида
Вернемся к примеру с расширенным алгоритмом Евклида, подробно рассмотренному в разделе 1.5.2. Напомним, что наибольший общий делитель двух целых чисел выражается в виде их линейной комбинации с целыми коэффициентами. Пусть x и y — два целых числа, хотя бы одно из которых не равно нулю. Тогда их наибольший общий делитель d = НОД(x,y) выражается в виде
d = ux+vy,
где u и v — некоторые целые числа. Алгоритм вычисления чисел d , u , v по заданным x и y называется расширенным алгоритмом Евклида. Мы уже выписывали его на псевдокоде, используя схему построения цикла с помощью инварианта.
Оформим расширенный алгоритм Евклида в виде функции на Си. Назовем ее extGCD (от англ. Extended Greatest Common Divizor ). У этой функции два входных аргумента x , y и три выходных аргумента d , u , v . В случае выходных аргументов надо передавать функции указатели на переменные. Итак, функция имеет следующий прототип:
void extGCD(int x, int y, int *d, int *u, int *v);
При вызове функция вычисляет наибольший общий делитель от двух переданных целых значений x и y и коэффициенты его представления через x и y . Ответ записывается по переданным адресам d , u , v .
Приведем полный текст программы. Функция main вводит исходные данные (числа x и y ), вызывает функцию extGCD и печатает ответ. Функция extGCD использует схему построения цикла с помощью инварианта для реализации расширенного алгоритма Евклида.
#include // Описания стандартного ввода-вывода // Прототип функции extGCD (расш. алгоритм Евклида) void extGCD(int x, int y, int *d, int *u, int *v); int main() < int x, y, d, u, v; printf(«Введите два числа:n»); scanf(«%d%d», y); if (x == 0 y == 0) < printf(«Должно быть хотя бы одно ненулевое.n»); return 1; // Вернуть код некорректного завершения >// Вызываем раширенный алгоритм Евклида extGCD(x, y, u, // Печатаем ответ printf(«НОД = %d, u = %d, v = %dn», d, u, v); return 0; // Вернуть код успешного завершения > void extGCD(int x, int y, int *d, int *u, int *v) < int a, b, q, r, u1, v1, u2, v2; int t; // вспомогательная переменная // инициализация a = x; b = y; u1 = 1; v1 = 0; u2 = 0; v2 = 1; // утверждение: НОД(a, b) == НОД(x, y) // a == u1 * x + v1 * y // b == u2 * x + v2 * y; while (b != 0) < // инвариант: НОД(a, b) == НОД(x, y) // a == u1 * x + v1 * y // b == u2 * x + v2 * y; q = a / b; // целая часть частного a / b r = a % b; // остаток от деления a на b a = b; b = r; // заменяем пару (a, b) на (b, r) // Вычисляем новые значения переменных u1, u2 t = u2; // запоминаем старое значение u2 u2 = u1 — q * u2; // вычисляем новое значение u2 u1 = t; // новое u1 := старое u2 // Аналогично вычисляем новые значения v1, v2 t = v2; v2 = v1 — q * v2; v1 = t; >// утверждение: b == 0 // НОД(a, b) == НОД(m, n) // a == u1 * m + v1 * n; // Выдаем ответ *d = a; *u = u1; *v = v1; >
Пример работы программы:
Введите два числа: 187 51 НОД = 17, u = -1, v = 4
Здесь первая и третья строка напечатаны компьютером, вторая введена человеком.
Источник: intuit.ru
Тема 5. Функции в Си. Создание и использование функций.
В языке Си все программы рассматриваются как функции. Обычно программы на этом языке состоят из большого числа небольших функций. Для каждой из используемых функций приводится описание и определение функции (Описание функции дает информацию о типе функции и о порядке следования параметров.
При определении функции указываются конкретные операторы, которые необходимо выполнить). Функции должны иметь тот же тип, что и значения, которые они возвращают в качестве результатов. По умолчанию предполагается, что функции имеют тип int. Если функция имеет другой тип, он должен быть указан и в вызывающей программе, и в самом определении функции.
Рассмотрим описание функций: можно использовать два различных стиля описания функций (классический и современный стиль). В первом случае формат описания функции следующий:
Эта спецификация описывает имя функции и тип возвращаемого значения.
Современный стиль используется в конструкциях расширенной версии Си, предложенной ANSI. При описании функций в этой версии Си используются специальные средства языка, известные под названием «прототип функции». Описание функции с использованием ее прототипа содержит дополнительно информацию о ее параметрах:
тип имя_функции (пар_инф1, пар_инф2, …);
где параметр пар_инфi– информация о имени и типе формальных параметров.
Определение функции. Так же, как и в описании функции, при определении функций можно использовать два стиля – классический и современный. Классический формат определения функций имеет следующий вид:
тип имя_функции (имена параметров )
Формат описания в современном стиле предусматривает определение параметров функции в скобках, следующих за именем функции:
тип имя_функции (пар_инф, пар_инф, …)
где определение параметра пар_инф – содержит информацию о передаваемом параметре: тип и идентификатор.
Необходимо отметить, что ряд описаний (константы, типы данных, переменные), содержащихся внутри функции (исключение составляет главная функция main0), определены только внутри этой функции. Поэтому язык Си не поддерживает вложенность функций, т.е. одна функция не может быть объявлена внутри другой функции.
Функции могут быть размещены в программе в различном порядке и считаются глобальными для всей программы, включая встроенные функции, описанные до их использования.
Вызов функции осуществляется по имени функции, в скобках указываются фактические аргументы.
Результат выполнения функции возвращается при помощи оператора return. Общий вид:
Оператор завершает выполнение функции и передает управление следующему оператору в вызывающей функции. Это происходит даже в том случае, если оператор returnявляется не последним оператором тела функции.
Можно использовать оператор returnв виде:
Его применение приводит к тому, что функция в которой он содержится, завершает свое выполнение управление (передается) возвращается в вызывающую функцию. Поскольку у данного оператора отсутствует выражение в скобках, никакое значение при этом не передается функции.
floatmult(v,k) /* описание в определении функции */
return(res); > /* возвращает значение типаfloat*/
С помощью оператора returnв вызывающую программу можно передать только одну величину. Если нужно передать две величины, нужно воспользоваться указателями.
Определение функции специфицирует имя, формальные параметры и тело функции. Оно может также специфицировать тип возвращаемого значения и класс памяти функции. Синтаксис определения функции следующий:
Спецификация класса памяти задает класс памяти функции.
в совокупности с описателем определяет тип возвращаемого значения и имя функции. представляет собой список (возможно, пустой) имен формальных параметров, значения которых передаются функции при вызове. задают идентификаторы и типы формальных параметров. составной оператор, содержащий объявления локальных переменных и операторы.
Объявление функции: классическая форма:
Объявление функции специфицирует имя функции, тип возвращаемого значения и, возможно, типы ее аргументов и их число.
Современный стиль описания (объявление прототипов). В списке типов аргументов прототип может содержать также и идентификаторы этих аргументов.
float f1(float a, float b)
int*u, *v; /*uиvявляются указателями*/
temp=*u; /*tempприсваивается значение, на которое указываетu*/
Данная функция изменяет значения переменных xиy. Путем передачи функции адресов переменных х и у мы предоставили ей возможность доступа к ним. Используя указатели и операцию (*), функция смогла извлечь величины, помещенные в соответствующие ячейки памяти, и поменять их местами.
Основная литература: 1осн[173-205],2осн[256-290]
Дополнительная литература:10доп[51-54]
Контрольные вопросы:
1. Могут ли имена формальных и фактических параметров подпрограмм совпадать между собой?
2. В чем состоит отличие описания процедуры и функции?
3. Какие параметры называются формальными и какие – фактическими?
4. Какие существуют стили описания и определения функций?
5. Назовите оператор для возвращения результата выполнения функции?
Источник: studfile.net
Программируем просто
То есть какой-либо последовательности инструкций(команд) можно дать имя, на практике создается функция с определенным именем внутри блока которой создаются инструкции.
Синтаксис объявления функции:
тип имя ( список параметров через запятую );
void foo();
Описание функции без тела(без реализации) называется объявлением.
Описание функции с телом(с реализацией) называется объявлением и определением. Обычно объявление функций делается в заголовочных файлах(.h), а определение в файлах реализации(.c).
Если в текущей единице трансляции присутствует только объявление функции(не берем в счет ошибки программиста), это значит что реализация функции находится в другой единице трансляции. Вы должны были уже заметить что для вывода сообщений на экран, мы использовали функцию printf(), для этого нам нужно было ввести это имя в нашу единицу трансляции, мы это делали с помощью подключения заголовочного файла #include , а вот реализации этой функции мы не наблюдали, все нормально, на этапе компоновки, компоновщик увидит использование функции printf(), найдет ее объявление в заголовочном файле, далее, он не найдет ее реализации в исходном коде, и начнет искать ее готовую реализацию в библиотеках которые присоеденены к проекту. К нашему проекту автоматически присоединяются(линкуются) стандартные библиотеки языка(если этого требует исходный код). Функция может возвращать значение любого из законченных типов (стандартных или пользовательских), либо не возвращать ничего(для этого ее тип должен быть void). Точно так же, функция может принимать аргументы, или не принимать их СОВСЕМ, рассмотрим пример ниже:
void foo_a() > void foo_b(void) >
Функция foo_a() определена как функция без параметров, но это не так!
В языке Си это означает что аргументов может не быть, или они могут быть, но их количество может меняться (аналогия со стандартной библиотечной функцией printf(), в которой количество аргументов всегда разное). Каким способом узнать сколько аргументов было передано, это отдельная тема.
Что касается второй функции foo_b(), то в месте отведенном для описания параметров, мы четко указали void что означает НИКАКИХ АРГУМЕНТОВ. Посмотрите на пример ниже, и попробуйте его отдать компилятору 🙂
void foo_a() > void foo_b(void) > int main() foo_a(); foo_a(1); foo_a(1,2,3); foo_b(); foo_b(1); // compilation error return 0; >
Когда мы что-то передаем в функцию, то то что мы передаем называется аргументом, а что называется параметром я думаю вы уже поняли.
Функции избавляют программиста от многократного написания одного и того же кода. Давайте посмотрим на printf(), представьте себе на сколько увеличилась бы наша программа, если бы вместо каждого использования именованной последовательности инструкций printf(), писали бы всю реализацию(если бы мы ее имели 🙂 ). Кроме того функцию можно использовать в различных частях программы, а в случае улучшения работы функции, нам нужно сделать это улучшение только один раз, в месте определения, а не искать все эти куски кода по всей программе.
К примеру, нам нужно вычислять площадь треугольника, используя формулу Герона, значит создаем следующую функцию:
#include double triangle_area_herons(double aA, double aB, double aC) double p = (aA + aB + aC) / 2.; return sqrt(p *(p — aA) * (p — sB) * (p — sC)); >
Для вычисления квадратного корня(функцией sqrt()), мы подключили стандартный заголовочный файл математической библиотеки math.
Наша функция по вычислению площади готова, теперь её можно смело использовать в коде программы:
#include #include double triangle_area_herons(double aA, double aB, double aC) double p = (aA + aB + aC) / 2.; return sqrt(p *(p — aA) * (p — sB) * (p — sC)); > int main() double tr_area = 0.0; tr_area = triangle_area_herons(12., 10., 6.); printf(«Area of triangle 1 = %dn», tr_area); printf(«Area of triangle 2 = %dn», triangle_area_herons(1., 2., 3.)); return 0; >
Если функция получается большой, к примеру на несколько страниц, то желательно разбивать такие большие функции, на несколько небольших. Это улучшит читаемость кода, и уменьшит вероятные ошибки, которые возникают из-за того что перед глазами программиста не весь текст функции, а приходится листать вверх-вниз для того чтобы понять, как же она работает. НО! Если это возможно.
Иногда невозможно разбить функцию, скажем в 400 строк кода на 5 функций по 80 строк. Наш идеал это — функция, реализация которой умещается в одну страницу кода. Не забываем всегда думать 🙂
В языке Си и С++ для объектов(переменных) и функций есть такое понятие как внутренняя связь(internal linkage) и внешняя связь(external linkage), что означает доступность в других единицах трансляции. Внешняя связь, я думаю вы уже догадались, делает возможность получить доступ к объекту или функции в другой единице трансляции, грубо говоря делает возможным объявить объект или функцию глобальной для всех файлов исходного кода. Смотрите код ниже:
void foo_a() // external linkage > extern void foo_b() // external linkage > static void foo_c() // internal linkage > int main() // external linkage foo_a(); foo_b(); foo_c(); return 0; >
К примеру, мы знаем что в другой единице трансляции есть нужная нам функция, значит нам нужно, перед тем как ее использовать, дать знать компилятору что есть где-то некая функция, и объявить ее прототип, смотрим ниже:
файл main.c
extern void hello(); int main() hello(); return 0; >
Запись extern void hello(); вводит имя внешней функции в нашу область видимости(единицу трансляции)
файл hello.c
#include void hello() printf(«I am from another translation unit!n»); >
После сборки программы, на экране появится нужное нам сообщение 🙂
Но стоит вам изменить void hello() на static void hello(), то вы получите ошибку при сборке.
Есть очень хорошая программа для того чтобы определить, какие обьекты являются со внешней связью а какие с внутренней, называется она NM из GNU Binary Utils, если вы используете один из дистрибутивов Linux(крайне рекомендую, особенно при изучении программирования) то проблем не должно быть с ее использованием, если же Windows . то .
вас никто не заставлял использовать Windows 🙂 , в принципе можно установить Linux Terminal под Windows, называется он Cygwin при установке которого, можно установить практически все полезные программы из Linux.
Приведу небольшой пример скрипта, который собирает из исходного файла объектный, и выводит информацию о том какие объекты и функции с внешней и внутренней связью:
build_and_info.sh
#!/bin/bash clear gcc -c main.c nm -C main.o #nm -C —extern-only main.o #nm —defined-only main.o #nm —undefined-only main.o
Если его выполнить в корне со следующим файлом,
main.c
void foo_a() // external linkage > extern void foo_b() // external linkage > static void foo_c() // internal linkage > int main() // external linkage foo_a(); foo_b(); foo_c(); return 0; >
То на экране вы увидите:
0000000000000000 T foo_a
0000000000000007 T foo_b
000000000000000e t foo_c
0000000000000015 T main
Где большие буковки, означают что функция со внешней связью, о том что и какие буковки означают, читайте в справке к NM, выше ссылка уже была, и есть она еще и тут
Источник: cppprosto.blogspot.com