J'ai récemment commencé à apprendre le C et je prends un cours avec C comme sujet. Je joue actuellement avec des boucles et je rencontre un comportement étrange que je ne sais pas expliquer.
#include <stdio.h>
int main()
{
int array[10],i;
for (i = 0; i <=10 ; i++)
{
array[i]=0; /*code should never terminate*/
printf("test \n");
}
printf("%d \n", sizeof(array)/sizeof(int));
return 0;
}
Sur mon ordinateur portable exécutant Ubuntu 14.04, ce code ne se casse pas. Il se termine. Sur l'ordinateur de mon école exécutant CentOS 6.6, il fonctionne également très bien. Sous Windows 8.1, la boucle ne se termine jamais.
Ce qui est encore plus étrange, c'est que lorsque je modifie l'état de la for
boucle en:, i <= 11
le code se termine uniquement sur mon ordinateur portable exécutant Ubuntu. Il ne se termine jamais dans CentOS et Windows.
Quelqu'un peut-il expliquer ce qui se passe dans la mémoire et pourquoi les différents systèmes d'exploitation exécutant le même code donnent des résultats différents?
EDIT: Je sais que la boucle for sort des limites. Je le fais intentionnellement. Je n'arrive pas à comprendre comment le comportement peut être différent selon les différents systèmes d'exploitation et ordinateurs.
i
est stockée juste après la fin dearray
, et vous la remplacez pararray[10]=0;
. Ce n'est peut-être pas le cas dans une version optimisée sur la même plate-forme, qui peut stockeri
dans un registre et ne jamais y faire référence en mémoire.Réponses:
Vous venez de découvrir un piétinement de mémoire. Vous pouvez en savoir plus à ce sujet ici: Qu'est-ce qu'un «coup de mémoire»?
Lorsque vous allouez
int array[10],i;
, ces variables entrent en mémoire (en particulier, elles sont allouées sur la pile, qui est un bloc de mémoire associé à la fonction).array[]
eti
sont probablement adjacents les uns aux autres en mémoire. Il semble que sur Windows 8.1,i
se trouve àarray[10]
. Sur CentOS,i
se trouve àarray[11]
. Et sur Ubuntu, ce n'est à aucun endroit (peut-être que c'estarray[-1]
?).Essayez d'ajouter ces instructions de débogage à votre code. Vous devriez remarquer qu'à l'itération 10 ou 11,
array[i]
pointez suri
.la source
array[10]
détruire le cadre de la pile, par exemple. Comment peut-il y avoir une différence entre du code avec ou sans la sortie de débogage? Si l'adresse dei
n'est jamais nécessaire, le compilateur peut s'optimiseri
. dans un registre, changeant ainsi la disposition de la mémoire sur la pile ...array[10]=0
. Si vous avez compilé votre code avec optimisation, cela ne se produirait probablement pas. (Parce que C a des règles d'alias qui limitent les types d'accès à la mémoire qui doivent être supposés chevaucher potentiellement d'autres mémoires. En tant que variable locale dont vous ne prenez jamais l'adresse, je pense qu'un compilateur devrait être capable de supposer que rien ne l'alias. Quoi qu'il en soit, en écrivant la fin d'un tableau est un comportement indéfini. Essayez toujours de ne pas dépendre de cela.Le bogue se situe entre ces morceaux de code:
Puisqu'il
array
n'a que 10 éléments, dans la dernière itérationarray[10] = 0;
est un débordement de tampon. Les débordements de tampon sont UN COMPORTEMENT NON DÉFINI , ce qui signifie qu'ils peuvent formater votre disque dur ou faire sortir les démons de votre nez.Il est assez courant que toutes les variables de pile soient disposées les unes à côté des autres. Si
i
est situé là où lesarray[10]
écritures à, l'UB réinitialisenti
à0
, conduisant ainsi à la boucle non terminée.Pour corriger, changez la condition de boucle en
i < 10
.la source
rm -rf /
même lorsque vous n'êtes pas root, pas "formater" le lecteur entier bien sûr, mais toujours détruire toutes vos données. Aie.Dans ce qui devrait être la dernière exécution de la boucle, vous écrivez
array[10]
, mais il n'y a que 10 éléments dans le tableau, numérotés de 0 à 9. La spécification du langage C indique qu'il s'agit d'un «comportement non défini». En pratique, cela signifie que votre programme essaiera d'écrire dans laint
mémoire de la taille qui se trouve immédiatement aprèsarray
dans la mémoire. Ce qui se passe alors dépend de ce qui se trouve en fait, et cela dépend non seulement du système d'exploitation, mais plus encore du compilateur, des options du compilateur (telles que les paramètres d'optimisation), de l'architecture du processeur, du code environnant , etc. Cela peut même varier d'une exécution à l'autre, par exemple en raison de la randomisation de l'espace d'adressage (probablement pas sur cet exemple de jouet, mais cela se produit dans la vie réelle). Certaines possibilités incluent:i
. La boucle ne se termine jamais cari
redémarre à 0.array
trouve juste à la fin d'une page de mémoire virtuelle et que la page suivante n'est pas mappée.Ce que vous avez observé sous Windows, c'est que le compilateur a décidé de placer la variable
i
immédiatement après le tableau en mémoire, et a doncarray[10] = 0
fini par l'affecter ài
. Sur Ubuntu et CentOS, le compilateur ne s'yi
trouvait pas. Presque toutes les implémentations C regroupent des variables locales en mémoire, sur une pile mémoire , à une exception près: certaines variables locales peuvent être placées entièrement dans des registres . Même si la variable est sur la pile, l'ordre des variables est déterminé par le compilateur, et il peut dépendre non seulement de l'ordre dans le fichier source mais aussi de leurs types (pour éviter de gaspiller de la mémoire pour des contraintes d'alignement qui laisseraient des trous) , sur leurs noms, sur une valeur de hachage utilisée dans la structure de données interne d'un compilateur, etc.Si vous voulez savoir ce que votre compilateur a décidé de faire, vous pouvez lui dire de vous montrer le code assembleur. Oh, et apprenez à déchiffrer l'assembleur (c'est plus facile que de l'écrire). Avec GCC (et certains autres compilateurs, en particulier dans le monde Unix), passez l'option
-S
pour produire du code assembleur au lieu d'un binaire. Par exemple, voici l'extrait d'assembleur pour la boucle de compilation avec GCC sur amd64 avec l'option d'optimisation-O0
(pas d'optimisation), avec des commentaires ajoutés manuellement:Ici, la variable
i
est de 52 octets sous le haut de la pile, tandis que le tableau démarre 48 octets sous le haut de la pile. Il se trouve donc que ce compilateur s'est placéi
juste avant le tableau; vous écraseriezi
s'il vous arrivait d'écrirearray[-1]
. Si vous passezarray[i]=0
àarray[9-i]=0
, vous obtiendrez une boucle infinie sur cette plate-forme particulière avec ces options de compilation particulières.Maintenant compilons votre programme avec
gcc -O1
.C'est plus court! Le compilateur a non seulement refusé d'allouer un emplacement de pile pour
i
- il n'est jamais stocké dans le registreebx
- mais il n'a pas pris la peine d'allouer de la mémoirearray
ou de générer du code pour définir ses éléments, car il a remarqué qu'aucun des éléments sont jamais utilisés.Pour rendre cet exemple plus révélateur, assurons-nous que les affectations de tableau sont effectuées en fournissant au compilateur quelque chose qu'il n'est pas en mesure d'optimiser. Un moyen facile de le faire est d'utiliser le tableau à partir d'un autre fichier - en raison de la compilation séparée, le compilateur ne sait pas ce qui se passe dans un autre fichier (à moins qu'il n'optimise au moment du lien, lequel
gcc -O0
ougcc -O1
non). Créez un fichier sourceuse_array.c
contenantet changez votre code source en
Compiler avec
Cette fois, le code assembleur ressemble à ceci:
Maintenant, le tableau est sur la pile, à 44 octets du haut. Et alors
i
? Il n'apparaît nulle part! Mais le compteur de boucles est conservé dans le registrerbx
. Ce n'est pas exactementi
, mais l'adresse duarray[i]
. Le compilateur a décidé que puisque la valeur dei
n'a jamais été utilisée directement, il était inutile d'effectuer une arithmétique pour calculer où stocker 0 lors de chaque exécution de la boucle. Au lieu de cela, cette adresse est la variable de boucle, et l'arithmétique pour déterminer les limites a été effectuée en partie au moment de la compilation (multiplier 11 itérations par 4 octets par élément de tableau pour obtenir 44) et en partie au moment de l'exécution mais une fois pour toutes avant le début de la boucle ( effectuer une soustraction pour obtenir la valeur initiale).Même sur cet exemple très simple, nous avons vu comment changer les options du compilateur (activer l'optimisation) ou changer quelque chose de mineur (
array[i]
enarray[9-i]
) ou même changer quelque chose d'apparemment sans rapport (ajouter l'appel àuse_array
) peut faire une différence significative dans ce que le programme exécutable a généré par le compilateur. Les optimisations du compilateur peuvent faire beaucoup de choses qui peuvent sembler peu intuitives sur les programmes qui invoquent un comportement non défini . C'est pourquoi le comportement indéfini est laissé complètement indéfini. Lorsque vous vous écartez très légèrement des pistes, dans les programmes du monde réel, il peut être très difficile de comprendre la relation entre ce que fait le code et ce qu'il aurait dû faire, même pour les programmeurs expérimentés.la source
Contrairement à Java, C ne fait pas de vérification des limites du tableau, c'est-à-dire qu'il n'y en a pas
ArrayIndexOutOfBoundsException
, la tâche de s'assurer que l'index du tableau est valide est laissée au programmeur. Faire cela exprès conduit à un comportement indéfini, tout peut arriver.Pour un tableau:
les index ne sont valides que dans la plage
0
de9
. Cependant, vous essayez de:accédez
array[10]
ici, changez la condition eni < 10
la source
Vous avez une violation de limites, et sur les plates-formes non terminales, je pense que vous définissez par inadvertance la valeur
i
zéro à la fin de la boucle, afin qu'elle recommence.array[10]
est invalide; il contient 10 éléments, àarray[0]
traversarray[9]
, etarray[10]
est le 11e. Votre boucle doit être écrite pour s'arrêter avant10
, comme suit:Là où les
array[10]
terrains sont définis par l'implémentation, et de manière amusante, sur deux de vos plates-formes, ils atterrissent suri
lesquels ces plates-formes semblent apparemment directement aprèsarray
.i
est mis à zéro et la boucle continue indéfiniment. Pour vos autres plates-formes,i
peut être situé avantarray
ouarray
peut avoir un rembourrage après.la source
Vous déclarer
int array[10]
moyenarray
est d' indice0
à9
(total des10
éléments entiers qu'il peut contenir). Mais la boucle suivante,en boucle
0
à des10
moyens11
temps. Par conséquent, quandi = 10
il va déborder le tampon et provoquer un comportement indéfini .Essayez donc ceci:
ou,
la source
Il n'est pas défini à
array[10]
et donne un comportement non défini comme décrit précédemment. Pensez-y comme ça:J'ai 10 articles dans mon panier d'épicerie. Elles sont:
0: une boîte de céréales
1: pain
2: lait
3: tarte
4: œufs
5: gâteau
6: 2 litres de soda
7: salade
8: burgers
9: crème glacée
cart[10]
n'est pas défini et peut donner une exception hors limites dans certains compilateurs. Mais beaucoup ne le font apparemment pas. Le 11ème article apparent n'est pas réellement dans le panier. Le 11ème élément pointe vers ce que je vais appeler un "élément poltergeist". Elle n'a jamais existé, mais elle était là.La raison pour laquelle certains compilateurs donnent
i
un index dearray[10]
ouarray[11]
ou mêmearray[-1]
est à cause de votre instruction d'initialisation / déclaration. Certains compilateurs interprètent cela comme:int
s pourarray[10]
et un autreint
bloc. Pour le rendre plus facile, placez-les les uns à côté des autres."array[10]
cela ne pointe pasi
.i
àarray[-1]
(car un index d'un tableau ne peut pas, ou ne devrait pas être négatif), ou allouez-le à un endroit complètement différent parce que le système d'exploitation peut le gérer, et c'est plus sûr.Certains compilateurs veulent que les choses aillent plus vite, et certains compilateurs préfèrent la sécurité. Tout dépend du contexte. Si je développais une application pour l'ancien système d'exploitation BREW (le système d'exploitation d'un téléphone de base), par exemple, il ne se soucierait pas de la sécurité. Si je développais pour un iPhone 6, il pourrait fonctionner rapidement quoi qu'il arrive, donc j'aurais besoin de mettre l'accent sur la sécurité. (Sérieusement, avez-vous lu les directives de l'App Store d'Apple ou lu des informations sur le développement de Swift et Swift 2.0?)
la source
Puisque vous avez créé un tableau de taille 10, la condition de boucle doit être la suivante:
Actuellement, vous essayez d'accéder à l'emplacement non attribué à partir de la mémoire à l'aide
array[10]
et cela provoque le comportement non défini . Un comportement indéfini signifie que votre programme se comportera de façon indéterminée, il peut donc donner des sorties différentes à chaque exécution.la source
Eh bien, le compilateur C ne vérifie généralement pas les limites. Vous pouvez obtenir une erreur de segmentation au cas où vous vous référez à un emplacement qui n'appartient pas à votre processus. Cependant, les variables locales sont allouées sur la pile et selon la façon dont la mémoire est allouée, la zone juste au-delà du tableau (
array[10]
) peut appartenir au segment de mémoire du processus. Ainsi, aucun piège de faute de segmentation n'est lancé et c'est ce que vous semblez expérimenter. Comme d'autres l'ont souligné, il s'agit d'un comportement non défini en C et votre code peut être considéré comme erratique. Puisque vous apprenez le C, vous feriez mieux de prendre l'habitude de vérifier les limites de votre code.la source
Au-delà de la possibilité que la mémoire soit disposée de manière à ce qu'une tentative d'écriture
a[10]
écrase réellementi
, il serait également possible qu'un compilateur d'optimisation détermine que le test de boucle ne peut pas être atteint avec une valeuri
supérieure à dix sans que le code n'ait d'abord accédé au élément de tableau inexistanta[10]
.Étant donné qu'une tentative d'accès à cet élément serait un comportement indéfini, le compilateur n'aurait aucune obligation quant à ce que le programme pourrait faire après ce point. Plus précisément, puisque le compilateur n'aurait aucune obligation de générer du code pour vérifier l'index de boucle dans tous les cas où il pourrait être supérieur à dix, il n'aurait aucune obligation de générer du code pour le vérifier du tout; il pourrait plutôt supposer que le
<=10
test donnera toujours la valeur true. Notez que cela serait vrai même si le code le lisaita[10]
plutôt que de l'écrire.la source
Lorsque vous parcourez le passé,
i==9
vous attribuez zéro aux «éléments du tableau» qui se trouvent réellement au- delà du tableau , de sorte que vous écrasez d'autres données. Vous remplacez très probablement lai
variable, qui se trouve aprèsa[]
. De cette façon, vous remettezi
simplement la variable à zéro et redémarrez ainsi la boucle.Vous pourriez le découvrir vous-même si vous imprimiez
i
dans la boucle:au lieu de juste
Bien sûr, ce résultat dépend fortement de l'allocation de mémoire pour vos variables, qui à son tour dépend d'un compilateur et de ses paramètres, il s'agit donc généralement d'un comportement indéfini - c'est pourquoi les résultats sur différentes machines ou différents systèmes d'exploitation ou sur différents compilateurs peuvent différer.
la source
l'erreur est dans le tableau de portions [10] w / c est également l'adresse de i (tableau int [10], i;). lorsque le tableau [10] est défini sur 0, alors le i serait 0 w / c réinitialise la boucle entière et provoque la boucle infinie. il y aura une boucle infinie si le tableau [10] est compris entre 0 et 10. la boucle correcte devrait être pour (i = 0; i <10; i ++) {...} int tableau [10], i; pour (i = 0; i <= 10; i ++) tableau [i] = 0;
la source
Je vais suggérer quelque chose que je ne trouverai pas ci-dessus:
Essayez d'affecter un tableau [i] = 20;
Je suppose que cela devrait terminer le code partout .. (étant donné que vous gardez i <= 10 ou ll)
Si cela fonctionne, vous pouvez fermement décider que les réponses spécifiées ici sont déjà correctes [la réponse liée à la mémoire en écrasant une par exemple.]
la source
Il y a deux choses qui ne vont pas ici. L'int i est en fait un élément de tableau, tableau [10], comme on le voit sur la pile. Parce que vous avez permis à l'indexation de faire réellement tableau [10] = 0, l'index de boucle, i, ne dépassera jamais 10. Faites-le
for(i=0; i<10; i+=1)
.i ++ est, comme K&R l'appelle, un «mauvais style». Il incrémente i de la taille de i, pas 1. i ++ est pour les mathématiques du pointeur et i + = 1 pour l'algèbre. Bien que cela dépende du compilateur, ce n'est pas une bonne convention pour la portabilité.
la source
i
est un élément de tableau NOTana[10]
, il n'y a aucune obligation ni même suggestion pour un compilateur de le mettre sur la pile immédiatement aprèsa[]
- il peut aussi bien être localisé avant le tableau, ou séparé avec un espace supplémentaire. Il pourrait même être alloué en dehors de la mémoire principale, par exemple dans un registre CPU. C'est également faux++
pour les pointeurs et non pour les entiers. Tout à fait faux est «i ++ incrémente i de la taille de i» - lisez la description de l'opérateur dans la définition du langage!i
- elle est deint
type. C'est un entier , pas un pointeur; un nombre entier, utilisée comme un indice à laarray
,.