Zazwyczaj instrukcje programu wykonywane są jedna po drugiej w porządku w jakim zostały napisane. Jest to wykonanie sekwencyjne. Język C++ pozwala programiście określić inny sposób wykonania programu tzn. kolejne wykonane wyrażenie będzie inne niż następne występujące w sekwencji jest to tzw. przekazywanie sterowania. Wszystkie programy mogą być napisane w oparciu o trzy struktury sterujące : sekwencji, wyboru, powtórzenia. Struktura sekwencji jest wbudowana w C++. Dopóki nie jest powiedziane inaczej instrukcje wykonywane są jedna po drugiej. C++ daje programiście trzy struktury wyboru :instrukcja if, instrukcja if/else oraz switch. Są również trzy struktury wyboru : while, do/while, for. Daje to nam w sumie siedem struktur sterujących. Słowa te są słowami kluczowymi nie mogą być one użyte jako identyfikatory, a także dla nazw zmiennych.
Instrukcja if.
Instrukcja if ma następującą składnię :
if(warunek)
{
instrukcja;
instrukcja;
instrukcja;
...;
}
Jeśli warunek jest prawdziwy instrukcje wewnątrz nawiasów klamrowych są wykonywane. Jeśli warunek jest fałszywy instrukcje te są pomijane. Jeśli po instrukcji if ma być wykonana tylko jedna instrukcja można pominąć nawiasy klamrowe
Intstrukcja if - else
Podstawowa instrukcja warunkowa wygląda następująco:
if( warunek )
{
instrukcja1;
...;
}
else
{
instrukcja2;
...;
}
Jeżeli spełniony jest warunek to wykonane zostaną instrukcje zawarte w bloku 1, a gdy warunek nie jest spełniony to wykonane zostaną instrukcje z bloku 2.
Wartość zwracana przez warunek w instrukcjach warunkowych nie musi być typu
bool ale może być także wartością liczbową i w takim
przypadku warunek nie jest spełniony wtedy i tylko wtedy gdy wartość ta wynosi 0!
Gdy wartość jest różna od zera (również mniejsza od zera) to warunek jest spełniony.
Przykład:
#include <iostream>
using namespace std;
int main()
{
int x;
cout << "Podaj liczbę całkowitą:" << endl;
cin >> x;
if(x & 1) cout << "Liczba nieparzysta.";
else cout << "Liczba parzysta.";
return 0;
}
Po instrukcji warunkowej if nie musi występować słowo kluczowe else (oczywiście nie ma wtedy instrukcji 2).
Jeżeli w jednym z bloków znajduję się tylko jedna instrukcja to nie musi być ona zawarta w klamrach {} tak jak ma to miejsce w przedstawionym przykładzie.
Instrukcja switch - case
Ostatnią instrukcją warunkową jest konstrukcja switch - case, która pozwala nam na wybór więcej niż jednej opcji:
switch( <wyrażenie> )
{
case <wartość1>:
instrukcja1;
instrukcja2;
break;
case <wartość2>:
instrukcja1;
instrukcja2;
break;
default:
instrukcja1;
instrukcja2;
}
Instrukcja switch oblicza wartość <wyrażenia> i dopasowywuje go do jednej z podanych opcji.
Wszystkie opcje muszą być zawarte w bloku. Po słowie kluczowym case podajemy <wartość>, a następnie dwukropek. Po dwukropku podajemy instrukcje, które z kolei nie muszą być już zawarte w nowym bloku.
Jeżeli wartość <wyrażenia> będzie odpowiadała jednej z <wartości> to wykonywane są wszystkie instrukcje występujące po niej aż do napotkania słowa kluczowego break!
Jeżeli żadna z <wartości> nie będzie pasowała do wartości <wyrażenia> to wykonane zostaną instrukcje znajdujące się po słowie kluczowym default.
Jeżeli nie występuje opcja default i inne możliwości także nie pasują to nie zostanie
wykonana żadna instrukcja.
Przyład:
#include <iostream>
using namespace std;
int main()
{
short x;
cout << "Podaj cyfrę od 0 do 5" << endl;
cin >> x;
switch(x)
{
case 0: cout << "Wybrano 0" << endl;
break;
case 1: cout << "Wybrano 1" << endl;
break;
case 2: cout << "Wybrano 2" << endl;
break;
case 3:
case 4:
case 5: cout << "Wybrano 3, 4 lub 5" << endl;
break;
default: cout << "Wybrana liczba nie zawiera się w [0,5]" << endl;
}
return 0;
}
Instrukcja goto
Instrukcja goto jest instrukcją skoku bezwarunkowego. Powoduje ona "przeskoczenie"
programu do innego miejsca w tym samym bloku wskazanego etykietą:
goto <etykieta>;
//...
<etykieta>:
//...
Instrukcja skoku bezwarunkowego goto może być najczęściej zastąpiona przez pozostałe instrukcje warunkowe, może być także używana w połączeniu z nimi. Powinna być używana w sposób rozsądny i tylko tam gdzie rzeczywiście powoduje ona przyspieszenie programu bądź zwiększa jego czytelność (np. przerywanie zagnieżdżonych pętli).
Pętla while
Składnia:
while( <warunek> )
{
instrukcja1;
instrukcja2;
//...
}
Po słowie kluczowym while w nawiasach podajemy warunek a następnie instrukcje zawarte w bloku.
Na początku sprawdzany jest warunek.Jeżeli jest on spełniony to wykonywane są instrukcje zawarte w bloku. Następnie ponownie jeśli warunek jest spełniony wykonywanie są te same instrukcje z bloku.
UWAGA:
Warunek nie jest spełniony wtedy i tylko wtedy gdy jego wartość logiczna wynosi false co liczbowo odpowiada 0 (zeru). W przeciwnym wypadku warunek jest spełniony.
Pętla kończy się jeżeli warunek nie zostanie jest spełniony (możliwe jest, że instrukcje nie zostaną wykonane ani raz jeżeli warunek nie zostanie spełniony już przy pierwszym sprawdzaniu).
Istnieją jeszcze inne możliwości przerwania pętli:
Jeżeli wewnątrz bloku pętli wystąpi słowo kluczowe break to kolejne instrukcje zawarte w tym bloku nie zostaną już wykonane i pętla kończy swoje działanie.
Instrukcja goto zawarta w bloku może spowodować skok to etykiety znajdującej się poza blokiem pętli tym samym powodując jej przerwanie.
Wewnątrz bloku pętli może znajdować się także instrukcja continue, która co prawda nie powoduje natychmiastowego przerwania pętli ale sprawia, że kolejne instrukcje zawarte w bloku nie zostaną już wykonane i nastąpi ponowne sprawdzenie warunku pętli.
Przykład:
int i = 5;
while(i < 10) i++; //po wykonaniu pętli i = 10
while(i++) if(i==15) break; //jeśli i wynosi 15 to przerywamy wykonywanie pętli
while(true) goto etykieta1; //skok do etykiety
etykieta1:
while(i <= 20) //wyswietla liczby 15, 16, 17, 19 i 20
{
if(i==18) { i++; continue; }
cout << i++ << endl;
}
Pętla do - while
Składania:
do {
instrukcja1;
instrukcja2;
//...
}while( <warunek> );
Zwracam uwagę, że po nawiasach w których zawarty jest warunek musimy postawić średnik!
Pętla do-while bardzo przypomina pętlę while. Działa ona niemal tak samo z tą różnicą, że instrukcje zawarte w bloku są wykonane minimum jeden raz! Dopiero po ich wykonaniu sprawdzany jest warunek i zależnie od jego spełnienia pętla ponownie wykonuje dane instrukcje bądź jest przerywana.
Pętlę tą możemy przerywać tymi samymi sposobami co pętlę while.
Pętla for
Tradycyjnie zacznę od przedstawienia składni:
for(intrukcja1 ; <warunek> ; instrukcja2)
{
//instrukcje
}
Na początku wykonywana jest instruckcja1 (może być to kilka instrukcji oddzielonych przecinkami). Następnie sprawdzany jest warunek. Jeżeli jest spełniony to wykonywane są instrukcje zawarte w
bloku a jeśli nie to pętla kończy swoje działanie. Po wykonaniu tych instrukcji (oczywiście jeżeli pętla nie została zakończona wykonywana jest instruckcja2 (znowu może to być kilka instrukcji oddzielonych przecinkami) po której ponownie sprawdzany jest warunek.
Podsumowując:
Instruckcja1 wykonywana jest zawsze tylko raz na samym początku. Następnie sprawdzany jest warunek i wykonywane są instrukcje zawarte w bloku (war. spełniony).
Instruckcja2 wykonywana jest zawsze po wykonaniu instrukcji z bloku (przed sprawdzeniem warunku) - wykonuje się zatem tyle razy ile wykonane zostały instrukcje z bloku chyba, że pętla została przerwana wewnątrz bloku (break, goto czy return).
Jeśli wewnątrz bloku wystąpi instrukcja continue to instrukcje w tym bloku występujące dalej zostaną pominięte i wykonana zostanie instruckcja2 po której sprawdzony zostanie ponownie warunek.
Co to są funkcje?
Funkcje w programowaniu przypominają funkcje w matematyce. Np. funkcja sinus pobiera jako argument dowolną liczbę rzeczywistą i zwraca wartość rzeczywistą z przedziału [-1,1].
Podobnie funkcje w c++ mogą pobierać pewne argumenty i zwracać określony typ wartości.
Funkcję deklarujemy w następujący sposób:
<typ_zwracanej_wartości> nazwa_funkcji( <argumenty_funkcji> );
Na początku wskazujemy jaki typ wartości będzie zwracany przez funkcję. Może to być jeden z typów podstawowych (char, int) jak również typy złożone (struktury i klasy).
Następnie podajemy nazwę funkcji a po niej w nawiasach po przecinku wskazujemy jakie argumenty będzie przyjmować.
Funkcje, które nie zwracają żadnej wartości deklarujemy jako void:
#include <iostream>
using namespace std;
void napis()
{ //definicja ciała funkcji
cout << "Funkcja dziala poprawnie." << endl;
}
int dodaj(int a, int b)
{
return a+b;
}
int main()
{
napis(); //wywołanie funkcji
int wynik = dodaj(5,7);
cout << "5 + 7 = " << wynik << endl;
return 0;
}
Jak widać w powyższym przykładzie wartość zwracamy poprzez słowo kluczowe return po którym podajemy wartość jaki ma zwrócić dana funkcja. Jeżeli w ciele funkcji następuje zwrócenie wartości poprzez return to funkcja jest przerywana (jeżeli dalej są jakieś instrukcje to już się nie wykonają).
Przeciążanie funkcji
C++ pozwala nam na przeciążanie nazw funkcji. Polega to na tym, że kilka funkcji może jednocześnie mieć taką samą nazwę. Funkcje te muszą różnić się przyjmowanymi argumentami, tak aby kompilator mógł wybrać odpowiednią funkcję gdy zostanie ona wywołana:
int dodaj(int a, int b)
{
return a+b;
}
double dodaj(double a, double b)
{
return a+b;
}
int main()
{
dodaj(5,4); // dodaj(int,int)
dodaj(5.0,4); // dodaj(double,double)
return 0;
}
Funkcje inline
Jeżeli definicja (nie deklaracja) funkcji jest poprzedzona słowem inline oraz funkcja nie jest rekurencyjna i nie zawiera żadnych pętli to w miejscu wywołania tej funkcji kompilator wstawi jej kod, co spowoduje zwiększenie rozmiaru pliku wykonywalnego ale może spowodować także przyspieszenie jego działania.
inline int dodaj(int a, int b)
{
return a+b;
}
int main()
{
int a,b,c;
a = 5;
b = 9;
c = dodaj(a,b);
return 0;
}
Rekurencja
Rekurencja polega na odwoływaniu się funkcji do samej siebie. Należy przy tym uważać na to, aby taka rekurencja miała koniec.
Dobrym przykładem ilustrowania rekurencji jest obliczanie silni:
unsigned long silnia(unsigned a)
{
if(a <= 1) return 1;
return a*silnia(a-1);
}
Rekurencja jest mało wydajna i gdy tylko można zastąpić ją pętlą (czasem jest to bardzo trudne) należy to czynić:
unsigned long silnia(unsigned a)
{
unsigned long x=1;
while(a > 1) x *= a--;
return x;
}
Wartości domyślne funkcji
Język c++ pozwala nam na nadanie wartości domyślnych argumentom funkcji w czasie jej definicji. Wartości domyślne możemy nadawać tylko argumentom idąc od końca listy argumentów tzn.:
int dodaj(int a = 5, int b) //źle - nie nadano wartości domyślnej
{ return a+b; } //arg. b więc nie można nadać go arg. a
double dodaj(double a=3.1415, double b=2.7181) //ok
{ return a+b; }
float dodaj(float a, float b=0.5772) //ok
{ return a+b; }
int main()
{
dodaj(); //dodaj(3.1415, 2.7181)
dodaj(1); //błąd - niejednoznaczne
dodaj(1.0); //dodaj(1,2.7181)
dodaj(float(1)); //dodaj(1,0.5772)
return 0;
}
Odwołując się do funkcji z parametrami domyślnymi możemy zatem pomijać TYLKO parametry
końcowe o ile mają one nadane wartości domyślne.
Funkcje statyczne
Jeżeli deklaracja funkcji globalnej jest poprzedzona słowem kluczowym static to taka funkcja ma zasięg lokalny tzn., że nie możemy używać jej w innej jednostce kompilacji.
Nieco inne znaczenie ma static dla metod i pól ale o tym w dziale struktury.
Pliki nagłówkowe
W tym miejcu chciałbym wyjaśnić kwestię plików nagłówkowych. Otóż pliki te pozwalają nam
deklarować prototypy funkcji, klasy (struktury), szablony i tym podobne rzeczy. Takie postępowanie
często zwiększa przejrzystość kodu.
W pierwszym programie dołączyliśmy plik nagłówkowy <iostream>, który zawiera deklaracje
funkcji, których będziemy używać (oczywiście nie wykorzystujemy wszystkich dostępnych tam funkcji.
Bez tego pliku kompilator nie widział by jaki jest prototyp funkcji do której się odwołujemy - można sprawdzić jakie błędy wyrzuci kompilator jeśli nie dołączymy tego pliku nagłówkowego.
Jeśli przejrzymy kod pliku <iostream> zauważymy, że nie ma tam żadnych definicji funkcji (poza
deklaracjami), a mimo to program wykorzystujący te funkcje działa. Jak to możliwe?
Odpowiedź jest prosta. Kompilator w czasie kompilacji nie potrzebuje definicji funkcji - musi jedynie znać parametry tej funkcji oraz typ wartości zwracanej przez daną funkcję. Dopiero linker łączy skompilowany kod naszego programu z odpowiednimi bibliotekami. Biblioteki zawierają skompilowane funkcje, których prototypy znajdują się właśnie w plikach nagłówkowych. Dzięki wykorzystaniu plików nagłówkowych możemy zatem "ukryć" sposób działania danych funkcji.
Pliki nagłówkowe zazwyczaj mają rozszerzenie "*.h", rzadziej "*.hpp". Pliki te możemy dołączać do naszego programu na dwa sposoby:
#include <cstdio>
#include "moj.h"
Jeśli nazwa pliku zostanie objęta znakami <_nazwa_> to kompilator będzie szukał w katalogach wymienionych w odpowiedniej zmiennej systemowej lub w katalogach dodanych poprzez odpowiednie opcje kompilatora.
Jeśli natomiast nazwa zostanie ujęta w znaki cudzysłowu "_nazwa_" to plik będzie szukany poprzez ścieżkę względem bieżącego katalogu (np. "moj.h" oznacza po prostu plik w bieżącym katalogu, "../moj.h" oznacza plik w katalogu macierzystym względem bieżącego). W cudzysłowie można także wpisać ścieżkę bezwzględną (np. "C:/moj.h","/home/user/moj.h").
Definicje funkcji zadeklarowanych w plikach nagłówkowych umieszcza się w plikach "*.cpp". Na początku takiego pliku dołączamy plik nagłówkowy po czym definiujemy nasze funkcje.
Przykład użycia plików nagłówkowych:
figury.h:
#ifndef __figury_h__ /* zabezpieczenie przed wielokrotną deklaracją */
#define __figury_h__ /* dzięki temu plik może być dołączany wielokrotnie
w różnych częściach programu */
struct kolo {
double r;
double pole();
};
#endif
figury.cpp:
#include "figury.h" /* dołączamy nasz plik nagłówkowy */
double kolo::pole()
{
return r*r;
}
main.cpp:
#include "figury.h"
#include <iostream>
using namespace std;
int main()
{
kolo k;
cout << "Podaj promień koła:\t";
cin >> k.r;
cout << "Pole koła o promieniu " << k.r << " wynosi " << k.pole() << endl;
return 0;
}
Przestawiony program jest bardzo prosty i stosowanie pliku nagłówkowego raczej nie zwiększa przejrzystości kodu ale ilustruje sposób wykorzystania plików nagłówkowych.
Kompilacja np. przy użyciu g++ na platormie Linux mogłaby wyglądać następująco:
g++ -c main.cpp
g++ -c kolo.cpp
ld -o program main.o kolo.o
Co prawda to samo uzyskalibyśmy kompilując całość komendą "g++ -o program main.cpp kolo.cpp" ale chciałam pokazać, że kompilacja pliku main.cpp może zostać wykonana bez definicji funkcji zawartych w naszym pliku nagłówkowym.