Par O. Bailleux, Maître de conférences HDR à l'université de Bourgogne.
Tableaux dynamiques
Les activités #.dec (dec pour Découverte) consistent à étudier des présentations explicatives et à traiter tous les exercices de découverte proposés. Si vous ne répondez pas correctement à un exercice de découverte, revenez en arrière pour relire les explications et identifier la cause de votre erreur.
Les activités #.ent (ent pour Entrainement) consistent à traiter des exercices d’entraînement. Vous devez d’abord tenter de traiter ces exercices sans dévoiler d’indice ni regarder la solution. Vous pouvez remonter dans le document pour relire certaines explications ou consulter les solutions des exercices de découverte ou d’entraînement précédemment traités.
Activité de découverte E.dec
E.dec1 : Tableau dynamique
Prérequis
Maîtriser la partie C (tableaux et pointeurs) et la partie D (chaînes de caractères) |
---|
Objectif
Créer un tableau pouvant être détruit lorsqu’on en a plus besoin. |
---|
Dans la partie C, nous avons vu deux sortes de tableaux :
- Les tableaux globaux, qui sont créés en début de l’exécution du programme et existent pendant toute la durée de son exécution. Ils peuvent être très grands (taille limitée par la mémoire disponible) et sont accessibles par toutes les fonctions du programme.
- Les tableaux locaux qui n’existent que pendant l’exécution de la fonction dans laquelle ils sont déclarés et ne sont accessibles que dans cette fonction où dans une autre fonction qui à accès à leur adresse mémoire. La taille d’un tableau local est limitée par celle de la pile d’exécution.
Mais il arrive qu’on souhaite utiliser un tableau de taille arbitrairement grande (dans la limite de la mémoire disponible) pendant une durée qui n’est ni celle de l’exécution du programme, ni celle de l’exécution d’une fonction particulière. Typiquement, un tableau créé dans une fonction et détruit dans une autre.
Moyen
Réservation d’un bloc mémoire dans le tas |
---|
Il existe une zone mémoire que nous n’avons encore jamais utilisée et qui est appelée le tas (Heap en anglais). Il est possible de réserver dans cette zone des blocs mémoire pouvant notamment être utilisés comme des tableaux. Voici un exemple de fonction qui crée dans le tas une chaîne de caractère contenant la concaténation de deux chaînes passées en arguments.
char* concat(const char* s1, const char* s2)
{
int n1 = strlen(s1);
int n2 = strlen(s2);
char* out = (char*) malloc((n1+n2+1)*sizeof(char)); // A
strcpy(out, s1); // B
strcpy(out + n1, s2); // C
return out; // D
}
La fonction main
suivante donne un exemple d’utilisation.
int main()
{
char t[] = "Tim ";
char u[] = "et Tom";
char* s = concat(t, u);
printf("%s\n", s);
return 0;
}
Il y a beaucoup de choses à dire à propos de cet exemple. La première est que, contrairement à la fonction standard strcat
, la fonction concat
crée une nouvelle chaîne, indépendante des deux chaînes désignées par les paramètres s1
et s2
, et qui est stockée dans une nouvelle zone mémoire située dans le tas.
Le bloc mémoire contenant la chaîne retournée par concat
est réservé dans le tas par un appel, en ligne A, à la fonction standard malloc
, dont le nom est une abréviation de "memory allocation".
void* malloc( size_t size );
Cette fonction réserve un bloc mémoire de la taille indiquée en paramètre et en retourne l’adresse. Si la réservation est impossible, la valeur de retour est NULL
. Cela se produit seulement quand il ne reste plus de zone mémoire de la taille demandée, soit parce que toute la mémoire à été utilisée, soit parce qu’elle est fragmentée.
La valeur de retour de malloc
est de type void*
, qu’on pourrait traduire par "pointeur sur n’importe quoi". En effet, malloc
peut être utilisée pour réserver des blocs permettant de stocker n’importe quel type de données. Un cast, aussi appelé transtypage, est nécessaire pour placer l’adresse du bloc dans la variable out
qui est de type pointeur sur char
. Dès lors, ce bloc pourra être utilisé comme un tableau de caractères, et donc contenir une chaîne.
Exercice de découverte E.dec.x1
Pourquoi la taille du bloc est-elle égale à 1 plus la somme des tailles des chaînes désignées par les paramètres ?
Le bloc doit recevoir la concaténation des chaînes désignées en paramètres. Il faut que sa taille permette le stockage des caractères de ces deux chaînes plus le marqueur de fin de chaîne.
L’exécution de la ligne A place donc dans la variable out
l’adresse d’un bloc mémoire de n1 + n2 + 1
octets situé dans le tas.
char* out = (char*) malloc((n1+ n2+1)*sizeof(char)); // A
Le bloc retourné par malloc n’est pas initialisé. Son contenu est imprévisible.
En vertu de la correspondance entre tableaux et pointeurs, l’adresse du bloc peut être utilisée comme un nom de tableau et le bloc peut être utilisé comme un tableau.
Exercice de découverte E.dec.x2
Donnez les lignes de code à ajouter juste après la ligne A pour initialiser à 0xFF
tous les octets du bloc venant d’être réservé.
for(int i=0; i< n1+n2+1; i++)
{
out[i] = 0xFF;
}
Le tableau dynamique s’utilise strictement de le même manière que les tableaux que nous avons vu jusqu’ici.
Le reste de la définition de la fonction concat
fait appel aux connaissances abordées précédemment. On copie bout à bout les chaînes à concaténer dans le nouveau bloc mémoire, dont l’adresse est ensuite retournée.
Exercice de découverte E.dec.x3
La compilation de la fonction suivante, censée avoir le même rôle que la version précédente, produit un avertissement.
char* concat(const char* s1, const char* s2)
{
int n1 = strlen(s1);
int n2 = strlen(s2);
char out[n1 + n2 + 1];
strcpy(out, s1);
strcpy(out + n1, s2);
return out;
}
Qu’est-ce qui ne va pas avec cette fonction ?
Le tableau out
est une variable locale. Contrairement à un tableau dynamique réservé dans le tas, ce tableau local est détruit à l’issue de l’exécution de la fonction. Son utilisation ultérieure peut avoir des conséquences imprévisibles et entraîner de graves problèmes tels que plantage, écrasement des valeurs de certaines variables…
Exercice de découverte E.dec.x4
La création de la chaîne retournée par la fonction concat
est réalisée par ces deux lignes :
strcpy(out, s1);
strcpy(out + n1, s2);
On aurait pu la réaliser comme ceci :
strcpy(out, s1);
strcat(out, s2);
Quel est l’intérêt de la première solution par rapport à la deuxième ?
La fonction strcat
doit d’abord localiser le marqueur de fin de la chaîne de destination avant de commencer à recopier à la suite la chaîne désignée par s2
. La première version est plus efficace, bien que la différence ne soit pas visible en pratique.
Bonus
Réserver un bloc initialisé à 0 avec calloc |
---|
Nous avons vu que la fonction malloc
n’initialise pas le bloc qu’elle réserve dans le tas. Il existe une autre fonction permettant de réserver un bloc et de le remplir avec des valeurs 0.
void* calloc( size_t elementCount, size_t elementSize );
La taille du bloc réservé est égale à elementCount * elementSize
. Ce bloc est initialisé avec des octets 0x00
. Par exemple, la ligne suivante…
int* tab = (int*)calloc(6, sizeof(int));
…réserve un bloc mémoire utilisable comme un tableau d’entiers de 6 cellules contenant initialement chacune le valeur 0. Ces cellules sont accessibles avec la notation habituelle tab[0]
, tab[1]
, … tab[5]
.
Exercice de découverte E.dec.x5
Donnez les lignes de code permettant de créer un tableau dynamique de double
de taille 10 initialisé avec des valeurs 0.0
double* t1 = (double*)calloc(10,sizeof(double));
Comme la valeur double 0.0 est constituée d’octets 0, le tableau dynamique est initialisé avec des valeurs 0.0.
Exercice de découverte E.dec.x6
Donnez les lignes de code permettant de créer un tableau dynamique de double
de taille 10 initialisé avec des valeurs 1.0.
double* t2 = (double*)malloc(10*sizeof(double));
for(int i=0; i<10; i++) t2[i] = t1.0;
Il aurait été dommage d’utiliser calloc
, qui prend du temps à remplir le bloc de 0, alors qu’immédiatement après on va remplie le tableau dynamique avec des valeurs 1.0.
E.dec2 : Gestion de la mémoire
Prérequis
Partie C et section précédente |
---|
Objectif
Utiliser de manière rationnelle la mémoire en fonction des besoins |
---|
Dans la section précédente, nous avons vu comment créer des tableaux dynamiques. Mais pour ne pas gaspiller de la mémoire, il est important de libérer tout bloc mémoire qui n’est plus utilisé.
On peut présenter les choses de la manière suivante :
- À chaque fois qu’un bloc mémoire est réservé par un appel à
calloc
oumalloc
, on contracte une dette mémoire. - Cette dette doit être remboursée en libérant chaque bloc ayant été réservé.
Moyen
La fonction free |
---|
Tout bloc mémoire réservé avec calloc
ou malloc
doit être libéré avec free
. Un bloc ne doit être libéré qu’une seule fois. Dans la suite, une tentative d’utiliser free à mauvais escient, comme par exemple avec une adresse ne correspondant pas à un bloc réservé dans le tas ou à un bloc déjà libéré, sera appelée accident mémoire. Ce type de situation provoque souvent un plantage du programme.
void free( void* pointer );
Observons cet exemple d’utilisation de la fonction concat
définie dans la section précédente.
int main()
{
char* s = concat("Tim ", "et Tom "); // Ligne A
s = concat(s, "sont sur un bateau.\n"); // Ligne B
printf("%s\n", s);
return 0;
}
Chaque concat
réserve un bloc mémoire dans le tas, mais aucun de ces blocs n’est libéré explicitement. On a ce qu’on appelle une fuite mémoire. Les blocs sont automatiquement libérés à l’issue de l’exécution du programme, mais les bonnes pratiques de programmation imposent qu’ils soit libérés avant la fin de l’exécution, par utilisation de free
.
Avant de corriger notre programme, examinons ce qui se passe en mémoire après l’exécution de la ligne A et après l’exécution de la ligne B.
On voit que le premier bloc, qui contient la chaîne "Tim et Tom "
, n’a pas été libéré avant que l’adresse du deuxième soit placée dans la variable s
. En plus, l’adresse de ce bloc a été perdue, écrasée dans s
par celle du nouveau bloc, il ne pourrait donc plus être libéré par free
, même si on le voulait. On dit qu’il y a une fuite mémoire lorsqu’un bloc mémoire est réservé dans le tas et n’est jamais libéré par free
.
Voici la bonne manière de procéder pour obtenir le même résultat, c’est à dire une chaîne obtenue par la concaténation de "Tim "
, "et Tom "
et "sont sur un bateau"
pointée par s
, qui est affichée à l’écran.
int main()
{
char* s = concat("Tim ", "et Tom ");
char* t = concat(s, "sont sur un bateau.\n");
free(s);
printf("%s\n", t);
free(t);
return 0;
}
La première chaîne produite par concat
est pointée par s
. On doit en garder l’adresse pour pouvoir libérer ultérieurement la mémoire qu’elle occupe dans le tas. On place donc le résultat du deuxième appel de concat
dans une nouvelle variable t
. On peut libérer le premier bloc, puis utiliser le deuxième, pointé par t
. Une fois qu’on en a plus besoin on libère la mémoire qu’il occupe dans le tas.
Exercice de découverte E.dec.x7
Tentez d’identifier le problème dans le programme suivant.
int main()
{
char* s = malloc(10 * sizeof(char)); // Ligne A
s = "Bonjour"; // Ligne B
printf("%s\n", s); // Ligne B
free(s); // Ligne C
}
On a une fuite mémoire et un accident mémoire. Après exécution de la ligne A, la variable s
pointe un bloc mémoire réservé dans le tas. Mais immédiatement après, en ligne B, la variable s reçoit l’adresse d’une chaîne située dans la zone mémoire réservée aux constantes.
L’instruction s = "Bonjour"
ne copie pas "Bonjour"
dans le bloc mémoire désigné par s
. Pour celà il faudrait utiliser strcpy
. Elle copie l’adresse de la chaîne "Bonjour"
dans la variable s
. La ligne B s’exécute sans problème et affiche "Bonjour", mais la ligne C tente de libérer un bloc mémoire qui n’est pas dans le tas et n’est pas celui réservé par malloc
. On a donc accident mémoire. Et comme le bloc réservé par malloc
‘est pas libéré par un appel à free, on a une fuite mémoire.
Voici une version correcte du programme :
int main()
{
char* s = malloc(10 * sizeof(char)); // Ligne A
strcpy(s, "Bonjour"); // Ligne B
printf("%s\n", s); // Ligne B
free(s); // Ligne C
}
Bonus
Changer la taille d’un tableau avec realloc |
---|
La fonction standard realloc
permet de modifier la taille d’un bloc déjà réservé. Seuls les bloc réservés dans le tas peuvent être redimensionnés. Redimensionner un bloc ne change pas la dette mémoire associée. Le bloc devra être libéré, une seule fois, par un appel de la fonction free
.
void* realloc( void* pointer, size_t memorySize );
Le premier paramètre est l’adresse du bloc à redimensionner. Si sa valeur est NULL
, alors realloc
réserve un nouveau bloc, comme le ferait malloc
. Le deuxième paramètre est la nouvelle taille du bloc. Elle peut être plus petite ou plus grande que la taille précédente.
Attention, cette opération peut déplacer le bloc et donc changer son adresse. La valeur de retour est la nouvelle adresse du bloc. Si le bloc est déplacé, sont ancien contenu est recopié au nouvel emplacement.
Cet exemple montre comment on utilise realloc
pour augmenter la taille d’un bloc.
int main()
{
char* s = malloc(4*sizeof(char)); // Ligne A
strcpy(s, "aaa"); // Ligne B
s = realloc(s, 7*sizeof(char)); // Ligne C
strcat(s, "bbb"); // Ligne D
printf("%s\n", s);
free(s); // Ligne E
}
En ligne A, on réserve un bloc mémoire dans lequel est recopié en ligne B la chaîne "aaa"
. On note que ce bloc doit avoir une taille permettant de stocker 4 caractères, en comptant le marqueur de fin.
En ligne C, on augmente la taille du bloc pointé par s
, de manière à pouvoir concéténer, en ligne D, la chaîne "bbb"
à celle initialement contenue dans le bloc.
En ligne E, la mémoire occupée par le bloc est libérée.
Exercice de découverte E.dec.x8
Voici une variante du programme précédent qui pose problème. Identifiez s’il y a fuite ou accident mémoire et expliquez pourquoi.
int main()
{
char* s = malloc(4*sizeof(char)); // Ligne A
strcpy(s, "aaa"); // Ligne B
char* t = realloc(s, 7*sizeof(char)); // Ligne C
strcat(s, "bbb"); // Ligne D
printf("%s\n", t);
free(s); // Ligne E
free(t); // Ligne F
}
Deux scénarios sont envisageables et conduisent tous les deux à un accident mémoire.
Soit realloc
ne change pas l’adresse du bloc. Dans ce cas, les variables t
et s
pointent le même bloc, qui sera détruit deux fois. On a donc un accident mémoire.
Soit la réallocation change l’adresse du bloc, alors t
pointe le nouvel emplacement et free(t)
libérera correctement la mémoire. Mais s
pointera l’ancien emplacement où ne se trouve plus de bloc réservé. L’appel free(s)
provoquera un accident mémoire.
E.dec3 : Tableau dynamique à 2 dimensions
Prérequis
Parties C et D et section précédentes de cette partie E. |
---|
Objectif
Exploiter des tableaux à 2 dimensions situé dans le tas |
---|
Jusqu’à maintenant, nous n’avons utilisé que des tableau à une dimension. Il existe plusieurs manières de gérer en C des tableaux à plusieurs dimensions. L’approche native consiste à représenter de tels tableaux par un seul bloc mémoire. Elle est compliquée et manque de souplesse. Nous ne l’aborderons pas dans cet enseignement. Nous allons adopter une approche plus moderne, comparable à celle utilisée par le langage Java
.
Moyen
Utiliser un tableau de pointeurs sur des tableaux |
---|
Nous allons présenter le principe utilisé avec un exemple dans lequel on représente des matrices carrées de dimension 3 qui pourraient être utilisées pour modéliser des transformations géométriques.
Pour éviter d’utiliser une constante numérique littérale et pour pouvoir modifier facilement la dimension de nos matrices, nous définissions la constante :
#define DIM 3
Voici la représentation en mémoire d’un tableau de 3 lignes et 3 colonnes.
La variable m
désigne un tableau dont chaque cellule pointe un tableau dont chaque cellule contient une valeur de type double
.
Les valeurs située dans le tableau désigné par m
sont de type double*
, donc m doit être de type double**
. Ce premier tableau doit être réservé par la ligne :
double** mat = (double**)malloc(DIM*sizeof(double*));
Chaque cellule de ce tableau désigne une ligne du tableau 2D, qui est elle même représentée par un tableau de double
. Donc par exemple pour créer la première ligne il faut écrire :
mat[0] = (double*)calloc(DIM, sizeof(double));
La création d’un tableau de 3 x 3 nécessite donc la réservation de 4 blocs mémoire dans le tas. Il est judicieux de regrouper toutes les opérations nécessaires dans une fonction.
double** creeMatrice()
{
double** mat = (double**)malloc(DIM*sizeof(double*));
for(int i=0; i<DIM; i++)
{
mat[i] = (double*)calloc(DIM, sizeof(double));
}
return mat;
}
La fonction creeMatrice
réalise la création d’une matrice carrée de dimension DIM
dont les valeurs, de type double
, sont initialisées à 0.0.
La valeur située en ligne i
et colonne j
est dans la cellule mat[i][j]
. En effet, mat[i]
est un pointeur qui désigne le tableau représentant la ligne i, et en vertu de la correspondance entre tableaux et pointeurs, mat[i][j]
est la cellule d’indice j
de cette ligne.
Exercice de découverte E.dec.x9
Définissez la fonction…
void afficheMatrice(double** mat);
…qui affiche la matrice désignée par mat
. Chaque valeur doit être affichée au format %.02f
. Les valeurs de chaque ligne doivent être séparées par des espaces.
void afficheMatrice(double** mat)
{
for(int i=0; i<DIM; i++)
{
for(int j=0; j<DIM; j++)
{
printf(" %.02f", mat[i][j]);
}
printf("\n");
}
}
Exercice de découverte E.dec.x10
Réalisez une fonction main
qui crée une matrice en utilisant la fonction creeMatrice
et qui l’affiche en utilisant la fonction afficheMatrice
.
int main()
{
double** m = creeMatrice();
afficheMatrice(m);
}
Pour libérer correctement la mémoire occupée par une matrice, il faut libérer tous les blocs mémoires qui la constituent. Pour une matrice de 3 x 3, cela suppose 4 appels à free
. Pour faciliter cette tâche, il est judicieux de définir une fonction :
void detruitMatrice(double** m)
{
for(int i=0; i<DIM; i++)
{
free(m[i]);
}
free(m);
}
Bonus
Simplifier les notation en définissant un type. |
---|
Les notations avec plusieurs étoiles peuvent paraître un peu lourdes. Il est possible de les alléger en définissant de nouveaux type à l’aide de typedef
. Par exemple, si on écrit…
typedef double** matrice;
…on définit un type matrice
qui est équivalent à double**
. Voici le code des fonctions de création, affichage et destruction de matrices utilisant ce type :
#define DIM 3
typedef double** matrice;
matrice creeMatrice()
{
matrice mat = (matrice)malloc(DIM*sizeof(double*));
for(int i=0; i<DIM; i++)
{
mat[i] = (double*)calloc(DIM, sizeof(double));
}
return mat;
}
void afficheMatrice(matrice mat)
{
for(int i=0; i<DIM; i++)
{
for(int j=0; j<DIM; j++)
{
printf(" %.02f", mat[i][j]);
}
printf("\n");
}
}
void detruitMatrice(matrice m)
{
for(int i=0; i<DIM; i++)
{
free(m[i]);
}
free(m);
}
Exercice de découverte E.dec.x11
Définissez une fonction main
qui crée la matrice :
\[ \begin{pmatrix}
1.0 & 0.0 & 0.0\\
0.0 & 1.0 & 0.0\\
0.0 & 0.0 & 1.0
\end{pmatrix}
\]
Puis l’affiche et la détruit.
int main()
{
matrice m = creeMatrice();
m[0][0] = 1.0; m[1][1] = 1.0; m[1][1] = 1.0;
afficheMatrice(m);
detruitMatrice(m);
return 0;
}
L’activité est terminée. Vous devez évaluer votre réussite de cette activité par une valeur entre 1 et 4.
- Vous n’avez presque rien compris et échoué à plus de la moitié des exercices de découverte.
- Vous estimez avoir compris une partie des explications et vous avez réussi au moins la moitié des exercices de découverte, mais certaines notions sont encore floues.
- Vous estimez avoir bien compris les explications et vous avez réussi presque tous les exercices de découverte, mais certaines notions abordées étaient nouvelles et vous penser avoir besoin d’un entrainement pour bien les assimiler et être en mesure de les utiliser en pratique.
- Vous connaissiez déjà tout ce qui a été présenté (sauf peut être quelques détails) et il ne sera pas nécessaire que vous reveniez sur ces notions.
Si votre score est 1 ou 2, vous devrez refaire cette activité rapidement, mais après au moins une nuit de sommeil, avant de passer aux activités suivantes. Si votre score est 3 ou 4, vous devez faire les activités suivantes et il ne sera pas nécessaire de revenir sur cette activité, à part pour rechercher des informations utiles pour d’autres activités.
Activité d’entraînement E.ent
Cette activité est essentielle. C’est grâce à elle que vous apprendrez vraiment à comprendre et utiliser les notions vues précédemment. Elle se décompose en plusieurs itérations : ent1, ent2, etc.
Vous devez réaliser au moins une itération et évaluer votre niveau de réussite par une valeur entre 1 et 4 selon les critères habituels. L’itération que vous avez traité le plus récemment est appelée itération courante. On appelle itération suivante celle qui suit l’itération courante, excepté si l’itération courante est la dernière disponible, auquel cas l’itération suivante est la première itération.
Si le résultat est 1, vous devrez traiter à nouveau l’activité de découverte précédente, en laissant passer au moins une nuit, avant de revenir à cette activité d’entraînement en réalisant à nouveau l’itération courante. Vous devez aussi essayer d’identifier les points de blocage et demander à votre enseignant de vous aider à les surmonter.
Si le résultat est 2, vous devrez d’abord comprendre ce qui vous a conduit à des erreurs et / ou ce qui vous a bloqué. Si vous avez des doutes, des difficultés, essayez de trouver les réponses dans les présentations et explication de l’activité de découverte précédente et demandez toute l’aide nécessaire à votre enseignant. Ensuite, vous devrez réaliser l’itération suivante de l’activité d’entraînement actuelle après un délai d’au moins deux jours et d’au plus une semaine.
Si le résultat est 3, vous devrez revenir traiter l’itération suivante après un délai de plusieurs semaines.
En traitant les exercices, vous devrez considérer que les paramètres des fonctions ont des valeurs correctes. Il ne vous est pas demandé de vérifier si certaines valeurs sont aberrantes au regard de ce que demande l’énoncé. Par exemple, si un paramètre doit être positif pour que le traitement demandé soit possible, vous considérerez que la fonction sera appelée avec une valeur positive de ce paramètre.
Vous pouvez utiliser un ordinateur pour tester vos solutions et les corriger en cas de comportement inattendu. C’est même recommandé, à condition de commencer par réfléchir avant de vous jeter sur le clavier. Si vous pensez avoir une solution différente de celle proposée, ce qui peut arriver souvent, vous ne devez en aucun cas rester dans le doute. Vous devez vérifier si votre solution est correcte ou non, soit en testant votre solution avec un ordinateur, soit en demandant à une personne enseignante.
Itération E.ent1
Exercice d’entrainement E.ent1.1
Définissez une fonction…
char* randStr(const char* caracteres, int n);
…qui produit une chaîne de n
caractères tirés aléatoirement dans la chaîne caracteres
. La chaîne produite doit être située dans le tas. Par exemple, la ligne…
char* s = randStr("abc", 4);
…produit une chaîne de 4 caractères tirés aléatoirement parmi les caractères a
, b
et c
. Une telle chaîne pourrait par exemple être "abba"
, ou "cabc"
.
char* randStr(const char* caracteres, int n)
{
char* out = (char*)malloc((n+1)*sizeof(char)); // Ligne A
int nchars = strlen(caracteres); // Ligne B
// À compléter
return out; // Ligne C
}
Voici le début du code de la fonction. La ligne A réserve le bloc mémoire qui contiendra la chaîne produite. Attention, la chaîne doit contenir n
caractères plus un marqueur de fin. La ligne B détermine la longueur de la chaîne contenant les caractères éligibles pour être placés dans le chaîna à produire. La ligne C retourne l’adresse de la chaîne produite après qu’elle ait été remplie avec des caractères tirés aléatoirement dans la chaîne caracteres
, sans oublier le marqueur de fin.
char* randStr(const char* caracteres, int n)
{
char* out = (char*)malloc((n+1)*sizeof(char));
int nchars = strlen(caracteres);
for(int i=0; i<n; i++)
{
out[i] = caracteres[rand()%nchars];
}
out[n] = 0;
return out;
}
Définissez une fonction main
qui crée (en utilisant randStr
) une chaîne aléatoire constituée de 6 lettre majuscules tirées au hasard dans l’alphabet, puis qui affiche cette chaîne. La mémoire réservée dans le tas doit être libérée après utilisation.
int main()
{
srand(time(NULL));
char* s = randStr("ABCDEFGHIJKLMNOPQRSTUVWXYZ", 6);
printf("%s\n", s);
free(s);
}
Exercice d’entrainement E.ent1.2
Donnez les lignes de code permettant de réaliser les actions suivantes :
- Créer dans la pile un tableau d’entiers de taille 5 initialisé avec les valeurs 1,2,3,4,5.
- Créer dans le tas un tableau d’entiers de taille 5 non initialisé.
- Recopier les valeurs du premier tableau dans le deuxième.
- Placer la valeur 12 dans la cellule d’indice 2 du deuxième tableau.
- Libérer la mémoire ayant été réservée dans le tas.
int t[] = {1,2,3,5,4};
int* w = (int*)malloc(5 * sizeof(int));
for(int i=0; i<5; i++) w[i] = t[i];
w[2] = 12;
free(w);
Exercice d’entrainement E.ent1.3
Cet exercice est un bon entraînement pour ce qui concerne les pointeurs, tableaux et gestion dynamique de la mémoire. Mais pour une implémentation plus professionnelle du concept, il est préférable d’utiliser en plus la notion de structure, qui sera présentée dans la partie suivante. Il ne faut donc pas considérer l’implémentation proposée ici comme une solution définitive, mais comme une étape d’apprentissage.
On reprend le tableau 2D définit par le type matrice
présenté précédemment.
#define DIM 3
typedef double** matrice;
matrice creeMatrice()
{
matrice mat = (matrice)malloc(DIM*sizeof(double*));
for(int i=0; i<DIM; i++)
{
mat[i] = (double*)calloc(DIM, sizeof(double));
}
return mat;
}
void afficheMatrice(matrice mat)
{
for(int i=0; i<DIM; i++)
{
for(int j=0; j<DIM; j++)
{
printf(" %.02f", mat[i][j]);
}
printf("\n");
}
}
void detruitMatrice(matrice m)
{
for(int i=0; i<DIM; i++)
{
free(m[i]);
}
free(m);
}
Vous devez faire une première modification pour représenter, avec un type que nous nommerons tab2Ddouble
, des tableaux dont le nombre de lignes est définit par une constante NLIGNES
et le nombre de colonnes par une constante NCOLONNES
.
#define NLIGNES 2
#define NCOLONNES 3
// À compléter
tab2Ddouble creeTab2D()
{
// À compléter
}
void afficheTab2D(tab2Ddouble tab)
{
// À compléter
}
void detruitTab2D(tab2Ddouble tab)
{
// À compléter
}
#define NLIGNES 2
#define NCOLONNES 3
typedef double** tab2Ddouble;
tab2Ddouble creeTab2D()
{
tab2Ddouble tab = (tab2Ddouble)malloc(NLIGNES*sizeof(double*));
for(int i=0; i<NLIGNES; i++)
{
tab[i] = (double*)calloc(NCOLONNES, sizeof(double));
}
return tab;
}
void afficheTab2D(tab2Ddouble tab)
{
for(int i=0; i<NLIGNES; i++)
{
for(int j=0; j<NCOLONNES; j++)
{
printf(" %.02f", tab[i][j]);
}
printf("\n");
}
}
void detruitTab2D(tab2Ddouble tab)
{
for(int i=0; i<NLIGNES; i++)
{
free(tab);
}
free(tab);
}
Une personne débutante en programmation C écrit la fonction suivante ayant pour rôle d’échanger les contenus de deux tableaux.
void exgTab2D(tab2Ddouble t1, tab2Ddouble t2)
{
tab2Ddouble tmp = t1;
t1 = t2;
t2 = tmp;
}
Mais la fonction ne remplit pas son rôle. Si on exécute le programme suivant :
int main()
{
tab2Ddouble tab1 = creeTab2D();
tab2Ddouble tab2 = creeTab2D();
tab1[0][0] = 3.0;
tab2[1][2] = 9.0;
printf("Avant :\n");
afficheTab2D(tab1);
printf("\n");
afficheTab2D(tab2);
exgTab2D(tab1, tab2);
printf("\nAprès :\n");
afficheTab2D(tab1);
printf("\n");
afficheTab2D(tab2);
return 0;
}
On obtient exactement le même affichage avant et après appel de la fonction. Les tableaux désignés par tab1
et tab2
n’ont pas été échangés. Vous devez menez une enquête afin de trouver une explication du phénomène.
Quand on appelle exTab2D(tab1, tab2)
, les valeurs de tab1
et tab2
, c’est à dire les adresses des 2 tableaux, sont copiées dans les paramètres t1
et t2
de la fonction. Ce sont ce copies que le fonction échange, ce qui n’affecte pas les contenus des variables tab1
et tab2
.
Voici la situation en mémoire au début de l’exécution de la fonction exTab2D
.
Et voici la situation en mémoire à la fin de l’exécution de cette fonction.
Vous devez trouver un moyen de coder cette fonction de manière à ce que les contenus des tableaux soient effectivement échangés.
On pourrait échanger chaque valeur du premier tableau avec la valeur située en même position dans le deuxième, mais une solution plus rapide est d’échanger le pointeur sur chaque ligne d’un des tableau avec le pointeur sur la ligne ayant la même position dans l’autre.
void exgTab2D(tab2Ddouble t1, tab2Ddouble t2)
{
for(int i=0; i<NLIGNES; i++)
{
double* tmp = t1[i];
t1[i] = t2[i];
t2[i] = tmp;
}
}
Définissez une variante qui accepte en paramètres les adresses des variables désignant les tableaux à échanger :
void exgTab2D(tab2Ddouble* t1, tab2Ddouble* t2);
Donnez la modification à faire dans la fonction main
précédente pour appeler correctement la nouvelle fonction d’échange.
void exgTab2D(tab2Ddouble* t1, tab2Ddouble* t2)
{
tab2Ddouble tmp = *t1;
*t1 = *t2;
*t2 = tmp;
}
Ce sont les valeurs pointées par t1 et t2 qu’il faut échanger, et on accède à ces valeurs par l’opérateur de "dépointage" *
.
Dans la fonction main
, il faut remplacer la ligne…
exgTab2D(tab1, tab2);
…par la ligne :
exgTab2D(&tab1, &tab2);
En effet, ce sont les adresses des variables tab1
et tab2
qui doivent être passées en arguments.
Dessinez la situation en mémoire au début et à la fin de l’exécution de cette nouvelle variante de la fonction appelée par main
.
Au début de l’exécution :
À la fin de l’exécution :
Itération E.ent2
Exercice d’entrainement E.ent2.1
Définissez une fonction…
char* cloneStr(const char* s);
…qui produit une chaîne située dans le tas, strictement identique à la chaîne désignée par s
, mais complètement indépendante en mémoire (une modification de la chaîne produite ne doit pas affecter la chaîne ayant servi de modèle, et réciproquement).
char* cloneStr(const char* s)
{
int n = strlen(s);
char* out = (char*)malloc((n+1)*sizeof(char));
// À compléter
}
char* cloneStr(const char* s)
{
int n = strlen(s);
char* out = (char*)malloc((n+1)*sizeof(char));
strcpy(out, s);
return out;
}
Définissez une fonction main
qui crée (en utilisant cloneStr
) une copie de la chaîne "Bonjour"
, puis qui affiche cette chaîne. La mémoire réservée dans le tas doit être libérée après utilisation.
int main()
{
char* s = cloneStr("Bonjour");
printf("%s\n", s);
free(s);
}
Exercice d’entrainement E.ent2.2
Donnez les lignes de code permettant de réaliser les actions suivantes :
- Créer dans le tas un tableau d’entiers de taille 5 dont les 4 premières cellules contiennent la valeur 0 et la dernière cellule contient la valeur 2.
- Créer dans la pile tableau d’entiers de taille 5 non initialisé.
- Recopier les valeurs du premier tableau dans le deuxième.
- Libérer la mémoire ayant été réservée dans le tas.
int* t = (int*)calloc(5, sizeof(int));
t[4] = 2;
int w[5];
for(int i=0; i<5; i++) w[i] = t[i];
free(t);
Exercice d’entrainement E.ent2.3
Cet exercice est un bon entraînement pour ce qui concerne les pointeurs, tableaux et gestion dynamique de la mémoire. Mais pour une implémentation plus professionnelle du concept, il est préférable d’utiliser en plus la notion de structure, qui sera présentée dans la partie suivante. Il ne faut donc pas considérer l’implémentation proposée ici comme une solution définitive, mais comme une étape d’apprentissage.
On définit la constante et le type suivants :
#define SIZE 3
typedef char** tabstr;
Le type tabstr
représente un tableau de chaînes de caractères de taille SIZE
dont certaines cellules peuvent être vides. Une cellule vide contient la valeur NULL
, alors qu’une cellule non vide contient un pointeur sur char
désignant une chaîne stockée dans le tas.
Les fonctions suivantes permettent l’utilisation de tableaux de chaîne de type tabstr
.
// Crée un tableau de SIZE cellules vides.
tabstr creeTabstr();
// Place une copie de la chaîne désignée par str dans la cellule i
// du tableau désigné par tab.
void assigne(tabstr tab, int i, char* str);
// Affiche le contenu du tableau désigné par tab.
// Les cellules vides sont représentées par ---
void affiche(tabstr tab);
// Libère toute la mémoire occupée par le tableau désigné par tab.
void detruit(tabstr tab);
Voici un exemple de mise en oeuvre de ces fonctions :
int main()
{
tabstr t = creeTabstr();
assigne(t,0,"Tim");
assigne(t,1,"Tom");
assigne(t,0,"Anne");
affiche(t);
detruit(t);
return 0;
}
L’exécution de main
produit l’affichage :
Anne
Tom
---
Dessinez la situation en mémoire juste après l’exécution de la fonction affiche
appelé par main
.
Donnez les définitions des fonctions suivantes :
creeTabstr
affiche
assigne
detruit
Vous devez prêter une attention particulière à éviter les fuites et accidents mémoire. Lorsqu’on crée un tableau dynamique de pointeurs avec calloc
, ces cellules contiennent toutes la valeur NULL
, qui est représentée en mémoire par des octets ayant la valeur 0.
Pour coder correctement les fonctions affiche
, assigne
et detruit
, il faut bien distinguer le cas d’une cellule contenant NULL
d’une cellule pointant une chaîne préalablement assignée.
tabstr creeTabstr()
{
tabstr out = (tabstr)calloc(SIZE, sizeof(char*));
return out;
}
On réserve un bloc mémoire qui sera utilisé comme tableau de SIZE
pointeurs de type char*
. La fonction calloc
initialise le bloc avec des 0, donc les cellules du tableau contiennent NULL
.
void affiche(tabstr tab)
{
for(int i=0; i<SIZE; i++)
{
if(tab[i]==NULL) printf("---\n");
else printf("%s\n", tab[i]);
}
}
Il ne faut pas utiliser printf
avec le format %s
dans le cas où la cellule contient NULL
, mais faire un affichage spécifique dans ce cas.
void assigne(tabstr tab, int i, char* str)
{
if(tab[i] != NULL) free(tab[i]);
int n = strlen(str);
tab[i] = (char*)malloc((n+1)*sizeof(char));
strcpy(tab[i], str);
}
Il faut penser à beaucoup de choses pour coder correctement cette fonction :
- Si la cellule à assigner pointe déjà une chaîne, c’est à dire si elle a déjà été préalablement assignée, il faut libérer la mémoire occupée par la chaîne précédente, sinon on aura une fuite mémoire.
- Il faut réserver un bloc mémoire pour y placer la copie de la chaîne à assigner à la cellule d’indice i.
- La taille du bloc à réserver doit prévoir d’y placer la copie de la chaîne à assigner sans oublier le marqueur de fin.
- La copie de la chaîne à assigner dans le bloc destiné à la contenir doit être réalisée par un appel à la fonction
strcpy
et en aucun cas avec=
.
void detruit(tabstr tab)
{
for(int i=0; i<SIZE; i++)
{
if(tab[i] != NULL) free(tab[i]);
}
free(tab);
}
Il faut libérer la mémoire utilisée pour stocker les chaînes, mais il peut y avoir des cellules vides (contenant NULL
), qui ne pointent donc aucune chaîne. Il ne faut jamais appeler free avec NULL
en argument, cela provoque un accident mémoire.
Itération E.ent3
Exercice d’entrainement E.ent3.1
Définissez une fonction…
char* addChar(const char* s, char c);
…qui produit une obtenue en ajoutant le caractère c
a une copie de la chaîne désignée par s
. Par exemple, l’appel addChar("Tim", 'e')
retourne l’adresse d’une chaîne "Time"
située dans le tas.
char* addChar(const char* s, char c)
{
int n = strlen(s);
char* out = (char*)malloc((n+2)*sizeof(char));
// À compléter
return out;
}
La partie de la fonction donnée ici réserve le bloc mémoire dans lequel sera placé la chaîne à produire. Il faut prévoir la place pour placer le caractère à ajouter et le marqueur de fin. Il reste à terminer la construction.
char* addChar(const char* s, char c)
{
int n = strlen(s);
char* out = (char*)malloc((n+2)*sizeof(char));
strcpy(out, s);
out[n] = c;
out[n+1] = 0;
return out;
}
Remarque : la fonction strcat
n’est pas utilisable pour concaténer un caractère à une chaîne. Le caractère est ajouté directement dans le tableau représentant la chaîne, à l’emplacement approprié.
Exercice d’entrainement E.ent3.2
Donnez les lignes de code permettant de réaliser les actions suivantes :
- Créer dans la pile une chaîne initialisée avec "abcde".
- Créer dans le tas un tableau de 6 caractères.
- Recopier dans ce tableau la chaîne créée à l’étape 1.
- Libérer la mémoire ayant été réservée dans le tas.
char t[] = "abcde";
char* s = malloc(6 * sizeof(char));
strcpy(s, t);
free(t);
Activité de consolidation E.top
Réaliser cette activité consiste à traiter un des exercices de consolidation proposés. Si vous réalisez plusieurs fois cette activité, laissez passer au moins une semaine entre chaque itération et traitez à chaque fois un exercice différent.
Exercice de consolidation C.top1
On appelle tableau informé de taille n un tableau d’entiers dont les valeurs sont stockées aux indices 1 à n et la longueur est stockée à l’indice 0. Un tel tableau est donc représenté par un tableau de n+1 cellules de type int
. Voici un exemple de tableau de valeur de type int
représentant le tableau informé de taille 4 contenant les valeurs 2, 3, 5, 7 :
On définit le type tabinf
pour représenter les tableaux informés :
typedef int* tabinf;
Pour rappel, cette notation signifie que l’identificateur tabinf
est équivalent à int*
. En effet, un tableau informé est représenté par un tableau de valeurs de type int
, qui est désigné, en vertu de la correspondance entre pointeurs et tableaux, par un pointeur sur int
.
Votre première mission consiste à définir une fonction…
tabinf creeTabinf(int n);
…qui crée un tableau informé situé dans le tas contenant n
valeurs 0.
tabinf creeTabinf(int n)
{
tabinf out = calloc(n+1, sizeof(int)); // Ligne A
out[0] = n; // Ligne B
return out; // Ligne C
}
En ligne A, un bloc mémoire permettant de stocker n+1 entiers est réservé. Ce bloc est initialisé avec des valeurs 0 et est utilisable comme un tableau classique de valeurs de type int
.
En ligne B, la valeur n
est placée dans la première cellule du tableau venant d’être créé, puisque ce tableau représente un tableau informé de taille n
.
La ligne C retourne l’adresse du tableau informé créé.
Vous devez maintenant définir une fonction…
void printTabinf(tabinf t)
…qui affiche les valeurs d’un tableau informé (pas sa taille) séparées par des espaces.
void printTabinf(tabinf t)
{
for(int i=1; i<=t[0]; i++)
{
printf(" %d", t[i]);
}
printf("\n");
}
Définissez une fonction main
qui
- Crée un tableau informé de taille 10 contenant des valeurs 0 sauf à l’indice 2 où il y a la valeur 12.
- Affiche ce tableau informé avec la fonction
printTabinf
. - Libère la mémoire utilisée par le tableau.
int main()
{
tabinf tab = creeTabinf(10);
tab[2] = 12;
printTabinf(tab);
free(tab);
return 0;
}
Nous attaquons à présent la partie plus technique de l’exercice. Vous devez définir une fonction…
void agrandit(tabinf* t, int delta);
…qui agrandit un tableau informé en lui ajoutant delta valeurs supplémentaires, toutes initialisées à 0. Vous ne devez pas utiliser (dans cette première version), la fonction realloc
. Donc vous devez créer un nouveau tableau plus grand et recopier dedans les valeur de l’ancien, sans oublier de détruire l’ancien ensuite, afin d’éviter toute fuite mémoire.
Il est très important de bien comprendre pourquoi le premier paramètre est de type tabinf*
, c’est à dire int**
, et ce que cela implique, à savoir que lors de l’appel de la fonction, il faut lui passer en premier argument l’adresse d’une variable de type tabinf
.
Voici un exemple de création, puis d’agrandissement d’un tableau informé.
int main()
{
tabinf tab = creeTabinf(10);
tab[2] = 12;
printTabinf(tab);
agrandit(&tab, 5);
printTabinf(tab);
free(tab);
return 0;
}
Vous devez réfléchir et répondre à la question suivante : Pourquoi une fonction agrandit
avec une paramètre de type tabinf
(c’est à dire int*
) ne pourrait pas permettre de réaliser correctement le travail demandé, à savoir remplacer le tableau informé initialement créé par une plus grand contenant les valeurs du premier et des 0 supplémentaires ?
L’agrandissement d’un tableau informé change son adresse mémoire, qui est contenue dans la variable tab
. Si on passe cette adresse en argument à la fonction d’agrandissement, c’est une copie de cette adresse qui sera transmise à la fonction. Lors de son exécution, la fonction agrandit
va modifier cette copie d’adresse, mais cela ne modifiera pas contenu de la variable tab
qui continuera de pointer sur l’ancien tableau ou sur son fantôme si la mémoire qu’il occupait a été libérée.
Voici une illustration en image du "scénario catastrophe".
Si vous avez bien compris pourquoi c’est l’adresse d’une variable pointant le tableau informé à agrandir qu’il faut passer en paramètre à la fonction agrandit
, et non directement l’adresse du tableau, alors vous pouvez continuer l’exercice en proposant une définition de la fonction demandée. Prenez le temps de bien réfléchir à toutes les étapes nécessaires.
void agrandit(tabinf* t, int delta)
{
tabinf tab = *t; // Ligne A
int n = tab[0]; // Ligne B
// À compléter
*t = newtab; // Ligne C
}
En ligne A, l’adresse d’implantation du tableau à agrandir, que nous appellerons ancien tableau, est récupérée et placée dans la variable tab
.
En ligne B, on place dans la variable n
la taille de l’ancien tableau.
En ligne C, on place dans la variable pointée par t
, c’est à dire la variable qui pointe initialement l’ancien tableau, l’adresse du nouveau tableau.
Voici la situation en mémoire après l’exécution de la ligne B, en supposant que la variable m
, dans la fonction main
, désigne le tableau à agrandir et que ce tableau, de taille 4, contient les valeurs 2, 3, 5 et 7.
Entre les ligne B et C, il faut :
- Créer le nouveau tableau en utilisant
calloc
. - Recopier les
n
valeurs de l’ancien tableau dans le nouveau. - Placer la valeur appropriée dans la première cellule du nouveau tableau.
- Détruire l’ancien tableau.
void agrandit(tabinf* t, int delta)
{
tabinf tab = *t;
int n = tab[0];
tabinf newtab = (tabinf)calloc(n+delta+1, sizeof(int));
for(int i=1; i<=n; i++) newtab[i] = tab[i];
newtab[0] = n+delta;
free(tab);
*t = newtab;
}
Proposez maintenant une variante de la fonction agrandit
utilisant realloc
au lieu de calloc
. Attention, realloc
, dans le cas d’un agrandissement, recopie les valeurs de l’ancien tableau dans le nouveau, mais n’initialise pas les valeurs suivantes du nouveau tableau.
void agrandit(tabinf* t, int delta)
{
tabinf tab = *t;
int n = tab[0];
tab = (tabinf)realloc(tab, (n+delta+1)*sizeof(int));
for(int i=n+1; i<=n+delta; i++) tab[i] = 0;
tab[0] = n+delta;
*t = tab;
}
Des variantes sont possibles. Chaque détail est important. Il est possible qu’une fonction se comporte apparemment correctement lors d’un test mais comporte néanmoins une erreur pouvant occasionner par exemple une fuite mémoire, ou placer des valeurs inopinées dans certaines cellules du nouveau tableau, mais pas à chaque fois. Un regard d’expert est nécessaire pour valider votre solution si elle est différente de celle-ci.