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).