W K&R (The C Programming Language 2nd Edition) rozdział 5 czytam:
Po pierwsze, wskaźniki mogą być porównywane w pewnych okolicznościach. Jeśli
p
iq
punkt do członków tej samej tablicy, stosunki wówczas jak==
,!=
,<
,>=
, itd pracę prawidłowo.
Co wydaje się sugerować, że można porównywać tylko wskaźniki wskazujące na tę samą tablicę.
Jednak kiedy wypróbowałem ten kod
char t = 't';
char *pt = &t;
char x = 'x';
char *px = &x;
printf("%d\n", pt > px);
1
jest drukowane na ekranie.
Po pierwsze, pomyślałem, że stanę się niezdefiniowany lub jakiś rodzaj lub błąd, ponieważ pt
i px
nie wskazują na tę samą tablicę (przynajmniej w moim rozumieniu).
Jest pt > px
tak również dlatego, że oba wskaźniki wskazują na zmienne przechowywane na stosie, a stos rośnie, więc adres pamięci t
jest większy niż adres x
? Co pt > px
jest prawdą?
Bardziej się mylę, gdy sprowadza się malloc. Również w K&R w rozdziale 8.7 napisano:
Istnieje jednak jedno założenie, że wskaźniki do różnych zwracanych bloków
sbrk
można znacznie porównać. Nie gwarantuje tego standard, który pozwala na porównywanie wskaźników tylko w obrębie tablicy. Dlatego ta wersjamalloc
jest przenośna tylko wśród komputerów, dla których ogólne porównanie wskaźników jest znaczące.
Nie miałem problemu z porównywaniem wskaźników wskazujących przestrzeń malloced na stercie ze wskaźnikami wskazującymi zmienne stosu.
Na przykład następujący kod działał poprawnie, z 1
wydrukowaniem:
char t = 't';
char *pt = &t;
char *px = malloc(10);
strcpy(px, pt);
printf("%d\n", pt > px);
Opierając się na moich eksperymentach z moim kompilatorem, doprowadzono mnie do wniosku, że każdy wskaźnik można porównać z dowolnym innym wskaźnikiem, niezależnie od tego, gdzie indywidualnie wskazują. Co więcej, myślę, że arytmetyka wskaźnika między dwoma wskaźnikami jest w porządku, bez względu na to, gdzie wskazują indywidualnie, ponieważ arytmetyka używa tylko adresów pamięci, w których przechowywane są wskaźniki.
Mimo to jestem zdezorientowany tym, co czytam w K&R.
Pytam dlatego, że mój prof. właściwie uczyniło z niego pytanie egzaminacyjne. Podał następujący kod:
struct A { char *p0; char *p1; }; int main(int argc, char **argv) { char a = 0; char *b = "W"; char c[] = [ 'L', 'O', 'L', 0 ]; struct A p[3]; p[0].p0 = &a; p[1].p0 = b; p[2].p0 = c; for(int i = 0; i < 3; i++) { p[i].p1 = malloc(10); strcpy(p[i].p1, p[i].p0); } }
Co oceniają, aby:
p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1
Odpowiedź jest 0
, 1
i 0
.
(Mój profesor zawiera wyłączenie odpowiedzialności na egzaminie, że pytania dotyczą środowiska programowania Ubuntu Linux 16.04, wersja 64-bitowa)
(uwaga redaktora: jeśli SO dopuszcza więcej tagów, ta ostatnia część uzasadnia x86-64 , linux , a może asemblację . Jeśli punktem pytania / klasy byłyby szczegóły implementacji systemu operacyjnego niskiego poziomu, a nie przenośne C.)
C
tym, co jest bezpieczne wC
. Porównywanie dwóch wskaźników z tym samym typem można zawsze wykonać (na przykład sprawdzanie równości), stosując arytmetykę wskaźników i porównywanie>
i<
jest bezpieczne tylko wtedy, gdy jest używane w obrębie danej tablicy (lub bloku pamięci).Odpowiedzi:
Według standardu C11 , relacyjnych operatorów
<
,<=
,>
i>=
mogą być wykorzystywane tylko dla wskaźników do elementów tego samego zespołu lub struktura obiektu. Jest to określone w sekcji 6.5.8p5:Zauważ, że wszelkie porównania, które nie spełniają tego wymogu, wywołują niezdefiniowane zachowanie , co oznacza (między innymi), że nie można polegać na powtarzalności wyników.
W twoim konkretnym przypadku, zarówno w przypadku porównania adresów dwóch zmiennych lokalnych, jak i adresu adresu lokalnego i adresu dynamicznego, operacja wydawała się „działać”, jednak wynik może się zmienić, dokonując pozornie niezwiązanej zmiany w kodzie lub nawet kompiluje ten sam kod z różnymi ustawieniami optymalizacji. Nieokreślone zachowanie tylko dlatego, że kod może ulec awarii lub wygenerować błąd, nie oznacza, że tak się stanie .
Na przykład procesor x86 działający w trybie rzeczywistym 8086 ma segmentowany model pamięci wykorzystujący segment 16-bitowy i 16-bitowe przesunięcie do zbudowania adresu 20-bitowego. Więc w tym przypadku adres nie jest konwertowany dokładnie na liczbę całkowitą.
Operatorzy równości
==
i!=
jednak nie mają tego ograniczenia. Można ich używać między dowolnymi dwoma wskaźnikami do zgodnych typów lub wskaźników NULL. Tak więc użycie==
lub!=
w obu twoich przykładach wygenerowałoby prawidłowy kod C.Jednak nawet z
==
i!=
możesz uzyskać nieoczekiwane, ale wciąż dobrze zdefiniowane wyniki. Zobacz Czy porównanie równości niepowiązanych wskaźników może dać wartość true? po więcej szczegółów na ten temat.Jeśli chodzi o pytanie egzaminacyjne podane przez profesora, zawiera on szereg błędnych założeń:
Jeśli uruchomisz ten kod na architekturze i / lub kompilatorze, który nie spełnia tych założeń, możesz uzyskać bardzo różne wyniki.
Ponadto oba przykłady wykazują także niezdefiniowane zachowanie podczas wywoływania
strcpy
, ponieważ właściwy operand (w niektórych przypadkach) wskazuje na pojedynczy znak, a nie na łańcuch zakończony znakiem zerowym, co powoduje, że funkcja odczytuje poza granice danej zmiennej.źródło
<
międzymalloc
rezultatu i zmiennej lokalnej (automatycznego składowania, tj stosu), może założyć, że ścieżka realizacji nigdy nie jest zrobione i tylko skompilować cały funkcji wud2
instrukcji (podnosi nielegalne -incept wyjątku, który jądro obsłuży dostarczając SIGILL do procesu). GCC / clang robią to w praktyce dla innych rodzajów UB, takich jak wypadnięcie końcavoid
niefunkcji. godbolt.org jest obecnie niedostępny , ale spróbuj skopiować / wkleićint foo(){int x=2;}
i zwróć uwagę na brakret
malloc
używa się w celu uzyskania większej ilości pamięci z systemu operacyjnego, więc nie ma powodu, aby zakładać, że lokalne zmienne (stos wątków) są powyżejmalloc
dynamicznie przydzielanych przechowywanie.int x,y;
implementację ...Podstawowym problemem przy porównywaniu wskaźników z dwoma odrębnymi tablicami tego samego typu jest to, że same tablice nie muszą być umieszczane w określonym względnym położeniu - jeden może skończyć się przed i po drugim.
Nie, wynik zależy od wdrożenia i innych nieprzewidzianych czynników.
Niekoniecznie jest stos . Gdy istnieje, nie musi rosnąć. Może dorastać. Może to być nieciągłe w jakiś dziwny sposób.
Spójrzmy na specyfikację C , §6.5.8 na stronie 85, która omawia operatory relacyjne (tj. Operatory porównania, których używasz). Pamiętaj, że nie dotyczy to bezpośredniego
!=
ani==
porównania.Ważne jest ostatnie zdanie. Podczas gdy ograniczam niektóre niepowiązane ze sobą przypadki, aby zaoszczędzić miejsce, jest jeden ważny dla nas przypadek: dwie tablice, nie będące częścią tego samego obiektu struct / agregate 1 , i porównujemy wskaźniki z tymi dwiema tablicami. To jest niezdefiniowane zachowanie .
Podczas gdy twój kompilator właśnie wstawił jakąś instrukcję maszynową CMP (porównaj), która numerycznie porównuje wskaźniki, i masz szczęście, UB jest dość niebezpieczną bestią. Dosłownie wszystko może się zdarzyć - Twój kompilator może zoptymalizować całą funkcję, w tym widoczne efekty uboczne. Może spawnować demony nosowe.
1 Wskaźniki do dwóch różnych tablic, które są częścią tej samej struktury, można porównać, ponieważ jest to objęte klauzulą, w której dwie tablice są częścią tego samego obiektu agregującego (struktury).
źródło
t
ix
będąc zdefiniowanymi w tej samej funkcji, nie ma żadnego powodu, aby zakładać cokolwiek o tym, w jaki sposób kompilator ukierunkowany na x86-64 rozłoży miejscowe w ramce stosu dla tej funkcji. Stos rosnący w dół nie ma nic wspólnego z kolejnością deklaracji zmiennych w jednej funkcji. Nawet w osobnych funkcjach, jeśli jedna mogłaby być połączona z drugą, to miejscowi funkcji „dziecka” nadal mogliby mieszać się z rodzicami.void
funkcja) g ++ i brzęk ++ naprawdę nie jest to w praktyce: godbolt.org/z/g5vesB one Załóżmy, że ścieżka wykonania nie jest pobierana, ponieważ prowadzi do UB, i skompiluj takie podstawowe bloki do niedozwolonej instrukcji. Lub w ogóle bez instrukcji, po prostu cicho przechodząc do następnego asm, jeśli ta funkcja zostanie kiedykolwiek wywołana. (Z jakiegoś powodugcc
tego nie robi, tylkog++
).Te pytania ograniczają się do:
Odpowiedzią na wszystkie trzy pytania jest „zdefiniowanie wdrożenia”. Pytania twojego profesora są fałszywe; oparli go na tradycyjnym układzie uniksowym:
ale kilka współczesnych jednorożców (i systemów alternatywnych) nie jest zgodnych z tymi tradycjami. Chyba że poprzedzili to pytanie „od 1992 r.”; upewnij się, że podajesz -1 na eval.
źródło
arr[]
jest takim obiektem, Standard nakazujearr+32768
porównywanie większych danych,arr
nawet jeśli porównanie wskaźnika ze znakiem zgłasza inaczej.Na prawie każdej zdalnie nowoczesnej platformie wskaźniki i liczby całkowite mają izomorficzną relację porządkowania, a wskaźniki do obiektów rozłącznych nie są przeplatane. Większość kompilatorów ujawnia to uporządkowanie programistom, gdy optymalizacje są wyłączone, ale Standard nie wprowadza rozróżnienia między platformami, które mają takie uporządkowanie, a tymi, które tego nie robią i nie wymaga, aby jakiekolwiek implementacje ujawniały programistom takie uporządkowanie, nawet na platformach, które Określ to. W związku z tym niektórzy autorzy kompilatorów wykonują różnego rodzaju optymalizacje i „optymalizacje” w oparciu o założenie, że kod nigdy nie będzie porównywał użycia operatorów relacyjnych na wskaźnikach do różnych obiektów.
Zgodnie z opublikowanym uzasadnieniem autorzy standardu zamierzali, aby implementacje rozszerzyły język, określając, jak będą się zachowywać w sytuacjach, które standard określa jako „zachowanie nieokreślone” (tj. Gdy standard nie nakłada żadnych wymagań ), gdy byłoby to przydatne i praktyczne , ale niektórzy pisarze kompilatorów woleliby raczej założyć, że programy nigdy nie będą próbowały czerpać korzyści z niczego poza tym, co nakazują Standardy, niż pozwolić programom na użyteczne wykorzystanie zachowań obsługiwanych przez platformy bez dodatkowych kosztów.
Nie znam żadnych komercyjnie zaprojektowanych kompilatorów, które robią cokolwiek dziwnego z porównaniami wskaźników, ale gdy kompilatory przechodzą do niekomercyjnego LLVM dla swojego zaplecza, coraz częściej przetwarzają nonsensowny kod, którego zachowanie zostało określone wcześniej kompilatory dla swoich platform. Takie zachowanie nie ogranicza się do operatorów relacyjnych, ale może nawet wpływać na równość / nierówność. Na przykład, mimo że Standard określa, że porównanie wskaźnika do jednego obiektu i wskaźnika „tuż za” do obiektu bezpośrednio poprzedzającego będzie porównywalne, kompilatory oparte na gcc i LLVM są skłonne do generowania bezsensownego kodu, jeśli programy wykonują takie porównania.
Jako przykład sytuacji, w której nawet porównanie równości zachowuje się bezsensownie w gcc i clang, rozważ:
Zarówno clang, jak i gcc wygenerują kod, który zawsze zwróci 4, nawet jeśli
x
jest dziesięć elementów,y
natychmiast następuje po nim ii
wynosi zero, co powoduje, że porównanie jest prawdziwe ip[0]
zapisywane z wartością 1. Myślę, że to, co się dzieje, to jedno przejście optymalizacji funkcja*p = 1;
została zastąpiona przezx[10] = 1;
. Ten ostatni kod byłby równoważny, gdyby kompilator zinterpretował*(x+10)
jako równoważny*(y+i)
, ale niestety dalszy etap optymalizacji rozpoznaje, że dostęp dox[10]
zdefiniowałby tylko, gdybyx
miał co najmniej 11 elementów, co uniemożliwiłoby wpływ na ten dostępy
.Jeśli kompilatory mogą uzyskać tę „kreatywność” dzięki scenariuszowi równości wskaźnika opisanemu przez Standard, nie ufałbym im, że powstrzymają się od bycia jeszcze bardziej kreatywnymi w przypadkach, w których Standard nie nakłada wymagań.
źródło
To proste: porównywanie wskaźników nie ma sensu, ponieważ lokalizacje pamięci dla obiektów nigdy nie są zagwarantowane w tej samej kolejności, w jakiej zostały zadeklarowane. Wyjątkiem są tablice. & array [0] jest niższy niż & array [1]. Właśnie to wskazuje K&R. W praktyce adresy członków struktury są również w kolejności, w jakiej je zadeklarujesz z mojego doświadczenia. Żadnych gwarancji na to .... Kolejnym wyjątkiem jest porównanie wskaźnika równości. Gdy jeden wskaźnik jest równy drugiemu, wiesz, że wskazuje na ten sam obiekt. Cokolwiek to jest. Złe pytanie egzaminacyjne, jeśli mnie o to poprosisz. W zależności od Ubuntu Linux 16.04, środowisko programowania wersji 64-bitowej na pytanie egzaminacyjne? Naprawdę
źródło
arr[0]
,arr[1]
itp oddzielnie. Deklarujeszarr
jako całość, więc porządkowanie poszczególnych elementów tablicy jest kwestią inną niż opisana w tym pytaniu.memcpy
do skopiowania ciągłej części struktury i wpłynąć na wszystkie elementy w niej i nie wpływać na nic innego. Standard nie podchodzi terminologicznie do tego, jakie rodzaje arytmetyki wskaźników można wykonać za pomocą struktur lubmalloc()
przydzielonej pamięci.offsetof
Makro będzie raczej bezużyteczne, jeśli jeden nie mógł do tego samego rodzaju wskaźnik arytmetycznych z bajtów struct jak zchar[]
, ale standard nie wyraźnie powiedzieć, że bajty struct są (lub mogą być wykorzystywane jako) obiekt tablicowy.Co za prowokujące pytanie!
Nawet pobieżne skanowanie odpowiedzi i komentarzy w tym wątku ujawni, jak emocjonalne jest Twoje pozornie proste i bezpośrednie zapytanie.
To nie powinno być zaskakujące.
Niezaprzeczalnie, nieporozumienia wokół koncepcji i stosowania z wskaźnikami stanowi przeważającą przyczyną poważnych awarii w programowaniu w ogóle.
Rozpoznanie tej rzeczywistości jest łatwo widoczne w wszechobecności języków zaprojektowanych specjalnie w celu rozwiązania, a najlepiej w celu uniknięcia wyzwań, które w ogóle wprowadzają wskaźniki. Pomyśl o C ++ i innych pochodnych C, Java i jego relacjach, Pythonie i innych skryptach - tylko jako bardziej znanych i rozpowszechnionych oraz mniej więcej uporządkowanych według wagi problemu.
Rozwijanie głębszego zrozumienia podstawowych zasad, dlatego muszą być adekwatne do każdego człowieka, który dąży do doskonałości w programowaniu - zwłaszcza na poziomie systemowym .
Wyobrażam sobie, że właśnie to nauczyciel chce pokazać.
A natura C sprawia, że jest to wygodny pojazd do tej eksploracji. Mniej zrozumiałe niż asemblowanie - choć być może bardziej zrozumiałe - i wciąż znacznie wyraźniejsze niż języki oparte na głębszej abstrakcji środowiska wykonawczego.
Zaprojektowany w celu ułatwienia deterministycznego tłumaczenia intencji programisty na instrukcje, które mogą zrozumieć maszyny, język C jest językiem systemowym . Choć klasyfikowany jako wysoki, naprawdę należy do kategorii „średniej”; ale ponieważ takiego nie ma, nazwa „systemowa” musi wystarczyć.
Ta cecha jest w dużej mierze odpowiedzialna za to, że jest to język wybrany dla sterowników urządzeń , kodu systemu operacyjnego i wbudowanych implementacji. Co więcej, zasłużenie uprzywilejowana alternatywa w aplikacjach, w których najważniejsza jest optymalna wydajność ; gdzie oznacza to różnicę między przetrwaniem a wyginięciem, a zatem jest koniecznością w przeciwieństwie do luksusu. W takich przypadkach atrakcyjna wygoda przenoszenia przenosi cały swój urok, a wybór lśniącego wykonania najmniej powszechnego mianownika staje się nie do przyjęcia szkodliwą opcją.
To, co czyni C - i niektóre jego pochodne - wyjątkowym, polega na tym, że pozwala użytkownikom na pełną kontrolę - kiedy tego właśnie chcą - bez nakładania na nie powiązanych obowiązków , gdy tego nie robią. Niemniej jednak nigdy nie oferuje więcej niż najcieńsze izolacje od maszyny , dlatego prawidłowe użycie wymaga dokładnego zrozumienia koncepcji wskaźników .
Zasadniczo odpowiedź na twoje pytanie jest wyjątkowo prosta i satysfakcjonująco słodka - potwierdzając twoje podejrzenia. Pod warunkiem jednak, że w niniejszym oświadczeniu przywiązuje się odpowiednią wagę do każdej koncepcji :
Ten pierwszy jest zarówno niezmiennie bezpieczny, jak i potencjalnie odpowiedni , podczas gdy drugi może być zawsze odpowiedni, gdy zostanie ustalony jako bezpieczny . Zaskakujące - dla niektórych - więc ustalenie ważności tego drugiego zależy od tego i wymaga tego pierwszego.
Oczywiście, część zamieszania wynika z efektu rekursji nieodłącznie występującego w zasadzie wskaźnika - oraz z wyzwań związanych z odróżnianiem treści od adresu.
Całkiem słusznie się domyśliłeś,
I kilku autorów potwierdzało: wskaźniki to tylko liczby. Czasami coś bliższego liczbom złożonym , ale wciąż nie więcej niż liczby.
Zabawne spory, w których dochodzi się do tego sporu, ujawniają więcej o ludzkiej naturze niż o programowaniu, ale są warte odnotowania i rozwinięcia. Być może zrobimy to później ...
Jak jeden komentarz zaczyna sugerować; całe to zamieszanie i konsternacja wynika z potrzeby rozróżnienia tego, co jest ważne od tego, co jest bezpieczne , ale jest to nadmierne uproszczenie. Musimy także odróżnić to, co funkcjonalne, a co niezawodne , co praktyczne i co może być właściwe , a ponadto: co jest właściwe w danych okolicznościach, od tego, co może być właściwe w bardziej ogólnym sensie . Nie wspominając; różnica między zgodnością a właściwością .
Pod tym celu, najpierw musimy docenić dokładnie co wskaźnik jest .
Jak zauważyło kilku: termin wskaźnik jest jedynie specjalną nazwą tego, co jest po prostu indeksem , a zatem niczym więcej niż jakąkolwiek inną liczbą .
Powinno to już być oczywiste, biorąc pod uwagę fakt, że wszystkie współczesne komputery głównego nurtu są maszynami binarnymi, które z konieczności działają wyłącznie na liczbach . Obliczenia kwantowe mogą to zmienić, ale jest to bardzo mało prawdopodobne i nie osiągnęło już pełnoletności.
Technicznie, jak zauważyłeś, wskaźniki są dokładniejszymi adresami ; oczywisty wgląd, który w naturalny sposób wprowadza satysfakcjonującą analogię korelowania ich z „adresami” domów lub działek na ulicy.
W płaskim modelu pamięci: cała pamięć systemowa jest zorganizowana w jedną, liniową sekwencję: wszystkie domy w mieście leżą na tej samej drodze, a każdy dom jest jednoznacznie identyfikowany tylko przez jego liczbę. Cudownie proste.
W schematach podzielonych na segmenty : hierarchiczna organizacja dróg numerowanych jest wprowadzana powyżej organizacji domów numerowanych, tak że wymagane są adresy złożone.
Doprowadza nas do dalszego zwrotu, który zamienia zagadkę w tak fascynująco skomplikowaną plątaninę . Powyżej wskazane było zasugerowanie, że wskaźniki są adresami, dla uproszczenia i jasności. Oczywiście to nie jest poprawne. Wskaźnik jest nie adres; wskaźnik jest odniesieniem do adresu , zawiera adres . Podobnie jak koperta zawiera odniesienie do domu. Rozważenie tego może doprowadzić do zrozumienia, co oznaczała sugestia rekurencji zawarta w koncepcji. Nadal; mamy tylko tyle słów i mówimy o adresach odniesień do adresówi takie wkrótce powstrzymuje większość mózgów od niepoprawnego wyjątku kodu operacyjnego . I w większości intencja jest chętnie wyłapywana z kontekstu, więc wróćmy na ulicę.
Pracownicy pocztowi w tym naszym wymyślonym mieście są bardzo podobni do tych, które znajdujemy w „prawdziwym” świecie. Nikt prawdopodobnie nie odniesie udaru, kiedy mówisz lub pytasz o nieprawidłowy adres, ale każdy ostatni będzie się bał, gdy poprosisz go o działanie na podstawie tych informacji.
Załóżmy, że na naszej wyjątkowej ulicy jest tylko 20 domów. Dalej udawaj, że jakaś wprowadzona w błąd lub dysleksyjna dusza skierowała list, bardzo ważny, na numer 71. Teraz możemy zapytać naszego przewoźnika Franka, czy istnieje taki adres, a on po prostu i spokojnie poinformuje: nie . Możemy nawet oczekiwać, żeby ocenić, jak daleko poza ulicy ta lokalizacja będzie leżeć jeśli nie istnieją: około 2,5 razy dalej niż do końca. Nic z tego nie spowoduje irytacji. Jednak gdybyśmy poprosić go, aby dostarczyć ten list, albo podnieść element z tego miejsca, jest on prawdopodobnie całkiem szczery o swoim niezadowoleniu i odmowy spełnienia.
Wskaźniki to tylko adresy, a adresy to tylko liczby.
Sprawdź dane wyjściowe następujących elementów:
Nazwij to tak wieloma wskazówkami, jak chcesz, ważne lub nie. Proszę nie pisać swoje spostrzeżenia, jeśli nie na swojej platformie, lub twój (współczesny) kompilator narzeka.
Ponieważ wskaźniki są po prostu liczbami, ich porównanie jest nieuniknione. W pewnym sensie właśnie to pokazuje twój nauczyciel. Wszystkie poniższe stwierdzenia są całkowicie poprawne - i prawidłowe! - C, a po kompilacji będzie działał bez problemów , mimo że żaden wskaźnik nie musi być inicjowany, a zawarte w nim wartości mogą być niezdefiniowane :
result
wyraźnie w trosce o przejrzystość i drukowanie go zmusić kompilator, aby obliczyć co inaczej byłoby zbędne, martwy kod.Oczywiście program jest źle sformułowany, gdy a lub b jest niezdefiniowany (czytaj: niepoprawnie zainicjowany ) w punkcie testowania, ale jest to całkowicie nieistotne dla tej części naszej dyskusji. Te fragmenty, podobnie jak poniższe instrukcje, są gwarantowane - przez „standard” - do kompilacji i działania bezbłędnie, bez względu na nieważność IN jakiegokolwiek zaangażowanego wskaźnika.
Problemy pojawiają się tylko wtedy, gdy nieprawidłowy wskaźnik jest wyłuskiwany . Gdy poprosimy Franka o odbiór lub dostarczenie pod nieprawidłowy, nieistniejący adres.
Biorąc pod uwagę dowolny dowolny wskaźnik:
Chociaż ta instrukcja musi się skompilować i uruchomić:
... jak to musi:
... dwa kolejne, w przeciwieństwie do tego, nadal będą łatwo kompilować, ale nie wykonają się, chyba że wskaźnik jest poprawny - przez co rozumiemy tutaj jedynie, że odwołuje się on do adresu, do którego przyznana została niniejsza aplikacja :
Jak subtelna zmiana? Różnica polega na różnicy między wartością wskaźnika - który jest adresem, a wartością zawartości: domu pod tym numerem. Problem nie powstaje, dopóki wskaźnik nie zostanie usunięty z listy ; dopóki nie zostanie podjęta próba uzyskania dostępu do adresu, do którego prowadzi łącze. Próbując dostarczyć lub odebrać paczkę poza odcinkiem drogi ...
Co za tym idzie, ta sama zasada bezwzględnie dotyczy bardziej skomplikowanych przykładów, w tym wspomnianego potrzeby do ustalenia wymaganego ważności:
Porównanie relacji i arytmetyka oferują identyczną użyteczność do testowania równoważności i są równoważnie ważne - w zasadzie. Jednak to, co oznaczałyby wyniki takich obliczeń , jest zupełnie inną sprawą - a dokładnie kwestią poruszoną w cytowanych przez ciebie cytatach.
W C tablica jest ciągłym buforem, nieprzerwaną liniową serią lokalizacji pamięci. Porównanie i arytmetyka zastosowana do wskaźników, które odnoszą się do lokalizacji w obrębie takiego pojedynczej serii są naturalnie i oczywiście znaczące zarówno w stosunku do siebie nawzajem, jak i do tej „tablicy” (która jest po prostu identyfikowana przez bazę). Dokładnie to samo dotyczy każdego bloku przydzielonego przez
malloc
lubsbrk
. Ponieważ relacje te są niejawne , kompilator jest w stanie ustalić prawidłowe relacje między nimi, a zatem może być pewien, że obliczenia dostarczą oczekiwanych odpowiedzi.Wykonywanie podobnej gimnastyki na wskaźnikach, które odnoszą się do różnych bloków lub tablic, nie oferuje żadnej takiej nieodłącznej i widocznej użyteczności. Tym bardziej, że jakakolwiek relacja istnieje w danym momencie może zostać unieważniona przez następującą realokację, przy której istnieje duże prawdopodobieństwo, że ulegnie zmianie, a nawet odwróceniu. W takich przypadkach kompilator nie jest w stanie uzyskać niezbędnych informacji w celu ustalenia zaufania do poprzedniej sytuacji.
Ty , jako programista, możesz mieć taką wiedzę! I w niektórych przypadkach są zobowiązani do wykorzystania tego.
Są zatem okoliczności, w których NAWET TO JEST całkowicie WAŻNE i doskonale WŁAŚCIWE.
W rzeczywistości jest to dokładnie to , co
malloc
musi zrobić wewnętrznie, gdy przyjdzie czas, aby spróbować połączyć odzyskane bloki - na zdecydowanej większości architektur. To samo dotyczy alokatora systemu operacyjnego, takiego jak tensbrk
; jeśli bardziej oczywiste , często , na bardziej odmiennych bytach, więcej krytycznie - i istotne również na platformach, gdziemalloc
możetonie być. A ile z nich nie jest napisanych w C?Ważność, bezpieczeństwo i powodzenie działania są nieuchronnie konsekwencją poziomu wglądu, na którym są one zakładane i stosowane.
W cytowanych przez ciebie cytatach Kernighan i Ritchie odnoszą się do ściśle powiązanej, ale jednak odrębnej kwestii. Są zdefiniowaniu tych ograniczeń na języku , i wyjaśnia, jak można wykorzystać możliwości kompilatora cię chronić przez co najmniej wykrywanie potencjalnie błędnych konstrukcji. Opisują długości, do których może przejść mechanizm - jest zaprojektowany - aby pomóc ci w twoim zadaniu programistycznym.Kompilator jest twoim sługą, ty jesteś panem. Mądry mistrz jest jednak dokładnie zaznajomiony z możliwościami swoich różnych sług.
W tym kontekście niezdefiniowane zachowanie służy wskazaniu potencjalnego niebezpieczeństwa i możliwości wyrządzenia szkody; nie sugerować bezpośredniego, nieodwracalnego losu lub końca świata, jaki znamy. Oznacza to po prostu, że my - „ mając na myśli kompilator” - nie jesteśmy w stanie wysnuć żadnych domysłów na temat tego, co to może być, ani reprezentować, i dlatego postanowiliśmy umyć ręce. Nie będziemy ponosić odpowiedzialności za jakiekolwiek nieszczęśliwe zdarzenia, które mogą wyniknąć z korzystania lub niewłaściwego korzystania z tego narzędzia .
W efekcie po prostu mówi: „Poza tym, kowboju : jesteś sam…”
Twój profesor stara się pokazać ci subtelniejsze niuanse .
Zwróć uwagę na to, jak wielką staranność przykuwają swój przykład; i jak kruche to nadal . Biorąc adres
a
, wkompilator jest zmuszany do przydzielania rzeczywistej pamięci dla zmiennej, zamiast umieszczania jej w rejestrze. Jest to zmienna automatyczna, jednak programista nie ma kontroli nad tym, gdzie jest ona przypisana, a zatem nie jest w stanie dokonać żadnej prawidłowej przypuszczenia na temat tego, co po niej nastąpi. I dlatego
a
należy ustawić wartość równą zero, aby kod działał zgodnie z oczekiwaniami.Zwykła zmiana tej linii:
do tego:
powoduje zachowanie programu niezdefiniowane . Przynajmniej pierwsza odpowiedź będzie teraz 1; ale problem jest o wiele bardziej złowieszczy.
Teraz kod zaprasza na katastrofę.
Mimo że nadal jest całkowicie poprawny, a nawet zgodny ze standardem , obecnie jest źle sformułowany i chociaż na pewno będzie się kompilował, może nie działać z różnych powodów. Na razie istnieje wiele problemów - żaden którego kompilator jest w stanie się rozpoznać.
strcpy
rozpocznie się od adresua
i przejdzie dalej, aby wykorzystać - i przenieść - bajt po bajcie, aż napotka zero.p1
Wskaźnik został zainicjowany do bloku dokładnie 10 bajtów.Jeśli
a
zdarzy się, że zostanie umieszczony na końcu bloku, a proces nie będzie miał dostępu do następujących, następny odczyt - p0 [1] - wywoła awarię. Ten scenariusz jest mało prawdopodobny w architekturze x86, ale jest możliwy.Jeśli obszar poza adresem
a
jest dostępny, nie wystąpi błąd odczytu, ale program nadal nie zostanie zapisany przed nieszczęściem.Jeśli w ciągu dziesięciu zaczynających się od adresu zdarzy się zero bajtów
a
, może on nadal przetrwać, ponieważ wtedystrcpy
przestanie działać i przynajmniej nie wystąpi naruszenie zapisu.Jeśli jest nie zarzucać czytania źle, ale nie zerowy bajt występuje w tym okresie o 10,
strcpy
będzie kontynuować i próbować pisać poza blokiem przydzielonejmalloc
.Jeśli ten obszar nie jest własnością procesu, należy natychmiast uruchomić segfault.
Jeszcze bardziej katastrofalna - i subtelna - sytuacja powstaje, gdy proces jest własnością następnego bloku , ponieważ wtedy błąd nie może zostać wykryty, żaden sygnał nie może zostać podniesiony, a więc może „wydawać się” nadal „działać” , podczas gdy faktycznie będzie to nadpisywać inne dane, struktury zarządzania alokatora, a nawet kod (w niektórych środowiskach operacyjnych).
To dlaczego wskaźnik podobne błędy mogą być tak trudne do śledzenia . Wyobraź sobie te wiersze głęboko ukryte w tysiącach wiernie misternie powiązanego kodu, napisane przez kogoś innego, a ty jesteś zmuszony przejrzeć.
Niemniej jednak program musi nadal się kompilować, ponieważ pozostaje całkowicie poprawny i zgodny ze standardem C.
Tego rodzaju błędy, żaden standardowy i żaden kompilator nie chroni przed nieostrożnym. Wyobrażam sobie, że właśnie tego zamierzają cię nauczyć.
Paranoidalne ludzie stale starają się zmienić ten charakter od C do dysponowania tymi problematycznymi możliwości i tak uratować nas od siebie; ale to jest nieuczciwe . Jest to obowiązek, który jesteśmy zobowiązani przyjąć, gdy zdecydujemy się dążyć do władzy i uzyskać swobodę, jaką oferuje nam bardziej bezpośrednia i kompleksowa kontrola nad maszyną. Promotorzy i poszukiwacze doskonałych wyników nigdy nie zaakceptują niczego mniej.
Przenośność i ogólność, którą reprezentuje, jest zasadniczo odrębnym zagadnieniem, a wszystko , co standard ma na celu rozwiązać:
Dlatego całkowicie właściwe jest odróżnianie go od definicji i specyfikacji technicznej samego języka. W przeciwieństwie do tego, co wielu uważa, ogólność jest przeciwna do wyjątkowych i wzorowych .
Podsumowując:
Gdyby to nie była prawda, programowanie, jakie znamy - i uwielbiamy to - nie byłoby możliwe.
źródło
3.4.3
jest także sekcją, na którą powinieneś spojrzeć: definiuje UB jako zachowanie „dla którego niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań”.C11 6.5.6/9
, pamiętając, że słowo „powinno” wskazuje wymaganie L ”. Po odjęciu dwóch wskaźników oba wskazują na elementy tego samego obiektu tablicy lub jeden za ostatnim element obiektu tablicy ".Wskaźniki to tylko liczby całkowite, jak wszystko inne w komputerze. Absolutnie nie można porównać je
<
i>
i wyników produkują bez powodowania program do wypadku. To powiedziawszy, standard nie gwarantuje, że wyniki te mają jakiekolwiek znaczenie poza porównaniami tablic.W twoim przykładzie zmiennych przypisywanych do stosu kompilator może przydzielać te zmienne do rejestrów lub adresów pamięci stosu i w dowolnej kolejności, którą wybierze. Porównania takie jak
<
i>
dlatego nie będą spójne we wszystkich kompilatorach lub architekturach. Jednak==
i!=
nie są tak ograniczone, porównywanie równości wskaźnika jest prawidłową i przydatną operacją.źródło
int x[10],y[10],*p;
, że jeśli kod oceniay[0]
, to oceniap>(x+5)
i pisze*p
bez modyfikowaniap
w międzyczasie, a na koniec oceniay[0]
ponownie ...(ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z')
zamiast tego,isalpha()
ponieważ jakie rozsądne wdrożenie sprawiłoby, że te postacie byłyby nieciągłe? Najważniejsze jest to, że nawet jeśli żadna implementacja, którą znasz, nie ma problemu, powinieneś kodować do standardu tak bardzo, jak to możliwe, jeśli cenisz przenośność. Doceniam etykietę „standard maven”, dzięki za to. Mogę zamieścić swoje CV :-)