Obsługa pamięci w C/C++

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.

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

Zastanów się teraz, co się dzieje w tym programie z punku widzenia zajmowania pamięci. Załóżmy, że pamięć to szafeczka z kolejnymi półkami (komórkami pamięci). Coś jak arkusz kalkulacyjny, w którym można w każde pole wpisać jakąś liczbę.

W linii 5 programu powyżej zapisałem deklarację trzech zmiennych typu int, czyli całkowitego.

pamiec_01-01Załóż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:

pamiec_01-02Moż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

pamiec_01-03

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.

pamiec_01-04

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:

pamiec_01-05

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?

pamiec_01-06

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.

pamiec_01-07

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ść.

pamiec_01-08

Samouczki filmowe

Polecam 2 samouczki filmowe jako uzupełnienie tej lekcji:

 

Lekcja w postaci PDF i ODP

Tutaj znajdziesz wersję PDF lekcji

Tutaj znajdziesz wersję ODP lekcji

Author: Przemysław Adam Śmiejek

#wieśniak 👨🏻‍🌾, #dziaders 👴🏻, #aktor 📺/🎭, #żeglarz ⛵