Référencement d’un caractère * hors de scope

J’ai récemment repris la programmation en C après avoir programmé en C ++ pendant un moment et ma compréhension des pointeurs est un peu rouillée.

Je voudrais demander pourquoi ce code ne provoque aucune erreur:

char* a = NULL; { char* b = "stackoverflow"; a = b; } puts(a); 

Je pensais que parce que b sortait du domaine, a devrait faire référence à un emplacement de mémoire inexistant, et serait donc une erreur d’exécution lors de l’appel de printf .

J’ai exécuté ce code dans MSVC environ 20 fois et aucune erreur n’a été affichée.

Dans la scope où b est défini, l’adresse d’un littéral de chaîne est affectée. Ces littéraux vivent généralement dans une section de mémoire en lecture seule, par opposition à la stack.

Lorsque vous faites a=b vous affectez la valeur de b à a , c’est a dire qu’il contient maintenant l’adresse d’un littéral de chaîne. Cette adresse est toujours valide après que b soit sorti de la scope.

Si vous aviez pris l’ adresse de b et tenté de déréférencer cette adresse, vous invoqueriez un comportement indéfini .

Donc, votre code est valide et n’appelle pas un comportement indéfini, mais ce qui suit:

 int *a = NULL; { int b = 6; a = &b; } printf("b=%d\n", *a); 

Un autre exemple, plus subtil:

 char *a = NULL; { char b[] = "stackoverflow"; a = b; } printf(a); 

La différence entre cet exemple et le vôtre est que b , qui est un tableau, se décompose en un pointeur sur le premier élément lorsqu’il est assigné à a . Donc, dans ce cas, a contient l’adresse d’une variable locale qui sort du champ d’application.

MODIFIER:

En passant, il est déconseillé de passer une variable en tant que premier argument de printf , car cela peut conduire à une vulnérabilité de format chaîne . Mieux vaut utiliser une constante de chaîne comme suit:

 printf("%s", a); 

Ou plus simplement:

 puts(a); 

Ligne par ligne, voici ce que votre code fait:

 char* a = NULL; 

a est un pointeur ne référençant rien (défini sur NULL ).

 { char* b = "stackoverflow"; 

b est un pointeur référençant le littéral de chaîne constant et statique "stackoverflow" .

  a = b; 

a est défini pour référencer également le littéral de chaîne constant et statique "stackoverflow" .

 } 

b est hors de scope. Mais comme a ne fait pas référence à b , alors cela n’a aucune importance (il s’agit de référencer le même littéral chaîne statique et constant que b faisait référence).

 printf(a); 

Imprime le littéral de chaîne constant et statique "stackoverflow" référencé par a .

Les littéraux de chaîne étant alloués de manière statique, le pointeur est valide indéfiniment. Si vous aviez dit char b[] = "stackoverflow" , vous alloueriez un tableau de caractères sur la stack qui deviendrait invalide à la fin de la scope. Cette différence apparaît également pour la modification des chaînes: char s[] = "foo" stack alloue une chaîne que vous pouvez modifier, tandis que char *s = "foo" ne vous donne qu’un pointeur sur une chaîne pouvant être placée en lecture seule. mémoire, donc le modifier est un comportement indéfini.

D’autres personnes ont expliqué que ce code est parfaitement valide. Cette réponse concerne votre attente que si le code avait été invalide , il y aurait eu une erreur d’exécution lors de l’appel de printf . Ce n’est pas nécessairement le cas.

Regardons cette variante de votre code, qui n’est pas valide:

 #include  int main(void) { int *a; { int b = 42; a = &b; } printf("%d\n", *a); // undefined behavior return 0; } 

Ce programme a un comportement indéfini, mais il est fort probable qu’il imprimera en fait 42, pour plusieurs raisons différentes – de nombreux compilateurs quitteront l’emplacement de stack pour b atsortingbué à l’ensemble du corps de la commande main , car rien d’autre n’a besoin de la l’espace et la réduction du nombre d’ajustements de stack simplifient la génération de code; même si le compilateur a officiellement désalloué l’emplacement de stack, le nombre 42 rest probablement en mémoire jusqu’à ce que quelque chose d’autre le remplace, et il n’y a rien entre a = &b et *a pour le faire; Les optimisations standard (“propagation constante et copie”) pourraient éliminer les deux variables et écrire la dernière valeur connue de *a directement dans l’instruction printf (comme si vous aviez écrit printf("%d\n", 42) ).

Il est absolument essentiel de comprendre que “comportement indéfini” ne signifie pas “le programme plantera de manière prévisible”. Cela signifie “tout peut arriver”, et tout comprend de paraître fonctionner comme le programmeur l’avait probablement prévu (sur cet ordinateur, avec ce compilateur, aujourd’hui).


Pour finir, aucun des outils de débogage agressifs auxquels j’ai facilement access (Valgrind, ASan, UBSan) ne permet pas la durée de vie des variables “auto” avec suffisamment de détails pour intercepter cette erreur, mais GCC 6 produit cet avertissement amusant:

 $ gcc -std=c11 -O2 -W -Wall -pedantic test.c test.c: In function 'main': test.c:9:5: warning: 'b' is used uninitialized in this function printf("%d\n", *a); // undefined behavior ^~~~~~~~~~~~~~~~~~ 

Je crois que ce qui s’est passé ici a été l’optimisation que je viens de décrire – copier la dernière valeur connue de b dans *a puis dans le printf – mais sa “dernière valeur connue” pour b était un “cette variable n’est pas initialisée” sentinelle plus de 42. (Il génère alors un code équivalent à printf("%d\n", 0) .)

Le code ne génère aucune erreur car vous assignez simplement le pointeur de caractère b à un autre pointeur de caractère a et cela convient parfaitement.

En C, vous pouvez affecter une référence de pointeur à un autre pointeur. ici, la chaîne “stackoverflow” est utilisée littéralement et l’adresse de base de cette chaîne sera affectée à a variable.

Bien que vous soyez hors de scope pour la variable b mais que l’affectation a été faite avec a pointeur. Ainsi, le résultat sera imprimé sans erreur.

Les littéraux de chaîne sont toujours alloués de manière statique et le programme peut y accéder à tout moment,

 char* a = NULL; { char* b = "stackoverflow"; a = b; } printf(a); 

Je pense que, comme preuve des réponses précédentes, il est bon de jeter un coup d’œil à ce qui se trouve vraiment dans votre code. Les gens ont déjà mentionné que les littéraux de chaîne se trouvaient dans la section .text. Donc, ils (littéraux) sont simplement, toujours, là. Vous pouvez facilement trouver cela pour le code

 #include  int main() { char* a = 0; { char* b = "stackoverflow"; a = c; } printf("%s\n", a); } 

en utilisant la commande suivante

 > cc -S main.c 

à l’intérieur de main.s vous trouverez, tout en bas

 ... ... ... .section __TEXT,__cssortingng,cssortingng_literals L_.str: ## @.str .asciz "stackoverflow" L_.str.1: ## @.str.1 .asciz "%s\n" 

Vous pouvez en savoir plus sur les sections assembleur (par exemple) ici: https://docs.oracle.com/cd/E19455-01/806-3773/elf-3/index.html

Et vous trouverez ici une couverture très bien préparée des exécutables Mach-O: https://www.objc.io/issues/6-build-tools/mach-o-executables/