web-dev-qa-db-fra.com

Quelles sont les règles pour lancer des pointeurs en C?

K & R ne le passe pas en revue, mais ils l'utilisent. J'ai essayé de voir comment cela fonctionnerait en écrivant un exemple de programme, mais cela ne s'est pas très bien passé:

#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 compile, mais mon instruction print crache des variables parasites (elles sont différentes chaque fois que j'appelle le programme). Des idées?

60
Theo Chronic

Quand 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 quoi prendre. La fonte du pointeur modifie le libellé de la flèche, mais pas l'emplacement de la flèche.

d dans main est un pointeur sur c de type char. Un char est un octet de mémoire. Ainsi, lorsque d est déréférencé, vous obtenez la valeur dans cet octet de mémoire. Dans le diagramme ci-dessous, chaque cellule représente un octet.

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

Lorsque vous convertissez d en int*, Vous dites que d pointe vraiment sur une valeur int. Sur la plupart des systèmes actuels, un int occupe 4 octets.

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

Lorsque vous déréférenciez (int*)d, Vous obtenez une valeur déterminée à partir de ces quatre octets de mémoire. La valeur que vous obtenez dépend de ce qui est dans ces cellules marqué ? Et de la façon dont un int est représenté en mémoire.

Un PC est little-endian , ce qui signifie que la valeur d'un int est calculée de cette façon (en supposant qu'elle couvre 4 octets): * ((int*)d) == c + ?₁ * 2⁸ + ?₂ * 2¹⁶ + ?₃ * 2²⁴. Vous verrez donc que si la valeur est fouillée, si vous imprimez en hexadécimal (printf("%x\n", *n)), les deux derniers chiffres seront toujours 35 (C'est la valeur du caractère '5').

Certains 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 constateriez que la valeur toujours commence avec 35 Lorsqu'elle est imprimée en hexadécimal. Certains systèmes ont une taille de int différente 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.

Selon votre compilateur et votre système d'exploitation, vous constaterez peut-être que la valeur est différente à chaque fois que vous exécutez le programme, ou qu'elle est toujours identique, mais qu'elle change lorsque vous apportez des modifications même mineures au code source.

Sur certains systèmes, une valeur int doit être stockée dans une adresse multiple de 4 (ou 2, ou 8). Ceci est appelé une exigence alignement . Selon que l'adresse de c soit correctement alignée ou non, le programme peut tomber en panne.

Contrairement à votre programme, voici ce qui se passe lorsque vous avez une valeur int et que vous la dirigez vers un pointeur.

int x = 42;
int *p = &x;
-+----+----+----+----+----+----+-
 |    |         x         |    | 
-+----+----+----+----+----+----+-
       ^~~~~~~~~~~~~~~~~~~
       | int
       p

Le pointeur p pointe sur une valeur int. L'étiquette sur la flèche décrit correctement ce qu'il y a dans la cellule de mémoire, il n'y a donc pas de surprise à la déréférencer.

115
Gilles
char c = '5'

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

char *d = &c;

Vous obtenez l'adresse de c et la stockez dans d, donc d = 0x12345678.

int *e = (int*)d;

Vous forcez le compilateur à supposer que 0x12345678 Pointe sur un int, mais qu'un entier n'est pas un octet (sizeof(char) != sizeof(int)). Il peut s'agir de 4 ou 8 octets selon l'architecture ou même d'autres valeurs.

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

36
Jack

La diffusion des pointeurs est généralement non valide en C. Il y a plusieurs raisons:

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

  2. Aliasing. En général, il est interdit d'accéder à un objet sauf via une lvalue du type correct pour l'objet. Il existe quelques exceptions, mais si vous ne les comprenez pas très bien, vous ne voulez pas le faire. Notez que le crénelage n’est un problème que si vous déréférenciez réellement le pointeur (appliquez la règle * ou -> _ opérateurs, ou passez-le à une fonction qui le déréférencera).

Les principaux cas notables dans lesquels les pointeurs sont acceptables sont:

  1. Lorsque le type de pointeur de destination pointe sur un type de caractère. Les pointeurs vers les types de caractères sont assurés de pouvoir représenter n'importe quel pointeur vers n'importe quel type et de le retourner au type d'origine, le cas échéant. Pointeur à annuler (void *) est exactement identique à un pointeur sur un type de caractère, sauf que vous n'êtes pas autorisé à le déréférencer ou à le calculer, et qu'il convertit automatiquement vers et à partir d'autres types de pointeurs sans nécessiter de transtypage; par conséquent, les pointeurs à annuler sont généralement préférable aux pointeurs aux 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.

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

12
R..

Vous avez un pointeur sur un char. Donc, comme votre système le sait, sur cette adresse mémoire, il y a une valeur char sur sizeof(char) espace. Lorsque vous le lançerez vers int*, Vous travaillerez avec les données de sizeof(int), de sorte que vous imprimerez votre caractère et un peu de mémoire en mémoire sous la forme d'un entier.

2
gkovacs90

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

Il n'y a pas de règles sur le casting de pointeurs en C! La langue vous permet de convertir n'importe quel pointeur en un autre sans commentaire.

Mais la chose est: il n'y a pas de conversion de données ou quoi que ce soit fait! C’est uniquement à vous que le système n’interprète pas de manière erronée les données après la conversion - ce qui serait généralement le cas, ce qui entraînerait une erreur d’exécution.

Ainsi, lors de la conversion, vous devez vous assurer que si les données sont utilisées à partir d'un pointeur converti, elles sont compatibles!

C est optimisé pour les performances, il manque donc de réflexivité à l’exécution des pointeurs/références. Mais cela a un prix - vous, en tant que programmeur, devez prendre davantage soin de ce que vous faites. Vous devez savoir vous-même si ce que vous voulez faire est "légal"

2
Ole Dittmann

La valeur de garbage est en fait due au fait que vous avez appelé la fonction bleh () avant sa déclaration .

Dans le cas de c ++, vous obtiendrez une erreur de compilation mais en c, le compilateur suppose que le type de retour de la fonction est int , alors que votre fonction renvoie un pointeur sur entier.

Voir ceci pour plus d'informations: http://www.geeksforgeeks.org/g-fact-95/

1
Saket