un define est remplacé syntaxiquement dans le code avant compilation, comme si dans un traitement de texte vous faisiez un "rechercher et remplacer"
ça simplifie certaines écritures et la lecture du code mais la validation sémantique n'est pas effectuée puisque souvent les éléments ne sont pas typés.
par exemple si vous faites
#define MS 1000 // 1000 microsecondes dans une milli-seconde
#define S 1000 * MS // 1000 millisecondes dans une seconde
const unsigned long tempsAttente = 60 * S; // une minute d'attente
le compilateur ne sait pas quel type utiliser et à la compilation le remplacement textuel va dégager les #define et vous vous retrouverez avec
unsigned long tempsAttente = 60 * 1000 * 1000; // une minute d'attente
à compiler et là c'est la cata car le compilateur quand il n'a pas d'Information prend un int par défaut pour les valeurs numériques et donc va effectuer (sur un UNO) le calcul 60 * 1000 * 1000 sur des entiers signés à 2 octets dont la valeur max est 32767... on voit bien que ça ne va pas marcher, même si on met le résultat dans un unsigned long, le calcul lui même sera faux.
si vous aviez défini
const unsigned long MS = 1000;
const unsigned long S = 1000 * S;
const unsigned long tempsAttente = 60 * S;
alors le compilateur va effectuer les calculs pour vous mais comme il connait le type des données il sait qu'il faut travailler sur des unsigned long et tout va bien se passer.
biens sûr si vous êtes précis vous pourriez dire
#define MS (1000ul) // 1000 microsecondes dans une milli-seconde
#define S (1000ul * MS) // 1000 millisecondes dans une seconde
const unsigned long tempsAttente = 60 * S; // une minute d'attente
et forcer le type des constantes numériques pour le remplacement. ce serait une bonne pratique mais bien souvent le programmeur n'y pense pas
c'est pour cela que je préfère des enum
et des const
on peut définir le type qui sous tend la donnée représentée.
Cela dit, avec un #define on peut aussi simplifier des écritures répétitives plus complexes (même si maintenant on peut utiliser des inline) par exemple vous pouvez définir de manière générique comment trouver le max de 2 éléments par
#define max(a,b) ((a)>(b)?(a):(b))
votre code sera plus lisible si vous employez max()
Vous en verrez plein définis ainsi dans Arduino.h, par exemple
#define min(a,b) ((a)<(b)?(a):(b))
#define max(a,b) ((a)>(b)?(a):(b))
#define abs(x) ((x)>0?(x):-(x))
#define constrain(amt,low,high) ((amt)<(low)?(low):((amt)>(high)?(high):(amt)))
#define round(x) ((x)>=0?(long)((x)+0.5):(long)((x)-0.5))
#define radians(deg) ((deg)*DEG_TO_RAD)
#define degrees(rad) ((rad)*RAD_TO_DEG)
#define sq(x) ((x)*(x))
#define interrupts() sei()
#define noInterrupts() cli()
#define clockCyclesPerMicrosecond() ( F_CPU / 1000000L )
#define clockCyclesToMicroseconds(a) ( (a) / clockCyclesPerMicrosecond() )
#define microsecondsToClockCycles(a) ( (a) * clockCyclesPerMicrosecond() )
le define vient du temps où les inline n'existaient pas et qu'un appel de fonction ou la lecture d'une variable en mémoire coûtait cher mais de nos jours les compilateurs savent optimiser tout cela pour vous.