Funktionen, die Zeichenfolgen zurückgeben, guter Stil?

In meinen C-Programmen benötige ich häufig eine Möglichkeit, eine Zeichenfolgendarstellung meiner ADTs zu erstellen. Selbst wenn ich die Zeichenfolge nicht auf irgendeine Weise auf dem Bildschirm drucken muss, ist es ordentlich, eine solche Methode zum Debuggen zu haben. Daher wird diese Art von Funktion häufig angezeigt.

char * mytype_to_string( const mytype_t *t ); 

Mir ist tatsächlich klar, dass ich hier (mindestens) drei Optionen habe, um den Speicher für die zurückzugebende Zeichenfolge zu verwalten.

Alternative 1: Speichern der Rückgabezeichenfolge in einem statischen Zeichenarray in der Funktion Ich brauche nicht viel nachzudenken, außer dass die Zeichenfolge bei jedem Aufruf überschrieben wird. Dies kann in einigen Fällen ein Problem sein.

Alternative 2: Ordnen Sie die Zeichenfolge auf dem Heap mit malloc innerhalb der Funktion zu. Wirklich ordentlich, da ich dann nicht an die Größe eines Puffers oder das Überschreiben denken muss. Allerdings muss ich daran denken, die Zeichenfolge freizugeben (), wenn ich fertig bin, und dann muss ich auch eine temporäre Variable so zuweisen, dass Ich kann freigeben. Und dann ist die Heap-Zuweisung wirklich viel langsamer als die Stapelzuweisung. Seien Sie daher ein Engpass, wenn dies in einer Schleife wiederholt wird.

Alternative 3: Übergeben Sie den Zeiger an einen Puffer und lassen Sie den Aufrufer zuweisen Dieser Puffer. Wie:

char * mytype_to_string( const mytype_t *mt, char *buf, size_t buflen ); 

Dies bringt dem Aufrufer mehr Aufwand. Ich stelle auch fest, dass diese Alternative mir eine andere Option in der Reihenfolge der Argumente bietet. Welches Argument sollte ich zuerst und zuletzt haben? (Eigentlich sechs Möglichkeiten)

Also, welches sollte ich bevorzugen? Warum? Gibt es einen ungeschriebenen Standard unter C-Entwicklern?

Kommentare

  • Nur eine Bemerkung, die meisten Betriebssysteme verwenden Option 3 – Anrufer weist sowieso Puffer zu, teilt dem Pufferzeiger und der Kapazität mit; Angerufene füllen Buff er und gibt auch die tatsächliche Länge der Zeichenfolge zurück, wenn der Puffer nicht ausreicht. Beispiel: sysctlbyname unter OS X und iOS

Antwort

Die Methoden, die ich am häufigsten gesehen habe, sind 2 und 3.

Der vom Benutzer bereitgestellte Puffer ist eigentlich recht einfach zu verwenden:

char[128] buffer; mytype_to_string(mt, buffer, 128); 

Obwohl die meisten Implementierungen die Menge des verwendeten Puffers zurückgeben.

Option 2 ist langsamer und gefährlich, wenn dynamisch verknüpfte Bibliotheken verwendet werden, in denen sie möglicherweise verwendet werden verschiedene Laufzeiten (und verschiedene Haufen). Sie können also nicht freigeben, was in einer anderen Bibliothek gespeichert wurde. Dies erfordert dann eine free_string(char*) -Funktion, um damit umzugehen.

Kommentare

  • Danke! Ich denke, Alternative 3 gefällt mir auch am besten. Ich möchte jedoch in der Lage sein, Dinge wie: printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf)); zu tun, und daher möchte ich ‚ nicht die verwendete Länge zurückgeben, sondern den Zeiger zum String. Der dynamische Bibliothekskommentar ist wirklich wichtig.
  • Sollte ‚ nicht sizeof(buffer) - 1 sein, um die \0 Terminator?
  • @ Michael-O nein, der Nullterm ist in der Puffergröße enthalten, was bedeutet, dass die maximale Zeichenfolge, die eingegeben werden kann, 1 kleiner ist als die übergebene Größe. Dies ist das Muster, das die sichere Zeichenfolge in der Standardbibliothek wie snprintf verwendet.
  • @ratchetfreak Vielen Dank für die Klarstellung. Es wäre schön, die Antwort mit dieser Weisheit zu erweitern.

Antwort

Zusätzliche Entwurfsidee für # 3

Geben Sie nach Möglichkeit auch die maximale Größe an, die für mytype in derselben .h-Datei wie mytype_to_string().

#define MYTYPE_TO_STRING_SIZE 256 

Jetzt kann der Benutzer entsprechend codieren.

char buf[MYTYPE_TO_STRING_SIZE]; puts(mytype_to_string(mt, buf, sizeof buf)); 

Bestellung

Die Größe der Arrays ermöglicht beim ersten Mal VLA-Typen.

char * mytype_to_string( const mytype_t *mt, size_t bufsize, char *buf[bufsize]); 

Nicht so wichtig für einzelne Dimensionen, aber nützlich für 2 oder mehr.

void matrix(size_t row, size_t col, double matrix[row][col]); 

Ich erinnere mich, dass das Lesen mit der Größe zuerst eine bevorzugte Redewendung im nächsten C ist. Diese Referenz muss gefunden werden ….

Antwort

Als Ergänzung zu @ratchetfreak s hervorragender Antwort möchte ich darauf hinweisen, dass Alternative Nr. 3 einem ähnlichen Paradigma / Muster folgt wie Standardfunktionen der C-Bibliothek.

Zum Beispiel strncpy.

 char * strncpy ( char * destination, const char * source, size_t num );  

Dem gleichen Paradigma zu folgen, würde helfen Um die kognitive Belastung für neue Entwickler (oder sogar für Ihr zukünftiges Selbst) zu verringern, wenn diese Ihre Funktion verwenden müssen.

Der einzige Unterschied zu dem, was Sie in Ihrem Beitrag haben, besteht darin, dass die destination Argument in den C-Bibliotheken wird in der Regel zuerst in der Argumentliste aufgeführt.Also:

 char * mytype_to_string( char *buf, const mytype_t *mt, size_t buflen );  

Antwort

Ich würde in den meisten Fällen @ratchet_freak wiedergeben (möglicherweise mit einer kleinen Änderung für sizeof buffer über 128) Aber ich möchte hier mit einer verrückten Antwort hineinspringen. Wie wäre es, komisch zu sein? Warum nicht, abgesehen von den Problemen, seltsame Blicke von unseren Kollegen zu bekommen und besonders überzeugend sein zu müssen? Und ich biete Folgendes an:

// Note allocator parameter. char* mytype_to_string(allocator* alloc, const mytype_t* t) { char* buf = allocate(alloc, however_much_you_need); // fill out buf based on "t" contents return buf; } 

Und Beispielverwendung:

void func(my_type a, my_type b) { allocator alloc = allocator_new(); const char* str1 = mytype_to_string(&alloc, &a); if (!str1) goto oom; const char* str2 = mytype_to_string(&alloc, &b); if (!str2) goto oom // do something with str1 and str2 goto finish; oom: errno = ENOMEM; finish: // Frees all memory allocated through `alloc`. allocator_purge(&alloc); } 

Wenn Sie dies auf diese Weise getan haben, können Sie Ihren Allokator sehr effizient (effizienter) gestalten als malloc sowohl hinsichtlich der Zuordnungs- / Freigabekosten als auch hinsichtlich der verbesserten Referenzlokalität für den Speicherzugriff). Es kann sich um einen Arena-Zuweiser handeln, bei dem in allgemeinen Fällen nur ein Zeiger für Zuweisungsanforderungen inkrementiert wird und Poolspeicher in sequentieller Weise aus großen zusammenhängenden Blöcken (wobei der erste Block nicht einmal einen Heap al benötigt Standort – kann auf dem Stapel zugewiesen werden). Es vereinfacht die Fehlerbehandlung. Auch, und dies mag am umstrittensten sein, aber ich denke, es ist praktisch ziemlich offensichtlich, wie es dem Anrufer klar macht, dass es dem Allokator, den Sie übergeben, Speicher zuweisen wird, was eine explizite Freigabe erfordert (allocator_purge in diesem Fall), ohne dass ein solches Verhalten in jeder einzelnen möglichen Funktion dokumentiert werden muss, wenn Sie diesen Stil konsistent verwenden. Die Akzeptanz des Allokatorparameters macht es hoffentlich ziemlich offensichtlich.

Ich weiß es nicht. Ich bekomme hier Gegenargumente wie die Implementierung des einfachstmöglichen Arena-Allokators (verwenden Sie einfach die maximale Ausrichtung für alle Anforderungen) und Der Umgang damit ist zu viel Arbeit. Mein stumpfer Gedanke ist: Was sind wir, Python-Programmierer? Wenn ja, können Sie auch Python verwenden. Bitte verwenden Sie Python, wenn diese Details keine Rolle spielen. Ich meine es ernst. Ich hatte viele C-Programmierkollegen, die sehr wahrscheinlich nicht nur korrekteren Code schreiben würden, sondern möglicherweise sogar noch effizienter mit Python, da sie Dinge wie die Referenzlokalität ignorieren, während sie über Fehler stolpern, die sie links und rechts verursachen. Ich sehe nicht, was an einem einfachen Arena-Allokator so beängstigend ist, wenn wir C-Programmierer sind, die sich mit Dingen wie Datenlokalität und optimaler Befehlsauswahl befassen, und dies ist wohl viel weniger zu bedenken als zumindest die Arten von Schnittstellen, die erforderlich sind Anrufer müssen jede einzelne Sache, die die Schnittstelle zurückgeben kann, explizit freigeben. Sie bietet eine Massenfreigabe gegenüber einer einzelnen Freigabe einer Art, die fehleranfälliger ist. Ein richtiger C-Programmierer fordert schleifenförmige Aufrufe von malloc heraus, wie ich es sehe, insbesondere wenn es in seinem Profiler als Hotspot angezeigt wird. Aus meiner Sicht muss es mehr “ oomph “ geben, um zu begründen, dass wir 2020 immer noch ein C-Programmierer sind, und wir können “ Ich scheue mich nicht mehr vor Dingen wie Speicherzuordnungen.

Und dies hat nicht die Randfälle, einen Puffer fester Größe zuzuweisen, in dem wir beispielsweise 256 Bytes zuweisen und die resultierende Zeichenfolge größer ist. Selbst wenn unsere Funktionen Pufferüberläufe vermeiden (wie bei sprintf_s), gibt es mehr Code, um solche Fehler ordnungsgemäß zu beheben, die erforderlich sind und die wir mit dem Allokator-Fall weglassen können, da dies nicht der Fall ist „Wir haben diese Randfälle nicht. Wir müssen uns nicht mit solchen Fällen im Allokatorfall befassen, es sei denn, wir erschöpfen wirklich den physischen Adressraum unserer Hardware (den der obige Code behandelt, aber er muss sich nicht mit “ nicht genügend vorbelegter Puffer “ getrennt von “ nicht genügend Speicher „).

Antwort

Neben der Tatsache, dass Sie vorhaben, a Schlechter Code-Geruch, Alternative 3 klingt für mich am besten. Ich denke auch wie @ gnasher729, dass Sie die falsche Sprache verwenden.

Kommentare

Antwort

Um ehrlich zu sein, möchten Sie möglicherweise zu einer anderen Sprache wechseln, in der das Zurückgeben einer Zeichenfolge keine komplexe, arbeitsintensive und fehleranfällige Operation ist.

Sie könnten C ++ oder Objective-C in Betracht ziehen, bei denen Sie 99% Ihres Codes unverändert lassen könnten.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.