2015-08-15 17 views
5

Sto ponendo questa domanda nel contesto del linguaggio C, sebbene si applichi davvero a qualsiasi linguaggio che supporta i puntatori o funzionalità di riferimento pass-by.Due approcci per scrivere le funzioni

Provengo da uno sfondo Java, ma ho scritto abbastanza codice di basso livello (C e C++) per aver osservato questo fenomeno interessante. Supponendo di avere qualche oggetto X (che non usa "oggetto" nel senso più stretto della parola) che vogliamo riempire di informazioni con altre funzioni, sembra che ci siano due approcci per farlo:

  1. Restituire un'istanza del tipo di quell'oggetto e assegnarlo, ad es. se X è di tipo T, allora avremmo:
    T func(){...}

    X = func();

  2. Passando un puntatore/riferimento all'oggetto e modificandola all'interno della funzione, e restituendo sia void o qualche altro valore (in C, ad esempio, molte funzioni restituiscono un int corrispondente al successo/errore dell'operazione). Un esempio di questo è:

    int func(T* x){...x = 1;...}

    func(&X);

La mia domanda è: in quali situazioni rende un metodo migliore rispetto agli altri? Sono approcci equivalenti per ottenere lo stesso risultato? Quali sono le restrizioni di ciascuno?

Grazie!

+1

se la funzione sarà sicuramente successo (che è raro nel mondo C), '1' e' 2' sono equivalenti. in caso contrario, '2' restituisce anche il codice di errore. in cortometraggi, '2' è più pratico. – HuStmpHrrr

risposta

3

C'è un motivo per cui si dovrebbe sempre considerare l'utilizzo del secondo metodo, piuttosto che del primo. Se si osservano i valori restituiti per l'intera libreria standard C, si noterà che quasi sempre un elemento di gestione degli errori gestisce.Ad esempio, si controlla il valore di ritorno delle seguenti funzioni prima di assumere che sono riusciti:

  • calloc, malloc e realloc
  • getchar
  • fopen
  • scanf e familiare
  • strtok

Ci sono altre funzioni non standard che seguono questo schema:

  • pthread_create, ecc
  • socket, connect, ecc
  • open, read, write, ecc

Generalmente parlando, un valore di ritorno trasmette un numero di elementi letti/scritti/convertiti con successo o un valore di successo/errore booleano flat-out, e in pratica avrete quasi sempre bisogno di tali un valore di ritorno, a meno che non si passi a exit(EXIT_FAILURE); a qualsiasi errore (nel qual caso preferirei non utilizzare i tuoi moduli, perché non mi danno alcuna possibilità di ripulire il mio codice).

Esistono funzioni che non utilizzano questo modello nella libreria C standard, in quanto non utilizzano risorse (ad es. Allocazioni o file) e quindi non vi è alcuna possibilità di errore. Se la tua funzione è una funzione di traduzione di base (ad esempio toupper, tolower e amici che traducono valori di carattere singolo), ad esempio, non hai bisogno di un valore di ritorno per la gestione degli errori perché non ci sono errori. Penso che troverai questo scenario abbastanza raro, ma se questo è il tuo scenario, usa la prima opzione!

In sintesi, si dovrebbe sempre considerare l'utilizzo dell'opzione 2, riservando il valore restituito per un uso simile, per coerenza con il resto del mondo e perché in un momento successivo si potrebbe decidere che è necessario il valore restituito per errori di comunicazione o numero di articoli elaborati.

1

Il metodo (1) passa l'oggetto in base al valore, che richiede che l'oggetto venga copiato. Viene copiato quando lo passi e copiato di nuovo quando viene restituito. Il metodo (2) passa solo un puntatore. Quando passi una primitiva, (1) va bene, ma quando passi un oggetto, una struttura o un array, è solo uno spreco di spazio e tempo.

In Java e in molti altri linguaggi, gli oggetti vengono sempre passati per riferimento. Dietro le quinte, viene copiato solo un puntatore. Ciò significa che anche se la sintassi è simile a (1), in realtà funziona come (2).

+0

non necessario.per esempio, 'gcc' lo implementa, anteponendo implicitamente un argomento di puntatore al prototipo di funzione, e passa implicitamente nell'indirizzo del valore di ritorno. in questa implementazione, non viene eseguita alcuna copia aggiuntiva. – HuStmpHrrr

1

Nel metodo 2, si chiama x un parametro di uscita . Questo è in realtà un disegno molto comune utilizzato in molti luoghi ... pensa alcune delle varie funzioni C incorporate che popolano un buffer di testo, come snprintf.

Questo ha il vantaggio di essere abbastanza efficiente dal punto di vista dello spazio, dal momento che non si copieranno strutture/matrici/dati nello stack e si ritorneranno istanze nuovissime.

Una qualità veramente, davvero conveniente del metodo 2 è che si può essenzialmente avere un numero qualsiasi di "valori di ritorno". Si "restituiscono" i dati attraverso i parametri di output, ma è possibile anche restituire un indicatore di successo/errore dalla funzione.

Un buon esempio del metodo 2 utilizzato efficacemente è nella funzione C incorporata strtol. Questa funzione converte una stringa in un long (in pratica, analizza un numero da una stringa). Uno dei parametri è un char **. Quando si chiama la funzione, si dichiara char * endptr localmente e si passa &endptr.

La funzione restituirà uno:

  • il valore convertito in caso di successo,
  • 0 se non è riuscito, o
  • LONG_MIN o LONG_MAX se fosse fuori portata

e impostare endptr in modo che punti alla prima cifra non trovata.

Questo è ottimo per la segnalazione di errori se il programma dipende dall'input dell'utente, perché è possibile verificare la presenza di errori in molti modi e segnalare errori diversi per ciascuno.

Se endptr non null è dopo la chiamata a strtol, poi si sa con precisione che l'utente ha inserito un non intero, ed è possibile stampare immediatamente il carattere che la conversione non è riuscita su se vuoi.

Come Thom fa notare, Java semplifica l'implementazione del metodo 2 simulando il comportamento pass-by-reference, che è solo un puntatore dietro le quinte senza la sintassi del puntatore nel codice sorgente.

Per rispondere alla tua domanda: Penso che C si presti bene al secondo metodo. Funzioni come realloc sono lì per darti più spazio quando ne hai bisogno. Tuttavia, non c'è molto che ti impedisce di usare il primo metodo.

Forse stai cercando di implementare una sorta di oggetto immutabile. Il primo metodo sarà la scelta lì. Ma in generale, opto per il secondo.

1

Penso di averti preso.

Questi approcci sono molto diversi. La domanda che devi porre a te stesso quando cerchi di decidere quale approccio prendere è:

Quale classe avrebbe la responsabilità?

Nel caso in cui si passi il riferimento all'oggetto si decapoli la creazione dell'oggetto al chiamante e si crei questa funzionalità per essere più funzionale e si sarebbe in grado di creare una classe di utilità che tutte le funzioni all'interno saranno senza stato, stanno ricevendo oggetti che manipolano l'input e lo restituiscono.

L'altro approccio è più probabile e API, si richiede un opperation.

Per un esempio, stai ricevendo una matrice di byte e vorresti convertirlo in stringa, probabilmente avresti scelto il primo approccio.

E se volessi fare un po 'di opperation in DB, sceglieresti il ​​secondo.

Quando mai avrai più di una funzione dal primo punto che copre la stessa area, la incapsulerai in una classe util, lo stesso applay al secondo, lo incapsulerai in un'API.

1

Risposta breve: prendere 2 se non si dispone di un motivo necessario per prendere 1.

Risposta lunga: nel mondo del C++ e delle lingue derivate, Java, C#, le eccezioni aiutano molto. Nel mondo C, non c'è molto che tu possa fare. Che segue è un esempio di API prendo dalla libreria CUDA, che è una libreria che mi piace e prendere in considerazione ben progettato:

cudaError_t cudaMalloc (void **devPtr, size_t size); 

confronta API con malloc:

void *malloc(size_t size); 

nelle interfacce vecchi C, ci sono molti esempi:

int open(const char *pathname, int flags); 
FILE *fopen(const char *path, const char *mode); 

direi alla fine del mondo, l'interfaccia CUDA sta fornendo è molto più evidente e portare a risultato corretto.

ci sono altre serie di interfacce che lo spazio valore di ritorno valida in realtà si sovrappone con il codice di errore, in modo che i progettisti di queste interfacce graffiato la testa e venire con non brillanti a tutte le idee, dicono:

ssize_t read(int fd, void *buf, size_t count); 

una funzione giornaliera come la lettura di un contenuto di file è limitata dalla definizione di ssize_t. poiché il valore di ritorno deve codificare anche il codice di errore, deve fornire un numero negativo. in un sistema a 32 bit, il massimo di ssize_t è 2G, che è molto limitato il numero di byte che è possibile leggere dal file.

Se il tuo identificatore di errore è codificato all'interno del valore di ritorno della funzione, scommetto che i programmatori di 10/10 non cercheranno di controllarlo, anche se sanno davvero che dovrebbero; semplicemente non lo fanno, o non lo ricordano, perché la forma non è ovvia.

E un altro motivo è che gli esseri umani sono molto pigri e non sono bravi nel trattare se lo sono. La documentazione di queste funzioni descriverà quanto segue:

se il valore di ritorno è NULL quindi ... blah.

se il valore di ritorno è 0 quindi ... blah.

yak.

Nella prima forma, le cose cambiano. Come giudichi se il valore è stato restituito? No NULL o 0 altro. È necessario utilizzare SUCCESS, FAILURE1, FAILURE2 o qualcosa di simile. Questa interfaccia costringe gli utenti a codificare più sicuro e rende il codice molto robusto.

Con queste macro, o enum, è molto più semplice per i programmatori apprendere l'effetto dell'API e la causa di diverse eccezioni. Con tutti questi vantaggi, in realtà non esiste nemmeno un sovraccarico extra di runtime.

1

(Supponendo parliamo restituendo solo uno valore dalla funzione.)

In generale, il primo metodo viene utilizzato quando il tipo T è relativamente piccolo. È decisamente preferibile con i tipi scalari. Può essere utilizzato con tipi più grandi. Ciò che è considerato "abbastanza piccolo" per questi scopi dipende dalla piattaforma e dall'impatto prestazionale atteso. (Quest'ultimo è causato dal fatto che l'oggetto restituito viene copiato.)

Il secondo metodo viene utilizzato quando l'oggetto è relativamente grande, poiché questo metodo non esegue alcuna copia. E con tipi non copiabili, come gli array, non hai altra scelta che usare il secondo metodo.

Ovviamente, quando le prestazioni non sono un problema, il primo metodo può essere facilmente utilizzato per restituire oggetti di grandi dimensioni.


Un aspetto interessante sono le opportunità di ottimizzazione disponibili per il compilatore C. Nei compilatori in linguaggio C++ è consentito eseguire l'ottimizzazione del valore restituito (RVO, NRVO), che trasforma efficacemente il primo metodo nel secondo "sotto il cofano" in situazioni in cui il secondo metodo offre prestazioni migliori. Per facilitare tali ottimizzazioni, il linguaggio C++ rilassa alcuni requisiti di identità degli indirizzi imposti sugli oggetti coinvolti. AFAIK, C non offre tali rilassamenti, prevenendo (o almeno impedendo) qualsiasi tentativo di RVO/NRVO.

1

cercherò di spiegare :)
diciamo che dovete caricare un razzo gigante in semi,

  • Metodo 1) Camionista pone un camion in un parcheggio, e va a trovare una prostituta, si impila con il carico sul carrello elevatore o qualche tipo di rimorchio per portarlo in pista.
  • Metodo 2) Camionista dimentica Hooker e spalle camion a destra al razzo, allora avete bisogno solo di spingere in.

Questa è la differenza tra questi due :)
. Che cosa si riduce al di programmazione è:

  • Metodo 1) riserve funzione chiamante e l'indirizzo per la chiamata funzione per restituire il suo valore di ritorno, ma come sta chiamando la funzione sta per ottenere quel valore non importa, si è devo prenotare un altro indirizzo o meno non importa, ho bisogno di qualcosa restituito, è il tuo lavoro portarmelo :). La cosiddetta funzione va e riserva l'indirizzo per i suoi calcoli e memorizza il valore nell'indirizzo quindi restituisce il valore al chiamante. Quindi il chiamante va e dice oh grazie fammi semplicemente copiare l'indirizzo che ho prenotato in precedenza.
  • Metodo 2) La funzione chiamante dice "Ehi ti aiuterò, ti darò l'indirizzo che ho prenotato, memorizzerò i calcoli che fai in esso", in questo modo non solo salvi la memoria ma salvi in ​​tempo .

E penso secondo è meglio, e qui è il motivo:


Quindi diciamo che avete struct con 1000 interi all'interno di esso, il metodo 1 sarebbe inutile, dovrà riserva 2 * 100 * 32 bit di memoria, che è 6400 in più devi copiarlo nella prima posizione piuttosto che copiarlo in seconda. Quindi, se ogni copia richiede 1 millisecondo, sarà necessario 6.4 secondi per memorizzare e copiare le variabili. Se hai un indirizzo, devi solo memorizzarlo una volta.

1

Sono equivalenti a me ma non nell'implementazione.

#include <stdio.h> 
#include <stdlib.h> 

int func(int a,int b){ 
    return a+b; 
} 

int funn(int *x){ 
    *x=1; 
    return 777; 
} 

int main(void){ 
    int sx,*dx; 
    /* case static' */ 
    sx=func(4,6); /* looks legit */ 
    funn(&sx); /* looks wrong in this case */ 
    /* case dynamic' */ 
    dx=malloc(sizeof(int)); 
    if(dx){ 
     *dx=func(4,6); /* looks wrong in this case */ 
     sx=funn(dx); /* looks legit */ 
     free(dx); 
    } 
    return 0; 
} 

In un approccio statico è più comodo per me eseguire il primo metodo. Perché non voglio fare confusione con la parte dinamica (con i puntatori legit). Ma in un approccio dinamico userò il tuo secondo metodo. Perché è fatto per questo. Quindi sono equivalenti ma non uguali, il secondo approccio è chiaramente fatto per i puntatori e quindi per la parte dinamica.

E finora più chiaro ->

int main(void){ 
    int sx,*dx; 
    sx=func(4,6); 
    dx=malloc(sizeof(int)); 
    if(dx){ 
     sx=funn(dx); 
     free(dx); 
    } 
    return 0; 
} 

rispetto ->

int main(void){ 
    int sx,*dx; 
    funn(&sx); 
    dx=malloc(sizeof(int)); 
    if(dx){ 
     *dx=func(4,6); 
     free(dx); 
    } 
    return 0; 
} 
Problemi correlati