In mijn C-programmas heb ik vaak een manier nodig om een stringvoorstelling van mijn ADTs te maken. Zelfs als ik de string op geen enkele manier naar het scherm hoef af te drukken, is het handig om een dergelijke methode voor foutopsporing te hebben. Dus dit soort functie komt vaak naar voren.
char * mytype_to_string( const mytype_t *t );
Ik realiseer me eigenlijk dat ik hier (ten minste) drie opties heb voor het omgaan met het geheugen voor de string die moet worden geretourneerd.
Alternatief 1: het opslaan van de retourstring in een statische char-array in de functie Ik hoef niet veel na te denken, behalve dat de string bij elke oproep wordt overschreven. Dat kan in sommige gevallen een probleem zijn.
Alternatief 2: Wijs de string op de heap toe met malloc binnen de functie. Echt netjes, want ik hoef dan niet te denken aan de grootte van een buffer of het overschrijven. Ik moet echter onthouden om de string vrij te geven () als ik klaar ben, en dan moet ik ook een tijdelijke variabele toewijzen, zodat Ik kan vrijmaken. En dan is heap-toewijzing echt veel langzamer dan stack-toewijzing, daarom een bottleneck als dit in een lus wordt herhaald.
Alternatief 3: geef de aanwijzer door aan een buffer en laat de beller toewijzen die buffer. Zoals:
char * mytype_to_string( const mytype_t *mt, char *buf, size_t buflen );
Dit brengt meer moeite voor de beller. Ik merk ook dat dit alternatief mij een andere optie geeft in de volgorde van de argumenten. Welk argument zou ik als eerste en laatste moeten hebben? (Eigenlijk zes mogelijkheden)
Dus, welke zou ik verkiezen? Enig waarom? Is er een soort ongeschreven standaard onder C-ontwikkelaars?
Opmerkingen
Antwoord
De methoden die ik “het meest heb gezien, zijn 2 en 3.
De door de gebruiker aangeleverde buffer is eigenlijk vrij eenvoudig te gebruiken:
char[128] buffer; mytype_to_string(mt, buffer, 128);
Hoewel de meeste implementaties de hoeveelheid gebruikte buffer zullen retourneren.
Optie 2 zal langzamer zijn en is gevaarlijk wanneer dynamisch gekoppelde bibliotheken worden gebruikt waar ze verschillende looptijden (en verschillende hopen). Zodat je niet kunt bevrijden wat er misleid is in een andere bibliotheek. Dit vereist dan een free_string(char*)
functie om ermee om te gaan.
Opmerkingen
- Bedankt! Ik denk dat ik Alternatief 3 ook het leukst vind. Ik wil echter dingen kunnen doen als:
printf("MyType: %s\n", mytype_to_string( mt, buf, sizeof(buf));
en daarom heb ik ‘ geen zin om de gebruikte lengte terug te geven, maar liever de aanwijzer aan de snaar. De opmerking van de dynamische bibliotheek is erg belangrijk. - Moet niet ‘ t dit
sizeof(buffer) - 1
zijn om te voldoen aan de\0
terminator? - @ Michael-O nee de null-term is opgenomen in de buffergrootte, wat betekent dat de maximale string die kan worden ingevoerd 1 kleiner is dan de doorgegeven in grootte. Dit is het patroon dat de veilige string functioneert in de standaard bibliotheek zoals
snprintf
gebruiken. - @ratchetfreak Bedankt voor de verduidelijking. Het zou leuk zijn om het antwoord met die wijsheid uit te breiden.
Antwoord
Extra ontwerpidee voor # 3
Geef indien mogelijk ook de maximale grootte op die nodig is voor mytype
in hetzelfde .h-bestand als mytype_to_string()
.
#define MYTYPE_TO_STRING_SIZE 256
Nu kan de gebruiker dienovereenkomstig coderen.
char buf[MYTYPE_TO_STRING_SIZE]; puts(mytype_to_string(mt, buf, sizeof buf));
Order
De grootte van arrays maakt VLA-types mogelijk.
char * mytype_to_string( const mytype_t *mt, size_t bufsize, char *buf[bufsize]);
Niet zo belangrijk met enkele dimensie, maar wel bruikbaar met 2 of meer.
void matrix(size_t row, size_t col, double matrix[row][col]);
Ik herinner me dat lezen met de grootte eerst een voorkeurstaal is in de volgende C. Moet die referentie vinden …
Antwoord
Als aanvulling op het uitstekende antwoord van @ratchetfreak, zou ik erop willen wijzen dat alternatief # 3 een vergelijkbaar paradigma / patroon volgt als standaard C-bibliotheekfuncties.
Bijvoorbeeld strncpy
.
char * strncpy ( char * destination, const char * source, size_t num );
Het volgen van hetzelfde paradigma zou helpen om de cognitieve belasting voor nieuwe ontwikkelaars (of zelfs je toekomstige zelf) te verminderen wanneer ze je functie moeten gebruiken.
Het enige verschil met wat je in je bericht hebt, is dat de destination
argument in de C-bibliotheken wordt meestal als eerste vermeld in de lijst met argumenten.Dus:
char * mytype_to_string( char *buf, const mytype_t *mt, size_t buflen );
Antwoord
Ik “echo @ratchet_freak in de meeste gevallen (misschien met een kleine aanpassing voor sizeof buffer
boven 128
) maar ik wil hier binnen springen met een rare reactie. Wat dacht je ervan om raar te zijn? Waarom niet, afgezien van de problemen om rare blikken van onze collegas te krijgen en extra overtuigend te moeten zijn? En ik bied dit aan:
// 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; }
En voorbeeldgebruik:
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); }
Als u het op deze manier deed, kunt u uw allocator zeer efficiënt (efficiënter dan malloc
zowel in termen van kosten voor toewijzing / ongedaan maken van toewijzing als verbeterde referentielocatie voor geheugentoegang). Het kan een arena-allocator zijn die alleen inhoudt dat een aanwijzer wordt verhoogd in veel voorkomende gevallen voor toewijzingsverzoeken en geheugen op een opeenvolgende manier samenvoegen van grote aaneengesloten blokken (waarbij het eerste blok niet eens een hoop al locatie – het kan op de stapel worden toegewezen). Het vereenvoudigt de afhandeling van fouten. Ook, en dit is misschien het meest discutabel, maar ik denk dat het praktisch vrij duidelijk is in termen van hoe het de beller duidelijk maakt dat het geheugen gaat toewijzen aan de allocator die je doorgeeft, wat expliciet vrijgeven vereist (allocator_purge
in dit geval) zonder dergelijk gedrag in elke mogelijke functie te moeten documenteren als je deze stijl consequent gebruikt. De acceptatie van de allocator-parameter maakt het hopelijk vrij duidelijk.
Ik weet het niet. Ik krijg hier tegenargumenten zoals het implementeren van de eenvoudigst mogelijke arena-allocator die mogelijk is (gebruik gewoon maximale uitlijning voor alle verzoeken) en ermee omgaan is te veel werk. Mijn botte gedachte is als, wat zijn wij, Python-programmeurs? Kunnen net zo goed Python gebruiken als dat zo is. Gebruik Python als deze details er niet toe doen. Ik meen het serieus. Ik heb veel C-programmeercollegas gehad die hoogstwaarschijnlijk niet alleen correctere code zouden schrijven, maar mogelijk zelfs efficiënter met Python, omdat ze dingen negeren als referentielocatie terwijl ze struikelen over bugs die ze links en rechts creëren. Ik zie niet wat er is dat zo eng is aan een of andere eenvoudige arena-allocator hier als we C-programmeurs zijn die zich bezighouden met zaken als datalocatie en optimale instructieselectie, en dit is aantoonbaar veel minder om over na te denken dan in ieder geval de soorten interfaces die vereisen bellers om expliciet elk afzonderlijk ding vrij te geven dat de interface kan retourneren. Het biedt bulk deallocation boven individuele deallocation van een soort dat meer foutgevoelig is. Een goede C-programmeur daagt loopy calls uit naar malloc
zoals ik het zie, vooral wanneer het verschijnt als een hotspot in hun profiler. Vanuit mijn standpunt moet er meer ” oomph ” zijn voor een grondgedachte om nog steeds C-programmeur te zijn in 2020, en we kunnen ” Ga dingen als geheugenallocators niet meer uit de weg.
En dit heeft niet de randgevallen van het toewijzen van een buffer met een vaste grootte waar we bijvoorbeeld 256 bytes toewijzen en de resulterende string groter is. Zelfs als onze functies bufferoverschrijdingen vermijden (zoals bij sprintf_s
), is er meer code om correct te herstellen van dergelijke fouten die nodig zijn, die we kunnen weglaten met het allocator-geval, omdat dit niet het geval is. “hebben die randgevallen niet. We hoeven dergelijke gevallen niet te behandelen in het allocator-geval, tenzij we echt de fysieke adresruimte van onze hardware uitputten (die de bovenstaande code afhandelt, maar het heeft niet te maken met ” geen vooraf toegewezen buffer ” apart van ” geen geheugen meer “).
Antwoord
Naast het feit dat wat u van plan bent te doen een slechte code geur, alternatief 3 klinkt mij het beste in de oren. Ik denk ook dat je net als @ gnasher729 de verkeerde taal gebruikt.
Reacties
- Wat beschouw je de geur van een code precies? Geef een toelichting.
- Zie en.m.wikipedia.org/wiki/Cod e_smell voor voorbeelden. Maar het omzetten van een niet-string naar een string, zodat u deze kunt afdrukken, is een slechte gewoonte. Zie ook en.m.wikipedia.org/wiki/Design_smell voor meer coderingsfout.
Antwoord
Om eerlijk te zijn, wil je misschien overschakelen naar een andere taal waar het retourneren van een string geen complexe, arbeidsintensieve en foutgevoelige bewerking is.
U kunt C ++ of Objective-C overwegen, waar u 99% van uw code ongewijzigd kunt laten.
sysctlbyname
in OS X en iOS