Подтвердить что ты не робот

Назначение прототипов C/С++

Я читал wikipedia в инструкциях прототипа C/С++, и я смущен:

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

и использует в качестве примера следующее:

#include <stdio.h>

 /* 
  * If this prototype is provided, the compiler will catch the error 
  * in main(). If it is omitted, then the error will go unnoticed.
  */
 int fac(int n);              /* Prototype */

 int main(void) {             /* Calling function */
     printf("%d\n", fac());   /* ERROR: fac is missing an argument! */
     return 0;
 }

 int fac(int n) {             /* Called function  */
     if (n == 0) 
         return 1;
     else 
         return n * fac(n - 1);
}

Но определение функции вызываемой функции уже включает в себя всю информацию, которую прототип сообщает компилятору, поэтому почему компилятор не может вывести эту информацию из определения вызываемой функции, поскольку они содержат одинаковые утверждения/информационное письмо для буквы?

Что мне не хватает? Похоже на дополнительную работу без очевидного выигрыша.

Редактировать: Спасибо, ребята. Я предполагал, что компиляторы были многопроходными, я думаю. Я испорчен на нынешних языках, таких как Python. Это имеет смысл, так как ему так нужно, чтобы некоторые клопы делали точные вещи за один проход. Теперь мне кажется более очевидным. По-видимому, это требует достаточно глубокого знания того, как компилятор связывает и компилирует.

4b9b3361

Ответ 1

Прототипы позволяют отделять интерфейс от реализации.

В вашем примере весь код живет в одном файле, и вы можете так же легко перенести определение fac(), где прототип в настоящее время и удалить прототип.

Реальные программы состоят из нескольких .cpp файлов (ака-единиц компиляции), часто скомпилированных и связанных в библиотеках, прежде чем они будут связаны с окончательной исполняемой формой. Для крупномасштабных проектов такого рода прототипы собираются в файлы .h(например, заголовочные файлы), где заголовок включается в другие единицы компиляции во время компиляции, чтобы предупредить компилятор о существовании и вызовах функциональных возможностей библиотеки. В этих случаях определение функции недоступно компилятору, поэтому прототипы (aka declarations) служат своего рода контрактом, определяющим возможности и требования библиотеки.

Ответ 2

Две причины:

  • Компилятор читает файл сверху вниз. Если fac используется в main, который выше fac, и прототип не существует, компилятор не знает, как проверить, что этот вызов выполняется правильно, поскольку он еще не достиг определения fac.

  • Можно разбить программу C или С++ на несколько файлов. fac может быть определен в совершенно другом файле из файла, который в настоящее время обрабатывает компилятор, и поэтому ему нужно знать, что эта функция существует где-то и как она должна быть вызвана.

Обратите внимание, что комментарии в приведенном выше примере применимы только к C. В С++ этот пример всегда будет вызывать ошибку, даже если прототип опущен (хотя он будет вызывать другую ошибку в зависимости от того, существует ли прототип или нет). В С++ все функции должны быть определены или прототипированы перед использованием.

В C вы можете опустить прототип, и компилятор позволит вам вызвать функцию с любым количеством аргументов (включая ноль) и примет тип возврата int. Но только потому, что он не кричит на вас во время компиляции, это не значит, что программа будет работать правильно, если вы не вызываете функцию правильно. Вот почему это полезно для прототипа в C: поэтому компилятор может дважды проверить от вашего имени.

Философия C и С++, которая мотивирует эту функцию, заключается в том, что это относительно низкоуровневые языки. Они не делают много рук, и они не делают много, если проверка во время выполнения. Если ваша программа делает что-то неправильное, она будет ругаться или вести себя странно. Поэтому языки включают такие функции, которые позволяют компилятору идентифицировать определенные типы ошибок во время компиляции, чтобы вы могли легко находить и исправлять их.

Ответ 3

Наиболее важной причиной для прототипа является разрешение круговых зависимостей. Если "main" может вызвать "fac", а "fac" вызывает "main", тогда вам понадобится прототип для его разрешения.

Ответ 4

C и С++ - это два разных языка, и в этом конкретном случае существует огромная разница между ними. Из содержания вопроса я предполагаю, что вы говорите о C.

#include <stdio.h>
int main() {
   print( 5, "hi" );  // [1]
}
int print( int count, const char* txt ) {
   int i;
   for ( i = 0; i < count; ++i ) 
      printf( "%s\n", txt );
}

Это правильная программа на C, которая делает то, что вы можете ожидать: печатает 5 строк, в которых говорится "привет" в каждом из них. Язык C находит вызов в [1], он предполагает, что print - это функция, которая возвращает int и принимает неизвестное количество аргументов (неизвестное компилятору, известное программисту), компилятор предполагает, что вызов исправлять и продолжать компиляцию. Поскольку определение функции и вызов совпадают, программа хорошо сформирована.

Проблема заключается в том, что когда компилятор анализирует строку в [1], он не может выполнять проверку типа любого типа, поскольку он не знает, что представляет собой функция. Если при написании этой строки мы будем ошибочно принимать порядок аргументов, и мы вводим print( "hi", 5 );, то компилятор все равно примет строку, так как она не имеет предварительного знания print. Поскольку вызов неверен, даже если компиляция кода завершится позже.

Объявляя функцию заранее, вы предоставляете компилятору необходимую информацию для проверки в месте вызова. Если декларация присутствует и такая же ошибка сделана, компилятор обнаружит ошибку и сообщит вам о вашей ошибке.

В С++, с другой стороны, компилятор не будет считать, что вызов правильный, и на самом деле вам потребуется предоставить объявление функции перед вызовом.

Ответ 5

Компилятор C обрабатывает исходные файлы сверху вниз. Функции, которые появляются после их использования, не учитываются при разрешении типов аргументов. Итак, в вашем примере, если main() находился в нижней части файла, вам не понадобится прототип для fac() (так как определение fac() уже было бы увидено компилятором при компиляции main()).

Ответ 6

Помимо всех хороших ответов, которые уже заданы, подумайте об этом: если вы были компилятором, и ваша задача заключалась в том, чтобы перевести исходный код на машинный язык, и вы (будучи послушным компилятором, которым вы являетесь) могли читать только исходный код line by line - как бы вы прочитали код, который вы вставили, если не было прототипа? Как вы узнали, что вызов функции действителен, а не синтаксическая ошибка? (да, вы могли бы сделать заметку и проверить в конце, если все будет соответствовать, но эта другая история).

Еще один способ взглянуть на него (на этот раз как на человека): предположим, что у вас нет функции, определенной как прототип, и ее исходный код недоступен. Вы знаете, однако, что в библиотеке, которую дал вам ваш помощник, есть машинный код, который при запуске возвращает определенное ожидаемое поведение. Как мило. Теперь, как вы, компилятор, знаете, что такой вызов функции действителен, если нет прототипа, говорящего: "Эй, чувак, доверься мне, есть функция, называемая такой-то, что принимает параметры и возвращает что-то"?

Я знаю, что это очень, очень, очень простой способ думать об этом. Добавление интенциональности к частям программного обеспечения, вероятно, является плохим знаком, не так ли?