Program #1
Problem: crash programu w Visual Studio.
Przyczyna: nieprawidłowe napisanie programu.
Krótkie wyjaśnienie: Do operatora delete[] można przekazać tylko wartość otrzymaną od new[], a nie inkrementowaną ileś tam razy.
Długie wyjaśnienie: Każda tablica jest jednocześnie wskaźnikiem, który pokazuje na dany obszar pamięci. Święta zasada tablic, którą każdy potrafi wyrecytować: "Nazwa tablicy jest jednocześnie wskaźnikiem na jej zerowy element". Po inkrementowaniu nazwy tablicy ta zasada przestaje obowiązywać! Nie należy uczyć, zachęcać ani pokazywać, że można inkrementować tablicę samą w sobie. Bo jak pokazuje powyższy kod, wszystko się kompiluje, program działa, do momentu wywołania delete[].
Zamiast tego proponuję uczyć od początku dobrych praktyk. Chcemy poruszać się po tablicy? Używajmy nawiasów [] albo ustawmy na tablicę wskaźnik i to jego przesuwajmy!
Krąży też opinia wśród użytkowników forum stackoverflow, że przekazywanie do delete[] przesuniętego wskaźnika (tak jak w powyższym kursie) nie zwalnia w ogóle pamięci. Jednak nigdzie nie znalazłem wiarygodnych źródeł na potwierdzenie tego.
(Jak ktoś ma Code::Blocks, to może zarezerwować tablicę na 1 GB RAM, przesunąć wskaźnik na ostatni element i zwolnić pamięć patrząc na systemowy menedżer zadań -> wydajność i napisać czy pamięć się zwolniła. Sam jestem ciekawy)
Teraz 2 ciekawe przykłady:
Tutaj wszystko działa.
const char * tablica = "Ala ma kota";
tablica++;
Ale tutaj będzie błąd kompilacji.
char tablica[] = "Ala ma kota";
tablica++;
Będzie błąd, bo kompilator trafia na zapis z nawiasami [] i wie, że to będzie tablica. Jako, iż jest to tablica, to nie pozwoli jej w ten sposób inkrementować. W pierwszym przykładzie nie może nam tego zabronić, bo jak... miałby uniemożliwić przesuwanie wskaźników w ogóle? Tak więc pozostaje tylko zdrowy rozsądek programisty albo...
Użycie stałego wskaźnika. (nikt tak nie robi, to tylko ciekawostka)
const char * const tablica = "Ala ma kota";
Wiem jednak, że stałe wskaźniki oraz wskaźniki na stałe wykraczają poza materiał tego odcinka. Wszystkie tablice utworzone jawnie w kodzie:
int tablica[ 100 ]; są domyślnie właśnie stałymi wskaźnikami.
Powtórzę więc: nie należy inkrementować samej tablicy, bo to prowadzi do poważnych problemów w przyszłości. Zamiast tego należy ustawić wskaźnik na tablicę i to jego inkrementować.
Rozwiązanie problemu: inkrementować wskaźnik pokazujący na tablicę, a nie samą tablicę, przekazać do delete[] prawidłową wartość.
Poprawiony kod: (Program #1 - poprawiony)
#include <iostream>
using namespace std;
int ile;
int main()
{
cout << "Ile liczb w tablicy: ";
cin >> ile;
//dynamiczna alokacja tablicy
int *tablica;
tablica = new int[ ile ];
int * wskaznik = tablica;
//pokaz kolejne adresy komorek w tablicy
for( int i = 0; i<ile; i++ )
{
cout << (int)wskaznik << endl;
wskaznik++;
}
delete[] tablica;
wskaznik = NULL;
return 0;
}
PS. Aby program #1 nie crashował można też (po wcześniejszym inkrementowaniu tab++) cofnąć się znowu do początku (tab--) i wtedy delete[] zadziała. (haniebne rozwiązanie)
Program #3
W programie #3 widzimy podobną, do opisywanej wcześniej, złą praktykę inkrementacji tablicy. W tym przypadku nie jest to groźne, bo wskaźnik na pierwszy element tablicy przekazywany jest do funkcji przez wartość, więc zmiana jego położenia (tab++) nie wpłynie w żaden sposób na zmianę położenia oryginalnego wskaźnika tablicy.
Tutaj nie trzeba koniecznie zmieniać kodu (choć powinien być napisany poprawniej). Zamiast zapamiętywać, kiedy tablicę można inkrementować , a kiedy nie, ja proponuję zasadę:
Inkrementujmy tylko wskaźnik pokazujący na tablicę — nigdy samą tablicę.
Btw. Co ciekawe, w drugim programie z odcinka (#2) Autor zastosował już prawidłową iterację tablicy w oparciu o dodatkowy wskaźnik. Gdyby tego nie zrobił, wystąpiłby problem z programu pierwszego (crash).
Pozdrawiam.