1 Wprowadzenie do programowania

Wykład 1 1 Wprowadzenie do programowania Większość aplikacji została stworzona do realizacji jednego, konkretnego, choć obszernego zadania. Przykłado...
4 downloads 1 Views 612KB Size
Wykład 1

1 Wprowadzenie do programowania Większość aplikacji została stworzona do realizacji jednego, konkretnego, choć obszernego zadania. Przykładowo, Notatnik potrafi edytować pliki tekstowe, Winamp – odtwarzać muzykę, a Paint tworzyć rysunki.

Rysunek 1.1. Widok głównego widoku Winmap

Możemy więc powiedzieć, że główną funkcją każdego z tych programów będzie odpowiednio edycja plików tekstowych, odtwarzanie muzyki czy tworzenie rysunków. Funkcję tę można jednak podzielić na mniejsze, bardziej szczegółowe. I tak Notatnik potrafi otwierać i zapisywać pliki, drukować je i wyszukiwać w nich tekst. Winamp zaś pozwala nie tylko odtwarzać utwory, ale też układać z nich playlisty. Idąc dalej, możemy dotrzeć do następnych, coraz bardziej szczegółowych funkcji danego programu. Przypominają one więc coś w rodzaju drzewka, które pozwala nam niejako „rozłożyć daną aplikację na części”.

Rysunek 1.2 Składowe programu Notatnik

Można się zastanawiać na jak drobne części możemy w ten sposób dzielić programy. Innymi słowy, czy dojdziemy wreszcie do takiego elementu, który nie da się rozdzielić na mniejsze. Oczywiście tak – w przypadku Notatnika byliśmy zresztą bardzo blisko.

1

Czynność zatytułowana Otwieranie plików wydaje się być już jasno określona. Kiedy wybieramy z menu Plik programu pozycję Otwórz, Notatnik robi kilka rzeczy: najpierw pokazuje nam okno wyboru pliku. Gdy już zdecydujemy się na jakiś, pyta nas, czy chcemy zachować zmiany w już otwartym dokumencie (jeżeli jakiekolwiek zmiany rzeczywiście poczyniliśmy). W przypadku, gdy je zapiszemy w innym pliku lub odrzucimy, program przystąpi do odczytania zawartości żądanego przez nas dokumentu i wyświetli go na ekranie. Proste, prawda? . Przedstawiona powyżej charakterystyka czynności otwierania pliku posiada kilka znaczących cech:  określa dokładnie kolejne kroki wykonywane przez program;  wskazuje różne możliwe warianty sytuacji i dla każdego z nich przewiduje odpowiednią reakcję. Pozwalają one nazwać niniejszy opis algorytmem. Pisanie programów jest czynnością polegającą na zapisaniu pewnej, ściśle określonej liczby poleceń w określony języku programowania. Cały program jest zbiorem poleceń. Algorytm jest metodą rozwiązywania problemu w skończonej liczbie kroków (operacji elementarnych). Algorytm to jednoznacznie określony sposób, w jaki program komputerowy realizuje jakąś elementarną czynność.

Przy rozwiązywaniu jakiegoś problemu przy pomocy komputera należy zrealizować następujące etapy:  sformułowanie problemu (najważniejsza faza procesu rozwiązania);  dokładna analiza problemu;  znalezienie właściwej metody rozwiązania problemu (opracowanie algorytmu);  napisanie programu w określonym języku programowania (np. Borland C++ lub C++Builder);  uruchomienie i przetestowanie programu;  wprowadzenie koniecznych zmian. W przypadku otwierania plików w Notatniku może to wyglądać na przykład tak: Algorytm Plik -> Otwórz Pokaż okno wyboru plików Jeżeli użytkownik kliknął Anuluj, To Przerwij Jeżeli poczyniono zmiany w aktualnym dokumencie, To Wyświetl komunikat "Czy zachować zmiany w aktualnym dokumencie?" z przyciskami Tak, Nie, Anuluj Sprawdź decyzję użytkownika Decyzja Tak: wywołaj polecenie Plik -> Zapisz Decyzja Anuluj: Przerwij Odczytaj wybrany plik Wyświetl zawartość pliku Koniec Algorytmu Jak widać, sprecyzowaliśmy tu kolejne kroki wykonywane przez program – tak aby „wiedział”, co należy po kolei zrobić. Fragmenty zaczynające się od Jeżeli i Sprawdź pozwalają odpowiednio reagować na różne sytuacje, takie jak zmiana decyzji użytkownika i wciśnięcie przycisku Anuluj.

2

Czy to wystarczy, by komputer wykonał to, co mu każemy? Otóż nie bardzo… Chociaż wprowadziliśmy już nieco porządku, nadal używamy języka naturalnego – jedynie struktura zapisu jest bardziej ścisła. Notacja taka, zwana pseudokodem, przydaje się jednak bardzo do przedstawiania algorytmów w czytelnej postaci. Jest znacznie bardziej przejrzysta oraz wygodniejsza niż opis w formie zwykłych zdań, które musiałyby być najczęściej wielokrotnie złożone i niezbyt poprawne gramatycznie. Dlatego też, kiedy będziesz wymyślał własne algorytmy, staraj się używać pseudokodu do zapisywania ich ogólnego działania.

3.8 Przegląd najważniejszych języków programowania Obecnie istnieje bardzo, bardzo wiele języków programowania. Niektóre przeznaczono do konkretnych zastosowań, na przykład sieci neuronowych, inne zaś są narzędziami ogólnego przeznaczenia. Zazwyczaj większe korzyści zajmuje znajomość tych drugich, dlatego nimi właśnie się zajmiemy. Należy zaznaczyć, nie ma czegoś takiego jak język, który będzie dobry do wszystkiego. Spośród języków „ogólnych” niektóre są nastawione na szybkość, inne na rozmiar kodu, jeszcze inne na przejrzystość itp. Jednym słowem, panuje totalny rozgardiasz ;) Należy koniecznie odróżniać języki programowania od innych języków używanych w informatyce. Na przykład HTML jest językiem opisu, gdyż za jego pomocą definiujemy jedynie wygląd stron www (wszelkie interaktywne akcje to już domena JavaScriptu). Inny rodzaj to języki zapytań w rodzaju SQL, służące do pobierania danych z różnych źródeł (na przykład baz danych). Niepoprawne jest więc (popularne skądinąd) stwierdzenie „programować w HTML”. Przyjrzyjmy się więc najważniejszym używanym obecnie językom programowania: 1. Visual Basic Jest to następca popularnego swego czasu języka BASIC. Zgodnie z nazwą (Basic znaczy prosty), był on przede wszystkim łatwy do nauki. Visual Basic pozwala na tworzenie programów dla środowiska Windows w sposób wizualny, tzn. poprzez konstruowanie okien z takich elementów jak przyciski czy pola tekstowe. Język ten posiada dosyć spore możliwości, jednak ma również jedną, za to bardzo poważną wadę. . Programy w nim napisane nie są kompilowane w całości do kodu maszynowego, ale interpretowane podczas działania. Z tego powodu są znacznie wolniejsze od tych kompilowanych całkowicie. Obecnie Visual Basic jest jednym z języków, który umożliwia tworzenie aplikacji pod lansowaną przez Microsoft platformę .NET, więc pewnie jeszcze o nim usłyszymy :) 2. Object Pascal (Delphi) Delphi z kolei wywodzi się od popularnego języka Pascal. Podobnie jak Visual Basic jest łatwy do nauczenia, jednakże oferuje znacznie większe możliwości zarówno jako język programowania, jak i narzędzie do tworzenia aplikacji. Jest całkowicie kompilowany, więc działa tak szybko, jak to tylko możliwe. Posiada również możliwość wizualnego konstruowania okien. Dzięki temu jest to obecnie chyba najlepsze środowisko do budowania programów użytkowych. 3. C++ jest teraz chyba najpopularniejszym językiem do zastosowań wszelakich. Powstało do niego bardzo wiele kompilatorów pod różne systemy operacyjne i dlatego jest uważany za najbardziej przenośny. Istnieje jednak druga strona medalu – mnogość tych narzędzi prowadzi do niewielkiego rozgardiaszu i pewnych trudności w wyborze któregoś z nich. Na szczęście sam język został w 1997 roku ostatecznie ustandaryzowany. O C++ nie mówi się zwykle, że jest łatwy – być może ze względu na dosyć skondensowaną składnię (na przykład odpowiednikiem pascalowych słów begin i end są po prostu nawiasy klamrowe { i }). To jednak dosyć powierzchowne przekonanie, a sam język jest spójny i logiczny. Jeżeli chodzi o możliwości, to w przypadku C++ są

3

one bardzo duże – w sumie można powiedzieć, że nieco większe niż Delphi. Jest on też chyba najbardziej elastyczny – niejako dopasowuje się do preferencji programisty. 4. Java Ostatnimi czasy Java stała się niemal częścią kultury masowej – wystarczy choćby wspomnieć o telefonach komórkowych i przeznaczonych doń aplikacjach. Ilustruje to dobrze główny cel Javy, a mianowicie przenośność – i to nie kodu, lecz skompilowanych programów! Osiągnięto to poprzez kompilację do tzw. bytecode, który jest wykonywany w ramach specjalnej maszyny wirtualnej. W ten sposób, program w Javie może być uruchamiany na każdej platformie, do której istnieje maszyna wirtualna Javy – a istnieje prawie na wszystkich, od Windowsa przez Linux, OS/2, QNX, BeOS, palmtopy czy wreszcie nawet telefony komórkowe. Z tego właśnie powodu Java jest wykorzystywana do pisania niewielkich programów umieszczanych na stronach www, tak zwanych apletów. Ceną za tą przenośność jest rzecz jasna szybkość – bytecode Javy działa znacznie wolniej niż zwykły kod maszynowy, w dodatku jest strasznie pamięciożerny. Ponieważ zastosowania tego języka nie są wielkie i nie wymagające aplikacje, lecz proste programy, zatem nie jest to aż tak wielki mankament. Składniowo Java bardzo przypomina C++. 5. PHP (skrót od Hypertext Preprocessor) jest językiem używanym przede wszystkim w zastosowaniach internetowych, dokładniej na stronach www. Pozwala dodać im znacznie większą funkcjonalność niż ta oferowana przez zwykły HTML. Obecnie miliony serwisów wykorzystuje PHP – dużą rolę w tym sukcesie ma zapewne jego licencja, oparta na zasadach Open Source (czyli brak ograniczeń w rozprowadzaniu i modyfikacji). Możliwości PHP są całkiem duże, nie można tego jednak powiedzieć o szybkości – jest to język interpretowany. Jednakże w przypadku głównego zastosowania PHP, czyli obsłudze serwisów internetowych, nie ma ona większego znaczenia – czas wczytywania strony WWW to przecież w większości czas przesyłania gotowego kodu HTML od serwera do odbiorcy. Jeżeli chodzi o składnię, to trochę przypomina ona C++. Kod PHP można jednak swobodnie przeplatać znacznikami HTML. Z punktu widzenia programisty gier język ten jest w zasadzie zupełnie bezużyteczny (chyba że kiedyś sam będziesz wykonywał oficjalną stronę internetową swojej wielkiej produkcji ), wspominam o nim jednak ze względu na bardzo szerokie grono użytkowników, co czyni go jednym z ważniejszych języków programowania. To oczywiście nie wszystkie języki – jest ich całe mnóstwo. Jednakże w ogromnej większości przypadków główną różnicą między nimi jest składnia, a więc sprawa mało istotna (szczególnie, jeżeli dysponuje się dobrą dokumentacją). Z tego powodu poznanie jednego z nich bardzo ułatwia naukę następnych – po prostu im więcej języków już znasz, tym łatwiej uczysz się następnych Musimy zatem zdecydować, którego języka będziemy się uczyć, aby zrealizować nasz nadrzędny cel, np. poznanie tajników programowania gier. Sprecyzujmy więc wymagania wobec owego języka:  programy w nim napisane muszą być szybkie – w takim wypadku możemy wziąć pod uwagę jedynie języki całkowicie kompilowane do kodu maszynowego;  musi dobrze współpracować z różnorodnymi bibliotekami graficznymi, na przykład DirectX;  powinien posiadać duże możliwości i zapewniać gotowe, często używane rozwiązania;  nie zaszkodzi też, gdy będzie w miarę prosty i przejrzysty :) Jeżeli uwzględnimy wszystkie te warunki, to spośród całej mnogości języków programowania (w tym kilku przedstawionych wcześniej) zostają nam aż… dwa – Delphi oraz C++. Przyglądając się bliżej Delphi, możemy zauważyć, iż jest on przeznaczony przede wszystkim do programowania aplikacji użytkowych. Na plus można jednak zaliczyć prostotę

4

i przejrzystość języka oraz jego bardzo dużą wydajność. Również możliwości Delphi są całkiem spore. Z kolei C++ zdaje się być bardziej uniwersalny. Dobrze rozumie się z ważnymi dla nas bibliotekami graficznymi, jest także bardzo szybki i posiada duże możliwości. Składnia z kolei jest raczej „ekonomiczna” i być może nieco bardziej skomplikowana. C++Builder6 wykorzystuje w języku C++ zalety języka Delphi. Biblioteka VCL wykorzystywana Delphi została zaadoptowana do języka C++, co ma miejsce w C++Builder6. Czyżbyśmy mieli zatem remis, a prawda leżała (jak zwykle) pośrodku? Otóż niezupełnie – nie uwzględniliśmy bowiem ważnego czynnika, jakim jest popularność danego języka. Jeżeli jest on szeroko znany i używany (np. do programowania gier), to z pewnością istnieje o nim więcej przydatnych źródeł informacji, z których mógłbyś korzystać. Z tego właśnie powodu Delphi jest gorszym wyborem, ponieważ ogromna większość dokumentacji, artykułów, kursów itp. dotyczy języka C++. Wystarczy chociażby wspomnieć, iż Microsoft nie dostarcza narzędzi pozwalających na wykorzystanie DirectX w Delphi – są one tworzone przez niezależne zespoły i ich używanie wymaga pewnego doświadczenia. A więc – C++! Język ten wydaje się najlepszym wyborem. A skoro mamy już tą ważną decyzję za sobą, została nam jeszcze tylko pewna drobnostka – trzeba się tego języka nauczyć.

3.8 Generacje języków programowania Język programowania to forma zapisu instrukcji dla komputera i programów komputerowych, pośrednia między językiem naturalnym a kodem maszynowym. Języki programowania można podzielić na 5 wyraźnie różniących się generacji (według innych autorów na 4 generacje) 1. Pierwsza generacja – programowanie komputerów odbywa się w kodzie binarnym, czyli za pomocą zer i jedynek. Programowanie tą metodą stwarza ogromne trudności ze względu na to, że każdy komputer posługuje się własnym kodem (kodem maszynowym inaczej zwanym wewnętrznym) i programista za każdym razem musi dostosować się do języka konkretnej maszyny. 2. Druga generacja –powstają języki symboliczne (asemblery), w których ciągi zer i jedynek zastąpiono łatwiejszymi do zrozumienia znakami mnemotechnicznymi. Chociaż języki te stanowią proste tłumaczenie języka maszynowego na symbole i są ściśle związane z danym modelem komputera, to znacznie ułatwiają pisanie instrukcji, czynią je bardziej czytelnymi. 3. Trzecia generacja – powstają języki programowania wysokiego poziomu, w których symbole asemblera zastąpiono łatwiejszym do zapamiętania językiem naturalny. 4. Czwarta generacja – odpowiednie narzędzia umożliwiają budowanie prostych aplikacji dzięki wykorzystaniu gotowych modułów. 5. Piąta generacja – to języki stosowane do tworzenia programów wykorzystujących tzw. sztuczną inteligencję lub systemów (ekspertowych)

3.8 Metody programowania W celu rozwiązania określonego problemu można zastosować różne metody programowania. Ten sam cel można osiągnąć przy pomocy różnych metod programowania, ale nakład pracy i podatność oprogramowania na zmiany będzie zasadniczo różna.

5

3.1.1 Programowanie proceduralne Oto pierwotny paradygmat1 programowania: Zadecyduj, jakie chcesz mieć procedury; stosuj najlepsze algorytmy, jakie możesz znaleźć Nacisk kładzie się na przetwarzanie, czyli zbudowanie algorytmu potrzebnego do wykonania obliczeń. Języki wspierają paradygmat przez dostarczanie mechanizmów do przekazywania argumentów do funkcji i wyników z funkcji. Literatura poświęcona temu sposobowi myślenia jest pełna rozważań dotyczących sposobów przekazywania argumentów, wyróżniania rozmaitych typów argumentów, różnych rodzajów funkcji (np. procedur, podprogramów, makrodefinicji) itd. Typowym przykładem dobrego stylu jest funkcja licząca pierwiastek kwadratowy. Dostarcza się jej argument a otrzymuje wynik. W tym celu są wykonywane dobrze rozumiane obliczenia matematyczne. double sqrt(double arg) { //kod obliczania pierwiastka kwadratowego } void f() { double pk2=sqrt(2); // .... } Nawiasy klamrowe {} wyrażają w C++ grupowanie. Tutaj wskazują początek i koniec funkcji. podwójny prawy ukośnik // rozpoczyna komentarz, który rozciąga się do końca wiersza. Słowo kluczowe void wskazuje, że funkcja nie przekazuje żadnej wartości. Z punktu widzenia organizacji programu funkcje używane do ustanawiania porządku w labiryncie algorytmów. Algorytmy natomiast zapisuje się za pomocą wywołań funkcji i innych udogodnień języka.

3.1.2 Programowanie modularne (strukturalne) Z biegiem lat nacisk w projektowaniu przesunął się z projektowania procedur w kierunku organizacji danych. Wiąże się pomiędzy innymi ze wzrostem rozmiaru programu. Zbiór powiązanych ze sobą procedur, łącznie z danymi , na których te procedury działają, często nazywa się modułem. Powstał nowy paradygmat programowania:

1

W Oxford English Dictionary paradygmat jest zdefiniowany jako wzorzec lub najogólniejszy model lub jako wzorcowy przykład. Termin ten jest używany w wielu naukach w powyższym sensie ale dotyczy tylko ich podstawowych założeń. Paradygmat - w rozumieniu wprowadzonym przez filozofa Thomasa Kuhna w książce Struktura rewolucji naukowych (The Structure of Scientific Revolutions) opublikowanej w 1962 r. - to zbiór pojęć i teorii tworzących podstawy danej nauki. Teorii i pojęć tworzących paradygmat raczej się nie kwestionuje, przynajmniej do czasu kiedy paradygmat jest twórczy poznawczo - tzn. za jego pomocą można tworzyć teorie szczegółowe zgodne z danymi doświadczalnymi (historycznymi), którymi zajmuje się dana nauka.

6

Zadecyduj, jakie chcesz mieć moduły: podziel program w taki sposób, aby ukryć dane w modułach: Ten paradygmat jest także znany jako zasada ukrywania danych. Tam, gdzie nie ma grupowania procedur z ich danymi, wystarcza styl programowania proceduralnego. Techniki projektowania dobrych procedur są teraz stosowane do każdej procedury w module. Najbardziej znanym przykładem modułu jest definicja stosu. Główne problemy, które trzeba rozwiązać, to:  dostarczenie interfejsu użytkownika do obsługi stosu (np. funkcji włóż( ) i funkcji zdejmij ( ));  zapewnienie, że reprezentacja stosu (np. tablica elementów) będzie dostępna jedynie przez interfejs użytkownika;  zapewnienie, że stos będzie inicjowany przed pierwszym użyciem. Język C++ dostarcza mechanizm grupowania ze sobą danych, funkcji, itd. w obrębie przestrzeni nazw. można np. zadeklarować interfejs modułu Stos i korzystać z niego w następujący sposób: namespace Stos { //interfejs void włóż(char); char zdejmij(); } void f() { Stos::włóż(‘c’); if(Stos::zdejmij() !=’c’) błąd(„niemożliwe”); } Kwalifikator Stos:: wskazuje, że włóż() i zdejmij() pochodzą z przestrzeni nazw Stos. Nie dojdzie dzięki temu do kolizji z innymi użyciami tych nazw i uniknie się zamieszania. Definicja modułu Stos może się znajdować w osobno kompilowanej części programu. namespace Stos { //implementacja const int max_rozmiar=200; char v[max_rozmiar]; int wierzchołek = 0; void włóż(char c) { /* sprawdź czy stos nie jest pełny i włóż c*/ } char zdejm() { /* sprawdź czy stos nie jest pusty i zdejm*/ } }

7

Kluczową właściwością modułu Stos jest to, że kod użytkownika jest oddzielny od reprezentacji danych za pomocą kodu implementującego Stos::włóż() i Stos::zdejmij(). Użytkownik nie musi wiedzieć, że Stos jest zaimplementowany z użyciem tablicy i zmiana implementacji nie wpłynie na kod użytkownika. Modularność to podstawowa cecha wszystkich poprawnie działających dużych programów. Moduły nie wystarczają jednak do czytelnego wyrażania złożonych systemów. Programowanie za pomocą modułów prowadzi do skupienia wszystkich danych jednego typu pod kontrolą modułu zarządcy typu. gdyby ktoś potrzebował wielu stosów, a nie tego jednego dostarczanego przez opisany moduł Stos, to zdefiniowałby moduł zarządcy stosu z takim oto interfejsem: namespace Stos { struct Rep; //definicja struktury stosu znajduje się gdzie indziej typedef Rep& stos; stos utwórz(); //utwórz nowy stos void usuń (stos s); //usun s void włóż(stos s, char c); //włóż c na s char zdejmij(stos s); //zdejmij ze stosu s } Deklaracja struct Rep; oznacza, że Rep jest nazwą typu, lecz sam typ będzie zdefiniowany później Deklaracja typedef Rep& stos; nadaje nazwę stos „referencji do Rep. Pomysł polega na tym, że stos identyfikuje się za pomocą stos::stos, a inne szczegóły są ukryte przed użytkownikami. Programowanie strukturalne to paradygmat programowania zalecający hierarchiczne dzielenie kodu na moduły, które komunikują się jedynie poprzez dobrze określone interfejsy. Jest to rozszerzenie koncepcji programowania proceduralnego. Według angielskiej Wikipedii jest to raczej pewna poddyscyplina lub podzbiór programowania proceduralnego zalecająca stosowanie konstrukcji języka takich jak pętle i instrukcje warunkowe, oraz unikanie instrukcji goto i wielokrotnych punktów wejścia i wyjścia z kodu danego podbloku programu. Budując program na podstawie języka wysokiego poziomu, należy zwrócić uwagę na to, jak będzie przebiegało jego wykonanie. Bardzo rzadko się zdarza, że program składający się z ciągu instrukcji będzie realizował je jedna po drugiej. Najczęściej pewne sekwencje muszą być powtarzane lub następuje rozgałęzienie wykonania, w zależności od warunków wynikowych. Klasyczna metoda przedstawiania kolejności wykonywania instrukcji podczas projektowania programów polega na rysowaniu schematu blokowego, który w sposób graficzny obrazuje działanie programu, bez zbytecznego zagłębiania się w szczegóły jego algorytmów. Na takich schematach instrukcje wykonywane mają postać prostokątów, natomiast warunki kontrolujące przebieg i koniec poszczególnych pętli są przedstawiane w postaci rombów. Starsze języki programowania np. FORTRAN lub BASIC, zawierały instrukcję skoku bezwarunkowego GOTO (pochodząca od angielskiego słowa „go to” – „idź do”). Instrukcja skoku bezwarunkowego GOTO pochodzi z języka symbolicznego- asemblera. Do instrukcji GOTO doda-

8

wano wiele instrukcji umożliwiających realizację skoków warunkowych oraz zmianę kolejności wykonywania poszczególnych linii kodu. Pod koniec lat 60-tych narodził się nowy styl programowania, który mocno ograniczył korzystanie z instrukcji GOTO, wprowadzając inne bardziej czytelne rozwiązania.

3.1.3 Programowanie obiektowe Ewolucja języków strukturalnych wykreowała w latach 80 – tych języki obiektowe i programowanie obiektowe. Programowanie to jest nowym sposobem programowania, w którym program jest rozumiany jako zespół obiektów wzajemnie na siebie oddziaływujących. Klasyczne programowanie oparte jest na procedurach i instrukcji skoku bezwarunkowego GOTO miało pewne wady, które wyeliminowało programowanie strukturalne. Nie był to postęp wystarczający z punktu widzenia programisty, w miarę jak rosły złożoność i stopień skomplikowania programów.

3.1.4 Programowanie wizualne Po ogromny sukcesie Delphi firma Borland postanowiła stworzyć podobne narzędzie dla języka C++. Nowy produkt w znacznej części wykorzystuje filozofię Delphi. C++ Builder to:  32 bitowe, w pełni zintegrowane środowisko RAD (Rapid Application Development) do szybkiego tworzenia aplikacji w systemie operacyjnym Windows;  wygodne wizualne środowisko programowania IDE (Integrated Development Enviroment) wspomagane biblioteką wizualnych komponentów VCL (Visual Component Library). Dzięki temu pisanie programów w takim środowisku programowania sprowadza się do wybrania komponentów i przeciągnięcia ich do obszaru projektowania.

2 Ogólne wiadomości o języku C i C++ Pomysłodawcą i pierwszym implementorem C++ jest Bjarne Stroustrup. Jeszcze przed uzyskaniem doktoratu w Cambridge sformułował wstępnie założenia pod wpływem kłopotów z pracą dyplomową, którą usiłował zrobić w Simuli (chwalił język, ale narzekał na aktualnie dostępną mu implementację), a w końcu zmuszony był sfinalizować w BCPL. Po podjęciu pracy w Bell Laboratories w AT&T zapoznawał się dokładnie z językiem C i - ze względu głównie na jego przenośność i niezależność od platformy - ustanowił podstawą jego wyśnionego języka. Pierwsza jego koncepcja, znana jako „C z klasami” pojawiła się w roku 1979 (z tego okresu pochodzą Modula-2 i Smalltalk-80). Pierwotnym założeniem było stworzenie nowego języka. W swojej książce pt. „Projektowanie i rozwój języka C++” przedstawiając swoją awersję do idealistów, którzy próbują uszczęśliwiać innych, wyjaśnia czym podyktowane są rozwiązania, jakie tam zastosowano. Z tego właśnie powodu język ten w założeniu miał być językiem, który „nie zmusza” użytkownika do robienia czegoś w ściśle określony sposób. Faktem jest jednak, że założenie to jest w wielu wypadkach niemal niemożliwe do uzyskania, a poza tym twórcy decydowali się często na zaimplementowanie i dobre wspieranie przez C++ tych właściwości, które oferują większą wydajność zarówno projektu jak i pracy nad projektem, raczej niż dobre wspieranie wszelkich możliwych technik programowania. Mimo tego C++ oferuje dużo różnych technik programowania, adekwatnych do różnych sytuacji. Jest językiem dużego wyboru, jednak w konsekwencji dla wielu ten wybór jest „za duży”. Przez to krąży wśród ludzi opinia, że tego języka nie da się w pełni opanować.

9

Język C++ jest bezpośrednio bazowany na Simuli 67 i C, jednak na jego rozwój miało wpływ wiele języków programowania, m.in. „ojciec języków strukturalnych” Algol 68 (przeciążanie operatorów, referencje, dowolne miejsce dla deklaracji zmiennych), Ada (wzorce, wyjątki, przestrzenie nazw), ML i Clu (wyjątki). Języki takie jak Modula, Smalltalk i CLOS specjalnie nie wpłynęły na C++, stąd ich modele programowania i projektowania są całkiem odmienne. Język C++ jest językiem o ogromnych możliwościach, często trudno osiągalnych w innych językach. Potencjał C++ nie jest jednak łatwy do okiełznania: narzędzie o takiej mocy musi być umiejętnie obsługiwane, inaczej łatwo stracić nad nim kontrolę, co na ogół kończy się niemiło. W początkowym okresie nauki przytrafi się zapewne wiele wpadek w rodzaju błędów przydziału pamięci, naruszeń ochrony segmentu i innych awarii.

3 Zmienne Samo pojęcie zmiennej (ang. variable) można zdefiniować jako nazwę przypisaną pewnej lokalizacji w pamięci. Zmienna (ang. variable) to miejsce w pamięci operacyjnej, przechowujące pojedynczą wartość określonego typu. Każda zmienna ma nazwę, dzięki której można się do niej odwoływać.

Aby zmienna można było wykorzystywać do manipulowania znajdującymi się w pamięci danymi, należy ją najpierw zadeklarować. Bezpośrednio po zadeklarowaniu zawartość zmiennej jest nieokreślona – odpowiednie komórki zawierają przypadkowe wartości, będące na ogół pozostałościami po działaniu poprzednich programów. Zawartość zmiennej zostaje zdefiniowana dopiero w chwili inicjalizacji, czyli nadania jej wartości. Pokazuje to poniższy przykład: int x; int y; x = y+10; //błąd! Wynik o nie przewidywalnej wartości Wyjątkiem od tej zasady są zmienne globalne i statyczne (deklarowane z modyfikatorem static), automatycznie inicjalizowane przez komputer wartością 0. Wartości pozostałych zmiennych są nieokreślone do momentu pierwszego przypisania. Nazwy zmiennych mogą zawierać małe i duże litery alfabety łacińskiego, a także cyfry oraz znak podkreślenia (_). Użycie w nazwach innych znaków, a szczególnie spacji jest niedopuszczalne. nazwa nie może się zaczynać cyfrą; również użycie jako pierwszego znaku podkreślenia jest, chociaż możliwe, nie jest polecane, gdyż znaki pojedynczego i podwójnego podkreślenia są często zarezerwowane do użytku wewnętrznego kompilatora. Od kompilatora zależy również długość nazwy zmiennej. Bezpieczną granica jest tu 31 znaków, jednak w praktyce ze względu na czytelność rzadko używa się nazw o długości przekraczającej 20 znaków.

10

3.8 Typy zmiennych Na początek zauważmy, że program pobiera od nas pewne dane i wykonuje na nich operacje. Są to działania dość trywialne (jak wyświetlenie rzeczonych danych w niezmienionej postaci), jednak wymagają przechowania przez jakiś czas uzyskanej porcji informacji. W językach programowania służą do tego zmienne. Przed pierwszym użyciem zmienną należy zadeklarować, czyli po prostu poinformować kompilator, że pod taką a taką nazwą kryje się zmienna danego typu. Może to wyglądać choćby tak: std::string strImie; Typ danej (ang. date type) określa sposób przechowywania w pamięci. Niektóre języki programowania umożliwiają utworzenie i zdefiniowanie danej poprzez zwykłe przypisanie wartości. Przykładem może być popularny BASIC w którym można spotkać instrukcję typu: x=-1 x=100 x=3.14 Intepreter BASICA sam troszczy się o przydzielenie obszaru pamięci odpowiednio dużego, żeby zmieścić daną wartość. W języku FORTRAN kompilator dla przypadku kiedy programista nie zadeklarował typu zmiennej, automatycznie określał typ na podstawie pierwszej litery nazwy zmiennej. Deklaracja zmiennej przed jej użyciem pozwala kompilatorowi na kontrolę poprawności jej wykorzystania w programie. Wszystkie operacje wykorzystujące zmienną są weryfikowane w trakcie kompilacji. i, jeśli się okaże, że któraś z nich jest nielegalna albo może być źródłem problemów, programista jest o tym informowany za pomocą komunikatu lub ostrzeżenia. Podstawowe typy danych mogą mieć swoje odmiany. na przykład typy całkowite mogą być opatrzone znakiem (ang. signed) lub go nie zawierać (ang. unsigned). Jak łatwo się domyślić, zmienna ze znakiem może przechowywać wartości dodatnie i ujemne. Zmienna bez znaku może przechowywać tylko wartości dodatnie. Zmienne deklaruje się następująco: Modyfikatory typu: signed  ze znakiem (), int char unsigned  bez znaku, int char short  krótka (mniejsza), int  long  długa (większa) int np. unsigned long int dluga_liczba_bez_znaku ; Wartości domyślne: long int char

= = =

11

long int signed int signed char

   

double

Rys. Przedział wartości typów liczbowych ze znakiem (signed) i bez znaku (unsigned) Poniżej przedstawiono podstawowe typy danych dla kompilatora 16 bitowego.

tabela 1. Typy danych dla kompilatora 16 bitowego Nazwa typu Zawartość Przedział wartości char znak -128  127 unsigned char znak 0 255 enum wyliczeniowy -32768  32767 int liczba całkowita -32768  32767 short int liczba całkowita -32768  32767 long liczba całkowita -2147mln  2147mln unsigned long liczba całkowita 0  4 294 967 295 float liczba rzeczyw. 10-38  1038 (7cyfr) double liczba rzeczyw. 10-308  10308 (15 cyfr) long double liczba rzeczyw. 3.4 * (10-4932)  1.1 * (10+4932)

Zajęt. pamięć 1 bajt 1bajt 2 bajty 2 bajty 2 bajty 4 bajty 4 bajty 4 bajty 8 bajtów 10 bajtów

Dla kompilatora 32 bitowego typy przedstawiono w tabeli 2 W systemie C++Builder, jak również w kompilatorach Borland C++ 5.x, typ bool jest „prawdziwym” typem danych, równorzędnym innym typom, zaś słowo bool jest słowem kluczowym języka. Niektóre kompilatory C++ (między innymi Borland C++ 3.1 do Borland C++ 4.5) nie oferują programiście typu bool. Borland C++4.5 oferuje programiście typ BOOL równoważny typowi całkowitemu int. Definicja taka nie jest integralnym elementem języka, lecz jest tworzona tabela 2. Typy danych dla kompilatora 32 bitowego Nazwa typu Zawartość Przedział wartości char znak -128  127 unsigned char znak 0 255 enum wyliczeniowy -32768  32767 int liczba całkowita -2147mln  2147mln short int liczba całkowita -32768  32767 long liczba całkowita -2147mln  2147mln unsigned long liczba całkowita 0  4 294 967 295 float liczba rzeczyw. 10-38  1038 (7cyfr) double liczba rzeczyw. 10-308  10308 (15 cyfr) long double liczba rzeczyw. 3.4 * (10-4932)  1.1 * 12

Zajęt. pamięć 1 bajt 1bajt 2 bajty 4 bajty 2 bajty 4 bajty 4 bajty 4 bajty 8 bajtów 10 bajtów

bool

logiczna

(10+4932) true (prawda) lub false (fałsz)

1 bajt

3.1.1 Typy i zmienne wyliczeniowe W C i C++ istnieje możliwość definiowania zmiennych wyliczeniowych. Typ wyliczeniowy służy do definiowania zbiorów stałych całkowitych. Ogólny schemat definicji typu wyliczeniowego przedstawia się następująco: enum identyfikator-_typu_wyliczeniowego { lisrta stałych wyliczeniowych ] [z,ienna –typu wyliczeniowego[=inicjator]]; przykład enum months [ Jan,Fab,Mar,Apr,mai,Jun,Jul,Aug,Sep,Okt,Nov,dec }; Zdefiniowano typ wyliczeniowy months składający się ze stałych reprezentujących poszczególne miesiące. Zasada nadawania wartości stałym Jeśli brak jest bezpośrednich inicjatorów (tak jak w przykładzie), pierwszej stałej nadawana jest wartość zero, a każdej następnej o jeden większa od poprzedniej (a więc 0,1,2,3...,) W przypadku wystąpienia inicjatora stałej nadawana jest wartość zgodna z ta wskazana w inicjatorze, zaś następne stałe inicjatora są inicjowane kolejnymi wartościami całkowitymi. enum months [ Jan=10,fab,Mar,Apr=20,May=Jan+Apr,Jun,Jul,Aug,Sep,Okt,Nov,Dec=50 }; Powyższy przykład ilustruje różne sposoby nadania wartości stałym wyliczeniowy. można więc:  dokonać bezpośredniego przypisania wartości stałym wyliczeniowym;  polegać na domniemaniu wartości o jeden większej od poprzednika – stałe fab,Mar mają wartości 11, 12;  uzyć już zdefiniowanych stałych(May=Jan+apr=30).

3.8 Typy danych Windows Nowoczesna idea programowania w Windows oparta na wykorzystaniu narzędzi programistycznych typu RAD, do których zalicza się C++Builder, pozwala programistom na

13

maksymalne uproszczenie procesu tworzenia oprogramowania. Jednym z przykładów dążenia do zminimalizowania czasu tworzenia aplikacji jest zastosowanie w Windows pewnych bardzo zwartych w zapisie typów danych, które oczywiście posiadają odpowiedniki w typach standardowych. W tabeli poniżej zebrano najistotniejsze typy danych, którymi bardzo często posługują się programy Windows. Należy zdawać sobie sprawę z faktu, że typy takie jak np. LPVOID i LPSTR nie są dosłownie typami nowymi, tzn. od początku stworzonymi na potrzeby aplikacji Win32, gdyż zostały zdefiniowane w plikach nagłówkowych za pomocą instrukcji typedef po to, aby uprościć zapis niektórych standardowych typów danych. Typ Windows BOOL BYTE DWORD LPDWORD LONG LPLONG LPCSTR LPCTSTR LPSTR LPVOID lub Pointer LPCVOID UINT WORD

Znaczenie int z dwoma wartościami TRUE oraz FALSE unsigned char unsigned long unsigned long * long long * const char * unsigned const char * char * void * const void * unsigned int unsigned short

Osobnym typem danych bardzo często stosowanym w aplikacjach Win32 jest typ HANDLE. Typ HANDLE jest 32 bitowym typem danych całkowitych oznaczających tzw. uchwyt (ang.handle). Należy rozumieć, że w rzeczywistości dane typu HANDLE nie obrazują jakichś tajemniczych uchwytów zakładanych na elementy aplikacji – są to po prostu 32bitowe liczby identyfikujące określony zasób aplikacji, systemu operacyjnego lub samego komputera. Z tego względu dane typu HANDLE często wygodniej i zręczniej jest określać mianem identyfikatorów, których wartości są przechowywane w określonym miejscu pamięci. Cechą charakterystyczną identyfikatorów jest to, że po zainicjowaniu ich określonymi wartościami na początku programu, w momencie zakończenia pracy aplikacji lub jej fragmentu należy przydzieloną im pamięć odpowiednio zwalniać. W tym celu wykorzystuje się funkcję API Windows: BOOL CloseHandle(HANDLE, hObject) z argumentem w postaci określonego identyfikatora.

3.8 Typ Currency Dane typu Currency zaliczane są do liczb rzeczywistych, jednak warto pamiętać, że w rzeczywistości jest to typ danych stałoprzecinkowych, tzn. dane zawsze reprezentowane są z dokładnością do 4 miejsc po przecinku. Typ Currency jest wykorzystywany w różnego rodzaju obliczeniach finansowych.

14

3.8 Typ void Jest to typ pusty. Wykorzystywany bywa w następujących sytuacjach:  po pierwsze, za jego pomocą możemy deklarować funkcje nie zwracające żadnych wartości;  po drugie możemy deklarować funkcje, które nie pobierają argumentów;  po trzecie umożliwia on tworzenie ogólnych wskaźników.

3.8 Typy logiczne Zmienne mogące przyjmować tylko dwie wartości: true (prawda) lub false (fałsz) charakteryzują cztery podstawowe typy logiczne, które są zamieszone w tabeli poniżej. Logiczne (bulowskie) typy danych Typ Rozmiar (bajty) 1 bool boolean (dostępny w VCL) 1 ByteBool (dostępny w VCL) 1 WordBool (dostępny w VCL) 2 LongBool (dostępny w VCL) 4 Jedynie typ boolean reprezentuje wartości mogące znajdować się w dwóch stanach: 1 (TRUE) i 0 (FALSE). Wszystkie pozostałe typy logiczne wartość false reprezentują jako 0, natomiast każda inna wartość odpowiada stanowi true.

3.8 Typy znakowe C++Builder pozwala na deklaracje zmiennych reprezentujących pojedyncze znaki. Obecnie tradycyjny zestaw 256 znaków ASCII o numerach od 0 do 255 jest podzestawem szerszego zbioru UNICODE, zawierającego 65536 znaków ponumerowanych od 0 do 65535. Operowanie znakami UNICODE ułatwiają typy wchar_t oraz WideChar. Znakowe typy danych Typ char wchar_t AnsiChar (dostępny w VCL) WideChar (dostępny w VCL)

Zawartość Pojedynczy znak ASCII Pojedynczy znak UNICODE Pojedynczy znak ASCII Pojedynczy znak UNICODE

Rozmiar (bajty) 1 2 1 2

Stałe typu znakowego obramowywane są pojedynczymi apostrofami: wchar_t znak =’X’ AnsiChar litera;

15

3.8 Typy łańcuchowe Typy łańcuchowe w C++Builderze stanowią naturalne rozwinięcie typów znakowych. C++Builder udostępnia trzy typy łańcuchów: krótkie (short), szerokie (wide) oraz (long), których reprezentantem jest AnsiString. Możemy ponadto posługiwać sie standardowym typem Pascala jakim jest String, należy jednak pamiętać, że przy włączonych ustawieniach domyślnych Builder interpretuje ten typ łańcucha jako AnsiString. Podstawowe typy łańcuchowe przedstawione w tabeli poniżej.

Łańcuchowe typy znakowe Typ SmallString SortString AnsiString WideString

Zawartość 255 znaków 255 znaków ~2^31 znaków ~2^30 znaków

Rozmiar (bajty) Od 1 do 256 Od 2 do 256 Od 4 do 2GB Od 4 do 2GB

Stałe typu łańcuchowego otaczane są podwójnymi apostrofami: AnsiString z= „Imię”; Smallstring a = “Bartek”; System Windows korzysta wyłącznie z łańcuchów zakończonych zerowym ogranicznikiem (tzw. null terminated strings). Aby zapewnić zgodność z funkcjami API, typy AnsiString oraz WideString reprezentują właśnie takie łańcuchy

3.8 Stałe Stałe są w swoim przeznaczeniu bardzo podobne do zmiennych - tyle tylko że są… niezmienne. Używamy ich, aby nadać znaczące nazwy jakimś niezmieniającym się wartościom w programie. Stała to niezmienna wartość, której nadano nazwę celem łatwego jej odróżnienia od innych, często podobnych wartości, w kodzie programu .

Jej deklaracja, na przykład taka: const int STALA = 10; przypomina nieco sposób deklarowania zmiennych – należy także podać typ oraz nazwę. Słówko const (ang. constant – stała) mówi jednak kompilatorowi, że ma do czynienia ze stałą, dlatego oczekuje również podania jej wartości. Wpisujemy ją po znaku równości =. W większości przypadków stałych używamy do identyfikowania liczb - zazwyczaj takich, które występują w kodzie wiele razy i mają po kilka znaczeń w zależności od kontekstu. Pozwala to uniknąć pomyłek i poprawia czytelność programu.

16

Stałe mają też tę zaletę, że ich wartości możemy określać za pomocą innych stałych, na przykład: const int NETTO = 2000; const int PODATEK = 22; const int BRUTTO = NETTO + NETTO * PODATEK / 100; Jeżeli kiedyś zmieni się jedna z tych wartości, to będziemy musieli dokonać zmiany tylko w jednym miejscu kodu – bez względu na to, ile razy użyliśmy danej stałej w naszym programie. I to jest piękne Inne przykłady stałych: const int DNI_W_TYGODNIU = 7; // :-) const float PI = 3.141592653589793; // w końcu to też stała! const int MAX_POZIOM = 50; // np. w grze RPG

3.8 Strumień wejścia Cóż by nam jednak było po zmiennych, jeśli nie mieliśmy skąd wziąć dla nich danych?… Prostym sposobem uzyskania ich jest prośba do użytkownika o wpisanie odpowiednich informacji z klawiatury. Tak też czynimy w aktualnie analizowanym programie – odpowiada za to kod: std::cin >> strImie; Wygląda on podobnie do tego, który jest odpowiedzialny za wypisywanie tekstu w konsoli. Wykonuje jednak czynność dokładnie odwrotną: pozwala na wprowadzenie sekwencji znaków i zapisuje ją do zmiennej strImie. std::cin symbolizuje strumień wejścia, który zadaniem jest właśnie pobieranie wpisanego przez użytkownika tekstu. Następnie kieruje go (co obrazują „strzałki” >>) do wskazanej przez nas zmiennej. Zauważmy, że w naszej aplikacji kursor pojawia się w tej samej linijce, co komunikat „Podaj swoje imię”. Nietrudno domyśleć się, dlaczego – nie umieściliśmy po nim std::endl, wobec czego nie jest wykonywane przejście do następnego wiersza. Jednocześnie znaczy to, iż strumień wejścia zawsze pokazuje kursor tam, gdzie skończyliśmy pisanie – warto o tym pamiętać.

17

Rysunek 3.1. Komunikacja między programem konsolowym i użytkownikiem

Strumienie wejścia i wyjścia stanowią razem nierozłączną parę mechanizmów, które umożliwiają nam pełną swobodę komunikacji z użytkownikiem w aplikacjach konsolowych.

3.8 Operatory Zmienne są przeznaczone do przechowywania danych, natomiast operatory pozwalają na manipulowanie nimi. Za pomocą operatorów wykonujemy obliczenia, sprawdzamy równość wartości, przypisujemy je zmiennym i realizujemy cały szereg wymyślnych operacji. Liczba operatorów zdefiniowanych w C++ jest całkiem spora. Warto zwrócić uwagę na operatory inkrementacji i dekrementacji, których działanie jest nieco inne w zależności od ich położenia w stosunku do argumentu. Operator inkrementacji użyty w formie ++x (umieszczony przed argumentem) zwany jest operatorem preinkrementacji, natomiast w formie x++ (umieszczony po argumencie) nosi nazwę operatora postinkrementacji. Poniżej najczęściej używane operatory języka C++ operatory arytmetyczne: + dodawanie  odejmowanie  mnożenie / dzielenie % reszta z dzielenia

x = y+z; x = y-z; x = y*z; x = y/z;

operatory przypisania: = += = = /= %= &= |=

zwykłe przypisanie x = 2; przypisanie sumy x+=2;  x = x + 2; przypisanie różnicy x=2;  x = x  2; przypisanie iloczynu x=2;  x = x  2; przypisanie ilorazu x /=2;  x = x / 2; przypisanie reszty x%=2; x = x % 2; przypisanie iloczynu bitowego x &=0x02; przypisanie sumy bitowej x |=0x02;

Znak = nie wskazuje tu absolutnie na równość dwóch wyrażeń – jest to bowiem operator przypisania, którego używamy do ustawiania wartości zmiennych . 18

operatory inkrementacji i dekrementacji: zmienna++  inkrementacja zmiennej po wyliczeniu wyrażenia ++zmienna  inkrementacja zmiennej przed wyliczeniem wyrażenia zmienna  dekrementacja zmiennej po wyliczeniu wyrażenia zmienna  dekrementacja zmiennej przed wyliczeniem wyrażenia np. int x, y = 1; x =  y ;  rezultat: x=2, y=2 x = y  ;  rezultat: x=1, y=2 operatory relacyjne: == równe if(x==10) {...} != różne if(x!=10) {…} < mniejsze if(x większe if(x.10) {...} =10) {...}

&& koniunkcja (AND);

||

operatory logiczne: alternatywa (OR);! negacja (NOT)

bitowe operatory logiczne: & ^ >>

bitowa koniunkcja (AND); bitowa różnica symetryczna (XOR); przesunięcie bitów w prawo;

| >n oznacza że z liczby a zabieramy n bitów przesuwając wszystkie bity w prawo. Przy przesunięciu o jeden bit wszystkie bity są przesuwane o jedną pozycję w prawo a najstarszy bit zachowuje swoją dotychczasowa wartość. Operacja ta odpowiada podzieleniu liczby przez 2n i zaokrągleniu wyniku w dół. Ma to znaczenie w liczbach ujemnych. Np. a = 80>>2 = (01010000)U2>>2 = (00010100)U2 = 20 b =127>>5 = (01111111)U2>>5 = (00000011)U2 = 3 c = -15>>2 = (11110001)U2>>2 = (11111100)U2 = -4 d = -15/4 = -3. bitowe przesunięcie w prawo. Zapis f = a>>n oznacza że z liczby a zabieramy n bitów przesuwając wszystkie bity w prawo. Przy przesunięciu o jeden bit wszystkie bity są przesuwane o jedną pozycję w prawo a najstarszy bit zachowuje swoją dotychczasowa wartość. Operacja ta odpowiada podzieleniu liczby przez 2n i zaokrągleniu wyniku w dół. Ma to znaczenie w liczbach ujemnych. Np. a = 80>>2 = (01010000)U2>>2 = (00010100)U2 = 20 b =127>>5 = (01111111)U2>>5 = (00000011)U2 = 3 c = -15>>2 = (11110001)U2>>2 = (11111100)U2 = -4 d = -15/4 = -3. operatory jednoargumentowe wyłuskanie & adres obiektu (referencja)

int x=*y; int* x =&y;

operatory związane z klasami i strukturami :: operator zakresu Klasa::funkcja(); -> operator dostępu pośredniego Klasa->funkcja(): . operator dostępu bezpośredniego Klasa.funkcja();

20

Priorytety operatorów w języku C: Operator Opis Przykład () wywołanie funkcji sin() [] element tablicy tab[10] . element struktury osoba.nazwisko  wskazanie elementu struktury wsk_osobynazwisko ! negacja logiczna if( ! (x max) ) kontynuuj; ~ negacja bitowa ~(001101)  (110010)  zmiana znaku (negacja) x = 10  ( y)  inkrementacja (zwiększenie o 1) x    y  (x )  y  dekrementacja (zmniejszenie o 1)   y    y   ( y) & operator referencji (adres elementu) wsk_x = &x  operator dereferencji wsk_x = 10 (type) zmiana typu (typecast ) (double) 10  10.0 sizeof rozmiar zmiennej lub typu (w bajtach sizeof( int )  2  mnożenie / dzielenie % operacja modulo (reszta z dzielenia) if( x%2 == 0 ) parzyste;  dodawanie  odejmowanie  przesunięcie bitowe w lewo 1  2  (0001)  2  (0100)  przesuniecie bitowe w prawo x  4 1  x2  mniejszy niż if( liczba  max ) max  liczba;  mniejszy lub równy  większy niż  większy lub równy  równy ! nierówny (różny od) & iloczyn bitowy ^ suma bitowa modulo (różnica symetryczna) | suma bitowa && iloczyn logiczny || suma logiczna ?: wyrażenie warunkowe  przypisanie  / %  przypisania arytmetyczne    & ^ | , operator przecinka Nawiasy chronią przed trudnymi do wykrycia błędami związanymi z pierwszeństwem operatorów, dlatego stosuj je w przypadku każdej wątpliwości co do kolejności działań.

Przykład:

int x1, y2, z3, wynik4 ; //rezultat: x=1, y=1, z=4, wynik=24

21

wynik  ++xx+y%++z; (???) wynik   (++x)  (x) + (y) % (++z); wynik  ((++x))  (x) + ((y)) % (++z); wynik  (((++x))(x)) + (((y))%(++z));

4 Tablice Do organizacji większych ilości danych służą w C++ tablice. Tablica reprezentuje grupę wartości danego typu (jednolitego) typu, do których dostęp jest realizowany poprzez podanie numeru wartości w grupie. Przykładowo, aby zapamiętać gdzieś w programie 5 liczb całkowitych, możesz użyć tablicy zawierającej pięć wartości typu int deklarowanej w sposób: int mojaTablica[5]; Organizacje pamięci przydzielonej tablicy pokazuje rysunek poniżej. Poniżej pojedyncza liczba typu int zajmuje cztery bajty, całkowita długość tablicy wyniesie 20 bajtów. Symbol bazowy adres_bazowy oznacza tu adres miejsca pamięci, w którym zaczyna się pierwsza wartość.

mojaTablica[0]

mojaTablica[1]

Adres_bazowy

Adres_bazowy+4

mojaTablica[2]

mojaTablica[3]

Adres_bazowy+8 Adres_bazowy+12

mojaTablica[4]

Adres_bazowy+16

Rys.4.1. Organizacja pamięci dla tablicy pięciu liczb typu int Jak widać z powyższego przykładu tablice indeksowane są od zera. Zadeklarowana w ten sposób tablicę można wypełnić wartościami używając w tym celu operatora indeksowania [ ]: mojaTablica [0]=-200; mojaTablica[1]=-100; mojaTablica[2]=-0; mojaTablica[3]=100; mojaTablica[4]=200; Ten sam operator wykorzystuje się aby odwołać się do pojedynczego elementu tablicy gdzieś w programie: int wynik=mojaTablica[3]+mojaTablica[4]; //wynik będzie równy 300 Jeśli się nie lubi dużo pisać, to istnieje możliwość połączenia deklaracji i inicjalizacji tablicy (wypełnienie jej wartościami) w jedną operację: int mojaTablica[5]={-200,-100,0,,100,200;} Znając wartości wszystkich elementów inicjalizujących, można pominąć specyfikację rozmiaru tablicy, co da w efekcie deklarację: int mojaTablica[]={-200,-100,0,100,200};

22

Deklaracja taka jest możliwa, gdyż kompilator ustala rozmiar tablicy na podstawie liczby jej wartości inicjalizujących ( w naszym przypadku 5), znają typ elementu (int) potrafi prawidłowo przydzielić odpowiedni obszar pamięci. Nic nie stoi na przeszkodzie tworzeniu tablic wielowymiarowych. Przykładowa deklaracja tablicy dwuwymiarowej ma postać: int mojaTablica[3][5]; i powoduje przydzielenie przez kompilator pamięci dla 15 liczb typu int, czyli bloku 60 bajtów. Odwołanie do wybranego elementu takiej tablicy realizowane jest tak jak poprzednio, z tą różnicą, że trzeba określić dwa indeksy zamiast jednego. int result=mojaTablica[1][1]=mojaTablica[1][2] Organizację tablicy dwuwymiarowej pokazuje rysunek 4.2 MojaTablica[0][]

MojaTablica[][0]

MojaTablica[0][1]

MojaTablica[][2]

adres_bazowy

adres_bazowy+4

adres_bazowy+8

MojaTablica[1][] adres_bazowy+20

adres_bazowy+24

adres_bazowy+28

MojaTablica[2][]

adres_bazowy+44

adres_bazowy+48

adres_bazowy+40

MojaTablica[][3]

MojaTablica[][4]

Rys.4.2. Organizacja pamięci dla tablicy dwuwymiarowej C++ daje programiście do ręki narzędzie równie potężne co niebezpieczne – możliwość bezpośredniego, nie poddanego żadnym restrykcjom dostępu do pamięci. Próba zapisania wartości do komórki pamięci znajdującej się poza przydzielonym programowi obszarem, nie jest sygnalizowane podczas kompilacji, chociaż jej skutki na ogół bywają fatalne. Efekt ten często spowodowany jest próbą zapisania wartości poza końcem tablicy, jak w przykładzie: int array[5]; array[5]=10; Wykonanie tej instrukcji może spowodować zawieszenie się programu lub nawet zdestabilizować system operacyjny. Odwołania podobne do opisanego są nie należą do rzadkości i biorą się z przeoczenia prostego fakt, że tablice zawsze są indeksowane od zera.

3.8

Tablice znakowe

Język C++ nie definiuje typu łańcuchowego, odpowiadającego zmiennej tekstowej. Ciągi (łańcuchy znaków są reprezentowane w C i C++ przez tablice złożone z elementów typu char. deklaracja tablicy znakowej i jej deklaracja może mieć postać: char text[]=”To jest napis.”; Deklaracja taka powoduje zarezerwowanie bloku pamięci o długości 15 bajtów, w którym zostaje zapisany tekst. W powyższym przykładzie tekst posiada 14 znaków, ale tablica ma długość o jeden większą. Dodatkowy bajt jest przeznaczony dla znacznika końca łańcucha, którym w C++ jest znak o kodzie 0.

23

Znacznikiem końca łańcucha (ang. terminatting null) jest w C i C++ znak o kodzie 0, zapisywany symbolicznie w postaci \0. Z tego powodu łańcuchy używane w C++ często są określane mianem łańcuchów zakończonych zerem lub ASCIIZ (ASCII-zero).

Wystąpienie wartości 0 w tablicy znaków jest interpretowane jako zakończenie zawartego w niej łańcucha i przerywa jego przetwarzanie. Aby się o tym przekonać wpisz program: //--------------------------------------------------------------------------#include #include #include #pragma hdrstop //--------------------------------------------------------------------------// Zmienne argc i argv są ujęte w znaki komentarza dla uniknięcia ostrzeżeń kompilatora int main(int /*argc*/, char /***argv*/) { char str[] = "To jest napis."; cout