Krótki kurs C++

Prezentujemy krótki kurs programowania w języku C++.

Z tego kursu dowiesz się także jak powstaje program komputerowy.

 

JAK POWSTAJE PROGRAM?

Komputer potrafi operować wyłącznie na liczbach. Jeśli chcemy przy jego pomocy opracować grafikę lub muzykę, to musimy je zamienić na postać cyfrową. Podobnie jest z programem: tekst programu musi być zamieniony na zrozumiałe dla komputera rozkazy cyfrowe. Do tego służy specjalny program, zwany kompilatorem. Czyta on tekst programu, który zapisałeś w pliki CPP i zamienia na cyfrowe komendy dla komputera, które umieszcza w pliku wynikowym OBJ. Nie jest to jeszcze gotowy program, który można uruchomić.

Program napisany w C++ najczęściej składa się z kilku plików. Te wszystkie pliki są kompilowane do postaci osobnych plików wynikowych OBJ. Pliki te zawierają puste miejsca, bo kompilator nie zna adresów funkcji zapisanych w innych plikach. Zostawia puste miejsca przy odwołaniach do tych funkcji. Gdy już wszystko zostanie skompilowane, rozpoczyna pracę specjalny program, tak zwany linker, który uzupełnia te puste miejsca i łączy wszystkie pliki wynikowe w gotowy plik wykonywalny, czyli taki, który można już uruchomić: program EXE.

Język C++ nie ma instrukcji do obsługi dysków, drukarek, grafiki itp. Jeśli chcemy, by nasz program mógł obsługiwać pliki czy rysować na ekranie, musimy dołączyć odpowiednią bibliotekę, która zawiera zestaw funkcji do realizacji konkretnych rzeczy (np. obsługi plików). Dlaczego tak jest? Dlatego, by programy pisane w C++ mogły być kompilowane na różnych platformach (np. Linux, Windows). Dajmy na to napisałeś program pod Windows. Chcesz jednak mieć też jego wersję Linuxową. Co zrobić? Jeśli korzystałeś tylko z bibliotek standardowych, to wystarczy, że skompilujesz program pod Linuxem z Linuxowymi wersjami bibliotek i sprawa załatwiona. Inną zaletą stosowania bibliotek jest to, że nie musisz dołączać na przykład biblioteki do obsługi plików, jeśli twój program tego nie wymaga. Sam wybierasz, co jest ci potrzebne. Dzięki temu program zajmuje mniej pamięci.

Istnieje wiele gotowych bibliotek przeznaczonych do różnych zadań. Ty również możesz napisać bibliotekę i udostępnić ją innym programistom. Jeśli jednak nie chcesz, żeby oni podglądali lub przerabiali twój kod to rozdzielasz źródło twojej biblioteki na dwa pliki - plik nagłówkowy (z rozszerzeniem h) i plik implementacji (z rozszerzeniem cpp). Pierwszy zawiera tylko definicje (zapowiedzi) wszystkich danych i funkcji, natomiast w drugim umieszczasz kod odpowiedzialny za działanie funkcji. Teraz wystarczy, że skompilujesz plik cpp do postaci pliku wynikowego, który wraz z plikiem h możesz udostępnić komuś innemu Ten ktoś w żaden sposób nie dowie się, jak działa twój kod, bo dostanie go w wersji skompilowanej. Może co najwyżej zajrzeć do pliku nagłówkowego by sprawdzić, jakie funkcje siedzą w twojej bibliotece i użyć tych funkcji w swoim programie linkując twoją bibliotekę ze swoim programem.

Kompilacja to skomplikowany proces: trzeba przepuścić wszystkie pliki przez kompilator, a następnie połączyć pliki wynikowe z bibliotecznymi przy pomocy linkera. Na szczęście istnieją specjalne programy, tak zwane Zintegrowane Środowiska Programistyczne (ang. Integrated Development Environments, w skrócie IDE). Aplikacje takie zawierają edytor tekstowy, w którym wpisujesz swoje programy. Niektóre - dla ułatwienia pracy - oznaczają też na kolorowo słowa kluczowe języka. Gdy już napiszesz program, uruchamiasz proces budowania aplikacji. Środowisko programistyczne samo skompiluje wszystkie potrzebne pliki, dołączy odpowiednie biblioteki i zlinkuje gotowy plik wykonywalny. Środowiska takie posiadają też zwykle sporą dokumentację do bibliotek standardowych. W systemie Linux jest dołączony kompilator C++ i linker. Użytkownicy Windowsa mają do dyspozycji Visual C++ firmy Microsoft lub Borland C++. Istnieją też darmowe wersja Gnucpp czy Dev C++ 4.

STRUKTURA PROGRAMU

Zaczniemy od najprostszego programu, czyli tzw. "Hello, world!", programu, który tylko pokazuje jakiś komunikat powitalny. Otwórz nowe okno programu (File | New). W tym pliku wpisz coś takiego:

#include <iostream.h>
void main() {
cout << "Mój pierwszy program"; //tu jest to, co program ma robić
}

Pierwsza linijka to dyrektywa kompilatora. Służy do dołączania bibliotek do programu (ang. include). Każda dyrektywa kompilatora zaczyna się od znaczka #. Nazwę dołączanej biblioteki podaje się między znaczkami < i >. W tym przypadku dołączamy bibliotekę iostream.h. Dzięki niej korzystanie z klawiatury i monitora jest bardzo proste.

Działanie każdego programu w C++ zaczyna się w funkcji zwanej main. Wszystko, co się w niej znajduje (między znaczkami { i }) zostanie wykonane. Tu właśnie umieszcza się instrukcje. W C++ każda instrukcja musi się kończyć średnikiem. Inaczej kompilator wyświetli komunikat o błędzie. Zwracaj też dużą uwagę na wielkość liter: dla kompilatora funkcja main, Main i MAIN to 3 różne funkcje.

Jedyna instrukcja programu spowoduje wysłanie tekstu Mój pierwszy program na ekran komputera.

Aby komunikat został wyświetlony dołączona jest biblioteka iostream, która obsługuje strumienie. Są 2 rodzaje strumieni: wejściowy i wyjściowy z odpowiadającymi im nazwami. Wejściowy to cin, a wyjściowy - cout. Żeby wyświetlić tekst na ekranie, program musi go skierować do strumienia wyjściowego, co właśnie zrobiliśmy w naszym programie.

Dalsza część wiersz zawiera komentarz. Komentarz zaczyna się od // i obejmuje wszystkie znaki do końca wiersza. Jest on ignorowany przez kompilator, więc możemy tu pisać dowolne teksty. Są też komentarze obejmujące wielowierszowy fragment tekstu: zaczynają się od /* i kończą przy */. Warto dawać komentarze w programach, bo często po miesiącu zapomina się jak działał dany fragment kodu. Można też używać komentarzy do chwilowego "remowania" fragmentu programu.

Skompiluj teraz i uruchom powyższy program. Zobacz, jak działa. Możliwe, że zaraz po wyświetleniu tekstu okienko konsoli zniknie, bo system zazwyczaj zamyka okna programów konsolowych, które się zakończyły. Aby zobaczyć, co wyświetlił program wciśnij Alt+F5. Pojawi się okno z komunikatem wyświetlonym przez twój program. Do programu wrócisz wciskając dowolny klawisz. Spróbujmy teraz wyświetlic 2 teksty. Trzeba dopisać kolejną instrukcję kierującą tekst do strumienia wyjściowego.

#include <iostream.h>
void main() {
cout << " Mój pierwszy program ";
cout << "A to jego kolejny komunikat";
}

Jeśli uruchomisz powyższy program, zauważyłeś, że nie tak miało to wyglądać. Oba teksty pojawiły się w jednej linijce i to bez żadnego odstępu. Odpowiedź jest prosta: nie kazałeś programowi przejść do następnej linii ekranu. Są dwa sposoby, by to zrobić:

  1. Umieścić w tekście znak specjalny, który przerzuci tekst do następnej linii. Znak taki składa się z dwóch znaczków: \ i litery oznaczającej jakąś akcję. Na przykład znak \n oznacza przejście do nowej linii.
  2. Dorzucić do strumienia odpowiedni manipulator, czyli specjalną wartość, która steruje strumieniem. W naszym przypadku będzie to manipulator endl, który każe strumieniowi przerzucić następny tekst do nowej linii.

Tak wyglądałby program przy zastosowaniu pierwszego sposobu:

#include <iostream.h>
void main() {
cout << " Mój pierwszy program\n";
cout << "A to jego kolejny komunikat\n";
}

A tak by wyglądał przy zastosowaniu drugiego sposobu:

#include <iostream.h>
void main() {
cout << " Mój pierwszy program" << endl;;
cout << "A to jego kolejny komunikat" << endl;;
}

Stosować możesz ten, który ci bardziej odpowiada.

ZMIENNE I ICH TYPY

Czasem trzeba w programie przechować jakąś wartość lub wynik obliczeń. Służy do tego pamięć RAM komputera. Jednak odwoływanie się do komórek pamięci po ich numerach (adresach) byłoby bardzo niewygodne. Dlatego wymyślono, żeby nadawać komórkom pamięci nazwy, po których łatwiej się zorientować, co się w nich znajduje. Taki nazwany obszar pamięci to właśnie jest zmienna. Dodatkowo, żeby nie było pomyłek, każda zmienna ma swój typ określający, jakiego rodzaju wartości może przechowywać. Poznamy teraz kilka z nich, a później dowiesz się, jak je wykorzystać w programie.

Zaczniemy od typu char. Zmienna tego typu zajmuje 1 bajt i może przyjmować wartości od -128 do +127. Dlaczego akurat taki zakres? Bierze się to z zapisu takiej liczby w pamięci. W jednym bajcie można pomieścić 256 różnych kombinacji bitów, czyli liczby od 0 do 255. Jednak żeby mieć możliwość korzystania z liczb ujemnych, pierwszy bit przeznaczono na znak liczby. Jeśli jest ustawiony, oznacza to liczbę ujemną, a gdy jest zgaszony, liczba jest dodatnia. Pozostałe siedem bitów oznacza liczbę, dlatego właśnie mamy zakres od -128 do +127.

Istnieje możliwość traktowania całego bajtu jako liczby - jeżeli rezygnujemy ze znaku i mamy do dyspozycji tylko liczby dodatnie. Służy do tego przedrostek unsigned. Liczba typu unsigned char ma zakres od 0 do 255. Odpowiada to zakresowi numerów znaków z tablicy kodów ASCII, więc zmienna tego typu jest często używana do przechowywania kodów liter i znaków z tablicy ASCII.

Kolejnym typem jest int - domyślny typ zmiennych i funkcji w C++. Zakres przechowywanych wartości zależy od systemu. W systemach 32-bitowych zajmuje cztery bajty i może pomieścić wartości od -2147483648 do +2147483647. Również w tym przypadku można skorzystać z przedrostka unsigned i zrezygnować z liczb ujemnych na rzecz wyłącznie liczb dodatnich. Wtedy zakres zmiennej zmieni się na 0 do 4294967295. Jeśli nie potrzebujesz tak dużego zakresu możesz też oszczędzić pamięć używając przedrostka short, dzięki czemu zmienna typu short int zajmuje tylko 2 bajty i mieści liczby od -32768 do +32767, a gdy dodamy przedrostek unsigned, zakres zmieni się na 0 - 65535. Zmienna typu int przechowuje tylko liczby całkowite.

Dla liczb z częścią dziesiętną mamy typ float. Zajmuje on 4 bajty i służy do przechowywania liczb zmiennoprzecinkowych od 3.4*10-38 do 3.4*10+38. Jeżeli precyzja do tylu miejsc po przecinku ci nie wystarcza, jest jeszcze typ double. Zajmuje 8 bajtów i służy do przechowywania liczb zmiennoprzecinkowych podwójnej precyzji, czyli od 1.7*10-308 do 1.7*10+308.

Przy funkcjach przyda się jeszcze jeden typ: jeżeli funkcja nie zwraca żadnej wartości, to trzeba jakoś "powiedzieć" kompilatorowi. Wtedy stosuje się typ void, który oznacza brak jakiegokolwiek typu.

Czas wypróbować działanie zmiennych w programie. Skoro konsola ma służyć do rozmowy z użytkownikiem, to przeprowadźmy taką rozmowę. Poprosimy użytkownika o podanie jakiejś liczby, a gdy ją poda, program wyświetli użytkownikowi to, co przed chwilą wpisał. Do przechowania liczby posłużymy się zmienną o nazwie liczba, zadeklarowana jako całkowitą. Tak wygląda deklaracja naszej zmiennej:

int liczba;

Jak widać, najpierw podajemy typ zmiennej, później nazwę, a na końcu średnik (bo każda instrukcja w C++ musi się kończyć średnikiem). Taka instrukcja przydziela pamięć dla zmiennej liczba już na etapie kompilacji. Jednak nie wiadomo, co znajdowało się wcześniej w tym miejscu pamięci przed jej przydzieleniem, więc wartość zmiennej liczba jest przypadkowa. Dopiero gdy przypiszemy jej jakąś wartość, np. tak:

liczba = 123;

będzie ona miała znaną wartość. Możemy to wszystko zrobić w jednej instrukcji, np. tak:

int liczba = 123;

Wiesz już, jak wyświetlić tekst na ekran. A jak pobrać dane z klawiatury? W podobny sposób, tylko zamiast strumienia wyjściowego (cout) trzeba użyć strumienia wejściowego (cin) i dać odwrotnie strzałki:

cin >> liczba;

Taka instrukcja spowoduje pobranie liczby ze strumienia wejściowego (czyli klawiatury) i wstawienie jej do zmiennej liczba. Strumień wejściowy sam rozpoznaje typ danych, który chcesz wczytać. Podobnie jest przy wysyłaniu zmiennej do strumienia wyjściowego. Tu również kompilator rozpoznaje, co chcesz wyświetlić i w odpowiedni sposób wykona. Możemy już więc napisać cały program:

#include <iostream.h>
void main() {
int liczba = 123;
cout << "Podaj jakąś liczbę " << endl;
cin >> liczba;
cout << "Podąłeś liczbę " << liczba << endl;
}

Jak to działa? Najpierw jest deklarowane użycie zmiennej liczba typu int, i od razu zmienna ta jest inicjowana wartością 123. Następnie na ekran pojawi się napis Podaj jakąś liczbę, a kursor przejdzie na początek nowego wiersza ekranu. Wtedy program pobiera od użytkownika liczbę - z klawiatury - i umieszcza ją w zmiennej liczba. Na końcu na ekranie pojawia się napis, oraz liczba ze zmiennej liczba.

OPERATORY

1. Operatory arytmetyczne

Zaczniemy od operatora +. Dodaje on dwie wartości, które znajdują się po jego lewej i prawej stronie. Całe wyrażenie zastępowane jest wynikiem. Jeśli liczba znajduje się tylko po jego prawej stronie, to traktowany jest on jako dodatni znak liczby. Wyrażenia z operatorem + można łączyć kaskadowo. Przykłady:

int liczba = 2+2; // dodanie dwóch liczb. Wynik = 4
liczba = +7; // liczba = 7
liczba = 3+7+5; // wynik = 15

Podobnie działa operator -. Służy do odejmowania dwóch liczb. Jeśli liczba znajduje się tylko po jego prawej stronie, to jest traktowany jako znak ujemny liczby. Również można łączyć wyrażenia kaskadowo. Przykłady:

liczba = 5-3; // odjęcie dwóch liczb. Wynik = 2
liczba = -8; // liczba = -8
liczba = 7-2-3; // wynik = 2

Do wykonywania mnożenia służy operator *, a do dzielenia jest operator /. Te operatory wymagają liczb po obu swoich stronach. Dodatkowo jest operator % (modulo), który zwraca resztę z dzielenia dwóch liczb całkowitych. Można je łączyć kaskadowo. Przykłady:

liczba = 3*3; // mnożenie, liczba = 9
liczba = 8/4; // dzielenie, liczba = 2
liczba = 11%4; // reszta z dzielenia, liczba = 3
liczba = 8/2*2; // liczba = 8
liczba = 8*2/2; // też liczba = 8

Zawsze trzeba pamiętać o kolejności wykonywania działań. Jeśli byś kiedykolwiek miał wątpliwość, który z operatorów zadziała pierwszy, po prostu zastosuj nawiasy. To co jest w nawiasie zawsze będzie wykonywane przed działaniami spoza nawiasów.

Istnieją też operatory do zmniejszania i zwiększania zmiennej o 1. Do zwiększania służy operator ++, a do zmniejszania operator --. Można je zastosować na dwa sposoby. Jeśli umieścimy operator przed zmienną, to najpierw zmieni on jej wartość, a dopiero potem ją zwróci. Jeśli umieścimy go za zmienną, to najpierw pokaże on zawartość, a dopiero potem ją zmieni. Przykładowy fragment kodu:

int Liczba = 123;
cout << "Liczba=" << liczba << endl; // wyświetli 123
cout << "Liczba=" << ++liczba << endl; // zwiększenie do 124 i wyświetli
cout << "Liczba=" << liczba-- << endl; // wyświetli 124 i zmniejszenie do 123
cout << "Liczba=" << liczba << endl; // wyświetli 123

2. Operatory przypisania

Najprostszy to operator . Przypisuje jakąś wartość znajdującą się po jego prawej stronie do zmiennej znajduje się po jego lewej stronie. Zmiennej, bo musi być możliwa zmiana tej wartości podczas przypisywania. Można łączyć kilka operatorów kaskadowo, ale trzeba pamiętać by dopiero ostatnia wartość była tą, którą przypisujemy. Przykłady:

liczba=7;
liczba = numer = 2;

Jeśli programowałeś w innych językach, to znane ci są instrukcje typu:

liczba = liczba + 3;

Jest to zwiększenie zmiennej liczba o 3. W C++ można to zapisać prościej:

liczba += 3;

To bardzo upraszcza i skraca zapis. W powyższym zapisie można stosować wszystkie operatory arytmetyczne i bitowe.

3. Operatory relacji

Służą do porównywania dwóch wartości. Operatory te zwracają wartości logiczne. Operator == zwraca prawdę tylko wtedy, gdy oba argumenty są sobie równe, a operator != tylko wtedy, gdy argumenty są różne. Operator > zwraca prawdę tylko wtedy, gdy lewy argument jest większy od prawego, a operator < tylko wtedy, gdy prawy jest większy od lewego. Operator >= zwraca prawdę tylko wtedy, gdy lewy argument jest nie mniejszy od prawego, a operator <= tylko wtedy, gdy lewy nie większy od lewego. Przykłady:

cout << 4 == 2+2; // prawda (1)
cout << 1 != 2; // prawda (1)
cout << 0 > 1; // fałsz (0)

4. Operatory logiczne

Służą do porównywania wyrażeń logicznych. Operator && (i / AND) zwraca wartość logiczną prawda tylko wtedy, gdy oba argumenty mają wartość logiczną prawda. Operator || (lub / OR) zwróci prawdę, gdy chociaż jeden z argumentów ma wartość logiczną prawda. I na koniec operator ! (nie / NOT) zwraca prawda, gdy argument po jego prawej stronie ma wartość logiczną fałsz. Gdy ma wartość logiczną prawda, operator ten zwraca fałsz. Przykłady:

cout << 0 && 1; // fałsz (0)
cout << 1 || 0; // prawda (1)
cout << !0; // prawda (1)

Wartością logiczną może być każda liczba lub zmienna zawierajaca liczbę. Liczba równa 0 oznacza fałsz, każda liczba różna od 0 oznacza prawdę.

INSTRUKCJE PROGRAMOWE

1. Instrukcje warunkowe

Dotychczas nasze programy potrafiły tylko wykonywać instrukcje, które były kolejno umieszczone w funkcji main. Teraz to się zmieni. Nauczymy programy podejmować decyzje. Do tego służą instrukcje warunkowe. Sprawdzają one, czy są spełnione określone warunki i jeśli tak, to wykonują jakieś działanie. Jeśli jednak warunki nie są spełnione, program może wykonać w zamian inne czynności.

Instrukcja warunkowa jest zbudowana w ten sposób:

if (warunek) instrukcja;

W tej składni warunek to zmienna lub jakiekolwiek inne wyrażenie, którego wynikiem jest wartość logiczna. Jeśli wartość ta różna od 0, instrukcja jest wykonywana. W przeciwnym wypadku program ją pominie i przejdzie do kolejnej instrukcji. Jeśli chcemy, by zamiast tego wykonała się jakaś instrukcja, musimy zapisać to tak:

if (warunek) instrukcja1; else instrukcja2;

Gdy warunek będzie prawdą, wykona się instrukcja1. W przeciwnym wypadku wykona się instrukcja2.

Spróbujmy to wykorzystać w praktyce. Zmienimy program wczytujący i wyświetlający liczbę. Po wczytaniu liczby wyświetlimy komunikat czy jest ona parzysta, czy nie:

#include <iostream.h>
void main() {
int liczba = 123;
cout << "Podaj jakąś liczbę " << endl;
cin >> liczba;
if (liczba % 2 == 0 ) cout << "Podąłeś liczbę parzystą" << endl;
else cout << "Podąłeś liczbę nieparzystą" << endl;
}

Jeśli po spełnieniu lub niespełnieniu warunku ma się wykonać więcej niż 1 instrukcja, trzeba taki ciąg instrukcji zamknąć w klamrach. Kilka instrukcji zamknięte w nawiasy klamrowe traktowane jest przez kompilator jako jedna instrukcja:

if (warunek) {
instrukcja1;
instrukcja2;
instrukcja3;
}
else instrukcja4;

W takim przypadku nie daje się już średnika przed else, bo zamykający nawias klamrowy nie jest instrukcją.

Zdarza się, że do wyboru mamy więcej niż 2 możliwości. Przykładowo robisz menu, z którego użytkownik wybiera opcję podając jej numer. Czy naprawdę trzeba będzie stosować tyle instrukcji if? Jest inna instrukcja warunkowa, która wybiera jedną z kilku instrukcji, zależnie od wartości warunku. Popatrz na przykładowy program:

switch (warunek) {
case 1: instrukcja1; break;
case 5: instrukcja2; break;
case 8: instrukcja3; break;
default: instrukcja4;
}

Gdy zmienna lub wyrażenie warunek przyjmie wartość 1, wykonają się wszystkie instrukcje od dwukropka do najbliższej instrukcji break. Gdy zmienna lub wyrażenie warunek przyjmie wartość 5, wykona się instrukcja2. Gdy zmienna lub wyrażenie warunek przyjmie wartość 8, wykona się instrukcja3. Jeżeli zmienna warunek nie będzie równa żadnej z wymienionych wartości, wykonają się instrukcje umieszczone po słówku default. Jeśli byś nie dał instrukcji break, program będzie wykonywał wszystkie instrukcje po kolei dopóki nie napotka instrukcji break gdzie indziej, albo dopóki nie skończy się instrukcja warunkowa switch. Zazwyczaj jest to efekt niepożądany.

Zrobimy teraz program Kalkulator. Trzeba podać dwie liczby i działanie, które ma być na nich wykonane. Na koniec program obliczy i wyświetli wynik. Oto listing programu:

#include <iostream.h>
void main() {
float x, y; //liczby
char dz; // działanie
cout << "Podaj pierwsza liczbe:"; cin >> x;
cout << "\nPodaj druga liczbe:"; cin >> y;
cout << "\nPodaj dzialanie [+ - * /]:\n";
cin >> dz;
cout << "\n\nWynik dzialania: ";
switch (dz) {
case '+': cout << x+y; break;
case '-': cout << x-y; break;
case '*': cout << x*y; break;
case '/':
if (y!=0) cout << x/y;
else cout << "Dzielnik jest zerem - działanie niewykonalne !\n"; break;
default: cout << "Podałeś błędny znak działania !\n";
}

}

Omówimy program wiersz po wierszu. Najpierw deklarujemy 2 zmienne typu float, ponieważ będziemy też dzielić i wartości mogą wyjść ułamkowe. Kolejna zmienna będzie przechowywać znak działania, który użytkownik wprowadzi. Program prosi o podanie pierwszej i drugiej liczby, a następnie znaku działania. Teraz wkracza instrukcja warunkowa. Zwróć uwagę, że pojedynczy znak przypisany do zmiennej dz ujmujemy w apostrofy. Jeśli użytkownik wybrał dodawanie, program wyświetla sumę. Jeśli wybrał odejmowanie - różnicę. Jeśli użytkownik wybrał mnożenie - iloczyn. Jeśli wybrał dzielenie, trzeba jeszcze sprawdzić czy druga liczba nie jest przypadkiem zerem (program wywołałby błąd). Jeśli nie jest zerem, wyświetla iloraz. No i wreszcie, jeżeli użytkownik podał nieprawidłowy znak działania, to program wyświetla stosowną informację.

2. Pętla iteracyjna

W programach często zdarza się, że jakieś działanie musimy powtarzać wielokrotnie, np. sumować liczby z jakiegoś przedziału czy wczytywać wiele wartości z klawiatury. Pisanie kodu, który wykonywałby to krok po kroku jest zajęciem pracochłonnym, a tak napisany program zaczyna gwałtownie "puchnąć". Czy da się tego uniknąć? Owszem, możemy skorzystać z instrukcji programowej, która nazywa się pętlą. W programach można korzystać z dwóch rodzajów pętli: pętli iteracyjnej i pętli warunkowej (o której nieco później).

Składnia pętli iteracyjnej wygląda tak:

for (wyrażenie1 ; wyrażenie2 ; wyrażenie3) instrukcja;

Wszystkie wyrażenia są opcjonalne. Wyrażenie1 jest obliczane przed wejściem do pętli (tylko raz!). Następnie oblicza się wyrażenie2 i sprawdza czy jest ono prawdziwe (różne od 0). Jeśli tak, wykonywana jest instrukcja i obliczane jest wyrażenie3. Następnie sprawdzana jest wartość wyrażenia2. Pętla jest wykonywana do momentu, gdy wartość wyrażenia2 będzie fałszywa (równa 0). Wyrażenie3 jest obliczane każdorazowo po wykonaniu instrukcji. Jeśli wszystkie trzy wyrażenia w pętli for są puste (pętla postaci: for(;;) instrukcja), to jest to pętla nieskończona. Instrukcja w pętli for może nie wykonać się ani raz, jeśli wyrażenie2 będzie od razu fałszywe (równe 0). Pętla for może być pętlą nieskończoną, jeśli wyrażenie2 nigdy nie przyjmie wartości fałsz (0). Wyrażenie pierwsze będzie zawsze obliczone (dokładnie 1 raz). Pętla for umożliwia zgrupowanie instrukcji inicjującej pętlę, warunku kontynuacji i instrukcji po wykonaniu pętli w jednym miejscu w programie.

Napiszmy program, który będzie sumował liczby naturalne od 1 do 100:

#include <iostream.h>
void main() {
int suma, x;
for (suma=0 , x=1 ; x<=100 ; x++) suma += x;
cout << "Suma liczb od 1 do 100 wynosi: " << suma;
}

Jak to zadziała? Przed wejściem w pętlę do zmiennej suma program przypisze wartość 0, a do zmiennej x - 1 (operacja wykonana jednorazowo). Następnie zostanie sprawdzone, czy x jest mniejsze lub równe 100. Ponieważ 1 jest mniejsze od 100, to do zmiennej suma dodana zostanie aktualna wartość x, a następnie wartość x zostanie powiększona o 1. Znowu nastąpi sprawdzenie czy x jest mniejsze lub równe 100... itd. Do następnej instrukcji po pętli for przejdziemy, gdy x będzie większy od 100, czyli gdy x osiągnie wartość 101.

3. Pętle warunkowe

W odróżnieniu od pętli iteracyjnych, pętle warunkowe wykorzystujemy w sytuacjach, gdy z góry nie wiemy ile razy pętla ma być wykonana, np.: wczytujemy dane z klawiatury aż do wprowadzenia przez użytkownika określonej wartości. Nie jesteśmy w stanie przewidzieć, która to a kolei będzie wartość. Pętla warunkowa ma 2 składnie:

while (warunek) instrukcja;

lub

do instrukcja while (warunek);

Obie pętle wykonują instrukcję tak długo, jak długo prawdziwy jest warunek. Różnica między nimi polega na miejscu sprawdzania warunku: pętla while sprawdza warunek przed wykonaniem instrukcji, a pętla do-while - po jej wykonaniu. Konsekwencja tego jest taka, że pętla do-while zawsze wykona się przynajmniej 1 raz niezależnie od prawdziwości warunku (jest on sprawdzany dopiero po wykonaniu instrukcji), natomiast jeżeli warunek jest fałszywy, to pętla while nie wykona się ani razu.

Żeby zobaczyć, jak to działa w praktyce napiszmy program, który będzie wczytywał liczby z klawiatury aż do wprowadzenia 0, a następnie wyświetli sumę wszystkich wprowadzonych liczb:

#include <iostream.h>
void main() {
int suma = 0, x;
do {
cout << "Podaj liczbę do sumowania (0 - koniec)";
cin >> x;
suma += x;
}while (x != 0);

cout << "Suma wprowadzonych liczb wynosi: " << suma;
}

Pętla w naszym programie będzie wykonywana tak długo, aż użytkownik poda liczbę 0. Użyliśmy tutaj pętli do-while, gdyż najpierw musimy wczytać liczbę, a dopiero potem możemy sprawdzić, czy użytkownik nie podał wartości 0. Wykonanie tego sprawdzianu na początku nie miałoby najmniejszego sensu.

TABLICE I WSKAŹNIKI

Tablica to ułożone kolejno w pamięci zmienne jednego typu. Stosowałeś już tablice nic o tym nie wiedząc. Chodzi o teksty, które przesyłałeś do strumienia. Każdy tekst to nic innego jak tablica - zmienne typu char ustawione kolejno w pamięci. Jeżeli zamiast liczb chciałbyś wczytać z klawiatury jakiś tekst, to powstaje problem: w jakiej zmiennej go przechować? Do tego służy właśnie tablica. Można w niej przechować np. ciąg znaków. Ttworzymy ją tak:

char napis[30];

Taka deklaracja przydziela pamięć na 30 kolejnych bajtów (zmiennych typu char). Nie wiadomo, co znajdowało się wcześniej w tym miejscu pamięci. Dlatego można od razu zainicjować taką tablicę jakimiś wartościami, na przykład kolejnymi znakami napisu. Robi się to tak:

char napis[30] = "jakiś tekst";

Takie przypisanie jest możliwe tylko raz, w deklaracji tablicy. Później już nie będzie to możliwe, o czym nieco dalej. Jak widać, nie trzeba w tablicy zapełnić wszystkich 30 znaków. Można zarezerwować pamięć dla 30 znaków, ale używać raz 10, raz 20, innym razem tylko 1.

Ile znaków może zapisać w tablicy 30-elementowej: 29. Komputer podczas wypisywania łańcucha znaków musi wiedzieć, gdzie łańcuch się kończy. Do oznaczenia końca używa bajtu o wartości 0 (\0). W C++ każdy łańcuch jest zakończony takim znakiem. Nie musisz go umieszczać w tekście, kompilator sam o to zadba. Ze względu na ten znak tablica 30-elementowa może pomieścić tylko 29 znaków - bo na 30 miejscu w tablicy będzie znak kończący.

Mamy już utworzoną i zainicjowaną tablicę. Jak się do tych wartości odwoływać? Ponieważ są ułożone w pamięci sekwencyjnie, możemy się do nich odwoływać poprzez indeksy, czyli ich numery porządkowe. Elementy tablicy są numerowane od 0. Pierwszy element ma numer 0, drugi element ma numer 1, itd.. Np. żeby wypisać na ekranie 5 element tablicy, używamy takiej instrukcji:

cout << napis[4];

co jest równoznaczne z instrukcją:

cout << 'ś';

bo 5 element tej tablicy to właśnie znak ś. Podobnej składni używamy, gdy chcemy zmienić zawartość wybranego elementu tablicy. Na przykład zmiana 1 znaku wygląda tak:

napis[0] = 'J';

Wyświetl cały tekst zawarty w tablicy, a zobaczysz, że odpowiednia litera rzeczywiście się zmieniła. Instrukcja:

cout << napis;

spowoduje teraz wyświetlenie na ekran napisu Jakiś tekst. Spróbuj teraz do 5 elementu tablicy wstawić znak \0 i zobacz, że tym razem wyświetli się tylko część napisu, bo komputer kończy wyświetlanie tekstu, gdy natrafi na znak \0.

Jednak reszta napisu dalej jest w tablicy, nigdzie nie ginie. Wystarczy, że zmienisz 5 element tablicy ponownie na znak spacji.

Podczas odwoływania się do elementów tablicy należy uważać, żeby nie odwoływać się do elementów nieistniejących. Jeśli zarezerwowałeś w pamięci miejsce dla tablicy 30-elementowej, nie możesz odwoływać się do jej 50 elementu, bo spowoduje to błąd dostępu do pamięci.

Przejdźmy teraz do wskaźników. Wskaźnik (ang. pointer) to taka zmienna, która nie przechowuje wartości, lecz jej adres w pamięci lub inaczej: wskazuje miejsce w pamięci, w którym znajduje się zmienna określonego typu. Przypuśćmy, że mamy zmienną liczba typu int. Deklaracja wskaźnika, który będzie wskazywał na ten typ zmiennej, wygląda tak:

int *wskaz;

Gwiazdka oznacza, że wskaz nie jest zmienną typu int, lecz wskaźnikiem do takiej zmiennej. Przy czym nie jest ważne, gdzie gwiazdkę umieścisz: bliżej typu, bliżej nazwy, czy nawet pośrodku między nimi, byle tylko tam była. Nowoutworzony wskaźnik wskazuje w jakieś nieznane miejsce w pamięci. Zanim go użyjemy, należy go ustawić tak, żeby wskazywał tam gdzie chcemy, czyli na naszą zmienną liczba.

wskaz = &liczba;

Operator & to operator adresu (zwany też operatorem referencji): zwraca adres obiektu, który stoi po jego prawej stronie. Powyższa instrukcja oznacza: do wskaźnika wskaz przypisz adres zmiennej liczba. Od tej chwili wskaz wskazuje na miejsce zmiennej liczba w pamięci.

Po takim przypisaniu obie poniższe instrukcje są równoważne (obie przypiszą do numer wartość zmiennej liczba):

int numer = liczba;
int numer = *wskaz;

Operator * nazywa się operatorem wyłuskania. Zwraca to, na co wskazuje wskaźnik stojący po jego prawej stronie czyli wyłuskuje z pamięci to, na co wskazuje wskaźnik. Jeżeli nie byłoby operatora wyłuskania, to do zmiennej numer trafiłby adres, na który wskazuje wskaźnik, a nie to co się pod tym adresem znajduje.

Wskaźnik możemy w każdej chwili przestawić na inne miejsce w pamięci. Przestawimy go teraz tak, by wskazywał na zmienną numer:

wskaz = &numer;

W C++ tablica i wskaźnik to to samo: nazwa tablicy to nic innego jak wskaźnik na miejsce w pamięci, gdzie ta tablica się zaczyna. Jedyną różnicą jest to, że nazwa tablicy jest wskaźnikiem stałym i dlatego nie można takiego "wskaźnika" przestawić na inne miejsce w pamięci. Z tego powodu niepoprawny jest taki kod:

char napis[50] = "Przykładowy tekst";
napis = "Inny tekst";

bo oznaczałoby to przestawienie "wskaźnika" na inne miejsce w pamięci. Jeżeli zamiast tablicy posłużymy się wskaźnikiem, to taka instrukcja jest poprawna, bo zwykły wskaźnik można przestawiać. Wskaźnik również możemy zadeklarować jako stały. Robi się to tak:,/P>

char * const napis = "To jest stały wskaźnik do tekstu";

napis działa identycznie jak nazwa tablicy - nie da się go przestawiać. Sprawiła to klauzula const. Nieraz istnieje potrzeba przestawienia wskaźnika z jednoczesnym zakazem modyfikacji obszaru pamięci, na który on wskazuje. Do tego służy wskaźnik do stałej. Różni się on od wskaźnika stałego tym, że można go przestawiać na inne miejsce pamięci. Deklarujemy go tak:

char const * napis;

Tym razem słówko const (oznaczające stałą) znajduje się za znakiem gwiazdki. Tym razem nie zmienna, ale jej typ jest wartością stałą.

Tablica i wskaźnik mają dużo wspólnego. W C++ to co można zrobić z użyciem tablicy, można też zrobić z użyciem wskaźników i na ogół będzie to działać szybciej. Przykłady odwoływania się do poszczególnych znaków zmiennej wskaźnikowej:

char *wskaz = "ABCDEFGHIJK";
char znak;
znak = *Wskaz; //znak='A'
znak = *(Wskaz+2); //znak='C'
znak = Wskaz[4]; //znak='E'

Wyjaśnienia wymaga użycie nawiasów w drugim odwołaniu do zmiennej wskaz: brak nawiasów kompilator zrozumiałby jako przypisanie do zmiennej znak tego, na co wskazuje wskaz powiększonego o liczbę 2. Operator * ma wyższy priorytet niż operator + i najpierw wartość jest wyciągana z pamięci, a dopiero potem zwiększana o 2.

Można to zapisać prościej, wiedząc że wskaźnik i tablica to to samo. Przykład takiego zapisu jest w trzecim odwołaniu. Poniżej masz zestawienie równoważnych odwołań:

*wskaz; <=> wskaz[0];
*wskaz + 3; <=> wskaz[0] + 3;
*(wskaz+4); <=> wskaz[4];

Spróbuj teraz sam napisać program, który będzie pytał użytkownika o nazwisko, a następnie wyświetli mu, na jaką literę się to nazwisko zaczyna, a na jaką kończy.

REFERENCJE

Teraz o czymś, co jest poniekąd odwrotnością wskaźnika: o referencji. Referencja to zmienna, która "podszywa się" pod inną zmienną. Jest ona szczególnie przydatna przy funkcjach, o czym nieco dalej. Oto przykładowy kod:

int liczba = 10;
int & adr_liczby = liczba;

Pierwszy wiersz jest oczywisty: zwyczajne zadeklarowanie zmiennej z inicjacją. Drugi wiersz to coś nowego. Mamy tu operator adresu (zwany operatorem referencji). To jest właśnie deklaracja specjalnej zmiennej zwanej referencją. Taka deklaracja oznacza, że zmienna adr_liczby jest typu referencyjnego i może "podszywać się" pod zmienną typu int. Zmienną referencyjną musimy od razu zainicjować, inaczej kompilator zgłosi błąd, bo musi wiedzieć, jaką zmienną "udaje" referencja. Przypisujemy referencji adr_liczby zmienną liczba. Co to daje? Sprawdź poniższy kod:

cout << "Oryginalna liczba: " << liczba << endl;
cout << "Jej referencja: " << adr_liczby << endl;

Zmienna referencyjna to jakby druga nazwa dla naszej zmiennej. Jak działa taka referencja? Każda zmienna dla kompilatora to nazwa jakiegoś miejsca w pamięci. Dla kompilatora nazwa zmiennej to odpowiednik jakiegoś adresu w pamięci. Referencja działa tak, jak działa, bo przejmuje adres wewnętrzny każdej zmiennej, którą się jej przypisze. Dla kompilatora wygląda to po prostu jak inna nazwa pewnego miejsca w pamięci, czyli inna nazwa jakiejś zmiennej.

Sprawdźmy, czy rzeczywiście chodzi o tą samą zmienną, tylko pod inną nazwą. Zmienimy wartość jednej z nich i zobaczymy, czy druga też się zmieni:

liczba = 10; cout << "liczba: " << adr_liczby << endl;
adr_liczby = 20; cout << "liczba: " << liczba << endl;

Jak widać zmiana referencji powoduje również zmianę drugiej zmiennej. Gdy program chce coś zrobić z referencją, to wskazuje mu ona zawsze miejsce w pamięci, gdzie przechowywana jest ta druga zmienna. Poniższy kawałek kodu powinien wyjaśnić resztę wątpliwości:

cout << "Adres liczby: " << (&liczba) << endl;
cout << "Adres referencji: " << (&adr_liczby) << endl;

Jak widać, w oby wypadkach chodzi o ten sam adres pamięci.

Spójrzmy na to jeszcze raz od strony kompilatora. Dla niego każda nazwa zmiennej to coś na kształt wskaźnika, bo jest odpowiednikiem jakiegoś adresu w pamięci. Zwykła zmienna ma swoje stałe miejsce. Referencja to jakby "bezdomna" zmienna, która korzysta z cudzych adresów. Natomiast wskaźnik - jak zwykła zmienna - ma stały adres pamięci, ale jest czymś w rodzaju informatora, który może wskazać nam adres innej zmiennej, gdy go o to zapytamy. Referencja i wskaźnik, mimo że wydają się być tym samym, są całkowitymi przeciwieństwami. Dlatego operator & (referencji) i operator * (dereferencji) znoszą się nawzajem. Sprawdź poniższy przykład:

int liczba = 123;
int *wskaznik = &liczba;
cout << (*&liczba) << "\t\t" << liczba << endl;
cout << (&*wskaznik) << "\t\t" << wskaznik << endl;

Deklarujemy zmienną i wskaźnik do niej. W 3 wierszu zawartość nawiasu działa tak: najpierw operator referencji zwraca adres zmiennej, po czym operator dereferencji zwraca to, co jest pod tym adresem, czyli zawartość zmiennej. Oba operatory działają całkowicie odwrotnie, co powoduje, że kasują się nawzajem. Dowodem na to jest 3 wiersz:. najpierw operator dereferencji wyciąga to, co jest pod adresem wskazywanym przez wskaźnik, po czym operator referencji zwraca adres tego czegoś (czyli to, co zawiera zmienna wskaźnikowa). Powinno to już wyjaśnić, czym się różni pojęcie REFERENCJI od pojęcia DEREFERENCJI. W kolejnej części poznasz funkcje i zobaczysz wreszcie, do czego może się przydać referencja.

WŁASNE FUNKCJE

Program często powtarza jakieś czynności wielokrotnie. Na przykład wczytuje jakieś dane ze skomplikowaną kontrola poprawności lub przeprowadza identyczne obliczenia. Jednak umieszczenie kodu pojedynczego wczytania w pętli nie jest możliwe, gdyż musimy wczytywać dane na różnych drogach programu i w dodatku różnie je rozmieszczać na ekranie. Do takich zadań służą w C++ funkcje. Używaliśmy już funkcji, nawet się nad tym nie zastanawiając. Przecież cały program to jedną funkcją, która nazywała się main.

Funkcja to jakby zastąpienie jedną instrukcją wielu instrukcji wykonujących jakieś konkretne zadanie. W naszych przykładach zajmiemy się sprawdzaniem typu znaku. Wpiszemy przed funkcją main coś takiego:

void znak()
{
//Tu jest ciało funkcji, czyli instrukcje, z których funkcja się składa
}

To jest definicja funkcji, czyli określenie, co funkcja ma robić. Przyjrzyjmy się dokładniej tej konstrukcji. Nasza funkcja będzie się nazywać znak. W nawiasach klamrowych, czyli w ciele funkcji, będą się znajdowały wszystkie instrukcje, które chcemy wykonywać przy pomocy tej funkcji. Na początek wyświetlimy wynik prostego działania. Zmienimy nasz kod:

void znak ()
{
cout << "\'A\' jest literą"<< endl;
}

Apostrof jako znak specjalny - podobnie jak znak nowego wiersza - musi być poprzedzony znakiem \. Teraz w funkcji main wywołamy naszą funkcję, czyli wydamy polecenie:

znak ();

a program wyświetli na konsoli komunikat: 'A' jest literą. Spróbuj teraz przenieść kod naszej funkcji za funkcję main. Kompilator wyświetli komunikat o braku naszej funkcji.

Zapowiedź funkcji

Nasz funkcja jest dla kompilatora nowym poleceniem, musi ją więc znać, żeby mógł dobrze wykonać pracę. Początkowo zdefiniowaliśmy całą funkcję przed funkcją main. Gdy kompilator analizuje funkcję main i napotka na wywołanie naszej funkcji kwadrat(), to już ją zna. W drugim przypadku jest inaczej. Kompilator dochodząc do wywołania nie zna jeszcze naszej funkcji kwadrat(). Stąd błąd. Często wygodniej i czytelniej jest definiować swoje funkcje za funkcją main. Jeśli tak robimy to należy jakoś powiadomić kompilator, że mamy zamiar użyć funkcji i będzie się ona nazywać tak a tak. Robimy to umieszczając przed funkcją main zapowiedź naszej funkcji (bez określania jej ciała), a zapisujemy ją tam, gdzie nam odpowiada. Jedynym miejscem, gdzie nie wolno definiować funkcji, jest ciało innej funkcji. Zapowiedź funkcji wygląda następująco:

void znak();

Parametry funkcji

Napisaliśmy funkcję, która wyświetla komunikat. Jaki to ma jedna sens, jeżeli zawsze wyświetli nam tekst 'A' jest literą? My byśmy chcieli, sprawdzał dowolny znak.

Zauważyłeś już, że przy deklaracji funkcji zawsze podajemy parę nawiasów. Służą one do umieszczenia między nimi jakiejś informacji dla funkcji, którą właśnie wywołujemy. Ta informacja nazywa się parametrami funkcji. My chcemy podać funkcji argument do sprawdzenia klasy znaku. Na przykład tak:

znak ('1');

I teraz funkcja powinna wyświetlić komunikat '2' jest cyfrą. Znak powinien być typu char. Musimy powiedzieć kompilatorowi, że nasza funkcja będzie pobierać parametr typu char:

void znak (char zn)
{
if (zn >='A' && zn <='Z') cout << zn << " jest wielką literą" <<endl;
if (zn >='a' && zn <='z') cout << zn << " jest małą literą" <<endl;
if (zn >='0' && zn <='0') cout << zn << " jest cyfrą" <<endl;
}

Jak widać, podaliśmy w nawiasach parametr wraz z typem (możesz podać w razie potrzeby wiele parametrów oddzielonych przecinkami). Przypomina to deklarowanie zmiennych. Parametry nie są niczym innym, jak zmiennymi jakiegoś typu, które przekazujemy do funkcji. Kompilator widząc taki kod zarezerwuje wewnątrz funkcji zmienną typu char i nazwie ją zn. Zmienna taka będzie wyłącznie własnością funkcji. Podobnie jak inne zmienne zadeklarowanymi wewnątrz funkcji. Na takie zmienne mówimy, że są lokalne dla funkcji. Gdy funkcja się zakończy, program automatycznie usunie te zmienne z pamięci. Zmienna lokalna ma jeszcze jedną zaletę: na jej nazwę nie ma wpływu to, czy gdzieś poza funkcją jest już zmienna o takiej samej nazwie. Dla funkcji ważniejsze są zmienne, które są jej własnością. Jeśli poza funkcją istnieje zmienna o nazwie identycznej jak zmienna lokalna, to zostanie przasłonięta przez zmienną lokalną.

Zapowiedź naszej funkcji można zapisać na 2 sposoby. Podając nagłówek funkcji, wraz z listą parametrów lub podając jedynie typy parametrów.

void znak(char zn);
void znak(char);

Wartość zwracana z funkcji

Funkcja testuje parametr i wyświetla komunikat, ale my chcielibyśmy jeszcze ją mieć w programie, aby użyć do dalszych działań. Do zakończenia działania funkcji służy instrukcja return. Gdy wykonamy testy i wyświetlimy komunikat, użyjemy return by wyjść z funkcji. return pozwala również przy wychodzeniu z funkcji zwrócić do programu jakieś dane.

int znak(char zn)
{ int retwar = 4;
if (zn >='A' && zn <='Z') {cout << zn << " jest wielką literą" <<endl; retwar = 0;}
if (zn >='a' && zn <='z') {cout << zn << " jest małą literą" <<endl; retwar = 1;}
if (zn >='0' && zn <='0') {cout << zn << " jest cyfrą" <<endl; retwar = 2;}
zn+=10; //wykonaliśmy zwiększenie parametru o 10
return (retwar)
}

Tera zmieniliśmy w nagłówku funkcji typ void na typ int. Co to znaczy? Poprzednio funkcja nie zwracała żadnej wartości do kodu, który ją wywołał. Czyli typ wartości zwracanej przez funkcję był void (żaden). Teraz funkcja zwraca wartość całkowitą, czyli typ wartości zwracanej jest int. Do zwracania wartości służy instrukcja return. W naszym przykładzie zwracamy kod typu podanego parametru. Co nam to daje? Możemy teraz wywołać funkcję w taki sposób:

if (znak (x)<2) cout << "To jest litera\n";
else cout << "To nie jest litera\n";

Jeżeli funkcja zwróci wartość < 2, to wykona się pierwszy wiersz. W przeciwnym razie - drugi. Zapowiedź funkcji będzie teraz wyglądała tak:

int znak(char);

W tym zapisie widać, co wchodzi do funkcji, a co z niej wychodzi. Wartością zwracana przez funkcję może być zmienną dowolnego typu, oprócz tablicy. Można to obejść, zwracając wskaźnik do tablicy. Podobnie z przekazywaniem parametrów do funkcji.

Przekazywanie parametrów przez wartość

Sprawdźmy poniższe użycie naszej funkcji:

void main()
{
char x = 'X';
cout << znak (x) << endl;
cout << "x=" << x<< endl;
}

Wszystko działa poprawnie, ale zwróć uwagę, że zmienna x ma nadal taką samą wartość. Przy przekazywaniu parametrów do funkcji, program tylko kopiuje ich wartości i przypisuje je do zmiennych liczba lokalnych wewnątrz funkcji. Takie przekazywanie parametru nazywa się przekazywaniem przez wartość. Wewnątrz funkcji pracujemy więc na kopii zmiennej podanej jako parametr, nie modyfikując oryginału. W podobny sposób zwracany jest wynik. Funkcja kopiuje wartość zwracaną do tymczasowego obiektu, którego nie widać. Po wyświetleniu jego wartości obiekt zostaje zlikwidowany. Ale my byśmy woleli pozostawić ten wynik przy życiu. Co więc zrobić, żeby funkcja znak zapisała wynik bezpośrednio w parametrze?

Przekazywanie parametrów przez referencję

I właśnie tutaj przychodzi nam z pomocą referencja. Jak pamiętasz, referencja to jakby inna nazwa zmiennej. Jeśli parametr funkcji będzie przyjmowany jako referencja, to tak jakbyśmy posłali do funkcji zmienną, a nie jej kopię. Wewnątrz funkcji będziemy używać już nie kopii, ale samej zmiennej, tylko będzie ona mieć inną nazwę dzięki referencji. Po takiej zmianie:

int znak(char &zn)
{ int retwar = 4;
if (zn >='A' && zn <='Z') {cout << zn << " jest wielką literą" <<endl; retwar = 0;}
if (zn >='a' && zn <='z') {cout << zn << " jest małą literą" <<endl; retwar = 1;}
if (zn >='0' && zn <='0') {cout << zn << " jest cyfrą" <<endl; retwar = 2;}
zn+=10; //wykonaliśmy zwiększenie parametru o 10
return (retwar)
}

funkcja będzie modyfikować wartość oryginału, a nie kopii jak poprzednio. Dzięki temu zmienna x będzie teraz powiększona o 10. I to dzięki jednemu dodatkowemu znaczkowi. Również funkcja może wynik poprzez referencję. Wtedy nagłówek funkcji wygląda tak:

int &znak(char &zn)

W naszym przypadku nic by to nie zmieniło. Czasami jednak taki sposób zwracania wartości jest przydatny. Trzeba uważać, żeby nie zwracać referencji czegoś, co już nie będzie istniało poza funkcją: na przykład jej zmiennych lokalnych.

Przekazywanie parametrów przez wskaźnik

Trzeci sposób przekazywania parametrów do funkcji lub zwracania wartości polega na użyciu wskaźników. Pamiętasz, że parametrem funkcji nie może być tablica? Teraz poznasz sposób, w jaki można tablicę wrzucić do funkcji. Dajmy na to, że masz taką tablicę znaków:

char napis[] = "Tablica przekazana jako parametr";

Jak można by ją przekazać do funkcji, która służy do wyświetlania tekstu? Przy okazji wskaźników mówiliśmy, że nazwa tablicy jest wskaźnikiem do jej pierwszego elementu. Jeśli dałoby się przekazać do funkcji taki wskaźnik, to problem będzie rozwiązany. Oto jak należy to zrobić:

void wyswietl(char * text) { cout << text << endl; }

Teraz już możemy posłać do funkcji tablicę, wiedząc, że jej nazwa jest adresem jej pierwszego elementu:

wyswietl(napis);

W podobny sposób możemy wyświetlić tekst zaczynając od innej litery. Wystarczy, że poślemy wskaźnik na inny element tablicy, jak w poniższej instrukcji:

wyswietl(&napis[8]);

Spowoduje to wyświetlenie tylko przekazana jako parametr. Zauważ, że odwoływanie się do parametru poprzez jego wskaźnik również pozwala na modyfikowanie oryginału. Podobnie jak referencję, funkcja może także zwracać wskaźnik. I tutaj też trzeba uważać, żeby nie zwracać wskaźnika do obiektów, które przestaną istnieć po opuszczeniu funkcji.

Na tym kończymy wstęp do C++. Teraz zostaje ci już tylko zacząć samodzielne pisanie kolejnych programów: im więcej napisanych programów, tym lepsza znajomość programowania i tym łatwiejszy następny program...