Wykład 5 - 3 godz.
Zakres tematyczny
1.Organizacja pamięci
2.Funkcje
1. Organizacja pamięci
Pamięć została podzielona na wiele różniących się części.
Mamy więc obszar kodu w którym znajdują się instrukcje programu, i jest to obszar nie zmieniany w trakcie pracy programu. Znajdują się tam instrukcje składające się na kod, np. program :
int x, y = 1;
main()
{
int z;
char *string;
x = 5;
x++;
y -=10;
string = malloc(5);
}
po przetłumaczeniu na język rozkazów procesora wygląda tak:
MOV x,5
INC x
SUB y,10
Instrukcje te pojawią się w obszarze kodu. Jak można się domyślić, obszar ten nie powinien ulegać zmianom po uruchomieniu programu. Jednak nie można tego powiedzieć o używanych danych, zajmijmy się więc obszarem danych.
Obszar danych w rzeczywistości składa się z dwóch części: dla danych zainicjowanych i dla danych niezainicjowanych.
W naszym programie zmienna x nie jest zainicjowana, a y jest. Oznacza to, że powinny być przechowywane w różnych obszarach (rys).
Obszar danych niezainicjowanych nazywany jest też obszarem BSS (block started segment - segment bloku początkowego.) Powodem wspólnego przechowywania danych niezainicjowanych jest możliwość jednoczesnego przypisania wszystkim danym wartości zero.
Jednak nie wszystkie dane zainicjowane lub niezainicjowane przechowywane są w obszarze danych. W rzeczywistości w obszarze danych przechowywane są trzy rodzaje zmiennych:
-globalne
-statyczne
-zainicjowane tablice i struktury.
Zasięg zmiennych
Omówimy teraz rodzaje obiektów w zależności od zasięgu i czasu ich życia. Mamy następujące typy zmiennych:
-automatyczne
-globalne
-statyczne
-register
Zmienne automatyczne: Wszystkie zmienne deklarowane wewnątrz funkcji -czy to będzie main, czy też funkcja zdefiniowana przez użytkownika są prywatne, czyli lokalne dla danej funkcji. Żadna inna funkcja nie ma do nich bezpośredniego dostępu. Każda zmienna lokalna funkcji, zaczyna dopiero żyć w chwili wywołania funkcji, a zanika po jej zakończeniu. Dlatego zmienne te nazywane są zwykle automatycznymi
Jeśli po raz drugi wywołamy funkcję, to zmienne te zostaną ponownie powołane do życia, ale nie ma pewności, że napewno znajdą się w tym samym miejscu w pamięci co poprzednio. Co więcej, nie zachowają się jej poprzednie wartości (z poprzedniego wywołania) i muszą być jawnie określane od nowa przy każdym wejściu do funkcji. Ponieważ zmienne te nie są zerowane w chwili definicji, to początkowo tkwią w nich śmieci - trzeba pamiętać, aby nie odczytywać tych zmiennych zanim w nich czegoś sensownego nie zapiszemy. Zmienne te przechowywane są na stosie - przedziela się tam tylko obszar dla tych zmiennych i nic więcej - nie inicjuje się.
Z tego typu obiektami wiąże się słowo kluczowe auto :
auto int x;
Jest ono rzadko używane, ponieważ obiekty są definiowane jako automatyczne przez domniemanie. Jeśli więc w bloku funkcji wystapi taka deklaracja jest ona równoważna :
int x;
Zmienne globalne: Przeciwieństwem zmiennych lokalnych są zmienne zewnętrzne w stosunku do wszystkich funkcji, zwane globalnymi. Oznacza to, że są one dostępne poprzez nazwę we wszystkich funkcjach pliku. Deklaracje takich zmiennych umieszcza się na początku programu (pliku). W naszym programie zmienne x i y zostały zadeklarowane jako globalne.
Ze względu na to, że zmienne globalne są ogólnie dostępne, mogą być stosowane zamiast listy argumentów przy przekazywaniu danych między funkcjami. Co więcej, ponieważ istnieją stale, a nie pojawiaja się i znikają razem z wywołaniem funkcji zachowują więc swoje wartości nawet po zakończeniu działania tych funkcji, które nadały im wartość. Zmienna globalna może być zdefiniowana tylko jeden raz (tylko jeden raz można przydzielić jej pamięc) na zewnątrz wszystkich funkcji. Taka zmienna musi być także deklarowana (okreslony charakter zmiennej bez przydzielania pamięci) we wszystkich funkcjach które chcą mieć do niej dostęp - można to zrobić poprzez deklaracje extern:
extern int x;
W pewnych przypadkach słówko to można pominąć: jeśli definicja zmiennej globalnej pojawia się w pliku źródłowym przed użyciem zmiennej w konkretnej funkcji. W rzeczywistości definicje wszystkich zmiennych globalnych umieszcza się na początku pliku żródłowego, a następnie pomija w całym pliku deklaracje extern.
Jeśli jednak np. program składa sie z 3 plików i zmienne globalne definiujemy np. w pliku 1, to w plikach korzystających z tych zmiennych nie można pominąć deklaracji extern.
Zmienne globalne przechowywane są w normalnym obszarze pamięci i są wstępnie inicjowane zerami
Zmienne statyczne: Są to zmienne deklarowane przy pomocy słowa kluczowego static. Można ją stosować zarówno do zmiennych zewnętrzych jak i wewnetrznych.
W odniesieniu do zmiennych globalnych przydomek static oznacza, że nie życzymy sobie, aby nazwa ta była znana w innych plikach składających sie na dany program. Nazwa jest nadal globalna, ale tylko dla danego pliku. Jest to więc sposób na ukrycie nazw zmiennych. np.:
static char x[10];
static int y=0;
................
int fun1(void) {...}
void fun2(int c) {...}
W danym pliku zdefiniowane są zmienne x, y jako zmienne globalne i chcemy aby tylko funkcje umieszczone w tym pliku: fun1 i fun2 mialy dostęp do zmiennych x i y. Wówczas żadne inne funkcje nie będą mial do nich dostępu, a nazwy nie będą kolidować z takimi samymi nazwami w innych plikach tego samego programu. Deklaracje takie stosuje sie także do nazw funkcji.
Lokalne zmienne statyczne są tak samo lokalne dla funkcji, jak zmienne automatyczne. Jednak w przeciwieństwie do automatycznych nie pojawiają się i nie znikają razem z wywołaniem funkcji, lecz istnieją między jej wywołaniami. Czyli przy powtórnym wejsciu do funkcji ma wartość taką jaka miała przy opuszczaniu funkcji.
Zmienne te są zakładane w tym samym obszarze co globalne i są wstepnie inicjowane zerami.
Zmienne register: zmienne rejestrowe informują kompilator, że zmienna będzie często używana i trzeba umieszczać ją jeśli się da w rejestrach przez co znacznie zwiększa sie szybkość progamu. Kompilator może tą deklarację zignorowac jeśli np. licza zmiennych zadeklarowanych jako register jest większa niż liczba rejestrów lub nieprawidlowy typ zmiennych deklarowany jest jako register (można deklarować tylko zmienne całkowite automatyczne) lub gdy nie ma takiej możliwości kompilator. Deklaracje tej zmiennej ma postać:
register int x;
Nie można uzyskać adresu takiej zmiennej, ponieważ rejestr to nie kawałek pamięci, a więc nie adresuje się go w zwykly sposób.
Stos tworzony jest w górnej cześci obszaru pamięci i w miarę zapełniania rozrasta się w kierunku dolnej części pamięci - to tu właśnie C++ umieszcza zmienne lokalne, wykorzystuje stos do przesyłania parametrów do funkcji, przechowuje adresy powrotne. W naszym programie miejsce na stosie dla zmiennej z zostanie utworzone w momencie ropoczęcia programu. O tym jak funkcje wykorzystują stos pomówimy przy okazji omawiania funkcji.
Sterta ostatni obszar pamięci znajduje się powyżej obszaru kodu i danych. Rozmiar sterty podobnie jak stosu zwiększa się, ale wzrost następuje w górę (do wyższej pamięci, nie jak w przypadku stosu).Obszar ten przydziela programista dla zmiennych dynamicznych.
Rozmiar stosu i sterty może się zmieniać. Obszar kodu i danych po utworzeniu pliku EXE ma ustaloną wielkość.
2. Funkcje
Jedną z najlepszych cech nowoczesnych języków programowania jest możliwość posługiwania się funkcjami. W przeciwieństwie do Pascala, gdzie były procedury i funkcje, w jezyku C/C++ mamy do czynienia tylko z funkcjami. Język ten opracowano tak, aby posługiwanie się funkcjami było łatwe, wygodne i skuteczne. Tam gdzie tylko jest możliwe, powinno się tworzyć funkcje, ponieważ zwiększa to czytelność programu. Zacznijmy od najprostszego programu wykrzystującego funcje:
#include
#include
int dodaj(int a, int b);
main()
{
int wynik;
wynik = dodaj(2,5);
cout<<"wynik obliczeń funkcji = "<< wynik;
}
int dodaj(int a, int b)
{
int w;
w = a + b;
return w;
}
Funkcję wywołuje się przez podanie jej nazwy i umieszczonych w nawiasie argumentów. Funkcja ma swoją nazwę która ją identyfikuje. Jak pamiętamy, aby można było użyć jakąś nazwę, musi ona być przed pierwszym użyciem zadeklarowana. Trzecia linijka programu to wlaśnie deklaracja funkcji zwana inaczej prototypem funkcji. Deklaracja funkcji informuje komlilator jaką wartość funkcja będzie zwracała i jakiego typu są jej argumenty. Definicja funkcji, czyli po prostu jej treść jest napisana na końcu, choc można napisac ją na początku, wtedy prototyp funkcji nie jest konieczny. Zatrzymajmy sie teraz przy deklaracjach:
float fun1( int a); float fun1(int)
void fun2(int a, char b); void fun2(int, char)
int fun3(void);
char fun4();
void fun5(...);
Na końcu deklaracji znajduje się średnik.
1. Funkcja może zwracać wartość wtedy przed jej nazwą umieszczamy typ zmiennej zwracanej przez funkcję;
2. Jeśli funkcja nie zwraca wartości jej nazwę poprzedzamy słowem void;
3. Funkcja może być wywoływana z argumentami, wtedy lista argumentów łącznie z typami umieszczana jest po nazwie funkcji w nawiasach;
4. Jeśli funkcja wywoływana jest bez argumentu wtedy nawias jest pusty lub słowo void w nawiasie ;
5. Możemy deklaraować funkcję z bliżej nie znaną liczba argumentów, wowczas w nawiasie umieszczamy ... .
W języku C++ zaszła zmiana w stosunku do C:
- zapis f() dla języka C oznaczało funkcję z nieznaną liczbą argumentów, czyli to samo co w C++ - f(...) a nie f(void).
- w języku C definicja funkcji nie wymagała typów w liście argumentów np.:
void fun(a,b)
int a,b;
{...}
Nowy styl programowania wymaga tego:
void fun(int a,int b)
{...}
Nazwy arumentów ( nie typy) są nieistotne dla komilatora i można je pominąć. Istotne to jest dopiera przy definicji zmiennej.
Przesyłanie argumentów do funkcji
Weźmy np. funkcję:
void func(int a)
{
cout<<"W funkcji parametr formalny modyfikowany ma wartość: a = "<
a += 5;
}
Nazwa a to tzw. argument (parametr) formalny funkcji.
W funkcji main wywołujemy ją w nastepujący sposób:
int a1=5;
cout<<"Parametr aktualny przed wywolaniem: a1 = "<
func(a1); // a = 10;
cout<<"Parametr aktualny po wywolaniu: a1 = "<
To co pojawia się w wywołaniu funkcji: a1 to jest parametr aktualny funkcji, czyli taki na którym ma pracować dana funkcja. Należy w tym miejscu zapytać się w jaki sposób przesyłane są argumenty do funkcji. Otóż w języku C++ mamy trzy takie sposoby:
- przez wartość
- przez wskaźnik
- przez referencję (przezwisko)
W standardzie języka dostepne były dwa pierwsze sposoby, C++ dodało trzeci.
Przesyłanie rzez wartość polega na tym, że do funkcji przesyłamy wartość argumentu aktualnego, ale funkcja nie pracuje na tym argumencie, nie ma możliwości modyfikowania go. Parametr aktualny służy jedynie do modyfikacji parametru formalnego przechowywanego tymczasowo na stosie. Jak widać, w programie parametr aktualny nie został zmieniony. Dodanie 5 nie nastapiło do parametru aktualnego, ale jedynie do zmiennej lokalnej na stosie, gdzie mieści się kopia ( o nazwie a).
Można spowodować jeśli to konieczne, aby funkcja zmieniała wartośc parametru aktualnego. W tym celu :
a)funkcja wywołująca musi przekazać adres zmiennej, a funkcja wywoływana musi zadeklarować odpowiedni parametr jako wskażnik - przekazywanie przez adres
#include
struct bigone
{
int serno;
char text[100];
} bo = {123,"This is a big structure"}
void(ptrfunc(const bigone *p1);
void main()
{
ptrfunc(&bo);
}
void ptrfunc(const bigone *p1)
{
cout<<'\n'<serno;
cout<<'\n'<text;
}
b)do funkcji należy przekazać referencje. To jest tzw. przesyłanie przez referencje.
Na początek powiedzmy sobie co to jest referencja. Jest to nowy typ zmiennej wprowadzony przez C++. Referencja jest to 16 lub 32 bitowa liczba przechowująca adress zmiennej, ale zachowujaca sie syntaktycznie jak zmienna. Możemy myśleć o referencji jako o przezwisku zmiennej. Jest ona alternatywnyną nazwą zmiennej. W czasie jej inicjacji referencje przypisujemy do danej zmiennej. Referencje tworzymy przy pomocy unarnego operatora &:
int actualint;
int &otherint = actualint; // deklaracja referencji
Powyższe instrukcje deklaruja zmienną całkowitą actualint i informuja kompilator, że inną nazwa actualint jest otherint. Od tej pory wszystkie operacje na każdej zmiennej mają ten sam rezultat:
// przykład uzycia referencji
#include
void main()
{
int actualint = 123;
int &otherint = actualint;
cout<< '\n'<< actualint; //123
cout<< '\n'<< otherint; //123
otherint++;
cout<< '\n'<< actualint; //124;
cout<< '\n'<< otherint; //124
actualint++;
cout<< '\n'<< actualint; //125
cout<< '\n'<< otherint; //125
}
Należy pamiętać, że referencja nie jest kopią zmiennej, ale tą samą zmienną pod inną nazwą. Poniższy program umożliwia wyświetlenie adresu zmiennej i referencji:
#include
void main()
{
int actualint = 123;
int &otherint = actualint;
cout<< &actualint << ' ' <<&otherint
}
Po uruchomieniu programu zostaną wydrukowane te same adresy dla obu identyfikatorów, zależne od konfiguracji systemu.
Referencje nie mogą istnieć bez zmiennej do której są przypisane i nie można na nich przeprowadzac żadnych operacji jako na niezależnych obiektach. W związku z tym przy deklarowaniu jednocześnie inicjujemy referencje.
Rozpatrzmy przykład funkcji :
void func(int a,int &b)
{
cout<<"W funkcji parametr formalny przed modyfikacją ma wartość: a = "<
cout<<"W funkcji parametr formalny przed modyfikacją ma wartość: b = "<
a += 5;
b += 6;
cout<<"W funkcji parametr formalny modyfikowany ma wartość: a = "<
cout<<"W funkcji parametr formalny modyfikowany ma wartość: b = "<
}
W funkcji main wywolujemy ją w nastepujący sposób:
int a1=5, b1=5;
cout<<"Parametr aktualny przed wywolaniem: a1 = "<
cout<<"Parametr aktualny przed wywolaniem: b1 = "<
func(a1,b1); // a = b = 5;
// a = 10 , b = 11;
cout<<"Parametr aktualny po wywolaniu: a1 = "<
cout<<"Parametr aktualny po wywolaniu: b1 = "<
Jak widać, funkcja wywoływana jest z dwoma argumentami.
Jeden klasycznie przekazywany jest przez wartość, w związku z tym zmienna aktualna a1 po wykonaniu funkcji nie zmieniła się, ale zmienna b1 została zmieniona, ponieważ przekazana została przez referencję.
Przy przekazywaniu parametru przez referencje do funkcji przesyłany jest adres przesyłanej zmiennej, a na stosie tworzona jest referencja. W naszym programie została zmieniona poprzez dodanie 6. Ponieważ wszystkie operacje wykonywane na referencji są operacjami wykonywanymi na zmiennej (referencja to adress zmiennej) do której przypisana jest referencja, w związku z tym zmodyfikowana została wartość argumentu aktualnego b1.
Po zakończeniu funkcji kopia zmiennej a1 przechowywana na stosie ( a po modyfikacji = 10) jest ze stosu usuwana - zmienna a1 - nie jest zmieniona.
Drugi argument przesyłany przez referencje, więc na stosie przechowywan był adres zmiennej, która w funkcji nazwaliśmy b. Po jego skasowaniu (ze stosu) pozostaje nadal zmienna b1, ale już zmieniona.
Przyjżyjmy się dokładnie wywołaniu funkcji. Oba sposoby przekazywania parametrów w momencie wywołania nie różnią się od siebie. Może to prowadzić do wielu nieporozumień szczególnie gdy wywołujemy dużo funkcji, których opisy znajdują się w innym module. Możemy zapomnieć, które parametry przekazywane są przez referencje i nie będziemy mogli zidentyfikowac błędu w przypadku gdy powstał on ana skutek modyfikowania parametru wywołania jakiejś funkcji.
Przesyłanie przez referencję stosuje się w przypadku, gdy argumentami funkcji są duże obiekty, do przesyłania których przez wartość konieczne było rezerwowanie na stosie setek bajtów.
Zastosowanie wskaźników w argumentach funkcji
Omawialiśmy już sposób przesyłania argumentów przez adres - na stosie przechowywany jest adres, a nie wartość zmiennej. Ten sposób przesyłania argumentów można z powodzeniem zastosować do przypadku gdy zmiennymi są tablice. Jeśli do funkcji przesyłamy całą tablicę to nie przesyłamy kolejno wszystkich elementów, ale jedynie jej adres, czyli jeśli mamy funkcje korzystającą z tablicy:
void fun_tab(int tab[]);
int tab[10];
to wywołanie takiej funkcji jest następujące:
fun_tab(tab);
pamiętamy, ze nazwa tablicy jest jej adresem. Ponieważ do funkcji przesłany został adres tablicy to wszystkie operacje w niej wykonywane będą się odbywać na orginale, anie na kopii. Po wysłaniu tablicy do funkcji może ona ją odebrać na dwa sposoby:
- jako tablicę
- jako wskaźnik. Przesłany adres służy do inicjacji lokalnego wskaźnika
#include
void f1(*wsk1,n);
void f2(tab[],n);
main()
{
int tablica[4] = {1,2,3,4};
.............// inicjacja tablicy lub wypełnienie
f1(tablica,4);
f2(tablica,4);
}
void f1(int *wsk1,li)
{
for(int i = 0;i
cout<<*(wsk++);
}
void f2(int tab[],n)
{
for(int i = 0;i
cout<
}
Wskaźniki do funkcji
Jak dotychczas wspominaliśmy, wskaźnikami możemy pokazywać na różne obiekty. nic nie stoi więc na przeszkodzie, aby wskazywać na funkcje. Ponieważ wskaźnik zawiera adres, wskaźnik do funkcji wskazuje więc na obszar gdzie zaczyna się kod będący instrukcjami żądanej funkcji. Zapis tego wskaźnika miałby postać:
int (*wsk_fun)()
co oznaczałoby, że wsk_fun jest wskaźnikiem do funkcji bezargumentowej zwracającej wartość typu int. Istotne są tu nawiasy bo zapis:
int *wsk_fun()
oznaczałby deklarację funkcji (a nie wskaźnika do funkcji) bezargumentowej zwracającej wskaźnik do int.
void test(char *p) //deklaracja funkcji
{
cout <
}
void (*tun_test)(char *) //wskaźnik do funkcji
main()
{
fun_test = test; // fun_test wskazuje na funkcje test
(*fun_test)("to jest test");
}
Do zmiennej fun_test wstawiamy adres funkcji test. Poniewaź nazwa funkcji jest jej adresem jak to było w przypadku tablic, operowanie wskaźnikami do funkcji jest stosunkowo proste.:
wskaxnik = nazwa_funkcji;
Nie ma tu nawiasów bo oznaczałoby to : wywołaj funkcje o takiej nazwie
Istnieją dwa sposoby zrealizowania funkcji:
- przez jej wywołanie: test("to jest test");
- lub przez pobranie jej adresu:
(*fun_test)("to jest test");
lub
fun_test("to jest test"):
Zapisy te są równoważne. Należy pamiętać, że typ wskaźnika do funkcji musi się zgadzać z typem funkcji na która wskazuje:
char fun(int,int);
imt (*wsk)(); wsk = fun; //niezgodność typów wskaźnika i funkcji
Na wskaźnikach do funkcji nie wolno wykonywać żadnych operacji arytmetycznych.
Kiedy więc przydają się wskaźniki do funkcji:
1)przy przekazywaniu funkcji jako argumentu innej funkcji
int f1(int a,void (*wsk_f2)());
void f2();
main()
{
int i;
............
i = f1(1,f2);
.......
}
int f1(int a, void (*wsk_f2)())
{
........
(*wsk_f2)();
..............
}
void f2() {...}
2)do tworzenia tablic wskaźników do funkcji
void f1(){...}
void f2(){...}
void f3(){...}
main()
{
int wsk;
void (*tab_fun[3])() = {f1,f2,f3}; //tab_fun jest 3-elementową tablica wskaźników do //funkcji bezargumentowej void.
cout<<"menu\n\t";
cout<<"1 - akcja 1\n\t";
cout<<"2 - akcja 2\n\t";
cout<<"3 - akcja 3\n\t";
cout<<"4 - koniec";
cout<<"wybierz opcje";
cin>>wsk;
switch(wsk)
{
case 1:
case 2:
case 3:
(*tab_fun[wsk])();break;
case 4: exit(1);
default:break;
}
}
Na zakończenie omawiania funkcji wspomnieć należy o funkcji main z argumentami. Wczesniej mówiliśmy, że main jest taką samą funkcja jak inne, w zwiazku z tym może wystepować z argumentami , nie tak jak to było dotychczas.
W środowisku języka C, do uruchamianego programu można przekazać parametry, czyli argumenty wywołania, w wierszu polecenia wywołujacego program. Działanie programu rozpoczyna się wtedy wywolaniem funkcji main z dwoma argumentami. Pierwszy umownie nazywany argc(argument counter) jest liczbą argumentów z jakimi program został wywołany. Drugi argument argv(argument vector) jest wskaźnikiem do tablicy zawierającej argumenty, każdy jako odrebny tekst.
Przykład:
#include
#include
#include
int main(int argc, char *argv[])
{
char zrodlo[13],cel[13];
if(argc !=3)
{
cout<<"niepoprawna liczba argumentów";
exit(1);
}
strcpy(zrodlo,argv[1]);
strcpy(cel,argv[2]);
if(strcmp(zrodlo,cel) == 0)
{
cout<<"Ta sama nazwa zbioru wejściowego i wyjściowego";
exit(1);
}
// wejście
ifstream pl_zrodlo(argv[1]);
if(!pl_zrodlo)
{
cout<<"Blad otwarcia zbioru wejściowego";
exit(1);
}
// wyjście
ofstream pl_cel(argv[2]);
if(!pl_cel)
{
cout<<"Blad otwarcia zbioru wyjściowego";
exit(1);
}
int c;
while((c = pl_zrodlo.get()) != EOF)
{
if(!pl_cel.put(c))
{
cout<<"blad zapisu do pliku";
break;
}
}
if(pl_zrodlo.eof)
cout<<"EOF pliku zródłowego - KONIEC ODCZYTU";
return 0;
}
W tablicy argumentów są dwa elementy specjalne:
argv[0] - nazwa programu
argv[argc] - 0 tablica kończy sie zerem.