Язык Си. Функция, пишем свою функцию. Технические детали.


Язык программирования Си.
Функции.

Небольшие технические детали (в тексте ниже).
лекция № 2 - Функция, пишем свою функцию. Часть 1.

лекция № 2 - Функция, пишем свою функцию. Часть 2.


Функция это именованная последовательность инструкций.
То есть какой-либо последовательности инструкций(команд) можно дать имя, на практике создается функция с определенным именем внутри блока которой создаются инструкции. 
Синтаксис объявления функции:
 тип имя(список параметров через запятую);
 void foo(); 
Описание функции без тела(без реализации) называется объявлением.
Описание функции с телом(с реализацией) называется объявлением и определением. Обычно объявление функций делается в заголовочных файлах(.h), а определение в файлах реализации(.c).
Если в текущей единице трансляции  присутствует только объявление функции(не берем в счет ошибки программиста), это значит что реализация функции находится в другой единице трансляции. Вы должны были уже заметить что для вывода сообщений на экран, мы использовали функцию printf(), для этого нам нужно было ввести это имя в нашу единицу трансляции, мы это делали с помощью подключения заголовочного файла #include<stdio.h>, а вот реализации этой функции мы не наблюдали, все нормально, на этапе компоновки, компоновщик увидит использование функции 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 <math.h>
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 <math.h>
#include <stdio.h>
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 = %d\n", tr_area);
 printf("Area of triangle 2 = %d\n", 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 <stdio.h>
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, выше ссылка уже была, и есть она еще и тут

Думаю ничего не забыл из важного, но в любом случае всегда можно дописать!
Спасибо за внимание!




Комментарии