Зачем использовать #ifndef CLASS_H и #define CLASS_H в файле .h, а не в .cpp?

137

Я всегда видел, как люди пишут

class.h

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

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

Допустим, у меня есть main.cppи main.cppвключает class.h. class.hФайл не includeчто - нибудь, так как же main.cppзнает , что в class.cpp?

user385261
источник
5
«импорт», вероятно, не то слово, которое вы хотите использовать здесь. Включают.
Кейт Грегори
5
В C ++ нет корреляции 1: 1 между файлами и классами. Вы можете поместить столько классов в один файл, сколько хотите (или даже разделить один класс на несколько файлов, хотя это редко бывает полезным). Поэтому макрос должен быть FILE_H, а не CLASS_H.
sbi
1
Смотрите мой совет по охране .

Ответы:

304

Во-первых, чтобы ответить на ваш первый запрос:

Когда вы видите это в .h файле:

#ifndef FILE_H
#define FILE_H

/* ... Declarations etc here ... */

#endif

Это препроцессорная техника предотвращения многократного включения заголовочного файла, что может быть проблематичным по разным причинам. Во время компиляции вашего проекта каждый файл .cpp (обычно) компилируется. Проще говоря, это означает, что компилятор возьмет ваш файл .cpp , откроет все его файлы #included, объединит их все в один массивный текстовый файл, а затем выполнит синтаксический анализ и, наконец, преобразует его в некоторый промежуточный код, оптимизирует / выполняет другие. задач и, наконец, сгенерировать выходные данные сборки для целевой архитектуры. Из-за этого, если файл #includedнесколько раз под одним .cppфайл, компилятор добавит содержимое своего файла дважды, поэтому, если в этом файле есть определения, вы получите ошибку компилятора, сообщающую, что вы переопределили переменную. Когда файл обрабатывается на этапе препроцессора в процессе компиляции, при первом достижении его содержимого первые две строки проверят, был ли FILE_Hон определен для препроцессора. Если нет, он определит FILE_Hи продолжит обработку кода между ним и #endifдирективой. В следующий раз, когда препроцессор увидит содержимое этого файла, проверка FILE_Hбудет ложной, поэтому он будет сразу же сканировать до #endifи продолжать после него. Это предотвращает ошибки переопределения.

И для решения вашей второй проблемы:

В программировании на C ++ в качестве общей практики мы разделяем разработку на два типа файлов. Один с расширением .h, и мы называем это «заголовочный файл». Обычно они предоставляют объявление функций, классов, структур, глобальных переменных, typedefs, макросов и определений предварительной обработки и т. Д. По сути, они просто предоставляют вам информацию о вашем коде. Затем у нас есть расширение .cpp, которое мы называем «файл кода». Это обеспечит определения для этих функций, членов класса, любых элементов структуры, которым нужны определения, глобальные переменные и т. Д. Таким образом, файл .h объявляет код, а файл .cpp реализует это объявление. По этой причине мы обычно во время компиляции компилируем каждый .cppфайл в объект и затем связать эти объекты (потому что вы почти никогда не видите, чтобы один файл .cpp включал другой файл .cpp ).

Как эти внешние проблемы решаются - работа для компоновщика. Когда ваш компилятор обрабатывает main.cpp , он получает объявления для кода в class.cpp , включая class.h . Нужно только знать, как выглядят эти функции или переменные (что дает вам объявление). Поэтому он компилирует ваш файл main.cpp в некоторый объектный файл (назовите его main.obj ). Аналогично, class.cpp компилируется в class.objфайл. Чтобы создать конечный исполняемый файл, вызывается компоновщик, который связывает эти два объектных файла вместе. Для любых неразрешенных внешних переменных или функций компилятор поместит заглушку, где происходит доступ. Затем компоновщик возьмет эту заглушку и найдет код или переменную в другом указанном объектном файле, и, если он найден, он объединит код из двух объектных файлов в выходной файл и заменит заглушку с окончательным расположением функции или переменная. Таким образом, ваш код в main.cpp может вызывать функции и использовать переменные в class.cpp ЕСЛИ И ТОЛЬКО ЕСЛИ ОНИ ОБЪЯВЛЕНЫ в class.h .

Я надеюсь, что это было полезно.

Джастин Саммерлин
источник
Я пытался понять .h и .cpp в течение последних нескольких дней. Этот ответ сэкономил мое время и интерес к изучению C ++. Хорошо написанный. Спасибо, Джастин!
Раджкумар Р
Вы действительно объяснили здорово! Возможно, ответ был бы довольно хорошим, если бы он был с изображениями
аламин
13

CLASS_HЭто включает охрану ; он используется для предотвращения многократного включения одного и того же заголовочного файла (по разным маршрутам) в один и тот же файл CPP (или, точнее, в одну и ту же единицу перевода ), что может привести к ошибкам с множественным определением.

Включение защиты не требуется для файлов CPP, поскольку по определению содержимое файла CPP читается только один раз.

Похоже, вы интерпретировали включающие охранники как выполняющие ту же функцию, что и importоператоры в других языках (таких как Java); однако это не так. Само по #includeсебе примерно эквивалентно importдругим языкам.

Мартин Б
источник
2
«в одном и том же файле CPP» следует читать «в одном и том же блоке перевода».
Dreamlax
@dreamlax: Хороший вопрос - это то, что я изначально собирался написать, но потом я подумал, что тот, кто не понимает, включает в себя охранников, будет только путать термин «единица перевода». Я отредактирую ответ, чтобы добавить «единицу перевода» в скобках - это должно быть лучшим из обоих миров.
Мартин Б
6

Это не так - по крайней мере, на этапе компиляции.

Перевод программы на С ++ из исходного кода в машинный код выполняется в три этапа:

  1. Предварительная обработка - Препроцессор анализирует весь исходный код для строк, начинающихся с #, и выполняет директивы. В вашем случае содержимое вашего файла class.hвставляется вместо строки #include "class.h. Поскольку вы можете быть включены в ваш заголовочный файл в нескольких местах, #ifndefпункты избегают повторяющихся ошибок объявления, поскольку директива препроцессора не определена только при первом включении заголовочного файла.
  2. Компиляция - теперь компилятор переводит все предварительно обработанные файлы исходного кода в двоичные объектные файлы.
  3. Linking - Linker связывает (отсюда и название) вместе объектные файлы. Ссылка на ваш класс или один из его методов (который должен быть объявлен в class.h и определен в class.cpp) разрешается до соответствующего смещения в одном из объектных файлов. Я пишу «один из ваших объектных файлов», поскольку ваш класс не нужно определять в файле с именем class.cpp, он может находиться в библиотеке, которая связана с вашим проектом.

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

sum1stolemyname
источник
4

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

Чтобы использовать что-то, вам нужно знать, что это декларация, а не определение. Только компоновщик должен знать определение.

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

Также ты имеешь ввиду #includeи не импорт.

Брайан Р. Бонди
источник
3

Это делается для заголовочных файлов, так что содержимое появляется только один раз в каждом предварительно обработанном исходном файле, даже если оно включено более одного раза (обычно потому, что оно включено из других заголовочных файлов). При первом включении символ CLASS_H(известный как « включить защиту» ) еще не был определен, поэтому все содержимое файла включено. Это определяет символ, поэтому, если он будет включен снова, содержимое файла (внутри блока #ifndef/ #endif) будет пропущено.

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

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

Майк Сеймур
источник
2

main.cpp не должен знать, что находится в class.cpp . Он просто должен знать объявления функций / классов, которые он использует, и эти объявления находятся в class.h .

Линкер связывает места, где используются функции / классы, объявленные в class.h , и их реализации в class.cpp.

Игорь Окс
источник
1

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

Кейт Грегори
источник
1

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

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

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

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

аутистический
источник
0

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

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

Quonux
источник