Obiekt
Programy w języku C++ tworzy, niszczy, odnosi się do, korzysta z dostępu do i manipuluje obiektami.
Obiekt w C++ to obszar składowania (ang. region of storage) posiadający:
- rozmiar - który można ustalić używając operatora sizeof;
- wymagania dotyczące wyrównania (ang. alignment) - które można ustalić używając operatora alignof;
- czas przechowywania (ang. storage duration) - które może być określony jako automatyczny, statyczny, dynamiczny oraz lokalny dla wątku;
- czas życia - wynikający z czas przechowywania lub z bycia obiektem tymczasowym;
- typ;
- wartość - która może być również nieokreślona (zob. inicjalizacja domyślna typów nie będących klasą);
- opcjonalnie: nazwę.
Następujące byty nie są obiektami: wartość, referencja, funkcja, enumeracja, typ, niestatyczna składowa klasy, pole bitowe, szablon, specjalizacja szablonu klasy lub funkcji, przestrzeń nazw, pakiet parametrów oraz wskaźnik this.
Zmienna' to obiekt lub referencja niebędąca niestatyczną składową danych, wprowadzona poprzez deklarację.
Obiekty są tworzone przez definicję, wyrażenie new, wyrażenie throw, kiedy zmieniana jest aktywna składowa unii oraz kiedy wymagane jest utworzenie [[../lifetime#Temporary object lifetime|temporary objects]].
Spis treści |
[edytuj] Reprezentacja obiektów i wartości
Dla obiektów typu T, reprezentacja obiektu to sekwencja długości sizeof(T) obiektów typu unsigned char - lub równoważnie, std::byte zaczynających się pod tym samym adresem, co adres obiektu T.
Reprezentacja wartości obiektu to sekwencja bitów które identyfikują wartość ze zbioru wartości typu T.
Dla typów realizujących koncept TriviallyCopyable (trywialnie kopiowalne), reprezentacja wartości jest częścią reprezentacji obiektu, co oznacza, że skopiowanie sekwencji bitów z obszaru składowania zajmowanego przez obiekt jest w pełni wystarczające by utworzyć kolejny obiekt z taką samą wartością. Wyjątek stanowi sytuacja gdy wartość nie reprezentuje prawidłowego obiektu danego typy, a jest znacznikiem błędu (ang. trap representation). Załadowanie takiej wartości do procesora powoduje sprzętowy wyjątek. Przykładem takiej wartości jest NaN (not-a-number, czyli nie-liczba) dla typów zmiennoprzecinowych.
Przeciwny fakt nie jest zawsze prawdziwy, to znaczy dwa obiekty TriviallyCopiable o różnych reprezentacjach obiektowych mogą reprezentować tę samą wartość. Przykładowo, wiele różnych układów bitów dla zmiennej typu zmiennoprzecinkowego może reprezentować tę samą wartość NaN. Często może się zdarzyć również, że część bitowej reprezentacji obiektu nie ma wpływu na wartość. Zachodzi to w przypadku odstępów (ang. padding) dodanych by zachować wymagania dotyczące wyrównanie, czy rozmiarów pól bitowych, itp.
#include <cassert> struct S { char c; // 1 bajt wartości // 3 bajty odstępu float f; // 4 bajty wartości bool operator==(const S& arg) const { // operator porówniania bazujący na wartościach return c == arg.c && f == arg.f; } }; assert(sizeof(S) == 8); S s1 = {'a', 3.14}; S s2 = s1; reinterpret_cast<char*>(&s1)[2] = 'b'; //zmieniamy drugi bajt assert(s1 == s2); // vartość nie zmienia się
Dla obiektów typu char, signed char oraz unsigned char (o ile nie są polami bitowymi ponadstandardowego rozmiaru}}), każdy bit reprezentacji obiektowej ma wpływ na reprezentację wartości oraz każdy możliwy układ bitów reprezentuje unikalną wartość. Dla powyższych typów nie są dopuszczalne jakiekolwiek bity wyrównania, bity specjalne czy niejednoznaczne reprezentacje.
[edytuj] Podobiekty
Obiekt może zawierać w sobie inne obiekty, zwane podobiektami Mogą to być:
- obiekty składowe
- obiekty klasy bazowej
- elementy tablicy.
Obiekt który nie jest podobiektem innego obiektu nazywamy obiektem kompletnym.
Obiekty kompletne, obiekty składowe oraz elementy tablic nazywamy również obiektami najbardziej pochodnymi, żeby odróżnić je od podobiektów klas bazowych. Wymagane jest, by rozmiar obiektu najbardziej pochodnego niebedącego polem bitowym był niezerowy. Może się zdarzyć, że rozmiar klasy bazowej wynosi zero (zob. optymalizacja pustej bazy).
Dla jakichkolwiek dwóch obiektów (niebędących [[../bit field|polami bitowymi]]), których czasy życia nie są rozłaczne, mamy gwarancję, że obiekty te będą mieć różne adresy, o ile nie jest tak, że jeden z tych obiektów jest podobiektem drugiego lub zapewnia drugiemu obszar przechowywania (ang. storage). Gwarancji różnego adresu nie bedziemy mieli również wówczas, gdy są to dwa podbiekty w tym samym obiekcie kompletnym a jeden z nich to obiekt bazowy rozmiaru zero.
static const char c1 = 'x'; static const char c2 = 'x'; assert(&c1 != &c2); // obiekty o tej samej wartości, ale różnych adresach
[edytuj] Obiekty polimorficzne
Obiekty klasy która deklaruje lub dziedziczy przynajmniej jedną składową funkcję wirtualną są obiektami polimorficznymi. W obiekcie polimorficznym implementacja przechowuje dodatkowe informacje służące do wołania funkcji wirtualnych oraz funkcjonalności RTTI (dynamic_cast czy typeid) służące do ustalenia w czasie wykonania typu utworzonego obiektu. W każdej istniejącej implementacji dodatkowe dane o których mowa powyżej to jeden wskaźnik do funkcji, który w odpowednich okolicznościach może całkowicie usunięty w trakcie optymalizacji.
Dla obiektów nie-polimorficznych, interpretacja wartości obiektu jest określona przez wyrażenie w którym obiekt jest używany i jest znana w trakcie kompilacji.
#include <iostream> #include <typeinfo> struct Base1 { // typ polimorficzny, gdyż definiuje wirtualny destruktor virtual ~Base1() {} }; struct Derived1 : Base1 { // typ polimorficzny, gdyż dziedziczy wirtualną składowową }; struct Base2 { // typ nie-polimorficzny }; struct Derived2 : Base2 { // typ nie-polimorficzny }; int main() { Derived1 obj1; // obiekt1 utworzony z typem Derived1 Derived2 obj2; // obiekt2 utworzony z typem Derived2 Base1& b1 = obj1; // b1 odnosi się do obiektu obj1 Base2& b2 = obj2; // b2 odnosi się do obiektu obj2 std::cout << "Typ wyrazenia b1: " << typeid(decltype(b1)).name() << ' ' << "Typ wyrazenia b2: " << typeid(decltype(b2)).name() << '\n' << "Typ obiektu dla b1: " << typeid(b1).name() << ' ' << "Typ obiektu dla b2: " << typeid(b2).name() << '\n' << "rozmiar b1: " << sizeof b1 << ' ' << "rozmiar b2: " << sizeof b2 << '\n'; }
Wynik:
Typ wyrazenia b1: Base1 Typ wyrazenia b2:: Base2 Typ obiektu dla b1: Derived1 Typ obiektu dla b2:: Base2 rozmiar b1: 8 rozmiar b2: 1
[edytuj] Ścisłe aliasowanie
Dostęp do obiektu przy użyciu wyrażenia którego typ jest różny niż typ z którym obiekt został utworzony jest niezdefiniowanym zachowaniem w wielu przypadkach. W haśle reinterpret_cast znajduje się lista wyjątków i przykładów.
[edytuj] Wyrównanie
Każdy typ obiektu ma właściwość zwaną wymaganym wyrównania (ang. alignment requirement), która jest liczbą całkowitą typu std::size_t będącą potęgą dwójki. Liczba ta reprezentuje liczbę bajtów między kolejnymi adresami pod którymi mogą być alokowane obiekty tego typu. Wymagane wyrównanie danego typu można sprawdzić używając alignof lub std::alignment_of. Funkcja std::align służy do otrzymania wskaźnika do podanego bufora, który spełnia podane wymagane wyrównanie. Funkcją std::aligned_storage można uzyskać bufor spełniający podane wymogi wyrównania.
Wymagania wyrównania danego typu odnoszą się bez wyjątku do każdego obiektu tego typu. Można jednak wzmocnić te wymagania (t.j. zwiększyć wartość alignof) przy użyciu alignas/
Aby spełnić wymagania dotyczące wyrównania wszystkich nie-statycznych składowych klasy, bajty odstępu mogą zostać wstawione za niektórymi ze składowych.
#include <iostream> // obiekt typu S może być alokowany pod dowolnym adresem // jako że S.a jak i S.b mogą być tak alokowane struct S { char a; // rozmiar: 1, wyrównanie: 1 char b; // rozmiar: 1, wyrównanie: 1 }; // rozmiar: 2, wyrównanie: 1 // obiekty typu X muszą być alokowane tak by zachować // wyrównanie od 4-bajtowych bloków, z uwagi na X.n; // wyrównanie typu int wynosi (zazwyczaj) 4 struct X { int n; // rozmiar: 4, wyrównanie: 4 char c; // rozmiar: 1, wyrównanie: 1 // trzy bajty odstepu }; // rozmiar: 8, wyrównanie: 4 int main() { std::cout << "sizeof(S) = " << sizeof(S) << " alignof(S) = " << alignof(S) << '\n'; std::cout << "sizeof(X) = " << sizeof(X) << " alignof(X) = " << alignof(X) << '\n'; }
Wynik:
sizeof(S) = 2 alignof(S) = 1 sizeof(X) = 8 alignof(X) = 4
Najsłabsze wymagane wyrównanie mają typy char, signed char oraz unsigned char - wartość alignof dla tych typów to 1. Największe bazowe wyrównanie jakie może mieć dowolny typ, to jest równe wyrównaniu typu std::max_align_t. Jeżeli ustalimy mocniejsze niż std::max_align_t wymagane wyrównanie dla danego typu przy pomocy alignas, to wówczas wyrówanie takiego typu nazywamy wyrównaniem rozszerzonym. Typ o wyrównaniu rozszerzonym lub klasę której nie-statyczna składowa danych ma wyrównanie rozszerzone nazywamy typem nad-wyrównanym. Od implementacji zależy, czy wyrażenie new, std::allocator::allocate oraz std::get_temporary_buffer wspierają typy nad-wyrównane. Allocator utworzone dla typów nad-wyrównanych mogą nie utworzyć się w trakcie kompilacji, rzucić wyjątek std::bad_alloc w trakcie wykonania, bez ostrzeżenia zignorować niewspierane wymagania dotyczące wyrównania lub zadziałać w pełni poprawnie.
[edytuj] See also
C documentation for object
|