INFO-F-105 Langages 1 - Homepage de l'Université libre de ... · L-value •Définition: une...

68
INFO-F-105 Langages 1 Gilles Geeraerts Année académique 2009-2010

Transcript of INFO-F-105 Langages 1 - Homepage de l'Université libre de ... · L-value •Définition: une...

INFO-F-105Langages 1

Gilles GeeraertsAnnée académique 2009-2010

Chapitre 2Expressions et

fonctions

Opérateurs

• Le C++ connaît énormément d’opérateurs:

• +, -, *, /, %

• <, >, <=, >=

• <<, >>

• ++, --, typeid, sizeof, [], new, delete

• ...

Opérateurs

• Tableau des opérateurs: voir livre de référence, page 120

Priorité et associativité• Comme en mathématique les opérateurs

ont une priorité:

• ex: a+b*c équivaut a + (b*c)

• L’associativité est à droite pour les opérateurs unaires et l’assignation, et à gauche sinon

• ex: a+b+c équivaut (a+b)+c

• ex: *p++ équivaut *(p++)

• ex: a=b=c équivaut a=(b=c)

Type du résultat

• Quel est le type du résultat d’une expression arithmétique ?

• Cela dépend des types qui y apparaissent

• Règles générale: on va toujours vers le plus précis

• ex:long int i ;int j ;... = i+j ; // i+j est long int

L-value

• Définition: une l-value est une valeur qui a une adresse en mémoire

• exemples:

• ++x renvoie la nouvelle valeur de x. Cette valeur est donc stockée dans x, et a donc une adresse (celle de x)

• x++ renvoie l’ancienne valeur de x. Cette valeur n’est plus stockée en mémoire. Ce n’est pas une l-value.

Type de retour• De manière générale, les opérateurs qui

reçoivent une l-value renvoient une l-value. Cela permet de combiner les opérateurs

• exemples:

• x = y ; renvoie la nouvelle valeur de x. On peut donc écrire z = x = y ; La valeur de y sera d’abord copiée dans x, puis dans z.

• int *p = &++x ; fait pointer p vers x

• int * p = &x++ ; n’est pas correct.

Dépassements

• Aucune garantie n’est offerte quant au dépassement de capacité.

• Ceux-ci ne sont pas détectés

• Les effets ne sont pas garantis

• exemple: qu’imprime ?int i = 1;while (i > 0) i++ ;cout << i ;

Ordre d’évaluation

• L’ordre dans lequel les composants d’une expressions sont évalués n’est pas garanti !

• Exemple: on ne sait pas si f(1) sera évalué avant g(2) ou pas dans:int x = f(1) + g(2) ;

• Exemple: que fait ?int i = 1;V[i] = i++ ;

Ordre d’évaluation

• Il y a heureusement des exceptions: && et || évaluent de gauche à droite

• Utile dans un cas comme:while (p != NULL && p->info != i) {...}

Car si p=NULL et qu’on évalue d’abord p->info, on a une erreur de segmentation

Opérateurs && et ||

• De plus, ces opérateurs sont évalués de façon «paresseuse»:

• Dans e1 && e2, on évalue d’abord e1, et puis e2 seulement si e1 est vrai

• Dans e1 || e2, on évalue d’abord e1, et puis e2 seulement si e1 est faux

Incrément et décrément

• Le C++ connaît les opérateurs ++ et -- pour incrémenter et décrémenter

• Il en existe deux versions:

• Une version préfixe ++x, --x, qui renvoie la nouvelle valeur

• Une version postfixe x++, x--, qui renvoie l’ancienne valeur (et donc pas une l-value)

++ et -- sur les pointeurs

• Quand on applique ces opérateurs sur un pointeur T * p cela a pour effet d’incrémenter le pointeur de sizeof(T).

• Utilité: parcours de taleaux !

Exercice...

• Que fait le code ci-dessous ?

char S[10] = «Hello !» ;char T[10] ;char * p = S, *q = T ;while (*q++ = *p++) ;

new et delete

• new T alloue sur le heap un objet de type T et retourne son adresse

• delete p, où p est un pointeur, libère l’espace mémoire pointé par p, à condition que cet espace ait été alloué par new

Utilité de delete

int main() {int * p, *q;p = new int ;q = new int ;p = q ;return 0 ;

}

stack heap

Codep q

Utilité de delete

int main() {int * p, *q;p = new int ;q = new int ;p = q ;return 0 ;

}

stack heap

Codep q

Utilité de delete

int main() {int * p, *q;p = new int ;q = new int ;p = q ;return 0 ;

}

stack heap

Codep q

Utilité de delete

int main() {int * p, *q;p = new int ;q = new int ;p = q ;return 0 ;

}

stack heap

Codep q

Utilité de delete

int main() {int * p, *q;p = new int ;q = new int ;p = q ;return 0 ;

}

stack heap

Codep q

La zone initialement pointée par p n’est plus

accessible par le programme mais reste

«réservée». Elle est donc perdue.

Utilité de delete

int main() {int * p, *q;p = new int ;q = new int ;delete p ;p = q ;return 0 ;

}

stack heap

Codep q

Utilité de delete

int main() {int * p, *q;p = new int ;q = new int ;delete p ;p = q ;return 0 ;

}

stack heap

Codep q

delete

• Attention ! delete ne modifie pas le pointeur.

• delete p libère la zone mémoire pointée par p...

• ...mais le pointeur p contient toujours l’adresse de cette zone, qu’on ne peut plus utiliser

• ne pas déréférencer p après un delete !

new[] et delete[]

• new T[n] crée un tableau de taille n d’objets de type T, et renvoie l’adresse du premier élément du tableau

• De manière symétrique delete[] peut être utiliser pour libérer la mémoire occupée par un tableau alloué à l’aide de new[]

Fonctions

• Rappel: on peut séparer les déclaration et définition d’une fonction. Les deux sont nécessaires mais la définition doit être unique

• Rappel: prototype d’une fonction =type retour f(type arg1, type arg2,...)

Variables statiques

• Rappel: par défaut les variables déclarées dans une fonction sont détruites quand on quitte la fonction (elles sont allouées sur le stack)

• Peut-on contourner ce comportement ?

Variables statiques

• Par exemple, dans la fonction:void f(int x) { int i = 0 ; i+=x ; cout << i ;}

• On aimerait que i ne soit pas réinitialisée à chaque entrée dans la fonction, mais qu’elle stocke la somme des x

Variables statiques

• Solutionvoid f(int x) { static int i = 0 ; i+=x ; cout << i ;}

• En déclarant la variable static, l’initialisation ne sera effectuée qu’une seule fois !

Arguments

• Rappel: par défaut, quand on passe une valeur à une fonction, une copie de la valeur est effectuée dans une variable locale de la fonction.

• Ce comportement peut être contourné en utilisant des pointeurs ou des références

Arguments

• ex:void swap(int i, int j) { int c = i ; i = j ; j = c ;}

• Ne va pas avoir le comportement attendu car on manipule des copies locales

Arguments

• ex:void swap(int &i, int &j) { int c = i ; i = j ; j = c ;}

• Va fonctionner: on modifie les variables de l’appelant

Arguments• ex:void swap(int *i, int *j) { int c = *i ; *i = *j ; *j = c ;}

• Fonctionne aussi, mais est plus lourd: l’utilisateur doit passer des adresses

• Ce sont les adresses qui seront copiées dans i et j, pas les contenus

Arguments const

• Avantage des références: évite la copie

• Inconvénient: la fonction peut modifier la variable de l’appelant !

• Solution: pn peut aussi déclarer les arguments const, quand il s’agit de pointeurs ou de références

• Cela permet d’interdire à la fonction de modifier les objets pointés ou référencés (tout en évitant la copie qui est lourde)

Arguments const• ex:int strlen (const char* S) { int i = 0 ; while (S[i]) i++ ; return i ;}Cette fonction reçoit un pointeur vers un const, ce qui lui permet de consulter l’objet, mais pas de le modifier

• A utiliser absolument pour éviter les erreurs !

Arguments: tableaux

• Quand un tableau est passé à une fonction, la fonction reçoit un pointeur vers le premier élement

• Conséquences:

• La fonction peut modifier le tableau de l’appelant (les tableaux sont toujours passés par référence)

• La fonction ne connaît pas la taille du tableau

Arguments: tableaux

• Quand un tableau est passé à une fonction, la fonction reçoit un pointeur vers le premier élement

• Conséquences:

• Quand on passe un tableau à une dimension, on ne doit pas spécifier la taille dans le type du tableau

• ex:void f(int M[]) {...}

Arguments: tableaux• Quand un tableau est passé à une fonction,

la fonction reçoit un pointeur vers le premier élement

• Conséquences:

• Quand on passe un tableau à plusieurs dimensions, la taille de la première coordonnée peut être omise

• ex:void f(int M[][n]) {...}Pourquoi ?

Valeurs de retour

• La sémantique du return est de

• créer une nouvelle variable anonyme, du type de retour de la fonction

• y stocker la valeur à retourner

• passer cette variable à la fonction appelante

Valeurs de retourint f(int i) { int k ; k = i +1 ; return k ;}

int main(void) { int l ; l = f(5) ; return 0 ;}

l

Valeurs de retourint f(int i) { int k ; k = i +1 ; return k ;}

int main(void) { int l ; l = f(5) ; return 0 ;}

l

k6

Valeurs de retourint f(int i) { int k ; k = i +1 ; return k ;}

int main(void) { int l ; l = f(5) ; return 0 ;}

l

k6

6

Valeurs de retourint f(int i) { int k ; k = i +1 ; return k ;}

int main(void) { int l ; l = f(5) ; return 0 ;}

l6

k6

6

Quand on quitte la

fonction, k est désallouée

Valeurs de retour

• On ne peut donc pas retourner un pointeur ou une référence vers une variable locale !

• ex:int * f(void) { int i ; return &i ; // pas bien}

Surcharge• Principe général: Le compilateur tolère que

plusieurs fonctions différentes aient le même nom, à condition que les paramètres lui permettent de déterminer laquelle appeler en pratique

• ex:void print(int i) { cout << "Entier " << i ;}void print(char c) { cout << "Caractère " << c ;}

Surcharge• La surcharge peut être pratique, mais peut

donner lieu à des ambiguïtés, si une conversion doit avoir lieu:

• ex:void g(double d) { cout << "Double " << d ;}void g(float fl) { cout << "Float " << fl ;}int main() { g(1) ; }

Surcharge• La surcharge peut être pratique, mais peut

donner lieu à des ambiguïtés, si une conversion doit avoir lieu:

• ex:void g(double d) { cout << "Double " << d ;}void g(float fl) { cout << "Float " << fl ;}int main() { g(1) ; }

Ambigu !Doit-on convertir le

int en double ou en float ?

Surcharge

• Quand le compilateur ne sait «pas décider» il affiche une erreur

• Le mécanisme utilisé par le compilateur pour «prendre sa décision» (pour résoudre la surcharge) est complexe

Mécanisme de résolution• A-t-on une fonction avec le prototype exact ?

• Si non, peut-on utiliser des promotions ?

• bool ☞ int

• char ☞ int

• short ☞ int

• float ☞ double

• double ☞long double

Mécanisme de résolution• Si non, peut-on utiliser des conversion

standards ?

• int ☞ double

• double ☞ int

• int ☞ unsigned int

• Si non peut-on utiliser des conversions définies par l’utilisateur ?

• ...

Ambiguïté

• Les problèmes d'ambiguïté sont plus importants quand on a plusieurs paramètres

• Ce que le mécanisme de résolution peut résoudre pour un paramètre n’est pas toujours gérable pour plusieurs paramètres

Ambiguïté• Exemple:void e(double d, int i) { cout << "double " << d ; cout << "int " << i ;}

void e(int i, double d) { cout << "int " << i cout << "double " << d ;}

int main() { e(1, 1) ; }

Arguments par défaut

• Par moment, on aimerait permettre à l’utilisateur d’appeler une fonction sans devoir toujours donner une valeur à tous ses arguments, car certains sont «habituels»

• ex: une fonction qui affiche un nombre dans une certaine base, 10 par défaut:void f(int v, int base) {...}f(5) ; // affiche en base 10f(5, 16) ; // base 16f(5, 8) ; // base 8

Arguments par défaut

• Solution 1: surcharge:void f(int i) {...}void f(int i, int base) {...}

• Problème: on va dupliquer du code

• Il faudrait pouvoir dire que «10» est une valeur «par défaut» !

• Solution 2:void f(int i, int base=10) {..}

Arguments par défaut

• Pour que cela fonctionne, il faut que le compilateur puisse décider, quand il manque des arguments à l’appel, lesquels correspondent aux valeurs par défaut:

• Règle: seuls les derniers arguments peuvent être «par défaut» et on associe les valeurs aux arguments de gauche à droite

Arguments par défaut

• Exemples:int f(int, int =0, char* =0) ; OKint g(int =0, int =0, char*) ; KOint h(int =0, int, char* =0) ; KO

Arguments par défaut

• Exemple:int f(int i, int j= 0, int j= 0) ;

int main() { f(5) ; // f(5, 0, 0) f(5, 1) ; // f(5, 1, 0) // pas f(5, 0, 1) f(5, 0, 1) ;}

Arguments non spécifiés• Il est également possible de déclarer des

fonctions sans spécifier le nombre de paramètres

• ex: Une fonction qui affiche une chaîne de caractères suivie d’une liste de valeurs, chacune spécifiée comme un paramètreint i = 9 ;f("Liste", 1, i) ;f("Liste", 5, 9, 15, 20) ;f("Liste", 5+2) ;C’est toujours la même fonction qui est appelée !

Arguments non spécifiés

• Déclaration de la fonction:void f(char * n ...)

• Ensuite, on accède aux paramètres effectifs à l’aide de macros spécifiées dans cstdarg (à inclure)

Arguments non spécifiés• Exemple:void f(char * n ...) { va_list arguments ; va_start(arguments, n) ;

int i ; cout << n << " : " ; do { i = va_arg(arguments, int) ; if (i) cout << i << " " ; } while(i) ; cout << endl ;}

Pointeurs vers des fonctions

• On peut aussi définir des pointeurs vers des fonctions

• Tout comme un pointeur vers un objet permet d’indiquer à une fonction une zone de mémoire à traiter, un pointeur vers une fonction permet d’indiquer à une autre fonction un traitement à appliquer

Pointeurs vers des fonctions

• Syntaxe:Tr (*fp)(T1, T2,..., Tn)Déclare un pointeur fp vers une fonction qui retourne un élément de type Tr et prend n arguments de types T1, T2,... Tn

• Exemple:int (*fp)(char *)ne pas confondre avec:int * fp(char *)

Pointeurs vers des fonctions

• Utilisation: Comme un pointeur vers une variable:int f(char * S) {...}

int main() { int (*fp)(char *) ; fp = &f ; // adresse char * V = ... ; (*fp)(V) ; // déréf. return 0 ;}

Pointeurs vers des fonctions

• Mais en pratique, une fonction est un pointeur !

• Une fonction n’est jamais que l’adresse mémoire où se trouve la première instruction du code généré

• On n’est donc pas obligé d’écrire explicitement &f, ni *f

Pointeurs vers des fonctions

• Alternative: int f(char * S) {...}

int main() { int (*fp)(char *) ; fp = f ; // adresse char * V = ... ; fp(V) ; // déréf. return 0 ;}

Pointeurs vers des fonctions

• Exemple de code... voir pointeurfonction.cpp

Fonctions inline

• Par défaut, un appel de fonction donne lieu à un jump dans le code compilé

• Pour des petites fonctions appelées fréquemment, il peut être préférable d’éviter le jump, et de demander au compilateur de d’insérer le code de la fonction à l’endroit de l’appel

Fonctions inline

• Cela s’obtient en déclarant la fonction inline

• Dans ce cas, la déclaration et la définition doivent se faire au même endroit

• Exemple:inline int vabs(int i) { if (i<0) i = -i ; return i ;}