Au chapitre C2‑III , on a vu qu'il existait différents types pour déclarer une donnée en langages C/C++ : bool, int, float, etc. On parle de types élémentaires car ces derniers permettent de déclarer des données scalaires, c'est‑à‑dire ne contenant qu'un seul nombre.
Mais, comme dans tous les langages de programmation, il existe bien évidemment des possibilités pour déclarer des données plus complexes – et on parle alors de types dérivés :
- On peut déclarer des données en série toute de même type, formant une structure homogène qu'on appelle un tableau.
- On peut déclarer des données composées de types différents, dans ce qu'on appelle une structure hétérogène – ou tout simplement une structure.
Par rapport à l'emploi d'une multitude de données élémentaires indépendantes, le fait de déclarer des tableaux et des structures hétérogènes permet de rationaliser le code des programmes, donc de le rendre à la fois plus concis, plus lisible et plus sûr.
Les types de données structurées sont difficiles à maîtriser et ne seront abordés en détail que dans la partie C5 du module, après avoir acquis la notion de pointeur. Néanmoins, au regard de tous les avantages qu'ils apportent même pour des programmes simples, il est souhaitable d'en acquérir dès à présent des rudiments, permettant d'en faire un usage élémentaire dans les programmes donnés en exercices.
Le présent chapitre a justement pour objectif d'apporter au codeur débutant tous les éléments de syntaxe, pour pouvoir commencer à utiliser tant les tableaux que les structures hétérogènes. Pour cela, il faut au moins être capable de :
- déclarer des données d'un tel type ;
- accéder aux valeurs des données d'un tel type, en lecture comme en écriture, sachant que c'est sur ce dernier point que se concentrent les difficultés et qu'on se limitera ici à des opérations simples.
Enfin, ce chapitre ne serait pas complet s'il ne faisait pas une introduction à la notion de classe, qui dérive de celle de structure hétérogène et qui est essentielle pour comprendre la programmation orientée objet (POO) permise par le langage C++ (cf. chap. C1‑I ).
Comme on l'a souligné à maintes reprise, le framework Arduino est codé en C++ et fait massivement appel aux concepts de la POO. Pour pouvoir exploiter des modules de ce framework, il est parfois nécessaire de consulter les fichiers d'en‑tête afin bien comprendre comment les classes y sont déclarées et quelles méthodes sont définies.
Mais bien évidemment, comme pour les tableaux et les structures hétérogènes, c'est un sujet complexe qu'il n'est pas question de traiter ici en profondeur. Les aspects syntaxiques ne seront pas détaillés de façon formelle, mais plutôt illustrés à travers des exemples simples, dont il est facile de s'inspirer pour coder à volonté d'autres petits programmes académiques.
Les tableaux
Intérêt des tableaux
En programmation, il est très fréquent de devoir manipuler des données en série. Pensons par exemple à un montage avec une série de led dont on voudrait connaître l'état de chacune individuellement. Il est alors fastidieux de déclarer individuellement ces données, pour plusieurs raisons :
- Même en procédant par copier/coller à partir d'une première déclaration, il faut encore changer l'identificateur de chaque donnée, typiquement avec un numéro distinct (dans notre exemple, cela donnerait quelque chose comme
led_1_state,led_2_state, etc.) Cela prend non seulement du temps, mais aussi de la place dans le code. - Une fois ces multiples identificateurs déclarés, il est impossible de leur faire subir un traitement en série – typiquement, avec une boucle de répétition
forou autre – puisque le numéro de chaque donnée n'est pas lui‑même une variable, mais une partie de son identificateur.
L'intérêt de la structure de donnée de type tableau, c'est justement de pouvoir désigner avec un même identificateur un grand nombre de données, en mettant en place une numérotation de ces données par une variable – l'indice des éléments – qui puisse être exploitée dans le code.
Dans le sujet de TP nº C3‑1 , on verra qu'il est possible d'utiliser un mot binaire (implémenté en Arduino par un type comme byte ou word) pour constituer une série de bits associés dans une même variable (chaque bit représentant l'état d'une led). Toutefois, cette technique de codage souffre de deux limites : d'une part les données doivent être forcément binaires, d'autres part leur nombre doit rester très limité (8 avec un mot de type byte, 16 avec un mot de type word…).
Notion de tableau et d'élément
D'une manière générale, en programmation, un tableau W – en anglais, array – est une donnée structurée qui contient une série de données toute de même type. On parle donc de structure homogène de données.
Chacune de ces données constitue un élément anonyme mais numéroté (cf. infra la notion d'indice ) du tableau. Le nombre d'éléments N d'un tableau peut être très petit ou très grand, selon les besoins du programme.
Le type des éléments d'un tableau peut être élémentaire bien sûr : entier, décimal, etc.
Un relevé annuel de températures mensuelles moyennes peut être structuré en un tableau unidimensionnel de 12 nombres décimaux, à l'instar de celui représenté ci‑dessous pour un cas particulier de lieu et d'année (Versailles La Lanterne, année 2020 ).
Mais ce type des éléments d'un tableau peut aussi être lui‑même structuré en tableau (voire même en une structure plus complexe). C'est notamment ainsi que l'on constitue un tableau multidimensionnel : par un tableau dont les éléments sont eux‑même des tableaux !
On peut agréger dans un relevé pluriannuel les relevés annuels de températures mensuelles de l'exemple précédent. On forme ainsi un tableau bidimensionnel dont chaque élément est lui‑même un tableau de 12 nombres, à l'instar de celui représenté ci‑dessous (même lieu mais pour les années 2018 à 2020).
Les langages de programmation modernes – Python, notamment – permettent l'emploi de séries de données hétérogènes, c'est‑à‑dire dont les éléments numérotés sont de types différents. Mais alors, on parle préférentiellement de listes.
Notion d'indice des éléments d'un tableau
Dans un tableau, les éléments sont organisées et numérotées selon un indice – en anglais, index – c'est‑à‑dire un nombre entier positif croissant qui détermine leur position respective dans le tableau.
Par convention, dans la plupart des langages de programmation, l'indice du premier élément du tableau est toujours 0.
Un élément de tableau peut alors être facilement repéré par le nom du tableau (son identificateur) associé à l'indice de l'élément.
Déclaration d'un tableau
Cas d'un tableau unidimensionnel
En langages C/C++, il n'existe pas de mot‑clef pour déclarer littéralement une donnée de type tableau. On utilise une instruction de la forme syntaxique suivante :
descripteur de type identificateur du tableau [N] [= {liste d'expressions}];
où [N] code le nombre d'éléments du tableau, et doit prendre la valeur d'un entier positif.
Quant à la liste d'expressions (facultative), elle permet d'initialiser les valeurs des éléments du tableau respectivement dans l'ordre de leur indexation.
- Pour caractériser de façon simplifiée un résistor par ses bandes de couleur normalisées (norme CEI 60757 W), on peut coder la déclaration d'un tableau de 4 entiers comme ci‑dessous :
int stripes_R[4] = {1, 0, 2, 5}; // brown, black, red, gold
chaque élément du tableau représentant la valeur numérique associée à la couleur de la bande correspondante – noir0, marron1, rouge2, orange3etc. - Pour stocker un relevé annuel de températures moyennes mensuelles, on peut par exemple coder la déclaration d'un tableau de 12 décimaux comme ci‑dessous :
float monthlyTemp_C[12] = {0.0};
sachant qu'ici, les éléments du tableau sont tous initialisés à zéro.
La syntaxe de déclaration en langage C/C++ d'un tableau est bien plus complexe et subtile que ce que pourrait laisser penser les rudiments exposés supra. Pour plus de détails, cf. le chap. C5‑III .
Cas d'un tableau bidimensionnel
Un tableau bidimensionnel se déclare via une instruction de la forme syntaxique suivante :
descripteur de type identificateur du tableau [M][N] [= {liste de listes d'expressions}];
où [M] et [N] codent respectivement le nombre d'éléments des deux dimensions du tableau, qu'on interprète typiquement comme des lignes et des colonnes.
Bien cela n'apparaisse pas explicitement dans la forme syntaxique présentée ci‑dessus, un tableau bidimensionnel est en fait un tableau qui encapsule une série de tableaux unidimensionnels. Dans le sens de lecture de la déclaration :
descripteur de type identificateur du tableau [M][N]
- la 1re dimension – celle à M éléments – est dite externe puisqu'elle encapsule la deuxième dimension – c'est celle qui est interprétée comme la dimension des lignes ;
- La 2e dimension – celle à N éléments – est dite interne puisqu'elle est encapsulée par la première dimension – c'est celle qui est interprétée comme la dimension des colonnes.
En prolongement de cette interprétation intuitive de la structure du tableau en lignes et colonnes, il est usuel de parler de cellules pour désigner les éléments dans la dimension interne.
- Pour programmer sur ordinateur un jeu de puissance 4 W, on peut coder la déclaration d'un tableau bidimensionnel de 6 × 7 entiers – en fait, 6 lignes de 7 colonnes chacune – par l'instruction suivante :
int board[6][7] = {0};
sachant que chaque cellule du tableau est destiné à valoir respectivement0,1ou2selon que l'emplacement correspondant est vide ou contient un jeton du joueur « 1 » ou « 2 ». - Pour stocker un relevé décennal de températures moyennes mensuelles, on peut par exemple coder la déclaration d'un tableau bidimensionnel de 10 lignes de 12 décimaux chacune comme ci‑dessous :
float decadeMonthlyTemp_C[10][12] = {0.0};
avec, encore ici, tous les éléments du tableau initialisés à zéro.
Manipulation d'un tableau
En langages C/C++, bien que cela puisse surprendre au premier abord, il n'est pas possible de manipuler globalement un tableau. Il faut nécessairement procéder élément par élément. Mais dans la mesure où les éléments d'un tableaux sont indicés, il est facile de factoriser les instructions de manipulation, typiquement à l'aide de boucles for.
Pour cela, il est indispensable de pouvoir identifier les éléments du tableau. Comme dans de nombreux langages de programmation, cela se code grâce à l'opérateur d'indexation [] (cf. chap. C2‑IV ).
Cas d'un tableau unidimensionnel
Après la déclaration d'un tableau unidimensionnel, chaque élément de ce tableau est identifiable par l'expression composée de la forme :
identificateur du tableau [i]
où i code l'indice de l'élément identifié.
Cette expression est une l‑value dont le type est celui des éléments du tableau. Autrement dit, sauf si le tableau a été déclaré constant, l'expression d'identification d'un de ses éléments peut faire l’objet d'une affectation, comme n'importe quelle variable.
Attention : si le tableau est déclaré à N éléments, le codeur doit respecter la contrainte :
0 ≤ i ≤ N - 1
Le compilateur n'effectuant aucune vérification d'indice, tout débordement de cet intervalle cible une portion de la mémoire à côté de celle allouée au tableau. Qu'il s'agisse d'une opération de lecture ou d'écriture, cela est susceptible de provoquer un dysfonctionnement lors de l'exécution du programme.
- En prolongement de l'exemple 2) supra de relevés mensuels de température, pour calculer la moyenne annuelle des températures à partir du relevé, on peut procéder comme dans le programme ci‑dessous :
- En prolongement de l'exemple 1) supra de codage des bandes de couleur d'un résistor , on peut calculer la valeur ohmique du résistor à partir de la donnée de ses couleurs de bandes, et vice‑versa, comme dans le programme codé ci‑dessous :
- à la ligne nº 7, on déclare une constante de type tableau
STRIPES_R1pour représenter les bandes de couleur d'un résistor ; - à ligne nº 8, on déclare une variable entière
resist_R1pour y calculer la valeur ohmique du résistor en appliquant la formule ①② × 10③ où ① ② ③ représentant les chiffres codés par les bandes de couleur – et c'est précisément ici qu'on accède en lecture aux éléments tableaux ; - à la ligne nº 9, on affiche cette valeur et son intervalle de tolérance calculé à l'aide de la 4e bande de couleur (3e élément du tableau).
- à la ligne nº 12, on déclare une constante entière
RESIST_R2pour mémoriser une valeur de résistance en ohm ; - à ligne nº 13, on déclare une variable de type tableau
stripes_R2dans laquelle on va déterminer les bandes de couleur correspondant à la résistance mémorisée ; - à la ligne nº 14, on déclare une variable intermédiaire
calc_R2pour la détermination des bandes de couleurs, initialisée à la valeur deRESIST_R2; - l'algorithme de détermination des bandes de couleurs procède par divisions successives par
10de la valeur decalc_R2(cf. les lignes nº 15 à 26) ; ici, les éléments du tableaux font l'objet d'affectations (accès en écriture) ; - à la ligne nº 27, on affiche dans l'ordre les 4 chiffres correspondant aux bandes de couleur (le 4e chiffre est supposé valoir toujours
5– il code la bande de qualité à 5 %).
#include <stdio.h>
int main(void)
{
const float MONTHLY_TEMP_C[12] = {6.0, 8.7, 8.1, 13.1, 14.3, 17.3, 19.2, 21.2, 17.2, 12.2, 9.3, 6.2};
float yearlySum = 0;
for (int month = 0; month <= 11; month++) {
yearlySum += MONTHLY_TEMP_C[month];
}
printf("Yearly average temp. = %g °C\n", yearlySum / 12);
return 0;
}
MONTHLY_TEMP_C est accédée en lecture, élément par élément, pour en faire la somme (cf. la ligne nº 8) dans la variable yearlySum. Ce calcul est effectué dans le cadre d'une boucle for où l'indice courant month va de 0 à 11 inclus (il s'agit donc du numéro du mois décalé de −1, donc 0 pour janvier, 1 pour février, etc.). yearlySum par l'effectif (12 – le nombre de mois dans l'année).
#include <stdio.h>
#include <math.h>
int main(void)
{
/* 1 - Resistance calculus from the color strips */
const int STRIPES_R1[4] = {1, 0, 2, 5}; // brown, black, red, gold
unsigned long resist_R1 = (STRIPES_R1[0] * 10 + STRIPES_R1[1]) * pow(10, STRIPES_R1[2]);
printf("Resistance R1 = %lu ± %g Ω\n", resist_R1, resist_R1 * STRIPES_R1[3] / 100.0);
/* 2 - Color strip calculus from the resistance */
const unsigned long RESIST_R2 = 56000; // ohm
int stripes_R2[4] = {0};
unsigned long calc_R2 = RESIST_R2;
while (calc_R2 % 10 == 0) {
calc_R2 = calc_R2 / 10;
stripes_R2[2]++;
}
if (calc_R2 > 10) {
stripes_R2[0] = calc_R2 / 10;
stripes_R2[1] = calc_R2 % 10;
}
else {
stripes_R2[0] = calc_R2;
stripes_R2[0] = 0;
}
printf("Strips R2 = %d, %d, %d, 5\n", stripes_R2[0], stripes_R2[1], stripes_R2[2]);
return 0;
}
main : main : Cas d'un tableau bidimensionnel
Après la déclaration d'un tableau bidimensionnel, chacun élément de ce tableau est identifiable par l'expression composée de la forme :
identificateur du tableau [j][i]
où j et i codent respectivement l'indice de l'élément sur chacune des deux dimensions du tableau – typiquement, j étant l'indice de ligne et i l'indice de colonne.
Là encore, cette expression est une l‑value.
Attention : si le tableau est déclaré à M lignes et N colonnes, le codeur doit respecter les contraintes :
0 ≤ j ≤ M - 1 0 ≤ i ≤ N - 1
avec les mêmes risques qu'exposé supra en cas de débordement.
En prolongement de l'exemple de déclaration d'un tableau bidimensionnel proposé supra , le programme ci‑dessous met en œuvre une version très rudimentaire du jeu Puissance 4 où, dans un terminal d'exécution, deux joueurs peuvent tour à tour placer des jetons jusqu'à ce que le support soit plein, sachant que :
- le programme ne vérifie pas si un joueur a gagné ;
- les jetons des deux joueurs sont respectivement représentés symboliquement par des «
1» et des «2», les emplacements vides par des «0» ; - l'affichage du support de jeu (tableau) est statique. À chaque coup, il est ré‑affiché en dessous du précédent.
/* Minimal C version ot the Connect-4 game
* only allows 2 players to drop tokens in the board until it is full
* do not check if a player has won
* statically displays the board with '1' and '2' as players tokens ('0' if place empty)
*/
#include <stdio.h>
#define NB_COL 7 // cannot be declared as a real constant (array dimension)
#define NB_ROW 6 // idem
const int MAX_MOVE = NB_COL * NB_ROW; // max number of moves than both players can do
void display(int board[][NB_COL])
{
for (int row = NB_ROW - 1; row >= 0; row--) {
for (int col = 0; col < NB_COL; col++) {
printf("%d ", board[row][col]);
if (col == NB_COL - 1) printf("\n");
}
}
}
int main(void)
{
int board[NB_ROW][NB_COL] = {{0}};
printf("***** CONNECT-4 MINIMAL GAME *****\n");
for (int move = 0; move < MAX_MOVE; move++) {
display(board);
/* offers the player to drop a token in the board */
int player = 1 + move % 2; // player alternates 1 / 2
int row = -1, col = -1; // -1 means not yet chosen
do {
printf("Player %d choose an UNFULL COLUMN NUMBER\n 0 - 6 (out of range to ABORT): ", player);
scanf(" %d", &col); // one col number (0 - 6) among those displayed on the board
if (0 > col || col >= NB_COL) return -1; // out of range => player aborts game
for (int r = 0; r < NB_ROW; r++) { // seeking the first free row in the chosen column
if (board[r][col] == 0) {
row = r;
board[row][col] = player;
break;
}
if (r == NB_ROW - 1) {
row = -1;
printf("\tColumn is full!\n");
}
}
}
while (row == -1);
}
display(board);
printf("Game over. Bye!\n");
return 0;
}
Dans ce programme :
- La variable
boardde type tableau bidimensionnel est déclarée à la ligne nº 27 ; les nombres de lignes et de colonnes (6 × 7) sont respectivement codés par les deux pseudo‑constantesNB_ROWetNB_COL, elles‑mêmes respectivement définies aux lignes nº 9 & 10. - Une fonction
displayest définie pour exécuter l'affichage du tableau (cf. les lignes nº 14 à 22). Elle est appelée deux fois dans la fonction principale du programme (cf. les lignes nº 31 & 55). - Dans cette fonction, les éléments du tableau
boardsont accédés en lecture (cf. la ligne nº 18) dans le cadre d'une double boucleforpour afficher à chaque coup l'état courant du support de jeu. - À chaque coup valable, l'élément du tableau
boardcorrespondant à l'emplacement du jeton joué est accédé en écriture à la ligne nº 43 ; il prend la valeur du numéro du joueur, mémorisé dans la variableplayer.
Exécuté sur OnlineGDB, ce programme produit typiquement une sortie comme ci‑dessous :
***** CONNECT-4 MINIMAL GAME *****
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
Player 1 choose an UNFULL COLUMN NUMBER
0 - 6 (out of range to ABORT): 3
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 1 0 0 0
Player 2 choose an UNFULL COLUMN NUMBER
0 - 6 (out of range to ABORT): 4
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 0 0 0 0
0 0 0 1 2 0 0
Player 1 choose an UNFULL COLUMN NUMBER
0 - 6 (out of range to ABORT): ...
Remarque importante
En langages C et C++, il n'est pas possible de coder une affectation globale sur une variable de type tableau. On est obligé de procéder élément par élément.
Et plus généralement, il n'est pas possible de coder, dans une expression, une valeur de type tableau. On ne peut identifier qu'un seul élément à la fois.
Les structures hétérogènes
Généralités
D'une manière générale, en programmation, une structure hétérogène W est une donnée structurée qui contient une collection de données individuellement nommées, a priori de types différents. On parle aussi d'enregistrement, en anglais record.
Chacune de ces données constitue un champ – en anglais, field – de la structure. Parce qu'ils sont nommés, le nombre de ces champs est nécessairement peu élevé.
Comme les éléments d'un tableau, les champs d'une structures peuvent être de types élémentaires bien sûr : entier, décimal, etc.
Pour mémoriser la température maximale mensuelle relevée en un lieu donné, il est utile de l'associer à la date à la quelle le relevé a été fait. On peut donc créer une structure à « un niveau » de la forme :
Température maximale mensuelle :
- jour (nombre entier)
- mois (nombre entier)
- année (nombre entier)
- température (nombre décimal)
Mais les types des champs d'une structure peuvent aussi être eux‑même structurés en structures ou en tableau. C'est notamment ainsi que l'on constitue des structures à plusieurs niveaux de détails.
Dans l'exemple précédent, pour mémoriser la température maximale mensuelle, on peut aussi concevoir une structure à « deux niveaux » comme ci‑dessous :
Température maximale mensuelle :
- date (structure) :
- jour (nombre entier)
- mois (nombre entier)
- année (nombre entier)
- température (nombre décimal)
Déclaration d'une structure hétérogène
En langages C/C++, il existe deux catégories de structures hétérogènes :
- celles qui procèdent par juxtaposition, déclarées avec le mot‑clef
struct; - celles qui procèdent par superposition, déclarées avec le mot‑clef
union.
On se limite ici à présenter brièvement (sans entrer dans les détails) la déclaration des structures par juxtaposition. Toutes les subtilités ainsi que les structures par superposition seront abordées au chapitre C5‑V .
En langages C/C++, la déclaration d'une donnée de type structure hétérogène de n champs procède par la forme syntaxique suivante :
[const] struct {
déclaration de champ 1;
déclaration de champ 2;
…
déclaration de champ n;
} identificateur de la structure [= {liste d'expressions}] ;
où :
- Pour stocker la température maximale mensuelle à une date donnée, par exemple en juillet 2020 à Versailles La Lanterne, atteinte le vendredi 31 avec la valeur de 38,5 °C , on peut coder la déclaration de la structure hétérogène suivante :
- Pour stocker les différentes caractéristiques d'un résistor, par exemple celui représenté en figure ci‑contre, à savoir R = 1 kΩ ±5 % et Pmax = ¼ W, on peut coder la déclaration d'une structure suivante :
const struct {
int day;
int month;
int year;
float temperature;
} MAX_TEMP_JULY_2020 = {31, 7, 2020, 38.5};
const struct {
float resistance;
float quality;
float maxPower;
} resistor_R1 = {1000.0, 0.05, 0.25};
Déclaration d'un type nommé de donnée structurée
Lorsqu'on a besoin de manipuler plusieurs données d'un même type de structure, il serait fastidieux de recoder dans la déclaration de chacune de ces données la liste des champs qui composent la structure. C'est pourquoi il est possible de déclarer la structure comme un type nommé. Par opposition, la forme de déclaration exposée supra est dite à type anonyme.
Une fois qu'on a déclaré un type nommé de donnée structurée, on peut l'utiliser comme identificateur de type pour déclarer des données, à condition de rappeler le mot‑clef struct au début d'une telle déclaration.
La déclaration d'un type nommé de donnée à structure hétérogène s'effectue selon la forme syntaxique suivante :
struct identificateur du type {
déclaration de champ 1;
déclaration de champ 2;
…
déclaration de champ n;
};
en notant bien que l'identificateur du type est placé au début de la déclaration, et non pas à la fin (contrairement à ce qu'on observe dans la forme syntaxique précédente).
Bonne pratique. Il est d'usage d'attribuer une majuscule initiale à tout identificateur de type choisi par le codeur.
La déclaration de la constante de type structure maxTempSept2020 de l'exemple supra peut être codée en deux temps, via la déclaration d'un type nommé comme ci‑dessous :
struct MaxTemp {
int day;
int month;
int year;
float temperature;
};
const struct MaxTemp MAX_TEMP_JULY_2020 = {31, 7, 2020, 38.5};
avec :
- la déclaration du type nommé
MaxTempaux lignes nº 10 à 15 ; - la déclaration de la constante
maxTempSept2020de typeMaxTempà la ligne nº 17.
Cette déclaration peut également est codée via deux niveaux de types structurés nommés, comme ci‑dessous.
struct Date {
int day;
int month;
int year;
};
struct MaxTemp {
struct Date date;
float temperature;
};
const struct MaxTemp MAX_TEMP_JULY_2020 = {{31, 7, 2020}, 38.5};
Remarque. Lorsqu'on ne déclare qu'une seule donnée d'un type structuré nommé, il est courant de lui donner le même identificateur que son type, mais avec une lettre minuscule initiale. C'est ce qui est pratiqué à la ligne nº 17 dans le code ci‑dessus.
Manipulation d'une donnée structurée
Affectation globale
Contrairement à une donnée de type tableau, il est possible de coder en langages C/C++ une affectation globale sur une données de type structure hétérogène, c'est‑à‑dire une instruction de la forme :
l‑value = r‑value
où typiquement, l‑value et r‑value sont deux identificateurs de données de même type struct.
En revanche, il n'est pas possible de former des constantes littérales d'un type structuré sous la forme de listes d'expressions ou de valeurs numériques.
En prolongation des exemples précédents de déclaration d'une structure pour mémoriser température maximale mensuelle (cf. supra ), supposons que l'on veuille mémoriser la température maximale annuelle. Pour cela, il suffit de déclarer une deuxième constante du type MaxTemp, nommée par exemple MAX_TEMP_2020, et dans cette déclaration, de coder une affectation globale de la valeur de celle du mois où ce maximum s'est produit. C'est ce que l'on peut lire ci‑dessous à la ligne nº 18.
struct MaxTemp {
int day;
int month;
int year;
float temperature;
};
const struct MaxTemp MAX_TEMP_JULY_2020 = {31, 7, 2020, 38.5};
const struct MaxTemp MAX_TEMP_2020 = MAX_TEMP_JULY_2020;
Accès aux champs d'une donnée structurée
En C/C++, comme dans beaucoup d'autres langages de programmation, l'accès aux champs d'une donnée de type struct se code grâce à l'opérateur de sélection . avec lequel on compose des expressions de la forme :
identificateur de la donnée.identificateur du champ
Une expression de cette forme est une l‑value – donc accessible en écriture – si le champ est lui‑même une l‑value (donc pas s'il s'agit d'un tableau).
L'opérateur de sélection permet donc de manipuler une donnée de type struct non pas de façon globale, mais au niveau de ses champs – et ce aussi bien en lecture qu'en écriture.
- Par un appel de la fonction
printf, le programme ci‑dessous effectue dans le terminal d'exécution l'affichage des détails d'un enregistrement de température maximale dans un texte d'accompagnement explicite. Ce faisant, on code donc un accès en lecture aux différents champs de la constante structuréeMAX_TEMP(cf. les lignes nº 16 & 17). - En utilisant la donnée structurée de résistor déclarée supra , le programme de décodage des bandes de couleurs proposé en section I peut se coder comme ci‑dessous avec des accès aux champs en écriture (cf. les lignes nº 14 & 15) et en lecture (cf. la ligne nº 18) :
#include <stdio.h>
struct MaxTemp {
int day;
int month;
int year;
float temperature;
};
const struct MaxTemp MAX_TEMP_2020 = {31, 7, 2020, 38.5};
int main(void)
{
printf("Capteur de Versailles La Lanterne\n");
printf("Température maximale pour l'année %d le %02d/%02d : %g °C\n",
MAX_TEMP_2020.year, MAX_TEMP_2020.day, MAX_TEMP_2020.month,
MAX_TEMP_2020.temperature);
return 0;
}
#include <stdio.h>
struct Date {
int day;
int month;
int year;
};
struct MaxTemp {
struct Date date;
float temperature;
};
const struct MaxTemp MAX_TEMP_2020 = {{31, 7, 2020}, 38.5};
int main(void)
{
printf("Capteur de Versailles La Lanterne\n");
printf("Temp. maxi. pour l'année %d le %02d/%02d : %g °C\n",
MAX_TEMP_2020.date.year, MAX_TEMP_2020.date.day, MAX_TEMP_2020.date.month,
MAX_TEMP_2020.temperature);
return 0;
}
#include <stdio.h>
#include <math.h>
struct {
float resistance;
float quality;
float maxPower;
} resistor_R1 = {0.0, 0.0, 0.25}; // alway 1/4 W
int main(void)
{
const int STRIPES_R1[4] = {1, 0, 2, 5}; // brown, black, red, gold
resistor_R1.resistance = (STRIPES_R1[0] * 10 + STRIPES_R1[1]) * pow(10.0, STRIPES_R1[2]);
resistor_R1.quality = STRIPES_R1[3] / 100.0;
printf("Resistance R1 = %.0f ± %g Ω\n", resistor_R1.resistance, resistor_R1.resistance * resistor_R1.quality);
return 0;
}
Introduction aux classes (en C++)
Généralités
Au chapitre C1‑I , on a très brièvement présenté le paradigme de la programmation orientée objet (POO) dont le concept central est celui de classe. On a cité le langage C++ qui permet justement de mettre en œuvre la POO. Et c'est particulièrement le cas dans le framework Arduino où les programmes sont codés en C++.
Comme on l'a également expliqué au chap. C1‑I, le principe de la POO consiste à intégrer les fonctions spécifiques à la manipulation de tel ou tel type de donnée dans la déclaration des types eux‑même – et on parle alors de méthodes.
Précisons enfin que, à l'instar d'une fonction, une méthode peut faire l'objet d'une déclaration – ou prototype séparée de sa définition (cf. chap. C2‑I ).
En C++, l'intégration des méthodes dans la structure d'une classe :
- est possible avec les structures hétérogènes de type
structetunion; - mais s'effectue de façon plus classique et plus lisible à l'aide du mot‑clef
class;
Tous les identificateurs – champs ou méthodes – déclarés dans une classe définissent ce qu'on appelle des membres de la classe.
La différence entre les classes de types struct (ou union) et class réside dans la définition implicite de :
- la partie publique de la classe, dont les membres déclarés peuvent être exploités partout dans le code du programme ;
- la partie privée de la classe, dont les membres déclarés peuvent être exploités seulement dans le code de la classe elle‑même et de ses méthodes.
Ainsi :
- Dans une classe déclarée de type
struct, tous les membres sont publics par défaut. - Dans une classe déclarée de type
class, tous les membres sont privés par défaut.
De plus, quelle que soit le mot‑clef utilisé pour définir une classe, ses parties publique et privée peuvent être explicitement codées respectivement par les étiquettes public: et private:.
En définitive, l'usage veut que l'on emploie :
- le mot‑clef
struct(ouunion) pour les classes les plus simples et/ou jouant un rôle secondaire dans un programme ; - le mot‑clef
classpour les classes plus complexes et/ou jouant un rôle central dans un programme.
La notion d'objet
On appelle objet toute variable ou constante déclarée d'une classe, elle‑même préalablement déclarée. Plutôt que de déclaration, on parle d'instanciation.
La syntaxe de déclaration d'un objet est la même que pour n'importe quel type (cf. chap. C2‑III ).
Attention, l'affectation de valeurs initiales dans l'instruction d'instanciation d'un objet est impossible si la classe comporte des champs privés. Dans ce cas, on peut néanmoins coder :
- des instructions séparées d'affectation directes sur les champs publics ;
- dans la déclaration de la classe, des valeurs par défauts à tous les champs, qu'ils soient publics ou privés.
Exemple d'utilisation du mot‑clef struct
Reprenons l'exemple de la structure hétérogène Date de type struct codée supra en langage C pour mémoriser une date calendaire. En C++, on peut partir de la même structure. Seuls le nom du fichier d'en‑tête est à adapter :
#include <cstdio>
struct Date {
int day; // 1 to 31
int month; // 1 to 12
int year;
};
Pour afficher dans le terminal d'exécution la valeur d'une donnée de ce type avec le format « dd/mm/yyyy », on peut coder séparément la fonction datePrintln suivante :
void datePrintln(Date date)
{
printf("%02d/%02d/%d\n", date.day, date.month, date.year);
}
où la date à afficher est son seul argument, nommé date. (Par ailleurs, pour comprendre le codage de la spécification de conversion %02d/%02d/%d, cf. le chap. C2‑VII .)
Dans la fonction principale du programme, il suffit alors d'appeler cette fonction datePrintln pour l'utiliser :
int main()
{
const Date MAX_TEMP_DATE_2020 = {31, 7, 2020};
datePrintln(MAX_TEMP_DATE_2020);
return 0;
}
Et sans surprise, sur OnlineGDB (en sélectionnant le langage C++), on obtient la sortie standard attendue :
31/07/2020
Mais on peut aussi intégrer cette fonction dans la déclaration de la structure Date, qui devient alors une classe (et non plus simplement un type). Pour cela, le programme se code typiquement comme ci‑dessous :
#include <cstdio>
struct Date {
int day; // 1 to 31
int month; // 1 to 12
int year;
void println() const
{
printf("%02d/%02d/%d\n", day, month, year);
}
};
int main()
{
const Date MAX_TEMP_DATE_2020 = {31, 7, 2020};
MAX_TEMP_DATE_2020.println();
return 0;
}
On observe que la fonction datePrintln est remplacée par la méthode println avec deux différences remarquables :
- Son identificateur n'a plus besoin de mentionner celui du type
Date, puisque la méthode y est intégrée – il n'y a pas de risque de conflit avec une méthode homonyme issue d'une autre classe. - Elle n'admet plus d'argument puisqu'elle possède un accès direct aux champs de l'objet auquel elle est associée lors de son appel.
Par ailleurs, remarquons à la ligne nº 8 le mot‑clef const qui est codé juste après la liste des arguments. Il indique que la méthode ne change pas la valeur des champs de l'objet qu'elle manipule. C'est une syntaxe obligatoire pour que la méthode puisse s'appliquer aux objets constants (comme c'est précisément le cas à la ligne nº 18).
Exemple d'utilisation du mot‑clef class
Reprenons l'exemple donné supra de programme rudimentaire du jeu de Puissance 4 codé en langage C. On se propose maintenant de le recoder en langage C++ en utilisant les concepts de la programmation orientée objet, comme ci‑dessous. En particulier :
- on encapsule la variable
board(le tableau bi‑dimensionnel qui mémorise l'état du jeu) dans la partie privée d'une classe nomméeConnect4board; - on déclare 2 méthodes publiques :
-
displaypour afficher l'état du jeu dans le terminal d'exécution ; -
playpour mettre le jeton d'un joueur dans la colonne qu'il a choisie.
Cette classe possédant une partie privée et plusieurs méthodes, c'est une bonne pratique de la déclarer avec le mot‑clef class, d'autant plus qu'elle constitue le type de donnée central du programme.
/* Minimal C++ version ot the Connect-4 game
* only allows 2 players to drop tokens in the board until it is full
* do not check if a player has won
* statically displays the board with '1' and '2' as players tokens ('0' if place empty)
*/
#include <cstdio>
using namespace std;
const int NB_COL = 7;
const int NB_ROW = 6;
const int MAX_MOVE = NB_COL * NB_ROW; // max number of moves than both players can do
class Connect4board {
private:
int board[NB_ROW][NB_COL] = {{0}};
public:
void display() const;
int play(int player, int col);
};
void Connect4board::display() const
{
for (int row = NB_ROW - 1; row >= 0; row--) {
for (int col = 0; col < NB_COL; col++) {
printf("%d ", board[row][col]);
if (col == NB_COL - 1) printf("\n");
}
}
}
int Connect4board::play(int player, int col)
{
int row = -1; // -1 means not yet set
for (int r = 0; r < NB_ROW; r++) { // seeking the first free row in the chosen column
if (board[r][col] == 0) {
row = r;
board[row][col] = player;
break;
}
if (r == NB_ROW - 1) {
row = -1;
printf("\tColumn is full!\n");
}
}
return row;
}
int main()
{
Connect4board connect4board;
printf("***** CONNECT-4 MINIMAL GAME *****\n");
for (int move = 0; move < MAX_MOVE; move++) {
connect4board.display();
/* offers the player to drop a token in the board */
int player = 1 + move % 2; // player alternates 1 / 2
int col = -1; // -1 means not yet chosen
do {
printf("Player %d choose an UNFULL COLUMN NUMBER\n 0 - 6 (out of range to ABORT): ", player);
scanf(" %d", &col); // one col number (0 - 6) among those displayed on the board
if (0 > col || col >= NB_COL) return 255; // out of range => player aborts game
}
while (connect4board.play(player, col) == -1);
}
connect4board.display();
printf("Game over. Bye!\n");
return 0;
}
D'un point de vue technique, on peut faire les observations suivantes sur ce code.
- À la ligne nº 53, on instancie un unique objet homonyme de la classe
Connect4board, donc nomméconnect4boardavec une initiale minuscule (cf. chap. C2‑X ). - Aux lignes nº 23 et 33, les deux méthodes déclarées dans la classe
Connect4boardsont définies à l'extérieur de celle‑ci en utilisant l'opérateur de résolution de portée::W (scope resolution operator). - Les constantes globales
NB_COL,NB_ROWetMAX_MOVEpourraient être intégrées dans la partie publique de la classeConnect4board, à condition d'être déclarées statiques (cf. chap. C4‑II ). - Le fait que le champ
boardde la classeConnect4boardsoit déclaré privé le rend directement inaccessible en lecture ou écriture dans la fonction principale du programme ou tout autre fonction étrangère à cette classe : son identificateur y est tout simplement interdit d'emploi.
play pour modifier les cellules du champ board, même si cette méthode n'est utilisée qu'une seule fois. La notion de constructeur
- Lorsqu'une classe comporte des champs privés, il n'est pas possible de coder l'initialisation de ces champs (ni d'aucun autres d'ailleurs) dans l'instanciation d'un objet de cette classe.
- Dans un tel cas, il existe des alternatives (valeurs par défaut, affectation directe des champs publics), mais ces dernières sont limitées.
Face à ces limitations, que faire l'on veut initialiser plusieurs objets avec des valeurs initiales différentes à chaque fois (comme on peut le faire pour des données de types usuels) ? Il n'y pas d'autre choix que de coder une méthode spécifique pour l'initialisation. Et c'est là que la notion de constructeur trouve sa pertinence.
En programmation orientée objet, un constructeur W est une méthode spéciale qui code dans un même bloc l'instanciation d'un objet d'une classe (préalablement déclarée) et l'affectation éventuelle de valeurs initiales à ses champs.
En langage C++, un constructeur est une méthode sans valeur de retour – et qu'il ne faut pas coder void. Elle est nécessairement homonyme de la classe dans laquelle elle est déclarée.
Prolongeons l'exemple de la classe Date de type struct proposée supra .
struct Date {
int day; // 1 to 31
int month; // 1 to 12
int year;
// ...
L'inconvénient d'une approche aussi simple avec une classe dont tous les membres sont publics est que l'on peut instancier des dates qui n'existent pas. En effet, rien n'empêche par exemple de coder dans la fonction main :
const Date productionDate = {29, 2, 2019}; // Does not exist!
Pour une meilleure robustesse aux erreurs, une solution consiste à déclarer privés les champs day, month et year, et coder un constructeur pour les initialiser avec une correction des éventuelles erreurs. C'est ainsi qu'est conçu le programme ci‑dessous.
#include <cstdio>
using namespace std;
class Date {
private:
int day; // 1 to 31
int month; // 1 to 12
int year;
bool isLeapYear();
int nbOfDaysInMonth();
public:
Date(int d, int m, int y); // constructor
void println() const;
};
bool Date::isLeapYear()
{
return (year % 4 == 0 && year % 100 != 0) || year % 400 == 0;
}
int Date::nbOfDaysInMonth()
{
switch (month) {
case 2 :
return (isLeapYear()) ? 29 : 28;
case 4 : case 6 : case 9 : case 11 :
return 30;
default :
return 31;
}
}
Date::Date(int d, int m, int y) // constructor
{
if (y < 1583) year = 1583; // Gregorian calendar first year
else year = y;
if (m < 1) month = 1;
else if (m > 12) month = 12;
else month = m;
int D = nbOfDaysInMonth();
if (d < 1) day = 1;
else if (d > D) day = D;
else day = d;
}
void Date::println() const
{
printf("%02d/%02d/%d\n", day, month, year);
}
int main()
{
Date year_too_low ( 1, 1, 1400);
Date month_too_low ( 1, 0, 1600);
Date day_too_high (29, 2, 1900);
Date all_wrong ( 0, 0, 1200);
year_too_low.println();
month_too_low.println();
day_too_high.println();
all_wrong.println();
return 0;
}
Comme on peut le voir ci‑dessus, dans la fonction main, on teste l'instanciation de 4 dates avec des valeurs incorrectes. Conformément aux attentes, leur affichage révèle les corrections automatiques apportées par le constructeur, comme le montre la sortie standard obtenue sur OnlineGDB :
01/01/1583 01/01/1600 28/02/1900 01/01/1583
À propos du constructeur Date, on peut noter les points suivants :
- Il possède 3 arguments formels
d,metyqui correspondent aux valeurs des 3 expressions à coder lors de l'instanciation pour initialiser le jour, le mois et l'année de la date. - Il fait appel à deux routines privées sans argument :
-
isLeapYearqui retourne si l'année de la date est bissextile ou non (cf. chap. R2‑VI R) ; -
nbOfDaysInMonthqui retourne le nombre de jours dans le mois de la date.