Przypisuję wartości w programie C ++ poza granicami w następujący sposób:
#include <iostream>
using namespace std;
int main()
{
int array[2];
array[0] = 1;
array[1] = 2;
array[3] = 3;
array[4] = 4;
cout << array[3] << endl;
cout << array[4] << endl;
return 0;
}
Program drukuje 3
i 4
. To nie powinno być możliwe. Używam g ++ 4.3.3
Oto polecenie kompilacji i uruchomienia
$ g++ -W -Wall errorRange.cpp -o errorRange
$ ./errorRange
3
4
Dopiero przy przypisywaniu array[3000]=3000
daje mi to błąd segmentacji.
Jeśli gcc nie sprawdza granic tablicy, jak mogę się upewnić, że mój program jest poprawny, ponieważ może to później doprowadzić do poważnych problemów?
Powyższy kod zamieniłem na
vector<int> vint(2);
vint[0] = 0;
vint[1] = 1;
vint[2] = 2;
vint[5] = 5;
cout << vint[2] << endl;
cout << vint[5] << endl;
i ten również nie powoduje błędu.
vector
nie zmienia rozmiaru automatycznie podczas uzyskiwania dostępu do elementów spoza zakresu! To tylko UB!Odpowiedzi:
Witaj u najlepszego przyjaciela programisty C / C ++: Undefined Behavior .
Jest wiele rzeczy, które nie są określone w standardzie językowym z różnych powodów. To jest jeden z nich.
Ogólnie rzecz biorąc, za każdym razem, gdy napotkasz niezdefiniowane zachowanie, wszystko może się zdarzyć. Aplikacja może się zawiesić, zawiesić, wysunąć napęd CD-ROM lub sprawić, że demony wyjdą Ci z nosa. Może sformatować dysk twardy lub wysłać wszystkie filmy porno do babci.
Może nawet wydawać się, że działa poprawnie , jeśli masz naprawdę pecha .
Język po prostu mówi, co powinno się stać, jeśli uzyskasz dostęp do elementów w granicach tablicy. Nieokreślone, co się stanie, jeśli wyjdziesz poza granice. Może się wydawać, że działa dzisiaj na twoim kompilatorze, ale nie jest to legalne C ani C ++ i nie ma gwarancji, że będzie nadal działać przy następnym uruchomieniu programu. Albo, że nie ma istotnych danych nadpisanych nawet teraz, i po prostu nie napotkały problemy, że ma zamiar przyczyny - jeszcze.
Jeśli chodzi o to, dlaczego nie ma sprawdzania granic, odpowiedź ma kilka aspektów:
std::vector
szablon klasy, który pozwala na oba te rozwiązania.operator[]
ma być wydajny. Standard języka nie wymaga sprawdzania granic (chociaż tego też nie zabrania). Wektor ma równieżat()
funkcję składową, która gwarantuje sprawdzanie granic. W C ++ uzyskujesz to, co najlepsze z obu światów, jeśli używasz wektora. Otrzymujesz wydajność podobną do tablicowej bez sprawdzania granic i masz możliwość korzystania z dostępu z kontrolą granic, kiedy tego chcesz.źródło
Korzystanie z g ++, można dodać opcję wiersza polecenia:
-fstack-protector-all
.Na twoim przykładzie skutkowało to:
To naprawdę nie pomaga znaleźć ani rozwiązać problemu, ale przynajmniej segfault poinformuje Cię, że coś jest nie tak.
źródło
-fsanitize=address
wyłapuje ten błąd zarówno w czasie kompilacji (w przypadku optymalizacji), jak iw czasie wykonywania.-fsanitize=undefined,address
. Ale warto zauważyć, że istnieją rzadkie przypadki narożne z biblioteką std, gdy dostęp poza granicami nie jest wykrywany przez środek dezynfekujący . Z tego powodu polecam dodatkowo skorzystać z-D_GLIBCXX_DEBUG
opcji, która dodaje jeszcze więcej sprawdzeń.g ++ nie sprawdza granic tablicy i być może nadpisujesz coś wartością 3,4, ale nic naprawdę ważnego, jeśli spróbujesz z wyższymi liczbami, nastąpi awaria.
Po prostu nadpisujesz części stosu, które nie są używane, możesz kontynuować, aż dotrzesz do końca przydzielonego miejsca dla stosu i w końcu ulegnie awarii
EDYCJA: Nie masz sposobu, aby sobie z tym poradzić, może statyczny analizator kodu mógłby ujawnić te awarie, ale to zbyt proste, możesz mieć podobne (ale bardziej złożone) awarie niewykryte nawet dla analizatorów statycznych
źródło
O ile wiem, jest to nieokreślone zachowanie. Uruchom z tym większy program, a gdzieś się zawiesi. Sprawdzanie granic nie jest częścią surowych tablic (ani nawet std :: vector).
std::vector::iterator
Zamiast tego użyj std :: vector z 's, więc nie musisz się tym martwić.Edytować:
Dla zabawy, uruchom to i zobacz, ile czasu minie do awarii:
Edit2:
Nie uruchamiaj tego.
Edit3:
OK, oto krótka lekcja na temat tablic i ich relacji ze wskaźnikami:
Kiedy używasz indeksowania tablic, naprawdę używasz wskaźnika w przebraniu (zwanego „odniesieniem”), który jest automatycznie wyłuskiwany. Dlatego zamiast * (tablica [1]) tablica [1] automatycznie zwraca wartość o tej wartości.
Gdy masz wskaźnik do tablicy, na przykład:
Wtedy "tablica" w drugiej deklaracji jest naprawdę rozpadana na wskaźnik do pierwszej tablicy. Jest to zachowanie równoważne z następującym:
Kiedy próbujesz uzyskać dostęp poza to, co przydzieliłeś, tak naprawdę używasz po prostu wskaźnika do innej pamięci (na co C ++ nie będzie narzekać). Biorąc mój przykładowy program powyżej, jest to równoważne z tym:
Kompilator nie będzie narzekał, ponieważ w programowaniu często trzeba komunikować się z innymi programami, zwłaszcza z systemem operacyjnym. Odbywa się to dość często za pomocą wskaźników.
źródło
Wskazówka
Jeśli chcesz mieć szybkie tablice rozmiaru ograniczeń ze sprawdzaniem błędów zakresu, spróbuj użyć boost :: array (również std :: tr1 :: array
<tr1/array>
będzie z niego standardowym kontenerem w następnej specyfikacji C ++). Jest znacznie szybszy niż std :: vector. Rezerwuje pamięć na stercie lub wewnątrz instancji klasy, tak jak int array [].Oto prosty przykładowy kod:
Ten program wydrukuje:
źródło
C lub C ++ nie będzie sprawdzać granic dostępu do tablicy.
Alokujesz tablicę na stosie. Indeksowanie tablicy za pomocą
array[3]
jest równoważne z *(array + 3)
, gdzie tablica jest wskaźnikiem do & array [0]. Spowoduje to niezdefiniowane zachowanie.Jednym ze sposobów na złapanie tego czasami w C jest użycie statycznego kontrolera, takiego jak szyna . Jeśli biegasz:
na,
wtedy otrzymasz ostrzeżenie:
źródło
Z pewnością nadpisujesz swój stos, ale program jest na tyle prosty, że efekty tego pozostają niezauważone.
źródło
Przeprowadź to przez Valgrind, a możesz zobaczyć błąd.
Jak zauważyła Falaina, valgrind nie wykrywa wielu przypadków uszkodzenia stosu. Właśnie wypróbowałem próbkę pod valgrind i rzeczywiście zgłasza zero błędów. Jednak Valgrind może odegrać kluczową rolę w znalezieniu wielu innych typów problemów z pamięcią, po prostu nie jest szczególnie przydatny w tym przypadku, chyba że zmodyfikujesz swój bulid tak, aby zawierał opcję --stack-check. Jeśli skompilujesz i uruchomisz przykład jako
valgrind będzie zgłosić błąd.
źródło
Nieokreślone zachowanie działa na Twoją korzyść. Bez względu na wspomnienie, które omijasz, najwyraźniej nie zawiera niczego ważnego. Zauważ, że C i C ++ nie sprawdzają granic tablic, więc takie rzeczy nie zostaną przechwycone podczas kompilacji lub wykonywania.
źródło
Podczas inicjalizacji tablicy przy pomocy
int array[2]
przydzielana jest przestrzeń na 2 liczby całkowite; ale identyfikatorarray
wskazuje po prostu początek tej przestrzeni. Kiedy następnie uzyskujesz dostęp doarray[3]
iarray[4]
, kompilator po prostu zwiększa ten adres, aby wskazać, gdzie te wartości byłyby, gdyby tablica była wystarczająco długa; spróbuj uzyskać dostęp do czegoś takiego jakarray[42]
bez inicjalizacji go najpierw, w końcu uzyskasz jakąkolwiek wartość, która już znajduje się w pamięci w tym miejscu.Edytować:
Więcej informacji o wskaźnikach / tablicach: http://home.netcom.com/~tjensen/ptr/pointers.htm
źródło
kiedy deklarujesz int array [2]; rezerwujesz 2 przestrzenie pamięci po 4 bajty każda (program 32-bitowy). jeśli wpiszesz array [4] w swoim kodzie, nadal odpowiada to poprawnemu wywołaniu, ale tylko w czasie wykonywania zgłosi nieobsługiwany wyjątek. C ++ używa ręcznego zarządzania pamięcią. W rzeczywistości jest to luka w zabezpieczeniach, która została wykorzystana do hakowania programów
może to pomóc w zrozumieniu:
int * somepointer;
somepointer [0] = somepointer [5];
źródło
Jak rozumiem, zmienne lokalne są przydzielane na stosie, więc wyjście poza granice własnego stosu może nadpisać tylko inną zmienną lokalną, chyba że zrobisz zbyt dużo i przekroczysz rozmiar stosu. Ponieważ nie masz zadeklarowanych innych zmiennych w swojej funkcji - nie powoduje to żadnych skutków ubocznych. Spróbuj zadeklarować inną zmienną / tablicę zaraz po pierwszej i zobacz, co się z nią stanie.
źródło
Kiedy piszesz „tablica [indeks]” w C, tłumaczy to na instrukcje maszynowe.
Tłumaczenie brzmi mniej więcej tak:
Wynik odnosi się do czegoś, co może, ale nie musi, być częścią tablicy. W zamian za niesamowitą prędkość instrukcji maszyny tracisz siatkę bezpieczeństwa komputera sprawdzającego rzeczy za Ciebie. Jeśli jesteś skrupulatny i ostrożny, nie stanowi to problemu. Jeśli jesteś niechlujny lub popełnisz błąd, zostaniesz poparzony. Czasami może generować niepoprawną instrukcję, która powoduje wyjątek, czasami nie.
źródło
Przyjemnym podejściem, które często widziałem i faktycznie byłem używany, jest wstrzyknięcie elementu typu NULL (lub utworzonego, jak np.
uint THIS_IS_INFINITY = 82862863263;
) Na końcu tablicy.Następnie przy sprawdzaniu warunku pętli
TYPE *pagesWords
jest jakiś rodzaj tablicy wskaźników:To rozwiązanie nie powiedzie się, jeśli tablica jest wypełniona
struct
typami.źródło
Jak już wspomniano w pytaniu, użycie std :: vector :: at rozwiąże problem i sprawi, że przed uzyskaniem dostępu nastąpi sprawdzenie związane.
Jeśli potrzebujesz tablicy o stałym rozmiarze, która znajduje się na stosie jako pierwszy kod, użyj nowego kontenera C ++ 11 std :: array; jako wektor jest funkcja std :: array :: at. W rzeczywistości funkcja istnieje we wszystkich standardowych kontenerach, w których ma znaczenie, tj. Tam, gdzie zdefiniowano operator [] :( deque, map, unordered_map) z wyjątkiem std :: bitset, w którym nazywa się std :: bitset: :test.
źródło
libstdc ++, które jest częścią gcc, ma specjalny tryb debugowania do sprawdzania błędów. Jest włączana flagą kompilatora
-D_GLIBCXX_DEBUG
. Między innymi sprawdza granicestd::vector
kosztem wydajności. Oto demo online z najnowszą wersją gcc.Więc faktycznie możesz sprawdzać granice w trybie debugowania libstdc ++, ale powinieneś to robić tylko podczas testowania, ponieważ kosztuje to znaczną wydajność w porównaniu z normalnym trybem libstdc ++.
źródło
Jeśli nieznacznie zmienisz program:
(Zmiany w wielkich literach - wpisz je małymi literami, jeśli masz zamiar tego spróbować.)
Zobaczysz, że zmienna foo została skasowana. Twój kod będzie przechowywał wartości w nieistniejącej tablicy [3] i tablicy [4], i będzie mógł je poprawnie pobrać, ale faktycznie używane miejsce będzie pochodziło z foo .
Możesz więc „uciec” z przekroczeniem granic tablicy w oryginalnym przykładzie, ale kosztem spowodowania szkody w innym miejscu - uszkodzenia, które mogą okazać się bardzo trudne do zdiagnozowania.
Dlaczego nie ma automatycznego sprawdzania granic - poprawnie napisany program tego nie potrzebuje. Gdy to zrobisz, nie ma powodu, aby sprawdzać granice czasu wykonywania, a zrobienie tego po prostu spowolniłoby program. Najlepiej, aby to wszystko zostało ustalone podczas projektowania i kodowania.
C ++ jest oparty na C, który został zaprojektowany tak, aby był jak najbardziej zbliżony do języka asemblera.
źródło