13.2 Unie

Innym, rzadziej stosowanym typem złożonym jest unia. Unie są nieco podobne do struktur — różnica między unią a strukturą polega na tym, że

w unii wszystkie składowe obiektu umieszczane są pod tym samym adresem. Zatem w każdej chwili dostępna jest tylko jedna składowa.

Zauważmy bowiem, że wpisanie którejś ze składowych zamazuje poprzednią, bo była ona umieszczona w dokładnie tym samym miejscu w pamięci komputera. Wynika z tego, że rozmiar obiektu unii musi być taki, aby mieściła się w nim składowa o największym rozmiarze, ale nie musi być większy, choć może – zależy to od typów i rozmiarów poszczególnych składowych i, niestety, od architektury komputera, w szczególności od stosowanego tzw. wyrównywania (ang. alignment).

W poniższym przykładzie


P96: un.cpp     Unie

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  union Bag;
      5.  
      6.  void wstaw(Bag*,float);
      7.  void wstaw(Bag*,long double);
      8.  void infor(const Bag*);
      9.  
     10.  union Bag {
     11.      float       liczbaF;
     12.      long double liczbaLD;
     13.  } bag ;
     14.  
     15.  int main() {
     16.      cout << "      sizeof(float)=" << sizeof(float) << endl;
     17.      cout << "sizeof(long double)="
     18.           << sizeof(long double) << endl;
     19.      cout << "        sizeof(Bag)=" << sizeof(Bag) << endl;
     20.  
     21.      wstaw(&bag, 3.14F);                
     22.      infor(&bag);
     23.  
     24.      wstaw(&bag, 3.14L);                
     25.      infor(&bag);
     26.  }
     27.  
     28.  void wstaw(Bag *w, float f) {
     29.      w->liczbaF = f;
     30.  }
     31.  
     32.  void wstaw(Bag *w, long double ld) {
     33.      w->liczbaLD = ld;
     34.  }
     35.  
     36.  void infor(const Bag *w) {
     37.      cout << "\nliczbaF : " << w->liczbaF  << endl;
     38.      cout <<   "liczbaLD: " << w->liczbaLD << endl;
     39.  }

unia Bag przechowuje liczbę typu float (4 bajty) lub liczbę typu long double (12 lub 16 bajtów), ale nie obie jednocześnie. Każde przypisanie do składowej liczbaF obiektu zamaże poprzednią wartość tej składowej, ale również poprzednią wartość składowej liczbaLD, ponieważ obie te składowe zapisywane są w tym samym obszarze pamięci.

W linii  wpisujemy wartość 3,14 do składowej liczbaF obiektu bag. Funkcja infor drukuje obie składowe: liczbaF jest rzeczywiście równa 3,14, a wartość składowej liczbaLD wygląda na przypadkową:

          sizeof(float)=4
    sizeof(long double)=16
            sizeof(Bag)=16

    liczbaF : 3.14
    liczbaLD: 3.93143e-4942

    liczbaF : 1.90232e+17
    liczbaLD: 3.14
Następnie w linii  wpisujemy wartość 3,14 do składowej liczbaLD tego samego obiektu bag. Po wydrukowaniu liczbaLD jest 3,14, ale składowa liczbaF uległa zamazaniu i teraz ona ma wartość wyglądającą na przypadkową (jest to wartość odpowiadająca układowi bitów w tym obszarze pamięci zinterpretowanemu jako zapis liczby typu float).

Zwróćmy uwagą na fakt, że funkcja wstaw jest przeciążona i występuje w dwóch wersjach. Właściwa jest wybierana przez kompilator na podstawie typu argumentu. Dlatego wywołując tę funkcję musieliśmy jawnie ten typ zaznaczyć przez dodanie przyrostka 'F' i 'L' do literałów liczbowych (patrz rozdział o typach danych ).

W obu wersjach tej funkcji pierwszym parametrem jest wskaźnik do obiektu typu Bag, tak aby funkcja mogła zmienić wartość tego obiektu, a nie tylko jego lokalnej kopii, jak byłoby, gdybyśmy przesłali ten obiekt przez wartość. Oczywiście inną możliwością było przesłanie tego argumentu przez referencję.

Z kolei funkcja infor korzysta z parametru wskaźnikowego raczej ze względu na efektywność niż z konieczności. Ta funkcja i tak nie zmienia obiektu, więc mogłaby równie dobrze pracować na przekazywanej przez wartość kopii. Ponieważ jednak wybraliśmy jako typ parametru typ wskaźnikowy, co daje funkcji dostęp do oryginału, zaopatrzyliśmy ten parametr w modyfikator const, aby zapewnić, że nawet przypadkowo oryginału w tej funkcji nie zmienimy.

Na początku programu wypisujemy rozmiar obiektu typu Bag: wynosi on dokładnie 16, tyle ile wynosi rozmiar dłuższej ze składowych (ale oczywiście mniej niż suma rozmiarów składowych).


Bardziej realistyczny przykład znajdujemy w programie następującym:


P97: unie.cpp     Unia anonimowa, kontrola typów

      1.  #include <iostream>
      2.  #include <cassert>
      3.  using namespace std;
      4.  
      5.  struct Bag;
      6.  enum Rodzaj {LICZBA, WSKAZNIK, ZNAK};      
      7.  
      8.  void wstaw(Bag*,double);
      9.  void wstaw(Bag*,int*);
     10.  void wstaw(Bag*,char);
     11.  
     12.  void daj(const Bag*,double&);
     13.  void daj(const Bag*,int*&);
     14.  void daj(const Bag*,char&);
     15.  
     16.  void info(const Bag&);
     17.  
     18.  struct Bag {
     19.      Rodzaj rodzaj;
     20.      union {                                
     21.          double dbl;
     22.          int   *wsk;
     23.          char   znk;
     24.      };
     25.  };
     26.  
     27.  int main() {
     28.      Bag bag;
     29.      double x = 3.14, y;
     30.      int    i = 10, *pi = &i;
     31.      char   c ='a', b;
     32.      cout << "sizeof(bag) = " << sizeof(bag)
     33.           << " bajtow\nAdresy skladowych:\n dbl: "
     34.           << &bag.dbl << "\n wsk: " <<  &bag.wsk
     35.           << "\n znk: " << (void*)&bag.znk << endl;
     36.  
     37.      wstaw(&bag,x);
     38.      info(bag);
     39.      daj(&bag,y);
     40.      cout << "Z funkcji main - y  = " <<    y << endl;
     41.  
     42.      wstaw(&bag,&i);
     43.      info(bag);
     44.      daj(&bag,pi);
     45.      cout << "Z funkcji main - *pi = " << *pi << endl;
     46.  
     47.      wstaw(&bag,c);
     48.      info(bag);
     49.      daj(&bag,b);
     50.      cout << "Z funkcji main - b   = " <<   b << endl;
     51.  }
     52.  
     53.  void wstaw(Bag *w, double x) {
     54.      w->rodzaj   = LICZBA;                  
     55.      w->dbl = x;
     56.  }
     57.  
     58.  void wstaw(Bag *w, int *pi) {
     59.      w->rodzaj   = WSKAZNIK;
     60.      w->wsk = pi;
     61.  }
     62.  
     63.  void wstaw(Bag *w, char c) {
     64.      w->rodzaj   = ZNAK;
     65.      w->znk = c;
     66.  }
     67.  
     68.  void daj(const Bag *w, double& x) {
     69.      assert(w->rodzaj == LICZBA);
     70.      x  = w->dbl;
     71.  }
     72.  
     73.  void daj(const Bag *w, int*& pi) {
     74.      assert(w->rodzaj == WSKAZNIK);
     75.      pi = w->wsk;
     76.  }
     77.  
     78.  void daj(const Bag *w, char& c) {
     79.      assert(w->rodzaj == ZNAK);             
     80.      c  = w->znk;
     81.  }
     82.  
     83.  void info(const Bag &w) {
     84.      cout << "\nZ funkcji info - ";
     85.      switch (w.rodzaj) {
     86.      case LICZBA:
     87.          cout << "Liczba:   " << w.dbl    << endl;
     88.          break;
     89.      case WSKAZNIK:
     90.          cout << "Wskaznik: " << *(w.wsk) << endl;
     91.          break;
     92.      case ZNAK:
     93.          cout << "Znak:     " << w.znk    << endl;
     94.          break;
     95.      }
     96.  }

Struktura Bag ma dwa pola: jedno o nazwie rodzaj typu Rodzaj, który to typ jest globalnie zdefiniowanym wyliczeniem (), a drugie, bez nazwy (sic!), które jest typu unii anonimowej zdefiniowanej lokalnie wewnątrz struktury (). Typ ten jest anonimowy; pomiędzy słowem kluczowym union a nawiasem klamrowym rozpoczynającym definicję unii nie podaliśmy bowiem żadnej nazwy. Po jego zdefiniowaniu, zaraz za zamykającym nawiasem klamrowym, nie zdefiniowaliśmy żadnego obiektu tego typu. Otóż w takiej sytuacji ujawnia się mało znana cecha unii anonimowych: jeśli zdefiniowana jest unia anonimowa, a nie została zdefiniowana żadna zmienna tego typu (zaraz za definicją a przed kończącym średnikiem), to kompilator sam tworzy pojedynczy egzemplarz unii i nazwy jego składowych przenosi do zakresu otaczającego. A zatem nazwy dbl, wskznk będą widoczne bezpośrednio w zakresie struktury Bag!

Wyliczenie Rodzaj składa się z trzech wartości: LICZBA, WSKAZNIKZNAK. Będziemy ich używać do określenia typu danej aktualnie przechowywanej w składowej unii. Funkcje wstaw są przeciążone. Każda z nich wywoływana jest dla innego typu danej przeznaczonej do wstawienia do składowej unii obiektu typu Bag. Kiedy dana jest wstawiana, każda z funkcji dba o to, aby w składowej rodzaj danego obiektu zapisać informację o typie wstawianej danej (jak w linii ).

Funkcje daj do pobierania danej z obiektu typu Bag też są przeciążone ze wzgędu na typ danej. Dane pobieramy poprzez drugi argument funkcji przez referencję. W ten sposób zabezpieczamy się przed niewskazanymi w tym przypadku konwersjami. Za każdym razem, gdy pobieramy dane, typ drugiego argumentu decyduje o wyborze funkcji. Każda z funkcji sprawdza za pomocą makra assert (np. linia ), czy typ żądanej danej jest zgodny z typem danej aktualnie przechowywanej w składowej unii. W ten sposób zabezpieczamy się przed błędami podczas sięgania do danych zapisanych w obiektach typu Bag.

    sizeof(bag) = 16 bajtow
    Adresy skladowych:
     dbl: 0x7fff0476ee48
     wsk: 0x7fff0476ee48
     znk: 0x7fff0476ee48

    Z funkcji info - Liczba:   3.14
    Z funkcji main - y  = 3.14

    Z funkcji info - Wskaznik: 10
    Z funkcji main - *pi = 10

    Z funkcji info - Znak:     a
    Z funkcji main - b   = a
Drukując adresy składowych unii zawartych w obiekcie bag na początku programu, przekonujemy się, że rzeczywiście wszystkie one są zapisane pod tym samym adresem.

T.R. Werner, 25 lutego 2017; 22:31