Прототипы функций

Практический курс по C/C++: https://stepik.org/course/193691

На прошлых занятиях мы целиком задавали функции до основной функции 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.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме