È brutto scrivere C orientato agli oggetti? [chiuso]

14

Mi sembra sempre di scrivere codice in C che è per lo più orientato agli oggetti, quindi dire che ho un file sorgente o qualcosa vorrei creare una struct quindi passare il puntatore a questa struct a funzioni (metodi) di proprietà di questa struttura:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

Questa cattiva pratica è questa? Come imparo a scrivere C il "modo corretto".

    
posta mosmo 28.01.2016 - 15:08
fonte

5 risposte

24

No, non è una cattiva pratica, è persino incoraggiato a farlo, sebbene si possano persino usare convenzioni come struct foo *foo_new(); e void foo_free(struct foo *foo);

Ovviamente, come dice un commento, fallo solo se appropriato. Non ha senso usare un costruttore per un int .

Il prefisso foo_ è una convenzione seguita da molte librerie, perché protegge contro il conflitto con la denominazione di altre librerie. Altre funzioni hanno spesso la convenzione per usare foo_<function>(struct foo *foo, <parameters>); . Ciò consente al tuo struct foo di essere un tipo opaco.

Dai un'occhiata alla documentazione di libcurl per la convenzione, in particolare con "subnamespaces", in modo che la chiamata di una funzione curl_multi_* sembra sbagliato a prima vista quando il primo parametro è stato restituito da curl_easy_init() .

Esistono ancora approcci più generici, vedi Programmazione orientata agli oggetti con ANSI-C

    
risposta data 28.01.2016 - 15:46
fonte
2

Non è male, è eccellente. La programmazione orientata agli oggetti è una buona cosa (a meno che tu non sia portato via, puoi avere troppe cose buone). C non è la lingua più adatta per OOP, ma ciò non dovrebbe impedirti di ottenere il meglio da esso.

    
risposta data 28.01.2016 - 17:47
fonte
0

Non è male. Appoggia l'utilizzo di RAII che impedisce molti bug (perdite di memoria, utilizzo di variabili non inizializzate, uso dopo la pubblicazione ecc. Che può causare problemi di sicurezza).

Quindi, se vuoi compilare il tuo codice solo con GCC o Clang (e non con il compilatore MS), puoi usare l'attributo cleanup , che distruggerà correttamente i tuoi oggetti. Se dichiari il tuo oggetto in questo modo:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

Quindi my_str_destructor(ptr) verrà eseguito quando ptr esce dall'ambito. Tieni presente che non può essere utilizzato con argomenti della funzione .

Inoltre, ricorda di utilizzare my_str_ nei nomi dei metodi, perché C non ha spazi dei nomi ed è facile scontrarsi con qualche altro nome di funzione.

    
risposta data 28.01.2016 - 16:26
fonte
-2

Ci possono essere molti vantaggi a tale codice, ma sfortunatamente lo standard C non è stato scritto per facilitarla. I compilatori hanno storicamente offerto garanzie comportamentali efficaci al di là di quanto richiesto dallo standard che rendesse possibile scrivere tale codice in modo molto più pulito di quanto sia possibile in Standard C, ma i compilatori hanno recentemente iniziato a revocare tali garanzie in nome dell'ottimizzazione.

In particolare, molti compilatori C hanno storicamente garantito (in base alla progettazione se non nella documentazione) che se due tipi di struttura contengono la stessa sequenza iniziale, un puntatore a un tipo può essere utilizzato per accedere ai membri di quella sequenza comune, anche se i tipi non sono correlati, e inoltre che allo scopo di stabilire una sequenza iniziale comune tutti i puntatori alle strutture sono equivalenti. Il codice che fa uso di tale comportamento può essere molto più pulito e più sicuro rispetto al codice che non lo è, ma sfortunatamente anche se lo standard richiede che le strutture che condividono una sequenza iniziale comune debbano essere disposte allo stesso modo, vieta al codice di utilizzare effettivamente un puntatore di un tipo per accedere alla sequenza iniziale di un altro.

Di conseguenza, se vuoi scrivere un codice orientato agli oggetti in C, devi decidere (e dovresti prendere questa decisione presto) o saltare attraverso molti cerchi per rispettare le regole del tipo di puntatore di C ed essere pronto a fare in modo che i compilatori moderni generino codice non sensato se uno scivola in alto, anche se i compilatori più vecchi avrebbero generato codice che funziona come previsto, oppure documentano un requisito che il codice sarà utilizzabile solo con compilatori configurati per supportare il comportamento puntatore vecchio stile (ad esempio usando un "-fno-strict-aliasing") Alcune persone considerano "-fno-strict-aliasing" come malvagio, ma suggerirei che è più utile pensare a "-fno-strict-aliasing" C come un linguaggio che offre maggiore potenza semantica per alcuni scopi rispetto a C "standard", ma a spese di ottimizzazioni che potrebbero essere importanti per altri scopi.

Con l'esempio, sui compilatori tradizionali, i compilatori storici interpreteranno il seguente codice:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

come eseguire i seguenti passi nell'ordine: incrementa il primo membro di *p , completa il bit più basso del primo membro di *t , quindi decrementa il primo membro di *p e completa il bit più basso del primo membro di *t . I compilatori moderni riorganizzeranno la sequenza di operazioni in modo tale che il codice sarà più efficiente se p e t identificano oggetti diversi, ma cambieranno il comportamento in caso contrario.

Questo esempio è ovviamente deliberatamente inventato, e in pratica il codice che usa un puntatore di un tipo per accedere ai membri che fanno parte della sequenza iniziale comune di un altro tipo di solito funziona, ma sfortunatamente dal non c'è modo di sapere quando un tale codice può fallire, non è possibile utilizzarlo in sicurezza, se non disabilitando l'analisi di aliasing basata sui tipi.

Un esempio un po 'meno forzato si verificherebbe se si volesse scrivere una funzione per fare qualcosa come scambiare due puntatori a tipi arbitrari. Nella stragrande maggioranza dei compilatori "C degli anni '90", ciò potrebbe essere realizzato tramite:

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

In Standard C, tuttavia, si dovrebbe usare:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

Se *p2 viene conservato nella memoria allocata e il puntatore temporaneo non viene mantenuto nella memoria allocata, il tipo effettivo di *p2 diventerà il tipo del puntatore temporaneo e il codice che tenta di utilizzare *p2 poiché qualsiasi tipo che non corrisponde al tipo di puntatore temporaneo invocherà il comportamento non definito. È molto improbabile che un compilatore possa notare una cosa del genere, ma dal momento che la moderna filosofia del compilatore richiede che i programmatori evitino il comportamento indefinito a tutti i costi, non posso pensare a nessun altro mezzo sicuro per scrivere il codice sopra senza usare memoria allocata .

    
risposta data 28.01.2016 - 21:06
fonte
-3

Il passo successivo è quello di nascondere la dichiarazione della struct. Lo inserisci nel file .h:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

E poi nel file .c:

struct foo_s {
    ...
};
    
risposta data 28.01.2016 - 18:13
fonte

Leggi altre domande sui tag