83 votes

Quelles sont les règles de distribution des pointeurs en C ?

K&R ne s'y attarde pas, mais l'utilise. J'ai essayé de voir comment cela fonctionnerait en écrivant un programme d'exemple, mais cela n'a pas très bien fonctionné :

#include <stdio.h> 
int bleh (int *); 

int main(){
    char c = '5'; 
    char *d = &c;

    bleh((int *)d); 
    return 0;  
}

int bleh(int *n){
    printf("%d bleh\n", *n); 
    return *n; 
}

Il se compile, mais mon instruction print recrache des variables inutiles (elles sont différentes à chaque fois que j'appelle le programme). Des idées ?

155voto

Gilles Points 37537

Lorsque l'on pense aux pointeurs, il est utile de dessiner des diagrammes . Un pointeur est une flèche qui pointe vers une adresse en mémoire, avec une étiquette indiquant le type de la valeur. L'adresse indique où chercher et le type indique ce qu'il faut prendre. Le moulage du pointeur modifie l'étiquette de la flèche, mais pas l'endroit où elle pointe.

d en main est un pointeur sur c qui est du type char . A char est un octet de mémoire, de sorte que lorsque d est déréférencé, vous obtenez la valeur de cet octet de mémoire. Dans le diagramme ci-dessous, chaque cellule représente un octet.

-+----+----+----+----+----+----+-
 |    | c  |    |    |    |    | 
-+----+----+----+----+----+----+-
       ^~~~
       | char
       d

Lorsque vous lancez d à int* vous dites que d indique en réalité un int valeur. Sur la plupart des systèmes actuels, un int occupe 4 octets.

-+----+----+----+----+----+----+-
 |    | c  | ? | ? | ? |    | 
-+----+----+----+----+----+----+-
       ^~~~~~~~~~~~~~~~~~~
       | int
       (int*)d

Lorsque vous déréférencez (int*)d vous obtenez une valeur déterminée à partir de ces quatre octets de mémoire. La valeur obtenue dépend de ce que contiennent ces cellules marquées ? et sur la manière dont un int est représenté dans la mémoire.

Un PC est little-endian ce qui signifie que la valeur d'un int est calculé de cette manière (en supposant qu'il couvre 4 octets) : * ((int*)d) == c + ? * 2 + ? * 2¹ + ? * 2² . Vous verrez donc que, bien que la valeur soit erronée, si vous l'imprimez en hexadécimal ( printf("%x\n", *n) ), les deux derniers chiffres seront toujours 35 (c'est la valeur du caractère '5' ).

D'autres systèmes sont big-endian et organisent les octets dans l'autre sens : * ((int*)d) == c * 2² + ? * 2¹ + ? * 2 + ? . Sur ces systèmes, vous constaterez que la valeur est toujours commence con 35 lorsqu'il est imprimé en hexadécimal. Certains systèmes ont une taille de int qui diffère de 4 octets. Quelques rares systèmes organisent int de différentes manières, mais il est très peu probable que vous les rencontriez.

En fonction de votre compilateur et de votre système d'exploitation, vous pouvez constater que la valeur est différente à chaque fois que vous exécutez le programme, ou qu'elle est toujours la même mais qu'elle change lorsque vous apportez des modifications, même mineures, au code source.

Sur certains systèmes, un int doit être stockée à une adresse multiple de 4 (ou 2, ou 8). C'est ce qu'on appelle un alignement exigence. Selon que l'adresse de c est correctement aligné ou non, le programme peut se bloquer.

Contrairement à votre programme, voici ce qui se passe lorsque vous avez une int et de prendre un pointeur sur cette valeur.

int x = 42;
int *p = &x;

-+----+----+----+----+----+----+-
 |    |         x         |    | 
-+----+----+----+----+----+----+-
       ^~~~~~~~~~~~~~~~~~~
       | int
       p

Le pointeur p pointe vers un int valeur. L'étiquette de la flèche décrit correctement le contenu de la cellule de mémoire, de sorte qu'il n'y a pas de surprise lors du déréférencement.

45voto

Jack Points 61503
char c = '5'

A char (1 octet) est allouée sur la pile à l'adresse 0x12345678 .

char *d = &c;

Vous obtenez l'adresse de c et le stocker dans d Ainsi d = 0x12345678 .

int *e = (int*)d;

Vous obligez le compilateur à supposer que 0x12345678 pointe vers un int mais un int n'est pas seulement un octet ( sizeof(char) != sizeof(int) ). Il peut être de 4 ou 8 octets selon l'architecture, voire d'autres valeurs.

Ainsi, lorsque vous imprimez la valeur du pointeur, l'entier est considéré en prenant le premier octet (qui était c ) et d'autres octets consécutifs qui se trouvent sur la pile et qui ne sont que des déchets pour votre intention.

21voto

R.. Points 93718

Le casting de pointeurs est généralement invalide en C. Il y a plusieurs raisons à cela :

  1. Alignement. Il est possible que, pour des raisons d'alignement, le type de pointeur de destination ne soit pas en mesure de représenter la valeur du type de pointeur source. Par exemple, si int * étaient intrinsèquement alignés sur 4 octets, le moulage de char * à int * perdrait les bits inférieurs.

  2. Alias. En général, il est interdit d'accéder à un objet autrement que par l'intermédiaire d'une lvalue du type correct pour l'objet. Il y a quelques exceptions, mais à moins que vous ne les compreniez très bien, vous ne devriez pas le faire. Notez que l'aliasing n'est un problème que si vous déréférencez le pointeur (en appliquant la méthode * o -> ou le transmettre à une fonction qui le déréférencera).

Les principaux cas notables où la distribution de pointeurs est acceptable sont les suivants :

  1. Lorsque le type de pointeur de destination pointe vers le type de caractère. Les pointeurs vers des types de caractères sont garantis de pouvoir représenter n'importe quel pointeur vers n'importe quel type, et de le ramener avec succès vers le type d'origine si nécessaire. Pointeur vers void ( void * ) est exactement la même chose qu'un pointeur sur un type de caractère, sauf que vous n'êtes pas autorisé à le déréférencer ou à faire de l'arithmétique dessus, et qu'il se convertit automatiquement vers et depuis d'autres types de pointeurs sans avoir besoin d'un cast, de sorte que les pointeurs sur void sont généralement préférables aux pointeurs sur les types de caractères à cette fin.

  2. Lorsque le type de pointeur de destination est un pointeur sur un type de structure dont les membres correspondent exactement aux membres initiaux du type de structure pointé à l'origine. Ceci est utile pour diverses techniques de programmation orientée objet en C.

D'autres cas obscurs sont techniquement corrects du point de vue des exigences linguistiques, mais ils sont problématiques et il vaut mieux les éviter.

5voto

Ole Dittmann Points 1604

Je pense que vous avez besoin d'une réponse plus générale :

Il n'y a pas de règles sur le moulage des pointeurs en C ! Le langage vous permet de convertir n'importe quel pointeur en n'importe quel autre pointeur sans commentaire.

Mais le fait est que : Il n'y a pas de conversion de données ou quoi que ce soit d'autre ! Vous êtes seul responsable du fait que le système n'interprète pas mal les données après la distribution - ce qui serait généralement le cas, entraînant une erreur d'exécution.

Ainsi, lors de la distribution, c'est à vous de veiller à ce que les données utilisées à partir d'un pointeur distribué soient compatibles !

Le langage C est optimisé pour les performances, il n'a donc pas la réflexivité des pointeurs/références au moment de l'exécution. Mais cela a un prix : en tant que programmeur, vous devez faire plus attention à ce que vous faites. Vous devez savoir par vous-même si ce que vous voulez faire est "légal"

4voto

gkovacs90 Points 1242

Vous avez un pointeur sur un char . Ainsi, comme le sait votre système, cette adresse de mémoire contient un char valeur sur sizeof(char) espace. Lorsque vous le lancez jusqu'à int* Vous travaillerez avec des données de sizeof(int) et donc vous imprimerez votre char et les déchets de mémoire qui le suivent sous forme d'un entier.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X