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

Почему функции должны быть объявлены до их использования?

При чтении некоторых ответов на этот вопрос я начал задаваться вопросом, почему компилятор действительно должен знать о функции, когда она впервые встречает его. Не было бы просто просто добавить дополнительный проход при разборе компиляционной единицы, которая собирает все символы, объявленные внутри, так что порядок, в котором они объявлены и используются, больше не имеет значения?

Можно утверждать, что объявление функций до их использования, безусловно, является хорошим стилем, но мне интересно, есть ли другая причина, почему это обязательное в С++?

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

Более распространенный пример (хотя и с классами, а не с функциями) относится к классам с конструкторами и фабриками private. factory должен знать класс, чтобы создавать экземпляры его, и класс должен знать factory для объявления friend.

Если это требование с древних времен, почему оно не было удалено в какой-то момент? Это не сломает существующий код, не так ли?

4b9b3361

Ответ 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.