Une ligne commençant par le
caractère # (dièse) permet de définir une directive au pré-processeur
#include
<stdio.h>
#define
JURA 39
A
noter qu’en C++, on lui préfère une déclaration du type
const
int JURA = 39 ;
ou
bien
enum
Departement
{
AIN=1,
AINE,
ALLIER,
JURA=39
}
;
Departement
dep = JURA; // Pratique au débogueur
/**/
et //
#if
0
//
Pratique en phase de test car les /**/ ne peuvent pas s’imbriquer
#endif
char,
int, long
float,
double, long double
·
Attention au choix du
type de données – se référer au tableau
·
Précision, débordement
L’utilisation
dépend du contexte :
·
On se représente mieux
le résultat d’un calcul sous forme décimale (base 10)
·
On se représente mieux
un pointeur en hexadécimal (base 16)
·
Note : La
calculette de Windows sait effectuer ces conversions
C'est
avant tout un entier.
Voir
table ascii = correspondance entre un nombre et un caractère
char
a = 65, b = ‘A’ ; //a et b sont égaux
Utilisé
pour stocker les chaînes de caractères. (Tableau de char)
char
sz[25] ;
strcpy(
sz, "coucou" ) ;
printf(
"sz contient ‘%s’ " , sz ) ;
Remarquer au passage l’affectation des chaînes (sz =
"coucou" ne marche pas)
La
taille du Type int fonction de l’architecture (16 bits / 32 bits)
Appelé
couramment casting
·
L’utiliser pour éviter
de perdre de la précision
·
Ne pas l’utiliser
n’importe comment
·
Ne pas hésiter à mettre
des (trop de) parenthèses
double
a;
int
b = 2, c = 4 ;
a
= b/c ; //
résultat : a = 0
a
= double(b)/double(c) ; //
résultat : a = 0.5
On
utilise =
int
a ;
a
= 12 ; // a prend la valeur 12
+,
-, *, / fonctionnent que les nombres soient entiers (ex : int) ou réels
(ex : double)
%
(modulo) donne le reste de la division et ne fonctionnent que pour les nombres
entiers.
(Dans
le cas de nombres réel, utiliser fmod() )
Utilisation
des opérateurs : +=, -= , *=, /=. ++, --. Exemple avec +
a
= a + 1 ; ó a ++ ; ó ++a
a
= a + b; ó a += b;
a
= ++b; ó b = b + 1; a = b;
a
= b ++; ó a = b; b = b+1;
Dans
tous les cas, privilégier la lisibilité du code, et mettre des parenthèses en
cas d’ambiguïté sur la précédence des opérateurs.
&
(et) , | (ou), ^ (ou exclusif),
>> (décalage de bits à droite), << (décalage de bits à gauche)
On
utilise les opérateurs binaires lorsque plusieurs informations sont stockées
dans un entier :
//
Par exemple,
//
bit 1 à 8 = code d’erreur,
//
bit 9 à 16 = code de fonction
int
code = 0xBAAD ;
char
erreur ; char fonction ;
erreur
= code & 0xff ; // erreur =
0xAD
fonction
= (code >> 8) ; // fonction
= 0xBA
Le
xor ( ^) peut être utilisé pour réaliser des codages simples, la clé d’encodage
et de décodage étant la même.
int
a_encoder = 0xF00D ;
int
encode ;
int
decode ;
int
cle = 0x1234 ;
encode
= a_encoder ^ cle ;
decode
= encoder ^ cle ; // decode vaut 0xF00D
>,
>=, <, <=, ==, !=, !(expression) :
·
En C/C++, dans un test,
un résultat différent de 0 ( != 0) est considéré comme vrai, un résultat
nul (==0) comme faux.
·
Les opérateurs logiques
sont utilisés dans les tests et dans les boucles :
//
Exemple
if(
i != 0 ) printf( "i n’est pas nul\n" ) ;
for(
i = 0 ; i < 10 ; i ++ ) printf( "i vaut %d\n", i
) ;
·
Remplace une succession
de if() else if()
·
Permet l’exécution de
plusieurs bloc de codes (voir exemple)
·
Ne fonctionne qu’avec n
entier (int, char, long)
·
Si aucun cas ne convient,
va dans la section default (si elle existe)
·
L’exécution de break
provoque la sortie du switch()
for(;;)
// Boucle infinie
{
int i;
printf( "Rentrez i : " );
scanf( « %d », &i );
switch(i)
{
case 0 :
printf(
"case 0 : i=%d\n", i );
// Pas de break, donc va dans case 1
case 1 :
printf(
"case 1 : i=%d\n", i );
break;
case 2 :
printf(
"case 2 : i=%d\n", i );
break;
default :
printf(
"default : i=%d\n", i );
break;
}
}
Affichage
:
Rentrez
i: 0
case
0 : i= 0
case
1 : i= 0
Rentrez
i: 2
case
2 : i= 2
Déconseillé
d’utilisation car complique souvent la lecture du code, donc la maintenance
·
On ne peut pas sortir
d’une fonction avec goto
·
Se rencontre
quelquefois pour sortir de plusieurs boucles imbriquées
·
Se rencontre parfois
pour concentrer le code de nettoyage à un seul endroit.
Trois
manières de créer des boucles
·
Pour quitter une
boucle, on utilise break ;
·
Pour quitter la boucle
et la fonction on utilise return.
·
Pour remonter au début
de la boucle on utilise continue
On
remarque que le while() et le for() sont équivalents en ce sens
que le code de la boucle n’est exécuté que si la condition est initialement
vraie alors que le code de l’instruction do {} while() ; est
toujours exécuté au moins une fois.
//
Exemple d'utilisation de continue;
int
touche;
do
{
int n;
clrscr();
printf( "Rentrez un nombre entre 1 et
5\n" );
scanf( "%d", &n );
// Retour début (sous le do) si le nombre
ne convient pas
if( n < 1 || n > 5 ) continue;
printf( "Valeur de n: %d\n", n );
printf( "Q/q pour quitter\n" );
touche = getch();
}
while( touche != 'q' && touche != 'Q' );
·
Mettre un point-virgule
après le if() ou le while()
·
Oublier les accolades
{} sous le if() quand il y a plusieurs instructions.
·
Oublier un break
dans le switch()
Ce type d’erreurs pourtant simple est parfois difficile à détecter quand le code est correctement indenté.
Vous
les utilisez déjà : main(), scanf(), printf(), getch()
etc.
· Possibilité de créer de nouvelles afin de décomposer le programme en sous programme.
//
fichier calcul.h
//
Protection du .h : Une bonne habitude à prendre
//
(Voir les structures)
#ifndef
CALCUL_H
#define
CALCUL_H
//
Déclarations des prototypes
int
somme( int a, int b);
int
multiplication( int a, int b);
int
division( int a, int b);
#endif
// CALCUL_H
//
fichier calcul.cpp
#include
<stdio.h>
#include
"calcul.h"
static
void fonctionstatic()
{
printf( "Cette fonction ne peut pas
être utilisée "
"ailleurs que dans
calcul.cpp car elle "
"est déclarée static"
);
}
int
somme( int a, int b )
{
return a + b;
}
//
etc..
//
fichier faitcalcul.cpp
#include
<stdio.h>
#include
"calcul.h"
void
main()
{
// Utilisation de la fonction somme() de
calcul.cpp
int a = somme( 1, 1 );
printf( "1 + 1 = %d\n", a );
}
·
Essayer de limiter les
fonctions à quelques lignes. Si la fonction devient trop
·
grande et difficile à
comprendre, peut être faut-il la découper en sous fonctions.
·
Eviter de dupliquer du
code. Les fonctions sont une première réponse à ce problème.
·
Une fonction doit
idéalement faire une seule chose : Par exemple, on essaie de ne pas mélanger
les affichages avec les calculs.
·
Quand le fichier
d'include est inclus entre < et >, le compilateur recherche les fichiers
dans le répertoire include du compilateur (c:\tc\include)
On
peut passer des paramètres aux fonctions:
·
Par valeur
·
Par adresse (voir les
pointeurs)
·
Par référence en C++
seulement
//
parametres.cpp
//
Prototypes
void
parvaleur( int );
void
paradresse( int * );
void
parreference( int & );
void
parvaleur( int a )
{
a = 1;
}
void
paradresse( int * pa )
{
*pa = 2;
}
void
parreference( int & a )
{
a = 3;
}
void
main()
{
int n = 0;
printf( "n vaut %d\n", n ); //
Affiche n vaut 0
parvaleur( n );
printf( "n vaut %d\n", n ); //
Affiche n vaut 0
paradresse( &n );
printf( "n vaut %d\n", n ); //
Affiche n vaut 2
parreference( n );
printf( "n vaut %d\n", n ); //
Affiche n vaut 3
}
Dans
l'exemple, pa est un pointeur sur un int, ça veut dire en fait que pa contient
l'adresse de la variable n déclarée dans main().
Pour
obtenir ou modifier le contenu pointé
par pa (donc n), il faut utiliser l'opérateur d'indirection *.
Le
passage par référence du C++ nous cache cela. Cependant, les pointeurs sont un
concept fondamental du langage C/C++ qu'il faut comprendre.
//
Autre exemple sur les pointeurs:
int
a;
int
*pa;
//
On initialise la variable pointeur sur
//
l'adresse mémoire de a
pa
= &a;
//
On modifie a
a
= 5;
//
Affiche a = 5, *pa = 5
printf(
"a =%d, *pa = %d\n", a, *pa );
//
On modifie le contenu de l'adresse pointée par pa
//
ce qui revient à modifier a
*pa
= 10;
//
Affiche a = 10, *pa = 10
printf(
"a =%d, *pa = %d\n", a, *pa );
En
C/C++, les paramètres sont empilés sur la pile (zone mémoire) par la fonction
appelante, puis dépilées par la fonction appelée. Ce principe permet à la
récursivité de fonctionner. On parle souvent de LIFO (Last In First Out)
En
assembleur, la traduction d'un appel C++ est du genre :
//
En C
result
= calc( 2, 3 );
;
En assembleur
push
3
push
2
call
calc
;
La fonction calc va ensuite récupérer les paramètres dans la pile
De
nombreux environnements de développements on une option permettant de dé
assembler un programme.
L'utilisation
du mot clé const permet de s'assurer qu'une fonction ne modifie pas le
paramètre.
void
fonction( const int & a )
{
// Erreur de compilation :
// error C2166: l-value specifies const
object
a = 5;
}
void
fonction2( const int & a )
{
int * pa;
// Erreur de compilation :
// error C2440: '=' : cannot convert from
'const int *'
// to 'int *'
pa = &a;
*pa = 5;
}
void
fonction3( const int & a )
{
int * pa;
pa = (int *)&a; // OK, ça marche mais
c'est dangereux !!!
*pa = 5; // A EVITER
}
Utiliser
const aussi souvent que possible :
//
variables.cpp
#include
<stdio.h>
//
Une variable globale déclarée dans un autre fichier
extern
int g_VarDeclareeAilleurs;
//
Une variable globale
int
g_VarGlobale;
//
Une variable globale qui ne peut être utilisée que
//
dans ce fichier
//
Elle est initialisée a 0 par le compilateur
static
int s_VarStatic;
//
Une variable globale
int
UneVariable = 0xbad;
void
UtiliseUneStatic()
{
// Une variable static : Elle est
initialisée à 0 par le
// compilateur, mais par soucis de
lisibilité,
// on refait cette initialisation
static int s_fDejaFait = 0;
// Utilisation d'une variable static pour
ne faire qu'une
// seule fois une initialisation
if( s_fDejaFait == 0 )
{
printf( "Ceci ne sera affiche
qu'une seule fois\n" );
s_fDejaFait = 1;
}
printf( "Ceci s'affiche à chaque appel
de Affiche()\n" );
}
void
UtiliseUneVariable()
{
{
int
UneVariable = 0xf00d;
// Utilisation de l'opérateur de
résolution de portée
// :: pour accéder à une
variable globale de même nom
//
Affiche :
// il y a 2 variables (globale et
locale) UneVariable
// : BAD F00D
printf( "il y a 2
variables (globale et locale) "
"UneVariable : %X
%X\n",
::UneVariable, UneVariable );
}
// Pas besoin de ::, car la portée de la
variable locale
// est limitée au bloc
// d'instruction précédent
// Cependant, on peut le mettre par soucis
de lisibilité
printf( "La variable globale
UneVariable vaut : %X %X\n",
::UneVariable, UneVariable );
}
void
main()
{
//
UtiliseUneStatic();
UtiliseUneStatic();
UtiliseUneVariable();
}
Quelques
généralités ;
//
Gestion de tableaux de caractères ou chaînes de caractères
//
Chaque élément du tableau contient le code ascii d'un caractère
char
sz[10];
char
szCoucou[] = { 'c', 'o', 'u', 'c', 'o',
'u', '\0' };
char
szCoucou2[] = "coucou"; // Exactement comme ci dessus
char
szCoucou3[20] = "coucou"; // Les éléments restants après
// "coucou" sont mis à 0
//
Gestion d'un jeu de dame
//
chaque élément du tableau (i,j) représente une case du
//
jeu de dame
//
La valeur de chaque élément vaut par exemple :
//
-0 si Pas de pièce
//
-1 si pièce blanche
//
-2 si pièce noire
//
-3 si dame blanche
//
-4 si dame noire
int
aJeuDeDames[10][10];
//
Gestion des notes des élèves
//
chaque élément du tableau est un élève
//
La valeur de chaque élément est la note
int
aNoteDesEleves[10];
//
Gestion de l'écran en mode texte
//
25 lignes
//
80 colonnes
//
Utilisation d'un type utilisateur
struct
{
char Caractere;
char Couleur;
}
ElementEcran;
ElementEcran
aElementEcran[25][80];
int
aTabInt[200];
for(
int i = 0; i < 200; i ++ )
{
aTabInt[i] = i;
}
On
remarque en particulier que le tableau commence à 0.
#include
<stdio.h>
#include
<math.h>
//
On définit la table sur 360 degrés, on pourrait
//
diminuer celle-ci à 90 degrés sachant que sin()
//
est impaire
static
double s_aTableSinus[360];
const
double PI = 3.1416;
//
Initialisation de la table
//
A faire une seule fois au début du programme
void
initTableSinus()
{
for( int i = 0; i < 360; i ++ )
{
s_aTableSinus[i] = sin( i * PI / 180.0
);
}
}
//
Renvoie la valeur du tableau plutôt que d'utiliser
//
la fonction sin(). Cela est plus rapide.
double
sinus( int degres )
{
return s_aTableSinus[degres%360];
}
//
Le main()
void
main()
{
initTableSinus();
for( int i = 360; i < 720; i ++ )
{
printf( "%f", (float)
sinus(i) );
}
}
Cette
exemple montre comment optimiser un traitement en utilisant une table de
valeurs plutôt qu'une fonction.
#include
<stdio.h>
//
L'écran fait ici 25 lignes et 80 colonnes
//
Le tableau représentant cet écran est global
static
unsigned char s_aEcran[25][80];
//
Initialise le tableau avec des blancs
void
initialiseEcran()
{
for( int i = 0; i < 25; i ++ )
{
for( int j = 0; j < 80; j ++ )
{
s_aEcran[i][j] = ' ';
}
}
}
//
Affiche les caractères stockés dans le
//
tableau à l'écran
void
afficheEcran()
{
for( int i = 0; i < 25; i ++ )
{
for( int j = 0; j < 80; j ++ )
{
// On se positionne au bon endroit
gotoxy( j + 1, i + 1 );
// On affiche le caractère
putchar( s_aEcran[i][j] );
}
}
}
//
Efface l'écran
void
effaceEcran()
{
// Met des blancs dans le tableau
initialiseEcran();
// Affiche les blancs, ce qui revient à
// effacer l'écran
afficheEcran();
}
//
Programme principal
void
main()
{
char szTexte[] = "TEXTE EN
DIAGONALE";
int lenTexte = strlen(szTexte);
// On commence par effacer l'écran
effaceEcran();
// On stocke la chaîne dans le tableau
for( int i = 0; i < lenTexte; i ++ )
{
s_aEcran[i][2*i]
= szTexte[i];
}
// On affiche la chaîne
afficheEcran();
}
Cet exemple illustre une utilisation possible des tableaux pour stocker le contenu de l'écran. Ce mécanisme peut être repris pour la gestion de jeux simples (pacman, casse brique, ping-pong etc..
Soit le code suivant et sa représentation mémoire en hexadécimal sur une machine 32 bits Intel (copie d'écran faite sous Visual Studio 6.0) . La partie gauche est la représentation hexadécimale, la partie droite est la correspondance ascii de chaque valeur hexadécimale. Les trois points qui suivent sont essentiels à la compréhension de ce dump mémoire :
void
main()
{
int
n = 0x12345678;
char
szCoucou[] = "coucou";
char
* p;
p =
szCoucou;
printf( "szCoucou=%s, p=%s\n",
szCoucou, p );
printf(
"Adresse de &n=%#X\n", &n );
printf(
"Adresse de &p=%#X\n", &p );
printf(
"Valeur de p=%#X\n", p );
printf(
"Adresse de &szCoucou=%#X\n", &szCoucou );
printf(
"Valeur de szCoucou=%#X\n", szCoucou );
}
Affichage
du programme :
szCoucou=coucou,
p=coucou
Adresse
de &n=0X12FF7C
Adresse
de &p=0X12FF70
Valeur
de p=0X12FF74
Adresse
de &szCoucou=0X12FF74
Valeur
de szCoucou=0X12FF74
Si
on regarde le dump mémoire, on retrouve nos petits :
On
remarque aussi que l'on a l'égalité szCoucou == p. On retiendra qu'on utilise
un tableau et un pointeur de la même
manière.
char
aTabChar2x5[2][5] = {
{ 0x01,
0x02, 0x03, 0x04,
0x05 },
{ 0x06,
0x07, 0x08, 0x09,
0x0A }
};
char
aTabChar2x3x4[2][3][4] = {
{ {0x01,
0x02, 0x03, 0x04},
{0x05, 0x06, 0x07, 0x08},
{0x09, 0x0A, 0x0B, 0x0C} },
{ {0x0D,
0x0E, 0x0F, 0x10},
{0x11, 0x12, 0x13, 0x14},
{0x15, 0x16, 0x17, 0x18} }
};
Dans
les deux cas, on voit que les valeurs se suivent en mémoire
char
szNom[20];
char
szPrenom[20];
char
szNomPrenom[40];
char
szNomPrenomCopie[40];
//
Utilisation de strcpy()
strcpy(
szNom, "MARECHAL" );
strcpy(
szPrenom, "Sylvain" );
strcpy(
szNomPrenom, szNom );
if(
strcmp(szNomPrenom, szNom ) == 0 )
{
strcat(szNomPrenom, szPrenom );
}
else
{
printf( "Cas impossible !\n" );
}
//
Copie sans utiliser strcpy() : On copie caractère
//
par caractère, jusqu'au 0 de fin
int
i;
for(
i = 0; szNomPrenom[i] != 0; i ++ )
{
szNomPrenomCopie[i] = szNomPrenom[i]
}
szNomPrenomCopie[i]
= 0;
char
szBuffer[256]; int age;
//
Saisie de l'age
printf(
"Quel age avez vous ?" );
scanf(
"%d", &age );
//
Formatage de la chaîne en mémoire
sprintf(
szBuffer, "Vous avez %d ans", age );
//
Affichage
printf(
"%s", szBuffer );
char
szAge[] = "20";
int
age;
//
Conversion ascii vers entier
age
= atoi(szAge);
char
szBuffer[256];
//
Convertit 16 en chaîne – en base 10
itoa(
16, szBuffer, 10 );
printf(
szBuffer ) // Affiche 16
//
Convertit 16 en chaîne – en base 16 (hexadécimal)
itoa(
16, szBuffer, 16 );
printf(
szBuffer ) // Affiche 10
Afin de comprendre le mécanisme des pointeurs, les
exemples suivants présentent des copies d'écran de dumps mémoire faites sous
Visual Studio 6.0.
On rappelle que, en 32 bits :
·
La taille d'un entier
fait 4 octets
·
La taille d'un pointeur
fait 4 octets
·
Les nombres sont
stockés en représentation "little endian", c'est à dire que les
octets de poids faible sont stockées en premier : 0x12345678 est stocké en
mémoire sous la forme 78 56 34 12
void main()
{
int a = 0x12345678;
// Affiche :
// Adresse de a :
&a=0X12FF7C, valeur de a : a=0X12345678
printf( "Adresse de a : &a=%#X,
valeur de a : a=%#X\n",
&a, a );
}
int
a; // Adresse mémoire 0x0012FF7C
int
*pt1; // Adresse mémoire 0x0012FF78
int
*pt2; // Adresse mémoire 0x0012FF74
//
Initialisation : pt1 et pt2 sont initialisés à 0 (NULL).
//
L'adresse 0 signifie que pt1 et pt2 ne
pointent
//
sur aucune variable
//
La mémoire située à l'adresse 0 , n'est pas accessible
//
C'est pour cela que l'on utilise cette valeur pour
// initialiser un pointeur
pt1
= NULL;
pt2
= NULL;
a
= 12; // 0C 00 00 00 en hexa
//
On initialise pt1 sur l'adresse de a.
//
l'opérateur & nous permet de récupérer l'adresse mémoire
// d'une variable
pt1
= &a;
//
On voit dans la copie d'écran
//
l'adresse de a soit 00 12 FF 7C
//
(7C FF 12 00) à l'emplacement
//
0012FF78 qui est l'adresse de pt1
//
Idem avec pt2.
//
Juste pour que l'on remarque que plusieurs
//
variables pointeurs peuvent pointer vers
// une même adresse mémoire
pt2
= &a;
//
Idem en 0012FF74, adresse de pt2
//
On affiche a de 3 manières différentes
//
L'opérateur * permet de récupérer la valeur pointée
//
Affiche a=12, *pt1=12, *pt2=12
printf(
"a=%d, *pt1=%d, *pt2=%d\n", a, *pt1, *pt2 );
//
Modification de a
a
= 15; // 0F 00 00 00 en hexa
//
Affiche de a : On voit que *pt1 et *pt2 affichent 15
//
Affiche a=15, *pt1=15, *pt2=15
printf(
"a=%d, *pt1=%d, *pt2=%d\n", a, *pt1, *pt2 );
char
szCoucou[] = "coucou";
char
* pSurChaine;
pSurChaine
= szCoucou;
printf(
"%s\n", pSurChaine ); // Affiche coucou
pSurChaine
++; // <=> pSurChaine = pSurChaine + 1;
printf("%s\n",
pSurChaine ); // Affiche oucou
// Les variables
debutMem et finMem servent de signets
// délimitant la
mémoire utilisée par le programme
// (On remarque par
ailleurs que les variables sont
// stockées en
mémoire dans l'ordre inverse de leur
// déclaration.)
// Les adresses des
variables sont :
// &finMem =
0x0012FF7C
// szCoucou =
0x0012FF74
// &pSurChaine = 0x0012FF70
// &lenChaine =
0x0012FF6C
// aTabInt =
0x0012FF44
// &pSurInt =
0x0012FF40
// &lenTabInt =
0x0012FF3C
// &i =
0x0012FF38
// &debutMem =
0x0012FF34
//
int
finMem = 0xFFFFFFFF;
char
szCoucou[] = "coucou";
//
Ou char szCoucou[] = { 'c','o','u','c','o','u',0 };
char
* pSurChaine;
int
lenChaine = strlen(szCoucou); // = sizeof(szCoucou) - 1
int
aTabInt[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int
* pSurInt;
//
sizeof() renvoie la taille prise en mémoire
int
lenTabInt = sizeof(aTabInt) / sizeof(int);
int
i;
int
debutMem = 0xDDDDDDDD;
//
On initialise les pointeurs
//
pSurChaine pointe sur le premier élément de szCoucou;
//
pSurInt pointe sur le premier élément de aTabInt
pSurChaine
= szCoucou; // <=>pSurChaine =
&szCoucou[0]
pSurInt
= aTabInt; // <=> pSurInt =
&aTabInt[0];
//
L'exemple suivant montre :
//
- que l'on manipule les tableaux et les pointeurs de
// la même manière
//
- Comment accéder à l'élément i d'un tableau avec un
// pointeur
for(
i = 0; i < lenChaine; i ++ )
{
// 4 accès équivalents
printf( "szCoucou[%d]=%c <=>
*(szCoucou+%d)=%c <=> "
"*(pSurChaine+%d)=%c <=>
pSurChaine[%d]=%c\n",
i, szCoucou[i], i, *(szCoucou+i),
i, *(pSurChaine + i), i, pSurChaine[i]
);
}
//
L'exemple suivant met en évidence l'arithmétique des
//
pointeurs.
//
En effet, lorsque l'on fait (aTabInt+i), on est positionné
//
sur la bonne adresse. Comme pour un tableau, l'adresse est
//
augmentée de la taille de l'élément.
//
Cela est plus visible avec des int qu'avec des char puisque
//
sizeof(char) = 1 et sizeof(int)=2 (16 bits) ou 4 (32 bits)
for(
i = 0; i < lenTabInt; i ++ )
{
// 4 accès équivalents
printf( "aTabInt[%d]=%d <=>
*(aTabInt+%d)=%d <=> "
"*(pSurInt+%d)=%d <=> pSurInt[%d]=%d\n",
i, aTabInt[i], i, *(aTabInt+i),
i, *(pSurInt+ i), i, pSurInt[i] );
}
Cet
exemple montre que l'on utilise les pointeurs et les tableaux de la même
manière. Plus précisément, l'utilisation des crochets est une commodité du
langage C/C++ mais un tableau n'est rien d'autre qu'un pointeur constant.
//
Exemple sur une machine 32 bits
int
aTabInt[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
int
* pSurInt;
char
* pSurChar;
//
On initialise les 2 pointeurs sur le tableau d'entiers
//
On est obligé de forcer le type pour pSurChar
pSurInt
= aTabInt;
pSurChar
= (char *) aTabInt;
//
On incrémente de 4.
//
- Dans le cas des char, l'adresse est augmentée de
// 4 * sizeof(char) = 4
//
- Dans le cas des int, l'adresse est augmentée de
// 4 * sizeof(int) = 16 (sizeof(int) = 4 en 32
bits)
pSurInt
= pSurInt + 4; // <=> pSurInt += 4;
pSurChar
= pSurChar + 4; // <=> pSurChar += 4;
//
Affiche :
//
pSurInt=4, *pSurChar=1
printf(
"*pSurInt=%d,*pSurChar=%d\n",
*pSurInt, (int)*pSurChar );
//
Affiche :
//
aTabInt=0X12FF58, pSurInt=0X12FF68, pSurChar=0X12FF5C
printf(
"aTabInt=%#X, pSurInt=%#X, pSurChar=%#X\n", aTabInt, pSurInt,
pSurChar );
Cet
exemple insiste sur le fait
void
modifieValeur( int * pa )
{
*pa = 10;
}
void
main()
{
int a = 5;
// Affiche a = 5
printf( "a = %d\n", a );
// Modification de la valeur
modifieValeur( &a );
// Affiche a = 10
printf( "a = %d\n", a );
}
Cet exemple rappelle que le passage de l'adresse d'une variable à une fonction peut être utilisé pour modifier la valeur. Le passage par référence n'existant qu'en C++, c'est la seule méthode utilisable en C.
void
modifieTableau( int Tableau[], int nbElements )
{
for( int i = 0; i < nbElements; i ++ )
{
Tableau[i] = i;
}
}
void
afficheTableau( const int Tableau[], int nbElements )
{
for( int i = 0; i < nbElements; i ++ )
{
printf( "%5d", Tableau[i] );
}
printf( "\n" );
}
void
main()
{
int tableau[] = { 0, 0, 0, 0, 0 };
// Affiche 0 0 0 0 0
afficheTableau( tableau,
sizeof(tableau)/sizeof(int) );
// Modification du tableau
modifieTableau( tableau,
sizeof(tableau)/sizeof(int) );
// Affiche 0 1 2 3 4
afficheTableau( tableau,
sizeof(tableau)/sizeof(int) );
}
On note ici qu'un tableau est toujours passé par référence à une fonction. Si on veut s'assurer qu'une fonction ne modifie pas un tableau, il faut utiliser le mot clé const.
Le fait de spécifier const au niveau du passage d'un paramètre indique au compilateur qu'il doit vérifier que la fonction ne modifie pas ce paramètre.
Si on veut éviter de sur dimensionner les tableaux, il est nécessaire d'allouer de la mémoire.
· On alloue de la mémoire avec la fonction malloc().
· Le type de données renvoyé par malloc() est void *, c'est à dire une adresse mémoire non typée. Il appartient à l'utilisateur à typer la donnée.
· Il ne faut pas oublier de libérer la mémoire lorsqu'on en a plus besoin. La fonction free() sert à cela.
// L'exemple
suivant créé un tableau dont la dimension est
// saisie par
l'utilisateur. Cela évite de sur dimensionner
// le tableau.
// Chaque fois
qu'une erreur est rencontrée, on quitte le
// programme avec
la fonction exit()
#include
<stdio.h>
void main()
{
int * ptSurTabInt = NULL;
int cbTabIntElems = 0;
int i;
// On saisit un nombre d'éléments
printf( "Rentrez le nombre
d'éléments: ");
scanf( "%d", &cbTabIntElems
);
if(cbTabIntElems <= 0 )
{
printf( "Erreur de saisie du
nombre d'éléments !" );
exit(1);
}
//
On alloue la quantité de mémoire nécessaire pour
//
stocker le tableau
ptSurTabInt = (int *)malloc(cbTabIntElems
* sizeof(int) );
if(ptSurTabInt == NULL )
{
printf( "Impossible d'allouer la
mémoire !" );
exit(2);
}
// On saisit des valeurs et on les stocke
dans le tableau
for( i = 0; i < cbTabIntElems; i ++ )
{
printf( "Rentrez une valeur pour
l'élément %d\n", i );
scanf( "%d",
&ptSurTabInt[i] );
}
// On affiche le tableau
for( i = 0; i < cbTabIntElems; i ++ )
{
printf( "L'élément %d vaut
%d\n", i, ptSurTabInt[i] );
}
// on libère la mémoire
free(ptSurTabInt);
}
Plutôt que de déclarer un tableau à 2 dimensions pour stocker des chaînes de caractères, il est préférable d'utiliser un tableau de pointeurs sur des chaînes de caractères. Cela évite de sur dimensionner le tableau
// Première
solution : Tableau surdimensionné
// On remarque que
l'on doit donner la deuxième dimension
// du tableau pour
que le compilateur s'y retrouve
char aJourSemaine[][9] = {
"Dimanche",
"Lundi", "Mardi", "Mercredi",
"Jeudi",
"Vendredi", "Samedi"
};
// Affichage de la
taille prise par le tableau
// en mémoire
printf(
"Taille mémoire du tableau:7*9=%d\n",
sizeof(aJourSemaine)
);
// Affichage des
jours
for( int i = 0; i
< 7; i ++ )
{
printf(
"aJourSemaine[%d][0]=%s=%s\n", i,
&aJourSemaine[i][0], //
aJourSemaine[i] ); // Comme ci dessus
}
L'adresse mémoire du tableau aJourSemaine étant 0x0012FF40, on voit bien le tableau de char de 7 par 9, avec des zéros pour compléter chaque ligne.
// Deuxième
solution : On utilise un tableau de pointeurs
// Au passage, on
remarque la similitude de la déclaration
char *aTabPtSurJourSemaine[] = {
"Dimanche",
"Lundi", "Mardi", "Mercredi",
"Jeudi",
"Vendredi", "Samedi"
};
// Affichage de la
taille prise par le tableau
// en mémoire.
// A cela il
faudrait ajouter la taille prise par
// les chaînes de
caractères.
printf(
"Taille d'un pointeur:sizeof(char*)=%d\n",
sizeof(char *) );
printf(
"Taille mémoire du tableau:7* sizeof(char*)=%d\n",
sizeof(aTabPtSurJourSemaine) );
// Affichage des
jours
for( int i = 0; i
< 7; i ++ )
{
printf(
"aTabPtSurJourSemaine[%d]=%s\n",
i, aTabPtSurJourSemaine[i]);
}
L'adresse du tableau aTabPtSurJourSemaine étant 0x0012FF64, on peut trouver les adresses des chaînes de caractères :
0x00420FEC, 0x00421000, 0x00420038, 0x00420064, 0x0042002C, 0x00420054, 0x00420048
(On rappelle que "little endian" oblige, on doit lire les adresses à l'envers)
Les chaînes de caractères, contrairement à la solution 1, ne sont plus stockées dans la pile (adresse commençant par 0x0012XXXX), mais dans la zone des variables globales constantes (0x0042XXXX). Par ailleurs, le compilateur cherche à optimiser la mémoire et si une chaîne est mentionnée plusieurs fois, elle n'est stockée qu'une seule fois, et tous les pointeurs sont dirigés sur elle.
On appelle structure un type de données défini par le programmeur. Ce concept a été étendu à celui de classe en C++.
Cela permet de regrouper ensemble les variables attachés à un même objet. C'est d'ailleurs le premier pas vers la programmation objet.
L'accès aux membres d'une structure se fait avec un point (.) lorsqu'il s'agit d'une variable, et avec une flèche (->) lorsqu'il d'agit d'un pointeur.
Les exemples suivants illustrent la manière d'utiliser les structures
struct Personne
{
char Nom[10];
char Prenom[10];
int Age;
char Sexe;
};
struct Personne p;
// Ou bien (C++) Personne p; (sans struct)
struct Personne *
pointeurSurp;
// Accès en
utilisant le point (.)
strcpy( p.Nom,
"DUPOND" );
strcpy( p.Prenom,
"Jacques" );
// Affectation de
la variable pointeurSurp
pointeurSurp =
&p;
// Accès en
utilisant la flèche
pointeurSurp->Age
= 50;
pointeurSurp->Sexe
= 'M';
(32 hexadécimal = 50 décimal (3*16+2) )
Cela est fait de manière analogue à un tableau, les derniers membres non indiqués sont initialisés à 0 :
struct Personne p2
= { "DURANT", "Josette", 49, 'F' };
Il est possible de copier tous les éléments d'une structure avec =
struct Personne p2
= { "DURANT", "Josette", 49, 'F' };
struct Personne
p3;
p3 = p2;
Il est possible de ne pas donner de nom explicite à une structure. L'exemple suivant déclare une variable Dupond :
struct
{
char Nom[20];
char Prenom[20];
int Age;
char Sexe;
} Dupond;
strcpy(Dupond.Nom,
"DUPOND" );
strcpy(Dupond.Prenom,
"Jacques" );
Dupond.Age = 50;
Dupond.Sexe = 'M';
Ci dessous, on déclare un tableau de 50 Personne :
Personne
tableauDePersonnes[50];
L'accès au 5° éléments du tableau peut se faire des manières suivantes :
strcpy(
tableauDePersonnes[4].Nom, "LAGAFFE" );
strcpy(
(tableauDePersonnes + 4)->Prenom, "Gaston" );
(&tableauDePersonnes[4])->Age
= 30;
(*(tableauDePersonnes
+ 4)).Sexe = 'M';
On préfèrera toujours la syntaxe qui nous paraît la plus lisible. L'exemple ci-dessus rappelle l'arithmétique des pointeurs. Lorsqu'on ajoute 4, le compilateur calcule la position en utilisant la taille de la structure.
On utilise l'opérateur sizeof() :
int taille =
sizeof(Personne);
Cette taille peut être supérieure à la taille prise par la somme de l'ensemble des types. De nombreux compilateurs optimisent par défaut l'accès à la mémoire en alignant les données sur des frontières multiples de 4 octets. Ainsi, la structure Personne fait 28 octets et non 25 octets avec le compilateur de Visual C++ 6.0.
Cela correspond à utiliser un type de données structure dans une autre structure :
struct Employe
{
Personne personne;
char Fonction[15];
int Salaire;
};
L'accès aux variables se fait de la même manière :
Employe employe;
strcpy(
employe.personne.Nom, "LAGAFFE" );
strcpy(
employe.personne.Prenom, "Gaston" );
employe.personne.Age
= 30;
employe.personne.Sexe
= 'M';
strcpy(
employe.Fonction, "Technicien" );
employe.Salaire =
1500;
L'utilisation de typedef permet de déclarer un nouveau type de données.
Pour la structure Personne, on fait comme suit :
typedef struct
{
char Nom[10];
char Prenom[10];
int Age;
char Sexe;
} Personne;
ou bien
typedef struct Personne
{
char Nom[10];
char Prenom[10];
int Age;
char Sexe;
} Personne;
En C, sans l'utilisation de typedef, on doit déclarer une variable de type struct Personne en utilisant struct Personne :
struct Personne p;
// en C, sans typedef
Il est nécessaire de mettre le nom Personne (différent ou non du typedef) si la structure contient une référence sur elle même (voir les liste chaînées) :
typedef struct
structmaillonentier
{
// La valeur à stocker dans ce maillon de
la
// liste chaînée
int valeur;
// Pointeur sur le maillon suivant
// Pointeur sur une donnée de type
//
struct structmaillonentier.
// A cet endroit, le compilateur ne connaît
pas encore
// le type de données MaillonEntier, donc
on doit utiliser
// struct structmaillonentier
struct structmaillonentier *
PointeurSurSuivant;
} MaillonEntier;
Le mot clé typedef n'est pas réservé aux structures, on peut par exemple recréer un type existant :
typedef int
ENTIER;
ENTIER i = 10;
La classe Personne suivante est équivalente à la structure personne déclarée plus haut :
class Personne
{
public:
char Nom[10];
char Prenom[10];
int Age;
char Sexe;
};
Une union permet de mémoriser à la même adresse mémoire des données de type différents.
L'union suivante permet de voir que l'octet de poins faible est situé en 2° position sur une machine Intel
struct DeuxChars
{
unsigned char c1;
unsigned char c2;
};
union Nombre
{
short int Entier;
struct DeuxChars;
};
void main()
{
Nombre
n;
n.Entier
= 0x1234;
//
Affiche :
//
n.Entier=0x1234, n.dc.c1=0x34, n.dc.c2=0x12
printf(
"n.Entier=%#x, n.dc.c1=%#x, n.dc.c2=%#x\n",
n.Entier, n.dc.c1, n.dc.c2 );
}
L'exemple suivant montre comment stocker deux informations différentes dans un minimum d'espace mémoire.
Ici, un message peut être un message souris (ex : La souris a bougé) ou un message clavier (ex : On a appuyé sur une touche
#include
<stdio.h>
// Définition d'un
message clavier
typedef struct
MessageClavier
{
int touche;
int scancode;
} MessageClavier;
// Définition d'un
message souris
typedef struct
MessageSouris
{
int x,y;
int NombreClicks;
} MessageSouris;
// Définition du
type de message
typedef enum
MessageTypes
{ MESSAGE_CLAVIER,
MESSAGE_SOURIS } MessageTypes;
// Définition du
message (soit clavier, soit souris)
typedef struct
Message
{
MessageTypes MesType;
union
{
MessageClavier MesClavier;
MessageSouris MesSouris;
};
} Message;
// Exemple
d'utilisation
void main()
{
Message mes;
mes.MesType = MESSAGE_CLAVIER;
mes.MesClavier.touche = 'A';
mes.MesClavier.scancode = 0;
}
On note toutefois que cette technique assez employée en C est remplacée en C++ par l'héritage : On définit par exemple une classe Message de laquelle hérite une classe MessageSouris et une classe MessageClavier.
Ils permettent de stocker une information sur un nombre limité de bits. L'accès à chaque bit se fait de la même manière que pour les structures, avec la notation pointée.
On note qu'il n'est pas possible d'utiliser l'opérateur & afin de récupérer l'adresse, l'unité mémoire étant l'octet et non le bit. De même, la taille d'un champ de bits est multiple de la taille d'un octet.
typedef struct
Octet_Bit
{
unsigned
bit_0:1;
unsigned
bit_1:1;
unsigned
bit_2:1;
unsigned
bit_3:1;
unsigned
bit_4:1;
unsigned
bit_5:1;
unsigned
bit_6:1;
unsigned
bit_7:1;
} Octet_Bit;
union Octet
{
unsigned
char octet_char;
Octet_Bit
octet_bit;
};
union Octet c = { 0x0f };
struct Date
{
unsigned nWeekDay : 3; // 0..7 (3 bits)
unsigned nMonthDay : 6; // 0..31 (6 bits)
unsigned nMonth : 5; // 0..12 (5 bits)
unsigned nYear : 8; // 0..100 (8 bits)
};
· Une énumération est une suite de valeurs entières constantes qui commence par défaut à 0, et s'incrémente de 1 à chaque nouvelle valeur
· C'est un type de données utilisateur
· Il est possible de changer les valeurs des constantes avec le signe =
· Une énumération remplace avantageusement les #define en ce sens qu'elle apporte un typage plus fort
· Les débogueurs actuels affichent le nom de la constante plutôt que sa valeur, ce qui est plus lisible
enum JourSemaine
{
DIMANCHE,
// =0
LUNDI,
// =1
MARDI,
// =2
MERCREDI,
// =3
JEUDI,
// =4
VENDREDI,
// =5
SAMEDI
// =6
};
JourSemaine lundi
= LUNDI;
enum
QuelquesNombres
{
MOINSUN=-1,
ZERO,
UN,
QUATRE=4,
CINQ,
DIX=10
};
QuelquesNombres cinq = CINQ;
qsdfq