È male scrivere C orientato agli oggetti? [chiuso]


14

Mi sembra sempre di scrivere codice in C che è principalmente orientato agli oggetti, quindi diciamo che avevo un file sorgente o qualcosa che avrei creato una struttura e poi avrei passato il puntatore a questa struttura 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 è una cattiva pratica? Come imparo a scrivere C nel "modo corretto".


10
Gran parte di Linux (il kernel) è scritto in questo modo, infatti emula persino concetti ancora più simili a quelli di OO come l'invio di metodi virtuali. Lo considero abbastanza appropriato.
Kilian Foth,

13
" [T] ha stabilito che il vero programmatore può scrivere programmi FORTRAN in qualsiasi lingua. " - Ed Post, 1983
Ross Patterson,

4
C'è qualche motivo per cui non vuoi passare al C ++? Non devi usare le parti che non ti piacciono.
svick,

5
Questo fa sorgere davvero la domanda: "Che cos'è" orientato agli oggetti "?" Non definirei questo oggetto orientato. Direi che è procedurale. (Non hai eredità, nessun polimorfismo, nessuna incapsulamento / capacità di nascondere lo stato e probabilmente ti mancano altri segni distintivi di OO che non stanno venendo fuori dalla mia testa.) Se la buona o cattiva pratica non dipende da quella semantica , anche se.
jpmc26,

3
@ jpmc26: se sei un prescrittivista linguistico, dovresti ascoltare Alan Kay, ha inventato il termine, può dire cosa significa e dice che OOP è tutto incentrato sulla messaggistica . Se sei un descrittore linguistico, verifichi l'utilizzo del termine nella comunità di sviluppo software. Cook ha fatto esattamente questo, ha analizzato le caratteristiche delle lingue che sostengono o sono considerate OO e ha scoperto che hanno una cosa in comune: la messaggistica .
Jörg W Mittag,

Risposte:


24

No, questa non è una cattiva pratica, è persino incoraggiata a farlo, anche se si potrebbero persino usare convenzioni come struct foo *foo_new();evoid foo_free(struct foo *foo);

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

Il prefisso foo_è una convenzione seguita da molte librerie, perché protegge dallo scontro con la denominazione di altre librerie. Altre funzioni hanno spesso la convenzione da usare foo_<function>(struct foo *foo, <parameters>);. Questo ti permette struct foodi essere un tipo opaco.

Dai un'occhiata alla documentazione di libcurl per la convenzione, in particolare con "subnamespace", in modo che la chiamata di una funzione curl_multi_*sembri errata a prima vista quando viene restituito il primo parametro curl_easy_init().

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


11
Sempre con l'avvertenza "dove appropriato". OOP non è un proiettile d'argento.
Deduplicatore

C non ha spazi dei nomi in cui è possibile dichiarare queste funzioni? Simile a std::string, non potresti averlo foo::create? Non uso C. Forse è solo in C ++?
Chris Cirefice,

@ChrisCirefice Non ci sono spazi dei nomi in C, ecco perché molti autori di biblioteche usano prefissi per le loro funzioni.
Residuum

2

Non è male, è eccellente. La programmazione orientata agli oggetti è una buona cosa (a meno che non ti lasci trasportare, puoi avere troppe cose buone). C non è il linguaggio più adatto per OOP, ma ciò non dovrebbe impedirti di trarne il meglio.


4
Non che io non possa essere d'accordo, ma la tua opinione dovrebbe davvero essere supportata da qualche elaborazione.
Deduplicatore il

1

Non è male. Approva l'uso di RAII che impedisce molti bug (perdite di memoria, utilizzo di variabili non inizializzate, uso dopo libero ecc. Che possono 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' cleanupattributo, che distruggerà correttamente i tuoi oggetti. Se dichiari il tuo oggetto così:

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

Quindi my_str_destructor(ptr)verrà eseguito quando ptr non rientra nell'ambito. Ricorda solo che non può essere utilizzato con argomenti di funzione .

Inoltre, ricorda di utilizzare my_str_nei nomi dei metodi, perché Cnon ha spazi dei nomi ed è facile scontrarsi con qualche altro nome di funzione.


2
Dopo tutto, RAII riguarda l'utilizzo della chiamata implicita dei distruttori per gli oggetti in C ++ per garantire la pulizia, evitando la necessità di aggiungere chiamate esplicite al rilascio delle risorse. Quindi, se non sbaglio molto, RAII e C si escludono a vicenda.
cmaster - ripristina monica il

@cmaster Se usi i #definetuoi nomi di battesimo, __attribute__((cleanup(my_str_destructor)))lo otterrai come implicito in tutto l' #defineambito (verrà aggiunto a tutte le dichiarazioni delle variabili).
Marqin,

Funziona se a) usi gcc, b) se usi il tipo solo nelle variabili locali della funzione, ec) se usi il tipo solo in una versione nuda (nessun puntatore al #definetipo 'd o array di esso). In breve: non è lo standard C e paghi con molta inflessibilità in uso.
cmaster - ripristina monica il

Come menzionato nella mia risposta, questo funziona anche nel clang.
Marqin,

Ah, non me ne sono accorto. Ciò rende il requisito a) significativamente meno severo, in quanto rende __attribute__((cleanup()))praticamente un quasi standard. Tuttavia, b) ec) sono ancora in piedi ...
cmaster - reintegrare monica il

-2

Ci possono essere molti vantaggi in questo codice, ma sfortunatamente lo standard C non è stato scritto per facilitarlo. I compilatori hanno storicamente offerto garanzie comportamentali efficaci oltre a quanto richiesto dalla norma che ha permesso di scrivere tale codice in modo molto più pulito di quanto sia possibile nella norma C, ma ultimamente i compilatori hanno iniziato a revocare tali garanzie in nome dell'ottimizzazione.

In particolare, molti compilatori C hanno storicamente garantito (in base alla progettazione se non alla documentazione) che se due tipi di struttura contengono la stessa sequenza iniziale, è possibile utilizzare un puntatore a entrambi i tipi 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 utilizza tale comportamento può essere molto più pulito e più sicuro rispetto al codice che non lo fa, ma sfortunatamente anche se lo Standard richiede che le strutture che condividono una sequenza iniziale comune debbano essere disposte allo stesso modo, proibisce al codice di utilizzare effettivamente un puntatore di un tipo per accedere alla sequenza iniziale di un altro.

Di conseguenza, se vuoi scrivere codice orientato agli oggetti in C, dovrai decidere (e dovresti prendere presto questa decisione) di saltare attraverso molti cerchi per rispettare le regole del tipo di puntatore di C ed essere pronto ad avere i compilatori moderni generano codice senza senso se uno scivola, anche se compilatori più vecchi avrebbero generato codice che funziona come previsto, oppure documentano un requisito secondo cui il codice sarà utilizzabile solo con compilatori configurati per supportare il comportamento del puntatore vecchio stile (ad es. un "-fno-rigoroso-aliasing") Alcune persone considerano il "-fno-rigoroso aliasing" come un male, ma suggerirei che è più utile pensare al "-fno-rigoroso-aliasing" C come un linguaggio che offre una potenza semantica maggiore per alcuni scopi rispetto alla C "standard",ma a scapito delle ottimizzazioni che potrebbero essere importanti per altri scopi.

A titolo di esempio, sui compilatori tradizionali, i compilatori storici interpretano 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;
}

eseguendo i seguenti passaggi nell'ordine: incrementare il primo membro di *p, integrare il bit più basso del primo membro di *t, quindi decrementare il primo membro di *pe completare il bit più basso del primo membro di *t. I compilatori moderni riorganizzeranno la sequenza delle operazioni in un modo che codifichi che sarà più efficiente se pe tidentificherà oggetti diversi, ma cambierà il comportamento se non lo fanno.

Questo esempio è ovviamente deliberatamente inventato, e in pratica il codice che utilizza un puntatore di un tipo per accedere ai membri che fanno parte della sequenza iniziale comune di un altro tipo di solito funzionerà, ma sfortunatamente poiché non c'è modo di sapere quando tale codice potrebbe fallire non è possibile utilizzarlo in modo sicuro se non disabilitando l'analisi di aliasing basata sul tipo.

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

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

Nella norma 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 *p2viene conservato nella memoria allocata e il puntatore temporaneo non viene conservato nella memoria allocata, il tipo effettivo di *p2diventerà il tipo di puntatore temporaneo e il codice che tenta di utilizzare *p2come qualsiasi tipo che non corrisponde al puntatore temporaneo type invocherà Undefined Behaviour. È sicuramente estremamente improbabile che un compilatore noterebbe una cosa del genere, ma poiché la moderna filosofia del compilatore richiede che i programmatori evitino il comportamento indefinito a tutti i costi, non riesco a pensare a nessun altro modo sicuro di scrivere il codice sopra senza utilizzare l'archiviazione allocata .


Downvoter: Vuoi commentare? Un aspetto chiave della programmazione orientata agli oggetti è la possibilità che più tipi condividano aspetti comuni e che un puntatore a tale tipo sia utilizzabile per accedere a quegli aspetti comuni. L'esempio dell'OP non lo fa, ma sta a malapena graffiando la superficie dell'essere "orientato agli oggetti". I compilatori C storici consentirebbero che il codice polimorfico sia scritto in un modo molto più pulito di quanto sia possibile nello standard C. di oggi. Progettare un codice orientato agli oggetti in C richiede quindi che si determini quale linguaggio esatto si sta prendendo di mira. Con quale aspetto le persone non sono d'accordo?
supercat

Hm ... ti dispiace mostrare come le garanzie fornite dallo standard non ti consentono di accedere in modo pulito ai membri della sotto-sequenza iniziale comune? Perché penso che questo sia ciò che la tua collera sui mali dell'audacia di ottimizzare entro i limiti del comportamento contrattuale dipende in questo momento? (Questa è la mia ipotesi su ciò che i due downvoter hanno ritenuto discutibile.)
Deduplicatore,

OOP non richiede necessariamente ereditarietà, quindi la compatibilità tra due strutture non è un grosso problema in pratica. Posso ottenere OO reale inserendo i puntatori di funzione in una struttura e invocando tali funzioni in un modo particolare. Naturalmente, questo foo_function(foo*, ...)pseudo-OO in C è solo un particolare stile API che sembra avere delle classi, ma dovrebbe essere più propriamente chiamato programmazione modulare con tipi di dati astratti.
Amon,

@Deduplicator: vedere l'esempio indicato. Il campo "i1" è un membro della sequenza iniziale comune di entrambe le strutture, ma i compilatori moderni falliranno se il codice tenta di utilizzare una "coppia di strutture *" per accedere al membro iniziale di un "trio di strutture".
supercat

Quale compilatore C moderno non supera questo esempio? Hai bisogno di opzioni interessanti?
Deduplicatore il

-3

Il prossimo passo è nascondere la dichiarazione di struct. Lo metti 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 {
    ...
};

6
Questo potrebbe essere il prossimo passo, a seconda dell'obiettivo. Che lo sia o no, questo non tenta nemmeno di rispondere in remoto alla domanda.
Deduplicatore il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.