Proszę zapoznać się z artykułem o pamięci komputerowej.

Tablice dynamiczne

Budowa pamięci komputera

Pamięć komputera (ang. computer memory) zbudowana jest z komórek 8-bitowych. Każda komórka posiada numer, który nazywamy adresem komórki. Dzięki adresom procesor może odwoływać się do komórek w pamięci i umieszczać w nich dane oraz odczytywać ich zawartość.

Dane nie mieszczące sie w pojedynczej komórce są umieszczane w kilku sąsiednich komórkach. W tym przypadku adres danej jest adresem pierwszej komórki.

Umawiamy się, iż adres jest liczbą całkowitą, 32 bitową bez znaku. Adres o wartości 0 (zero) nie wskazuje żadnej danej - tak zostało to przyjęte w języku C++ i lepiej zastosujmy się do tej umowy.

Wskaźniki

W języku C++ możemy tworzyć zmienne, które przechowują adresy danych w pamięci komputera. Takie zmienne nazywamy wskaźnikami (ang. pointers). Wskaźnik jest zatem zmienną całkowitą, 32 bitową (w systemach 64 bitowych rozmiar może być inny, zatem nie przyzwyczajaj się do tej wartości), której zawartość jest interpretowana jako adres danych. W języku C++ wskaźniki posiadają typy zależne od typu wskazywanego przez adres obiektu.

Zmienną typu wskaźnik tworzymy następująco:

typ_wskazywanego_obiektu * nazwa_zmiennej;

Przykłady:

int * p;      // p jest adresem danej całkowitej ze znakiem
double * px;  // px jest adresem danej zmiennoprzecinkowej podwójnej precyzji
char * pc;    // pc jest adresem danej znakowej

 

Po utworzeniu wskaźnika nie mamy w nim żadnego sensownego adresu. Wskaźnik powinniśmy potraktować jak pudełko, do którego mozemy dopiero wstawić sobie jakiś adres. Teraz mamy pudełko, ale jest ono puste.

Adres danej mozna umieścić we wskaźniku na kilka sposobów. Jednym z nich jest wykorzystanie operatora new. Operator ten tworzy w pamięci obiekt określonego typu i zwraca jego adres. Zatem składnia jest następująca:

wskaźnik = new typ_obiektu;

 

 Uwaga: Wskaźnik musi posiadać typ wskazania identyczny z typem przekazywanym do operatora new. W przeciwnym razie kompilator C++ zaprotestuje.

 

Przykład:

int * p;     // tworzymy wskaźnik danej typu int
p = new int; // tworzymy daną int i umieszczamy jej adres we wskaźniku
...

 

Gdy we wskaźniku mamy już poprawny adres danej, to możemy ją uzywać jak normalną zmienną. Odwołanie do danej następuje poprzez operator * (nie myl go z operatorem arytmetycznym mnożenia):

int * p;     // tworzymy wskaźnik danej typu int
p = new int; // tworzymy daną int i umieszczamy jej adres we wskaźniku
* p = 124;   // w danej umieszczamy liczbę 124
cout << * p << endl;
...

Gdy dana utworzona operatorem new przestanie nam już być potrzebna, możemy ją usunąć za pomocą delete:

delete wskaźnik;

Po tej operacji wskaźnik przestaje przechowywać ważny adres - nie mozna go używać, ponieważ nie wskazuje już danych - ten obszar pamięci został zwrócony przez delete do puli systemu i mógł zostać przydzielony innej danej. Wskaźnik natomiast jako zmienna wciąż istnieje i mozna wpisać do niego adres innej danej.

W tym momencie zapytasz - do czego to służy. Na razie przyjmij na "wiarę", iż jest to jedna z najpotężniejszych cech języka C++, która pozwala tworzyć dowolnie skomplikowane struktury danych, które bez wskaźników byłyby praktycznie niemożliwe do utworzenia.

Zapamiętaj jedną ważną rzecz - jeśli przydzielasz dane operatorem new, to zwalniaj je operatorem delete, gdy przestaną być potrzebne. W przeciwnym razie może dochodzić do tzw. wycieku pamięci (ang. memory leak). Polega on na tym, iż program w trakcie działania rezerwuje pamięć na swoje dane, lecz nie oddaje jej z powrotem po wykorzystaniu, lecz rezerwuje ją od nowa. Prowadzi to do wyczerpania pamięci w systemie i oczywiście zablokowania programu. Jedynym wyjątkiem jest zakończenie pracy programu - wtedy przydzielona pamięć jest automatycznie zwracana do systemu. Jednakże lepiej jest to zrobić poleceniem delete (dlaczego, dowiesz się przy okazji obiektów i klas).

Program

#include <iostream>

using namespace std;

main()
{
  double * p;
  
  p = new double;      // inicjujemy wskaźnik
  
  * p = 0.5;           // inicjujemy daną
  * p = 3 * * p;       // mnożymy daną
  cout << p << endl;   // wyświetlamy wskaźnik
  cout << * p << endl; // wyświetlamy daną
  delete p;            // zwalniamy pamięć danej
  system("PAUSE");
}
0x3e3c88
1.5

Na wyjściu powyższego programu otrzymaliśmy dwie liczby. Pierwsza z nich jest zawartością wskaźnika, czyli adresem danej. Strumień cout adresy standardowo prezentuje w postaci szesnastkowej. Druga liczba jest zawartością danej, którą wskazuje adres. Tym razem mamy liczbę z przecinkiem, ponieważ dana jest typu double.

 

Tablice dynamiczne

Tablica dynamiczna (ang. dynamic array) jest tablicą, która zostaje utworzona programowo oraz może zawierać wyliczoną przez program liczbę elementów. Z tego powodu lepiej dopasowuje się do aktualnych potrzeb niż tablica dynamiczna. Wyobraźmy sobie następującą sytuację - tworzymy program, który w tablicy przechowuje dane z czujników. Rezerwujemy na ten cel np. 1000 komórek. Nasz program będzie pracował poprawnie dopóki liczba danych nie przekroczy 1000. Jeśli tak się stanie, nie będzie dla nich miejsca. Z drugiej strony liczba danych może być mała - np. 5. A my rezerwujemy zawsze 1000 komórek - nieefektywnie wykorzystujemy pamięć.

Etapy tworzenia tablicy dynamicznej są następujące:

Deklaracja wskaźnika, który będzie przechowywał adres początku tablicy w pamięci:

typ_elementów  * wskaźnik;

Przykład:

double * T;

Rezerwacja obszaru pamięci na zadaną liczbę elementów tablicy i umieszczenie adresu tego obszaru we wskaźniku:

wskaźnik  = new typ_elementów[liczba_elementów];

Przykład:

T = new double[a+b+20]; // a+b+20 jest wyrażeniem, którego wartość da nam pożądaną liczbę elementów

Teraz do elementów tablicy możemy odwoływać się w zwykły sposób, za pomocą indeksów:

wskaźnik[indeks_elementu];

Przykład:

T[10] = 12;
cout << T[5] << endl;

Gdy tablica przestanie być otrzebna, możemy ją usunąć z pamięci. Zwolnioną pamięć program może wykorzystać do innych celów. Wskaźnik również może zostać użyty do utworzenia nowej tablicy dynamicznej.

delete [] wskaźnik;

Przykład:

delete [] T;

Zwróć uwagę, iż delete wymaga operatora [] przed nazwą wskaźnika. W ten sposób informujemy kompilator, iż usuwamy z pamięci nie pojedynczą daną, lecz tablicę danych.

Program

Poniższy program jest szablonem, który będziemy używać na następnych lekcjach. Odczytuje on z konsoli definicję tablicy. Definicja zbudowana jest następująco:

Pierwsza liczba określa ilość komórek tablicy. Oznaczmy ją przez n. Następne n liczb to zawartość kolejnych komórek. Na przykład tablica 5 elementowa może być zdefiniowana następująco:

5 1 2 3 4 8

Odczytaną tablicę program wyświetla.

Procedura wprowadzania większej ilości danych do okna konsoli jest następująca:

  1. W notatniku przygotuj definicję tablicy.

  2. Zaznacz wszystkie liczby i skopiuj je do schowka Windows (klawisz Ctrl-C).

  3. Kliknij prawym przyciskiem myszki w pasek tytułowy okna konsoli.

  4. Z menu wybierz opcje: Edytuj→Wklej. Skopiowany wcześniej do schowka tekst zostanie przekazany do programu (być może potrzebne będzie jeszcze wciśnięcie klawisza Enter).

#include <iostream>

using namespace std;

main()
{
  int * T;         // tworzymy wskaźnik
  int i,n;
  
  cin >> n;        // odczytujemy ilość komórek
  
  T = new int[n];  // tworzymy tablicę dynamiczną o n komórkach
  
  for(i = 0; i < n; i++)
    cin >> T[i];   // wczytujemy kolejne komórki
    
  // gotowe, wypisujemy odczytaną tablicę
  
  for(i = 0; i < n; i++)
    cout << endl << "T[" << i << "] = " << T[i];

  cout << endl << endl;
  
  system("PAUSE");
}
10 3 66 9 23 87 10 78 52 63 91

T[0] = 3
T[1] = 66
T[2] = 9
T[3] = 23
T[4] = 87
T[5] = 10
T[6] = 78
T[7] = 52
T[8] = 63
T[9] = 91

 


   I Liceum Ogólnokształcące   
im. Kazimierza Brodzińskiego
w Tarnowie

©2024 mgr Jerzy Wałaszek

Dokument ten rozpowszechniany jest zgodnie z zasadami licencji
GNU Free Documentation License.

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