A C Функциональная декларация Backgrounder
В C объявления функций не работают так же, как в других языках: сам компилятор C не выполняет поиск назад и вперед в файле, чтобы найти объявление функции из того места, где вы ее вызываете, и не работает сканируйте файл несколько раз, чтобы выяснить отношения: либо компилятор сканирует только вперед в файле ровно один раз сверху вниз. Подключение вызовов функций к объявлениям функций является частью работы компоновщика и выполняется только после , когда файл компилируется в необработанные инструкции по сборке.
Это означает, что, когда компилятор просматривает файл вперед, в самый первый раз, когда компилятор встречает имя функции, должна быть одна из двух вещей: он либо видит само объявление функции, и в этом случае компилятор точно знает, что это за функция и какие типы она принимает в качестве аргументов и какие типы она возвращает - или это вызов функции, и компилятор должен угадать как функция в конечном итоге будет объявлена.
(есть третий вариант, когда имя используется в прототипе функции, но пока мы его игнорируем, поскольку, если вы видите эту проблему в первую очередь, вы, вероятно, не используете прототипы.)
Урок истории
В первые дни C тот факт, что компилятору приходилось угадывать типы, на самом деле не был проблемой: все типы были более или менее одинаковыми - почти все были либо int, либо указателем, и они были одинакового размера. (На самом деле, в B, языке, предшествующем C, не было вообще никаких типов; все было просто int или указателем, а его тип определялся исключительно тем, как вы его использовали!) Таким образом, компилятор мог безопасно угадывать поведение любого Функция основана на количестве переданных параметров: если вы передадите два параметра, компилятор поместит две вещи в стек вызовов, и, предположительно, вызываемый объект объявит два аргумента, и все это выстроится в линию. Если вы передаете только один параметр, но функция ожидает два, она все равно будет работать, а второй аргумент будет просто проигнорирован / garbage. Если вы передали три параметра, а функция ожидала двух, то она все равно работала бы, и третий параметр игнорировался и растекался локальными переменными функции. (Некоторый старый код на C все еще ожидает, что эти правила несоответствующих аргументов тоже будут работать.)
Но наличие компилятора, позволяющего вам передавать что-либо чему-либо, на самом деле не является хорошим способом разработки языка программирования. В первые дни это работало хорошо, потому что ранние программисты на С были в основном волшебниками, и они знали, что нельзя передавать неправильный тип функциям, и даже если они действительно ошибались в типах, всегда были такие инструменты, как lint
, которые могли бы работать глубже перепроверьте ваш код C и предупредите вас о таких вещах.
Перенесемся в сегодняшний день, и мы не совсем в одной лодке. C вырос, и в нем программируют многие люди, которые не являются волшебниками, и, чтобы приспособиться к ним (и к тому, чтобы приспособиться ко всем, кто регулярно использует lint
), компиляторы взяли на себя многие из способностей, которые были ранее часть lint
- особенно часть, где они проверяют ваш код, чтобы убедиться, что он безопасен для типов Ранние компиляторы Си позволяли вам писать int foo = "hello";
, и он просто беспечно назначал указатель на целое число, и вы должны были убедиться, что вы не делаете ничего глупого. Современные компиляторы Си громко жалуются на неправильные типы, и это хорошо.
Типовые конфликты
Так как же все это связано с загадочной ошибкой конфликтующего типа в строке объявления функции? Как я уже говорил выше, компиляторы 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()
- не void
- поэтому многие современные стандарты кода рекомендуют всегда ставить void
для аргументов, если на самом деле не должно быть никаких.)
Исправление
Существует два распространенных исправления ошибки объявления функции.
Первое решение, которое рекомендуется многими другими ответами здесь, состоит в том, чтобы поместить прототип в исходный код над местом, где функция сначала вызывается. Прототип выглядит так же, как объявление функции, но имеет точку с запятой, где тело должно быть:
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 - главным образом для обратной совместимости с более ранним кодом - будут только предупреждать об отсутствующем прототипе и не будут считать это ошибкой.