Почему я не должен включать файлы cpp и вместо этого использовать заголовок?

Итак, я закончил свое первое программирование на С++ и получил оценку. Но, согласно классификации, я потерял отметки за including cpp files instead of compiling and linking them. Я не слишком четко понимаю, что это значит.

Оглядываясь назад на мой код, я решил не создавать файлы заголовков для своих классов, но все делал в файлах cpp (казалось, он отлично работал без файлов заголовков...). Я предполагаю, что грейдер означал, что я написал "#include" mycppfile.cpp ";" в некоторых моих файлах.

Мое рассуждение о #include 'в файлах cpp было: - Все, что должно было войти в заголовочный файл, было в моем файле cpp, поэтому я притворился, что это похоже на заголовочный файл - В monkey-see-monkey делаю моду, я видел, что другие файлы заголовков были #include 'd в файлах, поэтому я сделал то же самое для своего файла cpp.

Так что же я сделал неправильно, и почему это плохо?

Ответ 1

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

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

"О, это не имеет большого значения. Если это работает, все в порядке", Я слышу, как вы плачете. И в некотором смысле, вы были бы правы. Но прямо сейчас вы имеете дело с крошечной крошечной маленькой программой и неплохим и относительно свободным от процессора процессором, чтобы скомпилировать его для вас. Вам не всегда будет так повезло.

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

"О нет! Это звучит ужасно! Однако могу ли я предотвратить эту страшную судьбу?! К сожалению, вы не можете с этим поделать. Если для компиляции требуется несколько часов, для компиляции требуется несколько часов. Но это действительно действительно важно в первый раз - как только вы скомпилировали его один раз, нет никаких оснований для его компиляции.

Если вы ничего не изменили.

Теперь, если у вас было два миллиона строк кода, объединенных в один гигантский бегемот, и нужно сделать простое исправление ошибок, например, x = y + 1, это означает, что вам нужно снова скомпилировать все два миллиона строк чтобы проверить это. И если вы узнаете, что вы хотели сделать x = y - 1 вместо этого, то снова вас ждут два миллиона строк компиляции. Это многочасовое время потрачено впустую, что лучше потратить на то, чтобы что-то сделать.

"Но я ненавижу быть непродуктивным! Если бы только был способ скомпилировать отдельные части моей кодовой базы по отдельности и как-то связать их вместе!" Отличная идея в теории. Но что делать, если ваша программа должна знать, что происходит в другом файле? Невозможно полностью отделить свою кодовую базу, если вы не хотите запускать кучу крошечных крошечных файлов .exe.

"Но, конечно же, это должно быть возможно! Программирование звучит как чистая пытка в противном случае! Что, если бы я нашел способ отделить интерфейс от реализации? Скажем, взяв достаточно информации из этих отдельных сегментов кода, чтобы идентифицировать их для остальных программы и вместо этого помещать их в какой-то заголовочный файл? И таким образом я могу использовать директиву #include препроцессора, чтобы принести только информацию, необходимую для компиляции!"

Хм. Вы могли бы что-то там. Сообщите мне, как это работает для вас.

Ответ 2

Это, вероятно, более подробный ответ, чем вы хотели, но я думаю, что достойное объяснение оправдано.

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

Идея заключается в том, что функции и функции-члены класса/структуры компилируются и собираются один раз, тогда другие функции могут вызывать этот код из одного места, не создавая дубликатов. Ваши функции объявлены как "внешние" неявно.

/* Function declaration, usually found in headers. */
/* Implicitly 'extern', i.e the symbol is visible everywhere, not just locally.*/
int add(int, int);

/* function body, or function definition. */
int add(int a, int b) 
{
   return a + b;
}

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

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

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

Другое исключение - шаблоны. Поскольку компилятору необходимо видеть все определения типов шаблонов при их создании, невозможно отделить реализацию от определения, как в случае с автономными функциями или обычными классами. Что ж, возможно, это возможно сейчас, но получение широкой поддержки компилятором для ключевого слова "export" заняло много времени. Таким образом, без поддержки "экспорта" единицы перевода получают свои собственные локальные копии экземпляров шаблонизированных типов и функций, аналогично тому, как работают встроенные функции. С поддержкой "экспорта" это не так.

За этими двумя исключениями, некоторые люди находят "приятнее" помещать реализации встроенных функций, шаблонных функций и шаблонных типов в файлы .cpp, а затем #include файл .cpp. Является ли это заголовком или исходным файлом, на самом деле не имеет значения; препроцессор не заботится и является просто соглашением.

Краткое описание всего процесса от кода C++ (несколько файлов) до конечного исполняемого файла:

  • Запущен препроцессор, который анализирует все директивы, начинающиеся с "#". Например, директива #include объединяет включенный файл с подчиненным. Он также выполняет макро-замену и вставку токена.
  • Фактический компилятор запускается в промежуточном текстовом файле после этапа препроцессора и испускает код ассемблера.
  • Ассемблер запускает файл сборки и выдает машинный код, который обычно называется объектным файлом и следует двоичному исполняемому формату рассматриваемой операционной системы. Например, Windows использует PE (переносимый исполняемый формат), в то время как Linux использует формат ELF Unix System V с расширениями GNU. На этом этапе символы по-прежнему помечены как неопределенные.
  • Наконец, компоновщик запускается. Все предыдущие этапы выполнялись на каждом блоке перевода по порядку. Однако этап компоновщика работает со всеми сгенерированными объектными файлами, которые были сгенерированы ассемблером. Компоновщик разрешает символы и выполняет много волшебства, например, создает разделы и сегменты, что зависит от целевой платформы и двоичного формата. Программисты не обязаны знать это в целом, но это, безусловно, помогает в некоторых случаях.

Опять же, это было определенно больше, чем вы просили, но я надеюсь, что мелкие детали помогут вам увидеть более широкую картину.

Ответ 3

Типичным решением является использование файлов .h только для деклараций и .cpp для реализации. Если вам нужно повторно использовать реализацию, вы включаете соответствующий файл .h в файл .cpp, где используется необходимый класс/функция/все и ссылается на уже скомпилированный файл .cpp (либо файл .obj - обычно используется в одном проекте - или .lib файл - обычно используется для повторного использования из нескольких проектов). Таким образом вам не нужно перекомпилировать все, только если изменяется только реализация.

Ответ 4

Подумайте о файлах cpp как о черном ящике и файлах .h как о руководствах по использованию этих черных ящиков.

Файлы cpp могут быть скомпилированы заранее. Это не работает в вас, #include их, так как он должен "включать" код в вашу программу каждый раз, когда он ее компилирует. Если вы просто включаете заголовок, он может просто использовать файл заголовка, чтобы определить, как использовать предварительно скомпилированный файл cpp.

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

Также читайте об этом: Файл заголовка включает шаблоны

Ответ 5

Заголовочные файлы обычно содержат объявления функций/классов, а файлы .cpp содержат фактические реализации. Во время компиляции каждый .cpp файл скомпилируется в объектный файл (обычно расширение .o), а компоновщик объединяет различные объектные файлы в конечный исполняемый файл. Процесс связывания обычно намного быстрее, чем компиляция.

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

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

Ответ 6

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

Ответ 7

Если #include cpp файл в нескольких других файлах вашей программы, компилятор попытается скомпилировать файл cpp несколько раз и будет генерировать ошибку, так как будет несколько реализаций тех же методов.

Компиляция займет больше времени (что становится проблемой для больших проектов), если вы вносите изменения в #include cpp файлы, которые затем принудительно перекомпилируют любые файлы #including.

Просто разместите свои объявления в заголовочных файлах и включите их (поскольку они фактически не генерируют код как таковой), и компоновщик подключит объявления с соответствующим кодом cpp (который затем только компилируется один раз).

Ответ 8

Хотя это, безусловно, возможно сделать так же, как и вы, стандартная практика заключается в том, чтобы поместить общие объявления в заголовочные файлы (.h) и определения функций и переменных - реализацию - в исходные файлы (.cpp).

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

Ответ 9

повторное использование, архитектура и инкапсуляция данных

вот пример:

скажем, что вы создаете файл cpp, который содержит простую форму строковых подпрограмм, все в классе mystring, вы помещаете класс decl для этого в файл mystring.h, компилирующий mystring.cpp в файл .obj

теперь в вашей основной программе (например, main.cpp) вы включаете заголовок и ссылку с mystring.obj. для использования mystring в вашей программе вам не нужны детали , как mystring реализуется, поскольку заголовок говорит , что он может сделать

теперь, если приятель хочет использовать ваш класс мистических, вы даете ему mystring.h и mystring.obj, он также не обязательно должен знать, как он работает, пока он работает.

позже, если у вас есть больше таких файлов .obj, вы можете объединить их в файл .lib и ссылку на него.

вы также можете изменить файл mystring.cpp и реализовать его более эффективно, это не повлияет на вашу программу main.cpp или ваших друзей.

Ответ 10

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

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

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

В нескольких ответах обсуждается разделение интерфейса от реализации. Однако это не является неотъемлемой особенностью include файлов, и довольно часто используется #include "заголовочные" файлы, которые непосредственно включают их реализацию (даже стандартная библиотека С++ делает это в значительной степени).

Единственное, что действительно "нетрадиционное" в том, что вы сделали, это назвать ваши включенные файлы ".cpp" вместо ".h" или ".hpp".

Ответ 11

Когда вы компилируете и связываете программу, компилятор сначала компилирует отдельные файлы cpp, а затем связывает (связывает) их. Заголовки никогда не будут скомпилированы, если они не включены в файл cpp.

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

Ответ 12

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

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

Я все еще учусь как ты :)

Ответ 13

Как писать книгу, вы хотите распечатать готовые главы только один раз

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

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

Вернуться к программному обеспечению: у меня есть Linux и Ruby src. Грубая мера строк кода...

     Linux       Ruby
   100,000    100,000   core functionality (just kernel/*, ruby top level dir)
10,000,000    200,000   everything 

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