На прошлых занятиях мы целиком задавали функции до основной функции main(). Однако
довольно часто бывают ситуации, когда нужно лишь объявить заголовок функции, а
ее реализацию определить позже или даже прописать в другом модуле программы.
Например, пусть у нас имеется все та же функция вычисления периметра
прямоугольника:
#include <stdio.h>
double per_sq(double w, double h)
{
if(w < 0 || h < 0)
return 0;
return 2 * (w + h);
}
int main(void)
{
printf("per = %.2f\n", per_sq(2.5, 3.5));
return 0;
}
Так вот, чтобы
мы могли ее вызывать в функции main() достаточно прописать лишь заголовок
этой функции, а реализацию указать позже. Например, так:
#include <stdio.h>
double per_sq(double w, double h);
int main(void)
{
printf("per = %.2f\n", per_sq(2.5, 3.5));
return 0;
}
double per_sq(double w, double h)
{
if(w < 0 || h < 0)
return 0;
return 2 * (w + h);
}
Программа по-прежнему
компилируется и запускается. Такой заголовок еще называют прототипом функции.
Для формирования объектного файла текущего модуля, то есть, для преобразования
текста программы в машинный код, компилятору достаточно иметь описание
заголовка функции. На основе этой информации он сможет сформировать корректный
вызов функции в программе, а позже для этого вызова подставит адрес с телом
вызываемой функции на этапе сборки всего проекта. В частности, это означает,
что конкретную реализацию функции можно прописывать в любом другом модуле
проекта, или даже выносить ее в заранее скомпилированные объектные файлы,
которые, затем, следует указывать при линковке (сборке) проекта.
Давайте в этом
убедимся. Создадим в рабочем каталоге еще один файл с именем func.c и пропишем в нем
целиком функцию per_sq():
double per_sq(double w, double h)
{
if(w < 0 || h < 0)
return 0;
return 2 * (w + h);
}
А из файла с
функцией main() реализацию
функции per_sq() удалим. Если
прямо сейчас попробовать скомпилировать программу, то получим ошибку на этапе
линковки проекта, что отсутствует реализация для функции per_sq().
Это из-за того,
что второй файл func.c не был включен в
проект для совместной компиляции его с основным файлом программы. Давайте его
добавим. Для этого перейдем в настройки (файл settings.json) и для
компиляции Си-программ явно пропишем два файла (lessons.c и func.c):
gcc
-std=c99 lessons.c func.c -o lessons
Теперь второй
файл func.c также будет
скомпилирован и реализация функции per_sq() будет взята
из него.
Как видите,
прототипы функций предоставляют нам удобный инструмент оформления программы в
нескольких файлах. Этим очень часто пользуются на практике и, наверное, не
существует ни одного серьезного проекта, который бы не включал в себя множество
файлов с программой. Скорее, наоборот, использование одного файла – это
исключение, чаще всего, при обучении.
Итак, мы с вами
увидели, как можно объявлять заголовок (прототип) функции и узнали, где это
используется. Отмечу лишь, что в прототипе имена переменных можно опускать,
компилятор их все равно не учитывает:
double per_sq(double, double);
Для него главная
информация – это возвращаемый тип, имя функции и количество параметров с их
типами. Больше ничего ему не нужно для формирования вызова функции на уровне
машинных кодов.
Прототипы функций в заголовочном файле
Полученный текст
программы, описанный в двух модулях, имеет один существенный недостаток. Если
модулей станет больше и определение функции в файле func.c изменится, то
его придется менять во всех прототипах других модулей. Поэтому поступают
следующим образом. Все общие прототипы выносятся в заголовочные файлы,
например, в файл func.h:
#ifndef __FUNC_H__
#define __FUNC_H__
double per_sq(double, double);
#endif
А в модулях, где
используется функция per_sq() подключается
этот файл с помощью директивы #include:
#include <stdio.h>
#include "func.h"
int main(void)
{
printf("per = %.2f\n", per_sq(2.5, 3.5));
return 0;
}
Теперь, при
изменении сигнатуры функции ее достаточно будет изменить в заголовочном файле func.h и изменение
автоматически коснется всех модулей, где этот файл подключается.
Обратите также
внимание на оформление файла func.h. В нем
прописаны директивы препроцессора #ifndef, #define и #endif. Они
необходимы, чтобы при повторном подключении этого заголовочного файла в один и
тот же модуль дважды не попадало содержимое этого файла. Подробно об этом мы с
вами уже говорили, когда рассматривали условные директивы препроцессора.
Самих же
прототипов различных функций в программе может быть сколько угодно. Например, в
файле func.c можно объявить
еще несколько функций:
int abs_int(int x)
{
return (x < 0) ? -x : x;
}
int sq_to_int(double x)
{
return (int )(x * x);
}
Прописать их
прототипы в файле func.h:
#ifndef __FUNC_H__
#define __FUNC_H__
double per_sq(double, double);
int abs_int(int);
int sq_to_int(double);
#endif
И тогда все они
станут доступны во всех модулях, где подключен файл func.h.