Зачем нужны прототипы функций в MISRA:2012?

Мне интересно, почему MISRA: 2012 требует прототипы функций. В приведенном ниже примере два прототипа на самом деле не нужны.

#include <stdio.h>
#include <stdlib.h>

// >>> Truly useless in my opinion
void display(void);
int main(void);
// <<<

void display(void) {
    printf("Hello World!\n");
}

int main() {
    display();
    return EXIT_SUCCESS;
}

Обоснование, которое я могу прочитать на SO, например здесь мне не очень понятно. Например, если main попытается получить доступ к display до того, как оно будет объявлено, компилятор или статический анализатор выдаст ошибку: отображение функции, используемой до объявления.

Другими словами, стоит ли создавать отклонение для этого правила MISRA?


person nowox    schedule 05.10.2017    source источник
comment
Я думаю: 1) это означает, что вы можете упорядочивать свои функции, не нарушая код, 2) это исключает любые возвраты к неявным объявлениям.   -  person Bathsheba    schedule 05.10.2017
comment
ИМХО, это совершенно спорно с современным компилятором, предупреждающим вас о неявных объявлениях функций. Но у вас есть мотивация: с плохим компилятором или неправильными настройками компилятора вы можете внести незамеченные ошибки, переупорядочив свои функции. Всегда наличие прототипов предотвращает это.   -  person    schedule 05.10.2017
comment
Мой совет: включите все предупреждения -Wall, -Wextra. Теперь вы можете забыть половину правил из инструментов статического анализа.   -  person Jean-François Fabre    schedule 05.10.2017
comment
Этот код в любом случае плохой, поскольку display() не объявляется static, что, конечно, должно быть, если цель состоит в том, чтобы функция была локальной для файла, в котором она находится.   -  person unwind    schedule 05.10.2017
comment
@Bathsheba Я согласен с вами, но это не то обоснование, которое приведено в Lundin в этот ответ, поэтому я хотел бы больше понять это правило.   -  person nowox    schedule 05.10.2017
comment
@FelixPalmen, вы всегда можете включить предупреждения, которые сделают плохой компилятор лучше. Или, если вы не можете, вы всегда можете использовать статический анализатор.   -  person nowox    schedule 05.10.2017
comment
@unwind В этом самом примере yes display() должен быть статическим, но мы можем представить, что он используется и где-то еще.   -  person nowox    schedule 05.10.2017
comment
@nowox Вам нужно подумать о том, что происходит с аргументами, передаваемыми вызову функции, когда нет прототипа ...   -  person Andrew Henle    schedule 05.10.2017
comment
@AndrewHenle Это именно тот вопрос, который я не понимаю. Для меня сама функция действует как прототип. Таким образом, функция, определенная как int32_t foo(int32_t bar) { /*...*/ }, определена корректно. Если я попытаюсь вызвать эту функцию из другого файла, мне, конечно, нужно, чтобы extern int32_t foo(int32_t bar); было объявлено в заголовке, НЕ включенном в файл, где объявлено foo. Если foo используется только в одной и той же единице перевода, его необходимо объявить перед использованием.   -  person nowox    schedule 05.10.2017
comment
@nowox сама функция действует как прототип. Пока код не будет переупорядочен, а это не так. Если foo используется только в одной и той же единице перевода, его необходимо объявить перед использованием. Это неверно. Не всем компиляторам C требуется прототип для вызова функции. Устаревший код C может даже не компилироваться в соответствии с более новыми стандартами. Опять же: подумайте о том, что происходит с аргументами, переданными функции, у которой нет прототипа.   -  person Andrew Henle    schedule 05.10.2017
comment
Практически компилятор C99 и/или статический анализатор найдут все ошибки, вызванные отсутствием прототипов функций. Это еще одна причина, почему вы не должны отклоняться от правила.   -  person Lundin    schedule 05.10.2017
comment
Реализация не объявляет main(); для вас это было бы необычно. Каждая другая функция должна иметь прототип в области видимости, прежде чем она будет использована. Есть основания иметь прототип в области действия до его объявления, если только это не статическая функция, и в этом случае ее наличие с полным прототипом (без пустых скобок только потому, что функция не принимает параметров — это C, а не C++) дает прототип. В этом случае функция должна быть определена до ее использования. Правило MISRA разумно (если только оно не предусматривает объявление для main()); вы должны строго следовать ему.   -  person Jonathan Leffler    schedule 05.10.2017


Ответы (2)


void display(void); — это предварительное объявление функции. Он имеет формат прототипа.

Как указано в размещенной ссылке, функция prototype представляет собой объявление функции с указанными типами всех параметров. Если параметров нет, то список параметров должен быть (void) (без параметров), а не () (любой параметр).

Точное правило 8.2 гласит:

Правило 8.2 Типы функций должны быть в форме прототипа с именованными параметрами.

В представленном обосновании (читай, оно довольно хорошее) упоминается, что это делается для того, чтобы избежать старых программ K&R и C90, в которых указаны не все параметры. C99 по-прежнему допускает это в некоторой степени, пока типы параметров в объявлении функции не конфликтуют с типами параметров в определении функции.

По сути, правило направлено на запрет таких функций:

void func1 (x)  // K&R style
int x;
{}

void func2(x)  // sloppy style
{}

Все параметры (если они есть) должны иметь указанные типы и имена.

Однако я не нашел в MISRA-C ничего, что требовало бы от вас написания объявления функции для каждой функции. Это означает, что код вашего примера будет соответствовать этому правилу MISRA с объявлением функции или без него.


Хотя, как я упоминал в предыдущем ответе, написание файлов .c без объявлений функций (в формате прототипа) является небрежной практикой. Если ваши функции должны вызываться в определенном порядке, это должно быть очевидно с помощью дизайна программы, именования функций и комментариев/документации. Не в том порядке, в котором они были объявлены внутри файла .c.

Не должно быть жесткой связи между строкой исходного кода, где функция объявлена ​​в файле .c, и поведением/использованием этой функции.

Вместо этого функции должны быть определены в порядке, который имеет логический смысл. Обычный способ написания файлов .c состоит в том, чтобы хранить все общедоступные функции, объявление функций которых находится в файле .h, в верхней части файла .c. Затем пусть внутренние функции (с static/внутренней связью) будут внизу. Эта модель требует объявления всех внутренних функций. Другой вариант — поместить все внутренние функции вверху, а общедоступные — внизу. Пока вы последовательны, любой из них в порядке.

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

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


Обратите внимание, что стандарт C не допускает прототипа для main() в качестве особого случая.

Обратите внимание, что, кроме того, правила 8.7 и 8.8 запрещают вам использовать void display(void) без static, поскольку функция используется только в одной единице перевода.

person Lundin    schedule 05.10.2017
comment
Что интересно, у меня не получается ни 8.7, ни 8.8, а 8.4: [MISRAC2012-Rule-8.4]:Definition of externally-linked main()' не имеет совместимого объявления.. However if I add int main(void);` Я могу избавиться от этой ошибки. Ты знаешь почему? - person nowox; 06.10.2017
comment
@nowox - MISRA опубликовала техническое исправление в июне 2017 года, чтобы явно добавить исключение к правилу 8.4 для main(), поэтому рекомендуем вам получить обновление от поставщика инструмента. - person Andrew; 13.10.2017

Если вы не объявляете функцию, любой вызов функции будет вызывать default argument promotions для каждого аргумента, поскольку считается, что функция имеет семантику стандарта C89.

Case 1:

Рассмотрим вызов f(5), где параметр функции имеет тип double. Код f будет рассматривать 5 как двойное число, в то время как арифметические продвижения по умолчанию будут передавать только целое число.

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

Case 2:

Предположим, вы хотите использовать функцию strtok и не включаете заголовок string.h в объявление -- char *strtok(char *str, const char *delim)

Теперь вы делаете ошибку, рассматривая разделитель «А» вместо «А». Поэтому, если вы забудете подпись, но имейте в виду значение параметров, если вы не включите заголовок, код будет скомпилирован без предупреждения, и, конечно, код из strtok будет учитывать ваш char 'A' (который преобразуется в целое число (=95)) в качестве фактического аргумента. Код считает, что это указатель на строку, и попытается получить доступ к указателю с позиции 95, заканчивая segfault.

Case 3:

Вот еще один типичный пример кода, который дает сбой сегментации на некоторой архитектуре — даже если вы не делаете ошибок, он все равно дает сбой сегментации.

char *subtoken;
subtoken = strtok(str, delim, &saveptr);

В этом случае считается, что функция strtok (отсутствует объявление из string.h) возвращает int, поэтому выполняется неявное преобразование из int->char*. Если int представлен 32 битами, а указатель — 64 битами, очевидно, что значение subtoken будет ошибочным и приведет к ошибке seg.

person alinsoar    schedule 05.10.2017