Prezentowane materiały są przeznaczone dla uczniów szkół ponadgimnazjalnych. Autor artykułu: mgr Jerzy Wałaszek, wersja1.0 |
©2010 mgr
Jerzy Wałaszek |
Informacja przechowywana jest w pamięci w postaci bitów umieszczanych w komórkach (ang memory cell), których mogą być miliardy. Aby komputer mógł uzyskiwać w prosty sposób dostęp do każdej komórki pamięci, zostały one ponumerowane. Numery komórek nazywamy adresami komórek pamięci (ang. memory cell address). Poniżej przedstawiamy fragment logicznej struktury pamięci (czyli tak, jak widzi swoją pamięć komputer):
Pamięć | |
Adres | Zawartość komórki |
0 | 11000110 |
1 | 00001111 |
2 | 11000011 |
3 | 11111110 |
4 | 00000001 |
5 | 11100111 |
... | ... |
W pamięci komputer przechowuje swój program (binarne kody instrukcji procesora) oraz dane, które są przez ten program przetwarzane. Jedna komórka pamięci może przechowywać 8 bitów informacji. W takiej komórce mieści się np. zmienna typu char lub bool. Zmienne innych typów wymagają zwykle więcej komórek pamięci (poniższe dane dotyczą Ming32):
Typ | liczba bitów | Liczba komórek |
char | 8 | 1 |
bool | 8 | 1 |
short | 16 | 2 |
int | 32 | 4 |
long long | 64 | 8 |
float | 32 | 4 |
double | 64 | 8 |
long double | 80 | 10 |
Każda zmienna znajdująca się w pamięci komputera posiada unikalny adres, czyli numer pierwszej komórki, od której rozpoczyna się obszar pamięci zajęty przez zmienną.
// Adresy zmiennych // (C)2011 ILO w Tarnowie // KOŁO INFORMATYCZNE //----------------------- #include <iostream> using namespace std; int main() { int a,b; cout << "Adres a = " << & a << endl << "Adres b = " << & b << endl; return 0; } |
Powyższy program tworzy dwie zmienne a i b. Następnie przesyła do strumienia cout adresy tych zmiennych. Dostęp do adresu zmiennej uzyskujemy w C++ za pomocą operatora &:
& zmienna
Nie myl operatora adresu & z operatorem bitowej koniunkcji &. Wyglądają tak samo, lecz pierwszy jest zawsze jednoargumentowy, a drugi dwuargumentowy. Dodatkowo argumentem operatora adresu może być tylko obiekt pamięciowy, czyli taki, który posiada adres (zmienna, element tablicy). Argumentami operatora koniunkcji bitowej mogą być dowolne wyrażenia.
Operator adresu zmiennej | Operator koniunkcji bitowej |
& zmienna | wyrażenie1 & wyrażenie2 |
Strumień cout wyświetla adresy zawsze w postaci liczb szesnastkowych. Zwróć uwagę, iż w pamięci komputera zmienne a i b znajdują się w porządku odwrotnym: najpierw b, później a. To taka ciekawostka. Nie licz na tą cechę na innych platformach sprzętowych.
Zmienna przechowująca adres innej zmiennej nazywa się w języku C++ wskaźnikiem (ang. pointer). Wskaźnik tworzymy następująco:
typ * wskaźnik;
typ - określa rodzaj wskazywanego obiektu
* - oznacza, że jest to wskaźnik
wskaźnik - będzie przechowywał adres obiektu o danym typie
Wskaźnik jest zmienną. Możemy jej przypisać wartość - np. adres innej zmiennej:
wskaźnik = & zmienna;
Odwołanie do zawartości wskazywanego obiektu udostępnia nam operator * (nie myl go z mnożeniem - patrz powyżej, operator adresu &):
* wskaźnik
Zawartość wskazywanego obiektu możemy zmieniać następującą instrukcją przypisania:
* wskaźnik = wyrażenie;
// Wskaźniki // (C)2011 ILO w Tarnowie // KOŁO INFORMATYCZNE //----------------------- #include <iostream> using namespace std; int main() { int a, b; // to są zwykłe zmienne int * p; // to jest wskaźnik do danych typu int // ustawiamy w p adres zmiennej a p = & a; // pod tym adresem umieszczamy liczbę 10 * p = 10; // teraz zmieniamy adres w p na adres zmiennej b p = & b; // umieszczamy pod tym adresem liczbę 25 * p = 25; cout << "a = " << a << " b = " << b << endl; return 0; } |
Zwróć uwagę, iż dane wstawiliśmy do zmiennych a i b za pomocą wskaźnika p. Następny przykład demonstruje dostęp do danych za pomocą wskaźnika.
// Wskaźniki // (C)2011 ILO w Tarnowie // KOŁO INFORMATYCZNE //----------------------- #include <iostream> using namespace std; int main() { int a, b, * p1, * p2; // w zmiennych a i b umieszczamy jakieś liczby a = 10; b = 25; // wskaźniki ustawiamy na a i b p1 = & a; p2 = & b; cout << "a = " << * p1 << " b = " << * p2 << endl; return 0; } |
Język C++ jest bardzo konsekwentny przy operacji na wskaźnikach. Przypisanie:
wskaźnik1 = wskaźnik2;
jest możliwe tylko wtedy, gdy oba wskaźniki wskazują obiekty tego samego typu. Jeśli wskaźniki są różnego typu, to zwykle takie przypisanie nie ma większego sensu (jeśli ma sens dla programisty, to stosuje on rzutowanie, ale o takich technikach dowiemy się później). Poniższy program tworzy dwa wskaźniki do tego samego obiektu:
// Wskaźniki // (C)2011 ILO w Tarnowie // KOŁO INFORMATYCZNE //----------------------- #include <iostream> using namespace std; int main() { int a, * p1, * p2; // wskaźnik p1 ustawiamy na adres zmiennej a p1 = & a; // adres ten kopiujemy do wskaźnika p2 p2 = p1; // w zmiennej a umieszczamy jakąś liczbę poprzez wskaźnik p1 * p1 = 25; // wyświetlamy zawartość zmiennej a poprzez wskaźnik p2 cout << "a = " << * p2 << endl; return 0; } |
Jeśli wskaźnik p wskazuje element tablicy, to mają sens następujące operacje:
p ++; // wskaźnik będzie wskazywał element następny w tablicy p --; // wskaźnik będzie wskazywał element poprzedni w tablicy
// Wskaźniki i tablice // (C)2011 ILO w Tarnowie // KOŁO INFORMATYCZNE //----------------------- #include <iostream> using namespace std; int main() { int T[] = {5, 7, 9, 11, 2, 3, 6, 0}; int * p; // ustawiamy p na pierwszy element tablicy p = & T[0]; // przechodzimy przez kolejne elementy, aż do elementu 0 while(* p) cout << * (p++) << " "; cout << endl; // cofamy się aż do elementu 5 do cout << * (--p) << " "; while(* p != 5); cout << endl; return 0; } |
wskaźnik + n wskaźnik - n
W obu przypadkach otrzymujemy adres leżący o n obiektów wskazywanego typu (n może być dowolnym wyrażeniem arytmetycznym):
wskaźnik + n : n obiektów dalej za adresem wskazywanym przez wskaźnik wskaźnik - n : n obiektów wskazywanego typu przed adresem wskazywanym przez wskaźnik
// Wskaźniki i tablice // (C)2011 ILO w Tarnowie // KOŁO INFORMATYCZNE //----------------------- #include <iostream> using namespace std; int main() { int T[] = {5, 7, 9, 11, 2, 3, 6, 0}; int * p; // ustawiamy p na pierwszy element tablicy p = & T[0]; // wyświetlamy 7 komórek tablicy w przód for(int i = 0; i < 7; i++) cout << * (p + i) << " "; cout << endl; // ustawiamy p na przedostatni element tablicy p = & T[6]; // wyświetlamy 7 komórek tablicy wstecz for(int i = 0; i < 7; i++) cout << * (p - i) << " "; cout << endl; return 0; } |
Notacja równoważna:
* (wskaźnik + n) --> wskaźnik[n] * (wskaźnik - n) --> wskaźnik[-n]
// Wskaźniki i tablice // (C)2011 ILO w Tarnowie // KOŁO INFORMATYCZNE //----------------------- #include <iostream> using namespace std; int main() { int T[] = {5, 7, 9, 11, 2, 3, 6, 0}; int * p; // ustawiamy p na pierwszy element tablicy p = & T[0]; // wyświetlamy 7 komórek tablicy w przód for(int i = 0; i < 7; i++) cout << p[i] << " "; cout << endl; // ustawiamy p na przedostatni element tablicy p = & T[6]; // wyświetlamy 7 komórek tablicy wstecz for(int i = 0; i < 7; i++) cout << p[-i] << " "; cout << endl; return 0; } |
Z ostatniego przykładu widzimy wyraźnie, iż pomiędzy tablicami i wskaźnikami istnieje duże podobieństwo. W rzeczywistości nazwa tablicy jest wskaźnikiem jej pierwszego elementu, co obrazuje poniższy program:
// Wskaźniki i tablice // (C)2011 ILO w Tarnowie // KOŁO INFORMATYCZNE //----------------------- #include <iostream> using namespace std; int main() { int T[] = {5, 7, 9, 11, 2, 3, 6, 0}; // wyświetlamy tablicę "normalnie" for(int i = 0; i < 7; i++) cout << T[i] << " "; cout << endl; // wyświetlamy tablicę "wskaźnikowo" for(int i = 0; i < 7; i++) cout << * (T + i) << " "; cout << endl; return 0; } |
Jedyna różnica jest taka, iż nazwa tablicy jest stałą i nie możemy jej zmieniać w przeciwieństwie do zmiennej będącej wskaźnikiem. Zatem w programach możemy stosować wymiennie zapis:
Tablica[indeks] --> * (Tablica + indeks) * (wskaźnik + n) --> wskaźnik[n]
Tablica statyczna jest definiowana w kodzie programu. Określamy wtedy jej typ oraz liczbę elementów. Jednakże zdarza się, iż w trakcie pisania programu nie wiemy dokładnie jaki rozmiar tablicy będzie potrzebny - znamy jedynie rozmiar maksymalny. W takich przypadkach tworzenie tablicy o maksymalnym rozmiarze jest nieefektywnym wykorzystywaniem pamięci komputera. Np. tworzymy tablicę zawierającą 100.000 komórek, z których wykorzystujemy zaledwie 3. Pozostałe 99.997 leży sobie odłogiem i nie może być wykorzystane przez inne procesy.
Z tych powodów powstały tablice dynamiczne - tworzone w trakcie wykonywania programu. Aby utworzyć taką tablicę, należy:
Stworzyć wskaźnik do typu, który chcemy użyć na elementy tablicy:
typ * T;
Przypisać wskaźnikowi adres obszaru pamięci, w którym będą przechowywane elementy tablicy:
T = new typ[n];
Operator new przydziela pamięć zwracając adres zarezerwowanego obszaru. Wymaga on podania typu elementów, które będą w tym obszarze przechowywane oraz ich liczby. Adres obszaru trafia do przygotowanego wcześniej wskaźnika. Tablica jest gotowa. Możemy odwoływać się do jej elementów za pomocą notacji:
T[indeks] * (T + indeks)
Z tak utworzoną tablicą możemy zrobić wszystko to, co ze zwykłą tablicą statyczną. Gdy tablica dynamiczna przestanie nam być potrzebna, to po prostu zwalniamy przydzielony jej obszar pamięci:
delete [] T;
Zwolniony obszar można wykorzystać do innych celów, np. dla nowej tablicy dynamicznej.
Poniższy program prosi użytkownika o określenie rozmiaru tablicy dynamicznej, następnie tworzy taką tablicę i wypełnia kolejnymi wielokrotnościami liczb 2 i 3.
// Tablica dynamiczna // (C)2011 ILO w Tarnowie // KOŁO INFORMATYCZNE //----------------------- #include <iostream> using namespace std; int main() { int * T, n, i, w; // odczytujemy rozmiar tablicy cin >> n; // tworzymy obszar na elementy tablicy i zapamiętujemy // jego adres we wskaźniku T T = new int[n]; // tablicę dynamiczną wypełniamy kolejnymi wielokrotnościami 2 i 3 for(w = 1, i = 0; i < n; i++) { while((w % 2) && (w % 3)) w++; T[i] = w++; } // wyświetlamy zawartość tablicy for(i = 0; i < n; i++) cout << T[i] << " "; cout << endl; // tablicę usuwamy z pamięci delete [] T; return 0; } |
I Liceum Ogólnokształcące |
Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl
W artykułach serwisu są używane cookies. Jeśli nie chcesz ich otrzymywać,
zablokuj je w swojej przeglądarce.
Informacje dodatkowe