Wyjście poza zakres tablicy - odczyt i zapis - brak błędu - jak możliwe

0

Witajcie

Eksperymentowałem sobie i jakoś tak wyszło że w kodzie wychodziłem poza zakres tablicy statycznej jednak mimo ewidentnego błędu program wykonuje się poprawnie.
Oto kod który obrazuje problem:

#include<iostream>
int main()
{

    int tab[10] = {0,1,2,3,4,5,6,7,8,9};

    for(int i = 0; i < 12;++i)
    {
        if(i >= 10)std::cout << "(niepoprawny)";
        std::cout << "indeks: " << i << " wartosc: " << tab[i] << std::endl;
    }
    std::cout << "============" << std::endl;

    for(int i = 0; i < 12;++i)tab[i] = i+2;

    std::cout << "Po zwiekszeniu wartosci o 2 ..." << std::endl;
    std::cout << "============" << std::endl;
    for(int i = 0; i < 12;++i)
    {
        if(i >= 10)std::cout << "(niepoprawny)";
        std::cout << "indeks: " << i << " wartosc: " << tab[i] << std::endl;
    }

}

forum.png
Widać ze tablica ma w sobie 10 elementów - od 0 do 9 - jednak o dziwo zapis do elementu o indeksie 10 i 11 oraz odczyt nie powoduje błędu aplikacji.
Może mi ktoś wyjaśnić czemu tak się dzieje?

5

To jest UB, wszystko może się zdarzyć. A tak czepiając się słówek, ta tablica statyczną nie jest (jej czas życia jest automatyczny).

0

To teraz po deklaracji zmiennej z tablicą zadeklaruj sobie kolejne zmienne i zobacz jak zamazuje się ich wartość.

2

@ŁF Niekoniecznie, to jest UB, kompilator może zrobić co chce - np. uznać, że programista nie jest głupi i UB nigdy się nie wydarzy i skrócić pętlę (to robi w tym przypadku clang: https://godbolt.org/g/DxnjCZ )

0

Czyli tak jak się spodziewałem - zapis do elementu poza zakresem powoduje wesołe bazgroły po pamięci co może spowodować zamazanie wartości innych zmiennych ?
Moje pytanie dotyczy bardziej czemu kompilator nie rzy rzy rzuca błędem ( u mnie brak ostrzeżeń - muszę pogrzebać w opcjach)
Swoją droga takie coś może sprawić kłopot podczas nauki języka bo niby tablica 10 elementów od 0 do 9 a tu pisze po 10 i 11 i nic ....

3

Czemu - nie ma takiego obowiązku. Ale np. gcc rzuca (pewnie z -Wall albo -Wextra).

Jest to niestety pozostałość z C, gdzie arytmetyka wskaźników jest na porządku dziennym i ciężko tam formalnie coś takiego wykluczyć (chociaż patrz: Rust). Odpowiedzią na Twój problem jest używanie bardziej wysokopoziomowych konstruktów (np. std::array, std::array_view (C++17), iteratorów i funkcji std::begin() i std::end() - które działają również z C-tablicami).

Jeśli chodzi o trudność nauki - tak, ale to też trochę wina kursów które uczą potencjalnie niebezpiecznych rozwiązań, podczas gdy bezpieczniejsze i równie wydajne są dostępne.

4

C i C++ była tworzona pod kątem optymalizacji. To oznacza, że kompilator nie dodaje do kodu żadnych sprawdzeń poprawności logicznej. We wspomnianych wyżej w językach wyższego poziomu, położono większy nacisk na kontrolę błędów.

Do zrozumienia czemu twój kod działa, musisz zrozumieć jaki jest model pamięci współczesnych aplikacji.
Masz stos (stack), stertę (heap) i zmienne globalne (statyczna pamięć).
Stos to jednolite miejsce, gdzie tworzone są zmienne lokalne oraz zapamiętywane jest miejsce powrotu z funkcji/metod. Przydział i zwalnianie pamięci w tym obszarze jest bardzo szybki, ale za to bardzo ograniczony (rzędu 32kB). Twój kod wykorzystuję tę właśnie pamięć. Na dodatek kompilator w ramach optymalizacji wyrównuje pamięć to pełnego paragrafu (16bajtów), więc efektywnie ta twoja tablica może zajmować nie 104 bajtów, ale 48 bajtów (163) i akurat o tyle wykraczasz poza zakres tablicy.
Do tego przyrost heap-a następuje do tyłu (ku mniejszym adresom, starsze zmienne są pod dalszymi adresami), więc wykraczając poza zakres tablicy nadpisujesz adres powrotu z funkcji main(), więc potencjalny błąd wystąpi podczas powrotu z funkcji main.

Poeksperymentuj z wielkością tablicy i wielkością wykraczania poza jej zakres, uwzględniając moje uwagi.

1 użytkowników online, w tym zalogowanych: 0, gości: 1