Les tableaux

Poursuivons notre tour d’horizon des données complexes avec les tableaux.

Comme les structures, les tableaux sont des regroupements de plusieurs objets. Cependant, à l’inverse de celles-ci, les tableaux regroupent des données de même type et de manière contiguë (ce qui exclut la présence de multiplets de bourrage).

Un tableau est donc un gros bloc de mémoire de taille finie qui commence à une adresse déterminée : celle de son premier élément.

Les tableaux simples (à une dimension)

Définition

La définition d’un tableau nécessite trois informations :

  • le type des éléments du tableau (rappelez-vous : un tableau est une suite de données de même type) ;
  • le nom du tableau (en d’autres mots, son identificateur) ;
  • la longueur du tableau (autrement dit, le nombre d’éléments qui le composent). Cette dernière doit être une expression entière.
type identificateur[longueur];

Comme vous le voyez, la syntaxe de la déclaration d’un tableau est similaire à celle d’une variable, la seule différence étant qu’il est nécessaire de préciser le nombre d’éléments entre crochets à la suite de l’identificateur du tableau.

Ainsi, si nous souhaitons par exemple définir un tableau contenant vingt int, nous devons procéder comme suit.

int tab[20];
Initialisation

Comme pour les variables, il est possible d’initialiser un tableau ou, plus précisément, tout ou une partie de ses éléments. L’initialisation se réalise de la même manière que pour les structures, c’est-à-dire à l’aide d’une liste d’initialisation, séquentielle ou sélective.

Initialisation séquentielle
Initialisation avec une longueur explicite

L’initialisation séquentielle permet de spécifier une valeur pour un ou plusieurs membres du tableau en partant du premier élément. Ainsi, l’exemple ci-dessous initialise les trois membres du tableau avec les valeurs 1, 2 et 3.

int tab[3] = { 1, 2, 3 };
Initialisation avec une longueur implicite

Lorsque vous initialisez un tableau, il vous est permis d’omettre la longueur de celui-ci, car le compilateur sera capable d’en déterminer la taille en comptant le nombre d’éléments présents dans la liste d’initialisation. Ainsi, l’exemple ci-dessous est correct et définit un tableau de trois int valant respectivement 1, 2 et 3.

int tab[] = { 1, 2, 3 };
Initialisation sélective
Initialisation avec une longueur explicite

Comme pour les structures, il est par ailleurs possible de désigner spécifiquement les éléments du tableau que vous souhaitez initialiser. Cette initialisation sélective est réalisée à l’aide du numéro du ou des éléments.

Faites attention ! Les éléments sont numérotés à partir de zéro. Nous y viendrons dans la section suivante.

L’exemple ci-dessous définit un tableau de trois int et initialise le troisième élément.

int tab[3] = { [2] = 3 };
Initialisation avec une longueur implicite

Dans le cas où la longueur du tableau n’est pas précisée, le compilateur déduira la taille du tableau du plus grand indice utilisé lors de l’initialisation sélective. Partant, le code ci-dessous créer un tableau de cent int.

int tab[] = { [0] = 42, [1] = 64, [99] = 100 };

Comme pour les structures, dans le cas où vous ne fournissez pas un nombre suffisant de valeurs, les éléments oubliés seront initialisés à zéro ou, s’il s’agit de pointeurs, seront des pointeurs nuls.

Également, il est possible de mélanger initialisations séquentielles et sélectives. Dans un tel cas, l’initialisation séquentielle reprend au dernier élément désigné par une initialisation sélective. Dès lors, le code ci-dessous définit un tableau de dix int et initialise le neuvième élément à 9 et le dixième à 10.

int tab[] = { [8] = 9, 10 };

Pour un tableau de structures, la liste d’initialisation comportera elle-même une liste d’initialisation pour chaque structure composant le tableau, par exemple comme ceci.

struct temps tab[2] = { { 12, 45, 50.6401 }, { 13, 30, 35.480 } } ; 

Notez par ailleurs que, inversement, une structure peut comporter des tableaux comme membres.

Accès aux éléments d’un tableau

L’accès aux éléments d’un tableau se réalise à l’aide d’un indice, un nombre entier correspondant à la position de chaque élément dans le tableau (premier, deuxième, troisième, etc). Cependant, il y a une petite subtilité : les indices commencent toujours à zéro. Ceci tient à une raison historique : les performances étant limitées à l’époque de la conception du langage C, il était impératif d’éviter des calculs inutiles, y compris lors de la compilation1.

Plus précisément, l’accès aux différents éléments d’un tableau est réalisé à l’aide de l’adresse de son premier élément, à laquelle est ajouté l’indice. En effet, étant donné que tous les éléments ont la même taille et se suivent en mémoire, leurs adresses peuvent se calculer à l’aide de l’adresse du premier élément et d’un décalage par rapport à celle-ci (l’indice, donc).

Or, si le premier indice n’est pas zéro, mais par exemple un, cela signifie que le compilateur doit soustraire une unité à chaque indice lors de la compilation pour retrouver la bonne adresse, ce qui implique des calculs supplémentaires, chose impensable quand les ressources sont limitées.

Prenons un exemple avec un tableau composé de int (ayant une taille de quatre octets) et dont le premier élément est placé à l’adresse 1008. Si vous déterminez à la main les adresses de chaque élément, vous obtiendrez ceci.

Indice Adresse de l’élément
0 1008 (1008 + 0)
1 1012 (1008 + 4)
2 1016 (1008 + 8)
3 1020 (1008 + 12)
4 1024 (1008 + 16)
5 1028 (1008 + 20)

En fait, il est possible de reformuler ceci à l’aide d’une multiplication entre l’indice et la taille d’un int.

Indice Adresse de l’élément
0 1008 (1008 + (0 * 4))
1 1012 (1008 + (1 * 4))
2 1016 (1008 + (2 * 4))
3 1020 (1008 + (3 * 4))
4 1024 (1008 + (4 * 4))
5 1028 (1008 + (5 * 4))

Nous pouvons désormais formaliser mathématiquement tout ceci en posant T la taille d’un élément du tableau, i l’indice de cet élément, et A l’adresse de début du tableau (l’adresse du premier élément, donc). L’adresse de l’élément d’indice i s’obtient en calculant A + T × i. Ceci étant posé, voyons à présent comment mettre tout cela en œuvre en C.

Le premier élément

Pour commencer, nous avons besoin de l’adresse du premier élément du tableau. Celle-ci s’obtient en fait d’une manière plutôt contre-intuitive : lorsque vous utilisez une variable de type tableau dans une expression, celle-ci est convertie implicitement en un pointeur sur son premier élément. Comme vous pouvez le constater dans l’exemple qui suit, nous pouvons utiliser la variable tab comme nous l’aurions fait s’il s’agissait d’un pointeur.

#include <stdio.h>

int main(void)
{
    int tab[3] = { 1, 2, 3 };

    printf("Premier élément : %d\n", *tab);
    return 0;
}
Résultat
Premier élément : 1

Notez toutefois qu’il n’est pas possible d’affecter une valeur à une variable de type tableau (nous y viendrons bientôt). Ainsi, le code suivant est incorrect.

int t1[3];
int t2[3];

t1 = t2; /* Incorrect */

La règle de conversion implicite comprend néanmoins deux exceptions : l’opérateur & et l’opérateur sizeof.

Lorsqu’il est appliqué à une variable de type tableau, l’opérateur & produit comme résultat l’adresse du premier élément du tableau. Si vous exécutez le code ci-dessous, vous constaterez que les deux expressions donnent un résultat identique.

#include <stdio.h>


int main(void)
{
    int tab[3];

    printf("%p == %p\n", (void *)tab, (void *)&tab);
    return 0;
}

Notez toutefois que si l’adresse référencée par les deux pointeurs est identique, leurs types sont différents. tab est un pointeur sur int et &tab est un pointeur sur un tableau de 3 int. Nous en parlerons lorsque nous verrons les tableaux multidimensionnels un peu plus tard dans ce chapitre.

Dans le cas où une expression de type tableau est fournie comme opérande de l’opérateur sizeof, le résultat de celui-ci sera bien la taille totale du tableau (en multiplets) et non la taille d’un pointeur.

#include <stdio.h>


int main(void)
{
    int tab[3];
    int *ptr;

    printf("sizeof tab = %zu\n", sizeof tab);
    printf("sizeof ptr = %zu\n", sizeof ptr);
    return 0;
}
Résultat
sizeof tab = 12
sizeof ptr = 8

Cette propriété vous permet d’obtenir le nombre d’éléments d’un tableau à l’aide de l’expression suivante.

sizeof tab / sizeof tab[0]

Notez que pour obtenir la taille d’un type tableau, la syntaxe est la suivante.

#include <stdio.h>


int main(void)
{
    printf("sizeof int[3] = %zu\n", sizeof(int[3]));
    printf("sizeof double[42] = %zu\n", sizeof(double[42]));
    return 0;
}
Résultat
sizeof int[3] = 12
sizeof double[42] = 336
Les autres éléments

Pour accéder aux autres éléments, il va nous falloir ajouter la position de l’élément voulu à l’adresse du premier élément et ensuite utiliser l’adresse obtenue. Toutefois, recourir à la formule présentée au-dessus ne marchera pas car, en C, les pointeurs sont typés. Dès lors, lorsque vous additionnez un nombre à un pointeur, le compilateur multiplie automatiquement ce nombre par la taille du type d’objet référencé par le pointeur. Ainsi, pour un tableau de int, l’expression tab + 1 est implicitement convertie en tab + sizeof(int).

Voici un exemple affichant la valeur de tous les éléments d’un tableau.

#include <stdio.h>


int main(void)
{
    int tab[3] = { 1, 2, 3 };

    printf("Premier élément : %d\n", *tab);
    printf("Deuxième élément : %d\n", *(tab + 1));
    printf("Troisième élément : %d\n", *(tab + 2));
    return 0;
}
Résultat
Premier élément : 1
Deuxième élément : 2
Troisième élément : 3

L’expression *(tab + i) étant quelque peu lourde, il existe un opérateur plus concis pour réaliser cette opération : l’opérateur []. Celui-ci s’utilise de cette manière.

expression[indice]

Ce qui est équivalent à l’expression suivante.

*(expression + indice)

L’exemple suivant est donc identique au précédent.

#include <stdio.h>


int main(void)
{
    int tab[3] = { 1, 2, 3 };

    printf("Premier élément : %d\n", tab[0]);
    printf("Deuxième élément : %d\n", tab[1]);
    printf("Troisième élément : %d\n", tab[2]);
    return 0;
}
Parcours et débordement

Une des erreurs les plus fréquentes en C consiste à dépasser la taille d’un tableau, ce qui est appelé un cas de débordement (overflow en anglais). En effet, si vous tentez d’accéder à un objet qui ne fait pas partie de votre tableau, vous réalisez un accès mémoire non autorisé, ce qui provoquera un comportement indéfini. Cela arrive généralement lors d’un parcours de tableau à l’aide d’une boucle.

#include <stdio.h>


int main(void)
{
    int tableau[5] = { 784, 5, 45, -12001, 8 };
    int somme = 0;

    for (unsigned i = 0; i <= 5; ++i)
        somme += tableau[i];
    
    printf("%d\n", somme);
    return 0;
}

Le code ci-dessus est volontairement erroné et tente d’accéder à un élément qui se situe au-delà du tableau. Ceci provient de l’utilisation de l’opérateur <= à la place de l’opérateur < ce qui entraîne un tour de boucle avec i qui est égal à 5, alors que le dernier indice du tableau doit être quatre.

N’oubliez pas : les indices d’un tableau commencent toujours à zéro. En conséquence, les indices valides d’un tableau de n éléments vont de 0 à n - 1.

Tableaux et fonctions
Passage en argument

Étant donné qu’un tableau peut être utilisé comme un pointeur sur son premier élément, lorsque vous passez un tableau en argument d’une fonction, celle-ci reçoit un pointeur vers le premier élément du tableau. Le plus souvent, il vous sera nécessaire de passer également la taille du tableau afin de pouvoir le parcourir.

Le code suivant utilise une fonction pour parcourir un tableau d’entiers et afficher la valeur de chacun de ses éléments.

#include <stdio.h>


void affiche_tableau(int *tab, unsigned taille)
{
    for (unsigned i = 0; i < taille; ++i)
        printf("tab[%u] = %d\n", i, tab[i]);
}


int main(void)
{
    int tab[5] = { 2, 45, 67, 89, 123 };

    affiche_tableau(tab, 5);
    return 0;
}
Résultat
tab[0] = 2
tab[1] = 45
tab[2] = 67
tab[3] = 89
tab[4] = 123

Notez qu’il existe une syntaxe alternative pour déclarer un paramètre de type tableau héritée du langage B (voyez la section suivante).

void affiche_tableau(int tab[], unsigned taille)

Toutefois, nous vous conseillons de recourir à la première écriture, cette dernière étant plus explicite.

Retour de fonction

De la même manière que pour le passage en argument, retourner un tableau revient à retourner un pointeur sur le premier élément de celui-ci. Toutefois, n’oubliez pas les problématiques de classe de stockage ! Si vous retournez un tableau de classe de stockage automatique, vous fournissez à la fonction appelante un pointeur vers un objet qui n’existe plus (puisque l’exécution de la fonction appelée est terminée).

#include <stdio.h>


int *tableau(void)
{
    int tab[5] = { 1, 2, 3, 4, 5 };

    return tab;
}


int main(void)
{
    int *p = tableau(); /* Incorrect */

    printf("%d\n", p[0]);
    return 0;
}

  1. Pour les curieux, vous pouvez lire ce billet (en anglais) qui explique en détail les raisons de ce choix.

La vérité sur les tableaux

Nous vous avons dit qu’il n’est pas possible d’affecter une valeur à une variable de type tableau.

int t1[3];
int t2[3];

t1 = t2; /* Incorrect */

Dans cette section, nous allons vous expliquer pourquoi, mais pour cela, il va nous falloir faire un peu d’histoire. ^^

Un peu d’histoire

Le prédécesseur du langage C était le langage B. Lorsque le développement du C a commencé, un des objectifs était de le rendre autant que possible compatible avec le B, afin de ne pas devoir (trop) modifier les codes existants (un code écrit en B pourrait ainsi être compilé avec un compilateur C sans ou avec peu de modifications). Or, en B, un tableau se définissait comme suit.

auto tab[3];

Le langage B était un langage non typé, ce qui explique l’absence de type dans la définition. Le mot-clé auto (toujours présent en langage C, mais devenu obsolète) servait à indiquer que la variable définie était de classe de stockage automatique.

Toutefois, à la différence du langage C, cette définition crée un tableau de trois éléments et un pointeur initialisé avec l’adresse du premier élément. Ainsi, pour créer un pointeur, il suffisait de définir une variable comme un tableau de taille nulle.

auto ptr[];

Le langage C, toujours en gestation, avait repris ce mode de fonctionnement. Cependant, les structures sont arrivées et les problèmes avec. En effet, prenez ce bout de code.

#include <stdio.h>

struct exemple {
    int tab[3];
};


struct exemple exemple_init(void)
{
    struct exemple init = { { 1, 2, 3 } };

    return init;
}


int main(void)
{
    struct exemple s = exemple_init();

    printf("%d\n", s.tab[0]);
    return 0;
}

La fonction exemple_init() retourne une structure qui est utilisée pour initialiser la variable de la fonction main(). Dans un tel cas, comme pour n’importe quelle variable, le contenu de la première structure sera intégralement copié dans la deuxième. Le souci, c’est que si une définition de tableau crée un tableau et un pointeur initialisé avec l’adresse du premier élément de celui-ci, alors il est nécessaire de modifier le champ tab de la structure s lors de la copie sans quoi son champ tab pointera vers le tableau de la structure init (qui n’existera plus puisque de classe de stockage automatique) et non vers le sien. Voilà qui complexifie la copie de structures, particulièrement si sa définition comprend plusieurs tableaux possiblement imbriqués…

Pour contourner ce problème, les concepteurs du langage C ont imaginé une solution (tordue) qui est à l’origine d’une certaine confusion dans l’utilisation des tableaux : une variable de type tableau ne sera plus un pointeur, mais sera convertie en un pointeur sur son premier élément lors de son utilisation.

Conséquences de l’absence d’un pointeur

Étant donné qu’il n’y a plus de pointeur alloué, la copie de structures s’en trouve simplifiée et peut être réalisée sans opération particulière (ce qui était l’objectif recherché).

Toutefois, cela entraîne une autre conséquence : il n’est plus possible d’assigner une valeur à une variable de type tableau, seuls ses éléments peuvent se voir affecter une valeur. Ainsi, le code suivant est incorrect puisqu’il n’y a aucun pointeur pour recevoir l’adresse du premier élément du tableau t2.

int t1[3];
int t2[3];

t1 = t2; /* Incorrect */

Également, puisqu’une variable de type tableau n’est plus un pointeur, celle-ci n’a pas d’adresse. Dès lors, lorsque l’opérateur & est appliqué à une variable de type tableau, le résultat sera l’adresse du premier élément du tableau puisque seuls les éléments du tableau ont une adresse.

Les tableaux multidimensionnels

Jusqu’à présent, nous avons travaillé avec des tableaux linéaires, c’est-à-dire dont les éléments se suivaient les uns à la suite des autres. Il s’agit de tableaux dit à une dimension ou unidimensionnels.

Cependant, certaines données peuvent être représentées plus simplement sous la forme de tableaux à deux dimensions (autrement dit, organisées en lignes et en colonnes). C’est par exemple le cas des images (non vectorielles) qui sont des matrices de pixels ou, plus simplement, d’une grille de Sudoku qui est organisée en neuf lignes et en neuf colonnes.

Le langage C vous permet de créer et de gérer ce type de tableaux dit multidimensionnels (en fait, des tableaux de tableaux) et ce, bien au-delà de deux dimensions.

Définition

La définition d’un tableau multidimensionnel se réalise de la même manière que celle d’un tableau unidimensionnel si ce n’est que vous devez fournir la taille des différentes dimensions.

Par exemple, si nous souhaitons définir un tableau de int de vingt lignes et trente-cinq colonnes, nous procéderons comme suit.

int tab[20][35];

De même, pour un tableau de double à trois dimensions.

double tab[3][4][5];
Initialisation
Initialisation avec une longueur explicite

L’initialisation d’un tableau multidimensionnel s’effectue à l’aide d’une liste d’initialisation comprenant elle-même des listes d’initialisations.

int t1[2][2] = { { 1, 2 }, { [0] = 3, [1] = 4 } };
int t2[2][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } };
Initialisation avec une longueur implicite

Lorsque vous initialisez un tableau multidimensionnel, il vous est permis d’omettre la taille de la première dimension. La taille des autres dimensions doit en revanche être spécifiée, le compilateur ne pouvant déduire la taille de toutes les dimensions.

int t1[][2] = { { 1, 2 }, { [0] = 3, [1] = 4 } };
int t2[][2][2] = { { { 1, 2 }, { 3, 4 } }, { { 5, 6 }, { 7, 8 } } };

Comme pour les tableaux unidimensionnels, dans le cas où vous ne fournissez pas un nombre suffisant de valeurs, les éléments omis seront initialisés à zéro ou, s’il s’agit de pointeurs, seront des pointeurs nuls.

Utilisation

Techniquement, un tableau multidimensionnel est un tableau dont les éléments sont eux-mêmes des tableaux. Dès lors, vous avez besoin d’autant d’indices qu’il y a de dimensions. Par exemple, pour un tableau à deux dimensions, vous avez besoin d’un premier indice pour accéder à l’élément souhaité du premier tableau, mais comme cet élément est lui-même un tableau, vous devez utiliser un second indice pour sélectionner un élément de celui-ci. Illustration.

#include <stdio.h>


int main(void)
{
    int tab[2][2] = { { 1, 2 }, { 3, 4 } };

    printf("tab[0][0] = %d\n", tab[0][0]);
    printf("tab[0][1] = %d\n", tab[0][1]);
    printf("tab[1][0] = %d\n", tab[1][0]);
    printf("tab[1][1] = %d\n", tab[1][1]);
    return 0;
}
Résultat
tab[0][0] = 1
tab[0][1] = 2
tab[1][0] = 3
tab[1][1] = 4
Représentation en mémoire

Techniquement, les données d’un tableau multidimensionnel sont stockées les unes à côté des autres en mémoire : elles sont rassemblées dans un tableau à une seule dimension. Si les langages comme le FORTRAN mémorisent les colonnes les unes après les autres (column-major order en anglais), le C mémorise les tableaux lignes par lignes (row-major order).

          Un exemple de tableau
            à deux dimensions
                                         Lignes par lignes (row-major order)
            +---+ +---+ +---+   +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+
            | 1 | | 2 | | 3 |---> 1 | | 2 | | 3 | | 4 | | 5 | | 6 | | 7 | | 8 | | 9 |
            +---+ +---+ +---+   +---+ +---+ +---+ +-^-+ +---+ +---+ +-^-+ +---+ +---+
            +---+ +---+ +---+                       |                 |
            | 4 | | 5 | | 6 |-----------------------+                 |
            +---+ +---+ +---+                                         |
            +---+ +---+ +---+                                         |
            | 7 | | 8 | | 9 |-----------------------------------------+
            +---+ +---+ +---+
              |     |     |
  +-----------+     |     +-----------+
  |                 |                 |
+-v-+ +---+ +---+ +-v-+ +---+ +---+ +-v-+ +---+ +---+
| 1 | | 4 | | 7 | | 2 | | 5 | | 8 | | 3 | | 6 | | 9 |
+---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+ +---+
      Colonnes par colonnes (column-major order)

Le calcul d’adresse à effectuer est une généralisation du calcul vu au chapitre précédent.

Parcours

Le même exemple peut être réalisé à l’aide de deux boucles imbriquées afin de parcourir le tableau.

#include <stdio.h>


int main(void)
{
    int tab[2][2] = { { 1, 2 }, { 3, 4 } };

    for (unsigned i = 0; i < 2; ++i)
        for (unsigned j = 0; j < 2; ++j)
            printf("tab[%u][%u] = %d\n", i, j, tab[i][j]);

    return 0;
}
Tableaux multidimensionnels et fonctions
Passage en argument

Souvenez-vous : sauf exceptions, un tableau est converti en un pointeur sur son premier élément. Dès lors, qu’obtenons-nous lors du passage d’un tableau à deux dimensions en argument d’une fonction ? Le premier élément du tableau est un tableau, donc un pointeur sur… Un tableau ( oui). :p

La syntaxe d’un pointeur sur tableau est la suivante.

type (*identificateur)[taille];

Vous remarquerez la présence de parenthèses autour du symbole * et de l’identificateur afin de signaler au compilateur qu’il s’agit d’un pointeur sur un tableau et non d’un tableau de pointeurs. Également, notez que la taille du tableau référencé doit être spécifiée. En effet, sans celle-ci, le compilateur ne pourrait pas opérer correctement le calcul d’adresses puisqu’il ne connaîtrait pas la taille des éléments composant le tableau référencé par le pointeur.

La même logique peut être appliquée pour créer des pointeurs sur des tableaux de tableaux.

type (*identificateur)[N][M];

Et ainsi de suite jusqu’à ce que mort s’ensuive… :-°

L’exemple ci-dessous illustre ce qui vient d’être dit en utilisant une fonction pour afficher le contenu d’un tableau à deux dimensions de int.

#include <stdio.h>


void affiche_tableau(int (*tab)[2], unsigned n, unsigned m)
{
    for (unsigned i = 0; i < n; ++i)
        for (unsigned j = 0; j < m; ++j)
            printf("tab[%u][%u] = %d\n", i, j, tab[i][j]);
}


int main(void)
{
    int tab[2][2] = { { 1, 2 }, { 3, 4 } };

    affiche_tableau(tab, 2, 2);
    return 0;
}
Retour de fonction

Même remarque que pour les tableaux unidimensionnels : attention à la classe de stockage ! Pour le reste, nous vous laissons admirer la syntaxe particulièrement dégoûtante d’une fonction retournant un pointeur sur un tableau de deux int.

#include <stdio.h>


int (*tableau(void))[2] /* Ouh ! Que c'est laid ! */
{
    int tab[2][2] = { { 1, 2 }, { 3, 4 } };

    return tab;
}


int main(void)
{
    int (*p)[2] = tableau(); /* Incorrect */

    printf("%d\n", p[0][0]);
    return 0;
}

Les tableaux littéraux

Comme pour les structures, il est possible de construire des tableaux « littéraux » c’est-à-dire sans besoin de déclarer une variable. Comme pour les structures, ces tableaux littéraux ont une classe de stockage automatique, ils sont donc détruits à la fin du bloc qui les a vu naître.

La syntaxe est similaire à celle des structures.

(type[taille]) { élément1, élément2 }

Comme pour l’initialisation de n’importe quel tableau, il est possible d’omettre la taille afin que le compilateur la calcule en se basant sur les éléments fournis. L’exemple ci-dessous utilise un tableau littéral en lieu et place d’une variable.

#include <stdio.h>


static void affiche_tableau(int *tab, unsigned taille)
{
    for (unsigned i = 0; i < taille; ++i)
        printf("tab[%u] = %d\n", i, tab[i]);
}


int
main(void)
{
    affiche_tableau((int[]) { 1, 2, 3, 4, 5 }, 5);
    return 0;
}
Résultat
tab[0] = 1
tab[1] = 2
tab[2] = 3
tab[3] = 4
tab[4] = 5

Exercices

Somme des éléments

Réalisez une fonction qui calcule la somme de tous les éléments d’un tableau de int.

int somme(int *tableau, unsigned taille)
{
    int res = 0 ;

    for (unsigned i = 0; i < taille; ++i)
        res += tableau[i];

    return res;
}
Maximum et minimum

Créez deux fonctions : une qui retourne le plus petit élément d’un tableau de int et une qui renvoie le plus grand élément d’un tableau de int.

int minimum(int *tab, unsigned taille)
{
    int min = tab[0];

    for (unsigned i = 1; i < taille; ++i)
        if (tab[i] < min)
            min = tab[i];

    return min;
}


int maximum(int *tab, unsigned taille)
{
    int max = tab[0];

    for (unsigned i = 1; i < taille; ++i)
        if (tab[i] > max)
            max = tab[i];

    return max;
}
Recherche d’un élément

Construisez une fonction qui teste la présence d’une valeur dans un tableau de int. Celle-ci retournera 1 si un ou plusieurs éléments du tableau sont égaux à la valeur recherchée, 0 sinon.

int find(int *tab, unsigned taille, int val)
{
    for (unsigned i = 0; i < taille; ++i)
        if (tab[i] == val) 
            return 1;

    return 0;
}
Inverser un tableau

Produisez une fonction qui inverse le contenu d’un tableau (le premier élément devient le dernier, l’avant-dernier le deuxième et ainsi de suite).

Indice

Pensez à la fonction swap() présentée dans le chapitre sur les pointeurs.

Correction
void swap(int *pa, int *pb)
{
    int tmp = *pa;
    *pa = *pb;
    *pb = tmp;
}


void invert(int *tab , unsigned taille)
{
    for (unsigned i = 0; i < (taille / 2); ++i)
        swap(tab + i , tab + taille - 1 - i);
}
Produit des lignes

Composez une fonction qui calcule le produit de la somme des éléments de chaque ligne d’un tableau de int à deux dimensions (ce tableau comprend cinq lignes et cinq colonnes).

int produit(int (*tab)[5])
{
    int res = 1;

    for (unsigned i = 0; i < 5; ++i)
    {
        int tmp = 0;

        for (unsigned j = 0; j < 5; ++j)
            tmp += tab[i][j];

        res *= tmp;
    }

    return res;
}
Triangle de Pascal

Les triangles de Pascal sont des objets mathématiques amusants. Voici une petite animation qui vous expliquera le fonctionnement de ceux-ci.

Explication des triangles de Pascal en image
Explication des triangles de Pascal en image

Votre objectif va être de réaliser un programme qui affiche un triangle de Pascal de la taille souhaitée par l’utilisateur. Pour ce faire, nous allons diviser le triangle en lignes afin de pouvoir le représenter sous la forme d’un tableau à deux dimensions. Chaque élément du tableau se verra attribué soit une valeur du triangle soit zéro pour signaler qu’il n’est pas utilisé.

La première chose que nous allons faire est donc définir un tableau à deux dimensions (nous fixerons la taille des dimensions à dix) dont tous les éléments sont initialisés à zéro. Ensuite, nous allons demander à l’utilisateur d’entrer la taille du triangle qu’il souhaite obtenir (celle-ci ne devra pas être supérieure aux dimensions du tableau).

À présent, passons à la fonction de création du triangle de Pascal. Celle-ci devra mettre en œuvre l’algorithme suivant.

N = taille du triangle de Pascal fournie par l’utilisateur

Mettre la première case du tableau à 1

Pour i = 1, i < N, i = i + 1
    Mettre la première case de la ligne à 1

    Pour j = 1, j < i, j = j + 1
         La case [i,j] prend la valeur [i - 1, j - 1] + [i - 1, j]

    Mettre la dernière case de la ligne à 1

Enfin, il vous faudra écrire une petite fonction pour afficher le tableau ainsi créé.
Bon courage ! ;)

#include <stdio.h>
#include <stdlib.h>


void triangle_pascal(int (*tab)[10], unsigned taille)
{
    tab[0][0] = 1;

    for (unsigned i = 1; i < taille; ++i)
    {
        tab[i][0] = 1;

        for (unsigned j = 1; j < i; ++j)
            tab[i][j] = tab[i - 1][j - 1] + tab[i - 1][j];

        tab[i][i] = 1;
    }
}


void print_triangle(int (*tab)[10], unsigned taille)
{
    for (unsigned i = 0; i < taille; ++i)
    {
        for (int sp = taille - 1 - i; sp > 0; --sp)
            printf(" ");

        for (unsigned j = 0; j < taille; ++j)
            if (tab[i][j] != 0)
                printf("%d ", tab[i][j]);

        printf("\n");
    }
}


int main(void)
{
    int tab[10][10] = { { 0 } };
    unsigned taille;

    printf("Taille du triangle: ");

    if (scanf("%u", &taille) != 1)
    {
        printf("Mauvaise saisie\n");
        return EXIT_FAILURE;
    }
    else if (taille > 10)
    {
        printf("La taille ne doit pas être supérieure à 10\n");
        return EXIT_FAILURE;
    }

    triangle_pascal(tab, taille);
    print_triangle(tab, taille);	
    return 0;
}

Dans le chapitre suivant, nous aborderons un nouveau type d’agrégat, un peu particulier puisqu’il se base sur les tableaux : les chaînes de caractères.

En résumé
  1. Un tableau est une suite contiguë d’objets de même type ;
  2. La taille d’un tableau est précisée à l’aide d’une expression entière ;
  3. L’accès aux éléments d’un tableau se réalise à l’aide d’un indice ;
  4. Le premier élément d’un tableau a l’indice zéro ;
  5. Il n’est pas possible d’affecter une valeur à une variable de type tableau, seul ses éléments sont modifiables ;
  6. Un tableau est implicitement converti en un pointeur sur son premier élément, sauf lorsqu’il est l’opérande de l’opérateur sizeof ou &.