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

y5-rysiw1ecqkivj-siuk47yfgu.png

Стандарт 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 
#include 
#include 
#include 

/* Устаревшее объявление функции "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

/*  Устаревшее объявление функции "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 в параметрах, хотя делать это не обязательно.

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

© Habrahabr.ru