C++/Przestrzenie nazw

< C++

Słowem wstępu

edytuj

Załóżmy, że w grupie znajomych masz dwie osoby o tym samym imieniu, powiedzmy „Tomek”. Teraz za każdym razem, gdy wołasz Tomka, oboje się mylą - nie wiedzą o którego ci chodzi. Aby rozwiązać ten problem, zaczynasz używać ich pełnych nazwisk. Przestrzenie nazw w C ++ działają w ten sam sposób. (Źródło - Namespaces)

Jeśli użyjemy dowolnej wyszukiwarki internetowej, to powinniśmy bez problemu znaleźć prosty, szablonowy kod napisany w C++, który wyświetla napis „Hello World!”, w tłumaczeniu na polski „Witaj Świecie!”. Spójrzmy na niego:

#include <iostream>
using namespace std;

int main ()
{
   cout << "Hello World!" << endl;
   return 0;
}
 

Zaleca się używanie znaku nowej linii (\n) zamiast manipulatora wyjścia "endl". Chyba że jest to uzasadnione: endl wymusza opróżnienie bufora, ale na przykład przy wielokrotnym zapisie na dysk może to obciążyć jego pracę.

Osoby, które już znają C, na pewno się domyślą, co mniej więcej się dzieje w tym kodzie. Najpierw, pokrótce omówimy, co ten program właściwie robi.

Za pomocą #include <iostream> dołączyliśmy plik nagłówkowy do obsługi strumieni I/O, dzięki czemu możemy wypisywać dane na ekran (ściślej: na standardowe wyjście). Dodatkowo istnieje plik nagłówkowy iostream.h. Jest to jednak nagłówek niestandardowy, pomagający zachować wsteczną zgodność.

int main( ) {...} służy zdefiniowaniu funkcji głównej, która jest zawsze uruchomiana podczas startu naszego programu.

Wyrażenie cout << umożliwia nam wypisywanie pewnych informacji. W naszym przypadku wypisaliśmy napis „Hello World!”, a następnie „przedłużyliśmy” to polecenie za pomocą operatora <<, i użyliśmy endl, który m.in. dodaje znak nowej linii.

Za pomocą return 0 informujemy system, że program może zakończyć działanie bez zgłaszania błędów.

Na koniec zostawiliśmy linię z kodem using namespace std. Aby wyjaśnić jej znaczenie, musimy omówić, czym są przestrzenie nazw.

Przestrzenie nazw

edytuj

Podczas pracy nad dużymi projektami, w których używa się wielu bibliotek z licznymi deklaracjami, możemy w końcu natknąć się na problem konfliktu nazw - gdy kilka obiektów, typów czy funkcji ma tę samą nazwę. Rozwiązaniem może być np. zamknięcie nazw w "zakresach", w celu oddzielenia ich. Z pomocą przychodzi nam mechanizm przestrzeni nazw.

Przestrzeń nazw jest zatem zbiorem obiektów, która ogranicza dostęp do nich - oprócz nazwy obiektu niezbędne jest też wspomnienie, z której przestrzeni nazw chcemy go użyć, obchodząc tym samym problem konfliktu nazw.

Spójrzmy na kolejny program, zmienioną wersję poprzedniego:

#include <iostream>

int main ()
{
   std::cout << "Hello World!" << std::endl;
   return 0;
}

Widzimy tu wyrażenie std:: pojawiające się przed cout i endl. Zapis ten oznacza, że wspomniane obiekty chcemy zaczerpnąć z przestrzeni std, a przy okazji nie obchodzi nas, czy są jakieś inne obiekty o takich nazwach. Jeśli jednak pominiemy wzmiankę o std::, pojawi się informacja o błędzie.

W przestrzeni nazw std znajdziemy mnóstwo, a wręcz cały arsenał różnych narzędzi, począwszy od pewnych bardzo przydatnych funkcji, np. sortowania, wyszukiwania, a kończywszy na tak zwanych pojemnikach (kolekcjach), które pozwalają nam w łatwy sposób przechowywać pewne wartości. Oczywiście, aby mieć dostęp do tych narzędzi, musimy dołączyć odpowiedni plik nagłówkowy, używając do tego dyrektywy #include.

Przykład pierwszy ze wstępu pokazał nam, że nie musimy za każdym razem odwoływać się do przestrzeni nazw, kiedy chcemy użyć znajdujących się w niej rzeczy. Używając using namespace PrzestrzenNazw, podpowiadamy kompilatorowi, w którym miejscu może szukać używanych przez nas obiektów i funkcji, abyśmy mogli swobodnie używać wszystkiego co się znajduje w danej przestrzeni nazw, tzn. bez dodatkowej wzmianki jak np. std::.

Oczywiście nie musimy naraz "udostępniać" wszystkiego, co jest w danej przestrzeni nazw, możemy wykorzystać także pewne wybrane elementy. Używamy do tego operacji using PrzestrzenNazw::element. Zobaczmy przykład użycia tej operacji:

#include <iostream>

using std::endl;

int main ()
{
   std::cout << "Hello World!" << endl;
   return 0;
}

Za pomocą using std::endl poinformowaliśmy kompilator, że będziemy mogli używać w kodzie endl i będziemy mieli na myśli właśnie to pochodzące z przestrzeni std. Nie wykonaliśmy tej operacji na elemencie cout (nie wstawiliśmy instrukcji using std::cout), więc musieliśmy go dalej poprzedzić nazwą przestrzeni.

Tworzenie własnej przestrzeni nazw

edytuj

Przestrzeń nazw tworzymy za pomocą słowa kluczowego namespace, ograniczając jej zawartość klamrami. Możemy na przykład stworzyć przestrzeń nazw HelloWorld zawierającą funkcję hello( ):

#include <iostream>

namespace HelloWorld
{
   void hello ()
   {
      std::cout << "Hello World!" << std::endl;
   }
}

int main ()
{
   HelloWorld::hello ();
   return 0;
}

Oczywiście, gdybyśmy wstawili using HelloWorld::hello lub ogólnie using namespace HelloWorld przed funkcją main (a nawet wewnątrz tej funkcji), nie musielibyśmy odwoływać się jawnie do HelloWorld, wystarczyłoby samo hello( ).

Co ciekawe, nie musimy zamieszczać zawartości naszej przestrzeni nazw w jednym, ciągłym bloku. Możemy rozbić to na kilka części:

namespace Matematyka
{
   int dodaj (int a, int b)
   {
       return a+b;
   }

   int odejmij (int a, int b)
   {
       return a-b;
   }
}

namespace Matematyka
{
   int pomnoz (int a, int b)
   {
       return a*b;
   }

   int podziel (int a, int b)
   {
       return a/b;
   }
}

Wówczas wewnątrz przestrzeni nazw Matematyka znajdziemy wszystkie stworzone przez nas funkcje.

Tworząc funkcję w przestrzeni nazw możemy wstawić samą deklarację, a potem w innym miejscu podać pełną definicję tej funkcji. Możemy na co najmniej dwa sposoby podać definicję pewnej funkcji - wewnątrz przestrzeni nazw lub poza nią, pisząc typ_zwracany PrzestrzenNazw::nazwa_funkcji( ), na przykład:

#include <iostream>

namespace Matematyka
{
   int dodaj (int a, int b);
   int odejmij (int a, int b);
}

using namespace std;

int main ()
{ 
   cout << Matematyka::dodaj (10, 20) << endl;
   return 0;
}

namespace Matematyka
{
   int dodaj (int a, int b)
   {
       return a+b;
   }

   int odejmij (int a, int b)
   {
       return a-b;
   }
}

Jak wspomniano wcześniej, ostatnie dwie definicje funkcji moglibyśmy zapisać także w ten sposób:

int Matematyka::dodaj (int a, int b)
{...}

int Matematyka::odejmij (int a, int b)
{...}

Przestrzeń nazw std

edytuj

Wróćmy ponownie do standardowej przestrzeni nazw, jaką jest std. Dzięki plikowi nagłówkowemu iostream możemy operować na standardowym wejściu i wyjściu. Zobaczmy jak wczytywać pewne wartości do zmiennych, używając do tego cin:

#include <iostream>

int main ()
{
   int a, b;
   std::cout << "Podaj dwie liczby a i b" << std::endl;

   // wypisujemy komunikat i czekamy na wpisanie liczby a
   std::cout << "podaj a: ";
   std::cin >> a;

   // wypisujemy komunikat na wyjście i czekamy na wpisanie liczby b
   std::cout << "podaj b: ";
   std::cin >> b;

   // wypisujemy sumę tych dwóch liczb
   std::cout << "a+b= " << a+b << std::endl;
   return 0;
}

Dzięki std::cin >> możemy wczytać pewną wartość do zmiennej. Zmienna ta nie musi być liczbą, może być też np. napisem. W C++ tekst (łańcuch znaków) będziemy często przechowywali w obiektach typu string (który także znajduje się w std). Do jego obsługi będziemy musieli dołączyć do projektu bibliotekę <string>. Spójrzmy na przykład:

#include <iostream>
#include <string>

using std::cout;
using std::cin;
using std::endl;

int main ()
{
   std::string imie;
   std::string email;
   std::string informacja;

   // wczytujemy imię
   cout << "Podaj swoje imie: "; 
   cin >> imie;

   // wczytujemy email
   cout << "Podaj swój email: ";
   cin >> email;

   informacja = imie + " (" + email + ")";  // suma (konkatenacja) napisów
   cout << "Witaj " << informacja << endl;

   informacja += " czyta ten napis";
   cout << informacja << endl;
   return 0;
}

Zauważmy, jak prosto się korzysta zmienną typu string (dla wtajemniczonych jest to pewna klasa). Jeśli chcemy dodać dwa napisy, wystarczy wykorzystać operator +. Możemy także wykorzystywać operator +=, jeśli chcemy dokleić do tekstu dodatkowy napis.

Podając swoje imię jako Zdzichu, a e-mail jako zdzichu@zdzichowo.mars, zobaczymy wynik:

Podaj swoje imie: Zdzichu
Podaj swój email: zdzichu@zdzichowo.mars
Witaj Zdzichu (zdzichu@zdzichowo.mars)
Zdzichu (zdzichu@zdzichowo.mars) czyta ten napis

Więcej o stringach można przeczytać w dodatku opisującym bibliotekę STL.

Korzystanie z biblioteki standardowej C

edytuj

Ponieważ język C++ jest (w pewnym uproszczeniu) rozwinięciem C, w dalszym ciągu można korzystać z biblioteki standardowej C (tzw. libc). Ze względu na zachowanie wstecznej kompatybilności, umożliwiono korzystanie z niej tak jak wcześniej w C.

#include <string.h>

int main (int argc, char **argv)
{
   if (argc < 2)
      return -1;
   return strcmp (argv[0], argv[1]);
}

Jednak dostępna jest też wersja libc przygotowana specjalnie dla C++. Pliki nagłówkowe są w niej inaczej nazywane, wszystkie funkcje znajdują się dodatkowo w przestrzeni nazw std. Tak więc powyższy program napisany w sposób właściwy dla C++ mógłby wyglądać następująco:

#include <cstring>  // zamiast <string.h>
  
int main (int argc, char **argv)
{
   if (argc < 2)
      return -1;
   return std::strcmp( argv[0], argv[1]);
}

Zauważmy, że:

  1. dołączany plik nagłówkowy ma dodaną na początku literę c
  2. dostęp do funkcji jest możliwy przez pośrednictwo przestrzeni nazw std

Reguła ta dotyczy wszystkich plików, z których składa się biblioteka standardowa C.

W swoich programach lepiej jest używać wersji przygotowanej dla C++: #include <cxxxxx>. Po pierwsze, dzięki przestrzeniom nazw unikniemy kolizji nazw z własnymi funkcjami. Po drugie, wersja ta ma wbudowaną obsługę wyjątków. Po trzecie, czasami libc przygotowana dla C wywołuje ostrzeżenia lub błędy kompilacji w kompilatorach C++.