wiki:InsiaProgCSyntaxe2

Sommaire

Syntaxe - Types composés et tableaux

1. Types personnalisés

Il est possible de déclarer son propre type. Celui-ci ne peut se construire qu'à l'aide des types existants, et des structures et unions décrites plus loin.

typedef int note_t;

float moyenne(note_t* notes, int nb_notes);

Dans cet exemple, nous créons un type note_t qui est un simple entier de type int. Ceci a plusieurs intérêts:

  • rendre le programme plus lisible: on comprend immédiatement que le tableau en paramètre de la fonction moyenne doit être consistué de notes, sans avoir à le documenter plus avant;
  • rendre les modifications plus souples: si vous décidez que finalement vous avez besoin d'un type float pour représenter les notes, il vous suffit de modifier la déclaration du type, donc à un seul endroit du programme

Note: en règle général, on utilise le suffixe _t pour les noms de types, afin de ne pas les confondre avec les noms de variables.

2. Types composés

Les types composés permettent de construire des structures de données sur mesure en se basant sur les types simples du C, ou d'autres structures composées déjà définies dans des bibliothèques.

Struct

Un "struct" permet de rassembler dans un seul type un ensemble de variables. Par exemple, si l'on veut créer une variable eleve avec un type composé permettant de stocker l'âge, la dernière note et le nom d'un élève (déclarations équivalentes):

/* Type structure anonyme (on ne peut pas pas le réutiliser): */
struct {
  int age;
  int note;
  char* nom;
} eleve1; /* 'eleve' est une variable */

/* Déclaration d'un type tructure nommé: */
struct eleve_t {
  int   age;
  int   note;
  char* nom;
};

/* Déclaration d'une variable utilisant notre type structuré: */
struct eleve_t eleve2;

/* Création d'un type opaque (l'utilisateur ne peut pas savoir que ce type est une structure): */
typedef struct eleve_t un_eleve;
un_eleve eleve3;

Note: il est possible de déclarer un type "struct" en même temps qu'une variable de ce même type, mais ceci n'est pas recommandé pour la lisibilité. Dans l'exemple suivant, nous déclarons bien simultanément un type repas_t et une variable diner:

struct repas_t {
  char* entree;
  char* plat;
  char* dessert;
};
struct repas_t diner;

Initialisation

On peut initialiser les champs d'une variable de type "struct" lors de sa déclaration, en énumérant les valeurs des champs dans leur ordre de déclaration:

eleve_t ducobu = { 21, 15, "Stéphane Ducobu" };

Note: en C99, il existe une solution plus élégante, robuste et lisible qui permet de nommer les champs que l'on initialise:

eleve_t ducobu = { .age = 21, .note = 15, .nom = "Stéphane Ducobu" };

Utilisation

Les membres (variables) d'un "struct" sont donc nommés. Pour accéder à un membre particulier de la structure, on utilise l'opérateur "point". Dans notre exemple, eleve.age est une variable de type int et se manipule normalement:

eleve.age = 20;
printf("note: %d", eleve.note);

Il est impossible de passer une structure en paramètre ou retour d'une fonction. On a alors recours au pointeur, qui lui est un type simple. Il suffit alors de transmettre l'adresse de notre "struct" à l'aide du pointeur.

struct eleve_t {
  int   age;
  int   note;
  char* nom;
};

void affiche_eleve(struct eleve_t* p_eleve) {
  printf("Eleve %s: %d ans, dernière note = %d", p_eleve->nom, p_eleve->age, p_eleve->note);
}

int main(int argc, char** argv) {
  struct eleve_t eleve;
  eleve.age = 20;
  eleve.nom = "Jean Dupont";
  affiche_eleve(&eleve);
}

La première opération, qui consiste à obtenir l'adresse de notre "struct" avec l'opérateur &, s'appelle un référencement. Puis, dans le cors de la fonction affiche_eleve, l'accès aux membres de la structure à partir du pointeur sur celle-ci (avec l'opérateur flèche ->) s'appelle un déréférencement.

Union

Une "union" est un type à facettes: il permet de créer une variable capable de stocker plusieurs types distincts, mais un seul à la fois. Bien que sa syntaxe soit très proche du "struct", le fonctionnement est très différent.

Voici un type union permettant de représenter un entier, ou un réel ou un complexe:

union resultat_t {
  int   entier;
  float reel;
  struct { float: re; float: im; } complexe;
};

On peut alors accéder à un des membres de l'union comme on le ferait pour un "struct", mais à un seul à la fois:

resultat_t r;
r.entier = 45;       /* r.reel et r.complexe n'est pas défini */
r.reel = 4.5;        /* r.entier n'est plus défini */
r.complexe.re = 1.5; /* r.reel n'est plus défini */

Note (pour les programmeurs C++): il est possible de faire du polymorphisme très simpliste avec cet outil.

3. Les tableaux

Une dimension

On peut réaliser des tableaux à une dimension à partir de tous les types disponibles (types simples et composés). La façon la plus simple de déclarer un tableau est celle où l'on sait sa taille; on peut alors écrire:

int vecteur[10]; /* Déclaration: un tableau à 1 dimension de 10 entiers */

vecteur[0] = 42; /* Le premier élément a le numéro 0 */
vecteur[9] = 57; /* Le dernier élément est le numéro 9 */

Initialisation

De même que pour un type "struct", on peut initialiser tout ou les premiers éléments d'un tableau, les éléments non spécifiés valant alors automatiquement zéro:

int vecteur[10] = { 33, 66 }; /* vecteur[0] vaut 33, vecteur[1] vaut 66, vecteur[2] vaut 0, etc. */

Attention: si aucune initialisation n'est effectuée sur votre tableau, alors la valeur des éléments n'est pas définie.

Note: en C99, il est possible de spécifier les indices à initialiser:

int vecteur[10] = { [3] = 33, [5] = 66 }; /* vecteur[3] vaut 33, vecteur[5] vaut 66, vecteur[0] vaut 0, etc. */

Utilisation

Le type de la variable vecteur peut alors s'écrire de deux manières équivalentes: int[] ou int*. Ceci signifie que le type d'un tableau est équivalent à un pointeur, et qu'on le peut donc le passer ainsi sans problème à une fonction:

int somme_des_elements(int* vecteur, int nb_elements) {
  int i, resultat = 0;

  for (i = 0; i < nb_elements; i++) {
    resultat = resultat + vecteur[i];
  }
  return resultat;
}

Le prototype de cette dernière fonction aurait pu également s'écrire de deux autres façons:

int somme_des_elements(int vecteur[], int nb_elements);
int somme_des_elements(int vecteur[10], int nb_elements);

La deuxième forme donne la taille du tableau: cette information n'est qu'une suggestion (un hint), et n'est pas du tout utilisée par le compilateur (vous pourrez écrire vecteur[12] = 0; dans la fonction sans soulever le moindre warning).

Notez qu'une variable de type "tableau" ne donne que deux informations sur les trois nécessaires à la manipulation correcte de ses élements:

  • Son adresse (c'est-à-dire l'emplacement en mémoire du premier élément du tableau), qui est la valeur même de la variable puisqu'il s'agit d'un pointeur
  • Son type, qui permet au compilateur de connaître la taille d'un élément, et donc de pouvoir adresser les éléments suivants du tableau. En effet, les éléments sont stockés les uns après les autres enm mémoire, et il suffit de connaître l'incrément correspondant à la taille occupée en mémoire par un élément pour pouvoir accéder à n'importe quel élément du tableau.

L'élément manquant est la taille du tableau: cette donnée doit être prise en charge par le programmeur. En particulier, on passe souvent à une fonction une variable tableau en même temps que le nombre d'éléments à traiter. Dans tous les cas, le programmeur devra prendre soin de n'utiliser que les éléments du tableau qui ont été reservés (de 0 à N-1, où N est la taille du tableau).

Plusieurs dimensions

Il est possible de généraliser la notion de tableau à plusieurs dimensions. Voici par exemple, en dimension 2, une déclaration possible de matrice de réels:

float matrice[3][3];

matrice[0][0] = 1.717;
matrice[2][1] = 3.1416;

Note générale: il existe bien entendu deux conventions pour savoir si le premier indice adresse la ligne ou la colonne de la matrice. Comme toutes les conventions, il suffit de l'identifier et de la respecter tout au long du programme.

L'organisation en mémoire est toutefois plus complexe qu'un simple tableau: la mémoire reste fondamentalement linéaire et à 1 dimension, il nous faut donc un moyen pour représenter une matrice sur une seule dimension. Ceci a un impact important sur l'identification du type de la matrice et son passage en paramètre de fonction:

/* Cette fonction ne peut fonctionner qu'avec une matrice 3x3 */
int trace_matrice(int m[][3]) {
  return m[0][0] + m[1][1] + m[2][2];
}

Nous avons spécifié dans le type du paramètre la taille de la matrice selon sa deuxième dimension: cette fois-ci l'information n'est pas un simple hint, mais est requise par le compilateur. En effet, celui-ci représente notre matrice comme suit:

  • D'abord, on a un tableau (1D) de pointeurs vers des tableaux 1D: cette première indirection peut être considéré comme la sélection du vecteur ligne ou colonne;
  • Ensuite, on accède normalement au tableau sélectionné et on y extrait l'élément voulu

Ceci implique que pour une matrice 3x3, il y a en mémoire:

  • 1 tableau de 3 pointeurs vers des tableaux de taille 3
  • 3 tableaux d'entiers

En d'autres termes, le C ne peut réellement manipuler que des tableaux unidimensionnels, et gère les tableaux multidimensionnels en créant des tableaux de tableaux (de tableaux (de tableaux ...)) !

Pour préciser la généralisation, voici un exemple en dimension 3, où nous devons inclure la taille des deux dernières dimensions dans le type (il faut donc spécifier la taille de N-1 dimensions à un type de tableau de dimension N):

int valeur_du_coin(int volume[][3][3]) {
  return volume[0][0][0];
}

4. Les chaînes de caractères

Une structure très récurrente en C est celle qui permet de manipuler du texte: la chaîne de caractères.

Dans sa forme la plus simple, le texte est considéré comme un tableau de caractères. Chaque caractère utilise un code pour représenter des symboles, comme les lettres, chiffres, ponctuations, etc. Le code utilisé s'appelle un codage de caractère (text encoding en anglais), parfois appelé - à tort - jeu de caractères (ou charset). On assigne aussi traditionnellement certains codes à des fonctions particulières, comme par exemple le retour chariot (action: aller à la ligne), ou l'alarme (action: émettre un bip). Exemples de déclaration et initialisations :

char lettre_a = 'a';
char euro = '€';
char* bonjour = "Bonjour tout le monde...";
char hello[] = "Hello !";

printf("La lettre du jour: %c\n", lettre_a);
printf("Le mot du jour: %s\n", bonjour);
printf("Une lettre extraite du mot du jour: %c\n", bonjour[3]);

Remarques importantes sur cet exemple:

  • On initialise un caractère à l'aide des apostrophes
  • On initialise une chaîne à l'aide des guillemets
  • Le code qui permet d'aller à la ligne (retour chariot) s'écrit \n
  • Pour afficher une variable de type caractère avec printf, il faut utiliser le marqueur %c. Si vous voulez afficher le code (et non le symbole), utilisez le marqueur entier (%d).
  • Pour afficher une variable de type chaîne avec printf, il faut utiliser le marqueur %s
  • Le codage des symboles dans votre programme va dépendre de votre environnement. Ainsi le symbole "Euro" sera-t-il codé 128 sous Windows (codage CP1252) et probablement 164 sous Unix (codage ISO-8859-15).

La manipulation des chaînes se fait principalement via les fonctions de la bibliothèque standard <string.h>. Par exemple, pour comparer deux chaînes (égales, inférieure ou supérieur selon l'évaluation lexicographique):

int main(int argc, char** argv) {
  if (strcmp(argv[1], "bananes")) {
    printf("Vous avez demandé des bananes.");
  }
}
Last modified 14 years ago Last modified on Oct 30, 2006, 11:14:59 AM