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 :
- Czynności sprawdzania, porównywania i manipulowania wskaźnikami są zawsze i koniecznie ważne, podczas gdy wnioski wynikające z wyniku zależą od ważności zawartych wartości, a zatem nie muszą być.
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ś,
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.
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 .
- Wykazałeś się silną przyczepnością do koncepcji i, jak niektórzy inni, mogą uważać te ilustracje za protekcjonalnie uproszczone, ale poziom zamieszania widoczny tutaj wymaga takiej prostoty w wyjaśnieniu.
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.
- Niektóre implementacje są jeszcze bardziej skomplikowane, a ogół odrębnych „dróg” nie musi sumować się do ciągłej sekwencji, ale żadna z tych zmian niczego nie zmienia w odniesieniu do instrumentu bazowego.
- Z konieczności jesteśmy w stanie rozłożyć każde takie hierarchiczne łącze z powrotem na płaską organizację. Im bardziej złożona organizacja, tym więcej obręczy będziemy musieli przeskoczyć, aby to zrobić, ale musi to być możliwe. Rzeczywiście, dotyczy to również „trybu rzeczywistego” na x86.
- W przeciwnym razie mapowanie linków do lokalizacji nie byłoby bolesne , ponieważ niezawodne wykonanie - na poziomie systemu - wymaga, aby MUSI tak być.
- wiele adresów nie może być mapowanych na pojedyncze lokalizacje pamięci, oraz
- pojedyncze adresy nigdy nie mogą być mapowane do wielu lokalizacji w pamięci.
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:
void foo( void *p ) {
printf(“%p\t%zu\t%d\n”, p, (size_t)p, p == (size_t)p);
}
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 :
- Jesteśmy obliczania tylko
result
wyraźnie w trosce o przejrzystość i drukowanie go zmusić kompilator, aby obliczyć co inaczej byłoby zbędne, martwy kod.
void foo( size_t *a, size_t *b ) {
size_t result;
result = (size_t)a;
printf(“%zu\n”, result);
result = a == b;
printf(“%zu\n”, result);
result = a < b;
printf(“%zu\n”, result);
result = a - b;
printf(“%zu\n”, result);
}
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:
int *p;
Chociaż ta instrukcja musi się skompilować i uruchomić:
printf(“%p”, p);
... jak to musi:
size_t foo( int *p ) { return (size_t)p; }
... 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 :
printf(“%p”, *p);
size_t foo( int *p ) { return *p; }
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:
int* validate( int *p, int *head, int *tail ) {
return p >= head && p <= tail ? p : NULL;
}
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 przezmalloc
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 ten sbrk
; 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
, w
p[0].p0 = &a;
kompilator 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 dlategoa
należy ustawić wartość równą zero, aby kod działał zgodnie z oczekiwaniami.
Zwykła zmiana tej linii:
char a = 0;
do tego:
char a = 1; // or ANY other value than 0
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 adresu a
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ż wtedy strcpy
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 przydzielonej malloc
.
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ć:
Niniejszy dokument określa formę i ustala interpretację programów wyrażonych w języku programowania C. Jego celem jest promowanie przenośności , niezawodności, łatwości konserwacji i wydajnego wykonywania programów w języku C w różnych systemach komputerowych .
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:
- Samo badanie i manipulowanie wskaźnikami jest niezmiennie ważne i często owocne . Interpretacja wyników może, ale nie musi być znacząca, ale nie można zaprosić nieszczęścia, dopóki wskaźnik nie zostanie odwołany ; do momentu podjęcia próby uzyskania dostępu do adresu, do którego prowadzi link.
Gdyby to nie była prawda, programowanie, jakie znamy - i uwielbiamy to - nie byłoby możliwe.
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).