IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

C++ expliqué aux programmeurs C

Date de publication : 24 mars 2009


II. C++ comparé au C
II-A. Point d'entrée d'un programme
II-B. Bibliothèque standard
II-C. Les flux standards
II-D. Les instructions
II-E. Les types et les valeurs
II-E-1. Généralités
II-E-2. Les chaînes de caractères
II-E-2-a. Les types string et wstring
II-E-2-b. Les "stringstreams"
II-E-3. Le type booléen (bool)
II-E-4. Structures, unions et énumérations
II-E-5. Hiérarchie des types
II-E-6. Les références
II-E-6-a. Généralités
II-E-6-b. Références sur une rvalue
II-F. Fonctions
II-F-1. Les fonctions en C++
II-F-2. La surcharge des fonctions
II-F-3. Valeurs par défaut des paramètres
II-F-4. Les fonctions inline
II-F-5. Les déclarations et les prototypes
II-F-6. Interface avec les autres langages
II-F-6-a. Introduction
II-F-6-b. Appel de fonction C++ en C
II-F-6-c. Appel de fonction C en C++
II-G. Autres nouveautés
II-G-1. Les espaces de noms
II-G-1-a. Introduction
II-G-1-b. L'espace de noms global
II-G-1-c. Les espaces de noms anonymes
II-G-2. Les opérateurs new et delete
II-G-3. Les exceptions
II-G-3-a. Introduction
II-G-3-b. Comment lever un exception


II. C++ comparé au C


II-A. Point d'entrée d'un programme

Tout comme en C, le point d'entrée d'un programme C++ standard est cette chère et précieuse fonction main. Voici donc un programme C++ minimal.
main.cpp
int main()
{
    return 0;
}
En C++ (mais pas en C), void entre les parenthèses, que ce soit dans la déclaration ou dans la définition d'une fonction, est toujours facultative. La paire de parenthèses vide indique donc, que ce soit dans une déclaration ou dans une définition, que la fonction n'admet aucun paramètre.


II-B. Bibliothèque standard

La bibliothèque standard du C++ est plus vaste que celle du C et est compatible avec celle-ci. Cela signifie que toutes les fonctions de la bibliothèque standard du C (printf, scanf, malloc, free, etc.) se retrouvent également dans la bibliothèque standard du C++. Le programme suivant utilise printf pour écrire "Hello, world !" sur la sortie standard.
#include <cstdio>

int main()
{
    std::printf("Hello, world !\n");
    
    return 0;
}
cstdio, c'est le "nom C++" du fichier contenant la déclaration de la fonction printf. Le véritable nom du fichier dépend de l'implémentation. Ca peut être effectivement cstdio sinon cstdio.h ou encore cstdio.hpp ou un autre. A ne pas confondre avec stdio.h qui est un fichier de la bibliothèque standard du C et non de celle du C++.

std est l'espace de nom (une notion que nous allons étudier plus tard) dans lequel se retrouvent toutes les fonctions, types, variables globales, etc. composant la bibliothèque standard. L'opérateur ::, appelé ORP (Opérateur de Résolution de Portée), permet entre autres de sélectionner un élément d'un espace de nom (nous verrons d'autres utilisations de cet opérateur plus tard). Ainsi, std::printf signifie donc : la fonction printf de l'espace de nom std. Si votre programme utilise intensivement la bibliothèque standard, alors il est généralement préférable de mettre "using namespace std;" après l'inclusion des différents fichiers d'en-tête. Voici comment donc une deuxième version du programme précédent :
#include <cstdio>

using namespace std;

int main()
{
    printf("Hello, world !\n");
    
    return 0;
}
On aurait pu également mettre la directive using namespace std; à l'intérieur plutôt qu'à l'extérieur de la fonction main et dans ce cas, elle n'aura d'effet qu'à l'intérieur de cette fonction.


II-C. Les flux standards

Bien que les flux stdin, stdout et stderr ainsi que les fonctions qui leurs sont associées existent toujours en C++, on préfèrera généralement utiliser les objets std::cin, std::cout et std::cerr qui sont plus simples à utiliser (nous verrons ce que c'est qu'un objet plus tard). Si vous utilisez des chaînes de caractères larges, alors vous utiliserez plutôt std::wcin, std::wcout et std::wcerr. Tous ces objets sont déclarés dans le fichier <iostream>. Voici un exemple d'utilisation :
#include <iostream>

using namespace std;

int main()
{
    int a, b;
    
    cout << "a = ";
    cin >> a;
    
    cout << "b = ";
    cin >> b;
    
    cout << "a + b = " << a + b << "\n";
    
    wcout << L"Au revoir.\n";
    
    return 0;
}
L'opérateur << permet entre autres de passer des données à cout en vue d'être imprimées sur la sortie standard tandis que l'opérateur >> permet de lire des données depuis l'entrée standard.

En C++, tout comme en C, les flux d'entrées/sorties sont par défaut bufferisées. Cela signifie que les données que vous envoyez sur la sortie standard par exemple ne sont pas immédiatement écrites mais placées dans un tampon. La gestion avancée de ces tampons sera étudiée un peu plus tard mais pour le moment, il faut déjà au moins savoir que, si x désigne un flux de sortie, on peut forcer le vidage du tampon associé à x à l'aide de la fonction std::fflush bien sûr (un héritage laissé par C) mais aussi, à la manière purement C++, à l'aide de std::flush (c'est bien flush et non plus fflush). Il y a aussi std::endl qui provoque l'émission du caractère de fin de ligne suivi d'un appel à std::flush. En C++, on utilise donc souvent endl à la place d'un simple '\n' pour imprimer une ligne comme montré dans l'exemple ci-dessous.
#include <iostream>

using namespace std;

int main()
{
    cout << "Hello, world !" << endl; // Ce qui est equivalent a : cout << "Hello, world !" << "\n" << flush;
    
    return 0;
}

II-D. Les instructions

Comme nous l'avons déjà pu le constater, les instructions du C++ s'écrivent de la même manière que celles du C (point-virgule à la fin de chaque instruction élémentaire, accolades pour délimiter un bloc, etc.). Le C++ dispose cependant pas que de nouvelles instructions mais aussi de nouvelles règles comme la possibilité de déclarer des variables ailleurs qu'en début de bloc comme l'illustre le programme ci-dessous.
#include <iostream>

using namespace std;

int main()
{
    cout << " Voici la table de multiplication par 5 :" << endl ;
    
    int n = 5;

    for(int i = 0; i <= 10; i++)
        cout << n << " x " << i << " = " << n * i << endl;

    // i n'est plus défini ici.

    return 0;
}
Les autres nouveautés seront abordées en fonction de nos besoins.


II-E. Les types et les valeurs


II-E-1. Généralités

En C++ comme en C, les expressions peuvent être classées en trois catégories :

  • Les lvalues, qui qui possèdent une adresse et une valeur (par exemple une variable)
  • Les rvalues, qui possèdent au moins une valeur (donc en pratique, tout ce qui n'est pas de type void, variable ou pas, etc.)
  • Et les expressions de type void qui ne sont ni des lvalues ni des rvalues.

Les lvalues et rvalues possèdent un type et une valeur. Les expressions de type void possèdent un type (void) mais pas de valeur. On constate rapidement que la notion d'expression en C et en C++ sont très voisines. Les types char, int, float, double, tableaux, pointeurs, structures, unions, énumérations, etc. bref tous les types existant en C existent également en C++. Tous les opérateurs du C sont également présents en C++ et, fondamentalement, obéissent aux mêmes règles. Malgré tout cela, il y a tellement de nouveautés dont certaines sont si importantes pour la suite que nous devons en parler dès maintenant.


II-E-2. Les chaînes de caractères


II-E-2-a. Les types string et wstring
En C++, la manipulation des chaînes a été grandement facilitée avec l'introduction des types std::string et std::wstring, déclarés dans le fichier <string> (qu'il ne faut pas confondre avec <cstring>). std::string et std::wstring sont en fait ce qu'on appelle en POO des classes. Les classes sont une généralisation de ce qu'on appelait en C des "structures" (struct). Nous y reviendrons un peu plus tard mais pour l'instant, voyons un exemple d'utilisation de ces nouveaux types.
#include <iostream>
#include <string>

using namespace std;

int main()
{
    string s, x("A tres bientot !"); // Ou : string s, x = "A tres bientot !";

    cout << "Votre nom : ";
    cin >> s;

    cout << "Bonjour " << s << "." << endl;
    cout << "Votre nom comporte : " << s.length() << " caracteres." << endl;
    cout << "Votre initial est : " << s[0] << "." << endl;

    s = x + " ;)";
    cout <<  s << endl;

    return 0;
}
Premièrement, la ligne 'string x("A tres bientot !");' est ce qu'on appelle une construction d'objet avec initialisation. Un objet est tout simplement une instance d'une classe (par exemple une variable du type de la classe). Ici, l'objet (x) est initialisé avec la chaîne "A tres bientot !". Pour la classe string (ce n'est pas forcément applicable avec les autres classes), cette initialisation a également pu s'écrire 'string x = "A tres bientot !";'. Cela est plutôt logique étant donné que le but des nouveaux types "chaînes" est de faciliter la manipulation des chaînes.

Deuxièmement, le format de chaîne attendu par cin avec l'instruction "cin << s;" est une chaîne sans espace (comme dans scanf("%s", ...)). Pour lire une ligne complète (comme avec fgets()), on pourra utiliser la fonction std::getline() toujours déclarée dans <string>. Cette fonction requiert en premier argument le flux d'entrée depuis lequel la ligne sera lue et en deuxième argument l'objet chaîne dans laquelle la ligne lue sera stockée. Par exemple, pour lire une ligne depuis cin puis la stocker dans s, on pourra écrire 'getline(cin, s);'.

Troisièmement, remarquez bien la manière dont nous avons invoquée la fonction length(). length() est une fonction membre de la classe string. Elle retourne le nombre de caractères contenus dans l'objet qui l'a appelée (ici : s). En POO, les objets possèdent donc pas seulement des données membres (que nous appelions champs en C) mais aussi des fonctions membres. C'est un des apports majeurs de la POO (C++, Java, etc.) par rapport à la PP (Fortran, C, etc.).

Enfin, nous remarquons également que les chaînes du C++ supportent de nombreuses opérations (=, [], + , etc.) ce qui ne fait que faciliter encore plus leur utilisation.


II-E-2-b. Les "stringstreams"
Nous avons vu que l'utilisation des fonctions printf() et scanf() est dépréciée en C++ en faveur de l'utilisation des objets cin et cout, pas parce que ces fonctions sont obsolètes ou inefficaces, bien au contraire, mais parce qu'elles sont trop complexes et sources de nombreuses erreurs dans beaucoup de programmes. cin et cout sont respectivement des instances des classes std::istream et std::ostream. Ces types représentent en C++ un flux d'entrée (istream) et un flux de sortie (ostream).

Dans le fichier <sstream< sont également déclarés deux types très utiles pour la manipulation des chaînes. Il s'agit de std::istringstream et de std::ostringstream. Ils permettent d'effectuer des lectures et écriture formattées en mémoire (comme avec sprintf() et sscanf()) à la manière "C++", c'est-à-dire avec les opérateurs << et >>, etc. En fait, les stringstreams n'écrivent ni lisent les caractères directement dans un buffer (comme le faisaient les "strstreams" (istrstream et ostrstream) qui sont de nos jours obsolètes, notamment en raison de leur manque de souplesse) mais dans un objet string d'où leur appelation (stringstreams). Cet objet string est accessible à tout moment en appelant juste la fonction membre str().

Voici un exemple d'utilisation des types istringstream et ostringstream.
#include <iostream>
#include <string>
#include <sstream>

using namespace std;

int main()
{
    int a = 1, b = 2;
    ostringstream oss;

    oss << "a = " << a << endl;
    oss << "b = " << b << endl;
    oss << "a + b = " << (a + b) << endl;
    
    cout << oss.str() << flush;

    cout << endl;

    int jour, annee;
    string mois, date = "24 mars 2009";
    istringstream iss(date); // Ou : istringstream iss("24 mars 2009");

    iss >> jour >> mois >> annee;

    cout << "date = " << date << endl;
    cout << "jour = " << jour << endl;
    cout << "mois = " << mois << endl;
    cout << "annee = " << annee << endl;

    return 0;
}
Sortie du programme :
a = 1
b = 2
a + b = 3

date = 24 mars 2009
jour = 24
mois = mars
annee = 2009

II-E-3. Le type booléen (bool)

En C, on utilisait le type int pour représenter des valeurs booléennes. 0 représentait la valeur FAUX et toute valeur non nulle la valeur VRAI. Le C++ a introduit le type bool et les constantes true et false. Les expressions logiques ne renvoient plus int mais bool. Par exemple, l'expression 1 < 2 vaut true.

En fait, les constantes true et false (qui sont en fait, tout comme bool, des mots-clés du langage) vallent respectivement 0 et 1. La valeur entière 1 est donc équivalente à la constante true et la valeur entière 0 à la constate false. Les autres valeurs ne doivent pas être converties en bool.


II-E-4. Structures, unions et énumérations

En C, les mots-clés struct, union et enum étaient attachés aux noms des types créés avec, ce qui n'est plus le cas en C++. Vous pouvez bien sûr continuer à utiliser la syntaxe C mais cela ne ferait que surcharger inutilement votre code.

Voici une petite démonstration de la nouvelle syntaxe :
#include <iostream>
#include <string>

using namespace std;

enum Sexe {
    SX_M, SX_F
};

struct Personne {
    string nom;
    int age;
    Sexe sx;
};

int main()
{
    Personne jean;

    jean.nom = "Jean";
    jean.age = 24;
    jean.sx = SX_M;

    cout << "Nom : " << jean.nom << endl;
    cout << "Age : " << jean.age << endl;

    const char * s = jean.sx == SX_M ? "masculin" : "feminin";
    cout << "Sexe : " << s << endl;

    return 0;
}

II-E-5. Hiérarchie des types

Le C++, contrairement au C, est un langage dit fortement typé, ce qui signifie qu'il est assez strict sur la manière d'utiliser les types. A titre d'exemple, la conversion d'un type énuméré vers int est toujours implicite mais l'inverse, qui était également implicite en C, ne l'est plus. Pour convertir un int en un type énuméré, il faut désormais un cast explicite et il est également requis que la valeur castée corresponde effectivement à une valeur de l'énumération sinon le comportement est dépendant de l'implémentation. En règle générale, lorsqu'un type T2 "dérive" d'un type T1 (par exemple, les types énumérés qui dérivent tous du type int), la conversion de T2 vers T1 est implicite mais l'inverse non (nécessite un cast explicite et éventuellement des conditions supplémentaires). Un autre exemple bien courant : les types pointeurs dérivant tous de void *, la conversion de n'importe quel type pointeur vers void * est implicite mais l'inverse nécessite un cast explicite.



II-E-6. Les références


II-E-6-a. Généralités
Une référence à une variable est une variable qui sert d'alias à la première. En d'autres termes, si x est une variable et y une référence à x, alors &x = &y, ce qui signifie que x et y font en fait référence à un même emplacement mémoire (à une même variable). Contrairement au C, le C++ permet donc d'assigner plusieurs noms (x, y, etc.) à un même emplacement mémoire comme le montre l'exemple ci-dessous.
#include <iostream>

using namespace std;

int main()
{
    int x;
    int & y = x; // y est une reference a x.
    
    y = 0;
    
    cout << "x = " << x << endl; // Affiche 'x = 0'.
    
    return 0;
}
Une référence doit impérativement être initialisée car après son initialisation, elle s'utilise comme une variable tout à fait normale. Par exemple :
#include <iostream>

using namespace std;

int main()
{
    int x;
    int & y = x; // y est une reference a x.
    int z = 1;
    
    y = z; // Affecte la valeur 1 a y (et donc a x aussi).
    
    cout << "x = " << x << endl; // Affiche 'x = 1'.
    cout << "y = " << y << endl; // Affiche 'y = 1'.
    
    x = 2; // Affecte la valeur 2 a x (et donc a y aussi).
    
    cout << "x = " << x << endl; // Affiche 'x = 2'.
    cout << "y = " << y << endl; // Affiche 'y = 2'.
    
    return 0;
}
Les références sont surtout utilisées en paramètre d'une fonction pour donner à celle-ci la possibilité de lire ou écrire une variable sans passer explicitement son adresse. Par exemple :
#include <iostream>

using namespace std;

void init(int & n);

int main()
{
    int n = 1;
    
    init(n);
    
    cout << "n = " << n << endl; // Affiche 'n = 0'.
    
    return 0;
}

void init(int & n)
{
    n = 0;
}
Parfois, il reste toujours avantageux de passer une variable par référence même si la fonction ne modifie pas la variable, comme lorsque cette dernière est de taille importante par exemple (une structure par exemple). En effet dans ce cas, il est avantageux de passer tout simplement l'adresse de la variable que de passer une copie. L'utilisation du passage par référence permet d'utiliser la simplicité de la syntaxe du passage par valeur et le gain en performance apporté par le passage d'adresse. Comme la fonction ici ne doit pas modifier la variable, il est fortement recommandé de la déclarer const. Par exemple :
#include <iostream>
#include <string>

using namespace std;

enum Sexe {
    SX_M, SX_F
};

struct Personne {
    string nom;
    int age;
    Sexe sx;
};

const char * sexe(Personne x);

int main()
{
    Personne jean;

    jean.nom = "Jean";
    jean.age = 24;
    jean.sx = SX_M;

    cout << "Nom : " << jean.nom << endl;
    cout << "Age : " << jean.age << endl;

    cout << "Sexe : " << sexe(jean) << endl;

    return 0;
}

const char * sexe(const Personne & x)
{
    return x.sx == SX_M ? "masculin" : "feminin";
}

II-E-6-b. Références sur une rvalue
Logiquement, une référence doit être initialisée avec une lvalue car une adresse est requise pour créer une référence. Cependant, contre toute attente, le C++ permet également d'initialiser une référence à l'aide d'une rvalue ce qui mérite quelques bonnes explications.

Tout d'abord, lorsque le compilateur rencontre une initialisation d'une référence à l'aide d'une rvalue, sa réaction logique sera de nous suggérer d'utiliser une nouvelle mémoire pour la stocker au lieu de tenter de créer expressément une référence à queque chose qui n'est même pas forcément en mémoire (en effet la rvalue peut se trouver par exemple dans un registre du processeur ...). En clair, une déclaration telle que 'int & n = 1;' ou une instruction telle que f(1) où f est une fonction qui requiert en paramètre un int par référence sera rejettée par le compilateur. Il y a cependant une échappatoire apparente à cette règle, c'est de déclarer const l'initialisante. Dans ce cas, le compilateur supprimera tout simplement le caractère "référence" de la variable, ce qui signifie qu'une déclaration telle que 'const int & n = 1;' par exemple sera toujours transformée, 1 étant une rvalue, en 'const int n = 1;'.



II-F. Fonctions


II-F-1. Les fonctions en C++

A la différence du C, le C++ permet de créer plusieurs fonctions portant le même nom pour peu que le type et/ou le nombre d'arguments requis soient différents. On dit que le C++ permet la surcharge des fonctions. Il est, désormais, également possible de spécifier des valeurs par défaut aux paramètres des fonctions (la valeur par défaut d'un paramètre est la valeur à attribuer à ce paramètre lorsqu'il n'a pas été fourni lors de l'appel à la fonction). Et enfin, le C++ supporte également la définition de fonctions "inline" (en ligne) qui ont un fonctionnement similaire aux macros sauf que, contrairement à ces dernières, les fonctions inline traitent vraiment les paramètres à la manière des fonctions ordinaires.

Les paragraphes suivants traitent de ces différentes nouveautés apportées par le C++.


II-F-2. La surcharge des fonctions

Comme nous venons de le dire tout à l'heure, le C++ permet de créer plusieurs fonctions portant le même nom pour peu que le type et/ou le nombre d'arguments requis soient différents. Cela est possible car le C++ applique aux fonctions ce qu'on appelle la décoration de nom (name mangling). Cela signifie que lorsqu'on crée une fonction, double power(double, int) par exemple, le compilateur renomme en interne cette fonction (power) en quelque chose comme power@2_d_i (fonction power qui prend deux arguments : le premier de type double et le second de type int), le format effectivement utilisé étant dépendant de l'implémentation. Ainsi, à la rencontre d'une expression telle que power(0.5, 2), le compilateur saura que la fonction power à utiliser dans cette expression n'est autre que la fonction power@2_d_i et remplacera donc cette écriture en power@2_d_i(0.5, 2). Voici un exemple d'utilisation de la surcharge des fonctions :
#include <iostream>

using namespace std;

double power(double a, int n);
unsigned power(unsigned n);

int main()
{
    double a = 0.5;
    int n = 2;

    cout << "power(" << a << ", " << n << ") = " << power(a, n) << endl;
    cout << "power(" << n << ") = " << power(n) << endl;

    return 0;
}

double power(double a, int n)
{
    if (n < 0)
        return power(1 / a, -n);
    else if (n == 0)
        return 1.0;
    else
        return a * power(a, n - 1);
}

unsigned power(unsigned n)
{    
    if (n == 0)
        return 1;
    else
        return 10 * power(n - 1);
}
Il faut également savoir que, tout comme le C, le C++ effectue parfois des conversions implicites (par exemple int vers double, char vers int, etc.) ce qui signifie que si on écrit par exemple power(5, 2), ne trouvant pas de fonction portant la signature power@2_i_i (fonction power qui prend deux arguments tous de type int), le compilateur va chercher et utiliser la fonction "la plus voisine" qui n'est autre dans notre cas que la fonction power@2_d_i. Si aucune fonction "de second choix" n'a été trouvée, ou si la fonction de premier choix n'a pas été trouvée et qu'il existe plusieurs fonctions qui pourraient être utilisées en tant que second choix, le compilateur génèrera une erreur.

Et enfin, n'oubliez pas que la surcharge des fonctions ne peut s'appliquer qu'aux fonctions qui diffèrent par leurs arguments requis, pas à des fonctions qui ne diffèrent que par le type de la valeur retournée. En effet, considérons par exemple les fonctions void print(int n) et bool print(int n). Face à une instruction telle que "print(1);", laquelle des deux fonctions devrait choisir le compilateur ?


II-F-3. Valeurs par défaut des paramètres

Le C++ supporte une forme de prototype inexistante en C permettant de spécifier des valeurs par défaut à des paramètres. Pour spécifier la valeur par défaut d'un paramètre, il suffit de faire suivre son nom du symbole = et de la valeur par défaut à utiliser. A chaque appel de la fonction, si un ou plusieurs paramètres n'ont pas été spécifiés, le compilateur remplacera l'appel en un appel équivalent avec tous les paramètres spécifiés, en utilisant les valeurs par défaut pour ceux qui n'ont pas été spécifiés. Par exemple :
#include <iostream>

using namespace std;

bool do_div(unsigned a, unsigned b, unsigned * p1, unsigned * p2 = NULL);

int main()
{
    unsigned a, b, q, r;

    cout << "a = ";
    cin >> a;

    cout << "b = ";
    cin >> b;

    if (do_div(a, b, &q)) // <=> if (do_div(a, b, &q, NULL))
    {
        cout << "quotient = " << q << endl;

        do_div(a, b, NULL, &r);

        cout << "reste = " << r << endl;
    }

    return 0;
}

bool do_div(unsigned a, unsigned b, unsigned * p1, unsigned * p2)
{
    bool ret = false;

    if (b != 0)
    {
        if (p1 != NULL)
            *p1 = a / b;

        if (p2 != NULL)
            *p2 = a % b;

        ret = true;
    }

    return ret;
}
N'oubliez pas qu'en C comme en C++, il est toujours possible d'intégrer le prototype dans la définition d'une fonction, ce qui signifie que vous pouvez écrire quelque chose qui ressemble à ceci :
#include <iostream>

using namespace std;

int id(int x = 0)
{
    return x;
}

int main()
{
    cout << id() << endl;

    return 0;
}
Et enfin, pour utiliser les valeurs par défaut, il faut spécifier tous les arguments jusqu'au dernier que l'on désire spécifier. Il n'est par exemple pas possible d'écrire f(0,,2) ou f(,,2), etc.


II-F-4. Les fonctions inline

Les fonctions inline (en ligne) sont au compilateur ce que les macros sont au préprocesseur. Pour rendre une fonction inline, il suffit d'ajouter le mot-clé inline devant sa définition (le mot-clé inline ne sert absolument à rien devant la déclaration d'une fonction). Par exemple :
#include <iostream>

int f(int x); // Declaration de la fonction f() (utilisee dans main()).

int main()
{    
    cout << f(4) << endl;
    
    return 0;
}

// Definition de la fonction f().
// On demande au compilateur de rendre la fonction inline.

inline int f(int x)
{
    return x * x - 3;
}
Lorsqu'une fonction est inline, sa visibilité est par défaut restreinte au fichier source courant. Le mot-clé static est donc inutile devant la définition d'une fonction inline car comme nous venons tout juste de le dire, une fonction inline est déjà, par défaut, static. Pour la rendre visible dans tout le projet, il faut donc explicitement le spécifier à l'aide du mot-clé extern.

Il faut également savoir que la présence du mot-clé inline devant la définition d'une fonction ne force pas cette fonction à être inline mais demande au compilateur de la rendre inline si possible. Le compilateur ne pourra pas rendre la fonction inline dans deux cas :

  • La fonction est "trop compliquée" (fonction récursive par exemple).
  • L'adresse de la fonction est utilisée implicitement ou explicitement quelque part dans le programme. En effet les fonctions inline n'existent que dans le fichier source, elles n'existent pas dans le fichier objet.

II-F-5. Les déclarations et les prototypes

En langage C, il était possible de ne pas déclarer une fonction retournant un int ou, dans une définition de fonction, d'omettre le type de retour lorsque le type de retour est int ou encore de déclarer une fonction sans donner le prototype. En C++ :

  • Une fonction doit être déclarée ou définie avant son utilisation.
  • Dans la définition d'une fonction, le type de retour doit toujours être spécifié.
  • Dans la déclaration d'une fonction, le type de retour et les arguments requis doivent tous être spécifiés.
  • Que ce soit dans une déclaration ou une définition, des parenthèses vides après le nom de la fonction signifient que la fonction ne requiert aucun argument. Tout ce passe donc comme si on avait mis void à l'intérieur.

II-F-6. Interface avec les autres langages


II-F-6-a. Introduction
Comme nous venons juste de le voir, le C++ décore les noms de fonctions (et en fait, des variables également) pour assurer, entre autres, un meilleur contrôle des types. Cette décoration se fait par défaut selon une manière propre au langage (le C++) et aussi au compilateur. Il est cependant possible d'indiquer au compilateur d'utiliser une autre technique de décoration (référencée dans ce paragraphe par TD) si jamais vous en ressentiez le besoin. Pour simplifier les explications, admettons que "pas de décoration" est aussi une TD. Le C est un exemple de langage qui utilise cette technique.

Pour spécifier la TD à appliquer sur une variable ou une fonction, il suffit de précéder sa déclaration (qui peut être une déclaration-définition) de extern suivi d'une chaîne de caractères indiquant le langage dont la TD est à appliquer. La norme stipule que tous les compilateurs doivent au moins supporter les langages "C" et "C++" mais chaque implémentation est libre d'en supporter plus. Par défaut, c'est extern "C++" qui est appliquée.

Il ne faut cependant pas confondre spécification de TD (extern "<langage>") et spécification de visibilité (extern/static). Une déclaration peut à la fois comporter une spécification de TD et une spécification de visibilité. De plus, pour une variable, lorsqu'une spécification de TD est rencontrée et que la variable est de niveau extern (visibilité par défaut), alors le compilateur conclura qu'il s'agit d'une déclaration et non d'une définition.

Voici un extrait de code qui montre diverses utilisations de spécification de TD.
extern "C" int x; // x est de niveau extern et la ligne comporte une specification de TD => c'est une declaration.
int x; // Definition de x.

extern "C" static int y; // y est de niveau static => c'est une declaration-definition. Plus besoin de definir y.

extern "C" int f(int n = 0); // Declaration d'une fonction de niveau extern f.

int f(int n) // Definition de f
{
    return n;
}
Dans cet exemple, le compilateur appliquera la TD du C et non celle du C++ à x, y et f.

Il est également permis de regrouper plusieurs déclarations à l'intérieur d'un bloc extern "<langage>" comme le montre l'extrait de code ci-dessous.
extern "C"
{
    // Les fonctions ou variables declarees ou definies dans ce bloc seront decorees par defaut avec la TD du C.
    
    extern int x; // Declaration d'une variable de niveau extern x.
    static int y; // Declaration-definition d'une variable de niveau static y.
    int z; // Decalartion-definition d'une variable de niveau extern z.
    
    extern "C++" int f(int n = 0); // Declaration d'une fonction de niveau extern f.
}

int x; // Definition de x.

int f(int n) // Definition de f.
{
    return n;
}

II-F-6-b. Appel de fonction C++ en C
La seule manière standard d'appeler une fonction (ou d'accéder à une variable) C++ en C est d'avoir déclaré la fonction (ou la variable) extern "C". Le passage de paramètres depuis le code en C doit se faire en respectant scrupuleusement les attentes de la fonction. Il n'existe en effet en C aucun moyen de savoir à la compilation si l'appel à une fonction contenue dans un fichier objet (.obj, .lib ou .dll, etc.) s'est fait correctement ou non. En C++, si - grâce à la décoration de nom.

Voici un exemple de code C++ que nous allons utiliser depuis un programme C.
fonctions.cpp
#include <iostream>

using namespace std;

extern "C" void println(const char * s)
{
    cout << s << endl;
}

extern "C" void init(int & n)
{
    n = 0;
}
Et voici un programme C utilisant ces fonctions :
mixed.c
#include <stdio.h>

void println(const char * s);
void init(int * p);

int main()
{
    int n = 10;

    println("Ce programme utilise des fonctions ecrites en C++.");

    printf("Avant init() : n = %d\n", n);
    init(&n);
    printf("Apres init() : n = %d\n", n);

    println("A la prochaine !");

    return 0;
}
Remarquez bien la manière dont nous avons déclaré la fonction init(). D'après sa définition (C++), cette fonction requiert en argument une référence à la variable à initialiser. Or, une référence lors de son initialisation est implémenté par un pointeur puis, une fois l'initialisation du pointeur terminée, l'adresse reçue est associée à l'alias. Ainsi, lorsqu'une fonction C++ attend une référence, lors d'un appel en C, c'est une adresse qu'il faut donner.


II-F-6-c. Appel de fonction C en C++
Pour appeler une fonction (ou accéder à une variable) C depuis un programme C++, il suffit de la déclarer extern "C".

Voici un exemple de code C que nous allons utiliser depuis un programme C++.
fonctions.c
double power(double a, int n)
{
    if (n < 0)
        return power(1 / a, -n);
    else if (n == 0)
        return 1.0;
    else
        return a * power(a, n - 1);
}

void init(int * p)
{
    *p = 0;
}
Pour appeler la fonction init, on peut soit conserver le prototype void init(int *), soit masquer le passage d'adresse par un passage par référence ce qui, dans tous les cas, transmettra une adresse valide à la fonction. Pour la fonction power, nous n'avons pas d'autre choix que de garder le prototype original. Nous pouvons par contre demander au compilateur de rendre le deuxième argument optionnel par exemple afin de ne pas avoir à le spécifier dans certains cas.

Voici donc un exemple de déclaration et d'utilisation de ces fonctions en C++ :
mixed.cpp
#include <iostream>

using namespace std;

extern "C"
{
    double power(double a, int n = 2);
    void init(int & n);
}

int main()
{
    double a;
    int n = 3;

    cout << "a = ";
    cin >> a;
    cout << "n = " << n << endl;

    cout << "power(a) = " << power(a) << endl;
    cout << "power(a, n) = " << power(a, n) << endl;

    init(n);

    cout << "n = " << n << endl;

    return 0;
}

II-G. Autres nouveautés


II-G-1. Les espaces de noms


II-G-1-a. Introduction
Les espaces de noms permettent au programmeur de créer des bibliothèques avec l'assurance que les différents noms (de fonction, de type, etc.) utilisés dans ces bibliothèques n'entreront pas en conflit avec ceux de n'importe quelle autre bibliothèque. Nous avons déjà vu comment utiliser l'opérateur de résolution de portée (ORP) pour accéder à un élément d'un espace de noms donné. Nous allons voir cette fois-ci un exemple de création et d'utilisation d'espaces de noms.
#include <iostream>

// On est dans l'espace de noms global

namespace exo1
{
    // On est dans l'espace de nom appele exo1
    
    int f(int x);
    int g(int x);
}

// On est dans l'espace de noms global

namespace exo2
{
    // On est dans l'espace de nom appele exo2
    
    int f(int x);
    int g(int x);
    int h(int x);
}

// On est dans l'espace de noms global

int main()
{
    int x = 0;

    std::cout << "exo1::f(" << x << ") = " << exo1::f(x) << std::endl;
    std::cout << "exo2::f(" << x << ") = " << exo2::f(x) << std::endl;

    return 0;
}

namespace exo1
{
    // On est dans l'espace de nom appele exo1
    
    int f(int x)
    {
        return x * x - 3;
    }

    int g(int x)
    {
        return x * x - 2 * x - 1;
    }
}

// On est dans l'espace de noms global

namespace exo2
{
    // On est dans l'espace de nom appele exo2
    
    int f(int x)
    {
        return x * x - 4;
    }

    int g(int x)
    {
        return x * x - 4 * x + 2;
    }
}

// On est dans l'espace de noms global

int exo2::h(int x) // Definition de exo2::h.
{
    return x * x + 4 * x + 2;
}
Le mot-clé using permet de rejoindre un espace de noms ou encore de déclarer un nom (un élément d'un espace de noms) qui pourra alors par suite être utilisé sans référence complète. Voici un exemple d'utilisation :
#include <iostream>

namespace exo1
{
    int f(int x);
    int g(int x);
}

namespace exo2
{
    int f(int x);
    int g(int x);
    int h(int x);
}

using namespace std; // Directive using : importation de l'espace de noms std.
using namespace exo1; // Directive using : importation de l'espace de noms exo1.
using exo2::h; // Declaration using : declaration de exo2::h.

int main()
{
    int x = 0;

    cout << "exo1::f(" << x << ") = " << f(x) << endl;
    cout << "exo2::f(" << x << ") = " << exo2::f(x) << endl;
    cout << "exo2::g(" << x << ") = " << exo2::g(x) << endl;
    cout << "exo2::h(" << x << ") = " << h(x) << endl;

    return 0;
}

namespace exo1
{
    int f(int x)
    {
        return x * x - 3;
    }

    int g(int x)
    {
        return x * x - 2 * x - 1;
    }
}

namespace exo2
{
    int f(int x)
    {
        return x * x - 4;
    }

    int g(int x)
    {
        return x * x - 4 * x + 2;
    }

    int h(int x)
    {
        return x * x + 4 * x + 2;
    }
}
Attention ! Si nous avons par exemple aussi mis 'using namespace exo2;', alors l'appel à f dans la fonction main est ambigü et le compilateur aurait généré une erreur. Il fallait alors explicitement mettre dans ce cas exo1::f au lieu de f seulement.

Il faut également se garder d'importer un espace de noms (et plus généralement, de mettre une directive ou une déclaration using) dans un fichier d'en-tête. En effet, cela imposerait à quiconque inclura ce fichier d'utiliser les espaces de noms importés or l'intérêt des espaces de noms, c'est justement de permettre à l'utilisateur de sélectionner à tout moment l'espace qu'il veut utiliser.


II-G-1-b. L'espace de noms global
L'espace de nom global est l'espace de noms qui contient tous les noms déclarés et/ou définis au niveau global. Pour accéder à un élément de cet espace de noms depuis un autre, il suffit de placer l'ORP devant son nom. Par exemple :
#include <iostream>

using namespace std;

int f(int x);

namespace N
{
    int f(int x);
}

int main()
{
    int x = 4;

    cout << "f(" << x << ") = " << f(x) << endl;
    cout << "N::f(" << x << ") = " << N::f(x) << endl;

    return 0;
}

int f(int x)
{
    return x - 3;
}

namespace N
{
    int f(int x)
    {
        return ::f (x * x); // ::f designe la fonction f de l'espace de noms global.
    }
}

II-G-1-c. Les espaces de noms anonymes
Le C++ permet aussi de créer des espaces de noms dits anonymes. En fait, un espace de nom possède toujours un nom seulement, dans le cas présent, c'est le compilateur qui s'est chargé de lui en donner un. Ce nom est garanti être unique dans tout le projet.

Chaque fichier source peut disposer de son espace de noms anonyme. Les espaces de noms anonymes permettent donc de créer des variables globales, fonctions, types, etc. visibles uniquement dans le fichier source car le nom de l'espace de nom est unique dans tout le projet et n'est à priori connu que du compilateur.

Voici un exemple de création et d'utilisation d'un espace de nom anonyme.
#include <iostream>

using namespace std;

namespace
{
    int f(int x);
}

int main()
{
    int x = 4;

    cout << "private::f(" << x << ") = " << f(x) << endl;

    return 0;
}

namespace
{
    int f(int x)
    {
        return x * x - 3;
    }
}

II-G-2. Les opérateurs new et delete

Le C++ dispose de deux nouveaux opérateurs à savoir new et delete permettant respectivement d'allouer de la mémoire et de libérer la mémoire allouée dynamiquement. En fait, new et delete font plus qu'allouer et libérer la mémoire mais n'ou n'en parlerons pas pour le moment. Ce qu'il faut retenir, c'est que new n'est pas malloc comme delete n'est pas free, même s'il est vrai qu'habituellement, ils sont implémentés à l'aide de ces fonctions.

Le programme suivant montre une première utilisation possible de ces opérateurs :
#include <iostream>

using namespace std;

int main()
{
    char * p = new double; // Allocation d'une memoire pour stocker un double.

    *p = 0.5;

    cout << "*p = " << *p << endl;

    delete p; // Liberation de la memoire en p.

    return 0;
}
Et voici un autre exemple d'utilisation encore plus courant :
#include <iostream>
#include <cstring>

using namespace std;

int main()
{
    char * p = new char [50]; // Construction d'un tableau compose de 50 caracteres.

    strcpy(p, "Bonjour tout le monde !");

    cout << p << endl;

    delete [] p; // Destruction du tableau p.

    return 0;
}
Remarquez bien que cette fois-ci, on a utilisé la syntaxe delete [] p et non delete p pour détruire le tableau p. La raison est toute simple : p étant un pointeur sur char (char *), l'instruction "delete p;" va détruire la mémoire utilisée par *p autrement dit, la mémoire utilisée par p[0] uniquement ! Pour que le compilateur sache qu'on a affaire à un tableau, il faut utiliser la syntaxe prévue pour.

delete sur un pointeur nul n'a aucun effet.


II-G-3. Les exceptions


II-G-3-a. Introduction
Une exception est une erreur produite pendant l'exécution et qui requiert un traitement immédiat. A défaut de traitement, l'action par défaut du système sera de terminer l'application. Une exception ne peut donc être ignorée.

Lorsqu'on tente, par exemple, de demander au système plus de mémoire qu'il ne puisse en donner à l'aide de l'opérateur new, une exception de type std::bad_alloc (déclarée dans <new>) est levée. Si elle n'est pas traitée, l'application se terminera immédiatement.

Toutes les exceptions standards, comme std::bad_alloc, possèdent une fonction membre what() retournant une chaîne "C" (char []) permettant de connaître la cause de l'erreur.

Voici un exemple de code introduisant la manière de gérer les exceptions en C++ :
#include <iostream>
#include <cstring>
#include <new>

using namespace std;

int main()
{
    try // On entre dans une partie du code susceptible de lever une exception.
    {
        char * p = new char [50]; // <-- La ligne la plus susceptible !

        strcpy(p, "Bonjour tout le monde !");

        cout << p << endl;

        delete [] p;
    }
    catch(const bad_alloc & e) // Traitement des exceptions de type bad_alloc.
    {
        cout << "Exception levee : " << e.what() << "." << endl;
    }
    catch(...) // Traitement des autres types d'exception.
    {
        cout << "Une exception inattendue a ete levee." << endl;
    }

    return 0;
}
Le bloc try permet de délimiter une portion de code susceptible de lever une exception. Il doit être immédiatement suivi d'un ou plusieurs blocs catch chargés de traiter les diférentes exceptions possibles.


II-G-3-b. Comment lever un exception
Le C++ permet de lever une exception à n'importe quel moment à l'aide du mot-clé throw. N'importe quelle expression non void peut apparaître en argument de cet opérateur. A l'intérieur d'un bloc catch (généralement dans catch(...)), il peut également être utilisé sans argument ce qui aura pour effet de relancer l'exception.

Voici un exemple d'utilisation de throw :
#include <iostream>

using namespace std;

enum Exception {
    X_INT, X_STR
};

int main()
{
    Exception type = X_INT;

    try
    {
        if (type == X_INT)
        {
            cout << "Ce programme va lever une exception de type int." << endl;
            throw 1;
            cout << "Cette instruction ne sera pas executee." << endl;
        }
        else
        {
            cout << "Ce programme va lever une exception de type const char *." << endl;
            throw "Exception levee par throw";
            cout << "Cette instruction ne sera pas executee." << endl;
        }
    }
    catch(int code)
    {
        cout << "Exception de type int levee. Code = " << code << "." << endl;
    }
    catch(const char * desc)
    {
        cout << "Exception de type const char * levee : " << desc << "." << endl;
    }

    cout << "Fin du programme." << endl;

    return 0;
}
Voici ce que donne ce programme en sortie :
Ce programme va lever une exception de type int.
Exception de type int levee. Code = 1.
Fin du programme.
 

Valid XHTML 1.1!Valid CSS!

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2009 Melem. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.