W 2015 r. stworzyłem artykuł o obsłudze pamięci i wskaźnikach. Obecnie w C++ odchodzi się już od wskaźników, jednak rzeczywistość czasami do ich użycia zmusza.
Poza tym, zasady jakie tu opisuję, dotyczą też C i podobnych języków, więc uważam, że poniższy tekst może mieć wciąż zastosowanie. Miłego czytania.
W tej lekcji zastanowimy się trochę nad tym, jak C++ obsługuje pamięć oraz czym są wskaźniki.
Spis treści
Zmienne statyczne
Na początek napisałem następujący program:
#include
using namespace std;
int main() {
int a, b, suma;
a = 10;
b = 20;
suma = a + b;
cout << "Wynik: " << suma << endl;
return 0;
}
Kliknij, żeby zobaczyć działanie kodu: http://ideone.com/V3LJI
W linii 5 programu powyżej zapisałem deklarację trzech zmiennych typu int
, czyli całkowitego.
Załóżmy na potrzeby tej lekcji, że każda zmienna zajmuje dokładnie 1 komórkę pamięci. Tak nie jest, ale na tym etapie przyjmę takie uproszczenie. Program zarezerwował więc 3 komórki pamięci dla zmiennych a, b, c a następnie wpisał do nich wartości:
Można nawet sprawdzić gdzie w pamięci zostały zapisane dane, rozbuduję więc kod o dodatkową linię:
#include
using namespace std;
int main() {
int a, b, suma;
a = 10;
b = 20;
suma = a + b;
cout << "Wynik: " << suma << endl;
cout << "Zapisano w pamięci pod adresem: " << &suma << endl;
return 0;
}
Tu możesz sprawdzić jego działanie: http://ideone.com/eZBxEi
Jak widać, zmienna suma ma konkretny adres w pamięci, pokazywany w postaci liczby szesnastkowej zbliżonej do: 0xbfe1d0cc. Za każdym uruchomieniem programu może to być oczywiście inna wartość, gdyż system operacyjny przydzieli programowi inny obszar pamięci do wykorzystania.
Sprawdzenie adresu zmiennej w językach C/C++ dokonuje się przy pomocy operatora &, czyli &suma podaje nie wartość zmiennej suma, ale adres, pod jakim została umieszczona w pamięci.
Można oczywiście analogicznie sprawdzić adres dowolnej zmiennej:
Wskaźniki na zmienne
Czasami przydatne jest przechowywanie adresu zmiennej w innej zmiennej. Zastanów się nad następującym kodem (http://ideone.com/rjzVCw):
#include
using namespace std;
int main() {
int a, b, suma;
int *pa;
pa = &a;
a = 10; b = 20;
suma = a + b;
cout << "dodajemy liczbę a=" << a
<< " zapisaną w pamięci pod adresem: " << pa << endl;
cout << "dodajemy liczbę b=" << b
<< " zapisaną w pamięci pod adresem: " << &b << endl;
cout << "Wynik dodawania, czyli suma=" << suma
<< " zapisany w pamięci pod adresem: " << &suma << endl;
return 0;
}
W linii 6 pojawił się nowy sposób deklaracji zmiennej, tym razem przed nazwą umieszczona jest *. Oznacza ona, że zmienna o nazwie pa jest specjalną zmienną, która nie będzie zawierała liczby całkowitej (jak wskazywałby typ int), ale adres komórki pamięci zawierającej zmienną całkowitą. W linii 8 natomiast ten adres wyciągnąłem operatorem & i przypisałem do zmiennej pa. Jak wygląda pamięć dla tego programu?
Zmienne a, b i suma wyglądają tak samo, jak poprzednio. Pojawiła się jednak kolejna zmienna, która dostała swoje miejsce w pamięci i która zawiera nie inta, ale adres, ten sam adres, pod którym w pamięci program umieścił zmienną a. Na razie zysk żaden, ale w dalszej części pokażę, jak można to wykorzystać.
Jako uzupełnienie tego podrozdziału polecam nieco humorystyczny film (https://www.youtube.com/watch?v=6pmWojisM_E):
Przepływ danych pomiędzy funkcjami
Powiedzmy, że masz za zadanie napisać funkcje, która zwiększa wartość premii dla dwóch pracowników, a i b. Piszesz więc coś takiego (http://ideone.com/rHzWo7):
#include
using namespace std;
int dodaj_premie(int a, int b) {
a = a + 100;
b = b + 100;
cout << "W funkcji: a=" << a << ", b=" << b << endl;
return 0;
}
int main() {
int a = 10, b = 20;
cout << "Przed: a=" << a << ", b=" << b << endl;
dodaj_premie(a, b);
cout << "Po: a=" << a << ", b=" << b << endl;
return 0;
}
uruchamiasz i niestety wynik nie jest satysfakcjonujący:
Przed: a=10, b=20 W funkcji: a=110, b=120 Po: a=10, b=20
Czyli program zmieniał wartość zmiennej a i b, ale tylko wewnątrz funkcji dodaj_premie(). Po wyjściu z niej wartości a i b wróciły do poprzedniego stanu.
Dzieje się tak dlatego, że zmienne działają w określonym zakresie. W tym przypadku zmienna a i b wewnątrz funkcji main() to zupełnie inne zmienne niż a i b w funkcji dodaj_premie(). W momencie wywołania funkcji dodaj_premie() tworzone są nowe zmienne i kopiowana jest do nich wartość przekazywanych danych. Mamy więc dwa razy w pamięci liczbę 10 i dwa razy liczbę 20. Funkcja modyfikuje więc własną, lokalną kopię danych i po zakończeniu działania ta lokalna kopia nie jest dostępna dla głównej funkcji programu.
Dla sprawdzenia czy tak naprawdę jest, dodam do kodu dodatkowo wypisywanie jeszcze adresów zmiennych (http://ideone.com/Hx44yN):
#include
using namespace std;
int dodaj_premie(int a, int b) {
cout << "-----------------" << endl;
cout << "W funkcji: adres zmiennej a=" << &a
<< " a zmiennej b=" << &b << endl;
a = a + 100;
b = b + 100;
cout << "W funkcji: a=" << a << ", b=" << b << endl;
cout << "-----------------" << endl;
return 0;
}
int main() {
int a = 10, b = 20;
cout << "W main(): adres zmiennej a=" << &a
<< " a zmiennej b=" << &b << endl;
cout << "Przed: a=" << a << ", b=" << b << endl;
dodaj_premie(a, b);
cout << "W main(): adres zmiennej a=" << &a
<< " a zmiennej b=" << &b << endl;
cout << "Po: a=" << a << ", b=" << b << endl;
return 0;
}
Faktycznie wynik wskazuje na to, że są to różne zmienne pod różnymi adresami w pamięci:
W main(): adres zmiennej a=0xbfac9358 a zmiennej b=0xbfac935c Przed: a=10, b=20 ----------------- W funkcji: adres zmiennej a=0xbfac9330 a zmiennej b=0xbfac9334 W funkcji: a=110, b=120 ----------------- W main(): adres zmiennej a=0xbfac9358 a zmiennej b=0xbfac935c Po: a=10, b=20
Jak więc rozwiązać problem? Pomocne okażą się wskaźniki. Wprowadzę do programu dobrą zmianę:
#include
using namespace std;
int dodaj_premie(int *a, int *b) {
*a = *a + 100;
*b = *b + 100;
return 0;
}
int main() {
int a = 10, b = 20;
cout << "Przed: a=" << a << ", b=" << b << endl;
dodaj_premie(&a, &b);
cout << "Po: a=" << a << ", b=" << b << endl;
return 0;
}
Tym razem program działa jak należy:
Przed: a=10, b=20 Po: a=110, b=120
Wprowadzone zmiany to dodanie gwiazdek (wskaźnika) w liniach 4, 5 i 6 oraz operatora & w linii 13. Dzięki temu, w linii 13 wywoływana jest funkcja dodaj_premie(), do której przekazywane są nie wartości 10 i 20, ale adresy komórek pamięci, w których są one przechowywane. A funkcja, stosując operator *, operator wyłuskania, może sięgać do komórek umieszczonych pod tym adresem i modyfikować ich zawartość.
Samouczki filmowe
Polecam 2 samouczki filmowe jako uzupełnienie tej lekcji: