Wprowadzenie do programowania

Slides:



Advertisements
Podobne prezentacje
Tablice 1. Deklaracja tablicy
Advertisements

C++ wykład 2 ( ) Klasy i obiekty.
Język C/C++ Funkcje.
Programowanie obiektowe
Mgr inż.Marcin Borkowski Podstawy Java Krótkie wprowadzenie dla studentów Wyższej Szkoły Ekologii i Zarządzania
Wzorce.
Język ANSI C Funkcje Wykład: Programowanie komputerów
Wprowadzenie do C++ Zajęcia 2.
Języki programowania C++
PROGRAMOWANIE STRUKTURALNE
ALGORYTM Co to jest algorytm?
Informatyka Stosowana
formatowanie kodu źródłowego
Materiały do zajęć z przedmiotu: Narzędzia i języki programowania Programowanie w języku PASCAL Część 7: Procedury i funkcje © Jan Kaczmarek.
Programowanie imperatywne i język C Copyright, 2004 © Jerzy R. Nawrocki Wprowadzenie.
Programowanie imperatywne i język C
Programowanie imperatywne i język C Copyright, 2004 © Jerzy R. Nawrocki Wprowadzenie.
Struktury.
Tablice.
1 Dygresja: cztery płyty główne…. 2 Dygresja: osobliwości C /* cos o nieistniejacym typie Boolean */ /* oraz o operatorze przecinkowym */ #include int.
Język ANSI C Operacje we/wy
Wykład 2 struktura programu elementy języka typy zmienne
Algorytmy i struktury danych
Wstęp do programowania obiektowego
Wstęp do interpretacji algorytmów
Dr Anna Kwiatkowska Instytut Informatyki
Zapis informacji Dr Anna Kwiatkowska.
Budowa algorytmów Algorytm: skończony ciąg operacji wraz z ściśle sprecyzowanym porządkowaniem ich wykonywania, które po realizacji dają rozwiązanie dowolnego.
ALGORYTMY.
Programowanie w języku Fortran 95
Podstawy programowania
Podstawy programowania II
POJĘCIE ALGORYTMU Pojęcie algorytmu Etapy rozwiązywania zadań
Algorytmy.
Podstawy programowania II Wykład 2: Biblioteka stdio.h Zachodniopomorska Szkoła Biznesu.
opracowanie: Agata Idczak
Podstawy układów logicznych
Podstawy programowania w języku C i C++
Podstawy programowania. Język C i C++– podstawy Temat: 1
Podstawy programowania w języku C i C++
Prowadzący: Dr inż. Jerzy Szczygieł
TABLICE C++.
Informatyka I - Wykład ANSI C
Podstawy programowania w języku C i C++
Jerzy F. Kotowski1 Informatyka I Wykład 14 DEKLARATORY.
STEROWANIE Ale nie tylko
Algorytmy.
Elżbieta Fiedziukiewicz
Podstawy informatyki 2013/2014
PWSZ Gniezno // codefly 2009 Łukasz Tomczak
Projektowanie stron WWW
Podstawy języka Instrukcje - wprowadzenie
ZAPIS BLOKOWY ALGORYTMÓW
Algorytmika.
ALGORYTMY Co to jest algorytm ? Cechy algorytmu Budowa algorytmów
Treści multimedialne - kodowanie, przetwarzanie, prezentacja Odtwarzanie treści multimedialnych Andrzej Majkowski informatyka +
Programowanie strukturalne i obiektowe C++
WYKŁAD 3 Temat: Arytmetyka binarna 1. Arytmetyka binarna 1.1. Nadmiar
Diagram aktywności (czynności)
Zapis blokowy algorytmów
Podsumowanie wiedzy MPDI2 sem.3 INFORMATYKA. tworzenie nowego pliku i katalogu, nawigacja po katalogach, listowanie zawartości katalogu, zmiana nazw,
Wstęp do interpretacji algorytmów
Pętle – instrukcje powtórzeń
Wstęp do programowania Wykład 1
Wstęp do programowania Wykład 2 Dane, instrukcje, program.
 Formuła to wyrażenie algebraiczne (wzór) określające jakie operacje ma wykonać program na danych. Może ona zawierać liczby, łańcuchy znaków, funkcje,
Algorytmy, sposoby ich zapisu.1 Algorytm to uporządkowany opis postępowania przy rozwiązywaniu problemu z uwzględnieniem opisu danych oraz opisu kolejnych.
Programowanie strukturalne i obiektowe Klasa I. Podstawowe pojęcia dotyczące programowania 1. Problem 2. Algorytm 3. Komputer 4. Program komputerowy 5.
Zrozumieć, przeanalizować i rozwiązać
POJĘCIE ALGORYTMU Wstęp do informatyki Pojęcie algorytmu
Zapis prezentacji:

Wprowadzenie do programowania dr inż. Zbigniew Wesołowski zw@isi.wat.edu.pl

Cel przedmiotu Podstawowym celem przedmiotu jest nauczyć podstaw programowania strukturalnego z języku C.

Wybrane zagadnienia z programu przedmiotu Pojęcia podstawowe technik programowania Elementarne wprowadzenie do algorytmiki Zasada dekompozycji i abstrakcji Typy danych w C Instrukcje języka C Liniowe struktury danych Funkcje Rekursja Strumienie i pliki

Bezpośrednie powiązania przedmiotu z innymi przedmiotami Algorytmy i struktury danych Programowanie obiektowe Programowanie współbieżne Bazy danych Systemy operacyjne i język C Technologie portali internetowych

Literatura obowiązkowa Kernigham B.W., Ritchie D.M. (2003): Język ANSI C. WNT, Warszawa. Delannoy C. (1993): Ćwiczenia z języka C. WNT, Warszawa. Kernigham B.W., Pike R.(2002) : Lekcja programowania. WNT, Warszawa. Lippman S.B., Lajoie J.(2001): Podstawy języka C++. WNT, Warszawa. Tondo C.L., Leung B.P.(2003): Język ANSI C. Ćwiczenia i rozwiązania. WNT, Warszawa. Tondo C.L., Leung B.P.(2001): Podstawy języka C++. Ćwiczenia i rozwiązania. WNT, Warszawa. Wirth N.(2002): Algorytmy + struktury danych = programy. WNT, Warszawa.

Literatura zalecana Drozdek A., Simon D. L. (1996): Struktury danych w języku C. WNT, Warszawa. Knuth D.E. (2002): Sztuka programowania. Tomy 1,2,3. WNT, Warszawa. Hunt A., Thomas D. (2002): Pragmatyczny programista. Od czeladnika do mistrza. WNT, Warszawa. Stroustrup B. (2000): Język C++. WNT, Warszawa. Borowik W., Borowik B. (2001): Meandry języka C++. MIKOM, Warszawa. Lippman S.B.: Model obiektu w C++. WNT, Warszawa. Bentley J. (2001): Perełki oprogramowania. WNT, Warszawa.

Literatura dla zawansowanych Meyers S.(1998): Język C++ bardziej efektywny. WNT, Warszawa. Lippman S.B. (1999): Model obiektu w C++. WNT, Warszawa. Plauger P.J.(1997): Biblioteka standardowa C++. WNT, Warszawa. B.Stroustrup (1997): Projektowanie i rozwój języka C++. WNT, Warszawa. Sutter H.(2002): Wyjątkowy język C++. WNT, Warszawa.

Dwugłos o C/C++ Języki programowania dzielą się na takie, które wszyscy krytykują, oraz takie, których nikt nie używa. Przypisywane Bjarne Stroustrupowi W C kodujemy nasze własne błędy, natomiast w C++ możemy je odziedziczyć. Przypisywane prof. G.Karamowi z Uniwersytetu w Carleton:

Dlaczego C? W językach C i C++ zapisano niemal wszystkie liczące się w świecie systemy operacyjne i ich oprzyrządowanie programowe. Andrew Tanenbaum wręcz stwierdza: oprogramowanie podstawowe pisze się w C („...real operating systems are often written in C, rarely in Modula 2 and never in Pascal” [A.S.Tanenbaum, Modern Operating Systems, s.36. Englewood Cliffs, NJ, Prentice-Hall 1992]).

Pojęcia podstawowe

Nauka NAUKA, w polskiej tradycji, to przyswajanie jakichkolwiek treści (wiedzy), nabywanie umiejętności oraz to, czego się uczy lub naucza. W krajach zachodnioeuropejskich, angielskie i franc. science wywodzące się z łac. scientia (scio 'wiem') to wiedza poddana ustalonym wymaganiom treściowym i metodologicznym, ale niekoniecznie przeznaczona do nauczania. W językach i kulturach pozaeuropejskich nauka może mieć całkiem inne określenia, które do nas nie dotarły ze względu na bariery kulturowe.

Dziedziny i dyscypliny naukowe Naukę dzielimy na dziedziny nauki nauki techniczne nauki matematyczne, nauki przyrodnicze, nauki społeczne, .... Dziedziny dzielimy na dyscypliny naukowe. Architektura Budowa maszyn Inżynieria lądowa Informatyka

Informatyka Informatyka (ang. computer science, informatics) jest to dyscyplina naukowa w dziedzinie nauk technicznych zajmująca się gromadzeniem, przechowywaniem, przetwarzaniem i wizualizacją informacji za pomocą elektronicznych maszyn cyfrowych.

Informacja (Encyklopedia PWN) INFORMACJA [łac.] to obiekt abstrakcyjny, który w postaci zakodowanej ( dane) może być przechowywany (na nośniku danych), przesyłany (np. głosem, falą elektromagnetyczną, prądem elektrycznym), przetwarzany (w trakcie wykonywania algorytmu) i użyty do sterowania (np. komputerem steruje program, będący zakodowaną informacją).

Definicja informacji stosowana w teorii informacji Informacja to miara niepewności zajścia pewnego zdarzenia spośród skończonego zbioru zdarzeń możliwych.

Informacja Informacja jest pojęciem pierwotnym, płynnym i otwartym na wiele różnorodnych interpretacji. Jedną z najbardziej ogólnych definicji tego pojęcia przedstawili Schizias i Lochovsky. Według nich informacja to każdy czynnik zwiększający zasób naszej wiedzy.

Dane Dane to skwantyfikowana, mierzalna informacja.

Instancja Instancja (ang. instance), zwana też wystąpieniem, jest nazwanym obszarem pamięci operacyjnej. Termin ten wyraża związek konkretnego bytu z jego definicją (deklaracją) lub typem.

Zmienna W informatyce zmienna to opatrzony ustaloną, jednoznaczną nazwą instancja, której mogą być nadawane (przypisywane) różne wartości określonego typu (np. liczbowe, logiczne).

Zasięg zmiennej Zasięg zmiennej (ang. variable scope) określa miejsce i czas, w których możliwe jest odczytanie wartości zmiennej i jej modyfikacja. Wyróżnia się dwie techniki określania zasięgu zmiennych: Technika statycznego określania zasięgu Technika dynamicznego określania zasięgu  

Statyczny zasięg zmiennej W technice statycznej, zasięg zmiennej wynika z umiejscowienia jej nazwy w programie. Przykładowo, jeśli nazwa zmiennej zostanie umiejscowiona w pewnej funkcji, to jej zakres dotyczy tylko tej funkcji i funkcji w niej zagnieżdżonych. Przykład int a; // zmienna globalna o zasięgu statycznym   int f(void) { int b; // zmienna lokalna o zasięgu statycznym return 0; }

Dynamiczny zasięg zmiennej Dynamiczne określenie zasięgu zmiennej polega na czasowym przesłonięciu nazwy zmiennej o zasięgu statycznym. Przykład int a; // zmienna globalna o zasięgu leksykalnym   int f(void) { int a; // zmienna lokalna o zasięgu dynamicznym return 0; }

Nazwy zmiennych w programie Dobrze jest stosować rzeczowniki dla oznaczania nazw wszelkich danych (np. licznik), czasowniki dla funkcji i procedur (np. dziel, dodaj), przymiotniki dla zmiennych logicznych (np. prawdziwy). Jednolite nazwy są mało czytelne. Najlepiej jest używać nazw, które coś oznaczają, powiadamiając tym samym użytkownika (intencjonalnie) o możliwościach funkcji, czy też wskazując sens zmiennych, itd. Nazwy bardzo długie, chociaż mogą być komunikatywne, są mało praktyczne jeśli nie stosujemy wyróżnień takich jak duże litery czy podkreślenie (underscore) np. nazwa dodajdwieliczby jest mniej czytelna od nazwy DodajDwieLiczby lub od nazwy dodaj_dwie_liczby.

Wielkości stałe Należy nadawać nazwy wielkościom stałym (wykorzystując np. kwalifikator const ), a szczególnie tzw. liczbom magicznym (np. rozmiary tablic).

Oprogramowanie Oprogramowanie (ang. software) to zestaw instrukcji oraz danych przeznaczonych do wykonania dla komputera. Oprogramowanie występuje w dwóch postaciach: postać źródłowa, przeznaczona do przygotowania i obróbki przez ludzi, głównie programistów postać binarna, przeznaczona do wykonywania przez komputery, choć użytkownikiem jej działania może również być człowiek Programy przekształcające oprogramowanie z postaci źródłowej na binarną to kompilatory. Niektóre oprogramowanie, np. napisane w całości w językach interpretowanych, może występować tylko w jednej postaci, spełniającej oba zadania.

Czym jest programowanie? Programowaniem nazywamy ciąg wszystkich czynności związanych z rozwiązywaniem problemów przy użyciu komputera.

Historia języków programowania Fortran LISP Algol 1960 Simula 1970 Smalltalk-72 Prolog Pascal C Smalltalk-76 1980 Ada Objective C Object Pascal C++ Eiffel 1990 Ada 95 Java

Przyczyny zróżnicowania języków programowania Nowe osiągnięcia sprzętowe, np. komputery wieloprocesorowe umożliwiające równoległe i współbieżne wykonywanie programów Nowe i zróżnicowane dziedziny zastosowań.

Wprowadzenie do notacji Backusa-Naura (BNF- Backus-Naur Form)

Notacja BNF Notacja Backhusa-Naura jest formalizmem pozwalającym wyrazić zarówno składnię, tzn. zasady budowania słów i zdań jak również semantykę, tzn. zasady interpretacji różnorodnych konstrukcji służących m.in. do definiowania języków programowania. W notacji BNF każda konstrukcja języka, zwana kategorii składniowa, jest opisana za pomocą reguły lub kilku reguł. Reguły BNF mają następującą postać składającą się z czterech części (wliczając znak równości i kropkę): LHS = RHS. LHS (lewa strona reguły, ang. left-hand side) jest symbolem słownym oznaczającym nazwę definiowanej kategorii składniowej. Z kolei RHS (right-hand side) jest ciągiem składającym się z symboli reprezentujących różne kategorie definiowanego języka, z tak zwanych symboli terminalnych ujętych w cudzysłowy, oraz z wymienionych niżej operatorów BNF. Prawa strona reguły określa formę, jaką mogą przyjmować definiowana kategoria składniowa (jeśli występuje dla niej kilka różnych reguł, to może ona przyjąć dowolną z określonych przez te reguły postaci).

Operatory BNF Operatory BNF mogące występować w prawej stronie reguł (RHS) są następujące: alternatywa - zapisana symbolem ‘|’ oznacza, że konstrukcja może wystąpić w jednej dwóch, lub więcej, form; konkatenacja - operator domyślny, przyjmowany w przypadku ciągu symboli wypisanych jeden po drugim, oznacza, że definiowana konstrukcja może przyjąć formę będącą takąż sekwencją form reprezentowanych przez te symbole; opcja - zapisana nawiasami kwadratowymi ‘[‘ ‘]’ oznacza, że zawartość tych nawiasów może wystąpić w całości lub w ogóle nie wystąpić w opisywanej formie; powtórzenie - zapisane nawiasami klamrowymi ‘{‘ ‘}’, oznacza, że zawartość tych nawiasów może wystąpić w całości powtórzona dowolną ilość razy (0 lub więcej); grupowanie - zapisane zwykłymi nawiasami ‘(‘ ‘)’ nie ma żadnego specjalnego znaczenia w regułach BNF i służy w nich do zmiany interpretacji RHS ze względu na priorytety operatorów.

Przykład Liczba całkowita. Poniższy zestaw reguł BNF określa pojęcie liczby całkowitej. Liczba_calkowita = [znak-liczby] liczba_calkowita_bez_znaku. znak-liczby = ‘+’ | ‘-‘ . liczba_calkowita_bez_znaku = ciag_cyfr. ciag_cyfr = cyfra {cyfra}. cyfra = ‘0’ | ‘1’ | ‘2’ | ‘3’ | ‘4’ | ‘5’ | ‘6’ | ‘7’ | ‘8’ | ‘9’. Przykłady liczb całkowitych: 0; +123; -45.

Przykład Liczba zmiennopozycyjna. Poniższy zestaw reguł BNF określa pojęcie liczby zmiennopozycyjnej. liczba_ zmiennopozycyjna = [znak_liczby] liczba_ zmiennopozycyjna _bez_znaku. liczba_ zmiennopozycyjna_bez_znaku = liczba-calkowita-bez-znaku ‘.’ ciag-cyfr [ „e” mnoznik_skalujacy ] | liczba_calkowita_bez_znaku ‘e’. mnoznik-skalujacy . mnoznik-skalujacy = liczba_calkowita . Przykłady liczb rzeczywistych: 0.0; +1.23; 4e5; -0.025e-5.

Forma i znaczenie Zwróćmy uwagę w powyższych przykładach reguł BNF na fakt, że do obydwu kompletach reguł załączone są, wyjaśnienia słowne sensu tych reguł. Same reguły wyjaśniają, tylko formę, nie dotyczą zaś w żaden sposób znaczenia. Taka jest rola gramatyki - określa ona poprawną formę zdań języka, lecz nie daje informacji, które zdania są sensowne, a które nie, i jakie jest ich znaczenie. Jednym z często przytaczanych przykładów zdania ilustrującego to zjawisko jest: ”Bezbarwne zielone pomysły śpią wściekle”. Zdanie to, jakkolwiek niezrozumiałe i bezsensowne, jest całkowicie poprawne i zgodne z gramatyką języka polskiego.

Diagramy syntaktyczne

Identyfikator Przykłady: A A1 Mnoznik litera cyfra

Litera Przykłady: a b A B .... a b Z _

Cyfra Przykłady: 9 .... 1 8 9

Paradygmaty programowania

Najważniejsze paradygmaty programowania W celu usystematyzowania procesu programowania powstało wiele technik tworzenia programów określanych łącznie terminem programowania systematycznego. Obecnie uważa się, że najważniejszymi z nich są: programowanie proceduralne (ang. procedural programming) programowanie strukturalne (ang. structured programming) programowanie zorientowane obiektowo (ang. object-oriented programming) - w C nie ma mechanizmów wspierających ten paradygmat

Programowanie proceduralne (ang. procedural programming) Programowanie proceduralne jest metodą tworzenia oprogramowania bazująca na koncepcji jednostek programowych (modułów) i zasięgu zmiennej. Program proceduralny składa się z jednej lub wielu modułów. Każdy moduł składa się z jednej lub wielu procedur (funkcji, podprogramów, metod). Procedury mogą być zagnieżdżone. Każdy program może posiadać jeden lub więcej poziomów lub zakresów zawierających bloki. Na każdym poziomie mogą być definiowane zmienne niewidoczne z innych poziomów.

Programowanie strukturalne

Podstawy teoretyczne programowania strukturalnego Podstawy teoretyczne programowania strukturalnego dali C. Bohm i G. Jocopini. W roku 1966 wykazali, że wszystkie problemy obliczeniowe dają się doprowadzić do równoważnej postaci, w której występują tylko trzy struktury algorytmiczne: Sekwencje Wybór (selekcje) Powtórzenia (iteracje)

List Dijkstry W marcu 1968 r. w czasopiśmie Communication of the ACM (11,3, pp.147-148) został opublikowany słynny obecnie list prof. E.W. Dijkstry, pt. Uznając GOTO za szkodliwe. Autor omówił w nim anomalie zdania goto, skrytykował jego zły wpływ na czytelność i niezawodność programów oraz zaproponował usunięcie go ze wszystkich języków programowania. Na podstawie dowodu Bohm’a i Jocopini’ego Dijkstra stworzył on w latach 1968-1972 podstawy programowania strukturalnego.

Programowanie strukturalne (ang. structured programming) Programowanie strukturalne to technika tworzenia programów bazująca na zasadach programowania proceduralnego. Najbardziej znanymi podejściami są: Programowanie strukturalne Jackson’a , bazujące na uporządkowanych strukturach danych Programowanie strukturalne Dijkstra’y , bazujące na podziale programu na podsekcje, z których każda posiada pojedynczy punkt wejścia i pojedynczy punkt wyjścia. Generalnie programowanie strukturalne polega na podziale kodu na małe jednostki (funkcje), realizujące proste, pojedyncze zadania.

Elementarny wstęp do algorytmiki

Literatura Banachowski L., Diks K., Ryter W.(2003): Algorytmy i struktury danych. WNT, Warszawa. Cormen T.H., Leiserson Ch. E., Rivest R.L. (2002): Wprowadzenie do algorytmów. WNT, Warszawa. Wirth N.(2002): Algorytmy + struktury danych = programy. WNT, Warszawa. Harel D. (2001): Rzecz o istocie informatyki. Algorytmika. WNT, Warszawa.

Algorytmika Algorytmika ta nauka o algorytmach (Harel D. (2001): Rzecz o istocie informatyki. Algorytmika. WNT) – obszarze ludzkich dociekań, wiedzy i doświadczeniach w zakresie budowy algorytmów. Harel pisze, że algorytmika „tkwi w centrum wszystkich działów informatyki i ... można o niej powiedzieć, że jest ważna dla większości nauk matematyczno-przyrodniczych, ekonomii i techniki”.

Etymologia słowa „algorytm” Pojęcie algorytm pochodzi od nazwiska arabskiego matematyka Muhammeda ibn Musy al-Chorezmi (z Chorezmu), który opisał system dziesiętnego kodowania liczb i sztukę liczenia w tym systemie (ok. roku 820 n. e.)

Definicje algorytmu Algorytm jest pojęciem abstrakcyjnym. Jest to dokładny przepis podający sposób rozwiązania określonego zadania w skończonej liczbie kroków. Algorytm - informatyczny opis rozwiązania zadania [Turski] Obecnie przez algorytm rozumie się opis danych wraz z opisem czynności, które należy wykonać z tymi danymi, aby osiągnąć określony cel.

Rodzaje algorytmów Rozróżnia się rodzaje algorytmów: numeryczne, operujące na liczbach (np. algorytm Euklidesa) nienumeryczne, operujące na danych nieliczbowych (np. algorytm sortowania lub wyszukiwania dokumentów w dużych zbiorach). Algorytm jest sekwencyjny, gdy kolejność czynności jest określona jednoznacznie, a niesekwencyjny (równoległy, współbieżny), gdy porządek ten nie jest określony między pewnymi operacjami.

Najważniejsze cechy algorytmów (1/2) możliwość wyrażania algorytmu w różnych językach, np. języku naturalnym, w językach programowania, poprzez sieć działań; algorytm zapisany w języku programowania nazywa się programem możliwość wyrażania algorytmu przy pomocy skończonej liczby symboli, bez używania zwrotów odwołujących się do analogii, jak "itd." realizowalność, tzn. fizyczna możliwość wykonania poleceń możliwość wielokrotnego wykonywania (na ogół z różnymi danymi); pojedyncze wykonanie algorytmu nazywa się procesem; wyznaczone przezeń działania są sformułowane na poziomie elementarnym, uwzględniającym możliwości realizacyjne

Najważniejsze cechy algorytmów (2/2) Algorytm jest statycznym opisem procesu rozwiązania zadania. Dynamikę tego procesu osiąga się poprzez realizację programu będącego zakodowaną postacią algorytmu. Algorytm może być krótki, lub opisywać długi (nawet niekończący się) proces. Przykładów dostarcza statystyka zajmująca się opracowywaniem dużych zbiorów danych.

Formy zapisu i prezentacji algorytmów Metody słowne Metody graficzne

Metody opisowe (słowne) Metody te oparte są na: języku etnicznym lub branżowym języku formalnym, np. języku programowania

Metody graficzne (wykreślne) tablice decyzyjne sieci działań (schematy blokowe, schematy wykonawcze) strukturo-programy, zwane schematami Nassi-Schneidermana (N-S) schematy czynnościowe (w tym schematy obiegu dokumentów) schematy przetwarzania (systemów przebiegu) technika grafów tablice krzyżowe matryce sprzężeń

Sieci działań

Definicja sieci działań SIEĆ DZIAŁAŃ to graficzne przedstawienie przepisu rozwiązania zadania (algorytmu) w postaci tzw. skrzynek zawierających polecenia poszczególnych czynności, połączonych strzałkami wskazującymi kolejność ich wykonywania. Podstawowe rodzaje skrzynek: skrzynki krańcowe (skrzynki początkowe i skrzynki końcowe) skrzynki warunkowe skrzynki wyboru skrzynki bezwarunkowe (skrzynki operacyjne) skrzynki wejścia-wyjścia skrzynki kolekcyjne skrzynki łącznikowe skrzynki podprogramów

Skrzynki krańcowe Są dwa rodzaje skrzynek krańcowych. skrzynka początkowa (START) pokazuje miejsce rozpoczęcia wykonywania algorytmu. skrzynka końcowa (STOP) pokazuje miejsce lub miejsca zakończenia wykonywania algorytmu. START STOP

Skrzynki warunkowe Skrzynki warunkowe zawierają polecenia sprawdzenia wartości określonych wyrażeń logicznych. Wyrażenie logiczne? Tak Nie

Skrzynki wyboru Skrzynki wyboru obrazują wybór jednego z możliwych wariantów w zależności od wartości zmiennej lub wyrażenia Zmienna = (Wyrażenie =) w1 w2 ... wi ... wn

Skrzynki bezwarunkowe (skrzynki operacyjne) Skrzynki bezwarunkowe zawierają operacje inne niż sprawdzenie warunków logicznych. Operacja

Skrzynki wejścia-wyjścia Skrzynki wejścia-wyjścia obrazują operacje wprowadzania i wypisywania danych. Wprowadź

Skrzynki kolekcyjne Skrzynka kolekcyjna łączy dwie różne drogi algorytmu.

Skrzynki łącznikowe Wyróżnia się dwa rodzaje skrzynek łącznikowych: skrzynki łącznikowe na stronie obrazują przejście pomiędzy fragmentami algorytmu w ramach tej samej strony skrzynki łącznikowe międzystronicowe obrazują przejście pomiędzy fragmentami algorytmu znajdującymi się na różnych stronach Nr Strona | Nr

Skrzynki podprogramów Skrzynki podprogramów obrazuje wywołanie podprogramu i realizowane przez nią zadania. Nazwa podprogramu

Przykład Narysować sieć działań (schemat blokowy) algorytmu obliczającego sumę dwóch liczb całkowitych.

Zadania obliczeniowe

Określenie terminu „zadanie obliczeniowe” Algorytmy określają sposób rozwiązania pewnych rodzajów zadań, zwanych zadaniami obliczeniowymi lub algorytmicznymi. Zbiór danych wejściowych x Zbiór danych wyjściowych y Zadanie obliczeniowe składa się ze: scharakteryzowania dopuszczalnego, być może nieskończonego, zestawu danych wejściowych x; specyfikacji pożądanych wyników y jako funkcji obliczalnej f danych wejściowych Rys. Rozwiązanie algorytmiczne f

Rodzaje poleceń w algorytmach Algorytmy zawierają dwa rodzaje instrukcji (poleceń do wykonania): Instrukcje akcji (czynności), będące zleceniami wykonania działań elementarnych (podstawowych) Instrukcje sterujące, determinujące porządek wykonywania instrukcji czynności.

Problemy informatyki

Podział problemów informatyki Nie wszystkie problemy dają się rozwiązać algorytmicznie. Ogół problemów dzieli się (H. Simon, Newell (1958): Heuristic problem solving. Operations Research, t.4) na: dobrze ustrukturalizowane o nieokreślonej strukturze o słabo określonej strukturze

Problemy dobrze ustrukturalizowane Problemy dobrze ustrukturalizowane dają się przedstawić w formie ilościowej, a ich rozwiązanie – w postaci algorytmu. Do ich rozwiązania stosuje się metody badań operacyjnych (operations research).

Problemy o nieokreślonej strukturze Problemy o nieokreślonej strukturze można je przedstawić w postaci słownego opisu; rozwiązuje się je przy pomocy metod heurystycznych. Istotą metod heurystycznych jest dochodzenie do rozwiązania poprzez sformułowanie hipotezy.

Problemy o słabo określonej strukturze Problemy o słabo określonej strukturze to problemy mieszane o przewadze aspektów jakościowych; do ich rozwiązywania stosowane są metody analizy systemowej. Analiza systemowa jest nauką systemową, której przedmiotem jest postrzeganie i wyjaśnianie istotnych problemów związanych z wyborem decyzji lub linii postępowania.

Zastosowania algorytmów Algorytmy znajdują zastosowanie w różnych dziedzinach działalności człowieka: W informatyce, szczególnie w związku z tworzeniem oprogramowania; W matematyce i naukach przyrodniczych, gdzie przyjmują postać wzorów i formuł; W zarządzaniu, wymagającym posługiwania się różnorodnymi zasadami i metodami; W administracji, w której istotną rolę odgrywa wydawanie i posługiwanie się różnorodnymi przepisami.

Przykład algorytmizacji działań - algorytm wynalazku Altszullera (sposób rozwiązania dowolnego zadania) Faza formułowania zadania P: - wybór i zdefiniowanie P, - uściślenie warunków odnoszących się do P, jak i towarzyszących procesowi jego rozwiązywania. Faza analizy P: analiza uściślonej wersji P. Faza oceny P: - ocena istotności P, - eliminacja sprzeczności technicznych, - (ewentualnie powrót do fazy formułowania zadania P lub do fazy analizy P). Faza syntezy: konstruowanie rozwiązania P. Faza decyzji: - jeżeli rozwiązanie uzyskane w poprzedniej fazie spełnia warunki przyjęte w fazie formułowania zadania P, to przyjęcie tego rozwiązania do realizacji; - w przeciwnym przypadku powrót do fazy formułowania zadania.

Zasady poprawnego programowania

Cechy dobrych programów Poprawność (zgodność z wymogami użytkownika) Niezawodność (zdolność do wspomagania pracy użytkownika) Przenośność (łatwość instalacji na różnych komputerach) Łatwość konserwacji (podatność na zmiany) Czytelność (prostota jest jedną z najważniejszych cech dobrych programów) Efektywne wykorzystanie zasobów (procesor, pamięć) Szybkość

Styl programowania (1/4) Styl programowania to sposób pisania kodu, który będzie czytelny zarówno dla jego twórcy, jak i innych programistów. Dobry styl jest podstawą dobrego programowania. Dobrego stylu programowania można nauczyć się wyłącznie poprzez zdobycie praktyki. Nie ma uniwersalnej reguły gwarantującej pisanie dobrych programów. Istnieje kilka prostych i przydatnych zasad.

Styl programowania (2/4) Należy używać nazw opisowych dla definicji globalnych, krótkich zaś dla lokalnych. Należy programować w sposób jednolity. Używać nazw czynności dla funkcji. Dbać o precyzję. Stosować wcięcia, aby uwidocznić strukturę. Używać naturalnej postaci wyrażeń. Używać nawiasów, aby rozwiać niejasności. Dzielić wyrażenia złożone. Pisz jasno! Należy uważać na efekty uboczne.

Styl programowania (3/4) Stosować jednolity sposób rozmieszczania wcięć i nawiasów klamrowych. Należy używać idiomów. Programując konstrukcje wielokrotnego wyboru, używać instrukcji else-if. Należy wystrzegać się makroinstrukcji grających rolę funkcji. Treść i argumenty makroinstrukcji ujmować w nawiasy. Nadawać nazwy liczbom magicznym. Definiować liczby jako stałe. Używać stałych znakowych zamiast liczb całkowitych. Do obliczania rozmiarów instancji (zmiennych, typów) używać operatorów języka. Nie pisać banałów!

Styl programowania (4/4) Komentować funkcje i dane globalne. Nie komentować złego kodu, lecz go poprawiać. Komentarz nie może być sprzeczny z kodem. Wyjaśniać, nie gmatwać! To samo rób wszędzie tak samo. Zwalniaj zasoby w tej samej warstwie, w której były przydzielone. Objaśnij swój program komuś innemu. Trzymaj się standardu.

Styl akapitowy w C (C indent style, C bracing style) Styl akapitowy określa zasady agregacji kodu w postaci bloków w celu budowy prostej i czytelnej struktury programu. Wyodrębnia się kilka rodzajów stylu akapitowego: Styl BSD/Allman’a Styl Kernel’a/K&R Styl Whitesmiths’a Styl GNU

Styl BSD/Allman’a W stylu tym nawiasów klamrowych używa się po segmentach inicjujących. Przykład while(x == y) { something(); somethingelse(); } Główną zaletą tego stylu jest wyraźne rozdzielenie zawartości bloku od segmentów inicjujących, w powyższym przykładzie od instrukcji while.

Styl Kernel’a/K&R W tym stylu nawias klamrowy otwierający umieszczony jest an tym samym poziomie co segment inicjujący, natomiast nawias klamrowy zamknięty umieszczony jest pod ostatnią instrukcją bloku. Przykład while(x == y) { something(); somethingelse(); } Główną zaletą tego stylu jest podkreślenie tego, że blok jest logicznie związany z segmentem inicjującym. Wadą tego stylu w porównaniu ze stylem BSD jest trudność w lokalizacji początku bloku.

Styl Whitesmiths’a Styl ten jest podobny do BSD: while(x == y) { something(); somethingelse(); } W stylu tym sposób umieszczenia nawiasów klamrowych umożliwia łatwe, wizualnie wydzielenie blok.

Styl GNU Jest to styl pośredni między stylami BSD i Whitesmiths’a.   while(x == y) { something(); somethingelse(); } Nawiasy klamrowe umieszczane są w jednej kolumnie zajmującej pozycję środkową między lewym skajem położeniem, a kolumną, w której pisane są kolejne instrukcje bloku. Styl ten zachowuje zalety stylu BSD i satysfakcjonuje zwolenników stylu Whitesmiths’a.

Kryzys oprogramowania

Początki kryzysu oprogramowania Inżynieria oprogramowania wyrosła z tzw. kryzysu oprogramowania powstałego w latach 60. i 70. XX w., kiedy to wiele projektów programistycznych kończyło się fiaskiem. Wraz ze wzrastającą wszechobecnością komputerów, wzrastało zapotrzebowanie na coraz lepsze oprogramowanie.

Efektywność projektów informatycznych W latach 90. XX w. ok. 90% poważnych firm programistycznych w USA uważa, że często zdarzają się im opóźnienia w realizacji projektów informatycznych. W Polsce co prawda nie przeprowadzono podobnych badań, lecz z pobieżnej obserwacji wynika, że podobne problemy mają polskie firmy softwarowe.

Najważniejsze przyczyny kryzysu oprogramowania (1/2) Duża zawodność systemów komputerowych Duża złożoność SI Niepowtarzalność przedsięwzięć informatycznych Nie przejrzystość procesu budowy oprogramowania, co oznacza że proces ten jest bardziej sztuką niż zalgorytmizowaną działalnością inżynierską Nieprzewidywalność czasu wytwarzania oprogramowania. Pozorna łatwość wytwarzania i dokonywania poprawek w oprogramowaniu Eklektyczny charakter narzędzi i języków programowania Uzależnienia organizacji od systemów komputerowych i przyjętych technologii informatycznych

Najważniejsze przyczyny kryzysu oprogramowania (2/2) Frustracje projektantów oprogramowania i programistów wynikające ze zbyt szybkiego postępu w zakresie języków, narzędzi i metod oraz uciążliwości i długotrwałości procesów produkcji, utrzymania i pielęgnacji oprogramowania. Problemy współdziałania niezależnie zbudowanych modułów oprogramowania. Problemy przystosowania istniejących i działających systemów do nowych wymagań, tendencji i platform sprzętowo-systemowych

Przykład nieudanego projektu programistycznego - IBM OS 360 Jednym z najbardziej spektakularnych przykładów projektów zakończonych niepowodzeniem jest system operacyjny OS 360 dla komputerów IBM 360 Series. Projekt ten był realizowany przez dziesięć lat i był jednym z najbardziej złożonych projektów informatycznych kiedykolwiek realizowanych. I mimo iż składał się z ok. 1000 programów, to nigdy nie spełnił oczekiwań użytkowników. Ostatecznie, w późnych latach 90. ubiegłego wieku, został on włączony do Linux’a.

Kryzys oprogramowania - podsumowanie Kryzys oprogramowania od końca lat 60. Coraz lepszy i tańszy sprzęt  coraz większe potrzeby użytkowników  coraz większe programy Duże programy  dużo błędów -> duże koszty Koniecznością było opracowanie metodologii systematycznej pracy. Narodziny inżynierii oprogramowania. Programowanie funkcjonalne - programowanie deklaratywne - programowanie proceduralne / techniki strukturalne - programowanie modularne - programowanie zorientowane obiektowo – programowanie komponentowi – CASE - UML

Jak walczyć ze złożonością ? Zasada dekompozycji: rozdzielenie złożonego problemu na podproblemy, które można rozpatrywać i rozwiązywać niezależnie od siebie i niezależnie od całości. Zasada abstrakcji: eliminacja, ukrycie lub pominięcie mniej istotnych szczegółów rozważanego przedmiotu lub mniej istotnej informacji; wyodrębnianie cech wspólnych i niezmiennych dla pewnego zbioru bytów i wprowadzaniu pojęć lub symboli oznaczających takie cechy. Zasada ponownego użycia: wykorzystanie wcześniej wytworzonych schematów, metod, wzorców, komponentów projektu, komponentów oprogramowania, itd. Zasada sprzyjania naturalnym ludzkim własnościom: dopasowanie modeli pojęciowych i modeli realizacyjnych systemów do wrodzonych ludzkich własności psychologicznych, instynktów oraz mentalnych mechanizmów percepcji i rozumienia świata.

Język C

Język C Język C został zaprojektowany przez Dennisa Ritchie’go w latach 60. XX wieku w celu pisania oprogramowania systemu UNIX.

Główne cechy języka C Skupia się na realizacji paradygmatów programowania proceduralnego z udogodnieniami stylu programowania strukturalnego. Dostęp do oprogramowania niskopoziomowego poprzez wykorzystanie wskaźników do lokalizacji obszarów pamięci zajętych przez to oprogramowanie. Stosowanie standardowych funkcji bibliotecznych umożliwiających rozwiązanie typowych zadań. Wykorzystanie języka preprocesora umożliwiających definiowanie makr, włączanie różnych plików źródłowych oraz wykonywanie kompilacji warunkowej. Statyczny, leksykalny zasięg zmiennych. Stosowanie operatorów o złożoności O(1).

Historia C 1968 – 1973 okres tworzenia C 1973 rekomiplacja jądra UNIX napisanego w C 1978 Dennis Ritchie and Brian Kernighan publikują książkę The C Programming Language (zwana “białą księgą C” lub K&R). od początku lat 80. XX w, C zdobywa popularność poza Bell Labs; po końca lat 80. ubiegłego wieku Bjarne Stroustrup i współpracownicy z Bell Labs opracowują język programowania zorientowany obiektowo, nazwany później C++.

Wersje C K&R C ANSI C i ISO C C99

K&R C Ta wersja C została opisana w książce Kernighan’a I Ritchie'go The C Programming Language (1978). Wprowadzono w nim następujące elementy do C: Typy strukturalne long int data type unsigned int data type operator (=+) został przekształcony w do postaci (+=); K&R C został udokumentowany w standardzie ANSI C

ANSI C i ISO C W roku 1989, C został po raz pierwszy przedstawiony standard ANSI w dokumencie ANSI X3.159-1989 "Programming Language C". ANSI C stanowił rozwiniecie K&R C. ANSI C został wzbogacony o następujące elementy: funkcje zwracające wartości typu void (void functions); funkcje zwracające wartości typu struct i union; typ void *; kwalifikator const; separowanie nazw pól struktur; biblioteka stdio stała się biblioteką standardową; plik nagłówkowy stddef.h I szereg innych plików stało się standardowymi plikami nagłówkowymi.

C99 W roku 1999 opublikowany został standard ISO 9899:1999, który w marcu 2000 roku został zaadaptowany przez ANSI jako standard. Nowe elementy tego standardu: Funkcje inline; Nowe typy danych, w tym long long int, boolean i typ reprezentujący liczby zespolone (complex); Tablice o zmiennej długości (variable-length arrays); Komentarze jednowierszowe (//); Wiele nowych funkcji bibliotecznych i plików nagłówkowych, w tym stdint.h.

C++ C++, obiektowy język programowania zaprojektowany przez B. Stroustrupa i in., w warstwie proceduralnej osadzony w notacji języka C, wpływ języków Algol 68 i Simula 67 oraz Ada, ML i Clu. Charakterystyczne cechy: dociążanie funkcji i operatorów, dziedziczenie z wielu klas, obsługa wyjątków, klasy parametryczne. Wersja pierwotna (1979) nosiła nazwę C z klasami (C with classes), a jej kompilatory generowały kod w języku C. Standaryzowany od 1994, standard ostateczny ISO/ANSI C++ (ANSI 14882) z 1 września 1998. Liczne kompilatory, m. in. Borland C++, Microsoft C++, Watcom C++, implementacje systemów operacyjnych, baz danych. Java, Smalltalk

Przewodnik po języku C

Strumienie (ang. streams) Strumień to ciąg danych tego samego lub różnych typów danych, pozwalający na sekwencyjny dostęp do tych wartości. Wyróżnia się strumienie: binarne – są to sekwencje nie przetworzonych bajtów znaków - są to ciągi znaków podzielone na wiersze tekstowe – ciąg wierszy, w którym każdy wiersz zawiera zero lub więcej znaków i kończy się znakiem końca wiersza Strumienie przetwarza się najczęściej w ten sposób, że wiąże się je z zewnętrznymi plikami dyskowymi lub wewnętrznymi buforami, które można traktować jako plik dla czasowego przechowywania danych.

Przekierowanie Skompilowany program, np. proba.exe, można uruchomić z wiersza poleceń DOS: c>proba.exe > proba.txt, co spowoduje, ze zamiast na ekran dane będę wpisywane do utworzonego pliku tekstowego. Dane z pliku tekstowego (np. dane.txt) można wczytać do programu wykonywalnego przy pomocy polecenia: c:> proba.exe < dane.txt Dwie ww. operacje można połączyć (zdarza się to rzadko): c:> proba.exe < we.txt >wy.txt

"Hello, World!" w C Poniższy prosty przykład drukuje tekst "Hello, World!" na standardowym urządzeniu wyjściowym (zwykle jest to monitor). Program ten po raz pierwszy pojawił się w K&R C. #include <stdio.h>  int main(void) { printf("Hello, World!\n"); return 0; }

Formatowane wyjście: funkcja printf() Składnia funkcji printf(): int printf(const char *format[, argument, ...]); Funkcja ta pod nadzorem argumentu format, formatuje i wypisuje swoje argumenty do standardowego wyjścia (stdout). Format zawiera obiekty dwojakiego rodzaju: znaki, które są kopiowane do strumienia wyjściowego specyfikacje przekształceń, z których każda wskazuje sposób przekształcenia i wypisania kolejnego argumentu funkcji. Każdą specyfikację przekształcenia (slajd+1) rozpoczyna znak %, a kończy znak charakterystyczny dla danego przekształcenia (slajd+2) Przykład int l=1; char c[]="hello"; printf("%d %s",l,c);

Specyfikacje przekształcenia w printf() Między znakiem % i znakiem przekształcenia mogą (w podanej kolejności) wystąpić: Minus (-), zalecający przesunięcie argumentu do lewego krańca jego pola Liczba określająca minimalny rozmiar pola. W razie potrzeby pole będzie uzupełnione z lewej strony (lub z prawej, gdy żądano dosunięcia w lewo) Kropka (.) oddzielająca rozmiar pola dla precyzji. Liczba określająca precyzję, tj max. liczbę znaków dla tekstu, liczbę cyfr po kropce dla wartości zmiennopozycyjnej lub minimalną liczbę cyfr dla wartości całkowitej. Jedna z liter: h – jeśli argument całkowity należy wypisać jako short, l – jeśli jako long.

Podstawowe przekształcenia dla printf() Znak Dana wejściowa Typ argumentu d, i Liczba całkowita dziesiętna Int o Liczba całkowita ósemkowa (bez wiodącego O) x Liczba całkowita szesnastkowa (bez wiodących 0X) u Liczba całkowita dziesiętna bez znaku Unsigned int c Jeden znak Char s Ciąg znaków do napotkania ‘\0’ lub wyczerpania liczby znaków określonych przez precyzję Char * f [-]m.dddddd (precyzja – domyślnie 6 znaków) Double E, e [-]m.dddddde±xx G, g Wypisywana w formacie %e jeśli wykładnik jest mniejszy niż -4 albo większy lub równy precyzji; w przeciwnym przypadku wypisywany w formacie %f; nie wypisuje nieznaczących zer i kończącej kropki. p Wskaźnik, postać zależna od implementacji Void * % Nie będzie żadnego przekształcenia argumentu; wypisywany znak %

Formatowane wejście: funkcja scanf() Składnia funkcji scanf(): int scanf(const char *format[, address, ...]); - Funkcja ta wczytuje znaki ze standardowego wejścia, interpretuje je zgodnie ze specyfikacjami zawartymi w argumencie format i zapamiętuje znaki w miejscach wskazanych przez argument address. Funkcja kończy swe działanie, gdy zinterpretuje cały format. Wartości zwracane przez funkcję: W przypadku sukcesu, funkcja zwraca liczbę wprowadzonych wartości. W przypadku niepowodzenia, funkcja zwraca: - 0 – jeśli nie odczytano żadnych wartości - EOF – jeśli odczytany został znak końca ciągu znaków. Przykład int i,l; l=scanf("%d",&i); fflush(stdin);//ZALECANE-czyści standardowy bufor wej. printf("%d %d",l,i);

Podstawowe przekształcenia dla scanf() Znak Dana wejściowa Typ argumentu d Liczba całkowita dziesiętna Int * i Liczba całkowita; może wystąpić w postaci ósemkowej (z wiodącym O) lub szesnastkowej (z wiodącymi 0X) o Liczba całkowita ósemkowa (z lub bez wiodącego O) u Liczba całkowita dziesiętna bez znaku Unsigned int * x Liczba całkowita szesnastkowa (z lub bez wiodących 0X) c Znaki; następne znaki (domyślnie 1) umieszcza się we wskazanej tablicy; nie obowiązuje zasada pomijania białych znaków; aby przeczytać najbliższy czarny znak należy zastosować %1s Char * s Tekst (ale nie napis, tzn. bez cudzysłowów); argument powinien wskazywać na tablicę znakową o rozmiarze wystarczającym do przyjęcia tekstu z dodanym na końcu znakiem ‘\0’. e f g Liczba zmiennopozycyjna z opcjonalnymi: znakiem, kropką dziesiętna i wykładnikiem Float * % Nie będzie żadnego przypisania

Niektóre funkcje obsługujące znakowe operacje we/wy (1/2) getche() - funkcja zwraca znak wprowadzony z konsoli i wyświetla go na ekranie; nie czeka na enter; getchar() – funkcja zwraca znak wprowadzony z konsoli i wyświetla go na ekranie; wymaga wprowadzenia enter; działa nie tylko z konsolą, ale również z ogólnymi urządzeniami we/wy; getch() – funkcja zwraca znak wprowadzony z konsoli, ale nie wyświetla go na ekranie; putch() – funkcja wysyła pojedynczy znak na ekran; putchar() – funkcja wyprowadza znak na ekran; działa nie tylko z konsolą, ale również z ogólnymi urządzeniami we/wy.

Niektóre funkcje obsługujące znakowe operacje we/wy (2/2) #include <conio.h> #include <stdio.h> void main (void) { char c1, c2; clrscr(); printf("/n znak 1: "); c1 = getche(); //nie czeka na enter printf("\n znak2: "); c2 = getchar(); //czeka na enter printf("\n\n znaki: "); putch(c1); putchar(c2); getch(); }

Konwencje leksykalne Program w C składa się z jednej lub większej liczby jednostek leksykalnych zapisanych w plikach. Program jest tłumaczony w kilku fazach. W pierwszej fazie interpretowane są wiersze preprocesora zaczynające się znakiem # oraz wykonywane są makrodefinicje i makrorozwiązania. Po zakończeniu tej fazy program jest zredukowany do ciągu jednostek leksykalnych

Jednostki leksykalne Istnieje sześć jednostek leksykalnych: 1. Nazwy (identyfikatory) 2. Słowa kluczowe 3. Stałe 4. Napisy 5. Operatory 6. Separatory Ignorowane są białe znaki (odstępy, znaki tabulacji poziomej i pionowej, znaki nowego wiersza i nowej strony, komentarze), chyba że rozdzielają jednostki leksykalne.

Komentarze Istnieją dwa rodzaje komentarzy: 1. Jednowierszowe: zaczynają się od dwuznaku (//) 2. Wielowierszowe: zaczynają się od dwuznaku (/*), a kończą dwuznakiem (*/); Komentarze nie mogą być zagnieżdżone i nie mogą występować w napisach i stałych znakowych. Przykład int i=j/k; //dzielenie przez k int i = j//* dzielenie przez k*/k;

Nazwy (identyfikatory) Nazwa jest ciągiem liter i cyfr i znaku podkreślenia (_). Znak podkreślenia zalicza się do liter. Ograniczenia: nazwa może składać się z max. 32 lub 64 znaków pierwszym znakiem musi być litera rozróżniane są wielkie i małe litery. Identyfikatory rozpoczynające się od znaku pojedynczego lub podwójnego znaku podkreślenia (_), (__) są zarezerwowane na użytek biblioteki standardowej.

Słowa kluczowe (1/5) Opis auto Automatyczny Słowo kluczowe Odpowiednik polski Opis auto Automatyczny Specyfikacja klasy zmiennej (zmienna automatyczna) break Przerwij Instrukcja(przerwij wykonanie bloku/pętli) case Wariant Instrukcja (element konstrukcji warunkowej switch-case) char Znak Typ danych (pojedynczy bajt, interpretowany domyślnie jako kod ASCII znaku) const Stała Modyfikator (dodawany do deklaracji w przypadku stałych) continue Kontynuuj Instrukcja (wróć do początku pętli programowej)

Słowa kluczowe (2/5) default Domyślny Etykieta (element konstrukcji warunkowej switch-case) do Wykonaj Instrukcja (element instrukcji do-while) double podwójny Typ danych (liczba o podwójnej długości i podwójnej precyzji) else Jeśli nie, to Instrukcja (element instrukcji if-else-if...) enum Wyliczeniowy Typ danych (typ porządkowy, wyliczeniowy) extern Zewnętrzny Specyfikator klasy zmiennej (rodzaj pamięci)

Słowa kluczowe (3/5) float Zmiennoprzecinkowy Typ danych (liczby zmiennoprzecinkowe) for Dla Instrukcja (nagłówek pętli programowej) goto Idź do Instrukcja (przejście do etykiety) If Jeśli Instrukcja (element konstrukcji warunkowej if-else-if ...) Int Całkowity Typ danych (liczby całkowite, zazwyczaj 2-bajtowe) long Długi Typ danych (liczba o powiększonej długości i precyzji) register Rejestrowy Specyfikator rodzaju pamięci (zmienna do rejestru)

Słowa kluczowe (4/5) return Zwróć Instrukcja (zwrot wartości przez funkcję) short Krótki Typ danych (liczba całkowita, krótka) signed Ze znakiem Specyfikator typu danych (pierwszy bit liczby to znak +/-) static statyczny Specyfikator rodzaju pamięci (zmienna statyczna) struct Struktura Specyfikator typu danych (złożony z danych różnych typów) switch Przełącz Instrukcja (element konstrukcji warunkowej switch-case) typedef Definiuj typ Instrukcja (definiuje nowy typ danych)

Słowa kluczowe (5/5) union Unia Specyfikator typu danych unsigned Bez znaku Specyfikator typu danych (liczba bez znaku, tj. tylko dodatnia) void Nieokreślony volatile Ulotny Specyfikator klasy zmiennej (rodzaj pamięci); zmienna, która może ulegać niezauważalnemu „nadpisaniu”, dotyczy np. zmiennych rejestrowych. while Dopóki Słowo poprzedzające warunek (element pętli)

Stałe Wyróżnia się pięć rodzajów stałych: 1. Całkowite 2. Znakowe 3. Tekstowe 4. Zmiennopozycyjne 5. Wyliczeniowe Typ stałej zależy od jej postaci, wartości i przyrostka.

Stałe całkowite Stałe całkowite można zapisywać w postaci: Dziesiętnej: używa się cyfr 0,1,...,9; Ósemkowej: używa się cyfr 0,1,...,7. Stałą uważa się za ósemkową, jeżeli rozpoczyna się od 0 (liczba zero); Szesnastkowej: używa się cyfr 0,1,...,9 i liter A,B,...,F. Stałą uważa się za szesnastkową, jeżeli rozpoczyna się od 0X (liczba zero i znak x); Stała całkowita możne być opatrzona przedrostkiem U do oznaczenia, że jest bez znaku (unsigned). Może być opatrzona przyrostkiem L do oznaczenia, że jest długa (long). Przykłady 123 -18 1L U5 013 //liczba ósemkowa 0X13 //liczba szesnastkowa

Stałe znakowe Stała znakowa składa się z jednego znaku, zawartym w apostrofach np. ‘x’. Stałe znakowe nie zawierają m.in. Apostrofów (‘) i znaków nowego wiersza. Dla wyrażenia tych i innych znaków stosuje się sekwencje specjalne (zwanych niekiedy manipulatorami).

Sekwencje specjalne ‘\b’ – (BS - Backspace) – cofnięcie ‘\f’ – (FF - Form Feed) – nowa strona ‘\n’ – (NL(LF) - New Line) – nowa linia ‘\r’ – (CR - carriage return) – powrót karetki ‘\t’ – (HT - tabulator) – tabulator poziomy ‘\v’- (VT - vertical tabulator)- tabulator pionowy ‘\p’ - adres ‘\a – (BEL - alarm)’ – sygnał dźwiękowy ‘\0’ – NULL

Sekwencje specjalne ‘\\’ – ukośnik w lewo (\) ‘\?’ – znak zapytania (?) ‘\’’ – apostrof ‘\”’ – cudzysłów ‘\0oo’ – stała ósemkowa ‘\Xhh’ – stała szesnastkowa

Stałe zmiennopozycyjne Stała zmiennopozycyjna składa się z części całkowitej, kropki, części ułamkowej, litery E, wykładnika potęgi (ewentualnie ze znakiem) i opcjonalnego przyrostka F (dla oznaczenia typu float) lub L (dla oznaczenia typu long). Przykład 1.2; 88.5L; 1.0e+3; 12.34e-2;

Stałe wyliczeniowe Nazwy zadeklarowane jako wyliczniki (enum) są stałymi typu int. Przykład enum dni {pn=1,wt=2,sr=3,cz=4,pi=5,so=6,ni=7} dni_tyg;

Napisy Napis (stała napisowa) jest ciągiem znaków ujętym w znaki cudzysłowu, np. ”to jest napis”. Typem napisu jest tablica znakowa, a klasą pamięci – static. Do końca napisu jest dopisywany (o ile nie jest podany jawnie) znak ‘\0’ (NULL), aby program przeglądający tekst mógł znaleźć jego koniec.

Znaczenie identyfikatorów Identyfikatory odnoszą się w szczególności do: Funkcji; Etykiet; Struktur; Unii; Klas; Wyliczeń; Stałych wyliczeń; Nazw typów; Nazw zmiennych.

Podstawowe atrybuty zmiennej Zmienna jest miejscem w pamięci; jego interpretacja opiera się na dwóch podstawowych atrybutach: - Klasie pamięci - Typie Klasa pamięci decyduje o żywotności zmiennej, natomiast typ nadaje znaczenie wartości zmiennej.

Podstawowe cechy zmiennej Podstawowe cechy zmiennej to: - zasięg - łączność Zasięg to część programu, w której zmienna jest znana. Łączność określa, czy ta sama nazwa z innym zasięgiem odnosi się do tej samej zmiennej lub funkcji, czy też nie.

Klasy pamięci Istnieją dwie klasy pamięci: Automatyczna; Statyczna. Klasy pamięci są określone przez słowa kluczowe (auto, static i register) razem z kontekstem zawierającym deklarację zmiennej.

Zmienne automatyczne Zmienne automatyczne są lokalne w bloku i znikają, gdy sterowanie opuści blok. Deklaracje zawarte w bloku tworzą zmienne automatyczne, jeśli nie podano klasy pamięci lub użyto specyfikatora auto. Zmienne zadeklarowane jako register są automatyczne i umieszczane (jeśli to możliwe) w szybkich rejestrach maszyny. Przykład { int i; // zmienne automatyczne auto int a; register int b; // zmienna register }

Zmienne statyczne Zmienne statyczne mogą być lokalne dla bloku lub zewnętrzne dla wszystkich bloków; w każdym przypadku zachowują swoje wartości po opuszczeniu i ponownym wejściu do bloku. Wewnątrz bloku zmienne statyczne deklaruje się za pomocą słowa static. Zmienne zadeklarowane na zewnątrz wszystkich bloków, są zawsze statyczne. Przykład int i; // zmienna statyczna int main(void) { int i; // zmienna automatyczna static int a; // zmienna statyczna return 0 }

Typy danych

Rodzaje typów danych W C wyróżnia się dwa rodzaje typów danych: typy podstawowe (predefiniowane) typy pochodne (definiowane przez uzytkownika)

Przykładowe zastosowanie Typy podstawowe (1/2) Nazwa typu Wielkość (w bitach) Zakres wartości Przykładowe zastosowanie unsigned char 8 0 do 255 Zbiór znaków ASCII char -128 do 127 Małe wartości short int 16 -32,768 do 32,767 Zmienne sterujące pętli unsigned int 32 0 do 4,294,967,295 int -2,147,483,648 do 2,147,483,647

Typy podstawowe (2/2) unsigned long 32 0 to 4,294,967,295 Odległości astronomiczne enum -2,147,483,648 do 2,147,483,647 Uporządkowane zbiory wartości long Liczebności populacji float 3.4 *10-38 do 1.7 * 10+38 Obliczenia naukowe z dokładnością 7 cyfr double 64 1.7 * 10-308 do 3.4 * 10+308 Obliczenia naukowe z dokładnością 15 cyfr long double 80 3.4 * 10-4932 do 1.1 * 10+4932 Obliczenia naukowe z dokładnością 18 cyfr

Typy pochodne Oprócz typów podstawowych można utworzyć potencjalnie nieskończoną klasę typów pochodnych, konstruowanych z typów podstawowych. Są to: Tablice – złożone ze zmiennych danego typu; Funkcje – zwracające wartości danego typu; Wskaźniki – do zmiennej danego typu; Struktury – zawierające zestawy zmiennych różnych typów; Unie – zawierające dowolny z zestawu zmiennych o różnych typach; Klasy (w C++) - zawierające zestawy zmiennych różnych typów i funkcji zwracających wartości danego typu.

Kwalifikatory typów Z typem zmiennej mogą być związane kwalifikatory (modyfikatory). Deklarując zmienną jako: const (stały) wskazujemy, że ta zmienna nie będzie zmieniała wartości; volatile (ulotny) wskazujemy, że zmienna będzie miała specjalne właściwości, ważne przy optymalizacji. Przykład const int i=1; volatile int stan_miernika;

Zmienne i l-wartości Zmienna jest nazwanym obszarem pamięci; l-wartość (ang. l-value) jest wyrażeniem odnoszącym się do zmiennej. Oczywistym przykładem l-wartości jest identyfikator o odpowiednim typie i klasie pamięci. Przykład int *wsk; // l-wartości: int j=2; *wsk=j;

Przekształcenia typów W niektórych przypadkach zachodzi potrzeba przekształcenia wartości z jednego typu do innego. Przykład int main(void){ int i=2,j=1; float f; f=((float)j/float(i)); return(0); }

Promocja typu całkowitego Jeśli typ int może reprezentować wszystkie wartości oryginalnego typu, to dana wartość jest przekształcana do typu int; w przeciwnym przypadku tę wartość przekształca się do typu unsigned int.

Przekształcenia całkowite Dowolna wielkość całkowita jest przekształcana do danego typu bez znaku przez znalezienie takiej najmniejszej nieujemnej wartości, która jest przystająca (kongruentna) do tej wielkości całkowitej modulo największa wartość reprezentowalna w danym typie zwiększona o 1.

Wartości całkowite i zmiennopozycyjne Przy przekształcaniu wartości typu zmiennopozycyjnego do typu całkowitego gubi się jej część ułamkową. Przykład int k; float f; f=1.5; k=f; //k=1

Typy zmiennopozycyjne Przy przekształcaniu wielkości zmiennopozycyjnej do typu zmiennopozycyjnego o mniejszej precyzji i jej wartość zawiera się w reprezentowalnym przedziale, wynikiem jest najbliższa większa albo mniejsza reprezentowalna wartość. Jeśli wynik nie zawiera się w tym przedziale, skutek nie jest zdefiniowany. Przykład float f; double d; f=d; // przekształcenie wartości typu double do float

Przekształcenia arytmetyczne Wiele operatorów powoduje przekształcenia swoich argumentów i określa typ wyniku według podobnych zasad. W efekcie chodzi o dopasowanie typów argumentów do wspólnego typu, który jest także typem wyniku. Przykład int a=2; float r; r=a+2.5; //przekształcenie do typu float

Wskaźniki i wartości całkowite Wartość całkowitą można dodać do lub odjąć od wskaźnika. Przykład int x[3]={1,2,3}; int *wsk; wsk=x; for (int i=0;i<3;i++) { printf("x=%d\n",*wsk); wsk++; // wsk wskazuje na nastepny el. tablicy };

Typ void Wartości (nie istniejącej) zmiennej typu void nie można wykorzystać w żaden sposób ani nie można przekształcić, jawnie czy niejawnie, do innego typu różnego od void. Ponieważ wyrażenie typu void oznacza nie istniejącą wartość, takich wyrażeń można użyć jedynie w miejscach, gdzie wartość nie jest wymagana, na przykład jako instrukcję wyrażeniową (np. deklarowanie funkcji) (przykład 1) lub jako lewy argument operatora przecinkowego (przykład 2). Przykład 1 void komunikat(char *kom) {printf(kom);}; Przykład 2 int init(void) { return 1; }

Wskaźniki do typu void Każdy wskaźnik do zmiennej może być przekształcony do typu (void *) bez utraty informacji. Przekształcenie wyniku z powrotem do wskaźnika oryginalnego typu przywraca oryginalny wskaźnik. Przykład int x=1; int *wsk; void *vwsk; wsk=&x; vwsk=wsk; wsk=(int*)vwsk; printf(„wsk=%d”,*wsk);

Operatory

Operatory i ich priorytety (1/7) Kolejność wykonywania działań w wyrażeniach jest uzależniona od priorytetów operatorów. Priorytet Operator Opis Przykład 1.1 ( ) Ogranicza wyrażenia Izoluje wyrażenia warunkowe Wskazuje na wywołanie funkcji x* (y- 3.5 ) if ( x > 10 ) . . . getch ( ); 1.2 [] Wskazuje na wymiar/indeks macierzy char Tekst[12]: 1.3a . Kropka-odwołanie do elementu struktury, unii puts( STRUKTURA .Text ): 1.3b -> Wskazanie elementu struktury, unii str_a->d 1.4 :: Widoczność/dostęp (scope) puts ( : : Text );

Operatory i ich priorytety (2/7) 2.1 ! Operator negacji logicznej if (!x) . . . 2.2 ~ Operator dopełnienia jedynkowego - negacja bitowa (NEG) X = ~Y & Z; 2.3a - Jednoargumentowy minus printf( “96d”, -Y ); 2.3b + Jednoargumentowy plus a=+a; 2.4a ++ Operator jednoargumentowy “pre-inkrementacja” A = ++X ;

Operatory i ich priorytety (3/7) 2.4b ++ Operator jednoargumentowy “post-inkrementacja” for ( i=1; i < 10; i ++) . . . 2.5a -- Operator jednoargumentowy “pre-dekrementacja” A =--X; 2.5b Operator jednoargumentowy “post-dekrementacja” X--; 2.6 & Operator adresu Scanf(“%d”,&x); 2.7 * Operator adresowania pośredniego Char *string=“tekst“;

Operatory i ich priorytety (4/7) 2.8 sizeof Operator sizeof N=sizeof(x); 3.1 * Operator multiplikatywny „mnożenie” X=y * z; 3.2 / Operator multiplikatywny „dzielenie” X=y / z; 3.3 % Operator multiplikatywny „modulo” X=y % z; 4.1 + Operator addytywny „dodawanie” X=y + z; 4.2 - Operator addytywny „odejmowanie” X=y - z;

Operatory i ich priorytety (5/7) 5.1 << Operator przesunięcia w lewo x=x << 2; 5.2 >> Operator przesunięcia w prawo x=x >>2; 6.1 < Operator relacji „mniejsze niż” If (x < 0) ... 6.2 > Operator relacji „większe niż” If (x > 0) ... 6.3 <= Operator relacji „mniejsze lub równe” If (x <= 0) ... 6.4 >= Operator relacji „większe lub równe” If (x >= 0) ...

Operatory i ich priorytety (6/7) 7.1 == Operator przyrównania „równe” If (x == 0) ... 7.2 != Operator przyrównania „nie równe” If (x != 0) ... 8.1 & Bitowy operator koniunkcji (AND) x=x&y; 8.2 ^ Bitowy operator różnicy symetrycznej (XOR) x=x ^ y; 8.3 | Bitowy operator alternatywy (OR) x=x | y;

Operatory i ich priorytety (7/7) 9.1 && Operator iloczynu logicznego (logiczne AND) If ((x>0) && (y<0)) 9.2 || Operator sumy logicznej (logiczne OR) If ((x>0) || (y<0)) 10 ? : Operator warunkowy (odpowiednik if (x==0) x++ else x--) (x==0) ? x++ : x--; 11a = Operator przypisania X = 2; 11b +=, -=, *=,/=, %=, >>=, <<= Operatory przypisania X+=5; 12 , Przecinek – oddziela argumenty funkcji na liście argumentów Printf(”i=%d”,i);

Niektóre operatory i ich priorytety -podsumowanie Kolejność wyrażeń (); wskazujący na wymiar tablicy - [] unarne (+, -), post- i pre-inkrementacja (++,--) arytmetyczne (*,/, %) arytmetyczne (+, -) przesunięcia (<<, >>) relacji (<, >, <=, >=) relacji (==, !=) bitowe (&, ^, |) iloczynu i sumy logicznej (&&, ||) warunkowy (?:) przypisania (=,+=, -=, *=, /=, >>=, <<=) przecinek (,)

Wyrażenia proste Wyrażeniami prostymi są: identyfikatory, stałe, napisy lub wyrażenia w nawiasach. Przykład int x=1; int *wsk; char t[]={"tekst"}; *wsk=2; x=x+*wsk; printf(”x=%d”,x); printf("%s\n\n",*t); //tekst

Wyrażenia przyrostkowe Wyrażeniami przyrostkowymi są: Odwołania do tablic Wywołania funkcji Odwołania do klas i struktur Zwiększanie i zmniejszanie przyrostkowe Operatory w wyrażeniach przyrostkowych są lewostronnie łączne.

Odwołania do tablic Wyrażenie przyrostkowe, po którym następuje wyrażenie ujęte w nawiasy prostokątne, jest wyrażeniem przyrostkowym oznaczającym indeksowane odwołanie do tablicy. Przykład char t[]={"tekst"}; //tablica znakowa char *te="inny tekst"; //wskazanie do ciągu znaków int x[3]={1,2,3}; //tablica jednowymiarowa int y[2][3] ={{1,2,3},{4,5,6}}; //tab. wielowymiarowa

Wywołania funkcji Wywołanie funkcji składa się z wyrażenia przyrostkowego, zwanego oznacznikiem (identyfikatorem) funkcji, po którym następuje ujęta w nawiasy okrągłe lista (być może pusta) wyrażeń przypisania. Wyrażenia przypisania, oddzielone od siebie przecinkami, tworzą argumenty funkcji. Przykład void swap(int v[], int i, int j) //deklaracja funkcji ... swap(v,0,rand()%n); //wywołanie funkcji

Odwołania do klas i struktur (.) Wyrażenie przyrostkowe będące nazwą lub referencją do klasy, struktury lub unii, po którym następuje kropka i identyfikator, jest wyrażeniem przyrostkowym umożliwiającym dostęp do składowych tych struktur. Przykład class C_Kolejka { //deklaracja klasy public: int licznik; int kolejka[10]; };   C_Kolejka kol; //deklaracja klasy int main(void) { kol.licznik=0; } //odwołanie do klasy

Odwołania do klas i struktur (->) Wyrażenie przyrostkowe będące wskaźnikiem do klasy, struktury lub unii, po którym następuje strzałka (złożona ze znaków - i >) i identyfikator, jest wyrażeniem przyrostkowym umożliwiającym dostęp do składowych tych struktur. Przykład class C_Kolejka { //deklaracja klasy public: int licznik; int kolejka[10]; }; C_Kolejka kol,*wkol; //deklaracje klas   int main(void) { kol.licznik=1; wkol=&kol; printf(„licznik=%d”,wkol->licznik); //odwołanie do pola klasy }

Zwiększanie i zmniejszanie przyrostkowe Użyty po wyrażeniu przyrostkowym operator (++) lub (--) powoduje, odpowiednio, zwiększenie lub zmniejszenie wartości argumentu o jeden. Przykład x++; //x=x+1; - post-inkrementacja --x; //x=x-1; - pre-dekremantacja

Operatory jednoargumentowe Operatory jednoargumentowe są prawostronnie łączne. Są to: Przedrostkowe operatory zwiększania (++) i zmniejszania (--); Operator adresu (&); Operator adresowania pośredniego (*); Jednoargumentowy plus (+); Jednoargumentowy minus(-); Operator dopełnienia jedynkowego (~); Operator negacji logicznej (!); Operator sizeof.

Przedrostkowe operatory zwiększania i zmniejszania Operatorem (++) poprzedzający argument powoduje zwiększenie argumentu o jeden, natomiast operator (--) powoduje zmniejszenie argumentu o jeden. Przykład x++; //post-inkrementacja x--; //post-dekrementacja ++x; //pre-inkrementacja x=y- --x; //pre-dekrementacja

Operator adresu Jednoargumentowy operator (&) podaje adres swojego argumentu. Argument musi być l-wartością nie odnoszącą się ani do pola bitowego, ani do zmiennej zadeklarowanej jako register; argument może być funkcją. Wynikiem jest wskaźnik do zmiennej lub funkcji wskazanych l-wartością. Przykład int *r,x=0; r=&x;

Operator adresowania pośredniego Jednoargumentowy operator (*) oznacza adresowanie pośrednie i daje w wyniku zmienną lub funkcję wskazywaną przez argument. Wynik jest l-wartością, jeśli argument jest wskaźnikiem do typu arytmetycznego, klasy, struktury lub unii lub wskaźnika. Jeśli typem argumentu jest wskaźnik do typu T, to typem wyniku jest T. Przykład int main(void){ int *w1,*w2, i=1; w1=&i; w2=w1; printf("w1=%p w2=%p",w1,w2); //w1=0012FF88 w2=0012FF88 getch(); return(0); }

Jednoargumentowy plus Argument jednoargumentowego operatora (+) musi być typu arytmetycznego lub wskaźnikowy. Wynikiem jest wartość argumentu. Typem wyniku jest promowany typ argumentu. Przykład int i,x=-1; int *wsk; wsk=&x; i=+(*wsk); //i=-1

Jednoargumentowy minus Argument jednoargumentowego operatora (-) musi być typu arytmetycznego. Wynikiem jest wartość argumentu ze zmienionym znakiem. Wynikiem promocji jest typ całkowity. Przykład int i,x=-1; int *wsk; wsk=&x; i=-(*wsk); //i=1

Operator dopełnienia jedynkowego Argumentem operatora (~) jest typ całkowity. Wynikiem jest dopełnienie jedynkowe wartości argumentu. Promowany jest typ całkowity. Przykład int x=0x0000000f; printf(“~x=%x”,~x); //wynik ~x=fffffff0

Operator negacji logicznej Argumentem operatora (!) jest typ całkowity lub wskaźnikowy. Wynikiem jest 1, jeśli wartością argumentu jest 0, a 0 w przeciwnym przypadku. Przykład 1 int *wsk,x=0; printf(”!x=%x\n”,!x); //wynik !x=1 printf(”!wsk=%x”,!wsk); //wynik !wsk=1   Przykład 2 int *wsk,x=1; wsk=&x; printf(”!x=%x\n”,!x); //wynik !x=0 printf(”!wsk=%x”,!wsk); //wynik !wsk=0

Operator sizeof() Argumentem jest albo nazwa zmiennej, albo wyrażenie (które nie jest obliczane) albo nazwa typu ujęta w nawiasy. Wynikiem jest stała całkowita bez znaku oznaczająca liczbę bajtów stosowaną do reprezentowania w pamięci struktury będącej argumentem operatora. Operator nie może odnieść się do funkcji, niekompletnego typu lub pola bitowego. Przykład int *w=NULL, i=1; printf("s_w=%d s_i=%d",sizeof(w),sizeof(i)); //s_w=4 s_i=4

Rzutowanie Poprzedzenie wyrażenia jednoargumentowego nazwą typu ujętą w nawiasy, lub poprzedzenie tego wyrażenia nazwą typu i ujęcie całego wyrażenia w nawiasy, powoduje przekształcenie wartości tego wyrażenia do wskazanego typu. Takie przekształcenie nazywane jest rzutem. Przykład float f=1.5; int x,y; x=(int)f; //rzutowanie w stylu C y=int(3.5*f); //rzutowanie w stylu C++ printf(”x=%d\n”,x); //wynik x=1 printf(”y=%d\n”,y); //wynik y=5

Operatory multiplikatywne Operatory multiplikatywne: - mnożenia (*), - dzielenia (/) - modulo (%). Argumenty wszystkich operatorów muszą być arytmetyczne; argumenty operatora (%) muszą być całkowite.

Operatory addytywne Operatory addytywne: - dodawania (+) - odejmowania (-) są lewostronnie łączne.

Operatory przesunięcia Operatory przesunięcia w lewo (<<) i w prawo (>>) są lewostronnie łączne. Ich argumenty musza być całkowite i podlegają promocji typu całkowitego. Wynik nie jest zdefiniowany, jeśli prawy argument jest ujemny, a także jeśli jest większy lub równy liczbie bitów w typie lewego argumentu. Wartością E1<<E2 jest E1 (traktowane jako wzorzec bitowy) przesunięte w lewo o E2 bitów, co jest równoważne pomnożenie E1 przez 2^E2. Wartością E1>>E2 jest E1 przesunięte w prawo o E2 bitów, co jest równoważne podzieleniu E1 przez 2^E2.

Operatory relacji Wszystkie operatory relacji: - „mniejsze niż” (<); - „większe niż ” (>); - „mniejsze lub równe ” (<=); - „większe lub równe ” (>=); dają w wyniku wartość 1 jeśli relacja jest prawdziwa, zaś 0 jeśli jest fałszywa. Typem wyniku jest int. Możliwe jest porównywanie wskaźników, ale tylko wtedy gdy dotyczą zmiennych tego samego typu!

Operatory przyrównania Operatory przyrównania: - „równe” (==) - „nie równe” (!=), są podobne do operatorów relacji z tym, że mają niższy priorytet, tak więc zapis a<b==c<d jest równoważny zapisowi ((a<b)==(c<d)).

Bitowy operator koniunkcji (AND) Bitowy operator koniunkcji (&) daje w wyniku bitową koniunkcję argumentów. Operator odnosi się tylko do argumentów całkowitych. Przykład m 0000 1111 0000 1111 k 0000 1111 1111 0000 m&k 0000 1111 0000 0000

Bitowy operator różnicy symetrycznej (XOR) Bitowy operator różnicy symetrycznej (^) daje w wyniku bitową różnicę symetryczną argumentów. Operator odnosi się tylko do argumentów całkowitych. Przykład m 0000 1111 0000 1111 k 0000 1111 1111 0000 m^k 0000 0000 1111 1111

Bitowy alternatywy (OR) Bitowy operator alternatywy (|) daje w wyniku bitową alternatywę argumentów. Operator odnosi się tylko do argumentów całkowitych. Przykład m 0000 1111 0000 1111 k 0000 1111 1111 0000 m|k 0000 1111 1111 1111

Operator warunkowy Operator warunkowy o postaci: wyrażenie_1 ? wyrażenie_2 : wyrażenie_3 Najpierw obliczane jest wyrażenie_1. Jeśli jego wartość jest różna od 0 (tzn., że ma wartość TRUE), to wynikiem jest wartość drugiego wyrażenia (wyrażenie_2), w przeciwnym przypadku wynikiem jest wartość trzeciego wyrażenia (wyrażenie_3). Przykład //if-else: if (x < y) z = x; else z = y; //odpowiednik: z = (x < y) ? x : y;

Wyrażenia przypisania Wszystkie operatory przypisania są prawostronnie łączne. Są nimi: =, *=, /=, +=,-=, <,=, >>=, &=, ^= i |=. Dla wszystkich operatorów przypisania lewy argument musi być l-wartością. Wyrażenie o postaci: E1op=E2 jest równoważne z wyrażeniem E1=E1 op E2, z tym tylko, że wyrażenie E1 jest obliczane raz.

Operator przecinkowy Operatory przecinkowe stosuje się do rozdzielania elementów listy argumentów funkcji lub listy inicjatorów. Np. w wywołaniu funkcji: f(a,(t=3,t+2),c); występują trzy argumenty; drugi ma wartość 5.

Wyrażenia stałe Wyrażenie stałe wyrażone są za pomocą stałych. Są one stosowane w następujących kontekstach: Po słowie case; Jako rozmiar tablicy; Jako długość pola bitowego; Jako wartość stałej wyliczania; W inicjatorach i niektórych wyrażeniach dla preprocesora.

Deklaracje i definicje Deklaracje określają sposób interpretacji identyfikatorów, nie zawsze jednak rezerwują pamięć związaną z identyfikatorami. Deklaracje, które rezerwują pamięć nazywa się definicjami. Deklaracja musi zawierać przynajmniej jeden deklarator. Deklarator zawiera przynajmniej kwalifikator typu i nazwę deklatarora.

Przykłady deklaracji Składnia deklaracji Przykład Typ wynikowy typ nazwa; int count; typ typ nazwa[]; int count[]; Otwarta tablica określonego typu typ nazwa[3]; int count[3]; Tablica o ustalonym rozmiarze typ *nazwa; int *count; Wskaźnik do danego typu typ *nazwa[]; int *count[]; Otwarta tablica wskaźników do danego typu typ *(nazwa[]); int *(count[]); Jw. typ (*nazwa)[]; int (*count) []; Wskaźnik do typu “otwartej tablicy” typ &nazwa; int &count; Referencja do danego typu (tylko C++) typ nazwa(); int count(); Typ wartości zwracanej przez funkcję typ *nazwa(); int *count(); Funkcja zwracająca wskaźnik typ *(nazwa()); int *(count()); typ (*nazwa)(); int (*count) (); Wskaźnik do funkcji zwracającej określony typ danych

Specyfikatory klasy pamięci Specyfikatorami klasy pamięci są: auto register static extern typedef W deklaracji może wystąpić co najwyżej jeden specyfikator pamięci. Jeśli nie podano żadnego, to obowiązują następujące zasady: dla zmiennych zadeklarowanych wewnątrz funkcji przyjmuje się klasę auto; dla zmiennych zadeklarowanych na zewnątrz (wszystkich) funkcji przyjmuje się klasę extern lub static

Specyfikatory auto i register Specyfikatory auto i register nadają deklarowanej zmiennej automatyczną klasę pamięci i mogą być stosowane wyłącznie wewnątrz funkcji. Takie deklaracje są jednocześnie definicjami i powodują zarezerwowania pamięci. Deklaracja register jest równoważna z deklaracją auto, ale deklaruje, że instancja będzie często używana i w miarę możliwości powinna być umieszczona w rejestrach procesora. Do instancji zadeklarowanych jako register nie wolno stosować operatora adresu (&). Przykład int main() { register int k; auto int i; i = 5; return i; }

Zmienne automatyczne (1/2) Zmienne zdefiniowane lokalnie w swoim bloku (zasięg blokowy), np. zmienne lokalne funkcji. Powstają i są inicjowane wartościami początkowymi (o ile są podane) za każdym razem, gdy sterowanie wejdzie do ich bloku. W momencie, gdy następuje koniec realizacji bloku – zmienne znikają (automatyczny czas trwania). Domyślnie wszystkie zmienne lokalne funkcji są klasy auto. Można w tym przypadku pominąć w deklaracjach zmiennych słowo kluczowe auto. Umieszczenie tego słowa podkreśla zazwyczaj, że zmienna lokalna dubluje deklarację zmiennej zewnętrznej. Zmienne automatyczne nie posiadają łączności. Nie można zadekla­rować dwóch zmien­nych o tej samej nazwie w tym samym bloku. Zmienne automatyczne o tych samych nazwach, zdefiniowane w różnych blokach, są różnymi obiektami. Deklaracja zmiennej wewnątrz bloku przesłania jej deklarację zewnętrzną, także globalną. Domyślnie zmienne automatyczne nie otrzymują żadnej wartości początkowej. Nie można zakładać, że wartość początkowa zmiennej automatycznej jest równa 0.

Zmienne automatyczne (2/2) int y = 21; // definicja zmiennej globalnej void main() { int x; // deklaracja zmiennej klasy auto // definicja zmiennej klasy auto - przykrywa def. zmiennej globalnej: auto int y = 5; printf(”x=%d”,x); // wartość x przypadkowa printf(”y=%d”,y); // y = 5 printf(”::y=%d”,::y) // ::y = 21 (operator :: tylko w C++) { int y = 11; // definicja lokalna printf(”y=%d”,y); // y = 11 } printf(”y++=%d”,y++); // y++= 6

Specyfikator extern Specyfikator extern użyty na zewnątrz (wszystkich) funkcji nadaje zmiennej łączność zewnętrzną. Przykład extern int _fmode; extern void Factorial(int n); extern "c" void cfunc(int); /* ”c” - ochrona nazwy funkcji przed jej zniekształceniem w programach C++ */

Zmienne zewnętrzne (1/2) Zmienne zdefiniowane poza funkcjami (tylko to decyduje, że zmienne są zewnętrzne, a nie specyfikator extern). Istnieją i zachowują swoje wartości podczas wykonywania całego programu (statyczny czas trwania). Powstają i są inicjowane wartościami początkowymi raz przed rozpoczę­ciem wykonywania programu. W przypadku braku jawnej inicjalizacji otrzymują wartość początkową 0. Inicjacja zmiennych zewnętrznych może nastąpić tylko raz w miejscu ich definicji. Zmienne te mogą być wykorzysty­wane do komunikacji między funkcjami należącymi do różnych plików programu (łączność zewnętrzna). Deklaracja zmiennej ze słowem kluczowym extern, np. extern int w, nie powoduje zarezerwowania w pamięci miejsca dla zmiennej. Informuje ona kompilator, że definicja zmiennej zewnętrznej znajduje się w innym miejscu programu, być może w innym pliku. Jeżeli zmienna jest rzeczywiście zdefiniowana w innym pliku lub po definicji funkcji, w której jest wykorzystywana, to użycie specyfikatora extern jest obowiąz­kowe. W pozostałych przypadkach może być pominięte. Deklaracja extern x jest równoważna deklarcji extern int x.

Zmienne zewnętrzne (2/2) /**********plik1.cpp************/ int y = 5; void main(void) { extern int a; // deklaracja zmiennej zewnętrznej extern double z; // deklaracja zmiennej zewnętrznej printf(”y=%d”,y) // y = 5 printf(”a=%d”,a) // a = 0 printf(”z=%d”,z) // z =145.2 } int a; // definicja zmiennej int a = 0 /**********plik2.cpp*************/ double z = 145.2; // definicja zmiennej

Specyfikator static Specyfikator static nadaje deklarowanej zmiennej statyczną klasę pamięci i może być stosowany zarówno wewnątrz jak i na zewnątrz funkcji. Wewnątrz funkcji specyfikator ten powoduje zarezerwowania pamięci – oznacza więc definicję. Przykład static void printnewline(void) {}; int mai(void) { static int i; }

Zmienne statyczne (1/2) Zmienne o zasięgu blokowym (tak jak zmienne automatyczne), ale w odróżnieniu od zmiennych automatycznych istniejące i zachowujące swoje wartości przez cały czas wykonywania programu (statyczny czas trwania). Zmienne statyczne są inicjowane wartościami początkowymi tylko raz w miejscu ich definicji. W przypadku, gdy sterowanie opuści blok, w którym zmienna jest zdefiniowana aktualna wartość zmiennej zostanie zachowana. W przypadku, gdy zmienne statyczne nie są inicjowane żadnymi wartościami, podobnie jak zmienne zewnętrzne, otrzymują domyślnie wartość 0. Zmienne statyczne można definiować lokalnie wewnątrz funkcji. Są one wówczas widoczne tylko wewnątrz tych funkcji. Można również zdefiniować zmienną statyczną zewnętrzną. Zmienna tego typu, w odróżnieniu od zwykłej zmiennej zewnętrznej, jest widoczna tylko przez funkcje znajdujące się w tym samym pliku, co jej definicja.

Zmienne statyczne (2/2) static int p; // zmienna statyczna zewnętrzna void pisz(double x ) { static int licznik; // zmienna statyczna licznik = 0 printf(”x=%d”,x); licznik++; // zwiększ licznik wywołań funkcji }

Specyfikator typedef Specyfikator typedef nie rezerwuje pamięci. Przykład typedef unsigned char byte; typedef char str40[41]; typedef struct { double re, im; } s_zespolone;

Specyfikatory typów Specyfikatorami typów są: - Void - Char - Short - Int - Long - Float - Double - Signed - Unsigned. Wraz z int można użyć jednego ze specyfikatorów long lub short.; znaczenie jest takie samo jakby słowo int było pominięte. Słowo long można użyć z double. Jeden z identyfikatorów signed lub unsigned moze być użyty z int bądź z jego odmianą long, short lub char. Specyfikator signed można użyć do wymuszenia znaku arytmetycznego dla zmiennej typu char; dla pozostałych typów całkowitych jest zbędny, choć dozwolony. Jeśli specyfikator typu został pominięty to przyjmuje się int.

Kwalifikatory typów: const i volatile Ze specyfikatorami typów można użyć kwalifikatory typów: const lub volatile. Zmienną z kwalifikatorem const można inicjować, lecz nie można mu później przypisywać wartości. Kwalifikator volatile służy do wyłączenia deklarowanej zmiennej z ewentualnej optymalizacji wykonywania kodu. Służy to zabezpieczeniu zmiennej przed niepożądanymi działaniami podejmowanymi przez kompilator w celu, np. zwiększenia efektywności wykonywania kodu. Przykład volatile int temp; printf(”%d”,temp);

Statyczne struktury danych

Rodzaje statycznych struktur danych Najważniejszymi statycznymi strukturami danych w C są: Tablice Struktury Unie

Tablice

Definicja tablicy TABLICA to struktura danych złożona z elementów tego samego typu, przy czym dostęp do każdego z tych elementów odbywa się przez podanie jego numeru (indeksu w tablicy).

Deklarowanie tablic Deklaratory tablic mają składnię: Nazwa_typu deklarator_tablicy; Tablice można konstruować z typów predefiniowanych i typów definiowanych przez użytkownika. Przykład int t[9], *wskt[15]; int tw[2][2];

Inicjowanie tablic (1/2) Elementy tablic mogą być inicjowane w miejscu ich deklaracji. Wartości na liście inicjacyjnej muszą być zgodne z liczbą, kolejnością i typem danych tablicy. Przykład. Inicjowanie tablic tekstowych. char t[]={"tekst"}; //jednowymiarowa tablica znakowa printf("%s\n",*t); char *te="inny tekst"; //wskazanie do ciągu znaków printf("%s\n",te); //równoważne sposoby inicjowania wielowymiarowych tab. teks.: char kolory[][15]={{"CZEROWNY"},{"NIEBIESKI"},{"ZIELONY"}}; char *kolory[15]={{"CZEROWNY"},{"NIEBIESKI"},{"ZIELONY"}}; printf("%s\n",kolory[0]); //CZEROWNY

Inicjowanie tablic (2/2) Przykład. Inicjowanie tablic liczbowych. int x[3]={1,2,3}; //tablica jednowymiarowa int y[2][3] ={{1,2,3},{4,5,6}}; //tab. wielowymiarowa

Struktury

Deklaracje struktur Struktura jest obiektem składającym się ze zbioru nazwanych składowych o różnych typach. Specyfikator struktury ma postać: struct [<nazwa_typu_strukturalnego >] { [<typ> <nazwa_zmiennej [,nazwa_zmiennej, ...]>] ; } [<nazwa_zmiennej_strukturalnej>] ; Przykład struct s_przyjaciele { char nazwisko [20], imie[20], nr_telefonu[20]; int wiek, wzrost; } moj_przyjaciel; //dostęp do elementu: strcpy(moj_przyjaciel.nazwisko,”Abacki”); // deklarowanie danych: struct s_przyjaciele moi_przyjaciele[100];

Inicjowanie struktur (1/2) Pola struktur i unii mogą być inicjowane w miejscu ich deklaracji. Wartości na liście inicjacyjnej muszą być zgodne z liczbą, kolejnością i typami danych pól struktury lub unii. Przykład struct ts_osoba { char nazwisko[15]; char imie[15]; long PESEL; unsigned rok, miesiac, dzien; } os1={"Abacki", "Adam",12345678901, 1990, 8,20}, os2={"Babacki", "Bogdan",12345678901, 1991, 4,14};

Inicjowanie struktur (2/2) Jeśli na liście inicjacyjnej jest mniej stałych niż pól struktury lub unii, to w przypadku struktur statycznych i zewnętrznych pozostałe składowe są inicjowane wartościami zerowymi. W pzrypadku struktur automatycznych zależny to od kompilatora (najczęściej składowe są inicjowane wartościami zerowymi). Przykład #include <stdio.h> #include <conio.h> ... ts_osoba os3={"Cabacki"}; //struktura statyczna int main(void){ ts_osoba os4={"Dabacki"}; //struktura automatyczna printf("naz=%s im=%s pesel=%d",os3.nazwisko, os3.imie, os3.PESEL); //naz=Cabacki im= pesel=0 printf("naz=%s im=%s pesel=%d",os4.nazwisko, os4.imie, os4.PESEL); //naz=Dabacki im= pesel=0 getche(); return 0; }

Nadawanie wartości składowym struktur - przykład #include <stdio.h> #include <conio.h> #include <string.h> ... ts_osoba os; int main(void){ //posrednia aktualizacja wartosci pola: strcpy(os.nazwisko,"Abacki"); //podstawiania bezposrednie: os.PESEL=12345678901; os.rok=1990; os.miesiac=10; os.dzien=12; return 0; }

Rezerwacja pamięci dla struktur Pola struktury są umieszczane w pamięci zgodnie z kolejnością występowania w deklaracji. Każde pole struktury zajmuje osobny obszar pamięci. Łączny rozmiar struktury jest równy sumie rozmiarów jej składowych. Rozmiar struktury jest zawsze większy lub równy sumie rozmiarów jej składowych. Przykład struct ts_osoba { char nazwisko[15]; char imie[15]; long PESEL; unsigned rok, miesiac, dzien; } os; … //size=46 (15+15+4+4+4+4): printf("size=%d",sizeof(ts_osoba)); //adresy pol: printf("adr nazwisko=%p\n",&os.nazwisko[0]); printf("adr imie=%p\n",&os.imie[0]); printf("adr PESEL=%p\n",&os.PESEL);

Struktury zagnieżdżone Składowymi struktur mogą być również inne struktury. Odwołanie do pola struktury, która sama jest składową innej struktury odbywa się z wykorzystaniem dwóch kropek. Przykład struct ts_data { unsigned rok, miesiac, dzien; }; struct ts_osoba { char nazwisko[15]; char imie[15]; long PESEL; ts_data data; } os; … os.data.rok=1990;

Unie

Deklaracje unii Unia jest obiektem, który w danej chwili zawiera jedną ze swoich kilku składowych o różnych typach. Specyfikator unii jest analogiczne jak specyfikator struktury: union [<nazwa_typu_unii>] { <typ> <nazwa_zmiennej> ; ... } [<zmienna_unii>] ; Przykład union u_int_lub_long { int i; long l; } liczba;

Inicjowanie unii (1/2) W uniach wszystkie składowe są pamiętane we wspólnym obszarze pamięci. Dlatego unię inicjuj się jedną daną odpowiadającą typowi pierwszego pola unii. Dla unii statycznych i zewnętrznych pola niezainicjowane są ustawiane na wartości zerowe. Przykład union tu_znaki_liczba { char znaki[4]; int liczba; } zl={"ABC\0"};

Inicjowanie unii (2/2) Jeśli dane inicjujące są innego typu, to (o ile to możliwe) wykonywana jest automatyczna konwersja. Przykład #include <stdio.h> #include <conio.h> … tu_znaki_liczba zl1={65}; //znaki[0]='A' int main(void){ //znaki[0]=(char)65.66='A' –rzutowanie: tu_znaki_liczba zl2={65.56}; printf("znak=%c",zl1.znaki[0]); //znak=A printf("znak=%c",zl2.znaki[0]); //znak=A getche(); return 0; }

Rezerwacja pamięci dla unii Pola unii są zajmują wspólny obszar pamięci, w związku z tym rozmiar unii jest równy rozmiarowi jej największej składowej, a wszystkie składowe mają ten sam adres. Przykład union tu_znaki_liczba { char znaki[4]; int liczba; } z; … printf("size=%d",sizeof(z)); //size=4 (max{4,4}) printf("adr z.znaki=%p\n",&z.znaki[0]); //adr z.znaki=004D06E4 printf("adr z.liczba=%p\n",&z.liczba);

Unie anonimowe (C++) W języku C++ istnieje możliwość deklarowania statycznych unii bez nazwy typu i bez definiowania nazwy zmiennej. Pola unii są wówczas traktowane jak zwykłe zmienne, które zajmują ten sam obszar pamięci. W odwołaniach do składowych unii są wykorzystywane jedynie nazwy składowych. Dzięki wykorzystaniu unii anonimowej uzyskujemy oszczędność pamięci. Przykład static union { char znaki[4]; int liczba; }; int liczba; //błąd! int main(void){ liczba=2;

Pola bitowe

Ograniczenia dla wielkości pól bitowych W przypadku pól typu całkowitego można w deklaracji struktury (unii) określić ile bitów będą one zajmowały. Zakres wartości, które można przechowywać w danym polu zależy od liczby bitów przydzielonych polu oraz od tego czy jest to pole ze znakiem, czy bez znaku. Najmniejsza ilość pamięci przeznaczona na pojedyncze pole wynosi 1 bit. Największa wynosi 16 (instancje int). Jeśli pole 1-bitowe jest typu signed int (char), to może ono przyjmować wartości 0 lub –1. Jeśli pole jest typu unsigned int (char), to może ono przyjmować wartości 0 lub 1. Jeśli nie podamy czy pole typu int (char) jest signed czy unsigned, to sposób interpretacji zależy od kompilatora (na ogół int jest interpretowane jako signed int). Pole bitowe definiuje się podając typ danej, nazwę pola i po dwukropku liczbę bitów wykorzystywanych przez pole.

Pole bitowe w strukturze - przykład struct ts_bit { unsigned ubit : 1; int ibit : 1; } b; … b.ubit=1; b.ibit=1; //ubit=1 ibit=-1: printf("ubit=%d ibit=%d",b.ubit,b.ibit);

Dostęp do pól bitowych Dostęp do pól bitowych jest realizowany w taki sam sposób jak do zwykłych pól. Kompilator automatycznie dodaje kod umożliwiający odczytanie zawartości pola. Rozmiar struktury jest zawsze większy lub równy niż suma rozmiarów pól składowych. struct atrybut { int bit1 : 1; int bity2_10 : 9; int bit11 : 1; int bity12_23 : 12; // rozmiar struktury 23 bity } a; … a.bit1 = 1; a.bity2_10 =32; a.bit11 = 1; a.bity12_23=1024; printf("%d\n", a.bit1); // -1 printf("%d\n", a.bity2_10); // 32 printf("%d\n", a.bit11); // -1 printf("%d\n", a.bity12_23); // 1024 printf("%d\n", sizeof(a)); //3

Deklaracje klas (w C++) Klasa jest obiektem składającym się ze zbioru nazwanych składowych: - pól (fields) lub atrybutów (attributes) różnych typów; - metod (usług, operacji) (methods, services, operations) zwracających wartości różnych typów. Specyfikator klasy ma postać: class nazwa_klasy [(lista_inicjalizacyjna)] [:nazwa_klasy_bazowej [(lista_wyrazen_inicjujacych)]] [;] Przykład class student { private: float srednia; // pole public: int stu_ocena_sr(int ID_stu); // metoda };

Wyliczenia

Deklarowanie typu wyliczeniowego Wyliczenia są szczególnymi typami o wartościach ze zbioru nazwanych stałych – tzw. wyliczników. Postać specyfikatora wyliczania została zapożyczona od struktur i unii: enum [<nazwa_typu >] {<nazwa_wylicznika> [= <wyrazenie_stale>], ...}; Dopuszczalne jest, by dwa różne identyfikatory miały taką samą wartość (np. stosowane po to, aby wykonywać tę sama akcję dla różnych wartości typu wyliczeniowego). W przypisaniach mogą wystąpić wyrażenia stałe, kompatybilne z typem int. Wyliczenie obejmuje ciąg wartości stałych o logicznie powiązanym znaczeniu. Przykład enum trzy_kolory {CZERWONY, ZIELONY=10, NIEBIESKI=10} kol1,kol2=ZIELONY,kol3=NIEBIESKI,kol4,kol5; … kol1=CZERWONY; printf("%d\n",kol1); //0 printf("%d\n",kol2); //10 printf("%d\n",kol3); //10 kol4=0; printf("%d\n",kol4); //0 kol5=2; printf("%d\n",kol5); //2

Przykład (1/3) Kolorowe napisy. enum te_kolory {CZER=1, ZIEL=2, NIEB=3}; … te_kolory kol; int c; randomize(); c=1+random(16)%3; kol= (te_kolory)(c); // losowanie kolor u switch (kol) { default: textcolor(LIGHTGRAY); cprintf("Kolor szary \n\r"); break; case CZER : textcolor(RED); cprintf("Kolor czerwony \n\r"); break; case ZIEL : textcolor(GREEN); cprintf("Kolor zielony \n\r"); break; case NIEB: textcolor(BLUE); cprintf("Kolor niebieski \n\r"); break; }

Przykład (2/3) Wypisywanie nazw kolorów zdefiniowanych jako wyliczniki. enum te_kolory {CZER=1, ZIEL=2, NIEB=3}; … te_kolory kol; char *kolory[15]={{""},{"CZEROWNY"},{"ZIELONY"},{"NIEBIESKI"}}; for (kol=CZER; kol<=NIEB; kol++) printf("%s\n",kolory[kol]);

Przykład (3/3) Wykorzystanie typu wyliczeniowego w charakterze typu argumentu funkcji. void wypisz_kolor(te_kolory k) { char *kolory[15]={{""},{"CZEROWNY"},{"ZIELONY"},{"NIEBIESKI"}}; printf("%s\n",kolory[k]); }

Deklaratory

Definicja deklaratora Deklarator to notacja zapisu typów, funkcji, tablic i innych elementów składni języka. Deklaratory pojawiają się po specyfikatorach klas pamięci i typów.

Deklaratory wskaźników Deklaratory wskaźników mają składnię: Nazwa_typu *deklarator_wskaznikow; Przykład int i=1; int *a[3]; int *wsk; int *const cwsk=&i; /*cwsk wskazuje zawsze to samo miejsce pamięci, chociaż wartość tego miejsca można zmieniać*/ const int c=3, *pwsk; /* wartości c nie można zmieniać, ale można ją zainicjować; wartośc pwsk może się zmieniać, ale wskazanego miejsca pamięci nie można zmieniać*/

Deklaratory tablic Deklaratory tablic mają składnię: Nazwa_typu deklarator_tablicy; Przykład int t[9]; char tc[10];

Deklaratory funkcji Deklarator funkcji ma postać: Nazwa_typu deklarator_funkcji; gdzie deklarator_funkcji ma postać: nazwa_funkcji (lista_parametrow) Przykłady deklaracji parametrów formalnych funkcji: int func(void) //bez parametrów, np. przykład: printf(”f=%d”,func()); int func(int t1, int t2, int t3=1)  /* trzy parametry typu int, ostatni z domyślną wartością */ przykład: printf(”f=%d”,func(1,2)); int func(int* ptr1, int& tref)  // wskaźnik i referencja przykład: printf(”f=%d”,func(&a,b)); //int a,b;

Inicjowanie Deklarator zmiennej może zawierać inicjator. Inicjator, który poprzedza operator (=), jest albo wyrażeniem stałym, albo listą inicjatorów zawartą w nawiasach klamrowych. Przykład int i=0; const float pi=3.14; float ppi=2*pi; int t[3]={1,2,3}; int ta[]={2,3,5}; char string[] = ”to jest napis\n“; int y[2][3] ={{1,3,5},{2,4,6}}; /* inicjowanie tablicy wielowymiarowej*/ int y[2][3]={{1},{2}}; //inicjowanie pierwszej kolumny

Nazwy typów Podanie nazwy typu danych jest konieczne w kilku przypadkach: Przy wymuszaniu przekształcenia typu przez rzutowanie; Przy deklarowaniu typów w deklaratorze funkcji; W argumencie operatora sizeof. Przykład int x; x=int(x+1.5); void fun(void); l=sizeof(double);

Nazwy typedef Deklaracje, w których specyfikatorem klasy pamięci jest typedef, nie deklarują zmiennej, lecz definiują identyfikatory jako nazwy typów pochodnych (użytkownika). O takich identyfikatorach mówimy, że są nazwami typedef. Składnia nazw typedef ma postać: typedef <definicja_typu> <identyfikator> ; Przykład typedef unsigned char byte; typedef char str40[41]; typedef struct { double re, im; } complex;

Instrukcje

Instrukcje Jeśli nie określono inaczej, instrukcje są wykonywane sekwencyjnie. Instrukcje wykonuje się w celu osiągnięcia pewnych efektów, najczęściej stosowane są do zapisu algorytmu i do sterowania wykonywaniem programu. Instrukcje nie mają wartości. Rozróżnia się następujące grupy instrukcji: Instrukcje etykietowane; Instrukcje wyrażeniowe; Instrukcje złożone; Instrukcje wyboru; Instrukcje powtarzania; Instrukcje skoku.

Instrukcje etykietowane Etykieta służy do oznaczania miejsc w obrębie funkcji, gdzie zostanie przeniesione sterowanie wykonywaniem programu po wykonaniu stosownej instrukcji goto. Etykieta ma identyfikator skonstruowany wg tych samych zasad jak identyfikator zmiennej, przy czym kończy się on dwukropkiem (:). Etykietą można poprzedzić każdą instrukcję w tej samej funkcji, w której występuje instrukcja goto. Instrukcje mogą być poprzedzone przedrostkami etykietowanymi na dwa sposoby: identyfikator_etykiety : instrukcja; case wyrazenie_stale : instrukcja; default : instrukcja; Etykiety przypadków (case) i domyślna (default) są używane w instrukcji switch. Przykład 1 et1: // etykieta a=5; ... goto et1;

Instrukcje złożone (bloki) Aby można było użyć kilku instrukcji, tam gdzie powinna być jedna, wprowadzono instrukcję złożoną, zwaną blokiem. Składnia bloku jest następująca: {[lista_deklaracji] [lista_instrukcji] } Przykład int i=1; void main(void){ int i=2; { int i=3; printf("i_globalne=%d, i_lokalne=%d",::i,i); }

Instrukcje wyboru Instrukcje wyboru służą do wybrania jednej z kilku ścieżek wykonywania programu. Wyróżnia się następujące instrukcje wyboru: Instrukcja if; Instrukcja switch.

Instrukcja if Istnieją dwie postacie syntaktyczne instrukcji if: - if (wyrazenie) instrukcja; - if (wyrazenie) instrukcja; else instrukcja1; W obu postaciach instrukcji if wyrażenie musi mieć typ arytmetyczny lub wskaźnikowy. Jeśli wartość wyrażenie jest różna od zera (TRUE), to wykonywana jest instrukcja. W drugiej postaci, jeśli wartość wyrażenia jest równa zeru (FALSE), to wykonywana jest instrukcja1. Przykład if (val==1) printf(”TRUE”); else printf(”FALSE”)

Instrukcja switch Instrukcja switch powoduje przekazanie sterowania do jednej z szeregu instrukcji, zależnie od wartości wyrażenia, które musi mieć typ całkowity. Składnia instrukcji switch: switch ( <wyrazenie_stale> ) { case <wartosc_stala> : <instrukcja>; [break;] . [default : <instrukcja>;] } Przykład switch (ch) { case ‘a’ : printf(”A\n”); break; case ‘b’ : printf(”B\n”); break; default : printf(”KONIEC\n”); };

Pętle programowe

Rozdaje pętli programowych Wyróżnia się trzy rodzaje pętli programowych: Pętle z wartownikiem Pętle z licznikiem Pętle ogólne

Pętle z wartownikiem Pętle z wartownikiem, w której dane są czytane i przetwarzane aż do napotkania szczególnego, niedozwolonego elementu. Wartownik to wartość powodująca zakończenie pętli. Nie jest ona przetwarzana, a pętla zostaje przerwana. Pętle z wartownikiem wymagają wczytania danej przed pierwszym wykonaniem testu. Tę operację nazywa się odczytem początkowym. Pseudokod pętli z wartownikiem ma postać: do { wczytaj daną; przetwórz daną; } while (dana nie jest wartownikiem);

Pętle z licznikiem Pętle z licznikiem, stosuje się w sytuacjach, gdy a priori znana jest liczba powtórzeń pętli. Licznika używamy do kontroli liczby przebiegów pętli. Najczęściej do implementacji pętli z licznikiem wykorzystuje się instrukcję for, chociaż można użyc instrukcji while, aby wyraźnie rozdzielić inicjację od testu i treści pętli. Pseudokody pętli z licznikiem mają postacie: for (inicjowanie licznika; warunek końcowy; wyrażenie modyfikujące licznik) przetwarzanie danych; a=10; i=0; while (i<a) { -- przetwarzanie danych i++;}

Pętle ogólne Pętle ogólne, to wszystkie pętle, które nie są ani pętlami z wartownikiem, ani pętlami z licznikiem. Pętle ogólne implementowane są zazwyczaj z użyciem instrukcji while.

Instrukcje powtarzania (organizacji pętli programowych Instrukcje powtarzania wskazują iteracyjne wykonywanie programu. Istnieją trzy instrukcje organizacji pętli: while (wyrazenie) instrukcja; do instrukcja while (wyrazenie); for([wyrazenie1]; [wyrazenie2]; [wyrazenie3]) instrukcja;

Instrukcja while W instrukcji while wykonanie instrukcji powtarza się dopóty, dopóki wartość wyrażenia jest różna od zera (TRUE); wyrażenie musi mieć typ arytmetyczny lub wskaźnikowy. Przykład i=0; while (i<3) { printf(”%d”,a[i]); i++; };

Instrukcja do-while W instrukcji do-while wykonanie instrukcji powtarza się dopóty, dopóki wartość wyrażenia jest różna od zera (TRUE); wyrażenie musi mieć typ arytmetyczny lub wskaźnikowy. Przykład i=0; do { printf(”%d”,a[i]); i++; } while (i<3);

Instrukcja for W instrukcji for wyrazenie1 oblicza się tylko raz; jest to część inicjująca stan pętli. Nie ma ograniczeń dotyczących typu tego wyrażenie. Drugie wyrażenie musi mieć typ arytmetyczny lub wskaźnikowy; jest ono obliczane przed każdym przebiegiem pętli i jeśli stanie się zerem (FALSE), to wykonanie instrukcji for zostaje przerwane. Trzecie wyrażenie jest obliczane po każdym przebiegu pętli i określa zmianę stanu pętli. Nie ma ograniczeń dotyczących typu tego wyrażenie. Dowolne z trzech wyrażeń można pominąć. Pominięcie drugiego wyrażenia jest równoważne z zastąpieniem go stałą różną od zera. Składnia: for ([wyrazenie1]; [wyrazenie2]; [wyrazenie3]) instrukcja; Przykład for (i=0;i<3;i++) printf(”%d”,a[i]);

Instrukcje skoku Instrukcje skoku powodują bezwarunkowe przekazanie sterowania do innego miejsca w programie. Instrukcjami skoku są: - continue - break return <wyrażenie> goto identyfikator_etykiety

Instrukcja continue Instrukcja continue może wystąpić tylko w instrukcjach powtarzania. Powoduje przekazanie sterowania do miejsca wznawiania najciaśniej otaczającej ją pętli. Przykład void main (void) { for (i = 0; i < 20; i++) { if (array[i] == 0) continue; array[i] = 1/array[i]; }

Instrukcja break Instrukcja break może wystąpić tylko w instrukcjach powtarzania i instrukcji switch i powoduje przerwanie najciaśniej otaczającej ją takiej instrukcji; sterowanie przekazywane jest do następnej po przerwanej instrukcji. Przykład void main (void){ int l=0; for (i = 0; i < 20; i++) { if (array[i] == 0) break; l++; array[i] = 1/array[i]; }; printf(”l=%d”,l); }

Instrukcja return Instrukcja return powoduje powrót do miejsca wywołania. Jeśli po słowie return występuje wyrażenie, to wartość tego wyrażenia jest przekazywana do miejsca wywołania. Wartość wyrażenia jest przekształcana do typu zwracanego przez funkcję. Przykład double sqr(double x) { return (x*x); }

Instrukcja goto W instrukcji goto identyfikator etykiety musi być umieszczony w tej samej funkcji. Sterowanie jest przekazywane do instrukcji opatrzonej tą etykietą. Przykład powtorz: //etykieta i=x+2; … goto powtorz;

Deklaracje zewnętrzne Tekst programu przekazany do kompilatora jest jednostką tłumaczenia. Jednostka ta składa się z ciągu deklaracji zewnętrznych, które są albo instancjami, albo definicjami funkcji. Składnia deklaracji zewnętrznej jest taka sama, jak wszystkich innych deklaracji, jednakże tylko na tym poziomie można zdefiniować funkcję. Przykład int licznik; //deklaracje zewnętrzne int potega(float p, int w); void main (void){}

Definicje funkcji Definicja funkcji ma postać: typ identyfikator_funkcji([parametry]) Dozwolonymi specyfikatorami klas pamięci w specyfikatorach deklaracji są tylko extern i static. Funkcja może zwracać wartości typu arytmetycznego, klasę, strukturę, unię, wskaźnik lub void; natomiast nie może zwrócić funkcji ani tablicy. Przykład int max (int, int, int); //deklaracja funkcji int main(void) { return (0); } int max (int a, int b, int c) //definicja funkcji { int m; m=(a>b) ? a:b; return (m>c) ? m:c; };

Deklaracje zewnętrzne Deklaracje zewnętrzne opisują właściwości przestrzeni nazw, tj. instancji, funkcji, nazwy typedef, stałych wyliczeń (elementy enum), etykiet, identyfikatorów (etykietki) klas, struktur, unii i wyliczeń (enum). Klasę pamięci dla zmiennych zadeklarowanych na zewnątrz można pominąć lub można podać jako extern lub static. Zmienne zadeklarowane zewnętrznie są zawsze statyczne. Można je uczynić wewnętrznymi dla danej jednostki kompilacji (pliku) za pomocą słowa kluczowego static; to nadaje im łączność wewnętrzną. Zmienne stają się globalne dla całego programu, gdy pominie się deklarację klasy pamięci lub stosuje słowo kluczowe extern nadające im łączność zewnętrzną.

Zasięg i łączność nazw Cały program nie musi być tłumaczony w tym samym czasie: tekst źródłowy może być trzymany w kilku plikach zawierających jednostki tłumaczenia, a uprzednio przetłumaczone podprogramy można dołączać z bibliotek. Komunikacja między funkcjami programu może być realizowana zarówno przez wywołania, jak i przez manipulowanie danymi zewnętrznymi. Istnieją zatem dwa rodzaje zasięgu nazw: Wewnętrzny – dotyczy tego fragmentu programu, w którym znany jest identyfikator; jest to tzw. zasięg leksykalny identyfikatora. Zewnętrzny – dotyczy powiązań między identyfikatorami pochodzącymi z oddzielnie kompilowanych jednostek tłumaczenia.

Program w C

Model programu Program w C zawiera sekwencje deklaracji i definicji, które mogą określać dane lub funkcje.

System typów w C System typów opisuje wartości, które mogą być przyjmowane przez określone dane. W języku C istnieje nieskończony zbiór typów, z których każdy można połączyć z konkretną zmienna. Te typy oraz zasady ich konstruowania tworzą systemów typów języka C. Podstawowe typy danych języka C: Znaki (char (-128-127), unsigned char(0-255)); Liczby całkowite (int(-2*109-2*109), short int(-32K-32K), unsigned int (0-4*109), long(-2*109-2*109), unsigned long (0-4*109)); Liczby zmiennopozycyjne (float(3.4*10-38-3.4*10+38), double(1.7*10-308-1.7*10+308), long double(3.4*10-4932-1.1*10+4932)); Wyliczenia (enum) Wskaźniki (near (32 bity), far (32bity))

Reguły formułowania typów Reguły formułowania typów wymagają istnienia pewnych typów, które mogą być albo typami podstawowymi (predefiniowanymi), albo wcześniej skonstruowanymi za pomocą tych reguł. Przykłady reguł formułowania typów w C: Tablice Struktury Unie Wskaźniki Specyfikator typedef

Specyfikator typedef Specyfikator typedef umożliwia tworzenie synonimów dla nazw typów. Przykłady: typedef unsigned int odleglosc; typedef struct { double re, im; } s_zespol; typedef s_zespol t_zespol[10];

Łączenie danych Język C zawiera bogaty zestaw operatorów umożliwiających manipulowanie danymi i łączenie ich wartości. Operatory podstawowe: Operatory arytmetyczne Operatory logiczne Operatory porównania Operatory działań na poziomie bitowym Operatory przypisania Operatory rzutowania (koercji)

Operatory arytmetyczne Operatory arytmetyczne (atithmetic operators): dwuargumentowe (*, /, +, -) jednoargumentowe (+, -) operator modulo (i % j) operatory zwiększania i zmniejszania o jednostkę (++, --)

Operatory logiczne Operatory logiczne (logical operators): koniunkcji (&&): wyrażenie x && y zwraca wartość 1, jeśli oba operandy są różne od zera. Uwaga: wartość y w ogóle nie jest obliczana, jeśli x ma wartość 0; alternatywy (||):wyrażenie x || y zwraca wartość 1, jeśli przynajmniej jeden operand jest różny od zera. Uwaga: wartość y w ogóle nie jest obliczana, jeśli x ma wartość różną od zera; jednoargumentowy operator negacji (!): wyrażenie !x zwraca 0, jeśli x jest rózne od zera. trójargumentowy operator warunkowy (?):wyrażenie x? y : z zwraca wartość y, jeśli x jest różne od zera oraz wartość y, jeśli x jest zerem.

Operatory porównania Operatory porównania (comparasion operators). Wynikiem stosowania każdego z sześciu operatorów porównania (==,!=,<,>,<=,>=) jest 0 jeśli relacja jest nieprawdziwa i 1 jeśli relacja jest prawdziwa.

Operatory działań na poziomie bitowym Operatory działań na poziomie bitowym (bitwise manipulation operators). Operatory te traktują liczby całkowite jak ciągi bitów odpowiadające ich reprezentacjom binarnym. Należą do nich operatory: Koniunkcji poziomu bitowego (&) Alternatywy poziomu bitowego (|) Różnicy symetrycznej (^) Przesunięcia bitowego w lewo (<<) Przesunięcia bitowego w prawo (>>)

Operatory przypisania Operatory przypisania (assignment operators) (=,+=,-=,/=,*=, %=, <<=, >>=)

Operatory koercji Operatory koercji (rzutowania) (coersion operators). Koercja to proces przekształcania wartości jednego typu na odpowiadająca jej wartość innego typu. Przykład x=float(i)/2 ;

Funkcje

Określenia funkcji Funkcja to nazwana operacja lub nazwany kod. Funkcja jest grupą powiązanych instrukcji (fragmentem kodu programu), które realizują określone zadanie. Funkcje są podprogramami, które mogą być wielokrotnie wykonywane dla różnych danych wejściowych.

Deklaratory funkcji Deklarator funkcji ma postać: Nazwa_typu deklarator_funkcji; gdzie deklarator_funkcji ma postać: nazwa_funkcji (lista_parametrow) Przykłady deklaracji parametrów formalnych funkcji: int func(void) //bez parametrów, np. przykład: printf(”f=%d”,func()); int func(int t1, int t2, int t3=1)  /* trzy parametry typu int, ostatni z domyślną wartością */ przykład: printf(”f=%d”,func(1,2)); int func(int* ptr1, int& tref)  // wskaźnik i referencja przykład: printf(”f=%d”,func(&a,b)); //int a,b;

Parametry (argumenty) funkcji Funkcja może przyjmować i zwracać pewne wartości. Wartości przekazywane są do funkcji jako parametry. Wyróżnia się dwa rodzaje parametrów: Parametry formalne; Parametry aktualne. Parametr formalny (formal parameter) to nazwa i typ parametru określone w ramach jej definicji lub deklaracji. Parametr aktualny (actual parameter) wyrażenie występujące w miejscu pewnego parametru formalnego funkcji, albo wartość parametru obliczona w momencie wywołania funkcji.

Przekazywanie parametrów Przekazywanie parametrów (parameters passing) to przesyłanie wartości lub wyrażeń użytych w wywołaniu funkcji do środowiska wykonywania funkcji. W języku C istnieją dwie techniki przekazywania parametrów: Wywołanie przez wartość (call-by-value); Wywołanie przez referencję (call-by-reference), gdzie przekazywana jest referencja do zmiennej. Wywołanie przez wskaźnik (tylko w C++)

Wywołanie przez wartość (1/2) Wywołanie przez wartość realizowane jest poprzez przekazanie wartości, np. w postaci kopii wartości zmiennej. Argument formalny jest traktowany jak zmienna lokalna funkcji, tzn. wszelkie operacje na argumencie tracą ważność po zakończeniu funkcji. Przykład 2. int plus1(int s){ s=s+1; return s;} int s=1; printf("s=%d s+1=%d",s,plus1(s)); Przykład 1. int sign(float x){ int s; float eps=0.0001; if (x<0.0) s=-1; else if (fabs(x)<=eps) s=0; else s=1; return s;} //czy poprawnie?

Poprawna postać funkcji sign() int sign(float x) { const float eps=1.0e-5; if (fabs(x) <=eps) return 0; else if (x<eps) return -1; else return 1; }

Wywołanie przez wartość (2/2) Przykład 2. Tablice jednowymiarowe. float srednia(int n, float t[]) { unsigned int i; float sre; sre=0.0; for (i=0;i<n;i++) sre=sre+t[i]; sre=sre/n; return sre; } … printf("sre=%f",srednia(5,t)); Przykład 3. Tablice wielowymiarowe. float t[2][2]; … float srednia(int wierszy, int kolumn, float t[][2]){ unsigned int i,j; float sre; sre=0.0; for (i=0;i<wierszy;i++) for (j=0;j<kolumn;j++) sre=sre+t[i][j]; sre=sre/(wierszy*kolumn); return sre;} printf("sre=%f",srednia(2,2,t));

Wywołanie przez referencję (1/2) Wywołanie przez referencję polega na przekazywaniu referencji do zmiennej. Przykład void zer(int a, int &b) { a=0; b=0; } … int a=1,b=2; printf("a=%d b=%d",a,b); //a=1 b=2 zer(a,b); printf("a=%d b=%d",a,b); //a=1 b=0

Wywołanie przez referencję (2/2) Tablice przekazywane są przez referencję. Przykład. Transponowanie macierzy. //deklaracja (specyfikacja) funkcji: void transp(int n, int t[][3]); int main(void){ int t[3][3]; … transp(3,t); } //definicja funkcji: void transp(int n, int t[][3]){ int i,j,tmp; for (i=0;i<3;i++) for (j=i;j<3;j++) { tmp=t[i][j]; t[i][j]=t[j][i]; t[j][i]=tmp; }}

Wywołanie przez wskaźnik Jest to odmiana przekazywania parametrów przez wartość. Funkcja otrzymuje kopie wskaźnika, który może określać adres zmiennej należącej do innej funkcji. Zmiana zawartości obszaru wskazywanego przez parametry wskaźnikowe prowadzi do zmiany wartości zmiennych utworzonych w innych funkcjach programu. Przekazywanie parametrów przez wskaźniki jest stosowane: do przekazywania struktur danych o dużych rozmiarach (np. tablic); w celu umożliwienia zwrócenia więcej niż jednej wartości. t_struct lista(t_struct *head, t_struct *x) Argumentami aktualnymi mogą być wskazania do odpowiednich typów (wartości zmiennych wskaźnikowych odpowiednich typów): head=lista(head,x);

Własności funkcji typ zwracanej wartości jest podawany przed nazwą funkcji; lista parametrów funkcji może być pusta; można ten fakt podkreślić bezpośrednio posługując się słowem kluczowym void; funkcja może zwracać wynik dowolnego typu z wyjątkiem tablic i funkcji; jeśli nie podano typu funkcji, to domyślnie przyjmowany jest typ int; lista parametrów funkcji ma następującą postać: typ_1 par1, typ_2 par2, ... parametry deklarowane są tak jak zmienne; każdy parametr musi mieć indywidualnie zadeklarowany typ; parametry w liście są rozdzielone przecinkami; początek i koniec funkcji określają nawiasy klamrowe; w C parametry funkcji mogą być przekazywane przez wartość lub referencję, w C++ dodatkowo przez wskaźnik; wewnątrz funkcji nie mogą pojawiać się definicje innych funkcji; za pomocą słowa kluczowego return zwracana jest wartość funkcji; return wartość kończy działanie funkcji; jeżeli funkcja nie musi zwracać wyniku, to jako typu wartości należy podać void.

Deklaracja funkcji Deklaracja funkcji jest pojęciem logicznym. Stanowi informację dla kompilatora, że funkcja o określonej nazwie, typie wartości oraz liczbie i typie parametrów może zostać użyta (ale nie musi) w danym module programu. Deklaracja funkcji musi kończyć się średnikiem. Deklaracja funkcji nazywana jest prototypem funkcji. double sum(double, double); //prototyp funkcji Pliki nagłówkowe *.h, dołączane dyrektywą #include, zawierają prototypy funkcji, których kody wynikowe dołączane są na etapie łączenia z bibliotek standardowych. Prototyp funkcji mówi kompilatorowi jakiego typu wartość jest zwracana przez funkcję oraz jakie są jej parametry. Dzięki temu możliwe jest podczas kompilacji sprawdzenie czy w poprawny sposób wykorzystywana jest wartość zwracana przez funkcję oraz czy podawane są parametry odpowiednich typów. Ponadto, prototypy pozwalają szybko zorientować się jakie funkcje są dostępne i jak z nich korzystać.

Definicja funkcji Definicja funkcja mówi o tym co funkcja robi. Zawiera instrukcje umożliwiające realizację odpowiedniego fragmentu programu. W odróżnieniu od deklaracji przydzielany jest obszar pamięci, w którym znajdzie się kod wynikowy funkcji. Przykład double sum (double x, double y)// definicja funkcji { return (x+y); }

Uwagi (1/2) Jeżeli typy argumentów przekazywanych do funkcji nie są zgodne z typami określonymi w definicji czy prototypie, to przeprowadzane są konwersje do typów określonych w prototypie. Podczas kompilacji w trybie języka C++ wywołanie funkcji bez prototypu lub definicji traktowane jest jako błąd. Ponadto, błędy zgłaszane są w następujących przypadkach: podano za mało parametrów, podano zbyt wiele parametrów, występuje niezgodność typu parametrów (mimo konwersji). Zaleca się dołączanie prototypów funkcji poprzez włączanie odpowiedniego zbioru nagłówkowego dyrektywą #include.

Uwagi (2/2) Jeżeli podane są prototypy to, definicje o takich samych nagłówkach powinny znajdować się poza funkcją main. W prototypach zazwyczaj nie podaje się nazw argumentów, a jedynie ich typy. Nazwy podane w proto­typach nie muszą być powtórzone w defini­cji, gdyż i tak sprawdz­ane są typy danych. Prototyp: double sum(double x, double y); Definicja: double sum(double a, double b) { return (a+b); } Zaleca się, aby nazwy argumentów w prototypach i definicjach były zgodne. W języku C++ mogą istnieć funkcje o tych samych nazwach i różnych parametrach, które zwane są funkcjami przeciążonymi. Na podstawie typu funkcji i liczby parametrów kompilator wybiera właściwą funkcję.

Funkcje z atrybutem inline (1/2) Wywołanie funkcji wiąże się z umieszczeniem argumentów na stosie oraz wykonaniem skoku pod odpowiedni adres. W przypadku funkcji o niewielkich rozmiarach czas związany z jej wywołaniem może być porównywalny z czasem wykonania funkcji. Jeżeli chcemy, aby kod funkcji był umieszczany w miejscu jej wywołania, to należy poprzedzić definicję funkcji atrybutem inline. Pozwala to zwiększyć szybkość odwołań do funkcji. Funkcje z atrybutem inline nie powinny zawierać słów kluczowych: do, while, for, goto, switch, case, break, continue. Wystąpienie jednego z wymienionych słów spowoduje, że kompilator potraktuje funkcję jako zwykłą funkcję statyczną.

Funkcje z atrybutem inline (2/2) inline double pot(double x) { return x*x*x; } inline int max(int a, int b) { return (a > b ? a : b); } void main(void) { int a = 2; int b = 7; printf(”x^3=%d”,pot(a)); // x^3 = 8 printf(”Max=%d”, max(9, max(a,b))); // Max = 9 } W przypadku wywołania max(9, max(a,b)) funkcja max zostanie dwukrotnie włączona do kodu wynikowego.

Rekurencja

Definicja rekurencji Rekurencja (rekursja) to sposób definiowania (potencjalnie) nieskończonych struktur danych i podprogramów przy użyciu ich wywołań.

Definicja rekurencyjna Definicja rekurencyjna składa się z dwóch części. W pierwszej, zwanej przypadkiem brzegowym (base case) lub początkowym, są wyliczane elementy podstawowe, stanowiące części składowe wszystkich pozostałych elementów zbioru. W drugiej, zwanej krokiem indukcyjnym (recursion), są podane reguły są podane reguły umożliwiające konstruowanie owych bytów z elementów podstawowych lub bytów zdefiniowanych wcześniej. Reguły te można stosować wielokrotnie, tworząc nowe byty. Przykład. Do skonstruowania zbioru liczb naturalnych potrzebny jest element podstawowy jeden i operacja zwiększania o jeden: 1. 1N 2. jeśli nN, to (n+1) N. 3. nie ma żadnych innych bytów w zbiorze N oprócz tych, których istnienie wynika z reguł 1 i 2. Definicji rekurencyjnych używa się często do: Definiowania struktur danych Definiowania podprogramów

Rekurencyjne (rekursywne) typy danych Rekurencyjny (rekursywny) typ danych to taki typ danych, którego definicja zawiera odwołanie do typu definiowanego. Przykład. Drzewo genealogiczne. struct s_rod { char nazwisko[20]; char imie[20]; s_rod *ojciec, *matka; }; ... struct s_rod *my_dg, *my_dg_ojciec; my_dg=(s_rod *)malloc(sizeof(struct s_rod)); strcpy(my_dg->nazwisko,"Abacki"); my_dg_ojciec=(s_rod *)malloc(sizeof(struct s_rod)); strcpy(my_dg_ojciec->nazwisko,"Abacki-senior"); my_dg->ojciec=my_dg_ojciec; free(my_dg_ojciec); free(my_dg);

Rekurencyjne definiowanie podprogramów (1/2) Rekurencyjne definiowanie podprogramów stosowane jest najczęściej do rozwiązywania problemów poprzez rozłożenie ich na prostsze podproblemy tego samego rodzaju. Ta metoda jest czasem bardzo pomocna (np. w algorytmie szybkiego sortowania quicksort), a czasem bardzo kłopotliwa. Przykład. Definicja rekurencyjna funkcji silnia: n! = n*(n-1)*...*1 = n* (n-1)! Inna postać definicji rekurencyjnej:

Rekurencyjne definiowanie podprogramów (2/2) Przykład. Definicja rekurencyjna liczb wymiernych: Stosując tę definicję uzyskuje się ciąg liczb wymiernych: 1,2, 5/2, 29/10, 941/290, ... .

Techniki rekurencyjnego wywoływania podprogramów Rekurencyjne wywoływania podprogramów mogą być realizowane na wiele sposobów: bezpośrednio albo w sposób bardziej zawikłany, jedno- lub wielokrotnie. Może być wiele poziomów rekurencji i rzędów złożoności. Techniki rekurencyjnego wywoływania podprogramów: Rekurencja końcowa Rekurencja niekońcowa Rekurencja pośrednia Rekurencja zagnieżdżona

Rekurencja końcowa Rekurencja końcowa (tail recursion) cechuje się obecnością tylko jednego wywołania rekurencyjnego na samym końcu podprogramu. Zatem, w miejscu wywołania nie pozostają już żadne instrukcje do wykonania; nie ma żadnych wcześniejszych wywołań rekurencyjnych, bezpośrednich ani pośrednich. Przykład. Rekurencja końcowa void tail(int i) { if (i>0) { printf("%d\n",i); tail(i-1);} }; ... tail(3); ///3 2 1

Rekurencja niekońcowa (1/2) Rekurencja niekońcowa cechuje się obecnością tym, że w ciele funkcji występuje jedno lub większa liczba wywołań rekurencyjnych w dowolnym miejscu ciała funkcji. Przykład. Rekurencja niekońcowa. void NoTail(int i) { if (i>0) { NoTail(i-1); printf("%d\n",i); NoTail(i-1);} }; ... NoTail(3); //1 2 1 3 1 2 1

Rekurencja niekońcowa (2/2) Przykład. wypisywanie ciągu znaków w odwrotnej do wczytanej kolejności. void reverse(void) { char c; if ((c=getchar())!='\n') { reverse(); putchar(c); }}

Rekurencja pośrednia Omawiane dotychczas techniki wywołań rekurencyjnych polegały na bezpośrednim wywoływaniu funkcji przez samą siebie. Funkcja może byc wywoływana pośrednio – przez łańcuch innych wywołań. Przykład. Obliczanie wartości funkcji trygonometrycznych – sinus, cosinus, tangens. przy czym warunek brzegowy określa wyrażenie:

Rekurencja zagnieżdżona (1/2) Rekurencja zagnieżdżona ma miejsce wówczas, gdy funkcja jest nie tylko zdefiniowana przez samą siebie, ale także jest użyta jako jeden z parametrów.

Rekurencja zagnieżdżona (2/2) Przykład. Funkcja Ackermann’a. Funkcja Ackermann’a Na początku XX wieku wydawało się, że funkcje pierwotnie rekurencyjne stanowią klasę wszystkich funkcji obliczalnych. Plan Hilberta był skonstruować całą matematyce na podstawie elementarnych operacji typu tych z funkcji pierwotnie rekurencyjnych, ale funkcja Ackermann'a, która wydaje się bardzo dobrze zdefiniowaną (podwójną rekurencją) i intuicyjne obliczalną nie jest pierwotnie rekurencyjną, nie może zatem być rozpatrywana jako podstawa matematyki. Funkcja Ackermann’a, zwana również funkcją Ackermann’a-Peter’a, jest ważną w teorii obliczeń funkcją rekurencyjną, która ma dwa argumenty będące liczbami naturalnymi i zwraca liczbę naturalną.

Istota rekurencyjnego wywołania funkcji Rekurencyjne wywołanie funkcji polega na ponownym jej wywołaniu jeszcze przed jej zakończeniem. Przy każdym wywołaniu rekurencyjnym tworzona jest kopia środowiska funkcji łącznie ze wszystkimi parametrami i zmiennymi lokalnymi. Wywołanie każdej funkcji powoduje umieszczenie na stosie: informacji umożliwiającej powrót z funkcji do miejsca wywołania, parametrów funkcji, zmiennych lokalnych (automatycznych) istniejących w momencie wywołania funkcji. Ze względu na ograniczoną pojemność stosu funkcje rekurencyjne nie powinny posiadać zbyt dużej liczby parametrów i zmiennych lokalnych ani też zmiennych o dużych rozmiarach.

Rekurencyjne definiowanie funkcji Przykład rekurencyjnej definicji funkcji silnii (factorial fiunction) f(n): f(0) = 1 f(n) = n *f(n-1)   dla każdego naturalnej liczby n > 0 Dla n=3 otrzymujemy: f(3) = 3 · f(3-1) = 3 · f(2) = 3 · 2 · f(2-1) = 3 · 2 · f(1) = 3 · 2 · 1 · f(1-1) = 3 · 2 · 1 · f(0) = 3 · 2 · 1 · 1 = 6

Wywołania funkcji i realizacja rekurencji Wszystkie współczesne języki programowania umożliwiają implementację funkcji rekurencyjnych. Do rekurencyjnego wywołania w programie funkcji wykorzystywany jest stos. Stan każdej wywoływanej funkcji jest opisany wartością wszystkich zmiennych automatycznych i parametrów funkcji oraz adres powrotu, wskazujący miejsce wznowienia wykonywania procesu, który wywołał funkcję. Obszar danych zawierający te informacje nosi nazwę rekordu wywołań (aktywacji; activation record) lub ramki stosu (stack frame). Jest on umieszczany na stosie programu. Rekord wywołania istnieje aż do zakończenia działania funkcji, której dotyczy. W program C/C++ najdłużej istnieje rekord wywołania dla funkcji main().

Zawartość rekordu wywołania Rekord wywołania zawiera zwykle następujące dane: Wartości wszystkich parametrów funkcji (adres tablicy i kopie wszystkich pozostałych danych). Wartości wszystkich zmiennych lokalnych (automatycznych) lub adres miejsca ich przechowywania (zależy to od kompilatora). Adres powrotu – adres instrukcji następnej po instrukcji wywołującej funkcję. Dynamiczne dowiązanie – wskaźnik do rekordu wywołania procesu wywołującego funkcję. Zwracana wartość – tylko, gdy nie jest to wartość typu void.

Potęga Rekurencyjne wywoływanie funkcji obliczającej podnoszącą dowolną liczbę x do nieujemnej całkowitej potęgi n. Definicja funkcji potęgującej: Poziom wywołania Parametry wywoływanej funkcji Wartości zwracane przez wywoływaną funkcję 1 potega(x,4) x*x*x*x 2 potega(x,3) x*x*x 3 potega(x,2) x*x 4 potega(x,1) x 5 potega(x,0) Poziomy wywołania funkcji potęgującej x4

Schemat Hornera Wielomian: wn(x)=a0xn+a1xn-1+…+an-1x+an można zapisac w postaci: wn(x)=(…((a0x+a1)x+a2)x+.…+an-1)x+an lub w postaci rekurencyjnej: wn(x)= wn-1(x)x+ an, n=1,2,…, gdzie w0(x)=a0.

Rekurencyjny algorytm Euklidesa Przykładowo NWD(18,12)=NWD(6,12)=NWD(0,6)=6.

Niebezpieczeństwa rekurencji Ze stosowaniem rekurencji związanych jest wiele niebezpieczeństw. Dwa najważniejsze spośród nich to: dublowanie obliczeń przepełnienie stosu

Dublowanie obliczeń Dublowanie obliczeń polega na niepotrzebnym wykonywaniu tych samych obliczeń. Przykład. Obliczanie wartości ciągu Fibonacciego. Liczby Fibonacciego są zdefiniowane w sposób rekurencyjny: F0=0 F1=1 Fi=Fi-1+ Fi-2 , dla i>=2. Początkowe litery ciągu Fibonacciego: 0,1,1,2,3,5,8,13,21,34,55,… Liczby Fibonacciego rosną wykładniczo.

Implementacja funkcji obliczającej liczby Fibonacciego Funkcja fib() zwraca wartość ciągu Fibonacciego dla i>=0. int fib(int i) { If (i<2) return i; else return fib(i-1)+ fib(i-2); } Drzewo obliczeń dla funkcji fib(4).

Liczba wywołań funkcji fib() f(n) 6 25 10 177 15 1973 20 21891 242785 30 2692537 Wykazuje się, że w celu obliczenia wartości fib(n) liczba wywołań funkcji fib() wynosi f(n)=2*fib(n+1)-1. Liczba ta jest ogromna już dla niewielkich n, co obrazują tabela i wykres.

Programowanie dynamiczne Aby radzić sobie z takimi problemami można wykorzystać programowanie dynamiczne, które najogólniej polega na odejściu od rekurencji na rzecz programowania iteracyjnego. Programowanie dynamiczne można zastosować do tych problemów, które można podzielić na wiele podproblemów. Wyniki rozwiązań podproblemów są zapamiętywane w odpowiednich strukturach danych (najczęściej w tablicy), unikając w ten sposób wielokrotnych obliczeń dla tego samego podproblemu. Programowanie dynamiczne polega więc na wykonania obliczeń każdego podproblemu tylko raz i zapamiętaniu jego wyniku w tabeli. W każdym kolejnym kroku można z tej tabeli korzystać.

Przykład FIB_DYN(i) { f[0]1 f[1]  1 k  2 while (k<i){ Poniższa funkcja FIB_DYN zwraca wartość ciągu Fibonacciego dla i>=0 obliczoną metodą programowania dynamicznego. FIB_DYN(i) { f[0]1 f[1]  1 k  2 while (k<i){ f[k]=f[k-1]+f[k-2] k=k+1 }

Liczba wywołań funkcji MacCarthy() Przepełnienie stosu Programy rekurencyjne zwykle wykorzystują dużo zasobów pamięciowych. Przykład. Poniższa funkcja zwraca wartość funkcji MacCarthy’ego dla całkowitego x. MacCarthy(x) { If (x>100) return (x-10) else return MacCarthy(MacCarthy(x+11))} Liczba wywołań funkcji MacCarthy()

Wskaźniki

Wskaźnik Wskaźniki są mechanizmem ustanawiającym relacje między komórkami pamięci. Wskaźnik jest komórką, której zawartość jednoznacznie identyfikuje inną komórkę. Wskaźniki to adresy wskazujące miejsce położenia zmiennych dynamicznych w pamięci operacyjnej. Wskaźniki zawierają adresy logiczne, które są zapisane w postaci segment : offset, gdzie część segmentowa określa numer bloku pamięci o rozmiarze 64 KB względem, którego liczone jest przesunięcie offset. Na podstawie adresów logicznych zmiennych i funkcji można wyznaczyć ich adresy fizyczne. Przykład. int *wsk;

Klasyfikacja wskaźników (1/2) Wskaźniki dzielą się na: wskaźniki danych (zawierają adresy zmiennych), wskaźniki kodu (zawierają adresy funkcji). Wskaźniki mogą być dalekie (czterobajtowe), definiowane z wykorzysta­niem słowa kluczowego far i złożone z dwubajtowej części segmentowej i dwubajtowej części offsetowej – dane typu unsigned (np. void far *daleki), lub bliskie (dwubajtowe), definiowane za pomocą słowa kluczowego near i złożone tylko z dwubajtowego offsetu (np. void near *bliski). W przypadku braku słowa near lub far (ANSI C) rodzaj wskaźnika zależy od przyjmowanego w kompilatorze modelu pamięci (np. tiny, small, medium).

Klasyfikacja wskaźników (2/2) W przypadku, gdy w definicji wskaźnika występuje (tylko BC++): near (np. double near *y) wskaźniki są zawsze bliskie, far (np. double far *x, int far *d) wskaźniki są zawsze dalekie, huge (np. double huge *u) są zawsze dalekie znormalizowane, tzn. ich offsety należą zawsze do przedziału od 0 do 15 (przy wszelkich operacjach na wskaźnikach tego typu następuje automatyczna korekcja offsetu i segmentu).

Adresy logiczne i fizyczne Wskaźniki zawierają adresy logiczne, które mają następującą postać: adres_logiczny = segment : offset Interpretacja części segmentowej i offsetowej zależy od trybu adresowania. Sposób wyznaczania adresu fizycznego na podstawie znajomości adresu logicznego (segmentu i offsetu – dane typu unsigned) zależy od trybu adresowania. Wyróżnia się dwa tryby adresowania: adresowanie rzeczywiste adresowanie wirtualne

Adresowanie rzeczywiste W trybie adresowania rzeczywistego (aplikacje dla systemu DOS; kompilator BC) segment określa numer bloku pamięci o rozmiarze 64KB względem, którego liczone jest przesunięcie określone przez offset. Każdy taki blok może zaczynać się od adresu fizycznego (adresu bazowego) podzielnego przez 16 (przykładowe adresy bazowe 0, 16, 32, 48, itd.). Adres bazowy jest równy 16segment, natomiast adres fizyczny oblicza się ze wzoru 16segment + offset. Na przykład wskaźnik 0xb800:0x0001 reprezentuje adres fizyczny równy 0xb8001.

Adresowanie wirtualne W trybie adresowania wirtualnego (aplikacje dla systemu Windows; kompilator BCW) segment jest numerem selektora, który identyfikuje deskryptor definiujący adres początku (adres bazowy) bloku pamięci związanego ze wskaźnikiem. Selektor określa pozycję deskryptora bloku w tablicy deskryptorów, natomiast offset – przesunięcie w bloku. Adres fizyczny oblicza się ze wzoru adres_bazowy + offset.

Zakresy adresowania pamięci przy pomocy wskaźników Wskaźniki bliskie umożliwiają dostęp do komórek pamięci w obrębie bloku 64KB (zmiana offsetu od 0 do 0xFFFF). W trybie adresowania rzeczywistego wskaźniki dalekie umożliwiają dostęp do komórek pamięci o adresach z zakresu od 0 do 0xFFFFF (1 MB), natomiast w trybie adresowania wirtualnego dostęp do komórek pamięci o adresach 32-bitowych, tj. od 0 do 0xFFFFFFFF (4GB).

Notacja graficzna dla wskaźników Notacja graficzna pozwala zobrazować operacje wykonywane na wskaźnikach. W notacji tej występują kwadraty odpowiadająca komórkom pamięci związanym z poszczególnymi zmiennymi, zarówno statycznymi jak i dynamicznymi. Wskaźniki, tzn. wartości zmiennych wskaźnikowych, przestawia się za pomocą strzałek. Wskazane przez nie miejsca PAO są dostępne jedynie poprzez wskaźniki. Zmienne statyczne posiadają etykiety będące ich nazwami.

Przykład int *i1, *i2; i1=malloc(sizeof(int)); *i1=5; nieużytek 7 i1 5 i2

Definiowanie wskaźników (1/3) Przykłady: char *wsk_c; // wskaźnik do zmiennej typu char unsigned char *wsk_uc; // wsk. do zmiennej typu unsigned char int *wsk_i; // wskaźnik do zmiennej typu int long *wsk_l; // wskaźnik do zmiennej typu long float *wsk_f; // wskaźnik do zmiennej typu float double *wsk_d; // wskaźnik do zmiennej typu double Analogicznie definiuje się wskaźniki typu far i huge. float far *wsf; double huge *wsh; Wskaźnik danego typu powinien być wykorzystywany do przechowywania adresów zmiennych tego samego typu.

Definiowanie wskaźników (2/3) W celu inicjacji wskaźnika należy przypisać mu adres zmiennej wykorzystując operator &, który podaje adres swojego argumentu. Jeżeli x jest zmienną, to &x jest stałą określającą adres (położenie w pamięci) zmiennej x. Stała &x może być wykorzystana do zainicjowania wskaźnika pokazującego na początek bloku pamięci, w którym przechowywana jest zmienna x. Przykład int x = 5; // zmienna x typu int zainicjowana stałą 5 int *px = &x; // zmienna wskaźnikowa px zainicjowana stałą &x float v = 1.2; // zmienna v typu float zainicjowana stałą 1.2 // px = &v; // błąd – nie można inicjować wskaźnika do typu int // adresem (wskaźnikiem) do typu float (&v jest typu float *)

Definiowanie wskaźników (3/3) Jeżeli w miejscu deklaracji wskaźnika jego wartość początkowa nie zostanie określona (ustawiona na adres zmiennej odpowiedniego typu), to zostanie on zainicjowany automatycznie zgodnie z regułami obowiązującymi dla zmiennych. W szczególności, wskaźniki statyczne i zewnętrzne są domyślnie inicjowane zerami (wsk = NULL), natomiast wartości początkowe wskaźników lokalnych nie są określone (są wartościami przypadkowymi). Wygodnie jest inicjować wskaźniki lokalne wartością NULL, gdyż można taki przypadek łatwo wykryć w programie. Na przykład: if (wsk!=NULL) { wykonaj instrukcje; }.

Arytmetyka wskaźników Jedynymi dopuszczalnymi operacjami arytmetycznymi na wskaźnikach są: dodawanie i odejmowanie liczb naturalnych, co powoduje przesunięcie wskaźnika odejmowanie dwóch wskaźników pokazujących na elementy tej samej tablicy. Przykład int *w1,*w2; int t[3]={1,2,3}; w1=&t[0]; //w1=t; w2=&t[2]; printf("t=%d\n",(w2-w1)); //2

Porównywanie wskaźników Do porównywania wskaźników można wykorzystać wszystkie operatory relacji: ==, !=, <,<=, >, >=. Warto zauważyć, ze operatory <,<=, >, >= mają sens tylko dla wskaźników pokazujących na elementy tej samej tablicy. Przykład int *wsk, *wsk1,*wsk2; int t[3]={1,2,3}; wsk1=t; wsk2=&t[n]; if (wsk==NULL) printf(”wskaznik nie pokazuje na nic”); if (wsk1<=wsk2) … int *wsk; for (wsk=t;wsk<t+3;) printf("t=%d\n",*(wsk++)); Uwaga: zapis *(wsk++) oznacza najpierw wykonanie *wsk, a potem wsk++

Najważniejsze obszary zastosowania wskaźników Wyróżnia się cztery podstawowe domeny zastosowania wskaźników: praca z tablicami operacje na tekstach wskaźniki w argumentach funkcji; dynamiczne przydzielanie (alokacja) i zwalnianie (dealokacja) obszarów pamięci

Praca z tablicami (1/2) Wskaźniki można wykorzystać do indeksowania tablicy. Przykład 1 int *wsk; int t[10]; wsk=&t[n]; //wsk wskazuje na n. element tablicy t[] wsk++; //wsk wskazuje na (n+1). element tablicy t[] Przykład 2 int *wi; int t[3]={1,2,3}; wi=t; printf("t[0]=%d ma adres=%p\n",*wi,wi); *wi=4; printf("t[0]=%d\n",t[0]);

Praca z tablicami (2/2) Dostęp do tablic za pomocą wskaźników jest szybszy niż dostęp realizowany za pomocą indeksów. W przypadku posługiwania się wyrażeniem tab[i] za każdym razem jest obliczane wyrażenie &tab[0] + i*sizeof(int), które określa adres i-tego elementu tablicy. for (i=0; i<8; i++) printf("tab[%d] = %3d\n", i, tab[i]); Natomiast, w przypadku posługiwania się wskaźnikiem int *wsk=tab realizowana jest tylko operacja wsk++, która powoduje przesunięcie wskaźnika do kolejnej pozycji w tablicy. W przypadku programu: for (i=0, wsk=&tab[0]; i<8; i++) printf("*(wsk+%d) = %3d\n", i, *wsk++);

Operacje na tekstach (1/3) Wskaźniki pozwalają znacznie skrócić operacje na łańcuchach. char lan[] = "tekst"; char bufor[20]; unsigned h = 0; char *src = &lan[0]; char *dst = &bufor[0]; h = strlen(src); // strlen() zwraca liczbę znaków tekstu - bez znaku '\0' printf("Dlugosc lancucha = %d %d\n", h, strlen(lan) ); Zawartość łańcucha lan można przekopiować do łańcucha bufor na kilka sposobów.

Kopiowanie z wykorzystaniem indeksów tablic (2/3) for (i=0; i<=h; i++) bufor[i] = lan[i]; // dla i == h kopiowany znak ‘\0’ Nazwa tablicy jest stałym wskaźnikiem do jej pierwszego elementu. Można więc wyprowadzić teksty za pomocą printf("lan = %s\nbufor = %s\n", lan, bufor); lub uwzględniając, że src = &lan[0] oraz dst = &bufor[0] printf("lan = %s\nbufor = %s\n", src, dst);

Kopiowanie z wykorzystaniem wskaźników (3/3) char *src = &lan[0]; char *dst = &bufor[0]; Wariant 1. while (*src!=0) { *dst = *src; // przypisanie src++; dst++; // zmiana wskaźników } *dst = '\0'; // dodanie 0 na końcu printf("lan = %s\nbufor = %s\n", lan, bufor); Wariant 2. while (*src) { *dst++ = *src++; } *dst = '\0'; // dodanie 0 na końcu

Zastosowanie wskaźników w argumentach funkcji Wskaźniki można wykorzystać do definiowania przekazywania wartości argumentów funkcji przez referencję. Przykład Porównajmy dwa sposoby przekazywania wartości: 1. przez wartość: int plus1(int s){ s=s+1; return s;} … int s=1; printf("s=%d s+1=%d",s,plus1(s)); //1 2 2. przez refencję: void plus1(int *sf){ *sf=*sf+1;} plus1(&s); printf("s=%d",s); //2

Argument formalny będący wskaźnikiem do obiektu const Ponieważ, przy przekazywaniu do funkcji tablicy, przekazywany jest jedynie adres tablicy, więc w ciele funkcji jeśli element tablicy będzie l-wartością, to zmieni się wartość tego elementu. Jeśli nie chcemy, by funkcja modyfikowała zawartość tablicy należy przekazać ją jako wskaźnik do obiektu stałego. Przykład void t_pokaz(const int *wsk, int n){ int i,s=0; for (i=0;i<n;i++) printf("t=%d\n",*(wsk++)); } int t[3]={1,2,3}; int *wsk; t_pokaz(t,3);

Zmienne dynamiczne

Wprowadzenie Wskaźniki na ogół zawierają adresy zmiennych (obiektów), dla których pamięć jest przydzielana automatycznie przez kompilator na podstawie ich definicji. Dostęp do takich zmiennych odbywa się za pomocą ich identyfikatorów. Zmienne te posiadają pewne własności wynikające z ich zasięgu, łączności i czasu trwania. Stałe, zmienne statyczne i zewnętrzne mają zarezerwowane miejsce w kodzie wykonywalnym programu (są umieszczane w obszarze danych programu). Zmienne lokalne (automatyczne) funkcji są umieszczane na stosie w momencie, gdy sterowanie wejdzie do bloku, w którym zostały zdefiniowane. Zmienne tej klasy znikają po wyjściu sterowania z bloku. Istnieje możliwość dynamicznego tworzenia i kasowania zmiennych w trakcie działania programu. Służą do tego celu funkcje malloc() calloc(), i free() (język C/C++) oraz operatory new i delete (tylko C++).

Funkcja malloc() Postać syntaktyczna funkcji: void *malloc(size_t size); Funkcje malloc() i calloc przydzielają pamięć z obszaru sterty, zwanej kopcem (heap), w obszarze od 0 do 1 MB i zwracają wskazanie do przydzielonego obszaru. Rozmiary przydzielanej pamięci nie przekraczają 64 KB (size_t jest typu unsigned) . Jeżeli alokacja nie jest możliwa, to zwracany jest wskaźnik NULL. Funkcja calloc() dodatkowo zeruje przydzieloną pamięć. Funkcje malloc() i calloc() zwracają wskaźniki do typu void dlatego niezbędne są konwersje typu przy podstawieniach do wskaźników innych typów. Przykład. char *str; if ((str = (char *) malloc(10)) == NULL) { printf("Brak wolnej pamieci do alokacji bufora \n"); exit(1); } strcpy(str,"Hello"); printf("ciag= %s\n", str); free(str);

Przydzielanie obszarów większych niż 64kB Do przydzielania, w obszarze od 0 do 0xFFFFF (1 MB), bloków pamięci o rozmiarze większym niż 64 KB oraz alokowania pamięci w obszarze tzw. sterty dalekiej, adresowanej przez wskaźniki typu far (np. void far *p), służą w systemie BC++ odpowiedniki przedstawionych funkcji, tj. void far *farmalloc(long K) oraz void far *farcalloc(long N, long K). Do zwolnienia bloku służy funkcja farfree(). #include <alloc.h> … void *p = NULL; void *q = NULL; long a, b; a = 65000; b = 400000; q = farmalloc(b); // alokacja bloku 400000 bajtow printf("%p\n",q); // np. 009029E8 p = malloc(a); // alokacja bloku 65000 bajtow printf("%p\n",p); // np.0096446C free(p); p = NULL; farfree(q); q = NULL;

Uwagi o dynamicznym przydzielaniu pamięci W trybie adresowania rzeczywistego sterta może zajmować pamięć o adresach od 0 do 1 MB. Natomiast w trybie adresowania wirtualnego może być wykorzystywana cała dostępna pamięć w tym obszar powyżej 1 MB. Rozmiar przydzielanej pamięci jest proporcjonalny do rozmiaru tworzonego obiektu (zawsze nie mniejszy niż rozmiar tworzonej zmiennej). Utworzona zmienna nie posiada nazwy, ale jej adres jest podstawiany do wskaźnika odpowie­dniego typu. Można więc uzyskać do niej dostęp za pomocą operatora wyłuskania

Własności zmiennych utworzonych za pomocą funkcji malloc() istnieją od momentu ich utworzenia za pomocą funkcji malloc() do momentu likwidacji za pomocą funkcji free(); nie posiadają nazwy; dostęp do nich jest możliwy wyłącznie za pomocą wskaźników; nie podlegają regułom obowiązującym dla zwykłych zmiennych, a dotyczącym zasięgu, łączności i czasu trwania; nie są automatycznie inicjowane wartościami początkowymi w momencie utworzenia; przydzielony blok pamięci zawiera wartość przypadkową; po zwolnieniu pamięci wskaźnik zmiennej nadal zawiera ten sam adres, ale zwolniona pamięć może być udostępniona dla innych zmiennych (należy uważać, aby nie wstawiać danych do pamięci wskazywanej przez wskaźnik, który wskazuje na zwolniony obszar pamięci).

Funkcja free() Do zwalniania pamięci uprzednio przydzielonej przy pomocy funkcji calloc(), malloc() lub realloc() służy w C funkcja free() o postaci: void free(void *block); Funkcja free() nie zwraca żadnej wartości. Uwaga: Programista zawsze musi mieć świadomość, że nie można odwoływać się za pomocą wskaźników do komórek pamięci wewnątrz zwolnionego bloku; próba dostępu do takiej komórki spowoduje błąd!

Tworzenie zmiennych dynamicznych i zapamiętywanie ich adresów za pomocą wskaźników (1/3) int *wsk_do_int_1(void) //funkcja zwraca wskaźnik do int { return (int *)malloc(sizeof(int)); } … int *w=NULL; w=wsk_do_int_1(); if (w){ *w=1; printf("w=%d\n",*w); //w=1 free(w);

Tworzenie zmiennych dynamicznych i zapamiętywanie ich adresów za pomocą wskaźników (2/3) void wsk_do_int_2(int* *x){ //funkcja ustawia wskaznik do int *x=(int *)malloc(sizeof(int)); **x=1; } … int *w=NULL; wsk_do_int_2(&w); if (w) { //*w=1; - równoważny sposób printf("w=%d\n",*w); //w=1 free(w);

Tworzenie zmiennych dynamicznych i zapamiętywanie ich adresów za pomocą wskaźników (3/3) void wsk_do_int_3(int* &x){ //funkcja ustawia referencje do int x=(int *)malloc(sizeof(int)); *x=1; } … int *w=NULL; wsk_do_int_3(w); if (w){ //*w=1; - równoważny sposób printf("w=%d\n",*w); free(w);

Uwagi Nie należy stosować funkcji free() dla obiektów, które nie zostały utworzone za pomocą funkcji malloc() . Nie wolno również kasować dwa razy tego samego obiektu. Aby się ustrzec przed taką możliwością wystarczy po skasowaniu obiektu ustawić jego wskaźnik na NULL. Zwalnianie wskaźnika pustego nie powoduje błędu. Należy również uważać, aby nie utracić zawartości wskaźnika, który zawiera adres zmiennej utworzonej za pomocą funkcji malloc() . W takim przypadku nie będzie można zwolnić przydzielonej pamięci, co może doprowadzić do wyczerpania sterty. Zmienna dynamiczna może być utworzona wewnątrz funkcji. Przydzielony obszar pamięci nie znika po zakończeniu funkcji. Należy jednak zadbać o to, aby funkcja przekazała wskaźnik do utworzonego obszaru na zewnątrz.

Operatory new i delete (C++)

Operator new Operator new ma postać: new nazwa_typu <(inicjator)> Przykład int *wsk; wsk=new int(32); //*wsk=32

Operator delete Postać syntaktyczne operatora delete: delete nazwa_obiektu; Operator delete usuwa obiekt z pamięci zanim zostanie zlikwidowany wskaźnik do tego obiektu. Przykład typedef int tint10[10]; … tint10 *t; t=(tint10 *)new tint10; ... delete t;

Metody testowania dostępności pamięci sprawdzanie poprawności wykonania operacji przydziału pamięci wykorzystanie set_new_handler wykorzystanie instrukcji try/catch(xalloc)

Sprawdzanie poprawności wykonania operacji przydziału pamięci int *wsk; wsk= new int[1234]; if (!wsk) printf(”brak pamieci”);

Przykład Dynamiczne tworzenie tablicy dwuwymiarowej. int m = 3; //liczba wierszy. int n = 5; //liczba kolumn int i,j; int **data; try { // sprawdzanie mozliwosci utworzenia tablicy data = new long double*[m]; // krok 1: Tworzenie wierszy. for (j = 0; j < m; j++) data[j] = new long double[n]; // Krok 2: Tworzenie kolumn } catch (xalloc) {cout << "NIe mozna utworzyc tablicy"; exit(-1); for (i = 0; i < m; i++) for (j = 0; j < n; j++) { data[i][j] = i + j; //inicjoanie i drukowanie tablicy cout << data[i][j] << " "; cout << "\n" << endl; for (i = 0; i < m; i++) delete[] data[i]; // Krok 1: Usuwanie kolumn delete[] data;}} // Krok 2: Usuwanie wierszy

Wskaźniki stałe Wartości wskaźnika stałego nie wolno zmieniać po początkowym ustalenie jego wartości, ale wartość wskazywanej komórki pamięci może się zmieniać. int i=2; int k=2; int *const wsk = &i; //wsk jest wskaznikiem stalym wsk=&k; //bład! i=2; //poprawnie

Wskaźniki do stałej Wskaźnik do stałego wskazuje na miejsce pamięci, którego wartości nie można zmieniać, ale wartość samego wskaźnika może być zmieniana. int x[3]={1,2,3}; const int *wsk //wsk jest wskaznikiem do stalej typu int wsk=x; wsk++; //poruszanie wskaznkiem jest dozwolone *wsk=0; //blad!

Wskaźniki do funkcji Ogólna postać deklarowania wskaźnika do funkcji: typ (*nazwa_funkcji) (parametry_funkcji) Przykład 1 int (*fun) (void) Funkcja fun jest wskaźnikiem do funkcji wywoływanej bez żadnych parametrów, a zwracającej wartość typu int.

Przykład int pierwsza(void); //deklaracje dwoch funkcji, które maja być wywolywane int druga(void); int main(void) { int i; int (*fun)(void); printf("i="); scanf("%d",&i); switch(i){ //przypisanie wartosci funkcji fun() case 1: fun=pierwsza; break; case 2: fun=druga; break; default : fun=NULL; break; } if (fun) (*fun)(); //wywolanie funkcji o okreslonym adresie getch(); return(0); int pierwsza(void){ printf("pierwsza\n");} int druga(void){ printf("druga\n");}

Funkcje operujące na adresach pamięci Odczytanie wartości segmentu i offsetu wskaźnika umożliwiają funkcje: unsigned FP_SEG(void far *wsk); // segment wskaźnika unsigned FP_OFF(void far *wsk); // offset wskaźnika Utworzenie wskaźnika o określonym segmencie i offsecie umożliwia funkcja: void far *MK_FP(unsigned segment, unsigned offset). Przykład void far *d = MK_FP(0xB800, 0x0000); unsigned s = FP_SEG(d); // s = 0xB800; unsigned o = FP_OFF(d); // o = 0x0000;

Wskaźniki typu void (1/2) Wskaźnik do zmiennej określonego typu zawiera informację o adresie zmiennej oraz jej typie (co ma znaczenie podczas wykonywania operacji arytmetycznych z udziałem wskaźnika oraz interpretacji wskazywanego obszaru pamięci). Z definicji int *wi; // wskaźnik do obiektu typu int wynika, że wi wskazuje na obszar pamięci o rozmiarze sizeof(int), w którym można zapamiętać zmienną typu int. Można bezpośrednio odwołać się do pamięci o adresie wi za pomocą *wi. Istnieje możliwość definiowania wskaźników, które wskazują na typ void. void *x; // wskaźnik na dowolny typ Wskaźniki typu void mogą zawierać adres zmiennej dowolnego typu.

Wskaźniki typu void (2/2) W przypadku definicji int i = 5; float z = 1.3; int *wi = &i; float *wz = &z; void *x; możliwe są przypisania x = wi oraz x = wz. Wskaźnikowi typu void można przypisać wskaźnik dowolnego typu z wyjątkiem wskaźnika do zmiennej typu const. const long s = 4; // stała s typu long void *x = &s; //błąd, bo s jest stałą Można jednak wpisać adres obiektu stałego do wskaźnika typu void do stałej: const void *x = &s;

Rzutowanie wskaźników (1/2) W celu przypisania wskaźnika typu void innemu wskaźnikowi należy dokonać konwersji typu wskaźnika (rzutowania wskaźnika) do typu wymaganego. Operacja rzutowania wskaźników jest również niezbędna w przypadku, gdy przypisujemy wskaźnik jednego typu do wskaźnika innego typu. double zm = 11.2; void *x = &zm; double *d; int *wi; // d = x; // błąd – brak zgodności typów d = (double *) x; // rzutowanie wskaźnika void * do double * printf("%d\n",*d); //11.2 wi = (int *) d; // rzutowanie double * do int *

Rzutowanie wskaźników (2/2) Wskaźniki typu void umożliwiają przekazywanie do funkcji wskaźników dowolnego (niestałego) typu. Wewnątrz funkcji można dokonać konwersji typu wskaźnika (rzutowania wskaźnika) do innego typu w celu interpretacji wskazywanego obszaru pamięci. void CzytajPamiec(void *x) { char *c = (char *) x; int *i= (int *) x; long *z = (long *) x; }

Obsługa ekranu w trybie tekstowym

Podstawowe zadania obsługi ekranu Dostępne funkcje obsługi ekranu umożliwiają: zmianę trybu tekstowego ekranu, sterowanie atrybutami znaków wysyłanych na ekran, realizację operacji wejścia-wyjścia (klawiatura-ekran), obsługę pamięci ekranu (operacje na blokach pamięci), sterowanie położeniem i kształtem kursora. Ekran w trybie tekstowym jest podzielony na jednakowe komórki, w każdej może znaleźć się jeden znak, posiadający pewien atrybut (kolor i tło). Liczba komórek zależy od aktualnego trybu tekstowego. Na ekranie tekstowym zawsze jest określone pewne „okno” tekstowe, do którego wysyłane są informacje podczas wykonywania operacji wyjścia na ekran. Standardowo oknem jest cały obszar ekranu, ale można to zmienić.

Funkcje organizacji i sterowania atrybutami znaków wyświetlanych na ekranu Organizacja ekranu: window() – określanie obszaru ekranu zajmowanego przez okno tekstowe; textmode() – przełączanie między różnymi trybami pracy ekranu; gettextinfo() – informacja o stanie ekranu w trybie tekstowym. Sterowanie atrybutami znaków wysyłanych na ekran: textattr() – ustawianie atrybutu tekstu; textbackground() – ustawianie koloru tła; textcolor() – ustawianie koloru znaków #include <conio.h> int main(void) { window(10,10,40,11); textcolor(BLACK); textbackground(WHITE); cprintf("This is a test\r\n"); return 0; }

Sterowanie pozycją kursora gotoxy() – ustawianie kursora na zadanej pozycji; wherex(), wherey() – pobieranie współrzędnych kursora; Przykład #include <conio.h> int main(void) { clrscr(); gotoxy(35, 12); cprintf("Hello world"); cprintf(„Aktualne wspolrzedne X: %d Y: %d\r\n", wherex(), wherey()); getch(); return 0; }

Czyszczenie ekranu clreol() – usuwa znaki od pozycji kursora do końca linii; clrscr() – kasowanie ekranu; delline() – usuwa wiersz na pozycji kursora i przesuwa pozostałe; insline() – wstawia wiersz na pozycji kursora i przesuwa pozostałe Przykład clrscr(); cprintf("Nacisnij klawisz . . ."); //* Przesun kursor do 1. kolumny w 2. linii: gotoxy(1,2); getch(); delline();

Operacje we/wy (konsola-ekran) kbhit() – funkcja sprawdzająca czy naciśnięcie klawisza jest dostępne ungetch() – wprowadzanie znaku na początek bufora klawiatury, powodując, że będzie on następnym odczytanym znakiem cprintf() – działa jak printf(), ale znaki wysyłane są do aktualnego okna tekstowego i z odpowiednim atrybutem Przykład cprintf(„Nacisnij klawisz:"); while (!kbhit());// nic nie rob cprintf("\nNacisnieto klawisz...\n");

Operacje na blokach pamięci obrazu (1/2) gettext() – zapamiętanie prostokątnego fragmentu obrazu w buforze pamięci movetext() – kopiowanie prostokątnego fragmentu obrazu na zadaną pozycję; puttext() – kopiuje zawartość bufora pamięci do ustalonego prostokątnego fragmentu obrazu.

Operacje na blokach pamięci obrazu (2/2) #include <conio.h> char buffer[4096]; int main(void) { int i; clrscr(); for (i = 0; i <= 20; i++) cprintf("Line #%d\r\n", i); gettext(1, 1, 80, 25, buffer); gotoxy(1, 25); cprintf("Nacisnij klawisz, aby wyczyscic ekran..."); getch(); cprintf("Nacisnij klawisz, aby przywrocic ekran..."); puttext(1, 1, 80, 25, buffer); cprintf("Nacisnij klawisz, aby wyjsc..."); return 0; }

Preprocesor

Preprocesor Preprocesor umożliwia makrogenerację, kompilację warunkową oraz włączanie do programów zawartości wskazanych plików. Wiersze programu rozpoczynające się znakiem (#) służą do komunikacji z preprocesorem. Granice wierszy są istotne – każdy wiersz jest rozpatrywany indywidualnie.

Definicje i rozwinięcia makr Wiersz sterujący o postaci # define identyfikator ciąg-leksemów zleca preprocesorowi zastępowanie dalszych wystąpień identyfikatora wskazanym ciągiem leksemów; opuszcza się odstępy otaczające ciąg leksemów. # undef identyfikator zleca preprocesorowi, aby „zapomniał” definicję identyfikatora. Przykład #define ABSDIFF(a,b) ((a)>(b)?(a)-(b):(b)-(a)) ... x=ABSDIFF(r,y); #undef ABSDIFF

Więcej o wywoływaniach makrodefinicji Wywołanie makrodefinicji jest podobne do wywołania funkcji. Różnice występują w tych przypadkach, w których wyrażenie będące parametrem wywołania jest obliczane w kilku etapach. Np. #define KWADRAT(a)((a)(a)) … int i=5; long w; w = KWADRAT(i++); // w =25, ale i=7! Wniosek: w odwołaniach do makrodefinicji należy unikać przekazywania parametrów, których wartość jest obliczana wieloetapowo.

Włączanie plików Wiersz sterujący o postaci # include <nazwa-pliku> zleca wstawienie w jego miejsce całej zawartości pliku o podanej nazwie. Inne dopuszczalne postacie tego polecenia: #include <conio.h> - szukaj w folderze \include kompilatora; #include <d:\conio.h> - szukaj we wskazanym miejscu; #include “conio.h” – szukaj wg ścieżek ustawionych w parametrze PATH systemu operacyjnego; #include <cconio> - nowa notacja C++; #include <plik.hpp> - forma akceptowana tylko przez kompilator C++

Kompilacja warunkowa (1/2) Fragmenty programu mogą być kompilowane warunkowo, zgodnie z poniższą schematyczną składnią: #if wyrażenie_stałe_1 <sekcja_1> <#elif wyrażenie_stałe_2 newline sekcja_2> . <#elif wyrażenie_stałe_n newline sekcja_n> <#else <newline> skecja_końcowa> <#error komunikat> #endif

Kompilacja warunkowa (2/2) Przykład #define AUTO AUDI #if (AUTO==FIAT) printf (”to jest FIAT”); #if (AUTO==SEAT) printf (”to jest SEAT”); #if (AUTO==AUDI) printf (”to jest AUDI”); #else printf (”Nie ma takiej marki w bazie”); #error ”zle podana marka samochodu!” #endif #undef AUTO

Kompilacja modułów

Kompilacja modułów Dobrze zaprojektowany zbiór nagłówkowy powinien być zabezpieczony przed wielokrotnym włączeniem go do tekstu programu. // plik.h - zbiór nagłówkowy #ifndef PLIK_H #define PLIK_H < deklaracje pliku plik.h > #endif … #undef Jeśli makrodefinicja PLIK_H nie została jeszcze zdefiniowana wówczas kompilator musi przeanalizować wszystkie dyrektywy i instrukcje znajdujące się między #ifndef a #endif. W wyniku przetworzenia wspomnianego obszaru zbadane zostaną wszystkie deklaracje znajdujące się w zbiorze nagłówkowym plik.h oraz zdefiniowane zostanie makro PLIK_H. Przy ponownym włączeniu zbioru plik.h jego zawartość nie będzie analizowana.

Przykład (1/7) Przykład programu składającego się z pięciu jednostek kompilacji zapisanych w plikach: prot.h - zawiera deklaracje typów oraz prototypy funkcji: pisz_wynik(), suma1(), suma2() i suma3() pisz.cpp - zawiera definicję funkcji pisz_wynik() mod1.cpp - zawiera definicję funkcji suma1() mod2.cpp - zawiera definicję funkcji suma2() mod3.cpp - zawiera definicję funkcji suma3()

Przykład (2/7) #ifndef PROT.H #define PROT.H void pisz_wynik(double); /* plik nagłówkowy prot.h zawierający deklaracje typów oraz prototypy funkcji */ #ifndef PROT.H #define PROT.H void pisz_wynik(double); double suma1(double, double); void suma2(double, double, double *); void suma3(double, double, double &); #endif

Przykład (3/7) #ifndef PROT.H #include "PROT.H" #endif /* plik pisz.cpp zawierający definicję funkcji pisz_wynik */ #ifndef PROT.H #include "PROT.H" #endif #include <stdio.h> void pisz_wynik(double w) { printf("Wynik= %10.2lf\n",w); }

Przykład (4/7) #ifndef PROT.H #include "PROT.H" #endif /* plik mod1.cpp zawierający definicję funkcji suma1 */ #ifndef PROT.H #include "PROT.H" #endif #include <stdio.h> double suma1(double x, double y) { pisz_wynik(x+y); return x+y; }

Przykład (5/7) #ifndef PROT.H #include "PROT.H" #endif /* plik mod2.cpp zawierający definicję funkcji suma2 */ #ifndef PROT.H #include "PROT.H" #endif #include <stdio.h> void suma2(double x, double y, double *w) { pisz_wynik(x+y); *w = x+y; }

Przykład (6/7) #ifndef PROT.H #include "PROT.H" #endif /* plik mod3.cpp zawierający definicję funkcji suma3 */ #ifndef PROT.H #include "PROT.H" #endif #include <stdio.h> void suma3(double x, double y, double &w) { pisz_wynik(x+y); w = x+y; }

Przykład (7/7) /* program główny glowny.cpp */ #ifndef PROT.H #include "PROT.H” #endif #include "mod1.cpp" // dołącz kod modułu 1 #include "mod2.cpp" // dołącz kod modułu 2 #include "mod3.cpp" // dołącz kod modułu 3 #include "pisz.cpp" // dołącz kod modułu 4 #include <stdio.h> #include <conio.h> #include <iostream.h> void main(void) { ... }

Dyrektywa #error Dyrektywa ta ma postać: #error komunikat Dyrektywa ta powoduje, że po jej napotkaniu kompilacja jest przerywana i wypisywany jest komunikat. Przykład #if (WERSJA==1) … #elif (WERSJA==2) #else #error ”dopuszczale sa wersje 1 lub 2” #endif

Pusta instrukcja preprocesora Wiersz zawierający jedynie znak # nie wywołuje żadnego skutku.

Biblioteka standardowa

Biblioteka standardowa Biblioteka standardowa nie jest częścią samego języka, natomiast pochodzą z niej deklaracje funkcji oraz definicje typów i makr oferowanych przez standardowe środowisko C. Funkcje, typy i makra należące do biblioteki standardowej są zdefiniowane w standardowych plikach nagłówkowych

Pliki nagłówkowe Nagłówki mogą być dołączane w dowolnej kolejności i dowolną liczbę razy. Nagłówek musi być dołączony na zewnątrz jakiejkolwiek deklaracji czy definicji i przed użyciem czegokolwiek, co jest w nim zdefiniowane. Nagłówek nie musi być plikiem źródłowym.

Wejście i wyjście: <stdio.h>

System plików

System plików (file system) Pliki dyskowe są zorganizowane w sposób hierarchiczny. Na danym poziomie hierarchii, nazywanej folderem, katalogiem lub kartoteką plików, mogą istnieć indywidualne pliki lub podkatalogi. Każdy podkatalog jest normalnym katalogiem i może zawierać pliki lub podkatalogi. Pliki można tworzyć, kopiować, kasować lub zmieniać ich zawartość (aktualizować). Każdy system plików ma własny interfejs, za pomocą którego zleca się wykonywanie działań na plikach i katalogach. Do najpopularniejszych standardów systemu plików należą: system plików systemu UNIX; systemy plików wywodzące się z operacyjnego systemu MD-DOS: FAT (File Allocation Table (IBM/Microsoft)), FAT32, NTFS (NT File System)

Wejście i wyjście: <stdio.h> Strumień jest źródłem lub celem danych i może być skojarzony z dyskiem lub innym urządzeniem zewnętrznym. Biblioteka <stdio.h> obsługuje strumienie tekstowe i binarne. Strumień tekstowy jest ciągiem wierszy; każdy wiersz zawiera zero lub większa liczbę znaków i kończy się znakiem ‘\0’. Strumień binarny jest ciągiem bitów (mapa bitowa). Strumień wiąże się z plikiem lub urządzeniem za pomocą otwarcia; to połączenie przerywa się zamykając strumień.

Otwarcie pliku Otwarcie pliku udostępnia wskaźnik do zmiennej typu FILE, w którym gromadzi się wszelkie informacje niezbędne do obsługi strumienia. W momencie rozpoczęcia działania programu otwierane są trzy strumienie: - stdin – standardowe wejście; - stdout – standardowe wyjście; - stderr – standardowe wyjście błędów.

Funkcje operujące na plikach Zdefiniujmy zmienną stream: FILE *stream; Następujące funkcje operują na plikach: - FILE *fopen() – otwiera plik; - int fflush() – wypisuje dane z bufora; - int fclose() - wypisuje dane z bufora i zamyka plik; - int remove() – usuwa wskazany plik; - int rename() – zmienia nazwę pliku.

Funkcja fopen() (1/2) FILE *fopen(const char *filename, const char *mode) – otwiera plik o nazwie filename i zwraca strumień lub NULL, jeśli otwarcie się nie powiedzie. Parametr mode określa rodzaj dostępu do pliku: ”r” – otwórz plik tekstowy do czytania; ”w” - otwórz plik tekstowy do pisania; skasuj poprzednia zawartość, jeśli istnieje; ”a” –dopisuj; otwórz istniejący lub utwórz nowy plik do dopisywania na jego końcu; ”r+” - – otwórz plik tekstowy do aktualizacji (czytania i pisania); ”w+” – utwórz plik tekstowy do aktualizacji (do odczuta i zapisu); skasuj poprzednia zawartość, jeśli istnieje; ”a+” – otwórz istniejący lub utwórz nowy plik, dopisując dane na końcu pliku. Przykład FILE *we; if ((we = fopen(”c:\\AUTOEXEC.BAT", "rt")) == NULL) { fprintf(stderr, ”Nie można otworzyc pliku.\n"); return 1; }

Funkcja fopen() (2/2) - uwagi Ponieważ aktualizacja oznacza możliwość jednoczesnego czytania i pisania z tego samego pliku, między wykonywaniem tych czynności należy wywołać funkcję fflush() lub funkcje pozycjonujące plik. Jeśli w argumencie mode po początkowej literze występuje litera b, np. ”rb”, ”r+b”, oznacza to plik binarny. Długość nazwy plików ograniczona jest do FILENAME_MAX znaków (standardowo 80 znaków w <stdio.h>). Jednocześnie można otworzyć co najwyżej FOPEN_MAX plików znaków (standardowo 48 plików w <stdio>).

Funkcja fflush() int fflush(FILE *stream) – dla strumieni wyjściowych funkcja powoduje zapisania do pliku wszystkich danych znajdujących się w buforze, które jeszcze nie zostały w nim zapisane. Dla strumieni wejściowych działanie funkcji jest nieokreślone. Funkcja zwraca EOF w przypadku błędu pisania, a zero w pozostałych przypadkach. Wywołanie fflush(NULL) wypisuje dane ze wszystkich strumieni wyjściowych. Przykład FILE *stream; char msg[] = "to jest test"; stream = fopen("test.dan", "w"); fwrite(msg, strlen(msg), 1, stream); fflush(stream); //wypisywanie z bufora bez zamykania pliku ... fclose(stream);

Funkcja fclose() int fclose(FILE *stream) – zapisuje do pliku wszystkie nie wypisane dane związane ze strumieniem stream; kasuje wszelkie nie przeczytane dane z buforów wejściowych, zwalnia przydzielone bufory i zamyka plik. Funkcja zwraca EOF w przypadku błędu., a zero w pozostałych przypadkach. Przykład int main(void){ FILE *fp; char buf[11] = "0123456789"; fp = fopen(”test.txt", "w"); fwrite(buf, strlen(buf), 1, fp); fclose(fp); return 0; }

Funkcja remove() int remove(const char *filename) – usuwa wskazany plik tak, że późniejsza próba otworzenia go zakończy się niepowodzeniem. Funkcja zwraca wartość różną od zera w przypadku , gdy usunięcie pliku się nie powiedzie. Przykład int main(void) { char file[80]; printf("Nazwa pliku do usuniecia: "); gets(file); if (remove(file) == 0) printf("Usunieto %s.\n",file); else perror(”Blad!"); //drukuje do stderr komunikat return 0; }

Funkcja rename() int rename( const char *oldname, const char *newname) – zmienia nazwę pliku z oldname na newname. Funkcja zwraca wartość różną od zera w przypadku , gdy operacja się nie powiedzie. Przykład int main(void) { char oldname[80], newname[80]; printf("Stara nazwa pliku: "); gets(oldname); printf("Nowa nazwa pliku: "); gets(newname); if (rename(oldname, newname) == 0) printf("Zmiana nazwy pliku %s na %s.\n", oldname, newname); else perror(„Blad!"); return 0;}

Funkcje zapisujące do strumienia

Formatowane wyjście fprintf() int fprintf(FILE *stream, const char *format[, argument, ...]); - funkcja ta, pod kontrolą parametru format, przekształca i wypisuje pozostałe argumenty do strumienia wyjściowego stream. Wartość zwracana przez funkcję jest równa liczbie wypisanych znaków lub jest liczbą ujemną w przypadku błędu. Przykład int main(void) { FILE *stream; int i = 100; char c = 'C'; float f = 1.234; stream = fopen("dane.txt", "w+"); fprintf(stream, "%d %c %f", i, c, f); fclose(stream); return 0; }

Funkcja fwrite() size_t fwrite( const void *ptr, size_t size, size_t n, FILE *stream); - wypisuje do strumienia stream, n jednostek danych każdy o długości size bajtów, rozpoczynając od adresu ptr. Funkcja zwraca liczbę jednostek (a nie bajtów) danych wypisanych do strumienia lub wartość ujemną w przypadku błędu. Przykład struct mystruct { int i; char ch;} s; int main(void) { FILE *stream; if ((stream = fopen("TEST.$$$", "wb")) == NULL) { fprintf(stderr, "Nie mozna utworzyc pliku.\n"); return 1; } s.i = 0; s.ch = 'A'; fwrite(&s, sizeof(s), 1, stream); fclose(stream); return 0; }

Funkcje odczytujące ze strumienia

Formatowane wejście int fscanf( FILE *stream, const char *format[, argument, ...]); - funkcja ta, pod kontrolą parametru format, przypisuje przekształcone wartości kolejnym argumentom. Każdy z tych argumentów musi być wskaźnikiem. Funkcja kończy swe działanie, gdy zinterpretuje cały format. Funkcja zwraca EOF, jeśli napotka koniec pliku lub wystąpi jakiś błąd.; w przeciwnym przypadku zwraca liczbę przekształconych i przypisanych danych wejściowych. Przykład if (fscanf(stdin,"%d",&i)) printf("i=%d\n", i); else printf("Blad odczytu.\n");

Funkcja fread() (1/2) size_t fread( void *ptr, size_t size, size_t n, FILE *stream); - funkcja czyta z podanego strumienia n jednostek danych, każdy o długości size bajtów i kieruje je do obszaru pamięci wskazanego przez ptr. Rozmiar czytanych danych wynosi zatem (n*size). Funkcja zwraca liczbę odczytanych jednostek danych (a nie bajtów), natomiast w przypadku napotkania końca pliku lub wystąpienia błędu zwraca wartość zero.

Funkcja fread() int main(void) { FILE *stream; char msg[] = "to jest test"; char buf[20]; if ((stream = fopen("test.txt", "w+"))== NULL) { fprintf(stderr, "Nie mozna utworzyc pliku.\n"); return 1; } fwrite(msg, strlen(msg)+1, 1, stream); fseek(stream, 0,SEEK_SET); //początek pliku fread(buf, strlen(msg)+1, 1, stream); printf("%s\n", buf); fclose(stream); return 0; }

Funkcje realizujące wejście znakowe Podstawowe funkcje wykonujące wejście znakowe: int fgetc() char *fgets()

Funkcja fgetc() int fgetc(FILE *stream) - czyta ze strumienia stream następny znak i zwraca jego wartość. W przypadku napotkania końca pliku lub wystąpienia błędu zwraca EOF. Przykład int main(void) { FILE *stream; char string[] = "To jest test"; char ch; stream = fopen("test.dan", "w+"); fwrite(string, strlen(string)+1, 1, stream); fseek(stream, 0, SEEK_SET); while (ch = fgetc(stream)) putch(ch); //wyprowadza znak na ekran fclose(stream); return(0); }

Funkcja fgets() char *fgets( char *s, int n, FILE *stream); - czyta co najwyżej (n-1) znaków i wstawia do tablicy s. Funkcja przerywa czytanie po napotkaniu znaku nowego wiersza; znak ten także wstawia do tablicy. Cały tekst jest zakończony znakiem ‘\0'. Funkcja zwraca s lub NULL w przypadku napotkania końca pliku albo błędu. Przykład int main(void) { FILE *stream; char string[] = "To jest test"; char msg[20]; stream = fopen(”dane.txt", "w+"); fwrite(string, strlen(string)+1, 1, stream); fseek(stream, 0, SEEK_SET); fgets(msg, strlen(string)+1, stream); printf("%s", msg); fclose(stream); return 0; }

Funkcje realizujące wyjście znakowe Podstawowe funkcje wykonujące wyjście znakowe: - int fputc() - int fputs()

Funkcja fputc() int fputc( int c, FILE *stream); - wpisuje znak c (przekształcony do unsigned char) do strumienia stream. Zwraca wypisany znak lub EOF w przypadku błędu. Przykład #include <stdio.h> int main(void) { char msg[] = "Hello world"; int i = 0; while (msg[i]) { fputc(msg[i], stdout); i++;} return 0;}

Funkcja fputs() int fputs( const char *s, FILE *stream); - wypisuje tekst zawarty w tablicy s (nie musi zawierać '\n') do strumienia stream. Zwraca liczbę nieujemną lub EOF w przypadku błędu. Przykład #include <stdio.h> int main(void) { fputs("Hello world\n", stdout); return 0;}

Funkcje wyznaczające pozycję w pliku int fseek() long ftell () void rewind () int fgetpos() int fsetpos ()

Funkcja fseek() (1/2) int fseek( FILE *stream, long offset, int origin); - wyznacza pozycję w strumieniu stream; następne czytanie lub pisanie będzie odnosić się do danych rozpoczynających się od nowej pozycji. Dla plików binarnych nowa pozycja wypada w miejscu oddalonym o offset znaków od punktu odniesienia origin, który może mieć wartości: SEEK_SET (początek pliku), SEEK_CUR (bieżąca pozycja) lub SEEK_END (koniec pliku). Dla plików tekstowych wartość offset musi być równa zeru lub wartości zwróconej przez funkcję ftell(). Funkcja fseek() zwraca wartość różną od zera w przypadku błędu.

#include <stdio.h> long RozmiarPliku(FILE *stream); int main(void) { FILE *stream; stream = fopen("mojplik.txt", "w+"); fprintf(stream, "To jest test"); printf("Wielkosc pliku mojplik.txt wynosi %ld bajtow\n", RozmiarPliku(stream)); fclose(stream); return 0;} long RozmiarPliku(FILE *stream) { long AktPozycja, Dlugosc; AktPozycja = ftell(stream); fseek(stream, 0L, SEEK_END); Dlugosc = ftell(stream); fseek(stream, AktPozycja, SEEK_SET); return Dlugosc;}

Funkcja ftell() long ftell(FILE *stream) - zwraca wartość bieżącej pozycji dla strumienia stream lub -1L w przypadku błędu. Przykład #include <stdio.h> int main(void) { FILE *stream; stream = fopen("mojplik.txt", "w+"); fprintf(stream,"%s", "Test"); printf("Wskaznik pliku jest na pozycji %ld\n",ftell(stream)); //4 fclose(stream); return 0;}

Funkcja rewind() void rewind (FILE *stream); - przesuwa znacznik pliku na początek. Przykład #include <stdio.h> #include <dir.h> int main(void) { FILE *fp; char *fname = "TXXXXXX", *newname, first; //tworzy unikalną nazwę pliku wg podanego wzorca: newname = _mktemp(fname); fp = fopen(newname,"w+"); fprintf(fp, „%s","abcdefghijklmnopqrstuvwxyz"); rewind(fp); fscanf(fp,"%c",&first); printf("Pierwszy znak: %c\n",first); fclose(fp); remove(newname); //usuwa plik o podanej nazwie return 0;}

Funkcja fgetpos() int fgetpos(FILE *stream, fpos_t *ptr); - zapamiętuje bieżącą pozycję strumienia stream w miejscu wskazanym przez *ptr. Z tej wartości można później skorzystać w funkcji fsetpos(). Typ fpos_t jest odpowiednim typem zmiennej do przechowania takiej wartości. W przypadku błędu funkcja fgetpos() zwraca wartość różną od zera. Przykład FILE *stream; //typ przechowujący pozycję pliku <stdio.h> fpos_t pozycja; ... fgetpos(stream, &pozycja); printf("Pozycja pliku: %ld\n", pozycja);

Funkcja fsetpos() int fsetpos(FILE *stream, const fpos_t *ptr); - ustawia bieżącą pozycję strumienia stream według wartości zapamiętanej przez funkcję fgetpos() w miejscu wskazanym przez *ptr. W przypadku błędu funkcja fsetpos() zwraca wartość różną od zera.

#include <stdlib.h> #include <stdio.h> void showpos(FILE *stream); int main(void) { FILE *stream; fpos_t filepos; stream = fopen("dane.txt", "w+"); fgetpos(stream, &filepos); fprintf(stream, "To jest test"); showpos(stream); if (fsetpos(stream, &filepos) == 0) showpos(stream); else{fprintf(stderr,"Bład we wskazaniu pozycji w pliku.\n"); exit(1); } fclose(stream); return 0;} void showpos(FILE *stream) { fpos_t pos; fgetpos(stream, &pos); printf("Pozycja pliku: %ld\n", pos); }

Obsługa błędów

Wprowadzenie Wiele funkcji bibliotecznych ustawia znaczniki stanu dla strumieni po napotkaniu końca pliku lub gdy wystąpi błąd. Te znaczniki mogą być ustawiane i sprawdzane jawnie. Dodatkowo, wartością wyrażenia całkowitego errno (zadeklarowanego w <errno.h>) może być numer błędu, który bliżej określa ostatnio napotkany błąd. Do obsługi błędów wykorzystać można funkcje: - int ferror() - void clearerr() - void perror() - int feof()

Funkcja ferror() int ferror(FILE *stream) - zwraca wartość różną od zera w przypadku, gdy jest ustawiony znacznik błędu dla strumienia stream. void clearerr(FILE *stream) - kasuje znaczniki końca pliku i błędu dla strumienia stream. Przykład int main(void) { FILE *stream; stream = fopen("tes.txt", "w"); getc(stream); //wymuszenie bledu poprzez probe odczytu if (ferror(stream)) { printf("Blad\n"); clearerr(stream); } // kasowanie bledu strumienia fclose(stream); return 0;}

Funkcja perror() void perror(const char *s) -funkcja wypisuje tekst z tablicy s i (zależny od implementacji) komunikat o błędzie odpowiadający wartości errno. Działanie funkcji można przedstawić jako: fprintf(stderr, "%s: %s\n", s, "komunikat o bledzie"); Przykład int main(void) { FILE *fp; fp = fopen(”dane.dan", "r"); if (!fp) perror(”dane.dan"); //dane.dan: No such file or directory return 0;}

Funkcja feof() int feof(FILE *stream) - zwraca wartość różną od zera w przypadku, gdy jest ustawiony znacznik końca pliku dla strumienia stream. Przykład #include <stdio.h> int main(void) { FILE *stream; stream = fopen("dane.txt", "r"); fgetc(stream); if (feof(stream)) printf("Koniec pliku\n"); fclose(stream); return 0;}

Klasyfikowanie znaków: nagłówek <ctype.h> Nagłówek <ctype.h> zawiera deklaracje funkcji służących do klasyfikowania znaków. We wszystkich funkcjach argument c jest typu int o wartości albo równej EOF, albo wyrażalnej jako unsigned char, a wynik jest typu int. Funkcje zwracają wartość różną od zera (TRUE), jeśli argument c spełnia podany warunek albo należy do wskazanej klasy znaków; w przeciwnym przypadku zwracają zero.

Funkcje operujące na znakach isalnum(c) prawdą jest albo isalpha(c), albo isdigit(c) isalpha(c) prawdą jest albo isupper(c), albo islower(c) iscntrl(c) znak kontrolny isdigit(c) cyfra dziesiętna isgraph(c) znak drukowalny za wyjątkiem odstępu islower(c) mała litera isprint(c) znak drukowalny łącznie z odstępem ispunct(c) znak drukowalny za wyjątkiem odstępu, liter i cyfr isspace(c) odstęp, nowa strona, nowy wiersz, powrót karetki, tabulator, pionowy tabulator isupper(c) wielka litera isxdigit(c) cyfra szesnastkowa

Przykład #include <stdio.h> #include <ctype.h> int main(void) { char c = 'C'; if (isdigit(c)) printf("%c jest cyfra\n",c); else printf("%c nie jest cyfra \n",c); return 0; }

Funkcje zmiany wielkości liter Poniższe funkcje służą do zmiany wielkości liter: - int tolower(int c) - zamienia c na małą literę - int toupper(int c) - zamienia c na wielką literę Przykład #include <string.h> #include <stdio.h> #include <ctype.h> int main(void){ int length, i; char *string = ”oto tekst"; length = strlen(string); for (i=0; i<length; i++) {string[i] = toupper(string[i]);}; printf("%s\n",string); return 0; }

Operacje na tekstach

Operacje na tekstach: nagłówek <string.h> W nagłówku <string.h> są zdefiniowane dwie grupy funkcji operujących na tekstach. Do pierwszej grupy należą funkcje, których nazwy rozpoczynają się od str, do drugiej grupy - od mem. Z wyjątkiem funkcji memmove(), skutek kopiowania instancji, które częściowo się pokrywają, nie jest zdefiniowany. Argumenty funkcji porównujących są traktowane jak tablice zmiennych typu unsigned char.

char. strcpy(char. s, const char char *strcpy(char *s, const char *ct) - kopiuje tekst z ct do s łącznie ze znakiem '\0'; zwraca s char *strncpy(char *s, char *ct, int n) - kopiuje co najwyżej n znaków tekstu z ct do s; jeżeli ct ma mniej niż n znaków, dopełnia s znakami '\0'; zwraca s char *strcat(char *s, char *ct) - dopisuje znaki tekstu z ct na koniec tekstu w s; zwraca s char *strncat(char *s, char *ct, int n) - dopisuje co najwyżej n znaków tekstu ct na koniec tekstu w s; tekst w s kończy znakiem '\0'; zwraca s Przykład #include <stdio.h> #include <string.h> int main(void) { char string[10]; char *str1 = "abcdefghi"; strcpy(string, str1); printf("%s\n", string); return 0;}

int strcmp(char. cs, char int strcmp(char *cs, char *ct) - porównuje teksty zawarte w cs i ct; zwraca wartość < 0 dla cs<ct, wartość 0 dla cs==ct lub wartość > 0 dla cs>ct int strncmp(char *cs, char *ct, int n) - porównuje co najwyżej n znaków tekstów zawartych w cs i ct; zwraca wartość <0 dla cs<ct, wartość 0 dla cs==ct lub wartość >0 dla cs>ct char *strchr(char *cs, char *c) - zwraca wskaźnik do pierwszego wystąpienia znaku c w tekście cs lub NULL, jeśli ten znak nie występuje char *strrchr(char *cs,int c) - zwraca wskaźnik do ostatniego wystąpienia znaku c w tekście cs lub NULL, jeśli ten znak nie występuje Przykład #include <string.h> #include <stdio.h> int main(void) { char *buf1 = "aaa", *buf2 = "bbb"; int ptr; ptr = strcmp(buf2, buf1); if (ptr > 0) printf("bufor 2 jest wiekszy niz bufor 1\n"); else printf("bufor 2 jest mniejszy niz bufor 1\n"); return 0;}

char. strstr(char. cs, char char *strstr(char *cs, char *ct) - zwraca wskaźnik do pierwszego wystąpienia tekstu ct w tekście cs lub NULL, jeśli ct nie występuje w cs char *strtok(char *s, char *ct) - wyszukuje w tekście s ciągi znaków przedzielone znakami z ct Przykład #include <stdio.h> #include <conio> #include <string.h> int main(void) { char *str1 = "Hello World", *str2 = "orld", *ptr; ptr = strstr(str1, str2); printf("Podciag: %s\n", ptr); //”orld” getch(); return 0;}

Funkcje mem... Funkcje mem... służą do operowania na zmiennych traktowanych jak tablice znakowe. Z założenia mają one działać bardzo sprawnie. W funkcjach tych argumenty s i t są typu void*, argumenty cs i ct są typu const void*, argument n jest typu size_t, a argument c jest zmienną typu int przekształconym do typu unsigned char.

Funkcja memcpy() void *memcpy(s,ct,n) Przykład - kopiuje n znaków ze zmiennej ct do instancji s i zwraca s; Przykład #include <stdio.h> #include <string.h> #include <conio.h> int main(void) { char src[] = "***"; char dest[] = "abc0123456709"; char *ptr; printf("Przed kopiowaniem: %s\n", dest); ptr = (char *) memcpy(dest, src, strlen(src)); if (ptr) printf("Po kopiowaniu: %s\n", dest); else printf("Blad kopiowania\n"); getch(); return 0;}

Funkcja memset() void *memset(s,c,n) - wstawia znak c do początkowych n znaków zmiennej s i zwraca s Przykład #include <string.h> #include <stdio.h> #include <mem.h> int main(void) { char buffer[] = "Hello world\n"; printf("Przed memset: %s\n", buffer);//Hello world memset(buffer, '*', strlen(buffer)-1); printf("Po memset: %s\n", buffer); //********** return 0;}

Funkcja memchr() void *memchr(cs,c,n) - zwraca wskaźnik do pierwszego wystąpienia znaku c w obiekcie cs lub NULL, gdy c nie występuje wśród początkowych n znaków cs Przykład #include <stdio.h> #include <string.h> #include <conio.h> int main(void) { char str[17]; char *ptr; strcpy(str, "To jest string"); ptr = (char *) memchr(str, 'r', strlen(str)); if (ptr) printf("Znak 'r' jest na pozycji: %d\n", ptr - str); else printf("Znaku 'r' nie znaleziono\n"); getch(); return 0;}

Funkcja memcmp() int memcmp(cs,ct,n) - porównuje początkowe n znaków zawartych w zmiennej cs i ct; zwraca taką samą wartość jak strcmp(); (tzn. <0, gdy, csn<ctn; =0, gdy csn==ctn i >0, gdy csn>ctn) Przykład #include <stdio.h> #include <string.h> #include <conio.h> int main(void) { char *buf1 = "aaa"; char *buf2 = "bbb"; int stat; stat = memcmp(buf2, buf1, strlen(buf2)); if (stat > 0) printf("bufor 2 jest wiekszy niz bufor 1\n"); else printf("bufor 2 jest mniejszy niz bufor 1\n"); getch(); return 0;}

Funkcja strchr() char *strrchr(char *cs,int c) - zwraca wskaźnik do ostatniego wystąpienia znaku c w tekście cs lub NULL, gdy ten znak nie występuje Przykład int main(void) { char string[15]; char *ptr, c = 'r'; strcpy(string, ”Oto string"); ptr = strchr(string, c); if (ptr) printf("Znak %c jest na pozycji: %d\n", c, ptr-string); else printf("Nie ma takiego znaku\n"); return 0;}

Funkcje matematyczne

Funkcje matematyczne: nagłówek <math.h> Nagłówek <math.h> zawiera deklaracje funkcji i makr matematycznych. Błąd dziedziny występuje, gdy argument funkcji nie należy do dziedziny, dla której funkcja została zdefiniowana. Błąd zakresu występuje wówczas, gdy wartość funkcji nie może być wyrażona jako double. Jeśli wynik jest nadmiarowy, to funkcja zwraca HUGE_VAL z właściwym znakiem. Jeśli wynik jest nie domiarowy, to funkcja zwraca zero.

Funkcje matematyczne Argumenty x i y są typu double, a argument n jest typu int. Wszystkie funkcje zwracają wartość typu double. Wartości kątów dla funkcji trygonometrycznych wyraża się w radianach.

sin(x) sinus x cos(x) cosinus x tan(x) tangens x asin(x) sini-1(x) w przedziale [-/2, +/2], x  [-1, 1 ] acos(x) cos-1 (x) w przedziale [0, ], x  [-1, 1 ] atan(x) tan-1 (x) w przedziale [-/2, /2] atan2(y,x) tan-1 (y/x) w przedziale [-,] sinh(x) sinus hiperboliczny x cosh(x) cosinus hiperboliczny x tanh(x) tangens hiperboliczny x exp(x) funkcja wykładnicza e^x log(x) logarytm naturalny: ln(x), x > 0 log10(x) " logarytm o podstawie 10: log10(x), x > 0 pow(x,y) x^y

sqrt(x) x >x>= 0 ceil(x) najmniejsza liczba całkowita nie mniejsza niż x; wynik typu double floor(x) największa liczba całkowita nie większa niż x; wynik typu double fabs(x) wartość bezwzględna x idexp(x,n) x *2n frexp(x, int *exp) Rozdziela x na znormalizowaną część ułamkową z przedziału [ 1 /2, 1 ] i wykładnik potęgi 2; funkcja zwraca część ułamkową, a wykładnik potęgi wstawia do *exp; jeśli x jest równe zero, to obie części wyniku są równe zero.

Funkcje narzędziowe

Funkcje narzędziowe: nagłówek <stdlib.h> Nagłówek <stdlib.h> zawiera deklaracje funkcji służących do przekształcania liczb, przydzielania pamięci i innych podobnych zadań.

Funkcja atof() double atof(const char *s) - przekształca tekst zawarty w s na wartość typu double; Przykład #include <stdlib.h> #include <stdio.h> int main(void) { float f; char *str = "12345.67"; f = atof(str); printf("string = %s float = %f\n", str, f); return 0;}

Funkcja atoi() int atoi(const char *s) - przekształca tekst zawarty w s na wartość typu int Przykład int main(void) { int n; char *str = "12345.67"; n = atoi(str); printf("string = %s integer = %d\n", str, n); return 0; }

Funkcja rand() int rand(void) - zwraca pseudolosową liczbę całkowitą z przedziału między 0 i RAND_MAX. Wartość RAND_MAX wynosi co najmniej 32767. Przykład int main(void) { int i; randomize(); printf("Liczby losowe z przedzialu od 0 do 99\n\n"); for(i=0; i<10; i++) printf("%d\n", rand() % 100); return 0; }

Funkcja malloc() void *malloc(size_t size) - zwraca wskaźnik do obszaru pamięci przeznaczonego dla zmiennej o rozmiarze size. Funkcja zwraca NULL, jeśli to polecenie nie może być wykonane. Obszar nie jest inicjowany. Przykład #include <stdio.h> #include <string.h> #include <alloc.h> #include <process.h> int main(void) { char *str; if ((str = (char *) malloc(10)) == NULL) //rezerwacja pamieci { printf("Za malo pamieci\n"); exit(1); } strcpy(str, "Hello"); printf("String %s\n", str); free(str); return 0; }

Funkcja free() void free(void *p) zwalnia obszar pamięci wskazany przez p; nie robi nic, jeżeli p równa się NULL. Argument p musi być wskaźnikiem do obszaru uprzednio przydzielonego przez jedną z funkcji: calloc(), malloc() lub realloc(). Przykład char *str; str = (char *) malloc(10); strcpy(str, "Hello"); printf("String= %s\n", str); free(str);

Funkcja abort() void abort(void); - powoduje nienormalne zakończenie programu Przykład int main(void) { printf("Wywolanie abort()\n"); abort(); return 0; /* Do tego miejsca program nigdy nie dotrze */ }

Funkcja exit() void exit(int status); - powoduje normalne zakończenie programu Przykład #include <stdlib.h> #include <conio.h> #include <stdio.h> int main(void) { int status; printf("Wprowadz 1 lub 2\n"); status = getch(); exit(status - '0'); // ustalanie statusu bledu dla DOS return 0; // Do tego miejsca program nigdy nie dotrze }

Obsługa daty i czasu

Obsługa daty i czasu: nagłówek <time.h> W nagłówku <time.h> znajdują się deklaracje typów i funkcji służących do obsługi daty i czasu. Niektóre funkcje operują czasem lokalnym, który może być różny od czasu kalendarzowego na skutek np. różnicy stref czasowych. W time.h stosowane są typy clock_t i time_t, reprezentujące czas, oraz struktura struct tm, która mieści w sobie składniki czasu kalendarzowego

Funkcja clock() clock_t clock(void); - zwraca czas procesora wykorzystany przez program od rozpoczęcia działania lub -1, gdy ta informacja jest niedostępna. Wyrażenie clock()/CLOCKS_PER_SEC daje czas w sekundach. Przykład int main(void) { clock_t start, end; start = clock(); delay(2000); //opoznienie w ms end = clock(); printf(„Różnica czasu: %f\n", (end - start) / CLK_TCK); // CLK_TCK – liczba imp. Na sek. return 0; }

Funkcja time() time_t time(time_t *tp); - zwraca aktualny czas kalendarzowy lub -1, gdy ta informacja jest niedostępna. Jeśli argument tp jest różny od NULL, zwracana wartość jest również wstawiana do *tp. Przykład #include <time.h> #include <stdio.h> #include <dos.h> int main(void) { time_t t; t = time(NULL); printf("Liczba sekund od 1 stycznia 1970 = %ld",t); return 0; }

Struktura tm Struct tm{ int tm_sec; //sekundy, które upłynęły po minucie (0, 61) int tm_min; //minuty, które upłynęły po godzinie (0, 59) int tm _hour; //godziny, które upłynęły od północy (0, 23) int tm_mday; //dzień miesiąca (1, 31) int tm_mon; //miesiące, które upłynęły od stycznia (0, 11) int tm_year; //lata, które upłynęły od 1900 r. int tm _wday; //dni, które upłynęły od niedzieli (0, 6) int tm_yday; //dni, które upłynęły od 1 stycznia (0, 365) int tm_isdst; /*znacznik letniej zmiany czasu. Jest dodatni, gdy obowiązuje czas letni; zero - gdy nie obowiązuje, i ujemny - w przypadku braku takiej informacji*/} Struktuta tm wykorzystwana jest przez funkcje asctime, gmtime,localtime, mktime.

Funkcje wykorzystujące strukturę tm char *asctime(const struct tm *tblock); - funkcja konewrtuje biężącą datę i czas na ciag ASCII. struct tm *gmtime(const time_t *timer); - konwertuje bieżącą datę i czas do GMT (Greenwich mean time). struct tm *localtime(const time_t *timer); - konwertuje datę i czas GMT (Greenwich mean time) do daty i czasu lokalnego  time_t mktime(struct tm *t); - konwertuje bieżącą datę i czas do formatu kalendarzowego. Dopuszczalny zakres daty i czasu kalendarzowego: 01.01.1970 00:00:00 do 19.01.2038 03:14:07. Funkcja zwraca czas kalendarzowy lub –1 w przypadku błędu.

Funkcja asctime() char *asctime(const struct tm *tblock); - funkcja konewrtuje biężącą datę i czas na ciag ASCII. int main(void) { struct tm t; char str[80]; t.tm_sec = 1; /* Sekundy */ t.tm_min = 30; /* Minuty */ t.tm_hour = 9; /* godzina*/ t.tm_mday = 22; /* dzien miesiaca*/ t.tm_mon = 11; /* Miesiac*/ t.tm_year = 2003; /* rok */ t.tm_wday = 4; /* dzien tygodnia */ t.tm_yday = 0; /*kolejny dzien roku – nie pokazuj*/ t.tm_isdst = 0; /* nie obowiazuje czas letni */ strcpy(str, asctime(&t)); printf("%s\n", str); return 0;}

Rekurencja

Definicja rekurencji Definicja: Mówimy, że funkcja lub typ danych są rekurencyjne, jeżeli w ich definicji następuje odwołanie do nich samych. Przykład. Definicja rekurencyjna funkcji silnia: n! = n*(n-1)*...*1 = n* (n-1)! Definicja programowa jest następująca:

Rekurencyjne definiowanie funkcji Przykład rekurencyjnej definicji funkcji silnii (factorial fiunction) f(n): f(0) = 1 f(n) = n *f(n-1)   dla każdego naturalnej liczby n > 0 Dla n=3 otrzymujemy: f(3) = 3 · f(3-1) = 3 · f(2) = 3 · 2 · f(2-1) = 3 · 2 · f(1) = 3 · 2 · 1 · f(1-1) = 3 · 2 · 1 · f(0) = 3 · 2 · 1 · 1 = 6

Wywołania funkcji i realizacja rekurencji Wszystkie współczesne języki programowania umożliwiają implementację funkcji rekurencyjnych. Do rekurencyjnego wywołania w programie funkcji wykorzystywany jest stos. Stan każdej wywoływanej funkcji jest opisany wartością wszystkich zmiennych automatycznych i parametrów funkcji oraz adres powrotu, wskazujący miejsce wznowienia wykonywania procesu, który wywołał funkcję. Obszar danych zawierający te informacje nosi nazwę rekordu wywołań (aktywacji; activation record) lub ramki stosu (stack frame). Jest on umieszczany na stosie programu. Rekord wywołania istnieje aż do zakończenia działania funkcji, której dotyczy. W program C/C++ najdłużej istnieje rekord wywołania dla funkcji main().

Zawartość rekordu wywołania Rekord wywołania zawiera zwykle następujące dane: Wartości wszystkich parametrów funkcji (adres tablicy i kopie wszystkich pozostałych danych). Wartości wszystkich zmiennych lokalnych (automatycznych) lub adres miejsca ich przechowywania (zależy to od kompilatora). Adres powrotu – adres instrukcji następnej po instrukcji wywołującej funkcję. Dynamiczne dowiązanie – wskaźnik do rekordu wywołania procesu wywołującego funkcję. Zwracana wartość – tylko, gdy nie jest to wartość typu void.

Potęga Rekurencyjne wywoływanie funkcji obliczającej podnoszącą dowolną liczbę x do nieujemnej całkowitej potęgi n. Definicja funkcji potęgującej: Poziom wywołania Parametry wywoływanej funkcji Wartości zwracane przez wywoływaną funkcję 1 potega(x,4) x*x*x*x 2 potega(x,3) x*x*x 3 potega(x,2) x*x 4 potega(x,1) x 5 potega(x,0) Poziomy wywołania funkcji potęgującej x4

Schemat Hornera Wielomian: wn(x)=a0xn+a1xn-1+…+an-1x+an można zapisac w postaci: wn(x)=(…((a0x+a1)x+a2)x+.…+an-1)x+an lub w postaci rekurencyjnej: wn(x)= wn-1(x)x+ an, n=1,2,…, gdzie w0(x)=a0.

Rekurencyjny algorytm Euklidesa Przykładowo NWD(18,12)=NWD(6,12)=NWD(0,6)=6.

Niebezpieczeństwa rekurencji Ze stosowaniem rekurencji związanych jest wiele niebezpieczeństw. Dwa najważniejsze spośród nich to: dublowanie obliczeń przepełnienie stosu

Dublowanie obliczeń Dublowanie obliczeń polega na niepotrzebnym wykonywaniu tych samych obliczeń. Przykład. Obliczanie wartości ciągu Fibonacciego. Liczby Fibonacciego są zdefiniowane w sposób rekurencyjny: F0=0 F1=1 Fi=Fi-1+ Fi-2 , dla i>=2. Początkowe litery ciągu Fibonacciego: 0,1,1,2,3,5,8,13,21,34,55,… Liczby Fibonacciego rosną wykładniczo.

Implementacja funkcji obliczającej liczby Fibonacciego Funkcja fib() zwraca wartość ciągu Fibonacciego dla i>=0. int fib(int i) { If (i<2) return i; else return fib(i-1)+ fib(i-2); } Drzewo obliczeń dla funkcji fib(4).

Liczba wywołań funkcji fib() f(n) 6 25 10 177 15 1973 20 21891 242785 30 2692537 Wykazuje się, że w celu obliczenia wartości fib(n) liczba wywołań funkcji fib() wynosi f(n)=2*fib(n+1)-1. Liczba ta jest ogromna już dla niewielkich n, co obrazują tabela i wykres.

Programowanie dynamiczne Aby radzić sobie z takimi problemami można wykorzystać programowanie dynamiczne, które najogólniej polega na odejściu od rekurencji na rzecz programowania iteracyjnego. Programowanie dynamiczne można zastosować do tych problemów, które można podzielić na wiele podproblemów. Wyniki rozwiązań podproblemów są zapamiętywane w odpowiednich strukturach danych (najczęściej w tablicy), unikając w ten sposób wielokrotnych obliczeń dla tego samego podproblemu. Programowanie dynamiczne polega więc na wykonania obliczeń każdego podproblemu tylko raz i zapamiętaniu jego wyniku w tabeli. W każdym kolejnym kroku można z tej tabeli korzystać.

Przykład FIB_DYN(i) { f[0]1 f[1]  1 k  2 while (k<i){ Poniższa funkcja FIB_DYN zwraca wartość ciągu Fibonacciego dla i>=0 obliczoną metodą programowania dynamicznego. FIB_DYN(i) { f[0]1 f[1]  1 k  2 while (k<i){ f[k]=f[k-1]+f[k-2] k=k+1 }

Liczba wywołań funkcji MacCarthy() Przepełnienie stosu Programy rekurencyjne zwykle wykorzystują dużo zasobów pamięciowych. Przykład. Poniższa funkcja zwraca wartość funkcji MacCarthy’ego dla całkowitego x. MacCarthy(x) { If (x>100) return (x-10) else return MacCarthy(MacCarthy(x+11))} Liczba wywołań funkcji MacCarthy()

Dziękuję

Tryby adresowania i modele pamięci

Zagadnienia Tryb adresowania rzeczywistego Tryb adresowania wirtualnego Interpretacja pamięci Stronicowanie pamięci Modele pamięci Struktura programu wykonywalnego

Tryby adresowania i modele pamięci Adresy logiczne przechowywane we wskaźnikach mają następującą postać adres_logiczny = segment : offset Segment jest numerem bloku z zakresu od 0 do 0xFFFF o rozmiarze 64KB względem początku, którego jest liczone przesunięcie offset, w ramach bloku. Sposób wyznaczania adresu fizycznego na podstawie adresu logicznego zależy od trybu adresowania.

Tryb adresowania rzeczywistego W trybie adresowania rzeczywistego (tryb rzeczywisty procesora, np. tryb DOS-u; kompilator BC++) adres fizyczny określa zależność: adres_fizyczny = adres_bazowy + offset = = 16segment + offset. Segment (nr segmentu) i offset (przesunięcie w ramach bloku 64 KB o numerze segment) są liczbami 16 bitowymi bez znaku (typu unsigned). Adresy fizyczne są liczbami 20 bitowymi o wartościach od 0 do 0xFFFFF (adresowanie do 1 MB). Wartość 16segment jest adresem bazowym względem, którego liczone jest przesunięcie określone przez offset. Na przykład dla wskaźnika 0xB800 : 0x0002 adres bazowy wynosi 0xB8000 (mnożenie przez 16 polega na dopisaniu zera do wartości segmentu w postaci szesnastkowej), natomiast adres fizyczny wynosi 0xB8002. Funkcja wyznaczająca adres fizyczny wskaźnika typu void far * w trybie rzeczywistym może być zdefiniowana następująco: unsigned long adres(void far *x){ return 16UL* FP_SEG(x) + FP_OFF(x); // 16UL – zamiana na typ ul }

Tryb adresowania wirtualnego (1/) W trybie adresowania wirtualnego (tryb wirtualny, chroniony – wymaga przełączenia procesora, np. Windows NT; kompilator BCW) adresy logiczne przechowy­wane we wskaźnikach mają następującą postać: adres_logiczny = selektor : offset. Selektor jest liczbą dwubajtową (typu unsigned), która zawiera numer (indeks) pola w tablicy deskryptorów (TDskr) opisującego właściwości wskazywanego obszaru pamięci. Każdy deskryptor składa się z 8 bajtów i zawiera informacje o adresie początkowym (adresie bazowym) bloku, rozmiarze bloku oraz prawach dostępu do bloku (czytanie, zapis, usuwanie). Offset jest liczbą dwubajtową (typu unsigned), która określa przesunięcie od początku adresu bazowego (przesunięcie nie może przekroczyć rozmiaru bloku).

Na podstawie adresu bazowego można wyznaczyć adres liniowy (adres wirtualny) bloku (segmentu), który w przypadku braku stronicowania pamięci (automatycznej wymiany ramek stron pomiędzy pamięcią operacyjną i dyskową) jest równy adresowi fizycznemu bloku. adres_liniowy=adres_bazowy+offset

Wyznaczanie adresu liniowego Począwszy od procesorów 80386 adres bazowy może być 32-bitowy (czterobajtowy). Pozwala to adresować liniowy blok pamięci o rozmiarze do 4GB. Adresy wykraczające poza obszar pamięci operacyjnej komputera znajdują się w pamięci dyskowej. Dane lub fragmenty programu znajdujące się pod tymi adresami są ładowane z dysku do pamięci w momencie, gdy są potrzebne.

Tryb chroniony W trybie wirtualnym istnieje możliwość ograniczenia swobody dostępu programów (aplikacji) do bloków pamięci wykorzystywanych przez inne programy. Wystarczy w tym celu ustawić atrybuty segmentów i zapamiętać je w odpowiednich polach statusowych związanych z nimi deskryptorów (np. segmenty kodu mają domyślnie ustawiony atrybut tylko do czytania). Dzięki wspomnianej własności tryb adresowania wirtualnego często jest nazywany trybem chronionym. Praca w trybie chronionym pozwala na implementację wielozadaniowości.

Struktura deskryptora segmentu Każdy deskryptor składa się z 8 bajtów, opisujących fizyczny blok (segment) w pamięci wirtualnej. Znaczenie elementów deskryptora segmentu jest następujące: rozmiar segmentu (16 bitów); bajty 1,2; adres początkowy (bazowy) segmentu (bity 0...15); bajty 3,4; adres początkowy (bazowy) segmentu (bity 16...23); bajt 5; prawa dostępu do segmentu (dla 80286); bajt 6; rozszerzenie dla procesora 80386; bajty 7,8. W przypadku procesora 80286, bajty 7, 8 deskryptora segmentu są równe zero. W przypadku procesora 80386 ich znaczenie jest następujące: bity 0...3 stanowią rozszerzenie rozmiaru segmentu (20-bitowy rozmiar segmentu dla 80386) bity 4...7 określają rozszerzenie praw dostępu bity 8...15 stanowią rozszerzenie adresu początkowego segmentu (32-bitowy adres bazowy dla 80386). Z przedstawionych rozważań wynika, że wartości kolejnych selektorów w adresach logicznych różnią się zawsze o stałą 8. Tablica deskryptorów segmentów ma rozmiar 64KB, a więc można w niej zapamiętać 8K (8192) deskryptorów. W praktyce dostępne są dwie tablice deskryptorów, globalna (GDT) i lokalna (LDT). Adresy tablic deskryptorów pamiętane są w 40-bitowych rejestrach GDTR i LDTR.

Deskryptor segmentu Limit - rozmiar segmentu; Baza - adres początkowy (bazowy) segmentu; Status286 - prawa dostępu do segmentu - w przypadku procesora 80286; Status386 - prawa dostępu do segmentu - w przypadku procesora 80386. W przypadku procesora 80286 maksymalna długość segmentu wynosi 64KB, natomiast w przypadku procesora 80386 maksymalna długość segmentu wynosi 1MB, a przy włączonym bicie granulacji (bit nr 3 w polu Status386 deskryptora) wynosi 4K 1MB = 4GB. Dostępne są dwie tablice deskryptorów, globalna (GDT) i lokalna (LDT), co daje łączną, wirtualną przestrzeń adresową 16K 4GB = 64TB (tera bajtów).

Interpretacja pamięci Procesor 808x Pamięć wirtualna |----------------| 1 MB Pamięć liniowa |----------------| 1 MB Pamięć fizyczna RAM |-----------|----| 1 MB 640 KB Procesor 80286 Pamięć wirtualna |-------------------------| 16 MB Pamięć liniowa |-------------------------| 16 MB Pamięć fizyczna RAM |-----------| - - - - - - -| 16 MB Procesor 80386 pamięć dyskowa Pamięć wirtualna |-------------------------------------------| 64 TB 4 GB Pamięć liniowa (1 selektor) |-----------------------------| - - - - - - -| Pamięć fizyczna RAM |-----------|- - - - - - - - - -| 4 GB

Stronicowanie pamięci Procesor 80386 implementuje wirtualne stronicowanie pamięci sprzę­towo. Wszystkie informacje niezbędne do jego realizacji są przechowy­wa­ne na dysku w postaci katalogu stron i kilku tablic stron. Mechanizm stronicowania wykorzystuje pojęcie adresu liniowego (adresu wirtualnego). W procesorach 808x i 80286, które nie posiadają możliwości stronicowania pamięci oraz w przypadku pracy w trybie rzeczywistym lub przy wyłączonym stronicowaniu pamięci, adresy liniowe równają się adresom fizycznym. Jeżeli natomiast stronicowanie jest dostępne, to adresy wirtualne muszą być tłumaczone na adresy fizyczne - co jest realizowane sprzętowo.

Stronicowanie pamięci - wyznaczanie adresu fizycznego (1/2) CR3 – rejestr procesora. Katalog określa numer tablicy stron, w której znajduje się ramka strony. Strona określa pozycję adresu strony (ramki strony).

Stronicowanie pamięci - wyznaczanie adresu fizycznego (2/2) Sposób implementacji trybu chronionego zależy od wykorzystywanego kompilatora (np. w systemie Borland C++ 3.1 tryb chroniony można wykorzystywać w środowisku Windows w oparciu o kompilator BCW). Do zarządzania pamięcią w trybie chronionym można wykorzystywać funkcje klasy Global (np. GlobalAlloc, GlobalLock, GlobalFree). Za pomocą funkcji GlobalAlloc można przydzielić dynamicznie w pamięci rozszerzonej (powyżej 1 MB) blok o rozmiarze do 16 MB.

Modele pamięci (1/4) Adresowanie oparte na segmentach (selektorach) jest ściśle powiązane ze strukturą programu wykonywalnego (exe), który składa się z segmentu kodu, danych i stosu oraz obszaru przeznaczonego dla zmiennych dynamicznych - nazywanego stertą (stosem zmiennych dynami­cznych). W języku C/C++ program może składać się z wielu modułów. Po kom­pi­la­cji programu (modułu) utworzony zostanie kod wykonywalny (wyniko­wy), który może składać się z następujących segmentów: kodu (instrukcje do wykonania), danych zainicjowanych (np. stałe), danych niezainicjowanych, stosu (zmienne automatyczne i parametry funkcji), sterty (stosu zmiennych dynamicznych). Każdy z tych segmentów posiada swoją nazwę i klasę, które są łańcuchami znaków (np. segment kodu jest klasy CODE i ma domyślną nazwę _TEXT). Klasa stanowi informację dla programu łączącego o sposobie łączenia i kolejności rozmieszczania w pamięci segmentów należących do poszczególnych modułów.

Modele pamięci (2/4) Rozmiar segmentów kodu i danych nie może przekroczyć 64KB dla pojedynczego modułu programu. Stos programu nie może przekroczyć 64KB, natomiast sterta może zajmować całą dostępną pamięć (ograniczenia wynikają z możliwości kompilatora). W przypadku programów wielomodułowych całkowity rozmiar kodu i danych programu oraz rozmieszczenie modułów w pamięci zależy od przyjmowanego w kompilatorze modelu pamięci. Na przykład system BC++ 3.1 oferuje sześć modeli: TINY, SMALL, MEDIUM, COMPACT, LARGE i HUGE (BCW++3.1 – modele: SMALL, MEDIUM, COMPACT, LARGE). Każdy z tych modeli nakłada pewne ograniczenia na maksymalny rozmiar obszaru danych, rozmiar kodu programu oraz domyślny typ wskaźników kodu i danych (wskaźniki dalekie lub bliskie).

Modele pamięci (3/4) We wszystkich modelach, oprócz modelu HUGE, obszar danych statycznych jest wspólny dla wszystkich modułów programu, a jego rozmiar nie może przekroczyć 64KB. W modelu TINY maksymalny rozmiar, łącznie, kodu i danych (łącznie ze stertą, tj. obszarem zmiennych dynamicznych) nie może przekroczyć 64KB. Model SMALL ogranicza kod do 64KB i dane do 64KB. W modelu MEDIUM kod jest ograniczony do 1MB, natomiast dane do 64KB. Model COMPACT ogranicza kod do 64KB oraz dane do 1MB. W modelach LARGE i HUGE kod i dane, każde z osobna, nie mogą przekroczyć 1MB.

Modele pamięci (4/4) Model pamięci określa domyślny typ wskaźników kodu i danych (wskaźniki dalekie lub bliskie). W szczególności, wskaźnik zdefiniowany jako int *wsk jest wskaźnikiem bliskim (dwubajtowym) w modelach TINY, SMALL i MEDIUM, natomiast wskaźnikiem dalekim w pozostałych modelach. We wszystkich modelach pamięci jedynie zmienne automatyczne i parametry funkcji posiadają ściśle określone, niezależne od modelu pamięci miejsce przechowywania - stos procesora (segment stosu).

Struktura programu wykonywalnego

Struktura programu wykonywalnego Program wykonywalny ma różną strukturę w zależności od wykorzystywanego modelu pamięci.

Model TINY Model ten jest przeznaczony do tworzenia programów o niewielkich rozmiarach. W modelu tym rozmiar obszaru przeznaczonego na kod, dane i stos nie może przekroczyć 64KB. Wskaźniki kodu i danych są bliskie. Programy tej klasy mogą być przekształcone do wersji com.

Model SMALL (1/2) Model ten jest przeznaczony do tworzenia programów, w których kod i dane znajdują się w osobnych segmentach. Rozmiar kodu nie może przekroczyć 64 KB oraz rozmiar danych nie może przekroczyć 64KB. Wskaźniki kodu i danych są bliskie. Istnieją dwa rodzaje sterty: bliska – w obszarze segmentu danych oraz daleka – od początku stosu do końca dostępnej pamięci. Segmenty danych, sterty bliskiej i stosu zajmują ten sam obszar pamięci (segment danych). Do przydzielania pamięci w obszarze sterty bliskiej służą funkcje malloc() i calloc() – ANSI C (zwolnie­nie pamięci – funkcja free), natomiast w obszarze sterty dalekiej funkcje farmalloc() i farcalloc() – BC++ od 3.1 (zwolnienie pamięci – funkcja farfree). Operator new przydziela pamięć domyślnie w obszarze sterty bliskiej.

Struktura pamięci w modelu SMALL

Model MEDIUM (1/2) Model ten jest przeznaczony do tworzenia programów o znacznych rozmiarach kodu i niewielkiej ilości danych. Segmenty kodu należące do różnych modułów są umieszczane kolejno w pamięci. Rozmiar każdego z nich może mieć maksymalnie do 64KB, ale łącznie nie mogą zajmować więcej niż 64 KB. Adres segmentu kodowego jest ustalany w momencie odwołania do funkcji zdefiniowanej w danym module. Dane statyczne, sterta bliska i stos są połączone w jeden segment danych, którego rozmiar nie może przekroczyć 64KB. Wskaźniki kodu są dalekie, natomiast danych bliskie.

Struktura pamięci w modelu MEDIUM

Model COMPACT (1/2) Model ten jest przeznaczony do tworzenia programów o niewielkich rozmiarach (do 64KB), ale zarządzających wielkimi strukturami danych. Rozmiar obszaru danych nie może przekroczyć 64KB. Składa się on z segmentu danych zainicjowanych i niezainicjowanych. Dane i sterta łącznie nie mogą zajmować więcej niż 1MB. Stos jest niezależnym segmentem o rozmiarze do 64KB. Istnieje tylko daleka sterta zmiennych dynamicznych. Wskaźniki kodu są bliskie, danych – dalekie.

Struktura pamięci w modelu COMPACT

Model LARGE (1/2) Model ten jest przeznaczony do tworzenia programów o znacznych rozmiarach kodu i danych. Segmenty kodu należące do różnych modułów są umieszczane kolejno w pamięci. Rozmiar każdego z nich może mieć maksymalnie do 64KB, ale łącznie nie mogą zajmować więcej niż 1MB. Adres segmentu kodowego jest ustalany w momencie odwołania do funkcji zdefiniowanej w danym module. Obszar danych nie może przekroczyć 64KB. Składa się on z segmentu danych zainicjo­wanych i niezainicjowanych. Dane i sterta łącznie nie mogą zajmować więcej niż 1MB. Stos jest niezależnym segmentem o rozmiarze do 64KB. Istnieje tylko daleka sterta zmiennych dynamicznych. Wskaźniki kodu i danych są dalekie.

Struktura pamięci w modelu LARGE

Model HUGE Model ten jest przeznaczony do tworzenia programów o znacznych rozmiarach kodu i danych. Segmenty kodu należące do różnych modułów są umieszczane kolejno w pamięci. Rozmiar każdego z nich może mieć maksymalnie do 64KB. Obszar danych statycznych może mieć rozmiar większy niż 64KB. Składa się on z segmentów danych należących do poszczególnych modułów każdy o rozmiarze nie przekraczającym 64KB. Segmenty danych należące do różnych modułów są umieszczane kolejno w pamięci. Dane i sterta łącznie nie mogą zajmować więcej niż 1MB. Adres segmentu kodowego jest ustalany w momencie odwołania do funkcji zdefiniowanej w danym module. Podobnie adres segmentu danych jest ustalany w momencie odwołania do danej zdefiniowanej w odpowiednim segmencie. Stos jest niezależnym segmentem o rozmiarze do 64KB. Istnieje tylko daleka sterta zmiennych dynamicznych. Wskaźniki kodu i danych są dalekie.

Struktura pamięci w modelu HUGE

Sortowanie plików

Sortowanie plików metodą prostego scalania Wejście: danych jest mN plików z danymi. Dane każdego z plików kopiujemy do tablic A1[1..n1], A2[1..n2], …, Am[1..nm], gdzie niN, i=1,2,…,m są liczbami danych zapisanych w poszczególnych plikach. Algorytm: algorytm sortowania przez scalanie polega na iteracyjnym: scalaniu tablic Ai oraz Ai+1, dla i=1,2,..m-1; po scaleniu tablic Ai oraz Ai+1 wykonywana jest operacja ich sortowania. Wyjście: posortowana tablica A[1..n], gdzie n=n1+n2+…+nm.

Koniec

Systemy komputerowe

Komputer Komputer - elektroniczna maszyna cyfrowa służąca do automatycznego przetwarzania danych przedstawionych cyfrowo (tzn. za pomocą odpowiednio zakodowanych liczb). Istotną cechą odróżniającą komputer od innych urządzeń jest jego „programowalność”, tzn. wykonywanie konkretnych zadań (np. obliczeń) jest związane z wykonywaniem zapisanych w pamięci komputera programów.

ARCHITEKTURA KOMPUTERA Architektura komputera to struktura komputera wyrażająca się doborem sposobu łączenia jego urządzeń składowych, przy czym dotyczy ona związków funkcjonalnych między tymi elementami, a nie przestrzennych (jak w architekturze budowli); np. dobór typu procesorów, modułów pamięci, sterowników urządzeń peryferyjnych oraz sposób ich sprzęgania tak, aby całość spełniała pewne kryteria użytkowe.

Architektura Von Neumanna Architektura współczesnych komputerów została sformułowana po raz pierwszy w 1964 roku przez Von Neumanna. System komputerowy zbudowany w oparciu o architekturę Von Neumanna powinien: Mieć skończoną i funkcjonalnie pełną listę rozkazów Mieć możliwość wprowadzenia programu do systemu komputerowego poprzez urządzenia zewnętrzne i jego przechowywanie w pamięci w sposób identyczny jak danych Dane i instrukcje w takim systemie powinny być jednakowo dostępne dla procesora; Informacja jest tam przetwarzana dzięki sekwencyjnemu odczytywaniu instrukcji z pamięci komputera i wykonywaniu tych instrukcji w procesorze.

sieć działań komputera Video controller Controller Monitor Hard disk Disk Main memory CPU Floppy other peripherals bus

Komputer osobisty

Urządzenia zewnętrzne dla PC

Poziomy słuchania Ignorowanie Udawanie Słuchanie selektywne Słuchanie empatyczne

Postaraj się dostrzec piękne strony programowania