Перейти к содержимому

Как создать функцию в си

  • автор:

#6 — Работа с функциями в языке Си

Функции являются небольшими подпрограммами внутри вашей программы. За счёт функций можно вынести повторяющийся код в одно место и далее к нему обращаться. За урок мы научимся работать с функциями в Си.

Видеоурок

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

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

Все функции необходимо записывать перед созданием функции main() . В таком случае при вызове функций у вас не будет появляются ошибок.

Также функции можно лишь объявить перед функцией «main», а после неё прописать.

Создание функции

На основе всех данных наша функция будет выглядеть следующим образом:

void test ()

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

void test (char symbol)

Теперь функция принимает параметр, который будет отображен в консоли.
Также было бы логично прописать описание функции перед функцией «main»:

void test (char symbol);

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

double test (double some_number)
Весь код будет доступен после подписки на проект!

Задание к уроку

Необходимо оформить подписку на проект, чтобы получить доступ ко всем домашним заданиям

Большое задание по курсу

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

Функции внутри функций в Си

Можете пояснить, зачем в начале функция iterate_disk объявляется со спецификатором auto? Я такого никогда не встречал, даже в книжках.

virus
06.03.11 10:55:41 MSK

Если вкратце, то по-другому nested функцию не объявить.

AptGet ★★★
( 06.03.11 11:19:38 MSK )

auto

Defines a local variable as having a local lifetime.

Keyword auto uses the following syntax:

[auto] data-definition;

As the local lifetime is the default for local variables, auto keyword is extremely rarely used.

Note: GNU C extends auto keyword to allow forward declaration of nested functions.

A nested function always has internal linkage. Declaring one with extern is erroneous. If you need to declare the nested function before its definition, use auto (which is otherwise meaningless for function declarations).

int bar (int *array, int offset, int size) < __label__ failure; auto int access (int *, int); /* . */ int access (int *array, int index) < if (index >size) goto failure; return array[index + offset]; > /* . */ >

shty ★★★★★
( 06.03.11 11:21:18 MSK )
Ответ на: комментарий от AptGet 06.03.11 11:19:38 MSK
virus
( 06.03.11 11:24:07 MSK ) автор топика

Что-то в последнее время на лоре все реже и реже вспоминают про маргинальные языки. Чаще вспоминают С и C++, вот она суровая реальность.

anonymous
( 06.03.11 11:36:18 MSK )
Ответ на: комментарий от anonymous 06.03.11 11:36:18 MSK

Просто почти всех интересных людей выгнали.

anonymous
( 06.03.11 11:50:39 MSK )

4.4 Вложенные Функции

Вложенная функция — это функция, определенная внутри другой функции. Имя вложенной функции является локальным в блоке, где она определена. Например, здесь мы определяем вложенную функцию с именем square и вызываем ее дважды:

 foo (double a, double b) < double square (double z) < return z * z; >return square (a) + square (b); > 

Вложенная функция имеет доступ ко всем переменным объемлющей функции, которые видны в точке ее определения. Это называется «лексическая область действия». Например, ниже мы показываем вложенную функцию, которая использует наследуемую переменную с именем offset:

 bar (int *array, int offset, int size) < int access (int *array, int index) < return array[index + offset]; >int i; . for (i = 0; i

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

drBatty ★★
( 06.03.11 12:06:52 MSK )
Ответ на: комментарий от drBatty 06.03.11 12:06:52 MSK

нафик такой изврат ? какие то костыли что мы имеем ? ограничение области видимости глобальных переменных используемых обеими функциями но разве не для этого предназначены глобальные переменные с внутренним связыванием и отдельная единица трансляции ? зачем вводить лишние сущности ? тем более такие костыли так как реально они не нужны ну разве что паскалистам перешедшим на си нехватает такой хрени

anonymous
( 06.03.11 12:52:57 MSK )

Вот это отлично, теперь я могу писать каллбеки к g_hash_table_foreach прямо в функции. Я и не знал про такую фичу.

zombiegrinder_6000
( 06.03.11 12:55:15 MSK )
Ответ на: комментарий от zombiegrinder_6000 06.03.11 12:55:15 MSK

Лучше так не делай.

anonymous
( 06.03.11 14:06:15 MSK )
Ответ на: комментарий от anonymous 06.03.11 12:52:57 MSK

>нафик такой изврат ?

перед коллегами понтоваться же.

для работы за 15+ лет ни разу не надо было. просто как-то встретил в чужом (быдло)коде.

drBatty ★★
( 06.03.11 17:02:49 MSK )
Ответ на: комментарий от drBatty 06.03.11 17:02:49 MSK

А я частенько внутри функций делаю вложенные inline-функции, если хочется улучшить читабельность кода и не копипастить 100500раз одно и то же.

Eddy_Em ☆☆☆☆☆
( 06.03.11 17:35:30 MSK )
Ответ на: комментарий от Eddy_Em 06.03.11 17:35:30 MSK

>А я частенько внутри функций делаю вложенные inline-функции, если хочется улучшить читабельность кода и не копипастить 100500раз одно и то же.

ИМХО можно сделать внешнюю неинлайновую функцию. gcc сейчас умеет их инлайтить как я понял.

drBatty ★★
( 06.03.11 17:53:51 MSK )
Ответ на: комментарий от Eddy_Em 06.03.11 17:35:30 MSK

а никто не смотрел куда компилятор пихает разделяемые переменные ? и как вложенная функция до них дотягивается ? как глобальные переменные не может быть из за многопоточности тогда выходит вложенная функция как то хитро дотягивается в стековый фрейм выше

anonymous
( 06.03.11 17:55:02 MSK )
Ответ на: комментарий от drBatty 06.03.11 17:53:51 MSK

Это лишнее, если inline-функция должна использовать кучу временных переменных функции, из которой вызывается, а писать 100500 аргументов не хочется.

Если inline-функция ничего не возвращает, ее, конечно, проще всего вообще как макрос оформить (а чтобы знать, к чему этот макрос относится, удобно его определить прямо внутри той функции, из которой inline’ы вызываются).

Eddy_Em ☆☆☆☆☆
( 06.03.11 18:01:05 MSK )
Ответ на: комментарий от anonymous 06.03.11 17:55:02 MSK

Чуть мозг не закипел, пока пытался понять. Так и не понял.

Eddy_Em ☆☆☆☆☆
( 06.03.11 18:01:39 MSK )

вложенные функции не входят в стандарт, это расширение GCC => kill it with fire.

annulen ★★★★★
( 06.03.11 18:24:27 MSK )
Ответ на: комментарий от annulen 06.03.11 18:24:27 MSK

> вложенные функции не входят в стандарт, это расширение GCC => kill it with fire.

mukoh ★
( 06.03.11 18:27:37 MSK )
Ответ на: комментарий от annulen 06.03.11 18:24:27 MSK

Разве это в стандарт c99 не входит?

Eddy_Em ☆☆☆☆☆
( 06.03.11 18:43:29 MSK )
Ответ на: комментарий от Eddy_Em 06.03.11 18:43:29 MSK

Да, а если они в стандарт не входят, можно при помощи временных переменных и макросов их реализовать 🙂

Eddy_Em ☆☆☆☆☆
( 06.03.11 18:44:27 MSK )
Ответ на: комментарий от anonymous 06.03.11 17:55:02 MSK

Самое забавное когда вложенная функция передается как аргумент. Она и в этом случае может дотягиваться до локальных переменных «основной» функции. Так как никакого «нормального» способа передать контекст в точку вызова в этом случае нет, то просто генерируется фрагмент кода для данного конкретного вызова, именно адрес оного кода (содержащий загрузку адреса стека основной функции) передается как аргумент. Работает, но изврат однако.

Система защиты ЛОРа достала.

io ★★
( 06.03.11 18:51:06 MSK )
Ответ на: комментарий от Eddy_Em 06.03.11 18:44:27 MSK

>Да, а если они в стандарт не входят, можно при помощи временных переменных и макросов их реализовать 🙂

и усложнить себе-же жизнь 🙁

drBatty ★★
( 06.03.11 19:36:51 MSK )
Ответ на: комментарий от drBatty 06.03.11 19:36:51 MSK

Чем? Наоборот, вместо кучи копипасты с незначительными различиями, имеем несколько строчек.

Eddy_Em ☆☆☆☆☆
( 06.03.11 20:56:51 MSK )
Ответ на: комментарий от drBatty 06.03.11 17:02:49 MSK

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

tensai_cirno ★★★★★
( 06.03.11 22:19:48 MSK )
Ответ на: комментарий от io 06.03.11 18:51:06 MSK

enjoy your functional programming 🙂

Вообще забавно, возьму на заметку.

tensai_cirno ★★★★★
( 06.03.11 22:20:59 MSK )
Ответ на: комментарий от Eddy_Em 06.03.11 18:01:39 MSK

> Чуть мозг не закипел, пока пытался понять. Так и не понял.

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

anonymous
( 07.03.11 10:30:14 MSK )
Ответ на: комментарий от anonymous 06.03.11 17:55:02 MSK
anonymous
( 07.03.11 10:30:47 MSK )
Ответ на: комментарий от Eddy_Em 06.03.11 20:56:51 MSK

>Чем? Наоборот, вместо кучи копипасты с незначительными различиями, имеем несколько строчек.

с этим я не спорю. вот только не все компиляторы такое поддерживают, ваш код будет непереносим. Я лучше займусь сейчас копипастой, чем потом буду переделывать. Благо современные редакторы (vim, emacs, даже kate) отлично копипастят.

drBatty ★★
( 07.03.11 11:21:12 MSK )
Ответ на: комментарий от anonymous 07.03.11 10:30:14 MSK

Т.е. вы считаете, что это предложение написано на русском языке

как глобальные переменные не может быть из за многопоточности тогда выходит вложенная функция как то хитро дотягивается в стековый фрейм выше

Eddy_Em ☆☆☆☆☆
( 07.03.11 11:43:18 MSK )
Ответ на: комментарий от Eddy_Em 07.03.11 11:43:18 MSK

> Т.е. вы считаете, что это предложение написано на русском языке

Согласен, что предложение у него написано криво, тем не менее общий смысл вопроса понять можно.

anonymous
( 07.03.11 12:40:35 MSK )
Ответ на: комментарий от anonymous 07.03.11 12:40:35 MSK

Не, я в тарабарском не разбираюсь 🙂

Eddy_Em ☆☆☆☆☆
( 07.03.11 12:42:25 MSK )
Ответ на: комментарий от Eddy_Em 07.03.11 11:43:18 MSK

С пунктуацией беда просто.

А никто не смотрел, куда компилятор пихает разделяемые переменные? И как вложенная функция до них дотягивается? Как глобальные переменные — не может быть из-за многопоточности; тогда выходит вложенная функция как-то хитро дотягивается в стековый фрейм выше.

anonymous
( 07.03.11 13:01:54 MSK )
Ответ на: комментарий от io 06.03.11 18:51:06 MSK

[quote]Самое забавное когда вложенная функция передается как аргумент. Она и в этом случае может дотягиваться до локальных переменных «основной» функции. Так как никакого «нормального» способа передать контекст в точку вызова в этом случае нет, то просто генерируется фрагмент кода для данного конкретного вызова, именно адрес оного кода (содержащий загрузку адреса стека основной функции) передается как аргумент.[/quote]

Proof в студию. 3 года назад gcc никаких лексических замыканий не делал. С трудом верится, что сейчас что-то подобное есть.

anonymous
( 07.03.11 13:28:01 MSK )
Ответ на: комментарий от anonymous 07.03.11 13:28:01 MSK

С примером нет проблем.
Вот не знаю как его перекосит LOR.

Дабы было не скучно использовал допотопную систему
gcc 3.2 (2002 год) и gdb (2001):

(gdb) l
1 void xxx(int (*)());
2
3 int bimbom()
4 {
5 int a = 0;
6 int tiptop()
7 {
8 a = 1;
9 }
10 xxx(tiptop);
(gdb)
11 return a;
12 }
13
14 main()
15 {
16 printf («%d\n», bimbom());
17 }
18
19 void xxx(int (*f)())
20 {
(gdb)
21 f();
22 }
23
24
(gdb) b bimbom
Breakpoint 1 at 0x4010d0: file proof.c, line 10.
(gdb) run
Starting program: proof

Breakpoint 1, bimbom () at proof.c:10
10 xxx(tiptop);
(gdb) s
5 int a = 0;
(gdb)
10 xxx(tiptop);
(gdb)
xxx (f=0x22fd90) at proof.c:21
21 f();
(gdb) si
0x0022fd90 in ?? ()
(gdb) disas
Этот код был сгенерирован в стеке, сейчас несколько иначе.
Dump of assembler code from 0x22fd90 to 0x22fdd0:
0x22fd90: mov $0x22fda0,%ecx
0x22fd95: jmp 0x401090

_tiptop.0:
pushl %ebp
movl %esp, %ebp
subl $4, %esp
movl %ecx, -4(%ebp)
movl $1, -4(%ecx)
movl %ebp, %esp
popl %ebp
ret
_bimbom:
pushl %ebp
movl %esp, %ebp
leal -24(%ebp), %eax
Генерация кода в стеке:
movl $_tiptop.0-10, %edx
leal -8(%ebp), %ecx
subl %eax, %edx
subl $40, %esp
movl %edx, 6(%eax)
movb $-71, (%eax)
movl %ecx, 1(%eax)
movb $-23, 5(%eax)
movl %eax, (%esp)
movl $0, -12(%ebp)
call _xxx
movl -12(%ebp), %eax
movl %ebp, %esp
popl %ebp
.

Рекомендую попробовать в новом — отличия будут, но принцип
trampoline сохранен.

io ★★
( 07.03.11 20:30:54 MSK )
Ответ на: комментарий от io 07.03.11 20:30:54 MSK

Где здесь генерация «кода»? Вижу оффсеты

anonymous
( 07.03.11 20:41:57 MSK )
Ответ на: комментарий от anonymous 07.03.11 20:41:57 MSK

Здесь нет «генерации кода на стеке», больше похоже на обновление смещений в сегменте кода.

anonymous
( 07.03.11 20:44:48 MSK )
Ответ на: комментарий от anonymous 07.03.11 20:44:48 MSK

Состояние памяти с будущим кодом в момент начала выполнения bimbom:

(gdb) disas 0x22fd90 0x22fd90+10
Dump of assembler code from 0x22fd90 to 0x22fd9a:
0x22fd90: add %al,(%eax)
0x22fd92: add %al,(%eax)
0x22fd94: add %al,(%eax)
0x22fd96: add %al,(%eax)
0x22fd98: test $0xfd,%al
End of assembler dump.

(gdb) disas
Dump of assembler code for function bimbom:
0x4010b0 : push %ebp
0x4010b1 : mov %esp,%ebp
0x4010b3 : lea 0xffffffe8(%ebp),%eax
0x4010b6 : mov $0x401086,%edx
0x4010bb : lea 0xfffffff8(%ebp),%ecx
0x4010be : sub %eax,%edx
0x4010c0 : sub $0x28,%esp
0x4010c3 : mov %edx,0x6(%eax)
0x4010c6 : movb $0xb9,(%eax)
0x4010c9 : mov %ecx,0x1(%eax)
0x4010cc : movb $0xe9,0x5(%eax)
0x4010d0 : mov %eax,(%esp,1)
0x4010d3 : movl $0x0,0xfffffff4(%ebp)
0x4010da : call 0x401120
0x4010df : mov 0xfffffff4(%ebp),%eax
0x4010e2 : mov %ebp,%esp
0x4010e4 : pop %ebp
0x4010e5 : ret
End of assembler dump.
(gdb) b *0x4010da
Breakpoint 2 at 0x4010da: file proof.c, line 10.
(gdb) c
Continuing.

Breakpoint 2, bimbom () at proof.c:10
10 xxx(tiptop);

Состояние той же памяти перед вызовом xxx:

(gdb) disas 0x22fd90 0x22fd90+10
Dump of assembler code from 0x22fd90 to 0x22fd9a:
0x22fd90: mov $0x22fda0,%ecx 0x22fd95: jmp 0x401090
End of assembler dump.

Можно найти байты команд, которые генерируются выше (0xb9, 0xe9):

(gdb) x/16bx 0x22fd90
0x22fd90: 0xb9 0xa0 0xfd 0x22 0x00 0xe9 0xf6 0x12
0x22fd98: 0x1d 0x00 0x22 0x00 0x00 0x00 0x00 0x00
(gdb) p $sp
$3 = (void *) 0x22fd80

Пользовательские функции в Си

Итак, зачем нужны пользовательские функции? Пользовательские функции нужны для того, чтобы программистам было проще писать программы.

Помните, мы говорили о парадигмах программирования, а точнее о структурном программировании. Основной идеей там было то, что любую программу можно написать используя только три основных конструкции: следование, условие и цикл. Теперь к этим конструкциям мы добавим ещё одну – «подпрограммы» – и получим новую парадигму процедурное программирование» .

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

В принципе, мы уже используем эту парадигму. Если вам пока ещё не совсем ясно, почему это проще, то просто представьте, что вместо того чтобы вызвать функцию exp(x) из заголовочного файла math.h вам каждый раз необходимо было бы описывать подробно, как вычислить значение этой функции.

Итак, в этом уроке мы подробно обсудим то, как функции устроены изнутри. А также научимся создавать свои собственные пользовательские функции.

Как устроены функции

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

int main(void)< // заголовок функции // в фигурных скобках записано тело функции >

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

  • тип возвращаемого значения;
  • имя функции;
  • аргументы функции.

Сначала записывается тип возвращаемого значения, например, int , как в функции main . Если функция не должна возвращать никакое значение в программу, то на этом месте пишется ключевое слово void . Казалось бы, что раз функция ничего не возвращает, то и не нужно ничего писать. Раньше, кстати, в языке Си так и было сделано, но потом для единообразия всё-таки добавили. Сейчас современные компиляторы будут выдавать предупреждения/ошибки, если вы не укажете тип возвращаемого значения.
В некоторых языках программирования функции, которые не возвращают никакого значения, называют процедурами (например, pascal). Более того, для создания функций и процедур предусмотрен различный синтаксис. В языке Си такой дискриминации нет.

После типа возвращаемого значения записывается имя функции. Ну а уж после имени указываются типы и количество аргументов, которые передаются в функцию.

Давайте посмотрим на заголовки уже знакомых нам функций.

// функция с именем srand, принимающая целое число, ничего не возвращает void srand(int) //функция с именем sqrt, принимающая вещественное число типа float, возвращает вещественное число типа float float sqrt(float) //функция с именем rand, которая не принимает аргументов, возвращает целое число int rand(void) //функция с именем pow, принимающая два аргумента типа double, возвращает вещественное число типа double double pow(double, double)

Как создать свою функцию

Для того чтобы создать свою функцию, необходимо её полностью описать. Тут действует общее правило: прежде чем использовать – объяви и опиши, как должно работать. Для этого вернёмся к схеме структуры программы на языке Си, которая у нас была в самом первом уроке. Отметим на ней те места, где можно описывать функции.

Уточнение структуры программы. Объявление функций.

Рис.1 Уточнение структуры программы. Объявление функций.

Как видите, имеется аж два места, где это можно сделать.

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

#include // объявляем пользовательскую функцию с именем max_num // вход: два целочисленных параметра с именами a и b // выход: максимальное из двух аргументов int max_num(int a, int b) < int max = b; if (a >b) max = a; return max; > //основная программа int main(void)

Давайте я подробно опишу, как будет работать эта программа. Выполняется тело функции main . Создются целые переменные x , y и m . В переменные x и y считываются данные с клавиатуры. Допустим мы ввели 3 5 , тогда x = 3 , y = 5 . Это вам всё и так должно быть понятно. Теперь следующая строчка

m = max_num(x,y);

Переменной m надо присвоить то, что находится справа от знака = . Там у нас указано имя функции, которую мы создали сами. Компьютер ищет объявление и описание этой функции. Оно находится выше. Согласно этому объявлению данная функция должна принять два целочисленных значения. В нашем случае это значения, записанные в переменных x и y . Т.е. числа 3 и 5 . Обратите внимание, что в функцию передаются не сами переменные x и y , а только значения (два числа), которые в них хранятся. То, что на самом деле передаётся в функцию при её вызове в программе, называется фактическими параметрами функции.

Теперь начинает выполняться функция max_num . Первым делом для каждого параметра, описанного в заголовке функции, создается отдельная временная переменная. В нашем случае создаются две целочисленных переменных с именами a и b . Этим переменным присваиваются значения фактических параметров. Сами же параметры, описанные в заголовке функции, называются формальными параметрами. Итак, формальным параметрам a и b присваиваются значения фактических параметров 3 и 5 соответственно. Теперь a = 3 , b = 5 . Дальше внутри функции мы можем работать с этими переменными так, как будто они обычные переменные.

Создаётся целочисленная переменная с именем max , ей присваивается значение b . Дальше проверяется условие a > b . Если оно истинно, то значение в переменной max следует заменить на a .

Далее следует оператор return , который возвращает в вызывающую программу (функцию main ) значение, записанное в переменной max , т.е. 5 . После чего переменные a , b и max удаляются из памяти. А мы возвращаемся к строке

m = max_num(x,y);

Функция max_num вернула значение 5 , значит теперь справа от знака = записано 5 . Это значение записывается в переменную m. Дальше на экран выводится строчка, и программа завершается.

Внимательно прочитайте последние 4 абазаца ещё раз, чтобы до конца уяснить, как работает программа.

А я пока расскажу, зачем нужен нижний блок описания функций. Представьте себе, что в вашей программе вы написали 20 небольших функций. И все они описаны перед функцией main . Не очень-то удобно добираться до основной программы так долго. Чтобы решить эту проблему, функции можно описывать в нижнем блоке.

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

Прототип функции полностью повторяет заголовок функции, после которого стоит ; . Указав прототип в верхнем блоке, в нижнем мы уже можем полностью описать функцию. Для примера выше это могло бы выглядеть так:

#include int max_num(int, int); int main(void) < int x =0, y = 0; int m = 0; scanf("%d %d", &x, &y); m = max_num(x,y); printf("max(%d,%d) = %d\n",x,y,m); return 0; >int max_num(int a, int b) < int max = b; if (a >b) max = a; return max; >

Всё очень просто. Обратите внимание, что у прототипа функции можно не указывать имена формальных параметров, достаточно просто указать их типы. В примере выше я именно так и сделал.

Сохрани в закладки или поддержи проект.

Практика

Решите предложенные задачи:

Для удобства работы сразу переходите в полноэкранный режим

Дополнительные материалы

  1. пока нет

Функции

Теги: Функции в си, прототип, описание, определение, вызов. Формальные параметры и фактические параметры. Аргументы функции, передача по значению, передача по указателю. Возврат значения.

Введение

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

Но из сложившейся ситуации есть выход! Теперь мы научимся создавать функции на си. Функции, во-первых, помогут выделить в отдельные подпрограммы дублирующийся код, во-вторых, помогут логически разбить программу на части, в-третьих, с функциями в си связано много особенностей, которые позволят использовать новые подходы к структурированию приложений.

Функция – это именованная часть программы, которая может быть многократно вызвана из другого участка программы (в котором эта функция видна). Функция может принимать фиксированное либо переменное число аргументов, а может не иметь аргументов. Функция может как возвращать значение, так и быть пустой (void) и ничего не возвращать.

Мы уже знакомы с многими функциями и знаем, как их вызывать – это функции библиотек stdio, stdlib, string, conio и пр. Более того, main – это тоже функция. Она отличается от остальных только тем, что является точкой входа при запуске приложения.
Функция в си определяется в глобальном контексте. Синтаксис функции:

Самый простой пример – функция, которая принимает число типа float и возвращает квадрат этого числа

#include #include float sqr(float x) < float tmp = x*x; return tmp; >void main()

Внутри функции sqr мы создали локальную переменную, которой присвоили значение аргумента. В качестве аргумента функции передали число 9,3. Служебное слово return возвращает значение переменной tmp. Можно переписать функцию следующим образом:

float sqr(float x)

В данном случае сначала будет выполнено умножение, а после этого возврат значения. В том случае, если функция ничего не возвращает, типом возвращаемого значения будет void. Например, функция, которая печатает квадрат числа:

void printSqr(float x)

в данном случа return означает выход из функции. Если функция ничего не возвращает, то return можно не писать. Тогда функция доработает до конца и произойдёт возврат управления вызывающей функции.

void printSqr(float x)

Если функция не принимает аргументов, то скобки оставляют пустыми. Можно также написать слово void:

void printHelloWorld()
void printHelloWorld(void)

Формальные и фактические параметры

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

Например, пусть есть функция, которая возвращает квадрат числа и функция, которая суммирует два числа.

#include #include //Формальные параметры имеют имена a и b //по ним мы обращаемся к переданным аргументам внутри функции int sum(int a, int b) < return a+b; >float square(float x) < return x*x; >void main() < //Фактические параметры могут иметь любое имя, в том числе и не иметь имени int one = 1; float two = 2.0; //Передаём переменные, вторая переменная приводится к нужному типу printf("%d\n", sum(one, two)); //Передаём числовые константы printf("%d\n", sum(10, 20)); //Передаём числовые константы неверного типа, они автоматически приводится к нужному printf("%d\n", sum(10, 20.f)); //Переменная целого типа приводится к типу с плавающей точкой printf("%.3f\n", square(one)); //В качестве аргумента может выступать и вызов функции, которая возвращает нужное значение printf("%.3f\n", square(sum(2 + 4, 3))); getch(); >

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

#include #include void main() < char c; do < //Сохраняем возвращённое значение в переменную c = getch(); printf("%c", c); >while(c != 'q'); //Возвращённое значение не сохраняется getch(); >

Передача аргументов

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

#include #include void change(int a) < a = 100; printf("%d\n", a); >void main()

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

#include #include void change(int *a) < *a = 100; printf("%d\n", *a); >void main()

Вот теперь программа выводит
200
100
100
Здесь также была создана локальная переменная, но так как передан был адрес, то мы изменили значение переменной d, используя её адрес в оперативной памяти.

В программировании первый способ передачи параметров называют передачей по значению, второй – передачей по указателю. Запомните простое правило: если вы хотите изменить переменную, необходимо передавать функции указатель на эту переменную. Следовательно, чтобы изменить указатель, необходимо передавать указатель на указатель и т.д. Например, напишем функцию, которая будет принимать размер массива типа int и создавать его. С первого взгляда, функция должна выглядеть как-то так:

#include #include #include void init(int *a, unsigned size) < a = (int*) malloc(size * sizeof(int)); >void main() < int *a = NULL; init(a, 100); if (a == NULL) < printf("ERROR"); >else < printf("OKAY. "); free(a); >getch(); >

Но эта функция выведет ERROR. Мы передали адрес переменной. Внутри функции init была создана локальная переменная a, которая хранит адрес массива. После выхода из функции эта локальная переменная была уничтожена. Кроме того, что мы не смогли добиться нужного результата, у нас обнаружилась утечка памяти: была выделена память на куче, но уже не существует переменной, которая бы хранила адрес этого участка.

Для изменения объекта необходимо передавать указатель на него, в данном случае – указатель на указатель.

#include #include #include void init(int **a, unsigned size) < *a = (int*) malloc(size * sizeof(int)); >void main() < int *a = NULL; init(&a, 100); if (a == NULL) < printf("ERROR"); >else < printf("OKAY. "); free(a); >getch(); >

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

#include #include #include #include char* initByString(const char *str) < char *p = (char*) malloc(strlen(str) + 1); strcpy(p, str); return p; >void main()

В этом примере утечки памяти не происходит. Мы выделили память с помощью функции malloc, скопировали туда строку, а после этого вернули указатель. Локальные переменные были удалены, но переменная test хранит адрес участка памяти на куче, поэтому можно его удалить с помощью функции free.

Объявление функции и определение функции. Создание собственной библиотеки

В си можно объявить функцию до её определения. Объявление функции, её прототип, состоит из возвращаемого значения, имени функции и типа аргументов. Имена аргументов можно не писать. Например

#include #include //Прототипы функций. Имена аргументов можно не писать int odd(int); int even(int); void main() < printf("if %d odd? %d\n", 11, odd(11)); printf("if %d odd? %d\n", 10, odd(10)); getch(); >//Определение функций int even(int a) < if (a) < odd(--a); >else < return 1; >> int odd(int a) < if (a) < even(--a); >else < return 0; >>

Это смешанная рекурсия – функция odd возвращает 1, если число нечётное и 0, если чётное.

Обычно объявление функции помещают отдельно, в .h файл, а определение функций в .c файл. Таким образом, заголовочный файл представляет собой интерфейс библиотеки и показывает, как с ней работать, не вдаваясь в содержимое кода.

Давайте создадим простую библиотеку. Для этого нужно будет создать два файла – один с расширением .h и поместить туда прототипы функций, а другой с расширением .c и поместить туда определения этих функций. Если вы работаете с IDE, то .h файл необходимо создавать в папке Заголовочные файлы, а файлы кода в папке Файлы исходного кода. Пусть файлы называются File1.h и File1.c
Перепишем предыдущий код. Вот так будет выглядеть заголовочный файл File1.h

#ifndef _FILE1_H_ #define _FILE1_H_ int odd(int); int even(int); #endif

Содержимое файла исходного кода File1.c

#include "File1.h" int even(int a) < if (a) < odd(--a); >else < return 1; >> int odd(int a) < if (a) < even(--a); >else < return 0; >>

Наша функция main

#include #include #include «File1.h» void main()

Рассмотрим особенности каждого файла. Наш файл, который содержит функцию main, подключает необходимые ему библиотеки а также заголовочный файл File1.h. Теперь компилятору известны прототипы функций, то есть он знает возвращаемый тип, количество и тип аргументов и имена функций.

Заголовочный файл, как и оговаривалось ранее, содержит прототип функций. Также здесь могут быть подключены используемые библиотеки. Макрозащита #define _FILE1_H_ и т.д. используется для предотвращения повторного копирования кода библиотеки при компиляции. Эти строчки можно заменить одной

#pragma once int odd(int); int even(int);

Файл File1.c исходного кода подключает свой заголовочный файл. Всё как обычно логично и просто. В заголовочные файлах принято кроме прототипов функций выносить константы, макроподстановки и определять новые типы данных. Кроме того, именно в заголовочных файлах можно обширно комментировать код и писать примеры его использования.

Передача массива в качестве аргумента

К ак уже говорилось ранее, имя массива подменяется на указатель, поэтому передача одномерного массива эквивалентна передаче указателя. Пример: функция получает массив и его размер и выводит на печать:

#include #include void printArray(int *arr, unsigned size) < unsigned i; for (i = 0; i < size; i++) < printf("%d ", arr[i]); >> void main() < int x[10] = ; printArray(x, 10); getch(); >

В этом примере функция может иметь следующий вид

void printArray(int arr[], unsigned size) < unsigned i; for (i = 0; i < size; i++) < printf("%d ", arr[i]); >>

Также напомню, что правило подмены массива на указатель не рекурсивное. Это значит, что необходимо указывать размерность двумерного массива при передаче

#include #include void printArray(int arr[][5], unsigned size) < unsigned i, j; for (i = 0; i < size; i++) < for (j = 0; j < 5; j++) < printf("%d ", arr[i][j]); >printf("\n"); > > void main() < int x[][5] = < < 1, 2, 3, 4, 5>, < 6, 7, 8, 9, 10>>; printArray(x, 2); getch(); >

Либо, можно писать

#include #include void printArray(int (*arr)[5], unsigned size) < unsigned i, j; for (i = 0; i < size; i++) < for (j = 0; j < 5; j++) < printf("%d ", arr[i][j]); >printf("\n"); > > void main() < int x[][5] = < < 1, 2, 3, 4, 5>, < 6, 7, 8, 9, 10>>; printArray(x, 2); getch(); >

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

#include #include #include #include #define SIZE 10 unsigned* getLengths(const char **words, unsigned size) < unsigned *lengths = NULL; unsigned i; lengths = (unsigned*) malloc(size * sizeof(unsigned)); for (i = 0; i < size; i++) < lengths[i] = strlen(words[i]); >return lengths; > void main() < char **words = NULL; char buffer[128]; unsigned i; unsigned *len = NULL; words = (char**) malloc(SIZE * sizeof(char*)); for (i = 0; i < SIZE; i++) < printf("%d. ", i); scanf("%127s", buffer); words[i] = (char*) malloc(128); strcpy(words[i], buffer); >len = getLengths(words, SIZE); for (i = 0; i < SIZE; i++) < printf("%d ", len[i]); free(words[i]); >free(words); free(len); getch(); >

Можно вместо того, чтобы возвращать указатель на массив, передавать массив, который необходимо заполнить

#include #include #include #include #define SIZE 10 void getLengths(const char **words, unsigned size, unsigned *out) < unsigned i; for (i = 0; i < size; i++) < out[i] = strlen(words[i]); >> void main() < char **words = NULL; char buffer[128]; unsigned i; unsigned *len = NULL; words = (char**) malloc(SIZE * sizeof(char*)); for (i = 0; i < SIZE; i++) < printf("%d. ", i); scanf("%127s", buffer); words[i] = (char*) malloc(128); strcpy(words[i], buffer); >len = (unsigned*) malloc(SIZE * sizeof(unsigned)); getLengths(words, SIZE, len); for (i = 0; i < SIZE; i++) < printf("%d ", len[i]); free(words[i]); >free(words); free(len); getch(); >

На этом первое знакомство с функциями заканчивается: тема очень большая и разбита на несколько статей.

ru-Cyrl 18- tutorial Sypachev S.S. 1989-04-14 sypachev_s_s@mail.ru Stepan Sypachev students

email

Всё ещё не понятно? – пиши вопросы на ящик

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *