Почему функции должны быть объявлены до их использования? - PullRequest
18 голосов
/ 21 января 2011

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

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

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

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

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

Ответы [ 11 ]

12 голосов
/ 21 января 2011

Как вы предлагаете разрешить необъявленные идентификаторы, которые определены в другой единице перевода ?

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

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

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

9 голосов
/ 21 января 2011

Исторически C89 позволял вам это делать.Когда компилятор впервые увидел использование функции, и у него не было предопределенного прототипа, он «создал» прототип, который соответствует использованию функции.

Когда C ++ решил добавить строгую проверку типов вкомпилятором было решено, что теперь нужны прототипы.Кроме того, C ++ унаследовал однопроходную компиляцию от C, поэтому он не мог добавить второй проход для разрешения всех символов.

9 голосов
/ 21 января 2011

Поскольку C и C ++ являются старыми языками.Ранние компиляторы не имели много памяти, поэтому эти языки были разработаны таким образом, чтобы компилятор мог просто читать файл сверху вниз, не рассматривая файл как целое .

3 голосов
/ 21 января 2011

Я думаю о двух причинах:

  • Это облегчает анализ.Дополнительный проход не требуется.
  • Он также определяет scope ;символы / имена доступны только после его объявления.Значит, если я объявлю глобальную переменную 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

2 голосов
/ 21 января 2011

Язык программирования C был разработан таким образом, чтобы компилятор мог быть реализован как однопроходный компилятор . В таком компиляторе каждый этап компиляции выполняется только один раз. В таком компиляторе вы не можете ссылаться на сущность, которая будет определена позже в исходном файле.

Более того, в C компилятор интерпретирует только один модуль компиляции (обычно файл .c и все включенные файлы .h) за раз. Таким образом, вам нужен механизм для ссылки на функцию, определенную в другом модуле компиляции.

Решение о разрешении однопроходного компилятора и возможности разбить проект на небольшой модуль компиляции было принято, потому что в то время память и доступная вычислительная мощность были действительно невелики. А разрешение форвард-декларации может легко решить проблему с помощью одной функции.

Язык C ++ был создан на основе C и унаследовал эту особенность (поскольку он хотел быть максимально совместимым с C, чтобы облегчить переход).

2 голосов
/ 21 января 2011

Основная причина - сделать процесс компиляции максимально эффективным. Если вы добавите дополнительный проход, вы добавите и время, и хранилище. Помните, что C ++ был разработан еще до появления четырехъядерных процессоров:)

1 голос
/ 22 ноября 2016

Одна из главных причин, почему это стало обязательным даже в 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 в командной строке - и компилятор немедленно уведомит вас о вашей ошибке.

То же самое с C ++.Но в C ++ есть на самом деле другие важные причины, по которым нужны объявления функций, они обсуждаются в других ответах.

* Но имеет неопределенное поведение

1 голос
/ 21 января 2011

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

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

1 голос
/ 21 января 2011

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

0 голосов
/ 27 января 2017

Я помню, с Unix и Linux у вас есть Global и Local.В вашей собственной среде локальный работает для функций, но не работает для Global(system).Вы должны объявить функцию Global.

...