Plan de cours C/C++

 

Pré-processeur :

Une ligne commençant par le caractère # (dièse) permet de définir une directive au pré-processeur

Inclusion de fichiers d’entêtes :

#include <stdio.h>

Définition de valeurs :

#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

Commentaires :

/**/ et //

#if 0

// Pratique en phase de test car les /**/ ne peuvent pas s’imbriquer

#endif

 

types de données :

Entiers signés/ non signés :

char, int, long

 

Réels :

float, double, long double

 

·        Attention au choix du type de données – se référer au tableau

·        Précision, débordement

 

Représentation décimale, hexadécimale ou binaire :

 

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

 

Type char :

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)

 

Type int :

La taille du Type int fonction de l’architecture (16 bits / 32 bits)

 

Conversion entre types :

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

 

Affectation de valeurs à une variable :

On utilise =

int a ;

a = 12 ; // a prend la valeur 12


Opérateurs arithmétiques :

+, -, *, / 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.

 

Opérateurs binaires :

& (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

 

 


Opérateurs logiques :

 >, >=, <, <=, ==, !=, !(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 ) ;

 

 


Structures conditionnelles :

if(condition) else instruction ;/ { bloc d’instructions ;} ;

La forme contractée : a = (b < c) ? d : e ;

 

Le switch(n) :

·        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


goto :

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.


Les boucles :

 

Trois manières de créer des boucles

while( condition) instruction ;/ { bloc d’instructions ;}

do instruction ;/ { bloc d’instructions ;} while( condition) ;

for( code d’initialisation ; condition de test de début ; code de fin de boucle ) instruction ;/ { bloc d’instructions ;}

 

·        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' );

    


Remarques générales sur les structures conditionnelles et les boucles :

Quelques erreurs à éviter :

·        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é.

 

Quelques conseils :

 

 


Les fonctions :

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 );

}

 

Remarques :

 

·        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)

 


Paramètres des fonctions :

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

}

 


Remarque sur les pointeurs :

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 );

 

Remarque sur le passage des paramètres :

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.

 

Utilisation du mot clé const :

L'utilisation du mot clé const permet de s'assurer qu'une fonction ne modifie pas le paramètre.

Exemples :

 

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 :


Les  variables : Portée et durée de vie

 

 

 

 

// 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();

 

}

 

 

Remarques :

 


Les tableaux :

 

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];

Exemple 1 : Initialisation d'un tableau

int aTabInt[200];

 

for( int i = 0; i < 200; i ++ )

{

     aTabInt[i] = i;

}

 

On remarque en particulier que le tableau commence à 0.

 

Exemple 2 : Table de sinus

#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.

Exemple 3 : Tableau multidimensionnel :

#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..

 

Représentation d'un tableau mono dimensionnel en mémoire :

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. 

 


Représentation de tableaux à plusieurs dimensions en mémoire

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


Les chaînes de caractères :

 

Les fonctions les plus utilisées :

 

Exemple :

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;

 

Formatage des chaînes de caractères :

 

Exemple 1 :

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 );

 

Exemple 2 :

char szAge[] = "20";

int age;

 

// Conversion ascii vers entier

age = atoi(szAge);

 

 

Exemple 3 :

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

 

 

 


A savoir :

 

Gestion de chaînes non terminées par 0 :

 

 

 


Les pointeurs

 

 

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 );

}

 

Exemple 1 : Représentation d'un pointeur en mémoire

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 );

 

Exemple 2 : Incrémentation de pointeur

char szCoucou[] = "coucou";

char * pSurChaine;

 

pSurChaine = szCoucou;

printf( "%s\n", pSurChaine ); // Affiche coucou

pSurChaine ++; // <=> pSurChaine = pSurChaine + 1;

printf("%s\n", pSurChaine ); // Affiche oucou

 

 

Exemple 3 : Pointeurs et tableaux

// 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 4 : Importance du typage des pointeurs

// 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

 


Exemple 5 : Passage d'un pointeur à une fonction

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.

 


Exemple 6 : Passage d'un tableau à une fonction

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.

 

Exemple 7 : Allocation dynamique de mémoire :

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);

}

 


Exemple 8 : Tableau de pointeurs :

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.

 


Les structures :

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

 

Définition d'un nouveau type de données Personne

 

struct Personne

{

     char Nom[10];

     char Prenom[10];

     int Age;

     char Sexe;

};

Déclaration et utilisation d'une variable de type personne :

 

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';

 

 

Visualisation en mémoire de la variable p :

 

 

 

(32 hexadécimal = 50 décimal (3*16+2) )

 

 

Initialisation au moment de la déclaration

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' };

 

Copie d'une structure :

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;

 

Utilisation d'une structure non nommée :

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';

 

 

Déclaration d'un tableau de structures :

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.

 

Obtention de la taille d'une 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.

 

Imbrication des structures :

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;

 

Utilisation de typedef :

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;

 

Une structure est une classe dont tous les membres sont publiques :

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;

};

 

Les unions :

Une union permet  de mémoriser à la même adresse mémoire des données de type différents.

 

Exemple 1 :

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 );

}

 

Exemple 2 :

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.

 

Les champs de bits :

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.

 

La structure suivante permet d'accéder à chaque bit 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 };

 

 

 

 

Autre exemple : Définition d'une structure date :

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)
};

 


Les énumérations :

·        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

 

Exemples :

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