it-swarm.com.de

Warum verfolgen C-Arrays ihre Länge nicht?

Was war der Grund dafür, dass die Länge eines Arrays nicht explizit mit einem Array in C gespeichert wurde?

So wie ich das sehe, gibt es überwältigende Gründe daz aber nicht sehr viele, die den Standard unterstützen (C89). Zum Beispiel:

  1. Wenn die Länge in einem Puffer verfügbar ist, kann ein Pufferüberlauf verhindert werden.
  2. Ein Java-Stil arr.length ist sowohl klar als auch vermeidet, dass der Programmierer viele ints auf dem Stapel verwalten muss, wenn er mit mehreren Arrays arbeitet
  3. Funktionsparameter werden zwingender.

Aber der vielleicht motivierendste Grund ist meiner Meinung nach, dass normalerweise kein Platz gespart wird, ohne die Länge beizubehalten. Ich würde sagen, dass die meisten Verwendungen von Arrays eine dynamische Zuordnung beinhalten. Es kann zwar Fälle geben, in denen Benutzer ein auf dem Stapel zugewiesenes Array verwenden, dies ist jedoch nur ein Funktionsaufruf * - der Stapel kann 4 oder 8 Byte mehr verarbeiten.

Da der Heap-Manager ohnehin die freie Blockgröße verfolgen muss, die vom dynamisch zugewiesenen Array verbraucht wird, sollten Sie diese Informationen nutzbar machen (und die zusätzliche Regel hinzufügen, die beim Kompilieren überprüft wurde, dass die Länge nur dann explizit geändert werden kann, wenn dies der Fall ist sich gerne in den Fuß schießen).

Das einzige, was ich mir auf der anderen Seite vorstellen kann, ist, dass keine Längenverfolgung Compiler einfacher gemacht haben könnte, aber nicht das viel einfacher.

* Technisch gesehen könnte man mit einem Array mit automatischer Speicherung eine Art rekursive Funktion schreiben, und in diesem (sehr aufwändigen) Fall kann das Speichern der Länge tatsächlich zu einer effektiv höheren Speicherplatznutzung führen.

79
VF1

C-Arrays verfolgen ihre Länge, da die Array-Länge eine statische Eigenschaft ist:

int xs[42];  /* a 42-element array */

Normalerweise können Sie diese Länge nicht abfragen, müssen dies aber nicht, da sie sowieso statisch ist. Deklarieren Sie einfach ein Makro XS_LENGTH für die Länge, und Sie sind fertig.

Das wichtigere Problem ist, dass C-Arrays implizit in Zeiger zerfallen, z. wenn an eine Funktion übergeben. Dies ist zwar sinnvoll und ermöglicht einige nette Tricks auf niedriger Ebene, verliert jedoch die Informationen über die Länge des Arrays. Eine bessere Frage wäre also, warum C mit dieser impliziten Verschlechterung auf Zeiger entworfen wurde.

Eine andere Sache ist, dass Zeiger keinen Speicher außer der Speicheradresse selbst benötigen. Mit C können wir Ganzzahlen in Zeiger und Zeiger auf andere Zeiger umwandeln und Zeiger so behandeln, als wären sie Arrays. Dabei ist C nicht verrückt genug, um eine gewisse Array-Länge herzustellen, scheint aber auf das Spiderman-Motto zu vertrauen: Mit großer Kraft wird der Programmierer hoffentlich die große Verantwortung erfüllen, Längen und Überläufe zu verfolgen.

106
amon

Vieles hatte mit den damals verfügbaren Computern zu tun. Das kompilierte Programm musste nicht nur auf einem Computer mit begrenzten Ressourcen ausgeführt werden, sondern, was vielleicht noch wichtiger ist, der Compiler selbst musste auf diesen Computern ausgeführt werden. Zu der Zeit, als Thompson C entwickelte, verwendete er einen PDP-7 mit 8 KB RAM. Komplexe Sprachfunktionen, die kein unmittelbares Analogon zum tatsächlichen Maschinencode hatten, waren einfach nicht in der Sprache enthalten.

Ein sorgfältiges Durchlesen der Geschichte von C liefert mehr Verständnis für das Obige, aber es war nicht ganz das Ergebnis der Maschinenbeschränkungen, die sie hatten:

Darüber hinaus zeigt die Sprache (C) eine beträchtliche Fähigkeit, wichtige Konzepte zu beschreiben, beispielsweise Vektoren, deren Länge zur Laufzeit variiert, mit nur wenigen Grundregeln und Konventionen. ... Es ist interessant, den Ansatz von C mit dem von zwei nahezu zeitgleichen Sprachen zu vergleichen, ALGOL 68 und Pascal [Jensen 74]. Arrays in ALGOL 68 haben entweder feste Grenzen oder sind "flexibel": Sowohl in der Sprachdefinition als auch in Compilern ist ein beträchtlicher Mechanismus erforderlich, um flexible Arrays aufzunehmen (und nicht alle Compiler implementieren sie vollständig). Original Pascal hatte nur eine feste Größe Arrays und Strings, und dies erwies sich als einschränkend [Kernighan 81].

C-Arrays sind von Natur aus leistungsfähiger. Das Hinzufügen von Grenzen schränkt ein, wofür der Programmierer sie verwenden kann. Solche Einschränkungen können für Programmierer nützlich sein, sind aber notwendigerweise auch einschränkend.

38
Adam Davis

Damals, als C erstellt wurde, und zusätzliche 4 Bytes Speicherplatz für jeden String, egal wie kurz wäre eine ziemliche Verschwendung gewesen!

Es gibt noch ein anderes Problem: Denken Sie daran, dass C nicht objektorientiert ist. Wenn Sie also alle Zeichenfolgen mit einem Längenpräfix versehen, muss es als intrinsischer Compilertyp definiert werden, nicht als char*. Wenn es sich um einen speziellen Typ handelt, können Sie eine Zeichenfolge nicht mit einer konstanten Zeichenfolge vergleichen, d. H.:

String x = "hello";
if (strcmp(x, "hello") == 0) 
  exit;

es müssten spezielle Compilerdetails vorhanden sein, um diese statische Zeichenfolge entweder in eine Zeichenfolge zu konvertieren, oder es müssen unterschiedliche Zeichenfolgenfunktionen vorhanden sein, um das Längenpräfix zu berücksichtigen.

Ich denke aber letztendlich haben sie einfach nicht das Längenpräfix gewählt, anders als Pascal.

22
gbjbaanb

In C ist jede zusammenhängende Teilmenge eines Arrays ebenfalls ein Array und kann als solches bearbeitet werden. Dies gilt sowohl für Lese- als auch für Schreibvorgänge. Diese Eigenschaft würde nicht gelten, wenn die Größe explizit gespeichert würde.

11
MSalters

Das größte Problem bei der Kennzeichnung von Arrays mit ihrer Länge ist nicht so sehr der zum Speichern dieser Länge erforderliche Speicherplatz oder die Frage, wie sie gespeichert werden sollen (die Verwendung eines zusätzlichen Bytes für kurze Arrays wäre im Allgemeinen nicht zu beanstanden, und die Verwendung von vier zusätzliche Bytes für lange Arrays, aber die Verwendung von vier Bytes auch für kurze Arrays kann sein). Ein viel größeres Problem ist der gegebene Code wie:

void ClearTwoElements(int *ptr)
{
  ptr[-2] = 0;
  ptr[2] = 0;
}
void blah(void)
{
  static int foo[10] = {1,2,3,4,5,6,7,8,9,10};
  ClearTwoElements(foo+2);
  ClearTwoElements(foo+7);
  ClearTwoElements(foo+1);
  ClearTwoElements(foo+8);
}

die einzige Möglichkeit, dass der Code den ersten Aufruf von ClearTwoElements annehmen, den zweiten jedoch ablehnen kann, besteht darin, dass die Methode ClearTwoElements Informationen erhält, die ausreichen, um zu wissen, dass sie jeweils eine Referenz erhalten zu einem Teil des Arrays foo zusätzlich zu dem Wissen, welcher Teil. Dies würde normalerweise die Kosten für die Übergabe von Zeigerparametern verdoppeln. Wenn vor jedem Array kurz nach dem Ende ein Zeiger auf eine Adresse steht (das effizienteste Format für die Validierung), wird der optimierte Code für ClearTwoElements wahrscheinlich wie folgt aussehen:

void ClearTwoElements(int *ptr)
{
  int* array_end = ARRAY_END(ptr);
  if ((array_end - ARRAY_BASE(ptr)) < 10 ||
      (ARRAY_BASE(ptr)+4) <= ADDRESS(ptr) ||          
      (array_end - 4) < ADDRESS(ptr)))
    trap();
  *(ADDRESS(ptr) - 4) = 0;
  *(ADDRESS(ptr) + 4) = 0;
}

Beachten Sie, dass ein Methodenaufrufer im Allgemeinen einen Zeiger auf den Anfang des Arrays oder das letzte Element an eine Methode zu Recht übergeben kann. Nur wenn die Methode versucht, auf Elemente zuzugreifen, die außerhalb des übergebenen Arrays liegen, verursachen solche Zeiger Probleme. Folglich müsste eine aufgerufene Methode zuerst sicherstellen, dass das Array groß genug ist, damit die Zeigerarithmetik zum Überprüfen ihrer Argumente nicht selbst die Grenzen überschreitet, und dann einige Zeigerberechnungen durchführen, um die Argumente zu überprüfen. Die für eine solche Validierung aufgewendete Zeit würde wahrscheinlich die Kosten für echte Arbeit übersteigen. Darüber hinaus könnte die Methode wahrscheinlich effizienter sein, wenn sie geschrieben und aufgerufen würde:

void ClearTwoElements(int arr[], int index)
{
  arr[index-2] = 0;
  arr[index+2] = 0;
}
void blah(void)
{
  static int foo[10] = {1,2,3,4,5,6,7,8,9,10};
  ClearTwoElements(foo,2);
  ClearTwoElements(foo,7);
  ClearTwoElements(foo,1);
  ClearTwoElements(foo,8);
}

Das Konzept eines Typs, der etwas kombiniert, um ein Objekt zu identifizieren, mit etwas, um ein Stück davon zu identifizieren, ist gut. Ein Zeiger im C-Stil ist jedoch schneller, wenn keine Validierung durchgeführt werden muss.

8
supercat

Kurze Antwort :

Da C eine Low-Level-Programmiersprache ist, wird erwartet, dass Sie sich selbst um diese Probleme kümmern, dies erhöht jedoch die Flexibilität in genau wie Sie es implementieren.

C hat ein Konzept zur Kompilierungszeit eines Arrays, das mit einer Länge initialisiert wird, aber zur Laufzeit wird das Ganze einfach als einzelner Zeiger auf den Anfang der Daten gespeichert. Wenn Sie die Array-Länge zusammen mit dem Array an eine Funktion übergeben möchten, tun Sie dies selbst:

retval = my_func(my_array, my_array_length);

Oder Sie können eine Struktur mit einem Zeiger und einer Länge oder eine andere Lösung verwenden.

Eine höhere Sprache würde dies als Teil ihres Array-Typs für Sie tun. In C haben Sie die Verantwortung, dies selbst zu tun, aber auch die Flexibilität, zu entscheiden, wie es gemacht werden soll. Und Wenn der gesamte Code, den Sie schreiben, bereits die Länge des Arrays kennt, müssen Sie die Länge überhaupt nicht als Variable weitergeben .

Der offensichtliche Nachteil besteht darin, dass Sie ohne inhärente Begrenzung der als Zeiger übergebenen Arrays gefährlichen Code erstellen können. Dies liegt jedoch in der Natur der Low-Level-/Systemsprachen und dem Kompromiss, den sie bieten.

7
thomasrutter

Einer der grundlegenden Unterschiede zwischen C und den meisten anderen Sprachen der 3. Generation und allen neueren Sprachen, die mir bekannt sind, besteht darin, dass C nicht dazu gedacht war, dem Programmierer das Leben leichter oder sicherer zu machen. Es wurde mit der Erwartung entworfen, dass der Programmierer wusste, was er tat und genau und nur das tun wollte. Es macht nichts "hinter den Kulissen", so dass Sie keine Überraschungen bekommen. Sogar die Optimierung auf Compilerebene ist optional (es sei denn, Sie verwenden einen Microsoft-Compiler).

Wenn ein Programmierer Grenzen schreiben möchte, die seinen Code überprüfen, macht C dies einfach genug, aber der Programmierer muss sich dafür entscheiden, den entsprechenden Preis in Bezug auf Speicherplatz, Komplexität und Leistung zu zahlen. Obwohl ich es seit vielen Jahren nicht mehr im Zorn benutzt habe, benutze ich es immer noch, wenn ich Programmieren unterrichte, um das Konzept der beschränkungsbasierten Entscheidungsfindung zu vermitteln. Grundsätzlich bedeutet dies, dass Sie wählen können, was Sie wollen, aber jede Entscheidung, die Sie treffen, hat einen Preis, den Sie beachten müssen. Dies wird noch wichtiger, wenn Sie anderen mitteilen, was ihre Programme tun sollen.

7
Paul Smith

Das Problem des zusätzlichen Speichers ist ein Problem, aber meiner Meinung nach ein kleines. Schließlich müssen Sie die Länge ohnehin die meiste Zeit ohnehin verfolgen, obwohl amon darauf hingewiesen hat, dass sie häufig statisch verfolgt werden kann.

Ein größeres Problem ist wo, um die Länge zu speichern und wie lange es zu machen ist. Es gibt nicht einen Ort, der in allen Situationen funktioniert. Sie könnten sagen, speichern Sie einfach die Länge im Speicher kurz vor den Daten. Was ist, wenn das Array nicht auf den Speicher zeigt, sondern auf einen UART Puffer)?

Wenn Sie die Länge weglassen, kann der Programmierer seine eigenen Abstraktionen für die entsprechende Situation erstellen, und es stehen zahlreiche fertige Bibliotheken für den allgemeinen Fall zur Verfügung. Die eigentliche Frage ist, warum diese Abstraktionen in sicherheitsrelevanten Anwendungen nicht verwendet sind.

5
Karl Bielefeldt

Von Die Entwicklung der C-Sprache :

Es schien, dass Strukturen auf intuitive Weise auf den Speicher in der Maschine abgebildet werden sollten, aber in einer Struktur, die ein Array enthielt, gab es keinen guten Platz, um den Zeiger, der die Basis des Arrays enthielt, zu verstauen, und es gab auch keine bequeme Möglichkeit, dies anzuordnen initialisiert. Beispielsweise könnten die Verzeichniseinträge früherer Unix-Systeme in C als beschrieben werden
struct {
    int inumber;
    char    name[14];
};
Ich wollte, dass die Struktur nicht nur ein abstraktes Objekt charakterisiert, sondern auch eine Sammlung von Bits beschreibt, die aus einem Verzeichnis gelesen werden können. Wo könnte der Compiler den Zeiger auf name verstecken, den die Semantik verlangte? Selbst wenn Strukturen abstrakter gedacht würden und der Platz für Zeiger irgendwie verborgen sein könnte, wie könnte ich mit dem technischen Problem umgehen, diese Zeiger beim Zuweisen eines komplizierten Objekts richtig zu initialisieren, möglicherweise eines, das Strukturen spezifiziert, die Arrays enthalten, die Strukturen beliebiger Tiefe enthalten?

Die Lösung stellte den entscheidenden Sprung in der Evolutionskette zwischen typlosem BCPL und typisiertem C dar. Sie beseitigte die Materialisierung des im Speicher befindlichen Zeigers und verursachte stattdessen die Erstellung des Zeigers, wenn der Array-Name in einem Ausdruck erwähnt wird. Die Regel, die im heutigen C überlebt, lautet, dass Werte vom Array-Typ, wenn sie in Ausdrücken erscheinen, in Zeiger auf das erste der Objekte konvertiert werden, aus denen das Array besteht.

In dieser Passage wird erläutert, warum Array-Ausdrücke in den meisten Fällen in Zeiger zerfallen. Dieselbe Überlegung gilt jedoch auch dafür, warum die Array-Länge nicht im Array selbst gespeichert ist. Wenn Sie eine Eins-zu-Eins-Zuordnung zwischen der Typdefinition und ihrer Darstellung im Speicher wünschen (wie es Ritchie getan hat), gibt es keinen guten Ort zum Speichern dieser Metadaten.

Denken Sie auch an mehrdimensionale Arrays. Wo würden Sie die Längenmetadaten für jede Dimension so speichern, dass Sie immer noch mit so etwas durch das Array gehen können?

T *p = &a[0][0];

for ( size_t i = 0; i < rows; i++ )
  for ( size_t j = 0; j < cols; j++ )
    do_something_with( *p++ );
1
John Bode