2.8 – Namnkollisioner och en introduktion till namnrymder

låt oss säga att du kör till en väns hus för första gången, och adressen till dig är 245 Front Street i Mill City. När du når Mill City drar du upp din karta, bara för att upptäcka att Mill City faktiskt har två olika främre gator över staden från varandra! Vilken skulle du gå till? Om det inte fanns några ytterligare ledtrådar som hjälper dig att bestämma (t. ex. du kommer ihåg att hans hus ligger nära floden) du måste ringa din vän och be om mer information. Eftersom detta skulle vara förvirrande och ineffektivt (särskilt för din brevbärare), i de flesta länder måste alla gatunamn och husadresser i en stad vara unika.

på samma sätt kräver C++ att alla identifierare inte är tvetydiga. Om två identiska identifierare införs i samma program på ett sätt som kompilatorn eller länkaren inte kan skilja dem åt, kommer kompilatorn eller länkaren att producera ett fel. Detta fel kallas vanligtvis en namnkollision (eller namnkonflikt).

ett exempel på en namnkollision

a.cpp:

1
2
3
4
5
6

#inkludera < iostream>
ogiltig myFcn (int x)
{
std:: cout << x;
}

main.cpp:

1
2
3
4
5
6
7
8
9
10
11

#inkludera < iostream>
ogiltig myFcn (int x)
{
std:: cout << 2 * x;
}
int main()
{
tillbaka 0;
}

när kompilatorn sammanställer detta program kommer det att kompilera a.cpp och main.CPP oberoende, och varje fil kommer att kompilera utan problem.

men när länken körs kommer den att länka alla definitioner i a.cpp och main.cpp tillsammans, och upptäcka motstridiga definitioner för funktion myFcn. Länkaren avbryter sedan med ett fel. Observera att detta fel uppstår även om myFcn aldrig kallas!

de flesta namnkollisioner förekommer i två fall:
1) två (eller flera) definitioner för en funktion (eller global variabel) introduceras i separata filer som sammanställs i samma program. Detta kommer att resultera i ett länkfel, som visas ovan.
2) två (eller flera) definitioner för en funktion (eller global variabel) införs i samma fil (ofta via en #include). Detta kommer att resultera i ett kompilatorfel.

när program blir större och använder fler identifierare ökar oddsen för en namnkollision avsevärt. Den goda nyheten är att C++ ger många mekanismer för att undvika namnkollisioner. Lokalt omfång, som håller lokala variabler definierade inuti funktioner från att strida mot varandra, är en sådan mekanism. Men lokalt räckvidd fungerar inte för funktionsnamn. Så hur håller vi funktionsnamn från att strida mot varandra?

vad är en namnrymd?

tillbaka till vår adressanalogi för ett ögonblick, att ha två främre gator var bara problematiskt eftersom dessa gator fanns i samma stad. Å andra sidan, om du var tvungen att leverera post till två adresser, en på 209 Front Street i Mill City och en annan adress på 417 Front Street i Jonesville, skulle det inte vara någon förvirring om vart du ska gå. På ett annat sätt tillhandahåller städer grupperingar som gör att vi kan skilja adresser som annars kan komma i konflikt med varandra. Namnrymder fungerar som städerna gör i denna analogi.

ett namnområde är en region som låter dig deklarera namn inuti den för disambiguation. Namnrymden ger ett omfång (kallat namnrymdsomfång) till de namn som deklareras inuti det-vilket helt enkelt betyder att alla namn som deklareras i namnrymden inte kommer att misstas för identiska namn i andra omfång.

Key insight

ett namn som deklarerats i ett namnområde kommer inte att misstas för ett identiskt namn som deklarerats i ett annat omfång.

inom ett namnområde måste alla namn vara unika, annars kommer en namnkollision att resultera.

namnområden används ofta för att gruppera relaterade identifierare i ett stort projekt för att säkerställa att de inte oavsiktligt kolliderar med andra identifierare. Om du till exempel placerar alla dina matematiska funktioner i ett namnområde som heter math, kommer dina matematiska funktioner inte att kollidera med identiskt namngivna funktioner utanför math-namnområdet.

vi pratar om hur du skapar dina egna namnområden i en framtida lektion.

det globala namnområdet

i C++ anses alla namn som inte definieras i en klass, funktion eller ett namnområde vara en del av ett implicit definierat namnområde som kallas det globala namnområdet (ibland även kallat det globala omfånget).

i exemplet högst upp i lektionen definieras funktioner main() och båda versionerna av myFcn() i det globala namnområdet. Namnkollisionen i exemplet inträffar eftersom båda versionerna av myFcn () hamnar i det globala namnområdet, vilket bryter mot regeln att alla namn i namnområdet måste vara unika.

STD-namnområdet

när C++ ursprungligen designades var alla identifierare i C++ – standardbiblioteket (inklusive std::cin och std::cout) tillgängliga att användas utan STD:: prefix (De var en del av det globala namnområdet). Detta innebar dock att alla identifierare i standardbiblioteket kunde komma i konflikt med alla namn du valde för dina egna identifierare (även definierade i det globala namnområdet). Kod som fungerade kan plötsligt ha en namnkonflikt när du #inkluderade en ny fil från standardbiblioteket. Eller värre, program som skulle kompilera under en version av C++ kanske inte kompileras under en framtida version av C++, eftersom nya identifierare som introduceras i standardbiblioteket kan ha en namnkonflikt med redan skriven kod. Så c++ flyttade all funktionalitet i standardbiblioteket till ett namnområde som heter ”std” (kort för standard).

det visar sig att std::Couts namn inte är riktigt std:: cout. Det är faktiskt bara cout, och std är namnet på namnområdet som identifier cout är en del av. Eftersom cout definieras i std-namnområdet kommer namnet cout inte att strida mot några objekt eller funktioner som heter cout som vi skapar i det globala namnområdet.

på samma sätt måste du , när du öppnar en identifierare som definieras i ett namnområde (t.ex. std::cout), berätta för kompilatorn att vi letar efter en identifierare definierad i namnområdet (std).

Key insight

när du använder en identifierare som är definierad i ett namnområde (t.ex. std-namnområdet) måste du berätta för kompilatorn att identifieraren finns i namnområdet.

det finns några olika sätt att göra detta.

Explicit namespace qualifier std::

det enklaste sättet att berätta för kompilatorn att vi vill använda cout från std-namnområdet är genom att uttryckligen använda STD:: prefixet. Till exempel:

1
2
3
4
5
6
7

#inkludera < iostream>
int main()
{
std:: cout << ” Hej världen!”; // när vi säger cout, menar vi cout definieras i std namnrymden
retur 0;
}

symbolen:: är en operator som kallas scope resolution operator. Identifieraren till vänster om symbolen :: identifierar namnrymden som namnet till höger om symbolen :: finns i. Om ingen identifierare till vänster om:: – symbolen tillhandahålls antas det globala namnområdet.

så när vi säger std:: cout, säger vi ”cout som bor i namespace std”.

Detta är det säkraste sättet att använda cout, eftersom det inte finns någon tvetydighet om vilken cout vi refererar till (den i std-namnområdet).

bästa praxis

använd explicita namnrymdprefix för att komma åt identifierare definierade i ett namnrymd.

använda namespace std (och varför för att undvika det)

ett annat sätt att komma åt identifierare i ett namnområde är att använda ett using directive-uttalande. Här är vårt ursprungliga ”Hello world” – program med ett användardirektiv:

1
2
3
4
5
6
7
8
9

#inkludera < iostream>
using namespace std; / / detta är ett användardirektiv som berättar för kompilatorn att kontrollera std-namnområdet när man löser identifierare utan prefix
int main()
{
cout << ” Hej världen!”; // cout har inget prefix, så kompilatorn kommer att kontrollera om cout definieras lokalt eller i namnrymden std
retur 0;
}

a använda direktivet talar om för kompilatorn att kontrollera en viss namnrymd när man försöker lösa en identifierare som inte har något namnrymdprefix. Så i ovanstående exempel, när kompilatorn går för att bestämma vilken identifierare cout är, kommer den att kontrollera både lokalt (där den är odefinierad) och i std-namnområdet (där den matchar std::cout).

många texter, tutorials, och även vissa kompilatorer rekommendera eller använda en användning direktiv högst upp i programmet. Men används på detta sätt är detta en dålig övning och mycket avskräckt.

Tänk på följande program:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

#inkludera < iostream> / / importerar deklarationen av std:: cout
med hjälp av namnrymden std; // gör std::cout tillgänglig som ”cout”
int cout () / / förklarar vår egen” cout ” – funktion
{
återgå 5;
}
int main()
{
cout << ” Hej, världen!”; / / Kompilera fel! Vilken cout vill vi ha här? Den i std-namnområdet eller den vi definierade ovan?
avkastning 0;
}

ovanstående program kompilerar inte, eftersom kompilatorn nu inte kan berätta om vi vill ha cout-funktionen som vi definierade eller cout som definieras i std-namnområdet.

när du använder ett using-direktiv på detta sätt kan alla identifierare vi definierar komma i konflikt med alla identiskt namngivna identifierare i std-namnområdet. Ännu värre, medan ett identifieringsnamn kanske inte står i konflikt idag, kan det komma i konflikt med nya identifierare som läggs till i std-namnområdet i framtida språkrevisioner. Detta var hela poängen med att flytta alla identifierare i standardbiblioteket till std-namnområdet i första hand!

Varning

Undvik att använda direktiv (som att använda namnrymden std;) högst upp i programmet. De bryter mot anledningen till att namnrymder lades till i första hand.

vi kommer att prata mer om att använda uttalanden (och hur man använder dem på ett ansvarsfullt sätt) i lektion 6.12-använda uttalanden.

2.9 — introduktion till preprocessorn

Index

2.7 — program med flera kodfiler

Lämna ett svar

Din e-postadress kommer inte publiceras.