En C ou C++, un pointeur est un type de données qui représente une adresse mémoire (pointant vers certaines données).
C++ introduit la notion nouvelle de référence similaire à celle de pointeur mais qui permet l'usage d'une notation plus simple (ne nécessitant plus d'opérateurs de référencement) et la prévention de dangers liés aux pointeurs nuls.
Du type à son pointeur
Il est possible de créer un type pointeur à partir de tout type. Par exemple, nous pouvons déclarer un entier de type int :
int i = 42;
et ensuite déclarer une variable pi de type pointeur int qui contiendra à son initialisation l'adresse mémoire où est déclarée la variable i :
int * pi = &i;
Ainsi le type pointeur pour T (ici T= int), se note T *. L'opérateur & est un opérateur de référencement ; il permet d'obtenir l'adresse mémoire d'une lvalue, i.e. une variable locale, globale ou un champ d'objet (toute entité pour laquelle on peut lire et/ou affecter une valeur).
Il est possible de manipuler des types pointeur de pointeur de T que l'on notera T **. Il est possible d'appliquer plusieurs niveaux de référencement à une lvalue. Ici, nous référençons pi (ce qui équivaut à référencer deux fois i) :
int ** p2i = π
⚠ Cela n'aurait pas de sens d'écrire l'instruction suivante :
int ** q2i = &(&i);
En effet, l'opérateur & de référencement ne peut s'appliquer qu'à une lvalue qui est le symbole d'une zone mémoire pouvant accueillir des valeurs d'un type donné. De même nous ne pourrions écrire &42 car le référencement d'un littéral n'a pas plus de sens.
Nous pouvons afficher les valeur de i, pi et p2i :
cout << "Value of i: " << i << ", pi: " << pi << ", p2i: " << p2i << endl;
Nous obtenons (exemple d'exécution, les adresses mémoire peuvent varier) :
Value of i: 42, pi: 0x7fffb3e30c34, p2i: 0x7fffb3e30c38
Toutes les variables sont stockées sur la pile d'appel :
Appels de fonctions
En C et C++, un appel de fonction provoque l'ajout sur la pile d'appel d'un nouveau stack frame contenant les informations suivantes :
- Les paramètres de la fonction appelés
- L'adresse de retour de la fonction (à quelle instruction faut-il continuer l'exécution du code lorsque la fonction se termine ?)
- Les variables locales de la fonction
Le compilateur peut déterminer à l'avance la taille de la stack frame nécessaire pour chaque fonction.
Les paramètres de fonction sont toujours passés par valeur : cela signifie que les paramètres sont copiés dans la stack frame de la fonction. À l'issue de l'exécution de la fonction, l'adresse de retour est récupérée (pour savoir où continuer l'exécution) et la stack frame détruite. Ainsi si on modifie dans le code de la fonction appelée des paramètres, cela n'a pas d'influence sur la fonction appelante car on travaille sur une copie qui est détruite lorsque la fonction se termine.
Prenons l'exemple d'une fonction calculant la puissance d'un entier :
/** Raise n to the power k */ int intpow(int n, int k) { int result = 1; while (k > 0) { result *= n; k--; } return result; }
Cette fonction peut être appelée depuis une méthode main :
#include <iostream> #include <cstdlib> using namespace std; int main(int argc, char * argv[]) { if (argc < 2) { cout << "Not enough arguments" << endl; return -1; } else { int n = atoi(argv[1]); int k = atoi(argv[2]); int result = intpow(n, k); cout << n << "^" << k << "=" << result << endl; } }
On constate que intpow modifie la valeur du paramètre k ce qui n'impacte pas la variable locale k stockée dans la stack frame de main.
Dans certaines circonstances, il est nécessaire de modifier des variables locales de la fonction appelante depuis la fonction appelée. Pour cela, on utilisera des paramètres de fonction employant :
- soit des types pointeur
- soit des types référence
Prenons pour exemple une fonction swap chargée d'échanger les valeurs de deux entiers. Une fonction swap de signature swap(int a, int b) ne peut fonctionner : elle ne peut échanger que les valeurs de a et b sur sa stack frame, a et b étant des copies qui sont perdues à la sortie de la fonction. On propose une première implantation utilisant des types pointeur :
void swap(int * a, int * b) { int tmp = *a; // déréférencement nécessaire pour obtenir la valeur à l'adresse mémoire a *a = *b; // on copie la valeur contenue à l'adresse mémoire b vers l'adresse mémoire a *b = tmp; // on met la valeur initiale de *a dans l'adresse mémoire b }
Cette méthode swap s'utilise ainsi :
int a = 1; int b = 2; cout << "a=" << a << ", b=" << b << endl; // a=1, b=2 swap(&a, &b); // on référence a et b cout << "a=" << a << ", b=" << b << endl; // a=2, b=1
Les tableaux
Les tableaux peuvent se déclarer ainsi en tant que variable locale, globale ou champs d'une classe (exemple pour un tableau d'int) :
int array[N];
N est la taille du tableau qui doit être déclarée auparavant comme constante (cela ne peut pas être une valeur calculable à l'exécution pour un tableau alloué statiquement) :
const int N = 128;
Un tableau peut être initialisé statiquement au moment de sa déclaration (il reste néanmoins modifiable à l'exécution) :
int array[4] = {1, 2, 3, 4}; // on peut aussi ne pas indiquer la taille du tableau (déductible de l'initialisateur) : int array[] = {1, 2, 3, 4};
Il est possible aussi de créer des tableaux multi-dimensionnels en indiquant la taille de chaque dimension à la déclaration (la taille de la première dimension peut être omise uniquement si l'initialisation est réalisée avec la déclaration ) :
int array2[2][2] = {1, 0, 0, 1}; // que l'on peut aussi écrire: int array2[2][2] = { {0, 1}, {1, 0} }; // ou alors: int array2[][2] = {1, 0, 0, 1};
En pratique, les tableaux statiques sont alloués dans la zone mémoire data du programme (ou alors sur la pile lorsqu'ils sont déclarés comme variables locales de fonctions). Une variable de type tableau est considérée comme un pointeur par le compilateur.
Ainsi si pour obtenir l'élément d'indice i du tableau array unidimensionnel, on écrira :
int element = array[i]; // ou alors int element = *(array + i);
Pour un tableau multi-dimensionnel (par exemple de dimension 2), pour obtenir la cellule (i, j), on écrit :
int element = array2[i][j]; // ou alors int element = *(*(array2 + i) + j);
C(++) propose une arithmétique sur les pointeurs. Si nous avons un pointeur int * ptr ou alors int ptr[ARRAY_SIZE] (désignant aussi un pointeur), l'expression ptr + i désigne l'adresse mémoire où se trouve le i-ème élément à partir de l'adresse ptr, le compilateur prenant en considération la taille mémoire de chaque élément.
Lorsque l'on passe un tableau en paramètre à une fonction, on passe en fait le pointeur vers son premier élément. Aucune copie du tableau n'est réalisée : on travaille sur le tableau original. Testons cela avec une fonction multipliant les valeurs d'un tableau par une constante :
const int N = 4; void printArray(int tab[N]) { for (int i = 0; i < N; i++) { if (i > 0) cout << ","; cout << tab[i]; } cout << endl; } void mult(int tab[N], int multiplicator) { for (int i = 0; i < N; i++) tab[i] = tab[i] * multiplicator; // could also be written: *(tab + i) = *(tab + i) * multiplicator } int main(void) { int tab1[N] = {1, 2, 3, 4}; printArray(tab1); // 1, 2, 3, 4 mult(tab1, 2); printArray(tab1); // 2, 4, 6, 8 }
Pour résumer :
- Pointeurs et tableaux sont des types équivalents en C et C++ (même représentation en mémoire par un pointeur)
-
L'accès à l'élément d'indice i d'une variable a de type pointeur ou tableau est possible avec :
- *(a + i)
- a[i]
L'opérateur sizeof
L'opérateur sizeof s'applique sur le nom d'un type ou le nom d'une variable pour connaître sa taille en octets. Cette valeur est évaluée à la compilation.
Testons sizeof :
#include <iostream> using namespace std; int main(void) { int a = 10; uint64_t b = 20; int tab[3] = {1, 2, 3}; int * tabptr = (int *)tab; char * tabptr2 = (char *)tabptr; char c = 'a'; cout << "a: " << sizeof(a) << ", b:" << sizeof(b) << endl; cout << "tab: " << sizeof(tab) << endl; cout << "tabptr: " << sizeof(tabptr) << endl; cout << "tabptr2: " << sizeof(tabptr2) << endl; }
On aurait pu substituer sizeof(a) par sizeof(int), sizeof(b) par sizeof(uint64_t)...
La valeur évaluée par le compilateur dépend de l'architecture cible envisagée. Certains type tel que uint64_t utilisent une taille constante sur toutes les architectures (ici 64 bits soit 8 octets) tandis que d'autres tel que int ont une taille variable selon l'architecture (généralement pour des raisons d'optimisation). Une attitude de prudence recommanderait l'utilisation systématique de type avec taille constante afin de faciliter la portabilité du code écrit. On notera que le type char occupe invariablement un seul octet sur toutes les architectures.
Notons que sizeof(tabptr) retournera le nombre d'octets nécessaires pour stocker une adresse mémoire (8 octets pour une architecture 64 bits) ; tous les types pointeurs ont en règle générale la même taille sur une architecture donnée (nous pouvons nous attendre à sizeof(tabptr) == sizeof(tabptr2).
Notons que tab est un tableau de 3 int : tab est en fait représenté en mémoire par un pointeur. Toutefois par exception au principe général, sizeof(tab) ne retourne pas la taille du pointeur mais la taille réelle du tableau stockant les données. Ainsi nous aurons l'égalité suivante : sizeof(tab) = 3 * sizeof(int).
Le type void *
void est un mot-clé du langage C utilisé notamment dans la signature d'une fonction pour indiquer qu'elle ne retourne pas de résultat.
void * désigne un type pointeur (adresse mémoire) dont on ne connaît pas le type des données sur lesquelles il pointe. L'arithmétique sur le pointeurs est donc impossible :
void * ptr = ...; void * ptr2 = ptr + 1; // ne compilera pas
Certaines fonctions peuvent avoir besoin de manipuler des pointeurs en faisant abstraction des données qu'ils pointent : l'utilisation du type void * est alors indiqué. Par exemple la méthode memcpy de la bibliothèque C (module string.h) réalise la copie de données d'une source vers une destination. On utilise des pointeurs void * pour indiquer la source et la destination :
// attention, on indique d'abord la destination puis la source void *std::memcpy(void *dest, const void *src, size_t n);
Nous pouvons par exemple l'utiliser pour copier rapidement le contenu d'une tableau vers un autre :
#include <iostream> #include <cstring> using namespace std; int main() { int tab1[4] = {1,2,3,4}; int tab2[4]; std::memcpy(tab2, tab1, sizeof(tab1)); // attention au fait que l'on indique d'abord dest puis src for (int i = 0; i < 4; i++) cout << tab2[i] << ","; // tab2[i] doit bien contenir maintenant la valeur de tab1[i] cout << endl; }
Nous proposons maintenant une implantation possible de memcpy :
void * memcpy(void * dest, const void * src, size_t n) { char * dest1 = (char *)dest; char * src1 = (char*)src; for (int i = 0; i < n; i++) *(dest1++) = *(src1++); }
On réinterprète ici les pointeurs void * comme des pointeurs char * vers un tableau de caractères (chaque caractère est un octet). On peut ensuite utiliser l'arithmétique sur les pointeurs (ici en incrémentant dest1 et src1).
⚠ Lorsque l'on manipule des pointeurs (ou tableaux) que l'on déréférence pour lecture ou écriture, il est fondamental de bien vérifier que l'on ne dépasse pas les bornes de la zone pointée. Considérons le main suivant :
int f() { int a = 1; int b = 2; std::memcpy(&b, &a, 4); cout << b << endl; } int main() { f(); }
Ce code permet de copier la valeur de a dans la zone mémoire désignée par &b. Cela fonctionne pour la plupart des compilateurs et architectures qui représentent souvent un int sur 4 octets. Néanmoins ce n'est pas toujours le cas. En effet la spécification C précise qu'un int doit être représenté sur 2 octets au moins. Dès lors, rien n'empêche d'utiliser 2 octets ou plus. Si sizeof(int) == 2, la copie déborde la zone mémoire de la pile réservée pour b ; elle peut empièter sur a, ou pire si elle déborde sur l'adresse de retour de la fonction. Dans ce cas après l'exécution de la fonction on pourrait continuer l'exécution vers du code imprévu. Si un intervenant extérieur à le contrôle sur les données écrites, c'est la porte ouverte à une importante faille de sécurité permettant à un attaquant d'exécuter du code arbitraire.
Les chaînes de caractères char * à zéro terminal
Historiquement les chaînes de caractères en langage C sont représentées par le type char * (ou unsigned char *). Une chaîne est ainsi définie par l'adresse mémoire de son premier caractère. Les caractères suivants sont contigüs dans la mémoire et occupent chacun un octet. Une chaîne définie ainsi n'est ainsi compatible que pour les jeux de caractères qui utilisent invariablement pour chaque caractère un octet pour le représenter. C'est le cas du jeu ASCII ou latin1 (également appelée ISO-8859-15) qui définit plus de caractères latins (notamment les caractères accentués).
La chaîne de caractères se termine par convention par un caractère nul (caractère noté '\0').
Les chaînes char * à caractère nul terminal ont tendance à être abandonnées en C++ pour être supplantées par le type std::string. Les chaînes char * sont en effet délicates à manipuler et une utilisation non-attentive peut être source de problèmes de sécurité liés à des dépassements de tampon. Néanmoins leur maîtrise est indispensable car elles sont encore utilisées par de nombreuses bibliothèques de bas niveau (notamment pour les appels systèmes).
Il est possible de créer statiquement des chaînes de caractères :
void testCharStar() { static const char * hello = "hello"; // chaîne conservée dans la zone en lecture seule text static char mutableHello[] = "hello"; // chaîne initialisée statiquement et conservée dans data, peut être modifiée hello[0] = 'c'; // interdit car hello n'est pas modifiable mutableHello[0] = 'c'; // autorisé cout << "sizeof(hello)=" << sizeof(hello) << endl; // taille d'un pointeur cout << "sizeof(mutableHello)=" << sizeof(mutableHello) << endl; // taille du tableau: 6 octets (comprenant le zéro terminal) }
Nous pouvons trouver ici (module cstring) une liste des fonctions de manipulation de chaînes à zéro terminal dont voici quelques unes qui sont les plus importantes :
- size_t strlen( const char* str ) : calcule la longueur de la chaîne (sans compter le zéro terminal) ; ⚠ si la chaîne n'est pas terminée correctement par '\0', cette fonction peut lire des zones mémoire non-autorisées
- char *strncpy( char *dest, const char *src, std::size_t count ) : copie une chaîne dest vers src (avec au plus count caractères copiés) ; ⚠ peut ne pas terminer la chaîne dest par un '\0' si count est atteint
- int strcmp( const char *lhs, const char *rhs ); : compare deux chaînes de caractères et retourne une valeur < 0 si lhs est lexicographiquement plus petite que rhs, une valeur > 0 si c'est le contraire ou alors 0 si les deux chaînes sont égales.
Il existe également des fonctions pratiques pour assurer la conversion de chaînes en valeur de type numérique :
- int atoi( const char *str ) : convertit str en entier, retourne 0 en cas d'erreur
- double atof( const char *str ) : convertit str en flottant, retourne 0.0 en cas d'erreur
Afin de manipuler des chaînes, écrivons une fonction de signature bool isequal(const char * a, const char * b) testant si deux chaînes sont égales et retournant un booléen.
Le plus simple serait de proposer l'implantation suivante reposant sur strcmp :
bool isequal(const char * a, const char * b) { return strcmp(a, b) == 0; }
On peut également réaliser une implantation "à la main" :
bool isequal(const char * a, const char * b) { for (int i=0; true; i++) { if (*(a+i) != *(b+i)) return false; if (*(a+i) == '\0') return true; // end of the two strings } }
Les références
Les variables de type référence sont représentées en mémoire comme celles de type pointeur (adresse mémoire de la zone pointée) mais s'utilisent syntaxiquement comme un type direct.
Voici un premier exemple d'utilisation de type référence. Un type référence se définit en suffixant le type employé par un caractères & :
int main(void) { int a = 2; int& aref = a; cout << a << endl; // 2 cout << aref << endl; // 2 a = 3; cout << a << endl; // 3 cout << aref << endl; // 3 car aref pointe vers la même zone mémoire que a }
Crééons une méthode swap2 utilisant des types référence (inspirée de la méthode swap précédemment écrites avec des types pointeur) :
void swap2(int& a, int& b) { int tmp = a; a = b; b = tmp; }
On pourra appeler plus facilement swap2 avec des références que des pointeurs :
int a = 1; int b = 2; cout << "a=" << a << ", b=" << b << endl; // a=1, b=2 swap2(a, b); // on référence a et b cout << "a=" << a << ", b=" << b << endl; // a=2, b=1
Nous n'avons plus besoin de réaliser le référencement de a et b pour l'appel à swap2. Cela simplifie l'appel à la fonction. D'autre part, il n'est pas possible de passer une référence «nulle» alors qu'un appel à swap(0, 0) compilera (et générera une erreur lors du déférencement *a).
La plupart des langages modernes à gestion automatique de la mémoire (ce qui n'est pas le cas de C++) n'utilisent par défaut que des types référence. C'est le cas des langages Java, Scala. OCaml...
Détection de problèmes de mémoire
Les problèmes liés à la mémoire sont difficiles à déceler dans les programmes C ou C++ : un accès en lecture ou écriture à une zone inadaptée de la mémoire peut générer une erreur instantanée terminant le programme (erreur de segmentation) mais peut également être silencieuse et ne se manifester qu'à retardement. Heureusement il existe des outils qui peuvent aider à trouver les lectures/écritures illicites de mémoire. On peut citer :
- Valgrind qui met en place une sorte de machine virtuelle pour exécuter le code binaire et intercepter les allocations, libérations de mémoire ainsi que les lectures et écritures. Cela ralentit drastiquement l'exécution du programme.
- Electric Fence utilise une approche différente. Du code supplémentaire est intégré dans l'exécutable pour intercepter les allocations et libérations de mémoire et entourer les zones allouées de pages de mémoire interdites (technique du canari). En cas d'accès à ces pages interdites, le programme s'arrêtera. L'exécution est plus rapide qu'avec Valgrind, par contre la quantité de mémoire utilisée est plus importante.
Exercices d'application
Mini-tri
Ecrivez une fonction de signature void sort(int * a, int * b) vérifiant si l'entier pointé par a est plus petit que l'entier pointé par b. Si ce n'est pas le cas, on échangera les deux entiers. Ecrivez une autre version de la fonction avec deux références plutôt que deux pointeurs.
Rotation de trois éléments +
Ecrivez une fonction rot_forward prenant en paramètre trois arguments a, b, c (qui peuvent être des int par exemple) et qui place la valeur de a dans b, la valeur de b dans c et la valeur de c dans a. Voici un exemple d'utilisation :
int a = 1; int b = 2; int c = 3; cout << a << " " << b << " " << c << endl; // doit afficher 1 2 3 rot_forward(&a, &b, &c); cout << a << " " << b << " " << c << endl; // doit afficher 3 1 2
Ecrivez maintenant une nouvelle fonction rot_forward_rev qui s'utilise sans avoir à utiliser l'opérateur de référencement & :
rot_forward_ref(a, b, c);
Il serait utile également de disposer d'une fonction permettant de réaliser une rotation inverse. Cela consiste à placer la valeur de a dans c, la valeur de b dans a et la valeur de c dans b. Ecrivez une telle fonction que l'on nommera rot_reverse ainsi que son alter-ego rot_reverse_ref.
Proximité de deux pointeurs
Ecrivez une fonction uint64_t get_pointer_distance(void * p1, void * p2) retournant la distance dans la mémoire de deux pointeurs génériques p1 et p2 en nombre d'octets.
Voici un exemple d'exécution de cette fonction :
uint64_t a = 1; int b = 2; cout << "Distance between a and b; " << get_pointer_distance(&a, &b) << endl;
Statistiques sur un tableau de valeurs
Nous allons écrire un programme recueillant au clavier des réels (stockés en double en mémoire) et calculant des statistiques les concernant.
- Ecrivez une fonction int fetch_doubles(double array[], int n) remplissant un tableau avec des réels tapés au clavier par l'utilisateur (ligne par ligne). n est la taille du tableau : l'utilisateur ne pourra donc pas en fournir plus (on lui indiquera dans ce cas que la capacité est dépassée). En revanche, l'utilisateur peut en fournir moins : l'utilisateur tapera à cet effet la valeur NaN (pour not a number)[1]. La fonction retourne le nombre de réels lus.
- Ecrivez une fonction double find_max(double array[], int n) trouvant le maximum d'un tableau de valeurs double
- Ecrivez une fonction find_min sur le modèle de la fonction précédente pour trouver le minimum
- Ecrivez une fonction double compute_average(double array[], int n) pour calculer la moyenne des n valeurs du tableau.
- Ecrivez une fonction double compute_median(double array[], int n) pour trouver la médiane des n valeurs du tableau.
- Ecrivez pour terminer une fonction main qui demande les nombres à l'utilisateur puis affiche les statistiques à l'aide des fonctions précédemment écrites.
L'écriture des fonctions pour trouver le minimum, maximum et la moyenne ne présentent pas de difficultés techniques particulières. En revanche, trouver la médiane (nombre tel qu'il y ait autant d'éléments plus grands que plus petits) est un peu plus ardu. Une façon d'y arriver serait de trier le tableau ce qui nécessiterait l'allocation dynamique d'une structure de données pour ne pas modifier l'ordre des éléments du tableau initial. Une technique un peu plus lente mais laissant les données intactes sans nécessiter l'allocation d'une nouvelle structure consiste à employer l'algorithme suivant :
- On détermine le min et le max des éléments du tableau
- On émet l'hypothèse d'une médiane égale à m = (min + max) / 2 : on teste s'il y a autant d'éléments supérieurs que inférieurs à m, dans ce cas on a trouvé une médiane acceptable. Si ce n'est pas le cas, on détermine s'il y a plus d'éléments inférieurs à m ou supérieurs à m. Dans le premier cas, la médiane recherchée est comprise entre min et m, dans le second cas elle est comprise entre m et max. On répète alors le processus en émettant l'hypothèse d'une médiane qui serait à mi-chemin entre ces deux bornes.
Puissance d'une matrice
Une matrice peut être représentée par un tableau à deux dimensions de taille N (constante que nous fixons par exemple à 2). Nous utilisons ici des matrices carrées ayant le même nombre de lignes que de colonnes.
- Ecrivez une méthode de signature void matfill(int matrix[N][N]) remplissant une matrice carrée de taille N avec des valeurs fournies au clavier par l'utilisateur (depuis cin)
- Ecrivez une méthode de signature void matproduct(const int m1[N][N], const int m2[N][N], int result[N][N]) calculant le produit des matrices m1 et m2 en écrivant la matrice résultat dans result
- Ecrivez finalement une méthode de signature void matpow(const int m[N][N], int k) élévant la matrice m à la puissance k (i.e. la multipliant k fois par elle-même)
Pourrait-on réécrire les fonctions de telle sorte qu'elles puissent supporter des matrices carrées de n'importe quelle taille et pas uniquement de taille N ? Pourra-t-on utiliser la même représentation de données ?
Chaînes de caractères char * avec zéro terminal
Écrivons quelques fonctions qui manipulent des chaînes char * à caractère zéro terminal :
- Écrivez la fonction bool is_palindrome(const char * str) déterminant si str est un palindrome (chaîne se lisent identiquement à l'endroit comme à l'envers comme radar ou kayak)
- Écrivez la fonction bool find_occurrence(const char * haystack, char needle, char ** place) recherchant la première occurrence du caractère needle dans haystack. Si une telle occurrence est trouvée, la fonction retourne true et nous indiquons dans *place l'adresse mémoire de ce caractère dans la chaîne haystack. Si l'occurrence n'est pas trouvée, on retourne false et *place prend la valeur NULL.
- Écrivez la fonction int compute_occurrence_number(const char * haystack, char needle) calculant le nombre d'occurrences du caractère neddle. On impose d'utiliser la fonction réalisée précédemment.
- Écrivez la fonction void compute_histogram(const char * str, int histogram[]) calculant l'histogramme d'une chaîne de caractères. L'histogramme est un tableau de 256 cellules dont la cellule d'indice i contient le nombre de caractères de la chaîne de valeur i. Que se passerait-il si l'on passait en paramètre à la fonction un tableau contenant moins de 256 cellules ?
- Écrivez une fonction int revert_str(char * str) qui inverse l'ordre des caractères de la chaîne str (en modifiant la chaîne). La fonction retourne le nombre de caractères dans la chaîne.
- Écrivez une fonction int find(const char * haystack, char * needle) recherchant la chaîne needle dans haystack et retournant la position de départ où cette chaîne a été trouvée dans haystack
- Pour savoir si un double est NaN, la comparaison avec la constante NAN est inadaptée : en effet, deux valeurs NaN sont toujours considérées comme différentes par le processeur, ainsi NAN != NAN est toujours vrai). Pour savoir si un nombre x est NaN nous utilisons la macro isnan(x) définie dans cmath).