Restituzione dei dati locali dalle funzioni in C e C ++ tramite puntatore

Ho una discussione con il mio amico. Dice che posso restituire un puntatore ai dati locali da una funzione. Questo non è quello che ho imparato, ma non riesco a trovare un controsenso per dimostrare la mia conoscenza.

Ecco un caso illustrato:

char *name() { char n[10] = "bodacydo!"; return n; } 

Ed è usato come:

 int main() { char *n = name(); printf("%s\n", n); } 

Dice che questo è perfettamente OK perché dopo che un programma chiama il nome, restituisce un puntatore a n, e subito dopo lo stampa. Nient’altro accade nel programma nel frattempo, perché è single threaded e l’esecuzione è seriale.

Non riesco a trovare una contro-argomentazione. Non scriverei mai un codice del genere, ma è testardo e dice che è tutto ok. Se fossi il suo capo, lo licenzierei per essere un idiota testardo, ma non riesco a trovare una contro-argomentazione.

Un altro esempio:

 int *number() { int n = 5; return &n; } int main() { int *a = number(); int b = 9; int c = *a * b; printf("%d\n", c); } 

Gli invierò questo link dopo avrò delle buone risposte, quindi almeno imparerà qualcosa.

si verificherà un problema quando si chiama un’altra funzione tra name () e printf (), che a sua volta utilizza lo stack

 char *fun(char *what) { char res[10]; strncpy(res, what, 9); return res; } main() { char *r1 = fun("bla"); char *r2 = fun("blubber"); printf("'%s' is bla and '%s' is blubber", r1, r2); } 

Il tuo amico ha torto.

name sta restituendo un puntatore allo stack di chiamate. Una volta richiamato printf , non si può sapere come lo stack verrà sovrascritto prima di accedere ai dati del puntatore. Potrebbe funzionare sul suo compilatore e sulla sua macchina, ma non funzionerà su tutti loro.

Il tuo amico afferma che dopo la restituzione del name , “non succede nulla se non stamparlo”. printf è di per sé un’altra chiamata di funzione, con chissà quanta complessità al suo interno. Sta accadendo molto prima che i dati vengano stampati.

Inoltre, il codice non è mai finito, sarà modificato e aggiunto a. Codice il “non fa nulla” ora farà qualcosa una volta che è cambiato, e il tuo ragionamento da vicino si farà a pezzi.

Restituire un puntatore ai dati locali è una ricetta per il disastro.

Non appena l’ambito della funzione termina cioè dopo la parentesi di chiusura} della funzione, verrà lasciata la memoria allocata (sullo stack) per tutte le variabili locali. Quindi, restituire il puntatore ad una memoria che non è più valida richiama un comportamento indefinito. Inoltre, è ansible dire che la durata della variabile locale è terminata quando la funzione ha terminato l’esecuzione.

Inoltre maggiori dettagli puoi leggere QUI .

Le mie contro-argomentazioni sarebbero:

  • non è mai OK scrivere codice con comportamento indefinito ,
  • quanto tempo prima che qualcun altro usi quella funzione in un contesto diverso,
  • la lingua fornisce servizi per fare la stessa cosa legalmente (e possibilmente in modo più efficiente)

È un comportamento indefinito e il valore potrebbe facilmente essere distrutto prima che venga effettivamente stampato. printf() , che è solo una funzione normale, potrebbe utilizzare alcune variabili locali o chiamare altre funzioni prima che la stringa venga effettivamente stampata. Poiché queste azioni utilizzano lo stack, potrebbero facilmente corrompere il valore.

Se il codice capita di stampare il valore corretto dipende dall’implementazione di printf() e dal modo in cui le chiamate di funzione funzionano sul compilatore / piattaforma che si sta utilizzando (quali parametri / indirizzi / variabili vengono messi in pila, …). Anche se il codice “funziona” sulla tua macchina con determinate impostazioni del compilatore, è tutt’altro che sicuro che funzionerà in qualsiasi altro luogo o in condizioni di confine leggermente diverse.

Hai ragione – n vite in pila e quindi potrebbe andare via non appena la funzione ritorna.

Il codice del tuo amico potrebbe funzionare solo perché il percorso di memoria su cui punta n non è stato corrotto (ancora!).

Come già sottolineato dagli altri, non è illegale farlo, ma una ctriggers idea perché i dati restituiti si trovano nella parte non utilizzata dello stack e possono essere ignorati in qualsiasi momento da altre chiamate di funzione.

Ecco un contro-esempio che si blocca sul mio sistema se compilato con le ottimizzazioni triggerste:

 char * name () { char n[] = "Hello World"; return n; } void test (char * arg) { // msg and arg will reside roughly at the same memory location. // so changing msg will change arg as well: char msg[100]; // this will override whatever arg points to. strcpy (msg, "Logging: "); // here we access the overridden data. A bad idea! strcat (msg, arg); strcat (msg, "\n"); printf (msg); } int main () { char * n = name(); test (n); return 0; } 

Hai ragione, il tuo amico ha torto. Ecco un semplice controesempio:

 char *n = name(); printf("(%d): %s\n", 1, n); 

gcc: main.c: nella funzione ‘nome’: main.c: 4: warning: la funzione restituisce l’indirizzo della variabile locale

Ovunque sia stato fatto in quel modo (ma non è un codice sexy: p):

 char *name() { static char n[10] = "bodacydo!"; return n; } int main() { char *n = name(); printf("%s\n", n); } 

Attenzione non è thread-safe.

Restituire il puntatore alla variabile locale è sbagliato, anche se sembra funzionare in qualche rara situazione.

Una variabile locale (automatica) può essere allocata dallo stack o dai registri.

  • Se viene assegnato dallo stack, verrà sovrascritto non appena verrà eseguita la prossima chiamata alla funzione (ad esempio printf) o se si verifica un’interruzione.
  • Se la variabile è allocata da un registro, non è nemmeno ansible avere un puntatore che punta ad esso.

Anche se l’applicazione è “single threaded”, gli interrupt potrebbero utilizzare lo stack. Per essere relativamente sicuro, è necessario disabilitare gli interrupt. Ma non è ansible disabilitare l’NMI (Non Maskable Interrupt), quindi non si può mai essere sicuri.

Se è vero che non è ansible riportare i puntatori a variabili di stack locali dichiarate all’interno di una funzione, è tuttavia ansible allocare memoria all’interno di una funzione utilizzando malloc e quindi restituire un puntatore a tale blocco. Forse è questo che intendeva il tuo amico?

 #include #include #include char* getstr(){ char* ret=malloc(sizeof(char)*15); strcpy(ret,"Hello World"); return ret; } int main(){ char* answer=getstr(); printf("%s\n", answer); free(answer); return 0; } 

Per come la vedo io ho tre opzioni principali perché questa è pericolosa e utilizza un comportamento indefinito:

sostituire: char n[10] = "bodacydo!"

con: static char n[10] = "bodacydo!"

Ciò darà risultati indesiderati se si utilizza la stessa funzione più di una volta nella riga durante il tentativo di mantenere i valori in essa contenuti.

sostituire:
char n[10] = "bodacydo!"

con:
char *n = new char[10]; *n = "bodacydo!"

Con risolverà il problema di cui sopra, ma sarà quindi necessario eliminare la memoria heap o iniziare a incorrere in perdite di memoria.

O infine:

sostituire: char n[10] = "bodacydo!";

con: shared_ptr<char> n(new char[10]) = "bodacydo!"; shared_ptr<char> n(new char[10]) = "bodacydo!";

Il che ti solleva dall’eliminare la memoria dell’heap, ma poi cambierai il tipo di ritorno e il char * n in main in un shared_prt, così da liberare la gestione del puntatore. Se non lo si distribuisce, l’ambito di shared_ptr terminerà e il valore memorizzato nel puntatore verrà impostato su NULL.

Se prendiamo il segmento di codice che hai dato ….

 char *name() { char n[10] = "bodacydo!"; return n; } int main() { char *n = name(); printf("%s\n", n); } 

Va bene usare la var locale in printf () nel main ‘coz qui stiamo usando una stringa letterale che ancora non è qualcosa di local to name ().

Ma ora guardiamo un codice leggermente diverso

 class SomeClass { int *i; public: SomeClass() { i = new int(); *i = 23; } ~SomeClass() { delete i; i = NULL; } void print() { printf("%d", *i); } }; SomeClass *name() { SomeClass s; return &s; } int main() { SomeClass *n = name(); n->print(); } 

In questo caso, quando la funzione name () restituisce, verrà chiamato il distruttore SomeClass e il membro var i sarebbe deallocato e impostato su NULL.

Quindi, quando chiamiamo print () in main anche se il mem puntato da n non viene sovrascritto (lo presumo), la chiamata di stampa si bloccherà quando tenterà di de-referenziare un puntatore NULL.

Quindi in un certo senso il vostro segmento di codice molto probabilmente fallirà, ma molto probabilmente fallirà se il decostruttore degli oggetti sta eseguendo una deinitializzazione delle risorse e in seguito la useremo.

Spero che sia d’aiuto