Accéder à un tableau hors limites ne donne aucune erreur, pourquoi?
J'attribue des valeurs dans un programme c++ hors des limites comme ceci:
#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;
}
Le programme imprime 3
et 4
. Il ne devrait pas être possible. J'utilise g++ 4.3.3
Voici la commande compiler et exécuter
$ g++ -W -Wall errorRange.cpp -o errorRange
$ ./errorRange
3
4
Ce n'est que lors de l'affectation de array[3000]=3000
que cela me donne un défaut de segmentation.
Si gcc ne vérifie pas les limites du tableau, Comment puis-je être sûr que mon programme est correct, car cela peut entraîner des problèmes sérieux plus tard?
J'ai remplacé le code ci-dessus avec
vector<int> vint(2);
vint[0] = 0;
vint[1] = 1;
vint[2] = 2;
vint[5] = 5;
cout << vint[2] << endl;
cout << vint[5] << endl;
Et celui-ci ne produit également aucune erreur.
17 réponses
Bienvenue à chaque meilleur ami du programmeur C / C++: comportement indéfini .
Il y a beaucoup de choses qui ne sont pas spécifiées par la norme de langue, pour diverses raisons. C'est l'un d'entre eux.
En général, chaque fois que vous rencontrez un comportement indéfini, tout pourrait arriver. L'application peut se bloquer, il peut geler, il peut éjecter votre lecteur de CD-ROM ou faire sortir des démons de votre nez. Il peut formater votre disque dur ou envoyer tout votre porno à votre la grand-mère.
, Il peut même, si vous êtes vraiment malchanceux, apparaissent pour fonctionner correctement.
Le langage dit simplement ce qui devrait se passer si vous accédez aux éléments dans les limites d'un tableau. Il est laissé indéfini ce qui se passe si vous sortez des limites. Il pourrait sembler fonctionner aujourd'hui, sur votre compilateur, mais ce N'est pas légal C ou C++, et il n'y a aucune garantie que cela fonctionnera toujours la prochaine fois que vous exécuterez le programme. Ou qu'il n'a pas écrasé essentiel données même maintenant, et vous n'avez tout simplement pas rencontré les problèmes, qu'ilest va causer-encore.
Quant à pourquoi Il n'y a pas de vérification des limites, il y a quelques aspects à la réponse:
- Un tableau est un reste de C. C les tableaux sont à peu près aussi primitifs que vous pouvez obtenir. Juste une séquence d'éléments contigus adresses. Il n'y a pas de vérification des limites car il expose simplement la mémoire brute. La mise en œuvre d'un mécanisme de vérification des limites robuste aurait été presque impossible en C.
- en C++, la vérification des limites est possible sur les types de classes. Mais un tableau est toujours l'ancien compatible c. Ce n'est pas une classe. De plus, C++ est également construit sur une autre règle qui rend la vérification des limites non idéale. Le principe directeur C++ est "vous ne payez pas pour ce que vous n'utilisez pas". Si votre code est correct, vous n'avez pas besoin de vérification des limites, et vous ne devriez pas être obligé de payer pour la surcharge de la vérification des limites d'exécution.
- donc C++ offre la classe
std::vector
modèle, qui permet à la fois.operator[]
est conçu pour être efficace. La norme de langue n'exige pas qu'elle effectue la vérification des limites (bien qu'elle ne l'interdise pas non plus). Un vecteur a également la fonction membreat()
Qui est garantie pour effectuer une vérification des limites. Donc, en C++, vous obtenez le meilleur des deux mondes si vous utilisez un vecteur. Vous obtenez des performances de type tableau sans vérification des limites, et Vous avez la possibilité d'utiliser l'accès vérifié par limites lorsque vous le souhaitez.
En utilisant g++, vous pouvez ajouter l'option de ligne de commande: -fstack-protector-all
.
Sur votre exemple, il en a résulté ce qui suit:
> g++ -o t -fstack-protector-all t.cc
> ./t
3
4
/bin/bash: line 1: 15450 Segmentation fault ./t
Cela ne vous aide pas vraiment à trouver ou à résoudre le problème, mais au moins le segfault vous fera savoir que quelque chose est faux.
G++ ne vérifie pas les limites du tableau, et vous pouvez écraser quelque chose avec 3,4 mais rien de vraiment important, si vous essayez avec des nombres plus élevés, vous obtiendrez un crash.
Vous écrasez simplement des parties de la pile qui ne sont pas utilisées, vous pouvez continuer jusqu'à ce que vous atteigniez la fin de l'espace alloué pour la pile et il finirait par planter
Modifier: Vous n'avez aucun moyen de faire face à cela, peut-être qu'un analyseur de code statique pourrait révéler ces échecs, mais c'est trop simple, vous pouvez avoir défaillances similaires(mais plus complexes) non détectées même pour les analyseurs statiques
C'est un comportement indéfini pour autant que je sache. Exécutez un programme plus grand avec cela et il va planter quelque part en cours de route. La vérification des limites ne fait pas partie des tableaux bruts (ou même std::vector).
Utilisez std:: vector avec std::vector::iterator
à la place afin que vous n'ayez pas à vous en soucier.
Modifier:
Juste pour le plaisir, exécutez ceci et voyez combien de temps jusqu'à ce que vous plantiez:
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:
Ne fais pas ça.
Edit3:
OK, voici une leçon rapide sur les tableaux et leur relations avec les pointeurs:
Lorsque vous utilisez l'indexation de tableau, vous utilisez vraiment un pointeur déguisé (appelé "référence"), qui est automatiquement déréférencé. C'est pourquoi, au lieu de *(tableau[1]), tableau[1] renvoie automatiquement la valeur à cette valeur.
Lorsque vous avez un pointeur vers un tableau, comme ceci:
int array[5];
int *ptr = array;
Alors le "tableau" dans la deuxième déclaration se décompose vraiment en un pointeur vers le premier tableau. C'est un comportement équivalent à ceci:
int *ptr = &array[0];
Quand vous essayez d'accéder au-delà de ce que vous avez alloué, vous utilisez simplement un pointeur vers une autre mémoire (dont C++ ne se plaindra pas). En prenant mon exemple de programme ci-dessus, c'est équivalent à ceci:
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
}
Le compilateur ne se plaindra pas car en programmation, vous devez souvent communiquer avec d'autres programmes, en particulier le système d'exploitation. Ceci est fait avec des pointeurs un peu.
Indice
Si vous voulez avoir des tableaux de taille de contrainte rapide avec vérification d'erreur de plage, essayez d'utiliser boost::array, (aussi std::tr1::array de <tr1/array>
ce sera un conteneur standard dans la prochaine spécification C++). C'est beaucoup plus rapide que std::vector. Il réserve de la mémoire sur le tas ou à l'intérieur de l'instance de classe, tout comme int array[].
C'est un exemple de code simple:
#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;
}
Ce programme imprimera:
array.at(0) = 1
Something goes wrong: array<>: index out of range
Vous écrasez certainement votre pile, mais le programme est assez simple pour que les effets de ceci passent inaperçus.
C ou C++ ne vérifiera pas les limites d'un accès au tableau.
Vous allouez le tableau sur la pile. L'indexation du tableau via array[3]
est équivalente à *(array + 3)
, où array est un pointeur vers & array[0]. Cela entraînera un comportement indéfini.
Un moyen de rattraper cette , parfois, dans C, c'est d'utiliser un vérificateur statique, comme attelle. Si vous exécutez:
splint +bounds array.c
Sur,
int main(void)
{
int array[1];
array[1] = 1;
return 0;
}
Alors vous obtiendrez l'avertissement:
Tableau.c: (en fonction principale) tableau.c: 5: 9: probablement hors limites stocker: tableau[1] Impossible de résoudre la contrainte: nécessite 0 >= 1 nécessaire pour satisfaire la condition: nécessite maxSet(tableau @ tableau.c: 5: 9) > = 1 une écriture de mémoire peut écrire à une adresse au-delà de la tampon alloué.
Comportement indéfini qui fonctionne en votre faveur. Quelle que soit la mémoire que vous vous tapez apparemment n'a rien d'important. Notez que C et c++ ne font pas de vérification des limites sur les tableaux, donc des choses comme ça ne vont pas être interceptées à la compilation ou à l'exécution.
Exécutez ceci via Valgrind et vous pourriez voir une erreur.
Comme Falaina l'a souligné, valgrind ne détecte pas beaucoup d'instances de corruption de pile. Je viens d'essayer l'échantillon sous valgrind, et il ne signale en effet aucune erreur. Cependant, Valgrind peut être utile pour trouver de nombreux autres types de problèmes de mémoire, ce n'est tout simplement pas particulièrement utile dans ce cas, sauf si vous modifiez votre bulid pour inclure l'option --stack-check. Si vous construisez et exécutez l'échantillon comme
g++ --stack-check -W -Wall errorRange.cpp -o errorRange
valgrind ./errorRange
Valgrind sera signaler une erreur.
Lorsque vous initialisez le tableau avec int array[2]
, l'espace pour 2 entiers est alloué; mais l'identifiant array
pointe simplement vers le début de cet espace. Lorsque vous accédez ensuite à array[3]
et array[4]
, le compilateur incrémente simplement cette adresse pour pointer vers l'endroit où se trouveraient ces valeurs, si le tableau était assez long; essayez d'accéder à quelque chose comme array[42]
sans l'initialiser en premier, vous finirez par obtenir la valeur qui est déjà en mémoire emplacement.
Modifier:
Plus d'infos sur les pointeurs/tableaux: http://home.netcom.com/~tjensen/ptr/pointers.htm
Lorsque vous déclarez int array[2] ; Vous réservez 2 espaces mémoire de 4 octets chacun(programme 32 bits). si vous tapez array [4] dans votre code, il correspond toujours à un appel VALIDE mais ce n'est qu'au moment de l'exécution qu'il lancera une exception non gérée. C++ utilise la gestion manuelle de la mémoire. C'est en fait une faille de sécurité qui a été utilisée pour les programmes de piratage
Cela peut aider à comprendre:
Int * somepointer;
Somepointer[0]=somepointer[5];
Si je comprends bien, les variables locales sont allouées sur la pile, donc sortir des limites sur votre propre pile ne peut remplacer qu'une autre variable locale, sauf si vous allez trop oob et dépassez la taille de votre pile. Puisque vous n'avez pas d'autres variables déclarées dans votre fonction, cela ne provoque aucun effet secondaire. Essayez de déclarer une autre variable/tableau juste après votre premier et voyez ce qui se passera avec elle.
Lorsque vous écrivez 'array [index]' en C, il le traduit en instructions machine.
La traduction va quelque chose comme:
- 'récupère l'adresse du tableau'
- 'obtenir la taille du type d'objets tableau est composé de"
- 'multiplier la taille du type par index'
- 'ajouter le résultat à l'adresse de tableau'
- 'lire ce qui se trouve à l'adresse résultante'
Le résultat adresse quelque chose qui peut, ou non, faire partie du tableau. Dans échange pour la vitesse fulgurante des instructions de la machine vous perdez le filet de sécurité de l'ordinateur vérifier les choses pour vous. Si vous êtes méticuleux et prudent, ce n'est pas un problème. Si vous êtes bâclé ou faites une erreur, vous êtes brûlé. Parfois, il peut générer une instruction invalide qui provoque une exception, parfois pas.
Une bonne approche que j'ai souvent vue et que j'avais utilisée est d'injecter un élément de type nul (ou un élément créé, comme uint THIS_IS_INFINITY = 82862863263;
) à la fin du tableau.
Ensuite, à la vérification de la condition de la boucle, TYPE *pagesWords
est une sorte de tableau de pointeurs:
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;
}
}
Cette solution ne sera pas word si array est rempli avec struct
types.
Comme mentionné maintenant dans la question en utilisant std:: vector:: at va résoudre le problème et faire une vérification liée avant d'accéder.
Si vous avez besoin d'un tableau de taille constante situé sur la pile en tant que premier code, utilisez le nouveau conteneur C++11 std::array; en tant que vecteur, il y a la fonction std::array::at. En fait, la fonction existe dans tous les conteneurs standard dans lesquels elle a une signification, c'est-à-dire où operator[] est défini : (deque, map, unordered_map) à l'exception de std:: bitset dans lequel elle est appelé std::bitset::test.
Libstdc++, qui fait partie de gcc, a un mode de débogage spécial pour la vérification des erreurs. Il est activé par l'indicateur du compilateur -D_GLIBCXX_DEBUG
. Entre autres choses, il vérifie les limites de std::vector
au prix de la performance. Voici démo en ligne avec la version récente de gcc.
Donc, en fait, vous pouvez faire une vérification des limites avec le mode de débogage libstdc++ mais vous ne devriez le faire que lors des tests car cela coûte des performances notables par rapport au mode libstdc++ normal.
Si vous modifiez légèrement votre programme:
#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;
}
(Changements en majuscules-mettez-les en minuscules Si vous allez essayer ceci.)
, Vous verrez que la variable foo a été saccagé. Votre code stockera les valeurs dans le tableau inexistant[3] et le tableau [4], et pourra les récupérer correctement, mais le stockage réel utilisé sera de foo .
Donc, vous pouvez" vous en sortir " en dépassant les limites du tableau dans votre exemple original, mais à le coût de causer des dommages ailleurs-des dommages qui peuvent s'avérer très difficiles à diagnostiquer.
Quant à la raison pour laquelle il n'y a pas de vérification automatique des limites-un programme correctement écrit n'en a pas besoin. Une fois que cela a été fait, il n'y a aucune raison de faire la vérification des limites d'exécution et cela ne ferait que ralentir le programme. Mieux pour obtenir que tout compris pendant la conception et le codage.
C++ est basé sur C, qui a été conçu pour être aussi proche du langage d'assemblage que possible.