Po co używać #ifndef CLASS_H i #define CLASS_H w pliku .h, ale nie w .cpp?

137

Zawsze widziałem, jak ludzie piszą

class.h

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

Pytanie brzmi, dlaczego nie robią tego również dla pliku .cpp, który zawiera definicje funkcji klas?

Powiedzmy, że mam main.cppi main.cppobejmuje class.h. class.hPlik nie includewszystko, tak jak nie main.cppwiem co jest w class.cpp?

user385261
źródło
5
„import” prawdopodobnie nie jest słowem, którego chcesz użyć. Zawierać.
Kate Gregory
5
W C ++ nie ma korelacji 1 do 1 między plikami i klasami. Możesz umieścić dowolną liczbę klas w jednym pliku (lub nawet podzielić jedną klasę na kilka plików, chociaż rzadko jest to pomocne). Dlatego makro powinno być FILE_H, a nie CLASS_H.
sbi

Odpowiedzi:

303

Po pierwsze, aby odpowiedzieć na pierwsze zapytanie:

Kiedy zobaczysz to w pliku .h :

#ifndef FILE_H
#define FILE_H

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

#endif

Jest to technika preprocesora, która zapobiega wielokrotnemu dołączaniu pliku nagłówkowego, co może być problematyczne z różnych powodów. Podczas kompilacji projektu kompilowany jest każdy plik .cpp (zwykle). Mówiąc prościej, oznacza to, że kompilator weźmie plik .cpp , otworzy wszystkie pliki #includedprzez niego, połączy je wszystkie w jeden ogromny plik tekstowy, a następnie przeprowadzi analizę składni, a na koniec przekonwertuje go na jakiś kod pośredni, zoptymalizuje / wykona inne zadań, a na koniec wygeneruj dane wyjściowe zestawu dla architektury docelowej. Z tego powodu, jeśli plik jest #includedwielokrotnie pod jednym .cppplik, kompilator dwukrotnie dopisze zawartość pliku, więc jeśli w tym pliku znajdują się definicje, pojawi się błąd kompilatora informujący o przedefiniowaniu zmiennej. Gdy plik jest przetwarzany przez krok preprocesora w procesie kompilacji, przy pierwszym osiągnięciu jego zawartości pierwsze dwa wiersze sprawdzają, czy FILE_Hzostał on zdefiniowany dla preprocesora. Jeśli nie, zdefiniuje FILE_Hi będzie kontynuował przetwarzanie kodu między nim a #endifdyrektywą. Następnym razem, gdy zawartość tego pliku zostanie zobaczona przez preprocesor, sprawdzenie FILE_Hbędzie fałszywe, więc natychmiast skanuje w dół do #endifi kontynuuje po nim. Zapobiega to błędom redefinicji.

Aby zająć się drugim problemem:

W programowaniu C ++ jako ogólną praktykę rozdzielamy programowanie na dwa typy plików. Jeden ma rozszerzenie .h i nazywamy go „plikiem nagłówkowym”. Zwykle dostarczają deklaracji funkcji, klas, struktur, zmiennych globalnych, typów definicji, wstępnie przetwarzających makr i definicji, itp. Zasadniczo, po prostu dostarczają informacji o kodzie. Następnie mamy rozszerzenie .cpp, które nazywamy „plikiem kodu”. Zapewni to definicje tych funkcji, składowych klas, dowolnych składowych struktury, które potrzebują definicji, zmiennych globalnych itp. Zatem plik .h deklaruje kod, a plik .cpp implementuje tę deklarację. Z tego powodu zazwyczaj podczas kompilacji kompilujemy każdy plik .cppplik do obiektu, a następnie połącz te obiekty (ponieważ prawie nigdy nie widać jednego pliku .cpp zawierającego inny plik .cpp ).

Sposób rozwiązywania tych zewnętrznych elementów jest zadaniem konsolidatora. Kiedy kompilator przetwarza main.cpp , pobiera deklaracje kodu w class.cpp , dołączając class.h . Musi tylko wiedzieć, jak wyglądają te funkcje lub zmienne (co daje deklaracja). Więc kompiluje twój plik main.cpp do jakiegoś pliku obiektowego (nazwij go main.obj ). Podobnie class.cpp jest kompilowany do pliku class.objplik. Aby utworzyć ostateczny plik wykonywalny, wywoływany jest konsolidator, który łączy te dwa pliki obiektowe ze sobą. W przypadku wszelkich nierozwiązanych zewnętrznych zmiennych lub funkcji kompilator umieści kod pośredniczący w miejscu, w którym następuje dostęp. Konsolidator następnie pobierze ten kod i wyszuka kod lub zmienną w innym wymienionym pliku obiektowym, a jeśli zostanie znaleziony, łączy kod z dwóch plików obiektowych w plik wyjściowy i zastępuje kod pośredniczący ostateczną lokalizacją funkcji lub zmienna. W ten sposób twój kod w main.cpp może wywoływać funkcje i używać zmiennych w class.cpp JEŚLI I TYLKO JEŚLI ZADEKLAROWANE SĄ W class.h .

Mam nadzieję, że to było pomocne.

Justin Summerlin
źródło
Próbowałem zrozumieć .h i .cpp przez ostatnie kilka dni. Ta odpowiedź pozwoliła mi zaoszczędzić czas i zainteresowanie nauką C ++. Dobrze napisane. Dzięki Justin!
Rajkumar R,
naprawdę świetnie wyjaśniłeś! Być może odpowiedź byłaby całkiem dobra, gdyby
dotyczyła
13

CLASS_HJest to straż ; służy do unikania wielokrotnego włączania tego samego pliku nagłówkowego (różnymi drogami) w tym samym pliku CPP (lub dokładniej w tej samej jednostce tłumaczeniowej ), co prowadziłoby do błędów w wielu definicjach.

Uwzględnij strażników nie są potrzebne w plikach CPP, ponieważ z definicji zawartość pliku CPP jest odczytywana tylko raz.

Wygląda na to, że zinterpretowałeś strażników włączania jako pełniących tę samą funkcję, co importinstrukcje w innych językach (takich jak Java); tak jednak nie jest. #includeSamo jest w przybliżeniu równowartość importw innych językach.

Martin B.
źródło
2
„w tym samym pliku CPP” powinno brzmieć „w tej samej jednostce tłumaczeniowej”.
dreamlax
@dreamlax: Słuszna uwaga - to właśnie zamierzałem napisać, ale potem doszedłem do wniosku, że ktoś, kto nie rozumie włączania strażników, będzie zdezorientowany terminem „jednostka tłumacząca”. Zmienię odpowiedź, dodając „jednostkę tłumaczeniową” w nawiasach - to powinno być najlepsze z obu światów.
Martin B
6

Tak nie jest - przynajmniej na etapie kompilacji.

Tłumaczenie programu w języku C ++ z kodu źródłowego na kod maszynowy odbywa się w trzech fazach:

  1. Przetwarzanie wstępne - Preprocesor analizuje cały kod źródłowy w poszukiwaniu wierszy zaczynających się od znaku # i wykonuje dyrektywy. W twoim przypadku zawartość twojego pliku class.hjest wstawiana w miejsce wiersza #include "class.h. Ponieważ możesz znajdować się w pliku nagłówkowym w kilku miejscach, #ifndefklauzule pozwalają uniknąć podwójnych błędów deklaracji, ponieważ dyrektywa preprocesora jest niezdefiniowana tylko przy pierwszym dołączeniu pliku nagłówkowego.
  2. Kompilacja - Kompilator tłumaczy teraz wszystkie wstępnie przetworzone pliki kodu źródłowego na pliki obiektów binarnych.
  3. Łączenie - konsolidator łączy (stąd nazwa) razem pliki obiektowe. Odniesienie do twojej klasy lub jednej z jej metod (które powinny być zadeklarowane w class.h i zdefiniowane w class.cpp) jest tłumaczone na odpowiednie przesunięcie w jednym z plików obiektowych. Piszę „jeden z twoich plików obiektowych”, ponieważ twoja klasa nie musi być zdefiniowana w pliku o nazwie class.cpp, może znajdować się w bibliotece, która jest połączona z twoim projektem.

Podsumowując, deklaracje mogą być współużytkowane przez plik nagłówkowy, podczas gdy mapowanie deklaracji do definicji jest wykonywane przez konsolidator.

sum1stolemyname
źródło
4

Na tym polega różnica między deklaracją a definicją. Pliki nagłówkowe zwykle zawierają tylko deklarację, a plik źródłowy zawiera definicję.

Aby czegoś użyć, wystarczy znać deklarację, a nie definicję. Tylko konsolidator musi znać definicję.

Dlatego właśnie umieścisz plik nagłówkowy w jednym lub kilku plikach źródłowych, ale nie umieścisz pliku źródłowego w innym.

Ty też masz na myśli #includei nie importujesz.

Brian R. Bondy
źródło
3

Odbywa się to w przypadku plików nagłówkowych, dzięki czemu zawartość pojawia się tylko raz w każdym wstępnie przetworzonym pliku źródłowym, nawet jeśli jest dołączana więcej niż raz (zwykle dlatego, że jest zawarta w innych plikach nagłówkowych). Gdy jest dołączany po raz pierwszy, symbol CLASS_H(znany jako ochrona włączania ) nie został jeszcze zdefiniowany, więc uwzględniana jest cała zawartość pliku. W ten sposób definiuje symbol, więc jeśli zostanie dołączony ponownie, zawartość pliku (wewnątrz bloku #ifndef/ #endif) jest pomijana.

Nie ma potrzeby tego robić dla samego pliku źródłowego, ponieważ (normalnie) nie jest dołączony do żadnych innych plików.

Ostatnie pytanie class.hpowinno zawierać definicję klasy i deklaracje wszystkich jej członków, powiązanych funkcji i cokolwiek innego, tak aby każdy plik, który ją zawiera, zawierał wystarczającą ilość informacji do użycia klasy. Implementacje funkcji mogą znajdować się w oddzielnym pliku źródłowym; potrzebujesz tylko deklaracji, aby je wywołać.

Mike Seymour
źródło
2

main.cpp nie musi wiedzieć, co znajduje się w class.cpp . Musi tylko znać deklaracje funkcji / klas, których będzie używać, a te deklaracje znajdują się w class.h .

Linker łączy miejsca, w których używane są funkcje / klasy zadeklarowane w class.h, a ich implementacje w class.cpp

Igor Oks
źródło
1

.cpppliki nie są dołączane (używane #include) do innych plików. Dlatego nie muszą obejmować ochrony. Main.cppbędzie znać nazwy i sygnatury klasy, w której zaimplementowałeś, class.cpptylko dlatego, że określiłeś to wszystko w class.h- taki jest cel pliku nagłówkowego. (To do Ciebie należy upewnienie się, że class.hdokładnie opisuje kod, w którym zaimplementujesz class.cpp.) Kod wykonywalny class.cppzostanie udostępniony kodowi wykonywalnemu main.cppdzięki wysiłkom konsolidatora.

Kate Gregory
źródło
1

Generalnie oczekuje się, że moduły kodu, takie jak .cpppliki, są kompilowane raz i łączone w wielu projektach, aby uniknąć niepotrzebnego powtarzania kompilacji logiki. Na przykład g++ -o class.cpputworzy, class.októre możesz następnie połączyć z wielu projektów do using g++ main.cpp class.o.

Moglibyśmy użyć #includejako naszego linkera, jak zdajesz się sugerować, ale byłoby to po prostu głupie, gdybyśmy wiedzieli, jak poprawnie połączyć się za pomocą naszego kompilatora z mniejszą liczbą naciśnięć klawiszy i mniej marnotrawnym powtarzaniem kompilacji, zamiast naszego kodu z większą liczbą naciśnięć klawiszy i bardziej marnotrawnym powtórzenie kompilacji ...

Jednak pliki nagłówkowe nadal muszą być uwzględnione w każdym z wielu projektów, ponieważ zapewnia to interfejs dla każdego modułu. Bez tych nagłówków kompilator nie wiedziałby o żadnym z symboli wprowadzonych przez .opliki.

Ważne jest, aby zdać sobie sprawę, że to pliki nagłówkowe wprowadzają definicje symboli dla tych modułów; gdy zostanie to zrealizowane, ma sens, że wielokrotne wtrącenia mogą spowodować przedefiniowanie symboli (co powoduje błędy), więc używamy osłon włączających, aby zapobiec takim redefinicjom.

autystyczny
źródło
0

jego ponieważ pliki nagłówkowe definiują, co zawiera klasa (składowe, struktury danych), a pliki cpp ją implementują.

I oczywiście głównym powodem tego jest to, że jeden plik .h można dołączyć wielokrotnie do innych plików .h, ale spowodowałoby to wiele definicji klasy, co jest nieprawidłowe.

Quonux
źródło