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

130
задан hichris123 11 January 2014 в 03:46
поделиться

13 ответов

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Хм. Возможно, вы что-то там знаете. Дайте мне знать, как это работает для вас.

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

Хм. Возможно, вы что-то там знаете. Дайте мне знать, как это работает для вас.

и поместить их в какой-то файл заголовка ? И таким образом я могу использовать директиву препроцессора #include , чтобы вводить только ту информацию, которая необходима для компиляции! »

Хм. Возможно, вы что-то там знаете. Дайте мне знать, как это работает для вас.

и поместить их в какой-то файл заголовка ? И таким образом я могу использовать директиву препроцессора #include , чтобы вводить только ту информацию, которая необходима для компиляции! »

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

160
ответ дан 24 November 2019 в 00:10
поделиться

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

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

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

/* 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;
}

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

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

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

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

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

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

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

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

1132441]

43
ответ дан 24 November 2019 в 00:10
поделиться

Think of cpp files as a black box and the .h files as the guides on how to use those black boxes.

The cpp files can be compiled ahead of time. This doesn't work in you #include them, as it needs to actual "include" the code into your program each time it compiles it. If you just include the header, it can just use the header file to determine how to use the precompiled cpp file.

Although this won't make much of a difference for your first project, if you start writing large cpp programs, people are going to hate you because compile times are going to explode.

Also have a read of this: Header File Include Patterns

6
ответ дан 24 November 2019 в 00:10
поделиться

The typical solution is to use .h files for declarations only and .cpp files for implementation. If you need to reuse the implementation you include the corresponding .h file into the .cpp file where the necessary class/function/whatever is used and link against an already compiled .cpp file (either an .obj file - usually used within one project - or .lib file - usually used for reusing from multiple projects). This way you don't need to recompile everything if only the implementation changes.

9
ответ дан 24 November 2019 в 00:10
поделиться

Header files usually contain declarations of functions / classes, while .cpp files contain the actual implementations. At compile time, each .cpp file gets compiled into an object file (usually extension .o), and the linker combines the various object files into the final executable. The linking process is generally much faster than the compilation.

Benefits of this separation: If you are recompiling one of the .cpp files in your project, you don't have to recompile all the others. You just create the new object file for that particular .cpp file. The compiler doesn't have to look at the other .cpp files. However, if you want to call functions in your current .cpp file that were implemented in the other .cpp files, you have to tell the compiler what arguments they take; that is the purpose of including the header files.

Disadvantages: When compiling a given .cpp file, the compiler cannot 'see' what is inside the other .cpp files. So it doesn't know how the functions there are implemented, and as a result cannot optimize as aggressively. But I think you don't need to concern yourself with that just yet (:

6
ответ дан 24 November 2019 в 00:10
поделиться

The basic idea that headers are only included and cpp files are only compiled. This will become more useful once you have many cpp files, and recompiling the whole application when you modify only one of them will be too slow. Or when the functions in the files will start depending on each other. So, you should separate class declarations into your header files, leave implementation in cpp files and write a Makefile (or something else, depending on what tools are you using) to compile the cpp files and link the resulting object files into a program.

5
ответ дан 24 November 2019 в 00:10
поделиться

If you #include a cpp file in several other files in your program, the compiler will try to compile the cpp file multiple times, and will generate an error as there will be multiple implementations of the same methods.

Compilation will take longer (which becomes a problem on large projects), if you make edits in #included cpp files, which then force recompilation of any files #including them.

Just put your declarations into header files and include those (as they don't actually generate code per se), and the linker will hook up the declarations with the corresponding cpp code (which then only gets compiled once).

3
ответ дан 24 November 2019 в 00:10
поделиться

While it is certainly possible to do as you did, the standard practice is to put shared declarations into header files (.h), and definitions of functions and variables - implementation - into source files (.cpp).

As a convention, this helps make it clear where everything is, and makes a clear distinction between interface and implementation of your modules. It also means that you never have to check to see if a .cpp file is included in another, before adding something to it that could break if it was defined in several different units.

2
ответ дан 24 November 2019 в 00:10
поделиться

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

вот пример:

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

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

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

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

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

2
ответ дан 24 November 2019 в 00:10
поделиться

When you compile and link a program the compiler first compiles the individual cpp files and then they link (connect) them. The headers will never get compiled, unless included in a cpp file first.

Typically headers are declarations and cpp are implementation files. In the headers you define an interface for a class or function but you leave out how you actually implement the details. This way you don't have to recompile every cpp file if you make a change in one.

1
ответ дан 24 November 2019 в 00:10
поделиться

Я предлагаю вам пройти через Large Scale C ++ Software Design by John Lakos ]. В колледже мы обычно пишем небольшие проекты, где не сталкиваемся с такими проблемами. В книге подчеркивается важность разделения интерфейсов и реализаций. Точно так же изучение шаблонов, таких как идиома Virtual Constructor, поможет вам лучше понять эту концепцию.

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

1
ответ дан 24 November 2019 в 00:10
поделиться

Это похоже на написание книги: вы хотите распечатать законченные главы только один раз

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

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

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

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

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

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

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

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

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

1
ответ дан 24 November 2019 в 00:10
поделиться

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

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

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

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

Единственное, что действительно есть «Нетрадиционным» в отношении того, что вы сделали, было наименование ваших включенных файлов «.cpp» вместо «.h» или «.hpp».

2
ответ дан 24 November 2019 в 00:10
поделиться
Другие вопросы по тегам:

Похожие вопросы: