Il est né ?
Vous êtes ici : www.xgarreau.org >> aide >> devel >> cpp : Le monde merveilleux de g++
Version imprimable

Le monde merveilleux de g++

Cette série d'articles présentera le langage orienté objet C++. Nous aborderons rapidement l'origine de ce langage puis nous nous attellerons à l'étude des éléments de la librairie standard de C++. Nous verrons ensuite les notions de classe et d'objets ainsi que les différents sujets associés tels que l'héritage, le polymorphisme, les fonctions virtuelles, etc... Cette série d'articles présuppose des connaissances de base en C.

Ne déflorons pas le sujet et commençons par le commencement en voyant d'où sortent le C++ et les outils qu'il met à notre disposition.



Préalables pour cette partie

Le C++ est un langage objet.
Un objet possède des méthodes, ce sont des fonctions qui lui sont spécifiques.
L'appel d'une méthode A d'un objet O se note O.A(param1, param2)
En C++ une fonction/méthode peut avoir des paramètres par défaut, peut prendre un nombre variable de paramètres et peut avoir plusieurs prototypes avec des paramètres de types différents. Le choix de la version appelée se fait en fonction du type des paramètres transmis. On appelle cela la surcharge de fonction. Le C++ s'appuie énormément sur ce concept.

string::stroustrup (string& s_dest, int count); ?

Non, stroustrup n'est pas une fonction de manipulation de chaînes de caractères, c'est le nom du créateur de C++. Il ne s'agit pas ici de faire une étude détaillée de l'histoire de C++ mais savoir qui l'a créé me semble important.

Bjarne Stroustrup a créé le C++ pour étendre le langage C. Les premières versions se nommaient "C avec classes", ce qui exprime bien que le C et le C++ ont énormément de points communs comme nous le verrons au cours des prochains articles. Ces premières apparitions de ce qui deviendrait le langage C++ datent du début des années 80, à l'époque où Bjarne Stroustrup travaillait sur des simulations pour les laboratoires AT&T. Comme il ne trouvait pas de langage convenable pour faire ce qu'il voulait, il a fait ce que nous aurions tous fait à sa place ;-) ... Il en a créé un nouveau !

La similitude entre le C et le C++ peut être dangereuse car la tentation est grande pour les habitués de la programmation en C de "générer" du code ne tirant pas profit des améliorations du C++. A l'inverse, l'apprentissage du C++ sans aucune notion de C s'avérera sans doute plus longue.

La Librairie standard

Nous verrons lors d'articles futurs comment construire nos classes nous mêmes mais voyons d'abord en quelques articles celles qui sont fournies par la librairie standard. Cette dernière a pour but de nous simplifier la vie en fournissant la plupart des outils évolués dont on peut avoir besoin. Citons par exemple les flux d'entrée/sortie, les chaînes de caractères, les queues et piles, les vecteurs et les maps, les espaces de noms, l'allocation dynamique de mémoire...

Pour utiliser les fonctions d'une librairie, on en inclue les définitions, comme en C, grâce à la directive #include. En C++, on distingue les fichiers d'en-têtes de la librairie standard grâce à l'absence d'extension. A titre exemple, pour utiliser les piles, on écrira
#include <stack>
Si vous jetez un oeil dans le répertoire contenant les fichiers d'en-têtes pour le C++ (/usr/include/g++-3/ ou /usr/include/g++-v3/ sur une mdk8.2) vous verrez que les fichiers sans extension sont grosso modo uniquement des fichiers qui incluent le fichier de même nom mais avec une extension .h.
Une dernière chose à savoir sur les fichiers d'en-tête du C++ est que si leur nom commence par un c, il y a de grandes chances qu'ils servent à inclure un fichier d'entête de la librairie c. Par exemple #include <cstdio> revient à peu près à #include <stdio.h>

Entrées/Sorties

Comme c'est en pratiquant que l'on avance, voyons tout de suite un exemple concret utilisant les flux d'entrée sortie. Pour pouvoir y accéder en C++, on doit inclure l'entête iostream. Les flux basiques sont cout, cin et cerr et représentent respectivement la sortie, l'entrée et la sortie d'erreurs.

/* ex_1_1.c++ */
#include <iostream>

int main() {
	std::cout << "Entrez un nombre entier inférieur à 10: ";
	int i;
	std::cin >> i;
	std::cout << "Vous avez tapé : " << i << std::endl ;
	if (i>10) {
		std::cerr << "ERREUR: " << i << ">10" << std::endl;
		return 1;
	}
	std::cout << "Entrez un nombre à virgule : ";
	double d;
	std::cin >> d;
	std::cout << "Vous avez tapé : " << d << std::endl ;
	return 0;
}

Ce premier exemple simpliste invite l'utilisateur à taper deux valeurs, qu'il lit et affiche en écho à l'utilisateur. Pour la première variable, le programme teste si la valeur est supérieure à 10 et envoie un message sur la sortie d'erreur si cette condition n'est pas respectée. Vous constaterez tout cela en le compilant et en l'exécutant:

g++ -o ex_1_1 ex_1_1.c++
C'est ici que le titre prend toute sa valeur. On compile du C++ avec g++, qui est en fait (selon la page de manuel) un "script appelant gcc avec des options spécifiques pour la reconnaissance du C++". Ce qui veut dire que gcc et g++ acceptent une grosse majorité d'options communes. Si vous avez suivi jusqu'ici, vous pouvez essayer ce premier exemple en tapant ./ex_1_1

Examinons un peu ce code : la première ligne sert à inclure le fichier d'entête adéquat (iostream), comme en C.
La boucle principale (main) ne prend pas d'argument et renvoie une valeur entière, le code d'erreur, non nul en cas de problème.
Vous avez sans doute remarqué que nos variables sont déclarées au moment de servir et pas avant, c'est une des libertés offertes par le C++ par rapport au C où les variables doivent être déclarées au début du bloc dans lequel elles servent.
Pour envoyer des données dans un flux, on utilise l'opérateur <<. Pour en lire, on utilise l'opérateur >>.
std::cout << "Hello" << std::endl; écrit Hello sur la sortie standard, suivi d'un retour à la ligne.
std::cerr << "Error"; << i écrit Erreur sur la sortie d'erreur (le flux 2), suivi du contenu de la variable i.
std::cin >> i; lit un élément sur l'entrée standard et le place dans la variable i.

Il est possible d'afficher plusieurs données de types différents en une seule ligne comme c'est le cas dans notre premier exemple. On peut de façon semblable lire plusieurs variables en entrée en une seule instruction. On aurait pu écrire pour lire i et d :

double d;
int i;
std::cin >> i >> d;
Le fait que les variables affichées ou lues ne soient pas du même type ne pose aucun problème en C++ et ne nécessite pas de chaîne de formatage comme c'est le cas pour les fonctions printf et scanf en C.

Tous ces std:: vous paraissent sans doute lourds à taper (ils le sont)! La raison en est que le C++ utilise la notion de namespace ou espace de noms. La librairie standard est définie dans l'espace de noms std. Pour faire référence à cout, dans la librairie standard, on doit donc taper std::cout. Il est toutefois possible de s'affranchir de cette lourdeur syntaxique en disant que nous utilisons ce qui se trouve dans l'espace de noms std, grâce à la directive using namespace. Notre petit programme devient alors :

/* ex_1_1.c++ */
#include <iostream>
using namespace std;

int main() {
	cout << "Entrez un nombre entier inférieur à 10: ";
	int i;
	cin >> i;
	cout << "Vous avez tapé : " << i << endl ;
	if (i>10) {
		cerr << "ERREUR: i>10\n";
		return 1;
	}
	cout << "Entrez un nombre à virgule : ";
	double d;
	cin >> d;
	cout << "Vous avez tapé : " << d << endl ;
	return 0;
}

Ce qui, vous en conviendrez, est nettement plus simple.

Chaînes de caractères

Nous aurons l'occasion de revenir sur les flux et les espaces de noms mais poursuivons notre exploration sommaire du C++ en nous intéressant au type string, les chaînes de caractères. Si vous êtes un développeur C, vous devez vous souvenir de vos premières utilisations de chaînes de caractères dans ce beau langage avec un sentiment ému. C'est un bon moyen de s'énerver après un pauvre ordinateur. La glib fournit en C un type facilitant la gestion de chaînes de caractères, dans le C++, le type string est intégré à la librairie standard.

Basiquement, il n'y a pas de surprise. Une chaîne de caractères est un conteneur spécialisé pour stocker des caractères. On parle énormément de conteneurs en C++. La plupart des types fournis par la librairie standard sont des conteneurs. Pour ceux qui en doutent encore, le rôle d'un conteneur est de contenir des variables et on trouve parmi eux les chaînes de caractères bien sûr mais aussi les tables de hachage, les piles, les queues et bien d'autres types prédéfinis ...
En C++, une chaîne se déclare ainsi :

string s;
Cette construction est équivalente à string s = "";, Ce qui revient à dire que par défaut une chaîne est créée vide.
Il existe toutefois de nombreuses autres façons de déclarer une string :
string s1 = "une chaîne";
string s2 = s1; // s2 contient "une chaîne"
string s3(3, 'x'); // équivaut à string s3 = "xxx"
string s4(s1, 4, 6); // s4 contient "chaîne"

On peut affecter à une string un caractère, une série de caractères ou une autre chaîne. L'assignation se fait grâce à l'opérateur = ou à la méthode assign, plus fine.
On accède aux caractères qui la composent grâce aux opérateurs [] et at.
Pour connaître la taille d'une string on utilise la méthode length().

s1 = 'z';
s1 = "autre chaîne";
s1 = s2; // s1 contient "une chaîne"
s3.assign(3, 'y'); // s3 contient "yyy"
s4.assign(s2, 2, 7) // s4 contient "tre cha"

char c1 = s1[4]; // c1 contient 'c'
char c2 = s1.at(4); // c2 contient 'c' aussi

int l = s2.length(); // l vaut 10

Il est évidemment également possible de concaténer des string.

/* ex_1_2.c++ */
#include <iostream>
#include <string>
using namespace std;

int main() {
	string s5 = " chance";
	string s6 = "une" + s5; 
	string s7 = '1' + s5; 
	s5 = s6 + " = " + s7;
	cout << s6 << endl << s7 << endl;
	cout << s5 << endl;
	return 0;
}
La séquence de compilation et exécution par g++ -o ex_1_2 ex_1_2.c++ && ./ex_1_2 produit la sortie suivante :
une chance
1 chance
une chance = 1 chance

Pour affiner nos assignations, signalons qu'il existe également l'opérateur += et les méthodes insert et append pour ajouter des caractères à nos chaînes. Je vous laisse vous reporter aux ouvrages de référence pour les détails et poursuis avec les exemples les plus courants dans le code ci-dessous :

/* ex_1_3.c++ */
#include <iostream>
#include <string>
using namespace std;

int main() {
	string voyelles = "aei";
	voyelles.append("ou");
	cout << voyelles << endl;
	string alphabet = voyelles;
	alphabet.insert(1, "bcd");
	cout << alphabet << endl;
	alphabet.insert(5, "fgh").insert(9, "jklmn").insert(15, "pqrst");
	cout << alphabet << endl;
	alphabet += "vwxyz";
	cout << alphabet << endl;
	return 0;
}
La compilation et l'exécution de ce petit programme donne :
aeiou
abcdeiou
abcdefghijklmnopqrstu
abcdefghijklmnopqrstuvwxyz
L'instruction voyelles.append("ou"); ajoute "ou" à la fin de la chaîne voyelles. alphabet.insert(1, "bcd"); insère la chaîne "bcd" avant la position 1 de la chaîne alphabet, les chaînes étant numérotées à partir de 0, on obtient dans alphabetla chaîne "abcdeiou". La suite du code se passe de commentaire, remarquez juste que vous avez la possibilité de faire des insertions en cascade.
La mémoire étant allouée dynamiquement, on est bien loin des parties de casse-tête avec strcpy, realloc, strcat et sprintf que rencontrent tout débutant.

On peut effectuer des recherches dans les chaînes de caractères grâce aux méthodes find, rfind, find_first_of, find_last_of, find_first_not_of, find_last_not_of.

/* ex_1_4.c++ */
#include <iostream>
#include <string>
using namespace std;

int main() {
	string alphabet = "abcdefghijklmnopqrstuvwxyz";
	string voyelles = "uaoie";
	string mix = "acdecdicdocducd";

	cout << alphabet.find('e') << endl;
	cout << alphabet.at(alphabet.find_first_of(voyelles)) << endl;
	cout << alphabet.at(alphabet.find_last_of(voyelles)) << endl;
	cout << alphabet.at(alphabet.find_first_not_of(voyelles)) << endl;
	cout << alphabet.at(alphabet.find_last_not_of(voyelles)) << endl;

	cout << mix.find("cd") << endl;
	cout << mix.rfind("cd") << endl;
	cout << mix.find("cd",mix.find("cd")+1) << endl;

	return 0;
}
Ce programme affiche :
4
a
u
b
z
1
13
4
Toutes les méthodes de recherche prennent en paramètre une chaîne et optionnellement l'indice à partir d'où la chercher.
find cherche la chaîne passée en paramètre à partir du début de la chaîne ou de l'indice optionnellement passé en second paramètre.
rfind fait la même chose mais en partant de la fin de la chaîne.
find_first_of et find_last_of cherchent la première/dernière occurence d'un des caractères présents dans la chaîne passée en paramètre.
find_first_not_of et find_last_not_of cherchent la première/dernière occurence d'un des caractères absents de la chaîne passée en paramètre.
Toutes ces méthodes renvoient l'indice du premier caractère correspondant à la recherche.

L'opérateur "==" et la méthode compare permettent de comparer des string. compare renvoie une valeur nulle si les string comparées sont identiques, comme le fait strcmp. Elle prend en paramètre une string et optionnellement, l'indice de début de comparaison et le nombre de caractères à comparer.

string s1 = "allo";
string s2 = "allo";
string s3 = "allo toi";

if (s1 == s2) cout << "s1 et s2 sont identiques" << endl;
cout << s1.compare(s2) << endl; // Affiche 0
cout << s1.compare(s3,0,4) << endl;  // Affiche 0

Le remplacement de parties de chaînes se fait grâce à la méthode replace, qui prend en paramètres, un indice, le nombre de caractères à enlever et la chaîne à mettre à la place. Ce qui est intéressant, c'est que la chaîne de remplacement ne doit pas nécessairement faire la même taille que la chaîne remplacée et ce, une fois de plus, sans réallocation explicite de mémoire.

string s = "C'est toto le plus fort";
s.replace(s.find("toto"), 4, "xavier"); // s contient "C'est Xavier le plus fort"

Cette introduction au type string du C++ ne serait pas complète si nous n'abordions pas la méthode substr, qui permet d'obtenir une sous chaîne à partir d'un indice et d'une longueur comme on le voit ci-dessous :

string s1 = s.substr(21, 4); // s1 contient "fort"

Et enfin, si vous avez écrit de nombreuses fonctions pour des chaînes de types C (tableau de caractères terminé par un 0) vous pouvez en obtenir une à partir d'une string, grâce à la méthode c_str(). Prenons le cas de l'indispensable (?!) fonction strlen_plus_un qui renvoie la taille de la chaîne passée en paramètre plus un. (ce qui est la taille nécessaire à son stockage pour la petite histoire). Vous avez théoriquement besoin de l'entête cstring.

/* ex_1_6.c++ */
#include <string>
#include <cstring>

size_t strlen_plus_un (const char * s) {
    return (strlen(s)+1);
}

int main() {
    string s1(10, 'a');

    cout << strlen_plus_un(s1) << endl;
}
Si vous essayez de compiler ce code, vous obtiendrez une erreur indiquant que le type string ne peut être converti en const char *. Il faut fournir à strlen_plus_un une chaîne de type C. On écrit alors :
cout << strlen_plus_un(s1.c_str()) << endl;
On compile et on obtient logiquement la sortie 11 à l'exécution.

Piles et queues

Examinons maintenant deux types évolués fournis par la librairie standard, les piles et les queues. Ceux qui développent de façon régulière ont déjà eu a implémenter les concepts de pile et de queue mais précisons ce qu'ils représentent pour les non initiés.

Une pile est une structure dans laquelle on introduit des données pour ensuite les y prélever dans l'ordre inverse de leur introduction. C'est ce que l'on appelle le principe du LIFO (Last In First Out) et qui est bien représenté par un pile de quoi que ce soit. Si vous avez des assiettes chez vous, vous constaterez si vous y prêtez attention, que vous mangez souvent dans celle qui se trouve sur le dessus de la pile, celle que vous avez rangé en dernier. Ceci induit que celles du dessous de la pile prennent la poussière mais là n'est pas l'objet de cet article.

Recentrons nous un peu sur le sujet... Les files fonctionnent selon le principe du FIFO (First In First Out), ce qui signifie que le premier entré est le premier servi, comme dans une file d'attente (quand personne ne prend votre place).

Partons d'un exemple pour voir ces deux types :

/* ex_1_7.c++ */
#include <string>
#include <stack>
#include <queue>
#include <iostream>

int main() {
	stack<string> la_pile;
	queue<string> la_queue;
	string s;
	for (int i = 0; i<6; ++i) {
		getline (cin, s);
		la_pile.push(s);
		la_queue.push(s);
	}
	cout << "La pile contient " << la_pile.size() << " élements :" << endl;
	while (!la_pile.empty()) {
		cout << la_pile.top() << endl;
		la_pile.pop();
	}
	cout << "La queue contient " << la_queue.size() << " élements :" << endl;
	while (!la_queue.empty()) {
		cout << la_queue.front() << endl;
		la_queue.pop();
	}
}
On commence par inclure les en-têtes dont on a besoin, à savoir string pour les chaînes qui constitueront notre pile et notre queue, stack pour la pile et queue pour la file (aka queue).

La création d'une file et d'une pile sont très semblables et très simples. Il vous suffit de faire suivre stack ou queue du type de variables que vous voulez y mettre. Ce type doit être entre crochets. Ce type de déclaration tire parti du concept de modèle (ou patron, template) que nous étudierons plus tard.

Les files et les piles sont très semblables et partagent donc logiquement de nombreuses méthodes. size() renvoie le nombre d'éléments que contient la structure. empty() retourne une valeur booléenne (bool), vraie si la structure est vide et fausse sinon. La méthode pop(), quant à elle, supprime le prochain élément de la structure, c'est à dire celui du dessus pour une pile et celui de devant pour une file. La méthode d'ajout de valeurs est également commune aux deux structures, il s'agit de la méthode push(), qui ajoute un élément en bout de queue ou en dessus de pile.

On obtient l'élément suivant d'une pile grâce à la méthode top(), il s'agit du dernier empilé. Pour une file on utilise front() pour obtenir le prochain élément à enlever et back() pour obtenir le dernier "enfilé". Ces trois méthodes NE suppriment PAS l'élément. Pour supprimer l'élément quand il ne sert plus et donc pouvoir accéder au suivant on utilise la méthode pop(), vue ci-dessus. Pour accéder librement aux éléments d'une structure similaire, on utilisera un vector ou un autre type de conteneur, lesquels feront l'objet de futurs articles.

Avec ces explications, vous devez être en mesure de comprendre ce que produit notre programme, qui utilise toutes les méthodes citées :

[xavier@zaz2 1]$ g++ -o ex_1_7 ex_1_7.c++
[xavier@zaz2 1]$ ./ex_1_7
AZERTY
UIOP
QSDFGH
JKLM
WXCV
BN
La pile contient 6 élements :
BN
WXCV
JKLM
QSDFGH
UIOP
AZERTY
La queue contient 6 élements :
AZERTY
UIOP
QSDFGH
JKLM
WXCV
BN
[xavier@zaz2 1]$ 

Conclusion

Voilà, c'est tout pour cette fois, j'espère que vous n'êtes pas trop frustrés de n'avoir pas créé vos propres classes. Vous avez malgré tout manipulé des objets et entr'aperçu la puissance du langage que nous continuerons d'explorer les prochaines fois.
Naturellement, si vous avez des questions, des suggestions ou n'importe quelle remarque, n'hésitez pas à me contacter. C'est pour cela que je précise mon adresse e-mail à la fin de mes articles.
Et enfin, si vous êtes courageux et impatients, vous êtes sous linux, vous pouvez donc lire les sources de la librairie standard C++.

Merci aux relecteurs.

Xavier GARREAU - http://www.xgarreau.org/ - <xavier@xgarreau.org>

Ingénieur de recherche PRIM'TIME TECHNOLOGY
http://www.prim-time.com/

Président du ROCHELUG
http://lug.larochelle.tuxfamily.org/

Références :


Précédent Index Suivant

a+

Auteur : Xavier GARREAU
Modifié le 05.11.2005

Rechercher :

Google
 
Web www.xgarreau.org