Załóżmy, że po raz pierwszy jedziesz do domu znajomego, a podany ci adres to 245 Front Street w Mill City. Po dotarciu do Mill City wyjmujesz mapę i odkrywasz, że Mill City tak naprawdę ma dwie różne ulice Front Street po drugiej stronie miasta! Do którego byś poszedł? O ile nie istniała żadna dodatkowa wskazówka, która pomogłaby ci podjąć decyzję (np. pamiętasz, że dom twojego przyjaciela znajduje się w pobliżu rzeki), musiałbyś zadzwonić do przyjaciela i poprosić o więcej informacji. Ponieważ byłoby to mylące i nieefektywne (szczególnie dla Twojego operatora pocztowego), w większości krajów wszystkie nazwy ulic i adresy domów w mieście muszą być unikalne.
Podobnie C++ wymaga, aby wszystkie identyfikatory były jednoznaczne. Jeśli dwa identyczne identyfikatory zostaną wprowadzone do tego samego programu w taki sposób, że kompilator lub linker nie będzie w stanie ich rozróżnić, kompilator lub linker zgłosi błąd. Ten błąd jest ogólnie określany jako kolizja nazewnictwa (lub konflikt nazewnictwa).
Jeśli kolidujące identyfikatory zostaną wprowadzone do tego samego pliku, wynikiem będzie błąd kompilatora. Jeśli kolidujące identyfikatory zostaną wprowadzone do oddzielnych plików należących do tego samego programu, efektem będzie błąd linkera.
Przykład kolizji nazewnictwa
a.cpp:
#include <iostream>
void myFcn(int x)
{
std::cout << x;
}main.cpp:
#include <iostream>
void myFcn(int x)
{
std::cout << 2 * x;
}
int main()
{
return 0;
}Gdy kompilator kompiluje ten program, skompiluje a.cpp i main.cpp niezależnie i każdy plik skompiluje się bez problemów.
Jednak gdy linker się uruchomi, połączy się wszystkie definicje w a.cpp i main.cpp razem i odkryj sprzeczne definicje funkcji myFcn(). Linker zostanie wówczas przerwany z powodu błędu. Należy pamiętać, że ten błąd występuje, mimo że myFcn() nie jest nigdy wywoływany!
Większość kolizji nazewnictwa występuje w dwóch przypadkach:
- Dwie (lub więcej) identycznie nazwane funkcje (lub zmienne globalne) są wprowadzane do oddzielnych plików należących do tego samego programu. Spowoduje to błąd linkera, jak pokazano powyżej.
- Dwie (lub więcej) identycznie nazwane funkcje (lub zmienne globalne) są wprowadzone do tego samego pliku. Spowoduje to błąd kompilatora.
W miarę jak programy stają się większe i używają większej liczby identyfikatorów, ryzyko wystąpienia kolizji nazewnictwa znacznie wzrasta. Dobra wiadomość jest taka, że C++ zapewnia wiele mechanizmów pozwalających uniknąć kolizji nazewnictwa. Jednym z takich mechanizmów jest zasięg lokalny, który zapobiega konfliktom między zmiennymi lokalnymi zdefiniowanymi wewnątrz funkcji. Ale zasięg lokalny nie działa w przypadku nazw funkcji. Jak więc zapobiec konfliktowi nazw funkcji?
Regiony zasięgu
Wracając na chwilę do naszej analogii adresowej, posiadanie dwóch Front Street było problematyczne tylko dlatego, że te ulice istniały w tym samym mieście. Z drugiej strony, jeśli trzeba było dostarczać pocztę pod dwa adresy, jeden pod adresem 245 Front Street w Mill City i drugi pod adres 245 Front Street w Jonesville, nie byłoby wątpliwości, dokąd się udać. Innymi słowy, miasta zapewniają grupowanie, które pozwala nam ujednoznacznić adresy, które w przeciwnym razie mogłyby być ze sobą sprzeczne.
A region zasięgu to obszar kodu źródłowego, w którym wszystkie zadeklarowane identyfikatory są uważane za odrębne od nazw zadeklarowanych w innych zakresach (podobnie jak miasta w naszej analogii). Dwa identyfikatory o tej samej nazwie można zadeklarować w oddzielnych regionach zasięgu, nie powodując konfliktu nazewnictwa. Jednakże w obrębie danego obszaru zasięgu wszystkie identyfikatory muszą być unikalne, w przeciwnym razie nastąpi kolizja nazewnictwa.
Treść funkcji jest jednym z przykładów obszaru zasięgu. Dwa identyfikatory o identycznych nazwach można bez problemu zdefiniować w oddzielnych funkcjach — ponieważ każda funkcja zapewnia oddzielny obszar zakresu, nie ma kolizji. Jeśli jednak spróbujesz zdefiniować dwa identyfikatory o identycznych nazwach w tej samej funkcji, spowoduje to kolizję nazewnictwa i kompilator będzie narzekał.
Przestrzenie nazw
A przestrzeń nazw zapewniają inny typ obszaru zasięgu (zwany zakresem przestrzeni nazw), który pozwala na deklarowanie lub definiowanie znajdujących się w nim nazw w celu ujednoznacznienia. Nazwy zadeklarowane w przestrzeni nazw są izolowane od nazw zadeklarowanych w innych zakresach, dzięki czemu takie nazwy mogą istnieć bez konfliktów.
Kluczowa informacja
Nazwa zadeklarowana w obszarze zasięgu (takim jak przestrzeń nazw) różni się od każdej identycznej nazwy zadeklarowanej w innym zakresie.
Na przykład dwie funkcje z identycznymi deklaracjami można zdefiniować w różnych przestrzeniach nazw i nie wystąpi żadna kolizja nazw ani niejednoznaczność.
Przestrzenie nazw mogą zawierać tylko deklaracje i definicje (np. zmienne i funkcje). Instrukcje wykonywalne nie są dozwolone, chyba że stanowią część definicji (np. wewnątrz funkcji).
Kluczowa informacja
Przestrzeń nazw może zawierać tylko deklaracje i definicje. Wykonywalne instrukcje są dozwolone jedynie jako część definicji (np. funkcji).
Przestrzenie nazw są często używane do grupowania powiązanych identyfikatorów w dużym projekcie, aby zapewnić, że nie kolidują one przypadkowo z innymi identyfikatorami. Na przykład, jeśli umieścisz wszystkie funkcje matematyczne w przestrzeni nazw o nazwie math, wówczas funkcje matematyczne nie będą kolidować z funkcjami o identycznych nazwach poza math .
O tym, jak tworzyć własne przestrzenie nazw, porozmawiamy na przyszłej lekcji.
Globalna przestrzeń nazw
W C++ za każdą nazwę, która nie jest zdefiniowana w klasie, funkcji lub przestrzeni nazw, uważa się część niejawnie zdefiniowanej przestrzeni nazw zwanej globalną przestrzenią nazw (czasami nazywaną także zasięgiem globalnym).
W przykładzie na górze lekcji funkcje main() i obie wersje myFcn() są zdefiniowane w globalnej przestrzeni nazw. Kolizja nazewnictwa napotkana w przykładzie ma miejsce, ponieważ obie wersje z myFcn() wyląduje w globalnej przestrzeni nazw, co łamie zasadę, że wszystkie nazwy w obszarze zasięgu muszą być unikalne.
Globalną przestrzeń nazw omawiamy bardziej szczegółowo na lekcji 7.4 — Wprowadzenie do zmiennych globalnych.
Na razie powinieneś wiedzieć o dwóch rzeczach:
- Identyfikatory zadeklarowane w zasięgu globalnym obowiązują od momentu deklaracji do końca pliku.
- Chociaż Zmienne można definiować w globalnej przestrzeni nazw, generalnie należy tego unikać (omówimy, dlaczego w lekcji 7.8 — Dlaczego (nie stałe) zmienne globalne są złe).
Na przykład:
#include <iostream> // imports the declaration of std::cout into the global scope
// All of the following statements are part of the global namespace
void foo(); // okay: function forward declaration
int x; // compiles but strongly discouraged: non-const global variable definition (without initializer)
int y { 5 }; // compiles but strongly discouraged: non-const global variable definition (with initializer)
x = 5; // compile error: executable statements are not allowed in namespaces
int main() // okay: function definition
{
return 0;
}
void goo(); // okay: A function forward declarationPrzestrzeń nazw std
Gdy pierwotnie projektowano C++, wszystkie identyfikatory w standardowej bibliotece C++ (w tym std::cin i std::cout) były dostępne do użycia bez przedrostka std:: (były częścią globalnej przestrzeni nazw). Oznaczało to jednak, że dowolny identyfikator w bibliotece standardowej może potencjalnie kolidować z dowolną nazwą wybraną dla własnych identyfikatorów (również zdefiniowaną w globalnej przestrzeni nazw). Kod, który kiedyś działał, może nagle wywołać konflikt nazewnictwa, gdy dołączysz inną część standardowej biblioteki. Lub, co gorsza, kod skompilowany w jednej wersji C++ może nie skompilować się w następnej wersji C++, ponieważ nowe identyfikatory wprowadzone do standardowej biblioteki mogą powodować konflikt nazewnictwa z już napisanym kodem. Dlatego C++ przeniósł całą funkcjonalność ze standardowej biblioteki do przestrzeni nazw o nazwie std (skrót od „standard”).
Okazuje się, że std::coutnazwa nie jest tak naprawdę std::cout. W rzeczywistości jest to po prostu cout, I std nazwa przestrzeni nazw, której częścią jest identyfikator cout Ponieważ cout jest zdefiniowany w std przestrzeń nazw, nazwa cout nie będzie kolidować z żadnymi obiektami lub funkcjami o nazwie cout które utworzymy poza std przestrzenią nazw (np. w globalnej przestrzeni nazw).
Kluczowa informacja
Kiedy używasz identyfikatora zdefiniowanego w nieglobalnej przestrzeni nazw (np. przestrzeni nazw std ), musisz poinformować kompilator, że identyfikator żyje wewnątrz przestrzeni nazw.”
Istnieje kilka różnych sposobów, aby to zrobić.
Jawny kwalifikator przestrzeni nazw std::
Najprostszym sposobem poinformowania kompilatora, że chcemy używać cout z deklaracji i definicji funkcji std przestrzeni nazw jest jawne użycie przedrostka std:: . Na przykład:
#include <iostream>
int main()
{
std::cout << "Hello world!"; // when we say cout, we mean the cout defined in the std namespace
return 0;
}Symbol :: to operator wywoływany operator rozpoznawania zakresu. Identyfikator po lewej stronie symbolu :: identyfikuje przestrzeń nazw, w której zawarta jest nazwa po prawej stronie symbolu :: . Jeśli nie zostanie podany żaden identyfikator po lewej stronie symbolu :: , zakładana jest globalna przestrzeń nazw.
Więc kiedy będziemy powiedz std::cout mówimy „ cout zadeklarowany w przestrzeni nazw std“.
To najbezpieczniejszy sposób użycia cout, ponieważ nie ma dwuznaczności co do tego, cout odwołujemy się (tego w std przestrzeni nazw).
Najlepsza praktyka
Użyj jawnej przestrzeni nazw prefiksy umożliwiające dostęp do identyfikatorów zdefiniowanych w przestrzeni nazw.
Gdy identyfikator zawiera przedrostek przestrzeni nazw, identyfikator jest nazywany a kwalifikowana nazwa.
Using namespace std (i dlaczego tego unikać)
Innym sposobem uzyskania dostępu do identyfikatorów w przestrzeni nazw jest użycie instrukcji using-dyrektywy. Oto nasz oryginalny program „Hello world” z dyrektywą using:
#include <iostream>
using namespace std; // this is a using-directive that allows us to access names in the std namespace with no namespace prefix
int main()
{
cout << "Hello world!";
return 0;
}A za pomocą dyrektywy umożliwia nam dostęp do nazw w przestrzeni nazw bez użycia przedrostka przestrzeni nazw. Zatem w powyższym przykładzie, gdy kompilator ustala, jaki identyfikator cout jest, dopasuje go do std::cout, który ze względu na dyrektywę using jest dostępny jako cout.
Wiele tekstów, samouczków, a nawet niektóre IDE zaleca lub używa dyrektywy using na górze programu. Jednakże użycie tego sposobu jest złą praktyką i zdecydowanie odradzane.
Rozważ następujący program:
#include <iostream> // imports the declaration of std::cout into the global scope
using namespace std; // makes std::cout accessible as "cout"
int cout() // defines our own "cout" function in the global namespace
{
return 5;
}
int main()
{
cout << "Hello, world!"; // Compile error! Which cout do we want here? The one in the std namespace or the one we defined above?
return 0;
}Powyższy program nie kompiluje się, ponieważ kompilator nie może teraz stwierdzić, czy chcemy cout funkcji, którą zdefiniowaliśmy, czy też std::cout.
W przypadku użycia dyrektywy using w ten sposób, dowolnego definiowany przez nas identyfikator może kolidować z dowolnego identyfikatorem o identycznej nazwie w przestrzeń nazw std . Co gorsza, choć nazwa identyfikatora może dzisiaj nie powodować konfliktu, może powodować konflikt z nowymi identyfikatorami dodanymi do standardowej przestrzeni nazw w przyszłych wersjach językowych. O to właśnie chodziło, w pierwszej kolejności przenosząc wszystkie identyfikatory ze standardowej biblioteki do std przestrzeni nazw!
Ostrzeżenie
Unikaj używania dyrektyw (takich jak using namespace std;) na górze programu lub w plikach nagłówkowych. Naruszają one powód, dla którego w ogóle dodano przestrzenie nazw.
Powiązana treść
Więcej o deklaracjach użycia i dyrektywach użycia (oraz o tym, jak ich używać w sposób odpowiedzialny) mówimy w lekcji >7.13 -- Używanie deklaracji i dyrektyw.
Nawiasy klamrowe i kod wcięty
W C++ nawiasy klamrowe są często używane do wyznaczania obszaru zasięgu, który jest zagnieżdżony w innym obszarze zasięgu (nawiasy klamrowe są również używane w niektórych cele niezwiązane z zakresem, takie jak inicjalizacja listy). Na przykład funkcja zdefiniowana w obszarze zasięgu globalnego używa nawiasów klamrowych do oddzielenia obszaru zakresu funkcji od zasięgu globalnego.
W niektórych przypadkach identyfikatory zdefiniowane poza nawiasami klamrowymi mogą nadal należeć do zakresu określonego przez nawiasy klamrowe, a nie otaczającego zakresu — dobrym tego przykładem są parametry funkcji.
Na przykład:
#include <iostream> // imports the declaration of std::cout into the global scope
void foo(int x) // foo is defined in the global scope, x is defined within scope of foo()
{ // braces used to delineate nested scope region for function foo()
std::cout << x << '\n';
} // x goes out of scope here
int main()
{ // braces used to delineate nested scope region for function main()
foo(5);
int x { 6 }; // x is defined within the scope of main()
std::cout << x << '\n';
return 0;
} // x goes out of scope here
// foo and main (and std::cout) go out of scope here (the end of the file)Kod istniejący wewnątrz zagnieżdżonego obszaru zakresu jest konwencjonalnie wcięty o jeden poziom, zarówno dla czytelności, jak i dla ułatwienia wskazania, że istnieje w oddzielnym obszarze zasięgu.
Klasa #include oraz definicje funkcji dla foo() i main() istnieją w obszarze zasięgu globalnego, więc nie są wcięte. Instrukcje wewnątrz każdej funkcji istnieją w zagnieżdżonym obszarze zasięgu funkcji, więc są wcięte o jeden poziom.

