Problem z duplikacją definicji
W lekcji 2.7 -- Deklaracje przesyłania dalej i definicje zauważyliśmy, że identyfikator zmiennej lub funkcji może mieć tylko jedną definicję (regułę jednej definicji). Zatem program, który definiuje identyfikator zmiennej więcej niż raz, spowoduje błąd kompilacji:
int main()
{
int x; // this is a definition for variable x
int x; // compile error: duplicate definition
return 0;
}Podobnie programy, które definiują funkcję więcej niż raz, również spowodują błąd kompilacji:
#include <iostream>
int foo() // this is a definition for function foo
{
return 5;
}
int foo() // compile error: duplicate definition
{
return 5;
}
int main()
{
std::cout << foo();
return 0;
}Chociaż te programy można łatwo naprawić (usunąć zduplikowaną definicję), za pomocą plików nagłówkowych, całkiem łatwo jest skończyć w sytuacji, gdy definicja w pliku nagłówkowym zostanie dołączona więcej niż raz. Może się to zdarzyć, gdy plik nagłówkowy #zawiera inny plik nagłówkowy (co jest powszechne).
Nota autora
W nadchodzących przykładach zdefiniujemy niektóre funkcje w plikach nagłówkowych. Generalnie nie powinieneś tego robić.
Robimy to tutaj, ponieważ jest to najskuteczniejszy sposób zademonstrowania niektórych koncepcji przy użyciu funkcjonalności, które już omówiliśmy.
Rozważ następujący przykład akademicki:
square.h:
int getSquareSides()
{
return 4;
}wave.h:
#include "square.h"main.cpp:
#include "square.h"
#include "wave.h"
int main()
{
return 0;
}Ten pozornie niewinnie wyglądający program nie zostanie skompilowany! Oto, co się dzieje. Najpierw main.cpp #includes square.h, która kopiuje definicję funkcji getSquareSides do main.cpp. Następnie main.cpp #includes wave.h, która #zawiera square.h samą. Spowoduje to skopiowanie zawartości square.h (w tym definicję funkcji getSquareSides) do wave.h, która następnie zostanie skopiowana do main.cpp.
Zatem po rozwiązaniu wszystkich problemów #includes, main.cpp wygląda tak:
int getSquareSides() // from square.h
{
return 4;
}
int getSquareSides() // from wave.h (via square.h)
{
return 4;
}
int main()
{
return 0;
}Zduplikowane definicje i błąd kompilacji. Każdy plik z osobna jest w porządku. Ponieważ jednak main.cpp kończy się #włączeniem zawartości square.h dwa razy, napotkaliśmy problemy. Jeśli wave.h potrzebuje getSquareSides(), I main.cpp potrzebuje obu wave.h i square.h, jak rozwiążesz ten problem?
Ochronniki nagłówków
Dobra wiadomość jest taka, że możemy uniknąć powyższego problemu poprzez mechanizm o nazwie osłona nagłówka (zwany także włącz osłonę). Strażnicy nagłówka to dyrektywy kompilacji warunkowej, które przyjmują następującą formę:
#ifndef SOME_UNIQUE_NAME_HERE
#define SOME_UNIQUE_NAME_HERE
// your declarations (and certain types of definitions) here
#endifGdy ten nagłówek jest #included, preprocesor sprawdza, czy SOME_UNIQUE_NAME_HERE został wcześniej zdefiniowany w tej jednostce tłumaczenia. Jeśli po raz pierwszy dołączamy nagłówek, SOME_UNIQUE_NAME_HERE nie zostanie zdefiniowany. W związku z tym #definiuje SOME_UNIQUE_NAME_HERE i zawiera zawartość pliku. Jeśli nagłówek zostanie ponownie dołączony do tego samego pliku, SOME_UNIQUE_NAME_HERE będzie już zdefiniowany przy pierwszym dołączeniu zawartości nagłówka, a zawartość nagłówka zostanie zignorowana (dzięki #ifndef).
Wszystkie Twoje pliki nagłówkowe powinny mieć włączone zabezpieczenia nagłówka je. SOME_UNIQUE_NAME_HERE może być dowolną nazwą, ale zgodnie z konwencją jest ustawiana na pełną nazwę pliku nagłówkowego, wpisaną wielkimi literami, używając podkreśleń dla spacji i znaków interpunkcyjnych. Na przykład square.h będzie miał osłonę nagłówka:
square.h:
#ifndef SQUARE_H
#define SQUARE_H
int getSquareSides()
{
return 4;
}
#endifNawet standardowe nagłówki bibliotek używają osłon nagłówka. Gdybyś rzucił okiem na plik nagłówkowy iostream z Visual Studio, zobaczyłbyś:
#ifndef _IOSTREAM_
#define _IOSTREAM_
// content here
#endifDla zaawansowanych czytelników
W dużych programach możliwe jest posiadanie dwóch oddzielnych plików nagłówkowych (zawartych w różnych katalogach), które ostatecznie będą miały tę samą nazwę pliku (np. katalogA\config.h i katalogB\config.h). Jeśli dla osłony dołączanej zostanie użyta tylko nazwa pliku (np. CONFIG_H), te dwa pliki mogą ostatecznie używać tej samej nazwy osłony. Jeśli tak się stanie, żaden plik zawierający (bezpośrednio lub pośrednio) oba pliki config.h nie otrzyma zawartości pliku dołączanego, który ma zostać dołączony jako drugi. Prawdopodobnie spowoduje to błąd kompilacji.
Ze względu na możliwość wystąpienia konfliktów nazw strażników, wielu programistów zaleca używanie bardziej złożonych/unikalnych nazw w nagłówkach. Niektóre dobre sugestie to konwencja nazewnictwa PROJECT_PATH_FILE_H, FILE_LARGE-RANDOM-NUMBER_H lub FILE_CREATION-DATE_H.
Aktualizacja naszego poprzedniego przykładu za pomocą osłon nagłówka
Wróćmy do przykładu square.h , używając square.h z osłonami nadproża. Aby zachować dobrą formę, dodamy także osłony nagłówka do wave.h.
square.h
#ifndef SQUARE_H
#define SQUARE_H
int getSquareSides()
{
return 4;
}
#endifwave.h:
#ifndef WAVE_H
#define WAVE_H
#include "square.h"
#endifmain.cpp:
#include "square.h"
#include "wave.h"
int main()
{
return 0;
}Gdy preprocesor rozwiąże wszystkie dyrektywy #include, program będzie wyglądał następująco:
main.cpp:
// Square.h included from main.cpp
#ifndef SQUARE_H // square.h included from main.cpp
#define SQUARE_H // SQUARE_H gets defined here
// and all this content gets included
int getSquareSides()
{
return 4;
}
#endif // SQUARE_H
#ifndef WAVE_H // wave.h included from main.cpp
#define WAVE_H
#ifndef SQUARE_H // square.h included from wave.h, SQUARE_H is already defined from above
#define SQUARE_H // so none of this content gets included
int getSquareSides()
{
return 4;
}
#endif // SQUARE_H
#endif // WAVE_H
int main()
{
return 0;
}Przyjrzyjmy się, jak to ocenia.
Po pierwsze, preprocesor oceny #ifndef SQUARE_H. SQUARE_H nie został jeszcze zdefiniowany, więc kod od #ifndef do następnego #endif jest uwzględniany do kompilacji. Ten kod definiuje SQUARE_H i zawiera definicję getSquareSides .
Później oceniany jest następny #ifndef SQUARE_H . Tym razem SQUARE_H jest zdefiniowany (ponieważ został zdefiniowany powyżej), więc kod od #ifndef do następnego #endif jest wykluczony z kompilacji.
Ochronnicy nagłówka zapobiegają duplikacjom włączenia, ponieważ przy pierwszym napotkaniu strażnika makro strażnika nie jest zdefiniowane, więc strzeżona treść jest uwzględniana. Powyżej tego punktu zdefiniowane jest makro ochronne, więc wszelkie kolejne kopie chronionej treści są wykluczane.
Osłony nagłówka nie zapobiegają jednorazowemu włączeniu nagłówka do różnych plików kodu
Zauważ, że celem zabezpieczeń nagłówka jest zapobieganie otrzymywaniu przez plik kodu więcej niż jednej kopii chronionego nagłówka. Z założenia zabezpieczenia nagłówka nie zapobiegają (jednorazowemu) włączeniu danego pliku nagłówkowego do oddzielnych plików z kodem. Może to również powodować nieoczekiwane problemy. Rozważ:
square.h:
#ifndef SQUARE_H
#define SQUARE_H
int getSquareSides()
{
return 4;
}
int getSquarePerimeter(int sideLength); // forward declaration for getSquarePerimeter
#endifsquare.cpp:
#include "square.h" // square.h is included once here
int getSquarePerimeter(int sideLength)
{
return sideLength * getSquareSides();
}main.cpp:
#include "square.h" // square.h is also included once here
#include <iostream>
int main()
{
std::cout << "a square has " << getSquareSides() << " sides\n";
std::cout << "a square of length 5 has perimeter length " << getSquarePerimeter(5) << '\n';
return 0;
}Zauważ to square.h jest zawarty w obu main.cpp i square.cpp. Oznacza to, że zawartość square.h zostanie uwzględniona raz w square.cpp i raz w main.cpp.
Przyjrzyjmy się, dlaczego tak się dzieje bardziej szczegółowo. Gdy square.h jest zawarty z square.cpp, SQUARE_H jest zdefiniowany do końca square.cpp. Ta definicja zapobiega włączeniu square.h do square.cpp po raz drugi (co jest celem strażników nagłówka). Jednak po zakończeniu square.cpp SQUARE_H nie jest już uważany za zdefiniowany. Oznacza to, że gdy preprocesor działa na main.cpp, SQUARE_H nie jest początkowo zdefiniowany w main.cpp.
Końcowym rezultatem jest to, że oba square.cpp i main.cpp otrzymują kopię definicji getSquareSides. Ten program się skompiluje, ale linker będzie narzekał, że Twój program ma wiele definicji identyfikatora getSquareSides!
Najlepszym sposobem obejścia tego problemu jest po prostu umieszczenie definicji funkcji w jednym z plików .cpp, tak aby nagłówek zawierał tylko przekierowanie deklaracja:
square.h:
#ifndef SQUARE_H
#define SQUARE_H
int getSquareSides(); // forward declaration for getSquareSides
int getSquarePerimeter(int sideLength); // forward declaration for getSquarePerimeter
#endifsquare.cpp:
#include "square.h"
int getSquareSides() // actual definition for getSquareSides
{
return 4;
}
int getSquarePerimeter(int sideLength)
{
return sideLength * getSquareSides();
}main.cpp:
#include "square.h" // square.h is also included once here
#include <iostream>
int main()
{
std::cout << "a square has " << getSquareSides() << " sides\n";
std::cout << "a square of length 5 has perimeter length " << getSquarePerimeter(5) << '\n';
return 0;
}Teraz, gdy program zostanie skompilowany, funkcja getSquareSides będzie miała tylko jedną definicję (przez square.cpp), więc linker będzie zadowolony. Plik main.cpp jest w stanie wywołać tę funkcję (mimo że znajduje się w square.cpp), ponieważ zawiera square.h, który zawiera deklarację forward dla funkcji (linker połączy wywołanie getSquareSides z main.cpp z definicją of getSquareSides w square.cpp).
Czy nie możemy po prostu unikać definicji w plikach nagłówkowych?
Ogólnie mówiliśmy, żebyś nie umieszczał definicji funkcji w nagłówkach, więc możesz się zastanawiać, dlaczego powinieneś dołączyć osłony nagłówka, jeśli chronią cię przed czymś, czego nie powinieneś robić.
Jest sporo przypadków. w przyszłości pokażemy Ci, gdzie konieczne jest umieszczenie definicji niebędących funkcjami w pliku nagłówkowym. Na przykład C++ umożliwia tworzenie własnych typów. Te niestandardowe typy są zwykle definiowane w plikach nagłówkowych, więc definicje typów można propagować do plików z kodem, które muszą ich używać. Bez zabezpieczenia nagłówka plik z kodem może zawierać wiele (identycznych) kopii danej definicji typu, co kompilator oznaczy jako błąd.
A mimo to. na tym etapie serii tutoriali posiadanie osłon nagłówków nie jest absolutnie konieczne, wypracowujemy już dobre nawyki, więc nie musisz się później oduczać złych nawyków.
#pragma Once
Współczesne kompilatory obsługują prostszą, alternatywną formę osłon nagłówków przy użyciu #pragma dyrektywy preprocesora:
#pragma once
// your code here#pragma once służą temu samemu celowi, co osłony nagłówków: aby uniknąć wielokrotnego dołączania pliku nagłówkowego. W przypadku tradycyjnych zabezpieczeń nagłówka programista jest odpowiedzialny za ochronę nagłówka (za pomocą dyrektyw preprocesora #ifndef, #define, I #endif). W przypadku #pragma once żądamy, aby kompilator strzegł nagłówka. Sposób, w jaki dokładnie to robi, jest kwestią specyficzną dla implementacji.
Dla zaawansowanych czytelników
Istnieje jeden znany przypadek, w którym #pragma once zazwyczaj kończy się niepowodzeniem jest kopiowany tak, że istnieje w wielu miejscach w systemie plików, jeśli w jakiś sposób obie kopie nagłówka zostaną uwzględnione, zabezpieczenia nagłówka pomyślnie usuną duplikaty identycznych nagłówków, ale #pragma once nie zrobią tego (ponieważ kompilator nie zorientuje się, że w rzeczywistości mają identyczną treść).
W przypadku większości projektów #pragma once działa dobrze i wielu programistów woli to teraz, ponieważ jest to łatwiejsze i mniej podatne na błędy również automatycznie dołącza #pragma once na górze nowego pliku nagłówkowego wygenerowanego przez IDE.
Ostrzeżenie
Klasa #pragma Dyrektywa została zaprojektowana tak, aby osoby wdrażające kompilatory mogły jej używać do dowolnych celów. W związku z tym to, które pragmy są obsługiwane i jakie mają znaczenie, zależy całkowicie od implementacji. Z wyjątkiem #pragma once, nie należy oczekiwać, że pragma działająca na jednym kompilatorze będzie obsługiwana przez jeden kompilator. inny.
Ponieważ #pragma once nie jest zdefiniowany w standardzie C++, możliwe jest, że niektóre kompilatory mogą go nie zaimplementować. Z tego powodu niektóre firmy programistyczne (takie jak Google) zalecają używanie tradycyjnych osłon nagłówków. W tej serii tutoriali będziemy preferować osłony nagłówków, ponieważ są one najbardziej konwencjonalnym sposobem ochrony nagłówków. Jednakże obsługa #pragma once jest w tym momencie dość wszechobecna, a jeśli chcesz. zamiast tego użyj #pragma once co jest ogólnie akceptowane we współczesnym C++.
Streszczenie
Ochronniki nagłówka mają na celu zapewnienie, że zawartość danego pliku nagłówkowego nie zostanie skopiowana więcej niż raz do jednego pliku, aby zapobiec duplikowaniu definicji.
Duplikaty deklaracje są w porządku - ale nawet jeśli plik nagłówkowy składa się ze wszystkich deklaracji (brak definicji) nadal najlepszą praktyką jest uwzględnianie osłon nagłówka.
Zauważ, że osłony nagłówka nie zapobiegają jednorazowemu skopiowaniu zawartości pliku nagłówkowego do oddzielnych plików projektu. Jest to dobra rzecz, ponieważ często musimy odwoływać się do zawartości danego nagłówka z różnych plików projektu.
Czas quizu
Pytanie nr 1
Dodaj osłony nagłówka do tego nagłówka. plik:
add.h:
int add(int x, int y);
