Qu'est-ce qui rend cette utilisation des pointeurs imprévisible?

108

J'apprends actuellement des pointeurs et mon professeur a fourni ce morceau de code à titre d'exemple:

//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;
}

Il a écrit dans les commentaires que nous ne pouvons pas prédire le comportement du programme. Mais qu'est-ce qui le rend exactement imprévisible? Je ne vois rien de mal à cela.

trungnt
la source
2
Êtes-vous sûr d'avoir reproduit correctement le code du professeur? Bien qu'il soit formellement possible de soutenir que ce programme peut produire un comportement «imprévisible», cela n'a aucun sens de le faire. Et je doute qu'un professeur utilise quelque chose d'aussi ésotérique pour illustrer «imprévisible» aux étudiants.
Du
1
@Lightness Races in Orbit: les compilateurs sont autorisés à "accepter" du code mal formé après avoir émis les messages de diagnostic requis. Mais la spécification du langage ne définit pas le comportement du code. C'est-à-dire qu'en raison de l'erreur d'initialisation de s, le programme, s'il est accepté par un compilateur, a formellement un comportement imprévisible.
Du
2
@TheParamagneticCroissant: Non. L'initialisation est mal formée dans les temps modernes.
Courses de légèreté en orbite
2
@The Paramagnetic Croissant: Comme je l'ai dit plus haut, le langage ne nécessite pas de code mal formé pour "ne pas se compiler". Les compilateurs doivent simplement émettre un diagnostic. Après cela, ils sont autorisés à continuer et à "réussir" à compiler le code. Cependant, le comportement d'un tel code n'est pas défini par la spécification du langage.
Du
2
J'aimerais savoir quelle a été la réponse de votre professeur.
Daniël W. Crompton

Réponses:

125

Le comportement du programme est inexistant, car mal formé.

char* s = "My String";

C'est illégal. Avant 2011, il était obsolète depuis 12 ans.

La ligne correcte est:

const char* s = "My String";

A part ça, le programme est très bien. Votre professeur devrait boire moins de whisky!

Courses de légèreté en orbite
la source
10
avec -pedantic il fait: main.cpp: 6: 16: avertissement: ISO C ++ interdit la conversion d'une constante de chaîne en 'char *' [-Wpedantic]
marcinj
17
@black: Non, le fait que la conversion soit illégale rend le programme mal formé. Il était obsolète dans le passé . Nous ne sommes plus dans le passé.
Courses de légèreté en orbite
17
(Ce qui est idiot parce que c'était le but de la dépréciation de 12 ans)
Courses de légèreté en orbite
17
@black: Le comportement d'un programme mal formé n'est pas "parfaitement défini".
Courses de légèreté en orbite
11
Quoi qu'il en soit, la question concerne le C ++, pas une version particulière de GCC.
Courses de légèreté en orbite
81

La réponse est: cela dépend de la norme C ++ avec laquelle vous compilez. Tout le code est parfaitement bien formé dans toutes les normes ‡ à l'exception de cette ligne:

char * s = "My String";

Maintenant, le littéral de chaîne a un type const char[10]et nous essayons d'initialiser un pointeur non const vers lui. Pour tous les autres types autres que la charfamille des chaînes littérales, une telle initialisation était toujours illégale. Par exemple:

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

Cependant, dans le pré-C ++ 11, pour les littéraux de chaîne, il y avait une exception dans §4.2 / 2:

Un littéral de chaîne (2.13.4) qui n'est pas un littéral de chaîne large peut être converti en une rvalue de type « pointer to char »; [...]. Dans les deux cas, le résultat est un pointeur vers le premier élément du tableau. Cette conversion n'est prise en compte que lorsqu'il existe un type de cible de pointeur explicite approprié, et non lorsqu'il existe un besoin général de convertir une valeur l en une valeur r. [Remarque: cette conversion est obsolète . Voir l'annexe D. ]

Ainsi, en C ++ 03, le code est parfaitement correct (bien que déconseillé) et présente un comportement clair et prévisible.

En C ++ 11, ce bloc n'existe pas - il n'y a pas d'exception pour les chaînes littérales converties en char*, et donc le code est tout aussi mal formé que l' int*exemple que je viens de fournir. Le compilateur est obligé d'émettre un diagnostic, et idéalement dans des cas comme celui-ci qui sont des violations manifestes du système de type C ++, nous nous attendrions à ce qu'un bon compilateur ne soit pas seulement conforme à cet égard (par exemple en émettant un avertissement) mais qu'il échoue carrément.

Le code ne devrait idéalement pas compiler - mais le fait à la fois sur gcc et clang (je suppose qu'il y a probablement beaucoup de code là-bas qui serait cassé avec peu de gain, bien que ce type de trou système soit obsolète depuis plus d'une décennie). Le code est mal formé et il est donc illogique de raisonner sur ce que pourrait être le comportement du code. Mais compte tenu de ce cas spécifique et de l'historique de son autorisation, je ne pense pas qu'il soit déraisonnable d'interpréter le code résultant comme s'il s'agissait d'un implicite const_cast, quelque chose comme:

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

Avec cela, le reste du programme est parfaitement bien, car vous ne touchez splus jamais . La lecture d' un constobjet créé via un non- constpointeur est parfaitement OK. L'écriture d' un constobjet créé via un tel pointeur est un comportement indéfini:

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

Comme il n'y a aucune modification via sn'importe où dans votre code, le programme fonctionne bien en C ++ 03, devrait échouer à compiler en C ++ 11 mais le fait quand même - et étant donné que les compilateurs le permettent, il n'y a toujours pas de comportement indéfini † . En admettant que les compilateurs interprètent toujours [incorrectement] les règles de C ++ 03, je ne vois rien qui conduirait à un comportement "imprévisible". Écrivez à scependant, et tous les paris sont ouverts. Dans C ++ 03 et C ++ 11.


† Bien que, encore une fois, par définition, un code mal formé ne donne aucune attente d'un comportement raisonnable
‡ Sauf non, voir la réponse de Matt McNabb

Barry
la source
Je pense qu'ici "imprévisible" veut dire par le professeur que l'on ne peut pas utiliser le standard pour prédire ce qu'un compilateur fera avec un code mal formé (au-delà de l'émission d'un diagnostic). Oui, il pourrait le traiter comme C ++ 03 dit qu'il devrait être traité, et (au risque de l'erreur "No True Scotsman") le bon sens nous permet de prédire avec une certaine confiance que c'est la seule chose qu'un compilateur-rédacteur sensé choisira jamais si le code se compile du tout. Là encore, il pourrait le traiter comme signifiant d'inverser le littéral de chaîne avant de le convertir en non-const. Le C ++ standard s'en fiche.
Steve Jessop
2
@SteveJessop Je n'achète pas cette interprétation. Il ne s'agit ni d'un comportement indéfini ni de la catégorie de code mal formé que la norme qualifie comme aucun diagnostic requis. C'est une simple violation du système de type qui devrait être très prévisible (compile et fait des choses normales sur C ++ 03, ne parvient pas à compiler sur C ++ 11). Vous ne pouvez pas vraiment utiliser les bogues du compilateur (ou les licences artistiques) pour suggérer que le code est imprévisible - sinon tout le code serait tautologiquement imprévisible.
Barry
Je ne parle pas de bogues du compilateur, je parle de savoir si la norme définit ou non le comportement (le cas échéant) du code. Je soupçonne que le professeur fait de même, et «imprévisible» est juste une façon maladroite de dire que la norme actuelle ne définit pas le comportement. Quoi qu'il en soit, cela me semble plus probable que le fait que le professeur pense à tort qu'il s'agit d'un programme bien formé avec un comportement indéfini.
Steve Jessop
1
Non. La norme ne définit pas le comportement des programmes mal formés.
Steve Jessop
1
@supercat: c'est un bon point, mais je ne crois pas que ce soit la raison principale. Je pense que la principale raison pour laquelle la norme ne spécifie pas le comportement des programmes mal formés, est que les compilateurs peuvent prendre en charge les extensions du langage en ajoutant une syntaxe qui n'est pas bien formée (comme le fait Objective C). Permettre à l'implémentation de faire un total horlicks de nettoyage après une compilation échouée n'est qu'un bonus :-)
Steve Jessop
20

D'autres réponses ont couvert que ce programme est mal formé en C ++ 11 en raison de l'affectation d'un const chartableau à un char *.

Cependant, le programme était également mal formé avant C ++ 11.

Les operator<<surcharges sont là <ostream>. L'obligation iostreamd'inclure a ostreamété ajoutée dans C ++ 11.

Historiquement, la plupart des implémentations iostreamincluaient de ostreamtoute façon, peut-être pour faciliter la mise en œuvre ou peut-être pour fournir une meilleure QoI.

Mais il serait conforme iostreamde ne définir que la ostreamclasse sans définir les operator<<surcharges.

MM
la source
13

La seule chose légèrement erronée que je vois avec ce programme est que vous n'êtes pas censé attribuer une chaîne littérale à un charpointeur mutable , bien que cela soit souvent accepté comme une extension de compilateur.

Sinon, ce programme me paraît bien défini:

  • Les règles qui dictent la manière dont les tableaux de caractères deviennent des pointeurs de caractères lorsqu'ils sont passés en tant que paramètres (comme avec cout << s2) sont bien définies.
  • Le tableau est terminé par null, ce qui est une condition pour operator<<avec a char*(ou a const char*).
  • #include <iostream>inclut <ostream>, qui à son tour définit operator<<(ostream&, const char*), donc tout semble être en place.
zneak
la source
12

Vous ne pouvez pas prédire le comportement du compilateur, pour les raisons indiquées ci-dessus. (La compilation devrait échouer, mais peut-être pas.)

Si la compilation réussit, le comportement est bien défini. Vous pouvez certainement prédire le comportement du programme.

Si la compilation échoue, il n'y a pas de programme. Dans un langage compilé, le programme est l'exécutable, pas le code source. Si vous n'avez pas d'exécutable, vous n'avez pas de programme et vous ne pouvez pas parler du comportement de quelque chose qui n'existe pas.

Je dirais donc que la déclaration de votre prof est fausse. Vous ne pouvez pas prédire le comportement du compilateur face à ce code, mais cela est distinct du comportement du programme . Donc, s'il choisit des lentes, il ferait mieux de s'assurer qu'il a raison. Ou, bien sûr, vous l'avez peut-être mal cité et l'erreur réside dans votre traduction de ce qu'il a dit.

Graham
la source
10

Comme d'autres l'ont noté, le code est illégitime sous C ++ 11, bien qu'il était valide sous les versions antérieures. Par conséquent, un compilateur pour C ++ 11 est requis pour émettre au moins un diagnostic, mais le comportement du compilateur ou du reste du système de génération n'est pas spécifié au-delà. Rien dans le Standard n'interdirait à un compilateur de quitter brusquement en réponse à une erreur, laissant un fichier objet partiellement écrit qu'un éditeur de liens pourrait penser être valide, produisant un exécutable cassé.

Bien qu'un bon compilateur doive toujours s'assurer avant de quitter que tout fichier objet qu'il est censé avoir produit sera valide, inexistant ou reconnaissable comme invalide, ces problèmes ne relèvent pas de la compétence de la norme. Bien qu'il y ait historiquement eu (et peut-être encore) des plates-formes sur lesquelles une compilation échouée peut entraîner des fichiers exécutables d'apparence légitime qui plantent de manière arbitraire lors du chargement (et j'ai dû travailler avec des systèmes où les erreurs de lien avaient souvent un tel comportement) , Je ne dirais pas que les conséquences des erreurs de syntaxe sont généralement imprévisibles. Sur un bon système, une tentative de construction produira généralement un exécutable avec les meilleurs efforts d'un compilateur pour la génération de code, ou ne produira pas du tout un exécutable. Certains systèmes laisseront l'ancien exécutable après un échec de compilation,

Ma préférence personnelle serait que les systèmes basés sur disque renomment le fichier de sortie, pour permettre les rares occasions où cet exécutable serait utile tout en évitant la confusion qui peut résulter de la croyance erronée que l'on exécute un nouveau code, et pour la programmation intégrée systèmes pour permettre à un programmeur de spécifier pour chaque projet un programme qui devrait être chargé si un exécutable valide n'est pas disponible sous le nom normal [idéalement quelque chose qui indique en toute sécurité l'absence d'un programme utilisable]. Un ensemble d'outils de systèmes embarqués n'aurait généralement aucun moyen de savoir ce qu'un tel programme devrait faire, mais dans de nombreux cas, quelqu'un qui écrit du code "réel" pour un système aura accès à un code de test matériel qui pourrait facilement être adapté au objectif. Je ne sais pas si j'ai vu le comportement de changement de nom, cependant,

supercat
la source