L'accesso a un array fuori limite non dà alcun errore, perché?


177

Sto assegnando valori in un programma C ++ fuori dai limiti come questo:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    return 0;
}

Il programma stampa 3e 4. Non dovrebbe essere possibile. Sto usando g ++ 4.3.3

Ecco il comando di compilazione ed esecuzione

$ g++ -W -Wall errorRange.cpp -o errorRange
$ ./errorRange
3
4

Solo durante l'assegnazione array[3000]=3000mi dà un errore di segmentazione.

Se gcc non controlla i limiti dell'array, come posso essere sicuro che il mio programma sia corretto, dal momento che può portare ad alcuni problemi seri in seguito?

Ho sostituito il codice sopra con

vector<int> vint(2);
vint[0] = 0;
vint[1] = 1;
vint[2] = 2;
vint[5] = 5;
cout << vint[2] << endl;
cout << vint[5] << endl;

e anche questo non produce alcun errore.



16
Il codice è errato, ovviamente, ma genera un comportamento indefinito . Non definito indica che potrebbe essere eseguito o meno fino al completamento. Non vi è alcuna garanzia di un arresto anomalo.
dmckee --- ex gattino moderatore

4
Puoi essere sicuro che il tuo programma sia corretto non rovinandoti con array grezzi. I programmatori C ++ dovrebbero invece utilizzare le classi contenitore, ad eccezione della programmazione embedded / OS. Leggere questo per motivi relativi ai contenitori utente. parashift.com/c++-faq-lite/containers.html
jkeys

8
Tenere presente che i vettori non eseguono necessariamente il controllo della portata utilizzando []. L'uso di .at () fa la stessa cosa di [] ma fa il controllo della portata.
David Thornley,

4
A vector non si ridimensiona automaticamente quando si accede a elementi fuori limite! È solo UB!
Pavel Minaev,

Risposte:


364

Benvenuto nel migliore amico di ogni programmatore C / C ++: comportamento indefinito .

C'è molto che non è specificato dallo standard linguistico, per una serie di ragioni. Questo è uno di loro.

In generale, ogni volta che si verifica un comportamento indefinito, qualsiasi cosa potrebbe accadere. L'applicazione potrebbe bloccarsi, potrebbe bloccarsi, espellere l'unità CD-ROM o far uscire i demoni dal naso. Potrebbe formattare il tuo hard disk o inviare via email tutto il tuo porno a tua nonna.

Può anche sembrare , se sei davvero sfortunato, funzionare correttamente.

Il linguaggio dice semplicemente cosa dovrebbe accadere se si accede agli elementi entro i limiti di un array. Non è definito cosa succede se si esce dai limiti. Potrebbe sembrare che funzioni oggi sul tuo compilatore, ma non è C o C ++ legale e non vi è alcuna garanzia che funzionerà ancora la prossima volta che avvierai il programma. O che ha i dati essenziali non sovrascritti anche ora, e basta non hanno incontrato dei problemi, che si sta andando a causa - ancora.

Per quanto riguarda il motivo per cui non esiste un controllo dei limiti, ci sono un paio di aspetti della risposta:

  • Un array è un residuo di C. C array sono più primitivi che puoi ottenere. Solo una sequenza di elementi con indirizzi contigui. Non ci sono limiti perché sta semplicemente esponendo la memoria grezza. L'attuazione di un solido meccanismo di controllo dei limiti sarebbe stata quasi impossibile in C.
  • In C ++, il controllo dei limiti è possibile sui tipi di classe. Ma un array è ancora il semplice vecchio C-compatibile. Non è una classe. Inoltre, C ++ si basa anche su un'altra regola che rende il controllo dei limiti non ideale. Il principio guida C ++ è "non si paga per ciò che non si utilizza". Se il codice è corretto, non è necessario il controllo dei limiti e non si dovrebbe essere obbligati a pagare l'overhead del controllo dei limiti di runtime.
  • Quindi C ++ offre il std::vectormodello di classe, che consente entrambi. operator[]è progettato per essere efficiente. Lo standard linguistico non richiede che esegua il controllo dei limiti (anche se non lo proibisce neanche). Un vettore ha anche la at()funzione membro che è garantita per eseguire il controllo dei limiti. Quindi in C ++, ottieni il meglio da entrambi i mondi se usi un vettore. Ottieni prestazioni simili a array senza controllo dei limiti e hai la possibilità di utilizzare l'accesso controllato per limiti quando lo desideri.

5
@Jaif: stiamo usando questa cosa array da così tanto tempo, ma ancora perché non ci sono test per verificare un errore così semplice?
seg.server.fault,

7
Il principio di progettazione C ++ era che non dovrebbe essere più lento dell'equivalente codice C e C non esegue il controllo del limite di array. Il principio di progettazione C era sostanzialmente la velocità in quanto era finalizzato alla programmazione del sistema. Il controllo del limite di array richiede tempo e quindi non viene eseguito. Per la maggior parte degli usi in C ++, dovresti usare comunque un contenitore piuttosto che un array e puoi scegliere il controllo associato o nessun controllo associato accedendo a un elemento tramite .at () o [] rispettivamente.
KTC,

4
@seg Tale controllo costa qualcosa. Se scrivi il codice corretto, non vuoi pagare quel prezzo. Detto questo, sono diventato un completo convertito al metodo at () di std :: vector, che è stato verificato. Usarlo ha esposto alcuni errori in quello che pensavo fosse il codice "corretto".

10
Credo che le vecchie versioni di GCC abbiano effettivamente lanciato Emacs e una simulazione di Towers of Hanoi, quando ha riscontrato alcuni tipi di comportamento indefinito. Come ho detto, tutto può succedere. ;)
jalf

4
Tutto è già stato detto, quindi questo merita solo un piccolo addendum. Le build di debug possono essere molto tolleranti in queste circostanze rispetto alle build di rilascio. A causa dell'inclusione delle informazioni di debug nei binari di debug, c'è meno possibilità che qualcosa di vitale venga sovrascritto. Questo è talvolta il motivo per cui le build di debug sembrano funzionare correttamente mentre il build build di rilascio si arresta in modo anomalo.
Ricco

31

Utilizzando g ++, è possibile aggiungere l'opzione riga di comando: -fstack-protector-all.

Nel tuo esempio ha provocato quanto segue:

> g++ -o t -fstack-protector-all t.cc
> ./t
3
4
/bin/bash: line 1: 15450 Segmentation fault      ./t

Non ti aiuta davvero a trovare o risolvere il problema, ma almeno il segfault ti farà sapere che qualcosa non va.


10
Ho appena trovato un'opzione migliore: -fmudflap
Hi-Angel,

1
@ Hi-Angel: l'equivalente moderno è -fsanitize=addressche rileva questo bug sia in fase di compilazione (se ottimizzazione) che in fase di esecuzione.
Nate Eldredge,

@NateEldredge +1, al giorno d'oggi uso persino -fsanitize=undefined,address. Ma vale la pena notare che ci sono rari casi angolari con la libreria std, quando l'accesso non viene rilevato dal disinfettante . Per questo motivo, consiglierei di utilizzare l' -D_GLIBCXX_DEBUGopzione, che aggiunge ancora più controlli.
Ciao Angelo,

12

g ++ non controlla i limiti dell'array e potresti sovrascrivere qualcosa con 3,4 ma nulla di veramente importante, se provi con numeri più alti otterrai un crash.

Stai solo sovrascrivendo parti dello stack che non vengono utilizzate, potresti continuare fino a raggiungere la fine dello spazio allocato per lo stack e si bloccherebbe alla fine

EDIT: Non hai modo di affrontarlo, forse un analizzatore di codice statico potrebbe rivelare quei guasti, ma è troppo semplice, potresti avere guasti simili (ma più complessi) non rilevati anche per gli analizzatori statici


6
Dove prendi se da quello all'indirizzo array [3] e array [4], non c'è "niente di veramente importante" ??
namezero,

7

Per quanto ne so, è un comportamento indefinito. Esegui un programma più grande con quello e si bloccherà da qualche parte lungo la strada. Il controllo dei limiti non fa parte degli array grezzi (o anche di std :: vector).

Usa invece std :: vector con std::vector::iterator's in modo da non doverti preoccupare.

Modificare:

Solo per divertimento, esegui questo e vedi quanto tempo ci vorrà:

int main()
{
   int array[1];

   for (int i = 0; i != 100000; i++)
   {
       array[i] = i;
   }

   return 0; //will be lucky to ever reach this
}

Edit2:

Non farlo.

Edit3:

OK, ecco una breve lezione sugli array e le loro relazioni con i puntatori:

Quando si utilizza l'indicizzazione di array, si utilizza realmente un puntatore sotto mentite spoglie (chiamato "riferimento"), che viene automaticamente distaccato. Questo è il motivo per cui invece di * (array [1]), array [1] restituisce automaticamente il valore a quel valore.

Quando si dispone di un puntatore a un array, in questo modo:

int array[5];
int *ptr = array;

Quindi la "matrice" nella seconda dichiarazione sta realmente decadendo in un puntatore alla prima matrice. Questo è un comportamento equivalente a questo:

int *ptr = &array[0];

Quando si tenta di accedere oltre ciò che è stato allocato, si sta semplicemente utilizzando un puntatore ad altra memoria (di cui C ++ non si lamenterà). Prendendo il mio programma di esempio sopra, ciò equivale a questo:

int main()
{
   int array[1];
   int *ptr = array;

   for (int i = 0; i != 100000; i++, ptr++)
   {
       *ptr++ = i;
   }

   return 0; //will be lucky to ever reach this
}

Il compilatore non si lamenterà perché nella programmazione, spesso devi comunicare con altri programmi, in particolare il sistema operativo. Questo viene fatto con un po 'di puntatori.


3
Penso che tu abbia dimenticato di incrementare "ptr" nel tuo ultimo esempio lì. Hai accidentalmente prodotto un codice ben definito.
Jeff Lake,

1
Haha, vedi perché non dovresti usare array grezzi?
jkeys,

"Questo è il motivo per cui invece di * (array [1]), array [1] restituisce automaticamente il valore a quel valore." Sei sicuro * (array [1]) funzionerà correttamente? Penso che dovrebbe essere * (array + 1). ps: Lol, è come inviare un messaggio al passato. Ma comunque:
muyustan il

5

Suggerimento

Se si desidera disporre di array di dimensioni di vincolo veloci con controllo degli errori di intervallo, provare a utilizzare boost :: array , (anche std :: tr1 :: array da <tr1/array>esso sarà un contenitore standard nella prossima specifica C ++). È molto più veloce di std :: vector. Riserva la memoria sull'heap o all'interno dell'istanza della classe, proprio come int array [].
Questo è un semplice codice di esempio:

#include <iostream>
#include <boost/array.hpp>
int main()
{
    boost::array<int,2> array;
    array.at(0) = 1; // checking index is inside range
    array[1] = 2;    // no error check, as fast as int array[2];
    try
    {
       // index is inside range
       std::cout << "array.at(0) = " << array.at(0) << std::endl;

       // index is outside range, throwing exception
       std::cout << "array.at(2) = " << array.at(2) << std::endl; 

       // never comes here
       std::cout << "array.at(1) = " << array.at(1) << std::endl;  
    }
    catch(const std::out_of_range& r)
    {
        std::cout << "Something goes wrong: " << r.what() << std::endl;
    }
    return 0;
}

Questo programma stamperà:

array.at(0) = 1
Something goes wrong: array<>: index out of range

4

C o C ++ non controlleranno i limiti di accesso ad un array.

Stai allocando l'array nello stack. L'indicizzazione dell'array tramite array[3]equivale a * (array + 3), dove array è un puntatore a & array [0]. Ciò comporterà un comportamento indefinito.

Un modo per catturarlo a volte in C è usare un controllo statico, come una stecca . Se corri:

splint +bounds array.c

sopra,

int main(void)
{
    int array[1];

    array[1] = 1;

    return 0;
}

allora riceverai l'avvertimento:

array.c: (nella funzione principale) array.c: 5: 9: archivio probabilmente fuori limite: array [1] Impossibile risolvere il vincolo: richiede 0> = 1 necessario per soddisfare il requisito preliminare: richiede maxSet (array @ array .c: 5: 9)> = 1 Una scrittura in memoria può scrivere su un indirizzo oltre il buffer allocato.


Correzione: è già stata allocata dal sistema operativo o da un altro programma. Sta sovrascrivendo altra memoria.
jkeys,

1
Dire che "C / C ++ non controllerà i limiti" non è del tutto corretto - non c'è nulla che precluda l'esecuzione di una particolare implementazione conforme, sia per impostazione predefinita, sia con alcuni flag di compilazione. È solo che nessuno di loro si preoccupa.
Pavel Minaev,

3

Stai certamente sovrascrivendo il tuo stack, ma il programma è abbastanza semplice che gli effetti di questo passano inosservati.


2
Se lo stack viene sovrascritto o meno dipende dalla piattaforma.
Chris Cleeland,

3

Esegui questo attraverso Valgrind e potresti vedere un errore.

Come ha sottolineato Falaina, valgrind non rileva molti casi di corruzione dello stack. Ho appena provato il campione sotto valgrind, e in effetti riporta zero errori. Tuttavia, Valgrind può essere determinante nel trovare molti altri tipi di problemi di memoria, in questo caso non è particolarmente utile a meno che non modifichi il bulid per includere l'opzione --stack-check. Se si crea ed esegue l'esempio come

g++ --stack-check -W -Wall errorRange.cpp -o errorRange
valgrind ./errorRange

valgrind sarà un errore.


2
In realtà, Valgrind è abbastanza scarso nel determinare gli accessi errati dell'array nello stack. (e giustamente, il meglio che può fare è contrassegnare l'intero stack come una posizione di scrittura valida)
Falaina,

@Falaina - buon punto, ma Valgrind è in grado di rilevare almeno alcuni errori dello stack.
Todd Stout,

E valgrind non vedrà nulla di sbagliato nel codice perché il compilatore è abbastanza intelligente da ottimizzare l'array e semplicemente emettere 3 e 4 letterali. Quell'ottimizzazione avviene prima che gcc controlli i limiti dell'array, motivo per cui l'avviso di fuori campo gcc lo fa non è mostrato.
Goswin von Brederlow,

2

Comportamento indefinito che lavora a tuo favore. Qualunque sia il ricordo che stai ostruendo apparentemente non tiene nulla di importante. Si noti che C e C ++ non eseguono il controllo dei limiti sugli array, quindi roba del genere non verrà catturata in fase di compilazione o di esecuzione.


5
No, il comportamento indefinito "funziona a tuo favore" quando si blocca in modo pulito. Quando sembra funzionare, si tratta dello scenario peggiore possibile.
jalf

@JohnBode: Quindi sarebbe meglio se correggessi il testo secondo il commento di jalf
Distruttore

1

Quando si inizializza l'array con int array[2], viene allocato spazio per 2 numeri interi; ma l'identificatore arrayindica semplicemente l'inizio di quello spazio. Quando poi accedi array[3]e array[4], il compilatore semplicemente incrementa quell'indirizzo per indicare dove sarebbero quei valori, se l'array fosse abbastanza lungo; prova ad accedere a qualcosa di simile array[42]senza prima inizializzarlo, finirai per ottenere qualsiasi valore che fosse già in memoria in quella posizione.

Modificare:

Maggiori informazioni su puntatori / matrici: http://home.netcom.com/~tjensen/ptr/pointers.htm


0

quando si dichiara int array [2]; riservi 2 spazi di memoria di 4 byte ciascuno (programma a 32 bit). se digiti array [4] nel tuo codice corrisponde comunque a una chiamata valida ma solo in fase di esecuzione genererà un'eccezione non gestita. C ++ utilizza la gestione manuale della memoria. Questo è in realtà un difetto di sicurezza che è stato utilizzato per i programmi di hacking

questo può aiutare a capire:

int * somepointer;

somepointer [0] = somepointer [5];


0

A quanto ho capito, le variabili locali sono allocate nello stack, quindi uscire dai limiti sul proprio stack può solo sovrascrivere qualche altra variabile locale, a meno che non diventi troppo grande e superi le dimensioni dello stack. Dal momento che non hai altre variabili dichiarate nella tua funzione, non provoca effetti collaterali. Prova a dichiarare un'altra variabile / matrice subito dopo la tua prima e vedi cosa accadrà con essa.


0

Quando scrivi "array [indice]" in C, lo traduce in istruzioni macchina.

La traduzione è simile a:

  1. 'ottieni l'indirizzo dell'array'
  2. 'ottieni la dimensione del tipo di oggetti di cui è composta la matrice'
  3. 'moltiplica la dimensione del tipo per indice'
  4. 'aggiungi il risultato all'indirizzo dell'array'
  5. 'leggi cosa c'è nell'indirizzo risultante'

Il risultato è indirizzato a qualcosa che può o meno essere parte dell'array. In cambio della velocità incredibile delle istruzioni della macchina, perdi la rete di sicurezza del computer che controlla le cose per te. Se sei meticoloso e attento non è un problema. Se sei sciatto o fai un errore, ti bruci. A volte potrebbe generare un'istruzione non valida che causa un'eccezione, a volte no.


0

Un buon approccio che ho visto spesso e che in realtà ero stato usato è quello di iniettare un elemento di tipo NULL (o uno creato, come uint THIS_IS_INFINITY = 82862863263;) alla fine dell'array.

Quindi al controllo delle condizioni del loop, TYPE *pagesWordsc'è una sorta di array di puntatori:

int pagesWordsLength = sizeof(pagesWords) / sizeof(pagesWords[0]);

realloc (pagesWords, sizeof(pagesWords[0]) * (pagesWordsLength + 1);

pagesWords[pagesWordsLength] = MY_NULL;

for (uint i = 0; i < 1000; i++)
{
  if (pagesWords[i] == MY_NULL)
  {
    break;
  }
}

Questa soluzione non esprimerà se l'array è pieno di structtipi.


0

Come accennato ora nella domanda, usare std :: vector :: at risolverà il problema ed effettuerà un controllo associato prima di accedere.

Se è necessario un array di dimensioni costanti che si trova nello stack come primo codice, utilizzare il nuovo contenitore C ++ 11 std :: array; come vettore c'è std :: array :: at function. In effetti la funzione esiste in tutti i contenitori standard in cui ha un significato, ovvero dove è definito l'operatore [] :( deque, map, unordered_map) ad eccezione di std :: bitset in cui è chiamato std :: bitset: :test.


0

libstdc ++, che fa parte di gcc, ha una speciale modalità di debug per il controllo degli errori. È abilitato dal flag del compilatore -D_GLIBCXX_DEBUG. Tra le altre cose, limita la verifica std::vectora spese delle prestazioni. Ecco la demo online con la versione recente di gcc.

Quindi in realtà puoi fare il controllo dei limiti con la modalità debug di libstdc ++ ma dovresti farlo solo durante il test perché costa prestazioni notevoli rispetto alla normale modalità libstdc ++.


0

Se cambi leggermente il programma:

#include <iostream>
using namespace std;
int main()
{
    int array[2];
    INT NOTHING;
    CHAR FOO[4];
    STRCPY(FOO, "BAR");
    array[0] = 1;
    array[1] = 2;
    array[3] = 3;
    array[4] = 4;
    cout << array[3] << endl;
    cout << array[4] << endl;
    COUT << FOO << ENDL;
    return 0;
}

(Cambiamenti in maiuscolo - metti quelli in minuscolo se hai intenzione di provare questo.)

Vedrai che la variabile foo è stata eliminata. Il tuo codice sarà memorizzare i valori nella matrice inesistente [3] e la matrice [4], ed essere in grado di recuperarli correttamente, ma la memorizzazione effettivamente utilizzato sarà da foo .

Quindi puoi "cavartela" superando i limiti dell'array nell'esempio originale, ma a costo di causare danni altrove - danni che possono rivelarsi molto difficili da diagnosticare.

Per quanto riguarda il motivo per cui non esiste un controllo automatico dei limiti: un programma correttamente scritto non ne ha bisogno. Una volta che ciò è stato fatto, non vi è alcun motivo per cui il controllo e il controllo dei limiti di runtime rallentano il programma. Meglio capire tutto durante la progettazione e la codifica.

C ++ si basa su C, che è stato progettato per essere il più vicino possibile al linguaggio assembly.

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.