Итак, я закончил свое первое задание по программированию на C ++ и получил оценку. Но по оценке я потерял оценки за including cpp files instead of compiling and linking them
. Мне не очень понятно, что это значит.
Оглядываясь назад на мой код, я решил не создавать файлы заголовков для своих классов, но сделал все в файлах cpp (казалось, что все работает нормально без файлов заголовков ...). Я предполагаю, что грейдер имел в виду, что я написал '#include "mycppfile.cpp";' в некоторых из моих файлов.
Мои аргументы для #include
использования файлов cpp были: - Все, что должно было попасть в заголовочный файл, было в моем файле cpp, поэтому я притворился, что это похоже на заголовочный файл - В monkey-see-monkey do fashion я увидел, что другие заголовочные файлы были #include
в файлах, так что я сделал то же самое для моего файла cpp.
Так что именно я сделал не так и почему это плохо?
источник
Ответы:
Насколько мне известно, стандарт C ++ не знает разницы между заголовочными файлами и исходными файлами. Что касается языка, любой текстовый файл с юридическим кодом такой же, как и любой другой. Однако, хотя это и не запрещено, включение исходных файлов в вашу программу в значительной степени устранит любые преимущества, которые вы получили бы от разделения ваших исходных файлов.
По сути,
#include
это говорит препроцессору взять весь указанный вами файл и скопировать его в ваш активный файл до того, как компилятор получит его. Поэтому, когда вы включаете все исходные файлы в свой проект вместе, принципиально нет разницы между тем, что вы сделали, и просто созданием одного огромного исходного файла без какого-либо разделения.«О, это не имеет большого значения. Если это работает, все в порядке», я слышу, как ты плачешь. И в каком-то смысле вы были бы правы. Но сейчас вы имеете дело с крошечной крошечной программой и хорошим и относительно свободным процессором, чтобы скомпилировать ее для вас. Тебе не всегда повезет.
Если вы когда-нибудь углубитесь в сферу серьезного компьютерного программирования, вы увидите проекты с количеством строк, которые могут достигать миллионов, а не десятков. Это много строк. И если вы попытаетесь скомпилировать один из них на современном настольном компьютере, это может занять несколько часов вместо секунд.
«О, нет! Это звучит ужасно! Однако я могу предотвратить эту ужасную судьбу ?!» К сожалению, вы ничего не можете с этим поделать. Если для компиляции требуются часы, для компиляции требуются часы. Но это действительно имеет значение только в первый раз - после того, как вы его скомпилировали один раз, нет причин для его повторной компиляции.
Если вы не измените что-то.
Теперь, если у вас есть два миллиона строк кода, объединенных в одного гигантского бегемота, и вам нужно сделать простое исправление ошибки, например, скажем
x = y + 1
, это означает, что вам нужно снова скомпилировать все два миллиона строк, чтобы проверить это. И если вы обнаружите, чтоx = y - 1
вместо этого хотите сделать a , то опять два миллиона строк компиляции ждут вас. Это много потраченного времени, которое можно потратить на что-то еще.«Но я ненавижу быть непродуктивным! Если бы был какой-то способ скомпилировать отдельные части моей кодовой базы по отдельности и как-то связать их вместе потом»! Отличная идея, в теории. Но что, если вашей программе нужно знать, что происходит в другом файле? Невозможно полностью отделить вашу кодовую базу, если вы не хотите вместо этого запустить кучу крошечных крошечных файлов .exe.
«Но, конечно, это должно быть возможно! В противном случае программирование звучит как настоящая пытка! Что если я найду способ отделить интерфейс от реализации ? Скажем, беря достаточно информации из этих отдельных сегментов кода, чтобы идентифицировать их для остальной части программы, и помещая вместо этого в каком-то заголовочном файле? И таким образом, я могу использовать
#include
директиву препроцессора, чтобы вводить только информацию, необходимую для компиляции! "Хм. Вы можете быть там к чему-то. Дайте мне знать, как это работает для вас.
источник
Это, вероятно, более подробный ответ, чем вы хотели, но я думаю, что достойное объяснение оправдано.
В C и C ++ один исходный файл определяется как одна единица перевода . По соглашению заголовочные файлы содержат объявления функций, определения типов и определения классов. Реальные реализации функций находятся в единицах перевода, то есть файлах .cpp.
Идея заключается в том, что функции и функции-члены класса / структуры компилируются и собираются один раз, тогда другие функции могут вызывать этот код из одного места, не создавая дубликатов. Ваши функции объявлены как "внешние" неявно.
Если вы хотите, чтобы функция была локальной для единицы перевода, вы определяете ее как «статическую». Что это значит? Это означает, что если вы включите исходные файлы с внешними функциями, вы получите ошибки переопределения, потому что компилятор встречает одну и ту же реализацию более одного раза. Итак, вы хотите, чтобы все ваши блоки перевода видели объявление функции, но не тело функции .
Так как же все это в конце концов смешается? Это работа линкера. Компоновщик читает все объектные файлы, которые генерируются на этапе ассемблера, и разрешает символы. Как я уже говорил ранее, символ - это просто имя. Например, имя переменной или функции. Когда блоки перевода, которые вызывают функции или объявляют типы, не знают реализацию этих функций или типов, эти символы называются неразрешенными. Компоновщик разрешает неразрешенный символ, соединяя модуль перевода, который содержит неопределенный символ, с тем, который содержит реализацию. Уф. Это верно для всех видимых извне символов, независимо от того, реализованы они в вашем коде или предоставлены дополнительной библиотекой. Библиотека - это просто архив с многоразовым кодом.
Есть два заметных исключения. Во-первых, если у вас есть небольшая функция, вы можете сделать ее встроенной. Это означает, что сгенерированный машинный код не генерирует вызов функции extern, а буквально объединяется на месте. Поскольку они обычно небольшие, размер накладных расходов не имеет значения. Вы можете представить их статичными в том, как они работают. Так что безопасно реализовывать встроенные функции в заголовках. Реализации функций внутри определения класса или структуры также часто автоматически вставляются компилятором.
Другое исключение - шаблоны. Поскольку при создании экземпляра компилятору необходимо видеть все определение типа шаблона, невозможно отделить реализацию от определения, как в случае автономных функций или обычных классов. Что ж, возможно, это возможно сейчас, но получение широкой поддержки компилятором для ключевого слова "export" заняло много времени. Таким образом, без поддержки «экспорта» единицы перевода получают свои собственные локальные копии экземпляров шаблонизированных типов и функций, аналогично тому, как работают встроенные функции. С поддержкой «экспорта» это не так.
За этими двумя исключениями, некоторые люди считают, что «лучше» помещать реализации встроенных функций, шаблонных функций и шаблонных типов в файлы .cpp, а затем #include файл .cpp. Является ли это заголовком или исходным файлом, на самом деле не имеет значения; препроцессор не заботится и является просто соглашением.
Краткое описание всего процесса от кода C ++ (несколько файлов) до конечного исполняемого файла:
Опять же, это было определенно больше, чем вы просили, но я надеюсь, что мелкие детали помогут вам увидеть более широкую картину.
источник
int add(int, int);
это объявление функции . Часть прототипа это простоint, int
. Тем не менее, все функции в C ++ имеют прототип, поэтому этот термин действительно имеет смысл только в C. Я отредактировал ваш ответ на этот вопрос.export
Шаблоны были удалены из языка в 2011 году. Он никогда не был поддержан компиляторами.Типичным решением является использование
.h
файлов только для объявлений и.cpp
файлов для реализации. Если вам нужно повторно использовать реализацию, вы включаете соответствующий.h
файл в.cpp
файл, где используется необходимый класс / функция / что бы то ни было, и ссылаетесь на уже скомпилированный.cpp
файл (.obj
файл, обычно используемый в одном проекте, или файл .lib, обычно используемый для повторного использования из нескольких проектов). Таким образом, вам не нужно перекомпилировать все, если меняется только реализация.источник
Думайте о файлах cpp как о черном ящике, а о файлах .h как о том, как использовать эти черные ящики.
Файлы cpp могут быть скомпилированы заранее. Это не работает в вас #include их, так как необходимо «включать» код в вашу программу каждый раз, когда он компилирует его. Если вы просто включите заголовок, он может просто использовать файл заголовка, чтобы определить, как использовать предварительно скомпилированный файл cpp.
Хотя это не будет иметь большого значения для вашего первого проекта, если вы начнете писать большие программы на cpp, люди будут вас ненавидеть, потому что время компиляции будет стремительно расти.
Также прочитайте это: Заголовочный файл включает шаблоны
источник
Заголовочные файлы обычно содержат объявления функций / классов, в то время как файлы .cpp содержат фактические реализации. Во время компиляции каждый файл .cpp компилируется в объектный файл (обычно расширение .o), и компоновщик объединяет различные объектные файлы в конечный исполняемый файл. Процесс компоновки обычно намного быстрее, чем компиляция.
Преимущества такого разделения: если вы перекомпилируете один из файлов .cpp в своем проекте, вам не нужно перекомпилировать все остальные. Вы просто создаете новый объектный файл для этого конкретного файла .cpp. Компилятору не нужно смотреть на другие файлы .cpp. Однако, если вы хотите вызывать функции в вашем текущем файле .cpp, которые были реализованы в других файлах .cpp, вы должны сообщить компилятору, какие аргументы они принимают; это цель включения заголовочных файлов.
Недостатки: при компиляции данного файла .cpp компилятор не может «увидеть», что находится внутри других файлов .cpp. Так что он не знает, как там реализованы функции, и в результате не может оптимизироваться так агрессивно. Но я думаю, вам пока не нужно беспокоиться об этом (:
источник
Основная идея, что заголовки только включены, а файлы cpp только скомпилированы. Это станет более полезным, когда у вас будет много cpp-файлов, и перекомпиляция всего приложения при изменении только одного из них будет слишком медленной. Или когда функции в файлах начнут зависеть друг от друга. Итак, вы должны разделить объявления классов в ваших заголовочных файлах, оставить реализацию в файлах cpp и написать Makefile (или что-то еще, в зависимости от того, какие инструменты вы используете), чтобы скомпилировать файлы cpp и связать получившиеся объектные файлы в программу.
источник
Если вы #include файл cpp в нескольких других файлах в вашей программе, компилятор будет пытаться скомпилировать файл cpp несколько раз и выдаст ошибку, так как будет несколько реализаций одного и того же метода.
Компиляция займет больше времени (что становится проблемой для больших проектов), если вы вносите изменения в файлы #included cpp, которые затем вызывают принудительную перекомпиляцию любых файлов, включая их.
Просто поместите ваши объявления в заголовочные файлы и включите их (поскольку они фактически не генерируют код как таковой), и компоновщик соединит объявления с соответствующим кодом cpp (который затем компилируется только один раз).
источник
Хотя это, безусловно, возможно сделать так, как вы, стандартная практика - помещать общие объявления в заголовочные файлы (.h), а определения функций и переменных - реализацию - в исходные файлы (.cpp).
Как правило, это помогает прояснить, где все находится, и проводит четкое различие между интерфейсом и реализацией ваших модулей. Это также означает, что вам никогда не нужно проверять, включен ли файл .cpp в другой, прежде чем добавлять в него что-то, что может сломаться, если он был определен в нескольких различных единицах.
источник
повторное использование, архитектура и инкапсуляция данных
вот пример:
скажем, вы создаете файл cpp, который содержит простую форму строковых подпрограмм, все в классе mystring, вы помещаете класс decl для этого в mystring.h, компилируя mystring.cpp в файл .obj.
теперь в вашей основной программе (например, main.cpp) вы включаете заголовок и ссылку на mystring.obj. чтобы использовать mystring в вашей программе, вам не нужны подробности о том, как реализована mystring, поскольку заголовок говорит о том, что он может делать
теперь, если друг хочет использовать ваш класс mystring, вы даете ему mystring.h и mystring.obj, ему также не обязательно знать, как он работает, пока он работает.
позже, если у вас будет больше таких файлов .obj, вы можете объединить их в файл .lib и вместо этого ссылаться на него.
Вы также можете изменить файл mystring.cpp и реализовать его более эффективно, это не повлияет на ваш main.cpp или вашу программу друзей.
источник
Если это работает для вас, то в этом нет ничего плохого - за исключением того, что это потрясет перья людей, которые думают, что есть только один способ сделать что-то.
Многие из приведенных здесь ответов касаются оптимизации для крупномасштабных программных проектов. Это хорошие вещи, о которых нужно знать, но нет смысла оптимизировать маленький проект, как если бы это был большой проект - это то, что известно как «преждевременная оптимизация». В зависимости от среды разработки может возникнуть значительная дополнительная сложность при настройке конфигурации сборки для поддержки нескольких исходных файлов на программу.
Если в течение долгого времени, ваш проект развивается , и вы обнаружите , что процесс сборки занимает слишком много времени, то вы можете реорганизовывать код , чтобы использовать несколько исходных файлов для более быстрого инкрементального строит.
В нескольких ответах обсуждается отделение интерфейса от реализации. Тем не менее, это не присуще включаемым файлам, и довольно часто #include «заголовочные» файлы, которые напрямую включают их реализацию (даже стандартная библиотека C ++ делает это в значительной степени).
Единственное, что действительно «нетрадиционно» в том, что вы сделали, - это присвоение имен включенным файлам «.cpp» вместо «.h» или «.hpp».
источник
Когда вы компилируете и связываете программу, компилятор сначала компилирует отдельные файлы cpp, а затем они связывают (связывают) их. Заголовки никогда не будут скомпилированы, если они не включены в файл cpp.
Обычно заголовки - это объявления, а cpp - файлы реализации. В заголовках вы определяете интерфейс для класса или функции, но не учитываете, как вы на самом деле реализуете детали. Таким образом, вам не нужно перекомпилировать каждый файл cpp, если вы вносите в него изменения.
источник
Я предлагаю вам пройти через дизайн программного обеспечения Large Scale C ++ Джона Лакоса . В колледже мы обычно пишем небольшие проекты, в которых нам не приходится сталкиваться с такими проблемами. В книге подчеркивается важность разделения интерфейсов и реализаций.
Заголовочные файлы обычно имеют интерфейсы, которые не должны меняться так часто. Аналогичным образом, изучение таких шаблонов, как идиома Virtual Constructor, поможет вам лучше понять концепцию.
Я все еще учусь как ты :)
источник
Это как писать книгу, вы хотите распечатать законченные главы только один раз
Скажем, вы пишете книгу. Если вы помещаете главы в отдельные файлы, то вам нужно распечатать главу, только если вы ее изменили. Работа над одной главой не меняет других.
Но включение файлов cpp с точки зрения компилятора похоже на редактирование всех глав книги в одном файле. Затем, если вы измените его, вы должны распечатать все страницы всей книги, чтобы напечатать вашу исправленную главу. В генерации объектного кода нет опции «печать выбранных страниц».
Вернемся к программному обеспечению: у меня есть Linux и Ruby src. Грубая мера строк кода ...
Любая из этих четырех категорий имеет много кода, поэтому требуется модульность. Этот вид кодовой базы удивительно типичен для реальных систем.
источник