Jakie było uzasadnienie braku wyraźnego przechowywania długości tablicy z tablicą w C
?
Moim zdaniem istnieje wiele powodów, aby to zrobić, ale niewiele z tego powodu (C89). Na przykład:
- Dostępna długość bufora może zapobiec jego przepełnieniu.
- Styl Java
arr.length
jest zarówno przejrzysty, jak i pozwala programistom uniknąć konieczności utrzymywania wieluint
s na stosie, jeśli ma się do czynienia z kilkoma tablicami - Parametry funkcji stają się bardziej przekonujące.
Moim zdaniem jednak najbardziej motywującym powodem jest to, że zwykle nie oszczędza się miejsca bez zachowania długości. Zaryzykowałbym stwierdzenie, że większość zastosowań tablic wymaga dynamicznej alokacji. To prawda, że mogą być przypadki, w których ludzie używają tablicy przydzielonej na stosie, ale to tylko jedno wywołanie funkcji * - stos może obsłużyć dodatkowe 4 lub 8 bajtów.
Ponieważ menedżer sterty i tak musi śledzić rozmiar wolnego bloku zużywanego przez dynamicznie przydzielaną tablicę, dlaczego nie uczynić tej informacji użyteczną (i dodać dodatkową regułę, sprawdzaną podczas kompilacji, że nie można jawnie manipulować długością, chyba że lubię strzelać sobie w stopę).
Jedyną rzeczą, o której mogę myśleć po drugiej stronie, jest to, że żadne śledzenie długości nie mogło uprościć kompilatorów, ale nie było o wiele prostsze.
* Technicznie można napisać jakąś funkcję rekurencyjną z tablicą z automatycznym przechowywaniem, aw tym (bardzo skomplikowanym) przypadku przechowywania długości może faktycznie skutkować efektywnie większym wykorzystaniem miejsca.
malloc()
obszaru edycji w przenośny sposób?” To sprawia, że zastanawiam się kilka razy.Odpowiedzi:
Tablice C śledzą ich długość, ponieważ długość tablicy jest właściwością statyczną:
Zwykle nie możesz zapytać o tę długość, ale nie musisz, ponieważ jest ona statyczna - po prostu zadeklaruj makro
XS_LENGTH
dla długości i gotowe.Ważniejszą kwestią jest to, że tablice C domyślnie rozkładają się na wskaźniki, np. Po przekazaniu do funkcji. Ma to pewien sens i pozwala na kilka ciekawych sztuczek na niskim poziomie, ale traci informacje o długości tablicy. Lepszym pytaniem byłoby więc, dlaczego C zaprojektowano z tą ukrytą degradacją wskaźników.
Inną kwestią jest to, że wskaźniki nie potrzebują pamięci poza samym adresem pamięci. C pozwala nam rzutować liczby całkowite na wskaźniki, wskaźniki na inne wskaźniki i traktować wskaźniki tak, jakby były tablicami. Czyniąc to, C nie jest wystarczająco szalony, aby wytworzyć pewną długość tablicy, ale wydaje się ufać motto Spidermana: z wielką mocą programista ma nadzieję spełnić wielką odpowiedzialność za śledzenie długości i przelewów.
źródło
sizeof(xs)
gdziexs
tablica byłaby czymś innym w innym zakresie, jest rażąco fałszywe, ponieważ konstrukcja C nie pozwala tablicom opuścić swojego zakresu. Jeślisizeof(xs)
gdziexs
jest tablica jest inna niżsizeof(xs)
gdziexs
jest wskaźnik, nie jest to zaskoczeniem, ponieważ porównujesz jabłka z pomarańczami .Wiele miało to związek z dostępnymi wówczas komputerami. Skompilowany program musiał nie tylko działać na komputerze o ograniczonych zasobach, ale, co ważniejsze, sam kompilator musiał działać na tych komputerach. W czasie, gdy Thompson opracował C, korzystał z PDP-7 z 8k RAM. Skomplikowane funkcje językowe, które nie miały bezpośredniego odpowiednika w rzeczywistym kodzie maszynowym, po prostu nie zostały uwzględnione w tym języku.
Uważne przeczytanie historii języka C pozwala lepiej zrozumieć powyższe, ale nie było to całkowicie wynikiem ograniczeń maszynowych, które mieli:
Macierze C są z natury silniejsze. Dodanie do nich granic ogranicza to, do czego programista może ich użyć. Takie ograniczenia mogą być przydatne dla programistów, ale z konieczności są również ograniczające.
źródło
to avoid the limitation on the length of a string caused by holding the count in an 8- or 9-bit slot, and partly because maintaining the count seemed, in our experience, less convenient than using a terminator
- cóż za tego :-)Wracając do czasów, gdy C został utworzony, i dodatkowe 4 bajty miejsca na każdy ciąg, bez względu na to, jak krótkie byłoby marnotrawstwem!
Jest jeszcze jeden problem - pamiętaj, że C nie jest zorientowane obiektowo, więc jeśli wykonasz przedrostek długości wszystkich łańcuchów, musiałby zostać zdefiniowany jako wewnętrzny typ kompilatora, a nie a
char*
. Jeśli byłby to specjalny typ, to nie byłbyś w stanie porównać łańcucha z ciągiem stałym, tj .:musiałby mieć specjalne szczegóły kompilatora, aby albo przekonwertować ten ciąg statyczny na Ciąg, albo mieć różne funkcje ciągów, aby uwzględnić prefiks długości.
Myślę jednak, że ostatecznie nie wybrali przedrostka długości w przeciwieństwie do Pascala.
źródło
for
pętla była już skonfigurowana do przestrzegania granic.W C dowolny ciągły podzbiór tablicy jest również tablicą i może być obsługiwany jako taki. Dotyczy to zarówno operacji odczytu, jak i zapisu. Ta właściwość nie zachowałaby się, gdyby rozmiar był przechowywany jawnie.
źródło
&[T]
na przykład dla typów.Największym problemem związanym z oznaczaniem tablic ich długością jest nie tyle przestrzeń wymagana do przechowywania tej długości, ani pytanie, w jaki sposób powinna być przechowywana (użycie jednego dodatkowego bajtu dla krótkich tablic na ogół nie byłoby niekorzystne, ani użycie czterech dodatkowe bajty dla długich tablic, ale użycie czterech bajtów może być nawet dla krótkich tablic). Dużo większym problemem jest to, że dany kod, taki jak:
jedynym sposobem, w jaki kod byłby w stanie zaakceptować pierwsze wywołanie,
ClearTwoElements
ale odrzucić drugie, byłoby otrzymanie przezClearTwoElements
metodę informacji wystarczających do tego, aby wiedzieć, że w każdym przypadku otrzymywał odwołanie do części tablicyfoo
oprócz znajomości, która część. To zwykle podwaja koszt przekazywania parametrów wskaźnika. Ponadto, jeśli każda tablica była poprzedzona wskaźnikiem do adresu tuż za końcem (najbardziej wydajny format sprawdzania poprawności), zoptymalizowany kod dlaClearTwoElements
prawdopodobnie stałby się mniej więcej taki:Zauważ, że program wywołujący metodę może, ogólnie rzecz biorąc, całkowicie słusznie przekazać wskaźnik na początek tablicy lub ostatni element metody; tylko jeśli metoda próbuje uzyskać dostęp do elementów, które wychodzą poza tablicę przekazaną, takie wskaźniki spowodowałyby problemy. W związku z tym wywoływana metoda musi najpierw upewnić się, że tablica jest wystarczająco duża, aby arytmetyka wskaźnika w celu sprawdzenia poprawności jej argumentów nie wykroczyła poza granice, a następnie wykonała obliczenia wskaźnika w celu sprawdzenia poprawności argumentów. Czas poświęcony na taką weryfikację prawdopodobnie przekroczyłby koszt poświęcony na jakąkolwiek prawdziwą pracę. Ponadto metoda może być bardziej wydajna, gdyby została napisana i wywołana:
Koncepcja typu, który łączy coś w celu identyfikacji przedmiotu z czymś w celu zidentyfikowania jego części, jest dobra. Wskaźnik w stylu C jest jednak szybszy, jeśli nie jest konieczne przeprowadzenie sprawdzania poprawności.
źródło
[]
składnia może nadal istnieć dla wskaźników, ale byłaby inna niż dla tych hipotetycznych „prawdziwych” tablic, a opisany problem prawdopodobnie nie istniałby.Jedną z fundamentalnych różnic między C i większością innych języków 3. generacji oraz wszystkimi nowszymi językami, o których wiem, jest to, że C nie zostało zaprojektowane tak, aby ułatwić życie programistom. Został zaprojektowany z oczekiwaniem, że programista wie, co robi i chce robić dokładnie i tylko to. Nie robi nic „za kulisami”, więc nie dostajesz żadnych niespodzianek. Nawet optymalizacja poziomu kompilatora jest opcjonalna (chyba że używasz kompilatora Microsoft).
Jeśli programista chce napisać granice sprawdzając w swoim kodzie, C sprawia, że jest to wystarczająco proste, ale programiści muszą zapłacić odpowiednią cenę pod względem miejsca, złożoności i wydajności. Chociaż od wielu lat nie używałem go w gniewie, nadal go używam, ucząc programowania, aby przejść przez koncepcję podejmowania decyzji opartych na ograniczeniach. Zasadniczo oznacza to, że możesz zrobić wszystko, co chcesz, ale każda podejmowana decyzja ma swoją cenę, o której musisz wiedzieć. Staje się to jeszcze ważniejsze, gdy zaczynasz mówić innym, co chcesz robić w ich programach.
źródło
int f[5];
taka nie tworzyłabyf
tablicy pięcioelementowej; zamiast tego był równoważnyint CANT_ACCESS_BY_NAME[5]; int *f = CANT_ACCESS_BY_NAME;
. Poprzednia deklaracja mogła być przetwarzana bez kompilatora, który naprawdę musiałby „rozumieć” czasy tablicy; po prostu musiał wydać dyrektywę asemblera, aby przydzielić miejsce, a następnie mógł zapomnieć, żef
kiedykolwiek miał coś wspólnego z tablicą. Wynika to z niespójnych zachowań typów tablic.Krótka odpowiedź:
Ponieważ C jest językiem programowania niskiego poziomu , oczekuje się, że sam zajmiesz się tymi problemami, ale daje to większą elastyczność w sposobie jego implementacji.
C ma koncepcję czasu kompilacji tablicy, która jest inicjowana długością, ale w czasie wykonywania całość jest po prostu przechowywana jako pojedynczy wskaźnik na początku danych. Jeśli chcesz przekazać długość tablicy do funkcji wraz z tablicą, zrób to sam:
Lub możesz użyć struktury ze wskaźnikiem i długością lub dowolnego innego rozwiązania.
Język wyższego poziomu zrobiłby to za ciebie jako część swojego typu tablicy. W C masz obowiązek zrobienia tego sam, ale także elastyczność wyboru sposobu, w jaki to zrobić. A jeśli cały kod, który piszesz, zna już długość tablicy, nie musisz w ogóle podawać długości jako zmiennej.
Oczywistą wadą jest to, że bez nieodłącznych ograniczeń sprawdzania tablic przekazywanych jako wskaźniki można stworzyć niebezpieczny kod, ale taka jest natura języków niskiego poziomu / języków systemowych i kompromis, jaki dają.
źródło
Problem dodatkowej przestrzeni dyskowej jest problemem, ale moim zdaniem niewielki. W końcu przez większość czasu i tak będziesz musiał śledzić długość, chociaż amon miał dobrą opinię, że często można ją śledzić statycznie.
Większy problem polega na tym, gdzie przechowywać długość i jak długo ją robić. Nie ma jednego miejsca, które działałoby we wszystkich sytuacjach. Można powiedzieć, że wystarczy zapisać długość w pamięci tuż przed danymi. Co jeśli tablica nie wskazuje na pamięć, ale coś w rodzaju bufora UART?
Pozostawienie tej długości pozwala programiście tworzyć własne abstrakty dla odpowiedniej sytuacji, a istnieje wiele gotowych bibliotek dostępnych dla ogólnego zastosowania. Prawdziwe pytanie brzmi: dlaczego te abstrakcje nie są używane w aplikacjach wrażliwych na bezpieczeństwo?
źródło
You might say just store the length in the memory just before the data. What if the array isn't pointing to memory, but something like a UART buffer?
Czy mógłbyś wyjaśnić to nieco bardziej? A także, że coś, co może zdarzać się zbyt często lub to tylko rzadki przypadek?T[]
nie byłby równoważny,T*
ale raczej przekazałby krotkę wskaźnika i rozmiaru do funkcji. Tablice o stałym rozmiarze mogą rozpadać się na taki wycinek tablicy, zamiast rozkładać się na wskaźniki jak w C. Główna zaleta tego podejścia nie polega na tym, że jest samo w sobie bezpieczne, ale jest to konwencja, na której wszystko, w tym standardowa biblioteka, może budować.Z opracowania języka C :
Ten fragment wyjaśnia, dlaczego wyrażenia tablicowe rozpadają się na wskaźniki w większości przypadków, ale to samo rozumowanie dotyczy tego, dlaczego długość tablicy nie jest przechowywana z samą tablicą; jeśli chcesz mapowania typu jeden do jednego między definicją typu a jej reprezentacją w pamięci (tak jak zrobiła to Ritchie), to nie ma dobrego miejsca do przechowywania tych metadanych.
Pomyśl także o tablicach wielowymiarowych; gdzie miałbyś przechowywać metadane długości dla każdego wymiaru, tak abyś nadal mógł przechodzić przez tablicę czymś podobnym
źródło
Pytanie zakłada, że w C. są tablice. Nie ma. Rzeczy, które są nazywane tablicami, to po prostu cukier składniowy do operacji na ciągłych sekwencjach danych i arytmetyki wskaźników.
Poniższy kod kopiuje niektóre dane z src do dst w kawałkach o dużych rozmiarach, nie wiedząc, że jest to właściwie ciąg znaków.
Dlaczego C jest tak uproszczony, że nie ma odpowiednich tablic? Nie znam poprawnej odpowiedzi na to nowe pytanie. Ale niektórzy ludzie często mówią, że C jest (nieco) bardziej czytelnym i przenośnym asemblerem.
źródło
struct Foo { int arr[10]; }
.arr
jest tablicą, a nie wskaźnikiem.