Alokacja dynamiczna pamięci

Kurs: Wstęp do programowania
Lekcja 7: Wskaźniki i pamięć dynamiczna
Temat 4: Alokacja dynamiczna pamięci

⇓ spis treści ⇓


Alokacja dynamiczna pamięci to technika zarządzania pamięcią, która pozwala programom na przydzielanie i zwalnianie pamięci w czasie działania. W odróżnieniu od alokacji statycznej, gdzie rozmiar pamięci jest ustalany na etapie kompilacji, alokacja dynamiczna umożliwia programom reagowanie na zmienne potrzeby pamięciowe w trakcie ich działania. Jest to szczególnie przydatne w aplikacjach, które muszą obsługiwać różne rozmiary danych lub zarządzać pamięcią bardziej efektywnie.

Dlaczego potrzebujemy alokacji dynamicznej?

W wielu przypadkach nie możemy przewidzieć, ile pamięci będzie potrzebne w czasie kompilacji. Na przykład:

  • Gdy pracujemy z danymi wejściowymi od użytkownika, gdzie nie znamy z góry ilości danych.
  • Gdy musimy zarządzać dużymi strukturami danych, takimi jak listy, drzewa lub macierze, które mogą się zmieniać dynamicznie.
  • Gdy chcemy oszczędzać pamięć, alokując ją tylko wtedy, gdy jest potrzebna, i zwalniając, gdy nie jest już używana.
Podstawowe funkcje alokacji dynamicznej w C

W języku C do alokacji dynamicznej pamięci używamy funkcji z biblioteki <stdlib.h>:

  • malloc: Alokuje blok pamięci o określonym rozmiarze i zwraca wskaźnik do pierwszego bajtu tego bloku. Jeśli alokacja się nie powiedzie, zwraca NULL.
  • calloc: Alokuje pamięć dla tablicy o określonej liczbie elementów, inicjalizując wszystkie bajty na zero.
  • realloc: Zmienia rozmiar wcześniej przydzielonego bloku pamięci.
  • free: Zwalnia wcześniej przydzielony blok pamięci, aby mógł zostać ponownie użyty.
Przykłady użycia funkcji malloc i free
#include <iostream>
#include <cstdlib>

int main() {
    // Alokacja pamięci dla 5 liczb całkowitych
    int *ptr = (int *)malloc(5 * sizeof(int));
    if (ptr == nullptr) {
        std::cerr << "Błąd alokacji pamięci!" << std::endl;
        return 1;
    }

    // Inicjalizacja i wyświetlanie wartości
    for (int i = 0; i < 5; ++i) {
        ptr[i] = i + 1;
        std::cout << "Wartość: " << ptr[i] << std::endl;
    }

    // Zwolnienie pamięci
    free(ptr);
    return 0;
}

W powyższym przykładzie używamy malloc do dynamicznego przydzielenia pamięci dla tablicy pięciu liczb całkowitych. Jeśli alokacja się nie powiedzie, program zwraca błąd. Pamięć zostaje zwolniona za pomocą free, aby uniknąć wycieków pamięci.

Przykłady użycia funkcji calloc
#include <iostream>
#include <cstdlib>

int main() {
    // Alokacja pamięci dla 5 liczb całkowitych, inicjalizując je na zero
    int *ptr = (int *)calloc(5, sizeof(int));
    if (ptr == nullptr) {
        std::cerr << "Błąd alokacji pamięci!" << std::endl;
        return 1;
    }

    // Wyświetlanie wartości
    for (int i = 0; i < 5; ++i) {
        std::cout << "Wartość: " << ptr[i] << std::endl; // Wszystkie wartości wynoszą zero
    }

    // Zwolnienie pamięci
    free(ptr);
    return 0;
}

Funkcja calloc działa podobnie do malloc, ale dodatkowo inicjalizuje całą przydzieloną pamięć na zero. Jest to przydatne, gdy chcemy mieć pewność, że wszystkie wartości początkowe są wyzerowane.

Zmiana rozmiaru przydzielonej pamięci za pomocą realloc
#include <iostream>
#include <cstdlib>

int main() {
    // Alokacja pamięci dla 3 liczb całkowitych
    int *ptr = (int *)malloc(3 * sizeof(int));
    if (ptr == nullptr) {
        std::cerr << "Błąd alokacji pamięci!" << std::endl;
        return 1;
    }

    // Inicjalizacja wartości
    for (int i = 0; i < 3; ++i) {
        ptr[i] = i + 1;
    }

    // Zmiana rozmiaru pamięci na 5 liczb całkowitych
    ptr = (int *)realloc(ptr, 5 * sizeof(int));
    if (ptr == nullptr) {
        std::cerr << "Błąd realokacji pamięci!" << std::endl;
        return 1;
    }

    // Inicjalizacja nowych wartości
    for (int i = 3; i < 5; ++i) {
        ptr[i] = i + 1;
    }

    // Wyświetlanie wszystkich wartości
    for (int i = 0; i < 5; ++i) {
        std::cout << "Wartość: " << ptr[i] << std::endl;
    }

    // Zwolnienie pamięci
    free(ptr);
    return 0;
}

Funkcja realloc umożliwia zmianę rozmiaru wcześniej przydzielonego bloku pamięci. Jeśli nowy rozmiar jest większy, dodatkowa pamięć nie jest inicjalizowana, co oznacza, że jej wartości mogą być nieokreślone. Warto zawsze sprawdzać, czy realloc się powiodło, aby uniknąć błędów.

Wyciek pamięci i jak go unikać

Wyciek pamięci występuje, gdy przydzielona pamięć nie zostaje zwolniona po jej użyciu, co prowadzi do marnowania zasobów. W programach, które działają przez dłuższy czas, wycieki pamięci mogą prowadzić do zużycia całej dostępnej pamięci i awarii aplikacji. Aby unikać wycieków pamięci, należy:

  • Zawsze zwalniać pamięć za pomocą free po jej użyciu.
  • Uważać na wskaźniki, które mogą być nadpisywane bez zwalniania wcześniej przydzielonej pamięci.
  • Stosować narzędzia do analizy pamięci, takie jak Valgrind, aby wykryć potencjalne wycieki pamięci.
Przykład wycieku pamięci
#include <iostream>
#include <cstdlib>

void przydzielPamiec() {
    int *ptr = (int *)malloc(100 * sizeof(int));
    // Pamięć nie zostaje zwolniona - wyciek pamięci!
}

int main() {
    przydzielPamiec();
    return 0;
}

W powyższym przykładzie pamięć przydzielona w funkcji przydzielPamiec nie zostaje zwolniona, co prowadzi do wycieku pamięci. Zawsze należy pamiętać o zwalnianiu pamięci za pomocą free, gdy nie jest już potrzebna.

Alokacja dynamiczna w języku C++: new i delete

W języku C++ alokacja dynamiczna pamięci jest uproszczona za pomocą operatorów new i delete. Operator new przydziela pamięć dla pojedynczego obiektu lub tablicy, a delete zwalnia tę pamięć. Przykład:

#include <iostream>

int main() {
    // Alokacja pamięci dla jednej liczby całkowitej
    int *ptr = new int(10);
    std::cout << "Wartość: " << *ptr << std::endl;
    delete ptr; // Zwolnienie pamięci

    // Alokacja pamięci dla tablicy
    int *tablica = new int[5];
    for (int i = 0; i < 5; ++i) {
        tablica[i] = i * 2;
    }
    for (int i = 0; i < 5; ++i) {
        std::cout << "Tablica[" << i << "] = " << tablica[i] << std::endl;
    }
    delete[] tablica; // Zwolnienie pamięci dla tablicy
    return 0;
}

W C++ operator new automatycznie oblicza rozmiar pamięci potrzebnej dla obiektu, a delete zwalnia przydzieloną pamięć. Jeśli alokujemy tablicę, musimy użyć delete[], aby zwolnić całą pamięć.

Smart Pointers: Inteligentne wskaźniki w C++

W nowoczesnym C++ zarządzanie pamięcią zostało ułatwione dzięki inteligentnym wskaźnikom, takim jak std::unique_ptr, std::shared_ptr i std::weak_ptr. Te wskaźniki automatycznie zarządzają cyklem życia obiektów, zmniejszając ryzyko wycieków pamięci.

Przykład użycia std::unique_ptr
#include <iostream>
#include <memory>

int main() {
    std::unique_ptr<int> ptr = std::make_unique<int>(42);
    std::cout << "Wartość: " << *ptr << std::endl;
    // Pamięć zostanie automatycznie zwolniona, gdy ptr wyjdzie z zakresu
    return 0;
}

std::unique_ptr jest wskaźnikiem, który jest właścicielem zasobu i automatycznie zwalnia pamięć, gdy wskaźnik wychodzi z zakresu. Dzięki temu programista nie musi ręcznie zarządzać pamięcią.

Podsumowanie

Alokacja dynamiczna pamięci jest niezbędnym narzędziem w programowaniu, które umożliwia elastyczne zarządzanie zasobami. Choć daje dużą kontrolę nad pamięcią, niesie ze sobą ryzyko, takie jak wycieki pamięci czy wskaźniki wiszące. W języku C stosujemy funkcje malloc, calloc, realloc i free, podczas gdy w C++ preferowane są operatory new i delete oraz inteligentne wskaźniki.

Stosowanie dobrych praktyk w zarządzaniu pamięcią, takich jak użycie inteligentnych wskaźników, unikanie wycieków pamięci i dokładne sprawdzanie wskaźników, jest kluczowe dla pisania stabilnych i wydajnych aplikacji. Dzięki alokacji dynamicznej programy mogą być bardziej elastyczne i wydajne, ale odpowiedzialne zarządzanie pamięcią jest niezbędne, aby unikać problemów, które mogą być trudne do zdiagnozowania i naprawy.

Następny temat ==> Smart pointers: Unikalne i współdzielone wskaźniki



Spis Treści - Wstęp do programowania

Lekcja 3: Rozwiązywanie problemów i poprawność programów Lekcja 4: Praca z różnymi typami danych Lekcja 5: Obsługa plików i pamięci Lekcja 6: Zaawansowane techniki programistyczne Lekcja 7: Wskaźniki i pamięć dynamiczna Lekcja 8: Struktura kodu i abstrakcja Lekcja 9: Rekurencja i jej zastosowania Lekcja 10: Analiza wydajności algorytmów Lekcja 11: Technika "dziel i zwyciężaj" Lekcja 12: Struktury danych o dynamicznej budowie Lekcja 13: Struktury hierarchiczne: Drzewa Lekcja 14: Struktury danych z bibliotek Lekcja 15: Algorytmy z nawrotami Lekcja 16: Programowanie dynamiczne Lekcja 17: Programowanie zachłanne Lekcja 18: Praca z grafami

Jeśli chciałbyś być poinformowany o następnych kursach to zapisz się do naszego newslettera: