web-dev-qa-db-fra.com

Typage de pointeurs en C

Je sais qu'un pointeur sur un type peut être converti en un pointeur d'un autre type. J'ai trois questions:

  1. Que faut-il garder à l'esprit lors de la frappe de pointeurs?
  2. Quelles sont les exceptions/erreurs qui peuvent survenir avec le pointeur?
  3. Quelles sont les meilleures pratiques pour éviter les exceptions/erreurs?
14
user1871762

Un programme bien écrit n’utilise généralement pas beaucoup de conversion de type pointeur. Il pourrait être nécessaire d'utiliser ptr typecast pour malloc par exemple (déclaré (void *)malloc(...)), mais ce n'est même pas nécessaire en C (bien que quelques compilateurs puissent se plaindre).

  int *p = malloc(sizeof(int)); // no need of (int *)malloc(...)

Toutefois, dans les applications système, vous souhaitez parfois utiliser une astuce pour effectuer des opérations binaires ou spécifiques - et C, un langage proche de la structure de la machine, est pratique pour cela. Par exemple, vous voulez analyser la structure binaire d'un double (qui suit l'implémentation IEEE 754), et travailler avec des éléments binaires est plus simple, vous pouvez déclarer

  typedef unsigned char byte;
  double d = 0.9;
  byte *p = (byte *)&d;
  int i;
  for (i=0 ; i<sizeof(double) ; i++) { ... work with b ... }

Vous pouvez aussi utiliser un union, ceci est un exemple.

Une utilisation plus complexe pourrait être la simulation du polymorphisme C++ , qui nécessite de stocker la hiérarchie "classes" (structures) quelque part pour se rappeler quoi, et effectuer une conversion de type pointeur pour avoir, par exemple, une classe "parent". "variable de pointeur permettant de pointer à un moment donné sur une classe dérivée (voir aussi le lien C++)

  CRectangle rect;
  CPolygon *p = (CPolygon *)&rect;
  p->whatami = POLY_RECTANGLE; // a way to simulate polymorphism ...
  process_poly ( p );

Mais dans ce cas, il vaut peut-être mieux utiliser directement le C++!

Le transtypeur de pointeurs doit être utilisé avec soin pour bien déterminé situations qui font partie de l'analyse du programme - avant le début du développement. 

Pointer typcast dangers potentiels

  • utilisez-les quand ce n'est pas nécessaire - c'est sujet aux erreurs et complexifie le programme 
  • pointer sur un objet de taille différente pouvant entraîner un débordement d'accès, un résultat erroné ...
  • pointeur vers deux structures différentes comme s1 *p = (s1 *)&s2;: compter sur leur taille et leur alignement peut conduire à une erreur

(Mais pour être juste, un programmeur C compétent ne commettrait pas les erreurs ci-dessus ...)

Meilleur entrainement

  • utilisez-les seulement si vous en avez besoin, et commentez bien la partie qui explique pourquoi il est nécessaire
  • sachez ce que vous faites - encore une fois, un programmeur expérimenté peut utiliser des tonnes de typecasts à pointeur, c’est-à-dire qu’il ne faut pas essayer, il peut fonctionner sur un tel système/version/OS et peut ne pas fonctionner sur un autre.
9
Ring Ø

En clair, vous pouvez convertir n'importe quel type de pointeur en n'importe quel autre type de pointeur. Si vous transtypez un pointeur vers ou à partir d'un type incompatible, et écrivez de manière incorrecte la mémoire, vous risquez d'obtenir une erreur de segmentation ou des résultats inattendus de votre application.

Voici un exemple de code des pointeurs de structure de casting:

struct Entity { 
  int type;
}

struct DetailedEntity1 {
  int type;
  short val1;
}

struct DetailedEntity2 {
  int type;
  long val;
  long val2;
}

// random code:
struct Entity* ent = (struct Entity*)ptr;

//bad:
struct DetailedEntity1* ent1 = (struct DetailedEntity1*)ent;
int a = ent->val; // may be an error here, invalid read
ent->val = 117; // possible invali write

//OK:
if (ent->type == DETAILED_ENTITY_1) {
  ((struct DetailedEntity1*)ent)->val1;
} else if (ent->type == DETAILED_ENTITY_2) {
  ((struct DetailedEntity2*)ent)->val2;
} 

En ce qui concerne les pointeurs de fonction, vous devez toujours utiliser des fonctions parfaitement adaptées à la déclaration. Sinon, vous risquez d'obtenir des résultats inattendus ou des erreurs de segmentation.

Lorsque vous passez d'un point à un autre (structure ou non), vous devez vous assurer que la mémoire est alignée de la même manière. Lors de la conversion de structures entières, le meilleur moyen de s’assurer qu’il est d’utiliser le même ordre des mêmes variables au début, et de ne différencier les structures qu’après «l’en-tête commun». N'oubliez pas non plus que l'alignement de la mémoire peut différer d'une machine à l'autre. Vous ne pouvez donc pas simplement envoyer un pointeur struct sous forme de tableau d'octets et le recevoir sous forme de tableau d'octets. Vous pouvez rencontrer un comportement inattendu ou même des erreurs de segmentation. 

Lors de la conversion de pointeurs variables plus petits à plus grands, vous devez faire très attention. Considérons ce code:

char* ptr = malloc (16);
ptr++;
uint64_t* uintPtr = ptr; // may cause an error, memory is not properly aligned

Et aussi, il y a la règle aliasing stricte que vous devez suivre.

3
Dariusz

Vous avez probablement besoin de regarder ... le C-faq maintenu par Steve Summit (qui était posté dans les groupes de discussion, ce qui signifie qu'il a été lu et mis à jour par beaucoup des meilleurs programmeurs de le temps, parfois les concepteurs du langage lui-même). 

Il existe une version abrégée aussi, qui est peut-être plus acceptable et qui reste très, très, très, très utile. Je crois que la lecture de l’ensemble abrégé est obligatoire si vous utilisez C.

0
Olivier Dulac