profil

C++ - wykład 3

poleca 85% 119 głosów

Treść
Grafika
Filmy
Komentarze



Wykład 3 - 4 godz



Zakres tematyczny:

1. Operatory języka C/C++

2. Typy zmiennych



1. Operatory języka C/C++

Język C/C++ w porównaniu z innymi językami dostarcza programiście znaczną liczbę różnorodnych operatorów, umożliwiających bardzo efektywny zapis algorytmów. Aby jednak uniknąć późniejszych bardzo frustrujących i trudnych do wykrycia błędów, należy poświęcić nieco czasu na zapoznanie się dokładnie z tymi operatorami i zasadami konstrukcji wyrażeń. Duża różnorodność tych operatorów decyduje właśnie o unikalności języka C. Bo czy w innych językach programowania byłaby dopuszczalna sekwencja jak najbardziej prawidłowa w języku C:?

a = i +++j;

++x*=(a!=0)?a:b++;

Jak widać, wyrażenia w języku C/C++ są unikalne i nie przypominają Państwu nic wcześniej poznanego (chociażby z Pascala).

Kluczową rolę w konstrukcji wyrażeń w każdym języku odgrywają operatory. To właśnie one określają, co w jaki sposób i w jakiej kolejności zostanie obliczone. Operatory należą do jednej z dwu grup:

- operatory jednoargumentowe (unarne) wiązane z jednym operandem

- operatory dwuargumentowe (binarne) wiązane z dwoma operandami.

Język C++ zawiera wszystkie operatory języka C, a ponadto dołącza jeszcze pięć charakterystycznych dla siebie operatorów. W dalszej części wykładu omawiać będziemy kolejno wszystkie operatory.



Operatory arytmetyczne

-operatory : + - * /

nie wymagają szczegółowego wyjaśnienia. Należy tylko dodać, że należą one do grupy operatorów binarnych (ponieważ operują na dwóch obiektach - operandach).



- operator % czyli modulo

Jest on podobnie jak poprzednie operatorem binarnym. W wyniki działania tego operatora otrzymujemy resztę z dzielenia operandów stojących po obu stronach operatora:

45 % 6 -> 3

Operatora tego, nie można stosować dla typów: float i double. Dla ujemnych argumentów operacji kierunek zaokrąglania wyniku jak i znak liczby która jest wynikiem dzielenia modulo zależne są od maszyny.

Priorytet operatorów *, /, % jest wyższy niż operatorów +, -.



- operatory jednoargumentowe + ,-

Te operatory też są znane. Operator + właściwie nic nie robi, natomiast operator - zmienia warotść danego wyrażenia na przeciwną :

-(2*a + b)

Pamietajmy, że nie chodzi tu o żadne odejmowanie, ale o operację stworzenia liczby przeciwnej.



- operatory inkrementacji i dekrementacji

Operacje zwiększania i zmniejszania o jeden, występują w programowaniu niezmiernie często. Ze względu na to, większość procesorów wykonuje ją w jednym rozkazie. Język C/C++ aby to zrealizować, zapewnił osobny operator umożliwiający wykonanie tych działań. Z wyrażeniami np:

i = i +1;

i = i - 1;

mamy szczególnie często do czynienia w pętlach. Można je dla prostoty zastąpić poprzez wyrażenia:

i++;

i--;

Operatory dekrementacji i inkrementacji mogą mieć dwie formy:

- przedrostkową ( prefix): ++i;

- końcówkową (postfix): i++;

W obu przypadkach wynikiem jest zmiana wartości i, ale wyrażenie ++i zwiększa i przed użyciem jej wartości, natomiast wyrażenie i++ zwiększa zmienną i dopiero po użyciu jej poprzedniej wartości. W kontekście, w którym ważna jest wartość zmiennej i, a nie tylko jej zmiana, wyrażenia ++i oraz i++ są różne np:

i = 10;

x = i++;

cout<<" X = "<
daje w rezultacie X = 10, ale

i = 10;

x = ++i;

cout<<" X = "<
daje w rezultacie X = 11.



!!! Zadanie:

int m=3,n=1,r;

r = (m++) + (++n);

Jaka jest wartość r? ( r = 5);



Ponieważ są to operatory unarne (jednoargumentowe) to wyrażenie :

(i+j)++

jest błędne;

W przypadku, gdy chodzi tylko o sam efekt zwiększania o jeden, operatory post i prefiks-owe są równoważne.



- operator przypisania =

Powoduje on, że do obiektu stojącego po lewej stronie operatora przypisania zostaje zapisana wartość wyrażenia stojącego po prawej stronie. Jest to operator dwuargumentowy. Z założenia, operandy stojące po obu stronach powinny mieć taki sam typ. Jeśli natomiast nie mają, to jeśli to jest możliwe wykonana zostaje niejawna konwersja typów. O konwersji typów mówić będziemy osobno po omówieniu pozostałych typów zmiennych.











Operatory logiczne

- operatory relacji

Są to operatory: > >= < <=

W wyniku działania tych operatorów otrzymujemy odpowiedź: prawda (true - 1) lub fałsz (false - 0). Priorytet tych operatorów jest taki sam .



- operatory przyrównania

Są to operatory == !=.

Ich priorytet jest niższy niż priorytet operatorów relacji

Obie te grupy operatorów znalazły zastosowanie przy sprawdzaniu warunków np. w przypadku instrukcji if. Bardzo częstym błędem jest w przypadku używania operatorów przypisania pisanie jednego znaku = . Może to doprowadzic do powstania błędu, który niekoniecznie musi być wychwytywany przez kompilator (błąd logiczny). Weźmy np. fragment programu:

Przykład1

#include

main()

{

int x = 3, x1 = 10;

if(x = x1) //błąd - przypisanie - nawias = 100

// 100 != 0 - czyli wynik - TRUE

cout<<"x = x1";

else

cout<<"x != x1);

return 0;

}

W efekcie program twierdzi, że 3 = 10. Co za absurd. Niektóre kompilatory między innymi i Borland C++ wychwytują takie błędy, gdy po if występuje operacja przypisania, a nie operator przyrównania. Tak na wszelki przypadek.



- operatory sumy i iloczynu logicznego

Są to operatory:

|| - realizujący sumę logiczną (LUB - alternatywa)

&& - realizujący iloczyn logiczny (I - koniunkcja)

Przykład2

#include

main()

{

int ch;

ch = getchar();

if((ch >='a' && ch <='z')) || (ch >='A' && ch <='Z'))

cout<<"wprowadzony znak jest literą";

else

cout<<"Wprowadzony znak jest innym znakiem";

return 0;

}

Przypomnijmy, że wynik wyrażenia logicznego "prawda" daje 1, a "fałsz" daje 0. Wyrażenia logiczne obliczane są od lewej do prawej. Jest zasada, że komputeeń dlicza wartości wyrażci sopóty, dopóki nie jest pewien wyniku. Weźmy np. fragment kodu:

(x1 == 0) && (x1 == 15) && (z>10)

Komputer oblicza wartość wyrażenia od lewej do prawej. Jeśli zdarzy się taki przypadek, że już pierwsze wyrażenie jest nieprawdziwe, to komputer nie oblicza dalej, ponieważ jest już pewien, że taka koniunkcja nie może być prawdziwa. Na tą cechę operacji trzeba bardzo uważać np.:

int i = 6,d = 4;

if((i>0) && (d++))

{

...............

}

W instrukcji tej nie tylko wykonywana jest operacja logiczna , ale także chcemy zwiększyć wartość zmiennej d. Jest to swego rodzaju pułapka. Bo gdyby jak mówiliśmy, wartość (i>0) nie była prawdziwa to nie dojdzie do zwiększenia d, na czym nam mogło szczególnie zależeć. Podobne pułapki mogą dotyczyć i alternatywy zgodnie z zasadami matematyki.

Priorytet operatora && jest większy niż operatora ||, ale oba są niższe niż operatorów przyrównania i relacji, więc w wyrażeniu

i < gr - 1 && (c = getchar()) != '
' 77 c!= EOF

nie potrzeba dodatkowych nawiasów. Ponieważ jednak priorytet operatora != jest wyższy niż przypisania, więc w zwrocie (c=getchar()) != '
'

potrzebne są nawiasy. Najpierw bowiem ma zostać przypisana wartość zmiennej c, a potem dopiero przyrównana do '
'.



- operator negacji !

Jest to operator jednoargumentowy i stoi zawsze po lewej stronie operandu:

!n

Jeśli n = 0, to wartością wyrażenia jest PRAWDA.

Instrukcja: if(!n) jest równoważna instrukcji: if(n == 0)

Przykład 4

int i = 0;

if(!i)

cout<<"Wartość i jest zerowa";



Operatory bitowe

Operatory te są charakterystyczne dla sposobu przechowywania informacji w komputerze (w postaci binarnej). Wiadomo, że w komputerze informacja gromadzona jest w postaci zero-jedynkowej. Te elementarne jednostki informacji (0 lub 1) nazywane są bitami. Komputer jednak nie odnosi się najczęściej do poszczególnych bitów, lecz gromadzi je w słowa, najczęściej 2 bajtowe. Informacje zapisane w słowie nie koniecznie muszą oznaczać konkretnych liczb. W jednym słowie można przechowywać szereg informacji na poszczególnych bitach słowa. Do pracy na poszczególnych bitach słowa służa właśnie operatory bitowe:

<< - przesunięcie w lewo

>> - przesunięcie w prawo

& - bitowy iloczyn logiczny

| - bitowa suma logiczna

^ - bitowa różnica symatryczna (bitowe exclusive OR)

~ - bitowa negacja



- operator przesunięcia w lewo <<

Jest to operator unarny, operujący na operandach całkowitych (niezależnie od znaku):

zmienna << ile miejsc

Służy do przesuwania bitów operandu stojącego po lewej stronie operatora o liczbę pozycji określoną przez drugi operand (jego wartość musi być dodatnia). Zwolnione bity zostają uzupełnione zerami:

int x = 0x1010

int wynik;

wynik = x << 2;

Powyższy fragment programu przesunie bity liczby x o 2 miejsca w lewo:

x = 0001 0000 0001 0000

wynik = 0100 0000 0100 0000

W powyższym przykładzie sam obiekt - zmienna x nie została zmieniona. Posłużył on jako wartość początkowa, a rezultat operacji został przechowany w zmiennej wynik.

Jeśli chodzi nam o to, aby przesunąć bity w danej zmiennej i tam też tą operację zapisać, to działamy podobnie jak w przypadku wyrażenia : a = a + 5, czyli:

x = x << 2.

Operacja ta ( x<<2) jest równoważna pomnożeniu zmiennej przez cztery:

x 0001 = 1

x<<2 0100 = 4



- operator przesunięcia w prawo >>

Podobnie jak poprzedni operator, działa jedynie na operandach całkowitych. Przesuwa bity operandu stojącego polewej stronie operatora o ilość bitów wskazaną przez operand prawy.

Bity z prawego brzegu są gubione. Jest jedna cecha tego operatora, która różni go od poprzedniego:

Jeśli operator pracuje na danej unsigned( bez znaku) , lub signed( ze znakiem) ale jest to liczba ujemna, to bity z lewego brzegu są uzupełnione zerami lub jedynkami zależnie od implementacji.

unsigned int x = 0x0ff0;

unsigned int wynik;

wynik = x >>2;

x 0000 1111 1111 0000

wynik 0000 0011 1111 1100



signed int f = 0xff00;

signed int wynik;

wynik = x>>2;

x 1111 1111 0000 0000

wynik 0011 1111 1100 0000

wynik1 1111 1111 1100 0000

Implementacja Borland C++ daje wynik1.







- operatory sumy, iloczynu, negacji ,różnicy symetrycznej

Operatory te także działają na operandach całkowitych.

int x1 = 0x0f0f;

int x2 = 0x0ff0;

x1 0000 1111 0000 1111

x2 0000 1111 1111 0000

x1 & x2 0000 1111 0000 0000 zasłania pewną grupę bitów

x1 | x2 0000 1111 1111 1111 służy do ustawiania bitów

x1 ^ x2 0000 0000 1111 1111 tam gdzie bity operandów są takie same ustawia 0, a gdzie różne 1

Operacja XOR( różnica symetr.)

~ x1 1111 0000 1111 0000 zamienia bit 1 na 0 i odwrotnie.

Teraz podam Państwu przykład zastosowania operatorów bitowych w konkretnym prostym programie:

Przykład 5

/* funkcja getbits wycina n bitów ze zmiennej x od poz. p */

unsigned getbits(unsigned x, int p, int n)

{

return(x >> (p+1-n)) & ~(~0<
}

Funkcja zwraca n -bitowe pole wycięte ze zmiennej x od pozycji p, dosunięte do prawej strony wyniku. Wyrażenie x>>(p+1-n) dosuwa wybrane pole do prawego końca słowa. Wyrażenie ~0 oznacza same jedynki; przesunięcie ich w lewo o n pozycji bitowych (~0<


- różnica między operatorami logicznymi i bitowymi

Należy pamiętać, że wynikiem działania operatorów logicznych jest wynik "prawda lub falsz". Kompilatora nie interesuje analiza poszczególnych bitów, sprawdza tylko, czy jest w operandach zapisana 0 czy != 0. Na tych dwóch wartościach typu TRUE , FALSE dokonuje koniunkcji, a wynik jest = 0 lub 1.

Natomiast operatory bitowe wkraczają do wnętrza słowa , analizują poszczególne bity. Na nich dokonują np. koniunkcji bit po bicie i przepisują wynik 0 lub 1 na odpowiadający im bit zmiennej wynikowej. W rezultacie otrzymujemy wynik, będący specyficznym układem bitów, które mozna traktować jako liczbę, której wartość można np wydrukować:

cout<< (x1 & x2); // = 3840



- inne operatory przypisania.

Są to bardzo proste operatory. Wyrażeniem podobnym do:

i = i + 2;

jest bardziej zwięzły zapis:

i + = 2;

Taki zapis można stosować dla większości operatorów binarnych. Czyli np.:

+=, -=, *= itd.

Jednak takie skondensowane zapisy dla początkujących mogą być źródłem wielu błędów logicznych wynikających z błędnego wykorzystania tych operatorów. I tak np.:

x *= y + 1;

jest odpowiednikiem wyrażenia:

x = x * (y+1);

a nie : x = x * y + 1;

Działa tu bowiem zasada:

wyr1 op = wyr2 to to samo co wyr1 = (wyr1) op (wyr2)

Operatory te w przypadku skomplikowanych wyrażeń umożliwiają czytelniejszy zapis, poza tym pomagają kompilatorowi generowanie efektywniejszego kodu wynikowego, zmiejsza ilość koniecznych do wykonania operacji z dwóch do jednej.

Podam teraz przykład zastosowania operatorów w funkcji zliczającej bitowe jedynki argumentu całkowitego.



Przykład 6

int bitcount( unsigned x)

{

int b;

for(b = 0; x!=0; x>>=1)

if(x & 01)

b++;

return b;

}

Zadeklarowanie argumentu jako unsigned upewnia, że podczas przesuwania w prawo miejsca uzupełniane są zerami nie bitem znaku liczby.



Wyrażenie warunkowe

Widzimy, że język C idąc w kierunku zapewnienia efektywności kodu wynikowego programu, stosuje bardzo wiele skrótów. Innym skrótem jest badanie warunków przy pomocy wyrażenia warunkowego.

Instrukcja :

if( a > b)

z = a;

else

z = b;

może być zastąpiona trzyargumentowym operatorem " ? : " :

z = (a>b) ? a : b;

Nawiasy w wyrażeniu warunkowym nie są konieczne, ze względu na bardzo niski priorytet operatora ?. Ale lepiej je stosować dla uwypuklenia tego wyrażenia. Ogólnie można wyrażenie warunkowe zapisać:

wyr1 ? wyr2 : wyr3;

Wyr2 i wyr3 powinny mieć taki sam typ, jesli nie to wykonywane jest przekształcenie typów.

Wyrażenia warunkowe często wpływaja na zwięzłość programu np.:

printf("Masz %d częś%s.
",n,n ==1 ? "ć" : "ci");

lub:

for(i = 0; i < n; i++)

printf("%d6dc",a[i],(i%10 == 9 || i == n-1) ? '
' : ' ');

Znak nowego wiersza w powyższym przykładzie wypisywany jest po każdym dziesiątym elemencie tablicy, natomiast po innych wstawiany jest znak spacji.



Przekształcanie typów

Jeżeli argumentami operatora są obiekty różnych typów, to są one przekształcane do jednego wspólnego typu według kilku regół. Ogólna zasada mówi że: automatycznie wykonuje się tylko takie przekształcenia, w których argument "ciasniejszy jest zamieniany na obszerniejszy bez utraty informacji: np zamiana całkowitej na zmiennopozycyjną.

Niedozwolone jest indeksowanie tablic zmiennymi float ( nie odbywa się konwersja).

Wyrażenia w których może wystĄpić utrata informacji, jak np. przypisanie wartości o dłuższym typie całkowitym zmiennej krótszego typu, czy po zmianie zmiennopozycyjnej na całkowitą, mogą powodowac wypisanie ostrzeżenia, ale nie jest niedozwolone.

Obiekty char są krótkimi liczbami całkowitymi, zapewniającymi znaczną elastyczność przy różnego rodzaju przekształceniach.

Przykład 7

int atoi(char s[])

{

int i,n;

n = 0;

for(i = 0;s[i] >='0' && s[i]<='9'; ++i)

n = 10 * n + (s[i] - '0'); // konwersja char na int;

return ;

}

Wyrażenie s[i] - '0' daje numeryczną wartość cyfry zawartej w s[i].

Ogólnie konwersja wykonywana jest w następujący sposób (zasady powinny być stosowane wg kolejności wymieniania):

- jeśli jeden argument jest long double drugi przekształcany jest do long double.

- jeśli nie to jeśli jeden argument jest double drugi przekształcany jest do double.

- jeśli nie to jeśli jeden argument jest float drugi przekształcany jest do float.

- jeśli nie to jeśli jeden argument jest unsigned long drugi przekształcany jest do unsigned long

- jeśli nie to jeśli jeden argument jest signed long drugi przekształcany jest do signed long

- jeśli nie to jeśli jeden argument jest unsigned int drugi przekształcany jest do unsigned int

- w przeciwnym przypadku oba operandy są typu int

Sposoby wykonywania konwersji

- char na int

powielanie bitu znaku dla signed char lub wypełnienie zerami dla unsigned char

-short na int

ta sama wartość

- unsigned short na unsigned int

ta sama wartość

-konwersja dłuższego typu całkowitego do mniejszego

Odrzucenie bardziej znaczących bitów i w razie ich ustawienia utrata informacji

- konwersja między typami calkowitymi o tych samych dlugościach

bezpośrednie skopiowanie wartości jednej zmiennej do drugiej. Warto wiedzieć, że można uzyskać ciekawe wyniki np.:

int -1000 po konwersji do unsigned char 64536 ( bit znaku traktowany jako bit o wadze 2 do 15)

- konwersja rzeczywiste na całkowite

odrzucenie części ułamkowej liczby rzeczywistej



- konwersja całkowite na rzeczywiste

nadanie odpowiedniej liczbie rzeczywistej wartości liczby całkowitej

- konwersje między typami rzeczywistymi o różnych rozmiarach

zaokrąglenie do najbliższej wartości docelowego typu.



Oprócz automatycznego przekształcania typów, w dowolnym wyrażeniu można jawnie wymusić przekształcenie typów za pomocą:



- jednoargumentowego operatora rzutowania(casting)

Operator ten może mieć dwie formy:

(nazwa_typu) zmienna //forma języka C/C++

nazwa_typu(zmienna) //forma C++

Operator ten upewnia niejako komputer, że rzeczywiscie chcemy przeprowadzić "niedozwoloną" operacje konwersji np: int do char;



Przykład 8

/* rand : generowanie losowej liczby całkowitej z przedziału 0...32767 */

unsigned long int next = 1

int rand (void)

{

next = next * 1103515245 + 12345;

return(unsigned int)(next/65536) % 32768;

}

/* srand zarodek dla f-cji rand */

void srand(unsigned int seed)

{



next = seed;

}



Operator sizeof()

Jak wspomnieliśmy przy okazji omawiania typów podstawowych, rozmiary niektórych zmiennych są uzależnione od implementacji.

Poza tym, język C++ wprowadził możliwość definiowania przez użytkownika własnych typów obiektów. Często ważne jest znanie rozmiarów tych typów.

Do tych celów zaimplementowano w języku C/C++ bardzo wygodny operator: sizeof(), podający rozmiary:



a) typów zmiennych sizeof(nazwa_typu)

b)obiektów sizeof(nazwa_obiektu)



Przykład 9

#include

main()

{

int tab_of_int[10];

cout <<"Podaj rozmiar typu int " << sizeof(int);

cout<<"
A terza podaj rozmiar tablicy int-ów tab_of_int " << sizeof(tab_of_int;

return 0;

}



Operator przecinek

Ostatnim operatorem języka C/C++ jest przecinek. Jeśli kilka wyrażeń stoi obok siebie i są oddzielone przecinkami, to całość też jest wyrażeniem, a wartościa tego wyrażenia jest wartością prawego argumentu.

(2+4, a*4, 3<6, 77+2) wart.wyrażenia: 79

Poszczególne wyrażenia obliczane są od lewej do prawej.

Jest on np. stosowany w pętli for do równoległego sterowania dwoma indeksami np:

Przykład 10

void reverse(char s[])

{

int c,i,j;

for(i = 0, j = strlen(s)-1; i
{

c = s[i];

s[i] = s[j];

s[j] = c;

}

Funkcja ta odwraca kolejność znaków argumentu s.

Przecinki oddzielające argumenty funkcji nie są operatorami i nie gwarantują obliczeń od lewej do przwej strony.

Wyrażenia przecinkowe byłyby odpowiednie dla powyższej funkcji, którą można przepisać jako:

for(i = 0, j = strlen(s)-1; i
{

c = s[i], s[i] = s[j], s[j] = c;

Poszczególne wartości wyrażeń będą obliczane od lewej do prawej, a wynikiem wyrażenia jest wyrażenie s[j] = c; o co nam rów
cież chodziło.



Priorytet i łączność operatoowyc
Podsumujemy teraz wszystkie operatory pod kątem ich priorytetów (ważności kolejności obliczeń).

Priorytety(od największego) podaje zestawienie:

15. () [] -> L

14. ! ~ ++ -- + - * & sizeof p // adres i wskaźnik

13. * / % L

12 + - L

11. << >> L

10. < <= > >= L

9. == != L

8. & L

7. ^ L

6. | L

5. && L

4. || L

3. ?: P

2. = += -= *= /= %= ^= |= <<= >>= P

1. , L

Pamiętanie priorytetów (przynajmniej wszystkich) nie jest konieczne. Wystarczy tam, gdzie mamy wątpliwości wstawić nawiasy nie tylko dlatego,że mają one bardzo wysoki priorytet, ale również dla czytelności wyrażenia.

Litery L i P określają w jaki sposób grupowane jest wykonywanie wyrażenia i tak np:

dla wyrażenia z operatorem + (L)

a + b + c + d + e

lewostronna łączność oznacza:

((((a + b) + c) + d) + e)

natomiast dla wyrażenia z operatorem = (P)

a = b = c = d = e

oznacza:

(a = ( b = (c = (d = e))))



Zakończyliśmy już omawianie operatorów języka C/C++, a teraz zgodnie z obietnicą pomówimy o typach pochodnych. Zamin zaczniemy, uzupełnimy swoją wiedzę o dodatkowy typ zarezerwowany dla liczb calkowitych.



Typy wyliczeniowe enum

Jak wspomnieliśmy, jest to typ zarezerwowany dla liczb całkowitych i przydać się może w wielu sytuacjach.

Często zdarza się tak, że w obiekcie typu całkowitego chcemy przechowywać nie tyle liczbę, co pewien rodzaj informacji. Wtedy korzystamy z typu wyliczeniowego. Wyjaśnijmy to na przykładzie. Za pomocą liczb określimy działanie pewnego urządzenia (pralki). Określamy sobie pewną zmienną np. działanie, do której będziemy wstawiali liczbę określającą daną akcje. Niech akcje te będą miały następujące nazwy i kody:

0 - start

1 - wciąganie wody

2 - podgrzewanie wody

3 - plukanie

4 - wirowanie

5 - wyłączenie

typ wyliczeniowy definiujemy nastepująco:

enum nazwa_typu {lista wyliczeniowa}

Dla naszego przypadku definicja będzie wyglądała następująco:

enum pralka {

- start = 0,

- wciąganie wody = 1,

- podgrzewanie wody = 2,

- plukanie = 3 ,

- wirowanie = 4 ,

- wyłączenie = 5

};

W ten sposób zdefiniowaliśmy nowy typ o nazwie pralka. A teraz zdefiniujemy zmienną tego typu:

pralka działanie;

W zmiennej działanie może znajdować się tylko wartość określona w liście wyliczeniowej pralka. Sposób użycia tych zmiennych jest następujący:

działanie = start;

działanie = plukanie;

błędem jest natomiast podstawienie:

diałanie = 0;

działanie = 3;

Na liście wyliczeniowej znajdują się liczby. Jednak mimo, że funkcje pralki reprezentowane są przez liczby, to nie mogliśmy ich wstawić do zmiennej działanie. Jest to bardzo ważna cecha, gdyż nawet przez nieuwagę nie mogliśmy do zmiennej działanie wstawić czegoś innego, nawet gdyby to coś przez przypadek pasowało jako wartość liczbowa.

Brak określeń liczbowych w liście nie jest błędem, ponieważ wstawione by tam były przez domniemanie. Np.:

enum miesiąc {

styczeń,

luty,

marzec = 2,

..................

grudzień = 11

};

Przez domniemanie kompilator zakłada, że wyliczenie ma zacząć się od 0. Poza tym, reprezentacje liczbowe mogą być dowolne, bo nie do nich odwołujemy się, tylko do elementów listy.



2.Typy pochodne zmiennych

Powstają z typów podstawowych. Oznacza się je stosując nazwę typu od którego pochodzą i operator deklaracji typu pochodnego. Tych operatorów jest cztery, tak jak cztery typy pochodne:

[] - tablica obiektów danego typu. Tablica to inaczej macierz, albo wektor obiektów danego typu.

* - wskaźnik do pokazywania na obiekt danego typu. Wskaźnik to obiekt, w którym można umieścić adres jakiegoś innego obiektu w pamięci

() - funkcja zwracająca wartość danego typu. Jeśli stoi zamiast typu słówko void np. void fun() to oznacza, że funkcja nie zwraca wartości. Funkcja czyli podprogram.

& - referencja obiektu danego typu. To jakby przezwisko jakiegos obiektu. Dzięki referencji na tę samą zmienną można mówić używając jego drugiej nazwy.

Przykłady typów podstawowych i pochodnych:

int a; int tab[10];

short int b; float *p;

float x; char func();

Mimo bardzo skomplikowanego zapisu użycie tych typów pochodnych nie jest trudne. Teraz po kolei będziemy omawiać poszczególne typy pochodne.



Tablice

Jeśli mamy do czynienia z grupą zmiennych tego samego typu, to można z nich zrobić tablicę. Jest to ciąg obiektów tego samego typu, zajmujących ciagły obszar w pamięci. Taka cecha jest doskonała, bo zamiast odnosić się do każdej zmiennej z osobna, odnosimy się do n-tego elementu tablicy. Ze sposobem deklarowania tablic spotkaliśmy się już wcześniej.

Rozmiar tablicy musi być wartościa stałą, znaną już na etapie kompilacji, dlatego na sztywno musimy podawać ilość elementów tablicy, aby wiedzieć ile miejsca zarezerwować na tablicę w pamięci. W związku z tym rozmiar ten nie może być ustalony dopiero w trakcie działania programu. Jeśli jednak zachodzi konieczność definiowania " rozmiaru tablicy w czasie programu" ,to wtedy używamy tzw. dynamicznej alokacji pamięci, a do zmiennych odwołujemy się poprzez adres.

Tablice można tworzyć z:

- typów podstawowych

- typów wyliczeniowych

- wskaźników

- innych tablic

- z klas

- ze wskażników do pokazywania na składniki klasy.



Numeracja tablic w języku C/C++ zaczyna się od zera

Jeśli zadeklarowaliśmy tablicę:

float tablica[3];

to jest to zbiór trzech elementów typu float: tablica[0], tablica[1], tablica[2]. Może to być pewnym utrudnieniem dla tych z Państwa, którzy programowali w Pascalu, gdzie numeracja tablic była naturalna od 1. Próba zapisania czegoś do elementu tablica[3] nie będzie sygnalizowane jako błąd, gdyż język C/C++ tego nie sprawdza. Zapis taki powoduje zniszczenie czegoś co jest zapisane bezpośrednio za tablicą i co nam może być jeszcze przydatne. Sposób pracy na tablicach jest bardzo prosty. Zilustrujemy to prostym przykładem.



Przykład 11

/* Program zliczający wszystkie znaki stringu wprowadzonego z klawiatury */

#include

#include

#inclued

main()

{

char string[20];

int licznik = 0;

clrscr();

gets(string);

while(string[licznik] != NULL)

licznik ++;

cout<<"Liczba znaków w stringu: "<
while (!kbhit());

return 0;

}

W programie tym wystąpiły dwie nowe funkcje związane z obsługą konsoli: clrscr() - funkcja czyszcząca ekran, kursor ustawia się w lewym górnym rogu ekranu. kbhit() - funkcja czekająca na nacisnięcie klawisza ( testujaca klawiaturę) - obie z prototypem w zbiorze .

Najczęściej tablice wprowadza lub wyprowadza się przy pomocy instrukcji for:



wprowadzanie:

int tab [10], i;

for( i = 0;i<10;i++) // lub for (i=0; i<=9;i++)

cin>>tab[i];



wypisywanie:

int tab [10], i;

for( i = 0;i<10;i++) // lub for (i=0; i<=9;i++)

cout>>tab[i];

Wprowadziliśmy tu podwójny zapis pętli, różniący się stosowanym typem nierówności, lepiej jest stosować pierwszy sposób z nierównościa silną, ponieważ nie trzeba odejmować jedynki od rozmiaru tablicy ( co może doprowadzić do błędu);

Innym sposobem nadawania wartości tablicom jest ich inicjalizacja, czyli nadanie wartości w momencie deklaracji tablicy. Pamiętamy,że dla typów podstawowych odbywało się to w następujący sposób:

int i =123;

float j = 0.123;

W przypadku tablic należy inicjować każdy element z osobna. Dla tablicy int tab[3];

inicjacja tablicy ma postać:



int tab[3] = {1,2,3}; // lub nie podając rozmiaru tablicy: int tab[] = {1,2,3};



Jeslibyśmy w nawiasie klamrowym umieścili więcej liczb niż jest zadeklarowanych w tablicy, kompilator wykryje błąd, ponieważ sprawdza w inicjalizacji, czy nie został przekroczony zakres.

Można też w ten sposób zainicjować tablice:

int tab[4] = {1,2}

Wówczas pozostałe elementy są automatycznie inicjowane zerami.



Tablice wielowymiarowe

Ten typ tablic z jakim spotkaliśmy się dotychczas nazywał się tablicami jednowymiarowymi. Można też tworzyć tablice tablic, inaczej nazywane tablicami wielowymiarowymi. Oto przykład tablicy dwuwymiarowej:

int tab1[3][2];

Jest to trzyelementowa tablica tablic dwuelementowych. Jej elementami są:

tab[0][0],tab[0][1],

tab[1][0],tab[1][1],

tab[2][0], tab[2][1].

Należy sobie przyswoić zapis tablicy: podwójny operator [][]. Pascal przyjmował inną konwencję zapisu: [i,j].

Próba takiego zapisu spowoduje przyjęcie przez kompilator jako elementu tab[j], bo jak wiemy operator przecinka ma tą właściwość, że wartością wyrażenia jest wartość prawego argumentu. Elementy tablicy zapamiętywane są wierszami. Podobnie jak dla tablic jednowymiarowych można je inicjalizować:

tab[3][2] = {0,1, 10,11, 20,21, 30,31};

Można powiedzieć, że tablica tab ma 3 wiersze i dwie kolumny. Przykład zastosowania obu typów tablic przedstawia program obliczania sumy elementów wierszy macierzy 5x5.





Przykład 12

#include

#inclued

main()

{

int A[5][5], B[5];

int i,j;

clrscr();

/* wprowadzenie elementów tablicy A */

for(i = 0; i<5;i++)

for(j = 0; j<5;j++)

cin>>A[i][j];

/* sumowanie wierszy */

for(i = 0; i<5;i++)

for(j = 0; j<5;j++)

b[i] += A[i][j];

/* wyswietlanie wyników zapisanych w tablicy jednowymiarowej */

for(i = 0; i<5; i++)

cout << "b[" << i << "] = " << b[i];

return 0;

}

Tablice znakowe

Specjalnym rodzajem tablic są tablice znakowe, stąd przyglądniemy sie im bliżej. Deklaracja takiej tablicy:

char zdanie[50];

W tablicach takich można przechowywać tekst, dzięki temu, że każdy jej element może przechowywać kod znaków ( dla komputerów IBM - kod ASCII);

Teksty w tablicach przechowywane są tak, że po ciagu znaków nastepuje znak o kodzie 0 - tzw. NULL. Taki ciąg znaków zakończony NULL'em nazywa się stringiem.

Początek stringu w pamięci podaje nazwa stringu (nazwa jest adresem pierwszego elementu stringu w pamięci);

Podobnie jak inne tablice, tablica znakowa może być w trakcie deklaracji inicjalizowana:

char zdanie [50] = {"ala ma kota"};

Pamiętajmy o używaniu cudzysłowia.



Znak NULL został zapisany automatycznie, ponieważ przy inicjalizacji tablicy ciąg znaków ograniczyliśmy znakami cudzysłowia.

Można też inaczej inicjować tablicę:

char zdanie[50] = {'a','l','a',' ','m','a',' ','k','o','t','a'};

W tym przypadku jednak, nie dołączony zostanie znak NULL. Jest to ryzykowne.Chociaż przypomnijmy sobie, że w przypadku gdy ilość znaków tablicy jest mniejsza niż jej długość to pozostałe elementy inicjowane są bajtami o wartości zero

Czy tekst był przydatny? Tak Nie
Przeczytaj podobne teksty

Czas czytania: 29 minut