1. Модель компиляции C++

Обычно программа на C++ содержит только два типа файлов — файлы .cpp и файлы .h. Среди них файлы .cpp называются исходными файлами C++, которые содержат исходный код C++; файлы .h называются заголовочными файлами C++, которые также содержат исходный код C++.

Язык C++ поддерживает "раздельную компиляцию". То есть всё содержимое программы можно разделить на разные части и разместить в разных .cpp-файлах. Содержимое в одном .cpp-файле относительно независимо; при компиляции ему не нужно взаимодействовать с другими файлами. Нужно лишь скомпоновать его с другими объектными файлами после компиляции в объектные файлы. Например, глобальная функция "void a(){}" определена в файле a.cpp, а файлу b.cpp нужно вызвать эту функцию. Несмотря на это, файлам a.cpp и b.cpp не нужно знать о существовании друг друга. Их можно компилировать отдельно, а после компиляции в объектные файлы — скомпоновать, и вся программа сможет работать.

Как это достигается? С точки зрения написания программы это очень просто. В файле b.cpp перед вызовом функции "void a()" достаточно объявить функцию "void a();". Это потому, что когда компилятор компилирует b.cpp, он создаёт таблицу символов. Символы без определений, такие как "void a()", хранятся в этой таблице. Во время компоновки компилятор ищет определение этого символа в других объектных файлах. Как только оно найдено, программу можно успешно сгенерировать.

Обратите внимание, что здесь упоминаются два понятия: «определение» и «объявление». Проще говоря, «определение» полностью описывает символ: переменная это или функция, какой у него тип возвращаемого значения, какие параметры ему нужны, и так далее. «Объявление» лишь объявляет существование символа, то есть сообщает компилятору: этот символ определён в другом файле; я буду использовать его здесь, а при компоновке обратись в другое место, чтобы выяснить, что это на самом деле. При определении необходимо полностью определить символ (переменную или функцию) в соответствии с синтаксисом C++, а при объявлении достаточно лишь написать прототип символа. Следует отметить, что символ может быть объявлен в программе несколько раз, но определён он должен быть ровно один раз. Представьте, если у символа есть два разных определения, какое из них должен слушать компилятор? Этот механизм приносит программистам на C++ много преимуществ, а также приводит к способу написания программ. Рассмотрим такое: если есть часто используемая функция «void f() {}», которая вызывается во многих .cpp-файлах по всей программе, то нам нужно только определить функцию в одном файле и объявить её в остальных файлах. С одной функцией легко справиться; объявить её — это всего одно предложение. Но что, если функций много, например большой набор математических функций, несколько сотен? Можем ли мы гарантировать, что каждый программист сможет точно запомнить и выписать формы всех функций?

2. Что такое заголовочный файл?

Очевидно, ответ — невозможно. Но есть очень простой способ помочь программистам избавиться от необходимости запоминать столько прототипов функций: мы можем заранее записать все объявления этих сотен функций, поместить их в один файл, и когда программистам они понадобятся, они смогут скопировать всё это в свой исходный код.

Этот метод действительно работоспособен, но всё же слишком трудоёмок и довольно неуклюж. Поэтому свою роль могут сыграть заголовочные файлы. Так называемый заголовочный файл, фактически, имеет то же содержимое, что и файлы .cpp; оба являются исходным кодом C++. Однако заголовочные файлы не нужно компилировать. Мы помещаем все объявления функций в заголовочный файл. Когда они нужны исходному файлу .cpp, их можно включить в файл .cpp с помощью макрокоманды "#include", тем самым объединяя их содержимое с файлом .cpp. Когда файл .cpp компилируется, включённые файлы .h вступают в силу.

Например, предположим, есть только две математические функции: f1 и f2. Тогда мы помещаем их определения в math.cpp:

/* math.cpp */ double f1() { //сделать что-то здесь.... return; } double f2(double a) { //сделать что-то здесь... return a * a; } /* конец math.cpp */

А объявления "этих" функций поместите в заголовочный файл math.h:

/* math.h */ double f1(); double f2(double); /* конец math.h */

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

/* main.cpp */ #include "math.h" main() { int number1 = f1(); int number2 = f2(number1); } /* конец main.cpp */

Таким образом, это полная программа. Следует отметить, что файл .h не нужно указывать после команды компилятора, но он должен находиться в месте, где компилятор сможет его найти (например, в том же каталоге, что и main.cpp). Как main.cpp, так и math.cpp можно компилировать отдельно, создавая main.o и math.o. Затем, после компоновки этих двух объектных файлов, программа сможет запуститься.

3. #include

#include — это макрокоманда из языка C. Она срабатывает до того, как компилятор начнёт компиляцию, то есть во время предварительной обработки. Функция #include заключается в том, чтобы включить содержимое файла, указанного после него, в текущий файл полностью и без изменения ни одного слова. Стоит упомянуть, что у неё нет других функций или побочных эффектов. Её функция — заменить каждое место, где она встречается, содержимым файла, указанного после неё. Простая текстовая замена, и ничего больше. Поэтому первая строка в main.cpp (#include "math.h") будет заменена содержимым math.h до компиляции. То есть, когда процесс компиляции вот-вот начнётся, содержимое main.cpp уже изменилось:

/* ~main.cpp */ double f1(); double f2(double); main() { int number1 = f1(); int number2 = f2(number1); } /* конец ~main.cpp */

Ни больше, ни меньше, в самый раз. По той же логике, если помимо main.cpp есть много других .cpp-файлов, которые также используют функции f1 и f2, то им всем нужно только написать #include "math.h" перед использованием этих двух функций.


4. Что следует писать в заголовочном файле?

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

Итак, следует запомнить: в .h-заголовочных файлах должны находиться только объявления переменных или функций, а не определения. То есть в заголовочных файлах можно писать только выражения вида: extern int a; и void f();. Это объявления. Если написать выражения вида int a; или void f() {}, то, как только этот заголовочный файл будет включён двумя или более .cpp-файлами, компилятор сразу сообщит об ошибке. (Об extern мы говорили ранее; здесь мы снова не будем обсуждать разницу между определениями и объявлениями.)

Однако из этого правила есть три исключения:

  • Во-первых, определения const-объектов можно записывать в заголовочных файлах. Поскольку глобальные const-объекты по умолчанию не имеют extern-объявлений, они допустимы только в текущем файле. Если записать такие объекты в заголовочный файл, то даже если он включён в несколько других .cpp-файлов, объект допустим только в том файле, который его включает, и невидим для других файлов, поэтому это не приведёт к множественным определениям. В то же время, поскольку объект в этих .cpp-файлах включается из одного заголовочного файла, это гарантирует, что значение const-объекта в этих .cpp-файлах одинаково. Можно сказать, что одним выстрелом убивают двух зайцев. Аналогично, определения static-объектов также можно размещать в заголовочных файлах.
  • Во-вторых, определения inline-функций можно записывать в заголовочных файлах. Поскольку inline-функция требует, чтобы компилятор встроил её в месте, где она встречается, согласно её определению, и, в отличие от обычных функций, её нельзя сначала объявить, а затем скомпоновать (inline-функции не компонуются), компилятору нужно видеть полное определение inline-функции во время компиляции. Если бы inline-функцию можно было определить только один раз, как обычную функцию, это было бы затруднительно. Потому что в одном файле это нормально; я могу поместить определение inline-функции в самое начало, чтобы определение было видно везде, где она используется дальше. Но что, если я также использую эту функцию в других файлах? Хорошего решения почти нет. Поэтому C++ устанавливает, что inline-функция может быть определена в программе несколько раз, если только inline-функция встречается в одном .cpp-файле лишь один раз, и определение inline-функции одинаково во всех .cpp-файлах, — тогда она может пройти компиляцию. Очевидно, помещать определения inline-функций в заголовочный файл — очень мудрая практика.
  • В-третьих, определения классов можно записывать в заголовочных файлах. Поскольку когда программа создаёт объект класса, компилятор может узнать, как должен быть размещён объект этого класса, только когда определение класса полностью видимо. Поэтому требования к определениям классов в основном такие же, как к inline-функциям. Так что помещать определение класса в заголовочный файл и включать этот заголовочный файл в .cpp-файлы, использующие класс, — хорошая практика. Здесь стоит упомянуть, что определение класса содержит члены данных и функции-члены. Члены данных определяются (память выделяется) только при создании конкретного объекта, но функции-члены должны быть определены с самого начала, что мы обычно называем реализацией класса. Обычно мы поступаем так: помещаем определение класса в заголовочный файл, а код реализации функций-членов — в .cpp-файл. Это допустимо и является очень хорошим подходом. Однако есть и другой подход: также напрямую записать код реализации функций-членов внутри определения класса. В классах C++, если функция-член определена внутри тела определения класса, компилятор будет рассматривать эту функцию как inline. Поэтому определять функции-члены внутри тела определения класса и помещать их вместе в заголовочный файл законно. Обратите внимание: если определение функции-члена записано в заголовочном файле определения класса, но не внутри определения класса, это незаконно, потому что в этот момент функция-член больше не является inline. Как только заголовочный файл будет включён двумя или более .cpp-файлами, функция-член будет переопределена.

5. Меры защиты в заголовочных файлах

Рассмотрим следующее: если заголовочный файл содержит только объявления, то независимо от того, сколько раз он включается одним и тем же .cpp-файлом, проблемы нет — потому что появление объявлений не ограничено. Однако три исключения в заголовочных файлах, рассмотренные выше, также являются очень распространённым использованием заголовочных файлов. Поэтому как только любое из трёх вышеперечисленных исключений появляется в заголовочном файле, если он включается несколько раз .cpp-файлом, возникнет большая проблема. Это связано с тем, что синтаксические элементы в этих трёх исключениях, хотя они «могут быть определены в нескольких исходных файлах», «могут появляться только один раз в одном исходном файле». Представьте, что a.h содержит определение класса A, а b.h содержит определение класса B. Поскольку определение класса B зависит от класса A, b.h также #includes a.h. Теперь есть исходный файл, который использует и класс A, и класс B, поэтому программист включает в этот исходный файл и a.h, и b.h. В этот момент возникает проблема: определение класса A появляется в этом исходном файле дважды! В результате вся программа не может быть скомпилирована. Вы можете подумать, что это ошибка программиста — он должен был знать, что b.h включает a.h, — но на самом деле он не должен этого знать.

Использование «#define» вместе с условной компиляцией позволяет хорошо решить эту проблему. В заголовочном файле определите имя с помощью #define и используйте условную компиляцию #ifndef...#endif, чтобы компилятор мог решить, продолжать ли компиляцию остальной части заголовочного файла, в зависимости от того, определено ли это имя. Этот способ прост, но нужно не забыть включить его при написании заголовочных файлов.


Разница между заголовочными файлами и исходными файлами C++

1. Как исходные файлы связываются с заголовочными файлами на основе #include

  • 1. Предоставляемые системой заголовочные файлы заключаются в угловые скобки, поэтому компилятор будет искать их в системном каталоге файлов.
  • 2. Пользовательские файлы заключаются в двойные кавычки. Компилятор сначала выполняет поиск в каталоге пользователя, затем в каталоге установки C++ (например, в VC можно указать и изменить путь поиска библиотечных файлов, а в Unix и Linux его можно задать через переменные среды), и наконец выполняет поиск в системных файлах.

#include "xxx.h" (я всегда думал, что "" и <> не имеют никакой разницы, но tinyxml.h — несистемный файл, поэтому нужно использовать "")

2. Как заголовочные файлы связаны с исходными файлами

Этот вопрос на самом деле означает: при условии, что заголовочный файл "a.h" объявляет ряд функций, а "b.cpp" реализует эти функции, если я хочу использовать в "c.cpp" функции, объявленные в "a.h" и реализованные в "b.cpp", я обычно использую #include "a.h" в "c.cpp". Так как же c.cpp находит реализации в b.cpp?

На самом деле имена файлов .cpp и .h не имеют прямой связи. Многие компиляторы могут принимать другие расширения. Например, сейчас я вижу в исходном коде нашей компании, что файлы .cpp были заменены файлами .cc.

В Turbo C компиляция выполняется из командной строки, а параметром командной строки является имя файла, расширения по умолчанию — .cpp и .h, но их также можно настроить на .xxx и так далее.

В книге учителя Тань Хаоцяна "C Programming" упоминается, что во время предварительной обработки компилятором команда #include обрабатывается как "обработка включения файла": всё содержимое файла file2.c копируется в позицию #include "file2.c". Это также объясняет, почему многим компиляторам всё равно, какое у файла расширение, — потому что предварительная обработка #include просто выполняет работу по "копированию и вставке кода".

Во время компиляции компилятор не ищет реализации функций в b.cpp; это делается только на этапе компоновки. Использование #include "a.h" в b.cpp или c.cpp фактически вводит соответствующие объявления, чтобы компиляция могла пройти. Программе не важно, где находится реализация и как она реализована. После компиляции исходных файлов создаются объектные файлы (файлы .o или .obj). В этих объектных файлах эти функции и переменные рассматриваются как символы. На этапе компоновки makefile должен указать, какой .o или .obj файл компоновать (здесь .o или .obj файл, сгенерированный b.cpp). В этот момент компоновщик обращается к этому .o или .obj файлу, чтобы найти функции, реализованные в b.cpp, а затем собирает их в исполняемый файл, указанный в makefile.

В Unix можно даже не включать заголовочные файлы в исходные файлы; достаточно указать их в makefile (но это сильно снижает читаемость программы, что является плохой привычкой, ^_^). В VC, как правило, не нужно самому писать makefile; достаточно включить все необходимые файлы в проект, и VC автоматически напишет makefile за вас.

Обычно компилятор C++ ищет нужные символы в каждом .o или .obj файле, а не только в конкретном файле и не останавливается, найдя один. Поэтому, если одна и та же функция реализована в нескольких разных файлах или определена одна и та же глобальная переменная, компоновщик во время компоновки сообщит о "redefined".

В итоге

Файлы .h могут содержать:

  • Объявления данных-членов класса, но нельзя присваивать значения
  • Определения и присваивания значений статических членов-данных класса, но это не рекомендуется; достаточно просто объявления.
  • Объявления функций-членов класса
  • Объявления функций, не являющихся членами класса
  • Определения констант: например, constint a=5;
  • Определения статических функций
  • Определения inline-функций класса

Не может содержать:

  • 1. Объявления всех нестатических переменных (не членов данных класса)
  • 2. Не размещайте объявления пространства имен по умолчанию в заголовочных файлах; using namespace std; и т. п. следует размещать в файлах .cpp. В файлах .h используйте std::string.

Исходный URL: https://blog.csdn.net/qq_35038153/article/details/71293265