Dlaczego tablice C nie mogą mieć długości 0?

13

Standard C11 mówi, że tablice, zarówno o wielkości, jak i o zmiennej długości „powinny mieć wartość większą niż zero”. Jakie jest uzasadnienie niedopuszczenia długości 0?

Zwłaszcza w przypadku tablic o zmiennej długości doskonale jest mieć rozmiar zero co jakiś czas. Jest także przydatny w przypadku tablic statycznych, gdy ich rozmiar pochodzi z opcji konfiguracji makra lub kompilacji.

Co ciekawe, GCC (i clang) zapewniają rozszerzenia, które umożliwiają tablice o zerowej długości. Java pozwala także na tablice o długości zero.

Kevin Cox
źródło
7
stackoverflow.com/q/8625572 ... "Tablica o zerowej długości byłaby trudna i myląca, aby pogodzić się z wymogiem, aby każdy obiekt miał unikalny adres."
Robert Harvey
3
@RobertHarvey: Biorąc pod uwagę struct { int p[1],q[1]; } foo; int *pp = p+1;, ppbyłby prawidłowym wskaźnikiem, ale *ppnie miałby unikalnego adresu. Dlaczego ta sama logika nie mogła się utrzymać w przypadku tablicy o zerowej długości? Powiedzieć, że biorąc pod uwagę int q[0]; w ramach struktury , qby odnosić się do adresu, którego ważność będzie podobnie jak w p+1powyższym przykładzie.
supercat
@DocBrown Ze standardu C11 6.7.6.2.5 mówiącego o wyrażeniu użytym do określenia wielkości VLA „… za każdym razem, gdy jest ono oceniane, powinno mieć wartość większą niż zero”. Nie wiem o C99 (i wydaje się dziwne, że by to zmienili), ale wygląda na to, że nie możesz mieć długości zero.
Kevin Cox,
@KevinCox: czy jest dostępna darmowa wersja online standardu C11 (lub części, o której mowa)?
Doc Brown
Ostateczna wersja nie jest dostępna za darmo (szkoda), ale możesz pobrać wersje robocze. Ostatnia dostępna wersja robocza to open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf .
Kevin Cox,

Odpowiedzi:

11

Problemem, który postawiłbym, jest to, że tablice C są tylko wskaźnikami do początku przydzielonej części pamięci. Posiadanie rozmiaru 0 oznaczałoby, że masz wskaźnik do ... nic? Nie możesz mieć nic, więc musiałaby zostać wybrana dowolna rzecz. Nie możesz użyć null, ponieważ wtedy tablice o długości 0 wyglądałyby jak wskaźniki zerowe. I w tym momencie każda inna implementacja wybierze różne arbitralne zachowania, prowadzące do chaosu.

Telastyn
źródło
8
@delnan: Cóż, jeśli chcesz być pedantyczny, arytmetyka tablic i wskaźników jest zdefiniowana w taki sposób, że wskaźnik można wygodnie wykorzystać do uzyskania dostępu do tablicy lub do jej symulacji. Innymi słowy, to arytmetyka wskaźnika i indeksowanie tablicy są równoważne w C. Ale wynik i tak jest taki sam ... jeśli długość tablicy wynosi zero, nadal wskazujesz na nic.
Robert Harvey
3
@RobertHarvey Wszystko prawda, ale twoje końcowe słowa (i cała odpowiedź z perspektywy czasu) wydają się po prostu mylącym i mylącym sposobem na wyjaśnienie, że taka tablica ( myślę, że to, co ta odpowiedź nazywa „przydzieloną częścią pamięci”?) Miałaby sizeof0 i jak to spowodowałoby kłopoty. Wszystko to można wyjaśnić przy użyciu odpowiednich pojęć i terminologii bez utraty zwięzłości lub jasności. Pomieszanie tablic i wskaźników ryzykuje jedynie rozprzestrzenieniem tablic = nieporozumienie wskaźników (co jest ważniejsze w innych kontekstach) bez korzyści.
2
Nie możesz użyć wartości null, ponieważ wtedy tablice o długości 0 wyglądałyby jak wskaźniki null ” - w rzeczywistości to właśnie robi Delphi. Puste dynarrays i puste longstrings są technicznie zerowymi wskaźnikami.
JensG
3
-1, jestem tutaj z @delnan. To nic nie wyjaśnia, szczególnie w kontekście tego, co OP napisał o niektórych głównych kompilatorach obsługujących koncepcję tablic zerowej długości. Jestem pewien, że tablice zerowej długości mogłyby być dostarczone w C w sposób niezależny od implementacji, nie „prowadząc do chaosu”.
Doc Brown
6

Spójrzmy, jak tablica jest zwykle układana w pamięci:

         +----+
arr[0] : |    |
         +----+
arr[1] : |    |
         +----+
arr[2] : |    |
         +----+
          ...
         +----+
arr[n] : |    |
         +----+

Zauważ, że nie ma oddzielnego obiektu o nazwie, arrktóry przechowuje adres pierwszego elementu; gdy w wyrażeniu pojawia się tablica, C oblicza adres pierwszego elementu w razie potrzeby.

Więc pomyślmy o tym: tablica 0-element miałby żadnego przechowywania Odstawić na nim, czyli nie ma nic do obliczania adresu tablicy z (innymi słowy, nie ma odwzorowania obiektu dla identyfikatora). To tak, jakby powiedzieć: „Chcę utworzyć intzmienną, która nie zajmuje pamięci”. To nonsensowna operacja.

Edytować

Tablice Java to zupełnie inne zwierzęta niż tablice C i C ++; nie są typem prymitywnym, ale typem pochodnym Object.

Edytuj 2

Punkt poruszony w poniższych komentarzach - ograniczenie „większe niż 0” dotyczy tylko tablic, w których rozmiar jest określony przez wyrażenie stałe ; VLA może mieć długość 0 Deklaracja VLA z nietrwałym wyrażeniem o wartości 0 nie jest naruszeniem ograniczenia, ale wywołuje niezdefiniowane zachowanie.

Oczywiste jest, że VLA to inne zwierzęta niż zwykłe tablice , a ich implementacja może pozwolić na rozmiar 0 . Nie można ich zadeklarować staticani w zakresie plików, ponieważ rozmiar takich obiektów musi być znany przed uruchomieniem programu.

Nie jest również nic warte, że od C11 implementacje nie są wymagane do obsługi VLA.

John Bode
źródło
3
Przepraszam, ale IMHO nie rozumiesz, tak jak Telastyn. Tablice o zerowej długości mogą mieć sens, a istniejące implementacje, takie jak te, które OP powiedział nam o, pokazują, że można to zrobić.
Doc Brown
@DocBrown: Po pierwsze, zastanawiałem się, dlaczego standard językowy najprawdopodobniej ich nie zezwala. Po drugie, chciałbym podać przykład, w którym tablica o długości 0 ma sens, ponieważ szczerze mówiąc, nie mogę o niej myśleć. Najbardziej prawdopodobne wdrożenie należy traktować T a[0]jako T *a, ale dlaczego nie po prostu użyć T *a?
John Bode
Przepraszam, ale nie kupuję „teoretycznego uzasadnienia”, dlaczego standard tego zabrania. Przeczytaj moją odpowiedź, jak adres można łatwo obliczyć. Sugeruję skorzystanie z linku w pierwszym komentarzu Roberta Harveysa pod pytaniem i przeczytanie drugiej odpowiedzi, jest przydatny przykład.
Doc Brown
@DocBrown: Ah. structHack. Nigdy nie użyłem tego osobiście; nigdy nie pracował nad problemem, który wymagałby zmiennej wielkości struct.
John Bode
2
I nie zapominając o AFAIK od C99, C pozwala na tablice o zmiennej długości. A gdy rozmiar tablicy jest parametrem, brak konieczności traktowania wartości 0 jako specjalnego przypadku może uprościć wiele programów.
Doc Brown
2

Zazwyczaj chciałbyś, aby tablica rozmiarów zero (w rzeczywistości zmiennych) znała swój rozmiar w czasie wykonywania. Następnie spakuj to structi użyj elastycznych elementów tablicy , takich jak np .:

struct my_st {
   unsigned len;
   double flexarray[]; // of size len
};

Oczywiście elastyczny element tablicy musi być ostatnim w swoim rodzaju structi musisz mieć coś wcześniej. Często byłoby to coś związanego z faktyczną długością zajmowanego przez środowisko wykonawcze tego elastycznego elementu tablicy.

Oczywiście przydzieliłbyś:

 unsigned len = some_length_computation();
 struct my_st*p = malloc(sizeof(struct my_st)+len*sizeof(double));
 if (!p) { perror("malloc my_st"); exit(EXIT_FAILURE); };
 p->len = len;
 for (unsigned ix=0; ix<len; ix++)
    p->flexarray[ix] = log(3.0+(double)ix);

AFAIK, było to już możliwe w C99 i jest bardzo przydatne.

BTW, elastyczne elementy tablicy nie istnieją w C ++ (ponieważ trudno byłoby określić, kiedy i jak należy je zbudować i zniszczyć). Zobacz jednak przyszłość std :: dynarray

Basile Starynkevitch
źródło
Wiesz, mogą być ograniczone do trywialnych typów i nie byłoby trudności.
Deduplicator
2

Jeśli wyrażenie type name[count]jest zapisane w jakiejś funkcji, to powiesz kompilatorowi C, aby przydzielił sizeof(type)*countbajty ramki stosu i obliczył adres pierwszego elementu w tablicy.

Jeśli wyrażenie type name[count]jest zapisane poza wszystkimi definicjami funkcji i struktur, należy poinformować kompilator C, aby przydzielił sizeof(type)*countbajty segmentu danych i obliczył adres pierwszego elementu w tablicy.

namew rzeczywistości jest stałym obiektem, który przechowuje adres pierwszego elementu w tablicy, a każdy obiekt, który przechowuje adres pewnej pamięci, jest nazywany wskaźnikiem, dlatego jest to powód, dla którego traktujesz go namejako wskaźnik, a nie tablicę. Należy pamiętać, że do tablic w C można uzyskać dostęp tylko poprzez wskaźniki.

Jeśli countjest stałym wyrażeniem, którego wynikiem jest zero, to powiesz kompilatorowi C, aby przydzielił zero bajtów albo ramce stosu, albo segmentowi danych i zwrócił adres pierwszego elementu w tablicy, ale problem polega na tym, że pierwszy element tablicy zerowej długości nie istnieje i nie można obliczyć adresu czegoś, co nie istnieje.

Jest to racjonalne, że element nr. count+1nie istnieje w counttablicy -długości, dlatego kompilator C zabrania definiowania tablicy o zerowej długości jako zmiennej wewnątrz i na zewnątrz funkcji, ponieważ jaka jest wówczas jej zawartość name? Jaki namedokładnie adres przechowuje?

Jeśli pjest wskaźnikiem, to wyrażenie p[n]jest równoważne z*(p + n)

Tam, gdzie gwiazdka * w odpowiednim wyrażeniu oznacza dereferencję operacji wskaźnika, co oznacza dostęp do pamięci wskazanej przez p + nlub dostęp do pamięci, w której adres jest zapisany p + n, gdzie p + nwyrażenie wyrażenia, bierze adres pi dodaje do tego adresu liczbę npomnożoną przez rozmiar typu wskaźnika p.

Czy można dodać adres i numer?

Tak, jest to możliwe, ponieważ adres jest liczbą całkowitą bez znaku powszechnie reprezentowaną w notacji szesnastkowej.

użytkownik307542
źródło
Wiele kompilatorów używało dopuszczania deklaracji tablic zerowej wielkości, zanim Standard to zabronił, a wiele nadal dopuszcza takie deklaracje jako rozszerzenie. Takie deklaracje nie będą stanowiły problemu, jeśli rozpozna się, że obiekt wielkości Nma N+1skojarzone adresy, Nz których pierwszy identyfikuje unikatowe bajty, a ostatni Nz każdego punktu tuż za jednym z tych bajtów. Taka definicja działałaby dobrze nawet w zdegenerowanym przypadku, w którym Nwynosi 0.
supercat
1

Jeśli chcesz wskaźnik do adresu pamięci, zadeklaruj go. Tablica faktycznie wskazuje na zarezerwowaną część pamięci. Tablice zanikają do wskaźników po przekazaniu do funkcji, ale jeśli pamięć, na którą wskazują, znajduje się na stosie, nie ma problemu. Nie ma powodu, aby deklarować tablicę o rozmiarze zero.

ncmathsadist
źródło
2
Zasadniczo nie zrobiłbyś tego bezpośrednio, ale w wyniku makra lub podczas deklarowania tablicy o zmiennej długości z danymi dynamicznymi.
Kevin Cox,
Tablica nigdy nie wskazuje. Może zawierać wskaźniki, aw większości kontekstów faktycznie używasz wskaźnika do pierwszego elementu, ale to inna historia.
Deduplicator
1
Nazwa tablicy JEST stałym wskaźnikiem pamięci zawartej w tablicy.
ncmathsadist
1
Nie, w większości kontekstów nazwa tablicy rozpada się na wskaźnik do pierwszego elementu. Różnica jest często kluczowa.
Deduplicator
1

Od czasów oryginalnej wersji C89, kiedy norma C określała, że ​​coś ma niezdefiniowane zachowanie, oznaczało to: „Rób wszystko, co sprawi, że implementacja na konkretnej platformie docelowej będzie najbardziej odpowiednia do zamierzonego celu”. Autorzy Standardu nie chcieli zgadywać, jakie zachowania mogą być najbardziej odpowiednie do określonego celu. Istniejące implementacje C89 z rozszerzeniami VLA mogły mieć inne, ale logiczne zachowania, gdy otrzymały rozmiar zero (np. Niektóre mogły traktować tablicę jako wyrażenie adresowe dające wartość NULL, podczas gdy inne traktują ją jako wyrażenie adresowe, które może być równe adresowi inna dowolna zmienna, ale można bezpiecznie dodać zero bez pułapki). Gdyby jakikolwiek kod mógł polegać na tak różnych zachowaniach, autorzy Standardu nie „

Zamiast próbować odgadnąć, co mogą zrobić implementacje, lub sugerować, że każde zachowanie powinno być uważane za lepsze od innych, autorzy Standardu po prostu zezwolili implementatorom na użycie osądu przy rozpatrywaniu tej sprawy w sposób, który uznają za najlepszy. Implementacje, które wykorzystują malloc () za kulisami, mogą traktować adres tablicy jako NULL (jeśli malloc o rozmiarze zero daje zero), te, które używają obliczeń adresu stosu mogą dać wskaźnik, który pasuje do adresu innej zmiennej, a niektóre inne implementacje mogą to zrobić inne rzeczy. Nie sądzę, że spodziewali się, że autorzy kompilatorów zrobią wszystko, aby skrzynka narożna o zerowym rozmiarze zachowywała się celowo bezużyteczna.

supercat
źródło