Ist die Verwendung flexibler Array-Mitglieder in C eine schlechte Praxis?

73

Ich habe kürzlich gelesen, dass die Verwendung flexibler Array-Mitglieder in C eine schlechte Software-Engineering-Praxis ist. Diese Aussage wurde jedoch durch kein Argument gestützt. Ist das eine akzeptierte Tatsache?

( Flexible Array- Elemente sind eine in C99 eingeführte C-Funktion, mit der das letzte Element als Array mit nicht angegebener Größe deklariert werden kann. Zum Beispiel :)

struct header {
    size_t len;
    unsigned char data[];
};
maxschlepzig
quelle

Antworten:

27

Es ist eine akzeptierte "Tatsache", dass die Verwendung von goto eine schlechte Softwareentwicklungspraxis ist. Das macht es nicht wahr. Es gibt Zeiten, in denen goto nützlich ist, insbesondere bei der Bereinigung und beim Portieren vom Assembler.

Flexible Array-Mitglieder scheinen mir eine Hauptanwendung zu sein, nämlich die Zuordnung älterer Datenformate wie Fenstervorlagenformate unter RiscOS. Sie wären vor ungefähr 15 Jahren äußerst nützlich gewesen, und ich bin sicher, dass es immer noch Leute gibt, die sich mit solchen Dingen befassen und sie nützlich finden würden.

Wenn die Verwendung flexibler Array-Mitglieder eine schlechte Praxis ist, empfehle ich, dass wir alle den Autoren der C99-Spezifikation dies mitteilen. Ich vermute, sie könnten eine andere Antwort haben.

Airsource Ltd.
quelle
18
goto ist auch nützlich, wenn wir eine rekursive Implementierung eines Algorithmus unter Verwendung einer nicht rekursiven Implementierung implementieren möchten, wenn die Rekursion einen zusätzlichen Overhead für den Compiler verursachen kann.
Pranavk
7
@pranavk whileDann solltest du wahrscheinlich verwenden .
yyny
6
Netzwerkprogrammierung ist eine andere, Sie haben den Header als Struktur und das Paket (oder wie es in der Schicht heißt, in der Sie sich befinden ..) als flexibles Array. Wenn Sie die nächste Ebene aufrufen, entfernen Sie den Header und übergeben das Paket. Tun Sie dies für jede Schicht im Netzwerkstapel. (Sie Fall die Daten von unten wiederbelebt von der unteren Schicht zu Struktur für Schicht, die Sie inn sind)
fhtuft
1
@pranavk gotoist nicht für Schleifen.
10онстантин Ван
18

BITTE LESEN SIE DIE KOMMENTARE UNTER DIESER ANTWORT SORGFÄLTIG DURCH

Mit fortschreitender C-Standardisierung gibt es keinen Grund mehr, [1] zu verwenden.

Der Grund, warum ich es nicht tun würde, ist, dass es sich nicht lohnt, Ihren Code an C99 zu binden, nur um diese Funktion zu verwenden.

Der Punkt ist, dass Sie immer die folgende Redewendung verwenden können:

struct header {
  size_t len;
  unsigned char data[1];
};

Das ist voll tragbar. Dann können Sie die 1 berücksichtigen, wenn Sie den Speicher für n Elemente im Array zuweisen data:

ptr = malloc(sizeof(struct header) + (n-1));

Wenn Sie bereits C99 als Voraussetzung für die Erstellung Ihres Codes aus einem anderen Grund haben oder auf einen bestimmten Compiler abzielen, sehe ich keinen Schaden.

Remo.D
quelle
4
Vielen Dank. Ich habe die n-1 verlassen, da sie möglicherweise nicht als Zeichenfolge verwendet wird.
Remo.D
81
Die folgende Redewendung ist nicht vollständig portierbar, weshalb dem C99-Standard flexible Array-Mitglieder hinzugefügt wurden.
Jonathan Leffler
9
@ Remo.D: kleiner Punkt: Das n-1berücksichtigt die zusätzliche Zuordnung aufgrund der Ausrichtung nicht genau: Auf den meisten 32-Bit-Computern beträgt sizeof (Strukturheader) 8 (um ein Vielfaches von 4 zu bleiben, da es ein 32- Bitfeld, das eine solche Ausrichtung bevorzugt / erfordert). Die "bessere" Version ist:malloc(offsetof(struct header, data) + n)
Tom Leek
32
In C99 ist die Verwendung unsigned char data[1]nicht portierbar, da ((header*)ptr)->data + 2- selbst wenn genügend Speicherplatz zugewiesen wurde - ein Zeiger erstellt wird, der außerhalb des Array-Objekts der Länge 1 zeigt (und nicht des Sentinel-Objekts nach dem Ende). Gemäß C99 6.5.6p8: "Wenn sowohl der Zeigeroperand als auch das Ergebnis auf Elemente desselben Arrayobjekts oder eines nach dem letzten Element des Arrayobjekts zeigen, darf die Auswertung keinen Überlauf erzeugen. Andernfalls ist das Verhalten undefiniert " (Betonung hinzugefügt). Flexible Arrays (6.7.2.2p16) verhalten sich wie ein Array, das den zugewiesenen Speicherplatz ausfüllt, um UB hier nicht zu treffen.
Jeff Walden
23
* WARNUNG: Verwenden [1]gezeigt wurde GCC verursachen falschen Code zu generieren: lkml.org/lkml/2015/2/18/407
Jonathon Rein
10

Du meintest...

struct header
{
 size_t len;
 unsigned char data[];
}; 

In C ist das eine gängige Redewendung. Ich denke, viele Compiler akzeptieren auch:

  unsigned char data[0];

Ja, es ist gefährlich, aber andererseits ist es wirklich nicht gefährlicher als normale C-Arrays - dh SEHR gefährlich ;-). Verwenden Sie es mit Vorsicht und nur unter Umständen, in denen Sie wirklich eine Reihe unbekannter Größe benötigen. Stellen Sie sicher, dass Sie den Speicher korrekt verwenden und freigeben, indem Sie Folgendes verwenden: -

  foo = malloc(sizeof(header) + N * sizeof(data[0]));
  foo->len = N;

Eine Alternative besteht darin, Daten nur als Zeiger auf die Elemente zu verwenden. Sie können dann die Daten nach Bedarf auf die richtige Größe umverteilen.

  struct header
    {
     size_t len;
     unsigned char *data;
    }; 

Wenn Sie nach C ++ fragen, ist dies natürlich eine schlechte Praxis. Dann würden Sie normalerweise stattdessen STL-Vektoren verwenden.

Roddy
quelle
2
vorausgesetzt, Sie codieren auf einem System, auf dem STL unterstützt wird!
Airsource Ltd
6
C ++ aber keine STL ... Das ist kein angenehmer Gedanke!
Roddy
7
Nennen Sie einen Compiler, der Arrays mit der Länge Null akzeptiert. (Wenn die Antwort GCC war, nennen Sie jetzt eine andere.) Sie wird vom C-Standard nicht genehmigt.
Jonathan Leffler
3
Ich habe in einer C ++ - aber keiner STL-Umgebung gearbeitet - wir hatten unsere eigenen Container, die die häufig verwendeten Funktionen ohne die vollständige Allgemeinheit des STL-Iteratorsystems bereitstellten. Sie waren leichter zu verstehen und hatten eine gute Leistung. Dies war jedoch im Jahr 2001.
pjc50
@JonathanLeffler Akzeptiert von GCC und Clang, die zwei der drei heute verwendeten Hauptcompiler abdecken. (MSVC ist der andere große, und das ist nur auf einer - zugegebenermaßen sehr verbreiteten - Plattform wirklich relevant.)
Donal Fellows
10

Nein, die Verwendung flexibler Array-Elemente in C ist keine schlechte Praxis.

Diese Sprachfunktion wurde erstmals in ISO C99, 6.7.2.1 (16) standardisiert. Für die aktuelle Norm ISO C11 ist sie in Abschnitt 6.7.2.1 (18) angegeben.

Sie können sie folgendermaßen verwenden:

struct Header {
    size_t d;
    long v[];
};
typedef struct Header Header;
size_t n = 123; // can dynamically change during program execution
// ...
Header *h = malloc(sizeof(Header) + sizeof(long[n]));
h->n = n;

Alternativ können Sie es folgendermaßen zuordnen:

Header *h = malloc(sizeof *h + n * sizeof h->v[0]);

Beachten Sie, dass sizeof(Header)eventuelle Auffüllbytes enthalten sind. Daher ist die folgende Zuordnung falsch und kann zu einem Pufferüberlauf führen:

Header *h = malloc(sizeof(size_t) + sizeof(long[n])); // invalid!

Eine Struktur mit einem flexiblen Array-Element reduziert die Anzahl der Zuweisungen für dieses um 1/2, dh anstelle von 2 Zuweisungen für ein Strukturobjekt benötigen Sie nur 1. Dies bedeutet weniger Aufwand und weniger Speicherplatz für den Buchhaltungsaufwand für den Speicherzuweiser. Außerdem speichern Sie den Speicher für einen zusätzlichen Zeiger. Wenn Sie also eine große Anzahl solcher Strukturinstanzen zuweisen müssen, verbessern Sie messbar die Laufzeit und die Speichernutzung Ihres Programms (um einen konstanten Faktor).

Im Gegensatz dazu ist die Verwendung nicht standardisierter Konstrukte für flexible Array-Mitglieder, die ein undefiniertes Verhalten ergeben (z. B. wie in long v[0];oder long v[1];), offensichtlich eine schlechte Praxis. Daher sollte dies wie jedes undefinierte Verhalten vermieden werden.

Seit der Veröffentlichung von ISO C99 im Jahr 1999, vor fast 20 Jahren, ist das Streben nach ISO C89-Kompatibilität ein schwaches Argument.

maxschlepzig
quelle
6

Ich habe so etwas gesehen: von der C-Schnittstelle und der Implementierung.

  struct header {
    size_t len;
    unsigned char *data;
};

   struct header *p;
   p = malloc(sizeof(*p) + len + 1 );
   p->data = (unsigned char*) (p + 1 );  // memory after p is mine! 

Hinweis: Daten müssen nicht das letzte Mitglied sein.

Nyan
quelle
17
Dies hat in der Tat den Vorteil, datadass es nicht das letzte Mitglied sein muss, sondern dass jedes Mal, wenn es verwendet wird, eine zusätzliche Dereferenzierung auftritt data. Flexible Arrays ersetzen diese Dereferenzierung durch einen konstanten Versatz zum Hauptstrukturzeiger, der auf einigen besonders gängigen Maschinen kostenlos und an anderer Stelle billig ist.
R .. GitHub STOP HELPING ICE
@R .. Obwohl die Zieladresse notwendigerweise das Byte direkt nach dem Zeiger ist, ist garantiert, dass sie sich bereits zu 100% im L1-Cache befindet, was der gesamten Dereferenzierung einen halben Overhead-Zyklus verleiht. Der Punkt ist jedoch, dass flexible Arrays hier eine bessere Idee sind.
pmttavara
Mit unsigned char *, p->data = (unsigned char*) (p + 1 )ist in Ordnung. Doch mit double complex *, p->data = (double complex *) (p + 1 )können Ausrichtungsprobleme verursachen.
chux
Diese Antwort ist technisch irrelevant, da sie etwas anderes bewirkt (sie legt die Daten im Speicher unterschiedlich an). Das beschriebene Muster ist zwar oft nützlich, bedeutet aber nicht, dass es das andere ersetzen kann.
Donal Fellows
4

Als Randnotiz sollte eine solche Struktur aus Gründen der C89-Kompatibilität wie folgt zugewiesen werden:

struct header *my_header
  = malloc(offsetof(struct header, data) + n * sizeof my_header->data);

Oder mit Makros:

#define FLEXIBLE_SIZE SIZE_MAX /* or whatever maximum length for an array */
#define SIZEOF_FLEXIBLE(type, member, length) \
  ( offsetof(type, member) + (length) * sizeof ((type *)0)->member[0] )

struct header {
  size_t len;
  unsigned char data[FLEXIBLE_SIZE];
};

...

size_t n = 123;
struct header *my_header = malloc(SIZEOF_FLEXIBLE(struct header, data, n));

Wenn Sie FLEXIBLE_SIZE auf SIZE_MAX setzen, wird fast sichergestellt, dass dies fehlschlägt:

struct header *my_header = malloc(sizeof *my_header);
Diapir
quelle
4
Übermäßig komplex und es gibt keinen Vorteil gegenüber der Verwendung [1]für C89-Kompatibilität, wenn es überhaupt benötigt wird ...
R .. GitHub STOP HELPING ICE
4

Es gibt einige Nachteile in Bezug darauf, wie Strukturen manchmal verwendet werden, und es kann gefährlich sein, wenn Sie die Auswirkungen nicht durchdenken.

Wenn Sie für Ihr Beispiel eine Funktion starten:

void test(void) {
  struct header;
  char *p = &header.data[0];

  ...
}

Dann sind die Ergebnisse undefiniert (da niemals Speicher für Daten zugewiesen wurde). Dies ist Ihnen normalerweise bekannt, aber es gibt Fälle, in denen C-Programmierer wahrscheinlich daran gewöhnt sind, Wertesemantik für Strukturen zu verwenden, die auf verschiedene andere Arten zusammenbricht.

Zum Beispiel, wenn ich definiere:

struct header2 {
  int len;
  char data[MAXLEN]; /* MAXLEN some appropriately large number */
}

Dann kann ich zwei Instanzen einfach durch Zuweisung kopieren, dh:

struct header2 inst1 = inst2;

Oder wenn sie als Zeiger definiert sind:

struct header2 *inst1 = *inst2;

Dies funktioniert jedoch nicht, da das variable Array datanicht kopiert wird. Was Sie wollen, ist, die Größe der Struktur dynamisch zu ändern und mit memcpyoder einer Entsprechung über das Array zu kopieren .

Ebenso funktioniert das Schreiben einer Funktion, die eine Struktur akzeptiert, nicht, da Argumente in Funktionsaufrufen wiederum nach Wert kopiert werden und das, was Sie erhalten, wahrscheinlich nur das erste Element Ihres Arrays ist data.

Dies macht es nicht zu einer schlechten Idee, aber Sie müssen bedenken, diese Strukturen immer dynamisch zuzuweisen und sie nur als Zeiger weiterzugeben.

wds
quelle