Serwis Edukacyjny w I-LO w Tarnowie Materiały dla uczniów liceum |
Wyjście Spis treści Wstecz Dalej
Autor artykułu: mgr Jerzy Wałaszek |
©2024 mgr Jerzy Wałaszek |
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:
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:
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; } |
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 |
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.
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 |
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:
Serwis wykorzystuje pliki cookies. Jeśli nie chcesz ich otrzymywać, zablokuj je w swojej przeglądarce.
Informacje dodatkowe.