Хабрахабр

Неопределённое поведение с устаревшими объявлениями функций в 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]

4. Также обратите внимание, что даже с флагом -Wall компилятор gcc (Ubuntu 7. 04. 0-1ubuntu1~18. 4. 1) 7. 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!");
}

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

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

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть