Paradygmaty Sematyka Zmiennych, Składnia i semantyka


Paradygmaty - semantyka zmiennych

Imperatywne języki programowania oferują abstrakcyjne mechanizmy na bazie architektury von Neumanna. Dwa podstawowe składniki tej architektury to procesor i pamięć. Określenie „abstrakcyjne mechanizmy” oznacza, że mamy do dyspozycji narzędzia pozwalające wykorzystać możliwości komputera bez zagłębiania się w szczegóły techniczne. Jednym z takich mechanizmów są zmienne. Stanowią one abstrakcję komórek pamięci: programista może przechowywać dane w pamięci, nie martwiąc się o techniczne szczegóły (np. przydział pamięci). Odpowiedniość między zmiennymi a komórkami pamięci może być bardzo bezpośrednia (np. dla zmiennych typu całkowitego) lub dość odległa (np. wielowymiarowe tablice). Każdą zmienną można jednak scharakteryzować za pomocą sześciu atrybutów [3]:

Zajmiemy się teraz po kolei wspomnianymi cechami, kładąc nacisk na sprawy mniej oczywiste (np. statyczny i dynamiczny zakres widoczności); kwestie oczywiste omówimy zdawkowo na początku wykładu.

Nazwa jest cechą rozmaitych bytów, nie tylko zmiennych. To, co musimy ustalić w odniesieniu do nazw, to technikalia nie mające większego związku z przyjętym paradygmatem programowania. A więc [3]:

Zmienne

Przypomnijmy najważniejsze informacje. Zmienna to w języku programowania abstrakcja komórek pamięci. Każda zmienna ma sześć istotnych atrybutów: nazwę, adres, wartość, typ, okres życia i zakres widoczności.

Nazwy [3]:

Adresy [3]:

Teraz na przykładzie języka C++ omówimy wybrane zagadnienia związane z semantyką zmiennych.

Zmienne i stałe oraz typy danych

Reprezentacje informacji w dziedzinie komputerów nazywane są danymi. Program musi mieć możliwość przechowywania danych, na których wykonuje określone operacje (zgodnie z algorytmem).

Dzięki zmiennym i stałym istnieje możliwość reprezentowania, przechowywania i przetwarzania (manipulowania) danych. Zmienne to jedno z podstawowych pojęć związanych z programowaniem.

Język musi zarezerwować odpowiednio duży obszar pamięci do przechowywania każdego elementu danej (dana ma określoną strukturę - stąd używa się określenia struktury danych). W językach programowania w tym w języku C++ zmienna służy do przechowywania danych (reprezentacji informacji).

Jest to zaadresowane miejsce w pamięci operacyjnej (ulotnej) komputera, do którego zapisuje (umieszcza) się wartość, i z którego można odczytać zawartość (pobrać wartość) zmiennej.

W zmiennej przechowuje się wartość tymczasowo (wyłączenie komputera powoduje utratę przechowywanych danych w pamięci operacyjnej). Zwykle dane są przechowywane w sposób trwały, dzięki zapisaniu ich w pliku na dysku lub w bazie danych [6].

Pamięć operacyjną komputera można potraktować jako bez drzwiową szafę z ponumerowanymi półkami (położonych jedna nad drugą). Każda półka - komórka (miejsce, np. o organizacji bajtowej) pamięci - ma swój numer (adres). Zmienna rezerwuje zestaw (jedną lub więcej) komórek, w których może przechowywać odpowiednią wartość. Nazwa zmiennej (symboliczny adres) stanowi etykietkę takiego wydzielonego zestawu komórek w pamięci danych.

Dzięki nazwie zmiennej można taki zestaw komórek zlokalizować - bez znajomości jego rzeczywistego adresu.

Pamięć operacyjną można dzielić na pamięć programu i pamięć danych. Różne typy danych w danym komputerze zajmują różne, ale stałe, co do wielkości obszary pamięci (zestawy komórek, np. określone liczby bajtów - przy organizacji bajtowej pamięci). Przykładowy program (poniżej) wywołujący funkcję sizeof(), która umożliwia sprawdzenie rozmiarów poszczególnych typów danych ujętych w nawiasy jako parametr tej funkcji [5, 6].

W linii 1 polecenie #include <iostream> jest instrukcją preprocesora, informującą go: „Po mnie następuje nazwa pliku. Znajdź go i wstaw go w to miejsce”.

W tym programie użyto nowy operator (funkcję) sizeof() w liniach od 9 do 18 (bez linii 12, 14 i 17). Operator ten jest dostarczany z kompilatorem (zwraca rozmiar zmiennej określonego typu przekazanego mu jako parametr).

0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

// Program umożliwiający sprawdzenie wybranych rozmiarow typow zmiennych

#include <iostream> // dyrektywa PREPROCESORA
// w bibliotece standardowej C++ stosowana jest przestrzeń nazw standardowych
std
int main() // naglowek funkcji main()
{ // poczatek funkcji main()
using std::cout; // informacja, że będzie uzywany obiekt cout z biblioteki std

// polecenie cout wyprowadza dane w C++, argumenty sa oddzielane znakami <<

// dalej beda wyswietlane kolejno: opis typu, rozmiar (liczba bajtów), tekst i nowa linia

// znak formatujący /t - wstawienie znaku tabulacji w celu wyrównania wyników

cout << ”Rozmiar zmiennej typu int to: \t\t” << sizeof(int) << ” bajty. \n”;
cout << ”Rozmiar zmiennej typu short int to: \t\t” << sizeof(short) << ” bajty. \n”;

cout << ”Rozmiar zmiennej typu long int to: \t\t” << sizeof(long) << ” bajty. \n”;

cout << ”Rozmiar zmiennej typu char to: \t\t” << sizeof(char) << ” bajt. \n”;

cout << ”Rozmiar zmiennej typu float to: \t\t” << sizeof(float) << ” bajty. \n”;

cout << ”Rozmiar zmiennej typu double to: \t\t” << sizeof(double) << ” bajtow. \n”;

cout << ”Rozmiar zmiennej typu bool int to: \t\t” << sizeof(bool) << ” bajt. \n”;

system (”PAUSE”); // powstrzymanie zamkniecia okienka konsoli

return 0; // funkcja main() zwraca wartość 0

} // zakończenie funkcji main()

Przykładowo, zmienna typu int może rezerwować w jednym komputerze 2 bajty, w innym 4 (w danym komputerze ma zawsze ten sam rozmiar).

Zmienna typu char (przechowuje znaki alfabetu) ma najczęściej rozmiar 1 bajtu.

Zmienna typu short int rezerwuje (w większości komputerów) 2 bajty, zaś long int rezerwuje 4 bajty, a zmienna typu int rezerwuje 2 lub 4 bajty.

Ustalono, że typ short int musi mieć rozmiar mniejszy lub równy typowi int, który z kolei musi mieć rozmiar mniejszy lub równy typowi long int. (Najczęściej typ short int ma 2 bajty, zaś typy int i long int mają po 4 bajty.

Znak jest pojedynczą literą (w tym znak _), cyfrą (0 - 9) lub symbolem (np.: znaki interpunkcji, +, -, <, >, = itd.) i zajmuje 1 bajt.

W tym podrozdziale zostaną opisane sposoby deklarowania i definiowania zmiennych i stałych oraz sposoby przypisywania wartości zmiennym.

Program musi odpowiednio reprezentować dane, na których wykonywane są określone operacje - po jego uruchomieniu. Zmienne i stałe umożliwiają pracę z wartościami dowolnego typu, a w szczególności: liczbami, tekstami i wieloma innymi bardziej lub mniej złożonymi strukturami danych. Zatem zmienne pozwalają na przechowywanie w programie danych. Z programistycznego punktu widzenia zmienna to wybrany obszar (komórki pamięci) w pamięci komputera, w którym przechowuje się dane potrzebne do działania aplikacji i z którego można wartość tą pobrać (odczytać). Każda zmienna posiada przede wszystkim swój adres i typ (ponadto posiada: nazwę, wartość, czas życia, zakres widoczności). Jednak dla wygody w programie zmienne są identyfikowane nie poprzez ich adresy, lecz poprzez nazwy. Nazwa zmiennej to etykieta określonego obszaru pamięci (tzw. pamięci danych). Dzięki temu można używać takiej nazwy (adres symboliczny), zamiast adresu.

Gdy definiuje się zmienną w C++ (tutaj występują te same typy danych, co w języku C), to oprócz jej nazwy należy podać, jakiego typu wartość ma ona przechowywać (np.: liczby całkowite, liczby rzeczywiste, łańcuchy znaków itd.). A zatem typ zmiennej określa, jakiego rodzaju dane może ona przechowywać. Na przykład, jeśli chce się przechowywać liczby całkowite, to należy powołać do życia (zadeklarować lub zdefiniować) zmienną o odpowiednim typie pozwalającym na przypisanie jej liczb całkowitych.

Jeśli chce się przechowywać liczby ułamkowe (rzeczywiste: ułamki właściwe i niewłaściwe), to należy użyć zmiennej pozwalającej na przechowywanie liczb ułamkowych, i tak dalej.

Typ zmiennej to informacja dla kompilatora, jaki obszar pamięci ma zarezerwować na wartość przechowywaną przez daną zmienną.

Uwaga: Podczas uruchamiania jakiegoś programu jest on ładowany z pliku (zapisanym na nośniku np.: HDD, FDD, CDROM itp.) do pamięci operacyjnej komputera (RAM w tzw. pamięci programu) i również w tej pamięci przechowywane są powołane do wykonania zadania (określonego algorytmem) wszystkie zmienne (pamięć danych).

Zanim zostanie użyta jakaś nazwa w C++ (dotyczy to każdej nazwy), musi być wcześniej zadeklarowana. Zatem, deklaracje dotyczące zmiennych wprowadzają nazwy odnoszące się do danych (mogą one wystąpić w dowolnym miejscu w bloku, nie tylko na początku).

Deklaruje się, że opisuje ona obiekt określonego typu (całkowitego -int, zmiennoprzecinkowego - float,...) - jest to informacja dla kompilatora, jak ma postępować w przypadku napotkania takiej nazwy. Na przykład, deklaracja int b; informuje kompilator, że napotkana nazwa b jest zmienną typu int.

Taka deklaracja jest jednocześnie definicją, co oznacza, że poza informacją dla kompilatora, co oznacza nazwa b, żąda się by kompilator zarezerwował odpowiedni obszar w pamięci na zmienną b (powołał ją do życia).

Może się zdarzyć, że taka zmienna już istnieje gdzieś w programie, a chce się tylko przekazać do kompilatora informację o jej typie.

Na przykład konstrukcja składniowa, extern int b; informuje kompilator, że obiekt typu int o nazwie b już jest powołany do życia (np. na zewnątrz pliku, którym zajmuje się kompilator, lub może oznaczać to, że ta zmienna występuje w jakiejś funkcji bibliotecznej, którą dołącza się później na etapie linkowania).

Różnica między deklaracją a definicją polega na tym, że:

Istotą programowania obiektowego jest projektowanie i rozbudowywanie własnych typów danych, które pasują do danych. Wbudowane typy danych w C++ należą do dwóch kategorii: typy podstawowe (np.: int, float) i typy złożone (oparte na typach podstawowych np.: tablice, łańcuchy, wskaźniki). Program musi zabezpieczyć możliwość odwoływania się do przechowywanych danych. Aby zapisać w pamięci komputera jakąś daną, należy zapamiętać jej trzy podstawowe cechy:

Zwykle deklaruje się potrzebne zmienne. Typ użyty w deklaracji decyduje o rodzaju reprezentacji informacji, a jej nazwa symbolicznie opisuje wartość.

W C++ zalecanymi nazwami zmiennych są zmienne, których nazwy spełniają następujące zasady [12]:

Przykładowe deklaracje zmiennych w C++:

int Licznik; // poprawna deklaracja

int licznik; // poprawna deklaracja innej zmiennej niż Licznik

int 5Licznik; // niepoprawna deklaracja (zaczyna się od cyfry 5)

int double; // niepoprawna deklaracja (double jest słowem kluczowym)

int begin; // poprawna deklaracja (begin nie jest słowem kluczowym) int m-zmienna; // niepoprawna deklaracja (nie można stosować znaku -)

short wynik; // poprawna deklaracja zmiennej całkowitej typu short

long liczbaOsob // poprawna deklaracja zmiennej całkowitej typu long

char litera // poprawna deklaracja zmiennej znakowej typu char

Typy całkowitoliczbowe różnią się ilością miejsca (liczbą bitów przeznaczonych do reprezentacji liczby w pamięci komputera) przeznaczonego na zapis wartości.

Podstawowe typy całkowitoliczbowe uporządkowane według szerokości (liczby bitów na reprezentację), to short, int, long. Każdy ma odmianę bez znaku i ze znakiem.

Korzystając z różnej liczby bitów do reprezentacji wartości, typy short, int i long pozwalają zapisywać liczby całkowite o różnej szerokości (wielkości).

Język C++ jest dość elastycznym standardem i określa tylko gwarantowaną wielkość minimalną (analogicznie jak w C) [12]:

Wiele obecnie używanych systemów ma typy całkowite ustalone na minimalnym poziomie, tzn. short - 16 bitów, int - 16 bitów, 24 bity lub 32 bity, long - 32 bity. Niektóre implementacje C++ pozwalają ustawiać wielkości liczb typu int [12].

Zatem, program w C++ musi pamiętać: gdzie są przechowywane dane, jaka wartość jest przechowywana i jakiego typu są dane. Deklaracja zmiennych prostych opisuje typ i nazwę symboliczną wartości; wystarcza to programowi do zaalokowania pamięci na wartość i do wewnętrznego zapamiętania położenia tej danej.

Program obiektowy różni się od programu proceduralnego tym, że w programie obiektowym decyzje podejmowane są głównie na etapie (w czasie) wykonywania programu (program jest uruchomiony, decyzje podejmowane zapewniają elastyczność stosownie do okoliczności - np. decyzja o wielkości tablicy), a nie na etapie kompilacji (kompilator zestawia program w całość).

W języku C++ istnieje druga metoda przechowywania danych, która jest niezwykle istotna w przypadku tworzenia klas.

Strategia ta opiera się na wskaźnikach zmiennych (nie przechowują wartości zmiennych), które przechowują jedynie adresy tych zmiennych (tzn. zawartością wskaźników są adresy tych zmiennych).

Aby określić adres zwykłej zmiennej, wystarczy zastosować operator adresu, &, do zmiennej (np. dla zmiennej MojaZmienna, wyrażenie &MojaZmienna oznacza adres tej zmiennej).

Zastosowanie tego operatora można zobaczyć w programie adres.cpp [12]:

0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

19

20

// Program adres.cpp ilustruje użycie operatora & do wyznaczenia adresu zmiennych

#include <iostream> // dyrektywa PREPROCESORA
// w bibliotece standardowej C++ stosowana jest przestrzeń nazw standardowych
std


int main() // naglowek funkcji main()
{ // poczatek funkcji main()
using namespace std; // informacja o uzywaniu przestrzeni nazw standardowych std

int MzmiennaCalk = 5;

double MzmiennaReal = 3.5;

// polecenie cout wyprowadza dane w C++, argumenty sa oddzielane znakami <<

// tutaj cout wypr.: tekst (Wart. zm. typu =), tabul., zawart. zm., tekst, adres zm., nowa linia

cout << ”Wartosc zmiennej typu int = \t” << MzmiennaCalk;

cout << ”, a adres zmiennej MzmiennaCalk = \t” << &MzmiennaCalk << endl;

//Uwaga: moze okazac się koniecznym uzycie unsigned(&MzmiennaCalk)

// oraz uzycie unsigned(&MzmiennaReal), ze wzgledu na klopoty ze zgodnością

// implementacji ze standardem C++

cout << ”Wartosc zmiennej MzmiennaReal typu double = \t” << MzmiennaReal;

cout << ”, a adres zmiennej MzmiennaReal = \t” << &MzmiennaReal << endl;

system (”PAUSE”); // powstrzymanie zamkniecia okienka konsoli

return 0; // funkcja main() zwraca wartość 0

} // zakończenie funkcji main()

W linii 1 polecenie #include <iostream> jest instrukcją preprocesora, informującą go: „Po mnie następuje nazwa pliku. Znajdź go i wstaw go w to miejsce”.

Program obiektowy tym się różni od programu proceduralnego, że w programie obiektowym decyzje podejmowane są głównie na etapie wykonywania programu (czas, kiedy program jest uruchomiony), a nie w okresie kompilacji (okres, kiedy kompilator zestawia program w całość) [12].

Nowa strategia obsługi danych jest skoncentrowana na traktowaniu nazw jako nazwanych wielkości, a wartości są ich pochodnymi. Specjalny rodzaj zmiennej - wskaźnik - zawiera adres wartości, więc nazwa wskaźnika opisuje położenie wartości.

Zastosowanie operatora *, nazywanego operatorem dereferencji, pozwala pobrać wartość umieszczoną pod zadanym adresem. Na przykład, gdy specjalna zmienna Mwskaznik jest wskaźnikiem, to zawiera ona adres, a *Mwskaznik zawiera wartość umieszczoną pod tym adresem. Zatem, jeśli Mwskaznik jest wskaźnikiem wartości typu int, zapis *Mwskaznik staje się równoważny zwykłej zmiennej typu int.

Zmienna Mzmienna typu int (to wartość całkowita) oraz zmienna wskaźnikowa p_Mzmienna to dwa spojrzenia na ten sam byt. Zatem, zmienna o nazwie Mzmienna to przede wszystkim wartość, ale za pomocą operatora & można pobrać adres tej zmiennej.

Zmienna wskaźnikowa p_Mzmienna to przede wszystkim adres, ale za pomocą operatora * można pobrać wskazywaną przez tą zmienną wskaźnikową wartość.

Zmienna wskaźnikowa p_Mzmienna wskazuje zmienną Mzmienna typu int, zatem *p_Mzmienna i Mzmienna są równoważne pod każdym względem. Wyrażenia *p_Mzmienna można użyć wszędzie tam, gdzie normalnie używa się zmiennej typu int.

W programie zm_wskaznikowa.cpp zilustrowano deklarację wskaźnika i użycie operatorów: & i * [12].

0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

19

20

21

// Program zm_wskaznikowa.cpp używa zmienną wskaźnikową i operatory: & i *

#include <iostream> // dyrektywa PREPROCESORA
// w bibliotece standardowej C++ stosowana jest przestrzeń nazw standardowych
std
int main() // naglowek funkcji main()
{ // poczatek funkcji main()
using namespace std; // informacja o uzywaniu przestrzeni nazw standardowych

int MzmCalk = 5; // deklaracja zmiennej MzmCalk typu int i przypisanie jej wartości 5

int *p_MzmCalk; // deklaracja wskaznika p_MzmCalk na zmienna MzmCalk typu int

p_MzmCalk = &MzmCalk; //przypis. wskaz. p_MzmCalk adresu zmiennej MzmCalk

// polecenie cout wyprowadza dane w C++, argumenty sa oddzielane znakami <<

//Pokazanie wartości dwoma sposobami

cout << ”Wartosc zmiennej MzmCalk = \t” << MzmCalk;

cout << ”, a watosc wskazana wyrażeniem *p_MzmCalk = \t” << *p_MzmCalk << ”/n";

//Pokazanie adresu dwoma sposobami

cout << ”Adres zm. MzmCalk okresla &MzmCalk = \t” << &MzmCalk << endl;

cout << ”, a adres zapisany we wskazniku p_MzmCalk = \t” << p_MzmCalk << endl;

//Ilustracja uzycia wskaznika do zmiany wartosci wskazywanej zmiennej MzmCalk

*p_MzmCalk = *p_MzmCalk +2;

cout << ”Po operacji (linia 17) MzmCalk = ” << MzmCalk << endl;

system (”PAUSE”); // powstrzymanie zamkniecia okienka konsoli

return 0; // funkcja main() zwraca wartość 0

} // zakończenie funkcji main()

W linii 1 polecenie #include <iostream> jest instrukcją preprocesora, informującą go: „Po mnie następuje nazwa pliku. Znajdź go i wstaw go w to miejsce”.

Efektem wykonania tego programu są następujące wyniki:

Wartosc zmiennej MzmCalk = 5, a watosc wskazana *p_MzmCalk = 5

Adres zm. MzmCalk okresla &MzmCalk = 0x0065fd48,

a adres zapisany we wskazniku p_MzmCalk = 0x0065fd48

Po operacji (linia 17.) MzmCalk = 7

Typy występujące w języku C++ można podzielić na następujące główne rodzaje: typy arytmetyczne, typy logiczne, typy specjalne. Typy arytmetyczne to typy: znakowe, całkowite oraz zmiennopozycyjne, natomiast typy specjalne to: typ void, typy wskaźnikowe oraz typy referencyjne.

Schematycznie obrazuje to rysunek 1 [1]:

0x01 graphic

Rys. 1. Podział typów danych [1]

Podział ten jest umowny. Rodzina typów specjalnych zostanie opisana w dalszej części wykładu. Każda zmienna, zanim się zacznie z niej korzystać, musi zostać wcześniej zadeklarowana.

Deklaracja polega na podaniu typu oraz nazwy zmiennej. Np. można rozważyć fragment kodu [1]:

int main()
{
int liczba;
}

Została tu zadeklarowana zmienna typu int o nazwie liczba. Oznacza to, że będzie ona mogła przechowywać liczby całkowite. Deklaracja kończy się znakiem średnika, tak jak każda inna instrukcja programu.

Raz zadeklarowaną zmienną można używać w całym programie, w szczególności można przypisać jej jakąś wartość. Przypisania dokonuje się wykorzystując znak równości.

int main()
{
      int liczba;
      liczba = 100;
}

Takie pierwsze przypisanie nazywa się inicjowaniem zmiennej.

Aby przekonać się, że istotnie zmiennej liczba została przypisana wartość 100, najprościej będzie zlecić wyświetlenie jej zawartości na ekranie. Można do tego wykorzystać standardowy strumień wyjściowy cout  oraz operator <<.

Podobnie można postąpić w tym przypadku [1]:

0

1
2
3
4
5
6
7
8
9
10
11
12
13
14

15

// Program projekt1.cpp ilustruje przypisanie zmiennej liczba wartości 100

#include <cstdlib> // prototypy funkcji biblioteki standardowej
#include <iostream> // dyrektywa PREPROCESORA
// w bibliotece standardowej C++ stosowana jest przestrzeń nazw
std
using namespace std; // deklaracja umożliwiająca używanie nazw z przestrzeni nazw std

int main() // naglowek funkcji main()
{ // poczatek funkcji main()
int liczba; // deklaracja zmiennej typu int o nazwie liczba
liczba = 100; // przypisanie zmiennej liczba wartość 100
cout << liczba << ”\n”; // wyświetla zawartosc zmiennej liczba, zmiana wiersza
// polecenie
cout wyprowadza dane w C++, argumenty są oddzielane znakami <<
system("PAUSE"); // przekazuje polecenie PAUSE do systemu operac. (język C)
return 0; // funkcja main() zwraca wartość 0
} // zakonczenie funkcji main()

W linii 1 polecenie #include <iostream> jest instrukcją preprocesora, informującą go: „Po mnie następuje nazwa pliku. Znajdź go i wstaw go w to miejsce”.

W linii 11znajduje się instrukcja będącą poleceniem: wyślij do standardowego strumienia wyjściowego zawartość zmiennej liczba.

Innymi słowy: wyświetl na ekranie jej zawartość. Można przekonać się, że faktycznie wykonanie tego programu spowoduje wyświetlenie na ekranie liczby 100.

W poprzednio przedstawionych przykładach opisano szczególną funkcję main(). Jej szczególność polega na tym, że uruchomienie programu powoduje jej automatyczne wywołanie (inne funkcje trzeba samodzielnie wywołać w kodzie programu). Program jest wykonywany jest - linia za linią.

Każde wywołanie funkcji skutkuje wstrzymaniem wykonywania sekwencji instrukcji i skok do miejsca w programie, gdzie znajduje się wywołana funkcja. Po zakończeniu wykonywania sekwencji (bloku) instrukcji stanowiących ciało funkcji - sterowanie jest przekazywane do miejsca wywołania tej funkcji (w programie wywołującym tą funkcję).

Funkcje zwracają wartość lub nie zwracają żadnej wartości w zależności od ich parametru (funkcja typu void; oznacza to, że funkcja ta nie zwraca żadnej wartości).

Uwaga: funkcja main() zawsze zwraca wartość typu int.

Przykładowo funkcja wykonująca dodawanie dwóch liczb całkowitych (typu int) daje sumę - też liczbę całkowitą i powinna zwracać wartość typu int.

Natomiast funkcja, która ma za zadanie wyświetlenie na ekranie komunikatu (ciągu znaków ujętych w cudzysłów) nie zwraca żadnej wartości.

Oznacz to, że jest ona typu void. Każda funkcja musi posiadać swój nagłówek i treść (ciało). Nagłówek funkcji określa (zawiera informacje o) typ wartości zwracanej, nazwę funkcji i jej parametry. Parametry umożliwiają przekazanie wartości do funkcji. W opisywanej funkcji (dodawanie liczb całkowitych) parametrami są dwie zmienne typu int, natomiast nagłówek tej funkcji może być następujący [5]:

int Suma(int a, int b);

Typ parametrów a i b przekazuje informację o typie wartości przekazywanej do funkcji.

Konkretne wartości parametrów a i b przekazywane do funkcji są jej argumentami.

Nazwa funkcji i jej parametry (bez typu wartości zwracanej przez funkcję) nazywa się sygnaturą funkcji.

Ciało funkcji (jej treść) rozpoczyna się od otwierającego nawiasu klamrowego ({), instrukcji wewnętrznych i zamykającego nawiasu klamrowego. Tutaj zwrócenie wartości i zakończenie funkcji jest realizowane za pomocą polecenia return (brak tego polecenia w treści funkcji powoduje zwrócenie typu void). Zawsze typ zwracanej wartości musi być zgodny z typem zadeklarowanym w nagłówku funkcji. Przykładowy program dodaj.cpp zawierający funkcję Dodaj() może wyglądać następująco [5].

W linii 1 polecenie #include <iostream> jest instrukcją preprocesora, informującą go: „Po mnie następuje nazwa pliku. Znajdź go i wstaw go w to miejsce”. W liniach 3 - 8 zdefiniowano funkcję Dodaj(). Jest to funkcja dwuparametrowa (pobiera dwa parametry a i b) typu int i zwraca wartość też typu int. Początek programu głównego (nagłowek funkcji main() - linia 10). Polecenie wyświetlenia komunikatu o rozpoczęciu programu) jest w linii 12.

W trakcie wykonywania programu pojawi się prośba o podanie dwóch liczb całkowitych (linie 18 - 22). W linii 25 funkcja main() przekazuje wprowadzone z klawiatury wartości parametrów aktualnych a i b (w linii 20 i 21 - wypełnienie zmiennych a i b z klawiatury) do funkcji Dodaj().

Tutaj sterowanie z programu głównego przechodzi do funkcji Dodaj(). Na ekranie są wyświetlone wartości parametrów przekazywanych do funkcji Dodaj(). W linii 7 zwracany jest wynik działania funkcji (suma wartości parametrów aktualnych).

W linii 18 i 19 wykorzystano obiekt cin, który umożliwia wprowadzanie wartości danych (zmiennych a i b) z klawiatury.

Obiekt obserwuje klawiaturę i rejestruje wciskane klawisze (np. konstrukcja:

cin >> c

oznacza, że program oczekuje na wczytanie do zmiennej c wartości z klawiatury).

W linii 29 jest instrukcja, która umożliwia powstrzymanie okienka konsoli przed zbyt szybkim zamknięciem.

0

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

18

20

21

22

23

24

25

26

27

28

29

30

31

//Program Dodaj.cpp zawierajacy funkcję Dodaj()

#include <iostream> // dyrektywa PREPROCESORA

using namespace std; // deklaracja aby uzywac nazw z przestrzeni nazw std
int Dodaj(int x, int y) // nagłowek funk.: typ funkcji Dodaj(typy i nazwy parametrow)
{ // poczatek treści funkcji Dodaj()
// wyświetla: tekst ujęty w cudzyslow, wartości argumentow i zmiane linii
cout <<
"Funkcja Dodaj(), argumenty funkcji: " << x << " i " << y << "\n ";
return (x + y); // zakoncz. funkcji - zwracanie sumy z wartosci parametrow (x + y)
} // koniec tresci funkcji Dodaj()

int main() // nagłówek funkcji main()

{ // poczatek tresci funkcji main()
cout << "Funkcja main() ! \n "; // wyświetlenie tekstu i zmiana linii
int a, b, c; // deklaracja trzech zmiennych typu int: a, b, c

// obiekt cin steruje danymi wejść. z bufora strumieniowego związan. ze strumien. C stdin

//, aby użyć cin dolacza się plik nagłówkowy <iostream>; typem cin jest istream

// cin obsluguje wprowadzanie roznych typow danych z klawiatury; posiada on

// przeciążony operator ekstrakcji (>>) - efektem jest wypełnienie określonej zmiennej

cout << "Wprowadz dwie liczby calkowite: " // wyswietla tekst w cudzyslowie

cin >> a; // wypelnienie zmiennej a z bufora cin
cin >> b; // wypelnienie zmiennej b z bufora cin
cout << ”\t” <<
a << ”\t” << b << ”\n”; // wyswietla: znak tabulacji., zawartosc zm.

// a, znak tabulacji, zawartosc zm. b, zmiana linii

   cout << "Wywolanie funkcji Dodaj() \n"; // wyswietla tekst i zmienia linie
   c = Dodaj(a, b); // przypis. zm. c zwracana wart. funkcji Dodaj(), dla par. aktual. a i b
   cout << "Ponownie w main() . \n"; // wyswietla: zmiane linii, tekst, zmiane linii
   cout << "c ma teraz wartosc " << c; //wyswietla tekst w cudzysl. i zawartosc zm. c
   cout << "\n Koniec programu... \n"; // wyswietla: zmiane linii, tekst, zmiane linii

system (”PAUSE”); // powstrzymanie zamkniecia okienka konsoli

      return 0; // funkcja main() zwraca 0
} // koniec funkcji main()

Wynik wykonania programu jest następujący [5]:

Funkcja main() !

Wprowadz dwie liczby calkowite: 3 5

Wywołanie funkcji Dodaj()

Funkcja Dodaj(), argumenty funkcji: 3 i 5

Ponownie w main() .

c ma wartosc 8

Koniec programu...

Funkcje to moduły, z których składają się programy C++, ponadto są one kluczowe dla definicji obiektowych języka C++. Funkcje w tym języku mają dwie odmiany:

Przykłady obu rodzajów funkcji można odnaleźć w standardowej bibliotece funkcji. Ponadto można budować nowe własne funkcje obu tych odmian.

Jeżeli funkcja zwraca wartość, to tę wartość można przypisać zmiennej.

Przykładowo w bibliotece standardowej języka C/C++ można znaleźć funkcję sqrt(), która zwraca rzeczywisty pierwiastek z argumentu tej funkcji. Gdy chce się wyznaczyć pierwiastek z liczby 3.0 i przypisać go zmiennej x1, to należy napisać instrukcję [12]:

x1 = sqrt(9.0); // funkcja zwraca wartość 3.0 i przypisuje ją zmiennej x1

Wyrażenie sqrt(3.0) wywołuje funkcję sqrt(). Wyrażenie sqrt(3.0) to wywołanie funkcji, sqrt() jest funkcją wywoływaną. Na rysunku 2 przedstawiono wywołanie funkcji [12].

0x01 graphic

Rys. 2. Ilustracja wywołania funkcji [12]

Argumentem (parametrem) tej funkcji jest wartość 9.0, która jest przekazywana jako dana do funkcji. Funkcja sqrt() wylicza wynik (3.0) i odsyła go do funkcji wywołującej, a odsyłana wartość jest wartością zwracaną. Tę wartość można traktować jako wartość podstawianą w miejsce wywołania funkcji sqrt().

Zatem, ten przykład pokazuje sekwencję przypisania wartości zwracanej (przez funkcję) zmiennej x1.

Oznacza to, że parametr to wartość przekazywana funkcji, natomiast wartość zwracana to wartość przekazywana z funkcji.

Uwaga: program w języku C++ powinien zawierać prototypy wszystkich funkcji występujących w programie.

Prototyp funkcji pełni względem funkcji taką rolę jak deklaracja względem zmiennej. W bibliotece języka C++ zdefiniowano między innymi funkcję sqrt(), której parametrem jest liczba (z opcjonalną częścią ułamkową) oraz wartość zwracaną tego samego typu.

W innych językach np. PASCAL liczby z częścią ułamkową są nazywane liczbami rzeczywistymi - real, w C++ używa się typu double.

Prototyp funkcji sqrt() jest konstrukcją [12]:

double sqrt(double); // prototyp funkcji sqrt()

Słowo kluczowe double na początku tej konstrukcji (prototypu) oznacza, że ta funkcja zwraca wartość typu double. Natomiast słowo double ujęte w nawiasy okrągłe, oznacza, że funkcja sqrt() wymaga parametru typu double.

0x01 graphic

Rys. 3. Składnia wywołania funkcji sqrt() [12]

0

1
2
3
4
5
6
7
8
9
10
11
12
13
14

15

16

17

// Przykładowy program ilustrujący użycie funkcji sqrt()


#include <iostream> // dyrektywa PREPROCESORA
#include <cmath> // dyrektywa włączająca plik nagłówkowy cmath (lub cmath.h)

// w bibliotece standardowej C++ stosowana jest przestrzeń nazw std

int main() // nagłowek funkcji main()

{ // poczatek funkcji main()
using namespace std; // deklaracja o używaniu nazwy z przestrzeni nazw std

double Pow,
Bok; // deklaracja zmiennych typu double o nazwie Pow i Bok
cout << ”Wprowadz powierz. kwadratowego blatu stolika w metrach kwadratowych: .” ;
// polecenie
cout wyprowadza dane w C++, argumenty są oddzielane znakami <<
cin >> Pow; // wypełnienie zmiennej Pow (z klawiatury)

Bok = sqrt(Pow); // zwraca pierwiastek z zawart. zmiennej Pow i przypisuje go zm. Bok

cout << ”Bok kwadratowego blatu stolika w metrach = .” << endl;

system(”PAUSE”); // powstrzymanie okienka konsoli przed szybkim zamknieciem

return 0; // zwracanie przez funkcje main() wartosci 0

} // zakonczenie funkcji main()

Zatem, gdy w programie używa się funkcji sqrt(), należy podać jej prototyp. Można to zrealizować jedną z dwóch opcji [12]:

Nie należy mylić prototypu funkcji z jej definicją.

Prototyp opisuje jedynie interfejs funkcji (wskazuje, jakiego typu dane - parametry są przekazywana do funkcji i jakiego typu daną ta funkcja zwraca).

Definicja zawiera kod realizujący zadania funkcji.

W językach C/C++ prototyp i definicja są rozdzielone w przypadku funkcji bibliotecznych - biblioteka zawiera skompilowany kod funkcji, a prototypy są w plikach nagłówkowych.

Prototyp funkcji należy umieścić przed pierwszym wywołaniem jej wywołaniem. Najczęściej prototyp funkcji umieszcza się przed definicją funkcji main(). Funkcje biblioteczne C++ są umieszczane w plikach bibliotecznych. Kompilator na etapie kompilacji programu musi odnaleźć występujące w programie funkcje. W powyższym listingu programu użyto funkcję sqrt() zwracającą wartość.

Prototyp funkcji sqrt() jest włączany wraz z plikiem nagłówkowym cmath.

Występujące w C++ podstawowe typy danych arytmetycznych i logicznych
Standard języka C++ nie definiuje zakresów wartości, jakie mogą być reprezentowane przez poszczególne typy. Przedstawione w ostatniej kolumnie zakresy są prawidłowe dla większości współczesnych 32- bitowych kompilatorów, nie można jednak zakładać, że będzie tak w przypadku każdego systemu i każdego wykorzystywanego kompilatora.
Typ znakowy ma nazwę char i służy do reprezentacji znaków, czyli liter, cyfr, znaków przestankowych,  ogólnie znaków należących do zbioru ASCII. Zmienna typu char zajmuje w pamięci 1 bajt, czyli 8 bitów. Za jej pomocą można, zatem przedstawić 256 znaków (28 = 256). Zmienną typu char tworzy się analogicznie jak wszystkie inne zmienne (char NazwaZmiennej;). Jeśli chce się przypisać do niej jakiś znak, należy ująć go w znaki apostrofu, co zilustrowano niżej:

int main()
{
            char Mzmienna = 'a';
            cout << Mzmienna;
}

Dla zmiennych typu char w maszynach zgodnych ze standardem ASCII poniższe dwa wiersze przypisują zmiennej znakowej inicjuj taką samą wartość:

char inicjuj = `J';

char inicjuj = 74;

Zatem, typ char zalicza się do typów arytmetycznych, gdyż zmienne takie można traktować jako liczby. Np. kodem ASCII znaku a jest liczba (dziesiętna) 97, zatem zmienna typu char zawierająca ten znak w rzeczywistości będzie przechowywała liczbę 97. Oprócz liter łacińskich, cyfr i symboli wyróżnia się także znaki, które mają specjalne funkcje (sekwencje sterujące).

Do nich należą np.: znak nowego wiersza (/n), znak tabulacji (/t), apostrof (/').

Kolejnym wielkim usprawnieniem C++ w stosunku do języka C to uproszczenie posługiwania się łańcuchami znaków (strings). Łańcuchy takie to wartości ujęte w cudzysłów, które zawierają dowolna kombinację znaków, liczb, liter, spacji lub innych symboli.

Aby używać łańcuchów, konieczne jest dołączenie pliku nagłówkowego string, który zawiera definicję potrzebnych funkcji oraz umożliwia tworzenie zmiennych (łańcuchowych) typu string:

#include <string>

Wykorzystywany już we wcześniejszych przykładach typ int służy do przechowywania liczb całkowitych. Występuje on w kilku odmianach widocznych w tabeli typy danych C++.

Typy te tworzone są w prosty sposób. Jeśli do słowa kluczowego int doda się słowo short otrzyma się typ pozwalający na reprezentacje mniejszego zakresu liczb (patrz ostatnia kolumna tabeli), jeśli natomiast doda się słowo long otrzymuje się typ pozwalający na reprezentacje większego zakresu liczb.

Oprócz tego pojawiają się nowe słowa kluczowe signed i unsigned.

Określają one, czy dany typ całkowity będzie reprezentował tylko dodatnie (unsigned - bez znaku) czy też dodatnie i ujemne (signed - ze znakiem).

W języku C++, do reprezentacji liczb zmiennoprzecinkowych (zmiennopozycyjnych) służą typy float, double i longdouble. Obsługuje on dwa sposoby zapisu takich liczb. Pierwszy to standardowy zapis zawierający kropkę dziesiętną - rozdzielającą cześć całkowitą liczby od ułamkowej np.:

    1. // wartość zmiennoprzecinkowa

3.14159 // wartość zmiennoprzecinkowa

0.0001 // wartość zmiennoprzecinkowa

Druga metoda zapisu takich liczb nazywana jest notacją naukową.

Zawiera ona zapis (bez żadnej spacji) kolejno: liczbę (mantysę) zmiennoprzecinkową zapisywaną w sposób standardowy (z kropką dziesiętną lub bez), znak E (lub e) i liczbę całkowitą ze znakiem (cechę, wykładnik potęgi).

Na rysunku 3.4.2 przedstawiono zapis liczby zmiennoprzecinkowej w notacji naukowej [12].

0x01 graphic

Przykładowo następujące liczby są zapisane w notacji naukowej (wykładniczej):

1.414214e+0 // przybliżona wartość 0x01 graphic
, można zamiast e użyć E, znak + jest opcjonalny

17.32051e-01 // przybliżona wartość 0x01 graphic
, wykładnik (cecha) może być ujemny

3E8 // prędkość światła w próżni 3 * 108 [m/s] może być zapisana jako 3.e8

- 12.65E1 // wartość określonej całki oznaczonej (może być dodatnia lub ujemna)

5.98e24 // masa Ziemi 5,98 * 1024 [kg]

6.371e3 // promień ziemi r = 6371 [km]

9.11E-31 // masa elektronu 9,11 * 10-31 [kg]

Zapis w notacji naukowej pozwala na przedstawienie najszerszego zakresu wartości. Typ float pozwala na zapis liczb rzeczywistych z pojedynczą, double z podwójną a longdouble z rozszerzoną (wysoką) precyzją. W C++ istnieją dwa sposoby przypisywania wartości zmiennym typu float, co zostało zilustrowane w poniższym programie. Pierwszy sposób standardowy jest intuicyjny. Część całkowitą liczby od części ułamkowej rozdzielone są kropką dziesiętną (kropką a nie przecinkiem).

Sposób drugi to zapis wykładniczy w notacji naukowej, np.: 1.1e1 oznacza wartość wyrażenia 1,1*101, czyli wartość równą 11.

Oznacza to, że w linii 8 zmiennej DLiczba przypisano wartość 11.0. O tym, że zmienna DLiczba faktycznie zawiera wartość 11.0 można się przekonać po wykonaniu 9 linii kodu cout << DLiczba << '/n';

Oczywiście na początku programu musi być linia (2) z dyrektywą preprocesora #include <iostream>, gdyż inaczej kompilator nie rozpozna obiektu cout.

Jeśli wyświetli się ostrzeżenie kompilatora, nie należy się nim przejmować, kompilator po prostu przypomina, że zadeklarowano zmienną Pliczba, z której potem nie skorzystano. W tym programie jedynie zademonstrowano, że zmienna DLiczba faktycznie zawiera wartość 11. Wszystko jest, zatem w porządku.

0

1
2
3
4
5
6
7
8
9
10
11
12

// Program o nazwie program2.cpp

#include <cstdlib> // prototypy funkcji biblioteki standardowej
#include <iostream> // dyrektywa PREPROCESORA
using namespace std; // deklaracja umożliwiająca używanie nazw z przestrzeni nazw std

int main() // naglowek funkcji main()
{ // początek treści funkcji main()
float PLiczba; //deklaracja niewykorzystanej tutaj zmiennej typu float o nazwie PLiczba
    float DLiczba = 1.1e1; // przypisanie zm. DLiczba typu float stalej 1.1e1 (11)
    cout << DLiczba << '\n'; // wyswietlenie zawartości zmiennej DLiczba i zmiana linii
    system("PAUSE"); // przekazuje polecenie PAUSE do system. operacyjnego (j. C)
    return 0; // funkcja main() zwraca wartość 0

} // koniec treści funkcji main()

Po kompilacji i uruchomieniu programu na ekranie pokaże się wynik:

0x01 graphic

Zatem w C++ istnieją trzy typy zmiennoprzecinkowe: float, double i long double. Typy te przy opisie odwołują się do tzw. liczby cyfr znaczących i dopuszczalnego zakresu wykładnika (cyfry znaczące to cyfry, które maja wpływ na dokładność zapisu liczby).

Drugi sposób przedstawienia liczb (wykładniczy, pół logarytmiczny) to taki system przedstawienia liczb, w którym zakres wyrażalnych liczb jest niezależny od liczby cyfr znaczących [9].

Problem aliasowania [3]:

int x, *p, *q; //deklaracja trzech zmiennych typu int

//dwie ostatnie deklaracje informują, ze zmienna wskazywana wskaznikiem p i q jest typu int

p = &x; q = &x; //przypisanie zmiennym wskaznikowym adresu zmiennej x

Wartość [3]:

Typ [3]:

Okres życia i zakres widoczności omówimy później.

[Edytuj]

Wiązania

Będziemy teraz mówili o różnych bytach i atrybutach, które w pewnym momencie zostają powiązane. Rzecz dotyczy bardzo rozmaitych „bytów” takich jak zmienna, operator, wywołania podprogramu i cech takich jak wartość, typ, adres.

Na przykład, deklaracja zmiennej int x powoduje związanie zmiennej x z typem int, zaś wykonanie instrukcji podstawienia powoduje związanie zmiennej z (nową) wartością.

Wiązanie może następować w różnych momentach. Oto kilka przykładów [3]:

Rozważmy przykładowy fragment w języku C

int x;

...

x = x * 3;

Mamy tu do czynienia z następującymi wiązaniami:

Wiązania dzielimy na dwie klasy. Wiązania statyczne to te, które następują przed wykonaniem programu i nie zmieniają się w trakcie jego działania. Wiązania dynamiczne to te, które następują lub zmieniają się w trakcie działania programu.

Nie zajmujemy się wiązaniami sprzętowymi, które normalnie są dla nas niewidoczne, np. translacja adresów pamięci wirtualnej. Innymi słowy, wiązanie zmiennej statycznej z miejscem w pamięci uznajemy za statyczne, mimo że fizyczny adres tej zmiennej może zmieniać się na skutek zarządzania stronami pamięci wirtualnej.

W praktyce określenie „statycznie” oznacza „w czasie kompilacji”, zaś „dynamicznie” - „w czasie wykonania programu”.

[Edytuj]

Wiązanie typu

Każda zmienna musi zostać związana z typem przed pierwszym użyciem w programie. Rozpatrujemy dwa aspekty tej sprawy:

Odpowiedź na pytanie, jak określamy typ zmiennej?

Odpowiedź na pytanie, kiedy następuje wiązanie?

Dlaczego dynamiczne wiązanie typu jest kosztowne?

Jak wygląda wnioskowanie o typie?

fun f(x) = 2.0 * 3.14 * x;

fun g(x) = 2 * x;

fun h(x) = x * x;

fun h(x) : real = x * x;

fun h(x : real) = x * x;

fun h(x) = (x : real) * x;

fun h(x) = x * (x : real);

Wiązanie pamięci

Wprowadźmy parę pojęć związanych z wiązaniem pamięci:

Rozróżniamy cztery kategorie zmiennych, związane z ich okresem życia [3]:

Gdzie:

Sterta to najmniej skomplikowany sposób organizacji plików. Dane są gromadzone w kolejności ich pojawiania się. Każdy rekord przechowuje jedną operację transferu danych. Celem sterty jest proste gromadzenie danych i ich przechowywanie. Rekordy mogą mieć różne pola lub podobne pola występujące w innej kolejności. Tak, więc, każde pole powinno zawierać nazwę pola oraz jego wartość. Długość każdego pola musi być pośrednio określona za pomocą separatora, zwłaszcza dodanego jako podpole lub domyślnego dla danego rodzaju pola. Ponieważ plik typu sterta nie ma żadnej struktury, dostęp do rekordów wiąże się z intensywnym przeszukiwaniem. Oznacza to, że, jeżeli chcemy znaleźć rekord zawierający określone pole o określonej wartości, konieczne będzie sprawdzenie każdego rekordu w stercie do chwili znalezienia rekordu bądź przeszukania całego pliku. Pliki typu sterta są używane w sytuacji, gdy dane są gromadzone i przechowywane przed ich przetworzeniem lub, kiedy danych nie da się łatwo uporządkować. Ten rodzaj plików nie marnotrawi miejsca, gdy przechowywane dane są różnych rozmiarów. W stercie dane różnią się strukturą, umożliwiają zaawansowane przeszukiwanie oraz są łatwe w aktualizacji. Jednakże, poza tymi ograniczonymi zastosowaniami, tego typu plik jest nieodpowiedni w większości przypadków.

Zmienne statyczne [3]:

Zmienne dynamiczne na stosie [3]:

Gdzie: Stos (Stack) - uporządkowana lista, w której elementy są dodawane lub usuwane z tej samej listy zwanej szczytem. Oznacza to, że każdy kolejny dodawany element ląduje na szczycie stosu. Ze stosu usuwane są elementy, które są na nim najkrócej (znajdujące się na szczycie stosu). Jest to organizacja działająca wg zasady: pierwszy na wejściu, pierwszy na wyjściu.

Zmienne dynamiczne na stercie, jawne [3]:

Gdzie: Sterta to najmniej skomplikowany sposób organizacji plików. Dane są gromadzone w kolejności ich pojawiania się. Każdy rekord przechowuje jedną operację transferu danych. Celem sterty jest proste gromadzenie danych i ich przechowywanie. Rekordy mogą mieć różne pola lub podobne pola występujące w innej kolejności. Tak, więc, każde pole powinno zawierać nazwę pola oraz jego wartość. Długość każdego pola musi być pośrednio określona za pomocą separatora, zwłaszcza dodanego jako podpole lub domyślnego dla danego rodzaju pola. Ponieważ plik typu sterta nie ma żadnej struktury, dostęp do rekordów wiąże się z intensywnym przeszukiwaniem. Oznacza to, że, jeżeli chcemy znaleźć rekord zawierający określone pole o określonej wartości, konieczne będzie sprawdzenie każdego rekordu w stercie do chwili znalezienia rekordu bądź przeszukania całego pliku. Pliki typu sterta są używane w sytuacji, gdy dane są gromadzone i przechowywane przed ich przetworzeniem lub, kiedy danych nie da się łatwo uporządkować. Ten rodzaj plików nie marnotrawi miejsca, gdy przechowywane dane są różnych rozmiarów. W stercie dane różnią się strukturą, umożliwiają zaawansowane przeszukiwanie oraz są łatwe w aktualizacji. Jednakże, poza tymi ograniczonymi zastosowaniami, tego typu plik jest nieodpowiedni w większości przypadków.

int *p;

p = new int;

...

delete p;

Zmienne dynamiczne na stercie, niejawne [3]:

Wady: Wysoki koszt, związany z dynamicznym przechowywaniem atrybutów.

Sprawdzanie zgodności typów

Zgodność typów będziemy rozważali w szerokim sensie. Podprogramy będziemy traktowali jako operatory, których operandamiparametry. Instrukcję przypisania będziemy uważali za operację dwuargumentową, której operandamilewa i prawa strona przypisania.

Sprawdzanie zgodności typów to sprawdzenie, czy typy operandów są odpowiednie. Określenie „odpowiedni (lub zgodny) typ” oznacza typ bezpośrednio dozwolony w danym kontekście lub typ, który jest dozwolony po zastosowaniu niejawnej konwersji narzuconej przez reguły języka programowania.

Błąd typu to użycie operatora z operandem nieodpowiedniego typu. Wspomniane wyżej automatyczne, niejawne konwersje wykonywane są przez kod wygenerowany przez kompilator.

Przykład: W poniższym fragmencie wartość zmiennej j jest automatycznie zamieniana z typu int na float i wykonywane jest dodawanie zmiennopozycyjne.

float x, y; int j; x = y + j;

Zastanówmy się, kiedy sprawdzanie zgodności typów może być statyczne. Jeśli wiązanie typów jest statyczne, to sprawdzanie zgodności typów na ogół także może być statyczne, tzn. robione w czasie kompilacji. Istnieją jednak wyjątki, np. unie, które mogą zawierać wartości różnych typów w tym samym miejscu pamięci. Jeśli zatem odwołania do pól unii mają być sprawdzane pod względem zgodności typów, to musi się to dziać dynamicznie, tzn. w trakcie wykonania programu. Zauważmy, że unie trudno właściwie nazwać wyjątkiem: unie są przykładem „nieprawdziwego” wiązania statycznego typu. Jeśli natomiast wiązanie typów jest dynamiczne, to sprawdzanie zgodności typów musi być dynamiczne.

Języki silnie typowane

Język nazywamy silnie typowanym, jeśli błędy typu są w nim zawsze wykrywane. Obejmuje to również dynamiczne sprawdzanie pól w uniach. Oczywistą zaletą silnego typowania jest możliwość wykrywania wielu pospolitych błędów. Wiele języków jest „prawie” silnie typowanych. Bliskie tego są Ada, C# i Java - odstępstwem jest w nich możliwość wykonania jawnej konwersji typów. Pascal nie jest silnie typowany ze względu na unie, występujące tu pod nazwą wariantów w rekordzie, choć poza tym niewiele brakuje... Natomiast C i C++ zdecydowanie nie są silnie typowane. Można w nich np. uniknąć sprawdzania typów parametrów, nie mówiąc już o polach unii...

Jaki jest związek silnego typowania z niejawnymi konwersjami? Występowanie w języku niejawnych konwersji może istotnie osłabić sens silnego typowania.

Konwersje te powodują, że np. błędne podstawienia mogą formalnie przestać być błędami typu. Silne typowanie chcemy przecież mieć po to, by wykrywać jak najwięcej błędów. Zatem silne typowanie bez konwersji sprzyja niezawodności (kosztem wygody programisty). Przykładowo, zasady niejawnych konwersji (przede wszystkim) sprawiają, że sprawdzanie zgodności typów w Adzie jest bardziej skuteczne niż w Javie; to z kolei jest bardziej skuteczne niż w C++.

Jak konkretnie zdefiniować zgodność typów?

W zarysie już to zrobiliśmy... Rzecz nie jest jednak tak oczywista, jak z pozoru wygląda. Szczegółowe zasady zgodności typów mają wpływ na projektowanie i używanie typów. Zwykle rozróżniamy dwa rodzaje zgodności typów - zgodność nazwy i zgodność struktury, które omawiamy poniżej. Zajmujemy się też pokrótce podtypami i typami pochodnymi, jako że pojęcia te w naturalny sposób wiążą się z kwestią zgodności typów.

Zgodność nazwy [3]:

Zgodność struktury [3]:

Typy pochodne [3]:

type metry is new Float;

type stopy is new Float;

Podtypy [3]:

subtype SmallInt is Integer range 0..99;

Kwestie praktyczne [3]:

A, B: array (1..100) of Integer;

type Tab100 is array (1..100) of Integer;

A, B: Tab100;

Zakres widoczności

Zakres widoczności zmiennej to zbiór tych instrukcji programu, w których zmienna ta jest widoczna, tzn. można się do niej odwołać. Mówimy, że zmienna jest lokalna (w jednostce programu lub w bloku), jeśli jest zadeklarowana w tej jednostce. Mówimy, że zmienna jest nielokalna, jeśli jest widoczna, ale zadeklarowana gdzie indziej. Zasady rozstrzygania zakresu w danym języku mówią, w jaki sposób odwołania do nazw są wiązane ze zmiennymi.

Zakres statyczny

Zakres statyczny opiera się na kodzie programu (w sensie tekstowym). Jest powszechnie stosowany w językach wywodzących się z Algolu. Spotykamy dwie kategorie języków z zakresem statycznym: dopuszczające zagnieżdżanie podprogramów (np. Ada, Pascal) i nie dopuszczające takiego zagnieżdżania (np. C). W tym drugim przypadku zagnieżdżanie zakresów może jednak nastąpić poprzez zagnieżdżenie definicji klas.

W dalszych rozważaniach zakładamy, że wszystkie zakresy są związane z jednostkami programu i że rozstrzyganie zakresu jest jedyną metodą odwołania się do zmiennych nielokalnych. Powyższe założenie nie we wszystkich językach jest prawdziwe (np. operator :: w języku C++ pozwala na dostęp do „niewodocznej” zmiennej). Rozważamy teraz parę kwestii związanych ze statycznym zakresem widoczności.

Odwoływanie się do zmiennych w językach z zakresem statycznym

Dostęp do przesłoniętych zmiennych

Bloki

Problemy z zakresem statycznym

Przykład

P0 {

P1 {

P3 {

}

P4 {

}

}

P2 {

P5 {

}

}

}

Zakres dynamiczny

Zakres dynamiczny opiera się na kolejności wywołań podprogramów, a nie na ich rozmieszczeniu „przestrzennym”. Jako kryterium rozstrzygania o dostępie przyjmujemy więc bliskość czasową, a nie przestrzenną. Tym razem zastanówmy się więc nad problemami dynamicznego zakresu widoczności.

Odwoływanie się do zmiennych w językach z zakresem dynamicznym

Przykład

P0() {

int x;

P1() {

int x;

P2();

}

P2() {

Put(x);

}

P1();

}

Problemy z zakresem dynamicznym

Zakres widoczności a okres życia

Zakres widoczności i okres życia to dwa różne pojęcia. Zakres widoczności (zwłaszcza statyczny) to pojęcie związane z rozmieszczeniem kodu, czyli przestrzenią. Okres życia to pojęcie związane z czasem. W wielu typowych sytuacjach jest między nimi związek.

Przykładowo, rozważmy zmienną zadeklarowaną w metodzie, która nie wywołuje innych metod. Jej zakres widoczności rozciąga się od deklaracji do końca metody. Jej okres życia zaczyna się przy wejściu do metody i kończy się w chwili, gdy kończy się wykonanie metody. W tym przypadku obydwa pojęcia są bardzo bliskie.

Często jest jednak zupełnie inaczej. Zmienna zadeklarowana w języku C jako static wewnątrz funkcji ma lokalny zakres widoczności (tę funkcję, w której jest zadeklarowana), ale globalny okres życia (tzn. cały czas wykonania programu). Gdy wewnątrz podprogramu następuje wywołanie innego podprogramu, zmienne lokalne tego pierwszego przestają być widoczne na czas wywołania. Ich „życie” trwa jednak nadal i są ponownie widoczne po powrocie z podprogramu.

Inne problemy dotyczące zmiennych

Oto kilka problemów związanych ze zmiennymi, których nie mieliśmy dotychczas sposobności omówić.

Środowisko odwołań

Stałe nazwane

Inicjacja zmiennych

Źródło: "http://wazniak.mimuw.edu.pl/index.php?title=Paradygmaty_programowania/Wyk%C5%82ad_2:_Semantyka_zmiennych"

[Edytuj]

Reprezentacja algorytmu wymaga użycia pewnej formy języka. Ludzie mogą wykorzystywać język naturalny (etniczny, np. polski, angielski, francuski,…) lub język obrazowy (np. algorytm składania ptaka z kwadratowego kawałka papieru [2]). Często komunikowanie się w języku naturalnym prowadzą do nieporozumień ze względu na wieloznaczność (np. słowo: „lalka”, „zamek”; zdanie: „Odwiedziny wnuków mogą być stresujące” - może oznaczać problemy, gdy nas odwiedzają, albo też, że wizyta u nich jest stresująca). Często pojawiają się także problemy dotyczące wymaganego poziomu (stopnia) szczegółowości (poziom szczegółowości należy dobierać w zależności od tego, do kogo jest adresowany algorytm, zależy od jego wiedzy, doświadczenia…). Oznacza to, że źródłem problemów jest brak precyzyjnej definicji języka (języka algorytmicznego) stosowanego do opisu algorytmu lub niewłaściwy poziom szczegółowości przekazywanej informacji [2].

W informatyce rozwiązywane są te problemy poprzez tworzenie dobrze określonego zestawu cegiełek, z których można konstruować reprezentacje algorytmów. Takie cegiełki nazywane są konstrukcjami pierwotnymi (primitives). Problemy niejednoznaczności usuwa się, przypisując pierwotnym konstrukcjom ścisłe definicje. Natomiast wymaganie, aby algorytmy opisywano za pomocą tych konstrukcji pierwotnych (cegiełek), ustanawia jednolity poziom szczegółowości [2].

Zestaw konstrukcji pierwotnych razem z zestawem reguł definiujących sposób ich łączenia ze sobą (w tym zestaw reguł budowy złożonych konstrukcji z pierwotnych konstrukcji) w celu reprezentacji bardziej złożonych obiektów składają się na język programowania [2].

Każdą konstrukcję pierwotną definiuje się za pomocą dwóch elementów [2]:

Składnia opisuje symboliczną reprezentację konstrukcji pierwotnej.

<Składnia>::= <opis symbolicznej reprezentacji konstrukcji pierwotnej>.

Semantyka jest znaczeniem konstrukcji pierwotnej, czyli pojęcie, które konstrukcja reprezentuje [2].

< Semantyka >::= < znaczenie konstrukcji pierwotnej, pojęcie, które konstrukcja reprezentuje>.

Na przykład, napis „powietrze”- składniowo jest ciągiem 9 symboli (liter alfabetu), a semantyką jest znaczenie tego słowa - jest substancja lotna otaczająca kulę ziemską [2]. Zdanie „Ten kamień jest głupi” nie ma znaczenia (nie ma sensu).

Zestaw konstrukcji pierwotnych do reprezentacji algorytmów przeznaczonych do wykonania na komputerze można uzyskać na podstawie rozkazów maszynowych konkretnego komputera (z listy rozkazów mikroprocesora). Algorytm wyrażony na tym poziomie szczegółowości z całą pewnością pozwoliłby na otrzymanie programu, który można by wykonać na komputerze. Reprezentacja algorytmu na tym poziomie szczegółowości jest jednak żmudna (złożona)i podatna na błędy, dlatego też zwykle stosuje się pierwotne konstrukcje językowe „ z wyższego poziomu”. Każda z nich jest narzędziem abstrakcyjnym, które można zbudować, posługując się konstrukcjami pierwotnymi niższego poziomu dostępnymi w języku maszynowym komputera. Dzięki takiemu zabiegowi uzyskuje się formalny język programowania, w którym algorytmy można wyrażać na wyższym poziomie abstrakcji niż w języku maszynowym konkretnego komputera [2].

Składnia to zbiór reguł, opisujących jak wygląda poprawny program w danym języku, czyli np. [3]:

Jak widać z powyższego wyliczenia, jesteśmy tak bardzo przywiązani do języków imperatywnych i obiektowych, że chyba nie bardzo wyobrażamy sobie język bez poleceń, pętli while, zmiennych...

Semantyka to znaczenie wspomnianych wyżej form, czyli „co one robią”. Weźmy dla przykładu typową instrukcję warunkową [3]:

Powyższy opis semantyki instrukcji warunkowej jest, co prawda niezbyt formalny, ale zupełnie zrozumiały [3]:

Na razie warto zrezygnować z wprowadzenia formalnego języka programowania. W miejsce jego można zdefiniować mniej formalny, bardziej intuicyjny system notacyjny, zwany pseudokodem.

<Pseudokod>::= <intuicyjny system notacyjny, w którym nieformalnie wyraża się koncepcje związane z opracowywanym algorytmem>.

Pseudokod można uzyskać, rozluźniając wymagania stawiane językowi formalnemu, w którym ma być wyrażona końcowa wersja algorytmu. Takie podejście stosuje się często, jeśli jest znany z góry docelowy język programowania. Pseudokod stosowany we wczesnych fazach tworzenia oprogramowania składa się ze struktur składniowo - semantycznych, które przypominają struktury występujące w docelowym języku programowania, lecz są mniej formalne.

Czym jest język i jak go opisać?

Pojęcie język jest rozumiane jako skończony zbiór symboli języka (alfabet) wraz ze zbiorem reguł łączenia (składania) poszczególnych symboli (liter /znaków alfabetu) w ciągi (słowa) i zbiorem reguł (semantycznych) nadających ciągom symboli znaczenie (sens) merytoryczne.

Język jest to, więc ogólna nazwa zdefiniowanego zbioru znaków oraz reguł lub konwencji określających sposoby i kolejności, w jakich znaki te mogą być łączone dla nadania im znaczenia w tym języku.

Język można traktować jako uporządkowaną czwórkę elementów [26]:

J = {A, W, D, Z},

gdzie: A jest alfabetem języka, W - zbiorem wyrażeń poprawnych, D - dziedzina języka, Z - znaczenie języka.

Alfabet języka jest to zbiór skończony znaków zwanych literami. Każdy ciąg skończony liter nazywamy napisem.

Zbiór wyrażeń poprawnych języka jest to pewien wyróżniony zbiór napisów w alfabecie tego języka. O tym, czy dany napis jest wyrażeniem poprawnym języka, czy też nim nie jest decydują reguły gramatyczne języka, które określają strukturę wyrażeń poprawnych. Spis tych reguł tworzy gramatykę języka (w szerokim sensie obejmuje również reguły morfologii języka). Wyrażeniem poprawnym jest, więc nie tylko zdanie ( w tym ujęciu), lecz także każdy poprawnie zbudowany wyraz. Od reguł gramatycznych nie wymaga się, aby określały sens wyrażeń, uznanych przez nie za poprawne.

Dziedzina języka jest zbiór wiadomości /informacji, którego elementy stanowią treść (sens) wyrażeń poprawnych języka. Inaczej mówiąc, dziedziną języka jest zbiór wszystkich wyrażalnych w tym języku wiadomości / informacji (może zawierać pojęcia nie wyrażalne w innych językach).

Znaczenie języka jest relacja wiążąca wyrażenia poprawne języka z elementami jego dziedziny.

Element dziedziny pozostający w tej relacji z pewnym wyrażeniem poprawnym języka nazywa się znaczeniem tego wyrażenia. Jedno wyrażenie może mieć wiele znaczeń, np. lalka, skok, zamek itp. Wiele wyrażeń może mieć to samo znaczenie, np. samochód, auto, pojazd dwuśladowy itp.; wyrażenia pozbawione sensu nie maja znaczeń. Reguły określające omawianą relację nazywamy regułami znaczeniowymi lub semantycznymi języka. Język, który jest stosowany do przedstawienia algorytmów, nosi nazwę języka algorytmicznego, a przy stosowaniu go do celów programowania, określany jest jako język programowania.

Algorytm przedstawiony w języku programowania nazywa się programem.

Programem nazywamy opis sposobu przetwarzania danych. Jest to uporządkowany zbiór (ciąg) rozkazów (instrukcji) i innych danych (struktur danych) związanych z rozkazami [36]. Program działa na podstawie algorytmu, który z kolei jest ściśle związany ze strukturami danych (obiekty proste, złożone), na których operuje tzn. [37]:

ALGORYTMY + STRUKTURY DANYCH = PROGRAMY.

Język to zbiór napisów utworzonych ze znaków z ustalonego alfabetu [3]:

Mówiąc o językach programowania, chcemy takie właśnie języki móc precyzyjnie opisywać [3]:

Jak zatem my będziemy opisywać języki programowania lub ich elementy?

Notacja BNF (Backus Naur Form) jest powszechnie używana właśnie do opisu składni języków programowania. Stworzona została w trakcie prac nad językami Fortran i Algol na przełomie lat pięćdziesiątych i sześćdziesiątych XX wieku

Często stosuje się dodatkowe symbole i konwencje, upraszczające zapis

typ ::= char | int | float | double

instr_warunkowa ::= if wyr_logiczne then instr [ else instr ]

lista_arg ::= arg { "," arg }

liczba_ze_znakiem ::= ("+" | "-") liczba_bez_znaku

Chcąc opisać powtarzające się elementy, możemy stworzyć definicję rekurencyjną lub wykorzystać nawiasy klamrowe:

lista_identyfikatorów  ::= identyfikator

| lista_identyfikatorów "," identyfikator

lista_identyfikatorów  ::= identyfikator { "," identyfikator }

Klasyczny przykład użycia notacji BNF to opis składni notacji BNF:

składnia ::= { reguła }

reguła ::= identyfikator "::=" wyrażenie

wyrażenie ::= składnik { "|" składnik }

składnik ::= czynnik { czynnik }

czynnik ::= identyfikator

| symbol_zacytowany

| "(" wyrażenie ")"

| "[" wyrażenie "]"

| "{" wyrażenie "}"

identyfikator ::= litera { litera | cyfra }

symbol_zacytowany ::= """ { dowolny_znak } """

Zauważmy, że niektóre symbole, formalnie terminalne, są właściwie niedodefiniowanymi symbolami nieterminalnymi. Gwoli ścisłości powinniśmy, zatem dopisać:

litera ::= "A" | "B" | "C" | ...

cyfra ::= "0" | "1" | ... | "9"

dowolny_znak ::= ...

Jak widać, użyliśmy kolejnego niedodefiniowanego symbolu, a mianowicie wielokropka. Nie brnijmy jednak dalej w formalizowanie rzeczy jasnych i znanych...

Na pewno wszyscy wiedzą, jak uruchomić program napisany np. w języku C++. Dla naszych rozważań istotne jest to, że program taki musi zostać przetłumaczony na język wewnętrzny maszyny, na której program zamierzamy uruchomić. Owo tłumaczenie może być dokonane na jeden z dwóch sposobów:

Nie zamierzamy zagłębiać się tu w tajniki budowy kompilatorów, czyli programów tłumaczących

Dlaczego w takim razie chcemy mówić o implementacji?

Przykład

for i := 1 to length(s) do ...

for (i = 1; i <= strlen(s); ++i) ...

Uwagi bibliograficzne

Pisząc wykłady omawiające kwestie implementacyjne (2-4) oraz wykłady przeglądowe (5-7) korzystaliśmy z ciekawie opracowanej, rozległej tematycznie książki Sebesty „Concepts of Programming Languages”.

Wykład o rachunku lambda bazuje na „Wybranych zagadnieniach z teorii rekursji”, zaś wykład o rachunku sigma — na „A Theory of Objects” Abadiego i Cardellego. Wiele informacji do wykładów o Haskellu zaczerpnęliśmy z „Introduction to Functional Programming using Haskell” Birda, zaś do wykładów o Prologu — z „Logic, Programming and Prolog” Nilssona i Małuszyńskiego. Pozostałe pozycje ze spisu literatury również stanowiły cenne źródło informacji.

Wykaz literatury podstawowej:

  1. Bertrand M. Programowanie zorientowane obiektowo, Helion, Gliwice 2005

  2. Brookshear J. G. Informatyka w ogólnym zarysie. WNT, Warszawa 2003

  3. http://wazniak.mimuw.edu.pl/

  4. Kluźniak F., Szpakowicz S. Prolog, WNT, Warszawa 1983

  5. Oficjalna strona języka CLIPS: http://www.ghg.net/clips/CLIPS.html

  6. Prata S. Szkoła Programowania. Język C. Wyd. V, Helion, Gliwice 2006

  7. Prata S. Szkoła Programowania. Język C++. Wyd. V, Helion, Gliwice 2006

  8. Sebesta R., Concepts of Programming Languages, Addison Wesley, 2005

  9. Ullman L., Liyanage M. Programowanie w języku C. Szybki startEdytuj. Błyskawiczny kurs programowania w języku C. HELION, Gliwice 2005

  10. Ullman L., Singel A. Szybki start. Programowanie w języku C++. Błyskawiczny kurs tworzenia aplikacji w jednym z najpopularniejszych języków programowania. HELION, Gliwice 2007

Wykaz literatury uzupełniającej:

  1. Daniluk A. C++ Builder Borland Developer Studio 2006. Helion, Gliwice 2006

  2. Garbacz B., Koronkiewicz P., Grażyński A. Programowanie, koncepcje, techniki i modele. Helion, Gliwice 2005

  3. Grębosz J.: Symfonia C++ standard, Edition 2000, 2005

  4. Grębosz J.: Pasja C++, Oficyna Kallimach, Kraków, 2003

  5. Eckel B.: Thinking in Java. Edycja polska. Wydanie IV, Helion 2006

  6. Kochan S. G. Język C. Wprowadzenie do programowania. Helion 2005

  7. Liberty J. C++ księga eksperta. Helion, Gliwice 1999

  8. Liberty J. C++ dla każdego. Helion, Gliwice 2002

  9. Liberty J., MacDonald B.: C# 2005. Wprowadzenie, Helion 2007

  10. Neibauer A. R. Języki C i C++”. HELP Warszawa 1995, Wyd. IV, 2005

  11. Nilsson U., Małuszyński J., Logic, Programming and Prolog, John Wiley & Sons, 1995

  12. Savitch W. W tonacji C++. RM, Warszawa 2005

  13. Solter N. A., Kleper S. J. C++. Zaawansowane programowanie. Helion, Gliwice 2006

  14. Stasiewicz A. Wkrocz w świat programowania w C ++. Ćwiczenia praktyczne C++. Wyd. II, Helion, Gliwice 2006

  15. Thinking in C++ Edycja polska BRUCE ECKEL (tłumaczenie: Imiela P.). HELION, Gliwice 2002

Wykaz literatury dodatkowej:

  1. Borowiec J. i inni pod red. R. Marczyńskiego. Problemy przetwarzania informacji. WNT, Warszawa, 1970

  2. Jankowscy J. i M.: Przegląd metod i algorytmów numerycznych. Część 1, WNT, Warszawa 1988

  3. Majgier D. Kurs XHTML / HTML/CSS http://algorytmy.pl/doc/xhtml/

  4. Maguire S. Niezawodność oprogramowania. Helion, Gliwice 2002

  5. Myers G. J. Projektowanie niezawodnego oprogramowania. WNT, Warszawa 1980

  6. Myers G. J.,Sandler C., Badgett T., Thomas T. M. Sztuka testowania oprogramowania. Helion, Gliwice 2005

  7. Nabajyoti Brkakati. Biblia Turbo C++. Oficyna Wydawnicza LT&P

  8. Oualline S. Jak nie programować w C++, czyli 111 programów z błędami i 3 działające. MIKOM, Warszawa 2003

  9. Sysło M. Algorytmy. Wyd. Szkolne i Pedagogiczne, Warszawa 1997

  10. Tanenbaum A. S. Organizacja maszyn cyfrowych w ujęciu strukturalnym. WNT, Warszawa 1980

  11. Turski W. M. Propedeutyka informatyki. PWN, Warszawa 1989

  12. Wirth N. Algorytmy + struktury danych = programy. WNT, Warszawa 1980

55



Wyszukiwarka

Podobne podstrony:
Paradygmaty Składnia Semantyka, Składnia i semantyka
go czas semant, filologia polska, językoznawstwo, gramatyka opisowa, słowotwórstwo i składnia
Bierwisch Semantyka składnikowa
003 zmienne systemowe
Badanie korelacji zmiennych
Modsim Skladnia
prąd zmienny malej czestotliwosci (2)
Prezentacja Składniki chemiczne kwasu nukleinowego
zapotrzebowanie ustroju na skladniki odzywcze 12 01 2009 kurs dla pielegniarek (2)
W2 Chemiczne skladniki komorki
Składniki mineralne w diecie kobiet ciężarnych prezentacja
Analiza Składniowa
Istota , cele, skladniki podejscia Leader z notatkami d ruk
WEM 1 78 Paradygmat
FiR Zmienne losowe1
SKŁADNIA JM
4 operacje na zmiennych I
Wyklad 2 zmiennosc standaryzacja 5 III 2014 b
17 G11 H09 Składniki krwi wersja IHiT

więcej podobnych podstron