Pojęcia wstępne, ułatwiające zrozumienie procesu tworzenia programu.
Kompilator - to program, który tłumaczy kod źródłowy na kod maszynowy - kod zrozumiały dla danego typu procesora. Jedna instrukcja kodu źródłowego odpowiada wielu instrukcjom kodu maszynowego. Niektóre kompilatory tłumaczą najpierw kod źródłowy na kod assemblera, a następnie ten jest tłumaczony na kod maszynowy. Język C++ jest językiem wysokiego poziomu, oznacza to, że jednej instrukcji w tym języku odpowiada do kilkuset instrukcji w języku maszynowym. Język assembler jest językiem niskiego poziomu. Jednej instrukcji w tym języku odpowiada kilka instrukcji w kodzie maszynowym.
Przykładowe kompilatory dla języka C++:
Borland C++ - darmowy do użytku niekomercyjnego
Turbo C++ - darmowy
Dev-C++- darmowy
C++ Server Pages - tworzenie aplikacji webowych przy użyciu C++
Kompilacja - jest to proces tłumaczenia danego kodu źródłowego na program wykonywalny (kod maszynowy).
Kod źródłowy - jest to tekst wpisywany do okienka kompilatora, charakterystyczny dla danego języka programowania np. C++, Pascal, Java itd.
W języku C++, kod źródłowy zapisujemy w plikach tekstowych o następujących rozszerzeniach:
*.cpp - główny kod źródłowy, w którym znajduje sie funkcja main(), np. program.cpp
*.h - pliki nagłówkowe (bilbioteki)
*.hpp - definicje klas
Słowo kluczowe - jest to słowo zarezerwowane przez dany język programowania, które można używać zgodnie z przeznaczeniem i w określonej sytuacji, np. w C++: auto, for, if, continue, break, case, switch, while itd.
Ogólna struktura programu w C++ (generowana automatycznie przy tworzeniu projektu przez kompilator Dev-C++) składa się z kilku części, które zostaną opisane poniżej:
|
|
---|---|
|
|
|
|
|
Ta część programu opisuje biblioteki jakie mają być dołączone do programu. Biblioteki to pliki, których zawartość jest dołączana do programu za pomocą dyrektywy preprocesora#include. Biblioteka to plik o nazwie podanej w nawiasie "< nazwa biblioteki >", który posiada między innymi definicje przydatnych funkcji.
Użycie funkcji pierwiastkującej zdefiniowanej w bibliotece cmath |
---|
|
Np. biblioteka cstdlib zawiera funkcje ogólne takie jak konwersje, alokacja pamięci czy funkcje matematyczne. Biblioteka iostream (input/output stream) jest standardową biblioteką wejścia/wyjścia w C++. Jeśli chcemy coś wyświetlać na ekranie (za pomocą obiektu cout i przeciążonego operatora "<<"), lub pobierać dane z klawiatury (za pomocą obiektu cin i przeciążonego operatora ">>") musimy ją dodać do nagłówka programu.
Wcześniejsze wersje bibliotek miały rozszerzenie "*.h". Język C++ odchodzi od takiego nazewnictwa, gdyż pliki z tym rozszerzeniem były początkowo wykorzystywane w języku C (oczywiście C++ może nadal korzystać z tych bibliotek) i dla rozdzielenia bibliotek kojarzących się z danym językiem, C++ przyjął nazwy bez rozszerzenia. Niektóre biblioteki zostały przekształcone z C na C++, i w takich przypadkach pozbyto się rozszerzenia, ale dodano literkę "c" na początku takiego pliku np.:
|
|
---|---|
|
|
Dyrektywę using namespace musimy użyć w przypadku, gdy zamiast pliku iostream.h, będziemy używać iostream, w celu udostępnienia definicji zawartej w tym pliku. Generalnie chodzi o to, żeby nie pisać za każdym razem wywołanie obiektu cout czy cin z przedrostkiem std::. Np.:
|
---|
Wyobraźmy sobie, że mamy dwa pakiety, w których zdefiniowana jest funkcja o nazwie wspaniala(). Pierwszy pakiet jest od producenta JANEK, a drugi odMARCIN. Jeśli chcemy używać funkcji wspaniala() od MARCIN, bo uważamy, że jest lepsza, udostępniamy definicję przestrzeni nazw MARCIN:
using namespace MARCIN;
i nie musimy za każdym wywołaniem funkcji wspaniala(), pisaćMARCIN::wspaniala().
Wiąże się to także z uproszczeniem uaktualnienia starszych wersji programu, dodając tylko odpowiednią dyrektywę bez konieczności dopisywania do każdego elementu przedrostka.
Funkcja main() jest charakterystyczną funkcją w C++, która musi występować w każdym konsolowym programie. Wszystko co zaczyna się dziać w danej aplikacji, jest określana w ciele właśnie tej funkcji. Oczywiście wszystkie inne funkcje mogą być wywoływane z wnętrza tej funkcji.
Funkcja main() ma kilka postaci:
int main() lub int main(void) - postacie równoważne, oznaczające, że w ciele funkcji pojawia się informacja zwrotna (return 0 lub returnEXIT_SUCCESS), która zwraca do systemu operacyjnego informacje o zakończeniu działania danej aplikacji
void main() - nie posiada informacji zwrotnej o zakończeniu działania programu, niezalecane, a w niektórych systemach niedopuszczalne
main() - to samo co int main()
int main(int argc, char *argv[]) - funkcja z argumentami opisana poniżej.
Podczas uruchomienia programu za pomocą konsoli (w Windowsie: cmd.exe), oprócz podania nazwy programu, który chcemy uruchomić, możemy przekazać wstępne dane (parametry programu). Argument int argc, mówi ile tych danych jest, natomiast char *argv[] przechowuje te dane. Prześledźmy przykład programuprogram.exe, który wyświetli argumenty danego programu:
program.cpp |
---|
|
Przykładowe wywołanie programu program.exe i wynik działania:
Wywołana została aplikacja z argumentami: arg1, arg2, x oraz 8991. Zauważmy, że argumenty oddzielamy spacją.
Jeśli program wywołujemy bez dodatkowych parametrów, listę argumentów funkcji main() można pominąć.
W tej części programu zaczyna się życie naszej aplikacji. Wszystko to co tu napiszemy, będzie rzutowało na sposób zachowania się naszego programu. Oprócz słowa kluczowego return, które zostało wyjaśnione w części III, domyślnie dopisana zostaje przez Dev-C++instrukcja system("pause"). Powoduje ona zatrzymanie się programu w tym miejscu i wyświetlenie komunikatu: "Aby kontynuować, naciśnij dowolny klawisz . . .".
Zauważmy, że blok funkcji main(), zaczynamy nawiasem "{" i kończymy nawiasem "}".
W trakcie pisania programu orientujemy się za co odpowiadają poszczególne części kodu źródłowego. Wracając po miesiącu ponownie do tego samego programu, autor potrzebuje znacznie więcej czasu aby zrozumieć ideę działania poszczególnych bloków kodu. Popatrzmy na inny przykład. Analiza nieswojego kodu źródłowego bez odpowiedniego opisu (bez komentarzy) jest bardzo trudna, czasami niemożliwa. Aby uniknąć tego typu sytuacji powinno się opisywać kluczowe elementy programu stosując właśnie komentarze. Są one ignorowane przez kompilator i pozostawiają tylko informację dla programisty.
C++ umożliwia wstawianie komentarzy na dwa sposoby. Pierwszy rodzaj to komentarz jednolinijkowy. Po wpisaniu "//" wszystko do końca linii jest traktowane jako komentarz np.:
|
---|
Drugi rodzaj to komentarz wielolinijkowy. Wszystko co znajduje się między "/*" i"*/" jest traktowane jako komentarz np.:
|
---|
Do wyświetlania danych na ekran służy obiekt zdefiniowany w bibliotece "iostream" cout (out jak wyjście, np. na ekran monitora). Jego konstrukcja jest następująca:
std::cout << "Jakiś ciąg znaków :)";
Jeśli do programu dołączymy linijkę
using namespace std;
która jest omówiona tutaj, zapis możemy skrócić do postaci:
cout << "Jakiś ciąg znaków :)";
Jak łatwo zauważyć, powyższa konstrukcja pozwala wyświetlać napis na ekranie monitora, który jest umieszczony w cudzysłowie. Dodatkowym elementem jest operator wyjścia <<, który pokazuje kierunek przekierowania strumienia znaków. Jest to przeciążony operator przesunięcia bitowego.
Przy wyświetlaniu zmiennych lub wartości działań, zapisujemy je bez użycia cudzysłowu, np.:
cout << b*34 - 45 + a;
Możemy łączyć wyświetlanie ciągów znaków z wartościami zmiennych (lub samymi wyrażeniami) oddzielając każde z nich operatorem << np.:
cout <<"Wartość wyrażenia "<<23*7655<<" jest większa niż "<<22*7654 ;
Na ekranie monitora pojawi się komunikat:
"Wartość wyrażenia 176065 jest większa niż 168388"
Prześledźmy przykład wyznaczania sumy dwóch liczb:
|
---|
Do wstawienia znaku końca linii (znak enter), możemy wykorzystać dwie metody.
Pierwsza to wpisanie wewnątrz tekstu kombinacji: \n, natomiast druga polega na dopisaniu instrukcji endl (end line), którą wstawiając oddzielamy separatorem strumienia wyjścia. Prześledźmy przykład:
|
---|
W pierwszym i drugim przypadku zostanie wstawiony tekst "Ala ma kota", następnie dwa znaki enter, tekst "a kot ma Ale :)" i znak enter.
Innym znakiem specjalnym jest znak tabulacji, który możemy wstawić za pomocą kombinacji \t, lub sygnał z głośniczka dźwiękowego \a.
Do wstawiania znaków poprzez podanie kodów ASCII, możemy posłużyć się następującą kombinacją: \nr, gdzie nr to kod ASCII danego znaku zapisany w systemie ósemkowym.
Prześledźmy przykład:
|
---|
Podobnie jak w przypadku obiektu cout mamy do dyspozycji dwie możliwości:
std::cin>>nazwa_zmiennej;
lub po dodaniu linijki
using namespace std;
mamy postać:
cin>>nazwa_zmiennej;
Zauważmy, że w tym przypadku stawiamy znaki przekierowania strumienia w odwrotną stronę ">>". Inaczej mówiąc, strzałeczki pokazują, w którą stronę mają być przekierowane wartości: do zmiennych.
Jeśli chcemy podać wartości kilku zmiennych, można to zrobić oddzielając zmienne znakiem wejścia strumienia np.:
cin>>pierwsza_zmienna>>druga_zmienna>>trzecia_zmienna;
Prześledźmy przykład: Napisz program, który po podaniu dwóch liczb obliczy ich iloczyn.
|
---|
W tym artykule omówię krótko zasadę wykorzystania manipulatorów zdefiniowanych w standardowej bibliotece i w bibliotece iomanip, które formatują dane wyjściowe przy użyciu obiektu cout.
Zmiana systemu liczbowego wyświetlanych liczb - manipulatory hex, oct i dec
Mamy do dyspozycji trzy systemy: szesnastkowy (hex), ósemkowy (oct) oraz dziesiętny (dec) - domyślny. Do zmiany systemu liczbowego dla wyświetlania liczb za pomocą obiektu cout, musimy podać jeden z trzech dostępnych manipulatorów przed wyświetlaną liczbą. System będzie obowiązywał do momentu zmiany na inny. Domyślnie jest ustawiony dziesiętny.
Przykład |
---|
|
Szerokość pola
Aby ustawić ilość miejsc dla wyswietlanej liczby lub ciągu znaków, wykorzystujemy metodę width(int). W przeciwieństwie do manipulatorów ustawiających system, metada ta działa tylko dla jednej (następnej) liczby lub ciągu. Możemy ją wykorzystać, jeśli zależy nam na równym wyświetlaniu danych. Zasadę działania pokażę na przykładzie:
|
---|
Znaki wiodące
Gdy szerokość pola danych jest ustawiona na większą niż same dane, puste miejsca są wypełniane spacjami. Można zmienić znak wypełniający na inny za pomocą metodyfill(char);
|
---|
Ustawianie precyzji liczb zmiennoprzecinkowych
Do ustawienia wyświetlenia ilości cyfr danej liczby zmiennoprzecinkowej służy metoda precision(precyzja). Popatrzmy na przykład:
|
---|
Aby wyświetlić liczbę z zadaną ilość miejsc po przecinku należy użyć następującej konstrukcji:
cout<<fixed<<setprecision(ilość miejsc po przecinku)<<liczba_zmiennoprzecinkowa;
gdzie fixed usuwa notację wykładniczą z zadanej liczby - potrzebna jest bibliotekaiomanip (iomanip.h).
Popatrzmy na przykład:
|
---|
Metoda setf() - zdefiniowana w bibliotece iostream
Metoda setf() pobiera argument, który ustawia odpowiedni format danych. Mamy do dyspozycji kilka flag formatujących, oto niektóre z nich:
ios_base::showbase - dla danych wyjściowych ustawienie prefiksów liczbowych (0, 0x)
ios_base::uppercase - używanie dużych liter na wyjściu w systemie szesnastkowym oraz wielkiej litery E w notacji naukowej
ios_base::showpos - wymuszenie umieszczenia znaku + przed liczbami dodatnimi
Do unieważnienia flagi metody setf() służy metoda unsetf().
Popatrzmy na przykład:
|
---|
Biblioteka iomanip
Bibliotekę tą stworzono w celu uproszczenia stosowania manipulatorów. Oto przykładowe odpowiedniki dla manipulatorów z biblioteki standardowej:
|
---|
Wyróżniamy kilka rodzajów operatorów. W zależności od sytuacji, w której mają zastosowanie dzielimy na kilka grup:
Dodatkowo w tym artykule przedstawię priorytety operatorów wykorzystywanych w C++.
Warto zauważyć, że operatory dzielimy także ze względu na ilość argumentów, jakie potrzebują do prawidłowego działania. Operatory jednoargumentowe takie jak "++" czy "--" nazywamy unarnymi np.:
|
---|
Operatory, które wykorzystują dwa argumenty nazywamy binarnymi np.:
|
---|
Operatory arytmetyczne służą do wykonywania wszelkiego rodzaju działań na liczbach takich jak:
"+" - dodawanie
"-" - odejmowanie
"*" - mnożenie
"/" - dzielenie całkowite lub rzeczywiste. (jeśli argumentami są liczby całkowite, operator będzie wykonywał dzielenie całkowite, natomiast dla liczb rzeczywistych operator wykona dzielenie rzeczywiste. (przykład poniżej)
"%" - reszta z dzielenia dwóch liczb całkowitych
|
---|
Mają zastosowanie w miejscach, gdzie występują różnego rodzaju warunki - głównie w pętlach i instrukcjach warunkowych. Do operatorów logicznych zaliczamy:
|| - lub logiczne (klikamy dwa razy shift + backslash)
&& - i logiczne (klikamy dwa razy shift + 7)
! - zaprzeczenie (klikamy wykrzyknik)
Lub logiczne zwraca prawdę, gdy przynajmniej jeden z warunków jest prawdziwy, w przeciwnym razie zwraca fałsz np.:
Załóżmy, że nasze zmienne przyjmują następujące wartości:
|
---|
Logiczne i zwraca prawdę w przypadku, gdy wszystkie warunki są prawdziwe, w przeciwnym razie zwraca fałsz np.:
|
---|
Logiczne nie zaprzecza otrzymaną wartość z true na false lub z false na true np.:
|
---|
Operatory relacyjne stosujemy w sytuacje, gdzie jest potrzeba porównania dwóch elementów. Najczęściej w instrukcjach warunkowych i iteracyjnych. Wyróżniamy:
"<" - mniejszy
">" - większy
"<=" - mniejszy równy
">=" - większy równy
"==" - równy
"!=" - różny
Przykładowe zastosowanie:
|
---|
Przypisanie polega na nadaniu wartości dla zmiennej znajdującej się po lewej stronie, wartości znajdującej się po stronie prawej. Dla takiej zmiennej można bezpośrednio nadać wartość, przekazać wartość z innej zmiennej lub wartość może zostać nadana poprzez wcześniejsze wykonanie pewnych operacji. Podstawowym operatorem przypisania jest "=".
|
---|
Częstą operacją w języku C++ jest zwiększenie lub zmniejszenie wartości zmiennej całkowitej o 1. Tą operację nazywamy odpowiednio inkrementacją idekrementacją. Przeanalizujmy przykład:
|
---|
Pamiętaj!!! Inkrementacja i dekrementacja działa tylko na zmiennych typu całkowitego.
Jak już wspomniałem, najczęściej używaną opcją jest inkrementacja i dekrementacja zastosowana w trzecim przykładzie. Ten rodzaj inkrementacji dzielimy na dwa rodzaje: przyrostkowa (c++; c--;) oraz przedrostkowa (++c; --c;). Na pierwszy rzut oka, efekt działania będzie taki sam. Różnica jednak jest w priorytecie przy zastosowaniu z operatorem przypisania. Prześledźmy przykład:
|
---|
Operację przypisania połączoną z pewną operacją matematyczną na wartości zmiennej można przedstawić w następujący sposób:
zmienna [operator matematyczny][=] wartość;
Aby lepiej zrozumieć powyższą notację prześledźmy przykłady:
|
---|
|
---|
|
---|
|
---|
Nie będę opisywał wszystkich przypadków tego rodzaju przypisania. Wypiszę tylko operatory, które można zastosować analogicznie do powyższych przykładów:
"a /= 2;" - podzielenie aktualnej wartości zmiennej "a" przez 2 i nadpisanie tym wynikiem tą zmienną
"a >>= 2;" - przesunięcie o 2 bity w prawo
"a <<= 2;" - przesunięcie o 2 bity w lewo
"a &= 2;" - równoważne z operacją "a = a & 2" - operatory logiczne w następnym podrozdziale
"a |= 2;" - równoważne z operacją "a = a | 2" - operatory logiczne w następnym podrozdziale
"a ^= 2;" - równoważne z operacją "a = a ^ 2" - operatory logiczne w następnym podrozdziale
Operatory, o których mowa, wykonują operacje bezpośrednio na reprezentacji bitowej danej zmiennej.Załóżmy, że rozpatrujemy zmienną typu unsigned short, która zajmuje pole dwóch bajtów czyli 16 bitów. Teraz przypiszmy do tej zmiennej liczbę 19. Popatrzmy na tą liczbę w notacji binarnej:
19 = (00000000 00010011)2
Pierwszy operator bitowy to przesunięcie w prawo ">>". Przesuwa liczbę o n bitów w prawą stronę, gdzie n jest liczbą całkowitą, uzupełniając z lewej strony zerami:
|
---|
Gdy przesuniemy liczbę 19 o trzy w prawo, skasują się trzy bity z prawej strony, natomiast z lewej strony dopełnią się zerami:
19 = (00000000 00010011)2
przesuwamy 3 bity w prawo i otrzymujemy
2 = (00000000 00000010)2
Analogicznym operatorem jest operator przesunięcia w lewo. Popatrzmy na przykład:
|
---|
19 = (00000000 00010011)2
przesuwamy 2 bity w lewo i otrzymujemy
76 = (00000000 01001100)2.
Następnym operatorem bitowym jest koniunkcja bitowa - w notacji C++ "&". Zanim przejdziemy do przykładu poparzmy jak zachowują się bity potraktowane tym operatorem:
0 & 0 = 0
0 & 1 = 0
1 & 0 = 0
1 & 1 = 1
Zauważmy, że tylko w jednym przypadku otrzymamy jedynkę - gdy oba bity będą jedynkami. Rozpatrzmy już znaną nam liczbę 19 zapisaną pod typem unsigned short. Wykonajmy następującą operację:
|
---|
|
|
|
---|---|---|
|
|
|
|
|
Podobnie działa alternatywa bitowa - w notacji C++ "|". W tym przypadku otrzymamy 0, gdy dwa bity są zerami, w przeciwnym wypadku będzie 1. Popatrzmy:
0 | 0 = 0
0 | 1 = 1
1 | 0 = 1
1 | 1 = 1
|
---|
|
|
|
---|---|---|
|
|
|
|
|
Następnym ciekawym operatorem jest operator różnicy symetrycznej, wykorzystywany często w szyfrowaniu symetrycznym danych. W notacji C++ operator zapisujemy "^". Traktując dwa bity tym operatorem otrzymujemy 0, gdy te bity są takie same, 1 w przeciwnym razie:
0 ^ 0 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
Rozpatrzmy przykład:
|
---|
|
|
|
---|---|---|
|
|
|
|
|
Ostatnim operatorem z tej serii jest jednoargumentowy operator negacji bitowej. W notacji C++ zapisujemy "~". Zasada działania jest prosta:
~ 0 = 1
~1 = 0.
Rozpatrzmy zmienną typu unsigned int znajdującą się na polu 4 bajtów, czyli 32 bitów. Przypiszmy wartość 19. Jaka będzie wartość tej zmienne po negacji? Zobaczmy:
|
---|
|
|
|
---|---|---|
|
|
|
W tym podrozdziale będziemy rozpatrywać kolejność wykonywanych operacji przez operatory. Przyjrzyjmy się następującemu działaniu matematycznemu:
343 · 5 - 6 = …
Wiadomo, że w tym przypadku kolejność jest następująca:
Najpierw potęgowanie, potem mnożenie, a na końcu odejmowanie. Aby zmienić kolejność działań możemy posłużyć się nawiasami np.:
343 · (5 - 6) = …
W tym przypadku zmieniliśmy priorytety wykonywanych działań. Najpierw zostanie wykonane działanie w nawiasie, następnie potęgowanie (lub na odwrót), potem dopiero mnożenie.
Tak samo dzieje się z operatorami. Jedne działają wcześniej (mają wyższy priorytet), inne później. Oto tabelka z operatorami i ich priorytetami.
Priorytet | Ilość arg. | Operator | Opis | Przykład |
---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Warunkowy trójargumentowy operator ? :
W C++ i wielu innych językach programowania istnieje operator, który potrzebuje trzech argumentów do poprawnego działania. Tym operatorem jest warunkowy operator:
arg1 ? arg2 : arg3
arg1 - tu znajduje się warunek lub warunki
arg2 - tu znajduje się instrukcja, która wykona się, gdy warunek jest prawdziwy
arg3 - tu znajduje się instrukcja, która wykona się, gdy warunek będzie fałszywy.
W wielu sytuacjach stosowanie tego operatora skraca i upraszcza kod programu. Prześledźmy kilka przykładów.
Zadanie 1. Napisz program, który sprawdzi, czy dana liczba jest parzysta.
Rozwiązanie
|
---|
Zadanie 2. Napisz program, który przypisze do zmiennej max większą wartość z dwóch podanych liczb.
Rozwiązanie
|
---|
zmienna - to obiekt w programowaniu, który przechowuje różnego rodzaju dane niezbędne do działania programu. Zmienna podczas działania programu może zmieniać swoje wartości (jak wskazuje nazwa). Tworząc zmienną musimy nadać jej nazwę oraz typ, który określa co nasza zmienna będzie przechowywać. Nadając nazwę trzymamy się następujących reguł:
zmienna jest jednym ciągiem znaków bez spacji np. nazwa_zmiennej - dobrze, nazwa zmiennej - źle
nie zaczynamy nazwy od cyfry np. 12zmienna - źle, zmienna12 - dobrze
nie używamy polskich liter takich jak ą, ę itp.
nazwa zmiennej powinna kojarzyć się z przeznaczeniem tej zmiennej np. tablica_ciagu - dobrze
nazwa nie może być słowem kluczowym języka programowania np. auto - źle
typ zmiennej - tworząc zmienną musimy się zastanowić, jakie będzie jej zastosowanie. Zmienne mogą przechowywać znaki, liczby całkowite, liczby rzeczywiste, ciągi znaków lub wartość logiczną true lub false. W dalszej części dokumentu zostaną zilustrowane podstawowe typy zmiennych, ich rozmiar, zakres i zastosowanie.
Ogólna zasada tworzenia zmiennych jest następująca:
typ_zmiennej nazwa_zmiennej;
np.
int a - zmienna o nazwie "a" mająca typ całkowity int
char b - zmienna o nazwie "b" mająca typ znakowy char.
Prześledźmy przykłady:
|
---|
Pierwsza grupa to zmienne typu całkowitego. Jak sama nazwa mówi, przechowują tylko liczby całkowite. Różnią się one rozmiarem, czyli zakresem przechowywanych liczb. Im większy rozmiar, tym większe liczby mogą być przechowane.
Typy całkowite
|
|
|
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Warto zauważyć, że po dodaniu słowa kluczowego unsigned (bez znaku), wartości zmiennych stają się nieujemne i podwojony zostaje prawy zakres.
|
---|
Patrząc na dane w tabeli, łatwo jest dostosować dany typ do potrzeb programu. Gdy orientujemy się jakich wielkości będziemy używać (jak duże będą liczby w naszym programie), dobieramy optymalny typ.
Typ rzeczywisty - przechowuje liczby zmiennoprzecinkowe. Gdy mamy zamiar w naszym programie wykorzystać ułamki, ten typ będzie najbardziej odpowiedni. Wyróżniamy następujące typy:
Typy rzeczywiste |
---|
|
|
|
|
|
---|
Typ znakowy - przechowuje znaki, które są kodowane kodem ASCII. Tzn. znak w pamięci nie może być przechowany jako znak, tylko jako pewna liczba. Dlatego każdy znak ma swój odpowiednik liczbowy z zakresu [0, 255], który nazywamykodem ASCII. I na przykład litera "d" ma wartość 100, "!" = 33, itd.:
Typ znakowy |
---|
|
|
|
Popatrzmy jeszcze na operację przypisania stosowaną na zmiennych typu znakowego:
|
---|
Typ logiczny - przechowuje jedną z dwóch wartości - true (prawda) albo false (fałsz). Wartość logiczna true jest równa 1, natomiast false ma wartość 0.
Typ logiczny |
---|
|
|
Dla zmiennych tego typu możemy realizować przypisanie na dwa sposoby, podając wartość true lub fałsz, albo 1 lub 0.
|
---|
Programowanie w języku C++ opiera się na idei korzystania ze zmiennych lokalnych. Zmienne tego typy "widoczne" są tylko w określonym bloku. Dzięki temu nie ma niebezpieczeństwa omyłkowego przypisania wartości zmiennej, która jest wykorzystywana w innym miejscu. Prześledźmy przykłady:
Przykład 1 |
---|
|
Zmienne lokalne można także tworzyć w mniejszych blokach ograniczonych nawiasami klamrowymi:
Przykład 2 |
---|
|
Ważność zmiennych o tych samych nazwach jest nadpisywana zgodnie z następującą zasadą:
Przykład 3 |
---|
|
Zmienne globalne widoczne są w każdym "zakątku" programu. Oznacza to, że można z niej korzystać w każdym miejscu programu. Trzeba jedna zwrócić uwagę, że przy tworzeniu tego typu zmiennych, istnieje niebezpieczeństwo przypadkowego nadpisania jej wartości, co może spowodować nieprawidłowe działanie programu. Dlatego zaleca się korzystanie ze zmiennych lokalnych.
Zmienne globalne deklarujemy przed blokiem funkcji main():
Przykład 1 |
---|
|
Można nadpisywać zmienne globalne, tworząc zmienną lokalną o takiej samej nazwie:
Przykład 2 |
---|
|
Rzutowanie typów polega na konwersji jednego typu na drugi. W przypadku konwersji liczby typu rzeczywistego na całkowitą, obetniemy część ułamkową liczby rzeczywistej, w przypadku konwersji char -> int, zamiast znaku będziemy mieli do dyspozycji liczbę (czyli kod ASCII), pod jaką kryje się dany znak itd..
Konwersję typów wykonuje się według dwóch równoważnych zasad:
Przykład 1 |
---|
|
W przypadku zmiennych (jak sama nazwa wskazuje), wartości mogą się zmieniać podczas działania programu. Gdy tworzymy stałą, musimy jej nadać wartość początkową, która przez cały okres działania programu nie może zmienić swojej wartości. W C++ mamy do dyspozycji dwie metody tworzenia zmiennych:
za pomocą dyrektywy #define
za pomocą słowa kluczowego const
Pierwsza przestarzała metoda wykorzystuje preprocesor:
Przykład 1 |
---|
|
Skoro #define jest dyrektywą preprocesora, więc tworzenie stałej jest wykonane jeszcze przed kompilacją. Oznacza to, że każdy ciąg tekstu moja_stala zostanie zamieniona na liczbę 24, a więc kompilator nie widzi już ciągu tekstu tylko samą liczbę. Warto także zauważyć, że nie podajemy typu stałej.
Definiowanie stałych za pomocą słowa kluczowego const wygląda następująco:
const [typ_stałej] nazwa_stałej = wartość;
Przykład 2 |
---|
|
|
---|
ASCII (American Standard Code for Information Interchange ) to kod liczbowy, który jest przyporządkowany każdemu znaku. W C++ litery, cyfry, znaki przystankowe, znaki niedrukowalne takie jak spacja czy enter przechowujemy w zmiennej typu char, mamy wtedy do dyspozycji znaki z przedziału [0; 127], oraz w rozszerzonym typie unsigned char, tu znaki zawierają się w przedziale[0; 255].
W sytuacji, gdy nie jesteśmy pewni ile pamięci zajmuje typ wbudowany w strukturę C++ lub typ stworzony przez użytkownika (struktura, klasa), możemy skorzystać z operatora sizeof. Wyraz ten jest słowem kluczowym w C++, a więc nie może być używany jako nazwa zmiennych.
Prześledźmy kilka przykładów.
Sprawdźmy, ile pamięci zajmują podstawowe typy zmiennych w moim kompilatorze:
|
---|
Out:
|
---|
Możemy sprawdzić wielkość obiektów strukturalnych i klas:
|
---|
Out:
|
---|
Operator staje się przydatny w sytuacjach, w których należy przydzielić pamięć dynamicznie za pomocą funkcji, w których wymagane jest podanie wielkości obiektu.
Instrukcja warunkowa bez alternatywy ma postać:
if(warunek (warunki))
{ //początek bloku należącego do if
//instrukcje zostaną wykonane, gdy warunek (warunki) jest prawdziwy
//w przeciwnym wypadku ta część kodu zostanie pominięta
} //koniec bloku należącego do if
Przykład 1 |
---|
|
Zasada działania instrukcji warunkowej if opiera się na wartości, jaką przyjmuje warunek. W przypadku true (1), instrukcje dla bloku if zostaną wykonane. To oznacza, że w przypadku zmiennych logicznych (i w niektórych przypadkach) można użyć skróconej konstrukcji warunku:
Przykład 2 |
---|
|
|
---|
W Instrukcja warunkowa z alternatywą pojawia się drugi blok instrukcji rozpoczynany słowem kluczowym else, wykonany w przypadku, gdy warunek (warunki) jest fałszywy. Warto zauważyć, że w tym przypadku wykona się zawsze jeden z dwóch bloków:
if(warunek (warunki))
{ //początek bloku należącego do if
//instrukcje zostaną wykonane, gdy warunek (warunki) jest prawdziwy
//w przeciwnym wypadku część kodu należąca do bloku ifzostanie pominięta
} //koniec bloku należącego do if
else //do tego bloku należą instrukcje, które zostaną wywołane w przypadku, gdy warunek dla if będzie fałszywy
{ //początek bloku należącego do else
//instrukcje zostaną wykonane, gdy warunek (warunki) jest fałszywy
//w przeciwnym wypadku część kodu należąca do bloku elsezostanie pominięta
} //koniec bloku należącego do else
Prześledźmy przykład, który sprawdzi, czy podana osoba jest pełnoletnia:
Przykład 3 |
---|
|
Konstrukcja warunków instrukcji if może być prosta - złożona z jednego warunku, lub złożona z kilku warunków połączonych operatorami logicznymi. Rozpatrzmy przykład, który sprawdzi, czy dana liczba naturalna jest podzielna przez 3 lub reszta z dzielenia tej liczby przez 7 jest równa 2:
Przykład 1 - realizacja zadania z wykorzystaniem warunków prostych |
---|
|
To samo zadanie zrealizowane warunkami złożonymi:
Przykład 2 |
---|
|
Zagnieżdżenie instrukcji warunkowej polega na wywołaniu jej wewnątrz innej instrukcji warunkowej. W C++ można dowolnie zagnieżdżać if - else, pamiętając o tym, aby kod był czytelny. Stosuje się tu wcięcia określający kolejne poziomy zagnieżdżenia.
Dla przykładu rozwiążmy zadanie:
Zadanie. Tabela poniżej określa taryfikator mandatów w pewnym państwie:
|
|
---|---|
|
|
|
|
|
|
Użytkownik podaje liczbę km, jaką przekroczył kierowca. Zadaniem programu jest określenie wysokości mandatu.
Rozwiązanie:
|
---|
Instrukcja wielokrotnego wyboru switch case
Wyobraźmy sobie sytuację, że mamy do napisania program, który będzie wykonywał pewną czynność zależną od wybranej opcji. Zadanie to można zrealizować za pomocą instrukcji warunkowej, ale może to być dość uciążliwe. Z pomocą idzie nam instrukcja wielokrotnego wyboru. Konstrukcja jest następująca:
switch(opcja) // ta część przełącza nas w odpowiednie miejsce,
// w zależności jaką wartość ma zmienna opcja
{ //klamra otwierająca blok switch
case etykieta1:
instrukcje dla opcji o wartości etykieta1
break; // to słowo kluczowe przerywa dalsze wykonywanie instrukcji i wychodzi z bloku switch
case etykieta2:
instrukcje dla opcji o wartości etykieta2
break;
......................
case etykietan:
instrukcje dla opcji o wartości etykietan
break;
default: // ta część instrukcji switch jest opcjonalna
instrukcje, które wykonają się w przypadku, gdy podana opcja nie istnieje
} //klamra zamykająca blok switch
Zasada działania jest bardzo prosta. Tworzymy zmienną (w przykładzie powyżej jest to zmienna opcja), której nadajemy wartość. Instrukcja switch przekieruje nas w odpowiednie miejsce, gdzie wartość zmiennej jest równa wartości etykiety. Można opcjonalnie dodać alternatywę w postaci słowa kluczowego default. Gdy dana etykieta nie istnieje, program przeskoczy właśnie w to miejsce.
Jeśli chcemy zmienić zasięg zmiennych, można wstawić nawiasy klamrowe:
........
case etykietan:
{
instrukcje dla opcji o wartości etykietan
break;
}
........
Słowo kluczowe break przerywa działanie instrukcji switch. Jeśli jednak chcemy, aby kilka etykiet zostało podpiętych pod jeden ciąg instrukcji, wtedy omijamy słowobreak. Prześledźmy przykład:
Zadanie. Podajemy dzień tygodnia, program określa, czy jest to dzień roboczy, czy weekend.
Rozwiązanie:
|
---|
Przeanalizujmy powyższy program. Gdy np. zmienna dzien przyjmie wartość 2, to instrukcja switch przełączy nas w miejsce "case 2:", a następnie będzie przeskakiwać w dół do momentu napotkania słowa kluczowego break. W tym przypadku zatrzyma się dopiero po instrukcjach dla "case 5:".
Zacznijmy od wyjaśnienia pojęcia instrukcji iteracyjnej, nazywanej także pętlą. Instrukcja ta polega na powtarzaniu pewnego ciągu instrukcji skończoną ilość razy. Oczywiście dany ciąg instrukcji można powtórzyć, bez korzystania z pętli, ale wyobraźmy sobie sytuacje, w której chcemy wyświetlić milion liczb. Wypisanie instrukcji, które wyświetliły by te liczby zajęłoby nam bardzo dużo czasu:
załóżmy, że na stworzenie instrukcji wypisującej 10 liczb, potrzebujemy 10 sekund, a więc dla miliona liczb potrzebujemy 100 000 sekund, czyli około 1667 minut = około 28 godzin, czyli nieco ponad doba. No cóż, trochę żmudne zadanie.
Dzięki instrukcjom iteracyjnym wykorzystujemy moc obliczeniową procesora, dzięki czemu na wykonanie powyższego zadania potrzebujemy kilkadziesiąt sekund.
Licznik pętli to pewna zmienna, która kontroluje zachowanie się pętli, określa ona ile razy zostanie powtórzony dany ciąg instrukcji. Nazwy liczników pętli najczęściej oznaczamy małymi literami: "i", "j", "k", .....
|
---|
Instrukcja iteracyjna for jest bardzo elastyczną instrukcją (tak jak cała struktura języka C++). Złożona jest z trzech części oddzielonych średnikami:
for(część a; część b; część c)
{
//blok instrukcji, który będzie powtarzany
}
W części pierwszej najczęściej inicjujemy licznik lub liczniki pętli for. Inicjacja licznika, w tym przypadku polega na stworzeniu i nadaniu wartości początkowej. Popatrzmy na przykład, gdzie "i" jest licznikiem:
|
---|
W części drugiej definiujemy warunek, lub warunki, które wpływają na ilość powtórzeń pętli. Instrukcja for będzie powtarzać ciąg instrukcji tak długo, jak długo zdefiniowane warunki będą prawdziwe.
|
---|
Część c określa operacje (najczęściej na liczniku), od których zależy zachowanie się pętli. Jeśli chcemy, aby licznik zwiększał się o jeden, wtedy ustawiamy operację w trzeciej części na i++.
Dla przykładu zrealizujmy następujące zadanie:
Przykład. Wyświetl n kolejnych liczb parzystych, gdzie n podajemy z klawiatury.
Rozwiązanie - sposób pierwszy |
---|
|
Rozwiązanie - sposób drugi |
---|
|
Konstrukcja pętli for dopuszcza sytuację, w której, jakaś część (a, b lub c) pętli może być pusta np.:
|
---|
Zauważmy, że inicjacja licznika następuje przed pętlą for (wtedy ma ona większy zasięg - patrz. zasięg zmiennych), natomiast operacje na liczniku wykonywane są w bloku należącym do for.
Więcej przykładów w części - zadania.
Konstrukcja instrukcji iteracyjnej while jest znacznie prostsza od pętli for. Wyróżniamy tu tylko jedną część, w której definiujemy warunek lub warunki, od których zależy wykonywanie się pętli.
|
---|
Część inicjacyjną licznika tworzy się najczęściej przed blokiem pętli, natomiast część operacyjna jest wykonywana wewnątrz pętli. Popatrzmy na konstrukcję:
|
---|
Warto także zauważyć, że pętla może nie uruchomić się ani razu (gdy od samego początku warunek (warunki) będzie fałszywy).
Należy także zwrócić uwagę, że niepoprawna konstrukcja pętli może spowodować jej zapętlenie (wykonywanie bez końca).
Prześledźmy przykład ilustrujący działanie pętli while:
Przykład. Napisz program, który wyświetli n początkowych liczb naturalnych.
Rozwiązanie |
---|
|
Konstrukcja pętli do..while jest podobna do pętli while. Instrukcja ta wykonuje się tak długo, jak długo prawdziwy będzie warunek (warunki). Różnica jest taka, że ta pętla zawsze wykona się co najmniej raz, ponieważ warunek jest sprawdzany po wykonaniu instrukcji należących do bloku do..while.
Konstrukcja pętli do..while |
---|
|
Przykład. Napisz program, który pobiera liczby rzeczywiste tak długo, jak długo są one dodatnie, oraz wyznacza ich sumę.
Rozwiązanie |
---|
|
Zagnieżdżenie pętli, podobnie jak instrukcji warunkowej, polega na wywołaniu jednej pętli wewnątrz drugiej. Oznacza to, że na jedną iterację pętli zewnętrznej, zostanie wykonany cały przebieg pętli wewnętrznej. Instrukcje iteracyjne w C++ można dowolnie zagnieżdżać. Im więcej zagnieżdżeń tym większa złożoność obliczeniowa algorytmu.
Zagnieżdżenie pętli for |
---|
|
Przykład. Napisz program, który wyświetli wszystkie liczby trzycyfrowe o niepowtarzających się cyfrach oraz określi ilość takich liczb.
Rozwiązanie z wykorzystaniem zagnieżdżenia pętli for |
---|
|
Rozwiązanie z wykorzystaniem zagnieżdżenia pętli while |
---|
|
Skok bezwarunkowy goto jest najstarszym rodzajem pętli, który został wyparty przez pętlę for i pętlę while. Do konstrukcji pętli goto potrzebna jest etykieta, czyli takie słowo, które jest zakończone znakiem dwukropka (:). Etykieta musi występować w linii jako pierwsza. Program gdy napotka słowo kluczowe goto i nazwę tejże etykiety, przemieszcza się bezwarunkowo w miejsce gdzie została ona zadeklarowana.
Nie jest wskazane stosowanie tego rodzaju pętli, ponieważ kod staje się nieczytelny, trudny do analizy, oraz łamie ideę programowania strukturalnego.
Przykład. Napisz program, który wyznaczy sumę n kolejnych liczb naturalnych.
Rozwiązanie |
---|
|
W instrukcjach iteracyjnych słowo kluczowe break bezwarunkowo przerywa działanie pętli (wychodzi z tej pętli). Jeśli dalsze wykonywanie pętli nie ma sensu, przerywamy jej działanie właśnie tą instrukcją. Prześledźmy przykład.
Przykład. Napisz program, który stwierdzi, czy podana liczba naturalna dodatnia n posiada co najmniej jeden dzielnik z przedziału domkniętego
[2;n−−√]
Rozwiązanie | |
---|---|
|
Słowo kluczowe continue, powoduje wznowienie działania pętli i zaprzestanie wykonywania dalszych instrukcji dla danego powtórzenia. Stosuje się je w przypadku, gdy w danej iteracji pętli nie ma konieczności wykonania dalszych instrukcji pętli. Popatrzmy na przykład:
Przykład. Napisz program, który wyświetli wszystkie trzycyfrowe palindromy, w których środkowa cyfra jest równa zero lub pięć.
Rozwiązanie | |
---|---|
|
Tworzenie i wywoływanie funkcji w C++
Funkcje w C++ możemy tworzyć na kilka sposobów:
W artykule dodatkowo zostanie opisany sposób wywoływania funkcji oraz przykłady wywołania.
Między bibliotekami a funkcją main()
Rozpatrzmy pierwszy sposób tworzenia funkcji:
|
---|
Całą funkcję wraz z jej instrukcjami tworzymy przed główną funkcją main().
Deklaracja samych prototypów przed funkcją main()
Drugi sposób polega na wstawieniu przed funkcją main() prototypów funkcji, natomiast ciała tych funkcji są zaimplementowane poniżej funkcji main():
|
---|
Taki sposób tworzenia funkcji jest wygodny, w przypadku gdy w programie mamy ich większą ilość. Aby sterować naszą aplikacją nie musimy przedostawać się przez gąszcz funkcji, tylko przez same ich prototypy.
W oddzielnej bibliotece
Ostatni sposób polega na implementacji naszych funkcji w oddzielnym pliku nazywanym biblioteką. Plik ten posiada rozszerzenie *.h Aby korzystać z napisanych funkcji, trzeba dodać napisaną bibliotekę do nagłówka programu. Prześledźmy przykład:
Załóżmy, że nasza biblioteka ma nazwę biblioteka.h
biblioteka.h |
---|
|
|
---|
Zauważ, że pojawiły się dodatkowe dyrektywy:
#ifndef biblioteka_h
#define biblioteka_h
...
#endif
Podczas konsolidacji kodu obiektowego, do naszego programu dołączane są pliki zwane bibliotekami. Aby zapobiec wielokrotnemu dodaniu naszej biblioteki do programu, zabezpieczamy się dyrektywą, która sprawdza, czy dana biblioteka nie została już wcześniej dołączona. #ifndef biblioteka_h- można przetłumaczyć: "jeśli nie zdefiniowano ciągu znaków biblioteka_h", #define biblioteka_h - "zdefiniuj ciąg znaków biblioteka_h" i dołącz nagłówek pliku biblioteka.h do programu, #endif -koniec bloku #ifndef.
Przykład. Napiszmy dwie funkcje:
float max(float, float) - wyznaczającą wartość maksymalną, z podanych dwóch liczb
void hello() - wyświetlającą na ekranie treść: "Witaj programisto!!!"
sposobami omówionymi powyżej.
Sposób 1.
|
---|
Sposób 2.
|
---|
Sposób 3.
biblioteka.h |
---|
|
biblioteka.cpp |
---|
|
Wywoływanie funkcji
Aby wywołać funkcję w C++, należy podać jej nazwę, oraz argumenty (jeśli takie posiada), zwracając uwagę, na zgodność typów argumentów:
jeśli funkcja o nazwie "f" pobiera następujące typy f(int a, char b), oznacza to, że przy uruchomieniu tej funkcji pierwszym argumentem musi być liczba typu int, a drugim argumentem musi być znak np.:
|
---|
Funkcje niezwracające wartości wywołujemy podając tylko ich nazwę, natomiast funkcje z wartością zwrotną przypisujemy do odpowiednich zmiennych (do zmiennej typu zwracanej wartości), lub kierujemy na wyjście do pliku lub na ekran.
Podprogramy (funkcje) możemy wywoływać w głównej funkcji main() lub z wnętrza innych funkcji, metod, itd..
Prześledźmy przykłady:
Stworzymy dwie funkcje:
void sumaA(int, int) - wyświetlająca sumę dwóch liczb całkowitych
int sumaB(int, int) - zwracającą sumę dwóch liczb całkowitych
|
---|
Funkcja typu void (odpowiednik procedury w PASCALU) nie zwraca żadnych danych, które można byłoby poddać dalszej obróbce. Funkcja tego typu może wykonywać pewne czynności, ale nie przekazuje informacji zwrotnej. Słowo kluczowe void oznacza "pusty", informuje, że nic nie będzie zwracane.
Wyobraźmy sobie, że tworzymy funkcję, która będzie czyścić ekran. Zauważmy, że oprócz czyszczenia, żadne dane nie będą przekazywane dalej, czyli ten typ jak najbardziej sie do tego nadaje.
Weźmy przykład funkcji wyznaczającej ilość cyfr podanej liczby całkowitej. W tym przypadku tym void się nie sprawdzi, ponieważ informacją zwrotną w tym przypadku będzie ilość cyfr danej liczby.
Prześledźmy przykład:
Przykład. Napisz funkcję, która jako argument pobierze znak i powieli go 20 razy.
Rozwiązanie |
---|
|
Zauważmy, że funcje tego typu wywołujemy wypisując tylko jej nazwę z ewentualnymi argumentami (gdy tych argumentów nie ma, podajemy nazwę wywoływanej funkcji z pustym nawiasem np. powiel()).
Wartością zwrotną funkcji jest wyliczona dana, która jest przekazywana do dalszej obróbki. Typ tej danej wypisujemy przed definicją naszej funkcji i jest ona zwracana za pomocą słowa kluczowego return:
|
---|
Rozpatrzmy funkcję, która wyznaczy sumę cyfr zadanej liczby całkowitej. Zwracaną wartością będzie własnie ta suma. Informację tą w postaci wyniku można wykorzystać w dalszej części programu, np. wyświetlić ją na ekranie, sprawdzić czy ta suma jest parzysta lub czy mieści się w danym zakresie.
Przykład. Dla przykładu napiszemy program, który zwróci prawdę, jeśli dana osoba jest pełnoletnia, w przeciwnym razie zwróci fałsz.
Rozwiązanie |
---|
|
Argumenty funkcji to dane, które są przekazywane dla funkcji, na podstawie których wykonywane są instrukcje. Np. dla funkcji sumującej dwie liczby rzeczywiste argumentami będą te dwie liczby:
float funkcja(float argument1, float argument2)
Definiując funkcję, musimy zdefiniować listę argumentów, nadając im typy oddzielając je przecinkami. Gdy definiujemy tylko prototyp funkcji, wystarczy nadać same nazwy typówi:
float funkcja(float, float);
Pamiętaj, że kolejność argumentów ma znaczenie, tzn. jeśli pierwszym argumentem jest liczba całkowita, a drugim znak, to w takiej kolejności musisz wpisać dane przekazywane dla funkcji:
|
---|
Ważną rzeczą jest zrozumienie, że w momencie przekazywania zmiennych jako argumenty, funkcja tworzy ich kopie w pamięci. Oznacza to, że wszelkie zmiany na wartościach zmiennych nie będą widoczne w miejscu, gdzie zostały one przekazane do funkcji.
Dla lepszego zobrazowania problemu przeanalizujmy przykład zamiany wartości dwóch zmiennych.
|
---|
Jak już wspominałem w poprzednich artykułach, funkcje w C++ mogą zwracać co najwyżej jedną wartość, która może posłużyć dalszej obróbce (wartość zwrotna funkcji), ale co jeśli tych wartości potrzebujemy więcej? Jednym ze sposobów uporania się z tym problemem jest wykorzystanie referencji.
Pamiętamy, że w chwili przykazywania danych do funkcji tworzone są ich kopie w pamięci, czego konsekwencją jest to, że każda zmiana na wartościach tych zmiennych nie będzie "widziana" w miejscu, w którym tą funkcję wywołaliśmy.
Referencje powodują, że w chwili przekazywania argumentów, pracujemy na oryginałach zmiennych, ponieważ przekazujemy adresy naszych argumentów.
Aby przekazać referencję, należy dodać po nazwie typu znak "&" np.:
void funkcja(char &a, int &b);
Rozpatrzmy zadanie polegające na zamianie wartości dwóch zmiennych.
Zróbmy najpierw to zadanie bez referencji:
|
---|
Oczywiście efekt nie będzie zadowalający:
No i program z wykorzystaniem referencji:
|
---|
Tu widzimy, że funkcja spełnia kryteria zadania.
Warto wspomnieć, że podobne działanie uzyskamy w przypadku przekazania argumentów funkcji za pomocą wskaźników.
Tworzenie funkcji w C++ wiąże się z przydzieleniem pamięci w innym segmencie niż program główny. Oznacza to, że podczas wywoływania funkcji, program musi przeskoczyć w to miejsce, czego konsekwencją jest nieznaczne opóźnienie programu. Jeśli programiście zależy, aby program był szybki, można stworzyć funkcję inline (w lini).
Funkcje inline możemy rozumieć w ten sposób, że kod tej funkcji jest "wklejany" w miejsce, gdzie została ona wykonana (pracujemy wtedy na tym samym segmencie pamięci, oszczędzając czas). Funkcje tego typu tworzy się zazwyczaj dla krótkich kilku linijkowych funkcji. Jeśli kompilator uzna, że dany podprogram nie kwalifikuje się jako inline, to słowo inline zostanie zignorowane.
Słowo kluczowe inline wstawiamy przed typem funkcji:
inline int funkcja(argumenty);
inline void innafunkcja(argumenty);
itd.
Dla przykładu napiszemy funkcję sumującą dwie liczby:
|
---|
Przeciążanie nazw funkcji polega na wielokrotnym wykorzystaniu takiej samej jej nazwy, różniącej się tylko typem i ilością argumentów (przeciążanie funkcji można zaliczyć do elementów polimorfizmu).
Rozpatrzmy funkcję, która będzie liczyła pole pewnej figury. Pole figury jakiej ma liczyć uzależnimy od ilości argumentów i typów argumentów. Nasza funkcja będzie nosiła nazwę Pole.
float Pole(float p) - funkcja liczy pole kwadratu
double Pole(double p) - funkcja liczy pole koła
float Pole(float a,float b) - funkcja liczy pole prostokąta itd.
|
---|
Do przeciążenia funkcji nie wystarczy różny tym zwracanych wartości np.:
|
---|
W przykładzie powyżej, funkcje mają różne zwracane wartości, natomiast takie same argumenty - kompilator wyrzuci błąd!
Rekurencja zwana rekursją, polega na wywołaniu przez funkcję samej siebie. Algorytmy rekurencyjne zastępują w pewnym sensie iteracje. Zazwyczaj zadania rozwiązywane tą techniką są wolniejsze od iteracyjnego odpowiednika, natomiast rozwiązanie niektórych problemów jest znacznie wygodniejsze.
Prześledźmy program wyznaczający sumę n kolejnych liczb naturalnych.
|
---|
Załóżmy, że na wejściu podaliśmy liczbę 5 (program ma wyznaczyć sumę 1+ 2+ 3 + 4 + 5).
wynik = suma(5);
Funkcja suma(n), wywołała się z argumentem równym 5. Najpierw sprawdzamy, czy n< 1 (5 < 1). Warunek jest fałszywy, przechodzimy więc do następnej linijkireturn 5 + suma(5 - 1) . Funkcja suma wywołana została przez samą siebie z argumentem równym 4, a więc mamy:
wynik = suma(5) = 5 + suma(4),
daną czynność powtarzamy do momentu, gdy argument osiągnie wartość 0, wtedy funkcja zwróci 0 (0 < 1, prawda).
wynik = suma(5) = 5 + suma(4) = 5 + 4 + suma(3) = 5 + 4 + 3 + suma(2) = 5 + 4 + 3 + 2 + suma(1) =
5 + 4 + 3 + 2 + 1 + suma(0) = 5 + 4 + 3 + 2 + 1 + 0 = 15.
Definicja: Tablica to struktura danych, która przechowuje wiele elementów tego samego typu. Np., gdy potrzebujemy przechowywać 1000 liczb całkowitych, to możemy stworzyć tysiąc zmiennych (szkoda czasu), albo jedną tablicę typu całkowitego, składającą się z 1000 komórek.
Możemy tworzyć tablice dowolnego typu wbudowanego w strukturę C++, oraz tablice własnych typów takich jak struktury czy klasy.
Tablice jednowymiarowe można porównać do osi liczbowej. Aby odwołać się do jakiegoś elementu, wystarczy podać jedną współrzędną.
Tablice tego typu deklarujemy następująco:
|
---|
np.:
|
---|
czyli tablica, która może przechowywać tysiąc elementów typu całkowitego,
|
---|
czyli tablica, która może przechowywać 1000 znaków + znak końca tablicy.
Operację tą można zrobić tylko przy deklaracji tablicy (przy tworzeniu tablicy). Operację nadania wartości tablicy przedstawię na przykładzie pięcio-elementowej tablicy liczb całkowitych:
|
---|
Oznacza to, że w pierwszej komórce przechowywana jest liczba 1, w drugiej 3 itd..
Jeśli przypiszemy do tablicy mniejszą ilość elementów niż wielkość tablicy, to pozostałe komórki będą miały wartość zero:
|
---|
Powyższy zapis jest równoważny z zapisem:
|
---|
Przypisanie do tablicy pustego nawiasu oznaczać będzie, że wszystkie komórki będą miały wartość zero.
|
---|
Najważniejszą regułą jest to, że komórki numerujemy od zera. Numery te nazywamy indeksami tablicy. Oznacza to, że jeśli stworzyliśmy tablicę 10-elementową, to numer pierwszej komórki jest równy zero, drugiej jeden, ..., no i ostatniej dziewięć:
[0][1][2]⋯[n−1]indeksy komorek n−elementowej tablicy
Odwołujemy się do komórek tablicy podając jej indeks w nawiasie kwadratowym, np:
|
---|
|
---|
Zadanie. Napisz program, który nada następujące wartości początkowe tablicy 5-elementowej: 1, 2, 5, 0, 0, a następnie wyświetli najpierw wartości parzyste tej tablicy, a następnie nieparzyste.
Rozwiązanie:
|
---|
Warto zauważyć, że przeszukujemy tablicę o indeksach od 0 od 4, a więc w nawiasie kwadratowym tablicy wstawiliśm
Jako tablicę dwuwymiarową możemy sobie wyobrazić planszę prostokątną składającą się z pewnej liczby wierszy i kolumn (numerowanie zaczynamy od zera). Aby przypisać (pobrać) wartość do danej komórki, należy podać jej obie współrzędne.
Inicjacja tablicy polega na podaniu ilości wierszy i kolumn:
|
---|
Wartości początkowe tablicy możemy nadać przy jej deklaracji. Popatrzmy na przykład oparty na tablicy dwuwymiarowej liczb całkowitych:
|
---|
Przy nadawaniu wartości tablicy można pominąć wartość w pierwszym nawiasie kwadratowym:
|
---|
Aby odwołać się do każdej z komórek należy w nawiasach kwadratowych podać numer wiersza i kolumny komórki, do której się odwołujemy, pamiętając o tym, żenumerujemy je od zera:
|
---|
Zad. Napisz program, który wykona transpozycję macierzy 4x5. Liczby generujemy losowo z przedziału [-9; 9]. Elementami macierzy są liczby całkowite.
Rozwiązanie:
Macierz to obiekt, który doskonale nadaje się do przechowywania w tablicach dwuwymiarowych. Każda macierz składa się z pewnej ilości wierszy i kolumn. Przykładowa macierz spełniająca warunki zadania:
⎡⎣⎢⎢⎢12321234−506580−6−9254−8⎤⎦⎥⎥⎥
Transpozycja macierzy polega na zamianie wierszy z kolumnami. Powyższa macierz powinna wyglądać następująco:
⎡⎣⎢⎢⎢⎢⎢⎢11−58222005336−64245−9−8⎤⎦⎥⎥⎥⎥⎥⎥
|
---|
Tablice o większej liczbie wymiarów rzadko się stosuje. Sposób inicjacji, oraz operowania na tablicach tego typu jest analogiczny jak w przypadku tablic dwuwymiarowych.
Prześledźmy przykład tworzenia tablicy trójwymiarowej:
|
---|
Tablice znaków służą do przechowywania ciągów znaków, czyli tekstu. Przy deklaracji musimy pamiętać, żeby podać o jedną komórkę więcej niż potrzebujemy, ponieważ ciąg znaków musi być zakończony specjalnym znakiem '\0' (kod ASCII = 0). Dzięki temu "zakończeniu", między innymi program wie kiedy zakończyć wypisywanie tekstu.
Poniżej przedstawiony jest schemat przechowywania tekstu "Ala ma kota". W tym przypadku potrzebujemy co najmniej dwunastu komórek tablicy (pamiętajmy, że numerowanie zaczynamy od zera):
0′A′−− 1′l′−− 2′a′−− 3′ ′−− 4′m′−− 5′a′−− 6′ ′−− 7′k′−− 8′o′−− 9′t′−− 10′a′−− 11\'∖0\'−−−−
Tablicę można wypełnić przy deklaracji (podobnie jak inne obiekty), pamiętając o tym szczególnym znaku na końca tablicy:
|
---|
Wypełnianie tablicy poprzez przypisanie do danych komórek odbywa się tak samo jak na innych typach.
|
---|
Podobnie jak przy wypisywaniu tekstu, do wprowadzania posługujemy się tylko nazwą tablicy. W tym miejscu należy zwrócić uwagę na działanie obiektu "cin". Dane zostaną wczytane do napotkania pierwszej spacji lub znaku końca linii. Oznacza to, że tym sposobem możemy wczytać tylko jeden wyraz.
|
---|
Zrzut:
Metoda getline().
Drugim sposobem, jaki można tu zastosować jest wykorzystanie metody getline() obiektu cin. Funkcja ta jest ukierunkowana na wczytywanie całych wierszy. Konstrukcja wygląda następująco:
|
---|
gdzie tab to tablica znaków, a bufor to wielkość tej tablicy (tablica może przechować bufor - 1 znaków + znak końca tablicy).
Przeanalizujmy jeszcze raz nasz program:
|
---|
Zrzut:
Następną dostępną metodą jest funkcja o nazwie get() występująca w kilku wariantach. Metoda ta może działać podobnie jak getline, z tą różnicą, że znak nowego wiersza nie jest odrzucany (jak w przypadku getline()), tylko pozostaje w kolejce wejściowej. Oznacza to, że ponowne użycie get() nie pobierze ciągu znaków, ponieważ zakłada, że nastąpił już koniec wiersza. Aby zaradzić temu problemowi można użyć innej postaci get, a mianowicie metody get() bez argumentów. Pobiera ona następny znak, a wiec znak końca linii zostanie usunięty i można ponownie wczytywać dane:
|
---|
Dokładnie ten sam efekt można osiągnąć wywołując na przemian metodę get z argumentami i bez argumentów:
|
---|
Zadanie. Napisz program, który pobierze ze standardowego wejścia trzy zdania oraz wyświetli je w odwrotnej kolejności, zamieniając wszystkie male litery (tylko angielskie) na duże.
Rozwiązanie. Warto zauważyć, że numery ASCII małych liter mieszczą się w przedziale [97; 122]. Różnica między małymi i dużymi literami wynosi 32. Będziemy sprawdzać każdy znak, czy jest to mała litera. Gdy będzie spełniony warunek przesuniemy ją o 32 numery w dół aby przeskoczyć na dużą literę. Do przechowania zdań użyjemy tablicy dwuwymiarowej.
|
---|
Tablice znaków - przydatne funkcje
W tym artykule opiszę kilka podstawowych funkcji operujących na tablicach znaków:
W tym artykule opiszę w jaki sposób przekazujemy tablicę do wnętrza funkcji poprzez jej argumenty. Omówię tablice jedno i wielowymiarowe:
Na wstępie pragnę zauważyć, że nazwa tablicy jest wskaźnikiem na jej pierwszy element. Oznacza to, że przekazując tablicę będziemy pracować na jej oryginalnym adresie (wszelkie zmiany w modyfikacji tablicy będą widoczne w miejscu, gdzie ją stworzyliśmy).
typ_funkcji nazwa(typ_elementów_tablicy nazwa_tablicy[]);
Można opcjonalnie w nawiasie podać ilość elementów tablicy.
Przykładowa funkcja może wyglądać następująco:
void funkcja(int tablica[10]);
Zadanie. 1. Napisz program, który zamieni wartości tablicy 10 elementowej na ich kwadraty.
|
---|
Wyście:
0 3 4 3 6 7 11 -5 -10 87
0 9 16 9 36 49 121 25 100 7569
|
---|
typ_funkcji nazwa(typ_elementów_tablicy *nazwa_tablicy);
Drugim sposobem jest przekazanie tablicy poprzez wskaźnik na pierwszy element tej tablicy.
Program będzie się różnił tylko nagłówkiem funkcji:
|
---|
Pierwszy sposób jest podobny jak w przypadku tablicy jednowymiarowe. Przykład dotyczy tablicy dwuwymiarowej. Tablice o większej liczbie wymiarów przekazujemy podobnie. Przeanalizujmy zadanie:
Zadanie. 2. Napisz program, który zamieni wartości tablicy dwuwymiarowej o wymiarach 3x3, na ich kwadraty. Tablicę wypełniamy liczbami pseudolosowymi z przedzialu [0..9].
|
---|
Przykładowe wyjście:
1 2 3 4 5 6 -1 -2 -3
1 4 9 16 25 36 1 4 9
Drugi sposób opiera się na utworzeniu tablicy dynamicznej postaci:
typ_tablicy **nazwa_tablicy;
Przeanalizujmy powyższe zadanie rozwiązane tym sposobem:
|
---|
Tablice przydzielane dynamicznie opisane są tutaj.
Na wstępie przypomnę, że nazwa tablicy jest wskaźnikiem na jej pierwszy element. Oznacza to, że możemy odnieść się to tego elementu za pomocą nazwy tablicy i indeksu o wartości 0 lub poprzez ten wskaźnik. Popatrzmy na przykład:
|
---|
Po elementach tablicy możemy poruszać się przy użyciu indeksów podając kolejne numery komórek począwszy od 0, jak również przeskakując kolejne komórki tablicy z wykorzystaniem wskaźnika.
Warto zauważyć, że w przypadku elementów, które zajmują np. 4 bajty pamięci (liczby typu int), będziemy przy każdym zwiększeniu wskaźnika przeskakiwać o te cztery bajty, żeby dostać się do następnej wartości tablicy. W przypadku tablicy typu char przeskok będzie o jeden bajt - tyle ile zajmuje typ char. Reguła ta dotyczy każdego innego typu.
Przeanalizujmy program, który pokazuje sposób poruszania się po elementach tablicy z wykorzystaniem wskaźników:
|
---|
Wyjście:
1
5
1 2 4 5 6 4 3 2 1 0
Definicja. Wskaźnik to zmienna przechowująca adres innej zmiennej. Inaczej, jest to zmienna wskazująca na adres innej zmiennej. Skoro jest zmienną więc posiada także swój adres.
Wskaźniki mają szerokie zastosowanie w C++ co daje temu językowi ogromną elastyczność.
Zmienną wskaźnikową tworzymy podobnie jak zwykłe zmienne pamiętając tylko, aby przed nazwą wpisać operator "*". Operator ten służy także do wyłuskania wartości ze zmiennej wskaźnikowej (wyciągnięcia wartości ze zmiennej, na którą wskazuje).
|
---|
Aby przypisać wartość należy użyć nazwy zmiennej z operatorem "*" (operator wyłuskania). Tak samo postępujemy w sytuacji, gdy wartość tej zmiennej chcemy przypisać do innej zmiennej lub ją wyświetlić.
|
---|
Przypisując adres zmiennej należy wymusić aby przekazała swój adres a nie wartość. W takim przypadku należy użyć operatora "&".
Przypisując do zmiennej wskaźnikowej adres drugiej zmiennej wypisujemy tylko nazwę tej zmiennej, bez operatora "*":
|
---|
Za pomocą wskaźników można kontrolować wartości innych zmiennych:
|
---|
Wskaźniki jako argumenty funkcji. Przekazując wskaźniki jako argumenty mamy zapewnione, że każda zmiana wartości zmiennej wewnątrz funkcji będzie "widoczna" w miejscu, w którym została wywołana:
|
---|
Przykładowe wejście:
2 3
Wyjście:
3 2
Użycie zwykłych zmiennych nie spowoduje zamiany wartości, ponieważ funkcja będzie pracować na kopiach tych zmiennych.
Przydzielanie dynamiczne pamięci dla tablic, obiektów, struktur itp.
Gdy nie znamy wielkości tablicy lub tablica jest bardzo duża - większa niż stos programu, należy ją przydzielić dynamicznie.
|
---|
Przy tworzeniu dynamicznych struktur danych takich jak drzewa, kolejki, listyi stosy.