Skip to content

Effets de bord dans une expression booléenne

En parcourant un blog dont j’ai trouvé le lien dans Stack Overflow, je suis tombé sur un exemple de code en C intéressant (qui fonctionne de la même manière en C++ ou Java. Vous trouverez une version Pascal et une version Python de ce code plus bas) :

int x = 0;
int y = 0;
int z = 0;

while (x++ < 5 || y++ < 5) {
    z++;
}

Quelles seront les valeurs de x, y et z après l’exécution de ces instructions ? Ça a l’air simple ? Si vous répondez trop vite, vous tomberez (comme moi) dans le piège !

Pour ceux qui préfèrent le Pascal (il ne doit pas en rester beaucoup hélas…) voici une version équivalente dans ce langage (il n’y a pas d’opérateur de post-incrémentation en Pascal mais j’ai créé la fonction incr qui fait la même chose) :

program piege;

var x, y, z : integer;

function incr(var n : integer) : integer;
begin
    incr := n;
    inc(n);
end;

BEGIN
    x := 0;
    y := 0;
    z := 0;
    while ((incr(x)<5) or (incr(y)<5)) do
        incr(z);
    writeln('x=',x,' y=',y,' z=',z);
END.

Inc est une procédure (pas une fonction) de l’unité standard System du Pascal qui sert à incrémenter une variable passée en paramètre.

En Python, comme en Pascal, l’incrémentation (avec l’opérateur += par exemple) ne peut pas se faire dans une expression.
J’ai donc dû créer une fonction pour l’incrémentation mais il y a un obstacle supplémentaire : en Python les paramètres sont toujours passés par valeur (comme en C ou en Java) et même si la valeur qui est passée est la référence d’un objet, on ne peut pas modifier la valeur d’un objet de type int car il est immuable.
On peut cependant modifier les éléments d’une liste car les listes sont modifiables. Voici donc un programme Python équivalent :

def incr(t,i):
    t[i] += 1
    return t[i]-1

t = [0, 0, 0]
while incr(t,0)<5 or incr(t,1)<5 :
    incr(t,2)
print(t)
Publicités

void ou rien ?

Pour définir une fonction en C ou une méthode en Java, on écrit dans l’ordre :

  • son type de retour (c’est-à-dire le type de la valeur qu’elle va renvoyer),
  • l’identificateur de la fonction,
  • des parenthèses, avec entre elles la liste des types et noms des paramètres formels (séparés par des virgules),
  • un bloc entre accolades qui contient les instructions formant le corps de la fonction.

Exemple :

double distance(int x1, int y1, int x2, int y2)
{
        return sqrt((x2−x1)*(x2−x1) + (y2−y1)*(y2−y1));
}

void en type de retour

Normalement, si la fonction ne renvoie pas de valeur, il faut l’indiquer en écrivant à la place du type de retour le mot clé void.
Exemple :

void afficherPrix(double prix)
{
        printf("%.2f DA", prix); // ou System.out.printf en Java
}

La seule exception est en Java et concerne les constructeurs, qui ne sont pas des méthodes (d’après la documentation officielle): ils ne doivent rien renvoyer, mais il ne faut pas mettre void avant leur nom (sinon le compilateur les considère comme des méthodes).

Mais que se passe-t-il si l’on ne met ni type de retour ni void à la place ?

En Java

La réponse, simple, est donnée par le compilateur :

error: invalid method declaration; return type required

erreur : déclaration de méthode invalide ; type de retour obligatoire

En C

Bizarrement, cela ne provoque pas d’erreur (du moins, pas avec le compilateur GCC) mais un avertissement (warning) :

warning: return type defaults to ‘int’ [-Wimplicit-int]

ce qui veut dire « le type de retour ‘int’ a été pris par défaut ». C’est ce qui s’appelle un int implicite.

Le int implicite existait dans les anciennes versions du C. Il a été supprimé des nouvelles normes C99 et C11 mais les compilateurs l’ont gardé pour assurer la compatibilité avec les anciens programmes. Il s’agit d’une règle étrange qui indique que si une variable ou une fonction est déclarée sans type, le compilateur considère implicitement que son type est int. Regardez par exemple ce programme en C :

#include <stdio.h>
n = 42;
int main(void)
{
        printf("%d\n", n);
}

Vous croyiez qu’il provoquerait une erreur de compilation ? Eh bien non ! tout juste un avertissement comme celui de tout à l’heure. Le compilateur considère que le type de la variable est int, implicitement.

C’est la même chose pour les fonctions :

#include <stdio.h>

somme(int a, int b) {
        return a+b;
}

void main(void) {
        printf("%d\n", somme(27,15));
}

La fonction somme fonctionne très bien, elle renvoie un int. Le compilateur donne un fichier exécutable mais il vous avertit car ce n’est pas normal : peut-être que vous avez oublié le type de retour et que ce ne devait pas être int !

Autrefois certains programmeurs se permettaient probablement de ne pas mettre de type (en sachant que le compilateur mettrait int) mais cela a dû provoquer beaucoup de bugs car on ne peut savoir si cela a été omis exprès ou si c’est un oubli…

Aujourd’hui il ne faut surtout pas faire cela car en plus, ce n’est plus dans la norme du C. Il peut y avoir des compilateurs (ou même les prochaines versions de GCC) qui refuseront l’absence de type et provoqueront une erreur.

Reprenons la fonction de l’exemple de tout à l’heure dans un programme en C :

#include <stdio.h>
afficherPrix(double prix)
{
        printf("%.2f DA", prix);
}
int main(void){
        printf("Le prix est ");
        afficherPrix(42.1337);
        puts(".");
}

Comme on n’a rien mis à la place du type de retour de la fonction, le compilateur va mettre int, pas void, mais cela ne pose pas vraiment de problème. À la compilation on obtient juste l’avertissement de tout à l’heure.
Cependant, si l’on active l’option -Wall de GCC, il nous en ajoute un autre :

In function ‘afficherPrix’:
5:1: warning: control reaches end of non-void function [-Wreturn-type]

C’est pourquoi il faut toujours compiler avec -Wall. L’avertissement indique que la fonction, dont le type de retour n’est pas void, s’est terminée jusqu’à la fin, c’est-à-dire sans rencontrer d’instruction return.

En C, ce n’est pas une erreur pour le compilateur, contrairement au compilateur Java qui refuserait une telle situation.

De toute façon dans notre exemple, nous n’utilisons pas la valeur de retour, donc void ou un int c’est la même chose. Que ce soit en C ou en Java, on a le droit d’utiliser une fonction qui retourne une valeur comme si elle n’en retournait pas (c’est-à-dire utiliser une fonction comme si c’était une procédure) et ignorer sa valeur de retour.

Mais que renvoie cette fonction ? Vous pouvez essayer de mettre l’appel à la fonction dans une expression si vous êtes curieux. Elle peut renvoyer n’importe quelle valeur entière (comme celle que l’on trouve dans une variable locale qui n’a pas été initialisée). En C, la valeur renvoyée par une fonction qui n’a pas d’instruction return est indéfinie.

En conclusion, toujours mettre un type de retour correct au début de la définition d’une fonction. Ne rien mettre, ce n’est pas une bonne idée, et si la fonction ne renvoie rien il faut mettre void.

void comme paramètre

Quand une fonction en C ou une méthode ou un constructeur en Java n’a pas besoin de paramètres, il faut quand-même mettre les parenthèses après l’identificateur dans la définition. Mais doit-on les laisser vides, ou bien faut-il mettre void ?

En Java

Si l’on essaie avec void comme ceci :

/// NE SE COMPILE PAS !!///
class Voiture {
        void demarrer(void) {
                System.out.println("Vroom!");
        }
}

le compilateur refuse :

Voiture.java:3: error:  expected
void demarrer(void){
              ^
1 error

Il dit qu’il attendait un identificateur, mais si on en ajoute un :

        void demarrer(void x) {

il donne une autre erreur :

Voiture.java:3: error: 'void' type not allowed here
void demarrer(void x){
              ^
1 error

Type ‘void’ non autorisé ici

Donc en Java, void n’est jamais utilisé dans les paramètres de la définition d’une méthode (ni d’un constructeur). Avec Java, c’est toujours clair :-)…

En C

La norme du C dit que pour indiquer qu’une fonction ne prend pas de paramètres, elle doit être déclarée avec le mot-clé void entre les parenthèses.

Exemple:

void toto(void){
    printf("loulou");
}

Et si l’on ne met rien ? La norme dit que ne rien mettre, cela signifie qu’on ne précise pas quels sont le nombre et les types des paramètres. Cela ne veut pas dire qu’il n’y en a pas (ni qu’il y en a).

Qu’est-ce que cela implique ? La fonction de l’exemple précédent doit être appelée avec l’instruction :

toto();

mais si, par erreur on croyait qu’elle a un paramètre entier et si on essayait de l’appeler avec :

toto(42);

cela provoquerait une erreur du compilateur :

error: too many arguments to function ‘toto’

trop de paramètres passés à la fonction

Par contre si on déclare la fonction ainsi :

void toto(){
    printf("loulou");
}

Le compilateur accepterait sans problème l’appel :

toto(42);

Le programmeur qui a écrit cette instruction a clairement une idée fausse sur le comportement de la fonction, mais le compilateur ne l’a pas aidé à s’en rendre compte… Bonjour les bugs !

Conclusion : En C, habituez-vous à toujours mettre void entre les parenthèses quand une fonction ne prend pas de paramètres. Ne rien mettre c’est s’exposer à des bugs difficiles à détecter.

La méthode main() du C

Contrairement à Java, qui a une seule et unique façon d’écrire la méthode main, en C, on en voit de toutes les couleurs :

void main(void)
{ ...
main(void) 
{ ...
main() 
{ ...
int main() 
{ ...
int main(void) 
{ ...
int main(int argc, char* argv[]) 
{ ...
int main(int argc, char** argv) 
{ ...

…et toutes ces formes fonctionnent ! Mais sont-elles toutes correctes ? « Bien sûr, puisqu’elles fonctionnent ! » :-/ NON !!!

La norme, ou plutôt les normes de 1999 et 2011 (les anciennes sont dépassées) sont claires : seules les deux formes suivantes sont admises :

int main(void)
int main(int argc, char* argv[]) 

La première si les paramètres donnés à l’appel du programme sont ignorés, la deuxième si le programme les utilise.

Donc dans la liste précédente, des 7 possibilités qui fonctionnent, seules les trois dernières (la dernière est équivalente à celle qui la précède) sont correctes… Pourquoi ?

Cela a évidemment un rapport avec ce que l’on a dit de void, mais pas seulement:

POO – Examen final

Voici l’énoncé et le corrigé-type de l’examen final de Programmation Objet de cette année 2016-2017 :

POO – Examen final 2015/2016

Désolé, je croyais l’avoir déjà publié l’an dernier. Ce n’est qu’aujourd’hui qu’un étudiant me l’a demandé.

Voici donc le sujet et le corrigé-type de l’examen final de POO de l’an dernier :

POO – Chapitre 10 : Classes abstraites et interfaces

Voici les diapos du chapitre du cours que nous avons commencé lors de la séance précédente :

POO – Chapitre 9 : Héritage – Approfondissement

Voici les diapos du chapitre 9 du livre, que nous continuons cette semaine :

POO – TD 5 : Héritage

La série de TD N°5 est constituée d’une sélection d’exercices provenant des chapitres 8, 9 et 10 du livre de D. Barnes et M. Kölling « Conception objet en Java… » 4e édition :