Materiały do ćwiczeń z przedmiotu informatyka


Materiały do ćwiczeń z przedmiotu Informatyka
Elektronika i Telekomunikacja, Studia zaoczne, I rok, semestr ziomowy, 2007/2008
Przygotował i zebrał Marek Michalski
marek.michalski@et.put.poznan.pl
WWW.et.put.poznan.pl/~mmichal/
Polanka 3, pokój 209
Tel 665 3906
WWW.et.put.poznan.pl
yródła wiedzy oraz literatura:
pierwotne zródło materiałów - http://ddt.pl/
http://www.cplusplus.com/
http://galaxy.uci.agh.edu.pl/~chwastek/lectures/C/spis.html
http://www.ime.uz.zgora.pl/ssienkowski/informatyka.htm
http://www.pckurier.pl/archiwum/artykuly/kotowski_marek/wysokc_1/
Książki
Kernighan B. W., Ritche D.M., Język ANSI C, WNT, Warszawa, 2002
Niezła, ale trudna książka do C.
Schildt H., BORLAND C++, Wydawnictwo Nakom, Poznań 1998
Poradnik C/C++.
Grębosz J., Symfonia C++ tom I, II i III, Oficyna Kallimach, Kraków 1998
Najlepsza książka do C/C++.
http://www.deitel.com/books/cpphtp5/
Deitel  Jak programować w C++ (Angielska wersja)
Materiały do pobrania
" Bernat A., Internetowy kurs języka C++
Internetowy kurs języka C++.
" Strona internetowa programu DEV C++
Strona internetowa programu DEV C++.
" Strona umożliwiająca pobranie programu DEV C++
Strona umożliwiająca pobranie programu DEV C++.
" Strona z bibliotekami DEV C++
Strona z dodatkowymi bibliotekami do DEV C++.
Laboratorium 1 07-10-2007
I. Wybieramy środowisko pracy
1.1. Zanim postawimy pierwsze kroki
Jest wiele osób, które chcą nauczyć się programowania. Jest wiele osób, które próbują to robić. Jest wiele osób, które
zniechęciły się zanim zaczęły programować. Wybór środowiska programistycznego wraz z kompilatorem, który będzie nam
niezbędny jest bardzo trudną sprawą dla osoby, która chce się nauczyć programowania, a nie ma o tym zielonego pojęcia.
W czasopismach nie raz widzieliśmy wersję C++ Builder'a czy inne środowiska do programowania aplikacji. Z entuzjazmem
instalowaliśmy go, po czym po całym dniu walki z tym programem był on usuwany z dysku. Zapominaliśmy już o naszym
pięknym i ambitnym planie: nauczyć się programować w C++.
1.2. Przyjazne środowisko do nauki i pracy
Pomimo mojego kilkuletniego doświadczenia z programowaniem, nie mogłem znalezć odpowiedniego środowiska dla
mnie. Wraz z upływem czasu natrafiłem w końcu na program, który spełnił moje oczekiwania - jest to Dev-C++. Zaletą tego
programu jest przede wszystkim cena, uściślając dokładniej jest on za darmo. Program ten można ściągnąć ze strony
http://www.bloodshed.net/dev/. Razem z programem otrzymujemy darmowe kompilatory języków C i C++. Prawdopodobnie nie
będą to najbardziej aktualne kompilatory, jakie można znalezć w sieci Internet, jednak zaspokoją one w zupełności nasze
potrzeby.
Program, który zaproponowałem jest moim zdaniem najwygodniejszym rozwiązaniem do tego, aby zacząć uczyć się
programowania w C++. W tym też programie będą pisane wszystkie aplikacje, jakie przedstawię w tym kursie. Moim celem jest
nauczenie Cię od podstaw wykorzystywania możliwości języka C++, jak również nauczenia Cię dbania o wygląd kodu, który
będziesz wkrótce sam biegle pisał.
1.3. Śpiesz się powoli - wybierz język dla siebie przyjazny
Pobierz teraz ten program z Internetu i zainstaluj. Zalecam wybranie polskiej wersji językowej podczas instalacji,
bowiem wszystkie komunikaty omawiane w tym kursie będą nawiązywały do polskiej wersji językowej tego programu.
Dodatkowo język nie będzie dla Ciebie barierą, która mogłaby Cię zniechęcać do nauki programowania.
Co dalej? Uczymy się programować!
II. Podstawowa obsługa Dev-C++
2.1. Konfiguracja programu
Po zainstalowaniu programu, najważniejszą sprawą jest umiejętność posługiwania się podstawowymi rzeczami, które
będą nam potrzebne. Pierwsza sprawa to konfiguracja. Program jest oczywiście gotowy do pracy, niemniej jednak przyda się
usprawnić kilka rzeczy.
Na pasku głównym widnieje zakładka Narzędzia. Rozwiń ją i wybierz z niej przycisk Opcje edytora. Skonfiguruj sobie
edytor tak jak jest to pokazane na zamieszczonych rysunkach:
2.2. Tworzenie i zapisywanie plików
Nic więcej nie jest nam już do szczęścia potrzebne - tworzymy nowy dokument. Kliknij Plik/Nowy/Plik zródłowy (projektami
narazie się nie będziemy zajmowali). Utworzyłeś właśnie nowy plik. Wpisz teraz do pliku następujący tekst:
#include
#include
using namespace std;
int main()
{
cout<<"To jest mój pierwszy program!"< getch();
return(0);
}
Narazie nie musisz wiedzieć co powyższy tekst oznacza - na wszystko przyjdzie swój czas. Zapisz teraz plik gdzieś na swoim
dysku. Polecenie możesz wybrać z menu Plik/Zapisz bądz skorzystać ze skrótu [CTRL+S].
2.3. Kompilacja programu
Teraz uruchamianie programu. Z menu Uruchom wybierz Kompiluj. Polecenie to skompiluje Twój plik z kodu zródłowego do
kodu wykonywalnego (czyli z pliku *.cpp zrobi drugi plik *.exe, który jest już programem). Jeśli nie wystąpił żaden błąd, w nowej
ramce powinien pokazać się komunikat Done. co oznacza, że wszystko poszło po naszej myśli. Teraz możemy uruchomić nasz
program. Można go poszukać sobie na dysku (w katalogu, w którym zapisaliśmy plik *.cpp), lub można uruchamiać program
bezpośrednio ze środowiska, w którym pracujemy. Osobiście polecam skrót [F9], jednak każdy robi tak jak lubi. Przejrzyj sobie
opcje z menu Uruchom. Masz tam wypisane skróty, którymi można kompilować i uruchamiać pisane programy.
2.4. Wyszukiwanie miejsc wystąpienia błędów kompilacji
Wszystko jest proste, póki idzie po naszej myśli. Zobaczmy teraz co się stanie, jeśli usuniemy znak średnika, który jest
umieszczony za funkcją getch();. Wykasuj go i skompiluj projekt [CTRL+F9]. Na samym dole ekranu ukazała nam się ramka z
dwoma komunikatami:
" C:\KursCPP\Zad02x01.cpp In function 'int main()':
" 8 C:\KursCPP\Zad02x01.cpp expected ';' before "return"
Komunikat pierwszy mówi nam: w pliku Zad02.x01.cpp w funkcji int main() wystąpił błąd:
Komunikat drugi jest w pewnym sensie kontynuacją komunikatu pierwszego - precyzuje nam miejsce, w którym mamy szukać
tego błędu. Po przetłumaczeniu oznacza on: (wiersz 8 w pliku Zad02x01.cpp): oczekiwano znaku ; przed poleceniem return.
Jak widzisz, kompilator poinformował nas o tym, że brakuje średnika gdzieś w kodzie (w praktyce podał nam miejsce gdzie to
jest). Ramka, którą ujrzałeś, będzie Ci bardzo przydatna (pod warunkiem, że znasz trochę angielski i będziesz czytał ze
zrozumieniem to, co jest tam napisane). Zazwyczaj kompilator podaje dokładnie miejsce błędu (choć nie zawsze niestety tak
jest).
2.5. Przeglądarka klas
Po lewej stronie widzisz przeglądarkę projektu/klas. W praktyce możesz ją zamknąć. Narazie nie będzie nam
potrzebna przez dość długi czas nauki. Zamknąć można ją w następujący sposób: kliknij prawym przyciskiem na przeglądarkę
klas, następnie kliknij Przeglądarka projektu/klas.
Większej ilości informacji nie widzę sensu Ci przekazywać w tym rozdziale, tak więc zabieramy się już do poznawania postaw
programowania w C++.
III. Podstawy programowania w C++
3.1. Komentarze - by kod przyjazny był każdemu z nas
Programować czas już pora, więc od razu przejdzmy do przykładów, na których będę się starał wyjaśnić Ci działanie
wszystkiego, co może być niezrozumiałe.
/* To jest komentarz, który
zajmuje kilka linii.
Komentarz nie wpływa na działanie programu.
Jest on pomijany przez kompilator. */
#include
#include
using namespace std;
int main()
{
cout<<"Odwiedz nasza strone: http://ddt.pl"<klawisz aby zamknac program";
getch();//Czekaj na dowolny znak z klawiatury
//To jest komentarz, który trwa tylko jeden wiersz!!
return(0);
}
Komentarz jest to notatka sporządzana przez programistę podczas pisania programu. Nie wpływa ona na szybkość
wykonywania kodu, ani na szybkość kompilacji. Komentarze są pomijane zawsze przez kompilator, który konwertuje kod z pliku
zródłowego do pliku wykonywalnego. Prowadzi się je dla zwiększenia czytelności kodu. Często wracając do projektu po dwóch-
trzech tygodniach mamy problem przypomnieć sobie po co wpisaliśmy jakieś dziwne instrukcje w pliku (a na pierwszy rzut oka
wydają się zbędne). Nie opisuj jednak każdego wiersza - to nie ma być książka z kodem zródłowym i dokumentacją projektu
razem wzięte. W przykładzie zademonstrowałem Ci przykład dwóch komentarzy, jakie oferuje nam C++. Pierwszy z nich
rozpoczyna się ciągiem znaków /* i kończy */. Drugi z nich rozpoczyna się ciągiem znaków // i trwa aż do końca wiersza.
Komentarze przydają się też do 'wycinania' kodu, który jest nam tymczasowo zbędny w programie. Zamiast usuwać fizycznie
cały kod, którego chcemy się pozbyć, lepiej na początek go wykomentować, a dopiero pózniej (np. gdy napiszemy jego lepszy
odpowiednik) usunąć starą, wykomentowaną wersję kodu.
3.2. Dyrektywa #include
Czas zapoznać się z instrukcjami. Pierwszym wierszem, który będzie brany pod uwagę jest wiersz #include
. Warto tutaj dodać, że wszystkie wiersze, które zaczynają się znakiem # nazywamy dyrektywami preprocesora (w
Dev-C++ są one oznaczane kolorem zielonym). Dyrektywa #include <...> dołącza nowe pliki do naszego programu. W tym
przykładzie są to pliki o nazwach: iostream i conio.h. Każdy z tych plików rozszerza możliwości C++. Dzięki plikowi
iostream możemy wypisywać zarówno liczby jak i tekst oraz wczytywać w prosty sposób dane wprowadzane przez
użytkownika programu. Plik conio.h daje nam możliwość użycia funkcji getch(), która oczekuje na wciśnięcie dowolnego
klawisza z klawiatury.
W C++ istnieje wiele bibliotek. Każda z nich ma specyficzne funkcje, które możemy wykorzystać za pomocą prostego
dołączania plików (mowa tu o dyrektywie #include <...>).
3.3. C++ i przestrzenie nazw
Wraz z rozwojem języka ilość bibliotek stale rosła, a unikatowych nazw dla funkcji i zmiennych zaczynało brakować.
Wymyślono wówczas przestrzenie nazw, mające zapobiegać problemowi nakładania się nazw. Dawniej, aby wyświetlić tekst
wystarczyło napisać: cout<<"napis"<kompilator, że chcemy używać przestrzeni nazw std:: bez przedrostka. Linijka, którą informujemy kompilator o takiej zmianie to:
using namespace ...; gdzie za kropki wstawiamy nazwę przestrzeni.
3.4. Blok główny programu
Pozostała, niewyjaśniona część kodu jest to:
int main()
{
return(0);
}
Jest to tzw. blok główny programu. Wszystko co jest zawarte między klamrami funkcji int main() będzie wykonywane,
aż do momentu napotkania polecenia return(...) gdzie za kropeczki wstawiamy dowolną liczbę.
Ta liczba to kod wyjścia programu. Programy powinny zwracać kod wyjścia 0, co informuje środowisko systemowe, że
program zakończył się poprawnie. Ostatni fragment kodu zawsze będzie występował w naszych programach. Z czasem ulegnie
on oczywiście modyfikacjom, niemniej jednak na chwilę obecną ten fragment kodu jest dla nas w zupełności wystarczający.
IV. Pojęcie zmiennej i podstawowe typy danych
4.1. Zmienne
Programy, które wypisują komunikaty są zazwyczaj mało interesujące. Biblioteka pozwala nam nie tylko
wypisywać znaki, ale również je wczytywać do zmiennych. Zanim jednak zapoznamy się z instrukcją wczytywania, należy
zapoznać się z pojęciem zmiennej. Zmienna, jak sama nazwa wskazuje będzie się zmieniać w trakcie programu. Zmienna to
pewien dość mały obszar w pamięci, w którym możemy przechowywać dane różnego typu np. liczby całkowite, liczby
rzeczywiste (zmiennoprzecinkowe), znak, tekst oraz kilka innych informacji, które będą nas w przyszłości interesowały. Nie
można jednak wszystkiego zapisywać do jednej zmiennej. Każda zmienna ma swoje przeznaczenie, wielkość i właściwości. Na
zmiennych liczbowych możemy wykonywać operacje matematyczne, w innych z kolei możemy przechowywać tekst.
Zademonstruję teraz na przykładzie, jak się deklaruje zmienne i jak się z nich korzysta.
#include
#include
using namespace std;
int main()
{
int zmienna_liczba;//Deklaracja zmiennej typu "int" (liczby całkowite) o nazwie "zmienna_liczba"
unsigned int dodatnie;//Deklaracja zmiennej typu "int" bez znaku (czyli bez znaku minusa (liczby większe lub równe 0))
o nazwie "dodatnie"
float zmiennoprzecinkowa;//Deklaracja zmiennej typu "float" (liczby zmienno przecinkowej) o nazwie
"zmiennoprzecinkowa";
char jeden_znak;//deklaracja zniennej znakowej typu "char" o nazwie "jeden_znak"
unsigned char znak;//deklaracja zniennej znakowej typu "char" bez znaku minus (to będzie wymagało szerszego
wyjaśnienia)
int abc=-53;//Deklaracja zmiennej "abc" i przypisanie jej wartości początkowej równej -53;
dodatnie=22;//Przypisanie wartości 22 do zmiennej o nazwie "dodatnie";
zmiennoprzecinkowa=12.42;//Przypisanie wartości 12.42 do zmiennej o nazwie "zmiennoprzecinkowa";
znak='c';//Przypisanie do zmiennej o nazwie "znak" wartości 'c' (to również będzie wymagało szerszego wyjaśnienia)
cout<<"wypisujemy zmienne:"< cout<<" zmienna_liczbowa: "< cout<<" dodatnie: "< cout<<" abc: "< cout<<" zmiennoprzecinkowa: "< cout<<" jeden_znak: "< cout<<" znak: "< getch();//Czekaj na dowolny znak z klawiatury
return(0);
}
4.2. Instrukcja przypisania
Powyższy przykład pokazuje nam, jak tworzy się zmienne w programie. W ogólności można by to zapisać
następująco: typ_zmiennej nazwa_zmiennej; lub typ_zmiennej nazwa_zmiennej=wartość_zmiennej;. Pierwszy zapis,
rezerwuje nam pamięć w programie. Trzeba wiedzieć, że wartość w zmiennej będzie przypadkowa! Kompilator nie zeruje
wartości zmiennych! Jeśli chcemy od razu nadać zmiennej początkową wartość, możemy to zrobić korzystając z zapisu
drugiego. Jeśli nie chcemy, możemy zrobić to zawsze pózniej w następujący sposób:
nazwa_zmiennej=nowa_wartość_dla_zmiennej;. Należy wspomnieć tu jeszcze jak możemy nazywać zmienne (a raczej jak
nie możemy ich nazywać).
4.3. Nazewnictwo zmiennych
Nazwy zmiennych nie mogą zawierać polskich znaków. Dozwolone znaki to: (a..z), (A...Z), podkreślenie ( _ ) i cyfry
(0...9). Nazwa zmiennej nie może też się zaczynać od liczby. Zmienne muszą posiadać unikatową nazwę w obrębie całego
programu. Warto tu również podkreślić, że zmienna int abc; nie jest tą samą zmienną co int ABC;. Są to dwie różne zmienne,
o różnych nazwach.
4.4. Zasięg zmiennych
Każda zmienna ma również swój zasięg, w którym jest widoczna. Zasięg ten determinują klamry { ... }. Zmienna, którą
utworzysz będzie widoczna tylko w obrębie danego bloku. Narazie nie będziemy wgłębiali w szczegóły. Jak przyjdzie na to pora,
przypomnę tą informację, z szerszym wyjaśnieniem i odpowiednim przykładem.
4.5. Słowo kluczowe unsigned
Kolejną sprawą, której nie wyjaśniłem to słówko unsigned przed niektórymi zmiennymi. Wyraz ten możemy wpisywać
przed każdą zmienną całkowitą/znakową, jeśli chcemy, aby wartości były nieujemne. Dzięki temu do zmiennej możemy zapisać
dwa razy większą liczbę dodatnią. Nie możemy natomiast zapisać już do niej liczby ujemnej.
Wypisywanie wartości jest bardzo proste w C++. Przykład uważam na tyle prosty, że nie wymaga to żadnego komentarza z
mojej strony.
4.6. Zmienna typu char a kody ASCII
Ostatnią rzeczą, która została do poruszenia, to kwestia zmiennej char. Zmienna char jest rozmiaru 1 bajta.
Przechowywany jest w niej pojedynczy znak kodu ASCII z zakresu od (0..255). Aby zapisać znak do zmiennej należy podawać
go w pojedynczych apostrofach. Niektóre znaki są znakami specjalnymi dla C++, dlatego też trzeba je poprzedzić znakiem \ .
Lista takich znaków będzie w dodatkowym rozdziale. Jeśli chcemy zapisać do zmiennej znak za pomocą kodu ASCII, wystarczy
zrobić to w następujący sposób: char zmienna=50;. Kod ASCII o numerze 50 odpowiada znakowi '2'. Zapis unsigned char i
zapis char jest nam obojętny tak długo, jak nie mamy potrzeby wypisania kodu ASCII tego znaku. Jeśli będziemy chcieli
wypisać kod ASCII zmiennej char to wartość kodu ASCII, jaką zwróci nam komputer, może nie być taka, jakiej byśmy chcieli się
spodziewać. Wszystkie znaki których kod ASCII jest większy od 127, będą zwracały liczby ujemne, zamiast faktycznego numeru
kodu ASCII, jaki jest im przyporządkowany. Warto więc posługiwać się zapisem unsigned char, zamiast char.
Laboratorium 2 20-10-2007
V. Podstawy wejścia/wyjścia, zmienne i operacje na
nich
5.1. Podstawowe operacje na zmiennych
Wiemy już co to są zmienne - teraz zajmiemy się ich praktycznym wykorzystaniem. Zacznijmy od prostego przykładu.
#include
#include
using namespace std;
int main()
{
int a;
int b;
cout<<"Podaj pierwsza liczbe: ";
cin>>a;
cout<<"Podaj druga liczbe: ";
cin>>b;
cout<<"liczba a: "< cout<<"liczba b: "< cout<<"Suma a+b wynosi: "< cout<<"Roznica a-b wynosi: "< cout<<"Roznica b-a wynosi: "< cout<<"Iloczyn a*b: "< cout<<"Wynik rownania ((a+b)*b+a)*a wynosi: "<<((a+b)*b+a)*a< cout<<"Calkowity wynik z dzielenia a/b wynosi: "< cout<<"Reszta z dzielenia a/b wynosi: "< getch();
return(0);
}
Program ten pokazuje nam podstawowe operacje matematyczne, jakie możemy wykonywać na zmiennych. W C++ obowiązują
takie same zasady wykonywania działań na liczbach jak w matematyce. Najpierw mnożymy (dzielimy), pózniej dodajemy
(odejmujemy). Jeśli chcemy, aby działania wykonały się w innej kolejności, korzystamy z okrągłych nawiasów. Operację
dodawania symbolizuje + operację odejmowania -, operację mnożenia *, operację dzielenia /, a operację reszty z dzielenia %.
5.2. Jak poprawiać czytelność programu
Ponieważ operacje arytmetyczne są instrukcjami, które najczęściej są pisane, na przestrzeni wielu lat powstało sporo ułatwień
do wykonywania operacji matematycznych. Kolejny przykład prezentuje ich zapis i działanie.
#include
#include
using namespace std;
int main()
{
int a;
cout<<"Podaj liczbe: ";
cin>>a;
cout<<"liczba a: "< a=a+10;// dodaj do siebie: (a+10) i przypisz otrzymany wynik do zmiennej (a)
cout<<"liczba a: "< a+=15;// do aktualnej wartości zapisanej w zmiennej o nazwie 'a' dodaj wartość 15
cout<<"liczba a: "< a++;//Inkrementacja wartości o 1 (post inkrementacja)
cout<<"liczba a: "< ++a;//Inkrementacja wartości o 1 (pre inkrementacja)
cout<<"liczba a: "< int b;
cout<<"Podaj liczbe: ";
cin>>b;
cout<<"liczba b: "< b=b-9;
cout<<"liczba b: "< b-=8;
cout<<"liczba b: "< b--;
cout<<"liczba b: "< --b;
cout<<"liczba b: "< int c;
c=a+b;
cout< int d,e,f;//deklaracja kilku zmiennych naraz
cout<<"Podaj liczbe d: ";
cin>>d;
cout<<"Podaj liczbe e: ";
cin>>e;
cout<<"Podaj liczbe f: ";
cin>>f;
d*=5;//aktualną wartość zmiennej mnoży razy 5;
e/=2;//aktualną wartość zmiennej dzieli przez 2;
f%=3;//liczy resztę z dzielenia aktualnej wartości w zmiennej modulo 3;
cout<<"liczba d: "< cout<<"liczba e: "< cout<<"liczba f: "< getch();
return(0);
}
Pojęcia:
" Inkrementacja - zwiększenie wartości zmiennej o jeden;
" Dekrementacja - zmniejszenie wartości o jeden;
W podanym przykładzie łatwo można zauważyć, że polecenie a++; jest równoważne a+=1; jak również a=a+1; i ++a;.
Każdy z tych zapisów jest wygodny ze względu na swoją długość. Jednak jeśli nasza zmienna zmieni swoją nazwę na np. int
bardzo_niewygodna_nazwa; to inkrementacja wartości zmiennej o 1 będzie niezbyt wygodna korzystając np. z tej instrukcji
przypisania: bardzo_niewygodna_nazwa= bardzo_niewygodna_nazwa+1;. W takim wypadku warto korzystać z krótszych
zapisów, chociażby po to, żeby oszczędzić i tak już wytarte klawisze na klawiaturze. Co więcej, powinieneś dbać o estetykę
swojego kodu. Na pewno zgrabniej będzie wyglądała krótka i treściwa wiadomość, niż wiersz na pół ekranu.
5.3. Podstawowe wczytywanie danych do zmiennych
Tak się rozpędziłem z przykładami pokazującymi operacje na zmiennych, że zapomniałem wtrącić słowo o cin>>. Jak
już pewnie zdążyłeś zauważyć, za pomocą tego polecenia (strumienia), wczytujemy sobie w bardzo wygodny sposób dane do
zmiennej. Poleceniem tym będziemy wczytywali zmienne każdego typu prostego (czyli np. unsigned int, char, string - tego
jeszcze nie znasz). Pózniej oczywiście poznasz inne metody na wczytywanie danych, jednak na chwilę obecną nie masz
wystarczającej wiedzy, by coś takiego napisać.
VI. Tworzenie warunków prostych if ... else
6.1. Intuicja i podejmowanie decyzji
W życiu stale podejmujemy decyzje. Jedne są lepsze, inne gorsze jednak zawsze wybieramy jakąś drogę. Komputer
również podejmuje decyzje, lecz trochę inne niż my. Komputer postępuje zgodnie z tym, co mu napiszemy. Nie ma własnej woli,
nie ma intuicji - to tylko maszyna. Wszystko więc musi być precyzyjnie sformułowane.
6.2. Warunek if ... else ...
Język C++ pozwala nam sterować przebiegiem programu za pomocą warunków. Żeby umieć poprawnie posługiwać się
warunkami, należy zapoznać się zarówno ze składnią warunku, jak również z operatorami warunkowymi.
/*************************************************/
//Składnia warunku w C++
if(warunek) jedna_instrukcja; //średnik na końcu!
/*************************************************/
if(warunek)
{//pierwszy blok instrukcji
wiele_instrukcji;
}
/*************************************************/
if(warunek) jedna_instrukcja; //średnik na końcu!
else jedna_instrukcja;//średnik na końcu!
/*************************************************/
if(warunek)
{//pierwszy blok instrukcji
wiele_instrukcji;
}else
{//drugi blok instrukcji
wiele_instrukcji;
}
/*************************************************/
Jeśli warunek będzie spełniony (prawdziwy), wykonuje się pierwszy blok instrukcji. Jeśli natomiast warunek nie został spełniony
i istnieje drugi blok instrukcji (rozpoczyna się on słówkiem else), to wykonuje się drugi blok instrukcji.
6.3. Operatory warunkowe
//Operatory warunkowe w C++
== //Równe
!= //Różne
>= //Większe równe
<= //Mniejsze równe
> //Większe
< //Mniejsze
Operatory warunkowe zademonstruję na przykładzie, po czym szerzej je omówię.
#include
#include
using namespace std;
int main()
{
int a,b;
cout<<"Podaj liczbe a: ";
cin>>a;
cout<<"Podaj liczbe b: ";
cin>>b;
if(a==b)
{
cout<<"liczba a jest rowna liczbie b"< }
if(a>18)
{
cout<<"liczba a="< }else
{
cout<<"liczba a="< }
if(a>b)
{
cout<<"liczba a="< cout<<"Funkcja ma postac: f(x)=a*x+b"< float a,b;
cout<<"Podaj a: ";
cin>>a;
cout<<"Podaj b: ";
cin>>b;
//miejsce zerowe to: 0=a*x+b <=> -b=a*x <=> (-b)/a=x
if(...)//tu uzupełnić kod
{
cout<<"Funkcja f(x) przyjmuje wartosc=0 dla x="<<(-b)/a< }else cout<<"Funkcja f(x) nie posiada rozwiazania."< getch();
return(0);
}
//Zadanie: Zad06x05.cpp
#include
#include
using namespace std;
int main()
{
cout<<"Program sluzy do sprawdzenia, czy rownanie kwadratowe ma miejsca zerowe."< cout<<"Funkcja ma postac: f(x)=a*x^2+b*x+c"< float a,b,c,delta;
cout<<"Podaj a: ";
cin>>a;
cout<<"Podaj b: ";
cin>>b;
cout<<"Podaj c: ";
cin>>c;
delta=b*b-4*a*c;
if(...)//Tu pierwszy warunek
{
cout<<"Funkcja posiada dwa miejsca zerowe."< }else
if(...)//Tu drugi warunek
{
cout<<"Funkcja posiada jedno miejsce zerowe."< }else
{
cout<<"Funkcja nie posiada miejsc zerowych."< }
getch();
return(0);
}
6.5. Uwagi dotyczące rozdziału
" Sformułowanie operatory warunkowe zamienić na operatory porównania
VII. Tworzenie warunków złożonych if ... else
7.1. Co to warunek złożony
Pierwsze słowa wymagają małego sprostowania odnośnie warunków złożonych. Warunki złożone nie istnieją
(bynajmniej tak mi się wydaje...). Do czego zmierzam? Nazwę warunków złożonych wprowadziłem ja, w celu podzielenia
materiału związanego z warunkami na bardziej przystępne części do nauki. Jeśli nie zrozumiałeś rozdziału poświęconego
warunkom prostym, polecam się do niego teraz wrócić, ponieważ ten rozdział będzie bazował głównie na informacjach o
których wspominałem we wcześniejszym rozdziale.
Warunek złożony, to złożenie kilku warunków prostych, połączonych operatorem logicznym.
7.2. Podstawy logiki
Warunki proste już znamy, pora zapoznać się z trzema podstawowymi operatorami logicznymi.
&& // nazwa angielska: AND; tłumaczenie: operator logiczny "i"
|| // nazwa angielska: OR; tłumaczenie: operator logiczny "lub"
! //nazwa angielska: NEG; tłumaczenie: negacja
/* Logika (dział matematyki):
---------------------------------------------
| a | b | (!a) | (!b) | (a && b) | (a || b) |
---------------------------------------------
| 0 | 0 | 1 | 1 | 0 | 0 |
| 0 | 1 | 1 | 0 | 0 | 1 |
| 1 | 0 | 0 | 1 | 0 | 1 |
| 1 | 1 | 0 | 0 | 1 | 1 |
---------------------------------------------
*/
Wydaje mi się, że tabelka nie wymaga komentarza. Jednak dla osób, które nie miały jeszcze logiki postaram się
trochę rozjaśnić sytuację. Przyjmijmy, że a i b są zmiennymi. Zmienne te mogą przyjmować wartości 0 i 1. Wartość
0 symbolizuje fałsz (false), natomiast wartość 1 symbolizuje prawdę (true). W logice wartością przeciwną do 0 jest 1 i na odwrót
dla wartości 1 przeciwną wartością jest 0. Wartość przeciwną nazywamy negacją (a w C++ negację zapisujemy znakiem
wykrzyknika (!).
7.3. Warunki złożone w praktyce
Załóżmy teraz, że znamy wartości dwóch zmiennych (czyli zmiennej a i b) i jednocześnie chcemy od tych zmiennych
uzależnić dalszy przebieg naszego programu. Jeżeli nastąpi taka sytuacja, że wszystkie wartości zmiennych będą prawdziwe
(1), to ma wykonać określony dodatkowy blok instrukcji. Aby uzyskać ten efekt, w C++ należy napisać następujący kod:
//... tu jakiś kod wcześniejszy
if(a&&b)
{//początek bloku
//tu kod dodatkowy, który chcemy wykonać,
//gdy obie zmienne będą miały wartość 1
/*----------------------------------------------------
informacja:
precyzyjniejszym sformułowaniem byłoby, że obie
zmienne będą różne od zera, ponieważ dla C++,
każda wartość różna od 0 jest wartością
prawdziwą, czyli true (1).
Nie chciałem Ci tego jednak wcześniej powiedzieć,
ponieważ nie chciałem zaciemniać Ci teorii logiki.
---------------------------------------------------*/
}//koniec bloku
//... tu dalszy kod programu
Teraz wystarczy zastąpić literki a i b warunkami i mamy gotowy poprawny warunek złożony. Wprowadziłem te
symbole po to, żebyś nie koncentrował się za bardzo na wiedzy z poprzedniego rozdziału. W praktyce symbole a i b mogą być
fizycznie zmiennymi, jednak nie będziemy raczej mieli powodów, by zastosowane symbole były faktycznie zmiennymi.
7.4. Utrwalenie wiadomości przez analizę przykładów
Jedyne co pozostaje teraz zrobić, to prześledzić zamieszczone przykłady i napisać kilka zadań w celu utrwalenia
wiedzy zdobytej w tym rozdziale.
#include
#include
using namespace std;
int main()
{
float a,b;
cout<<"Podaj a: ";
cin>>a;
if((a>=50)&&(a<=100))
{
cout<<"Podana liczba miesci sie w przedziale (a>=50)&&(a<=100)"< }else cout<<"ERROR! Liczba nie miesci sie w przedziale (a>=50)&&(a<=100)"< getch();
return(0);
}
#include
#include
using namespace std;
int main()
{
float a,b;
cout<<"Podaj a: ";
cin>>a;
if( ((a>=50)&&(a<=100)) || ((a>10)&&(a<30)) )
{
cout<<"Podana liczba miesci sie w jednym z przedzialow: ((a>=50)&&(a<=100)) lub ((a>10)&&(a<30))"< }else cout<<"ERROR! Liczba mie miesci sie w zadnym z przedzialow ((a>=50)&&(a<=100)) ((a>10)&&(a<30)))"< getch();
return(0);
}
#include
#include
using namespace std;
int main()
{
float a,b;
cout<<"Podaj a: ";
cin>>a;
if((a>=50)&&(a<=100)&&(a!=75))
{
cout<<"Podana liczba miesci sie w przedziale (a>=50)&&(a<=100) i jest rozna od 75"< }else cout<<"ERROR! Liczba mie miesci sie w przedziale (a>=50)&&(a<=100) lub jest rowna 75"< getch();
return(0);
}
VIII. Warunek wielokrotnego wyboru switch ... case
8.1. Gdy mamy więcej niż dwie możliwości
Do tej pory poznaliśmy warunek if ... else .... Po co nam kolejny? Trudno powiedzieć, ale na pewno nie po to, żeby
Cię zniechęcać do programowania. Moim zdaniem warunek switch ... case został wprowadzony, w celu poprawienia
czytelności kodu. Nie umożliwia on bowiem bardziej zaawansowanych warunków, niż oferuje nam to wcześniej omawiany
warunek typu if ... else. Można nawet powiedzieć, że warunek switch ... case ma dużo mniejsze możliwości, od if ... else. Po
co więc nam on? Otóż są sytuacje, w których warto skorzystać warunku switch ... case i dlatego zostanie on omówiony.
Przyjrzyjmy się najpierw składni.
switch(zmienna)
{
case wartosc1:
//tu instrukcje zostaną wykonane jeśli (zmienna==wartosc1)
break;//koniec warunku; wychodzi z warunku switch
case wartosc2:
//tu instrukcje zostaną wykonane jeśli (zmienna==wartosc2)
break;//koniec warunku; wychodzi z warunku switch
//tu mogą być kolejne case ...
default://instrukcja warunkowa switch wykonuje ten kod
//wtedy i tylko wtedy, gdy nie został spełniony
//żaden inny wyżej wymieniony warunek
break;//wychodzi z warunku switch;
}
Warto w tym miejscu wymienić kilka przykładów, gdzie warto stosować warunek typu switch...case - są to głównie
listy wyboru, gdzie na podstawie np. naciśniętego klawisza, wybieramy dalszy przebieg programu. Można również porównywać
napis wprowadzony przez użytkownika i gdy któryś z nich będzie taki sam, jak w warunku switch...case, to wykona się
określony kod.
8.2. O czym należy pamiętać korzystając ze switch ... case
Zostały jeszcze dwie ważne sprawy, które powinieneś wiedzieć jako przyszły programista. Sprawa pierwsza to: w
warunku switch...case nie można deklarować zmiennych. Jeśli potrzebujesz koniecznie zmienną tymczasową, to musisz ją
zadeklarować przed warunkiem switch...case. Druga sprawa: jeśli zapomnisz słowa kluczowego break;, każdy kolejny
warunek się wykona w switch'u (aż do napotkania słówka break;).
8.3. Praktyczny przykład wykorzystania switch ... case
Poniżej zamieszczam przykład prezentujący działanie switch...case.
#include
#include
using namespace std;
int main()
{
float a;
float b;
cout<<"Podaj pierwsza liczbe: ";
cin>>a;
cout<<"Podaj druga liczbe: ";
cin>>b;
cout<<"liczba a: "< cout<<"liczba b: "< cout<<"Menu wyboru:"< cout<<"[1] Oblicz: a+b"< cout<<"[2] Oblicz: a-b"< cout<<"[3] Oblicz: a*b"< cout<<"[4] Oblicz: a/b"< cout<<"Wpisz numer: ";
int wybor;
cin>>wybor;
cout< switch(wybor)
{
case 1:
cout<<"Opcja "< break;
}
getch();
return(0);
}
Laboratorium 3 18-11-2007
IX. Biblioteka console.ddt
9.1. Konsola i problemy z nią związane
W dzisiejszych czasach jednym z poważnych problemów, na jaki może natrafić początkujący programista to
wykorzystywanie konsoli. Standardowe biblioteki Dev-C++ nie posiadają poleceń takich jak gotoxy() czy clrscr(), co może
odstraszyć, a co gorsza zniechęcić początkującego programistę od dalszej nauki tego języka. Co więcej na forum prawie
zawsze słyszymy odpowiedz, że Dev-C++ nie obsługuje takich poleceń i powinniśmy zainstalować sobie C++ Buildera lub
jeszcze coś innego, co posiada funkcje o które pytamy. Otóż korzystanie z konsoli pod Windowsem jest trudniejsze, niż było to
pod DOSem, jednak nie jest niemożliwe. Ponieważ moim celem nie jest zniechęcanie do programowania w Dev-C++, na stronie
http://pliki.ddt.pl zamieściłem bibliotekę console.ddt, która w tym rozdziale zostanie szczegółowo omówiona. Do biblioteki
zamieszczonej na stronie dołączona jest aktualna dokumentacja, która umożliwi Ci ich łatwe wykorzystanie bez konieczności
wgłębiania się w kod biblioteki.
9.2. Instalacja biblioteki console.ddt
Cała instalacja biblioteki console.ddt sprowadza się do pobrania pliku ze strony Internetowej http://pliki.ddt.pl, a
następnie wgraniu jej do katalogu, w którym znajdują się nasze pliki z kodem zródłowym.
9.3. Jak korzystać z nowej biblioteki
Wykorzystywanie bibliotek jest już prostą sprawą. W tym celu wystarczy wpisać na początku pliku:
#include "console.ddt"
using namespace ddt::console;
Jeśli plik nie znajduje się w tym samym katalogu co kod Twojego programu, kompilator pokaże błąd kompilacji i
poinformuje Cię, że nie mógł znalezć określonego pliku.
9.4. Przestrzeń nazw
Wszystkie funkcje, jakie zostały umieszczone w tej bibliotece znajdują się w przestrzeni nazw ddt::console. Oznacza
to, że jeśli chcesz wykorzystać jakąkolwiek funkcję z tego programu musisz ją poprzedzić zapisem ddt::console, czyli
ddt::console::nazwa_funkcji();. Jeśli nie chcesz pisać co chwilę tak długiej nazwy, wystarczy że dodasz na początku programu
zapis using namespace ddt::console;, przez co Twoje wywołanie funkcji skróci się do zapisu nazwa_funkcji();.
9.5. Poznajemy polecenia konsoli
Ponieważ obsługa konsoli pod Windowsem nie należy do najłatwiejszych postanowiłem, że utworzę do Twojego
użytku bibliotekę, która będzie dawała Ci możliwość korzystania z funkcji w tak samo prosty sposób w Dev-C++ jak programiści
używający płatnego środowiska programowania.
9.5.1. void clrscr(void);
Funkcja clrscr służy do czyszczenia całego ekranu konsoli. Ekran zostanie wyczyszczony na aktualnie ustawiony
kolor tła. Domyślnym kolorem tła zazwyczaj jest kolor czarny, więc jeśli chcesz uzyskać inny kolor, będziesz musiał najpierw
wywołać funkcję odpowiedzialną za zmianę koloru tła. Zapis void clrscr(void); oznacza tyle, że funkcja o nazwie clrscr nie
zwraca żadnej wartości i nie przyjmuje żadnych parametrów wejściowych.
9.5.2. void gotoxy(int x, int y);
Funkcja gotoxy służy do ustawiania migającego kursora w wybranym miejscu na ekranie. Lewy górny narożnik ma
współrzędne (1,1), natomiast prawy dolny ma współrzędne (80,25). Słówko void oznacza, że funkcja nie zwraca żadnej
wartości. Parametrami nazywamy zmienne umieszczone między nawiasami. W tym przypadku są to dwie zmienne typu int. W
pierwszym polu podajemy współrzędną X, a w drugim współrzędną Y. Wywołanie takiej funkcji może wyglądać na przykład tak:
ddt::console::gotoxy(70,11);//kolumna=70; wiersz=11;
9.5.3. int wherex(void);
Jeśli chcemy pobrać aktualną kolumnę w której znajduje się kursor w konsoli, wykorzystujemy do tego funkcję wherex. Funkcja
zwraca liczbę, która określa pozycję kursora w osi X. Parametr void oznacza, że funkcja nie przyjmuje żadnych parametrów
wejściowych.
9.5.4. int wherey(void);
Jeśli chcemy pobrać aktualny wiersz w której znajduje się kursor w konsoli, wykorzystujemy do tego funkcję wherey.
Funkcja zwraca liczbę, która określa pozycję kursora w osi Y. Parametr void oznacza, że funkcja nie przyjmuje żadnych
parametrów wejściowych.
9.5.5. void textattr(int kolor);
Kolejną funkcją, którą zamieściłem w tej bibliotece to textattr. Służy ona do ustawiania koloru tła i czcionki, który
będzie pisany. Funkcja nie zwraca żadnej wartości. Jedynym parametrem jaki funkcja przyjmuje jest to liczba typu
int reprezentująca kolor tła i tekstu. Maksymalna ilość kolorów dla tekstu i dla tła wynosi 16. Pierwszy kolor to 0 ostatni kolor to
15 zarówno dla tła jak i dla czcionki. Aby ustawić odpowiednie kolory tła i tekstu posługujemy się wzorem:
int kolorTla=1;//min wartosc=0; max wartosc=15;
int kolorTekstu=14;//min wartosc=0; max wartosc=15;
ddt::console::textattr(kolorTla*16+kolorTekstu);
9.5.6. void textcolor(int kolorTekstu);
Jeśli mamy potrzebę zmienić tylko kolor tekstu, możemy do tego celu wykorzystać funkcję textcolor. Funkcja nie
zwraca żadnej wartości. Jedynym parametrem, jaki przyjmuje to liczba typu int reprezentująca nowy kolor czcionki.
9.5.7. void textbackground(int kolorTla);
Jeśli mamy potrzebę zmienić tylko kolor tła, możemy do tego celu wykorzystać funkcję textbackground. Funkcja nie
zwraca żadnej wartości. Jedynym parametrem, jaki przyjmuje to liczba typu int reprezentująca nowy kolor tła.
9.6. Uwagi
" Biblioteka do pobrania: ddt-console.zip.
X. Pętla for
10.1. Złap oddech to nie zawody
Do tej pory poznałeś podstawowe narzędzia bez których nie da rady napisać jakiegokolwiek programu. Jeśli nie
zrozumiałeś jakiejś partii materiału jaka była omówiona do tej pory - wróć do niej, przeanalizuj i napisz kilka własnych
przykładów, które umożliwią Ci dokładne zrozumienie działania poszczególnych instrukcji. Nie brnij na siłę do przodu, bo tak nie
nauczysz się programowania, co najwyżej improwizowania. Sztuka wymaga czasu i pracy, a efekty przyjdą z czasem.
Programowanie to nie konkurs, kto pierwszy ten wygrywa.
10.2. Składnia pętli for(...)
Jeśli uważasz, że opanowałeś dobrze materiał z poprzednich rozdziałów najwyższy czas poznać składnię pętli for(...) i
jej zastosowanie.
Pętlę for można podzielić na cztery części:
" Inicjacja początkowych wartości zmiennych (A)
" Ustalenie warunku kończącego pętlę (B)
" Zwiększenie (zmniejszenie) licznika pętli (C)
" Powtarzany blok instrukcji (D);
Prosty przykład pętli:
for(int i=1;i<=10;i++)
{
//Powtarzany blok instrukcji
}
//lub
for(int i=1;i<=10;i++) jedna_powtarzana_instrukcja;
Zapis int i=1 jest to inicjacja początkowej wartości zmiennej. Zapis ten oznacza, że tworzymy zmienną typu int,
nazywamy ją i oraz nadajemy jej wartość początkową równą 1. Kolejną część pętli for stanowi warunek. W tym przypadku jest
to i<=10 i oznacza tyle, że dopóki warunek jest prawdziwy, to ma wykonywać blok instrukcji. Trzecią częścią, bez której pętla for
nie byłaby sobą to zwiększenie bądz zmniejszenie wartości zmiennej. W tym przypadku jest to zapis i++. Zmienna ta jest
zwiększana za każdym razem po wykonaniu wszystkich instrukcji z bloku.
10.3. Pętla for(...) a jej użyteczność
Pętlę for używamy praktycznie zawsze, gdy znamy ilość danych, jaką mamy wczytać, wypisać lub zmienić. Jeśli
chcemy policzyć średnią z określonej liczby liczb, wczytać określoną ilość danych z pliku lub wypisać określoną ilość danych na
ekran, pętla for jest do tego po prostu idealna.
10.4. Analizujemy przykłady
Przeanalizuj teraz dokładnie działanie poniższych programów. Przepisz je i uruchom, a następnie wykonaj kilka
eksperymentów modyfikując jego działanie. Eksperymenty są zalecane przy nauce programowania. Warto też wprowadzać
celowo po jednym błędzie w kodzie, żeby zapoznawać się stopniowo z komunikatami, jakie zacznie nam pokazywać kompilator
jeśli zapis będzie nieprawidłowy lub jak zacznie zachowywać się nasz program. Nie bój się eksperymentów, to one dają Ci
doświadczenie!
10.4.1. Liczenie średniej ocen
#include
#include
using namespace std;
int main()
{
int ilosc;
float ocena;
float srednia;
cout<<"Podaj ilosc ocen: ";
cin>>ilosc;
srednia=0;
for(int i=1;i<=ilosc;i++)
{
cout<<"Podaj ocene nr "<cin>>ocena;
srednia+=ocena;
}
if(ilosc>0) srednia/=ilosc;
cout<<"Srednia ocen to: "< getch();
return(0);
}
10.4.2. Tabliczka mnożenia
#include
#include
#include "console.ddt"
using namespace ddt::console;
using namespace std;
int main()
{
clrscr();
for(int k=1;k<=10;k++)
for(int i=1;i<=15;i++)
{
gotoxy((i-1)*5+1,k);
cout< }
getch();
return(0);
}
XI. Tablice zmiennych
11.1. Po co wprowadzono tablice
Praktycznie zawsze w programach mamy potrzebę zorganizowania naszych danych w jakiś wygodny sposób do pózniejszego
ich przetwarzania. Tworzenie wielu zmiennych w celu np. przechowania wieku każdej wprowadzonej osoby zmuszałoby nas do
deklarowania zmiennych w programie w następujący sposób:
int wiek1,wiek2,wiek3,wiek4,wiek5; // i tak dalej
Takie tworzenie zmiennych jak nie trudno zauważyć jest niewygodne i zajmuje dużo miejsca w pliku. Dodatkowo wczytanie
teraz wieku dla tych pięciu osób wiązałoby się z napisaniem czegoś takiego:
cout<<"Podaj wiek osoby 1: ";
cin>>wiek1;
cout<<"Podaj wiek osoby 2: ";
cin>>wiek2;
cout<<"Podaj wiek osoby 3: ";
cin>>wiek3;
cout<<"Podaj wiek osoby 4: ";
cin>>wiek4;
cout<<"Podaj wiek osoby 5: ";
cin>>wiek5;
Jak nietrudno zauważyć napisanie kodu w celu wczytania danych pięciu osób jest już dość męczące, nudne i czasochłonne. Z
pomocą przychodzą tutaj tablice.
11.2. Tablice jednowymiarowe
Tablice tworzy się podobnie jak zwykłe zmienne. Jedynymi różnicami jakie występują między tablicami zmiennych, a
zmienną to fakt, że tablica zajmuje więcej miejsca w pamięci i w nawiasach kwadratowych podaje się ilość elementów jaka ma
się znalezć w tablicy.
int nazwa_tablicy[123];
//lub
int inna_nazwa[]={0,1,3};
Zapis deklaracji tablicy można uogólnić do następującej postaci: typ_zmiennej nazwa_zmiennej
[ilość_elementów_w_tablicy];. W przypadku wiersza pierwszego z zamieszczonego przykładu typ zmiennej to int, nazwa
zmiennej to nazwa_tablicy, a ilość elementów w tablicy to 123. W trzecim wierszu podałem również drugi zapis w którym nie
podajemy kompilatorowi ilości elementów w tablicy. Kompilator na podstawie danych zawartych w klamrach sam oceni
potrzebną ilość elementów i dodatkowo odrazu nada im odpowiednie wartości.
11.3. Odczytywanie i zapisywanie danych do tablic
Odczytywanie i zapisywanie danych do zmiennej z tablicy odbywa się tak samo jak dla zwykłych zmiennych. Jedyną
istotną różnicą jest podanie numeru indeksu, do którego dane mają zostać zapisane. Tak więc zapis
nazwa_tablicy[10]=33; będzie oznaczał przypisanie wartości 33 do jedenastego pola w tablicy. Należy tu wyraznie podkreślić,
że numerowanie tablic w C++ zaczyna się zawsze od zera.
11.4. Wczytywanie wieków jeszcze raz
Dzięki tablicom wczytanie wieku każdej wprowadzonej osoby jest o wiele wygodniejsze niż w przypadku
pojedynczych zmiennych. Wygląda to następująco:
#include
using namespace std;
int main()
{
int wiek[10];
for(int i=0;i<10;i++)
{
cout<<"Podaj wiek osoby "<<(i+1)<<": ";
cin>>wiek[i];
}
}
Odczytywanie danych z tablicy jest analogiczne do zapisywania. Aby wypisać i'ty element z tablicy wiek, należy
napisać cout< 11.5. Tablice mające więcej niż jeden wymiar
C++ umożliwia również tworzenie tablic wielowymiarowych. Ich zapis jest bardzo prosty i wygodny w użyciu np. int
mapa[10][20][30]; utworzy tablicę trzywymiarową, w której będziemy mogli przechowywać liczby typu int. Zapis danych do
takiej tablicy jak nie trudno się domyślić będzie wyglądał tak: mapa[9][3][1]=13;.
11.6. Tablice, a zużycie pamięci
Tworząc tablice należy pamiętać o zasobach pamięci, jakie tablica nam pochłonie. Tablica jednowymiarowa zabierze
nam: sizeof(typ_zmiennej)*ilość_elementów; bajtów pamięci. Przykładowa tablica z rozdziału 11.5., czyli int
mapa[10][20][30]; pochłonie 4*10*20*30 bajtów pamięci, co daje prawie 24KB zarezerwowanej pamięci. Rozmiar zmiennej
int wynosi zazwyczaj 4 bajty, jednak wartość ta może być różna w zależności od architektury komputera, na którym pracujemy i
kompilatora jaki użyjemy do kompilacji.
XII. Pętla while i do ... while
12.1. Pętle po raz drugi
Wczytując dane często zdarza się również tak, że nie chcemy z góry określać ilości wczytywanych danych do
programu. Decyzję o tym, czy dane będą dalej wprowadzane czy nie użytkownik ma podejmować w każdym kroku. Pętla for(...)
już na pierwszy rzut oka byłaby niewygodna do tego celu. Co więcej wiedza, którą aktualnie posiadasz uniemożliwia Ci narazie
wykorzystanie pętli for(...) do takiego zadania.
12.2. Poznajemy pętlę do ... while(...);
W przypadku, gdy chcemy wczytać określone dane conajmniej jeden raz, wykorzystujemy do tego celu pętlę do ...
while(...). Składnia tej pętli wygląda następująco:
do
{
//tu instrukcje które mają być powtarzane
}while(warunek);
Pętla ta wykona najpierw blok instrukcji zawarty między klamrami, a następnie sprawdzi czy warunek jest spełniony
(prawdziwy) czy też nie. Pętla wykonuje się tak długo, dopóki warunek jest prawdziwy.
12.2.1. Liczymy średnią ocen jeszcze raz
Ręczne liczenie ilości ocen jest dość monotonnym zajęciem. Ponieważ chcemy tego uniknąć, napiszemy program,
który wczytuje oceny dopóki nie napotka stopnia 0. Wiemy przecież, że nie istnieje ocena 0, więc dla programu będzie to znak,
że nie mamy więcej ocen do wczytania i chcemy poznać średnią. Program taki będzie wyglądał następująco:
#include
#include
using namespace std;
int main()
{
int ilosc=0;
float srednia=0;
float ocena;
do
{
cout<<"Podaj ocene (0 konczy wprowadzanie): ";
cin>>ocena;
if(ocena>0)
{
srednia+=ocena;
ilosc+=1;
}
}while(ocena!=0);
if(ilosc>0) srednia/=ilosc;
cout<<"Wprowadziles "< cout<<"Srednia ocen to: "< getch();
return(0);
}
12.3. Pętla while (...)
Do tej pory poznaliśmy dwie pętle: for(...) i do ... while(...). Istnieje również trzecia pętla i jest nią pętla while(...).
Składnia tej pętli wygląda następująco:
while(warunek)
{
//powtarzany blok instrukcji
}
Różnica jest subtelna pomiędzy pętlą do ... while(...), a aktualnie omawianą while (...). W pętli while(...) warunek jest
sprawdzany zanim wykona się blok instrukcji. W konsekwencji, jeśli warunek nie będzie spełniony, to blok zawarty poniżej pętli
while(...) nie wykona się ani razu. Blok jest powtarzany tak długo, jak długo jest spełniony warunek pętli while(...).
12.4. Zastosowanie pętli while (...)
Pętlę while(...) zazwyczaj używa się częściej niż pętlę do ... while(...). Prawdopodobnie jest to podyktowane faktem, że dobrym
zwyczajem jest i tak inicjacja początkowa wszystkich niezbędnych zmiennych. Jeśli będziemy chcieli, żeby pętla wykonała się
conajmniej jeden raz to ustawimy wartości początkowe zmiennych tak, aby warunek tej pętli był spełniony. Nie jest to jednak
żadna reguła i nie należy przywiązywać do tego wagi. Wszędzie tam, gdzie możemy użyć pętli do ... while(...), możemy również
użyć pętli while(...) i na odwrót. Oczywiście zamiana jednej pętli na drugą będzie się wiązała z dodatkowymi, zazwyczaj
drobnymi zmianami w kodzie.
Laboratorium 4 08-12-2007
XIII. Polecenia continue; break;
13.1. Sukcesywna nauka to podstawa sukcesu
Skoro poznaliśmy już wszystkie możliwe pętle, warto w tym miejscu wtrącić słowo o instrukcjach continue; i break;.
Jeśli nadal masz problemy z posługiwaniem się pętlami, wróć do wcześniejszego materiału ponieważ ten rozdział zakłada już,
że znasz dobrze zasady działania każdej pętli.
13.2. Słowo kluczowe continue;
Kompilator C++, jak w większości języków programowania umożliwia nam modyfikowanie w dość specyficzny sposób
działanie pętli. Słowo kluczowe continue; użyte wewnątrz pętli powoduje przerwanie wykonywania bloku instrukcji i przechodzi
do sprawdzenia warunku kończącego pętlę. Jeśli jest to pętla for(...), wykonuje się jeszcze inkrementacja (lub dekrementacja)
zmiennej.
#include
#include
using namespace std;
int main()
{
int i=0;
do
{
cout<<"i="< if(i==0)
{
i+=1;
continue;
}
cout<<"koniec"< }while(i==0);
for(i=0;i<5;i++)
{
cout<<"[for] i="< if(i>2) continue;
cout<<"[for]koniec"< }
getch();
return(0);
}
13.3. Słowo kluczowe break;
Drugim słowem kluczowym, które modyfikuje działanie każdej pętli to break;. Użycie tego słowa wewnątrz pętli zmusi
ją do natychmiastowego jej przerwania. Słowo kluczowe break; zatrzymuje pracę tylko jednej pętli. Oznacza to, że jeżeli
zagniezdzimy dwie pętle, to zostanie zatrzymana pętla ta, w której zostało słowo kluczowe break; użyte. Poniższy przykład
ilustruje działanie tej instrukcji w praktyce.
#include
#include
using namespace std;
int main()
{
int j;
for(j=0;j<10;j++)
{
cout<<"Aktualne j to: "< for(int i=0;i<5;i++)
{
cout<<"[for] i="<if(i>2) break;
cout<<"[for]koniec"< }
if(j>6) break;
}
getch();
return(0);
}
13.4. Wskazana powściągliwość
Zanim zaczniesz nagminnie używać tych poleceń, zastanów się lepiej jak można napisać kod unikając używania słów
kluczowych continue; i break;. Praktycznie zawsze istnieje możliwość napisania pętli bez użycia wymienionych słów
kluczowych. Używaj ich tylko w skrajnie uzasadnionych przypadkach czyli wtedy gdy naprawdę uważasz, że czytelniejszym
rozwiązaniem będzie użycie jednego z wymienionych słów kluczowych, niż modyfikacja pewnej partii kodu.
XIV. Ciągi znaków
14.1. Wczytywanie tekstu do zmiennej
Do tej pory poznaliśmy metodę wczytywania i wyświetlania liczb. Tak samo jak liczby możemy wczytać jeden znak,
jednak co zrobić jeśli chcielibyśmy wczytać cały wyraz? Wczesne języki programowania nie posiadały zaawansowanej funkcji
do wczytywania tekstu, jednak radzono sobie z tym w inny sposób - tworzono tablicę znaków. Metoda ta przeszła również na
ten język i zaraz jak się przekonasz jest bardzo łatwa w użyciu.
#include
#include
using namespace std;
int main()
{
char wyraz[100];
cout<<"Podaj tekst: ";
cin>>wyraz;
cout<<"Wprowadziles: \""< getch();
return(0);
}
14.1.1. Co należy wiedzieć o wczytywaniu tekstu
Jeśli wprowadzisz tekst o długości n znaków to znak n+1 będzie zawsze równy 0. Dla większości funkcji, które
operują na łańcuchach znaków jest to informacja, aby zakończyć wyświetlanie kolejnych znaków z tablicy. Inaczej mówiąc: tak
się oznacza koniec tekstu. Pamiętaj, że numerowanie indeksów tablicy zaczyna się od zera! Pierwszym znakiem przykładowej
zmiennej wyraz to wyraz[0].
Kolejną sprawą, jaka jest tu ważna podkreślenia to wczytywanie danych za pomocą strumienia cin. Strumień ma to
do siebie, że wczytuje dane do zmiennej aż do napotkania białego znaku. Białymi znakami nazywamy wszystkie niewidoczne
znaki takie jak spacja, tabulator, enter no i jeszcze parę innych mniej znanych. Jeśli napiszemy dwa wyrazy oddzielone spacją,
to ten drugi będziemy musieli wczytać pózniej do tej samej lub nowej zmiennej w taki sam sposób jak ten pierwszy.
14.1.2. Wczytywanie tekstu po raz drugi
Domyślam się, że rozwiązanie, które oferuje nam strumień cin niespecjalnie Cię zachwyciło. Co więcej myślę, że
jesteś nim zdegustowany i być może nawet zniechęciło Cię to trochę do programowania. Na szczęście programiści na
przełomie wielu lat rozwijali ten język i po raz kolejny dają nam funkcję rozwiązującą ten problem. Funkcja nazywa się gets. Za
pomocą tej funkcji możesz wczytać cały tekst, aż do naciśnięcia klawisza enter. Jedyną zmianą, jakiej musimy dokonać w
naszym poprzednim programie to zamienić wiersz cin>>wyraz; na gets(wyraz);.
14.2. Sprawdzamy długość wprowadzonego tekstu
Jeśli chcemy sprawdzić długość wprowadzonego tekstu, możemy zrobić to na conajmniej dwa sposoby:
" Napisać pętlę, która znak po znaku będzie szukała końca (czyli znaku o kodzie 0);
" Skorzystać z funkcji, która już jest w jednej ze standardowych bibliotek.
Pierwszy sposób wyglądałby tak:
int dlugosc=0;
while(wyraz[dlugosc]!=0) dlugosc+=1;
W tym przypadku otrzymujemy długość tekstu w zmiennej dlugosc. Drugi sposób, który jest o wiele wygodniejszy to:
int dlugosc=strlen(wyraz);
Jak widać, zapis jest krótszy i bardziej przejrzysty.
14.3. Podsumowanie rozdziału
W C++ istnieje wiele funkcji operujących na tekście. Większość z nich mógłbyś napisać sam bez większych
problemów jednak pamiętaj, że znajomość standardowych bibliotek jest dużo więcej warta, gdy przychodzi pracować w dużej
firmie zajmującej się programowaniem aplikacji. Nie zaniedbuj więc poznawania nowych bibliotek, ponieważ właśnie w
bibliotekach tkwi cała siła C++. Nie twórz rzeczy, które są już znane, popularne, sprawne i użyteczne. Przynajmniej nie na tym
etapie programowania. Nic szybszego nie wymyślisz teraz, więc takie przedsięwzięcia podejmuj narazie głównie jako element
edukacyjno naukowy i testuj je na małych programach. Jednak jeśli będziesz chciał pisać już jakiś własny większy projekt
gorąco polecam używania tego, co oferują nam standardowe biblioteki C++ i jeśli czegoś tam nie ma to dopiero pisz własne
funkcje. Zapamiętaj tą radę, bo to prawdopodobnie najważniejsza wskazówka dla Ciebie jako przyszłego programisty jaką
możesz znalezć w tym kursie.
XV. Obsługa klawiatury za pomocą
15.1. Biblioteki istotnym elementem nauki
Do tej pory poznałeś większość podstawowych poleceń, które umożliwiają już pisanie prostych programów. Zanim
przejdziemy do bardziej zaawansowanych technik programowania, warto żebyś poznał kilka bibliotek, dzięki którym będziesz
mógł lepiej i łatwiej wykorzystywać możliwości współczesnych komputerów.
15.2. Funkcja getch();
Jak już zapewne zauważyłeś kilkukrotnie dołączaliśmy już bibliotekę conio.h. Na końcu prawie każdego programu
pisaliśmy getch(); dzięki czemu nasz program nie zamykał się odrazu i mogliśmy przeczytać komunikaty, jakie zostały
wyświetlone na ekranie. Funkcja ta należy do biblioteki conio.h. Gdybyśmy nie dołączyli pliku conio.h kompilator pokazałby
komunikat, że funkcja getch(); jest nieznana i nasz program by się nie skompilował.
Funkcja getch(); oprócz oczekiwania na znak, pozwala nam również odczytać kod wciśniętego klawisza na klawiaturze.
Funkcja jest zdefiniowana następująco:
int getch(void);
Pierwszy wyraz, czyli w tym wypadku int informuje nas jakiego typu dane są zwracane przez funkcję. Drugi wyraz, czyli tutaj
getch stanowi nazwę funkcji. Pozostała część wiersza podana w nawiasach to parametry funkcji jakie przyjmuje. Słowo
kluczowe void oznacza, że funkcja nie przyjmuje żadnych parametrów. Tak więc, jeśli chcemy odczytać kod znaku jaki został
wciśnięty na klawiaturze, wystarczy napisać cout<Funkcja getch(); odczytuje tylko po jednym znaku naraz, więc jeśli chcemy odczytać drugi znak znajdujący się w buforze,
wywołujemy ponownie funkcję getch();.
W tym miejscu wiele książek przytacza całą długą listę kodów znaków, które mają poinformować Cię jaki kod znaku za jaki
klawisz odpowiada. Moim zdaniem takie podejście do poważnego przyszłego programisty jest absurdalne i lekceważące. Każdy
programista powinien sobie sam bez większych problemów sprawdzić jaki kod znaku stanowi dany klawisz. Na dowód moich
słów prezentuję poniższy program:
#include
#include
int main()
{
unsigned char znak;
std::cout<<"klawisz [ESC] wychodzi z programu."< do
{
znak=getch();
std::cout<<"znak: '"<(znak)< while(kbhit())
{
znak=getch();
std::cout<<"*znak: '"<(znak)< }
std::cout< }while(znak!=27);//ESC
return(0);
}
Przedstawiony program wyświetla kod wciśniętego klawisza. Jak nietrudno zauważyć program jest bardzo krótki,
bardzo prosty i wykorzystujący podstawowe zagadnienia, jakie na tym etapie programowania powinieneś już biegle znać.
Wyjątkiem jest tu funkcja kbhit(); której jeszcze nie znasz, a właściwie zaraz ją dokładnie poznasz.
15.3. Funkcja kbhit();
Czasami zachodzi potrzeba sprawdzenia czy w buforze klawiatury znajdują się jeszcze jakieś dane do odczytania. Z
pomocą przychodzi nam funkcja kbhit();. Funkcja ta sprawdza, czy w buforze klawiatury znajdują się dane do odczytania. Jeśli
tak, funkcja zwraca wartość 1 (true), w przeciwnym wypadku zwraca wartość 0 (false).
int kbhit(void);
//można się też spotkać z zapisem:
bool kbhit(void);
Zapis funkcji się czyta analogicznie jak w przypadku funkcji getch();. Zwracany typ danych to int (bool), nazwa to
kbhit i void oznacza, że funkcja nie przyjmuje dodatkowych parametrów.
15.4. Rzutowanie danych
W zaprezentowanym programie został użyty zapis, static_cast(znak). Bez uruchomienia tego programu zapis
może być dla Ciebie niejasny i nic w tym dziwnego, bo do tej pory tego jeszcze nie omawialiśmy. Najprościej mówiąc to
poinformowaliśmy kompilator, że nie interesuje nas aktualna postać zmiennej znak (która jest typu char) i chcielibyśmy, aby
dane były traktowane przez kompilator jak liczba, czyli typ int. W efekcie tego, zamiast wypisania przez program znaku,
otrzymaliśmy kod znaku. Temat rzutowania zostanie omówiony szerzej i dokładniej w jednym z dalszych rozdziałów. Na chwilę
obecną warto zapamiętać tą sztuczkę i nie przejmować się jeśli pojęcie rzutowania jest niejasne. W końcu będzie poświęcony
temu zagadnieniu osobny rozdział :)
15.5. Pozostałe informacje
Biblioteka conio.h posiada również inne funkcje, jednak nie są one na tyle interesujące, żeby opisywać je tutaj. Nie
polecam też drążyć na siłę wszystkich nie omówionych funkcji z bibliotek, ponieważ i tak nie będziesz w stanie zapamiętać ich
działania. Szukaj informacji, gdy nie masz narzędzia by osiągnąć cel. W Internecie prawdopodobnie nakierują Cię na dobrą
drogę. W tym celu polecam skorzystanie ze strony http://forum.ddt.pl. A jeśli tam nie znajdziesz pomocy, szukaj jej
gdziekolwiek. Stron w Internecie jest dużo :)
XVI. biblioteka
16.1. Biblioteka matematyczna
Pisząc programy często okazuje się, że podstawowe działania matematyczne, jakie udostępnia nam C++ są
niewystarczające. Z pomocą przychodzi tu kolejna biblioteka: math.h.
16.2. Stałe matematyczne
Dwoma podstawowymi stałymi, jakie najczęściej przyjdzie nam wykorzystywać z biblioteki matematycznej to i . Wartość liczby
możemy odczytać ze stałej M_PI, natomiast wartość liczby ze zmiennej M_E.
#include
#include
#include
using namespace std;
int main()
{
cout<<"Wartosc liczby 'pi' wynosi: "< cout<<"Wartosc liczby 'e' wynosi:"< getch();
return(0);
}
Ważne, jest tu podkreślenie faktu, że strumień cout ogranicza automatycznie ilość wyświetlanych liczb po przecinku.
Jeśli będziesz wykorzystywał stałe matematyczne z biblioteki math.h z pewnością nie musisz się martwić o dokładność
wyników, ponieważ stałe te zawierają conajmniej 18 cyfr po przecinku.
16.3. Zaokrąglanie liczb
Wraz z dołączeniem pliku nagłówkowego math.h poza stałymi dostajemy bardzo dużą ilość funkcji. Pierwszymi
funkcjami, które chcę omówić to funkcje zaokrąglające liczbę i są to:
double round (double);
double ceil (double);
double floor (double);
Każda z tych funkcji przyjmuje jako jedyny parametr liczbę rzeczywistą i zwraca zaokrągloną liczbę typu
rzeczywistego (zmiennoprzecinkowego). Różnice są natomiast w wynikach, jakie otrzymamy z każdej z tych funkcji.
16.3.1. Funkcja round
Funkcja round odpowiada standardowemu zaokrąglaniu, jakie znamy ze szkoły. Każda liczba, której pierwsza liczba
po przecinku jest większa lub równa 5 jest zaokrąglana w górę, natomiast w przeciwnym przypadku liczba jest zaokrąglana w
dół.
16.3.2. Funkcja ceil
Funkcja ceil zaokrągla liczby zawsze w górę. Tak więc, liczba np. 3.0001 zostanie zaokrąglona do 4.
16.3.3. Funkcja floor
Funkcja floor zaokrągla liczby zawsze w dół. Przykładowo liczba 5.9999 zostanie zaokrąglona do 5.
16.3.4. Tabela porównawcza funkcji zaokrąglających
W celu lepszego zobrazowania zachowania każdej z wymienionych funkcji prezentuję tabelę, pokazującą jak przykładowe liczby
będą zaokrąglane w zależności od użytej funkcji.
liczba round(liczba) ceil(liczba) floor(liczba)
10 10 10 10
10.0001 10 11 10
10.4999 10 11 10
10.5 11 11 10
10.9999 11 11 10
-6 -6 -6 -6
-6.4 -6 -6 -7
-6.5 -7 -6 -7
-6.9 -7 -6 -7
16.3.5. Przykład
Jeśli nadal masz wątpliwości co do działania tych funkcji, zamieszczam przykład, który powinien rozwiać Twoje ewentualne
pytania pod warunkiem, że skompilujesz go i potestujesz.
#include
#include
#include
using namespace std;
int main()
{
double liczba;
cout<<"Podaj liczbe: ";
cin>>liczba;
cout<<"round("< cout<<"ceil("< cout<<"floor("< getch();
return(0);
}
16.4. Podstawowe funkcje trygonometryczne
Jak nie trudno się domyślić po nazwie działu omówione zostaną tu trzy podstawowe funkcje trygonometryczne: sinus, cosinus i
tangens.
double sin (double);
double cos (double);
double tan (double);
Wszystkie wymienione wyżej funkcje przyjmują jako parametr wejściowy kąt. Kąt dla tych funkcji musimy podawać w
radianach. Używając funkcji trygonometrycznych nie należy zapominać o asymptotach jakie występują w niektórych funkcjach i
należy pamiętać o zabezpieczaniu programów przed ewentualnymi błędami, jakie mogą się pojawić w wyniku wykonywania
nieprawidłowych działań matematycznych.
Funkcje te wykorzystuje się analogicznie do round, ceil i floor.
16.4.1. Zamiana stopni na radiany
Jeśli uważałeś na lekcjach w szkole, powinieneś wiedzieć, że 1 radian = = 180 stopni. Stąd wyznaczenie wzoru
przekształcającego dowolny kąt na radiany jest już bardzo proste i formuła zapisana w C++ będzie wyglądała następująco:
double stopnie;
cout<<"Podaj kat w stponiach: ";
cin>>stopnie;
double radiany=(stopnie*M_PI)/180.0f;
16.5. Potęgowanie liczb
Jeśli mamy potrzebę podnieść jakąkolwiek liczbę do dowolnej potęgi możemy to zrobić używając funkcji pow. Funkcja
ta przyjmuje dwa parametry i zwraca wynik potęgowania. Pierwszym parametrem jest liczba, którą chcemy potęgować, drugim
natomiast potęga do której chcemy wybraną liczbę podnieść.
double pow (double, double);
16.5.1. Przykład
#include
#include
#include
using namespace std;
int main()
{
double liczba,potega;
cout<<"Podaj liczbe: ";
cin>>liczba;
cout<<"Do ktorej potegi podniesc liczbe "< cin>>potega;
cout<<"pow("< getch();
return(0);
}
16.6. Pierwiastek stopnia drugiego
Biblioteka math.h została również wyposażona w funkcję sqrt. Służy ona do obliczenia pierwiastka stopnia drugiego.
Pierwszym i jedynym parametrem tej funkcji jest liczba z której chcemy policzyć pierwiastek. Funkcja zwraca wartość
pierwiastka z liczby, którą przekazaliśmy parametrem do funkcji.
double sqrt (double);
Pamiętaj, że pierwiastek z liczby ujemnej nie istnieje (w ciele liczb rzeczywistych, na których są dokonywane obliczenia w
komputerze). Gdy używasz jakichkolwiek funkcji matematycznych myśl jak matematyk, nie jak informatyk. Unikniesz pózniej
błędów i co ważniejsze nerwów jakie mogą się z błędami wiązać.
16.7. Pierwiastki wyższego stopnia
Jeśli chcesz policzyć wartość pierwiastka dowolnego stopnia, powinieneś tu skorzystać z funkcji pow.
sqrt(liczba)==pow(liczba,(1/2.0))
Czyli jeśli chcemy uzyskać pierwiastek stopnia piątego, zapis będzie następujący:
double liczba;
cout<<"Podaj liczbe: ";
cin>>liczba;
double wynik=pow(liczba,(1/5.0)); //lub poprostu: pow(liczba,0.2);
cout<<"Pierwiastek stopnia piatego z liczby "<16.8. Logarytmy
Ostatnimi funkcjami, jakie postanowiłem jeszcze omówić z biblioteki math.h to logarytmy. Funkcja log i
log10 przyjmują po jednym parametrze wejściowym. Obie funkcje obliczają logarytm z liczby przekazanej przez parametr po
czym zwracają wynik. Wyniki jednak będą różne, ponieważ funkcja log to logarytm naturalny (czyli o podstawie ), natomiast
funkcja log10 to logarytm o podstawie 10.
double log (double);
double log10 (double);
Jeśli wymienione funkcje będą dla Ciebie niewystarczające będziesz zmuszony poszukać informacji na ten temat w
Internecie.
16.9. Pozostałe informacje
Biblioteka math.h zawiera znacznie więcej stałych i funkcji niż te, które zostały w tym rozdziale wymienione. Po raz
kolejny odsyłam Cię do innych zródeł i zachęcam do skorzystania ze strony http://forum.ddt.pl, jeśli będzie taka potrzeba.
Laboratorium 5 15-12-2007
XVII. Funkcje w C++
17.1. Ogólna budowa funkcji
Do tej pory miałeś okazję niejednokrotnie wykorzystywać istniejące funkcje we własnych programach. Jak zapewne
zauważyłeś, wykorzystywanie funkcji jest bardzo wygodne i proste w użyciu. Dobrze by było, gdyby równie proste było pisanie
własnych funkcji... i tak w rzeczywistości właśnie jest.
Każda funkcja posiada trzy własności:
" zwraca dane (lub nie jeśli tego nie chcemy);
" posiada nazwę (bezwarunkowo musi być nazwa o unikatowej nazwie);
" może posiadać dowolną ilość parametrów wejściowych (lub może nie mieć żadnego, jeśli tego nie chcemy).
17.2. Deklaracja funkcji
Zgodnie z wymienionymi podpunktami ogólna budowa funkcji prezentuje się następująco:
zwracany_typ_danych nazwa_funkcji(parametry_funkcji)
{
//blok funkcji
return(wartosc_zwracana);
}
//Przykładowo:
int MojaPierwszaFunkcjaDodawania(int a,long b=10)
{
//tu można umieścić jakieś jeszcze instrukcje, pętle itp.
//można by powiedzieć, że funkcja to Twój podprogram, któremu dajesz
//jakieś parametry a on na zakończenie daje Ci wynik
//który pózniej wykorzystujesz gdzieś dalej
return(a+b);
}
17.2.1. Co ważnego powinniśmy wiedzieć o funkcjach
Wszystkie zadeklarowane zmienne wewnątrz funkcji lub w parametrach funkcji są widoczne tylko i wyłącznie w obrębie
funkcji. Ich modyfikacja nie wpływa na wartości zmiennych poza obrębem funkcji. Parametry funkcji możesz traktować jak
zwykłe zmienne wewnątrz funkcji, które różnią się tylko tym, że mają przypisaną wartość podaną podczas wywołania funkcji.
Pozostałe informacje przedstawiam w punktach:
" Każda funkcja musi mieć unikatową nazwę w obrębie całego programu (są odstępstwa ale o tym pózniej);
" Parametry funkcji oddzielone są przecinkami;
" Słowo kluczowe void informuje kompilator, że funkcja nie zwraca żadnych danych.
" Słowo kluczowe void użyte w miejscu definiowania parametrów funkcji informuje kompilator, że funkcja nie przyjmuje
żadnych parametrów;
" Parametrom funkcji można przypisywać domyślne wartości, które zostaną użyte wtedy gdy podczas jej wywołania
parametr nie zostanie uzupełniony;
" Funkcja może zwracać tylko jeden prosty typ danych;
" Za pomocą parametrów możemy przekazywać również tablice.
17.3. Parametry funkcji przekazywane przez referencję
Jeśli chcemy, aby parametr wejściowy modyfikował wartość zmiennej, którą podajemy jako parametr funkcji, musimy
w tym celu wykorzystać referencję. Referencję oznaczamy symbolem & i piszemy go przed nazwą zmiennej. Wygląda to tak:
#include
#include
using namespace std;
void PodniesDoPotegiDrugiej(int &liczba)
{
liczba*=liczba;
}
int main()
{
int liczba;
cout<<"Podaj liczbe: ";
cin>>liczba;
PodniesDoPotegiDrugiej(liczba);
cout<<"Liczba="< getch();
return(0);
}
Jeśli zapomnielibyśmy o referencji (czyli o dodaniu znaku &), wartość zmiennej na zewnątrz funkcji nie uległaby
zmianie. W efekcie wyświetliłaby się wartość ta, którą wprowadziliśmy zamiast liczba podniesiona do potęgi drugiej.
Pamiętajmy jednak, że jeżeli używamy referencji to typ zmiennej przekazywanej przez parametr musi być dokładnie taki sam
jak parametr w definicji funkcji.
17.4. Definicja funkcji
Jeśli chcemy poinformować kompilator, że gdzieś w kodzie (lub poza nim np. w zewnętrznej bibliotece) znajduje się
deklaracja funkcji musimy użyć do tego definicji funkcji. Aby zdefiniować funkcję wystarczy napisać:
zwracany_typ_danych nazwa_funkcji(parametry_funkcji);
//Przykładowo
void nowaFunkcja(long,short,bool);
Pisząc definicję funkcji możemy dodatkowo pisać nazwy dla parametrów. Są one jednak ignorowane przez
kompilator i pełnią tylko i wyłącznie rolę informacyjną dla użytkownika. Jak pewnie zauważyłeś zamiast bloku głównego na
końcu definicji funkcji jest średnik.
17.4.1. Kiedy potrzebujemy definicję funkcji
Definicja funkcji jest nam potrzebna prawie zawsze wtedy, gdy jedna funkcja zależy od drugiej. Najlepiej będzie
przytoczyć tu przykład:
#include
int ObliczCos(int a,int b);
int Wynik(int a,int b)
{
return(ObliczCos(a,b)*a-b);
}
int ObliczCos(int a,int b)
{
return(a+b*a);
}
int main()
{
return(0);
}
Teraz, jeśli usuniemy z tego programu definicję funkcji to program wyrzuci błąd kompilacji i poinformuje Cię, że nie
zna funkcji ObliczCos, którą chce wywołać funkcja Wynik. Jeśli jesteś spostrzegawczy to pewnie zauważyłeś, że gdybyś
zamienił kolejność funkcji Wynik i ObliczCos miejscami to niebyła by Ci potrzebna definicja funkcji. Czasami jednak nie da rady
dokonać takiej zamiany i będziesz musiał wpisać wcześniej definicję, aby kod mógł Ci się skompilować.
17.5. Przeciążanie nazwy funkcji
Jeśli chcemy mieć koniecznie dwie lub więcej funkcji o tej samej nazwie możemy to uczynić pod warunkiem, że lista
wszystkich parametrów każdej funkcji będzie różna. Różnica musi być w typie conajmniej jednej zmiennej lub w ilości
parametrów, która umożliwi kompilatorowi jednoznacznie określić w momencie wywołania funkcji o którą programiście chodzi.
Poniżej zamieszczam przykład przeciążania funkcji.
#include
#include
using namespace std;
int PotegaCzwarta(int n)
{
return(n*n*n*n);
}
int PotegaCzwarta(int a,int b)
{
return(PotegaCzwarta(a+b));
}
int main()
{
long long liczba;
cout<<"Podaj liczbe: ";
cin>>liczba;
cout<<"Liczba "< cout<<"Liczba ("< getch();
return(0);
}
17.6. Rekurencja
Jak każdy szanujący się język programowania, C++ daje Ci możliwość tworzenia rekurencji. Rekurencją nazywamy
wywoływanie funkcji wewnątrz jej deklaracji. Nieumiejętne posługiwanie się rekurencją może w bardzo łatwy sposób zawiesić
Twój program. Wystarczy, że dasz zły warunek wyjścia z rekurencji i wywoływanie będzie zapętlone w nieskończoność.
Nieskończoność prędzej czy pózniej w przypadku rekurencji nastanie, ponieważ zasoby pamięciowe komputera są
ograniczone. Poniżej zamieszczam funkcję, która liczy silnię i jest napisana rekurencyjnie.
long long Silnia(long long n)
{
if(n<=1) return(1); else return(Silnia(n-1)*n);
}
XVIII. Biblioteka
18.1. Bardziej profesjonalne podejście do tekstu
Jak już wspomniałem język C++ był rozwijany latami i między innymi efektem tych prac jest biblioteka string. Do tej
pory wczytywanie tekstu wiązało się z koniecznością podawania maksymalnej długości tekstu, jaki może być wczytany.
Klasa ta daje dużo większe możliwości i eliminuje ten problem z życia codziennego programisty.
18.1.1. Co to jest klasa
Klasa to bardziej złożony typ danych, posiadający własne zmienne i funkcje. Większe szczegóły nie są na tym etapie
Tobie potrzebne.
18.2. Jak korzystamy z klasy string
Klasa string, umieszczona jest w przestrzeni nazw std::. Nazwa string stanowi typ zmiennej. Deklaracja zmiennej
wygląda więc następująco:
std::string nazwa_zmiennej;
Wczytywanie i zapisywanie tekstu za pomocą cin i cout odbywa się dokładnie tak samo, jak w przypadku zwykłej
zmiennej. Skoro jednak można wczytać dowolnie długi tekst może się pojawić w tym miejscu nurtujące pytanie: jakiego
rozmiaru jest ta zmienna i ile pamięci pochłania? Odpowiedz jest napawająca optymizmem i brzmi następująco: zmienna
zajmuje niewiele więcej bajtów, niż długość tekstu. Jak to się dzieje? Klasa string dynamicznie zarządza danymi i alokuje bądz
zwalnia sobie pamięć w zależności od potrzeb. Programista może się cieszyć prostotą użycia tej klasy, a klasa
string wykonuje za niego brudną robotę.
18.3. Poznajemy klasę string
Jak zapewne już zauważyłeś funkcje, których używałeś do sprawdzania długości tekstu, czy też do wczytania tekstu
za pomocą gets nie działają dla typu string. Ponieważ klasa zarządza dynamicznie pamięcią to również w tym celu zostały
stworzone odpowiednie funkcje, dostosowane do takiego podejścia do tekstu.
18.3.1. Sprawdzamy długość tekstu
Aby sprawdzić długość tekstu jaki znajduje się w zmiennej string, musimy wywołać funkcję, która znajduje się
wewnątrz klasy string. Zadanie to może brzmieć strasznie, ale jak zobaczysz jest to bardzo prosta i wygodna sprawa. Oto
przykład:
#include
#include
using namespace std;
int main()
{
string wyraz;
cout<<"Podaj tekst: ";
cin>>wyraz;
cout<<"Wprowadziles: \""< cout<<"Długosc wyrazu: "< getch();
return(0);
}
Jak widać, wywołanie funkcji length(), znajdującej się wewnątrz klasy string spowodowało wypisanie długości
zmiennej z której nastąpiło wywołanie. Tutaj wywołaliśmy tą funkcję dla zmiennej wyraz.
18.4. Wczytywanie wiersza znaków
W celu wczytania wiersza znaków, tym razem posłużymy się funkcją std::getline. Funkcja przyjmuje trzy parametry.
Pierwszym z nich jest strumień, z którego dane mają zostać wczytane. Do drugiego parametru podajemy nazwę zmiennej typu
string, do której dane mają zostać zapisane. Trzeci parametr jest opcjonalny. Informuje on o tym kiedy ma zakończyć się
wczytywanie danych (jaki znak oznacza koniec wczytywania danych) i domyślnie przyjmowaną wartością dla tego parametru
jest '\n', czyli znak końca lini (enter). Przykład:
#include
#include
using namespace std;
int main()
{
string wyraz;
cout<<"Podaj tekst: ";
getline(cin,wyraz);
cout<<"Wprowadziles: \""< cout<<"Długosc wyrazu: "< getch();
return(0);
}
18.5. Aączenie tekstów
Klasa string ma bardzo wiele pożytecznych i wygodnych funkcji. Jednak oprócz tego posiada jeszcze jedną bardzo ważną i
wygodną własność: łatwe łączenie tekstów ze sobą. Jeśli mamy dwie lub więcej zmiennych typu string i chcemy je połączyć
tak, aby dostać zmienną w której będziemy mieli wszystkie teksty połączone razem wystarczy wykorzystać do tego operator
matematyczny +. Poniższy przykład demonstruje tą własność.
#include
#include
using namespace std;
int main()
{
string wyraz,nastepny,wynik;
cout<<"Podaj tekst: ";
getline(cin,wyraz);
cout<<"Podaj drugi tekst: ";
getline(cin,nastepny);
wynik=wyraz+" wlasny napis "+nastepny;
cout<<"Polaczony tekst to: \""< cout<<"Długosc tekstu: "< getch();
return(0);
}
18.6. Podsumowanie rozdziału
Informacje jakie znalazły się w tym rozdziale są tak naprawdę wstępem do klasy string i jej wykorzystywania.
Wiadomości te powinieneś poszerzać we własnym zakresie w miarę potrzeb. W tym kursie opisuję tylko te funkcje, które wydają
mi się najbardziej użyteczne i są najczęściej wykorzystywane przez programistów.
XIX. Struktury danych
19.1. Organizowanie danych w paczki
Jeśli próbowałeś już pisać jakąś bazę danych, np. przechowującą kontakty telefoniczne do różnych osób to
zauważyłeś pewnie, że szukanie nazw tablic i posługiwanie się wieloma tablicami dla jednej osoby nie jest wygodne. Z pomocą
przychodzą tu struktury, które pozwalają na zorganizowanie danych w wygodniejszy sposób. Deklaracja struktury wygląda
następująco:
struct wlasna_nazwa_typu
{
//tutaj deklarujemy zmienne jakie mają się znaleść nowym, własnym typie
};
Przykładowo, jeśli chcemy utworzyć strukturę do przechowywania danych osoby, możemy to zrobić tak:
struct OsobaST
{
std::string imie;
std::string nazwisko;
std::string telefon;
int wiek;
};
19.2. Jak wykorzystujemy strukturę w praktyce
Ponieważ wiemy już jak utworzyć własny, bardziej złożony typ danych to dobrze by było, gdybyśmy teraz wiedzieli jak go
możemy wykorzystać. Zmienną deklarujemy tak samo, jak w przypadku standardowych typów danych. Czyli zapis OsobaST
nowa_osoba; utworzy nam zmienną o nazwie nowa_osoba, której typem jest OsobaST.
19.2.1. Odczytywanie i zapisywanie danych do struktury
Odczytywanie i zapisywanie danych do zmiennej, która jest strukturą jest bardzo prosta. Mianowicie podajmy nazwę zmiennej,
stawiamy kropkę a następnie podajemy nazwę pola jakie ma być wczytane/wyświetlone. Przykład wczytywania/wypisywania
danych do/ze struktury.
cin>>nowa_osoba.imie;
cin>>nowa_osoba.nazwisko;
cin>>nowa_osoba.telefon;
cin>>nowa_osoba.wiek;
cout<cout<<"tel: "<19.3. Struktury a funkcje
Język C++ umożliwia tworzenie funkcji wewnątrz struktury. Nie będziemy jednak tego teraz omawiali, ponieważ temat
ten zostanie omówiony dla klas.
19.4. Tworzymy dużą aplikację
Zanim przystąpimy do pisania dużej aplikacji musimy uświadomić sobie, co chcemy napisać. Następnie ustalamy
jakie możliwości ma mieć nasz pisany program. Określenie możliwości programu już na samym jego początku jest kluczowe dla
każdego projektu. Dzięki temu, wiemy co chcemy pisać i dążymy do rozwiązywania problemów, jeśli na takie natrafimy.
19.4.1. Nazwa projektu
Nazwij plik tak, aby kojarzył Ci się z zadaniem które wykonuje. Ponieważ napiszemy prostą książkę telefoniczną, to
plik nazwiemy ksiazkatel.cpp.
19.4.2. Określenie zadań
Zadania organizujemy sobie najlepiej w krótkie punkty, aby mieć możliwość odhaczania rzeczy, które zostały napisane. Tak
więc lista rzeczy, które chcielibyśmy napisać to:
" Wczytywanie danych osoby
" Wyświetlanie danych osoby wybranej z listy
" Aktualna lista osób
" Kasowanie wybranego wpisu na podstawie indeksu
" Dodawanie wpisu na końcu listy
" Edycja wybranego wpisu
Ponieważ nie znamy obsługi plików to nie deklarujemy chęci wczytywania i zapisywania danych. Zresztą na tym etapie
projektu wczytywanie i zapisywanie danych jest zbędne - narazie trzeba myśleć o napisaniu wymienionych elementów.
19.4.3. Ustalamy ograniczenia
Ponieważ nie znasz jeszcze dynamicznego zarządzania pamięcią, musimy wstępnie przyjąć, że program ma mieć
ograniczoną ilość danych. Maksymalna ilość osób jaka może być w bazie to będzie załóżmy 50 osób. Na początku programu
utworzymy stałą, która ułatwi nam pózniej ewentualne zwiększenie limitu osób.
19.4.4. Ustalamy dane jakie ma posiadać osoba
Kolejnym etapem jest ustalenie danych, jakie mamy wczytywać do programu. Dla nas będzie to:
" imię (tekst)
" nazwisko (tekst)
" telefon (tekst)
Jeśli chcesz mieć więcej pól utwórz już strukturę, która będzie magazynowała dane jednej osoby.
19.4.5. Dzielimy zadania na funkcje
Aby kod nie przerósł naszych możliwości i nie zapanował w nim chaos, musimy ustalić deklaracje funkcji, jakie
przydałby nam się do zarządzania danymi. Ja proponuję takie rozwiązanie:
OsobaST WczytajOsobe(void);
void WyswietlDaneOsoby(OsobaST);
void WyswietlListeOsob(OsobaST tablica[],int iloscElelemtow);
void KasujOsobe(OsobaST tablica[],int &iloscElelemtow,int indeksDoSkasowania);
void DodajOsobe(OsobaST tablica[],int &aktualnaIloscElementow);
void EdytujDaneOsoby(OsobaST&);
Ponieważ chcielibyśmy mieć listę, którą będziemy przesuwali strzałkami, to będzie nam potrzebna taka informacja jak element
zaznaczony na liście. Modyfikujemy więc deklarację funkcji odpowiedzialną za wyświetlanie danych i dopisujemy nowy
parametr.
void WyswietlListeOsob(OsobaST tablica[],int iloscElelemtow,int zaznaczElement);
Jak się dłużej zastanowimy, to dojdziemy do wniosku, że gdy będzie w bazie więcej osób, niż wierszy na ekranie, to nie
zmieszczą nam się osoby wszystkie osoby na niej. Będziemy więc musieli zacząć wyświetlać listę od któregoś indeksu
(niekoniecznie od zerowego). Znowu modyfikujemy deklarację funkcji tak, aby uwzględniała to.
void WyswietlListeOsob(OsobaST tablica[],int iloscElelemtow,int zaznaczElement,int WyswietlOdElementu);
19.4.6. Określamy zadania dla bloku głównego
Blok główny chcemy, żeby zarządzał klawiaturą i wywoływał zadania. Wywoływanie zadań będziemy dodawali stopniowo, w
miarę rozwoju programu. W bloku głównym umieścimy tablicę z danymi, którą będziemy przekazywali za pomocą parametrów
do odpowiednich funkcji.
19.4.7. Przystępujemy do kodowania
Po wprowadzonych poprawkach na etapie projektowania aktualne funkcje wydają się rozsądnie zaprojektowane, więc
przystępujemy do kodowania bloku głównego programu. Jeśli będzie potrzeba wprowadzania jeszcze modyfikacji, oczywiście
zrobimy to już w locie. Nie należy jednak wychodzić z założenia, że wszystko będziemy pisali w locie, bo tak to mało co będzie
nam prawdopodobnie poprawnie działało.
#include
#include
#include "console.ddt"
#define MAX_OSOB 50
using namespace ddt::console;
using namespace std;
struct OsobaST
{
string imie;
string nazwisko;
string telefon;
};
OsobaST WczytajOsobe(void);
void WyswietlDaneOsoby(OsobaST);
void WyswietlListeOsob(OsobaST tablica[],int iloscElelemtow);
void KasujOsobe(OsobaST tablica[],int &iloscElelemtow,int indeksDoSkasowania);
void DodajOsobe(OsobaST tablica[],int &aktualnaIloscElementow);
void EdytujDaneOsoby(OsobaST&);
void WyswietlKomunikaty(void)
{
gotoxy(1,25);
cout<<"[ESC] Wyjscie ";
cout<<"[INSERT] Dodaj wpis ";
return;
}
int main()
{
OsobaST osoby[MAX_OSOB];
int iloscOsob=0;
bool koniec=false;
while(koniec!=true)
{
clrscr();
WyswietlKomunikaty();
unsigned char znak=getch();
switch(znak)
{
case 27://ESC
koniec=true;
break;
}
}
return(0);
}
Jedyną rzeczą, której do tej pory nie omówiliśmy to #define nazwa wartosc;. Generalnie zapis taki wykorzystujemy
do zdefiniowania stałej, która podczas kompilacji jest wstawiana w miejsca w których pojawia się nazwa. Dyrektywy
preprocesora zostaną omówione w jednym z dalszych rozdziałów.
Aktualną postać kodu rozwijamy już stopniowo i małymi partiami, dzięki czemu zapanowanie nad kodem staje się bardzo
proste, pomimo iż sam program po ukończeniu będzie zawierał bardzo dużo linii kodu.
19.5. Ukończony kod ksiazkatel.cpp
Jeśli nie udało Ci się napisać całego programu działającego zgodnie z założeniami jakie postawiliśmy w fazie
projektowania, poniżej zamieszczam cały kod programu. Jeśli jednak sam napisałeś cały program jesteś pewnie z siebie
dumny. Polecam jednak przejrzeć wszystkie funkcje bo być może wyciągniesz z nich jakieś pożyteczne wnioski dla siebie.
#include
#include
#include "console.ddt"
#define MAX_OSOB 50
#define WYSWIETL_OSOB 20
using namespace ddt::console;
using namespace std;
struct OsobaST
{
string imie;
string nazwisko;
string telefon;
};
OsobaST WczytajOsobe(void);
void WyswietlDaneOsoby(OsobaST);
void WyswietlListeOsob(OsobaST tablica[],int iloscElelemtow,int zaznaczElement,int wyswietlOdElementu);
void KasujOsobe(OsobaST tablica[],int &iloscElementow,int indeksDoSkasowania);
void DodajOsobe(OsobaST tablica[],int &aktualnaIloscElementow);
void WyswietlKomunikaty(int iloscElementow)
{
textbackground(0);
textcolor(7);
gotoxy(1,1);
cout<<"Ilosc osob: "< gotoxy(1,24);
cout<<"[ESC] Wyjscie ";
cout<<"[INSERT] Dodaj wpis ";
cout<<"[GORA/DOL] Poruszanie sie po liscie ";
gotoxy(1,25);
cout<<"[DELETE] Kasuj osobe ";
cout<<"[ENTER] Pokaz dane osoby ";
cout<<"[CTRL+ENTER] Edytuj osobe ";
return;
}
void EdytujTekst(string &fTekst);
void EdytujDaneOsoby(OsobaST&);
int main()
{
OsobaST osoby[MAX_OSOB];
int iloscOsob=0;
bool koniec=false;
int pozycja=0;
int pozycjaListy=0;
while(koniec!=true)
{
textcolor(7);
textbackground(0);
clrscr();
if(pozycja>=iloscOsob) pozycja=iloscOsob-1;
if(pozycja<0) pozycja=0;
if(pozycja>pozycjaListy+(WYSWIETL_OSOB-1)) pozycjaListy=pozycja-(WYSWIETL_OSOB-1);
if(pozycjaWyswietlListeOsob(osoby,iloscOsob,pozycja,pozycjaListy);
WyswietlKomunikaty(iloscOsob);
unsigned char znak=getch();
switch(znak)
{
case 224://klawisze specjalne
znak=getch();
switch(znak)
{
case 72://strzałka w górę
pozycja-=1;
break;
case 80://strzałka w dół
pozycja+=1;
break;
case 82://klawisz insert
DodajOsobe(osoby,iloscOsob);
break;
case 83://klawisz delete
KasujOsobe(osoby,iloscOsob,pozycja);
break;
}
znak=0;
break;
case 10://CTRL+ENTER
if(iloscOsob>0) EdytujDaneOsoby(osoby[pozycja]);
break;
case 13://ENTER
if(iloscOsob>0) WyswietlDaneOsoby(osoby[pozycja]);
break;
case 27://ESC
koniec=true;
break;
}
}
return(0);
}
void DodajOsobe(OsobaST tablica[],int &aktualnaIloscElementow)
{
if(aktualnaIloscElementow {
tablica[aktualnaIloscElementow]=WczytajOsobe();
aktualnaIloscElementow+=1;
}
return;
}
OsobaST WczytajOsobe(void)
{
OsobaST tOsoba;
clrscr();
cout<<"Podaj imie: ";
getline(cin,tOsoba.imie);
cout<<"Podaj nazwisko: ";
getline(cin,tOsoba.nazwisko);
cout<<"Podaj telefon: ";
getline(cin,tOsoba.telefon);
return(tOsoba);
}
void WyswietlListeOsob(OsobaST tablica[],int iloscElementow,int zaznaczElement,int wyswietlOdElementu)
{
int max=wyswietlOdElementu+WYSWIETL_OSOB;
if(max>iloscElementow) max=iloscElementow;
for(int i=wyswietlOdElementu;i {
if(i==zaznaczElement)
{
textbackground(12);
textcolor(14);
}else
{
textbackground(0);
textcolor(7);
}
gotoxy(1,3+i-wyswietlOdElementu);
for(int j=0;j<60;j++) cout<<" ";
gotoxy(1,wherey());
cout<<(i+1);
gotoxy(10,wherey());
cout<gotoxy(30,wherey());
cout< }
return;
}
void WyswietlDaneOsoby(OsobaST osoba)
{
clrscr();
cout<<"Imie wybranej osoby to: "< cout<<"Nazwisko wybranej osoby to: "< cout<<"Telefon kontaktowy do tej osoby to: "< cout< cout<<"Nacisnij dowolny klawisz aby powrocic do listy osob."< getch();
return;
}
void KasujOsobe(OsobaST tablica[],int &iloscElementow,int indeksDoSkasowania)
{
if(iloscElementow>0)
{
int minx=20;
int miny=11;
int maxx=80-(minx-1);
int maxy=25-(miny-1);
textcolor(15);
textbackground(1);
for(int y=miny;y<=maxy;y++)
{
gotoxy(minx,y);
for(int x=minx;x<=maxx;x++) cout<<" ";
}
gotoxy(minx+1,miny);
textcolor(15);
cout<<"Imie: ";
textcolor(14);
cout<gotoxy(minx+1,miny+1);
textcolor(15);
cout<<"Nazwisko: ";
textcolor(14);
cout<gotoxy(minx+1,miny+2);
textcolor(15);
cout<<"Telefon: ";
textcolor(14);
cout<gotoxy(minx+1,maxy);
textcolor(15);
cout<<"Czy chcesz usunac ten wpis? (T/N)";
unsigned char tOdpowiedz=0;
do
{
tOdpowiedz=getch();
if(tOdpowiedz==224)//znak specjalny
{
getch();
tOdpowiedz=0;
}
}while((tOdpowiedz!='t')&&(tOdpowiedz!='T')&&(tOdpowiedz!='N')&&(tOdpowiedz!='n'));
//kasowanie:
if((tOdpowiedz=='t')||(tOdpowiedz=='T'))
{
for(int i=indeksDoSkasowania+1;iiloscElementow-=1;
}
}
return;
}
void EdytujTekst(string &fTekst)
{
string tNowy;
getline(cin,tNowy);
if(tNowy.length()==0)
{
cout<<"Nie wprowadzono zmian!"< }else
{
fTekst=tNowy;
cout<<"Wprowadzono zmiany!"< }
}
void EdytujDaneOsoby(OsobaST &fOsoba)
{
clrscr();
cout<<"Pole, ktore zostawisz puste nie zostanie zmienione."< cout<<"Podaj nowe imie (stare: \""< EdytujTekst(fOsoba.imie);
cout<<"Podaj nowe nazwisko (stare: \""< EdytujTekst(fOsoba.nazwisko);
cout<<"Podaj nowy telefon (stary: \""< EdytujTekst(fOsoba.telefon);
return;
}
19.5.1. Podsumowanie
Jak widać przedstawiony program nie jest trudny. Zawiera on praktycznie rzecz biorąc podstawowe pojęcia, które do
tej pory omówiliśmy. Pamiętaj jednak o wstępnym projektowaniu programu i wypisaniu sobie istotnych celów, jakie program ma
spełniać. Jeśli tego nie zrobisz to jest bardzo mała szansa, że go skończysz. Staraj się również kończyć programy, które już są
blisko końca, a nie chce Ci się tego robić bo elementy, które zostały do napisania są proste, nudne i czasochłonne. Jeśli
nauczysz się tego złego nawyku nie kończenia własnych programów za kilka lat staniesz przed faktem takim, że kodujesz już
kilka lat, a nie masz nawet jednej pracy którą mógłbyś komuś pokazać. Kolejny problem jaki się pojawi to brak umiejętności
kończenia projektów - czyli brak sił, chęci i motywacji na wykonywanie elementów końcowych projektu do których nierzadko
trzeba poświęcić bardzo dużo czasu, którego Ci szkoda. Nie pozwalaj również na to, abyś był krytykowany. Jeśli już padnie
krytyka pod adresem Twojego programu jednym uchem wpuść, a drugim wypuść ją. Programy piszesz dla siebie i zbierasz
doświadczenie dla siebie. Niejeden, który krytykuje nie jest w stanie napisać czegokolwiek od początku do końca, pomimo, iż
rzekomo zna się na programowaniu. Bądz konstruktywny i wytrwały w działaniu. Oceniaj realnie szanse na wykonanie
programu przed jego rozpoczęciem. Szanse na ukończenie programu są wtedy, gdy posiadasz większość niezbędnej wiedzy do
zakodowania rzeczy o których jeszcze nie masz pojęcia. Nie masz natomiast szans, gdy nie umiesz dobrze posługiwać się
językiem programowania i gdy nie masz pojęcia o rzeczach które będziesz pisał.
19.5.2. Trening dodatkowy
Jeśli napisałeś cały program i chciałbyś jeszcze go trochę rozbudować, proponuję dodać obsługę klawiszy home, end,
pgup i pgdn. W jednym z dalszych rozdziałów zapoznasz się z obsługą plików. Będziesz mógł wtedy również rozszerzyć
program o możliwość zapisywania danych. Nie przeskakuj jednak odrazu do rozdziału poświęconego plikom. Przerób najpierw
materiał znajdujący się wcześniej. Umieszczenie obsługi plików tak daleko ma swój cel, pomimo iż sama ich obsługa nie jest
trudna.
19.5.3. Archiwum projektów skończonych
Utwórz sobie na dysku katalog z programami, które ukończyłeś. Kopiuj tam całe pliki z kodami zródłowymi. Mieć
zebrane ukończone prace w jednym miejscu to bardzo dobra sprawa.
XX. Wskazniki
20.1. Odczytywanie adresu pamięci istniejących zmiennych
Język C++ w bardzo łatwy sposób umożliwia nam pobieranie adresu pamięci wybranych zmiennych. Wskaznik
zajmuje zazwyczaj 4 bajty bez względu na jaki typ danych wskazuje. Rozmiar wskaznika może być jednak różny w zależności
od użytego kompilatora (np. gdy użyjemy 64 bitowego kompilatora). Wskaznik zwraca adres pierwszego bajta danych wybranej
zmiennej. Aby pobrać adres dowolnej zmiennej wystarczy napisać: &nazwa_zmiennej.
#include
#include
using namespace std;
int main()
{
int zmienna1=213;
int tablica[]={1,2,3,4,5,6,7,8,9,10};
struct
{
int liczba;
long long duzaLiczba;
}struktura;
cout<<"Adres zmienna1="<<&zmienna1<cout<<"Adres tablica="<<&tablica<cout<<"Adres tablica[0]="<<&tablica[0]<cout<<"Adres tablica[1]="<<&tablica[1]<cout<<"Adres struktura="<<&struktura<cout<<"Adres struktura.liczba="<<&(struktura.liczba)<cout<<"Adres struktura.duzaLiczba="<<&(struktura.duzaLiczba)<getch();
return(0);
}
Zauważmy, że wskaznik ze zmiennej tablica i ze zmiennej tablica[0] jest taki sam. Dzieje się tak dlatego, że
wskaznik ze zmiennej tablica wskazuje na początek wszystkich danych w tablicy, a pierwszym elementem jest tablica[0]. To
samo dotyczy adresu zmiennej struktura i struktura.liczba. Adresy zmiennych są wyświetlane w postaci szesnastkowej.
20.2. Deklaracja zmiennej wskaznikowej
Deklaracja zmiennej wskaznikowej jest również prosta. Aby utworzyć zmienną wskaznikową, to po typie zmiennej
dopisujemy *. Tak więc, jeśli chcemy utworzyć wskaznik, który ma wskazywać na liczbę typu long long, zapis ten będzie
wyglądał tak:
long long* wskaznik;
20.3. Zapisywanie adresu zmiennej do wskaznika
Aby przypisać adres zmiennej do wskaznika wystarczy napisać:
long long zmienna;
long long* wskaznik=&zmienna;
20.4. Wyświetlanie adresu wskaznika
Jeśli wypiszemy teraz wartość zmiennej wskaznik, otrzymamy liczbę wyświetloną szesnastkowo.
#include
#include
using namespace std;
int main()
{
long long zmienna=213;
long long* wskaznik=&zmienna;
cout<<"&zmienna="<<&zmienna< cout<<"wskaznik="< getch();
return(0);
}
Jak pokazuje ten przykład i jak można było się tego spodziewać, wartość wskaznika jest taka, jaką do niego zapisaliśmy.
20.5. Wyświetlanie danych, na które wskazuje adres wskaznika
Aby wyświetlić dane jakie znajdują się pod adresem jaki mamy zapisany we wskazniku, musimy przed nazwą
zmiennej dopisać *. Tak więc, modyfikując poprzedni program, będzie to wyglądało tak:
#include
#include
using namespace std;
int main()
{
long long zmienna=213;
long long* wskaznik=&zmienna;
cout<<"zmienna="< cout<<"*wskaznik="<<*wskaznik< getch();
return(0);
}
20.6. Modyfikacja danych, na które wskazuje wskaznik
Mając zapisany adres do zmiennej we wskazniku, mamy możliwość zmiany wartości zmiennej nie używając nazwy
zmiennej, z której pobraliśmy adres. Przykład:
#include
#include
using namespace std;
int main()
{
long long zmienna=213;
long long* wskaznik=&zmienna;
cout<<"zmienna="< *wskaznik=50;
cout<<"zmienna="< getch();
return(0);
}
20.7. Dostęp do danych struktury za pośrednictwem wskaznika
Jeśli chcemy odczytać lub zapisać dane do struktury za pomocą wskaznika wskazującego na nią, postępujemy
prawie tak samo jak w przypadku zwykłej zmiennej - poprzedzamy wskaznik znakiem *. Wskaznik ten musimy jednak umieścić
w okrągłe nawiasy, żeby kompilator wiedział czego się tyczy symbol *. Kolejny przykład:
#include
#include
using namespace std;
int main()
{
struct daneST
{
int liczba;
char znak;
};
daneST dane;
dane.liczba=55;
dane.znak='a';
daneST* wskaznik=&dane;
cout<<"(*wskaznik).liczba="<<(*wskaznik).liczba< (*wskaznik).liczba=99;
cout<<"dane.liczba="< getch();
return(0);
}
20.8. Wskazniki i struktury po raz drugi
Oprócz przedstawionej wyżej metody uzyskiwania dostępu do danych istnieje również drugi, który jest równoważny
pierwszemu. Jest on moim zdaniem wygodniejszy w użyciu, jednak chciałem pokazać Ci różne zapisy ponieważ starsi
programiści, którzy 'przesiedli' się z C na C++ korzystają zazwyczaj z pierwszego zapisu. Zamiast poprzedzać zmienną
wskaznikową gwiazdką i wstawiać ją w nawiasy, wystarczy kropkę zastąpić zapisem takim zapisem: ->. Przykład z
poprzedniego podrozdziału ze zmodyfikowanym zapisem przedstawiam poniżej.
#include
#include
using namespace std;
int main()
{
struct daneST
{
int liczba;
char znak;
};
daneST dane;
dane.liczba=344;
dane.znak='a';
daneST* wskaznik=&dane;
cout<<"wskaznik->liczba="<liczba< wskaznik->liczba=221;
cout<<"dane.liczba="< getch();
return(0);
}
20.9. Podsumowanie
Przeanalizuj dokładnie cały materiał, jaki znalazł się w tym rozdziale. Dobra znajomość całej teorii o wskaznikach będzie
niezbędna, gdy dojdziesz do rozdziału poświęconemu dynamicznemu zarządzaniu pamięcią.
Laboratorium 6
XXI. Przestrzenie nazw
21.1. Składnia przestrzeni nazw
Do tej pory mieliśmy okazję tylko korzystać z przestrzeni nazw takich jak: std:: i ddt::console::. Teraz pokażę Ci, jak
się tworzy własne przestrzenie nazw. Składnia jest bardzo prosta i wygląda następująco:
namespace twoja_nazwa
{
//tutaj możemy tworzyć funkcje, struktury i zmienne
}
21.1.1. Przykład
#include
#include
using namespace std;
namespace jakasNazwa
{
struct daneST
{
int liczba;
char znak;
};
int dodaj(int a,int b)
{
return(a+b);
}
}
int main()
{
jakasNazwa::daneST dane;
dane.liczba=344;
dane.znak='a';
cout<<"dane.liczba="< cout<<"dane.liczba+22="< {
using namespace jakasNazwa;
daneST zmienna;
zmienna.liczba=13;
cout<<"zmienna.liczba="<cout<<"zmienna.liczba+43="< }
getch();
return(0);
}
XXII. Biblioteka
22.1. Biblioteka do obsługi czasu
Jeśli chcemy odczytać aktualny czas na swoim komputerze, policzyć różnicę czasu lub skorzystać z innych operacji
związanych z czasem musimy skorzystać w tym celu z biblioteki time.h. Funkcje są proste w użyciu, jednak wymagana jest tu
już znajomość rozdziału poświęconego wskaznikom.
22.2. Podstawowe funkcje obsługi czasu
Niniejszy rozdział nie został jeszcze napisany. Dokumentację niniejszej biblioteki możesz znalezć pod adresem:
http://www.cplusplus.com/reference/clibrary/ctime/.
XXIII. Obsługa plików
23.1. Biblioteka odpowiedzialna za obsługę plików
Pisząc nasze programy, prędzej czy pózniej zajdzie potrzeba zapisywania danych na dysku. Z pomocą przychodzi tu
biblioteka fstream, dzięki której uzyskujemy funkcje pozwalające nam zarówno zapisywać pliki jak i je odczytywać.
23.2. Typ zmiennej fstream
Zanim zaczniemy odczytywać, bądz zapisywać dane z/do pliku, musimy posiadać zmienną, dzięki której będziemy
mogli wykonywać operacje na wybranym pliku. W tym celu utworzona została klasa fstream. Klasa ta jest umieszczona w
przestrzeni nazw std::. Klasa ta udostępnia nam cały interfejs, dzięki któremu będziemy mogli obsłużyć dowolny plik znajdujący
się na dysku lub innym nośniku danych.
std::fstream plik;
23.3. Otwieranie pliku
Zmienna, którą utworzyliśmy aktualnie nie wskazuje na żaden plik. Aby przypisać konkretny plik do zmiennej
wywołujemy funkcję open(), której definicja wygląda następująco:
void open(const char* nazwa_pliku,ios_base::openmode tryb_otwarcia_pliku);
Pierwszy parametr funkcji (nazwa_pliku) określa ścieżkę dostępu i nazwę pliku do jakiego chcemy uzyskać dostęp. Drugi
parametr funkcji, czyli tryb_otwarcia_pliku służy do poinformowania kompilatora w jakim trybie dany plik chcemy otworzyć.
Lista dostępnych trybów wraz z opisami w poniższej tabeli.
Tryb Opis trybu
ios::app (append - dopisywanie danych do pliku) Ustawia wewnętrzny wskaznik zapisu pliku na jego koniec. Plik otwarty w
trybie tylko do zapisu. Dane mogą być zapisywane tylko i wyłącznie na końcu pliku.
ios::ate (at end) Ustawia wewnętrzny wskaznik pliku na jego koniec w chwili otwarcia pliku.
ios::binary (binary) Informacja dla kompilatora, aby dane były traktowane jako strumień danych binarnych, a nie jako strumień
danych tekstowych.
ios::in (input - wejście/odczyt) Zezwolenie na odczytywanie danych z pliku.
ios::out (output - wyjście/zapis) Zezwolenie na zapisywanie danych do pliku.
ios::trunc (truncate) Zawartość pliku jest tracona, plik jest obcinany do 0 bajtów podczas otwierania.
Wszystkie wymienione tryby możemy łączyć ze sobą - oznacza to, że jeśli chcemy otrzymać plik do odczytu i zapisu
wystarczy oddzielić je pojedynczym operatorem |.
std::fstream plik;
plik.open("nazwa_pliku.txt",std::ios::in|std::ios::out);
23.3.1. Czy udało otworzyć się plik?
Zdecydowana większość kursów pomija ten bardzo ważny krok, jaki należy implementować we własnych kodach
zródłowych - sprawdzanie czy plik został otwarty prawidłowo. Operacja jest bardzo prosta, jednak prawie zawsze zaniedbywana
nawet w książkach poświęconych programowaniu!
Po wykonaniu operacji otwarcia pliku, wewnątrz klasy ustawiane są odpowiednie flagi, które informują o tym, czy otrzymaliśmy
dostęp do pliku czy też nie. Funkcje, jakie umożliwiają nam sprawdzenie tego stanu to good() oraz is_open(). Definicja tych
funkcji wygląda następująco:
bool good();
bool is_open();
Obie funkcje zwrócą wartość true, jeśli uzyskano dostęp do pliku, w przeciwnym wypadku otrzymamy wartość false.
std::fstream plik;
plik.open("nazwa_pliku.txt",std::ios::in|std::ios::out);
if(plik.good())==true)
{
std::cout<<"Uzyskano dostep do pliku!"< //tu operacje na pliku
}else std::cout<<"Dostep do pliku zostal zabroniony!"<23.3.2. Kiedy nie uzyskamy dostępu do pliku
Próba odczytu:
" Plik nie istnieje na dysku;
" Nie posiadamy uprawnień odczytu do pliku.
Próba zapisu:
" Nie posiadamy uprawnień pozwalających nam modyfikować plik;
" Nie posiadamy uprawnień do katalogu w którym chcemy utworzyć plik;
" Nośnik, na którym chcemy dokonać zapisu jest tylko do odczytu.
23.4. Zamykanie pliku
Każdy plik należy zamykać po zakończeniu pracy z nim. Jeśli plik ma być używany tylko przez jednego użytkownika
szkodliwość jest stosunkowo mała - klasa fstream sama zamknie plik przed usunięciem zmiennej z pamięci. Jeśli natomiast
zapomnisz zamknąć plik, którego dane mają być współdzielone przez kilku użytkowników, automatycznie uniemożliwisz im
dostęp do tego zasobu. Funkcja odpowiedzialna na zamykanie pliku nosi nazwę close(). Deklaracja wygląda następująco:
void close(void);
23.4.1. Przykład
Poniższy przykład pokazuje jak należy prawidłowo posługiwać się otwartym plikiem.
#include
int main()
{
std::fstream plik;
plik.open("nazwa_pliku.txt",std::ios::in|std::ios::out);
if(plik.good())==true)
{
//tu operacje na pliku (zapis/odczyt)
plik.close();
}
return(0);
}
23.5. Odczytywanie danych z pliku
Jeśli uzyskamy już dostęp do pliku w trybie do odczytu, możemy rozpocząć odczytywanie danych z pliku. Język C++
oferuje więcej niż jedną metodę odczytu danych z pliku.
23.5.1. Pobieranie danych za pomocą strumienia
Pierwszą, a zarazem bardzo wygodną metodą odczytywania danych z pliku jest strumień. Ponieważ zapis jest
analogiczny do strumienia std::cin>>, przedstawiam tylko formę zapisu.
nazwa_zmiennej_plikowej>>zmienna_do_ktorej_dane_maja_zostac_zapisane;
Co należy wiedzieć o strumieniu:
" Dane odczytywane za pomocą strumienia są zawsze traktowane jako tekst, niezależnie czy podczas otwierania
użyliśmy trybu ios::binary czy nie.
" Strumień działa analogicznie do std::cin>>, co w konsekwencji oznacza, że za pomocą tej funkcji nie odczytamy
żadnej informacji o białych znakach (tj. enter, tabulacja, spacja itp).
23.5.2. Pobieranie danych wierszami
Kolejną metodą na odczytanie danych, to użycie funkcji geline(). Funkcja ta została omówiona w rozdziale XVIII.
Biblioteka . Przykład:
std::fstream plik("nazwa_pliku.txt",std::ios::in);//zakładamy, że plik istnieje
std::string dane;
getline(plik,dane);//wczytanie CAAEGO jednego wiersza danych
Istnieje również druga funkcja służąca do wczytywania danych wierszami, jednak wydaje się ona mniej wygodna w użyciu. Jest
nią funkcja geline(), zaszyta wewnątrz klasy fstream.
istream& getline (char* odczytane_dane, streamsize ilosc_danych, char znak_konca_linii);
Parametry oznaczają kolejno:
" (odczytane_dane) wskaznik zmiennej, do której mają zostać wczytane dane z pliku;
" (ilosc_danych) maksymalna ilość znaków jakie mogą zostać zapisane do zmiennej;
" (znak_konca_linii) parametr jest opcjonalny. Umożliwia zmianę znaku końca linii.
Przykład wykorzystania tej funkcji:
std::fstream plik("nazwa_pliku.txt",std::ios::in);//zakładamy, że plik istnieje
char dane[255];
plik.getline(dane,255);//wczytanie jednego wiersza danych (lub częśći wiersza jeśli sie nie zmieści)
Co należy wiedzieć o obu funkcjach getline():
" Dane odczytywane za pomocą funkcji getline() są zawsze traktowane jako tekst, niezależnie czy podczas otwierania
użyliśmy trybu ios::binary czy nie.
23.5.3. Pobieranie danych blokami
Pobieranie danych blokami jest jedną z najszybszych metod na odczytywanie danych. Co więcej jest to bezpieczna
metoda dla danych binarnych (pod warunkiem włączenia trybu ios::binary). Minusem tej metody jest niezbyt poręczna forma w
jakiej otrzymujemy dane. Budowa tej funkcji wygląda następująco:
istream& read(char* bufor, streamsize rozmiar_bufora);
Pierwszym parametrem przekazywanym do funkcji jest wskaznik do którego mają zostać wczytane dane. Drugi parametr
określa rozmiar bufora. Pamiętaj, że bufor może nie być wypełniony do końca danymi. Aby sprawdzić ile bajtów danych zostało
faktycznie wczytanych do bufora, należy posłużyć się tu funkcją gcount(). Przykład:
std::fstream plik("nazwa_pliku.txt",std::ios::in);//zakładamy, że plik istnieje
char bufor[1024];
plik.read (bufor,1024);//wczytuje tyle danych ile się zmieści do bufora
std::cout<<"Wczytano "<Co należy wiedzieć o funkcji read():
" Dane odczytywane za pomocą funkcji read() są traktowane jako dane binarne, jeśli użyliśmy trybu ios::binary.
23.5.4. Inne metody na odczytywanie danych
C++ oferuje również inne metody odczytywania danych. Nie będą one jednak tu omówione ponieważ te, które zostały
poruszone w tym rozdziale są wystarczające do pełnego wykorzystywania możliwości wczytywania danych z plików.
23.6. Zapisywanie danych do pliku
Zapisywanie danych do pliku jest równie proste jak ich odczytywanie. Po otwarciu pliku do zapisu możemy korzystać
z kilku technik umożliwiających zapisywanie danych. Zanim jednak je poznasz musisz zdać sobie sprawę, że dane do pliku
można albo dopisywać tylko i wyłącznie na końcu pliku albo nadpisywać dane jeśli nie jesteśmy na jego końcu. Nie można
dopisywać tekstu pomiędzy istniejące dane jak to często robimy w edytorach tekstowych. Pamiętaj więc, jeśli chcesz otworzyć
plik do zapisu zastanów się conajmniej dwa razy, bo tutaj błąd może kosztować nawet utratę całej zawartości pliku. Jeśli chcesz
testować działanie funkcji służących do zapisu danych polecam utworzyć najpierw pusty plik i wpisać ręcznie do niego jakieś
nieistotne dane lub pracować na kopii pliku, zawierającego ważne dane. W razie wykonania jakiegoś rażącego błędu będziesz
mógł przywrócić szybko dane.
23.6.1. Zapisywanie danych za pomocą strumienia
Zapisywanie danych za pomocą strumienia jest analogicznym działaniem do std::cout<<. Jedyną istotną różnicą,
jaka ma tu miejsce to fakt, że wyjściem jest teraz plik, a nie konsola.
nazwa_zmiennej_plikowej<Tak samo jak w przypadku odczytywania danych za pomocą strumienia, zapisywane dane tą techniką są zawsze traktowane
jako tekst niezależnie od ustawienia trybu ios::binary. Każdorazowe zapisanie danych powoduje przesunięcie wskaznika o tyle
znaków ile zostało zapisanych do pliku.
23.6.2. Zapisywanie danych blokami
Gdy zapisywanie danych w postaci tekstu jest dla nas niewystarczające (a przy profesjonalnym podejściu do
większości projektów tak właśnie jest) z pomocą przychodzi nam kolejna funkcja klasy fstream i jest to write(). Definicja tej
funkcji wygląda następująco:
ostream& write(const char* bufor, streamsize ilosc_danych_do_zapisu );
Pierwszy parametr (bufor) to wskaznik bufora, w którym znajdują się dane jakie chcemy zapisać do pliku. Drugim parametrem
(ilosc_danych_do_zapisu) informujemy kompilator ile danych ma zostać zapisanych do pliku z bufora. Wraz z wykonaniem tej
operacji wskaznik wewnętrzny pliku przesuwa się do przodu o ilość bajtów zapisanych do pliku.
std::fstream plik("nazwa_pliku.txt",std::ios::out);//zakładamy, że nie wystąpił błąd (plik otwarto/utworzono)
std::string napis;
getline(std::cin,napis);
plik.write (&napis[0],napis.length());//zapisuje dane poczynając od 0 indeksu
23.6.3. Zapisywanie danych w szczegółach
Jeśli napiszesz sobie program, który będzie zapisywał do pliku wczytywane wiersze z klawiatury aż do napotkania
pustego wiersza pewnie zauważysz, że rozmiar pliku się nie zmienia zaraz po dopisaniu danych. Dzieje się tak dlatego, że
klasa fstream ma wewnętrzny bufor, który ma na celu przyśpieszenie operacji dyskowych. Każdorazowy dostęp do wybranego
obszaru dysku wymaga bardzo dużego czasu w porównaniu do szybkości pamięci podręcznej. Dane zanim trafią na dysk są
umieszczane najpierw w buforze, a następnie gdy bufor się zapełni zostają zapisywane na dysk. Dzięki takiemu podejściu do
zapisywania danych w pliku proces jest dużo szybszy. Przykładowo, jeśli jednorazowe ustawienie głowicy dysku na określonej
pozycji zajmuje np. 2ms, to zapisanie długiego zdania znak po znaku zajęłoby: 2ms*ilość_znaków czasu. Wbudowany system
buforowania danych zamiast zapisywać tak często dane, zapisze je najpierw do bufora, a pózniej wyśle je na dysk
oszczędzając jednocześnie mnóstwo zasobów sprzętowych komputera. System ten jest zawsze sprawny niezależnie od tego
czy skaczesz po pliku w różne miejsca, czy dopisujesz stale dane na jego końcu.
23.6.4. Kontrola bufora zapisu
Klasa fstream umożliwia nam 'kontrolowanie' wewnętrznego bufora zapisu. Cała ta kontrola sprowadza się do
zmuszenia klasy fstream, aby zapisała całą obecną zawartość bufora na dysk bez względu na to czy jest on zapełniony czy nie.
W tym celu utworzono funkcję flush(). Poniżej zamieszczam przykład demonstrujący użycie tej funkcji.
#include
using namespace std;
int main ()
{
fstream plik("plik.txt",ios::out);
if(plik.good())
{
for(int i=1;i<=100;i++)
{
plik<plik.flush();
}
plik.close();
}
return(0);
}
Pamiętaj jednak, że takie zapisywanie danych jak tu zostało zaprezentowane nie jest wydajne. Funkcja
flush() pomimo iż wydaje się w obecnym świetle dla Ciebie bezużyteczna znajduje ona swoje praktyczne zastosowanie
chociażby w serwerach profesjonalnych baz danych.
23.7. Poruszanie się po pliku z danymi
Do tej pory odczytywaliśmy (zapisywaliśmy) dane z (do) pliku zawsze od tego miejsca na którym skończyliśmy operację
odczytu (zapisu) ostatnim razem. Taka forma odczytu i zapisu danych jest bardzo wygodna, jednak czasem zachodzi potrzeba
poruszania się po pliku w bardziej nietypowy sposób. Z pomocą przychodzą tu funkcje seekg() i seekp(). Obie funkcje służą do
ustawiania nowej pozycji wewnętrznego wskaznika pliku. Jest jednak między nimi jedna zasadnicza różnica:
" seekg() ustawia wewnętrzny wskaznik pliku dla funkcji odczytujących dane;
" seekp() ustawia wewnętrzny wskaznik pliku dla funkcji zapisujących dane.
Parametry obu tych funkcji są analogiczne:
istream& seekg (streamoff offset, ios_base::seekdir kierunek);
ostream& seekp (streamoff offset, ios_base::seekdir kierunek);
Pierwszy parametr (offset) to przesunięcie, które informuje o ile bajtów ma zostać przesunięty wewnętrzny wskaznik pliku.
Drugi parametr (kierunek) jest opcjonalny i informuje klasę fstream względem czego ma zostać dokonane przesunięcie
wskaznika. Domyślną wartością, jaka jest przyjmowana za zmienną kierunek, to ios_base::beg. Kierunki jakie mamy do
wyboru to:
Kierunek Opis
ios_base::beg Przesunięcie względem początku pliku (domyślne)
ios_base::cur Przesunięcie względem aktualnej pozycji
ios_base::end Przesunięcie względem końca pliku
23.7.1. Odczytywanie aktualnej pozycji wewnętrznego wskaznika pliku
Jeśli będziemy mieli potrzebę odczytania aktualnej pozycji wewnętrznego wskaznika pliku, możemy to zrobić za pomocą funkcji
tellg() i tellp(). Definicja obu funkcji wygląda następująco:
streampos tellg();
streampos tellp();
Obie funkcje wyglądają tak samo, różnią się jednak działaniem.
" Funkcja tellg() zwraca aktualną pozycję wewnętrznego wskaznika pliku od której będzie następowało wczytywanie
danych z pliku.
" Funkcja tellp() zwraca aktualną pozycję wewnętrznego wskaznika pliku od której będzie następowało zapisywanie
danych do pliku.
23.7.2. Gdy wyjdziemy poza zasięg pliku
Aby sprawdzić, czy skok na nową pozycję zakończył się sukcesem możemy dokonać tego na dwa sposoby:
" Sprawdzić aktualną pozycję pliku i porównać z tą, którą chcieliśmy otrzymać;
" Wywołać funkcję fail(), należącą do klasy fstream.
Pierwsza metoda jest logiczna, druga wymaga krótkiego omówienia. Definicja funkcji fail() wygląda następująco:
bool fail();
Jeśli wystąpi błąd podczas wykonywania skoku (i nie tylko skoku) funkcja ta zwróci wartość true informując nas, że ostatnia
operacja na pliku nie powiodła się. Przykładowo:
std::fstream plik("plik.txt",std::ios::in);//zakładamy, że plik udało się otworzyć
plik.seekg(+2,std::ios_base::end);//skok do przodu o 2 względem końca pliku
if(plik.fail()) std::cout<<"Error! Nie udalo sie przesunac wewnetrznego wskaznika pliku"<23.8. Pozostałe funkcje, wykorzystywane podczas pracy z plikami
Ostatnią funkcją, jaką chciałbym omówić jest eof(). Funkcja ta służy do sprawdzania, czy wskaznik pliku znajduje się na końcu
pliku. Definicja funkcji:
bool eof();
Funkcja zwróci wartość true wtedy, gdy nie będzie już w pliku więcej danych do odczytu. Dzięki tej funkcji możemy w
bardzo łatwy sposób odczytać zawartość całego pliku. Poniższy przykład wyświetli zawartość całego pliku na ekran konsoli.
#include
#include
#include
using namespace std;
int main()
{
fstream plik;
plik.open("dane.txt",ios::in);
if(plik.good())
{
string napis;
cout<<"Zawartosc pliku:"< while(!plik.eof())
{
getline(plik,napis);
cout< }
plik.close();
}else cout<<"Error! Nie udalo otworzyc sie pliku!"< getch();
return(0);
}
23.8.1. Dokumentacja
Dobrym zródłem dostępnych funkcji w klasie fstream jest dokumentacja. Dokumentację dla klasy fstream znajdziesz pod
adresem: http://www.cplusplus.com/reference/iostream/fstream/
XXIV. Dynamiczne zarządzanie pamięcią new i delete
24.1. Ograniczanie maksymalnego rozmiaru danych odchodzi w zapomnienie
Do tej pory pisząc programy w których organizowałeś dane, byłeś zmuszany do określania górnej granicy danych,
jakie może pomieścić Twój program. Takie ograniczanie bardzo często nie jest jednak komfortowe i skuteczną alternatywą jest
tu dynamiczne zarządzanie pamięcią.
W języku C do przydzielania i zwalniania pamięci służyły głównie funkcje malloc() i free(). Korzystanie z nich było i
jest nadal bardzo popularne, jednak w C++ zostały one zastąpione operatorami new i delete.
24.2. Dynamiczne przydzielenie pamięci
W języku C++ do przydzielania nowego bloku pamięci służy operator new. Jego składnia wygląda następująco:
wskaznik1=new typ_zmiennej;
wskaznik2=new typ_zmiennej[ilosc_elementow_danego_typu];
Wskaznik jak już dowiedziałeś się w rozdziale, który był temu poświęcony wskazuje na dane, a sam najczęściej
zajmuje 4 bajty bez względu na to, na jakie dane wskazuje. Typ zmiennej informuje operator new, o rozmiarze pamięci jaka ma
zostać przydzielona. Jeśli chcemy aby nowo przydzielony blok był tablicą to aktualną składnię uzupełniamy o dodatkowy
parametr, w którym określamy ilość elementów tak samo jak robiliśmy to w przypadku tablic. Operator new na podstawie
wszystkich podanych informacji przydzieli odpowiednią ilość pamięci tak, aby na pewno zmieściła się taka ilość danych o którą
zażądałeś.
Jeśli przydział pamięci powiódł się, to wartość zmiennej wskaznik będzie różna od zera. Jeśli wartość wskaznika będzie
równa 0, to pamięć nie została przydzielona. Wartość 0 bardzo często jest zastępowana stałą NULL. Zalecane jest
jednocześnie korzystanie ze stałej NULL, ponieważ standardy związane z wartością 0 mogą się kiedyś zmienić, a w związku z
tym Twoje programy przestałyby działać. Powody, dla których pamięć nie mogła zostać przydzielona to:
" Rozmiar bloku pamięci, który chcesz zarezerwować jest zbyt duży;
" System nie posiada więcej zasobów pamięci i w związku z tym nie może Ci jej przydzielić.
Dostęp do danych za pomocą wskazników został już omówiony w rozdziale poświęconym wskaznikom i nie będzie tu
ponownie poruszany.
24.3. Zwalnianie pamięci przydzielonej dynamicznie
Zwalnianie pamięci przydzielonej dynamicznie jest jeszcze prostsze od jej przydziału i służy do tego operator delete.
Jeśli pamięć dla danych, na które wskazuje zmienna wskaznik została przydzielona bez parametru określającego ilość
elementów w tablicy, to usuwana jest następującą składnią:
delete wskaznik;
Jeśli natomiast przydzieliliśmy pamięć z użyciem parametru określającego ilość elementów tablicy to musimy
poinformować operator delete o tym, że wskaznik wskazywał na tablicę rekordów. Aby to zrobić dopisujemy zaraz za
operatorem nawiasy kwadratowe []. Nie podajemy jednak w nich rozmiaru tablicy, ponieważ operator ten sam ustala rozmiar
bloku jaki został przydzielony, a następnie go usuwa z pamięci. Składnia tej operacji wygląda następująco:
delete[] wskaznik_do_tablicy;
24.4. Kopiowanie bloków pamięci
Jeśli będziemy chcieli przekopiować zawartość pamięci z jednego miejsca do drugiego możemy zrobić to conajmniej
na dwa sposoby. Sposób pierwszy to wykorzystanie jakiejkolwiek pętli i kopiowanie danych bajt po bajcie. Przykład:
for(int i=0;iProblem jest w bardzo prosty sposób rozwiązany, jednak nie należy on do najwydajniejszych. Wydajniejszą metodą
jest wykorzystanie funkcji, która służy do kopiowania bloków pamięci. Jej definicja wygląda następująco:
void* memcpy(void* adres_docelowy,const void* adres_zrodlowy,size_t ilosc);
Jako pierwszy parametr (adres_docelowy) podajemy adres do pamięci pod którym mają się znalezć nowe dane.
Drugi parametr (adres_zrodlowy) to miejsce z którego dane mają zostać pobrane i również określamy je za pomocą adresu.
Trzecim, a zarazem ostatnim parametrem (ilosc) jest ilość bajtów, jaka ma zostać przekopiowana ze zródła do celu.
Kopiując małe bloki pamięci różnicy w szybkości działania programu nie zaobserwujesz, jednak gdy przyjdzie Ci kopiować kilka
MB danych różnice czasowe mogą być już bardzo odczuwalne.
24.5. Przykład
Przeanalizuj dokładnie działanie tego programu i poeksperymentuj z nim.
#include
#include
using namespace std;
int main()
{
int rozmiar=0;
int dlugosc=0;
char* tablica=NULL;
cout<<"Pusty wiersz konczy dzialanie programu."< for(int i=0;i<40;i++)cout<<"-";
cout< string tWiersz;
do
{
getline(cin,tWiersz);
if(tWiersz.length()>0)
{
tWiersz+="\r\n";//dopisanie nowego wiersza
if(dlugosc+tWiersz.length()+1>rozmiar)//potrzeba więcej pamięci niż jest dostępne
{
cout<<"Tworzy nowy blok pamieci!"<int tNarzutDanych=20;//jeśli ustawisz 0 to rezerwacja będzie się odbywała za każdym razem
rozmiar=tWiersz.length()+dlugosc+1+tNarzutDanych;//nowy rozmiar bloku
char* tNoweDane=new char[rozmiar];//rezerwacja nowego bloku pamięci, który pomieści stare i nowe dane
if(tablica!=NULL) memcpy(tNoweDane,tablica,dlugosc);//jeśli stara tablica istnieje to skopiuj dane do nowej tablicy
memcpy(&tNoweDane[dlugosc],&tWiersz[0],tWiersz.length());//skopiuj dane do nowej tablicy w wyznaczone miejsce
if(tablica!=NULL) delete[] tablica;//zwolnij pamięć zajmowaną przez stare dane
tablica=tNoweDane;//nadaj nowy wskaznik zmiennej tablica
}else
{//jest wystarczająca ilość pamięci nie wymagana rezerwacja
cout<<"Jest wystarczajaca ilosc miejsca!"<}
memcpy(&tablica[dlugosc],&tWiersz[0],tWiersz.length());//skopiuj dane do tablicy w wyznaczone miejsce
dlugosc=tWiersz.length()+dlugosc;//zapisz długość tekstu
tablica[dlugosc]=0;//oznacz miejsce końca tekstu w tablicy
}
}while (tWiersz.length()!=0);
if(tablica!=NULL)
{
cout<<"Dane jakie wypisales to: "<cout< delete[] tablica;
}else cout<<"Nie wpisales niczego!";
getch();
return(0);
}
Jeśli przeanalizowałeś przykład i go zrozumiałeś to zapewne stwierdziłeś, że taką funkcjonalność otrzymujesz korzystając z
klasy std::string. Masz rację, jednak przykład ma na celu zademonstrowanie Tobie praktycznego, dynamicznego zarządzania
pamięcią. Jeśli nie nauczysz się dynamicznie zarządzać pamięcią, możesz zapomnieć o realizowaniu jakiegokolwiek większego
projektu z którego będzie płynął jakiś większy użytek, niż domowe wykorzystywanie własnych programów. Jest co prawda
biblioteka szablonów, która umożliwia łatwe zarządzanie danymi jednak programista, który sam nie potrafi posługiwać się
prawidłowo operatorami new i delete (lub funkcjami malloc() i free()) jest tylko jego imitacją, z której żaden pracodawca nie
będzie miał pożytku.
24.6. Informacje dodatkowe
" Funkcji malloc() nie można stosować zamiennie z operatorem new.
" Funkcji free() nie można stosować zamiennie z operatorem delete.
" Zamiana operatora new na funkcję malloc() może spowodować nieprawidłowe funkcjonowanie programu - operator
new wykonuje czynności, które przy użyciu funkcji malloc() trzeba wywołać ręcznie.
" Jeśli dokonujesz zamiany funkcji malloc() na operator new, pamiętaj aby pozamieniać również funkcję free()
na operator delete (lub delete[] w zależności od sytuacji).
XXV. Dzielenie kodu na kilka plików zródłowych
25.1. Poznajemy dyrektywę #include na nowo
Wraz z rozwojem każdego programu kodu przybywa, a poruszanie się po nim staje się coraz bardziej uciążliwe ze
względu na jego długość. Z pewnością starasz się grupować tematycznie większość funkcji w programie, jednak i to niewiele
daje, gdy przychodzi pracować z kodem, który ma conajmniej 1000 wierszy. Z pomocą przychodzi tu dyrektywa #include, którą
już miałeś okazję niejednokrotnie poznać, stosując ją nawet do najprostszego programu.
Język C++ to zbiór logicznych zasad umożliwiających programowanie. Same zasady umożliwiają zarządzać danymi,
jednak nie są one wystarczające do tego, aby wykorzystywać w łatwy sposób możliwości sprzętowe komputera. Ponieważ język
C++ umożliwia łatwe organizowanie danych, to oczywistym też jest, że równie potrzebnym elementem jest interfejs,
umożliwiający prezentację danych oraz interfejs reagujący na wszystkie urządzenia, jakie posiadamy w komputerze (np. mysz,
klawiaturę, kartę graficzną, kartę sieciową itd.).
Zadaniem dyrektywy #include jest umożliwienie łatwego wykorzystywania zasobów sprzętowych komputera, poprzez
dołączanie plików nagłówkowych bibliotek, które są odpowiedzialne za komunikację z różnymi urządzeniami. Innymi słowy za
każdym razem, gdy korzystałeś z dyrektywy #include, dołączałeś do swojego programu interfejs umożliwiający łatwy dostęp do
wybranych zasobów komputera. Za pomocą tego polecenia będziesz mógł w łatwy sposób pisać serwery TCP/UDP, używać
OpenGL'a, czy też innych modułów umożliwiających łatwy dostęp do sprzętowych zasobów komputera.
Niniejsza dyrektywa pomimo, iż jest tak prosta w użyciu jest potężnym narzędziem w ręku programisty. Oprócz dołączania
istniejących bibliotek, pozwala Ci ona dołączać własne biblioteki, które wkrótce sam zaczniesz pisać. Dzięki tej własności
będziesz mógł zorganizować swój kod zródłowy lepiej, a wszelkie modyfikacje kodu staną się szybsze i łatwiejsze.
25.2. Ustawianie ścieżki do pliku nagłówkowego
Do tej pory gdy chciałeś dołączyć bibliotekę do programu, stosowałeś tylko i wyłącznie zapis #include <ścieżka do
pliku>. Użycie ostrych nawiasów <...> informuje kompilator, aby przeszukiwał domyślne ścieżki, w których znajdują się pliki
nagłówkowe. Ścieżki te są ustawione w edytorze Dev-C++, które wskazują na katalogi w których znajdują się wszystkie
standardowe biblioteki. Jeśli chcesz sprawdzić jakie ścieżki są domyślnie dołączane podczas kompilacji programów wejdz w
następujące ustawienia:
" wybierz: Narzędzia/Opcje kompilatora
" kliknij zakładkę: Katalogi
" w ramce która się pokazała, kliknij Pliki nagłówkowe C++
Lista którą widzisz, to zbiór katalogów, które są przeszukiwane w celu odnalezienia odpowiedniego pliku nagłówkowego.
Jeśli kompilator nie odnajdzie żądanego pliku nagłówkowego, kompilator zwróci błąd informując Cię, że pliku o podanej nazwie
nie znaleziono.
Drugą metodą na dołączanie plików nagłówkowych jest wykorzystanie zapisu #include "ścieżka do pliku". Zapis z użyciem
podwójnych apostrofów informuje kompilator, że plik nagłówkowy ma być poszukiwany tylko i wyłącznie względem aktualnego
katalogu.
Nie stosuj zapisów takich jak #include "C:/pliki nagłówkowe/nazwa_pliku.hpp", ponieważ kod automatycznie staje się
nieprzenośny. Nie wymuszaj na programiście miejsca przechowywania plików nagłówkowych, bo każdy programista ma własną
wizję na organizację danych na dysku, a Ty na pewno nie narzucisz jemu własnej wizji świata. Używaj raczej zapisów "../pliki
nagłówkowe/nazwa_pliku.hpp", które są czytelne i umożliwiają każdemu programiście trzymać pliki Twojego projektu w
dowolnym miejscu na dysku.
Kolejną ważna sprawą, jaką należy tu omówić to zapis ścieżki. Jak zauważyłeś wszystkie ścieżki do katalogów podaję
następująco: "katalog1/katalog2/katalog3/plik.hpp". Korzystanie ze slashy / w celu określenia katalogu docelowego jest
rozumiane przez wszystkie kompilatory bez względu na system operacyjny. Pomimo, iż Windows używa backslashy w celu
określenia ścieżki na dysku; używanie ich w C++ nie jest wskazane. Jeśli już koniecznie chcesz korzystać z backslashy musisz
zapisywać ścieżki następująco: "katalog1\\katalog2\\katalog3\\plik.hpp". Pamiętaj jednak, że taki zapis nie jest zalecany, więc
staraj się używać zapisu unixowego (ze slashami / ).
25.3. Rozszerzenia plików i ich znaczenie
Na przestrzeni lat język C, a od jakiegoś czasu również i C++ wypracowały sobie nazwy rozszerzeń dla plików, które
automatycznie sugerują co się w nich znajduje i w jakim standardzie były pisane.
25.3.1. Pliki *.h *.hpp
Pliki *.h i *.hpp, są nazywane plikami nagłówkowymi (ang. header files). Pierwszy z nich, tj. *.h oznacza, że był on
pisany zgodnie ze standardami języka C. Rozszerzenie *.hpp natomiast mówi nam, że program pisany był zgodnie ze
standardami języka C++. Jeśli masz kompilator C++ to nie musisz obawiać się o problemy z wykorzystywaniem bibliotek
pisanych zarówno w C jak i C++, ponieważ standard C++ powstał w oparciu o C. Problemy możesz mieć natomiast w sytuacji
odwrotnej, ponieważ mogą być zastosowane polecenia których język C po prostu nie zna.
Każdy plik nagłówkowy powinien zawierać tylko i wyłącznie interfejs. Przez słowo interfejs rozumiemy:
" definicje typów
" definicje funkcji
" definicje struktur
" definicje klas
" deklarację ewentualnych zmiennych globalnych
Dodatkowo dołączamy do niego niezbędne pliki nagłówkowe, jakie będą wykorzystywane przez daną bibliotekę. W pliku
nagłówkowym nie umieszczamy natomiast bloków funkcji. Można powiedzieć po prostu, że w pliku nagłówkowym umieszczamy
wszystko oprócz bloków funkcji.
25.3.2. Pliki *.c *.cpp
Pliki *.c i *.cpp nazywamy plikami zródłowymi. Jak nietrudno domyślić się, *.c oznacza standard użytego języka C,
natomiast *.cpp standard użytego języka C++. W plikach z takim rozszerzeniem umieszczamy tylko i wyłącznie deklaracje
funkcji, czyli nazwę funkcji razem z jej ciałem (czyli blokiem funkcji).
25.3.3. Inne rozszerzenia plików
Język C++ nie narzuca nazw dla rozszerzeń plików. Zalecane jest jednak stosowanie się do wymienionych
standardów, ponieważ są one jasne i zrozumiałe przez wszystkich programistów C i C++. Dodatkowo edytory często rozpoznają
typ pliku po rozszerzeniach i w zależności od nich kolorują Tobie składnię.
25.4. Budowa pliku nagłówkowego *.h *.hpp
Istnieje conajmniej kilka wersji budowy plików nagłówkowych dla języka C i C++. Niektóre z nich nie działają jednak
pod wszystkimi kompilatorami, dlatego też skupię się tylko i wyłącznie na jednej, która jest akceptowana przez wszystkie
kompilatory.
#ifndef nazwaPliku_hpp
#define nazwaPliku_hpp
/*
tutaj piszesz cały interfejs
*/
#endif
Opis użytych instrukcji preprocesora:
#ifndef Instrukcja preprocesora, która sprawdza czy zmienna preprocesora o podanej nazwie istnieje. Jeśli
zmienna_preprocesora zmienna preprocesora nie istnieje to wszystkie instrukcje, które się znajdują poniżej #ifndef, zostaną
wykonane. Jeśli natomiast zmienna będzie istniała, kompilator pominie wszystkie instrukcje jakie
znajdują się pomiędzy słowami preprocesora #ifndef, a #endif.
#endif instrukcja oznacza miejsce końca bloku warunkowego preprocesora.
#define nazwa_zmiennej Polecenie służy do tworzenia zmiennych preprocesora.
Zaprezentowany przykład czytasz następująco:
#ifndef dowolna_nazwa jeśli nie istnieje dowolna_nazwa, wykonuj blok
#define dowolna_nazwa utwórz zmienną o nazwie dowolna_nazwa
#endif koniec bloku warunkowego
Wykorzystując instrukcje preprocesora zabezpieczasz bibliotekę przed wielokrotnym dołączaniem tego samego kodu
do własnego programu. Jeśli go nie użyjesz, a dołączysz tą samą bibliotekę conajmniej dwukrotnie w programie (nawet w
różnych plikach), otrzymasz błąd kompilacji nawet jeśli wszystko będzie poprawnie napisane. Nazwy zmiennych, które
definiujesz za pomocą preprocesora muszą być unikatowe podczas kompilacji projektu dla każdego używanego pliku, tak więc
w każdym pliku musi się znajdować inna nazwa zmiennej preprocesora. Najpopularniejszą metodą zapewnienia sobie
unikatowych nazw zmiennych, jest używanie nazwy pliku dla zmiennej preprocesora. Nie jest to jednak obowiązek i masz tu
praktycznie pełną dowolność. Nazwy zmiennych preprocesora mają takie same kryteria dla nazewnictwa jak zmienne języka
C++.
25.5. Budowa pliku zródłowego *.c *.cpp
Plik zródłowy ma bardzo prostą budowę. Jedyne co musisz zrobić to dołączyć plik nagłówkowy pliku, który opisuje
interfejs jaki znajduje się w tym pliku.
#include "nazwaPliku.hpp"
/*
tutaj piszesz deklaracje funkcji
*/
25.6. Błędy kompilacji
Jeśli będziesz próbował skompilować plik *.cpp i nie będzie w nim żadnych błędów, otrzymasz następujący błąd
kompilacji:
[Linker error] undefined reference to `WinMain@16'
ld returned 1 exit status
Komunikat ten informuje Cię, że w programie nie ma 'ciała' programu, czyli głównej funkcji programu. Ponieważ pliki, które
utworzyłeś nie mają być programem, tylko zródłem dołączanym do programu, który będzie zawierał funkcję main(), wskazane
jest aby ten plik nie zawierał funkcji o którą 'doczepił się' kompilator.
Dołączając natomiast plik nagłówkowy (*.hpp) do programu głównego za pomocą dyrektywy #include, uzyskasz w pełni
sprawny kod, który się kompiluje (pod warunkiem, że nie ma w nim innych błędów).
25.7. Przykład
25.7.1. Pliki zródłowe
//Plik: main.cpp
#include
#include
#include "nazwaPliku.hpp"
using namespace std;
int main()
{
cout<<"Wynik dodawania to: "< getch();
return(0);
}
//Plik: nazwaPliku.hpp
#ifndef nazwaPliku_hpp
#define nazwaPliku_hpp
int dodajLiczby(int a,int b);
#endif
//Plik: nazwaPliku.cpp
#include "nazwaPliku.hpp"
int dodajLiczby(int a,int b)
{
return(a+b);
}
25.7.2. Problemy z kompilacją
Jeśli otworzysz teraz plik main.cpp i będziesz chciał go skompilować, otrzymasz następujący błąd:
[Linker error] undefined reference to `dodajLiczby(int, int)'
ld returned 1 exit status
Kompilator poinformował Ciebie, że nie zna ciała funkcji, którą chcesz wywołać. Ponieważ Twój program zawiera więcej niż
jeden plik, musisz utworzyć projekt, który umożliwi Ci skompilowanie programu.
25.7.3. Tworzenie projektu
Aby utworzyć projekt dla tego programu musisz wykonać następujące kroki:
" Kliknij: Plik/Nowy/Projekt...
" W zakładce Basic, wybierz projekt zatytułowany Empty Project
" W polu tekstowym, podaj nazwę dla projektu, np. Pierwszy projekt
" Zaznacz pole Projekt C++
" Naciśnij OK
" Wybierz ścieżkę, gdzie ma zostać utworzony plik projektu i naciśnij Zapisz
Projekt został utworzony. Po lewej stronie edytora Dev-C++ powinieneś widzieć przeglądarkę klas. Jeśli jej nie widzisz,
wybierz: Widok, a następnie kliknij Przeglądarka projektu/Klas. Następnie kliknij zakładkę Projekt w przeglądarce
projektu/klas.
Na samej górze przeglądarki projektu/klas powinieneś widzieć nazwę utworzonego przez Ciebie projektu. Aby dodać do
projektu pliki, kliknij prawym klawiszem na nazwie projektu, który utworzyłeś, a następnie wybierz opcję Dodaj do projektu.
Dodaj wszystkie trzy pliki do projektu, a następnie skompiluj projekt (robi się to tak samo jak dla zwykłych plików).
Jeśli wszystko wykonałeś poprawnie, program powinien się skompilować poprawnie. W przeciwnym wypadku sprawdz czy
jakiegoś kroku nie pominąłeś lub zle nie wykonałeś.
Jeśli nadal masz problemy z uruchomieniem programu, przejdz do rozdziału Dev-C++, a projekty, który dokładnie opisuje
wykorzystywanie projektów.
XXVI. Dev-C++, a projekty
26.1. Co to jest projekt
Polskie słowo projekt, które jest bardzo podobne w pisowni do angielskiego wyrazu project uważane jest przez
przeciętnego śmiertelnika za wyrazy o takim samym znaczeniu. Co więcej, przeciętny śmiertelnik stawia znak równości
pomiędzy tymi wyrazami, co z punktu widzenia programisty jest niedopuszczalne. Znaczenie polskiego wyrazu projekt jest
wypaczone. W naszym kraju projektem nazywamy zarówno kilka kresek narysowanych podczas przerwy śniadaniowej, jak i
ogromne projekty, zawierające mnóstwo dodatkowych dokumentów, wchodzących w skład projektu. Za granicą, słowo
projekt dla pierwszego przedstawionego przypadku nie ma prawa bytu i co najwyżej takie działanie można nazywać
stworzeniem szkieletu do prac nad projektem.
26.2. Do czego służą projekty
W środowisku Dev-C++, projekt służy głównie do takich rzeczy jak:
" sprawne zarządzanie bibliotekami
" zarządzanie katalogami plików nagłówkowych
" konfigurowanie kompilatora na indywidualne potrzeby projektu
Właśnie na tych właściwościach się skupimy w dalszej części tego rozdziału. Zakładam, że wiesz już jak tworzyć projekty i jak
wchodzić we właściwości projektu. Czynności te zostały opisane w poprzednim rozdziale, zatytułowanym Dzielenie kodu na
kilka plików zródłowych.
26.3. Zakładka: ogólne
Pierwszą zakładką, jaka jest widoczna po otworzeniu opcji projektu, są ustawienia ogólne. W tej zakładce możesz
ustawić nazwę projektu (pole tekstowe widoczne na samej górze), ikonę dla programu, którą możesz wybrać z biblioteki
standardowej Dev-C++ lub skorzystać z przycisku Przeglądaj, pozwalającego na wybranie dowolnej ikony z dysku. Opcje te są
mało interesujące z naszego punktu widzenia, a opisywanie dokładnie ich działania nie ma najmniejszego sensu, ponieważ
szybciej będzie jeśli przeklikasz wymienione opcje i sam posprawdzasz jakie są tego efekty.
Najbardziej interesujące opcje, jakie warto tu omówić są zawarte na liście opisanej jako Typ. Pozycje jakie występują na liście
to:
" Win32 GUI
" Win32 Konsola
" Win32 Biblioteka Statyczna
" Win32 DLL
26.3.1. Typ: Win32 GUI
Jeśli będziesz chciał kiedyś pisać aplikacje okienkowe pod Windowsa (obojętnie czy to będzie wykorzystywanie
OpenGL, czy zwykły interfejs graficzny okienek) będziesz musiał zaznaczyć tą właśnie opcję. Opcja ta jak nietrudno się
domyślić umożliwia skompilowanie programu wykorzystującego system okienkowy. Plikiem wynikowym kompilacji jest program
o rozszerzeniu *.exe.
26.3.2. Typ: Win32 Konsola
Tryb konsolowy jest Ci już bardzo dobrze znany i w nim aktualnie piszesz wszystkie swoje programy. Plikiem
wynikowym kompilacji jest również program o rozszerzeniu *.exe, tak samo jak to miało miejsce dla typu Win32 GUI.
26.3.3. Typ: Win32 Biblioteka Statyczna
Jeśli będziesz budował bibliotekę statyczną, będziesz musiał zaznaczyć tą opcję jako aktywną. Biblioteki statyczne
przechowują skompilowany kod, który jest dołączany do każdego programu, który chce z nich korzystać. Plikiem wynikowym
kompilacji jest biblioteka statyczna o rozszerzeniu *.a. Bibliotekom statycznym zostanie poświęcony osobny rozdział,
wyjaśniający wszystkie najważniejsze zagadnienia jakie są z nimi związane.
26.3.4. Typ: Win32 DLL
Jeśli będziesz miał potrzebę stworzyć bibliotekę dynamiczną, będziesz musiał wykorzystać do tego celu tą właśnie
opcję. Biblioteki dynamiczne przechowują skompilowany kod tak samo, jak to było w przypadku bibliotek statycznych. Różnicą
zasadniczą, jaka jest między biblioteką statyczną, a biblioteką dynamiczną to fakt, że kod binarny biblioteki nie jest dołączany
do programu. Plikiem wynikowym kompilacji jest biblioteka dynamiczna o rozszerzeniu *.dll. Bibliotekom dynamicznym zostanie
poświęcony osobny rozdział, wyjaśniający wszystkie najważniejsze zagadnienia jakie są z nimi związane.
26.4. Zakładka: parametry
W zakładce parametry zebrane są trzy bardzo ważne pola tekstowe, które umożliwiają Ci niestandardowe
skonfigurowanie pracy kompilatorów C i C++, oraz konfigurowanie konsolidatora odpowiedzialnego za dołączanie bibliotek
statycznych jak i dynamicznych.
26.4.1. Kompilator C/C++
Pracując z kompilatorem C/C++ prawdopodobnie rzadko będziesz miał potrzebę modyfikowania konfiguracji
kompilatora. Jednak jeśli już zajdzie taka potrzeba jest to jedyne słuszne miejsce, w którym powinieneś takie zmiany robić.
Wprowadzone zmiany będą dotyczyły tylko i wyłącznie obecnego projektu.
26.4.2. Konsolidator
Konsolidator służy do dołączania bibliotek statycznych i dynamicznych do programu.
Jeśli masz potrzebę dołączenia bibliotek statycznych *.lib, *.a (lub pliku obiektowego *.o), wystarczy że podasz pełną nazwę
pliku razem ze ścieżką (jeśli nie jest w bieżącym katalogu).
nazwa_pliku.a
inny_plik.o
Jeśli chcesz dołączyć bibliotekę dynamiczną *.dll, musisz napisać następującą linijkę:
-l nazwa_pliku_dll
Pisanie rozszerzenia dla plików dynamicznych nie jest konieczne.
26.5. Zakładka: pliki/katalogi
Zakładka pliki/katalogi została stworzona do ułatwienia organizacji plików w dużych projektach. Za pomocą
zakładek, jakie są widoczne na ekranie, tj. katalogi bibliotek, katalogi plików nagłówkowych, katalogi zasobów, możesz
dodawać standardowe ścieżki poszukiwań plików, dzięki czemu zamiast pisać pełne ścieżki dostępu do plików, wystarczy że
podasz jego nazwę, a program odpowiedzialny za kompilację poszuka używanych plików nie tylko w standardowych katalogach
ale i w tych, które wprowadzisz w odpowiednich zakładkach.
26.6. Pozostałe zakładki
Na chwilę obecną pozostałe, nie wymienione zakładki są dla nas mało interesujące, więc nie będziemy wnikali w
ustawienia, jakie można za pomocą nich konfigurować. Jeśli jesteś ciekaw poeksperymentuj na nich we własnym zakresie,
testując jednocześnie efekty wprowadzonych zmian.


Wyszukiwarka

Podobne podstrony:
Materialy do cwiczenia 8
Ćw Materiały do ćwiczeń z elektrotechniki
PG materiały do ćwiczeń testy
BAL materiały do ćwiczeń
Materiały do cwiczenia nr 11
Fwd materialy?ukacyjne do cwiczen z rachunkowosci ?zNazwy1
Materiały do ćwiczeń z geologii te co umieć
Materiały do cwiczenia 11
Materiały do ćwiczeń projektowych cz 1 Wodociągi
material do cwiczen
material do cwiczen1
MATERIALY DO CWICZENIA BIOLOGIA CYTOMETR
material do cwiczen 3
CHROMATOGRAFIA JONOWA materialy do cwiczen
Mikroekonomia materiały do ćwiczeń

więcej podobnych podstron