c

OD 9 brak Strona www.elektronet.prv.pl



Cześć 1 kursu C/C++



1. Wprowadzenie do kursu


Są dwa powody, dla których zdecydowałem się napisać kurs języka C. Pierwszy jest taki, że nie udało mi się znaleźć w internecie strony, która zawierałaby taki kurs w języku polskim. Owszem, można znaleźć kilka tego typu publikacji, ale wszystkie są napisane w języku angielskim. Drugiego powodu, który skłonił mnie do tego kroku nie zdradzę, ale pewna grupa ludzi wie o co chodzi (sorry, że tak wyszło, ale sami chcieliście ;) Przejdźmy wreszcie do rzeczy...

Na wstępie chciałbym przedstawić założenia, jakie przyjąłem odnośnie Twojej wiedzy oraz kilka ogólnych informacji dotyczących języka C.

Język C, podobnie jak Pascal, jest językiem strukturalnym. Jednak w przeciwieństwie do Pascala jest on o wiele mniej rygorystyczny w stosunku do programisty, dzięki czemu osoba pisząca program w C ma o wiele większą swobodę działania. Poza tym język ten jest bardziej kompaktowy - w jednej linijce programu w C można zawrzeć to, co w przypadku Pascala zajęłoby dziesięć. Niektórzy uważają to za wadę, bo powoduje to trudności w późniejszym analizowaniu kodu programu, jednak według mnie jest to ogromna zaleta tego języka. To programista decyduje tu jak ma wyglądać jego program - jeśli chce czysty, przejrzysty kod to pisze dziesięć linijek kodu, ale jeśli wygodniej jest mu to samo zawrzeć w jednej linijce to dlaczego standard języka miałby go ograniczać ?

Jednak mimo tego (a może właśnie dlatego) sądzę, że język C nie powinien być pierwszym językiem programowania, który się poznaje. Według mnie powinno się zacząć o bardziej "szkoleniowego" języka jakim jest Pascal. Dopiero po zapoznaniu się z ogólnymi koncepcjami programowania można myśleć o rozpoczęciu nauki języka C. Dlatego też nie będę w tym kursie tłumaczył takich podstawowych pojęć jak np. zmienna. Zakładam, że masz już ogólne pojęcie o programowaniu. Do zrozumienia tego kursu nie będzie jednak konieczna znajomość Pascala, może to być dowolny inny język programowania. Będzie to jednak bardzo pomocne, gdyż często będę podawał odpowiedniki danej instrukcji właśnie w tym języku. Powinieneś także pamiętać, że samo przeczytanie tego kursu nic Ci nie da. Ja przekażę Ci tylko koncepcję języka, jego słowa kluczowe i przykłady ich zastosowania, jednak umiejętność praktycznego ich wykorzystania nabędziesz tylko i wyłącznie podczas samodzielnego pisania programów w języku C. Tak więc pamiętaj - ćwiczyć, ćwiczyć i jeszcze raz ćwiczyć !

Dołożyłem wszelkich starań, aby kurs ten w sposób jasny i czytelny przedstawiał zasady pisania w języku C. Jeśli jednak coś jest nie do końca zrozumiałe to możesz do mnie napisać. Mój email to motzel@panda.bg.univ.gda.pl lub motzel@polbox.com. Preferuję ten pierwszy, jeśli jednak on nie działa (bo na przykład oblałem jakiś egzamin i wyrzucili mnie ze studiów ;) to napisz na drugi. Postaram się w miarę możliwości wytłumaczyć dane zagadnienie. Proszę jednak, żebyś z pytaniami wstrzymał się aż do momentu, gdy przeczytasz punkt "Mój pierwszy program". Do tego momentu coś może wydawać się niejasne ponieważ brak jest odpowiednich przykładów, jednak ten pierwszy program powinien rozwiać wszelkie wątpliwości.
Mam też prośbę do osób znających już język C, a czytających ten kurs (są tacy ?). Jeśli znaleźliście jakieś nieścisłości lub też po prostu błędy, to proszę o informację, abym mógł to poprawić.

Wszystkie przykładowe programy przedstawione w tym kursie były kompilowane przy użyciu DJGPP (jest to dosowa wersja gcc, znanego z systemów unixowych), jednak powinny kompilować się bez problemu przy użyciu dowolnego innego kompilatora C. . Zakładając, że używasz tego samego kompilatora, aby skompilować te pliki musisz wydać polecenie:

gcc nazwa.c -o nazwa.exe

Po poprawnej kompilacji na dysku zostanie utworzony plik exe, który jest gotowym do uruchomienia programem.






Cześć 2 kursu C/C++



2. Wprowadzenie do języka C


Program w języku C jest podzielony na bloki zawarte między nawiasami klamrowymi { i } (są to odpowiedniki "begin" i "end" z Pascala). Każdy blok może być kompletną funkcją, albo po prostu fragmentem kodu w ramach danej funkcji. Funkcja jest to po prostu wydzielona cząstka programu, która wykonuje jakieś zadanie (np. oblicza pole powierzchni prostokąta o danych bokach). W każdym programie napisanym w języku C musi znaleźć się funkcja o nazwie "main". Jest to główna programu, od której zaczyna on swój bieg. Najprostszy program w języku C wygląda więc tak:

void main(void)

{

}

Pomińmy na razie słówko "void" (przejdziemy do tego w następnym punkcie) i przeanalizujmy ten program. Mamy tutaj jedną, jedyną funkcję "main", która w naszym przypadku nie zawiera żadnego kodu (czyli nic nie robi). Jak widzimy definicja funkcji składa się z nagłówka, który określa jej nazwę, parametry wejściowe i rodzaj zwracanej wartości (to także omówimy później) oraz z właściwego ciała funkcji zawartego między { i } . Zapisując to inaczej definicja funkcji wygląda to tak:

typ_zwracanej_wartości nazwa_funkcji(lista parametrów)

{

}

Analogiczna definicja funkcji w Pascalu wygląda tak:

function nazwa_funkcji(lista parametrów) : typ_zwracanej_wartości;

begin

end;

Podobne, prawda ?

Na uwagę zasługuje jeszcze fakt, że po nazwie funkcji w języku C nie stawiamy średnika, tak jak to było w Pascalu. Jest to sensowne ponieważ średnik symbolizuje zakończenie pewnej logicznej całości, a tu mamy do czynienia z sytuacją wręcz przeciwną - od tego miejsca funkcja się zaczyna, nie kończy.

Na zakończenie tego punktu powiemy sobie jeszcze o zasadach konstrukcji nazw funkcji (tyczy to się także nazw zmiennych, o których będzie następny punkt). W języku C każdy identyfikator musi zaczynać się od litery, albo od podkreślenia dolnego "_". Pozostałe znaki identyfikatora mogą oprócz liter i "_" zawierać także cyfry. Długość jest ograniczona do 32 znaków (może być większa, ale pozostałe znaki są po prostu ignorowane). I tutaj ważna uwaga: język C rozróżnia wielkość liter, tak więc "PoleProstokata" i "poleprostokata" to dwie różne nazwy ! Szczególnie muszą uważać osoby, które pisały wcześniej w języku Pascal (w którym jest brak takiego rozróżnienia), gdyż może prowadzić to do trudnych do zlokalizowania błędów.

Cześć 3 kursu C/C++



3. Typy danych


W języku C są cztery podstawowe proste typy danych: znak, liczba całkowita, liczba rzeczywista i typ bezwartościowy. Typy te można nieco zmodyfikować poprzez dodanie przy deklaracji następujących słów: "signed", aby uzyskać liczbę ze znakiem (jest to ustawienie domyślne); "unsigned", aby uzyskać liczbę bez znaku; "short", aby zmniejszyć wielkość zmiennej (ale zmniejszeniu ulega także zakres); "long", aby zwiększyć wielkość zmiennej (zwiększeniu ulega zakres, ale zmienna zajmuje więcej miejsca w pamięci).

Poniższa tabela zawiera wspomniane typy:

PRIVATE<![endif]--><![endif]-->Nazwa typu

Typ

Zakres

Wielkość (w bajtach)

Uwagi

char

całkowity

-128..127

1


unsigned char

całkowity

0..255

1


int

całkowity

-2^31..2^31-1

4

Na kompilatorach 16 bitowych ma 2 bajty

unsigned int

całkowity

0..2^32

4

Na kompilatorach 16 bitowych ma 2 bajty

short int

całkowity

-32768..32767

2


unsigned short int

całkowity

0..65535

2


long int

całkowity

-2^31..2^31-1

4


unsigned long int

całkowity

0..2^32

4


float

rzeczywisty

6 znaków precyzji

4


double

rzeczywisty

10 znaków precyzji

8



Jak widać w przypadku wielkość zmiennej mierzona w bajtach jest wprost proporcjonalna do zakresu danych, jaki może ona przechowywać. Dla zmiennych całkowitych zakres ten wynosi od 0 do 2^n (w przypadku liczb bez znaku), lub od -2^n do 2^n-1 (w przypadku liczb ze znakiem), gdzie n to ilość _bitów_ na których zapisana jest zmienna. Dla przypomnienia: jeden bajt ma osiem bitów...

Deklaracja zmiennej wygląda tak:

typ_danych nazwa_zmiennej; (czyli dokładnie odwrotnie niż w Pascalu, w którym było: "nazwa_zmiennej : typ_danych;")


Czyli, jeśli chcemy zadeklarować zmienną znakową o nazwie "znak" to robimy to w ten sposób:

char znak;

W tym miejscu muszę także przedstawić jeden z ważniejszych operatorów języka C (pozostałe zostaną opisane w następnych punktach). Jest to oczywiście operator przypisania "=". Zapewne już wiesz do czego on służy, ale dla dopełnienia formalności napiszę, że przypisuje wartość wyrażenia z lewej strony do zmiennej umiejscowionej po prawej jego stronie. Operator ten to oczywiście odpowiednik Pascalowego ":=". Od razu przedstawię praktyczne jego zastosowanie - zadeklarujemy zmienna taką, jak w powyższym przykładzie i przypiszemy jej wartość początkową:

char znak='A';

Konstrukcja taka nie występuje w Pascalu, gdzie blok deklaracji zmiennych jest wydzielony i trzeba do osiągnięcia tego samego celu wpisać dwie linijki kodu.


Deklaracja zmiennej całkowitej o nazwie "dlugosc" (wraz z przypisaniem jej wartości początkowej) wygląda tak:

unsigned long int dlugosc=5;


Natomiast deklaracja zmiennej rzeczywistej o nazwie "pole" (wraz z przypisaniem jej wartości początkowej) tak:

float pole=3.48;

Jak widzimy w powyższym przykładzie część całkowitą oddzielamy od części ułamkowej przy pomocy kropki, nie przecinka !


Tutaj ważna uwaga. Deklaracja wszystkich zmiennych musi być wpisana na logicznego początku bloku programu (czyli zaraz za znakiem { , który to taki blok rozpoczyna). Zmienna taka jest lokalna, czyli jest dostępna tylko wewnątrz bloku, w którym jest zadeklarowana.

Do omówienia pozostał jeszcze typ bezwartościowy - jak się zapewne domyślacie, jest to wspomniane w poprzednim punkcie "void". Nie jest to typ danych w ścisłym tego słowa znaczeniu ponieważ nie można utworzyć zmiennej tego typu (można jednak utworzyć zmienną typu wskaźnik na void, ale to omówię gdzieś pod koniec tego kursu). Służy on głownie do deklaracji, że funkcja nie zwraca lub też nie przyjmuje żadnych danych. Czyli przykład z pierwszego punktu:

void main(void)

można przetłumaczyć jako informację dla kompilatora, że chcemy zdefiniować funkcję o nazwie "main", która nie zwraca żadnej wartości oraz nie przyjmuje żadnych parametrów.

Jeśli na razie coś nie jest jasne to się nie martw, w następnym punkcie przedstawię jeszcze podstawowe operatory arytmetyczne i wreszcie będziemy już mogli napisać swój pierwszy program, w którym pokażę praktyczne zastosowanie przestawionych rzeczy. A dobry przykład jest lepszy niż tysiąc słów....

Cześć 4 kursu C/C++



4. Operatory arytmetyczne

<![endif]-->
Język C jest bardzo bogato wyposażony we wszelkiego rodzaju operatory arytmetyczne. Poznaliśmy już operator przypisania, przyszła pora na następne. Przedstawię to w formie tabeli, która będzie także zawierać pascalowy odpowiednik.

PRIVATE<![endif]--><![endif]-->C

Pascal

Opis

+

+

Dodanie dwóch wartości

-

-

Odjęcie jednej wartości od drugiej

/

/ lub div

Podzielenie jednej wartości przez drugą

*

*

Pomnożenie jednej wartości przez drugą (ma także inne znaczenie, ale o tym później)

%

mod

Reszta z dzielenia dwóch liczb

++

brak

Zwiększenie o 1 (występują tu dwa różne przypadki omówione poniżej)

--

brak

Zmniejszenie o 1 (także występują tu dwa różne przypadki)

<<

shl

Przesunięcie bitowe w lewo (omówimy je szczegółowo poniżej)

>>

shr

Przesunięcie bitowe w prawo (omówimy je szczegółowo poniżej)

4.1. Mój pierwszy program


Gwoli ścisłości pierwszy program mamy już za sobą (został on przedstawiony we "Wprowadzeniu do języka C"), ale wtedy nic on nie robił. Teraz napiszemy pierwszy program, który wykonuje jakaś operację. Zanim jednak przejdziemy do napisania (i co najważniejsze przeanalizowania) tego programu muszę wspomnieć jeszcze o jednej rzeczy, a mianowicie o komentarzach. Komentarz jest tekstem wpisanym do kodu programu, który jest jednak pomijany przy jego analizie przez kompilator. Jak sama nazwa wskazuje, służy on do skomentowania danego fragmentu kodu tak, aby po powrocie po miesiącu do kodu programu wiedzieć jak to naprawdę działa. W języku C komentarzem jest wszystko, co znajduje się między znakami /* i */ . Dla przypomnienia - w Pascalu komentarz był zawarty między { i } lub {* i *) . Trzeba także wspomnieć o drugim typie komentarza, a mianowicie // . W tym przypadku komentarzem jest wszystko od tych znaczków, aż do końca linii. Co prawda komentarz tego typu został dopiero wprowadzony w C++, ale większość kompilatorów pozwala na jego użycie także dla programów pisanych w C. Ja także w przestawionych przykładach będę częściej stosował komentarz tego typu, ponieważ uważam, że jest on wygodniejszy. Jeśli jednak Twój kompilator go nie obsługuje, po prostu zamień "// jakiś tekst" na "/* jakiś tekst */" i program skompiluje się bez żadnych problemów. Dobrze, możemy wreszcie przejść do napisania programu. Oto on:

void main(void)

{

// ------------------------------

// Deklaracja używanych zmiennych

// ------------------------------

int a, b, c; // Deklaracja trzech zmiennych typu int (całkowita).

// Jak widać możemy zadeklarować kilka zmiennych tego samego

// typu w jednej linijce. Wystarczy je rozdzielić przecinkiem.

float r = 5.3; // Deklaracja zmiennej typu float (rzeczywista) wraz z

// przypisaniem wartości początkowej.

// ---------------------

// Właściwy kod programu

// ---------------------

a = 5; b = 3; // Przypisujemy zmiennym a i b wartości, odpowiednio, 5 i 3.

// Jak widać w jednej linijce programu można wpisać kilka

// instrukcji kończąc każda przy pomocy średnika.

c = a + b; // Dodanie zmiennych a oraz b i wpisanie wyniku do zmiennej c.

// Zmienna c jest teraz równa 8.

c = a - b; // Odjęcie zmiennej b od a i wpisanie wyniku do zmiennej c.

// Zmienna c jest teraz równa 2.

c = a * b; // Pomnożenie zmiennej a przez b i wpisanie wyniku do c.

// Zmienna c jest teraz równa 15;

// --- poniższe instrukcje są omówione szczegółowo w tekście kursu ---

c++; // Zwiększenie zmiennej c o 1. Teraz jest ona równa 16.

++c; // Zwiększenie zmiennej c o 1. Teraz jest ona równa 17.

--c; // Zmniejszenie zmiennej c o 1. Teraz jest ona równa 16.

c--; // Zmniejszenie zmiennej c o 1. Teraz jest ona równa 15.

c = a % b; // Wpisanie do c reszty z dzielenia a przez b.

// Zmienna c jest teraz równa 2.

r = a / b; // Podzielenie zmiennej a przez b i wpisanie wyniku do r.

// Zmienna r jest teraz równa 1.

r = a; // Przypisanie wartości zmiennej a do zmiennej r. Teraz r jest

// równe 5. Jak widać mimo różnych typów (a jest całkowite,

// natomiast r jest rzeczywiste) można bezproblemowo dokonać

// takiego przypisania. Zmienna typu całkowitego jest konwertowana

// na zmienną typu rzeczywistego. Natomiast w Pascalu przy próbie

// kompilacji czegoś takiego zostałby zgłoszony błąd.

r = r / b; // Podzielenie zmiennej r przez b. Teraz r jest równe 1.666667

// ---------------------------

// tworzymy nowy blok programu

// ---------------------------

{

// --------------------------------------

// Deklaracja używanych w bloku zmiennych

// --------------------------------------

int x=5; // Deklarujemy lokalną dla tego bloku zmienna typu całkowitego.

// Zmiennej tej nie możemy wykorzystywać poza obrębem tego bloku,

// w takim przypadku kompilator zgłosi błąd.

int r=7; // Tutaj mamy ciekawą rzecz. Deklarujemy wewnątrz tego bloku

// zmienna lokalną o takiej samej nazwie jak zmienna występująca

// w bloku nadrzędnym, jednak o innym typie (wcześniej był to

// float).

// --------------------------------------

// Kod bloku

// --------------------------------------

x += r; // Dodajemy do zmiennej x wartość zmiennej r. Teraz zmienna x

// jest równa 12. Widzimy więc, że w przypadku, gdy zmienna

// lokalna ma taką samą nazwę jak zmienna występująca w bloku

// nadrzędnym używana jest zmienna lokalna.

x += a; // Dodajemy do zmiennej x wartość zmiennej a. Teraz zmienna x

// jest równa 17.

// Jak widzimy wewnątrz tego bloku możemy używać zmiennych

// należących zarówno do tego bloku programu, jak i zmiennych

// zadeklarowanych w blokach nadrzędnych (w tym przypadku

// zmiennej a)

}

}


Wpiszmy go w edytorze i nazwijmy go "first.c". Teraz już możemy spróbować go skompilować. W zależności od kompilatora robi się to w różny sposób, więc nie będę tego omawiał - przeczytaj w dokumentacji od Twojego kompilatora. Mamy już więc nasz pierwszy program, przejdźmy do jego analizy.
Pierwsza linijka to definicja funkcji main, która nie pobiera, ani nie zwraca żadnych parametrów (zostało to omówione w punkcie Typy danych). Następnie deklarujemy zmienne, które będziemy używać. Jak widać na przykładzie, możemy zadeklarować od razu kilka zmiennych jeśli są one tego samego typu. Zmiennar jest zadeklarowana wraz z przypisaniem wartości początkowej. Następnych parę linijek to przykłady użycia podstawowych operatorów. Myślę, że nie trzeba ich szczegółowo omawiać, gdyż jest to logiczne. Jednak następne cztery linijki to coś nowego. Jak widać w komentarzu instrukcje c++; i ++c; wydają się działać identycznie. Po co więc dwie instrukcje, które robią to samo ? Diabeł tkwi w szczegółach. W przedstawionym przykładzie ich działanie jest rzeczywiście identyczne jednak obie różnią się sposobem działania. Operator ++ użyty jako przyrostek to tzw. postinkrementacja, natomiast użyty jako przedrostek to tzw. preinkrementacja. Aby pokazać różnicę w ich działaniu posłużę się przykładem.
Załóżmy, że zmienna a zawiera wartość pięć i wykonujemy taką oto instrukcję:

c = a++;

W takim przypadku do zmiennej c zostanie przypisana wartość znajdująca się w zmiennej a (czyli pięć) i dopiero po tym przypisaniu zmienna a zostanie zwiększona o jeden. Czyli w efekcie po wykonaniu tej instrukcji zmienna c będzie zawierała wartość pięć, natomiast zmienna a będzie równa sześć.

Teraz przy założeniach takich samych jak powyżej wykonujemy taką instrukcję:

c = ++a;

W takim przypadku najpierw zmienna a zostanie zwiększona o jeden (czyli teraz będzie równa sześć) i następnie ta wartość będzie przypisana do zmiennej c . Czyli w efekcie po wykonaniu tej instrukcji obie zmienne będą równe sześć.

Analogicznie działa operator -- tylko zamiast zwiększania, zmniejsza wartość o jeden.

Następna linia zawiera operator % . Dzięki niemu możemy uzyskać resztę z dzielenia całkowitego dwóch liczb. W tym przypadku dzielimy pięć przez trzy, czyli w wyniku otrzymujemy resztę z dzielenia równą dwa.

Przejdźmy do dalszej analizy programu. Znowu widzimy dziwną rzecz - po wykonaniu:

r = a / b;

przy a równym pięć i b równym trzy otrzymujemy w wyniku jeden. Natomiast wykonanie ciągu instrukcji:

r = a; r = r / b;

powoduje, że otrzymujemy wynik którego oczekiwaliśmy, czyli 1.666667.

Dlaczego tak się dzieje ? Otóż trzeba zwrócić uwagę na typy zmiennych, które biorą udział w operacji dzielenia. W pierwszym przypadku dzielimy liczbę całkowitą przez liczbę całkowitą. W takiej sytuacji wynik, który otrzymujemy jest również całkowity i jest on wpisywany po konwersji do zmiennej r. A ponieważ liczba całkowita nie posiada części ułamkowej, wynik tej operacji pokazuje ile razy trzy całkowicie mieści się w piątce, a reszta z tego dzielenia jest odrzucana. Aby zaradzić takiej sytuacji można wykorzystać tzw. rzutowanie, ale o tym napiszę w dalszej części. Uwaga dla znających Pascala: operacji dzielenia odpowiada tutaj operator div z tego języka.

Natomiast w drugim przypadku nie ma takiego problemu ponieważ zmienna r jest typu rzeczywistego. Tak więc dzielimy liczbę rzeczywistą (która jest równa 5.0 - zwróćcie uwagę na to zero po kropce) przez trzy i w efekcie otrzymujemy wynik, który jest także rzeczywisty. Dlatego właśnie jego część ułamkowa nie jest tracona i wynik jest zgodny z oczekiwanym. Operacji dzielenia w tym przypadku odpowiada Pascalowe / .

Idźmy dalej... Jak widzimy deklarujemy tutaj nowy logiczny blok programu. Dla przypomnienia - blokiem jest tekst zawarty między { i } . Każdy blok może mieć swoje zmienne, więc tutaj także je deklarujemy - zmienną x typu int oraz zmienną r także tego typu. Z oboma zmiennymi wiążą się ciekawe rzeczy. Po pierwsze zmienna zadeklarowana wewnątrz danego bloku jest dostępna tylko i wyłącznie dla tego bloku (oraz wszystkich bloków, które będą zadeklarowane wewnątrz niego). Tak więc próba użycia zmiennej x po znaku kończącym blok } , spowoduje, że kompilator zgłosi błąd. Po drugie zmienna r była także zadeklarowana w bloku nadrzędnym ! Tak więc, która z nich zostanie użyta przy próbie dodania r do x ? Odpowiedź brzmi następująco: użyta zostanie zmienna zadeklarowana "bliżej" miejsca jej użycia.

Po zadeklarowaniu zmiennych widzimy nową konstrukcję: x += r; Cóż to takiego ? Otóż jest to kompaktowa wersja operatora + . Programiści to leniwy naród i lubią sobie upraszczać życie ;)

Instrukcja a += b; odpowiada zapisowi a = a + b; Którego z tych dwóch zapisów będziesz używał jest to obojętne, oba działają w identyczny sposób. Ja preferuję ten drugi - mniej trzeba stukać w klawiaturę ;)
Analogicznie wygląda sytuacja w przypadku pozostałych przedstawionych operatorów (oprócz oczywiście ++ i -- , gdyż nie miałoby to wtedy sensu).

Pozostały jeszcze do wyjaśnienia dwa operatory, które nie zostały użyte w tym przykładowym programie. Są nimi bliźniacze << i >> . Operator << przesuwa wszystkie bity argumentu w lewo, natomiast >> przesuwa je w prawo. Być może nie wiesz nic o systemie dwójkowym i nie rozumiesz co to tak naprawdę znaczy, ale nie martw się. Jedyne co musisz zapamiętać, to fakt, że przesunięciu liczby w lewo o jeden bit odpowiada pomnożenie jej przez dwa, natomiast przesunięcie w prawo podzieleniu jej przez dwa (całkowicie). Poniżej podałem kilka przykładów:

a = a << 1; // pomnożenie zmiennej a przez 2^1, czyli 2

a = a << 2; // pomnożenie zmiennej a przez 2^2, czyli 4

a = a << 3; // pomnożenie zmiennej a przez 2^3, czyli 8

a = a >> 1; // podzielenie zmiennej a przez 2^1, czyli 2

a = a >> 2; // podzielenie zmiennej a przez 2^2, czyli 4

Mógłbyś jeszcze zapytać - po co stosować przesunięcia bitowe, skoro mogę użyć zwykłego mnożenia lub dzielenia ? Owszem możesz, z tym, że przesunięcia bitowe są o wiele szybsze, dzięki czemu możesz zwiększyć szybkość działania swojego programu. Co prawda nowoczesne kompilatory starają się optymalizować Twój kod poprzez zamienienie mnożenia, czy dzielenia na odpowiednie przesunięcia bitowe, jednak nie robią tego we wszystkich przypadkach.

Mamy więc już za sobą nasz pierwszy program. Co prawda wyników jego działania nie widać na ekranie, ale miał on tylko wytłumaczyć zasadę używania operatorów w języku C. Następny nasz program także będzie miał za zadanie zobrazować pewny sposób pisania programu i nic nie wyświetli, ale zaraz po nim napiszemy program, który wyświetli wreszcie pierwszy tekst. Mam nadzieję, że po przeanalizowaniu naszego pierwszego programu wszystko stało się dla Ciebie bardziej jasne i zrozumiałe.

Cześć 5 kursu C/C++



5. Użycie funkcji


Przyjrzyj się poniższemu programowi - liczy on pole powierzchni prostokąta o podanych długościach boków.

void main(void)

{

float a, b; // deklarujemy zmienne przechowujące boki prostokąta

float dlugosc; // deklarujemy zmienną zawierającą wynik obliczeń

a = 5; b =10; // przypisujemy im wartości

dlugosc = a * b; // obliczamy pole prostokąta (tu równe 50)

}


A co jeśli chciałbyś obliczyć także pole prostokąta o innych długościach boków ? Odpowiesz pewnie: "Nic prostszego ! Wystarczy skopiować fragment tego programu i zmienić długości boków". Owszem, ale co jeśli chciałbyś obliczyć pola stu prostokątów ? Albo gdybyś liczył coś bardziej skomplikowanego, co nie zajęłoby tylko jednej linijki, jak w naszym przypadku, a na przykład trzydzieści ? Przy użyciu tego sposobu program zająłby trzysta linijek ! Właśnie dlatego w języku C istnieją funkcje, które rozwiązują ten problem. Jak zbudowane są funkcje to już wiesz z poprzednich punktów, ale nie znasz praktycznego ich zastosowania. Poniższy program wykonuje tą samą operację jak ostatni, jednak obliczenia pola prostokąta wykonywane jest w funkcji:

float PoleProstokata(float bok1, float bok2)

{

// w tym miejscu bok1 jest równy 5,

// natomiast b jest równe 10

float wynik;

wynik = bok1 * bok2;

return wynik;

}

void main(void)

{

float a, b, dlugosc;

a = 5; b = 10;

dlugosc = PoleProstokata(a, b);

}


Jak widzisz, program zawiera dwie funkcje - main , która jest "obowiązkowa" w każdym programie oraz PoleProstokąta . Analizując nagłówek funkcji PoleProstokąta możemy zauważyć, że zwraca ona wynik obliczeń w postaci liczby rzeczywistej ( float ) oraz przyjmuje dwa parametry - bok1 i bok2 . W tym przypadku oba parametry są także typu float , jednak funkcja może przyjmować dowolną ilość argumentów dowolnego typu, wystarczy je wpisać w formie "typ_argumentu nazwa" i oddzielać przecinkami jeden od drugiego.
Pojawiło się nam tu także nowe słowo kluczowe - return . Wykonując to polecenie program powraca z funkcji do miejsca jej wywołania zwracając wartość podanego argumentu (który jest takiego typu, jak to określono w nagłówku funkcji). W naszym przypadku po tym poleceniu występuje nazwa zmiennej wynik , tak więc wartość zwracana przez tą funkcję jest równa wartości tej zmiennej, która z kolei jest obliczana linijkę wyżej (iloczyn dwóch podanych argumentów). Zacznijmy jednak analizę programu od miejsca, w którym się on rozpoczyna, czyli od funkcji main (pamiętaj, że jest to zawsze pierwsza funkcja wywoływana po uruchomieniu programu).
Na początku mamy znane już rzeczy - deklarację trzech zmiennych i przypisanie wartości. Ostatnia linijka jest jednak nowością, do zmiennej długość jest coś przypisywane. No właśnie, co ? Otóż jest to wartość zwrócona przez wywołaną funkcję. Jako parametry dla tej funkcji przekazujemy nasze zmienne a i b . Jest to tzw. przekazanie przez wartość, co znaczy, że nie przekazujemy samych zmiennych, a tylko wartości, które one zawierają. Wartości te są po prostu kopiowane do parametrów bok1 i bok2 i nawet jeśli w ciele funkcji zmienimy ich wartości to zmianie ulegną tylko te lokalne kopie, natomiast po powrocie do funkcji main zmienne a i b będą miały starą wartość.
Po wywołaniu funkcji PoleProstokata przenosimy się do ciała tej funkcji. W tym momencie pierwszy parametr funkcji o nazwie bok1 jest równy wartości zmiennej a (czyli pięć), natomiast drugi o nazwie bok2 jest równy wartości zmiennej b (czyli dziesięć). Pierwszą rzeczą jest deklaracja zmiennej wynik - to już znamy. Następnie, w wyniku pomnożenia zmiennej bok1 przez bok2 , do zmiennej wynik wpisana jest wartość piętnaście. Właśnie tą wartość zwracamy przy pomocy return do miejsca, w którym funkcja PoleProstokata została wywołana i właśnie tą wartość będzie zawierała zmienna długosc po wykonaniu ostatniej linijki programu.
Mam nadzieję, że zrozumiałeś rzeczy poruszone w tym punkcie, gdyż funkcje to podstawa języka C. Na zakończenie jednak wspomnę o jeszcze jednej ważnej rzeczy dotyczącej funkcji, żebyś mógł bezboleśnie zrozumieć następny punkt. Załóżmy, że nasz ostatni program zapiszemy w trochę inny sposób, tzn. najpierw zapiszemy funkcję main , a dopiero pod nią funkcję PoleProstokata . Czyli wyglądałoby to następująco:

void main(void)

{

float a, b, dlugosc;

a = 5; b = 10;

dlugosc = PoleProstokata(a, b);

}

float PoleProstokata(float bok1, float bok2)

{

// w tym miejscu bok1 jest równy 5,

// natomiast b jest równe 10

float wynik;

wynik = bok1 * bok2;

return wynik;

}


Jak myślisz, czy taki program skompiluje się bez żadnego problemu ? Odpowiesz zapewne: "Oczywiście, dlaczego kolejność zapisu funkcji miałaby wpływać na jego poprawność ?". Masz rację, jednak tylko częściowo. W zależności od typu używanego przez Ciebie kompilatora, próba kompilacji tego programu albo zakończy się zupełnym niepowodzeniem, albo zostaną wyświetlone ostrzeżenia. Dlaczego ? Podejdźmy do zagadnienia od strony tego, w jaki sposób działa kompilator. Otóż analizuje on program, linijka po linijce, sprawdzając czy jest on poprawny. Tak więc sprawdza on pierwsze pięć linijek programu, aż dochodzi do linii, w której mamy wywołanie funkcji PoleProstokata . I tutaj wyświetla błąd ponieważ nie wie co ta nazwa oznacza - ani nie jest to żadne ze słów kluczowych języka C, ani nie jest to też wcześniej zadeklarowana zmienna. Sposób zapisu co prawda sugeruje, że jest to jakaś funkcja, ale skąd ma on wiedzieć jakie parametry powinna ona przyjmować ? Zapytasz zapewne: "Jak to nie wie ? Przecież parę linijek niżej jest ta funkcja zdefiniowana !". Właśnie - parę linijek niżej. A ponieważ kompilator analizuje poprawność programu zaczynając od jego początku to nie wie, że definicja tej funkcji znajduje się gdzieś niżej (albo zupełnie w innym pliku). Aby zaradzić tej sytuacji stosuje się w języku C tzw. prototypy. Jest to po prostu informacja dla kompilatora, że gdzieś niżej znajdzie funkcję o podanej nazwie oraz określonych parametrach. W naszym przypadku chcemy powiadomić kompilator o funkcji PoleProstokata . Wystarczy, że dodany na samym początku taką oto linijkę:

float PoleProstokata(float bok1, float bok2);

Zauważ, że jest to dokładna kopia nagłówka naszej funkcji zakończona średnikiem. Teraz kompilator sprawdzając nasz program w pierwszej linijce znajdzie informację o tym, że w przypadku napotkania nazwy PoleProstokata jest to funkcja, która przyjmuje dwa parametry typu float i zwraca także float .

Cześć 6 kursu C/C++



6. Preprocesor po raz pierwszy


W poprzednim punkcie obiecałem, że napiszemy teraz nasz pierwszy program wyświetlający tekst na ekranie. Zastanówmy się co nam będzie potrzebne do realizacji tego zadania ? Ponieważ program w języku C zbudowany jest z funkcji to potrzebujemy oczywiście odpowiedniej funkcji, która zadanie to wykona. Funkcja taka nazywa się printf . Jak każe tradycja, pierwszy program powinien wyświetlać napis "Hello world !", nasz nie będzie wyjątkiem:

void main(void)

{

printf("Hello world !");

}

Spróbuj zapisać ten program pod nazwą hello.c i skompilować. Zapewne kompilator zgłosił znany już Ci błąd. Zapewne już się domyślasz dlaczego. Występuje tu taka sama sytuacja jak w naszym ostatnim programie. Kompilator nie wie co symbolizuje nazwa printf . Czego brakuje ? Oczywiście prototypu tej funkcji ! Zanim jednak przejdziesz do wyszukania jej prototypu i wpisania go na początku programu przedstawię Ci dyrektywę #include . Pomyśl, funkcji podobnych do printf są setki, jeśli chciałbyś użyć ich w swoim programie musiałbyś wcześniej wpisać prototyp każdej z nich - jaka to strata czasu ! Dlatego producent Twojego kompilatora zrobił to za Ciebie - wpisał wszystkie prototypy funkcji do plików z rozszerzeniem .h (od header - nagłówek), które dostarczył razem z kompilatorem. Jednak nie bój się, nie musisz także pracowicie używać kombinacji "kopiuj i wklej" Twojego edytora - standard języka C udostępnia wspomnianą już wcześniej dyrektywę #include , która pozwala zautomatyzować tą operację. Jej użycie wygląda tak, że na początku programu należy wpisać:

#include <stdio.h>

W nawiasach należy podać nazwę pliku, który kompilator ma sobie dołączyć podczas kompilacji programu. W naszym przypadku jest to stdio.h (skrót od standard input/output - standardowe wejście/wyjście), w którym to właśnie pliku znajduje się prototyp funkcji "printf". Zauważ także, że linijka ta nie jest zakończona średnikiem. Dodaj teraz podaną linię do programu, skompiluj go i uruchom. Pełen sukces, na ekranie pojawił się napis !
Na zakończenie omawiania tej dyrektywy mam dla Ciebie jeszcze jedną uwagę odnośnie fizycznej lokalizacji pliku nagłówkowego. Skąd kompilator ma wiedzieć, na którym dysku i w którym katalogu znajduje się ten plik ? Otóż każdy kompilator ma z góry określony katalog, w którym się pliki nagłówkowe znajdują. Na ogół jest pliki te znajdują się w katalogu o nazwie include , który z kolei znajduje się w głównym katalogu kompilatora. Jeśli nazwę pliku zawrzesz, tak jak jest przedstawione powyżej, wewnątrz nawiasów to właśnie w tym katalogu kompilator będzie poszukiwał podanego pliku. Jeśli go nie znajdzie to wyświetli błąd. Istnieje także inny sposób użycia tej dyrektywy, a mianowicie nazwę pliku podaje się wewnątrz cudzysłowia, czyli np. tak:

#include "mojplik.h"

Jeśli nazwę pliku określisz w ten sposób to kompilator będzie tego pliku szukał w aktualnie wybranym katalogu.

W punkcie tym omówimy jeszcze jedno z zastosowań innej dyrektywy, a mianowicie "#define". Pozostałe dyrektywy zostaną omówione w dalszej części kursu (w punkcie "Preprocesor po raz drugi").

Dzięki dyrektywie #define można zastąpić często używany ciąg znaków za pomocą identyfikatora. Ponieważ dość ciężko jest wytłumaczyć sens takiego postępowania, napiszemy program, który to zilustruje.
Załóżmy, że chcesz napisać program liczący pole powierzchni oraz obwód koła o podanym promieniu. Do obu tych operacji będziesz potrzebował liczby PI . Czyli dwa razy użyjesz w swoim programie sekwencji znaków 3.1415. Będzie to wyglądało tak:

#include &ltstdio.h>

float ObliczPole(float promien);

float ObliczObwod(float promien);

void main(void)

{

float pole, obwod;

pole = ObliczPole(5);

obwod = ObliczObwod(5);

}

float ObliczPole(float promien)

{

// wzór na pole to PI*R^2

return (3.1415 * promien * promien);

}

float ObliczObwod(float promien)

{

// wzór na obwód to 2*PI*R

return (2 * 3.1415 * promien);

}


Pomyśl teraz, czy nie byłoby wygodniej, gdybyś zamiast każdorazowego wpisywania wartości odpowiadającej liczbie PI , mógł wpisać po prostu PI ? Jeśli myślisz, że to żaden problem to mam dla Ciebie jeszcze jedno pytanie. A co byłoby, gdybyś nagle zapragnął zwiększyć precyzję obliczeń i do określenia liczby PI zamiast czterech, chciałbyś zastosować pięć liczb po przecinku ? Musiałbyś w takim przypadku pracowicie przeszukiwać kod programu i zamienić wszelkie wystąpienia ciągu "3.1415" na "3.14159". Przy dużym programie o pomyłkę nietrudno. Wszystkie te problemy można rozwiązać właśnie przy pomocy dyrektywy #define . Nasz program z zastosowaniem tej dyrektywy wyglądałby następująco:

#include &ltstdio.h>

#define PI 3.1415

float ObliczPole(float promien);

float ObliczObwod(float promien);

void main(void)

{

float pole, obwod;

pole = ObliczPole(5);

obwod = ObliczObwod(5);

}

float ObliczPole(float promien)

{

// wzór na pole to PI*R^2

return (PI * promien * promien);

}

float ObliczObwod(float promien)

{

// wzór na obwód to 2*PI*R

return (2 * PI * promien);

}


W trzeciej linijce mamy deklarację naszego symbolu o nazwie PI (zauważ, że wyrażenie to nie jest zakończone średnikiem). Teraz kompilator każde wystąpienie takiego tekstu w kodzie programu zastąpi tekstem "3.1415". Jeśli teraz chcielibyśmy zwiększyć precyzję obliczeń, wystarczy, że zmienimy tylko tą właśnie linijkę, a cały program będzie już używał tej nowej wartości. Prawda, że wygodne ?


Cześć 7 kursu C/C++



7. Printf i wyświetlanie danych

<![endif]-->
W poprzednim punkcie napisaliśmy program wyświetlający na ekranie tekst. Dla przypomnienia - skorzystaliśmy z funkcji "printf". Jednak co to za program, który wyświetla z góry ustalone teksty i nie ma możliwości zaprezentowania wyników przeprowadzonych operacji ? W tym właśnie punkcie zaprzęgniemy wspomnianą funkcję do takiej właśnie pracy. Jak zwykle posłużymy się przykładowym programem.

#include &ltstdio.h>

void main(void)

{

float f = 0.521;

int i = -123;

unsigned int u = 24;

char c = 'A';

printf("Zmienna f = %f, natomiast zmienna i jest rowna %d.\n", f, i);

printf("Zmienna c = %c, a zmienna u jest rowna %u.\n", c, u);

printf("Zmienna u w zapisie szestnastkowym jest rowna %x, \n", u);

printf("natomiast w zapisie osemkowym jest rowna %o.", u);

}


Jak widzimy funkcja "printf" może także wyświetlać wartości zmiennych. Ogólny jej zapis wygląda tak:

printf(ciag_formatujący, lista parametrów);

Ciąg formatujący jest zwykłym ciągiem znaków do wyświetlenia na ekranie. Jednak niektóre znaki mają funkcję specjalną i nie zostaną one po prostu wyświetlone. Takim właśnie znakiem jest znak % . Gdy funkcja printf go napotka to wie, że po nim wystąpi określenie rodzaju argumentu i formatu jego wyświetlenia na ekranie. Ogólnie wygląda to tak:

% [flagi] [szerokość] [precyzja] [modyfiktor wielkości] typ_parametru

Tylko "typ_parametru" musi wystąpić po znaku % , natomiast parametry podane w nawiasach kwadratowym są opcjonalne i może ich w ogóle nie być (tak jest w przedstawionym przykładzie). Poniżej podałem najczęściej stosowane typy parametrów:

PRIVATE<![endif]--><![endif]-->%d

zmienna typu int (ze znakiem)

%u

zmienna typu int (bez znaku)

%c

zmienna typu char (litera)

%f

zmienna typu float (rzeczywista)

%x

zmienna typu int (bez znaku) wyświetlana w postaci szesnastkowej

%o

zmienna typu int (bez znaku) wyświetlana w postaci ósemkowej

%s

ciąg znaków (zostanie omówiony później)

%p

wskaźnik (zostanie omówiony później)


Nie przerażaj się jeszcze, po przeanalizowaniu zasady działania funkcji printf wszystko stanie się jasne. Zrobimy to na podstawie pierwszej linijki z tą funkcją w naszym programie, czyli:

printf("Zmienna f = %f, natomiast zmienna i jest rowna %d.\n", f, i);

Po wywołaniu funkcja printf sprawdza znak po znaku ciąg formatujący. Ciąg ten rozpoczyna się od "Zmienna f = ". Ponieważ na razie nie wystąpił znak % wszystkie te znaki zostaną po prostu wyświetlone na ekranie. Następnie napotyka się na znak % , którego jednak już nie wyświetla, ale sprawdza co stoi za nim. Okazuje się, że jest to literka "f", czyli razem otrzymuje %f , która to kombinacja oznacza, że w tym miejscu powinna wyświetlić liczbę typu rzeczywistego (patrz tabelka powyżej). Bierze więc pierwszy argument (który jest wypisany po przecinku zaraz za znakiem cudzysłowia kończącego ciąg formatujący), którym w naszym przypadku jest zmienna f i wyświetla jego wartość na ekranie. Następnie przechodzi do dalszego sprawdzania ciągu formatującego. Wyświetla ciąg ", natomiast zmienna i jest rowna ", ponieważ także nie występuje w nim żaden znak kontrolny i dochodzi do miejsca, w którym mamy drugi znak % . Tym razem typem parametru jest "d", czyli jest to informacja, że ma wyświetlić liczbę całkowitą (ze znakiem). Tak więc funkcja printf pobiera drugi argument, którym w naszym przypadku jest zmienna i, a następnie wyświetla jej zawartość na ekranie. Po tym, jak już to zrobi wraca do sprawdzania ciągu. Tym razem natyka się na ".", którą to wyświetla i pobiera następny znak, którym jest \ . Jest to drugi, po % , znak kontrolny. Ciąg \n daje informację dla printf , że w tym miejscu powinna ona przejść do następnej linii ekranu. Innymi często używanymi są:

PRIVATE<![endif]--><![endif]-->\t

tabulacja

\r

powrót karetki (kursor przesuwa się do początku linii)

\b

backspace (cofnięcie o jeden znak)

\a

bell (krótki sygnał dźwiękowy)

\"

znak cudzysłowia (zauważ, że jakbyś po prostu go wpisał to kompilator potraktowałby to jako zakończenie ciągu formatującego)


Czyli w efekcie, po wykonaniu tej funkcji na ekranie pojawi się:

"Zmienna f = 0.521000, natomiast zmienna i jest rowna -123."

Jak widzimy, wartość zmiennej f jest wyświetlona z dokładnością do sześciu miejsc po przecinku. Jednak nas mogą interesować na przykład jedynie dwa miejsca po przecinku, reszty nie potrzebujemy. Wtedy należy użyć opcjonalnych parametrów, które mogą wystąpić po znaku % . Dla przypomnienia ogólny zapis wygląda tak:

% [flagi] [szerokość] [precyzja] [modyfiktor wielkości] typ_parametru

Zaczniemy od omówienia flag:

PRIVATE<![endif]--><![endif]-->-

wyrównuje liczbę do lewej (normalnie byłaby wyrównana do prawej)

+

liczba zawsze zaczyna się od znaku "+" (dla dodatnich) lub "-" (dla ujemnych), normalnie znak jest wyświetlany tylko dla liczb ujemnych


Teraz parametr szerokość:

PRIVATE<![endif]--><![endif]-->n

gdzie n jest liczbą określającą ile znaków zostanie wyświetlonych. Jeśli n jest większe od szerokości liczby to zostanie ona uzupełniona spacjami. Jeśli jest mniejsze to liczba nie zostanie ucięta.

0n

gdzie n jest liczbą określającą ile znaków zostanie wyświetlonych. Jeśli n jest większe od szerokości liczby to zostanie ona uzupełniona zerami. Jeśli jest mniejsze to liczba nie zostanie ucięta.


Teraz parametr precyzja:
Parametr ten zawsze zaczyna się od kropki, a następnie podajemy ilość liczb do wyświetlenia po przecinku (tak jak to było przy szerokości.

Pozostał jeszcze jeden parametr, a mianowicie modyfikator wielkości:

PRIVATE<![endif]--><![endif]-->l

określa, że parametr jest typu long (np. long int to %ld)

h

określa, że parametr jest typu short (np. short int to %hd)



Wszystko zostało już omówione, na zakończenie podam jeszcze przykłady użycia. Wszystkie będą pokazywały to, co zostanie wyświetlone na ekranie, przy założeniu, że zmienne mają wartości takie jak w naszym przykładowym programie.

PRIVATE<![endif]--><![endif]-->"[%6.3f]"

wyświetli się "[ 0.521]". Pamiętaj, że liczba sześć oznacza szerokość całej liczby, a nie tylko części przed przecinkiem. Ponieważ szerokość liczby jest równa pięć, to została dodana jedna spacja _przed_ liczbą.

"[%-6.3f]"

wyświetli się "[0.521 ]". Jak wyżej, tylko spacja została dodana _po_ liczbie (wyrównanie do lewej).

"[%06.3f]"

wyświetli się "[00.521]". Czyli zamiast spacji, zostało dodane zero.

"[%+6.3f]"

wyświetli się "[+0.521]". Oczywiście dla f równego -0.521 wyświetli się znak minus, nie plus.




Cześć 8 kursu C/C++



8. Operacje logiczne


O ile w przypadku operacji artymetycznych wynikiem mogła być dowolna liczba (oczywiście z określonego zakresu), to wynikiem operacji logicznej jest jeden z dwóch możliwych stanów - prawda lub fałsz . W języku C za fałsz uznaje się liczbę zero, natomiast wszystkie pozostałe są uznawane za prawdę . Wynikiem takiego traktowania stanów logicznych jest możliwość używania kompaktowych wersji porównań logicznych, co zobaczymy w następnej części tego punktu.


8.1. Porównania


W tym podpunkcie przedstawię je tylko, natomiast przykłady ich wykorzystania zostaną zaprezentowane w następnym podpunkcie. W języku C mamy następujące operatory porównania:

PRIVATE<![endif]--><![endif]-->C

Pascal

Opis

>

>

większe niż

<

<

mniejsze niż

>=

>=

większe lub równe

<=

<=

mniejsze lub równe

==

=

równe (zwróć uwagę na podwójny znak równości)

!=

<>

nie równe

8.2. Instrukcja if


Instrukcja ta jest istotnym elementem każdego programu, ponieważ pozwala ona na modyfikację sposobu, w jaki działa program w zależności od wartości danych. W języku C, podobnie jak w Pascalu, ma ona następującą składnię:

if(wyrażenie) wyrażenie1 else wyrażenie2

Jeśli wyrażenie w nawiasie ma wartość logiczną prawda to zostanie wykonane wyrażenie1, w przeciwnym wypadku zostanie wykonane wyrażenie2. Przykładowo:

if( a > 10 ) printf("Zmienna a jest większa od dziesięciu !"); else printf("Zmienna a jest mniejsza lub równa dziesięć !");

Zauważ, że zarówno wyrażenie po if , jak i wyrażenie po else jest zakończone średnikiem. Uwagę tą kieruję szczególnie do osób znających Pascala, w którym średnik stawia dopiero na samym końcu. Oczywiście, tak jak w Pascalu, części "else" możesz w ogóle nie używać, jeśli nie jest Ci to akurat potrzebne. Czyli możesz napisać tak:

if( a > 10 ) printf("Zmienna a jest większa od dziesięciu !");

Jeśli chcesz wykonać kilka instrukcji jeśli spełniony jest pewien warunek to musisz je zawrzeć w bloku ograniczonym znakami { i } . Czyli wyglądałoby to następująco:

if( a > 10 ) {

printf("Zmienna a jest wieksza od dziesięciu !\n");

printf("Jest bowiem równa %d.", a);

} else {

printf("Zmienna a jest mniejsza lub równa dziesięć !\n");

if(a != 5) printf("Jednak nie jest równa pieć !");

}

Oczywiście to, jak sformatujesz ten tekst (np. możesz znak rozpoczynający blok wpisań w nowej linii) zależy tylko od Ciebie. Ja jednak preferuję taki sposób, według mnie jest to bardziej czytelne.

Pozostała do omówienia jeszcze jedna rzecz dotycząca instrukcji if , a związana z traktowaniem przez język C wartości logicznych. Zamiast:

if ( a != 0 ) printf("a jest rozne od zera");

możemy napisać:

if( a ) printf("a jest rozne od zera");

i będzie to działało w identyczny sposób. Jak myślisz dlaczego ? Jeśli przeczytałeś uważnie wstęp do operacji logicznych to nie powinieneś mieć większych problemów z odpowiedzią na to pytanie. W pierwszym przypadku mamy do czynienia z porównaniem wartości zmiennej a do zera. Przykładowo, jeśli zmienna a jest równa dziesięć to warunek "a != 0" zwróci prawdę do instrukcji if i zostanie wyświetlony na ekranie tekst. Natomiast co z drugim przypadkiem ? Nie mamy tu do czynienia z żadnym porównaniem, do instrukcji if jest od razu przekazywana wartość zmiennej a, czyli w naszym przypadku dziesięć. Zauważ, nie prawda , nie fałsz , ale liczba dziesięć ! I co się stanie teraz ? Po prostu na ekranie pojawi się tekst. Pamiętasz jak język C traktuje wartości logiczne ? Stanowi fałsz odpowiada liczba zero, natomiast stanowi prawda każda inna wartość. Czyli także liczba dziesięć ! Tak więc liczba ta zostanie potraktowana jako stan prawda i w wyniku tego zostanie wykonana odpowiednia instrukcja (blok instrukcji) - w naszym przypadku zostanie wywołana funkcja printf .

Na zakończenie tego punktu zamieściłem jeszcze przykład często popełnianego błędu (który na dodatek bardzo trudno jest zlokalizować), związanego z właśnie takim traktowaniem stanów logicznych przez język C:

if( a = 0 ) printf("a jest równe zero");
else printf("a jest różne od zera");

Jak myślisz, co zostanie wyświetlone na ekranie, jeśli powiem Ci, że zmienna a jest równa zero ? Jeśli odpowiesz, że będzie to "a jest równe zero" to niestety nie będziesz miał racji. Możesz zapytać: "Ale dlaczego ? Przecież mamy porównanie a do zera i ponieważ a jest równe zero, to powinien wyświetlić się pierwszy napis.". Miałbyś rację, jeśli naprawdę byłoby tam porównanie. Jednak przyjrzyj się uważnie - tam nie ma porównania ! Porównanie w języku C to podwójny znak równości, natomiast pojedynczy (tak jak jest w tym przypadku) oznacza przypisanie. Tak więc najpierw zostanie tu przypisana wartość zero do zmiennej a, a następnie (tak jak w poprzednim przykładzie) wartość tej zmiennej zostanie przekazana do instrukcji if , która potraktuje ją jako fałsz (bo zero właśnie to oznacza) i w efekcie wywoła funkcję printf występującą po else .


8.3. Podstawowe operacje logiczne


W podpunkcie tym przedstawię cztery podstawowe operacje logiczne - OR, AND, NOT i XOR. Jeśli wiesz na jakiej zasadzie one działają to możesz przejść do następnego podpunktu. Zero w tabeli odpowiada stanowi fałsz , natomiast jedynka odpowiada stanowi prawda .

Operacja OR (lub)

PRIVATE<![endif]--><![endif]-->Wejście


Wyjście

0

0

0

1

0

1

0

1

1

1

1

1


Wynikiem operacji OR jest zero, gdy oba argumenty są równe zero, lub jedynka w przeciwnym wypadku.

Operacja AND (i)

PRIVATE<![endif]--><![endif]-->Wejście


Wyjście

0

0

0

1

0

0

0

1

0

1

1

1


Wynikiem operacji AND jest jedynka, gdy oba argumenty są równe jeden, lub zero w przeciwnym wypadku.

Operacja XOR

PRIVATE<![endif]--><![endif]-->Wejście


Wyjście

0

0

0

1

0

1

0

1

1

1

1

0


Wynikiem operacji XOR jest jedynka, gdy tylko jeden z argumentów jest równy jeden, lub zero w przeciwnym wypadku.

Operacja NOT

PRIVATE<![endif]--><![endif]-->Wejście

Wyjście

0

1

1

0


Wynikiem operacji NOT jest jedynka, gdy argument był równy zero, lub zero, gdy argument był równy jeden.


8.4. Operacje logiczne w języku C


Przedstawione w poprzednim punkcie operacje mają w języku C dwa aspekty - logiczny i arytmetyczny (operacje na bitach). Co prawda drugi z nich należałoby przedstawić już wcześniej, przy okazji omawiania operacji arytmetycznych, jednak zrobię to dopiero w tym punkcie z uwagi na podobieństwo. Najpierw jednak skupmy się na pierwszym aspekcie. Oto jak operacje logiczne zapisujemy w języku C:

PRIVATE<![endif]--><![endif]-->Operacja

C

Pascal

OR

||

or

AND

&&

and

NOT

!

not


Ponieważ o wiele łatwiej jest uczyć się na przykładach, podam teraz program, który zilustruje wykorzystanie poznanych operacji logicznych.

#include &ltstdio.h>

void main(void)

{

int a = 17;

// przykład użycia operacji logicznej OR

if( (a 10) ) {

printf("Zmienna a jest mniejsza od pięciu _lub_ większa od dziesięciu.\n\n");

}

// przyklad użycia operacji logicznej AND

if( (a 10) && (a


Ponieważ początek programu nie wymaga chyba komentarza (jeśli wymaga to cofnij się, proszę, do poprzednich punktów), analizę naszego programu zaczniemy od linijki z pierwszym wystąpieniem instrukcji if .

W naszym przypadku chcemy wykonać jakieś działanie (wyświetlenie tekstu na ekranie), gdy zmienna a jest mniejsza od pięciu lub też większa od dziesięciu. Idealnie do tego celu nadaje się operacja logiczna OR , której używa się, gdy chcemy sprawdzić czy chociaż jeden z podanych warunków jest prawdziwy . Widzimy, że mamy tu dwa porównania - pierwsze (a < 5) , dla zmiennej a równej siedemnaście zwraca fałsz , jednak drugie (a > 10) , zwraca prawdę (bo 17 > 10). Operacja logiczna OR dla takich parametrów zwraca prawdę (zobacz w tabeli przedstawionej powyżej), tak więc zostanie wyświetlony na ekranie odpowiedni tekst. Zapamiętaj więc - jeśli chcesz sprawdzić, czy którykolwiek z warunków jest spełniony, użyj operacji OR.

W sytuacji, gdy wykonanie działania ma zależeć od spełnienia wszystkich warunków, używamy natomiast instrukcji logicznej AND . Pokazane jest to przy okazji następnej instrukcji if . W tym wypadku chcemy wykonać działanie tylko wtedy, gdy zmienna a jest większa od dziesięciu i mniejsza od dwudziestu (czyli zawiera się w określonym przedziale). Pierwszy warunek (a > 10) , dla zmiennej a równej siedemnaście zwraca prawdę , drugi (a < 20) także zwraca prawdę . Patrząc do tabeli widzimy, że w takim przypadku wynikiem operacji AND jest prawda , więc na ekranie zostanie wyświetlony tekst o tym informujący. Zapamiętaj więc - jeśli chcesz wykonanie działania uzależnić od spełnienia wszystkich warunków, użyj operacji AND.

Przejdźmy teraz do omówienia ostatniej, najprostszej operacji logicznej, a mianowicie NOT . Jest ona po prostu negacją (zaprzeczeniem) danego parametru - tzn. że dla parametru prawda zwróci fałsz, natomiast dla parametru fałsz zwróci wartość prawda. Pokazuje to wyraźnie trzecia z instrukcji if w naszym programie. Warunek (a < 10) jest sformułowany tak, że dla zmiennej a równej siedemnaście, zwróci fałsz , czyli normalnie tekst nie zostałby wyświetlony na ekranie. Jednak w naszym przypadku występuje jeszcze negacja, która "zamienia" fałsz na prawdę i w efekcie na ekranie pojawi się odpowiedni napis.


8.5. Operacje na bitach


Pozostało jeszcze do omówienia zastosowanie poznanych operacji logicznych do wykonywania działań na bitach. Do zrozumienia (i wykorzystania w swoich programach) podanych w tym punkcie informacji konieczna będzie znajomość podstaw dwójkowego systemu zapisu liczb. Jeśli nie wiesz co to takiego to sugeruję, żebyś pominął teraz ten punkt i powrócił do niego po zaglądnięciu do książki z matematyki (ja to miałem w 7 lub 8 klasie podstawówki, teraz to chyba będzie w gimnazjum). Najpierw przedstawię operatory języka C stosowane do operacji na bitach:

PRIVATE<![endif]--><![endif]-->Operacja

C

Pascal

OR

|

or

AND

&

and

XOR

^

xor

NOT

~

not


Wszystkie przedstawione w tabeli operacje mają taką samą zasadę działania. Pierwsze trzy operują na dwóch argumentach, natomiast ostatnia tylko na jednym. Algorytm działania tych pierwszych wygląda tak:

1. Najpierw zamieniasz oba argumenty na postać binarną.

2. Teraz bierzesz zerowe bity obu argumentów i wykonujesz odpowiednią operację logiczną

3. To, co otrzymałeś zapisujesz w zerowym bicie wyniku.

4. Punkty dwa i trzy wykonujesz kolejno dla pierwszego, drugiego, itd. bitu argumentu.

Natomiast algorytm działania operacji NOT jest dużo prostszy - po prostu neguje ona wszystkie bity argumentu. Czyli w efekcie tam, gdzie były zera, teraz będą jedynki i odwrotnie.

Być może nie zrozumiałeś wszystkiego z powyższego opisu. Jednak nie martw się - wszystko się wyjaśni przy analizie przykładowego programu:

#include &ltstdio.h>

void main(void)

{

char a=12, b=9;

printf("%d and %d = %d\n", a, b, a & b);

printf("%d or %d = %d\n", a, b, a | b);

printf("%d xor %d = %d\n", a, b, a ^ b);

printf("not %d = %d\n", b, ~b);

}


Każde z wywołań funkcji printf prezentuje inny operator. Zacznijmy od operacji AND.

Jak widzisz wykonujemy operację AND na dwóch argumentach - zmiennej a równej dwanaście i zmiennej b równej dziewięć. Po zapisaniu tego w postaci dwójkowej wygląda to następująco:

1100 (12 w systemie dziesiętnym)

AND

1001 (9 w systemie dziesiętnym)

====

1000 (8 w systemie dziesiętnym)

Na podstawie algorytmu działania przedstawionego powyżej pokażę w jaki sposób otrzymaliśmy w efekcie liczbę osiem. Najpierw zamieniamy obie liczby na postać binarną. Dla lepszego zobrazowania zapisałem je jedna nad drugą. Bierzemy teraz zerowy (ten z prawej) bit pierwszej liczby (12) - jest on równy zero. Następnie bierzemy zerowy bit drugiej liczby (9) - jest on równy jeden. Wykonujemy operację AND na tych danych - w efekcie otrzymujemy zero (zobacz w tabeli dla operacji AND), którą to zapisujemy jako zerowy bit wyniku. Teraz to samo wykonujemy dla pierwszych bitów obu liczb. Tym razem oba są równe zero, więc w wyniku także otrzymujemy zero. Następnie wykonujemy operację dla bitów numer trzy obu liczb. Wynikiem działania dla argumentów równych jeden i zero jest ponownie zero. Dokonujemy tej samej operacji dla bitów numer cztery obu liczb - w efekcie wykonania operacji AND na obu argumentach równych jeden, otrzymujemy jedynkę. Wynikiem całej operacji jest więc liczba 1000 w systemie dwójkowych. Liczba ta w systemie dziesiętnym jest równa osiem i właśnie ona zostanie wyświetlona na ekranie.

Następną operacją jest operacja OR . Po zapisaniu jej podobnie jak poprzednio otrzymujemy:

1100 (12 w systemie dziesiętnym)

OR

1001 (9 w systemie dziesiętnym)

====

1101 (13 w systemie dziesiętnym)

Algorytm działania jest identyczny jak poprzednio, tylko zamiast operacji AND wykonujemy operację OR. Wynikiem tej operacji jest jedynka, gdy którykolwiek z argumentów jest jedynką, stąd też tylko dla bitu pierwszego otrzymaliśmy zero, a reszta bitów jest równa jeden.

Przedostatnią operacją jest XOR :

1100 (12 w systemie dziesiętnym)

XOR

1001 (9 w systemie dziesiętnym)

====

0101 (5 w systemie dziesiętnym)

I znowu wykonujemy identyczne operacje, zamieniając tylko operację na XOR. Dla przypomnienia - wynikiem tej operacji jest jedynka, gdy tylko jeden z argumentów jest równy jeden.

Ostatnią operacją jest operacja NOT . Jak zapewne zauważyłeś po uruchomieniu programu, wynikiem operacji NOT 9 jest liczba minus dziesięć, którą binarną reprezentacją jest 11110110. A oto jak ją otrzymaliśmy:

NOT

00001001 (9 w systemie dziesiętnym)

========

11110110 (-10 w systemie dziesiętnym)

Jest to najprostsza z operacji logicznych - ja widać odwraca po prostu wszystkie bity argumentu.



Cześć 9 kursu C/C++


9. Instrukcje pętli


Instrukcje pętli służą do wielokrotnego, uzależnionego od jakiegoś warunku, wykonywania danego fragmentu kodu. Język C posiada trzy podstawowe konstrukcje pętli. W tym punkcie zostaną one przedstawione wraz z przykładami ich użycia.



9.1. Pętla "while"

 


Zaczniemy od razu od przykładowego programu:

 

#include &ltstdio.h>

 

void main(void)

{

int licznik = 10;

 

printf("Poczatek petli\n");

while(licznik 0)

{

printf("Zmienna licznik = %d\n", licznik);

licznik--;

}

printf("Koniec petli\n");

}

 


Skompiluj go proszę i uruchom. Na ekranie pojawi się dziesięć linii tekstu o treści: "Zmienna licznik = xx", gdzie xx symbolizuje kolejne numerki od dziesięć do jeden. Zapewne już się domyślasz jak działa pętla oparta na instrukcji while , lecz dla dopełnienia formalności opiszę to pokrótce. Pętla rozpoczyna się od słowa kluczowego while , po którym, podobnie jak w przypadku instrukcji if , podajemy warunek kontynuowania pętli. Pętla będzie wykonywana dopóty, dopóki warunek ten jest spełniony. Następnie podajemy instrukcję, która będzie wykonywana w pętli. Jeśli chcesz wykonywać w pętli kilka instrukcji, musisz zawrzeć je w bloku ograniczonym znakami { i } (podobnie jest w wszystkich konstrukcjach języka C). W naszym przykładowym programie wykonujemy w pętli dwie instrukcje - wywołujemy funkcję printf i zmniejszamy zawartość zmiennej licznik o jeden. Algorytm działania tego programu wygląda następująco:



1. Wpisz wartość dziesięć do zmiennej licznik.

2. Wyświetl na ekranie tekst informujący o rozpoczęciu działania pętli.

3. Napotykamy się na instrukcję "while" - sprawdza ona czy wartość

zmiennej licznik jest większa od zera (na początku programu jest,

bo wpisaliśmy do niej dziesięć) i jeśli tak to przechodzi do linijki

z wywołaniem funkcji "printf", wewnątrz bloku. Zauważ, że gdybyśmy

zainicjalizowali zmienna licznik wartością np. minus jeden, to od razu

skoczylibyśmy do kroku numer siedem i na ekranie pokazałby się napis o

zakończeniu działania pętli ! Warunek przy instrukcji "while" byłby

bowiem od razu fałszywy, w związku z tym działanie programu byłoby

kontynuowane od pierwszej linijki za blokiem instrukcji dla pętli.

Jak zobaczysz w następnym punkcie, istnieje także pętla, która sprawdza

warunek na końcu pętli i w wyniku tego wykona się ona, w przeciwieństwie

do "while", przynajmniej jeden raz.

4. Jesteśmy teraz wewnątrz bloku i za pomocą instrukcji "printf"

wyświetlamy informację o aktualnym stanie licznika.

5. Zmniejszamy wartość zmiennej licznik o jeden.

6. Skaczemy do kroku numer trzy - dokonuje się tam znowu sprawdzenie

warunku i w zależności od tego, czy zmienna licznik osiągnęła już zero,

czy nie, program będzie kontynuował swe działanie od kroku numer cztery,

albo siedem.

7. Wyświetlamy informację o zakończeniu działania pętli i program kończy

swe działanie.

Jeszcze jedna uwaga dla znających Pascala - oczywiście odpowiednikiem while w języku C jest instrukcja Pascala o tej samej nazwie.





9.2. Pętla "do-while"


Pętla "do-while" jest bardzo podobna do poznanej w poprzednim punkcie "while". Jedyną różnicą jest fakt, że o ile pętla while mogła w ogóle się nie wykonać, jeśli warunek nie był spełniony, o tyle pętla do-while wykona się przynajmniej jeden raz, bo warunek sprawdzany jest dopiero na samym końcu. A oto jak będzie wyglądał nasz program z poprzedniego punktu przy użyciu nowej konstrukcji:



 

 

 

#include &ltstdio.h>

 

void main(void)

{

int licznik = 10;

 

printf("Poczatek petli\n");

do

{

printf("Zmienna licznik = %d\n", licznik);

licznik--;

} while(licznik 0);

 

printf("Koniec petli\n");

}

 

 


Jak widzisz różnica jest minimalna. Zwróć tylko uwagę, że po while tym razem stawiamy średnik. Odpowiednikiem tej konstrukcji w Pascalu jest pętla repeat-until . Występuje tu jednak różnica - pętla repeat-until wykonuje się do momentu, gdy warunek _stanie się_ prawdziwy, natomiast do-while wykonuje się _dopóki_ warunek jest prawdziwy.





9.3. Pętla "for"

 


Najbardziej rozbudowaną konstrukcją pętli jest pętla "for". Ogólny jej zapis wygląda następująco:



for(inicjalizacja; warunek; inkrementacja)

{

instrukcje do wykonania

}

Pętla ta działa na podstawie takiego oto algorytmu:



1. Wykonywane są instrukcje zawarte w części "inicjalizacja". Jest to wykonywane

tylko jeden raz, na samym początku pętli.

2. Sprawdzany jest warunek - jeśli jest fałszywy to następuje skok do kroku

numer sześć.

3. Wykonywane są "instrukcje do wykonania".

4. Wykonywane są instrukcje zawarte w części "inkrementacja".

5. Następuje skok do kroku numer dwa.

6. Pętla kończy się - wykonywane są instrukcje poza pętlą.

Żaden z podanych parametrów nie jest wymagany, tak więc najprostszą pętlą przy użyciu "for" jest:



for(;;)

{

instrukcje do wykonania

}

Taką konstrukcję nazywa się czasem "forever" ponieważ jest to pętla nieskończona (będzie wykonywała się aż do momentu, gdy użytkownik zresetuje komputer lub gdy napotka instrukcję break lub goto, o których to powiemy później).

Pokażmy wreszcie jak będzie wyglądał nasz przykładowy program z poprzednich punktów przy użyciu pętli "for":



 

#include &ltstdio.h>

 

void main(void)

{

int licznik;

 

printf("Poczatek petli\n");

 

for(licznik=10; licznik 0; licznik--)

{

printf("Zmienna licznik = %d\n", licznik);

}

 

printf("Koniec petli\n");

}

 

 


Jak widzisz konstrukcja ta jest już nieco inna od poznanych wcześniej. W części inicjalizacyjnej dokonujemy ustawienia zmiennej licznik na dziesięć. Moglibyśmy to zrobić tak jak poprzednio (od razu ją ustawić przy deklaracji) i część tą zostawić pustą, ale napisałem to w ten sposób, żeby pokazać, że można coś takiego zrobić. Następnie widzimy warunek - tu nic nowego, wygląda on dokładnie tak samo jak poprzednio. Zmniejszenia licznika nie dokonujemy jednak wewnątrz pętli, lecz w części "inkrementacja". Oczywiście moglibyśmy to zrobić wewnątrz pętli, a tą cześć pozostawić pustą, ale zostało to wykonane w ten sposób, aby zaprezentować sposób użycia.

Części "inicjalizacja" i "inkrementacja" mogą zawierać także po kilka instrukcji - oddzielamy je wtedy przecinkami. Dla zobrazowania takiej konstrukcji zamieściłem nieco zmodyfikowaną wersję poprzedniego programu - została dodana jeszcze jedna zmienna, która jest zwiększana z każdym przejściem pętli o dwa:



 

 

 

#include &ltstdio.h>

 

void main(void)

{

int licznik, a;

 

printf("Poczatek petli\n");

 

for(licznik=10, a=5; licznik 0; licznik--, a+=2)

{

printf("Zmienna licznik = %d\n", licznik);

printf("Zmienna a = %d\n", a);

}

 

printf("Koniec petli\n");

}

 

 

 


<BR




9.4. Break, continue i goto

 


Czasem może się zdarzyć potrzeba pominięcia jednego przebiegu pętli lub wcześniejszego jej przerwania. Służą do tego celu dwie wspomniane instrukcje. Instrukcja break jest wręcz konieczna w przypadku zastosowania pętli nieskończonej (pokazanej w poprzednim punkcie). Przyjrzyjmy się nieco zmodyfikowanemu programowi z poprzedniego punktu:



 

 

 

#include &ltstdio.h>

 

void main(void)

{

int licznik;

 

printf("Poczatek petli\n");

 

for(licznik=10; licznik 0; licznik--)

{

if(licznik == 5) continue;

if(licznik == 2) break;

printf("Zmienna licznik = %d\n", licznik);

}

 

printf("Koniec petli\n");

}

 

 


Jak widzimy program niewiele różni się od poprzedniego - zostały tylko dodane dwie linijki z instrukcją warunkową if . W przypadku, gdy zmienna licznik jest równa pięć wykonywana jest instrukcja continue , a gdy jest równa dwa, wykonywana jest instrukcja break . Skompiluj teraz, proszę, program i uruchom go. Widzisz jaka jest różnica ? Z początku program działa identycznie z poprzednim - do wartości zmiennej licznik równej sześć jest wyświetlana odpowiednia linia. Jednak linijka informująca, że zmienna licznik jest równa pięć w ogóle nie została wyświetlona - odpowiedzialna jest za to instrukcja continue . Na początku nie jest ona wykonywana, jednak gdy zmienna licznik dojdzie do wartości pięć, zostaje ona wykonana. Powoduje to, że pętla for pomija wszystkie następujące po continue instrukcje wewnątrz pętli (dla danego przebiegu) i przechodzi od razu do następnej iteracji. W następnych dwóch przebiegach żaden z warunków występujących po instrukcjach if nie jest spełniony i program wyświetla informacje, że zmienna licznik jest równa, kolejno, cztery i trzy. Dochodzimy teraz do momentu, gdy zmienna licznik osiąga wartość dwa. W takim wypadku spełniony jest warunek drugiej z instrukcji if i wykonywana jest instrukcja break . Jaki jest tego rezultat widzisz na ekranie - pętla kończy swe działanie i zostaje wyświetlony komunikat o tym informujący.

Alternatywą instrukcji break jest instrukcja goto . Mimo, że w przypadku języków strukturalnych (jakim jest język C, jak również Pascal) jej stosowanie jest wręcz tępione (szczególnie na poziomie nauczania języka), to jest jednak sytuacja, gdy można ją zastosować ponieważ nie istnieje żaden inny prosty sposób osiągnięcia celu. Sytuacją tą jest wyjście z zagnieżdżonej pętli. Od razu to zilustrujemy odpowiednim przykładem - załóżmy, że mamy do czynienia z takim przypadkiem:



#include &ltstdio.h>

 

void main(void)

{

int a, b;

 

printf("Poczatek petli\n");

 

for(a=0; a < 4; a++)

{

for(b=0; b < 4; b++)

{

// if((a==2) && (b==1)) break;

printf("a = %d, b = %d\n", a, b);

}

}

printf("Koniec petli\n");

}

 


Jak widzimy mamy tu zagnieżdżoną pętlę, tzn. jedna pętla jest wykonywana w drugiej. W sumie otrzymamy więc na ekranie 4*4=16 linijek tekstu. Załóżmy teraz, że chcielibyśmy zakończyć wykonywanie obu pętli przy zmiennej a równej dwa i zmiennej b równej jeden. Jeśli użylibyśmy instrukcji break (tak jak to pokazano w komentarzu) to przerwalibyśmy tylko pętlę, w której jest zwiększana wartość zmiennej b. W efekcie na ekranie pojawiłoby się:



Poczatek petli

a=0, b=0

a=0, b=1

a=0, b=2

a=0, b=3

a=1, b=0

a=1, b=1

a=1, b=2

a=1, b=3

a=2, b=0

a=3, b=0

a=3, b=1

a=3, b=2

a=3, b=3

Koniec petli

Czyli jak widzimy pętla "b" rzeczywiście została przerwana przy a równym dwa, ale następnie została ona ponownie wykonana dla a równego trzy. A my chcieliśmy zakończyć obie pętle ! Rozwiązaniem jest tutaj użycie instrukcji goto:



 

 

 

#include &ltstdio.h>

 

void main(void)

{

int a, b;

 

printf("Poczatek petli\n");

 

for(a=0; a < 4; a++)

{

for(b=0; b < 4; b++)

{

if((a==2) && (b==1)) goto koniec;

printf("a = %d, b = %d\n", a, b);

}

}

 

koniec:

 

printf("Koniec petli\n");

}

 

 

 


W programie tym instrukcja break została zastąpiona przez goto koniec . "koniec" jest to tzw. etykieta. Zadeklarowaliśmy ją przed ostatnią instrukcją programu - jest to po prostu dowolna nazwa zakończona dwukropkiem. W programie możemy zadeklarować dowolną liczbę etykiet (oczywiście każda musi mieć inną nazwę). Aby przenieść wykonywanie programu do innego miejsca należy wykonać instrukcję goto podając jej nazwę etykiety, do której ma nastąpić skok. W naszym przypadku po wykonaniu instrukcji goto koniec program wyświetli informację o końcu pętli i zakończy swe działanie. Po uruchomieniu powyższego programu na ekranie uzyskamy:



Poczatek petli

a=0, b=0

a=0, b=1

a=0, b=2

a=0, b=3

a=1, b=0

a=1, b=1

a=1, b=2

a=1, b=3

a=2, b=0

Koniec petli

czyli dokładnie to, o co nam chodziło.



Cześć 10 kursu C/C++


 10. Instrukcja switch

Instrukcja switch jest odpowiednikiem pascalowej instrukcji case . Jeśli znasz pascala to już wiesz o co chodzi, jeśłi jednak nie, to zaraz zobaczysz jak to wygląda w praktyce i kiedy może się przydać. Załóżmy, że mamy taki oto program:



#include &ltstdio.h>

 

void main(void)

{

int a = 5;

 

if(a == 4) printf("Zmienna a jest rowna cztery.\n");

else if((a == 5) || (a==6)) printf("Zmienna a jest równa piec lub szesc.\n");

else printf("Zmienna a jest nie jest rowna ani cztery, ani piec, ani szesc.\n");

}

 

 


Jaki widzisz program na podstawie wartości zmiennej a wypisuje na ekranie odpowiedni tekst. Aby ten cel osiągnąć musieliśmy zastosować sekwencję instrukcji if . Nie wygląda to jednak zbyt elegancko. Właśnie takim przypadku możemy użyć instrukcję switch . A oto jak będzie wyglądał nasz program przy jej użyciu:



 

 

 

#include &ltstdio.h>

 

void main(void)

{

int a = 5;

 

switch(a)

{

case 4 : printf("Zmienna a jest rowna cztery.\n");

break;

case 5 :

case 6 : printf("Zmienna a jest rowna piec lub szesc.\n");

break;

default: printf("Zmienna a jest nie jest rowna ani cztery, ani piec, ani szesc.\n");

}

}

 

 


Według mnie jest to o wiele bardziej czytelne, czyż nie ?
Dobrze, napiszę więc coś więcej o samej konstrukcji. Generalnie wygląda ona tak:

switch(wyrażenie)

{

case wartosc1: instrukcje; break;

case wartosc2: instrukcje; break;

.

.

.

default: instrukcje;

}

Jak widzisz zaczynamy od słowa kluczowego switch . Zaraz po nim podajemy w nawiasie wyrażenie, od którego wartości będzie miała zależeć akcja, która zostanie wykonana. W naszym przykładowym programie chcieliśmy, żeby napis na ekranie był zależny od zmiennej a, więc wpisaliśmy tam po prostu "a". Jednak nic nie stoi na przeszkodzie, aby na przykład wpisać tam "a*3+b" (zakładając oczywiście, że zadeklarowaliśmy wcześniej zmienną b). Następnie otwieramy blok i wpisujemy po słowie kluczowym case wartości, które będą porównywane z tym wyrażeniem. Konstrukcja switch szuka pierwszej wartości, która jest równa wyrażeniu podanemu w nawiasie. Jeśli ją znajdzie to przekazuje wykonywanie programu do tego miejsca. Następnie wykonywane są kolejne instrukcje, aż do napotkania nawiasu klamrowego kończącego blok switch , albo aż do napotkania instrukcji break. Jeśli żadna z wartości podanych po case'ach nie jest równa danemu wyrażeniu to wykonywane są instrukcje po slowie kluczowym default (jego używanie nie jest konieczne - tak więc, jeśli się go nie wpisze to żadne działanie nie zostanie wykonane). W naszym przypadku zmienna a była równa pięć, tak więc wykonywanie programu zostało przekazane do miejsca, w którym mamy przypadek "case 5:". Ponieważ nie występuje po nim instrukcja break , to wykonywanie programu jest kontynuowane. W następnej linijce wyświetlany jest napis informujący o wartości zmiennej i program przechodzi do następnej linii. Mamy tam słowo kluczowe break , które kończy konstrukcję switch .
Gdyby zmienna a była równa cztery to przeszlibyśmy do linijki z "case 4:", wywołanaby była funkcja printf występująca dalej i program napotkałby instrukcję break , która zakończyłaby działanie konstrukcji switch .
Gdyby zaś zmienna ta była równa dziesięć, to przeszlibyśmy do linijki z "default:", wyświetlony byłby odpowiedni napis.
Na koniec jeszcze uwaga dla osób znających pascala - oczywiście odpowiednikiem przedstawionej tu instrukcji jest w tym języku instrukcja case.



Cześć 11 kursu C/C++


 11. Operator "?"

a
Operator ? jest bardzo użyteczną, lecz często nie docenianą konstrukcją. Jak zwykle jego zastosowanie pokażemy na przykładzie. Załózmy, że mamy do czynienia z dwoma zmiennymi a i b i chcielibyśmy wyświetlić na ekranie wartość większej z nich. Program przy użyciu znanej już instrukcji if wyglądałby tak:

#include <stdio.h>

 

void main(void)

{

int a = 5, b = 6, c;

 

if(a > b) c=a; else c=b;

 

printf("Max(a,b) = %d\n", c);

}

Jak widzisz programik jest prościutki - gdy zmienna a jest większa od zmiennej b to przypisuje zmiennej c wartość zmiennej a, natomiast w przeciwnym wypadku przypisuje jej wartość zmiennej b. Następnie na ekranie wyświetla wartość zmiennej c.
Jednak teoria o tym, że programiści to leniwe bestie potwierdza się i tym razem ;) Co z tego, że zapis jest prościutki, skoro można zrobić to jeszcze prościej ? Do tego celu służy właśnie operator ? . Nasz programik przy jego użyciu wyglądałby tak:

#include <stdio.h>

 

void main(void)

{

int a = 5, b = 6, c;

 

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

 

printf("Max(a,b) = %d\n", c);

}

Jak widzisz instrukcja if została zamieniona na coś, co z początku może wydawać się strasznie dziwnym zapisem. Jednak nie przerażaj się - wcale nie jest to takie straszne. Zauważ, że mamy tu do czynienia z takim przypadkiem:

c = jakieś_wyrażenie; // gdzie wyrażenie to jest operatorem "?"

Ogólna konstrukcja tego operatora wygląda tak:

(wyrażenie_logiczne) ? wyrażenie_gdy_prawda : wyrażenie_gdy_fałsz

Najpierw sprawdzane jest wyrażenie logiczne w nawiasie, jeśli jest ono prawdziwe, to obliczana jest wartość wyrażenia znajdującego sie po znaku ? i jest ona zwracana, natomiast w przypadku, gdy wyrażenie logiczne było fałszywe to obliczana jest wartość wyrażenia znajdującego się po znaku : i właśnie ona jest zwracana. Czyli w naszym przykładowym programie najpierw sprawdzamy, czy zmienna a jest większa od zmiennej b. Ponieważ a jest równe pięć, natomiast b jest równe sześć to wyrażenie to jest fałszywe i obliczana jest wartość wyrażenia występującego po znaku : . W naszym przypadku jest tam po prostu zmienna b, ktorej wartość jest zwracana i przypisana do zmiennej c.

Zapewne powiesz: "No dobrze, ale ten zapis wcale nie jest dużo krótszy od poprzedniego, a poza tym jest o wiele mniej czytelny". Masz rację, jednak cała siła operatora ? tkwi w tym, że można go używać praktycznie w każdej sytuacji. Przykładowo, nasz program można zapisać w ten sposób:

#include <stdio.h>

 

void main(void)

{

int a = 5, b = 6;

 

printf("Max(a,b) = %d\n", (a>b) ? a : b);

}

Jak widzisz wartość wyrażenia jest od razu przekazywana do funkcji printf . Czegoś takiego przy użyciu instrukcji if nie da się zrobić, bo nie zwraca ona żadnej wartości, a jedynie, w zależności od warunku, wykonuje te, lub inne instrukcje.

Cześć 12 kursu C/C++


12. Rzutowanie


O rzutowaniu wspomniałem już w punkcie Mój pierwszy program. Mieliśmy tam do czynienia z taką oto sytuacją:



 

#include &ltstdio.h>

 

void main(void)

{

int a = 5, b = 3;

float r;

r = a / b;

printf("r = %f\n", r);

}


Jak zapewne pamiętasz wynik tej operacji nieco Cię zaskoczył - nie było to "1.666667", lecz po prostu "1.000000". Działo się tak dlatego, że argumenty operacji dzielenia były typu całkowitego, a ten, jak wiesz, nie przechowuje informacji o części ułamkowej. W efekcie wynik operacji także był całkowity i ta właśnie całkowita wartość była przypisana zmiennej r. Aby temu zaradzić można wykorzystać tzw. rzutowanie. Jest to tymczasowa, tylko dla potrzeb obliczenia danego wyrażenia, zmiana typu. Typ ten jednak nie zmienia się fizycznie, po prostu kompilator traktuje zmienną danego typu tak, jakby była typu, na który ją rzutujemy. Tak więc nasz program możemy zapisać następująco:



 

#include &ltstdio.h>

 

void main(void)

{

int a = 5, b = 3;

float r;

r = (float)a / b;

printf("r = %f\n", r);

}


Jak widzisz program ten niewiele różni się od poprzedniego. Jedyną różnicą jest to, że przed zmienną a przy operacji dzielenia dodany został taki oto ciąg znaków: "(float)". Właśnie ten ciąg informuje kompilator, że zmienna (lub wyrażenie) następujące za nim ma być traktowane tak, jakby było typu float . W naszym przypadku mówimy kompilatorowi, żeby potraktował zmienną a jakby była typu rzeczywistego (czyli tak naprawdę rozszerza ją do postaci "5.0"), a następnie tą rzeczywistą liczbę podzielił przez zmienną b, która jest typu całkowitego.
Podumowując, rzutowanie wykorzystujemy, aby przy operacjach przypisania do siebie różnych typów poinformować kompilator, że wiemy, co robimy i ma on traktować to zgodnie z naszym życzeniem. Aby dokonać rzutowania danej zmiennej, czy wyrażenia na inny typ, należy przed nią (nim) wpisać w nawiasach typ, na który chcemy rzutować.


Cześć 13 kursu C/C++


 13. Definicja własnych typów

Język C, poza wbudowanymi typami danych (np. int , czy float ), umożliwia także definicję własnych typów danych. W tym punkcie zostaną przedstawione różne aspekty tego tematu.


13.1. Typ wyliczeniowy


Typ wyliczeniowy nie jest typem danych w ścisłym tego słowa znaczeniu, gdyż jest to odpowiednik typu int . Ma on jednak ciekawą cechę, a mianowicie kolejne jego elementy możemy nazwać wedle swojego uznania. Jak zwykle prezentację nowych rzeczy zaczniemy od przykładowego programu:



 

#include &ltstdio.h>

 

void main(void)

{

enum {Pn, Wt, Sr, Czw, Pt, Sb=10, Nd} DzienTyg;

 

DzienTyg = Pn; printf("Wartosc dla poniedzialku = %d\n", DzienTyg);

DzienTyg = Wt; printf("Wartosc dla wtorku = %d\n", DzienTyg);

DzienTyg = Sr; printf("Wartosc dla srody = %d\n", DzienTyg);

DzienTyg = Czw; printf("Wartosc dla czwartku = %d\n", DzienTyg);

DzienTyg = Pt; printf("Wartosc dla piatku = %d\n", DzienTyg);

DzienTyg = Sb; printf("Wartosc dla soboty = %d\n", DzienTyg);

DzienTyg = Nd; printf("Wartosc dla niedzieli = %d\n", DzienTyg);

}

 

 
W programie tym chcemy operować na zmiennej, która będzie przechowywać dzień tygodnia. Moglibyśmy po prostu zadeklarować ją jako int i przyjąć założenie, że poniedziałkowi odpowiada wartość zero, wtorkowi wartość jeden itd. Jednak, gdy przy dalszej rozbudowie programu chcielibyśmy przypisać tej zmiennej wartość "Środa" to musielibyśmy sobie przypominać jaka liczba jej odpowiada. Możemy jednak ułatwić sobie to zadanie dzięki zastosowaniu enum . Każdej kolejnej wartości możemy przydzielić identyfikator, który łatwiej będzie zapamiętać. Jak widzisz w naszym programie zadeklarowaliśmy identyfikatory "Pn", "Wt", "Sr" itd. enum już sam zadba o przydzielenie im konkretnych wartości - tzn. "Pn" będzie odpowiadało wartości zero, "Wt" jeden itd. Jeśli z jakiegoś powodu chciałbyś, aby od pewnego identyfikatora nastąpił przeskok i żeby liczenie zaczynało się od innej wartości to podajesz ją po znaku równości. W naszym przykładzie identyfikatorowi "Sb" przydzieliliśmy wartość dziesięć (pamiętaj, że kolejnym identyfikatorom będa odpowiadały zmienione już wartości - w naszym przykładzie niedzieli będzie przydzielona wartość jedenaście). Teraz zamiast pisać:

DzienTyg = 2;

możemy po prostu napisać:

DzienTyg = Sr;

Ważne jest jednak, abyś pamiętał, że mimo faktu przypisywania zmiennej wartości poprzez nadane identyfikatory, to nadal są to zwykłe liczby.





13.2. Typedef

 


Przedstawione w poprzednim podpunkcie enum mimo faktu, że mamy tam wpływ na nadawanie nazw identyfikatorom, nie deklaruje jednak nowego typu. Zmienna typu wyliczeniowego jest ciągle zmienną typu int . Do deklaracji nowego typu danych służy instrukcja typedef . A oto przykład jej użycia:



  

#include &ltstdio.h>

 

typedef float rzeczywista;

 

void main(void)

{

rzeczywista a=4.5;

 

printf("%f\n", a);

}

  


Ogólna postać deklaracji to wygląda następująco:

typedef definicja_typu nazwa_nowego_typu;

W naszym przypadku przykład deklaracji nowego typu danych, któremu nadaliśmy nazwę "rzeczywista", mamy w drugiej linijce programu. Określiliśmy tam, że nowy typ będzie po prostu typem float tylko ze zmienioną nazwą. Ponieważ identyfikator "rzeczywista" odpowiada od tego momentu nowemu typowi danych to możemy zadeklarować sobie zmienną tego typu. W funkcji main deklarujemy zmienną o nazwie a. Ponieważ jej typ został wyprowadzony z typu float to możemy używać jej tak, jakby była to zmienna typu float. W naszym programie po prostu ją wyświetlamy na ekranie.

Zastanawiasz się zapewne czemu służy to polecenie, skoro możemy po prostu używać wbudowanego typu float . Odpowiedzi są trzy. Po pierwsze, deklaracja ta może dotyczyć o wiele bardziej złożonego typu danych. Po drugie, dzięki temu możemy w łatwy sposób przenieść nasz program na inny kompilator lub system. Przypomnij sobie, że na przykład zmienna typu int może na jednym kompilatorze zajmować cztery bajty, a na innym tylko dwa. Możemy w łatwy sposób temu zaradzić poprzez deklarację naszego nowego typu o nazwie na przykład "MOJINT" i wszędzie go używać. Teraz jeśli chcielibyśmy go przenieśc na inny kompilator to wystarczy jedynie zmienić deklarację typu i gotowe ! Trzecim powodem może być chęć zwiększenia precyzji obliczeń. Jeśli program wszelkie obliczenia wykonywał na zmiennych typu float to w takim przypadku musielibyśmy zmienić wszelkie wystąpienia tego typu na typ double . A tak wystarczy jedynie zadeklarować nowy typ, na przykład "MOJFLOAT" i używać go zamiast float , a przy konieczności zwiekszenia precyzji zmienić jedynie deklarację typu "MOJFLOAT" tak, żeby wyprowadzony był z typu double .





13.3. Struktury

 


W tym punkcie powiemy sobie o strukturach (odpowiednikach pascalowych rekordów), czyli o złożonym typie danych. Jest to typ danych tworzony przez programistę, który jest kombinacją wcześniej zdefiniowanych typów, włączając w to, oprócz typów prostych, inne typy zdefiniowane przez programistę (także inne struktury). Zaczniemy, jak zwykle, od przykładowego programu:



  

#include &ltstdio.h>

 

typedef struct {

int godziny;

int minuty;

int sekundy;

} CZAS;

 

void main(void)

{

CZAS teraz;

int ile_sekund;

 

teraz.godziny = 23;

teraz.minuty = 53;

teraz.sekundy = 21;

 

printf("Teraz jest %d:%d:%d\n", teraz.godziny, teraz.minuty, teraz.sekundy);

 

ile_sekund = teraz.sekundy + teraz.minuty*60 + teraz.godziny*3600;

printf("Od poczatku dnia uplynelo %d sekund.\n", ile_sekund);

}

 


Analizę programu zaczniemy od miejsca definicji nowego typu danych, który tym razem nie będzie, tak jak ostatnio, tylko odpowiednikiem prostego typu, lecz całkowicie nowym, złożonym typem (strukturą). Ogólna postać definicji struktury wygląda następująco:

typedef struct {

typ nazwa_pola1;

typ nazwa_pola2;

.

.

.

typ nazwa_polaN;

} nazwa_struktury;

W naszym przypadku zdefiniowaliśmy sobie strukturę o nazwie CZAS zawierającą trzy pola typu int (godziny, minuty i sekundy), która została zaprojektowana do przechowywania informacji o konkretnym czasie (stąd nazwa ;) Moglibyśmy co prawda te same informacje przechowywać w trzech osobnych zmiennych, ale co jeśli chcielibyśmy mieć dane o dwóch różnych godzinach ? Musielibyśmy dodać trzy nowe zmienne, co wkrótce doprowadziłoby do kompletnego chaosu. Struktura pozwala nam przechowywać potrzebne infromacje, przy czym wszystko znajduje sie w jednym miejscu - zamiast trzech, mamy tylko jedną zmienną. Przejdźmy dalej - na początku funkcji main widzimy deklarację zmiennej typu CZAS o nazwie "teraz", która będzie przechowywać potrzebne nam informacje. Czyli narazie nic nowego. Natomiast w następnych trzech linijkach widzimy zupełnie nową kostrukcję. Przedstawia ona w jaki sposób odwołujemy się do poszczególnych pól struktury - czyli: podajemy nazwę zmiennej (u nas nazywa się ona "teraz"), potem stawiamy kropkę, a następnie podajemy nazwę pola, do którego się odnosimy. Poza tym, że do poszczególnych pól odwołujemy się w nowy sposób, możemy z nich korzystać tak jakby byłaby to normalna zmienna o danym typie - czyli możemy przypisywać jej wartość, czy też używać wszelkich operatorów, co zostało pokazane na przykładzie obliczania liczby sekund, które upłynęły od początku dnia.

Strukturę można także zdeklarować także w nieco inny sposób niż przedstawiony wcześniej, a mianowicie:

struct nazwa_struktury {

typ nazwa_pola1;

typ nazwa_pola2;

.

.

.

typ nazwa_polaN;

};

Jednak ja zalecam stosowanie tego pierwszego sposobu, gdyż przy deklaracji zmiennej nie wystarczy napisać tak jak w poprzednim przypadku:

nazwa_struktury nazwa_zmiennej;

ale należy zastosować nieco dłuższa składnię:

struct nazwa_struktury nazwa_zmiennej;

Poza tym faktem, oba sposoby deklaracji nie różnią się niczym.





13.4. Unie

 


Unie "z wyglądu" są bardzo podobne do znanych Ci już struktur. Inny jest jednak cel ich wykorzystania. Służą one mianowicie efektywnemu wykorzystaniu pamięci. Każde ich pole zajmuje fizycznie tą samą komórkę pamięci, z tego też względu, w danym momencie może być wykorzystywane tylko jedno z ich pól. Jeśli wydaje Ci się to dziwne to przyjrzyj się następującemu przykładowi:



  

 

#include &ltstdio.h>

 

typedef union {

float szybkosc_w_wezlach;

int szybkosc_w_km;

} POJAZD;

 

void main(void)

{

POJAZD samochod, statek;

 

samochod.szybkosc_w_km = 220;

statek.szybkosc_w_wezlach = 34.5;

 

printf("Max. szybkosc samochodu to %d km/h\n", samochod.szybkosc_w_km);

printf("Max. szbkosc statku to %3.1f wezlow\n", statek.szybkosc_w_wezlach);

}

 

 


Założeniem programu jest przechowywanie informacji o maksymalnej szybkości danego pojazdu. Jednak pojazdem może być zarówno samochód, który ma tą informację wyrażoną w km/h, jak również statek, w przypadku którego wyraża się ją w węzłach. Tak więc, gdybyśmy wykorzystali do tego celu strukturę to jedno z pól nigdy nie byłoby wykorzystywane, przez co tracilibyśmy miejsce w pamięci. Co prawda w tym przypadku byłyby to zaledwie cztery bajty, ale gdybyśmy mieli dużą tablicę takich struktur (o których powiemy później) to strata byłaby już znaczna. W takim przypadku możemy wykorzystać unię - zadeklarowaliśmy dwa pola, do których możemy odwoływać się używając różnych nazw, jednak tak na prawdę zajmują one tylko tyle miejsca w pamięci, ile zajmuję największy element (w naszym przypadku oba pola mają wielkość cztery bajty, więc unia zajmuje w pamięci także cztery bajty). Myślę, że nie trzeba omawiać problemu od strony skłdniowej - wystarczy powiedzieć, że składnia jest dokładnie taka sama jak w przypadku struktur. Jedyną różnicą jest to, że zamiast słowa kluczowego struct używamy słowa kluczowego union .





13.5. Pola bitowe

 


Pola bitowe są kolejnym odcinkiem z serii "Oszczędzanie pamięci". Mają zastosowanie przy definicji struktur - przy pomocy tej konstrukcji możemy zadeklarować pole, którego wielkość będzie mniejsza niż jeden bajt (jeden lub kilka bitów - stąd nazwa). A oto przykład:



   

#include &ltstdio.h>

 

typedef struct {

unsigned char szyberdach : 1;

unsigned char abs : 1;

unsigned char ilosc_miejsc : 4;

} SAMOCHOD;

 

void main(void)

{

SAMOCHOD ford;

 

ford.szyberdach = 1;

ford.abs = 1;

ford.ilosc_miejsc = 2;

 

printf("Ilosc miejsc : %d\n", ford.ilosc_miejsc);

if(ford.szyberdach) printf("Posiada szyberdach.\n");

if(ford.abs) printf("Posiada ABS.\n");

}

  


Program ten przechowuje i wyświetla informacje o samochodzie, a konkretnie o liczbie miejsc, o tym, czy posiada szyberdach i ABS. Normalnie potrzebowalibyśmy zadeklarować strukturę o trzech polach, która, przy zastosowaniu jednobajtowego typu char, zajęłaby trzy bajty w pamięci. Dzięki zastosowaniu pól bitowych wszystkie te informacje zajmują jeden, jedyny bajt pamięci (i to, w przeciwieństwie do unii, z możliwością odwoływania się do wszystkich pól jednocześnie). Jak to możliwe ? Otóż ograniczyliśmy zakres poszczególnych pól. Zastanówmy się, ile potrzeba miejsca w pamięci, aby przechować informację o fakcie wyposażenia, bądź nie, samochodu w ABS ? Są dwa możliwe stany - jest lub nie ma. Czyli innymi słowy jedynka, albo zero - wystarczy jeden bit ! To samo tyczy się szyberdachu. Jeśli chodzi o liczbę miejsc to w tym przypadku ograniczyłem liczbę możliwych wartości do 16 - po prostu przeznaczyłem na to pole cztery bity (a 2^4 = 16). W ten sposób zamiast użwyać trzech, struktura ta używa zaledwie jednego bajtu.
Od strony składniowej dostęp do pól bitowych jest identyczny, jak do tych "normalnych". Jedyną różnicą jest sposób ich deklaracji - tzn. deklaruje się je tak samo, ale po typie i nazwie pola podaje się dodatkowo, po dwukropku, ilość bitów, które zamierzamy przeznaczyć na dane pole.


Cześć 14 kursu C/C++


 14. Tablice


Ponieważ istotę tablic najłatwiej jest pokazać na przykładach, to w następnych kilku podpunktach zostaną zaprezentowane przykładowe programy. Jednak zanim do nich dojdziemy przedstawię ich podstawowe cechy.
Tablic używa się w przypadku, gdy chcemy przechowywać dużą ilość danych tego samego typu przy zachowaniu łatwego do nich dostępu. Mimo tego, że tablica może przechowywać wiele danych jednego typu, odwołujemy się do niej za pomocą jednej nazwy. Jednak aby móc określić, o który dokładnie element nam chodzi, musimy użyć dodatkowo indeksu, czyli kolejnego (liczonego od zera) numeru elementu. Kolejne elementy są umiejscowione w pamięci komputera jeden za drugim.

 


14.1. Tablice o elementach typu prostego <![endif]--> 

 


W poprzednim punkcie zostały przedstawione ogólne informacje dotyczące tablic. Ponieważ jednak suchy tekst nigdy nie wyjaśni tematu tak dobrze jak przykład, posłużymy się właśnie tym narzędziem. Załóżmy, że chcemy napisać program, który obliczy nam średnią z pięciu ocen. Będzie on wyglądał następująco:



  

#include &ltstdio.h>

 

void main(void) {

float srednia;

int i;

float oceny[5];

 

// wpisujemy do tablicy przykladowe oceny z pieciu przedmiotow

oceny[0] = 3; oceny[1] = 5; oceny[2] = 5; oceny[3] = 3.5; oceny[4] = 3;

 

// sumujemy wszystkie oceny

srednia = 0;

for(i=0; i<5; i++) srednia += oceny[i];

 

// dzielimy sume przez ilosc ocen

srednia /= 5;

 

// wypisujemy wynik na ekranie

printf("Srednia ocen wynosi %1.1f\n", srednia);

}

 

 
Na początku programu nie ma nic nowego - włączenie pliku nagłówkowego, deklaracja dwóch zmiennych o nazwach srednia i i. Jednak w następnej linijce jest nowa rzecz - jak zapewne się już domyśliłeś jest to właśnie deklaracja tablicy. Przyjrzyjmy się jej uważnie - wygląda ona praktycznie identycznie, jak deklaracja zwykłej zmiennej. Najpierw wpisujemy typ danych, następnie nazwę naszej tablicy (tutaj oceny). Różnica pomiędzy deklaracją zwykłej zmiennej, a tablicą jest widoczna w ostatnim członie. Przy deklaracji tablicy musimy jeszcze podać jej wielkość. Robi się to podając tą wartość w nawiasach klamrowych. W naszym przypadku chcemy obliczyć średnią z pięciu ocen, tak więc zadeklarowaliśmy tablicę o wielkości pięć.
Do poszczególnych elementów w tablicy odwołujemy się przy pomocy indeksu. Indeks jest liczony od zera, tak więc w naszym przypadku do poszczególnych elemetów tablicy możemy się dostać używając indeksów o numerach od zera do cztery. Pokazane jest to w następnej linijce programu - wpisujemy tu kolejne oceny do tablicy. Własnie w dostępie poprzez indeks tkwi cała siła tablic. Szczególnie objawia się to w dwóch następnych linijkach programu. Pomyśl co by było, gdybyś chciał wykorzystać zwykłe zmienne zamiast tablic - musiałbyś zadeklarować pięć osobnych zmiennych typu float (np. o nazwach ocena1, ocena2... itd.), a następnie wpisać coś takiego:

srednia = ocena1 + ocena2 + ocena3 + ocena4 + ocena5;

srednia /= 5;

Przy tej ilości ocen do zsumowania jest to jeszcze akceptowalne, ale jeśli byłoby ich więcej to linijka ta koszmarnie by się wydłużyła. Dzięki temu zaś, że do tablicy możemy odwoływać sie używając indeksu, mogliśmy zawrzeć całe sumowanie w pojedynczej pętli przebiegającej od zera do czterech. W każdym jej przebiegu zmienna srednia zostaje zwiększona o wartość, która jest zawarta w elemencie tablicy o numerze i. Po całkowitym wykonaniu pętli zmienna srednia zawiera sumę wszystkich elementów tablicy i wystarczy ją tylko podzielić przez ich ilość (w naszym przypadku przez pięć), aby otrzymać średnią ocen, która zostanie wyświetlona w ostatniej linijce programu.





14.2. Tablice struktur

 


Oprócz tablic, których elementy będą typu prostego, w języku C można także budować bardziej złożone tablice. Dla przykładu w tym punkcie utworzymy tablicę struktur. Załózmy, że mamy do czynienia z bardzo małą firmą, w której pracuje trzech pracowników. Dla każdego pracownika chcemy mieć możliwość wyświetlenia informacji o jego pensji oraz numerze identyfikacyjnym. Poza tym chcemy także, aby program wyświetlał raport o kwocie, którą będziemy musieli co miesiąc przeznaczyć na wypłaty dla pracowników. Program realizujący te zadania będzie wyglądał następująco:

 

 

#include &ltstdio.h>

 

typedef struct {

int nr_id;

float pensja;

} PRACOWNIK;

 

void main(void) {

float suma_wyplat;

int i;

PRACOWNIK kadra[3]={ {25803, 1299.10}, {25809, 2100}, {7, 1500} };

 

// wyswietlamy informacje o pracowniku - jego nr id, oraz pensje

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

printf("Nr identyfikacyjny: %d\nPensja: %5.2f\n\n",

kadra[i].nr_id, kadra[i].pensja);

 

// obliczamy kwote potrzebna na wyplaty

suma_wyplat = 0;

for(i=0; i<3; i++) suma_wyplat += kadra[i].pensja;

 

printf("Suma wyplat wynosi: %5.2f\n", suma_wyplat);

}

 

 

 Zacznijmy analizę tego programu. Początek programu to dla nas nic nowego - deklaracja struktury o nazwie PRACOWNIK oraz dwóch zmiennych. Następna linijka też wygląda już znajomo, mamy tu deklarację tablicy o nazwie kadra, której elementy są strukturą PRACOWNIK. Jednak mamy tu także przykład pokazujący w jaki sposób możemy od razu zainicjalizować tablicę. Tak jak to było w przypadku "zwykłych" zmiennych po nazwie stawiamy znak równości, a następnie wartość, którą chcemy przypisać. Jednak o ile w tamtym przypadku od razu wpisywaliśmy liczbę, czy teżznak, to teraz jest to trochę bardziej skomplikowane. Wszystkie wartości, które chcemy wpisać do tablicy musimy zawrzeć w nawiasach klamrowych, oddzielając je od siebie przecinkami. W naszym przypadku mamy jednak do czynienia z sytuacją, gdzie każdy element tablicy jest także typem złożonym. Musimy więc zastosować tą technikę także oddzielnie dla każdego elementu tablicy, wpisując w nawiasach klamrowych wartości poszczególnych pól struktury - pierwsza wartość zostanie przypisana polu nr_id, natomiast druga wartość polu pensja. Tylko na pierwszy rzut oka wydaje się to strasznie zagmatwane. Wystarczy jednak uruchomić program i porównać wyniki wyświetlone na ekranie z wartościami wpisanymi w tej linijce i wszystko stanie się jasne. Dobrze, przejdźmy do analizy następnej linijki. Mamy tu do czynienia z podobną sytuacją jak w poprzednim punkcie. Pętla przebiega po kolei wszystkie elementy tablicy, jednak o ile w poprzednim programie były one sumowane, to w tym są po prostu wyświetlane na ekranie. Mamy tu także przykład odwołwania się do poszczególnych pól struktury, która jest elementem tablicy. Tak jak to było w przypadku jednej zmiennej strukturalnej, odwołujemy się do poszczególnych pól oddzielając je od nazwy zmiennej przy pomocy kropki. Ostatnie trzy linijki programu są praktycznie identyczne do tych, które były w poprzednim programie - po prostu sumujemy wartości pól pensja wszystkich elementów tablicy i sumę tą wpisujemy do zmiennej suma_wyplat. Otrzymaną wartość wypisujemy na ekranie w ostatniej linijce programu.




14.3. Tablice o wielkości ustalanej przy kompilacji

 


W tym punkcie zostanie przedstawiona jeszcze jedna ciekawa cecha tablic w języku C, a mianowicie określanie wielkosći tablicy przez kompilator na podstawie danych podanych przy jej automatycznej inicjalizacji. Rozważmy program z poprzedniego punktu, jednak zapisany w nieco inny sposób:



  

#include &ltstdio.h>

 

typedef struct {

int nr_id;

float pensja;

} PRACOWNIK;

 

void main(void) {

float suma_wyplat;

int i;

PRACOWNIK kadra[]={ {25803, 1299.10}, {25809, 2100}, {7, 1500} };

int wielkosc = sizeof(kadra) / sizeof(PRACOWNIK);

 

// wyswietlamy informacje o pracowniku - jego nr id, oraz pensje

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

printf("Nr identyfikacyjny: %d\nPensja: %5.2f\n\n",

kadra[i].nr_id, kadra[i].pensja);

 

// obliczamy kwote potrzebna na wyplaty

suma_wyplat = 0;

for(i=0; i < wielkosc; i++) suma_wyplat += kadra[i].pensja;

 

printf("Suma wyplat wynosi: %5.2f\n", suma_wyplat);

}

 

 


Z początku program ten wydaje się być identyczny jak poprzedni. Pierwsza różnica występuje dopiero w linijce, w której mamy deklarację tablicy. Zauważ, że w nawiasach kwadratowych nie została podana jej wielkość. Jednak mimo tego konstrukcja taka jest poprawna, ponieważ kompilator może domyślić się wielkości tablicy na podstawie ilości danych wprowadzonych w części inicjalizacyjnej. Ponieważ wpisaliśmy tam dane o trzech pracownikach to kompilator utworzy tablicę o trzech elementach. Jednak wynika z tego dla nas następne zadanie - musimy określić ile pracowników zawiera tablica, aby móc wyświetlić dane na ekranie. Poprzednio mieliśmy na stałe ustawioną wielkość tablicy na trzy, tak więc obie pętle for ustawiliśmy tak, aby wykonały się trzykrotnie. Zastanawiasz się zapewne po co w takim razie zastosowaliśmy taką konstrukcję, skoro musimy wykonywać dodatkowe prace aby program działał tak jak poprzednio. Otóż rozwiązanie jest proste - pomyśl co by było gdybyś zatrudnił czwartego pracownika. Musiałbyś wpisać go do tablicy i zmienić jej wielkość na cztery. Jednak to nie wszystko - we obu pętlach musiałbyś także zmienić warunek kontynuacji tak, aby wykonywały się cztery razy. A przy zastosowaniu konstrukcji użytej w tym programie jedyne co będziesz musiał zrobić, to wpisać nowego pracownika do tablicy - reszta wykona się automatycznie. Dodatkowy pracę, polegającą na napisaniu wyrażenia obliczającego wielkość tablicy będziesz musiał wykonać tylko jeden raz - podczas pisania programu. Korzyści są chyba oczywiste ? Przejdźmy więc do następnej linijki, która obliczy ilość elementów w tablicy. Zastanówmy się w jaki sposób możaby obliczyć tą wartość. Najprościej jest chyba podzielić wielkość całej tablicy przez wielkość pojedynczego jej elementu - właśnie ten sposób jest zastosowany w naszym programie. Aby uzyskać wielkość jakiejś danej musimy użyć funkcji sizeof . Zwraca ona wielkość (w bajtach) podanej jako parametr danej. W naszym przypadku użyliśmy jej dwa razy. Najpierw do określenia wielkości całej tablicy - "sizeof(kadra)", a następnie do określenia wielkości pojedynczego jej elementu (w naszym przypadku struktury PRACOWNIK) - "sizeof(PRACOWNIK)". Po podzieleniu pierwszej wartości przez drugą otrzymaliśmy ilość elementów w tablicy, którą to ilość przypisaliśmy zmiennej o nazwie wielkosc. Teraz, mając ilość elementów w tablicy, jedyne co musimy zrobić to zamienić w stosunku do poprzedniego programu obie trójki w pętli for na naszą zmienną wielkosc i to wszystko ! Program działa zgodnie z naszymi zamierzeniami.





14.4. Tablice wielowymiarowe

 


Oprócz jednowymiarowych tablic poznanych w poprzednich podpunktach, język C umożliwia tworzenie tablic wielowymiarowych. Ich zastosowanie zostanie pokazane na przykładzie dwuwymiarowej tablicy, która będzie odpowiadała planszy do gry "w statki." Jako założenie programu przyjmiemy, że polu pustemu odpowiada wartość zero w tablicy, natomiast jeśli na danym polu znajduje się jakiś statek, to ma ono wartość jeden. Poza tym dla uproszczenia programu postawimy dla planszy tylko trzy statki jednomasztowe. A oto jak wygląda taki program:



  

#include &ltstdio.h>

 

void main(void) {

int plansza[10][10];

int i, j;

 

// wyczyszczenie planszy - wypelnienie jej zerami

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

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

plansza[i][j] = 0;

// ustawienie trzech jednomasztowcow

plansza[3][6] = 1; plansza[8][3] = 1; plansza[2][9] = 1;

 

// wyswietlenie informacji na ktorych polach znajduja sie statki

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

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

if(plansza[i][j]) printf("Statek znajduje sie na polu %d,%d\n", i, j);

 

}

 

 
Zaraz w pierwszej linijce funkcji main mamy do czynienia z deklaracją tablicy dwuwymiarowej. Jak widzisz nie różni się ona mocno od deklaracji zwykłej, jednowymiarowej tablicy. Jedyną różnicą jest to, że występują tu dwie sekcje z nawiasami kwadratowymi, które określają wielkości poszczególnych wymiarów tablicy. W naszym przykładzie obie mają wielkość dziesięć, choć oczywiście mogą mieć różne wymiary. Jeśli chciałbyś utworzyć tablicę o więcej niż dwóch wymiarach, to wystarczy, że dopiszesz jeszcze jedną, lub więcej takich sekcji. Następnie mamy wyczyszczenie naszej tablicy zerami. Odbywa się to przy pomocy dwóch pętli for przebiegających od zera do dziewięciu. Pętla zewnętrzna, w której zwiększana jest zmienna i odpowiada poszczególnym wierszom, natomiast pętla wewnętrzna poszczególnym kolumnom tablicy. Masz tu także przykład w jaki sposób można odwoływać się do poszczególnych elementów tablicy wielowymiarowej. Różnica jest taka, jak przy deklaracji - wystarczy dodać dodatkową sekcję z nawiasami kwadratowymi, w których wpisujemy indeks dla danego wymiaru. W następnej linijce ustawiamy na planszy trzy statki jednomasztowe, czyli po prostu w trzech miejscach w tablicy wpisujemy wartość jeden. Ostatnie trzy linijki służa do wyświetlenia informacji, na których polach znajdują się statki. Sprawdzamy po kolei wszystkie elementy tablicy (czyli naszej planszy do gry) i jeśli którymś z nich ma wartość różną od zera to wyświetlamy informację o pozycji statku.




14.5. Tablice jako parametr funkcji

 


Długo się zastanawiałem, czy napisać o tym już teraz, w tym punkcie, czy też dopiero w następnym - już po wyjaśnieniu istoty wskaźników. Jednak zdecydowałem się umieścić to tutaj, gdyż mimo wszystko informacje te dotyczą głównie tablic. Jednak jeśli będziesz miał problemy ze zrozumieniem tego podpunktu to przeczytaj najpierw następny punkt o wskaźnikach, a następnie powróć do czytania tego podpunktu.

Na koniec wykładu o tablicach pozostała nam jeszcze do omówienia jedna rzecz, a mianowicie przekazywanie tablic do funkcji jako jeden z parametrów. Wiesz już co to są funkcje oraz jak przekazywać do nich zmienne jako ich parametry, tak więc z samą składniową stroną tego problemu nie powinieneś mieć kłopotów. Jedynym problemem może być zrozumienie istoty przekazywania tablic, ale myślę, że nawet jeśli teraz tego nie zrozumiesz, to po przeczytaniu następnego punktu zrozumiesz to z pewnością.
Jak zapewne pamiętasz z wcześniejszych punktów, w języku C mamy do czynienia z tzw. przekazywaniem przez wartość parametrów do funkcji. Znaczyło to, że nawet jeśli w ciele funkcji zmienna, która została do niej przekazana została zmieniona (tzn. przypisano jej nową wartość) to po powrocie do miejsca wywołania funkcji miała dalej starą wartość. Po prostu w momencie wywołania funkcji została utworzona kopia zmiennej, która została przekazana jako parametr i zmianom ulegałą właśnie ta kopia, a nie oryginalna zmienna. Od reguły tej jest jednak pewien wyjątek. Jak się zapewne domyślasz wyjątkiem tym jest przekazywanie do funkcji tablic. Związane jest to z tym, że tablice mogą mieć na prawdę ogromną wielkość i przy tworzeniu ich kopii mogłoby np. zabraknąć wolnej pamięci. Poza tym samo kopiowanie trwało by dość długo, a język C był zaprojektowany z myślą o jak najszybszym działaniu programów w nim napisanych. Tablice w języku C nie są więc przekazywane przez wartość, lecz zamiast tego przekazuje się tzw. wskaźnik do pierwszego elementu. Innymi słowy do funkcji przekazuje się adres pamięci, pod którym znajduje się pierwszy element tablicy (ułożenie tablicy w pamięci zostało omówione na początku tego punktu).
Aby pokazać sposób w jaki możesz przekazać tablicę do funkcji oraz udowodnić fakt, że nie są one przekazywane przez wartość napisałem program, który to zademonstruje. Przed dalszą lekturą skompiluj go proszę, uruchom i przyjżyj się uważnie wynikom jego działania.



 

 

#include &ltstdio.h>

 

int Suma(int tab[], int ilosc) {

int i, suma;

 

// obliczenie sumy wszystkich wartosci w tablicy

suma = 0;

for(i=0; i < ilosc; i++) suma += tab[i];

 

// robimy to aby udowodnic, ze tablica nie jest, natomiast zwykla zmienna

// jest przekazywana przez wartosc

tab[ilosc-1] = 11; ilosc = 100;

 

return suma;

}

 

void main(void) {

int tablica[]={6, 3, 123, 3, 5, 200};

int ilosc = sizeof(tablica) / sizeof(int);

 

printf("Przed wywolaniem funkcji ostatni element jest rowny %d.\n",

tablica[ilosc-1]);

printf("Przed wywolaniem funkcji zmienna ilosc jest rowna %d.\n",

ilosc);

 

printf("Suma wszystkich elementow jest rowna %d.\n", Suma(tablica, ilosc));

 

printf("Po wywolaniu funkcji ostatni element jest rowny %d.\n",

tablica[ilosc-1]);

printf("Po wywolaniu funkcji zmienna ilosc jest rowna %d.\n",

ilosc);

}

 

 

 


Większość konstrukcji użytych w tym programie powinna być dla Ciebie zrozumiała. Praktycznie wszystko było już użyte w poprzednich programach, z jednym wyjątkiem, a mianowicie deklaracją pierwszego parametru funkcji Suma jako tablicy. Przyjrzyj się sposobowi deklaracji - wygląda ona bardzo podobnie jak deklaracja parametru typu prostego. Jedyną różnicą jest to, że po nazwie podaliśmy jeszcze nawiasy klamrowe. Czyli

int Suma(int tab[], int ilosc)

przeczytamy jako: definicja funkcji o nazwie Suma zwracającej wartość typu int, która przyjmuje dwa parametry, z których pierwszy jest typu tablica intów i nazywa się tab, natomiast drugi jest typu int i nazywa się ilość.
Skoro już wiesz wszystko co potrzebne odnośnie składni zastosowanej w programie możemy przejść do jego analizy.
Zaczynamy, jak zwykle, od funkcji main . Początek to nic nowego - deklaracja tablicy intów o wielkości automatycznie obliczanej przez kompilator oraz deklaracja zmiennej o nazwie ilosc, która tą wielkość będzie przechowywać. Następnie, przy pomocy funkcji printf , wyświetlamy na ekranie zawartość ostatniego elementu tablicy oraz ilość elementów w niej zawartych. Następnej linijce będziemy musieli się przyglądnąć dokładniej - zawiera ona wywołanie funkcji Suma . Wartość, którą ta funkcja zwróciła zostaje przekazana jako parametr do funkcji main i zostaje wyświetlona na ekranie. Funkcja Suma oblicza sumę tylu elementów tablicy, która została przekazana jako pierwszy parametr, ile zostło przekazanych jako parametr numer dwa. W naszym przypadku funkcja ma obliczyć sumę wszystkich (bo zmienna ilość zawiera ilość elementów w tablicy) tablicy o nazwie... tablica. Przyjżyj się dokładnie sposobowi, w jaki przkazaliśmy oba parametry do funkcji Suma - po prostu podaliśmy nazwy zmiennych. Skoro wywołaliśmy naszą funkcję to przejdźmy do analizy jej wnętrza. Początek to nic nowego - sumujemy elementy tablicy w sposób taki, jak w poprzednich programach i uzyskaną wartość przypisujemy zmiennej o nazwie suma. Wartość tą zwracamy do miejsca wywołania przy użyciu return w ostatniej linijce ciała funkcji. Jednak we wcześniejszej linijce użyliśmy dwóch przypisań, aby zaprezentować to, o czym wspomniałem na początku tego podpunktu - tablice są przekazywane przez wskaźnik, w przeciwieństwie do innych typów przekazywanych przez wartość. Aby to udowodnić do ostatniego elementu tablicy wpisaliśmy liczbę jedenaście (na początku było tam dwieście), natomiast zmiennej ilosc przypisaliśmy wartość sto (była tam początkowo wartość sześć ponieważ tyle właśnie jest elementów w naszej tablicy). Po wykonaniu funkcji wyświetlamy ponownie zawartość obu tych danych, aby sprawdzić czy fakt, że zmodyfikowaliśmy je w funkcji return ma jakiekolwiek znaczenie. A oto jaki jest wynik wyświetlony na ekranie po uruchomieniu tego programu:

Przed wywolaniem funkcji ostatni element jest rowny 200.

Przed wywolaniem funkcji zmienna ilosc jest rowna 6.

Suma wszystkich elementow jest rowna 340.

Po wywolaniu funkcji ostatni element jest rowny 11.

Po wywolaniu funkcji zmienna ilosc jest rowna 6.

Jak widzisz wynika z tego jednoznacznie, że o ile w przypadku zmiennej ilosc jej wartość w ogóle się nie zmieniła, to w przypadku tablicy jej wartość uległa zmianie.

Mimo faktu, że język C nie posiada tzw. przekazania przez referencję (czyli odpowiednika var z pascala) to jednak sposób, który kompilator wykorzystuje przy przekazywaniu tablic do funkcji, możemy samodzielnie zastosować także w stosunku do typów prostych przy zastosowaniu tzw. wskaźników. Jest to jednak temat na tyle obszerny, że poświecony zostanie temu zagadnieniu cały następny punkt.

Cześć 15 kursu C/C++


15. Wskaźniki


Załóżmy, że zadeklarowaliśmy sobie w programie zmienną typu int o nazwie x i od razu przypisaliśmy jej wartość 124. Czyli wyglądałoby to następująco:

int x=124;

Załóżmy teraz, że kompilator umieścił naszą zmienną w pamięci pod adresem 1000. Czyli wyglądałoby to tak jak na rysunku przedstawionym powyżej. Spróbój teraz wczuć się w rolę kompilatora w przypadku, gdy w swoim programie każdesz mu wykonać następujące polecenie:

i = x;

Czyli do jakiejś zmiennej o nazwie i (która jest tu nieistotna) przypisujesz wartość zmiennej x. Pomyśl co musi zrobić kompilator, żeby wykonać to polecenie. Interesuje nas skąd kompilator wie jaką wartość ma nasza zmienna x ? Odpowiedź brzmi: nie wie. Jednak wie, w którym miejscu pamięci znajduje się zmienna. Sprawdza więc jaki jest jej adres - w naszym przypadku jest to adres 1000. Następnie odczytuje komórkę pamięci znajdującą się pod tym adresem i voila ! Otrzymuje w efekcie liczbę 124, która się tam znajduje. Zastanawiasz się teraz zapewne po co Ci to wszystko mówię ? Otóż podobnie, z naszego (czyli programisty) punktu widzenia, działają wskaźniki. Załóżmy, że oprócz naszej zmiennej x mamy jeszcze jedną zmienną (której deklaracji na razie nie podam), która ma wartość 1000. W naszym przypadku jest ona umieszczona pod adresem 1016. Przypatrz się teraz uważnie rysunkowi. Czy nic nie wydaje Ci się podejrzane ? Nasza druga zmienna zawiera wartość, która jest równa adresowi zmiennej x ! Tą drugą zmienną nazywamy wskaźnikiem i w przypadku, gdy chcemy odczytać zawartość komórki pamięci o adresie, który jest zawarty w tej zmiennej to mówimy kompilatorowi (oczywiście przy pomocy odpowiedniej składni): "Słuchaj, podaj mi zawartość komórki pamięci, której adres jest zawarty w tej oto zmiennej (i tu ją podajemy)". Kompilator oczywiście stosuje się do naszego polecenia i w efekcie mamy to o co nam chodziło.
To by było na tyle jeśli chodzi o wyjaśnienie pojęcia wskaźnika. W następnych podpunktach zostaną pokazane na podstawie przykładowych programów sposoby ich deklaracji i wykorzystania. Mam prośbę, żebyś ten punkt czytał szczególnie uważnie, gdyż jest to temat, który po pierwsze jest trudny, a po drugie bardzo ważny. Praktycznie nie ma żadnego poważniejszego programu, który nie operowałby na wskaźnikach.

I jeszcze jedna uwaga na koniec tego wstępu do wskaźników. Nie bierz, proszę, zbyt dosłownie sposobu odczytu watości zmiennej przez kompilator. Opisałem sposób, w jaki robiłby to gdyby był człowiekiem. Kompilator człowiekiem jednak nie jest i robi to w rzeczywistości trochę inaczej. Z Twojego punktu widzenia nie jest jednak ważne jak, ważne jest to, żeby zrobił to dobrze. Zastosowałem taką formę opisu w takim celu, żebyś mógł łatwiej zrozumieć istotę wskaźników. Mam nadzieję, że dzięki temu uda mi się wyjaśnić ten trudny dla początkującego temat w sposób przystępny.





15.1 Pierwszy program korzystający ze wskaźników

 


Ponieważ masz już teoretyczne podstawy nadszedł już czas, aby zaprezentować istotę wskaźników w praktyce. A oto nasz pierwszy program, w którym wykorzystamy wskaźniki:



 

 

 

#include &ltstdio.h>

 

void main(void) {

int a;

int *b;

 

a = 11;

printf("Zmienna a jest rowna %d\n", a);

 

b = &a;

printf("Wartosc wskazywana przez wskaznik b wynosi %d\n", *b);

 

*b = 14;

printf("Zmienna a jest rowna %d\n", a);

}

 

 


Zanim przejdziesz do dalszego czytania kursu skompiluj i uruchom powyższy program. Na ekranie powinno pojawić się:



Zmienna a jest rowna 11

Wartosc wskazywana przez wskaznik b wynosi 11

Zmienna a jest rowna 14

Jak zwykle przejdziemy teraz do analizy naszego programu. Na początku mamy zwykłą deklarację zmiennej o nazwie a, która jest typu int. Jednak już w następnej linijce mamy coś nowego - jak się domyślasz jest to deklaracja wskaźnika. Zauważ jak niewiele różni się ona od deklaracji zwykłej zmiennej. Na początku piszemy jakiego typu będzie nasz wskaźnik (tutaj jest to wskaźnik na int), a następnie nazwę zmiennej, którą jednak, w odróżnieniu od deklaracji zwykłej zmiennej, poprzedzamy gwiazdką. Następne kilka linijek ma za zadanie pokazać istotę wskaźników. Mam nadzieję, że to co po wyjaśnieniu teoretycznym mogło wydawać się niejasne, teraz stanie się zrozumiałe. Pierwsze dwie linijki tego fragmentu to nic nowego - zmiennej a&mbsp; przypisujemy wartość jedenaście i następnie wyświetlamy ją na ekranie. Jednak w następnej linijce mamy do czynienia z nową konstrukcją. Do zmiennej b (która, jak zapewne pamiętasz, jest typu wskaźnik na int) przypisujemy adres zmiennej a. Aby otrzymać adres danej zmiennej wystarczy postawić przed nią znak ampersand "&". Tak więc wyrażenie "&a" określa nam adres zmiennej a. Zwróć uwagę na to, co zostało podkreślone - adres, a nie wartość ! Tak jak to było w wyjaśnieniu teoretycznym: jeśli nasza zmienna a znajdowałaby się w pamięci komputera pod adresem 1000 to zmienna b będzie miała wartość 1000, a nie 11 (która jest wartością przechowywaną przez zmienną a). Następna linijka pokazuje jak odczytać wartość wskazywaną przez zmienną b - wystarczy przed nazwą zmiennej wskaźnikowej postawić gwiazdkę. Zapamiętaj więc: jeśli b jest zmienną wskaźnikową to wyrażenie "b" określa nam adres, który jest przechowywany w tej zmiennej, natomiast wyrażenie "*b" określa wartość przechowywaną w pamięci komputera pod adresem, który zawiera ta zmienna. W następnej linijce mamy pokazane do czego może nam to się przydać. Mamy tu do czynienia z taką oto sytuacją:



*b = 14;

Pamiętasz co oznaczało wyrażenie "*b" ? Byłąa to wartość wskazywana przez zmienną b. A ponieważ zmienna b wskazywałą na zmienną a (wskazania tego dokonaliśmy w linijce "b = &a;") to mimo tego, że w całym tym wyrażeniu nie jest wspomniana nawet zmienna a, to tak naprawdę dokonamy zmiany właśnie jej wartości ! Aby to udowodnić w następnej linijce wypisujemy na ekranie wartość zmiennej a - i rzeczywiście jest ona teraz równa czternaście. Jeśli ciągle nie wiesz dlaczego to przeczytaj uważnie jeszcze raz ten podpunkt (oraz podpunkt teoretyczny). Jest to niezmiernie ważne, gdyż bez jego zrozumienia nie uda Ci się zrozumieć podpunktów następnych.





15.2 Wskaźniki a tablice

 


Między tymi dwoma konstrukcjami w języku C zachodzi ścisły związek. Nazwa tablicy może być traktowana jako stały wskaźnik do pierwszego jej elementu. Wynika z tego, że tablicę można bezpośrednio (bez rzutowania) przypisać do wskaźnika (oczywiście zarówno tablica, jak i wskaźnik muszą być tego samego typu). Pokazuje to następujący program:



 

 

 

#include &ltstdio.h>

 

void main(void) {

int tablica[3] = {5, 10, 15};

int *wskaznik;

int i;

 

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

printf("tablica[%d] = %d\n", i, tablica[i]);

 

wskaznik = tablica;

for(i=0; i<3; i++, wskaznik++)

printf("*(wskaznik+%d) = %d\n", i, *wskaznik);

}

 

 


Na początku deklarujemy trzyelementową tablicę typu int (z automatyczną inicjalizacją), jedną zmienną będącą wskaźnikiem na int oraz "zwykłą" zmienną typu int. Zaraz po deklaracji zmiennych używanych przez program wyświetlamy w pętli wartości poszczególnych elementów tablicy. Czyli na razie nic nowego. To o czym traktuje ten podpunkt jest zawarte w następnych trzech linijkach programu. W pierwszej z nich dokonujemy przypisania tablicy do wskaźnika. Możemy to zrobić, gdyż, tak jak napisałem we wstępie do tego podpunktu, nazwa tablicy może być traktowana jako wskaźnik do jej pierwszego elementu. Zapamiętaj jednak że ta operacja przypisuje wskaźnikowi adres pierwszego elementu tablicy, a nie jego wartość ! Następne dwie linijki pokazują z kolei, że na wskaźnikach można także dokonywać pewnych operacji arytmetycznych. W naszym przykładowym programie w pętli for dokonujemy zwiększenia wskaźnika o jeden przy pomocy operatora ++ . Zapamiętaj jednak, że operacja ta zwiększa wskaźnik o jeden element typu, na który wskazuje wskaźnik, a nie o jeden bajt ! Czyli jeśli wskaźnik wskazuje (tak jak w naszym przypadku) na int to operacja zwiększenia wskaźnika spowoduje, że zmienna wskaźnikowa będzie zawierała adres następnego elementu typu int (czyli de facto będzie większa o cztery bajty, bo tyle właśnie zajmuje int). W większości przypadków wiedza ta co prawda nie będzie Ci potrzebna, ale czasami może okazać się przydatna (np. w sytuacji odnoszenia się do tego samego obszaru pamięci przy pomocy różnych wskaźników). Ostatnia linijka programu ma za zadanie wyświetlenie wartości, na którą wskazuje nasz wskaźnik. Zauważ, że użyliśmy tu konstrukcji "*wskaznik" ponieważ właśnie w ten sposób otrzymujemy tą wartość.





15.3 Wskaźniki do struktur


Kolejnym zagadnieniem, które chcę poruszyć jest używanie wskaźników, które wskazują na strukturę. Odwoływanie się do poszczególnych pól struktury jest nieco inne w takim przypadku, jednak różnica jest minimalna. Pokazuje to poniższy program (który jest drobną przeróbką programu już wcześniej przez nas analizowanego):



 

 

 

#include &ltstdio.h>

 

typedef struct {

int godziny;

int minuty;

int sekundy;

} CZAS;

 

void main(void)

{

CZAS teraz={23,53,21};

CZAS *wsk = &teraz;

 

printf("Teraz jest %d:%d:%d\n", teraz.godziny, teraz.minuty, teraz.sekundy);

printf("Teraz jest %d:%d:%d\n", wsk-godziny, wsk-minuty, (*wsk).sekundy);

}

 

 


Program ten ma za zadanie wyświetlić na ekranie godzinę, która zawarta jest w strukturze typu CZAS. Na początku deklarujemy sobie zmienną strukturalną o nazwie teraz i od razu przypisujemy jej konkretną wartość. W następnej linijce mamy do czynienia z deklaracją wskaźnika na strukturę typu CZAS, któremu także przypisujemy od razu wartość - w naszym przypadku inicjalizujemy go tak, aby wskazywał na naszą zmienną strukturalną teraz. Następne dwie linijki wyświetlają na ekranie ten sam tekst tylko używając do tego celu różnych zmiennych. W pierwszej z nich robimy to przy użyciu "normalnej" zmiennej strukturalnej - w takim przypadku, jak zapewne pamiętasz, nazwę pola oddzielamy kropką. W drugiej linijce mamy do czyniania z sytacją, w której odwołujemy się do pola struktury poprzez wskaźnik do niej. W takim przypadku jedyną różnicą jest to, że zamiast kropki stosujemy swego rodzaju strzałkę zbudowaną z dwóch znaków: minusa oraz znaku większości. W naszym programie odwołujemy się w ten sposób do dwóch pierwszych pól: godziny i minuty. Aby zademonstrować inną możliwość do pola sekundy odwołaliśmy się w nieco inny sposób. Jak zapewne pamiętasz konstrukcja "*nazwa", gdzie nazwa jest wskaźnikiem powoduje, że w efekcie otrzymujemy wartość, na którą wskazuje nasz wskaźnik. W tym przypadku właśnie to wykorzystaliśmy - użycie konstrukcji "*wsk" powoduje, że teraz mamy do czynienia z wartością, na którą wskazuje zmienna wsk (w naszym przypadku jest to strukturą CZAS). A ponieważ mamy teraz do czynienia ze zwykłą strukturą to do pola możemy się "dobrać" przy pomocy zwykłej kropki. Należy jednak tu pamiętać o tym, aby "*wsk" zawrzeć w nawiasach ponieważ operator * ma mniejszy priorytet od operatora .





15.4 Dynamiczny przydział pamięci

 


Pamiętasz tablice, prawda ? Wielkość tablicy musiała być ustalona już w momencie pisania programu. W przypadku prostych programów nie jest to problemem, ale co zrobić, gdy potrzebną wielkość tablicy będzie można określić dopiero po uruchomieniu programu ? Na przykład dane o pracownikach jakiejś firmy mają być odczytywne z pliku - w takim przypadku, aż do momentu otwarcia pliku wielkość ta nie jest znana. Można co prawda zadeklarować tablicę o wielkości dużo większej niż to prawdopodobnie będzie potrzebne, ale wiaże się z tym duże marnotrawstwo pamięci. Poza tym może okazać się, że w pewnym momencie wielkość ta i tak nie będzie wystarczająca. Z pomocą przychodzą nam wskaźniki i możliwość, którą udostępniają a mianowicie dynamiczny (czyli już w trakcie pracy programu) przydział pamięci operacyjnej. Ich zastosowanie do tego celu pokażemy jak zwykle na przykładowym programie. Aby nie wprowadzać niepotrzebnego zamieszania zdecydowałem się dokonać przeróbki programu już analizowanego (w podpunkcie poświeconemu tablicom struktur). Omówione zostaną tu tylko różnice. A oto program:



 

 

 

#include &ltstdio.h>

#include &ltstdlib.h>

 

typedef struct {

int nr_id;

float pensja;

} PRACOWNIK;

 

void main(void) {

int i;

PRACOWNIK *kadra;

 

// przydzielamy pamiec na dynamiczna tablice

kadra = (PRACOWNIK*) malloc(3 * sizeof(PRACOWNIK));

 

// sprawdzenie czy udalo sie zaalokowac pamiec

if(kadra != NULL) {

printf("Brak pamieci !"); exit(0);

}

 

// wpisujemy wartosci dla poszczegolnych pracownikow

kadra-nr_id = 25803; kadra-pensja = 1299.10;

(kadra+1)-nr_id = 25809; (kadra+1)-pensja = 2100;

(kadra+2)-nr_id = 7; (kadra+2)-pensja = 1500;

 

// wyswietlamy informacje o pracowniku - jego nr id, oraz pensje

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

printf("Nr identyfikacyjny: %d\nPensja: %5.2f\n\n",

kadra[i].nr_id, kadra[i].pensja);

 

// zwalniamy przydzielona pamiec

free(kadra);

}

 

 


Pierwsza różnica jest już przy dołączają plików nagłówkowych. Oprócz "standardowego" stdio.h dołączamy jeszcze stdlib.h . Plik ten zawiera deklaracje nowych funkcji, które użyjemy w naszym programie - malloc oraz free . Przejdźmy dalej - mamy tu deklarację struktury o nazwie PRACOWNIK, następnie zmiennej i. W następnej linijce natykamy się na następną różnicę - zamiast trzyelementowej tablicy struktur typu PRACOWNIK deklarujemy tu wskaźnik na strukturę typu PRACOWNIK. Następna linijka zawiera już całkiem nową rzecz - wywołanie funkcji malloc . Funkcja ta alokuje (przydziela dla programu) pamięć operacyjną o wielkości podanej przy wywołaniu (w bajtach). W naszym przypadku każemy przydzielić jej pamięć o wielkości "3 * sizeof(PRACOWNIK)". Jak zapewne pamiętasz operator sizeof zwraca wielkość w bajtach podanego parametru. Tak więc wynika z tego, że kazaliśmy funkcji malloc przydzielić tyle pamięci, aby zmieściły się w niej trzy struktury typu PRACOWNIK. Funkcja ta, w przypadku gdy przydział pamięci się powiódł, zwraca adres pierwszego zaalokowanego bajtu pamięci. Ponieważ nasz wskaźnik o nazwie kadra wskazuje na strukturę PRACOWNIK, a funkcja malloc zwraca wskaźnik typu void* to musimy jeszcze dokonać rzutowania. Rzutowanie wskażników robi się w ten sam sposób jak rzutowanie typów prostych - jedynym wyjątkiem jest dodanie gwiazdki za nazwą typu. Tak więc, aby dokonać rzutowania wskaźnika typu void* na wskaźnik na strukturę PRACOWNIK musimy dodać jeszcze "(PRACOWNIK*)" i dopiero teraz możemy przypisać ten adres naszej zmiennej. Jak już do tego doszliśmy to wyjaśnię jeszcze zastosowanie wskaźnika na void. Jak zapewne pamiętasz słowo kluczowe void określa brak typu. Nie można deklarować zmiennych typu void, jedynym dopuszczalnym miejscem, gdzie można go użyć jest określenie typu, który zwraca funkcja. Natomiast wskaźnika na void jak najbardziej można używać i na dodatek jest on bardzo przydatny. Możesz więc zadeklarować sobie zmienną typu wskaźnik na void. Bardzo ważną zaletą tego wskaźnika jest to, że możesz dokonać przypisania na niego dowolnego innego wskaźnika. Oczywiście, z uwagi na to, że typ na który on wskazuje nie jest określony nie możesz dokonywać na nim operacji arytmetycznych. Dobrze to taka mała dygresja, wróćmy do analizy naszego programu. Otóż napisałem, że funkcja malloc zwraca nam adres przydzielonego bloku pamięci. Co jednak jest w sytuacji, gdy przydział pamięci nie jest możliwy np. w sytuacji, gdy próbujemy sobie zaalokować 100 MB pamięci na komputerze wyposażonym w tylko 32 MB (pomijam tu istnienie pamięci wirtualnej). Otóż w takim przypadku zamiast adresu zwrócona zostaje wartość NULL . Jest to specjalnie zadeklarowana wartość mówiąca o tym, że wskaźnik jest pusty (czyli nie wskazuje na żadne dane). Dlatego, aby zabezpieczyć się przed używaniem pamięci, która nie została nam przydzielona należy zaraz po wywołaniu tej funkcji sprawdzić wartość, która zostałą zwrócona. Dokonujemy tego w następnej linijce. W naszym przypadku w sytuacji, gdy przydział pamięci nie był możliwy wypisujemy tylko na ekranie odpowiedni komunikat i wychodzimy z programu. Program możemy zakończyć w dowolnym momencie przy użyciu funkcji exit . Jedynym jej parametrem jest kod wyjścia, który zostanie przekazany do systemu operacyjnego. W przypadku, gdy nasz program nie będzie używany w plikach wsadowych nie jest on ważny. Skoro już udało nam się przydzielić pamięć i jesteśmy tego pewni to musimy wpisać do niej jakieś dane. W naszym programie będą to dane o pracownikach naszej firmy. Przypisania dokonujemy w taki sposób jak przedstawione to było w poprzednich podpunktach. W następnych trzech linijkach programu wyświetlamy dopiero co wpisane dane. Zauważ tu, że mimo tego, że zmienna kadra jest wskaźnikiem to uźywamy jej tu tak samo jakby była tablicą. Wynika to z zależności między wskaźnikami a tablicami przedstawionymi w jednym z poprzednich podpunktów kursu. W ostatniej linijce programu mamy do czynienia z drugą większą nowością - funkcją free . Funkcja ta zwraca do systemu przydzielona wczeniej przy pomocy funkcji malloc pamięć. Mimo, że w tym przypadku nie musieliśmy tego robić bo przy wyjściu z programu pamięć ta zostałaby automatycznie zwrócona, to jednak do dobrego zwyczaju programistycznego należy zwrócenie wszystkich zaalokowanych zasobów. Pamiętaj, że po zwolnieniu pamięci nie możesz już używać tego obszaru pamięci ! Teoretycznie możesz go modyfikować (bo ciągle masz do niego wskaźnik), ale nie powienineś, gdyż system operacyjny mógł przydzielić tą pamięć innemu programowi i takie grzebanie po zasobach drugiego programu może zakończyć tym, że program ten (lub nawet cały system) się zawiesi. W systemach operacyjnych z ochroną pamięci natomist może to spowodować błąd ochrony i niekontrolowane wyjście z twojego programu z błędem.


49




Wyszukiwarka

Podobne podstrony:
`C) Karta tytulowa czyli jak powinno wygladac spra
Lepkość-sciaga, Elektrotechnika AGH, Semestr II letni 2012-2013, Fizyka II - Laboratorium, laborki,
ćw- agresja[1], Przedszkole, Agresja, uczucia
Podstawowe pojŕcia Mikro
Psychologia Ogólna cz C 04 2013
M Łobocki ?C Wychowania
Spis?ch szczególnych preparatów
Morandi Don't look?ck
Psychologia Ogólna cz C 03 2013
Kiedy lepiej ćwiczyć
ćw 6(1)
c
2+cz%ca%8c%c6 NCW4X2CNEFZVBSFTPWNSNBTANFCHEQIZHJMRWNA
4 czucie bod%c5%bac%c3%b3w akustycznych oraz grawitacji cz I
ćw7 - Refrakcja i wyznaczanie momentu dipolowego, studia, chemia fizyczna
mostek Wheatstone'a(1), Elektrotechnika AGH, Semestr II letni 2012-2013, Fizyka II - Laboratorium, l
Badanie widma par rtęci za pomocą spektroskopu, studia, Budownctwo, Semestr II, fizyka, Fizyka labor
Test dla dor wersja skrˇcona
ćwiczenia2 slajd1
Pytania z Patofizjologii zebrane do 12 wИеcznie wersja 0 01 DODANE TESTY z wyjШЖ, wykИadвwek i egza