Ответ 8
A C-декларация функций
В C объявления функций не работают так, как на других языках: сам компилятор C не ищет назад и вперед в файле, чтобы найти объявление функции из того места, которое вы его называете, и оно не сканирование файла несколько раз, чтобы выяснить отношения: компилятор только сканирует вперед в файле ровно один раз, сверху вниз. Подключение вызовов функций к объявлениям функций является частью задания компоновщика и выполняется только после того, как файл скомпилирован до исходных инструкций сборки.
Это означает, что по мере того, как компилятор просматривает файл вперед, первый раз, когда компилятор сталкивается с именем функции, должно быть одно из двух: оно либо видит само объявление функции, и в этом случае компилятор точно знает, что такое функция и какие типы она принимает в качестве аргументов, и какие типы она возвращает - или это вызов функции, а компилятор должен угадать, как будет объявлена функция.
(Существует третий вариант, в котором имя используется в прототипе функции, но на этот раз мы будем игнорировать, так как если вы видите эту проблему в первую очередь, вы, вероятно, не используете прототипы.)
Урок истории
В самые ранние дни C факт, что компилятор должен был угадывать типы, не был действительно проблемой: все типы были более или менее одинаковыми - почти все было либо int, либо указателем, и они были того же размера. (На самом деле, в B, языке, который предшествовал C, не было типов вообще, все было просто int или указателем, и его тип определялся исключительно тем, как вы его использовали!) Таким образом, компилятор мог смело предположить поведение любого функцию, основанную только на количестве переданных параметров: если вы передали два параметра, компилятор переместил бы две вещи в стек вызовов, и предположительно, у вызываемого было бы объявлено два аргумента, и это все выровнялось бы. Если вы передали только один параметр, но функция ожидала два, это все равно будет работать, а второй аргумент будет просто проигнорирован/мусор. Если вы передали три параметра и функцию, ожидаемую двумя, она также будет сортировать работу, а третий параметр будет проигнорирован и заменен локальными переменными функции. (Некоторый старый код C все еще ожидает, что эти правила несоответствующих аргументов тоже будут работать.)
Но наличие компилятора позволяет вам что-либо передавать чему-либо, это не очень хороший способ программирования языка программирования. Он работал хорошо в первые дни, потому что ранние программисты на C были в основном волшебниками, и они знали, что не передают неправильный тип функциям, и даже если они действительно ошибаются, всегда были такие инструменты, как lint
, которые могли бы глубже двойная проверка вашего кода на C и предупреждение о таких вещах.
Быстро переходите к сегодняшнему дню, и мы не совсем в одной лодке. C вырос, и в нем много программистов, которые не являются волшебниками, и для размещения их (и для размещения всех, кто регулярно использовал lint
) в любом случае, компиляторы взяли на себя многие из способностей, которые были ранее часть lint
- особенно часть, где они проверяют ваш код, чтобы обеспечить его безопасный тип. Ранние компиляторы C позволят вам писать int foo = "hello";
, и он просто blithely назначает указатель на целое число, и вам решать, чтобы вы не делали ничего глупого. Современные компиляторы C громко жалуются, когда вы ошибаетесь, и это хорошо.
Конфликты типов
Итак, что все это связано с таинственной конфликтной ошибкой в строке объявления функции? Как я уже сказал выше, компиляторы C все равно должны либо знать, либо предполагать, что имя означает, что в первый раз они видят это имя при сканировании вперед по файлу: они могут знать, что это означает, если оно само декларацию самой функции (или "прототип" функции "больше об этом коротко), но если это просто вызов функции, они должны угадать. И, к сожалению, предположение часто ошибочно.
Когда компилятор увидел ваш вызов do_something()
, он посмотрел, как он был вызван, и он пришел к выводу, что do_something()
в конечном итоге будет объявлен следующим образом:
int do_something(char arg1[], char arg2[])
{
...
}
Почему он это сделал? Потому что, как вы это называли! (Некоторые компиляторы C могут заключить, что это было int do_something(int arg1, int arg2)
или просто int do_something(...)
, оба из которых еще далеки от того, что вы хотите, но важно то, что независимо от того, как компилятор угадывает типы, он догадывается о них иначе что использует ваша фактическая функция.)
Позже, когда компилятор сканирует вперед в файле, он видит ваше фактическое объявление char *do_something(char *, char *)
. Объявление этой функции даже не близко к объявлению, которое предположил компилятор, а это означает, что строка, в которой компилятор компилировал вызов, был скомпилирован неправильно, и программа просто не работает. Поэтому он правильно выводит сообщение об ошибке, указывающее, что ваш код не будет работать так, как написано.
Возможно, вам интересно: "Почему я предполагаю, что возвращаю int
?" Ну, он предполагает этот тип, потому что нет никакой информации об обратном: printf()
может принимать любые типы в своих переменных аргументах, поэтому без лучшего ответа int
так же хорошо догадается, как и любой другой. (Многие ранние компиляторы C всегда предполагали int
для каждого неопределенного типа и предположили, что вы имели в виду ...
для аргументов для каждой объявленной функции f()
- not void
- вот почему многие современные стандарты кода рекомендуют всегда помещать void
in для аргументов, если они действительно не должны быть.)
Исправление
Существуют две распространенные ошибки для ошибки объявления функции.
Первое решение, которое предлагается многими другими ответами здесь, заключается в том, чтобы поместить прототип в исходный код выше того места, где функция сначала вызвана. Прототип выглядит так же, как объявление функции, но имеет точку с запятой, где тело должно быть:
char *do_something(char *dest, const char *src);
Сначала поставив прототип, компилятор тогда узнает, как будет выглядеть эта функция, поэтому ее не нужно угадывать. По соглашению, программисты часто помещают прототипы в верхнюю часть файла, просто под операторы #include
, чтобы гарантировать, что они всегда будут определены до их потенциальных использования.
Другое решение, которое также появляется в каком-то реальном коде, состоит в том, чтобы просто переупорядочить ваши функции, чтобы объявления функций всегда были перед тем, что их называет! Вы можете переместить всю функцию char *do_something(char *dest, const char *src) { ... }
выше первого вызова на нее, и тогда компилятор точно знает, как выглядит функция, и не нужно было гадать.
На практике большинство людей используют прототипы функций, потому что вы также можете использовать прототипы функций и переместить их в файлы заголовков (.h
), чтобы код в других файлах .c
мог вызывать эти функции. Но любое решение работает, и многие кодовые базы используют оба.
C99 и C11
Полезно отметить, что правила немного отличаются в новых версиях стандарта C. В более ранних версиях (C89 и K & R) компилятор действительно угадал типы во время вызова функции (а компиляторы K & R-времена часто даже не предупредили вас, если они были неправы). C99 и C11 требуют, чтобы объявление функции/прототип должно предшествовать первому вызову, и это ошибка, если это не так. Но многие современные компиляторы C - в основном для обратной совместимости с более ранним кодом - будут предупреждать только о пропущенном прототипе и не считать его ошибкой.