Зачем размещать объявления и определения функций в отдельных файлах?

В уроках codeacademy по функциям вас учат использовать три файла, если вы собираетесь вызывать функции в своей программе:

  • файл int main(), который я нашел путем проб и ошибок, является неотъемлемой частью программной части программы на С++ (я думаю...) с расширением файла .cpp.

  • файл заголовка для функций DECLARING с расширением .hpp.

  • отдельный файл с функцией DEFINITIONS с расширением .cpp

Будет ли работать как объявление, так и определение функций в заголовочном файле отдельно и просто включение их выше int main()? Мне кажется, что наличие отдельных файлов для объявлений и определений может привести к путанице в более крупном проекте.


person Community    schedule 06.06.2020    source источник
comment
На работе я пишу программы длиной в 50+ тысяч строк. Представьте, сколько времени потребовалось бы на компиляцию, если бы все было в одном файле. Я бы абсолютно ничего не сделал.   -  person drescherjm    schedule 06.06.2020
comment
На самом деле все наоборот. Даже в умеренно большом проекте могут быть десятки файлов .cpp с несколькими вызывающими функциями, которые определены в другом исходном файле. Чтобы вызвать функцию, ее необходимо объявить, но (кроме особых случаев, таких как встроенные функции, которые не следует использовать без разбора) ни одна функция не может быть определена более одного раза во всем проекте. Использование заголовочных файлов только для объявлений позволяет включать каждый заголовочный файл в каждый исходный файл, если это необходимо. Определение функций в заголовке предотвращает включение этого заголовка более чем в один исходный файл проекта.   -  person Peter    schedule 06.06.2020
comment
@drescherjm Да. Приложение, над которым я работаю, состоит примерно из 600 тысяч строк кода, и если бы все было в заголовках, его сборка была бы кошмаром. В настоящее время полная сборка на машине с 20 ядрами/40 потоками занимает около 5 минут, но раньше она занимала 10+ минут, и сокращение времени компиляции в значительной степени достигалось за счет удаления элементов из заголовков и удаления ненужных включений. Это имеет огромное значение в крупных проектах.   -  person Jesper Juhl    schedule 06.06.2020
comment
@dresherjm: GCC содержит несколько файлов C++, содержащих более дюжины тысяч строк C++. Это работает, и люди вносят в это свой вклад. Например, его gcc/go/gofrontend/expressions.cc содержит 19711 строк в GCC 10.1, и они пишутся от руки и ежедневно компилируются.   -  person Basile Starynkevitch    schedule 06.06.2020


Ответы (5)


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

Вы также легко столкнетесь с проблемами ODR (правило одного определения), если все не помечено inline.

person Jesper Juhl    schedule 06.06.2020

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

person stark    schedule 06.06.2020

Небольшая программа на C++ может быть (и часто состоит) из одной единицы перевода. например несколько тысяч строк кода C++. В этом случае у вас может быть один исходный файл myprog.cc C++ (с несколькими #include внутри).

Но когда вы работаете над более крупной программой в команде, удобно иметь несколько исходных файлов C++.

Некоторые файлы C++ генерируются другой программой (это называется метапрограммированием или компиляция из исходного кода) и может иметь миллион строк строк C++. ANTLR или GNU Bison или TypeScript2Cxx способны генерировать код C++.

Но если вы работаете в команде таких людей, как Алиса и Боб, удобно решить, что Алиса отвечает за alice.cc, а Боб пишет bob.cc, и оба совместно работают над общим заголовочным файлом header.hh, который #include-d как в alice.cc, так и в bob.cc. . Этот header.hh фактически определяет API программного проекта.

Узнайте больше о системах контроля версий (я предпочитаю git) и автоматизация сборки инструменты (например, ninja или make).

Ищите вдохновение в коде C++ существующих проектов с открытым исходным кодом на gitlab или github или в другом месте (в в частности, внутри исходного кода Clang и GCC, оба являются основными компиляторами C++).

FWIW, в GCC 10.1 (от мая 2020 г.) gcc/go/gofrontend/expressions.cc написан от руки и содержит 19711 строк кода C++, то есть почти двадцать тысяч строк. Они составляются ежедневно. Я знаю людей, которые над этим работают, они замечательные и приятные профессионалы. Самый большой файл FTLK 1.4 — это src/Fl_Text_Display.cxx с 4175 строками C++.

По личному опыту, у вас может быть одна функция C++ из нескольких десятков тысяч строк C++ (это имеет практический смысл только тогда, когда этот код C++ сгенерирован), но тогда время компиляции на оптимизирующий компилятор является сдерживающим фактором. Вы можете адаптировать мою программу manydl.c для создания файлов C++. (в настоящее время он генерирует случайные файлы C с функциями настраиваемого размера) произвольного размера. Но код C++, сгенерированный Fluid или Qt Designer может быть довольно большим, а код C++, сгенерированный для GUI часто состоит из длинных, но концептуально простых функций.

Ничего в стандарте C++11 (см. n3337) требует нескольких единиц перевода. У вас может быть (для примера см. sqlite) один файл C++ foo.cc из миллиона строк. И вы можете сгенерировать часть исходного кода C++. Проект Qt, проект GCC. В книге Жака Питра Искусственные существа: совесть сознательной машины ISBN 978-1848211018 на многих страницах объясняется, почему такой подход стоит того.

person Basile Starynkevitch    schedule 06.06.2020

На этот вопрос есть два ответа

  1. зачем ВАМ, программисту, хотеть разбивать файлы на несколько файлов .h/.hpp и .cpp?

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

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

    • WriteImageToScreen(int position_x, int position_y)
    • ClearScreen()

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

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

    Теперь, когда все сказано, не все согласны с тем, что это правильный способ делать что-то или что это полезно.

  2. почему компилятору необходимо разделить вещи на несколько файлов .h/.hpp и .cpp?

    На всякий случай, если вы не знакомы с этим термином, компилятор — это программа, которая превращает текст вашего исходного кода в программу, которую ваш компьютер может выполнить.

    Так зачем же компилятору нужны отдельные файлы .hpp/.cpp? Другие уже в значительной степени попали в точку с этим, но компиляторы С++ путаются, если что-то определено несколько раз. Если вы поместите все в файл заголовка, то, когда вы включите этот заголовок в несколько файлов, он будет определен несколько раз. Так что по существу это возвращается к организационному вопросу.

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

#include "SomeFile.cpp"
#include "AnotherFile.cpp"
// ...
#include "SoManyFiles.cpp"

int main()
{
   DoStuff();
}

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

person Scott M    schedule 06.06.2020

Если у вас есть игрушечный проект, вы можете.

Если у вас есть 1 000 000 строк кода, время сборки будет ужасным.

C++20 вводит модули, которые должны решить всю проблему.

В других языках есть инструменты, которые могут извлекать интерфейс из «модуля». Будем надеяться, что когда появится C++20, инструменты станут доступны.

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

person Pete D.    schedule 06.06.2020
comment
Я вижу, что проблема времени сборки часто поднимается в ответах, пишу огромные (ну, мне это только кажется, потому что самая большая программа, которую я сделал, имеет около 250 строк...) такие программы, как эта целое поле, с которым я совершенно не знаком. Итак, если бы у меня был один файл с 1 миллионом строк кода, насколько ужасным было бы время сборки (оценка просто прекрасна, спасибо...) - person ; 06.06.2020
comment
Проект из миллиона строк будет разбит на сотни или тысячи файлов .h и .cpp. Я должен был сказать инкрементные сборки. При внесении модификации перекомпилируются только затронутые части, а затем связывается вся партия. Если мод затрагивает только один .cpp, перекомпилируется только один файл. Если все в заголовках. затрагиваются все файлы, включающие его, что каскадно распространяется по системе. В пределе, изменение одной строки может означать, что придется перекомпилировать миллион. - person Pete D.; 07.06.2020