C Le programme se bloque lors de l’ajout d’un extra int

Je suis nouveau sur C et utilise Eclipse IDE Le code suivant fonctionne très bien:

#include  #include  #include  int main() { char *lineName; int stationNo; int i; while (scanf("%s (%d)", lineName, &stationNo)!=EOF) { for (i=0; i<5 ; i++ ){ printf("%d %d",i); } } return 0; } 

Consortingbution:

 Green (21) Red (38) 

Sortie:

 Green (21) Red (38) 0123401234 

Cependant, lorsque vous ajoutez simplement un nouvel int:

 #include  #include  #include  int main() { char *lineName; int stationNo; int i,b=0; while (scanf("%s (%d)", lineName, &stationNo)!=EOF) { printf("%d",b); for (i=0; i<5 ; i++ ){ printf("%d",i); } } return 0; } 

Le programme va planter avec la même entrée. Quelqu’un peut-il me dire pourquoi?

Vous avez dit que votre premier programme “fonctionne”, mais il ne fonctionne que par accident. On se croirait dans une voiture qui roule sur la route sans que les roues ne tiennent sur les roues avant, mais par miracle, elles ne sont pas encore tombées – pour le moment.

Tu as dit

 char *lineName; 

Cela vous donne une variable de pointeur qui peut pointer sur certains caractères, mais elle ne pointe pas encore . La valeur de ce pointeur n’est pas définie. C’est un peu comme dire ” int i ” et demander quelle est la valeur de i .

Ensuite vous avez dit

 scanf("%s (%d)", lineName, &stationNo) 

Vous demandez à scanf de lire un nom de ligne et de stocker la chaîne dans la mémoire lineName par lineName . Mais où est ce souvenir? Nous n’avons aucune idée de ce que c’est!

La situation avec les pointeurs non initialisés est un peu plus délicate à penser car, comme toujours, avec les pointeurs, nous devons faire la distinction entre la valeur du pointeur et les données de la mémoire pointées par le pointeur . J’ai mentionné plus tôt en disant int i et en demandant quelle est la valeur de i . Maintenant, il y aura un motif de bits dans i – cela peut être 0, ou 1, ou -23, ou 8675309.

De la même façon, il y aura un motif de bits dans lineName – cela pourrait “pointer vers” l’emplacement de mémoire 0x00000000, ou 0xffe01234, ou 0xdeadbeef. Mais ensuite, les questions sont les suivantes: y a-t-il réellement de la mémoire à cet emplacement, et avons-nous la permission d’écrire dessus, et est-il utilisé pour autre chose? S’il y a de la mémoire et que nous avons la permission et que nous ne l’utilisons pas pour autre chose, le programme peut sembler fonctionner – pour le moment. Mais ce sont trois très gros si! Si la mémoire n’existe pas ou si nous n’avons pas la permission d’y écrire, le programme va probablement se bloquer quand il essaiera. Et si la mémoire est utilisée pour autre chose, quelque chose va se passer lorsque nous demandons à scanf d’écrire une chaîne à cet endroit.

Et vraiment, si ce qui nous intéresse, c’est d’écrire des programmes qui fonctionnent (et qui fonctionnent pour les bonnes raisons), nous n’avons à poser aucune de ces questions. Nous n’avons pas à demander où lineName pointe quand nous ne l’initialisons pas, ou s’il y a de la mémoire, ou si nous avons l’autorisation d’écrire ou si elle est utilisée pour autre chose. Au lieu de cela, nous devrions simplement initialiser lineName ! Nous devrions explicitement indiquer à la mémoire que nous possédons et à laquelle nous avons le droit d’écrire et que nous n’utilisons rien d’autre!

Il y a plusieurs moyens de le faire. Le plus simple est d’utiliser un tableau pour lineName , pas un pointeur:

 char lineName[20]; 

Ou, si nous avons le cœur décidé à utiliser un pointeur, nous pouvons appeler malloc :

 char *lineName = malloc(20); 

Cependant, si nous faisons cela, nous devons vérifier que malloc réussi:

 if(lineName == NULL) { fprintf(stderr, "out of memory!\n"); exit(1); } 

Si vous apportez l’une ou l’autre de ces modifications, votre programme fonctionnera.

… Eh bien, en fait, nous sums toujours dans une situation où votre programme semblera fonctionner, même s’il a encore un autre problème assez grave. Nous avons alloué 20 caractères à lineName , ce qui nous donne 19 caractères réels, plus le '\0' lineName . Mais nous ne soaps pas ce que l’utilisateur va taper. Que se passe-t-il si l’utilisateur tape 20 caractères ou plus? Cela entraînera le scanf de 20 caractères ou plus dans lineName par lineName , au-delà de la fin de ce lineName la mémoire de lineName est autorisée à conserver, et nous sums de nouveau en train d’écrire dans la mémoire que nous ne possédons pas et qui pourrait être en usage pour autre chose.

Une solution consiste à agrandir lineName – déclarez-le sous la forme char lineName[100] ou appelez malloc(100) . Mais cela ne fait que déplacer le problème – nous devons maintenant nous préoccuper de la possibilité (peut-être moindre) que l’utilisateur tape 100 caractères ou plus. La prochaine chose à faire est donc de dire à scanf ne pas écrire plus de lineName dans lineName que lineName que nous avons lineName . C’est en fait assez simple: si lineName est toujours configuré pour contenir 20 caractères, appelez simplement

 scanf("%19s (%d)", lineName, &stationNo) 

Ce spécificateur de format, %19s indique à scanf qu’il est uniquement autorisé à lire et à stocker une chaîne de 19 caractères au maximum.


Maintenant, j’ai beaucoup parlé ici, mais je me rends compte que je n’ai pas vraiment réussi à répondre à la question de savoir pourquoi votre programme est passé de vrille à un plantage alors que vous avez apporté ce changement apparemment sortingvial, apparemment sans rapport. Cela finit par être une question difficile à répondre de manière satisfaisante. Pour revenir à l’analogie avec laquelle j’ai commencé, c’est comme demander pourquoi vous pouviez conduire la voiture sans noix au magasin sans problème, mais lorsque vous avez essayé de vous rendre chez ma grand-mère, les roues sont tombées et vous vous êtes écrasé dans un fossé. . Il y a un million de facteurs possibles qui ont pu entrer en jeu, mais aucun d’eux ne change le fait que conduire une voiture avec les roues non attachées est une idée folle, cela ne garantit pas du tout de fonctionner.

Dans votre cas, les variables dont vous parlez – lineName , stationNo , i , puis b – sont toutes des variables locales, généralement allouées sur la stack. Maintenant, l’une des caractéristiques de la stack est qu’elle s’utilise pour toutes sortes de choses et qu’elle n’est jamais effacée entre les utilisations. Ainsi, si vous avez une variable locale non initialisée, les bits aléatoires particuliers qu’elle finit par contenir dépendent de ce qui utilisait la dernière fois cet élément de la stack. Si vous modifiez légèrement votre programme afin que différentes fonctions soient appelées, ces différentes fonctions peuvent laisser des valeurs aléatoires différentes qui traînent sur la stack. Ou si vous modifiez votre fonction pour allouer différentes variables locales, le compilateur peut les placer à différents endroits de la stack, ce qui signifie qu’elles finiront par prendre différentes valeurs aléatoires parmi celles qui existaient la dernière fois.

Quoi qu’il en soit, d’une manière ou d’une autre, avec la première version de votre programme, lineName finissait par contenir une valeur aléatoire qui correspondait à un pointeur lineName vers la mémoire à laquelle vous pouviez vous échapper en écrivant. Mais lorsque vous avez ajouté cette quasortingème variable b , les choses ont bougé juste assez pour que lineName finisse par être un pointeur vers une mémoire qui n’existait pas ou pour laquelle vous n’aviez pas l’autorisation d’écrire, et votre programme s’est bloqué.

Avoir un sens?


Et maintenant, encore une chose, si vous êtes toujours avec moi. Si vous vous arrêtez et réfléchissez, tout cela pourrait être assez déroutant. Vous aviez un programme (votre premier programme) qui semblait bien fonctionner, mais qui présentait en réalité un bogue assez horrible. Il a écrit à la mémoire aléatoire, non allouée. Mais lorsque vous l’avez compilé, vous ne recevez aucun message d’erreur fatale, et lorsque vous l’exécutez, rien n’indique que quelque chose ne va pas. Quoi de neuf avec ça?

La réponse, comme le mentionnent quelques commentaires, implique ce que nous appelons un comportement indéfini .

Il s’avère qu’il existe trois types de programmes C, que nous pourrions appeler les bons, les mauvais et les mauvais.

  • Les bons programmes fonctionnent pour les bonnes raisons. Ils n’enfreignent aucune règle, ils ne font rien d’illégal. Ils ne reçoivent aucun avertissement ni message d’erreur lorsque vous les comstackz, et lorsque vous les exécutez, ils fonctionnent.

  • Les programmes défectueux enfreignent certaines règles et le compilateur le détecte, émet un message d’erreur fatal et refuse de générer un programme endommagé que vous pouvez exécuter.

  • Mais il y a aussi les programmes laids, qui adoptent un comportement indéfini . Ce sont ceux qui enfreignent un ensemble de règles différent, ceux pour lesquels le compilateur n’est pas obligé de se plaindre pour diverses raisons. (En effet, le compilateur peut ou non même être capable de les détecter). Et les programmes qui adoptent un comportement indéfini peuvent faire n’importe quoi .

Pensons un peu plus à ces deux derniers points. Le compilateur n’est pas obligé de générer des messages d’erreur lorsque vous écrivez un programme qui utilise un comportement indéfini. Vous risquez donc de ne pas vous en rendre compte. Et le programme est autorisé à faire n’importe quoi, y compris le travail que vous attendez. Mais comme il est autorisé à faire n’importe quoi, il pourrait cesser de fonctionner demain, apparemment sans aucune raison, soit parce que vous y avez apporté des modifications apparemment anodines, soit simplement parce que vous n’êtes pas là pour le défendre, car il fonctionne silencieusement. amok et supprime toutes les données de vos clients.

Alors qu’est-ce que vous êtes censé faire à ce sujet?

Une chose à faire est d’utiliser un compilateur moderne si vous le pouvez, d’activer ses avertissements et d’y prêter attention. (Les bons compilateurs ont même une option appelée “Traiter les avertissements comme des erreurs”, et les programmeurs soucieux des programmes appropriés activent généralement cette option.) Même si, comme je l’ai dit, ils ne sont pas obligés de le faire, les compilateurs deviennent de mieux en mieux. détecter un comportement indéfini et vous en avertir, si vous le lui demandez.

Et puis, si vous faites beaucoup de programmation en C, vous devez apprendre à apprendre le langage, ce que vous êtes autorisé à faire, ce que vous n’êtes pas censé faire. Veillez à écrire des programmes qui fonctionnent pour les bonnes raisons . Ne vous contentez pas d’un programme qui semble simplement fonctionner. Et si quelqu’un indique que vous dépendez d’un comportement indéfini, ne dites pas: “Mais mon programme fonctionne – pourquoi devrais-je m’en soucier?” (Vous n’avez pas dit cela, mais certaines personnes le font.)