Programowanie strukturalne w
C++, pętle, instrukcje if, switch,
Podstawowa budowa programu;
strumienie
Najprostszy program w C++, jaki jest, każdy
widzi:
#include <iostream>
using namespace
std;
int main()
{ // pierwszy program
cout
<<
"Hello, I'm Jan B.\n"
;
return
0;
}
// pierwszy program – pierwszy program w C++
to komentarz, czyli dowolny opis słowny. Jest on całkowicie
ignorowany przez kompilator, natomiast może być pomocny dla
piszącego i czytającego kod.
Komentarze
piszemy w celu wyjaśnienia pewnych fragmentów kodu
programu, oddzielenia jednej jego części od drugiej, oznaczania funkcji
i modułów itp. Odpowiednia ilość komentarzy ułatwia zrozumienie
kodu, więc stosuj je często
W C++ komentarze zaczynamy od
//
(dwóch slashy), lub umieszczamy
je między /*
i */, na przykład:
/* Ten komentarz może być bardzo długi i składać się z kilku
linijek. */
Rys.1. Translacja programu C++
Funkcja
main()
Kiedy uruchamiamy nasz program, zaczyna on wykonywać kod
zawarty w funkcji main(). Od niej więc rozpoczyna się działanie aplikacji
– a nawet więcej: na niej też to działanie się kończy. Zatem program
(konsolowy) to przede wszystkim kod zawarty w funkcji main() –
determinuje on bezpośrednio jego zachowanie.
Rys.2. Struktura funkcji main()
W przypadku rozważanej aplikacji funkcja ta nie jest zbyt
obszerna, niemniej zawiera wszystkie niezbędne elementy.
Najważniejszym z nich jest nagłówek, który u nas prezentuje
się następująco:
W C++ Builder ta sama funkcja może mieć postać:
#include <iostream>
#include <conio.h>
//using namespace
std;
//int
main() // standard
void main()
{ // pierwszy program
std::
cout
<<
"Hello, I'm Jan B.\n"
;
getch();
// return 0; //standard
}
std::cout oznacza tak zwany strumień wyjścia. Jego
zadaniem jest wyświetlanie na ekranie konsoli wszystkiego, co doń
wyślemy – a wysyłać możemy oczywiście tekst.
Korzystanie z tego strumienia umożliwia zatem pokazywanie nam w oknie
konsoli wszelkiego rodzaju komunikatów i innych informacji. Będziemy go
używać bardzo często, dlatego musisz koniecznie zaznajomić się ze
sposobem wysyłania doń tekstu.
Dołączanie plików nagłówkowych
#include
<iostream>
#include
<conio.h>
Linijki które wcale nie są tak straszne, jak wyglądają na pierwszy rzut
oka. Przede wszystkim zauważmy, że zaczynają się one od znaku
#
,
czym niewątpliwie różnią się od innych instrukcji języka C++. Są to
bowiem specjalne polecenia wykonywane jeszcze przed kompilacją - tak
zwane dyrektywy. Przekazują one różne informacje i komendy,
pozwalają więc sterować przebiegiem kompilacji programu.
Pliki nagłówkowe
umożliwiają korzystanie z pewnych funkcji, technik,
bibliotek itp. wszystkim programom, które dołączają je do swojego kodu
źródłowego.
Preprocesor
"Chiałbym się kiedyś dowiedzieć, że preprocesor został usunięty.
Jednak jedyny realny i odpowiedzialny sposób, który może do tego
doprowadzić, polega na tym, żeby najpierw sprawić, że stanie się zbędny,
po czym zachęcić ludzi do używania jego lepszych odpowiedników."
Bjarne Stroustrup
Preprocesor
to specjalny mechanizm języka, który przetwarza tekst
programu jeszcze przed jego kompilacją
.
Rys. Linkowanie (łączenie) tworzy wolny od luki kod
wykonywalny
Zwyczajowy przebieg budowania programu
W
języku
programowania
nieposiadającym
preprocesora
generowanie docelowego pliku z programem przebiega, jak wiemy, w
dwóch etapach.
Pierwszym jest kompilacja, w trakcie której kompilator przetwarza
kod źródłowy aplikacji i produkuje skompilowany kod maszynowy,
zapisany w osobnych plikach. Każdy taki plik - wynik pracy
kompilatora - odpowiada jednemu modułowi kodu źródłowego.
W drugim etapie następuje linkowanie (łączenie) skompilowanych
wcześniej modułów oraz ewentualnych innych kodów, niezbędnych do
działania programu. W wyniku tego procesu powstaje gotowy
program.
Rys. Najprostszy proces budowania programu z kodu
źródłowego
Przy takim modelu kompilacji zawartość każdego modułu musi
wystarczać do jego samodzielnej kompilacji, niezależnej od innych
modułów. W przypadku języków z rodziny C oznacza to, że każdy moduł
musi zawierać deklaracje używanych funkcji oraz definicje klas, których
obiekty tworzy i z których korzysta.
Gdyby zadanie dołączania tych wszystkich deklaracji spoczywało na
programiście, to byłoby to dla niego niezmiernie uciążliwe. Pliki z kodem
zostały ponadto rozdęte do nieprzyzwoitych rozmiarów, a i tak
większość zawartych weń informacji przydawałyby się tylko przez
chwilę. Przez tą chwilę, którą zajmuje kompilacja modułu.
Nic więc dziwnego, że aby zapobiec podobnym irracjonalnym
wymaganiom wprowadzono mechanizm preprocesora.
Dodajemy preprocesor
Ujawnił się nam pierwszy cel istnienia preprocesora: w języku C(+
+) służy on do łączenia w jedną całość modułów kodu wraz z
deklaracjami, które są niezbędne do działania tegoż kodu. A skąd
brane są te deklaracje?… Oczywiście - z plików nagłówkowych.
Zawierają one przecież prototypy funkcji i definicje klas, z jakich
można korzystać, jeżeli dołączy się dany nagłówek do swojego
modułu.
Jednak kompilator
nic nie wie
o plikach nagłówkowych. On tylko
oczekuje, że zostaną mu podane pliki z kodem źródłowym, do którego
będą się zaliczały także deklaracje pewnych zewnętrznych elementów
- nieobecnych w danym module.
Kompilator potrzebuje tylko ich
określenia „z wierzchu”, bez wnikania w implementację, gdyż ta może
znajdować się w innych modułach lub nawet innych bibliotekach i staje
się ważna dopiero przy linkowaniu
. Nie jest już ona sprawą kompilatora
- on żąda tylko tych informacji, które są mu potrzebne do kompilacji.
Niezbędne deklaracje powinny się znaleźć na początku każdego
modułu
. Trudno jednak oczekiwać, żebyśmy wpisywali je ręcznie w
każdym module, który ich wymaga. Byłoby to niezmiernie uciążliwe,
więc wymyślono w tym celu pliki nagłówkowe… i preprocesor. Jego
zadaniem jest tutaj połączenie napisanych przez nas modułów oraz
plików nagłówkowych w pliki z kodem, które mogą być bez przeszkód
przetworzone przez kompilator.
Rys. Budowanie programu C++ z udziałem preprocesora
Skąd preprocesor wie, jak ma to zrobić?… Otóż, mówimy o tym
wyraźnie, stosując dyrektywę
#include
. W miejscu jej pojawienia się
zostaje po prostu wstawiona treść odpowiedniego pliku nagłówkowego.
Włączanie nagłówków nie jest jednak jedynym działaniem
podejmowanym przez preprocesor. Gdyby tak było, to przecież nie
poświęcalibyśmy mu całego rozdziału. Jest wręcz przeciwnie:
dołączanie plików to tylko jedna z czynności, jaką możemy zlecić temu
mechanizmowi - jedna z wielu czynności…
Wszystkie zadania preprocesora są różnorodne, ale mają też kilka
cech wspólnych. Przyjrzyjmy się im w tym momencie.
Dyrektywy
Polecenie dla preprocesora nazywamy jego dyrektywą (ang.
directive). Jest to specjalna linijka kodu źródłowego, rozpoczynająca się
od znaku
#
(hash), zwanego płotkiem:
#.
Na nim też może się zakończyć
- wtedy mamy do czynienia z dyrektywą pustą. Jest ona ignorowana
przez preprocesor i nie wykonuje żadnych czynności.
Przed hashem mogą znajdować się wyłącznie tzw. białe znaki, czyli
spacje lub tabulatory. Zwykle nie znajduje się nic.
Bardziej praktyczne są inne dyrektywy, których nazwy piszemy zaraz
za znakiem
#
. Nie oddzielamy ich zwykle żadnymi spacjami (choć można
to robić), więc w praktyce płotek staje się częścią ich nazw. Mówi się więc
o instrukcjach
#include
,
#define
,
#pragma
i innych, gdyż w takiej
formie zapisujemy je w kodzie.
Dalsza część dyrektywy zależy już od jej rodzaju. Różne „parametry”
dyrektyw poznamy, gdy zajmiemy się szczegółowo każdą z nich.
Dyrektywy preprocesora kończą się zawsze przejściem do następnego
wiersza.
Zapamiętaj!
Nie kończ dyrektyw preprocesora średnikiem. Nie są to przecież
instrukcje
języka
programowania,
lecz
polecenia
dla
modułu
wspomagającego kompilator
.
Oto pliki należące do standardowej biblioteki C (włącznie z ISO C 9X).
assert.h - makra do asercji
ctype.h -klasyfikacje znaków typu char (isspace, isalpha itd.)
errno.h - deklaracja errno
fenv.h - środowisko dla liczb zmiennoprzecinkowych (ISO C 9X)
float.h - definicje specjalne dla liczb zmiennoprzecinkowych
limits.h - makra określające granice dla typów ścisłych
locale.h - definicje lokali
math.h - funkcje matematyczne
setjmp.h - funkcje setjmp i longjmp
signal.h - sygnały
stdarg.h - narzędzia dla funkcji o zmiennej liście parametrów
stddef.h - standardowe definicje (ptrdiff_t i size_t głównie)
stdio.h - operacje wejścia/wyjścia
stdlib.h - zespół funkcji użytkowych
string.h - funkcje operujące na tablicach znaków
time.h - narzędzia do odczytywania, interpretacji i prezentacji czasu
wchar.h - obsługa "szerokiego" (wide-char) zestawu znaków
wctype.h - wersja `ctype.h' dla szerokich znaków
Dyrektywa #define
Dyrektywa ta pozwala tworzyć makrodefinicje. Pozwala ona zastąpić
dowolny ciąg znaków (również pusty) identyfikatorem. Może służyć
np. do definiowania stałych:
#define przyciaganie_ziemskie 9.81
Taka makrodefinicja może również posiadać argumenty, np.
#define ctg( x ) 1/tan( x )
Znajomość tej dyrektywy raczej nie będzie Ci potrzebna; najwyżej
do tego, żeby wiedzieć, co to jest. Jest to uniwersalne dosyć
narzędzie, ale bardzo niebezpieczne i dające mnóstwo okazji do
popełniania błędów.
Kompilator nie stwierdzi błędu w definicji makra (jeśli np. zdefiniujesz
jako desygnat makra jakąś konstrukcję, która jest niepoprawna
składniowo w C++), a ewentualnie dopiero w miejscu, gdzie zostało ono
użyte (kompilator nie widzi makr ani ich używania; preprocesor jest
właśnie od tego, żeby je usuwać).
W przypadku zastępowania nimi
wyrażeń arytmetyczno-logicznych należy używać dla pewności jak
najwięcej nawiasów, oraz - o czym też wielu zapomina - NIE WOLNO
używać
żadnych
operatorów
modyfikujących
na
zmiennych
przekazywanych jako parametry makra
(tzn. jako argumenty makra
należy podawać wartości lub zmienne raczej, niż wyrażenia).
Jednym też
z typowych błędów jest zakończenie tej dyrektywy średnikiem
.
Kompilacja warunkowa
Podstawową dyrektywą warunkową jest
#if
. Jako argument przyjmuje
ona warunek, który ma być spełniony (pamiętaj jednak, że interpretuje
go preprocesor, a nie kompilator!). Najczęściej jednak do kompilacji
warunkowej stosuje się makrowartowniki. Są to puste makra,
definiowane w plikach nagłówkowych, dla np. zabezpieczenia przed
kilkakrotnym wstawianiem tego samego pliku. Sprawdzenia tego
dokonujemy dyrektywami #ifdef i #ifndef, czasem używa się też #if
i funkcji preprocesora defined():
#ifndef __STDLIB_H
lub
#if !defined( __STDLIB_H )
i dalej:
#define __STDLIB_H ... (tutaj deklaracje) #endif
Inne dyrektywy
Znacznie rzadziej są w programach używane dyrektywy takie, jak:
1. #error
- powoduje wyrzucenie błędu kompilacji z podanym jako
argument komunikatem (używane tylko wespół z dyrektywami
warunkowymi),
2. #pragma
- zmienia ustawienia kompilatora (użycie tej dyrektywy
zależy wyłącznie od implementacji)
3. #line
- udaje, że następna linia jest inną linią z innego pliku
Dyrektywa pusta (składająca się tylko ze znaku `#') jest dopuszczalna i
nie daje żadnego efektu
Instrukcje i bloki
Instrukcja prosta jest to wyrażenie występujące w przeznaczonym dla
niego miejscu i zakończone średnikiem. Instrukcja złożona zaś, zwana też
blokiem, jest to jedna lub więcej instrukcji prostych, ujętych w
{ }.
Blok
taki -
proszę pamiętać
-
posiada już osobny, lokalny zasięg
;
toteż
identyfikatory w nim definiowane mają zasięg tylko wewnątrz tego bloku i
mogą "przysłaniać
" (ang. hide)
identyfikatory znajdujące się w wyższym
zasięgu.
No to może tak mały przykład:
Zauważ, że zadeklarowano zmienną w wyrażeniu będącym
argumentem instrukcji
if.
W C++ jest to dopuszczalne, ale proszę się
jednak starać tego nie nadużywać. Zmienna ‘a' jak widać jednak, ma
zasięg tylko dla instrukcji podporządkowanej
if
(jest podana instrukcja
prosta, ale można też podać złożoną, tak jak w każdym przypadku).
Podobnie jest ze wszystkimi tego typu instrukcjami.
Jak widać, mamy trzy różne zmienne x o różnych zasięgach.
Ponieważ zdarza się deklarować zmienne lokalne o takich samych
nazwach, jak globalne, dlatego istnieje operator ::, zwany operatorem
zasięgu. Jego jednoargumentowa (przedrostkowa) postać nakazuje
uzyskać identyfikator z najwyższego zasięgu.
#include <iostream>
using namespace std;
int x = 5;int main()
{ // blok funkcji main
int x = 0; // zmienna lokalna dla bloku funkcji main
{ // lokalny blok
int x = 2; cout << "Wewnętrzna: " << x << endl;
}
cout << "Lokalna: " << x << endl;
cout << "Globalna: " << ::x << endl;
if ( int a = x + 2 > 0 ) cout << a << endl; // instrukcja
podporządkowana if ( ) return 0;}
Operator ::
ten istniał już w
C
, ale tylko jako operator
jednoargumentowy, czyli o takim znaczeniu, jakie tu zostało podane.
Później poznamy ten operator w wersji dwuargumentowej. W
przeciwieństwie do swojego jednoargumentowego przodka, ten
dwuargumentowy jest operatorem jednym z częściej używanych w
C+
+.
Nadal zresztą nazywa się operatorem zasięgu
Instrukcje sterujące
Słowa
kluczowe C++
Program byłby „głupi”, gdyby przebiegał krokowo od początku do
końca. Dlatego też w programie praktycznie zawsze używa się instrukcji
sterujących, na które składają się:
instrukcje odgałęzienia warunkowego: if, else ;
pętle: while, do-while, for
instrukcje przełączające: switch/case;
instrukcje skoku: goto, break, continue;
instrukcja powrotu z funkcji, return.
if
( <warunek> )
{nstr}
else
{instr}
Oczywiście
else
z całą resztą jest opcjonalne.
if
Przejdźmy zatem do pętli. Najprostszą pętlą z wyrażeniem
warunkowym jest pętla
`while
':
while ( <warunek> )
<instr>
Instrukcja ta nakazuje powtarzać <instr> dopóki spełniony jest
<warunek>. Zwraca się uwagę, że <warunek> sprawdzany jest na
początku, a więc przed pierwszym wejściem i każdym następnym
powtórzeniem. Nieco inne możliwości prezentuje nam pętla do-while:
Pętla
while
może nie wykonać się ani razu, jeżeli jej warunek będzie od
początku nieprawdziwy.
do
{
<instr>
}while ( warunek );
Tu <warunek> sprawdzany jest na końcu pętli, toteż pętla wykona się
bezwzględnie co najmniej raz.
Rysunek Działanie przykładowej pętli
do
for
( int i = 0; i < 5; i++ )
{
//instrukcje
….
…..
}
for (;;)
{ // tu coś robimy...
// tu trzeba sprawdzić warunek
// i przerwać... tylko jak? // (dalej)
}
Pętla nieskończona
Do przerywania wykonywania pętli służy instrukcja
break
. Może
być ona używana w dowolnej pętli i powoduje wykonanie skoku do
pierwszej instrukcji za pętlą. Jeśli zależy nam z kolei na skoku na
początek bieżącej pętli, służy do tego celu słowo
continue
. Proszę
pamiętać jednak że słowo
continue
ma zupełnie inne znaczenie dla
pętli
while
i
do-while
, niż dla pętli for! W przypadku tych pierwszych
powoduje normalny skok na początek pętli, podczas gdy w ‘for'
powoduje przejście do następnej iteracji. W praktyce więc skacze do
wyrażenia <next> i dopiero potem jest sprawdzany <warunek>.
switch
( <wartość> )
{
case <jedna_możliwość>: <instr> <instr> ...
case <druga_możliwość>: <instr> <instr> ...
default: <instr> <instr> ...
}
W większości przypadków należy zatem kończyć fragment kodu
rozpoczęty przez
case
instrukcją
break
- gwarantuje to, iż tylko jedna z
możliwości ustalonych w
switch
zostanie wykonana.
Pytanie?, co się stanie, kiedy umieszczę
continue
wewnątrz
switch
(pytanie sugeruje, że switch rozpocznie sprawdzanie od
początku). Najlepsza odpowiedź: po prostu, continue może się
pojawić tylko wewnątrz pętli ;*).
Instrukcja continue w ogóle nie ma związku ze switch
.
Instrukcja przełączająca
switch
porównuje po kolei <wartość> z
kolejnymi wariantami. Etykieta default - jak się zapewne można
domyślać - jest miejscem skoku w przypadku gdy <wartość> nie pasuje
do żadnego wariantu.
Dlaczego radzę się jej wystrzegać? Dlatego, że jest bardzo prosta i
zachęcająca w stosowaniu do tego stopnia, że
większość programujących
zapomina o tym, że bywa powolna
.