konwersje, przestrzenie nazw
Download
Report
Transcript konwersje, przestrzenie nazw
C++
wykład 8 (25.04.2013)
Rzutowanie i konwersje
Przestrzenie nazw
Wskaźniki do składowych
Przykład zwykłych wskaźników:
class K {
public: int skl;
//…
};
//…
K *wskob;
K ob, tab[10];
//…
wskob = &ob;
int x = wskob->skl;
wskon = &tab[4];
Wskaźniki do składowych
Definicja wskaźnika do składowych w klasie:
TYP klasa::*wsk;
gdzie TYP jest typem składnika w klasie klasa
Pobranie adresu składowej w klasie:
&klasa::składowa
Aby odnieść się do wskazywanego pola w obiekcie
stosujemy operator .* lub ->*:
ob_ref.*wsk
wskaźnik->*wsk
Wskaźnikami do składowych możemy pokazywać
na pola i na metody w klasie.
Wskaźniki do składowych
Przykład wskaźników do składowych:
class K {
public:
int r, s;
int f (int);
//…
};
//…
int K::*wskint = &K::r;
int (K::*wskfun) (int) = &K::f;
//…
K a;
K *p = &a;
wskint = &K::s;
int x = p->*wskint;
int y = (a.*wskfun)(3);
Tradycyjne
operatory rzutowania
Tradycyjne operatory rzutowania jawnie
przekształcają typ danych.
Tradycyjne operatory konwersji mogą przyjmować
dwie formy:
(typ)wyrażenie
typ(wyrażenie)
Przykłady:
(int)3.1415926 // forma rzutowania
double(7*11+5) // forma funkcyjna
Operacja jawnej konwersji typów jest niebezpieczna
i należy ją stosować bardzo ostrożnie (tylko w razie
konieczności).
Zaleca się używać funkcyjnej formy przy rzutowaniu
tradycyjnym.
Tradycyjne
operatory rzutowania
Kompilator umie przekształcać na siebie wszystkie
typy podstawowe.
Operator rzutowania eliminuje ostrzeżenia
kompilatora przy przekształcaniu typów
podstawowych.
Kompilator nie będzie generował ostrzeżeń w
przypadku konwersji na typach podstawowych, w
których mamy do czynienia z promocją (konwersje
niejawne).
Przykłady:
const double e = 2.71828182845904523;
int x = (int)e; // wymagana konwersja
double y = 2*x+1; // konwersja niejawna
Konstruktory konwertujące
Konstruktor konwertujący to konstruktor bez
deklaratora explicit, który można wywołać
z jednym parametrem:
K::K (typ x) {/*…*/} // typ!=K
Konstruktorów konwertujących może być
wiele w jednej klasie.
Deklarator explicit zabrania używać
konstruktora konwertującego niejawnie.
Konstruktory konwertujące
Przykład konstruktora konwertującego i jego niejawnego użycia:
class zespolona {
double re, im;
public:
zespolona (double r=0, double i=0);
// …
};
// …
zespolona a;
zespolona b = zespolona(1.2); // jawna konwersja
zespolona c = 3.4; // niejawna konwersja
zespolona d = (zespolona)5.6; // rzutowanie
zespolona e = static_cast<zespolona>(7.8);
zespolona f(9.0,0.9);
Operatory konwersji
Operator konwersji ma następującą postać:
operator typ ();
Operator konwersji ma pustą listę argumentów
i nie ma określonego typu wyniku (typ wyniku
jest określony poprzez nazwę tego operatora).
Operator konwersji musi być funkcją składową
w klasie.
Operator konwersji jest dziedziczony.
Operator konwersji może być wirtualny.
Operatorów konwersji może być wiele w jednej
klasie.
Operator static_cast
Operator rzutowania static_cast ma
następującą postać:
static_cast<typ>(wyrażenie)
Rzutowanie to działa tak jak rzutowanie
tradycyjne – jeśli jest zdefiniowana operacja
rzutowania to zostanie ona wykonana.
Rzutowania static_cast używa się do
konwersji typów pokrewnych (zmiana typu
wskaźnikowego w tej samej hierarchii klas,
wyliczenia do typu całkowitego, typu
zmiennopozycyjnego do całkowitego, itp).
Rzutowanie const_cast
Operator rzutowania const_cast ma
następującą postać:
const_cast<typ>(wyrażenie)
przy czym typ powinno być wskaźnikiem,
referencją lub wskaźnikiem do składowej.
Rzutowanie to pozwala dodać albo
zlikwidować deklarator const lub volatile
w typie wyrażenia (ale nie pozwala zmienić
typu głównego).
Rzutowanie
reinterpret_cast
Operator rzutowania reinterpret_cast
ma następującą postać:
reinterpret_cast<typ>(wyrażenie)
Rzutowanie to ma zmienić interpretację typu
wyrażenia (kompilator nie sprawdza sensu
tego rzutowania).
Operator rzutowania reinterpret_cast
tworzy wartość nowego typu, który ma ten
sam wzorzec bitowy co podane wyrażenie.
Rzutowanie to nie gwarantuje przenośności.
Rzutowanie dynamic_cast
Operator rzutowania dynamic_cast ma
następującą postać:
dynamic_cast<typ>(wyrażenie)
przy czym wyrażenie powinno być wskaźnikiem
lub referencją do typu polimorficznego.
Rzutowanie to wykonuje się w trakcie działania
programu.
dynamic_cast<T*>(p) zwraca wskaźnik typu T*
gdy obiekt wskazywany przez p jest typu T lub ma
unikatową klasę bazową typu T (w przeciwnym
przypadku zwraca 0).
dynamic_cast<T&>(r) zwraca referencję typu T&
gdy obiekt wskazywany przez r jest typu T lub ma
unikatową klasę bazową typu T (w przeciwnym
przypadku rzuca wyjątek bad_cast).
RTTI
Operator typeid() zwraca referencję do obiektu
opisującego typ wyrażenia w nawiasach (można też
podać nazwę typu).
Klasa type_info zdefiniowana w <typeinfo>
służy do opisu typu danych lub wyrażeń.
W klasie type_info są zdefiniowane operatory ==
i != do porównywania informacji o typie.
W klasie type_info jest zdefiniowana metoda
name() dostarczająca nazwę typu w postaci const
char *.
Przestrzenie nazw
Przestrzeń nazw to obszar, w którym
umieszcza się różne deklaracje i definicje.
Przestrzeń nazw definiuje zasięg, w którym
dane nazwy będą obowiązywać i będą
dostępne.
Przestrzenie nazw rozwiązują problem kolizji
nazw.
Przestrzenie nazw wspierają modularność
kodu.
Definicja przestrzeni nazw
Przestrzeń nazw tworzymy za pomocą słowa kluczowego
namespace, ograniczając zawartość klamrami:
namespace przestrzeń
{
// deklaracje i definicje
}
Aby odnieść się do typu, funkcji albo obiektu umieszczonego w
przestrzeni nazw musimy stosować kwalifikator zakresu
przestrzeń:: poza tą przestrzenią.
Funkcja main() musi być globalna, aby środowisko
uruchomieniowe rozpoznało ją jako funkcję specjalną.
Do nazw globalnych odnosimy się za pomocą pustego
kwalifikatora zakresu ::, na przykład ::wspolczynnik.
Jeśli w przestrzeni nazw zdefiniujemy klasę to do składowej
statycznej w takiej klasie odnosimy się kwalifikując najpierw
nazwą przestrzeni a potem nazwą klasy
przestrzeń::klasa::składowa.
Definicja przestrzeni nazw
Przykład przestrzeni nazw:
namespace wybory
{
int min2 (int, int);
int min3 (int, int, int);
}
int wybory::min2 (int a, int b)
{ return a<b ? a : b; }
int wybory::min3 (int a , int b , int c)
{ return min2(min2(a,b),c); }
int min4 (int a, int, b, int c, int d)
{
return wybory::min2(
wybory::min2(a,b),
wybory::min2(c,d));
}
Deklaracja użycia
Deklaracja użycia wprowadza lokalny synonim nazwy z innej
przestrzeni nazw (wskazanej nazwy można wówczas używać
bez kwalifikowania jej nazwą przestrzeni).
Deklaracja użycia using ma postać:
using przestrzeń::symbol;
Deklaracja użycia obowiązuje do końca bloku, w którym
wystąpiła.
Deklaracje użycia stosujemy w celu poprawienia czytelności
kodu.
Deklaracje użycia należy stosować tak lokalnie, jak to jest
możliwe.
Jeśli większość funkcji w danej przestrzeni nazw korzysta z
jakiejś nazwy z innej przestrzeni, to deklaracje użycia można
włączyć do przestrzeni nazw.
Dyrektywa użycia
Dyrektywa użycia udostępnia wszystkie nazwy z określonej
przestrzeni nazw.
Dyrektywa użycia using namespace ma postać:
using namespace przestrzeń;
Dyrektywy użycia stosuje się najczęściej w funkcjach, w których
korzysta się z wielu symboli z innej niż ta funkcja przestrzeni
nazw.
Globalne dyrektywy użycia są stosowane do transformacji kodu i
nie powinno się ich stosować do innych celów.
Globalne dyrektywy użycia w pojedynczych jednostkach
translacji (w plikach .cpp) są dopuszczalne w programach
testowych czy w przykładach, ale w produkcyjnym kodzie jest to
niestosowne i jest uważane za błąd.
Globalnych dyrektyw użycia nie wolno stosować w plikach
nagłówkowych!
Anonimowe przestrzenie nazw
Anonimową przestrzeń nazw tworzymy za pomocą słowa kluczowego
namespace bez nazwy, ograniczając zawartość klamrami:
namespace
{
// deklaracje i definicje
}
Anonimowa przestrzeń nazw zastępuje użycie deklaratora static przy
nazwie globalnej – dostęp do nazw zdefiniowanych w przestrzeni
anonimowej jest ograniczony do bieżącego pliku.
Dostęp do anonimowej przestrzeni nazw jest możliwy dzięki niejawnej
dyrektywie użycia.
namespace $$$
{
// deklaracje i definicje
}
using namespace $$$;
W anonimowej przestrzeni nazw $$$ jest unikatową nazwą w zasięgu,
w którym jest zdefiniowana ta przestrzeń.
Przeszukiwanie nazw
Gdy definiujemy funkcję z jakiejś przestrzeni nazw
(przed nazwą definiowanej właśnie funkcji stoi
kwalifikator przestrzeni) to w jej wnętrzu dostępne
są wszystkie nazwy z tej przestrzeni.
Funkcja z argumentem typu T jest najczęściej
zdefiniowana w tej samej przestrzeni nazw co T.
Jeżeli więc nie można znaleźć funkcji w kontekście,
w którym się jej używa, to szuka się jej w
przestrzeniach nazw jej argumentów.
Jeżeli funkcję wywołuje metoda klasy K, to
pierwszeństwo przed funkcjami znalezionymi przez
typy argumentów mają metody z klasy K i jej klas
bazowych.
Aliasy przestrzeni nazw
Jeżeli użytkownicy nadają przestrzeniom nazw krótkie nazwy, to
mogą one spowodować konflikt. Długie nazwy są niewygodne w
użyciu. Dylemat ten można rozwiązać za pomocą krótkiego aliasu
dla długiej nazwy przestrzeni nazw.
Aliasy dla przestrzeni nazw tworzymy za pomocą słowa
kluczowego namespace z dwiema nazwami
namespace krótka = długa_nazwa_przestrzeni;
Przykład:
namespace American_Telephone_and_Telegraph
{
// tutaj zdefiniowano Napis
}
namespace ATT = American_Telephone_and_Telegraph;
American_Telephone_and_Telegraph::Napis n = "x";
AAT::Napis nn = "y";
Nadużywanie aliasów może prowadzić do nieporozumień!
Komponowanie i wybór
Interfejsy projektuje się po to, by
zminimalizować zależności pomiędzy
różnymi częściami programu. Minimalne
interfejsy prowadzą do systemów
łatwiejszych do zrozumienia, w których lepiej
ukrywa się dane i implementację, łatwiej się
je modyfikuje oraz szybciej kompiluje.
Eleganckim narzędziem do konstruowania
interfejsów są przestrzenie nazw.
Komponowanie i wybór
Gdy chcemy utworzyć interfejs z istniejących już interfejsów to
stosujemy komponowanie przestrzeni nazw za pomocą dyrektyw
użycia, na przykład:
namespace His_string {
class String { /* ... */ };
String operator+ (const String&, const String&);
String operator+ (const String&, const char*);
void fill (char) ;
// ... }
namespace Her_vector {
template<class T> class Vector { /* ... */ };
// ... }
namespace My_lib {
using namespace His_string;
using namespace Her_vector;
void my_fct(String&) ; }
Dyrektywa użycia wprowadza do zasięgu wszystkie deklarację z
podanej przestrzeni nazw.
Komponowanie i wybór
Teraz przy pisaniu programu można posługiwać się My_lib:
void f () {
My_lib::String s = "Byron";
// znajduje My_lib::His_string::String
// ...
}
using namespace My_lib;
void g (Vector<String> &vs) {
// ...
my_fct(vs[5]);
// ...
}
Komponowanie i wybór
Gdy chcemy utworzyć interfejs i dołożyć do niego kilka nazw z
innych interfejsów to stosujemy wybór za pomocą deklaracji
użycia, na przykład:
namespace My_string {
using His_string::String;
using His_string::operator+;
// …
}
Deklaracja użycia wprowadza do zasięgu każdą deklarację o
podanej nazwie. Pojedyncza deklaracja użycia może wprowadzić
każdy wariant funkcji przeciążonej.
Komponowanie i wybór
Łączenie komponowania (za pomocą dyrektyw
użycia) z wyborem (za pomocą deklaracji użycia)
zapewnia elastyczność potrzebną w praktyce. Z
użyciem tych mechanizmów możemy zapewnić
dostęp do wielu udogodnień, a zarazem rozwiązać
problem konfliktu nazw i niejednoznaczności
wynikających z komponowania.
Nazwy zadeklarowane jawnie w przestrzeni nazw
(łącznie z nazwami wprowadzonymi za pomocą
deklaracji użycia) mają pierwszeństwo przed
nazwami wprowadzonymi za pomocą dyrektyw
użycia.
Nazwę w nowej przestrzeni nazw można zmienić za
pomocą instrukcji typedef lub poprzez
dziedziczenie.
Przestrzenie nazw są otwarte
Przestrzeń nazw jest otwarta, co oznacza, że można do niej
dodawać nowe pojęcia w kilku deklaracjach (być może
rozmieszczonych w różnych plikach), na przykład:
namespace NS {
int f (); // NS ma nową składową f()
}
namespace NS {
int g (); // teraz NS ma dwie składowe f() i g()
}
Definiując wcześniej zadeklarowaną składową w przestrzeni nazw,
bezpieczniej jest użyć operatora zakresu niż ponownie otwierać
przestrzeń (kompilator nie wykryje literówek w nazwie składowej), na
przykład:
namespace NS {
int h ();
}
int NS::hhh () // błąd - zamiast h napisano hhh
{ /*…*/ }
Przestrzeń nazw std
W języku C++ wszystkie nazwy z biblioteki standardowej są
umieszczone w przestrzeni nazw std.
W języku C tradycyjnie używa się plików nagłówkowych i wszystkie
nazwy w nich deklarowane są w przestrzeni globalnej (dostępne
bez żadnych kwalifikacji).
Aby zapewnić możliwość kompilowania przez kompilatory C++
programów napisanych w C przyjęto, że jeśli użyjemy tradycyjnej
(pochodzącej z C) nazwy pliku nagłówkowego, to odpowiedni plik
jest włączany i zadeklarowane w nim nazwy są dodawane do
globalnej przestrzeni nazw. Jeśli natomiast ten sam plik
nagłówkowy włączymy pod nową nazwą, to nazwy w nim
deklarowane są dodawane do przestrzeni nazw std. Przyjęto przy
tym konwencję, że pliki nagłówkowe z C nazwie nazwa.h są
w C++ nazywane cnazwa (pary plików <math.h> i <cmath>, itp).
Nowości z C++11 – polecenie
utworzenia domyślnych metod
Polecenie to realizujemy za pomocą specyfikatora
=default za nagłówkiem metody.
W przypadku domyślnego konstruktora, mogłaby być
przydatna możliwość jawnego przekazania
kompilatorowi polecenia utworzenia go (kompilator nie
stworzy konstruktora domyślnego, jeśli obiekt posiada
dowolne konstruktory):
struct SomeType {
// domyślny konstruktor jest jawnie
określony
SomeType() = default;
SomeType(OtherType value);
// …
};
Nowości z C++11 – polecenie
blokowania domyślnych metod
Polecenie to realizujemy za pomocą specyfikatora =delete
za nagłówkiem metody.
Generowanie pewnych metod przez kompilator może być
jawnie zablokowane:
struct NonCopyable {
NonCopyable & operator= (const NonCopyable&) = delete;
NonCopyable (const NonCopyable&) = delete;
NonCopyable () = default;
// …
};
struct NonNewable {
void *operator new(std::size_t) = delete;
// …
};
Nowości z C++11 – polecenie
blokowania domyślnych metod
Specyfikator =delete może być użyty do zablokowania
wywołania dowolnej metody, co może być użyte do
zablokowania wywołania metody z określonymi
parametrami:
struct NoDouble {
void f(int i);
void f(double) = delete;
// …
};
Próba wywołania f() z argumentem typu double będzie
odrzucona przez kompilator (kompilator nie wykona
niejawnej konwersji do int).