Cosa rende imprevedibile questo utilizzo dei puntatori?


108

Attualmente sto imparando i puntatori e il mio professore ha fornito questo pezzo di codice come esempio:

//We cannot predict the behavior of this program!

#include <iostream>
using namespace std;

int main()
{
    char * s = "My String";
    char s2[] = {'a', 'b', 'c', '\0'};

    cout << s2 << endl;

    return 0;
}

Ha scritto nei commenti che non possiamo prevedere il comportamento del programma. Ma cosa lo rende imprevedibile? Non vedo nulla di sbagliato in questo.


2
Sei sicuro di aver riprodotto correttamente il codice del professore? Sebbene sia formalmente possibile sostenere che questo programma potrebbe produrre un comportamento "imprevedibile", non ha senso farlo. E dubito che qualsiasi professore userebbe qualcosa di così esoterico per illustrare "imprevedibile" agli studenti.
AnT

1
@Lightness Races in Orbit: I compilatori possono "accettare" codice mal formato dopo aver emesso i messaggi diagnostici richiesti. Ma la specifica del linguaggio non definisce il comportamento del codice. Cioè a causa dell'errore di inizializzazione di s, il programma, se accettato da qualche compilatore, ha formalmente un comportamento imprevedibile.
AnT

2
@TheParamagneticCroissant: No. L'inizializzazione è mal formata nei tempi moderni.
Gare di leggerezza in orbita il

2
@ The Paramagnetic Croissant: Come ho detto sopra, il linguaggio non richiede un codice mal formato per "non riuscire a compilare". I compilatori devono semplicemente emettere una diagnostica. Dopodiché possono continuare e compilare il codice "con successo". Tuttavia, il comportamento di tale codice non è definito dalle specifiche del linguaggio.
AnT

2
Mi piacerebbe sapere qual è stata la risposta che ti ha dato il tuo professore.
Daniël W. Crompton

Risposte:


125

Il comportamento del programma è inesistente, perché mal formato.

char* s = "My String";

Questo è illegale. Prima del 2011, era stato deprecato per 12 anni.

La riga corretta è:

const char* s = "My String";

A parte questo, il programma va bene. Il tuo professore dovrebbe bere meno whisky!


10
con -pedantic fa: main.cpp: 6: 16: warning: ISO C ++ vieta la conversione di una costante di stringa in 'char *' [-Wpedantic]
marcinj

17
@black: No, il fatto che la conversione sia illegale rende il programma mal formato. È stato deprecato in passato . Non siamo più nel passato.
Gare di leggerezza in orbita il

17
(Il che è sciocco perché questo era lo scopo della deprecazione di 12 anni)
Gare di leggerezza in orbita il

17
@black: il comportamento di un programma mal formato non è "perfettamente definito".
Gare di leggerezza in orbita il

11
Indipendentemente da ciò, la domanda riguarda C ++, non una particolare versione di GCC.
Gare di leggerezza in orbita il

81

La risposta è: dipende da quale standard C ++ stai compilando. Tutto il codice è perfettamente strutturato in tutti gli standard ‡ ad eccezione di questa riga:

char * s = "My String";

Ora, la stringa letterale ha tipo const char[10]e stiamo cercando di inizializzare un puntatore non const ad essa. Per tutti gli altri tipi diversi dalla charfamiglia dei letterali stringa, tale inizializzazione era sempre illegale. Per esempio:

const int arr[] = {1};
int *p = arr; // nope!

Tuttavia, in pre-C ++ 11, per i letterali stringa, c'era un'eccezione in §4.2 / 2:

Una stringa letterale (2.13.4) che non è una stringa letterale ampia può essere convertita in un valore di tipo " pointer to char "; [...]. In entrambi i casi, il risultato è un puntatore al primo elemento della matrice. Questa conversione viene presa in considerazione solo quando esiste un tipo di destinazione puntatore esplicito appropriato e non quando è necessaria la conversione da un lvalue a un rvalue. [Nota: questa conversione è obsoleta . Vedi allegato D. ]

Quindi in C ++ 03, il codice è perfettamente a posto (anche se deprecato) e ha un comportamento chiaro e prevedibile.

In C ++ 11, quel blocco non esiste: non esiste un'eccezione del genere per i valori letterali stringa convertiti char*, quindi il codice è mal formato come l' int*esempio che ho appena fornito. Il compilatore è obbligato a emettere una diagnostica, e idealmente in casi come questo che sono evidenti violazioni del sistema di tipo C ++, ci aspetteremmo che un buon compilatore non solo si conformasse a questo riguardo (es. Emettendo un avviso) ma fallisse a titolo definitivo.

Il codice idealmente non dovrebbe essere compilato, ma lo fa sia su gcc che su clang (presumo perché probabilmente c'è molto codice là fuori che sarebbe rotto con poco guadagno, nonostante questo buco del sistema di tipo sia deprecato per oltre un decennio). Il codice è mal formato e quindi non ha senso ragionare su quale potrebbe essere il comportamento del codice. Ma considerando questo caso specifico e la storia in cui è stato consentito in precedenza, non credo che sia un tratto irragionevole interpretare il codice risultante come se fosse un implicito const_cast, qualcosa del tipo:

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

Con ciò, il resto del programma va perfettamente bene, poiché non tocchi mai spiù. Leggere un constoggetto creato tramite un non constpuntatore è perfettamente OK. Scrivere un constoggetto creato tramite un tale puntatore è un comportamento indefinito:

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

Poiché non ci sono modifiche da snessuna parte nel codice, il programma va bene in C ++ 03, dovrebbe non riuscire a compilarsi in C ++ 11 ma lo fa comunque - e dato che i compilatori lo consentono, non c'è ancora alcun comportamento indefinito in esso † . Considerando che i compilatori stanno ancora interpretando [erroneamente] le regole del C ++ 03, non vedo nulla che possa portare a comportamenti "imprevedibili". Scrivi a sperò e tutte le scommesse sono annullate. Sia in C ++ 03 che in C ++ 11.


† Anche se, ancora una volta, per definizione un codice mal formato non offre alcuna aspettativa di un comportamento ragionevole
‡ Tranne che no, vedere la risposta di Matt McNabb


Penso che qui "imprevedibile" sia inteso dal professore per significare che non si può usare lo standard per prevedere cosa farà un compilatore con codice mal formato (oltre a fornire una diagnostica). Sì, potrebbe trattarlo come C ++ 03 dice che dovrebbe essere trattato e (a rischio dell'errore "No True Scotsman") il buon senso ci permette di prevedere con una certa sicurezza che questa è l'unica cosa che un compilatore-scrittore ragionevole sceglierà mai se il codice viene compilato. Poi di nuovo, potrebbe trattarlo nel senso di invertire la stringa letterale prima di lanciarla a non-const. Il C ++ standard non si preoccupa.
Steve Jessop il

2
@SteveJessop Non compro quell'interpretazione. Questo non è né un comportamento indefinito né della categoria di codice mal formato che lo standard identifica come nessuna diagnostica richiesta. È una semplice violazione del sistema di tipi che dovrebbe essere molto prevedibile (compila e fa cose normali su C ++ 03, non riesce a compilare su C ++ 11). Non puoi davvero usare bug del compilatore (o licenze artistiche) per suggerire che il codice è imprevedibile, altrimenti tutto il codice sarebbe tautologicamente imprevedibile.
Barry

Non sto parlando di bug del compilatore, sto parlando se lo standard definisce o meno il comportamento (se presente) del codice. Sospetto che il professore stia facendo lo stesso, e "imprevedibile" è solo un modo frettoloso per dire che lo standard attuale non definisce il comportamento. Comunque mi sembra più probabile che il professore creda erroneamente che questo sia un programma ben formato con un comportamento indefinito.
Steve Jessop il

1
No non lo fa. Lo standard non definisce il comportamento dei programmi mal formati.
Steve Jessop il

1
@supercat: è un punto giusto, ma non credo che sia il motivo principale. Penso che il motivo principale per cui lo standard non specifica il comportamento dei programmi mal formati, è che i compilatori possono supportare estensioni al linguaggio aggiungendo sintassi non ben formata (come fa Objective C). Consentire all'implementazione di fare una pulizia totale dopo una compilazione fallita è solo un bonus :-)
Steve Jessop

20

Altre risposte hanno spiegato che questo programma è mal formato in C ++ 11 a causa dell'assegnazione di un const chararray a un file char *.

Tuttavia, il programma era mal formato anche prima di C ++ 11.

I operator<<sovraccarichi sono entrati <ostream>. Il requisito per iostreaml'inclusione è ostreamstato aggiunto in C ++ 11.

Storicamente, la maggior parte delle implementazioni iostreamincludeva ostreamcomunque, forse per facilità di implementazione o forse per fornire una migliore QoI.

Ma sarebbe conforme per iostreamdefinire solo la ostreamclasse senza definire i operator<<sovraccarichi.


13

L'unica cosa leggermente sbagliata che vedo con questo programma è che non dovresti assegnare una stringa letterale a un charpuntatore mutabile , sebbene questo sia spesso accettato come estensione del compilatore.

Altrimenti, questo programma mi sembra ben definito:

  • Le regole che determinano il modo in cui le matrici di caratteri diventano puntatori di caratteri quando vengono passate come parametri (come con cout << s2) sono ben definite.
  • L'array ha terminazione null, che è una condizione per operator<<con a char*(o a const char*).
  • #include <iostream>include <ostream>, che a sua volta definisce operator<<(ostream&, const char*), quindi tutto sembra essere a posto.

12

Non è possibile prevedere il comportamento del compilatore, per i motivi sopra indicati. ( Dovrebbe non riuscire a compilare, ma potrebbe non farlo.)

Se la compilazione riesce, il comportamento è ben definito. Puoi certamente prevedere il comportamento del programma.

Se non riesce a compilare, non esiste alcun programma. In un linguaggio compilato, il programma è l'eseguibile, non il codice sorgente. Se non hai un eseguibile, non hai un programma e non puoi parlare del comportamento di qualcosa che non esiste.

Quindi direi che la dichiarazione del tuo professore è sbagliata. Non è possibile prevedere il comportamento del compilatore di fronte a questo codice, ma è diverso dal comportamento del programma . Quindi se sta per scegliere le lendini, è meglio che si assicuri di avere ragione. O, naturalmente, potresti averlo citato erroneamente e l'errore è nella tua traduzione di ciò che ha detto.


10

Come altri hanno notato, il codice è illegittimo in C ++ 11, sebbene fosse valido nelle versioni precedenti. Di conseguenza, è necessario un compilatore per C ++ 11 per emettere almeno una diagnostica, ma il comportamento del compilatore o del resto del sistema di compilazione non è specificato oltre. Niente nello standard impedirebbe a un compilatore di uscire improvvisamente in risposta a un errore, lasciando un file oggetto parzialmente scritto che un linker potrebbe ritenere valido, producendo un eseguibile danneggiato.

Sebbene un buon compilatore dovrebbe sempre garantire prima di uscire che qualsiasi file oggetto che si prevede abbia prodotto sia valido, inesistente o riconoscibile come non valido, tali problemi non rientrano nella giurisdizione dello Standard. Anche se storicamente ci sono state (e potrebbero ancora esserci) alcune piattaforme in cui una compilazione non riuscita può comportare file eseguibili apparentemente legittimi che si bloccano in modo arbitrario quando vengono caricati (e ho dovuto lavorare con sistemi in cui gli errori di collegamento spesso avevano tale comportamento) , Non direi che le conseguenze degli errori di sintassi sono generalmente imprevedibili. Su un buon sistema, un tentativo di compilazione generalmente produrrà un eseguibile con il miglior sforzo di un compilatore nella generazione del codice, o non produrrà affatto un eseguibile. Alcuni sistemi lasceranno il vecchio eseguibile dopo una compilazione fallita,

La mia preferenza personale sarebbe che i sistemi basati su disco rinominassero il file di output, per tenere conto delle rare occasioni in cui quell'eseguibile sarebbe utile evitando la confusione che può derivare dal credere erroneamente che si stia eseguendo un nuovo codice e per la programmazione incorporata sistemi per consentire a un programmatore di specificare per ogni progetto un programma che dovrebbe essere caricato se un eseguibile valido non è disponibile con il nome normale [idealmente qualcosa che indichi in modo sicuro la mancanza di un programma utilizzabile]. Un set di strumenti per sistemi integrati non avrebbe generalmente modo di sapere cosa dovrebbe fare un programma del genere, ma in molti casi qualcuno che scrive codice "reale" per un sistema avrà accesso ad un codice di test hardware che potrebbe essere facilmente adattato al scopo. Non so di aver visto il comportamento di ridenominazione, tuttavia,

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.