To będzie pewne uproszczenie ale wystarczające do tego by zrozumieć.
Zakładając że mówimy o systemach typu GNU/Linux czy MS Windows (a nie o wbudowanych lub na nietypowych architekturach innych niż von Neumanna https://pl.wikipedia.org/wiki/Architektura_von_Neumanna ), pamięć dla języka C i C++ jest w nich podzielona na główne 3 obszary (tu nie będę komplikował dokonując jeszcze dokładniejszego podziału):
1. Pamięć statyczną - zajmowaną w trakcie uruchomienia programu przez zmienne/struktury/obiekty inicjowane jeszcze przed main() oraz struktury poprzedzone static alokowane w funkcjach, klasach czy metodach.
2. Pamięć automatyczną - zajmowaną na stosie przez zmienne które występują międzyinnymi w zakresach kodu takich jak funkcje/metody.
3. Pamięć dynamiczną - przydzielaną ze sterty poprzez malloc(...) i pochodne w C i new w C++.
Pamięć automatyczna (czyli stosu), ma wielkość ograniczoną przez kompilator. Można oczywiście w trakcie kompilacji wymusić jej wielkość, ale alokowanie w niej dużych struktur (np. tablic), może skończyć się błędem... braku pamięci :-) Domyślnie jest jej ok 2 do 4MB (dla wymienionych systemów - rząd wielkości). Pamięć ta ma tę zaletę że po zakończeniu zakresu (najprościej klamer lub funkcji/metody), automatycznie jest zwalniana i dla C++ niszczona przez wywołanie destruktora (znów uproszczenie ale nie będę wchodził w szczegóły).
Pamięć statyczna, dostępna jest (w zasadzie) w całym programie (to wielkie uproszczenie ale nie będę teraz opisywał możliwości uzyskania do niej dostępu przez wskaźniki, łączność wewnętrzną/zewnętrzną itp). Jej zaletą jest to że .. pamięta stan. Dla elementów "przed main()", masz gwarancję że zostaną wyzerowane przez część uruchamiającą program w systemie operacyjnym (dla C++ będą uruchomione ich konstruktory), dla elementów w funkcjach, funkcja (metoda) będzie pamiętała stan tej zmiennej a będzie ją inicjowała przy 1 wejściu do niej. Co ciekawe C++ od C++11 robi to atomowo.
Pamięć dynamiczna, alokowana ze sterty (nazywanej w standardzie C++ storage ze względu na obsługę różnych architektur sprzętowych), może być przydzielana na etapie działającego programu. Jeśli jednak ciągle będziesz alokował tę pamięć bez jej zwalniania, także i ją wyczerpiesz. Dodatkowo ciągła alokacja i dealokacja może powodować jej fragmentację. Dla architektur wbudowanych ważne jest także że alokacja nie jest deterministyczna (może zajmować różne odcinki czasu ze względu na poszukiwanie ciągłej wolnej przestrzeni zażądanej pamięci). Brak dealokacji i "zgubienie wskaźnika" do tej pamięci, to utrata zasobów nazywana wyciekiem pamięci.
I teraz dochodząc do sedna. Jeśli wiesz że danych będzie ... nie wiadomo ile na początku działania programu lub będzie ich sporo (ponad to na co pozwala stos), będziesz decydował się na alokację dynamiczną.
Jeśli wiesz już na wstępie ile i jakich danych potrzebujesz lub masz arch. wbudowaną, najprawdopodobniej będziesz alokował statycznie jeszcze przed main() i nie dotkniesz malloc()/new.
Poniżej przykładowy pozbawiony logiki kod który ilustruje "jaka pamięć jest gdzie":
#include <iostream>
int global_table1[10]; // Wypełniona zerami statyczna część pamięci dostępna dla całości
// tej mikrej aplikacji. Będzie widoczna dla linkera na zewnątrz.
static int global_table2[10]; // To co wyżej ale nie będzie widoczna dla linkera na zewnątrz.
// Jakaś klasa...
struct MyClass {
double getValues(size_t i) const {
return values[i];
}
private:
int values[20];
static double dValues[10];
};
double MyClass::dValues[10]{}; // Uruchomienie konstruktora danych statycznych. Tu będą zera...
MyClass myClass1; // Obiekt inicjowany statycznie przed wykonaniem main()
int foo() {
int data[20]; // Dane automatyczne niszczone po wyjściu z funkcji
static int value = 10; // Dane statyczne które przetrwają wywołanie foo()
// i będą inicjowane 1 raz w trakcie 1 wejścia do funkcji.
return value++; // Licznik będzie inkrementowany dla każdego wywoałnia.
}
int main() {
MyClass myClass2; // Obiekt automatyczny inicjowany w funkcji main()
{
MyClass myClass3; // Obiekt automatyczny inicjowany po wejściu do zakresu (klamer)
// i niszczony po wyjściu z klamer.
}
std::cout << foo() << '\n'; // Zwraca 10...
std::cout << foo() << '\n'; // Zwraca 11 a wartość licznika value w foo() to 12.
MyClass * myClassPtr; // Wkaźnik na obiekt z MyClass
{
myClassPtr = new MyClass(); // Przypisanie do wskaźnika pamięci ze sterty
// Po wyjściu z tych klamer, nie nastąpi dealokacja z myClassPtr bo dane są na
// stercie...
// Uwaga: Błąd, wyciek pamięci...
int * data = new int[20]; // Po wyjściu z tych klamer... tracisz wskaźnik i masz wyciek pamięci.
}
std::cout << myClassPtr->getValues(3) << '\n';
// Sprzątamy myClassPtr
delete myClassPtr;
}
// Wynik:
// 10
// 11
// 0
Oczywiście to co napisałem to uproszczenie ale wystarczające byś zrozumiał :-)