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


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


Устаревшие прототипы


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


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


/* #1 (Устаревшее объявление функции "foo") */
void foo();

/* #2 (Прототип функции "bar") */
void bar(int count, const char *word);

Устаревшие определения


Давайте перенесёмся прямиком в 1972 год (год выхода языка Си) и вспомним, как программисты того времени определяли свои функции. Напомню, что определение функции связывает её сигнатуру с соответствующим исполняемым блоком (телом). Данный код демонстрирует определение функции add в стиле K&R:


void add(right, left, result)
    int right;
    int left;
    int *result; {
    *result = right + left;
}

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


Неоднозначные ситуации


Не исключено, что при несоблюдении нового синтаксиса прототипов и определений функций, введённых стандартом ANSI C, возможно возникновение трудно отслеживаемых неоднозначных ситуаций. Рассмотрим пример:


#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <limits.h>

/* Устаревшее объявление функции "print_number" */
void print_number();

int main(void) {
        /* Правильно */
        print_number((double)13.359);
        print_number((double)9238.46436);
        print_number((double)18437);

        /* Разврат и беззаконие */
        print_number(UINT64_MAX);
        print_number("First", "Second", "Third");
        print_number(NULL, "Breakfast", &print_number);
}

void print_number(double number) {
        printf("Предоставленное число: [%f]\n", number);
}

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


$ gcc illegal.c -o illegal -Wall
$ ./illegal
Предоставленное число: [13.359000]
Предоставленное число: [9238.464360]
Предоставленное число: [18437.000000]
Предоставленное число: [0.000000]
Предоставленное число: [0.000000]
Предоставленное число: [0.000000]

Также обратите внимание, что даже с флагом -Wall компилятор gcc (Ubuntu 7.4.0-1ubuntu1~18.04.1) 7.4.0 не сгенерировал никаких предупреждений (но было бы крайне желательно).


Исправить данную программу не составит особого труда, достаточно лишь дописать double number в круглых скобках объявления функции print_number на седьмой строчке, после чего любой компилятор, следующий стандарту, укажет на ошибки в функции main():


$ gcc -Wall illegal.c -o illegal
illegal.c: In function ‘main’:
illegal.c:17:22: error: incompatible type for argument 1 of ‘print_number’
         print_number("First", "Second", "Third");
                      ^~~~~~~
illegal.c:7:6: note: expected ‘double’ but argument is of type ‘char *’
 void print_number(double number);
      ^~~~~~~~~~~~
illegal.c:17:9: error: too many arguments to function ‘print_number’
         print_number("First", "Second", "Third");
         ^~~~~~~~~~~~
illegal.c:7:6: note: declared here
 void print_number(double number);
      ^~~~~~~~~~~~
illegal.c:18:22: error: incompatible type for argument 1 of ‘print_number’
         print_number(NULL, "Breakfast", &print_number);
                      ^~~~
illegal.c:7:6: note: expected ‘double’ but argument is of type ‘void *’
 void print_number(double number);
      ^~~~~~~~~~~~
illegal.c:18:9: error: too many arguments to function ‘print_number’
         print_number(NULL, "Breakfast", &print_number);
         ^~~~~~~~~~~~
illegal.c:7:6: note: declared here
 void print_number(double number);
      ^~~~~~~~~~~~

Функции без параметров


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


#include <stdio.h>

/*  Устаревшее объявление функции "do_something" */
void do_something();

int main(void) {
    /* Функцию "do_something" можно вызвать с совершенно
        любыми аргументами */
    do_something(NULL, "Papa Johns", 2842, 1484.3355);
}

void do_something() {
    puts("I am doing something interesting right now!");
}

Исправить приведённый выше код необходимо вставкой ключевого слова void в определении и объявлении функции do_something(), иначе данная программа скомпилируется без ошибок. В данном примере функция main() тоже определена с лексемой void в параметрах, хотя делать это не обязательно.


Заключение


На написание данной статьи меня вдохновила книга Стивена Прата "Язык программирования Си. Лекции и упражнения. Шестое издание", а конкретно секция "Функции с аргументами" пятой главы.

Комментарии (17)


  1. u_235
    19.06.2019 13:27
    +2

    -Wstrict-prototypes -Wmissing-prototypes помогает в таких случаях.


    1. gecube
      19.06.2019 17:44
      +1

      Поддержу. Автор статьи как будто это только сейчас увидел. Там ещё много чудес со стандартами ANSI C разных годов. Ждать цикла статей ?


      1. Gymmasssorla Автор
        19.06.2019 18:02
        +1

        Думал над циклом, но решил ограничиться текущей статьей (т.к. видимо тема не особо интересна большинству). Как думаете насчёт обзора нововведений C11: многопоточность, выравнивания типов, _Generic?


        1. gecube
          19.06.2019 18:14

          Не пишу на С++11, т.к. мне кажется, что язык превратили во Франкенштейна. Реально лучше уж на Раст катануться, чем писать на плюсах.


          1. Gymmasssorla Автор
            19.06.2019 18:15

            Я имел ввиду именно C11, не C++11)


          1. Gymmasssorla Автор
            19.06.2019 18:17

            Я так и сделал, выучил Rust за пару недель и сейчас пишу на нём проекты. C++ в 2019 году выучить на гране с невозможным, слишком язык много в себя набрал нужного и ненужного.


      1. Gymmasssorla Автор
        19.06.2019 18:09
        +1

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


        1. gecube
          19.06.2019 18:14
          +2

          Они просто строгие ворнинги в компиляторе не выставляют. Слабаки (


        1. berez
          19.06.2019 20:03

          Просто на голом С пишет не так и много народу. А многие из тех, кто пишет, учили С++ (это ж типа «си с классами»). В плюсах семантика другая, там пустые скобки — это отсутствие аргументов у функции (т.е. то же самое, что (void)).


          1. Gymmasssorla Автор
            20.06.2019 07:34

            Поддерживаю, C++ это не «Си с классами», у этих языков совершенно другая идеология, синтаксис много где разнится.


  1. mouse_king
    20.06.2019 10:41

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


    1. gecube
      20.06.2019 11:10

      Указатели — это вообще отдельная история ;)


  1. ne_zabudka
    20.06.2019 18:43
    -1

    Компилятор в прототипе игнорирует имена переменных


  1. edo1h
    21.06.2019 20:43

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


    и в чём печальность последствий? код сгенерируется вполне корректный


    1. Gymmasssorla Автор
      21.06.2019 21:10

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


      do_something(NULL, "Papa Johns", 2842, 1484.3355);


  1. edo1h
    21.06.2019 21:26

    запутаться и вызвать не ту функцию


    это уж как-то совсем за уши притянуто )

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

    два предложения вместо двух экранов текста.


    update: нет, я бы не стал вообще писать статью об этой «проблеме», не заслуживает она того, тут можно написать только банальности


    1. Gymmasssorla Автор
      22.06.2019 17:03

      Так лучше, исправил.