Почему функции должны быть объявлены до их использования?
При чтении некоторых ответов на этот вопрос я начал задаваться вопросом, почему компилятор действительно должен знать о функции, когда она впервые встречает его. Не было бы просто просто добавить дополнительный проход при разборе компиляционной единицы, которая собирает все символы, объявленные внутри, так что порядок, в котором они объявлены и используются, больше не имеет значения?
Можно утверждать, что объявление функций до их использования, безусловно, является хорошим стилем, но мне интересно, есть ли другая причина, почему это обязательное в С++?
Изменить - пример для иллюстрации: Предположим, что вам нужны функции, которые определены внутри строки в файле заголовка. Эти две функции вызовут друг друга (возможно, рекурсивный обход дерева, где нечетные и четные слои дерева обрабатываются по-разному). Единственный способ разрешить это - сделать прямое объявление одной из функций перед другой.
Более распространенный пример (хотя и с классами, а не с функциями) относится к классам с конструкторами и фабриками private
. factory должен знать класс, чтобы создавать экземпляры его, и класс должен знать factory для объявления friend
.
Если это требование с древних времен, почему оно не было удалено в какой-то момент? Это не сломает существующий код, не так ли?
Ответы
Ответ 1
Как вы предлагаете разрешать необъявленные идентификаторы , определенные в другой единицы перевода?
С++ не имеет понятия модуля, но имеет отдельный перевод как наследование от C. Компилятор С++ будет компилировать каждую единицу перевода самостоятельно, не зная ничего о других единицах перевода вообще. (За исключением того, что export
сломал это, что, вероятно, почему это, к сожалению, никогда не снималось.)
Заголовочные файлы, где вы обычно помещаете декларации идентификаторов, которые определены в других единицах перевода, на самом деле являются всего лишь очень неуклюжим способом сбрасывания тех же деклараций в разные единицы перевода. Они не будут сообщать компилятору о наличии других единиц перевода с определенными в них идентификаторами.
Изменить ваши дополнительные примеры:
При использовании всего текстового включения вместо правильной концепции модуля компиляция уже требует от С++ многого, что требует еще одного компиляционного прохода (где компиляция уже разделена на несколько проходов, не все из которых могут быть оптимизированы и объединены, IIRC) ухудшит уже плохая проблема. И изменение этого, вероятно, изменит разрешение перегрузки в некоторых сценариях и, таким образом, сломит существующий код.
Обратите внимание, что для С++ требуется дополнительный проход для определения классов разбора, поскольку функции-члены, определенные внутри строки в определении класса, анализируются так, как если бы они были определены непосредственно за определением класса. Тем не менее, это было решено, когда C с классами был придуман, поэтому не существовало существующей базы кода для разрыва.
Ответ 2
Потому что C и С++ - старые языки. Ранние компиляторы не имели большой памяти, поэтому эти языки были разработаны таким образом, чтобы компилятор мог просто читать файл сверху донизу, не считая файл в целом.
Ответ 3
Исторически C89 позволяет вам это делать. В первый раз, когда компилятор увидел использование функции и у нее не было предопределенного прототипа, он "создал" прототип, который соответствовал использованию функции.
Когда С++ решила добавить строгую проверку typechecking к компилятору, было решено, что теперь требуются прототипы. Кроме того, С++ унаследовал однопроходную компиляцию из C, поэтому он не смог добавить второй проход для разрешения всех символов.
Ответ 4
Основная причина заключается в том, чтобы сделать процесс компиляции максимально эффективным. Если вы добавите дополнительный пропуск, вы добавляете как время, так и хранилище. Помните, что С++ был разработан до времени четырехъядерных процессоров:)
Ответ 5
Я думаю о двух причинах:
- Это упрощает синтаксический анализ. Никакого дополнительного прохода не требуется.
- Он также определяет область действия; символы/имена доступны только после его декларации. Значит, если я объявляю глобальную переменную
int g_count;
, более поздний код после этой строки может использовать ее, но не код перед строкой! Тот же аргумент для глобальных функций.
В качестве примера рассмотрим этот код:
void g(double)
{
cout << "void g(double)" << endl;
}
void f()
{
g(int());//this calls g(double) - because that is what is visible here
}
void g(int)
{
cout << "void g(int)" << endl;
}
int main()
{
f();
g(int());//calls g(int) - because that is what is the best match!
}
Вывод:
void g (double)
void g (int)
Смотрите вывод на ideone: http://www.ideone.com/EsK4A
Ответ 6
Язык программирования C был разработан таким образом, что компилятор может быть реализован как однопроходный компилятор. В таком компиляторе каждая фаза компиляции выполняется только один раз. В таком компиляторе вы не можете ссылаться на сущность, которая определена позже в исходном файле.
Кроме того, в C компилятор интерпретирует только единый блок компиляции (как правило, файл .c и все входящие в него файлы .h) за раз. Таким образом, вам нужен механизм для ссылки на функцию, определенную в другом модуле компиляции.
Решение разрешить однопроходный компилятор и иметь возможность разделить проект в небольшой компиляционной единице было принято потому, что в то время, когда доступная память и вычислительная мощность были очень плотными. И разрешение forward-declare может легко решить проблему с помощью одной функции.
Язык С++ был получен из C и унаследовал от него эту функцию (поскольку он хотел быть как можно более совместимым с C, чтобы облегчить переход).
Ответ 7
Я думаю, потому что C довольно старый, и в то время, когда C был спроектирован, эффективная компиляция была проблемой, потому что процессоры были намного медленнее.
Ответ 8
Поскольку С++ является статическим языком, компилятору необходимо проверить, совместим ли тип значений с типом, ожидаемым в параметрах функции. Конечно, если вы не знаете сигнатуру функции, вы не можете делать такие проверки, тем самым игнорируя цель статического компилятора. Но, поскольку у вас серебряный значок на С++, я думаю, вы уже это знаете.
Спецификации языка С++ были сделаны правильно, потому что разработчик не хотел форсировать многопроходный компилятор, когда оборудование было не так быстро, как доступно сегодня. В конце концов, я думаю, что если бы С++ был разработан сегодня, это наложение исчезло бы, но тогда у нас был бы другой язык: -).
Ответ 9
Одна из главных причин, почему это было сделано даже в C99 (по сравнению с C89, где вы могли иметь неявно объявленные функции) заключается в том, что неявные объявления очень подвержены ошибкам. Рассмотрим следующий код:
Первый файл:
#include <stdio.h>
void doSomething(double x, double y)
{
printf("%g %g\n",x,y);
}
Второй файл:
int main()
{
doSomething(12345,67890);
return 0;
}
Эта программа является синтаксически действующей программой * C89. Вы можете скомпилировать его с помощью GCC с помощью этой команды (если исходные файлы называются test.c
и test0.c
):
gcc -std=c89 -pedantic-errors test.c test0.c -o test
Почему он печатает что-то странное (по крайней мере, на linux-x86 и linux-amd64)? Вы можете сразу заметить проблему в коде? Теперь попробуйте заменить c89
на c99
в командной строке - и вы будете немедленно уведомлены о вашей ошибке компилятором.
То же самое с С++. Но в С++ существуют другие важные причины, по которым требуются объявления функций, они обсуждаются в других ответах.
* Но имеет undefined поведение
Ответ 10
Тем не менее, вы можете использовать функцию до того, как она будет объявлена иногда (строгое формулирование: "before" - это порядок, в котором считывается источник программы) - внутри класса!:
class A {
public:
static void foo(void) {
bar();
}
private:
static void bar(void) {
return;
}
};
int main() {
A::foo();
return 0;
}
(Изменение класса в пространстве имен не работает, на мои тесты.)
Вероятно, потому, что компилятор действительно выводит определения функции-члена из класса сразу после объявления класса, как это указал кто-то здесь в ответах.
Такой же подход может быть применен ко всему исходному файлу: сначала отбросьте все, кроме объявления, а затем обработайте все отложенные. (Либо двухпроходный компилятор, либо достаточно большой объем памяти для хранения отложенного исходного кода.)
Ха-ха! Итак, они думали, что весь исходный файл будет слишком большим для хранения в памяти , но один класс с определениями функций не будет: они могут допускать целый класс сидеть в памяти и ждать, пока объявление не будет отфильтровано (или сделать 2-й проход для исходного кода классов)!
Ответ 11
Я помню, что в Unix и Linux у вас есть Global
и Local
. В вашей собственной среде локальная работа для функций, но не работает для Global(system)
. Вы должны объявить функцию Global
.