Język C/C++ Funkcje
Funkcje - wstęp Funkcję można uważać za operację zdefiniowaną przez programistę i reprezentowaną przez nazwę funkcji. Operandami funkcji są jej argumenty, ujęte w nawiasy okrągłe i oddzielone przecinkami. Typ funkcji określa typ wartości zwracanej przez funkcję.
Kiedy używamy funkcji Często wykonujemy jakieś zadanie, np. obliczanie wartości sinus, zadanie jest bardzo trudne.
Metoda zstępująca w programowaniu Całe zadanie dzielimy na podzadania, podzadania są instrukcjami programu, są to instrukcje abstrakcyjne (nie występują w języku programowania), muszą mieć sformalizowaną postać (funkcje), w funkcjach należy określić jaki algorytm realizują i jak się komunikują z otoczeniem.
Deklaracja funkcji Deklaracja funkcji ma postać: typ nazwa(deklaracje argumentów); Występujące tutaj trzy elementy: typ zwracany, nazwa funkcji i wykaz argumentów nazywane są łącznie prototypem funkcji. W języku C++ obowiązuje zasada deklarowania prototypu każdej funkcji przed jej użyciem. Prototyp funkcji o tym samym identyfikatorze może wystąpić w tym samym programie wiele razy (przeciążanie funkcji), natomiast brak prototypu funkcji wywoływanej w programie jest błędem syntaktycznym. Argumenty w deklaracji funkcji nazywa się również argumentami formalnymi lub parametrami formalnymi. Wykonanie instrukcji deklaracji funkcji nie alokuje żadnego obszaru pamięci dla parametrów formalnych.
Przykłady prototypów funkcji int f1(); void f3(); void f3(void); int* f5(int); int (*fp6) (const char*, const char*); extern double sqrt(double); extern char *strcpy(char *to, const char *from); extern int strlen(const char *st); extern int strcmp(const char *s1, const char *s2); int printf(const char *format, ...);
Wywołanie funkcji Wywołanie funkcji jest poleceniem obliczenia wartości wyrażenia, zwracanej przez nazwę funkcji. Instrukcja wywołania ma składnię: nazwa (argumenty aktualne); gdzie nazwa jest zadeklarowaną wcześniej nazwą funkcji, argumenty aktualne są wartościami argumentów formalnych, zaś para nawiasów okrągłych () jest operatorem wywołania. Liczba, kolejność i typy argumentów aktualnych powinny dokładnie odpowiadać zadeklarowanym argumentom formalnym. Przy niezgodności typów argumentów kompilator stara się wykonać konwersje niejawne; jeżeli nie może dokonać sensownej konwersji, sygnalizuje błąd.
Co się dzieje przy wywoływaniu funkcji ? Zastosowanie operatora wywołania do nazwy funkcji powoduje alokację pamięci dla argumentów formalnych i przekazanie im wartości argumentów aktualnych. Od tej chwili argumenty formalne stają się zmiennymi lokalnymi o wartościach inicjalnych równych przesłanym do nich wartościom argumentów aktualnych. Zasadniczym sposobem przekazywania argumentów do funkcji jest przekazywanie przez wartość: do każdego argumentu formalnego jest przesyłana kopia argumentu aktualnego. Ponieważ argumenty formalne po wywołaniu stają się zmiennymi lokalnymi funkcji, zatem wszelkie wykonywane na nich operacje nie zmieniają wartości argumentów aktualnych. Wyjątkiem od tej zasady jest przesłanie do funkcji adresów argumentów aktualnych za pomocą wskaźników lub referencji - później.
Definicja funkcji Składnia definicji funkcji jest następująca: typ nazwa (deklaracje argumentów) { instrukcje } Definicja podaje typ zwracany przez funkcję, jej nazwę oraz argumenty formalne. Argumentami formalnymi funkcji mogą być zmienne wszystkich typów podstawowych, struktury, unie oraz wskaźniki i referencje do tych typów, a także zmienne typów definiowanych przez użytkownika. Nie mogą być nimi tablice, ale mogą być wskaźniki do tablic (jeśli są tablice, to są traktowane jako wskaźniki). Typ funkcji nie może być typem tablicowym ani funkcyjnym, ale może być wskaźnikiem do jednego z tych typów. Zarówno typ zwracany, jak i typy argumentów muszą być podawane w postaci jednoznacznych identyfikatorów. Identyfikatory mogą być nazwami typów wbudowanych (np. char) lub zdefiniowanych wcześniej przez użytkownika. Inaczej mówiąc, w nagłówku funkcji nie mogą występować definicje typów.
Ciało funkcji Instrukcje w bloku (nazywanym również ciałem funkcji) mogą być instrukcjami deklaracji. Ostatnią instrukcją przed nawiasem klamrowym zamykającym blok funkcji musi być instrukcja return; jedynie dla funkcji typu void instrukcja return; jest opcją. Zatem definicja int f() { }; jest błędna, natomiast definicja void f() { }; jest poprawna.
Instrukcja return Instrukcja return; występuje często w postaci: return wyrażenie; gdzie wyrażenie określa wartość zwracaną przez funkcję. Jeżeli typ tego wyrażenia nie jest identyczny z typem funkcji, to kompilator będzie próbował osiągnąć zgodność typów drogą niejawnych konwersji. Jeżeli okaże się to niemożliwe, to kompilacja zostanie zaniechana. Zgodność typu zwracanego z zadeklarowanym typem funkcji można również wymusić drogą konwersji jawnej. W bloku funkcji może wystąpić więcej niż jedna instrukcja (instrukcje warunkowe) return;.
Uwagi Deklaracje argumentów muszą podawać oddzielnie typ każdego argumentu; nazwy argumentów są opcjonalne. Definicja, która nie zawiera nazw argumentów, a jedynie ich typy, jest syntaktycznie poprawna.
Przykład #include <iostream.h> //deklaracja funkcji - prototyp int dods(int, int); using namespace std; int main() { int i, j, k; cout <<”Wprowadz dwie liczby typu int: ”; cin >> i >> j; cout << '\n'; k = dods (i,j); //wywolanie funkcji cout << ”i=” << i << ”\tj=” << j << '\n'; cout << ”dods(i,j)= ”<< k << '\n'; return 0; } int dods (int n, int m) { if (n + m > 10) return n + m; else return n;
Wywołanie funkcji Wywołanie funkcji zawiesza wykonanie funkcji wołającej i powoduje zapamiętanie adresu następnej instrukcji do wykonania po powrocie z funkcji wołanej. Adres ten, nazywany adresem powrotnym, zostaje umieszczony w pamięci na stosie programu (ang. run-time stack). Wywołana funkcja otrzymuje wydzielony obszar pamięci na stosie programu, nazywany rekordem aktywacji lub stosem funkcji. W rekordzie aktywacji zostają umieszczone argumenty formalne, inicjowane jawnie w deklaracji funkcji, lub niejawnie przez wartości argumentów aktualnych.
Argumenty domyślne Jawne inicjowanie argumentów w deklaracji (nie w definicji) funkcji można traktować jako przykład przeciążenia funkcji; tematem tym zajmiemy się później bardziej szczegółowo. Weźmy następujący przykład: w prototypie funkcji, która symuluje ekran monitora, wprowadźmy inicjalne wartości domyślne dla szerokości, wysokości i tła ekranu char* ekran(int x=80, int y=24, char bg = ' '); Wprowadzone wartości początkowe argumentów x, y oraz bg są domyślne w tym sensie, że jeżeli w wywołaniu funkcji nie podamy argumentu aktualnego, to na stosie funkcji zostanie “położona” wartość domyślna argumentu formalnego. Jeżeli teraz zadeklarujemy zmienną char* kursor, to wywołanie kursor = ekran(); jest równoważne wywołaniu kursor = ekran(80, 24, ' ');
Argumenty domyślne Jeżeli w wywołaniu podamy inną od domyślnej wartość argumentu, to zastąpi ona wartość domyślną, np. wywołanie kursor = ekran(132); jest równoważne wywołaniu kursor = ekran(132, 24, ' '); zaś wywołanie kursor = ekran(132, 66); kursor = ekran(132, 66, ' '); Składnia ostatniego wywołania pokazuje że nie można podać wartości pierwszego z prawej argumentu nie podając wszystkich wartości po lewej.
Argumenty domyślne Deklaracja funkcji nie musi zawierać wartości domyślnych dla wszystkich argumentów, ale podawane wartości muszą się zaczynać od skrajnego prawego argumentu, np. char* ekran( int x, int y, char bg = ' ' ); char* ekran( int x, int y = 24, bg = ' ' ); Wobec tego deklaracje char* ekran (int x = 80, int y, char bg = ' '); char* ekran ( int x, int y = 24, char bg ); są błędne.
Przekazywanie argumentów przez wartość W języku C++ przekazywanie argumentów przez wartość jest domyślnym mechanizmem językowym. Przy braku takiego mechanizmu każda zmiana wartości argumentu formalnego nie poprzedzonego modyfikatorem const wywołałaby taką samą zmianę argumentu aktualnego. Założenie to zostało podyktowane tym, że ewentualne zmiany wartości argumentów aktualnych, będące wynikiem wykonania jakiejś funkcji, są na ogół traktowane jako niepożądane efekty uboczne.
Przekazywanie argumentów przez wartość #include <iostream> using namespace std; int imax(int x, int y); int main() { float zf = 35.7; double zd = 11.0; int ii; ii = imax( zf, zd ); cout << ”ii =”<< ii << endl; return 0; } int imax(int x, int y) { if (x > y) return x; else return y;
Przekazywanie argumentów przez wartość Przekazywanie argumentów przez wartość nie będzie dogodnym mechanizmem w dwóch przypadkach: gdy wartości argumentu aktualnego muszą być przez funkcję zmodyfikowane, gdy przekazywany argument reprezentuje duży obszar pamięci (np. tablica, struktura). Dla tablic jest stosowany mechanizm, który nakazuje kompilatorowi przekazanie argumentów aktualnych w postaci adresu pierwszego elementu tablicy zamiast kopii całej tablicy. Natomiast w pierwszym przypadku należy znaleźć własne rozwiązanie.
Przekazywanie argumentów przez wartość Klasycznym przykładem nieprzydatności przekazywania argumentów przez wartość jest operacja zamiany wartości dwóch zmiennych. Funkcja void zamiana(int x, int y) { int pomoc = y; y = x; x = pomoc; }
Przekazywanie argumentów przez wskaźnik Pierwszy z nich polega na zastosowaniu wskaźników. Sposób ten ilustruje poniższy przykład. #include <iostream> using namespace std; void zamiana1 (int*, int* ); int main() { int i = 10; int j = 20; zamiana1( &i, &j ); cout <<"Po zamianie i=" << i << "\tj="<< j << endl; return 0; } void zamiana1(int* x, int* y) { int pomoc = *y; *y = *x; *x = pomoc; p=1234564 *p=7 &n - pobranie adresu miejsca, gdzie jest zmienna &n=1234564 (*wsk).dane!=*wsk.dane – np. lab 8 *x=&i
Przekazywanie argumentów przez adres Poprzedni program wydaje się być mało czytelny. Alternatywne rozwiązanie tego samego zadania wykorzystuje referencje zamiast wskaźników, dając prostszą i bardziej czytelną notację. #include <iostream> using namespace std; void zamiana2 (int& i, int& j ); int main() { int i = 10; int j = 20; zamiana2( i, j ); cout <<"Po zamianie i=" << i << "\tj="<< j << endl; return 0; } void zamiana2(int& x, int& y) { int pomoc = y; y = x; x = pomoc;
Wskaźniki do funkcji Tak jak nazwa tablicy, nazwa funkcji jest stałym wskaźnikiem. Ponieważ nazwa funkcji jest stałym wskaźnikiem, to wskaźnik do funkcji jest wskaźnikiem do stałego wskaźnika, który jest nazwą funkcji. int f(int); // deklaracja funkcji f int (*pf) (int); // deklaracja wskaźnika do funkcji typu int funkcja(int) pf = &f; // adres f przypisany jest do pf
Przykład #include <iostream> using namespace std; int suma(int (*) (int), int); // dwa parametry: wskaźnik do funkcji int square(int); int cube(int); main() { cout << suma(square,5) << endl; cout << suma(cube,5) << endl; } int suma(int (*pf)(int k), int n) // k nie jest używane ale wymagane int s = 0; for (int i =0; i <= n; i++) s += (*pf)(i); return s;
Przykład int square(int k) { return k*k; } int cube(int k) {return k*k*k;
Zapowiedź funkcji, zmienne globalne #include <iostream> #include <math.h> using namespace std; int a[10], n; float war; void wprowadz(); void wariancja(); main() { wprowadz(); wariancja(); cout <<"wariancja= "<<war; getchar(); } void wprowadz() cout<<"n="; cin>>n; for (int i=1; i<=n; i++) cout <<"a["<<i<<"]="; cin >>a[i]; //************************************************ float srednia() float s=0; s+=a[i]; return s/=n; void wariancja() float sr=srednia(); war=0; war=war+(a[i]-sr)*(a[i]-sr); war=sqrt(war/n);
Przeciążanie funkcji Funkcje można w C++ (ale nie w C) przeciążać (przeładowywać). Oznacza to sytuację, gdy w tym samym zakresie są widoczne różne definicje funkcji o tej samej nazwie. wywołania funkcji muszą się różnić, tak by można było jednoznacznie wybrać jedną z nich na podstawie wywołania.
Przeciążanie funkcji Sygnatury nie różnią się: Sygnatury różnią się: int f(int i); float f(int j); int f(int k;int l=0); Sygnatury różnią się: int f(float i); int f(int i; int j);
Przykład #include <iostream> #include <conio.h> using namespace std; int f(int i) { return i; } int f(float i) return 2*i; int main(int argc, char* argv[]) cout<<"Pierwsze wywolanie: "<<f(1)<<endl; cout<<"drugie wywolanie: "<<f((float)1); getch(); return 0;
Kiedy przeciążać funkcje Przeciążanie stosujemy wtedy, gdy funkcje realizują podobny algorytm, ale różny algorytm. Przykładem może być maksimum dla jednej, dwu i trzech liczb. #include <iostream> #include <conio.h> using namespace std; int max(int i) { return i;} int max(int i, int j){ return (i>j?i:j);} int max(int i, int j, int k){ return ((i>j)&&(i>j)?i:(j>k)?j:k);} int main(int argc, char* argv[]) { cout<<"max(1)="<<max(1)<<endl; cout<<"max(1,2)="<<max(1,2)<<endl; cout<<"max(1,2,3)="<<max(1,2,3)<<endl; getch(); return 0; }
Podsumowanie zagadnień związanych z funkcjami Co nam daje funkcja: Wydzielamy kod, Łatwiej zarządzać zmiennymi (funkcje korzystają ze zmiennych lokalnych), Budowa funkcji: Nagłówek funkcji, Ciało funkcji Kiedy używamy funkcji: Trudny problem, Powtarzalne wykorzystanie algorytmu, Jeśli coś wykonujemy, to funkcja zwraca wartość typu void, w przeciwnym razie (coś obliczamy), zwraca pewną wielkość o określonym typie.
Przykład pokazujący na łatwiejsze zarządzanie zmiennymi #include <iostream> #include <conio.h> using namespace std; void cos(int i) { int j=i*2; //zmienne i i j nie mają ZADNEGO związku ze zmiennymi i i j w funkcji głównej return; } int main(int argc, char* argv[]) int i, j; j=9; i=10; cos(2); j=i; i=i+2; //zmienne i i j z cos nie licza sie getch(); return 0;
Przykład pokazujący róznicę między funkcją typu void (instrukcja) i typu zdefiniowanego #include <iostream> #include <conio.h> using namespace std; void cos_rob(int i) { cout<<i; return; } int cos_innego(int i) int j=i*2; //zmienne i i j nie mają ZADNEGO związku ze zmiennymi i i j w funkcji głównej return j; int main() cos_rob(2);// złe iint i=cos_rob(2);i=cout<<…; int i=cos_innego(); //zle cos_innego(2);4; getch(); return 0;
Podsumowanie zagadnień związanych z funkcjami Bardzo ważnym elementem w projektowaniu funkcji jest określenie sposobu komunikacji funkcji z programem. Są cztery różne możliwości: Zmienne globalne, Przez wartość, Przez wskaźnik, Przez referencję.
Komunikacja przez zmienne globalne Wtedy, gdy mamy w programie bardzo ważne zmienne, z których korzystamy wszędzie, Wada – niebezpieczeństwo wystąpienia efektów ubocznych (ostatni przykład ale zmienne i i j są globalne), Zaleta – łatwy dostęp do danych, krótszy tekst programu. Mimo zalet zmienne globalne należy używać bardzo rzadko, najlepiej wcale.
Przekazywanie przez wartość Kiedy używamy: Gdy chcemy do funkcji przekazać wartość jakiejś danej, Czym jest parametr formalny: <typ> identyfikator Czym jest parametr aktualny: Wyrażeniem Przykłady: int f(int); f(1), f(1+1), f(i), f(2+i), f(f(8))
Przykład #include <iostream> #include <conio.h> using namespace std; //************************************************ float moja_potega(float x, int n) { float il=1.; for (int i=0;i<n;i++) il=il*x; return il; } main() cout <<"4^5="<<moja_potega(4,5)<<endl; float x=2.; int i=4; cout <<"2^4="<<moja_potega(x,i); getchar();
Przekazywanie danych przez referencję Kiedy używamy: Kiedy chcemy zmienić wartość zmiennej (najczęściej), Możemy także w ten sposób przekazywać dane (gdy tylko przekazujemy, to nie jest zalecane), Schemat przekazywania: Nagłówek funkcji: int f(int &n) Wywołanie: f(liczba) Niepoprawne wywołanie: f(i+4), f(5)
Obliczanie wariancji - przekazywanie danych przez referencje – uwaga na tablice #include <iostream> #include <conio.h> using namespace std; void wprowadz(int a[10], int &n) { cout<<"n="; cin>>n; for (int i=1; i<=n; i++) cout <<"a["<<i<<"]="; cin >>a[i]; } //************************************************* float srednia(int a[10], int n) float s=0; s+=a[i]; return s/n; //************************************************ //float wariancja(int *a, int n) void wariancja(int a[10], int n, float &war) float sr=srednia(a,n); war=0; war=war+(a[i]-sr)*(a[i]-sr); war=war/n; // return (war/n); //************************************************ main() { int n,a[10]; float war; wprowadz(a,n); wariancja(a,n,war); cout <<"wariancja= "<<war; //cout<<"wariancja="<<wariancja(a,n); getchar(); }
Przekazywanie danych przez wskaźniki Kiedy używamy: Kiedy chcemy zmienić wartość zmiennej (najczęściej), Możemy także w ten sposób przekazywać dane (gdy tylko przekazujemy, to nie jest zalecane), Schemat przekazywania: Nagłówek funkcji: int f(int *n) Wywołanie: f(&liczba) Niepoprawne wywołanie: f(i+4), f(5)
Obliczanie wariancji - przekazywanie danych przez wskaźniki– uwaga na tablice #include <iostream.h> #include <math.h> void wprowadz(int *a, int *n) { cout<<"n="; cin>>*n; for (int i=0; i<*n; i++) cout <<"a["<<i<<"]="; cin >>a[i]; } //************************************************ float wariancja(int *a, int n) float sr=0; for (int i=0; i<n; i++) sr+=a[i]; sr/=n; float war=0; war=war+(a[i]-sr)*(a[i]-sr); //war=sqrt(war/*n); return sqrt(war/n); main() { int n,a[10]; float war; wprowadz(a,&n); cout <<"wariancja= "<<wariancja(a,n); getchar();
Co jeszcze dają nam funkcje? Możliwość wykonywania różnych algorytmów dla danych (to, że wykonujemy TEN SAM algorytm dla różnych danych, to oczywiste), Przeciążania funkcji, Stosowania ustawień domyślnych (parametry domyślne).
W jaki sposób dokonywać podziału całego algorytmu na funkcje? (trochę już było)
Algorytm abstrakcyjny W momencie rozwiązywania złożonego problemu należy skupić się na wyrażeniu rozwiązania w kategoriach języka naturalnego. Takie rozwiązanie będzie nazywane algorytmem abstrakcyjnym. Wyraża on tylko ogólną strategię rozwiązania problemu wraz z ogólną strukturą rozwiązania, które chcemy otrzymać. Algorytm abstrakcyjny zawiera instrukcje abstrakcyjne i dane abstrakcyjne (tzn. takie, które nie mają swoich odpowiedników w danym języku programowania).
Metoda zstępująca Tworzenie programu polega więc na określaniu kolejnych uściśleń, tak aby w kolejnych krokach instrukcje i dane abstrakcyjne wyrażać w kategoriach wybranego języka programowania. Można więc powiedzieć, że kolejne uściślenia prowadzą do programu na niższym programie abstrakcji (instrukcje i dane abstrakcyjne za każdym razem są coraz bliższe danemu językowi programowania). Taki proces kończy się w momencie wyrażenia całego rozwiązania w wybranym języku programowania. Taki stopniowy rozkład problemu i jednoczesne uściślanie rozwiązania nosi nazwę metody zstępującej. Ważne jest, by wybrany język programowania pozwalał na zapis takiej drogi otrzymania rozwiązania.
Projektowanie metodą zstępującą Sam proces projektowania programu powinien uwzględniać możliwości (instrukcje danego j. programowania). Należy jednak używać możliwie najdłużej symboliki naturalnej dla danego problemu i odkładać jak najdłużej niezbędne decyzje zależne od szczegółów komputera i samego języka programowania. Dzięki temu zmiana języka lub komputera wymaga możliwie małych zmian. Także z pewnych błędnych rozwiązań można się wycofać stosunkowo łatwo (czasem może być potrzebny powrót do pierwszego szkicu rozwiązania).
Sprawdzanie poprawności Ważną cechą stopniowego uściślania programu jest to, że równolegle można sprawdzać poprawność tworzonego programu. Należy dowodzić poprawności każdej kolejnej wersji programu (oczywiście na pewnym poziomie abstrakcji). Dowody to nie są operacje w sensie matematycznym. Raczej zdroworozsądkowym, logicznym.
Sprawdzanie poprawności Na każdym kolejnym poziomie dowodu poprawności zakłada się, że dalsze uściślenia prowadzące do kolejnego niższego poziomu abstrakcji (bliższego danemu językowi programowania) zachowują odpowiednie kryteria poprawności. Oznacza to, że zaczynamy od pierwszego, najbardziej abstrakcyjnego etapu, i dowodzimy jego poprawności, zakładając, że instrukcje i operacje abstrakcyjne są uściślone poprawnie. Ponieważ programy abstrakcyjne są znacznie prostsze i krótsze więc dowody poprawności są również krótkie i mniej uciążliwe. W kolejnym etapie stosujemy to samo podejście do poszczególnych instrukcji abstrakcyjnych kończąc w chwili, gdy każdą instrukcję zapiszemy w wybranym języku programowania.
Przykład – szukamy liczb, których kwadraty są palindromami Def. Palindromem jest taki ciąg znaków, który tak samo czyta się od początku i od końca, np. 121, oko. Rozwiązanie: Uściślenie: przyjmiemy, że będziemy rozpatrywać pewien zakres liczb: 1..N. Zauważmy, także, że łatwiej jest sprawdzić, czy liczba jest palindromem, niż to, że jest kwadratem. Dlatego też problem sprowadzimy do sytuacji, gdy generujemy liczby, które są kwadratami liczb z przedziału 1..N, a następnie sprawdzamy, czy są one palindromami (można np. przeglądać wszystkie liczby będą kwadratami, a następnie sprawdzać, czy są one kwadratami i ew. wybierać palindromy).
Algorytm abstrakcyjny Najbardziej abstrakcyjny algorytm ma postać: Algorytm I n=0; powtarzaj n=n+1; generowanie kwadratu; if (dziesiętna reprezentacja kwadratu jest palindromem) pisz(n); tak długo jak n<>N;
Algorytm abstrakcyjny, cd Aby zbliżyć się do języka programowania zauważmy, że należy wprowadzić zmienne reprezentujące wynik jednego kroku obliczeń. Analiza programu pokazuje, że możemy mieć do czynienia z następującymi zmiennymi: Zmienna całkowita s, której przypiszemy kwadrat, zmienna tablicowa d dla reprezentacji dziesiętnej liczby s, zmienna boolowska p dla wyniku testu, czy liczba jest palindromem.
Algorytm abstrakcyjny, cd Uwzględniając te oznaczenia możemy Algorytm I zapisać w postaci: Algorytm II n=0; powtarzaj n=n+1; s=n*n; d=reprezentacja dziesiętna liczby s; p=d jest palindromem; if (p) pisz(n); tak długo jak n<>N;
Algorytm abstrakcyjny, cd Dalej należy rozważyć dwie abstrakcyjne instrukcje. Są to d=reprezentacja dziesiętna liczby s; p=d jest palindromem; Pierwszą z nich można zapisać następująco: d=reprezentacja dziesiętna liczy s d=reprezentacja dziesiętna liczby n2, a to oznacza, że np. n=7, mamy n2=49 i reprezentacja tej liczby jest postaci: d[1]=9, d[2]=4, gdzie L=2.
Algorytm abstrakcyjny, cd Drugą z nich możemy następująco zinterpretować: ponieważ aby instrukcja p:=d jest palindromem powinna zachodzić następująca relacja:
Algorytm abstrakcyjny, cd Podane wcześniej instrukcje d=reprezentacja dziesiętna liczby s; p=d jest palindromem; uściśla się za pomocą instrukcji: oraz
Algorytm abstrakcyjny, cd Pierwszą z nich uściśla się następująco: Algorytm IVa L=0; do { L=L+1; d[L]=s % 10; s=s / 10; } while (s!=0);
Algorytm abstrakcyjny, cd Uściślimy teraz instrukcję „p=d jest palindromem”, można ją zapisać następująco: i=1; j=L; do { p=(d[i]==d[j]); i=i+1; j=j-1; }while ((i<j) && p)
Podsumowanie - cały program (zapis miał być z założenia bardzo podobny do języka C/C++ ale nie koniecznie taki, by kompilacja była poprawna – pseudokod) n=0; do { n=n+1; s=n*n; L=0; // wyznaczenie d (reprezentacji dziesiętnej) L=L+1;d[L]=s %10; s=s / 10; }while (s!=0); i=1; j=L; // czy d jest palindormem? do { p=(d[i]==d[j]); i=i+1; j=j-1; } while ((i<j) && p) if (p) pisz(n); } while (n!=N);
Przykład Sortowania tablic Rozważamy porządek sortowania malejący. Sortowanie przez wybór. Dana jest tablica A z granicami indeksu 0..N-1 i składowymi skalarnymi. Zadanie – poprzestawiać elementy tablicy tak, by na końcu zachodziło: A[0]<=A[2]<=...<=A[N-1] - otrzymana tablica w takiej sytuacji będzie posortowana.
Przykład Sortowania tablic Idea algorytmu polega na tym, by tablicę A podzielić na części A[0..i-1] złożona z elementów A[0], ... A[i-1] i część A[i..N-1] złożoną z elementów A[i],..., A[N-1]. Na początku część A[0..i-1] jest pusta. Dla pierwszego kroku część A[0..i-1] jest posortowana i w każdym kroku tę część rozszerza się o kolejny jeden element. Na końcu algorytmu część A[0..N-1] jest posortowana.
Przykład Sortowania tablic - cd Początkowy szkic programu można przedstawić następująco: for(i=0; i<N; i++) Przestawienie wartości A[i..N] umieszczające najmniejszą wartość w A[i] Po wykonaniu tej pętli tablica A[0..N-1] ma być posortowana, tzn. musi zachodzić: p,q ((0<=p<q<=N-1)=>(A[p]<=A[q]) – to jest stan oczekiwany.
Przykład Sortowania tablic - cd Uściślimy teraz instrukcję przestawienie wartości A[i..N-1] umieszczające najmniejszą w A[i] tak, by rzeczywiście przestawiała elementy. Można to zapisać za pomocą następujących instrukcji: m=i; for(j=i+1;j<N;j++) if (A[i]<A[m]) m=j Zamiana A[i] i A[m]
Przykład Sortowania tablic - cd Natomiast instrukcję Zamiana A[i] i A[m] Możemy zapisać następująco: t=A[i]; A[i]=A[m]; A[m]=t;
Program (zasadnicza część) for (i=0; i<N; i++) { m=i; for (j=i+1; j<N; j++) if (A[i]<A[m]) { m=j; t=A[i]; A[i]=A[m]; A[m]=t; }
Uwagi dotyczące zasad wyboru funkcji Funkcja powinna być „logiczna” w tym sensie, że powinna wykonywać pewne, ściśle określone zadanie, powinno ono wynikać z algorytmu, Funkcja powinna wykonywać wszystkie operacje powiązane ze sobą logicznie, Funkcja powinna komunikować się za pomocą parametrów (zmienne globalne używamy w ostateczności, a najlepiej wcale), Należy dokładnie rozważyć, co funkcja robi i jak się komunikuje – jakie dane potrzebuje, co powinna obliczać: Gdy potrzebuje danych – przekazywanie przez wartość, Gdy oblicza parametr – przez wskaźnik i przez referencję, Gdy oblicza jedną wartość, to używamy return, Gdy obliczamy dwie lub więcej wartości, to obliczane wielkości przekazujemy przez parametry.
Uwagi dotyczące metody zstępującej i wstępującej Metoda zstępująca (od ogółu do szczegółu): Wtedy, gdy znamy całe zagadnienie, dla którego piszemy program, Dzielimy całe (duże) zagadnienie na mniejsze podzagadnienia, Mniejsze podzagadnienia dzielimy na jeszcze mniejsze, Iteracje powtarzamy tak długo, aż wszystkie operacje będą mogły być zapisane za pomocą elementów danego języka.
Przykłady problemów, które możemy projektować z wykorzystaniem metody zstępującej Są to problemy, które są w sposób zamknięty zdefiniowane. Czyli są to np. problemy opisane za pomocą odpowiednich przepisów prawnych, wynikają ze znanych procedur obowiązujących w firmie itd., Przykładem takich problemów mogą być systemy płacowe, kadrowe, finansowe.
Przykłady problemów, które możemy projektować z wykorzystaniem metody wstępującej Są to problemy, które nie są zdefiniowane w sposób ściśle określony, Czyli w trakcie eksploatacji mogą pojawiać się nowe potrzeby, które nie dyskwalifikują naszego rozwiązania, Przykładem takich problemów mogą być np. aplikacje typu system operacyjny, kalkulator.