k cpl 03



Kurs C++ #3



Kurs C++ #3


|========== #03 ==========|
+-------------------------+
| K U R S C + + |
+-------------------------+
t y p y z ł o ż o n e

...czyli autor pokazuje, jak w jednej zmiennej zmieścić wszystkie dane osobowe.

Łatwo można korzystać z typów prostych. Czasem potrzebujemy jednak czegoś bardziej rozbudowanego. No bo jak za pomocą kilku zmiennych przedstawić zawartość biblioteki? Niby można, jednak spotkamy wiele trudności, a niektórych problemów nie będziemy w stanie rozwiązać bez pomocy bardziej złożonych konstrukcji. I właśnie tutaj z pomocą przychodzą nam typy złożone.


Tablice statyczne


Wiemy już, jak posługiwać się pojedynczymi zmiennymi. Co jednak zrobić, gdy potrzebujemy 100 zmiennych jednego typu? Możemy oczywiście zadeklarować sto zmiennych, nazywając je np. a1, a2, a3, ... a99, a100. Jednak takie rozwiązanie nie jest dobre. O wiele lepszym rozwiązaniem jest stworzenie tablicy stu elementów danego typu:


int liczby[100];


Teraz zmienna liczby jest typu tablicowego. Do jej kolejnych elementów można się odwoływać poprzez wpisanie nazwy zmiennej, a następnie podanie w nawiasach kwadratowych odpowiedniego indeksu, czyli liczby miejsc w tablicy (numery indeksów zawsze są liczone od zera):


liczby[0] = 5; // przypisanie pierwszemu elementowi tablicy liczby 5
liczby[2] = liczby[1]; // przypisanie drugiemu elementowi wartości elementu pierwszego
int i = liczby[99]; // inicjalizacja zmiennej i wartością setnego elementu tablicy
int j = 7;
cout > liczby[0] >> liczby[1] >> liczby[2]; // wczytanie z klawiatury trzech kolejnych elementów

Jak widać, na elementach tablicy można pracować podobnie jak na pojedynczych zmiennych. Jednak wczytywanie elementów tablicy bądź ich wypisywanie na ekranie można zrealizować o wiele łatwiej, niż dla zestawu pojedynczych zmiennych - poprzez korzystanie z pętli, które poznamy w lekcji piątej. Poza tym, o wiele prościej można powiększyć taką tablicę, i wiąże się to z niewielkimi zmianami w kodzie. Jednak tablice statyczne mają jedno ograniczenie - rozmiar tablicy podanej w nawiasach musi być liczbą stałą, czyli musi być znany w momencie kompilacji programu. Tablica może więc wyglądać mniej więcej w ten sposób:

Indeks : 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ...
Wartość: 2 7 5 4 8 10 1 0 -2 1 3 7 8 6 3 13 0 2 ...

Ale żeby nie było tak kolorowo, jednej rzeczy trzeba się wystrzegać - nie wolno przekroczyć zakresu tablicy. Kompilator nie sprawdza, czy podana wartość indeksu mieści się w zakresie tablicy. Przy czytaniu poza końcem tablicy (lub przed początkiem, gdy indeks jest liczbą ujemną) są dwa scenariusze - albo spowodujemy, że system wygeneruje błąd naruszenia pamięci (Access Violation) oraz bezkompromisowo przerwie wykonywanie naszego programu (fatalny scenariusz), albo odczytamy zupełnie inne dane, które mogą zawierać inne zmienne lub kod programu; w każdym razie, wynik będzie zupełnie bezsensowny i różny przy kolejnych uruchomieniach programu (scenariusz troszkę lepszy, co nie znaczy, że dobry). Natomiast próba zapisu danych poza obszar tablicy jest o wiele gorszy w skutkach - może wystąpić błąd naruszenia pamięci z podobnym skutkiem, jak przy próbie odczytu (scenariusz jeszcze nie najgorszy), lub możemy przypadkowo zmodyfikować inne dane lub kod programu (najczarniejszy scenariusz), co w efekcie może dać nieprzewidziane skutki - od całkowitego braku objawów, poprzez błędne wyniki aż do zawieszenia się programu, jego przerwania lub - w ekstremalnych przypadkach - awarii systemu operacyjnego - wtedy konieczny może się okazać restart komputera. W zasadzie w komputerach z 32-bitowym systemem operacyjnym z pamięcią chronioną, program nie ma dostępu do danych systemu zawartych w pamięci, ale - jak wiadomo - nic nie jest doskonałe. Tego typu błędy są jednymi z najtrudniejszych do zlokalizowania i usunięcia, więc jeszcze raz przestrzegam przed nimi.

Tablicę statyczną można również inicjalizować, poprzez wpisanie po przecinku w nawiasach klamrowych kolejnych wartości tablicy, np.:

int cyfry[10] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

Dzięki temu możemy tworzyć stałe tablice zawierające potrzebne nam dane - wystarczy dodać słowo const, które omawialiśmy w poprzedniej lekcji. Należy uważać na ilość danych - musi być ona identyczna, jak pojemność tablicy. Możemy jednak problem liczenia pozostawić kompilatorowi:

int cyfry[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };

Jest to dosyć wygodne, ale lepiej jednak nie pozwalać kompilatorowi na zbyt dużo - często jest to powodem wielu błędów.

C++ nakłada na tablice jeszcze jedno ograniczenie - nie można przypisywać między sobą tablic statycznych. Nie ma natomiast tego ograniczenia przy tablicach dynamicznych czy próbie przypisania tablicy statycznej do dynamicznej. Jednak tego również nie należy robić, ponieważ tablica dynamiczna nie jest niczym innym, niż wskaźnikiem, a przypisując do niego wartość zmieniamy miejsce wskazywane w pamięci, przez co poprzednia tablica jest niedostępna i nie zostaje zwrócona do systemu operacyjnego aż do zakończenia działania programu. Poza tym przypisany zostaje tylko adres nowej tablicy, co oznacza, że wszelkie operacje będą się odbywać na tych samych, oryginalnych danych. Na razie to wszystko może się wydać niezrozumiałe, ale już wkrótce wszystko sobie (mam nadzieję) wyjaśnimy.

Skoro jesteśmy przy tablicach, omówimy teraz specyficzny rodzaj tablic - łańcuchy tekstowe. Łańcuch tekstowy jest tablicą typu char, w którym kolejne elementy są kodami znaków alfabetu ASCII, i kończy się on znakiem NULL (czyli po prostu zerem). Zero oznacza koniec tekstu. Gdy nie ma zera na końcu tekstu, jako łańcuch będą interpretowane kolejne bajty pamięci znajdujące się poza tablicą, dopóki nie zostanie odczytany bajt zerowy. A chyba pamiętacie moje ostrzeżenia przed czytaniem poza zakresem?... Łańcuch tekstowy jest zawsze o jeden znak (NULL) dłuższy. Wobec tego nasz tekst "Hello, world!" posiada nie 13, a 14 znaków i tablica, która go przechowuje potrzebuje 14 mejsc.

Typ tablicy, który przed chwilą poznaliśmy, to tablica statyczna. Nazywa się ją statyczną, ponieważ jej rozmiar jest z góry ustalony i nie może ulec zmianie podczas wykonywania programu. Już za chwlę poznamy tablice dynamiczne, czyli o zmiennym rozmiarze, ale aby zrozumieć ich funkcjonowanie, musimy najpierw poznać...


Wskaźniki


Jak łatwo zauważyć, wskaźniki wskazują na coś - konkretnie na zmienne. Mając np. zmienną typu double, możemy zadeklarować kilka wskaźników na tę zmienną, które w rzeczywistości będą wskazywały to samo miejsce w pamięci - miejsce przechowywania wskazywanej zmiennej. Wskaźnik jest zazwyczaj czterobajtową liczbą całkowitą (wielkość zależy od systemu), której wartość jest adresem w pamięci. Każdy wskaźnik ma określony typ, podobnie jak zmienne. Oto, jak deklarujemy wskaźniki:


int zmienna = 12;
int *wskaznik = &zmienna;
*wskaznik = 32;
cout << zmienna;

Jak widać, wskaźniki deklarujemy poprzez zapisanie gwiazdki między nazwą typu a nazwą zmiennej. Stworzyliśmy w ten sposób nowy typ - int *, który reprezentuje wskaźnik na int. Co ważne, wskaźniki też mają określony typ - char *, float * - i nie można ich między sobą dowolnie przypisywać, bo każda ze zmiennych ma inny rozmiar, a w przypadku zmiennych rzeczywistych - również inną reprezentację w pamięci. Dlatego wskaźniki różnych typów są między sobą przypisywane tylko na wyraźne żądanie programisty - rzutowanie. Jest jeszcze jeden typ wskaźnikowy: void *. Jest to tzw. ogólny typ wskaźnikowy, i można do niego przypisać wskaźnik dowolnego typu. Jednak w drugą stronę operacja ta jest niemożliwa bez rzutowania.

Wiemy już, że operator * jest arytmetycznym znakiem mnożenia, jednak w C++ posiada on trzy znaczenia: jako znak mnożenia, jako oznaczenie w deklaracji wskaźnika oraz jako operator dereferencji. W języku programistów, wskaźnik na zmienną posiada adres tej zmiennej. Natomiast dereferencja wskaźnika polega na zwróceniu wartości wskazywanej przez ten adres. Stąd właśnie w trzeciej linii przykładu do wartości zmiennej wskazywanej przez wskaźnik przypisano wartość 32 - teraz zmienna ma wartość nie 12, a właśnie 32. Jednak przy takim przypisaniu należy koniecznie pamiętać o operatorze dereferencji - jeśli o nim zapomnimy, zamiast zmiany wartości zmienej wskazywanej, zmienimy adres tej zmiennej - a to nigdy nie wróży nic dobrego. W takim wypadku wskaźnik będzie wskazywał przypadkowe miejsce pamięci (miejsce o adresie 32 - prawdopodobnie zarezerwowane dla systemu operacyjnego), czego skutki mogą być podobne, jak przy sięganiu poza zakres tablicy. Operator & jest tak zwanym operatorem adresowym, czyli zwraca adres zmiennej, przed którą stoi. Adres ten może być przypisany wskaźnikowi - wtedy wskaźnik wskazuje dane zmiennej, a więc staje się jej aliasem.

Używanie wskaźników w taki sposób nie ma w zasadzie sensu - bo po co tworzyć zmienną, która wskazuje na inną zmienną? Główne zastosowanie wskaźników jest nieco inne. Otóż sam wskaźnik zajmuje jedynie 4 bajty pamięci. Wobec tego można zadeklarować wskaźnik na dowolny typ - bez inicjalizacji adresem innej zmiennej - a następnie w dowolnym momencie zaalokować (czyli zarezerwować) mu pamięć na zmienną. Po wykorzystaniu zmiennej można zaalokowaną pamięć zwrócić, dzięki czemu możemy zaoszczędzić pamięć. Alokacja pamięci wymaga użycia operatora new, natomiast zwolnienie pamięci przebiega po użyciu operatora delete. Oto, w jaki sposób możemy przydzielić i zwolnić pamięć:


float *p1; // deklaracja wskaźnika
p1 = new float; // przydział pamięci dla wskaźnika
char *p2 = new char; // deklaracja i przydział pamięci

... // operacje na zmiennych

delete p1;
delete p2;

Przy operacjach na wkaźnikach należy zachować pewną ostrożność. Oto kilka zasad, które pomogą uniknąć wielu błędów:

! Jeżeli nie przydzielasz pamięci od razu przy deklaracji wskaźnika, dobrze jest zainicjalizować go wartością 0. Łatwiej później sprawdzić, czy wskaźnik ma przydzieloną pamięć - wystarczy sprawdzić, czy ma on wartość 0, czy też inną. Wskaźnik sam w sobie niestety nie niesie informacji, czy pamięć dla niego jest rzeczywiście przydzielona.

! Po dealokacji (zwolnieniu) pamięci, dobrze jest przypisać wskaźnikowi wartość 0. Pozwala to uniknąć błędów dwukrotnego zwolnienia pamięci. Efekt próby zwolnienia pamięci już zwolnionej jest nieprzewidywalny. ZAWSZE, gdy piszę, że efekt jest nieokreślony lub nieprzewidywalny oznacza to, że może nic się nie stać, może zostać uszkodzony kod programu lub wartości zmiennych, a nawet program może zostać zakończony. W takich sytuacjach musimy postępować zgodnie z prawami Murphy'ego - przewidywać najgorsze.

! Pamiętaj o operatorze dereferencji, gdy chcemy zmienić wartość zmiennej wskazywanej przez wskaźnik.

! NIGDY nie zmieniaj samodzielnie adresu wskazywanej pamięci! W systemach 16-bitwych z adresacją rzeczywistą mogło to dać efekty, jednak w systemach 32-bitowych mamy tryb adresacji wirtualnej z ochroną, więc przy każdym uruchomieniu programu ten sam adres może w rzeczywistości wskazywać inne miejsce w pamięci.

! Każdy przydział pamięci dla zmiennej (operator new) musi zostać zwolniony (operator delete). W innym przypadku pamięć zostanie zwrócona do systemu operacyjnego dopiero po zakończeniu programu. Nosi to miano wycieku pamięci. Przy długich sesjach programu może zabraknąć pamięci operacyjnej. Jest to bardzo poważny błąd, w dodatku jeden z najtrudniejszych do zlokalizowania.

! Uważaj na sytuację, gdy wskaźnik został zainicjalizowany adresem innej zmiennej. Zwolnienie takiego wskaźnika oznacza, że do zmiennej, na którą wskazywał, nie można się odwoływać! Może również zostać zgłoszony błąd. Bardzo uważaj na takie błędy.


Warto zapamiętać, że indeks tablicy jest zamienny z operatorem dereferencji, a więc do wartości wskazywanej przez niego można się dostać na dwa sposoby:

int *p = new int;
*p = 1; // sposób 1.
p[0] = 21; // sposób 2.
delete p;

W przypadku wskaźników na pojedyncze zmienne lepiej stosować sposób 1. Sposób drugi - za pomocą indeksu - przyjął się do tablic dynamicznych. Warto więc stosować rozróżnienie za pomocą tych operatorów, bo czasami można nie wiedzieć, czy dany wskaźnik jest po prostu wskaźnikiem, czy też tablicą dynamiczną; kompilator też tego nie sprawdza. W tym samym wskaźniku z poprzednich dwóch przykładów możliwa byłaby operacja:

p[1] = 100; // UWAGA!

W najlepszym przypadku spowodowałoby to zmianę wartości zmiennej zadeklarowanej zaraz po naszym wskaźniku. Jednak nadal mamy tu przekroczenie zakresu tablicy. Mam nadzieję, że już sam ten termin powoduje u was atak dreszczy... Dlatego ostrzegam raz jeszcze (i będę to robił przy każdej okazji): PAMIĘTAJCIE O ZAKRESACH!

Jeszcze jedna uwaga: jak niektórzy zauważyli, w przykładzie z łańcuchem tekstowym w komentarzu zapisałem, że &text[0] (czyli adres pierwszego elementu tablicy) jest tym samym, co text (czyli nazwa zmiennej tablicowej). Jest to związane z tym, że kompilator wewnętrznie interpretuje każdą tablicę statyczną jako wskaźnik. Jest to istotna informacja, którą będziemy często wykorzystywać, na przykład przy lekcji o funkcjach. Jest jednak spora różnica między tablicą statyczną a wskaźnikiem czy też tablicą dynamiczną (która w sumie też jest wskaźnikiem, ale ma przydzieloną większą ilość pamięci). Tę różnicę zauważymy na przykładzie. Użyjemy tutaj operatora sizeof, który zwraca wielkość (w bajtach) zmiennej lub typu:

double stat[12];
double *dyn = stat;

cout << "Wielkość tablicy statycznej: " << endl << sizeof(stat) << endl;
cout << "Wielkość tablicy dynamicznej: " << endl << sizeof(dyn) << endl;

Oczywiście nie jest prawdą, że tablica dynamiczna zajmuje tylko cztery bajty. Po prostu zwracana tutaj wielkość jest wielkością wskaźnika. Wobec tego nie można ustalić liczby elementów tej tablicy, w przeciwieństwie do tablicy statycznej - za pomocą instrukcji sizeof(stat)/sizeof(double). Ale kompilator leniwy jest i nie pilnuje przekroczenia zakresu w tablicach statycznych (w przeciwieństwie do np. Pascala), więc musielibyśmy robić to ręcznie. Stąd jeszcze jedno podobieństwo tablicy statycznej do wskaźnika.


Tablice dynamiczne


Wiemy już, jak posługiwać się wskaźnikami na pojedyncze zmienne. Poznaliśmy również możliwość tworzenia pseudotablicy dynamicznej poprzez użycie wskaźnika na zmienną statyczną. Na szczęście możemy sami przydzielić wskaźnikowi pamięć na, powiedzmy, sto zmiennych. A później możemy tę pamięć zwolnić. Potem przydzielić pamięć na dwie zmienne... Jednym słowem - wskaźnik może działać jako typowa tablica dynamiczna.

Oczywiście, sposób deklaracji różni się od deklaracji wskaźnika na pojedynczą zmienną, jednak różnica jest naprawdę niewielka. Oto, w jaki sposób możemy stworzyć tablicę 65 liczb rzeczywistych:

float *p = new float[65];

Jak widzimy jedyna różnica, to podanie w nawiasach kwadratowych ilości miejsc w tablicy. Do takiej tablicy możemy odnosić się podobnie, a wręcz identycznie, jak do tablicy statycznej. Indeksy również są zliczane od zera. W zasadzie ktoś mógłby powiedzieć, że deklaracja tablicy dynamicznej nie rożni się od statycznej. Jednak różnica między nimi jest dość znaczna - i nie chodzi mi tylko o to, że tablicę dynamiczną możemy zwolnić w dowolnym momencie. Istota działania tablic dynamicznych polega na tym, że wartością w nawiasach kwadratowych może być zmienna! Wobec tego możliwa jest taka operacja:

int index;
cin >> index;
int *tab = new int[index];

Tworzymy tutaj tablicę o rozmiarze podanym podczas wykonywania programu. Oczywiście mam nadzieję, że nie muszę już powtarzać apelu o nieodwoływanie się poza zakres tablicy?...

Zwolnienie tak przydzielonej pamięci również wygląda trochę inaczej. Zamiast operatora delete musimy użyć operatora delete[]. I jest to istotna różnica, nie można bowiem pamięci zaalokowanej operatorem new[ilość_miejsc] (czyli tablicy dynamicznej) zwolnić operatorem delete i odwrotnie. Reakcja na taką operację jest, jak zwykle, nieprzewidziana.


Arytmetyka wskaźników


W tym rozdziale dowiemy się jeszcze więcej o wskaźnikach, a konkretnie o ich współpracy z tablicami. Na początek stwórzmy sobie dwie tablice:

float bilon[9] = { 0.01, 0.02, 0.05, 0.10, 0.20, 0.50, 1.00, 2.00, 5.00 };
int banknot[5] = { 10, 20, 50, 100, 200 };

Stworzymy sobie też dwa wskaźniki na te tablice:

float *pbilon = &bilon[0]; // pierwszy sposób pobrania adresu - bardziej dosłowny
int *pbanknot = banknot; // drugi sposób pobrania adresu - prostszy

Teraz wskaźnik pbilon wskazuje na pierwszy element tablicy bilon, a wskaźnik pbanknot wskazuje na pierwszy element tablicy banknot. Teraz trochę teorii: wskaźnik jest (zazwyczaj - zależnie od systemu) czterobajtową liczbą całkowitą - podobnie, jak typ long. Wiemy, co się stanie, gdy do zmiennej long dodamy jedynkę - to jest oczywiste, będzie większa o 1. Ale co stanie się, gdy dodamy jedynkę do wskaźnika? Gdyby dodać jeden do adresu, to wskaźnik wskazywałby następny bajt w pamięci. Na szczęście (czy też nie) C++ jest na tyle sprytny, że w takim przypadku doda do adresu wielkość typu, dzięki czemu wskaźnik będzie wskazywał następną zmienną typu, jaki wskazuje. Zostanie więc przesunięty nie o 1 bajt, a o rozmiar zmiennej typu wskazywanego. Co to oznacza? Spójrzmy na poniższy przykład (w tym rozdziale będziemy korzystać ze zmiennych, które wcześniej zadeklarowaliśmy):

cout << pbilon[0] << " " << pbilon[1] << " " << pbilon[2] << endl;
pbilon += 2;
cout << pbilon[0] << " " << pbilon[1] << " " << pbilon[2] << endl;

W pierwszej linii program wyświetli ciąg: 0.01 0.02 0.05. W drugiej linii wskaźnik zostaje (teoretycznie) zwiększony o 2 i wskazuje trzeci element tablicy, więc ciąg w trzeciej linii będzie wyglądał tak: 0.05 0.10 0.20. Gdyby zwiększenie wskaźnika o 2 powodowało przesunięcie o 2 bajty, to odczytane dane byłyby idiotyczne, bo typ double ma wielkość 8 bajtów.

Z tym wiąże się również dereferencja (lub też indeksowanie tablicy, jak kto woli). Otóż indeksowanie tablicy nie jest jedynym sposobem na dostanie się do poszczególnych elementów tablicy. Mówiłem już, że za pomocą indeksu można dostać się do wartości wskaźnika na pojedynczą zmienną. To działa również w drugą stronę (bo wskaźnik = tablica): za pomocą operatora dereferencji (*) możemy dostawać się do poszczególnych elementów tablicy:

// odwołanie za pomocą indeksu
cout << pbilon[0] << " " << pbilon[1] << " " << pbilon[2] << " " << pbilon[3] << endl;

// odwołanie za pomocą dereferencji
cout << *pbanknot << " " << *(pbanknot+1) << " " << *(pbanknot+2) << endl;

Oba z tych sposobów są równoważne, jednak o wiele czytelniejszy jest sposób indeksowania. W drugim sposobie musieliśmy wskaźnik podać w nawiasach, ponieważ operator dereferencji ma wyższy priorytet od operatora dodawania, czyli wykonywany jest pierwszy. Wtedy odczytywana byłaby wartość wskazywana przez sam wskaźnik, a nie przez wskaźnik przesunięty o wybraną wartość. Warto znać oba sposoby, bo pozwala to na większą elastyczność, np. w sposobie z operatorem dereferencji można zastosować parę trików ułatwiających pracę, często trudnych do zrobienia przy indeksowaniu. Podczas kolejnych lekcji poznamy niektóre z nich.


Enumeratory


Dość często potrzebne są nie liczby, a etykiety, które możemy przypisać jakiemuś obiektowi - mogą to być na przykład jednostki masy. Nie będzie tu wygodne stosowanie liczb, które będą kolejnymi indeksami, np. 1 - kilogram, 2 - funt, 3 - uncja... Do tego celu wygodniejsze jest stosowanie enumeratorów.

W zasadzie sama budowa enumeratorów również korzysta z liczb indeksowych, bo komputer rozumie tylko i wyłącznie liczby. Jednak dla nas, jako programistów, o wiele łatwiej zapamiętać nazwy - więcej nam one mówią od liczb. Kompilator i tak podstawi tam liczby - ale nas to już nie obchodzi. :) Oto, jak można zadeklarować przykładowy enumerator:

enum jednostka {
  kilogram, funt, uncja, tona
 }; // na końcu deklaracji musi być średnik

W ten sposób zadeklarowaliśmy enumerator o nazwie jednostka, który reprezentuje jednostki masy. Teraz możemy zadeklarować zmienną typu jednostka:

jednostka j;
j = kilogram;
jednostka k = j;

Jak już mówiłem, kompilator i tak zamienia etykiety enumeratora na liczby. Jednak nie traktuje enumeratora jako typu liczbowego. A więc przypisanie liczby do enumeratora i odwrotnie może zajść tylko na wyraźną prośbę programisty - poprzez rzutowanie.

A teraz pytanie: na jakie liczby zamienia kompilator etykiety? Na pewno na liczby całkowite - cały komputer liczy tylko i wyłącznie na liczbach całkowitych, nawet liczby zmiennoprzecinkowe są reprezentowane przez liczby całkowite. Nasze etykiety zostaną zamienione na kolejne liczby naturalne, zaczynając od 0 (identycznie jak indeksy w tablicy), więc etykiety naszego enumeratora będą wyglądały tak:

kilogram - 0
funt     - 1
uncja    - 2
tona     - 3

Jak widać numerowanie jest identyczne jak w przypadku tablic. Możemy również nadać własne numery etykietom, ba - możemy również kilku etykietom nadać tę samą wartość. Robimy to w bardzo prosty sposób:

enum jednostka {
  kilogram, funt = 2, uncja, tona = 0
 };

Teraz nasze etykiety będą wyglądały tak:

kilogram - 0
funt     - 2
uncja    - 3
tona     - 0

Kompilator każdej następnej etykiecie, której wartości nie zdefiniujemy, nadaje wartość o jeden większą od poprzedniej, dlatego gdy etykiecie funt nadaliśmy wartość 2, to następna etykieta (uncja) przyjęła wartość 3. Etykieta następna po tonie miałaby wartość 1, następna 2 itd. Z reguły lepiej nie ustalać samodzielnie wartości ani nie rzutować wartości etykiet na liczby ani odwrotnie - chyba, że rzeczywiście jest to konieczne. Łatwo bowiem przekroczyć zakres enumeratora, powtórzyć wartości etykiet itp. Więcej o przydatności enumeratorów dowiemy się w lekcji o sterowaniu przebiegiem programu.


Struktury


A co, jeśli w zmiennej chcemy umieścić wszystkie dane osobowe (jak napisałem w podtytule)? W tym celu najlepiej jest wykorzystać struktury. Pozwalają one na upakowanie kilku typów zmiennych w jednej strukturze, która również jest zmienną. W taki sposób deklarujemy strukturę:

    struct DANE
    {
        char imie[15];
        char nazwisko[25];
        char plec;
        // inne zmienne lub struktury
    }; // pamiętaj o średniku na końcu deklaracji!

Struktury definiujemy poza funkcją main() i przed jej pierwszym użyciem. Teraz można utworzyć zmienną (obiekt) typu DANE. Do pól struktury odwołujemy się za pomocą operatora odwołania, czyli kropki. Wobec tego piszemy:

DANE osoba;

cout << osoba.imie;
cin >> osoba.nazwisko;
osoba.plec = 'M';

Struktury są bardzo przydatnym i jednym z bardziej funkcjonalnych elementów języka C++. W przyszłości poznamy klasy, które znacznie rozwijają założenia i możliwości struktur. Co ważne, możemy również stworzyć również tablicę struktur. A w strukturach możemy jako pola zawierać inne struktury lub tablice - statyczne bądź dynamiczne - struktur. A te z kolei mogą zawierać jeszcze inne struktury itd. Jest to jeden z rodzajów dziedziczenia, które jest podstawą programowania obiektowego. Klasy znacznie rozwijają i ulepszają mechanizm dziedziczenia, ale wszystko w swoim czasie.

Struktury możemy również inicjalizować podobnie jak proste zmienne, a sposób ich inicjalizacji jest podobny do inicjalizacji tablic:

DANE ktos = { "Hermenegilda", "Kociubinska", 'K' };

Dla większej czytelności wartości poszczególnych pól możemy zapisywać w osobnych liniach, np.:

    DANE d = { "Victor",
               "Troska",
               'M'
              };

Pamiętajcie jednak o kolejności pól! Muszą one być inicjalizowane zgodnie z deklaracją.

Uzbrojeni w nową wiedzę, spróbujmy napisać program, który prosi o podanie danych osobowych, a następnie je wyświetla.

dane.cpp

#include <cstdlib>
#include <iostream>

using namespace std;

struct DANE
{
    char imie[12];
    char nazwisko[24];
    char plec;
    char tel[15];
}; // średnik!

int main(int argc, char *argv[])
{
    DANE osoba;
    
    // wczytywanie danych
    cout << "Podaj dane osobowe:" << endl;
    cout << "Imie: ";
    cin >> osoba.imie;
    cout << endl << "Nazwisko: ";
    cin >> osoba.nazwisko;
    cout << endl << "Plec (M/K): ";
    cin >> osoba.plec;
    cout << endl << "Nr Telefonu: ";
    cin >> osoba.tel;
    
    // Wypisywanie danych
    cout << endl << endl << endl;
    cout << "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+" << endl;
    cout << "Imie i nazwisko:" < endl;
    cout << osoba.imie << " " << osoba.nazwisko << endl << endl;
    cout << "Plec: " << osoba.plec << endl << endl;
    cout << "Telefon: " << osoba.tel << endl;
    cout << "+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+" << endl << endl;

    system("PAUSE");
    return EXIT_SUCCESS;
}

Muszę jeszcze dodać jedną, bardzo ważną uwagę. Typ łańcuchowy (char*) nie jest zwykłym typem wbudowanym, ale typem wskaźnikowym, więc nie można normalnie przypisywać do niego wartości, jak również porównywać czy sklejać dwa łańcuchy w jeden. Przypisanie powoduje skopiowanie tylko wskaźnika, przez co oba wskaźniki wskazują to samo miejsce w pamięci, a przy porównaniu porównywane są wartości adresów. Aby dokonać skopiowania, porównania czy sklejenia dwóch łańcuchów, należy dodać plik nagłówkowy cstring (lub string.h) za pomocą dyrektywy #include. Do wykonywania wspomnianych operacji udostępniony jest zestaw funkcji, które są opisane w dodatku do tego kursu. Na razie wróćmy do naszych struktur.

Można również korzystać z tablic struktur oraz z wskaźników na struktury. Korzystanie z tablic struktur realizowane jest w sposób identyczny, jak przy typach prostych:

DANE osoby[10];

strcpy(osoby[2].imie, "Neil"); // funkcja kopiująca łańcuchy; opisana w dodatku
strcpy(osoby[2].nazwisko, "Armstrong");
osoby[2].plec = 'M';
strcpy(osoby[3].imie, "Edwin");
strcpy(osoby[3].nazwisko, "Aldrin");
osoby[3].plec = 'M';

Tablice struktur również możemy inicjalizować.

Odbywa się to w dość intuicyjny sposób. Natomiast podczas używania wskaźnika na strukturę, to aby odwołać się do jej pól, musimy skorzystać z tzw. pośredniego operatora odwołania, który przybiera postać "strzałki":

DANE *osoba;

strcpy(osoba->imie, "Hieronim");
strcpy(osoba->nazwisko, "Maruszeczko");

Oczywiście można również korzystać z operatora dereferencji, ale ze względu na kolejność operatorów musielibyśmy stosować udziwniony zapis:

cin >> (*osoba).imie;

Można również korzystać z indeksowania, ale mówiłem już, że we wskaźnikach na pojedyncze zmienne jest to sposób wysoce niewskazany.

Tutaj mam dodatkową uwagę. Ponieważ w początkach mojej zabawy z C++ stosowanie odpowiednich operatorów odwołania sprawiało mi problemy, chciałbym pomóc wam uniknąć takich sytuacji. Rozważmy np. taki przypadek:

struct DANE_EX
{
    DANE dane; // wykorzystujemy strukturę z poprzedniego przykładu
    char fax[12];
    char pesel[12];
};

DANE_EX *dane_ex;

Wiemy, że do wartości fax lub pesel odwołujemy się poprzez strzałkę - bo posługujemy się wskaźnikiem na strukturę. Co natomiast z zawartą w niej drugą strukturą? Ponieważ pole 'dane' nie jest wskaźnikiem, więc tutaj już należy użyć kropki. Więc wczytanie wartości imie do obiektu dane_ex będzie wyglądać tak:

cin >> dane_ex->dane.imie;

Ja początkowo myślałem, że jeżeli do "pierwszego poziomu" pól struktury odwołujemy się za pomocą strzałki, to tak samo automatycznie dzieje się dla pól zawartych struktur. Jednak do pól struktury dostajemy się za pomocą odwołania pośredniego jedynie wtedy, gdy posługujemy się wskaźnikiem na tę strukturę. A jak już wspomniałem, pole 'dane' NIE JEST wskaźnikiem. Uff...

Gdyby natomiast składowa 'dane' struktury DANE_EX była wskaźnikiem, odwoływalibyśmy się do niej za pomocą strzałki. Czyli wyglądałoby tak:

cin >> dane_ex->dane->imie;

Początkowo może to sprawiać pewne problemy, jednak dość szybko można sobie to przyswoić. Temat struktur nie został został tutaj potraktowany nieco po macoszemu; większe możliwości struktur pokażę w lekcji #7, w której poznamy programowanie obiektowe. Natomiast tutaj dowiedzieliśmy się chyba wszystkiego o wskaźnikach. I o to chodziło, bo C++ funkcjonuje głównie właśnie dzięki wskaźnikom. Cała reszta nie istniałaby bez nich. We wskaźnikach tkwi właśnie potęga C++.

Unie


Unie są wykorzystywane niezwykle rzadko, jednak czasem są przydatne (co nie znaczy - niezbędne). Unia to taka postać danych, która pozwala zapisywać różne typy danych, ale zawsze tylko jeden na raz. Wobec tego struktura pozwala np. na zapis wartości long i wartości double oraz wartości char, a unia pozwala na zapis wartości long lub wartości double lub wartości char. Składnia unii jest identyczna ze składnią struktur, jednak działa ona inaczej. Oto przykład:

union Przykladowa_unia
{
    char a[4];
    short b;
    float c;
};
    
Przykladowa_unia test;
    
test.a = "abc";
cout << test.a;
    
test.b = 123;
cout << test.b;
    
test.c = 12.3;
cout << test.c;
W zasadzie unia to zestaw zmiennych o tym samym adresie w pamięci. Wielkość unii to wielkość największego jej elementu. Wygląda to tak, że bajty pamięci są interpretowane na potrzeby wybranego typu. Jak już wspomniałem, unie nie mają szerszego zasosowania. Nie ma sytuacji, w której byłyby one niezbędne, ale czasami mogą dość znacznie ułatwić pracę.


Pojedyncze zmienne strukturalne


Czasami trzeba stworzyć tylko jedną zmienną o unikalnej strukturze, której typ nie musi, a nawet nie powinien być udostępniony. Wtedy możemy skorzystać z innego sposobu deklaracji:

struct
{
    int x;
    int y;
    int z;
} point3d;

Teraz mamy tylko jedną zmienną, której typ jest nieokreślony, a więc nie można na niego rzutować itp. W ten sam sposób możemy również deklarować enumeratory. Można również stworzyć kika zmiennych tego samego, choć nieokreślonego, typu - wystarczy na końcu podać po przecinku ich nazwy:

enum {
    mg, g, kg, t
} masa1, masa2;

Z takich konstrukcji korzysta się dość rzadko, ale warto wiedzieć o takiej możliwości.


To tyle na dziś. Ta lekcja była długa i nieco monotonna - prawie w całości poświęcona wskaźnikom. Niestety, jest to temat trudny i konieczny; bez wskaźników nie można w C++ napisać prawie nic (poza najprostszymi przykładami). Przy okazji: musiałem nieco zmienić kolejność lekcji, mianowicie lekcję o zasięgu zmiennych i przestrzeniach nazw przesunę o dwie lekcje - na miejsce szóste. Za miesiąc więc będzie lekcja o sterowaniu przebiegiem programu, później lekcja o funkcjach, a następnie o zasięgu zmiennych; omawianie zasięgu zmiennych bez wiedzy o blokach i funkcjach będzie bezproduktywne. A więc za miesiąc powiemy sobie trochę o podejmowaniu decyzji przez komputer. Jak zwykle zachęcam do pisania na mój mail - z wszelkimi problemami, wątpliwościami czy też pomysłami. Służę również poprzednimi częściami kursu - bez nich wiele się nie nauczycie. koment_new('A ja tu się zastanawiałem, dlaczego nic z tego nie rozumiem ;)','WRIM',240);

autor("ArchiE","archie007@wp.pl")



Wyszukiwarka

Podobne podstrony:
k cpl
k cpl2
k cpl?
k cpl
k cpl1
k cpl?
k cpl?
k cpl
r08 cpl t (3)
t p cpl
k cpl0
k cpl0
k cpl
k cpl
k cpl1
k cpla

więcej podobnych podstron