Навіщо використовувати #ifndef CLASS_H та #define CLASS_H у .h файлі, але не у .cpp?


137

Я завжди бачив, як люди пишуть

клас.х

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

Питання в тому, чому вони також не роблять цього для файлу .cpp, що містить визначення функцій класу?

Скажімо, у мене є main.cpp, і main.cppвключає class.h. У class.hфайлі нічого не includeвиходить, тож як main.cppзнати, що є в class.cpp?


5
"імпорт" - це, мабуть, не слово, яке ви хочете використати тут. Включати.
Кейт Григорій

5
У C ++ немає кореляції між файлами та класами 1 до 1. Ви можете помістити стільки класів в один файл, скільки хочете (або навіть розділити один клас на кілька файлів, хоча це рідко буває корисним). Тому макрос повинен бути FILE_H, а не CLASS_H.
sbi

Відповіді:


303

Спочатку зверніться до свого першого запиту:

Коли ви бачите це у .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 ++. Добре написано. Дякую Джастін!
Rajkumar R

ви дійсно пояснили чудово! Можливо, відповідь була б досить хорошою, якби це було із зображеннями
аламін

13

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

У файлах CPP не включати охорону, оскільки вміст файлу CPP за визначенням читається лише один раз.

Здається, ви інтерпретували охоронці include як такі, що мають таку ж функцію, як і importзаяви на інших мовах (наприклад, Java); однак це не так. Сам #includeпо собі приблизно еквівалентний importіншим мовам.


2
"в одному файлі CPP" повинен читати "в межах однієї одиниці перекладу".
dreamlax

@dreamlax: Добрий момент - це те, що я спочатку збирався писати, але тоді я зрозумів, що той, хто не розуміє, що охоплює охоронців, буде лише плутати термін "перекладацька одиниця". Я відредагую відповідь, щоб додати "одиницю перекладу" в дужки - це повинно бути найкращим з обох світів.
Мартін Б

6

Це не так - принаймні на етапі компіляції.

Переклад програми c ++ з вихідного коду на машинний код виконується в три фази:

  1. Попередня обробка - Препроцесор аналізує весь вихідний код для рядків, що починаються з # та виконує директиви. У вашому випадку вміст вашого файла class.hвставляється замість рядка #include "class.h. Оскільки ви можете включати в свій заголовок файл у декількох місцях, #ifndefпункти уникають повторюваних помилок декларації, оскільки директива препроцесора не визначена лише вперше, коли файл заголовка включений.
  2. Компіляція - Компілятор тепер переводить усі попередньо оброблені файли вихідного коду у файли бінарних об'єктів.
  3. Зв'язування - Посилання Linker (звідси назва) разом об'єктні файли. Посилання на ваш клас або на один із його методів (який повинен бути оголошений у class.h та визначений у class.cpp) вирішується на відповідне зміщення в одному з файлів об'єкта. Я пишу "один із ваших об'єктних файлів", оскільки ваш клас не потрібно визначати у файлі з назвою class.cpp, він може бути в бібліотеці, яка пов'язана з вашим проектом.

Підсумовуючи, декларації можна ділити через файл заголовка, тоді як зіставлення декларацій до визначень здійснює лінкер.


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файлами.

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


0

його через Headerfiles визначають, що містить клас (Члени, структури даних), а файли cpp реалізують його.

І звичайно, головна причина цього полягає в тому, що ви можете включити один .h файл кілька разів в інші .h файли, але це призведе до декількох визначень класу, який недійсний.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.