Serwis Edukacyjny
w I-LO w Tarnowie
obrazek

Materiały dla uczniów liceum

  Wyjście       Spis treści       Wstecz       Dalej  

Autor artykułu: mgr Jerzy Wałaszek
Zmodyfikowano 28.01.2024

©2024 mgr Jerzy Wałaszek
I LO w Tarnowie

Matura - programowanie w C++

Tablice dynamiczne

SPIS TREŚCI

Wskaźniki i tablice

Przypomnijmy, wskaźnik (ang. pointer) jest zmienną, która przechowuje adres w pamięci innej zmiennej.  Ponieważ tablica jest zmienną, to możemy utworzyć wskaźnik, który będzie wskazywał jej komórki. Możemy to zrobić na kilka sposobów. Załóżmy, że mamy tablicę liczb całkowitych T o dziesięciu komórkach:
T[0]
T[1]
T[2]
T[3]
T[4]
T[5]
T[6]
T[7]
T[8]
T[9]
1
5
8
12
21
39
45
60
83
99

Nazwa tablicy T jest adresem początku obszaru pamięci, w którym znajdują się jej komórki. Aby zatem ustawić wskaźnik na adres pierwszego elementu tablicy, wykonujemy przypisanie:

int T[] = {1,5,8,12,21,39,45,60,83,99};
int p = T;

Wskaźnik p zawiera teraz adres pierwszej komórki T[0], czyli wskazuje liczbę 1.

Jeśli chcemy ustawić wskaźnik na adres określonej komórki tablicy, to korzystamy z operatora adresu &:

int T[] = {1,5,8,12,21,39,45,60,83,99};
int p = &T[5];

Wskaźnik p wskazuje teraz komórkę T[5], czyli liczbę 39. Dlaczego tak? W pierwszym przykładzie T jest adresem początku tablicy. Jako adres może zostać bezpośrednio wstawione do wskaźnika, o ile typ elementów tablicy jest zgodny z typem wskaźnika: tj. nie możesz normalnie przypisać adresu tablicy elementów typu double do wskaźnia liczb typu int, ponieważ adresy muszą być zgodne co do typów wskazywanych obiektów. W drugim przykładzie T[5] jest pojedynczą komórką tablicy, czyli tutaj jest zmienną typu int. Adres zmiennej uzyskuje się przy pomocy operatora adresu &.

Uruchom poniższy program:

// Wskaźniki i tablice
//--------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    int T[] = {1,5,8,12,21,39,45,60,83,99};
    int n = 10;
    int * p, * r;

    cout << "Zawartość tablicy:" << endl;
    for(int i = 0; i < n; i++)
        cout << "T[" << i << "] = "
             << setw(2) << T[i] << endl;

    cout << endl;

    p = T;
    r = &T[5];

    cout << "T[0] = " << setw(2) << * p << endl
         << "T[5] = " << setw(2) << * r << endl;

    cout << endl;

    return 0;
}
Zawartość tablicy:
T[0] =  1
T[1] =  5
T[2] =  8
T[3] = 12
T[4] = 21
T[5] = 39
T[6] = 45
T[7] = 60
T[8] = 83
T[9] = 99

T[0] =  1
T[5] = 39

Na wskaźniku tablicy można wykonywać różne działania.

++ – zwiększa adres we wskaźniku tak, aby wskazywał na następny element tablicy. Jeśli przykładowo wskaźnik p zawiera adres elementu T[5], to po operacji p++ lub ++p będzie on zawierał adres elementu T[6], czyli następnego w tablicy.

-- – zmniejsza adres we wskaźniku tak, aby wskazywał element poprzedni. Jeśli wskaźnik p zawiera adres elementu t[5], to po operacji p-- lub --p, będzie zawierał adres elementu T[4].

Operacje takie mogą być przydatne do przeglądania tablic. Uruchom poniższy program:

// Wskaźniki i tablice
//--------------------

#include <iostream>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    int T[] = {1,5,8,12,21,39,45,60,83,99};
    int n = sizeof(T) / sizeof(int);
    int * p = T;

    cout << "Zawartość tablicy:" << endl;

    do
        cout << * p++ << " ";
    while(p < &T[n]);

    cout << endl;

    return 0;
}
Zawartość tablicy:
1 5 8 12 21 39 45 60 83 99

W programie pojawiło się kilka nowych elementów:

sizeof( ) - operator rozmiaru, zwraca rozmiar zmiennej w bajtach
sizeof(T) - ilość bajtów pamięci komputera zajętych przez tablicę T
sizeof(int) - ilość bajtów zajętych przez pojedynczy element tablicy typu int.
sizeof(T) / sizeof(int) - liczba elementów tablicy

* p++ - wynikiem jest bieżący element tablicy, po czym jego wskaźnik zostaje zwiększony tak, aby wskazywał następny element. Operator ++ odnosi się do wskaźnika, nie do wskazywanego elementu. Jeśli chcesz zwiększyć element, a nie sam wskaźnik, to musisz zastosować nawiasy:
(* p++)++ lub ++(* p++)
Wtedy to, co jest w nawiasach, odnosi się do wskaźnika, a to, co jest poza nimi, odnosi się do wskazywanego elementu.

p < &T[n] - jest to wyrażenie logiczne porównania. Jeśli adres przechowywany we wskaźniku jest mniejszy od adresu elementu T[n], to wynikiem jest prawda, jeśli nie, to wynikiem jest fałsz. W pętli do while znaczy to: dopóki wskaźnik p wskazuje element w tablicy, wykonuj pętlę. Zauważ, iż w tablicy T nie ma elementu T[n], jest to wirtualny element leżący tuż za ostatnim elementem tablicy, zatem, gdy wskaźnik p osiągnie jego adres, to wszystkie elementy tablicy będą przetworzone i pętlę można zakończyć. Jeśli nie chcesz stosować elementu wirtualnego, to zmień warunek na:
p <= &T[n - 1]

Kolejny program przegląda tablicę od końca:

// Wskaźniki i tablice
//--------------------

#include <iostream>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    int T[] = {1,5,8,12,21,39,45,60,83,99};
    int n = sizeof(T) / sizeof(int);
    int * p = &T[n];

    cout << "Zawartość tablicy:" << endl;

    while(p > T)
        cout << * --p << " ";

    cout << endl;

    return 0;
}
Zawartość tablicy:
99 83 60 45 39 21 12 8 5 1

Wyjaśnij pracę pętli while w tym programie. To twoje zadanie.

Wskaźnik może być zmienną sterującą w pętli for:

// Wskaźniki i tablice
//--------------------

#include <iostream>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    int T[] = {1,5,8,12,21,39,45,60,83,99};
    int n = sizeof(T) / sizeof(int);
    
    cout << "Zawartość tablicy:" << endl;

    for(int * p = T; p < &T[n]; p++)
        cout << * p << " ";

    cout << endl;

    return 0;
}

Wskaźnik p jest tutaj zmienną lokalną pętli for, ponieważ jest definiowany wewnątrz tej pętli.

// Wskaźniki i tablice
//--------------------

#include <iostream>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    int T[] = {1,5,8,12,21,39,45,60,83,99};
    int n = sizeof(T) / sizeof(int);

    cout << "Zawartość tablicy:" << endl;

    for(int * p = &T[n - 1]; p >= T; p--)
        cout << * p << " ";

    cout << endl;

    return 0;
}

Do zapamiętania:


Na początek:  podrozdziału   strony 

Zmienne dynamiczne

W trakcie działania programu możemy utworzyć nowe zmienne. W tym celu będą nam potrzebne wskaźniki. Procedura jest następująca:

Tworzymy wskaźnik zmiennej:

typ * wskaźnik;

Tak utworzony wskaźnik nie zawiera jeszcze adresu zmiennej. Adres wprowadzamy do wskaźnika przy pomocy operatora new:

wskaźnik = new typ;

Operator new rezerwuje w pamięci komputera obszar tak duży, aby można było w nim umieścić obiekt danego typu. Następnie new zwraca adres początku obszaru. Adres ten zostaje umieszczony we wskaźniku. Dostęp do obiektu uzyskuje się poprzez wskaźnik i operator *:

* wskaźnik = nowa wartość dla zmiennej dynamicznej;

Gdy zmienna dynamiczna przestanie być potrzebna, możemy zwrócić zajętą przez nią pamięć, usuwając ją operatorem delete:

delete wskaźnik;

Po tej operacji wskaźnik można użyć do innych celów. Operator delete usuwa obszar pamięci (zwraca go do puli pamięci programu) zajęty przez zmienną dynamiczną, którą wskazywał wskaźnik, sam wskaźnik pozostaje nienaruszony, lecz zawarty w nim adres staje się już nieaktualny. Ważne jest, aby adres we wskaźniku pochodził od operatora new, gdy wskaźnik przekazujemy do operatora delete. Spowodowane jest to tym, iż operator new zapamiętuje w przydzielonym obszarze pamięci informacje na temat tego obszaru, np. jego wielkość w bajtach. Z kolei operator delete wykorzystuje tę informację do prawidłowego zwrócenia zarezerwowanego obszaru do puli pamięci programu. Jeśli we wskaźniku nie będzie adresu przydzielonego obszaru lecz np. adres zwykłej zmiennej, to najprawdopodobniej program zakończy się z błędem.

Uruchom poniższy program:

// Zmienne dynamiczne
//--------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");
    cout << fixed << setprecision(2);

    const double PI = 3.1415;

    // Tworzymy wskaźniki na zmienne dynamiczne
    double * o, * p, * r;

    // Przydzielamy pamięć

    o = new double;
    p = new double;
    r = new double;

    cout << "Obliczanie obwodu i pola koła" << endl
         << "-----------------------------" << endl << endl
         << "promień = ";
    cin >> *r;

    * o = 2 * PI * *r;
    * p = PI * *r * *r;

    cout << endl
         << "Obwód = " << setw(7) << *o << endl
         << "Pole  = " << setw(7) << *p << endl << endl;

    // Usuwamy zmienne dynamiczne

    delete o;
    delete p;
    delete r;

    return 0;
}
Obliczanie obwodu i pola koła
-----------------------------

promień = 6

Obwód =   37.70
Pole  =  113.09

Do zapamiętania:


Na początek:  podrozdziału   strony 

Działania arytmetyczne na wskaźnikach

W języku C++ operacje arytmetyczne na wskaźnikach podlegają nieco innym regułom niż zwykłe operacje arytmetyczne na liczbach. Wskaźnik jest adresem. Dodanie do adresu wartości x powoduje wyliczenie nowego adresu, który jest o x wskazywanych elementów dalej od adresu pierwotnego.  Operacja ta podlega regule:

adres + x → adres + x * (rozmiar wskazywanego obiektu)

Z tego powodu wskaźniki w języku C++ posiadają odpowiedni typ, który informuje kompilator o rozmiarze obiektu wskazywanego przez adres we wskaźniku. Przykładowo:

double * p;

utworzy wskaźnik p do zmiennej typu double, która zajmuje w pamięci 8 bajtów. Dodanie do wskaźnika liczby 3 spowoduje przesunięcie zawartego w nim adresu o 3 dane typu double, czyli o 3 * 8 = 24 bajty, zatem wskaźnik p po dodaniu 3 będzie wskazywał o 3 obiekty typu double dalej w stosunku do pierwotnego adresu. Jest to wykonywane automatycznie i programista nie musi się o to martwić, ale należy rozumieć ten mechanizm.

Na przykład mamy tablicę:

T[0] T[1] T[2] T[3] T[4] T[5] T[6] T[7] T[8] T[9]
1 5 8 12 21 39 45 60 83 99

Załóżmy, że do wskaźnika p wpisano adres elementu T[3]:

T[0] T[1] T[2] T[3] T[4] T[5] T[6] T[7] T[8] T[9]
1 5 8 12 21 39 45 60 83 99
 
p
 

Jeśli dodamy do wskaźnika p liczbę 4, to w wyniku otrzymamy adres elementu leżącego o 4 komórki dalej w tablicy, czyli T[7]:

T[0] T[1] T[2] T[3] T[4] T[5] T[6] T[7] T[8] T[9]
1 5 8 12 21 39 45 60 83 99
 
p

p ← p + 4

p
 

Podobnie jest z odejmowaniem. Odjęcie od wskaźnika wartości y przesuwa adres o y pozycji w kierunku początku tablicy:

T[0] T[1] T[2] T[3] T[4] T[5] T[6] T[7] T[8] T[9]
1 5 8 12 21 39 45 60 83 99
 
p

p ← p - 7

p
 

Uruchom poniższy program:

// Operacje na wskaźniku
//----------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    int T[] = {1,5,8,12,21,39,45,60,83,99};

    // tworzymy wskaźnik i ustawiamy go na T[3]

    int * p = &T[3];

    cout << "Zawartość tablicy:" << endl;
    for(int i = 0; i < 10; i++)
        cout << "T[" << i << "] = "
             << setw(2) << T[i] << endl;

    cout << endl
         << "Przed dodaniem 4 wskaźnik p wskazuje T[3] = "
         << * p
         << endl;

    // Modyfikujemy wskaźnik dodając do niego liczbę 4:

    p += 4;

    cout << endl
         << "Po dodaniu 4 wskaźnik p wskazuje T[7] = "
         << * p
         << endl << endl;

    return 0;
}
Zawartość tablicy:
T[0] =  1
T[1] =  5
T[2] =  8
T[3] = 12
T[4] = 21
T[5] = 39
T[6] = 45
T[7] = 60
T[8] = 83
T[9] = 99

Przed dodaniem 4 wskaźnik p wskazuje T[3] = 12

Po dodaniu 4 wskaźnik p wskazuje T[7] = 60

W pierwszym podrozdziale powiedzieliśmy, iż nazwa tablicy jest jej adresem w pamięci. W rzeczywistości nazwa ta jest wskaźnikiem do obszaru pamięci, który zajmuje tablica. Różnicą jest to, iż wskaźnika tego nie możemy zmieniać, czyli jest to wskaźnik stały. Zatem *T jest pierwszym elementem tablicy, czyli T[0]. Do adresu tablicy możemy dodawać wartości i otrzymamy wtedy adresy odpowiednich elementów tablicy:

* (T + 0) : T[0]
* (T + 1) : T[1]
* (T + 2) : T[2]
* (T + 3) : T[3]
...
* (T + n) : T[n]

Uruchom poniższy program:

// Operacje na wskaźniku
//----------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    int T[] = {1,5,8,12,21,39,45,60,83,99};

    cout << "Zawartość tablicy:" << endl;
    for(int i = 0; i < 10; i++)
        cout << "T[" << i << "] = "
             << setw(2) << * (T + i) << endl;

    cout << endl;

    return 0;
}
Zawartość tablicy:
T[0] =  1
T[1] =  5
T[2] =  8
T[3] = 12
T[4] = 21
T[5] = 39
T[6] = 45
T[7] = 60
T[8] = 83
T[9] = 99

Wynika stąd, iż poniższe notacje są równoważne:

* (T + i) : T[i]

Gdzie T jest nazwą tablicy (czyli jej adresem w pamięci komputera). Tutaj T zachowuje się jak wskaźnik. W drugą stronę to też będzie działało. Jeśli p jest wskaźnikiem początku tablicy T, to można użyć jednej z notacji przy dostępie do jej elementów:

* (p + i) : p[i]

Uruchom poniższy program i przeanalizuj go dokładnie:

// Operacje na wskaźniku
//----------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");

    int T[] = {1,5,8,12,21,39,45,60,83,99};
    int * p = T; // Wskaźnik p wskazuje T[0]

    cout << "Zawartość tablicy wskazywanej przez p:" << endl;
    for(int i = 0; i < 10; i++)
        cout << "p[" << i << "] = "
             << setw(2) << p[i] << endl;

    cout << endl;

    return 0;
}
Zawartość tablicy:
p[0] =  1
p[1] =  5
p[2] =  8
p[3] = 12
p[4] = 21
p[5] = 39
p[6] = 45
p[7] = 60
p[8] = 83
p[9] = 99

Wynika z tego, iż wskaźniki i tablice są ze sobą ściśle powiązane:

* (p + 0) : T[0]
* (p + 1) : T[1]
...
* (p + n) : T[n]
      
* (T + 0) : p[0]
* (T + 1) : p[1]
...
* (T + n) : p[n] 
     
p[0] : T[0]
p[1] : T[1]
...
p[n] : T[n]
 
T[0] : p[0]
T[1] : p[1]
...
T[n] : p[n]

W programach możesz zamiennie stosować obie notacje, najłatwiejsza do zapamiętania jest notacja z indeksami w klamerkach kwadratowych, dlatego też będziemy ją systematycznie używali.

Do zapamiętania:


Na początek:  podrozdziału   strony 

Tablice dynamiczne

Tablica dynamiczna (ang. dynamic array) jest tworzona w trakcie wykonywania programu w podobny sposób jak zmienna dynamiczna. Tworząc tablicę statycznie, programista określa jej rozmiar w definicji. Rozmiaru tablicy statycznej nie można zmienić. Jeśli nie wykorzystujemy wszystkich komórek tablicy, to marnujemy pamięć komputera. W przypadku tablicy dynamicznej problem ten nie występuje. Co więcej, gdy tablica przestanie nam być potrzebna, możemy ją usunąć i odzyskać w ten sposób zajmowaną przez nią pamięć.

Tworzenie tablicy dynamicznej rozpoczynamy od przygotowania dla niej wskaźnika odpowiedniego typu. Załóżmy, iż chcemy utworzyć tablicę o komórkach przechowujących liczby typu double. Tworzymy zatem wskaźnik:

double * p;

Gdy mamy wskaźnik, wykorzystujemy operator new do utworzenia tablicy w pamięci. Załóżmy, że chcemy utworzyć tablicę o 10 komórkach:

p = new double[10];

Za operatorem new umieszczamy nazwę typu komórek tablicy, a w klamerkach kwadratowych liczbę komórek tablicy. Ilość dostępnej pamięci musi być wystarczająca na utworzenie w niej tablicy. Jeśli tablica jest za duża i nie mieści się w pamięci, to operator new wygeneruje tzw. wyjątek (ang. exception "std::bad_alloc" - zły przydział pamięci), który można obsłużyć w programie, ale tym się tutaj zajmować nie będziemy. Zadania maturalne nie powinny spowodować zużycia całej pamięci typowego komputera. Operator new zwraca adres początku utworzonej tablicy, czyli adres komórki o indeksie 0. Dostęp to wybranych komórek tablicy dynamicznej uzyskujemy poprzez:

* (p + indeks komórki) lub p[indeks komórk]

Gdy tablica przestanie już być potrzebna, możemy ją usunąć operatorem delete:

delete [] p;

Za delete umieszczamy puste klamerki oraz wskaźnik z adresem tablicy dynamicznej. Klamerki informują komputer, iż wskazywany obszar zawiera tablicę, a nie pojedynczy element. Po usunięciu tablicy adres we wskaźniku przestanie być ważny i nie wolno z niego korzystać. Sam wskaźnik można użyć do innych celów.

Uruchom poniższy program:

Dane do programu (pierwsza liczba określa ilość danych - skopiuj je do schowka i wklej do okna konsoli po uruchomieniu programu):

15
8.2 13 10.1 27.7 8.43 -6.5 0.832 19.89 89.999 100.001 999.99 5497 168.3421 -33.965 1.006

// Tablica dynamiczna
//-------------------

#include <iostream>
#include <iomanip>

using namespace std;

int main()
{
    setlocale(LC_ALL,"");
    cout << fixed << setprecision(4);

    cout << "Wklej ze schowka liczby." << endl
         << "Pierwsza liczba określa ilość danych" << endl
         << "------------------------------------" << endl << endl;

    int n; // Rozmiar tablicy dynamicznej

    cin >> n;

    // Tworzymy wskaźnik

    double * p;

    // Tworzymy tablicę dynamiczną

    p = new double[n];

    // Odczytujemy dane do tablicy

    for(int i = 0; i < n; i++) cin >> p[i];

    // Wyświetlamy zawartość tablicy

    cout << endl;

    for(int i = 0; i < n; i++)
        cout << "p[" << setw(2) << i << "] = "
             << setw(10) << p[i] << endl;

    // Usuwamy tablicę

    delete [] p;

    cout << endl << endl;

    return 0;
}
Wklej ze schowka liczby.
Pierwsza liczba określa ilość danych
------------------------------------

15
8.2 13 10.1 27.7 8.43 -6.5 0.832 19.89 89.999 100.001 999.99 5497 168.3421 -33.965 1.006

p[ 0] =      8.2000
p[ 1] =     13.0000
p[ 2] =     10.1000
p[ 3] =     27.7000
p[ 4] =      8.4300
p[ 5] =     -6.5000
p[ 6] =      0.8320
p[ 7] =     19.8900
p[ 8] =     89.9990
p[ 9] =    100.0010
p[10] =    999.9900
p[11] =   5497.0000
p[12] =    168.3421
p[13] =    -33.9650
p[14] =      1.0060

Do zapamiętania:


Na początek:  podrozdziału   strony 

Zespół Przedmiotowy
Chemii-Fizyki-Informatyki

w I Liceum Ogólnokształcącym
im. Kazimierza Brodzińskiego
w Tarnowie
ul. Piłsudskiego 4
©2024 mgr Jerzy Wałaszek

Materiały tylko do użytku dydaktycznego. Ich kopiowanie i powielanie jest dozwolone
pod warunkiem podania źródła oraz niepobierania za to pieniędzy.

Pytania proszę przesyłać na adres email: i-lo@eduinf.waw.pl

Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.

Informacje dodatkowe.