Qual è il modo corretto di implementare un tipo di dati astratto in C?

1

Nel suo libro Patterns in C , Adam Petersen descrive l'uso di un puntatore a una struttura, dichiarata in un file di intestazione, per creare un tipo di dati astratto di prima classe:

/* Customer.h */
/* A pointer to an incomplete type (hides the implementation details). */
typedef struct Customer* CustomerPtr;
/* Create a Customer and return a handle to it. */
CustomerPtr createCustomer(const char* name, const Address* address);
/* Destroy the given Customer. All handles to it will be invalidated. */
void destroyCustomer(CustomerPtr customer);

La struct e le due funzioni sono definite in Customer.c:

#include ”Customer.h”
#include <stdlib.h>
struct Customer
{
    const char* name;
    Address address;
    size_t noOfOrders;
    Order orders[42];
};
CustomerPtr createCustomer(const char* name, const Address* address)
{
    CustomerPtr customer = malloc(sizeof * customer);
    if(customer)
    {
        /* Initialize each field in the customer... */
    }
    return customer;
}
void destroyCustomer(CustomerPtr customer)
{
    /* Perform clean-up of the customer internals, if necessary. */
    free(customer);
}

Il modo in cui organizzerei il mio codice è un po 'diverso. Il file di intestazione dichiarerebbe la struct stessa, non un puntatore ad esso. Ecco come mi è stato insegnato a scrivere qualcosa di simile:

/* Customer.h */
struct Customer;
extern struct Customer* newCustomer(const char* name);

e dichiarazioni:

/* Customer.c */
struct Customer {
    char name[CUSTNAMELENGTH +1];
};
struct Customer* newCustomer(const char* name) {
    struct Customer* newCustomer;
    newCustomer = malloc(sizeof(struct Customer));
    /* initialize customer instance */
    return newCustomer;
}

Qual è la differenza pratica tra i due stili? C'è un potenziale problema con il mio modo che non vedo? Sembra incapsulare l'oggetto con successo e fornire un'interfaccia simile al design di Petersen. Quando dovrei usare il suo design piuttosto che quello che ho fatto?

    
posta ridthyself 04.10.2016 - 15:56
fonte

2 risposte

3

Questi non sono così diversi.

Entrambi i file .h dichiarano effettivamente struct Customer come dichiarazione anticipata. Il primo definisce anche un tipo di puntatore, che il secondo no. Alcuni preferiscono typedef in quanto non gli piace scrivere struct e * ; Probabilmente sarei d'accordo

La dichiarazione extern è buona, ma non necessaria, credo. Altrimenti, la denominazione createCustomer rispetto a newCustomer è abbastanza arbitraria per me.

Uno potrebbe essere più critico nei due stili eccetto che nessuno dei due è molto completo; il secondo esempio anche meno.

FYI, entrambi hanno potenziali errori.

Il primo malloc è solo un puntatore singolo invece di una struct (fa riferimento a struct * customer invece di struct Customer , quindi non verrà compilato neanche). Assegna anche una dimensione fissa di 42 per gli ordini, il che non ha molto senso per me; sarebbe probabilmente meglio renderlo una struttura di dati espandibile (sebbene, il lettore astuto potrebbe notare che in C / C ++ una struttura che termina con una matrice è potenzialmente espandibile - ancora se lo facessimo userebbe qualcosa come zero come dimensione dell'array.)

Il secondo presuppone una lunghezza fissa del nome del cliente, che se sufficientemente grande è dispendiosa quando non è necessaria e, presumibilmente, può ancora essere superata. Probabilmente sarebbe meglio assegnare le stringhe alle dimensioni, il che eviterebbe sia lo spreco di spazio che il problema dell'overflow.

    
risposta data 04.10.2016 - 16:27
fonte
2

Preferisco il tuo approccio: presentare esplicitamente il contesto dell'API come puntatore a una struttura.

Alcune persone pensano invece di presentare il contesto come una "cosa" non specificata più pulita, quindi usano un typedef per nascondere la struttura e il puntatore. Non posso dire che si sbagliano, ma la mia opinione è che, per un programmatore C, un puntatore a una struttura astratta come un typedef di tipo sconosciuto.

C'è un piccolo vantaggio, però, e cioè che il file di intestazione ha il controllo di ciò che è effettivamente il contesto. Quindi, ad esempio, un'API potrebbe iniziare la sua vita con un contesto che è solo un numero intero ("typedef int CustomerContext") potrebbe successivamente cambiarlo in un puntatore a una struttura ("typedef struct Customer * CustomerContext") senza richiedere alcuna modifica al codice che utilizza l'API (a condizione che il codice utente non faccia presupposti non validi sul fatto che il contesto sia un puntatore o un intero). Ma non importa se la tua API sta iniziando la sua vita con una struttura che già detiene il contesto.

    
risposta data 12.10.2016 - 21:36
fonte

Leggi altre domande sui tag