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 <stdio.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 <stdio.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 <stdio.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 <stdio.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 <stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.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
<stdio.h>
#include
<stdlib.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.