Jak działa porównywanie wskaźników w C? Czy można porównywać wskaźniki, które nie wskazują tej samej tablicy?

33

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 pi qpunkt 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ż pti pxnie wskazują na tę samą tablicę (przynajmniej w moim rozumieniu).

Jest pt > pxtak również dlatego, że oba wskaźniki wskazują na zmienne przechowywane na stosie, a stos rośnie, więc adres pamięci tjest większy niż adres x? Co pt > pxjest 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 sbrkmożna znacznie porównać. Nie gwarantuje tego standard, który pozwala na porównywanie wskaźników tylko w obrębie tablicy. Dlatego ta wersja mallocjest 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 1wydrukowaniem:

    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:

  1. p[0].p0 < p[0].p1
  2. p[1].p0 < p[1].p1
  3. p[2].p0 < p[2].p1

Odpowiedź jest 0, 1i 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 , , a może . Jeśli punktem pytania / klasy byłyby szczegóły implementacji systemu operacyjnego niskiego poziomu, a nie przenośne C.)

Shisui
źródło
17
Jesteś może mylące, co jest ważne w Ctym, co jest bezpieczne w C. 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).
Adrian Mole
13
Tak na marginesie, to powinien nie być nauki C z K & R. Po pierwsze, język przeszedł wiele zmian od tego czasu. I, szczerze mówiąc, przykładowy kod z tamtego okresu pochodzi z okresu, w którym ceniono zwięzłość, a nie czytelność.
paxdiablo
5
Nie, nie gwarantuje się, że zadziała. Może to zawieść w praktyce na komputerach z segmentowanymi modelami pamięci. Zobacz Czy C ma odpowiednik std :: less z C ++? Na większości nowoczesnych maszyn będzie działał pomimo UB.
Peter Cordes
6
@Adam: Zamknij, ale tak naprawdę jest to UB (chyba że kompilator, którego używał OP, GCC, zdecyduje się go zdefiniować. Może). Ale UB nie oznacza „zdecydowanie wybucha”; jednym z możliwych zachowań UB jest działanie zgodne z oczekiwaniami !! To sprawia, że ​​UB jest tak paskudny; może działać poprawnie w kompilacji debugowania i zawieść z włączoną optymalizacją, lub odwrotnie, lub przerwać w zależności od otaczającego kodu. Porównanie innych wskaźników nadal da ci odpowiedź, ale język nie określa, co ta odpowiedź będzie oznaczać (jeśli w ogóle). Nie, awarie są dozwolone. To naprawdę UB.
Peter Cordes,
3
@Adam: O tak, bez względu na pierwszą część mojego komentarza, źle go odczytałem. Ale twierdzisz, że porównanie innych wskaźników nadal da ci odpowiedź . To nieprawda. Byłby to nieokreślony wynik , nie pełny UB. UB jest znacznie gorszy i oznacza, że ​​twój program może segfaultować lub SIGILL, jeśli wykonanie osiągnie tę instrukcję z tymi danymi wejściowymi (w dowolnym momencie przed lub po tym, co się faktycznie wydarzy). (Możliwe tylko na x86-64, jeśli UB jest widoczny w czasie kompilacji, ale ogólnie wszystko może się zdarzyć.) Częścią UB jest pozwolenie kompilatorowi na przyjmowanie „niebezpiecznych” założeń podczas generowania asm.
Peter Cordes

Odpowiedzi:

33

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:

Po porównaniu dwóch wskaźników wynik zależy od względnych lokalizacji w przestrzeni adresowej wskazywanych obiektów. Jeśli dwa wskaźniki do typów obiektów oba wskazują na ten sam obiekt lub oba wskazują jeden za ostatnim elementem tego samego obiektu tablicowego, są one równe. Jeśli wskazywane obiekty są elementami tego samego obiektu agregującego, wskaźniki do elementów struktury zadeklarowanych później porównują większe niż wskaźniki do elementów zadeklarowanych wcześniej w strukturze, a wskaźniki do elementów tablicy o większych wartościach indeksu dolnego są większe niż wskaźniki do elementów tej samej tablicy z niższymi wartościami indeksu dolnego. Wszystkie wskaźniki dla członków tego samego obiektu unii są równe.

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ń:

  • Istnieje model płaskiej pamięci, w którym istnieje zgodność 1: 1 między adresem a wartością całkowitą.
  • Przekształcone wartości wskaźnika mieszczą się w typie całkowitym.
  • To, że implementacja po prostu traktuje wskaźniki jako liczby całkowite podczas wykonywania porównań bez korzystania z wolności wynikającej z nieokreślonego zachowania.
  • Czy stos jest używany, a zmienne lokalne są tam przechowywane.
  • To, że stos jest używany do pobierania przydzielonej pamięci.
  • Że stos (a zatem zmienne lokalne) pojawia się pod wyższym adresem niż sterta (a zatem przydzielone obiekty).
  • Te stałe ciągów pojawiają się pod niższym adresem niż sterty.

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.

dbush
źródło
3
@Shisui Nawet biorąc to pod uwagę, nadal nie powinieneś polegać na wynikach. Kompilatory mogą stać się bardzo agresywne, jeśli chodzi o optymalizację, i wykorzystają niezdefiniowane zachowanie jako okazję. Możliwe, że użycie innego kompilatora i / lub różnych ustawień optymalizacji może generować różne dane wyjściowe.
dbush
2
@Shisui: Zasadniczo będzie działać na komputerach z płaskim modelem pamięci, takich jak x86-64. Niektóre kompilatory dla takich systemów mogą nawet zdefiniować zachowanie w swojej dokumentacji. Ale jeśli nie, może dojść do „szalonego” zachowania z powodu widocznego w czasie kompilacji UB. (W praktyce nie sądzę, aby ktokolwiek tego chciał, więc nie jest to coś, czego szukają kompilatorzy głównego nurtu i „próbują się złamać”).
Peter Cordes
1
Jak gdyby kompilator widzi, że jedna ścieżka realizacji doprowadziłoby do <między mallocrezultatu 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 w ud2instrukcji (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ńca voidniefunkcji. godbolt.org jest obecnie niedostępny , ale spróbuj skopiować / wkleić int foo(){int x=2;}i zwróć uwagę na brakret
Peter Cordes
4
@Shisui: TL: DR: to nie jest przenośny C, pomimo tego, że działa dobrze na Linuksie x86-64. Jednak przyjmowanie założeń dotyczących wyników porównania jest po prostu szalone. Jeśli nie jesteś w głównym wątku, stos wątków zostanie przydzielony dynamicznie przy użyciu tego samego mechanizmu, którego mallocuż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żej mallocdynamicznie przydzielanych przechowywanie.
Peter Cordes
2
@PeterCordes: Potrzebne jest rozpoznanie różnych aspektów zachowania jako „opcjonalnie zdefiniowanych”, tak aby implementacje mogły je zdefiniować lub nie, w wolnym czasie, ale muszą wskazać w sposób testowy (np. Wstępnie zdefiniowane makro), jeśli tego nie robią. Dodatkowo, zamiast charakteryzować, że każda sytuacja, w której skutki optymalizacji byłyby obserwowalne jako „Nieokreślone zachowanie”, o wiele bardziej użyteczne byłoby stwierdzenie, że optymalizatorzy mogą uznać niektóre aspekty zachowania za „nieobserwowalne”, jeśli wskazują, że Zrób tak. Na przykład, biorąc pod uwagę int x,y;implementację ...
supercat 29.12.19
12

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.

Po pierwsze, pomyślałem, że zostanę niezdefiniowany lub jakiś rodzaj lub błąd, ponieważ pt a px nie wskazują na tę samą tablicę (przynajmniej w moim rozumieniu).

Nie, wynik zależy od wdrożenia i innych nieprzewidzianych czynników.

Jest także pt> px, ponieważ 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? Dlaczego pt> px jest prawdą?

Niekoniecznie jest stos . Gdy istnieje, nie musi rosnąć. Może dorastać. Może to być nieciągłe w jakiś dziwny sposób.

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.

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.

Po porównaniu dwóch wskaźników wynik zależy od względnych lokalizacji w przestrzeni adresowej wskazywanych obiektów. ... Jeśli wskazywane obiekty są członkami tego samego obiektu agregującego, ... wskaźniki do elementów tablicy o większych wartościach indeksu dolnego porównują większe niż wskaźniki do elementów tej samej tablicy o niższych wartościach indeksu dolnego.

We wszystkich innych przypadkach zachowanie jest niezdefiniowane.

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).

nanofarad
źródło
1
Co ważniejsze, mając ti xbę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.
Peter Cordes
1
Twój kompilator mógł zoptymalizować się cały funkcję w tym widocznych skutków ubocznych nie zawyżenia: dla innych rodzajów UB (jak spadając koniec niebędącego voidfunkcja) 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ś powodu gcctego nie robi, tylko g++).
Peter Cordes
6

Potem zapytałem co

p[0].p0 < p[0].p1
p[1].p0 < p[1].p1
p[2].p0 < p[2].p1

Oceń do. Odpowiedź to 0, 1 i 0.

Te pytania ograniczają się do:

  1. Czy stos jest powyżej lub poniżej stosu.
  2. Jest stertą powyżej lub poniżej dosłownej sekcji programu.
  3. tak samo jak [1].

Odpowiedzią na wszystkie trzy pytania jest „zdefiniowanie wdrożenia”. Pytania twojego profesora są fałszywe; oparli go na tradycyjnym układzie uniksowym:

<empty>
text
rodata
rwdata
bss
< empty, used for heap >
...
stack
kernel

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.

mevets
źródło
3
Nie zdefiniowano implementacji, niezdefiniowana! Pomyśl o tym w ten sposób, pierwsze mogą się różnić w zależności od implementacji, ale implementacje powinny dokumentować sposób decydowania o zachowaniu. To ostatnie oznacza, że ​​zachowanie może się różnić w dowolny sposób, a implementacja nie musi ci mówić: przysiad :-)
paxdiablo
1
@paxdiablo: Według uzasadnienia autorów standardu „Niezdefiniowane zachowanie ... identyfikuje również obszary możliwego rozszerzenia języka zgodnego: implementator może ulepszyć język, podając definicję zachowania nieokreślonego”. Uzasadnienie mówi dalej: „Celem jest umożliwienie programistom walki o stworzenie potężnych programów C, które są również wysoce przenośne, bez zdawania się poniżać doskonale użytecznych programów C, które nie są przenośne, a zatem przysłówek ściśle”. Komercyjni autorzy kompilatorów to rozumieją, ale niektórzy inni pisarze kompilatorów nie.
supercat
Istnieje inny aspekt zdefiniowany w ramach implementacji; porównanie wskaźnika jest podpisane , więc w zależności od maszyny / systemu operacyjnego / kompilatora niektóre adresy mogą być interpretowane jako ujemne. Na przykład 32-bitowa maszyna, która umieściła stos na 0xc << 28, prawdopodobnie pokazywałaby zmienne automatyczne pod adresem wynajmującego niż sterta lub rodata.
mevets
1
@mevets: Czy standard określa jakąkolwiek sytuację, w której można zaobserwować podpisywanie wskaźników w porównaniach? Spodziewałbym się, że jeśli platforma 16-bitowa zezwala na obiekty większe niż 32768 bajtów i arr[]jest takim obiektem, Standard nakazuje arr+32768porównywanie większych danych, arrnawet jeśli porównanie wskaźnika ze znakiem zgłasza inaczej.
supercat
Nie wiem; standard C okrąża dziewiąty krąg Dantego, modląc się o eutanazję. PO konkretnie wspomniał o K&R i pytaniu egzaminacyjnym. #UB to szczątki leniwej grupy roboczej.
mevets
1

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ż:

extern int x[],y[];
int test(int i)
{
    int *p = y+i;
    y[0] = 4;
    if (p == x+10)
        *p = 1;
    return y[0];
}

Zarówno clang, jak i gcc wygenerują kod, który zawsze zwróci 4, nawet jeśli xjest dziesięć elementów, ynatychmiast następuje po nim i iwynosi zero, co powoduje, że porównanie jest prawdziwe i p[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 przez x[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 do x[10]zdefiniowałby tylko, gdyby xmiał co najmniej 11 elementów, co uniemożliwiłoby wpływ na ten dostęp y.

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ń.

supercat
źródło
0

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ę

Hans Lepoeter
źródło
Technicznie, tablice nie są naprawdę wyjątek, ponieważ nie deklarują arr[0], arr[1]itp oddzielnie. Deklarujesz arrjako całość, więc porządkowanie poszczególnych elementów tablicy jest kwestią inną niż opisana w tym pytaniu.
paxdiablo
1
Elementy struktury są gwarantowane w porządku, co gwarantuje, że można użyć memcpydo 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 lub malloc()przydzielonej pamięci. offsetofMakro będzie raczej bezużyteczne, jeśli jeden nie mógł do tego samego rodzaju wskaźnik arytmetycznych z bajtów struct jak z char[], ale standard nie wyraźnie powiedzieć, że bajty struct są (lub mogą być wykorzystywane jako) obiekt tablicowy.
supercat
-4

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 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 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 przezmalloclubsbrk. 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 mallocmusi 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, gdziemallocmoż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ć.

strcpyrozpocznie się od adresu ai przejdzie dalej, aby wykorzystać - i przenieść - bajt po bajcie, aż napotka zero.

p1Wskaźnik został zainicjowany do bloku dokładnie 10 bajtów.

  • Jeśli azdarzy 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 strcpyprzestanie 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, strcpybę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.

Ghii Velte
źródło
3
Ta odpowiedź jest niestety z natury nieprawidłowa. Nie można nic uzasadnić niezdefiniowanym zachowaniem. Porównanie nie musi być wykonywane na poziomie maszyny.
Antti Haapala
6
Ghii, właściwie nie. Jeśli spojrzeć na załącznik J C11 i 6.5.8, sam akt porównania to UB. Dereferencje to osobny problem.
paxdiablo
6
Nie, UB może nadal być szkodliwe, nawet zanim wskaźnik zostanie odwołany. Kompilator może całkowicie zoptymalizować funkcję z UB do pojedynczego NOP, nawet jeśli to oczywiście zmienia widoczne zachowanie.
nanofarad
2
@ Ghii, załącznik J (fragment, o którym wspomniałem) to lista rzeczy, które są niezdefiniowanym zachowaniem, więc nie jestem pewien, jak to popiera twój argument :-) 6.5.8 wyraźnie przywołuje porównanie jako UB. W komentarzu do supercata nie ma porównania, kiedy drukujesz wskaźnik, więc prawdopodobnie masz rację, że się nie zawiesi. Ale nie o to pytał OP. 3.4.3jest także sekcją, na którą powinieneś spojrzeć: definiuje UB jako zachowanie „dla którego niniejsza Norma Międzynarodowa nie nakłada żadnych wymagań”.
paxdiablo
3
@GhiiVelte, ciągle stwierdzasz rzeczy, które są po prostu złe, pomimo tego , że ci o tym mówiono . Tak, opublikowany fragment kodu musi się skompilować, ale Twoje twierdzenie, że działa bez problemów, jest nieprawidłowe. Sugeruję, abyś rzeczywiście przeczytał standard, szczególnie (w tym przypadku) 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 ".
paxdiablo
-5

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ą.

nickelpro
źródło
2
Stos słów pojawia się dokładnie zero razy w standardzie C11. Niezdefiniowane zachowanie oznacza, że wszystko może się zdarzyć (w tym awaria programu).
paxdiablo
1
@paxdiablo Czy powiedziałem, że tak?
nickelpro
2
Wspomniałeś o zmiennych przypisanych do stosu. W standardzie nie ma stosu, to tylko szczegół implementacji. Poważniejszym problemem z tą odpowiedzią jest spór, w którym możesz porównywać wskaźniki bez szansy na awarię - to po prostu źle.
paxdiablo
1
@nickelpro: Jeśli ktoś chce napisać kod zgodny z optymalizatorami w gcc i clang, konieczne jest przeskakiwanie przez wiele głupich obręczów. Oba optymalizatory będą agresywnie poszukiwały okazji do wyciągania wniosków na temat tego, do których rzeczy dostęp będą mieli wskaźniki, ilekroć istnieje sposób, w jaki Standard może zostać przekręcony, aby je uzasadnić (a nawet czasami, gdy nie ma). Biorąc pod uwagę int x[10],y[10],*p;, że jeśli kod ocenia y[0], to ocenia p>(x+5)i pisze *pbez modyfikowania pw międzyczasie, a na koniec ocenia y[0]ponownie ...
supercat
1
nickelpro, zgadzam się zgodzić, że się nie zgadzam, ale twoja odpowiedź jest zasadniczo błędna. Porównuję twoje podejście do ludzi, którzy używają (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 :-)
paxdiablo,