Zmienne wskaźnikowe, koncepcja, podstawowe
Transkrypt
Zmienne wskaźnikowe, koncepcja, podstawowe
Podstawy programowania Część siódma Zmienne wskaźnikowe — wprowadzenie Autor Roman Simiński Kontakt [email protected] www.programowanie.siminskionline.pl Niniejsze opracowanie zawiera skrót treści wykładu, lektura tych materiałów nie zastąpi uważnego w nim uczestnictwa. Opracowanie to jest chronione prawem autorskim. Wykorzystywanie jakiegokolwiek fragmentu w celach innych niż nauka własna jest nielegalne. Dystrybuowanie tego opracowania lub jakiejkolwiek jego części oraz wykorzystywanie zarobkowe bez zgody autora jest zabronione. Co to jest zmienna — przypomnienie Zmienna Zmienna jest jest obiektem obiektem w w programie, programie, rezydującym rezydującym w w pamięci pamięci operacyjnej, operacyjnej, przeznaprzeznaczonym czonym do do przechowywania przechowywania wartości wartości pewnego pewnego typu. typu. Każda zmienna ma swoją nazwę, oraz typ wartości. Zmienne są przechowywane w pamięci operacyjnej, liczba zajętych bajtów zależy od typu zmiennej. Nazwa zmiennej identyfikuje zmienną w programie zwalniając programistę od zastanawiania się, pod jakim adresem w pamięci zmienna jest zlokalizowana. Adres w pamięci int i; Nazwa zmiennej ... 02c4a45fh 10 i i = 10; Wartość zmiennej ... 2 Dziwne pojęcia ― l-wartość i r-wartość Obiekt Obiekt jest jest pewnym pewnym nazwanym nazwanym obszarem obszarem pamięci. pamięci. Pod Pod pojęciem pojęciem l-wartości l-wartości rozumiemy rozumiemy wyrażenie wyrażenie lokalizujące lokalizujące ten ten obiekt obiekt w w pamięci pamięci Zmienna może występować po lewej stronie operatora przypisania, mówi się, że jest wtedy l-wartością. Wszystko, co może występować po prawej stronie operatora przypisania jest r-wartością. int int . . j = i = 5 = i; j; . 5; j; i; jj to to l-wartość l-wartość 55 to to r-wartość r-wartość ii to to l-wartość l-wartość jj to to r-wartość r-wartość Nie Nie każda każda r-wartość r-wartość to to l-wartość l-wartość 3 Zmienne wskaźnikowe — motywacja do nauki W języku C intensywnie wykorzystuje się l-wartości oparte na zmiennych wskaźnikowych oraz na wyrażeniach te zmienne zawierających. Dokładne opanowanie zasad posługiwania się wskaźnikami jest niezbędne do efektywnego i sprawnego programowania w C i C++. Tej umiejętności nie można pominąć, przeskoczyć lub zostawić na później. Nie oszukujmy się — ten, kto nie opanuje zasad posługiwania się wskaźnikami nigdy nie będzie prawdziwym, profesjonalnym programistą, wykorzystującym język C lub C++. Koncepcja Koncepcja wskaźników wskaźników oraz oraz metody metody ich ich wykorzystania wykorzystania są są proste. proste. Wymagają Wymagają one one jednak jednak uwagi, uwagi, zrozumienia zrozumienia ii myślenia. myślenia. 4 Po co są zmienne wskaźnikowe? Zmienna wskaźnikowa przeznaczona jest do lokalizowania (inaczej wskazywania) obiektów w pamięci operacyjnej. Jedyną rolą zmiennej wskaźnikowej jest umożliwienie odwoływania się do obiektów wskazywanych. Pamięć operacyjna Obiekt wskazywany Zmienna wskaźnikowa Zmienna wskaźnikowa może lokalizować w pamięci operacyjnej: inne zmienne, nienazwane bloki pamięci, bloki zawierające kod programu, np. funkcje. 5 Czym jest zmienna wskaźnikowa? Zmienna wskaźnikowa rezyduje w pamięci operacyjnej. Sama zmienna wskaźnikowa może być również „wskazywana” przez inną zmienną wskaźnikową. Pamięć operacyjna Obiekt wskazywany Zmienna wskaźnikowa Zmienna wskaźnikowa 6 Trzy stany zmiennej wskaźnikowej Zmienna wskaźnikowa wskazuje na konkretny obiekt w pamięci: Pamięć operacyjna Obiekt wskazywany Zmienna wskaźnikowa OK Zmienna wskaźnikowa nie wskazuje na żaden obiekt: Pamięć operacyjna OK Zmienna wskaźnikowa Zmienna wskaźnikowa wskazuje na nie wiadomo co: Pamięć operacyjna Zmienna wskaźnikowa ? Kiepsko 7 Co zawiera zmienna wskaźnikowa? Zwykle przyjmuje się, że zmienna wskaźnikowa zawiera w sobie adres obiektu wskazywanego. Jednak zmienna wskaźnikowa nie musi w sobie zawierać adresu bezpośredniego (fizycznego). Zawartość zmiennej wskaźnikowej może zawierać inną informację, pozwalającą na precyzyjne i jednoznaczne zidentyfikowanie położenia obiektu w pamięci. Pamięć operacyjna Zmienna wskaźnikowa 345fa012h Obiekt wskazywany Adres: 345fa012h 8 Przykład implementacji zmiennej wskaźnikowej — Intel 8086 Zmienna wskaźnikowa zawiera przesunięcie (ang. offset) obiektu względem początku segmentu gdy wskaźniki są „krótkie” (ang. near) — odwołania wewnątrz segmentu. Przesunięcie ... 02c4a45fh +0 +1 +2 +3 +4 Obiekt „Krótka” zmienna wskaźnikowa 0003 ... ... Segment Adres segmentu 9 Przykład implementacji zmiennej wskaźnikowej — Intel 8086 Zmienna wskaźnikowa zawiera adres segmentu i przesunięcie obiektu gdy wskaźniki są „długie” (ang. far) — odwołania międzysegmentowe. Przesunięcie ... 02c4a45fh +0 +1 +2 +3 +4 Obiekt „Długa” zmienna wskaźnikowa 02c4a45f:0003 ... ... Segment Adres segmentu 10 Deklaracja zmiennej wskaźnikowej Deklarowana Deklarowana zmienna zmienna będzie będzie wskaźnikiem, wskaźnikiem, kompilator kompilator wie, wie, ile ile dla dla niej niej zarezerwować zarezerwować pamięci. pamięci. To To oznacza, oznacza, że że deklarowana deklarowana zmienna zmienna wskaźnikowa wskaźnikowa będzie będzie przeznaczona przeznaczona do do lokalizowania lokalizowania w w pamięci pamięci obiektów obiektów typu typu int. int. int int i = 10; int * pi; * pi ; Nazwa Nazwa deklarowanej deklarowanej zmiennej zmiennej wskaźnikowej wskaźnikowej zbudowana zbudowana wg. wg. zwykłych zwykłych reguł, reguł, często często zawiera zawiera pp lub lub ptr ptr od od pointer. pointer. int i = 10; int * pi = 0; Nazwa zmiennej Nazwa zmiennej ... 10 i ... i Wartość zmiennej 10 Wartość zmiennej pi ? ... ? pi ... 11 Rola wskaźnika pustego NULL Tak zdefiniowana zmienna wskaźnikowa: int * pi; pi ? ? ma wartość początkową zależną od kontekstu deklaracji. Jeżeli ta zmienna jest klasy auto, to jej wartość jest przypadkowa — zmienna „wskazuje” zatem na bliżej nieznany obiekt w pamięci. W pliku nagłówkowym stddef.h zdefiniowana stałą NULL, reprezentującą wskaźnik pusty, niezależny od platformy i implementacji. Tak zdefiniowana zmienna: int * pi = NULL; pi jest wskaźnikiem pustym, a więc nie wskazuje żadnego obiektu w pamięci. W języku C++ preferuje się wykorzystanie wartości 0 zamiast stałej NULL. int * pi = 0; pi 12 Wartość NULL kontra 0 Stała NULL jest definiowana jako wartość 0 lub 0L. Można zatem zamiast wartością NULL, posługiwać się wartością 0. W W języku języku CC praktykuje praktykuje stosowanie stosowanie wartości wartości NULL NULL aa nie nie wartości wartości 0. 0. W W języku języku C++ C++ praktykuje praktykuje stosowanie stosowanie wartości wartości 00 zamiast zamiast NULL. NULL. Niezależnie Niezależnie od od przyjętej przyjętej wartości wartości wskaźnika wskaźnika pustego, pustego, jawnie jawnie inicjowanie inicjowanie zmiennych zmiennych wskaźnikowych wskaźnikowych oraz oraz posługiwanie posługiwanie się się wartością wartością pustą pustą dla dla wskaźników wskaźników niezakotwiczonych niezakotwiczonych jest jest dobrą dobrą praktyką praktyką programistyczną programistyczną w w języku języku CC ii C++. C++. To, czy zmienna wskaźnikowa jest wskaźnikiem pustym można sprawdzić: if( pi != NULL ) { // Tu jakieś operacje na obiekcie // wskazywanym przez pi . . . } if( pi == NULL ) { // Nie odwołujemy się do obiektu // wskazywanego przez pi — nie ma go! . . . } 13 Przypisywanie wartości zmiennym wskaźnikowym ... int i = 10; int * pi = 0; i ... int i = 10; int * pi = 0; . . . pi = &i; 10 10 pi pi ... Od Od momentu momentu tego tego przypisania, przypisania, pi pi wskazuje wskazuje zmienną zmienną i,i, umożliwiając umożliwiając realizację realizację dowolnych dowolnych operacji operacji na na tej tej zmiennej. zmiennej. i ... pi = & i ; Wyrażenie Wyrażenie wskaźnikowe wskaźnikowe lokalizujące lokalizujące zmienną zmienną ii w w pamięci. pamięci. Jednoargumentowy Jednoargumentowy operator operator & & buduje buduje wyrażenie wyrażenie wskaźnikowe wskaźnikowe lokalizujące lokalizujące zmienną zmienną w w pamięci pamięci operacyjnej. operacyjnej. Argument Argument musi musi być być l-wartością, l-wartością, nie nie odnoszącą odnoszącą się się do do obiektu obiektu register register ani ani pola pola bitowego. bitowego. 14 Odwoływanie się do obiektu wskazywanego ... int i = 10; int * pi = 0; . . . pi = &i; *pi = 20; i 20 *pi == i pi ... * pi Ten Ten zapis zapis oznacza oznacza obiekt obiekt wskazywany wskazywany przez przez pi. pi. Zapis Zapis *pi *pi może może wystąpić wystąpić wszędzie wszędzie tam, tam, gdzie gdzie może może wystąpić wystąpić i. i. = 20 ; Jednoargumentowy Jednoargumentowy operator operator adresowania adresowania pośredniego pośredniego *, *, daje daje w w wyniku wyniku obiekt obiekt wskazywany wskazywany przez przez argument argument pi. pi. Dowolne Dowolne wyrażenie wyrażenie typu typu zgodnego zgodnego zz typem typem obiektu obiektu wskazywanego. wskazywanego. 15 Odwoływanie się do obiektu wskazywanego, uwagi Po przypisaniu: pi = &i; te fragmenty kodu są równoważne: cin >> *pi; cin >> i; if( *pi == 0 ) cout << "Bledna wartosc"; else { y = *pi * x; cout << "Wynik: " << y; } if( i == 0 ) cout << "Bledna wartosc"; else { y = i * x; cout << "Wynik: " << y; } Jeżeli Jeżeli wskaźnik wskaźnik pi pi wskazuje wskazuje na na zmienną zmienną i,i, to to *pi *pi może może wystąpić wystąpić wszędzie wszędzie tam, tam, gdzie gdzie może może wystąpić wystąpić i.i. Zmienna Zmienna pi pi jest jest linkiem linkiem (odnośnikiem) (odnośnikiem) do do zmiennej zmiennej i,i, aa wyrażenie wyrażenie *pi *pi jest jest aliasem aliasem (alternatywną (alternatywną nazwą) nazwą) zmiennej zmiennej i.i. 16 Jednoargumentowe operatory & i * — podsumowanie Operatory & i * występują jako jedno i dwuargumentowe. W wersji dwuargumentowej oznaczają odpowiednio bitową koniunkcję i iloczyn arytmetyczny. W wersji jednoargumentowej oznaczają operacje wskaźnikowe. Wyrażenie &coś_tam oznacza „gdzie jest coś_tam” — operator & to zatem lokalizator lub pobieracz adresu. Wyrażenie *wskaźnik oznacza „obiekt lokalizowany przez wskaźnik” — operator * to zatem ekstraktor (wydobywacz) obiektu wskazywanego. Wydobywanie obiektu wskazywanego nazywa się dereferencją wskaźnika. 17 Typowe zastosowania zmiennych wskaźnikowych Realizacja przekazywania parametrów przez zmienną. Wykorzystanie pamięci zarządzanej dynamicznie. Manipulowanie tablicami (osobny wykład). Budowa rekurencyjnych struktur danych (osobny wykład). 18 Przypomnienie: przekazywanie parametrów przez wartość void inc( int i ) { i = i + 1; } . . . int a = 5; inc( a ); cout << a; Przed wywołaniem inc( a ) a 5 Wywołanie inc( a ) Wykonanie inc( a ) a 5 a i 5 i 5 6 Po wykonaniu inc( a ) a 5 X 5 i=i+1 19 Przypomnienie: przekazywanie parametrów przez referencję (tylko C++) void inc( int & i ) { i = i + 1; } Parametr Parametr formalny formalny ii jest jest referencją referencją do do parametru . . . parametru aktualnego aktualnego wywołania wywołania funkcji. funkcji. int a = 5; inc( a ); cout << ”a = ” << a; Przed wywołaniem inc( a ) a 5 Wywołanie inc( a ) a 5 Wykonanie inc( a ) i a 6 X 5 Po wykonaniu inc( a ) i a 5 6 i=i+1 20 Wskaźniki a przekazywanie parametrów prawie jak przez referencję void inc( int * i ) { *i = *i + 1; } Parametr Parametr formalny formalny ii jest jest wskaźnikiem. wskaźnikiem. Parametr . . . Parametr aktualny aktualny wywołania wywołania również. również. int a = 5; inc( &a ); cout << a; Przed wywołaniem inc( a ) a 5 Wywołanie inc( a ) a 5 Wykonanie inc( a ) *i == a a 6 X 5 Po wykonaniu inc( a ) *i == a a 5 6 *i=*i+1 i i 21 Wskaźniki a przekazywanie parametrów W W języku języku CC wykorzystuje wykorzystuje się się parametry parametry będące będące wskaźnikami wskaźnikami do do realizacji realizacji przekazywania przekazywania parametrów parametrów działającego działającego podobnie podobnie do do przekazywania przekazywania przez przez referencję. referencję. Przykład przekazywania parametrów za pośrednictwem wskaźnika: void zamien( int * pierwszy , int * drugi ) { int s; // Schowek aa 5 s = *pierwszy; *pierwszy = *drugi; *drugi = s; } int a = . . . cout << zamien( cout << ss bb 5 10 5, b = 10; "\na=" << a << " " << "b=" << b; &a, &b ); "\na=" << a << " " << "b=" << b; a=5 b=10 a=10 b=5 22 Wskaźniki a przekazywanie parametrów — modyfikacja wewnątrz funkcji W W języku języku C/C++ C/C++ często często wykorzystuje wykorzystuje się się parametry parametry wskaźnikowe wskaźnikowe po po to, to, żeby żeby przekazywanie przekazywanie parametrów parametrów odbywało odbywało się się szybciej szybciej ii nie nie zabierało zabierało dodatkowej dodatkowej pamięci, pamięci, jednocześnie jednocześnie nie nie oczekuje oczekuje się, się, że że wnętrze wnętrze funkcji funkcji będzie będzie modyfikować modyfikować parametr parametr przekazywany przekazywany za za pośrednictwem pośrednictwem wskaźnika. wskaźnika. Dzieje się tak szczególnie wtedy, gdy przekazywany parametr jest duży. Przekazanie dużego parametru za pośrednictwem wskaźnika jest rzeczywiście szybsze i nie powoduje konieczności utworzenia kopii (oszczędzamy pamięć) w parametrze formalnym i skopiowania zawartości parametru aktualnego do parametru formalnego (oszczędzamy czas). W C++ w tym samym celu wykorzystuje się parametry referencyjne. Załóżmy na chwilę, że dana typu double jest duża i opłaca się ją przekazywać do wnętrz funkcji via wskaźnik, nie chcąc jednocześnie modyfikować jej zawartości we wnętrzu tej funkcji... . Języki programowania obiektowego i graficznego 23 | Wskaźniki a przekazywanie parametrów — modyfikacja wewnątrz funkcji Załóżmy również, że korzystamy z napisanych przez kogoś innego funkcji, które nie posiadają dokumentacji, znamy tylko ich prototypy. int main() { double cena = 100; doliczVat23IWypisz( &cena ); ksiegujKwoteNetto( &cena ); . . . Czy tam w środku nie zmodyfikują mi czasem ceny? } Prototypy: void doliczVat23IWypisz( double * cenaNetto ); void ksiegujKwoteNetto( double * cenaNetto ); . . . Czy obawy są uzasadnione? Języki programowania obiektowego i graficznego 24 | Wskaźniki a przekazywanie parametrów — modyfikacja wewnątrz funkcji Tak! Prototyp wprost nie mówi niczego o realizacji funkcji! void doliczVat23IWypisz( double * cenaNetto ); Realizacja może być taka (źle): void doliczVat23IWypisz( double * cenaNetto { *cenaNetto *= 1.23; ) cout << *cenaNetto; } Realizacja może też być taka (dobrze): void doliczVat23IWypisz( double * cenaNetto { cout << *cenaNetto * 1.23; } ) Języki programowania obiektowego i graficznego 25 | Wskaźniki a przekazywanie parametrów — modyfikator const Aby ustrzec się przed niezamierzoną modyfikacją parametru przekazywanego przez wskaźnik, można użyć słowa kluczowego const. Umieszczenie const przed typem obiektu wskazywanego jest obietnicą tego, że będzie on obiektem niemodyfikowalnym (read-only). Funkcja nieskutecznie usiłuje zmodyfikować obiekt wskazywany przez cenaNetto: void doliczVat23IWypisz( const double * cenaNetto { *cenaNetto *= 1.23; cout << *cenaNetto; } ) error: assignment of read-only location Funkcja nie modyfikuje obiektu wskazywanego przez cenaNetto: void doliczVat23IWypisz( const double * cenaNetto { cout << *cenaNetto * 1.23; } ) Języki programowania obiektowego i graficznego 26 | Wskaźniki a przekazywanie parametrów — modyfikator const Wystąpienie słowa kluczowego const jest obietnicą, że wnętrze funkcji nie będzie modyfikować obiektu wskazywanego. Ta informacja umieszczona w prototypie pozwala oczekiwać, że parametr nie zostanie zmodyfikowany w sposób niezamierzony. void doliczVat23IWypisz( const double * cenaNetto ); void doliczVat23IWypisz( const double * cenaNetto { cout << *cenaNetto * 1.23; } ) Jest const, to już chyba śpię spokojnie... Czy rzeczywiście można spać spokojnie? Języki programowania obiektowego i graficznego 27 | Wskaźniki a przekazywanie parametrów — modyfikator const Nie! Wystąpienie słowa kluczowego const jest obietnicą, że wnętrze funkcji nie będzie modyfikować obiektu wskazywanego w sposób niezamierzony. Ale można w sposób zamierzony tej obietnicy nie dotrzymać! void doliczVat23IWypisz( const double * cenaNetto { *( ( double * ) cenaNetto ) *= 1.23; ) cout << *cenaNetto; } Taka sytuacja to zamierzona złośliwość, całe szczęście nie spotyka się jej zbyt często. Rzutowanie Rzutowanie wskaźnika wskaźnika typu typu const const double double ** na na double double ** „zdejmujące” „zdejmujące” atrybut atrybut read-only. read-only. Ale skoro to nie jest niemożliwe... . Języki programowania obiektowego i graficznego 28 | Parametry referencyjne w C++ też mogą być const Wystąpienie słowa kluczowego const jest obietnicą, że wnętrze funkcji nie będzie modyfikować obiektu referencyjnego. Ta informacja umieszczona w prototypie pozwala oczekiwać, że parametr nie zostanie zmodyfikowany w sposób niezamierzony. void doliczVat23IWypisz( const double & cenaNetto ); void doliczVat23IWypisz( const double & cenaNetto { cout << cenaNetto * 1.23; } ) Jest const, to już chyba śpię spokojnie... Czy rzeczywiście można spać spokojnie? Języki programowania obiektowego i graficznego 29 | Parametry referencyjne w C++ też mogą być const Nie! Wystąpienie słowa kluczowego const jest obietnicą, że wnętrze funkcji nie będzie modyfikować obiektu referencyjnego w sposób niezamierzony. Ale można w sposób zamierzony tej obietnicy nie dotrzymać! void doliczVat23IWypisz( const double & cenaNetto { ( double & )cenaNetto *= 1.23; ) cout << cenaNetto; } Zatem referencje w C++ też pozwalają nieźle zamieszać! Rzutowanie Rzutowanie referencji referencji typu typu const const double double & & na na double double & & „zdejmujące” „zdejmujące” atrybut atrybut read-only. read-only. Języki programowania obiektowego i graficznego 30 | Wariacje na temat wskaźników i słowa kluczowego const Można modyfikować wartość wskaźnika p, można modyfikować obiekt wskazywany *p: int i = 10; int * p; // . . . p = &i; *p = 20; cout << *p; Zwykły wskaźnik na zwykły obiekt // Modyfikowalny wskaźnik // Modyfikowalny obiekt // Wolno odczytywać wartość wskaźnika i obiektu Można modyfikować wartość wskaźnika p, nie można modyfikować obiektu wskazywanego *p, który staje się obiektem tylko do odczytu: int i = 10; const int * . . . p = &i; *p = 20; cout << *p; p; // Zwykły wskaźnika na niemodyfikowalny obiekt // Modyfikowalny wskaźnik // Niemodyfikowalny obiekt // Wolno odczytywać wartość wskaźnika i obiektu Języki programowania obiektowego i graficznego 31 | Wariacje na temat wskaźników i słowa kluczowego const Nie można modyfikować wartość wskaźnika p, można modyfikować obiekt wskazywany *p, wskaźnik p jest zakotwiczony „na zawsze”: int i = 10; int * const . . . p = &i; *p = 20; cout << *p; p = &i; // Ustalony wskaźnika na modyfikowalny obiekt // Inicjalizacja takiego wskaźnika jest obowiązkowa // Niemodyfikowalny wskaźnik // Modyfikowalny obiekt // Wolno odczytywać wartość wskaźnika i obiektu Nie można modyfikować wartość wskaźnika p, nie można modyfikować obiektu wskazywanego *p, wskaźnik p jest zakotwiczony „na zawsze” o obiekt read-only: int i = 10; const int * . . . p = &i; *p = 20; cout << *p; const p = &i; // Ustalony wskaźnika na niemodyfikowalny obiekt // Inicjalizacja takiego wskaźnika jest obowiązkowa // Niemodyfikowalny wskaźnik // Modyfikowalny obiekt // Wolno odczytywać wartość wskaźnika i obiektu Języki programowania obiektowego i graficznego 32 | Wskaźniki typu void * Typ void * oznacza wskazanie niezwiązane z żadnym typem. Wskaźnik takiego typu może wskazywać daną dowolnego typu. float int char f = 2.5; i = 5; c = 'A'; void * ptr; ptr = &f; . . . ptr = &i; . . . ptr = &c; . . . Wskaźnik Wskaźnik ptr ptr może może pokazywać pokazywać na na obiekty obiekty różnych różnych typów. typów. Uwaga! Uwaga! Po Po przypisaniu przypisaniu do do wskaźnika wskaźnika typu typu void void ** tracimy tracimy informację informację oo typie typie obiektu obiektu wskazywanego. wskazywanego. Dlatego Dlatego operacja operacja *ptr *ptr nie nie ma ma sensu sensu — — kompilator kompilator nie nie wie, wie, czym czym jest jest obiekt obiekt wskazywany, wskazywany, ile ile zajmuje zajmuje bajtów bajtów w pamięci w pamięci operacyjnej. operacyjnej. Wiadomo Wiadomo tylko, tylko, gdzie gdzie taki taki obiekt obiekt jest. jest. 33 Wskaźniki typu void *, cd. ... Nie można wprost odwoływać się do obiektu wskazywanego przez wskaźnika void * — inaczej mówiąc, nie można dokonać dereferencji takiego wskaźnika. Aby odwołać się do obiektu wskazywanego, należy poinformować kompilator jaki jest jego typ, dokonując konwersji (tzw. rzutowania) typu wskaźnika. void * ptr; ptr = &f; cout << endl << *( ( float * ) ptr ); Rzutowanie Rzutowanie wskaźnika wskaźnika ptr ptr ― ― wskazanie wskazanie na na obiekt obiekt typu typu float. float. void * ptr; ptr = &f; cout << endl << * ( float * ) ptr; 34 Wskaźniki typu void *, cd. ... Wskaźnik void * można rzutować na różne typy: float f = 2.5; int i = 5; char c = 'A'; void * ptr; ptr = &f; cout << endl << *( ( float * )ptr ); ptr = &i; cout << endl << *( ( int * )ptr ); ptr = &c; cout << endl << *( ( char * )ptr ); 35 Dynamiczny przydział pamięci — pojęcie sterty, sterta a stos Sterta (ang. heap) to wydzielony obszar pamięci wolnej: ... przeznaczony do przechowywania danych dynamicznych, ... Sterta kontrolowany ręcznie przez programistę, ograniczony pod względem rozmiaru, Stos (ang. stack) to wydzielony obszar pamięci roboczej: ... Dane przydzielany pasującymi fragmentami. Stos przeznaczony do przechowywania danych automatycznych, nie jest bezpośrednio kontrolowany przez programistę, ograniczony pod względem rozmiaru, ... przydzielany wg. zasady LIFO (ang. last in, first out). 36 Dynamiczny przydział pamięci Dynamiczny Dynamiczny przydział przydział pamięci pamięci polega polega na na zarezerwowaniu zarezerwowaniu fragmentu fragmentu pamięci pamięci w w obszarze obszarze pamięci pamięci wolnej wolnej (sterty), (sterty), dla dla obiektu obiektu pamięciowego pamięciowego zwanego zwanego dynamicznym. dynamicznym. Typowy scenariusz wykorzystania dynamicznego przydziału pamięci: Określenie wielkości potrzebnego obszaru pamięci. Przydział pamięci i zapamiętanie wskazania tego obszaru w zmiennej wskaźnikowej. Sprawdzenie czy przydział pamięci się powiódł, jeżeli tak to: Wykorzystanie przydzielonego bloku pamięci. Zwolnienie przydzielonego bloku pamięci, gdy nie jest już potrzebny. 37 Dynamiczny przydział pamięci w języku C++ — etap I ... Definicja zmiennej wskaźnikowej p, zainicjowanej wskaźnikiem pustym. ... Sterta int main() { int * p = 0; } ... Stos if( p != 0 ) { *p = 10; . . . cout << ++(*p); . . . delete p; } . . . Dane p = new int; p ... 38 Dynamiczny przydział pamięci w języku C++ — etap II ... ... Sterta Operator new przydziela na stercie blok pamięci dla danej typu int. Rezultatem funkcji jest wskaźnik do przydzielonego obszaru lub 0 jeżeli polecenie nie może być zrealizowane (czasem jest inaczej, o tym za chwilę). int main() { int * p = 0; ... Stos if( p != 0 ) { *p = 10; . . . cout << ++(*p); . . . delete p; } . . . Dane p = new int; p ... } 39 Dynamiczny przydział pamięci w języku C++ — etap III ... ... Sterta Zawsze należy sprawdzić poprawność przydziału pamięci. Odwołanie do obiektu lokalizowanego przez wskaźnik pusty jest błędem. int main() { int * p = 0; ... Stos if( p != 0 ) { *p = 10; . . . cout << ++(*p); . . . delete p; } . . . Dane p = new int; p ... } 40 Dynamiczny przydział pamięci w języku C++ — etap IV ... 10 *p Sterta int main() { int * p = 0; ... Wykorzystanie przydzielonego bloku pamięci. Ponieważ zmienna wskaźnikowa p jest skojarzona z typem int, przydzielony obszar traktowany jest jak dana typu int. ... Stos if( p != 0 ) { *p = 10; . . . cout << ++(*p); . . . delete p; } . . . Dane p = new int; p ... } 41 Dynamiczny przydział pamięci w języku C — etap IV, cd. ... ... 11 *p Sterta int main() { int * p = 0; ... Z obiektem wskazywanym przez zmienną p można robić wszystko to, co dozwolone dla danej typu int. Wyrażenie ++(*p) zwiększa obiekt wskazywany przez zmienną p. ... Stos if( p != 0 ) { *p = 10; . . . cout << ++(*p); . . . delete p; } . . . Dane p = new int; p ... } 42 Dynamiczny przydział pamięci w języku C++ — etap V ... ... Sterta Wywołanie operatora delete powoduje zwolnienie bloku pamięci wskazywanego przez p, blok ten zwracany jest do puli bloków wolnych. Uwaga — po wywołaniu delete wskaźnik p dalej pokazuje na zwolniony blok pamięci! int main() { int * p = 0; ... Stos if( p != 0 ) { *p = 10; . . . cout << ++(*p); . . . delete p; } . . . Dane p = new int; p ... } 43 Dynamiczny przydział pamięci w języku C++ — uwagi Mimo, że po wywołaniu free wskaźnik p dalej pokazuje na zwolniony obszar, próba odwołania się do niego jest błędem. ... Ten obszar być może został właśnie przydzielony ponownie. ... Sterta int main() { int * p = 0; ??? ... Stos if( p != 0 ) { *p = 10; . . . cout << ++(*p); . . . delete p; *p = 0; } Błąd. Błąd. Odwołanie Odwołanie do do zwolnionego zwolnionego *p = 100; lub lub nieprzydzielonego nieprzydzielonego bloku bloku . . . Dane p = new int; p ... } 44 Dynamiczny przydział pamięci w języku C++ — uwagi, cd. ... ... ... Sterta Dobrą praktyką jest zerowanie zmiennych wskaźnikowych na etapie ich deklaracji, po zwolnieniu pamięci oraz testowanie czy wskaźnik nie jest pusty przed odwołaniem do obiektu wskazywanego. int main() { int * p = 0; ... Stos if( p != 0 ) { *p = 10; . . . cout << ++(*p); . . . delete p; p = 0; } . . . Dane p = new int; p ... } 45 Dynamiczny przydział pamięci w języku C++ — dla int nie ma sensu... ... Dana typu TBitmap Sterta int main() { TBitmap * p = 0; ... Dynamiczny przydział pamięci dla pojedynczych danych typu int, char czy double najczęściej nie ma sensu. Ale ma sens dla obiektów zajmujących dużo pamięci operacyjnej oraz dla złożonych struktur danych. ... Stos if( p != 0 ) { Operacje na bitmapie wskazywanej przez p; . . . delete p; p = 0; } . . . Dane p = new TBitmap; p ... } 46 Dynamiczny przydział pamięci w języku C++, zaszłości Od standardu C++ z 2003 operator new działa inaczej. Aby zachować omówiony styl przydziału pamięci, należy użyć jego specjalnej wersji: nothrow. int main() { int * p = 0; } Stare Stare dzieje dzieje w w CC ++ ++ int main() { int * p = 0; Aktualnie Aktualnie w w CC ++ ++ p = new int; p = new (nothrow) int; if( p != 0 ) { *p = 10; . . . cout << ++(*p); . . . delete p; p = 0; } . . . if( p != 0 ) { *p = 10; . . . cout << ++(*p); . . . delete p; p = 0; } . . . } Aktualnie, gdy operator new nie potrafi przydzielić pamięci to generuje wyjątek, zamiast oddawania rezultatu w postaci wskaźnika pustego. 47 Dynamiczny przydział pamięci w języku C++, wyjątki Jeżeli aktualnie użyjemy operatora w wersji new, wygenerowany zostanie wyjątek klasy bad_alloc. int main() { try { int * p = new int; Aktualnie Aktualnie w w CC ++ ++ *p = 10; . . . cout << ++(*p); . . . delete p; p = 0; } catch( ... ) { // Zrob cos gdy brak pamieci } } Mechanizm Mechanizm obsługi obsługi wyjątków wyjątków oraz oraz zasady zasady stosowania stosowania try-catch try-catch zostaną zostaną omówione omówione osobno. osobno. 48 Zarządzanie pamięcią dynamiczną to rzecz podwójnie nieprosta Zarządzanie Zarządzanie pamięcią pamięcią rozgrywane rozgrywane na na poziomie poziomie kodu kodu programu programu wymaga wymaga uwagi uwagi od od programisty. programisty. To To pierwsza pierwsza rzecz. rzecz. Druga Druga jest jest po po stronie stronie systemu systemu operacyjnego. operacyjnego. Uczestniczy Uczestniczy on on w w przydziale przydziale pamięci pamięci dla dla procesów, procesów, oferując oferując pamięć pamięć wirtualną. wirtualną. W W rzeczywistości rzeczywistości bloki bloki pamięci pamięci naszego naszego programu programu mogą mogą czasem czasem znajdować znajdować się się na na dysku... dysku... .. Proces Proces zarządzania zarządzania pamięcią pamięcią wirtualną wirtualną bywa bywa czasem czasem bardzo bardzo złożony. złożony. Interesujące artykuły na temat zarządzania pamięcią: ● http://www.cprogramming.com/tutorial/virtual_memory_and_heaps.html ● http://www.ibm.com/developerworks/linux/library/l-memory/ ● http://www.cantrip.org/wave12.html ● http://linuxdevcenter.com/pub/a/linux/2003/05/08/cpp_mm-1.html 49 Wyrażenia wskaźnikowe Wskaźniki Wskaźniki lokalizują lokalizują obiekty obiekty w w pamięci pamięci operacyjnej. operacyjnej. Można Można budować budować wyrażenia wyrażenia zawierające zawierające wskaźniki, wskaźniki, wyrażenia wyrażenia te te lokalizują lokalizują również również pewne pewne obiekty obiekty w w pamięci pamięci operacyjnej. operacyjnej. W W językach językach C/C++ C/C++ obowiązuje obowiązuje specjalna specjalna arytmetyka arytmetyka na na wskaźnikach. wskaźnikach. // ASCII: 0x41 - A 0x42 - B short int n = 0x4241; char * p; p = ( char * )&n; ... n cout << endl << *p; 0x41 | A 0x42 | B ++p; cout << endl << *p; p ... 50 Wyrażenia wskaźnikowe Wskaźnik Wskaźnik pp lokalizuje lokalizuje młodszy młodszy bajt bajt zmiennej zmiennej m. m. Reszta Reszta tej tej liczby liczby nie nie jest jest dla dla wskaźnika wskaźnika pp „widoczna” „widoczna” ponieważ ponieważ służy służy on on do do lokalizowania lokalizowania znaków znaków (bajtów). (bajtów). Wyprowadzenie Wyprowadzenie obiektu obiektu wskazywanego wskazywanego przez przez pp do do cout cout spowoduje spowoduje potraktowanie potraktowanie młodszego młodszego bajtu bajtu zmiennej zmiennej m m jako jako znaku znaku ii wyprowadzenie wyprowadzenie go go do do strumienia strumienia wyjściowego. wyjściowego. // ASCII: 0x41 - A 0x42 - B short int n = 0x4241; char * p; p = ( char * )&n; ... n cout << endl << *p; 0x41 | A *p A 0x42 | B ++p; cout << endl << *p; p ... 51 Wyrażenia wskaźnikowe Do Do zmiennej zmiennej wskaźnikowej wskaźnikowej wolno wolno dodać dodać (odjąć) (odjąć) liczbę liczbę całkowitą. całkowitą. Takie Takie wyrażenie wyrażenie lokalizuje lokalizuje w w pamięci pamięci operacyjnej operacyjnej obiekt obiekt przesunięty przesunięty w w stosunku stosunku do do wskaźnika wskaźnika bazowego. bazowego. Wyrażenie Wyrażenie pp == pp ++ 11 przesuwa przesuwa wskaźnik wskaźnik do do następnego następnego obiektu obiektu w w pamięci, pamięci, wyrażenie wyrażenie pp == pp –– 11 do do poprzedniego poprzedniego obiektu, obiektu, zgodnie zgodnie zz typem typem wskaźnika. wskaźnika. Wyrażenia Wyrażenia te te można można oczywiście oczywiście zapisać: zapisać: ++p ++p oraz oraz --p. --p. // ASCII: 0x41 - A 0x42 - B short int n = 0x4241; char * p; p = ( char * )&n; ... n cout << endl << *p; 0x41 | A 0x42 | B ++p A ++p; cout << endl << *p; p ... 52 Wyrażenia wskaźnikowe Liczby Liczby dodawane dodawane lub lub odejmowana odejmowana od od wskaźnika wskaźnika są są skalowane skalowane przez przez rozmiar rozmiar typu typu wskaźnika. wskaźnika. Oznacza Oznacza to, to, że że dla dla char char ** pp operacja operacja ++p ++p spowoduje spowoduje przesunięcie przesunięcie wskaźnika wskaźnika do do następnego następnego znaku znaku w w pamięci pamięci operacyjnej, operacyjnej, aa dla dla int int ** pp operacja operacja ++p ++p spowoduje spowoduje przesunięcie przesunięcie wskaźnika wskaźnika do do następnej następnej liczby liczby całkowitej, całkowitej, itp. itp. // ASCII: 0x41 - A 0x42 - B short int n = 0x4241; char * p; p = ( char * )&n; ... n cout << endl << *p; 0x41 | A 0x42 | B *p A B ++p; cout << endl << *p; p ... 53 Arytmetyka na wskaźnikach — zasady Dozwolone operacje wskaźnikowe to: przypisywanie wskaźników do obiektów tego samego typu, przypisywanie wskaźników do obiektów innego typu po konwersji, dodawanie lub odejmowanie wskaźnika i liczby całkowitej, odejmowanie lub porównanie dwóch wskaźników, przypisanie wskaźnikowi wartości zero (lub wskazania puste NULL) lub porównanie ze wskazaniem pustym. 54 Wyrażenia wskaźnikowe — kolejny przykład // ASCII: 0x41 - A 0x42 - B 0x43 - C 0x44 - D int m = 0x44434241; // Zakładamy, że int jest 32 bitowy char * p; p = ( char * cout << endl ++p; cout << endl ++p; cout << endl ++p; cout << endl )&m; << *p ; << *p ; << *p ; A B C D << *p ; lub: // ASCII: 0x41 - A 0x42 - B 0x43 - C 0x44 - D int m = 0x44434241; // Zakładamy, że int jest 32 bitowy char * p; int i; for( i = 0, p = ( char * )&m; i < sizeof( int ); i++ ) cout << endl << *( p++ ); 55 Dla dociekliwych — funkcja ze zmienną liczbą parametrów int addInts( int count, ... ) { int total = 0; va_list argList; va_start( argList, count ); for( ; count; count-- ) total += va_arg( argList, int ); va_end( argList ); return total; } . . . cout cout cout cout << << << << endl endl endl endl << << << << "Suma: "Suma: "Suma: "Suma: " " " " << << << << addInts( addInts( addInts( addInts( 2, 1, 2 ); 3, 4, -1, 6 ); 0 ); 5, 1, 2, 3, 4, 5 ); Suma: Suma: Suma: Suma: 3 9 0 15 56 Dla dociekliwych — funkcja ze zmienną liczbą parametrów Aby Aby obsłużyć obsłużyć zmienną zmienną liste liste parametrów parametrów nie nie trzeba trzeba koniecznie koniecznie używać używać makr makr zz pliku pliku stdarg.h, stdarg.h, wystarczy wystarczy wiedzieć wiedzieć jak jak są są przekazywane przekazywane parametry parametry ii rozumieć rozumieć wskaźniki... wskaźniki... .. Poniżej Poniżej przykład przykład do do indywidualnego indywidualnego przemyślenia. przemyślenia. int addIntsOwn( int count, ... ) { int total = 0; char * argList; argList = (( char * )&count ) + sizeof( count ); for( ; count; count-- ) total += *( ( int * )( ( argList += sizeof( int ) ) - sizeof( int ) ) ); return total; } . . . cout cout cout cout << << << << endl endl endl endl << << << << "Suma: "Suma: "Suma: "Suma: " " " " << << << << addInts( addInts( addInts( addInts( 2, 1, 2 ); 3, 4, -1, 6 ); 0 ); 5, 1, 2, 3, 4, 5 ); Suma: Suma: Suma: Suma: 3 9 0 15 57 Wskaźniki do funkcji — koncepcja Skoro funkcje posiadają swoje adresy, to za możliwe jest operowanie na adresach funkcji z wykorzystaniem zmiennych wskaźnikowych. Sterta pisz 10001010 void pisz () { cout << ”Witaj!”; } 01010101 Kod 01001011 00110011 ... Dane Nazwa funkcji w językach C/C++ jest właśnie adresem funkcji w pamięci operacyjnej. ... Stos Każda funkcja w programie posiada określony adres, począwszy od tego adresu rozpoczyna się ciało funkcji w postaci kodu maszynowego. ... W trakcie uruchamiania programu, jego kod maszynowy odczytywany z pliku wykonywalnego, jest ładowany do pamięci operacyjnej. ... 58 Wskaźniki do funkcji — jak deklarować Wskaźniki do funkcji są deklarowane w specyficzny sposób. W deklaracji wskaźnika do funkcji należy precyzyjnie określić informacje o funkcji, jaką będzie mógł dany wskaźnik lokalizować. Te informacje obejmują: typ rezultatu funkcji, liczbę i typy kolejnych parametrów, Nazwy parametrów są nieistotne. 59 Wskaźniki do funkcji — jak deklarować Zakładamy, że wskazywana funkcja ma następującą definicję: void pisz() { cout << "Witaj!"; } Zmienną wskaźnikowa, która może lokalizować taką funkcję, deklarujemy : void ( *funPtr )(); co oznacza, że funPtr jest wskaźnikiem na bezparametrowe funkcje, które nie mają rezultatu (rezultat typu void). Nawiasy są niezbędne, bez nich następująca deklaracja: void *funPtr(); oznaczała by, że funPtr to nazwa bezparametrowej funkcji, której rezultatem jest wskaźnik typu void *. 60 Wskaźniki do funkcji — „kotwiczenie” wskaźnika Wskaźnik do funkcji może być zerowany na etapie deklarowania: pisz Wskaźnik do funkcji może być inicjowany na etapie deklarowania: 01001011 10001010 01010101 void ( *funPtr )() = &pisz; // Wersja nr 1 00110011 // Wersja nr 2 ... funPtr = &pisz; // Wersja nr 2 funPtr = pisz; // Wersja nr 2 Stos Po deklaracji, do wskaźnika można przypisywać wskazanie na funkcje pisząc: Dane void ( *funPtr )() = pisz; Kod void ( *funPtr )() = 0; Sterta ... ... funPtr ... 61 Po „zakotwiczeniu” wskaźnika o funkcję: ... funPtr = pisz; ... pisz można wywołać jej kod pisząc: Sterta Wskaźniki do funkcji — wywołanie funkcji via wskaźnik 10001010 lub: 01010101 Kod 01001011 ( *funPtr )(); // Wersja nr 1 00110011 W sensie semantycznym wskaźnik na funkcję i nazwa funkcji są tożsame, zatem wersja 1 powyżej jest niepotrzebnie skomplikowana, większość programistów wykorzysta wersję nr 2. ... Dane // Wersja nr 2 Stos funPtr(); funPtr ... 62 Wskaźniki do funkcji — wskaźniki bywają niezbyt wierne... Wskaźnik zadeklarowany w ten sposób: void ( *funPtr )() = 0; tak na prawdę, może pokazywać na dowolną bezparametrową funkcję, która nie ma rezultatu (rezultat typu void). void pisz() { cout << "\nWitaj!"; } void write() { cout << "\nHello!"; } void schreiben() { cout << "\nHallo!"; } funPtr = pisz; funPtr(); funPtr = write; funPtr(); funPtr = schreiben; funPtr(); Witaj! Hello! Hallo! 63 Wskaźniki do funkcji — tablicujemy kod? Tablica wskaźników na bezparametrowe funkcje nieudostępniające rezultatu: const int N = 3; void ( * funTab[ N ] )(); „Zakotwiczenie” kolejnych elementów tablicy o funkcje: funTab[ 0 ] = pisz; funTab[ 1 ] = write; funTab[ 2 ] = schreiben; funTab void pisz() { cout << "\nWitaj!"; } void write() { cout << "\nHello!"; } void schreiben() { cout << "\nHallo!"; } Wywołanie funkcji, lokalizowanych przez kolejne elementy tablicy: for( int i = 0; i < N; ++i ) funTab[ i ](); 64 Wskaźniki do funkcji — inicjalizacja tablicy wskaźników funkcyjnych . . . void pisz () { cout << "\nWitaj!"; } void write() { cout << "\nHello!"; } void schreiben() { cout << "\nHallo!"; } int main() { const int N = 3; void ( * funTab[ N ] )() = { pisz, write, schreiben }; for( int i = 0; i < N; ++i ) funTab[ i ](); . . . } 65 Wskaźniki do funkcji — a po co to wszystko? Jest wiele bardzo ciekawych zastosowań wskaźników do funkcji. Ich przykłady będą sukcesywnie omawiane. Jednym z nich określanie funkcji, która ma być wywołana we wnętrzu innej funkcji. Przykład — sortowanie tablic z wykorzystaniem bibliotecznej funkcji qsort (wymaga włączenia stdlib.h lub cstdlib). Funkcja qsort pozwala na sortuje metodą quick sort dowolną tablicę. Funkcja qsort to kwintesencja wykorzystania wskaźników, również do funkcji. 66 Wskaźniki do funkcji — qsort Prototyp funkcji qsort (może różnić się w zależności od kompilatora): Wskaźnik Wskaźnik na na obszar obszar pamięci, pamięci, zawierający zawierający dane dane do do posortowania. posortowania. void qsort( void *base , int nelem , int width , int ( *fcmp )( const void *, const void *) ); Liczba Liczba elementów elementów do do posortowania. posortowania. Wskaźnik Wskaźnik na na funkcję, funkcję, która która we we wnętrzu wnętrzu qsort qsort zostanie zostanie wykorzystana wykorzystana do do porównania porównania dwóch dwóch elementów elementów sortowanej sortowanej tablicy. tablicy. Wyrażony Wyrażony w w bajtach bajtach rozmiar rozmiar elementu elementu tablicy. tablicy. 67 Wskaźniki do funkcji — qsort w akcji #include <cstdlib> #include <iostream> using namespace std; ? int main() { const int N = 5; int tab[ N ] = { 5, 3, 4, 1, 2 }; qsort( tab, N, sizeof( tab[ 0 ] ), compInt ); for( int i = 0; i < N; ++i ) cout << endl << tab[ i ]; . . . } 68 Wskaźniki do funkcji — qsort, qsort, rola funkcji porównującej Funkcja qsort musi porównywać ze sobą pary elementów. Jednak funkcja ta przecież nie wie, jakie są elementy sortowanej tablicy. Programista musi zdefiniować odpowiednią funkcję porównującą i przekazać wskaźnik do tej funkcji do wnętrza funkcji qsort. Funkcja porównująca powinna mieć następującą postać: int jakasNazwa( const void * a, const void * b ) { . . . } Parametry a i b to wskaźniki na elementy do porównania. Rezultatem funkcji powinna być: wartość ujemna gdy a < b, wartość zero gdy a == b, wartość dodatnia gdy a > b. 69 Wskaźniki do funkcji — qsort w akcji, funkcja porównująca #include <cstdlib> #include <iostream> using namespace std; int compInt( const void * a, const void * b ) { return ( *( int * )a ) - ( *( int *)b ) ); } int main() { const int N = 5; int tab[ N ] = { 5, 3, 4, 1, 2 }; qsort( tab, N, sizeof( tab[ 0 ] ), compInt ); for( int i = 0; i < N; ++i ) cout << endl << tab[ i ]; . . . } 70 Suplement I: dynamiczny przydział pamięci w języku C Przydział pamięci realizują funkcje: void * malloc( size_t size ) void * calloc( size_t nitems, size_t size ) void * realloc( void * ptr, size_t size ) Obszary pamięci przydzielone tymi funkcjami należało zwolnić funkcją: void free( void * ptr ) Funkcje zarządzające przydziałem/zwalnianiem bloków pamięci operują na wskaźnikach void *. Przydzielane bloki są amorficzne ― są to „kawałki” pamięci o rozmiarze liczonym w bajtach. Wykorzystanie powyższych funkcji wymaga włączenia pliku nagłówkowego dyrektywą #include <stdlib.h> lub #include <cstdlib> w C++. 71 Suplement I: dynamiczny przydział pamięci w języku C — etap I ... Definicja zmiennej wskaźnikowej p, zainicjowanej wskaźnikiem pustym. ... Sterta int main() { int * p = NULL; } ... Stos if( p != NULL ) { *p = 10; . . . cout << ++(*p); . . . free( p ); } . . . Dane p = malloc( sizeof( int ) ); p ... 72 Suplement I: dynamiczny przydział pamięci w języku C — etap II ... Funkcja malloc przydzielan na stercie blok pamięci o rozmiarze sizeof(int). Rezultatem funkcji jest wskaźnik do przydzielonego obszaru lub NULL jeżeli polecenie nie może być zrealizowane. ... Sterta int main() { int * p = NULL; } ... Stos if( p != NULL ) { *p = 10; . . . cout << ++(*p); . . . free( p ); } . . . Dane p = malloc( sizeof( int ) ); p ... 73 Suplement I: dynamiczny przydział pamięci w języku C — etap III ... Zawsze należy sprawdzić poprawność przydziału pamięci. Odwołanie do wskaźnika pustego jest błędem. ... Sterta int main() { int * p = NULL; } ... Stos if( p != NULL ) { *p = 10; . . . cout << ++(*p); . . . free( p ); } . . . Dane p = malloc( sizeof( int ) ); p ... 74 Suplement I: dynamiczny przydział pamięci w języku C — etap IV ... 10 *p Sterta int main() { int * p = NULL; ... Wykorzystanie przydzielonego bloku pamięci. Ponieważ zmienna wskaźnikowa p jest skojarzona z typem int, przydzielony obszar traktowany jest jak dana typu int. } ... Stos if( p != NULL ) { *p = 10; . . . cout << ++(*p); . . . free( p ); } . . . Dane p = malloc( sizeof( int ) ); p ... 75 Suplement I: dynamiczny przydział pamięci w języku C — etap IV, cd. ... ... 11 *p Sterta int main() { int * p = NULL; ... Z obiektem wskazywanym przez zmienną p można robić wszystko to, co dozwolone dla danej typu int. Wyrażenie ++(*p) zwiększa obiekt wskazywany przez zmienną p. } ... Stos if( p != NULL ) { *p = 10; . . . cout << ++(*p); . . . free( p ); } . . . Dane p = malloc( sizeof( int ) ); p ... 76 Suplement I: dynamiczny przydział pamięci w języku C — etap V ... Wywołanie funkcji free powoduje zwolnienie bloku pamięci wskazywanego przez p, blok ten zwracany jest do puli bloków wolnych. Uwaga — po wywołaniu free wskaźnik p dalej pokazuje na zwolniony blok pamięci! ... Sterta int main() { int * p = NULL; } ... Stos if( p != NULL ) { *p = 10; . . . cout << ++(*p); . . . free( p ); } . . . Dane p = malloc( sizeof( int ) ); p ... 77 Suplement I: dynamiczny przydział pamięci w języku C — uwagi ... W C do zerowania wskaźnika wykorzystuje się zwyczajowo stała symboliczną NULL. ... Sterta int main() { int * p = NULL; ... Stos if( p != NULL ) { *p = 10; . . . cout << ++(*p); . . . free( p ); p = NULL; } . . . Dane p = malloc( sizeof( int ) ); p ... } 78 Suplement II: opis funkcji z biblioteki języka C void * malloc( size_t size ); Rezultatem funkcji malloc jest wskaźnik do obszaru pamięci przeznaczonego dla obiektu o rozmiarze size. Rezultatem jest NULL, jeżeli polecenie nie może być zrealizowane. Obszar nie jest inicjowany. void * calloc( size_t nitems, size_t size ); Rezultatem funkcji calloc jest wskaźnik do obszaru pamięci przeznaczonego dla nitems obiektów o rozmiarze size. Rezultatem jest NULL, jeżeli polecenie nie może być zrealizowane. Obszar jest inicjowany zerami. 79 Suplement II: opis funkcji z biblioteki języka C void * realloc( void * ptr, size_t size ); Funkcja dokonuje próby zmiany rozmiaru bloku wskazywanego przez ptr, który był poprzednio przydzielony wywołaniem funkcji calloc lub malloc. Zawartość wskazywanego obszaru pozostaje niezmieniona. Jeżeli nowy rozmiar jest większy od poprzednio przydzielonego, dodatkowe bajty mają nieokreśloną wartość. Jeżeli nowy rozmiar jest mniejszy, bajty z różnicowego obszaru są zwalniane. Jeżeli ptr == NULL to funkcja działa jak malloc. Rezultatem funkcji jest wskaźnik na obszar pamięci o nowym rozmiarze (może być ulokowany w pamięci w innej lokalizacji niż poprzednio). Rezultatem jest NULL w przypadku błędu lub próby przydziału bloku o zerowym rozmiarze. void free( void * ptr ); Zwalnia obszar pamięci wskazywany przez ptr. Parametr musi być wskaźnikiem do obszaru pamięci przydzielonego uprzednio przez malloc, calloc lub realloc. 80