2009-03-21 24 views
41

Stavo sperimentando con C++ e ho trovato il codice seguente come molto strano.Accesso ai membri della classe su un puntatore NULL

class Foo{ 
public: 
    virtual void say_virtual_hi(){ 
     std::cout << "Virtual Hi"; 
    } 

    void say_hi() 
    { 
     std::cout << "Hi"; 
    } 
}; 

int main(int argc, char** argv) 
{ 
    Foo* foo = 0; 
    foo->say_hi(); // works well 
    foo->say_virtual_hi(); // will crash the app 
    return 0; 
} 

So che il metodo di chiamata virtuali si blocca perché richiede una ricerca vtable e può funzionare solo con oggetti validi.

Ho le seguenti domande

  1. Come funziona il metodo virtuale say_hi lavoro non su un puntatore NULL?
  2. Dove viene assegnato l'oggetto foo?

Qualche idea?

+3

Vedere [this] (http://stackoverflow.com/questions/2474018/when-does-invoking-a-member-function-on-a-null-inull-instance-result-in-undefined-behav) per cosa la lingua dice a riguardo. Entrambi sono comportamenti indefiniti. – GManNickG

risposta

80

L'oggetto foo è una variabile locale con tipo Foo*. Quella variabile probabilmente viene allocata nello stack per la funzione main, proprio come qualsiasi altra variabile locale. Ma il valore memorizzato in foo è un puntatore nullo. Non punta da nessuna parte. Non è presente alcuna istanza di tipo Foo rappresentata da nessuna parte.

Per chiamare una funzione virtuale, il chiamante deve sapere a quale oggetto viene chiamata la funzione. Questo perché l'oggetto stesso è ciò che indica quale funzione dovrebbe essere effettivamente chiamata. (Questo è spesso implementato dando all'oggetto un puntatore a un vtable, una lista di puntatori di funzione, e il chiamante sa solo che dovrebbe chiamare la prima funzione dell'elenco, senza sapere in anticipo dove punta quel puntatore.)

Ma per chiamare una funzione non virtuale, il chiamante non ha bisogno di sapere tutto questo. Il compilatore sa esattamente quale funzione verrà chiamata, quindi può generare un'istruzione CALL codice macchina per andare direttamente alla funzione desiderata. Passa semplicemente un puntatore all'oggetto in cui la funzione è stata richiamata come parametro nascosto per la funzione. In altre parole, il compilatore traduce tua chiamata di funzione in questo:

void Foo_say_hi(Foo* this); 

Foo_say_hi(foo); 

Ora, dal momento che l'attuazione di tale funzione non fa riferimento a tutti i membri dell'oggetto puntato da suo argomento this, schivare efficacemente il proiettile di dereferenziamento di un puntatore nullo perché non ne viene mai denotato uno.

Formalmente, chiamare qualsiasi funzione - anche non virtuale - su un puntatore nullo è un comportamento non definito. Uno dei risultati consentiti di un comportamento indefinito è che il codice sembra funzionare esattamente come previsto. L'utente non dovrebbe fare affidamento su questo, anche se a volte si trovano librerie dal proprio fornitore del compilatore che do si basano su questo. Ma il fornitore del compilatore ha il vantaggio di poter aggiungere un'ulteriore definizione a quello che altrimenti sarebbe un comportamento indefinito. Non farlo da solo

+1

Buona risposta. Grazie –

+0

Sembra anche che ci sia confusione sul fatto che il codice della funzione e i dati dell'oggetto sono due cose diverse. Dai un'occhiata a questo http://stackoverflow.com/questions/1966920/more-info-on-memory-layout-of-an-executable-program-process. I dati dell'oggetto non sono disponibili dopo l'inizializzazione in questo caso a causa del puntatore nullo, ma il codice è sempre stato disponibile in memoria altrove. – Loki

+0

FYI questo è derivato da '[C++ 11: 9.3.1/2]': "Se una funzione membro non statica di una classe' X' viene chiamata per un oggetto che non è di tipo 'X', o di un tipo derivato da 'X', il comportamento non è definito." Chiaramente '* foo' non è di tipo' Foo' (poiché non esiste). –

7

Il dereferenziamento di un puntatore NULL causa un "comportamento non definito". Ciò significa che potrebbe accadere qualsiasi cosa, anche il tuo codice potrebbe sembrare funzionare correttamente. Non devi comunque dipendere da questo, se esegui lo stesso codice su una piattaforma diversa (o forse sulla stessa piattaforma) probabilmente si bloccherà.

Nel codice non c'è nessun oggetto Foo, solo un puntatore che è initalizzato con il valore NULL.

+0

Grazie. Cosa ne pensi della seconda domanda? Dove viene assegnato * Foo *? –

+0

foo non è un oggetto, è un puntatore. Quel puntatore è allocato nello stack (come qualsiasi variabile che non è contrassegnata come 'statica' o assegnata con 'nuovo'. E non punta mai a un oggetto valido. – jalf

+0

Non c'è Foo :-) Foo è puntatore. –

16

La funzione membro say_hi() di solito è implementato dal compilatore come

void say_hi(Foo *this); 

Dal momento che non accedere tutti i membri, la chiamata ha esito positivo (anche se si sta entrando in un comportamento indefinito secondo lo standard).

Foo non viene assegnato affatto.

+0

Grazie. Se * Foo * non viene assegnato, come avviene la chiamata? Sono un po 'confuso .. –

+1

Processor o assembly rispettivamente, non ha idea dei dettagli HLL del codice. Le funzioni non virtuali di C++ sono semplicemente normali funzioni con un contratto che il puntatore "questo" è in un dato luogo (registro o stack, dipende dai compilatori). Finché non si accede al puntatore "this", tutto va bene. – arul

2

a) Funziona perché non esegue la deviazione di nulla attraverso il "puntatore" implicito. Non appena lo fai, boom. Non sono sicuro al 100%, ma penso che le deduzioni dei puntatori nulli siano eseguite da RW che protegge il primo 1K di spazio di memoria, quindi c'è una piccola possibilità che la nullferenza non venga rilevata se si denomina solo la riga 1K (ad esempio una variabile di istanza che otterrebbe assegnato molto lontano, come:

class A { 
    char foo[2048]; 
    int i; 
} 

poi a-> avrei forse essere non rilevata quando a è nullo

b) Da nessuna parte, è dichiarata solo un puntatore, che viene assegnata sulla principale (.): s stack.

2

La chiamata a say_hi è vincolata staticamente. Quindi il computer esegue semplicemente una chiamata standard a una funzione. La funzione non utilizza campi, quindi non ci sono problemi.

La chiamata a virtual_say_hi è vincolata dinamicamente, quindi il processore va alla tabella virtuale, e poiché non c'è una tabella virtuale lì, salta da qualche parte in modo casuale e arresta il programma.

+0

Ciò ha perfettamente senso. Grazie –

1

Nei giorni originali di C++, il codice C++ è stato convertito in metodi di un oggetto C. vengono convertiti in metodi non-oggetto come questo (nel tuo caso):

foo_say_hi(Foo* thisPtr, /* other args */) 
{ 
} 

Naturalmente, il nome foo_say_hi è semplificata. Per maggiori dettagli, cerca mangling del nome in C++.

Come potete vedere, se thisPtr non viene mai dereferenziato, il codice va bene e ha esito positivo. Nel tuo caso, non è stata utilizzata alcuna variabile di istanza o qualsiasi cosa che dipende da thisPtr.

Tuttavia, le funzioni virtuali sono diverse. Esistono molte ricerche di oggetti per assicurarsi che il puntatore all'oggetto giusto venga passato come parametro alla funzione. Questo dosternerà il thisPtr e causerà l'eccezione.

5

È un comportamento non definito. Ma la maggior parte dei compilatori ha fornito istruzioni che gestiranno correttamente questa situazione se non si accede alle variabili membro e alla tabella virtuale.

Vediamo lo smontaggio in studio visivo per capire cosa succede

Foo* foo = 0; 
004114BE mov   dword ptr [foo],0 
    foo->say_hi(); // works well 
004114C5 mov   ecx,dword ptr [foo] 
004114C8 call  Foo::say_hi (411091h) 
    foo->say_virtual_hi(); // will crash the app 
004114CD mov   eax,dword ptr [foo] 
004114D0 mov   edx,dword ptr [eax] 
004114D2 mov   esi,esp 
004114D4 mov   ecx,dword ptr [foo] 
004114D7 mov   eax,dword ptr [edx] 
004114D9 call  eax 

come si può vedere Foo: say_hi chiamato come al solito, ma con funzione questo nel registro ECX. Per semplificare si può supporre che questo passato come parametro implicito che non usiamo mai nel tuo esempio.
Ma nel secondo caso calcoliamo l'indirizzo della funzione tabella virtuale dovuta, a causa di addre e diventa core.

+0

Grazie. Puoi dirmi come posso ottenere questo smontaggio in Visual Studio? Sto usando VS2008 –

+2

Debug-> Windows-> Disassembly under debuger – bayda

1

È importante rendersi conto che le chiamateproducono un comportamento non definito e che il comportamento può manifestarsi in modi imprevisti. Anche se la chiamata sembra funzionare, potrebbe essere in atto un campo minato.

considerare questa piccola modifica al tuo esempio:

Foo* foo = 0; 
foo->say_hi(); // appears to work 
if (foo != 0) 
    foo->say_virtual_hi(); // why does it still crash? 

Dal momento che la prima chiamata a foo consente un comportamento indefinito se foo è nullo, il compilatore è ora libero di assumere che foo è non nullo. Ciò rende il if (foo != 0) ridondante e il compilatore può ottimizzarlo! Potresti pensare che questa sia un'ottimizzazione molto insensata, ma gli scrittori di compilatori sono diventati molto aggressivi, e qualcosa del genere è successo nel codice reale.

Problemi correlati