Есть (источник):
void f(); // declaration (1)
void f(void); // declaration with prototype (2)
void f() { ... } // definition (3)
void f(void) { ... } // definition with prototype (4)
В чем разница между 3 и 4? Источник не объясняет эту разницу, и мне 4 кажется излишним.
Ваша связанная страница объясняет это довольно подробно, начиная с «Определения без прототипа». Что конкретно ты не понял?
В частности, почему мне нужно использовать 4? Я хочу всегда использовать 3, возникнут ли у меня проблемы?
Итак, ваш актуальный вопрос: «Для чего нужны прототипы функций»?
@Useless, нет, мой вопрос скорее в том, «для чего нужны определения функций с прототипами»? Более конкретно, могу ли я не использовать void
в определении функции? Это нормально?
Да, вы можете опустить список типов параметров void
в определении функции.
@IanAbbott, у меня нет проблем с 1 и 2, моя проблема только 3 против 4.
Прототипы предназначены для проверки типов. Если вы хотите, чтобы компилятор проверял, что ваш вызов функции имеет правильные параметры, ему необходимо знать, какие параметры ожидаются. Примерно до C11 (о котором говорит ваша связанная страница) существовала практическая разница между функцией, объявленной без аргументов, и функцией, объявленной без списка аргументов. В какой-то момент это изменилось, по крайней мере, к C23.
Хорошей практикой является обеспечение соответствия объявлений и определений функций. По этой причине лучше избегать (3). В идеале f()
означало бы то же, что f(void)
, но по историческим причинам f()
указывает на отсутствующий прототип. В случае определения функции это не имеет особого значения, но для объявления функции имеет значение.
@Useless Это изменилось именно с C23. (Одна из нескольких причин, почему мне не нравится эта редакция. Несмотря на то, что по большей части она действительно устарела, объявления без прототипов в <C23 все же можно использовать для получения полезных, иначе недоступных, функций из C: stackoverflow.com/questions/1749233/… )
Возможно, я не дам самый научный ответ, но вот моя попытка.
Для новичка определения 3 и 4 идентичны. Однако в более серьезных проектах важна не только имеющаяся информация (и требует проверки), но и недостающая информация.
Итак, суть такова:
Что еще хуже, если локальные переменные имеют те же имена, что и некоторые глобальные переменные, компилятор примет функцию без какого-либо сообщения - и тогда, через некоторое время, вы откроете для себя удовольствие от отладки.
Вот почему я предпочитаю всегда делать вещи ясными. Если чего-то не хватает (неявно), я знаю, что мне нужно еще поработать.
«Крайний» вариант объяснения таков:
Опять же, очевиден тот же вывод: функция 3 не уточняет ожидания/потребности.
У этой разницы есть и более скрытый аспект, и это еще больше (потенциально) вредит новичкам. НЕ использование "void" в качестве списка параметров "учит" начинающего программиста тому, что "void" эквивалентно "ничто", или, скорее, наоборот - что "ничто" - это то же самое, что и "void".
Однако, как вы можете легко заметить, тип возвращаемого значения функции также может быть «void» или «nothing». Но в этом случае «ничего» (как тип возвращаемого значения функции) больше не означает «void», а на самом деле (буквально) означает «int». Опять же, забавный источник отладочной деятельности и седых волос.
Позволю себе не согласиться. Определение 3 кристально ясно, особенно для опытных программистов на языке C, которых вы ожидаете встретить в «серьезном проекте». То, что определение функции может быть неправильным, потому что «программист [...] забыл вернуться, чтобы закончить работу», не имеет значения, поскольку вопрос заключается в том, что означают определения, как они даны. С другой стороны, нет, обсуждение не одинаково справедливо для объявлений 1 и 2. Во всех версиях C на сегодняшний день (но не в следующей версии) 1 и 2 фактически семантически различны.
@JohnBollinger, я откатил неверное утверждение. Пожалуйста, не обсуждайте здесь 1 и 2. Тем уже десятки, правда! ;)
Функции, созданные с помощью (3) и (4), идентичны — обе не имеют параметров, и компилятор может генерировать для них идентичный код. Однако в C 2018 (на данный момент все еще являющемся стандартом) информация о типе, прикрепленная к имени функции, отличается, и это может повлиять на вызовы функции. Ожидается, что к 2024 году это изменится, сделав (3) эквивалентным (4).
Предполагая, что нет другого видимого объявления f
, которое изменяет объявление, тогда вызовы f
, объявленные в (4), подлежат этому ограничению в C 2018 6.5.2.2 2:
Если выражение, обозначающее вызываемую функцию, имеет тип, включающий прототип, количество аргументов должно соответствовать количеству параметров…
Стандарт C требует диагностики нарушения ограничений; если программа содержит нарушение ограничений, компилятор должен выдать диагностическое сообщение. Напротив, вызовы f
, объявленные в (3), не подпадают под это ограничение, и компилятору не требуется выдавать диагностическое сообщение (хотя он может это делать).
Кроме того, после (3) компилятор анализирует вызов f
, используя правила C 2018 6.5.2.2 6:
Если выражение, обозначающее вызываемую функцию, имеет тип, не включающий прототип,…
После (4) компилятор анализирует вызов f
с использованием 6.5.2.2 7:
Если выражение, обозначающее вызываемую функцию, имеет тип, включающий прототип,…
Однако, как правило, это не имеет никаких последствий, поскольку в этих параграфах указаны только две вещи:
Когда выражение, обозначающее вызываемую функцию, не имеет прототипа, поведение не определено, если количество аргументов не равно количеству параметров.
Как преобразуются аргументы при подготовке к вызову.
Если аргументов нет, то 2. не применяется. Если есть аргументы, то поведение в обоих случаях не определено, и я не вижу возможности для другого преобразования аргументов вызвать какое-либо значимое различие в поведении.
Интересно, что хотя стандарт не требует, чтобы компилятор диагностировал, когда функция в (3) вызывается с параметром, он требует, чтобы компилятор диагностировал, когда она переобъявляется с параметром, например void f(int x);
. C 6.7 2 имеет это ограничение, требующее диагностики:
Все объявления в одной области действия, которые ссылаются на один и тот же объект или функцию, должны указывать совместимые типы.
а спецификация совместимых типов функций в 6.7.6.3 15 гласит:
… Если один тип имеет список типов параметров, а другой тип указан определением функции, которая содержит (возможно, пустой) список идентификаторов, оба типа должны согласовываться по количеству параметров…
Поскольку void f(int x)
имеет список типов параметров, а void f() {}
содержит пустой список идентификаторов, для совместимости они должны согласовываться в количестве параметров. Они не согласуются, поэтому несовместимы, поэтому необходимо диагностировать нарушение ограничений.
Кажется, это недосмотр в стандарте; поскольку это ограничение вынуждает компилятор сохранять информацию о количестве параметров в определении функции, даже если он использует список идентификаторов, а не прототип, стандарт также мог бы потребовать от компиляторов диагностировать вызовы с неправильным количеством параметров, не требуя компилятору, чтобы сохранить любую дополнительную информацию о функции.
Однако это было бы полезно только в ситуациях, когда определение функции было видно в момент вызова, что часто бывает не так, поскольку функция может быть определена в другой единице трансляции или позже в той же единице. Так что, возможно, это не такой уж важный случай, который стоит освещать. Тем не менее, он мог обнаружить некоторые ошибки.
Спасибо за объяснение, узнал что-то новое. Мое решение простое: всегда используйте 2 и 4.
Вы рассматривали 6.7.6.3p14? «… Пустой список в деклараторе функции, который является частью определения этой функции, указывает, что у функции нет параметров. …».
@IanAbbott: Да, и какие последствия из этого следуют? Как я писал, функция не имеет параметров. Разница между (3) и (4) заключается в информации о типе, связанной с идентификатором (именем функции). Идентификатор и функция — это разные вещи.
Я думал, что void f{) { }
объявит определяемую функцию не имеющей таких же параметров, как void f(void) { }
, но я ошибся. 6.7.6.3p14 применяется только к самому определению функции, а не к объявлению функции, которое видно при вызовах, где определение функции находится в области видимости и в области видимости нет других объявлений функции, включающих прототип. Например. void f() {}
void g(void) { f(1); }
не имеет нарушения ограничений, но void f(void);
void f() {}
void g(void) { f(1); }
имеет нарушение ограничений (поэтому требуется диагностика). У обоих есть УБ.