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