Неопределённое поведение с устаревшими объявлениями функций в ANSI C


    Стандарт 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 в параметрах, хотя делать это не обязательно.


    Заключение


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

    Поддержать автора
    Поделиться публикацией

    Комментарии 18

      +2

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

        +1

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

          +1

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

            0

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

              0

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

                0

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

              +1

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

                +2

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

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

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

                      +1
                      Особенно радует когда нужно писать два параллельных проекта под контроллеры на С и С++.
                      Ещё недавно создал «Hello world» под техасовский DSP 5510, но случайно сделал проект С. После чего некоторое время удивлялся почему нельзя писать так:
                      for ( int i = 0; i < 10; ++i ) ну и т.д.
                      В 2019 году уже как-то немного забывается, что в C89 нет многих привычных вещей.
              0
              мне всегда казалось, что отсутствие аргументов в скобках подразумевает, что у функции будут любые аргументы.
              только что нашел у себя в коде константный массив указателей на функции обработчики вызовов ядра и это именно массив указателей на функции с любыми аргументами.
                +1

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

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


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

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


                    do_something(NULL, "Papa Johns", 2842, 1484.3355);
                    0
                    запутаться и вызвать не ту функцию


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

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

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


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

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

                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                    Самое читаемое