Fonctionnement de strncpy() et souci de pointeurs. Utiliser char et tableau char

Bonjour,

J'ouvre un nouveau topic spécifique au char, tableau de char et problèmes de pointeurs ainsi que du fonctionnement de strncpy().

N'étant pas un anglophobe accompli, j'ai un peu de mal à comprendre le fonctionnement de strncpy(). Peut-être que par manque d'informations, je l'utilise mal !

Dans un cas général, j'ai écris une fonction qui doit remplir un tableau extérieur à la fonction. Je me retrouve avec un tableau de char qui est rempli et qui affiche lorsque on fait un Serial.println() tout ce qu'il contient. Pour l'exemple toto, tata et titi.

En matière d'algorithme j'obtiens ceci:

//char tbl1[10];

//Appel de fonction test()

//Fonction test()
  //char tbl2[10];
  
  //Faire tout ce qui faut et remplir tbl2[i++];
  
  //Remplir tbl1[j++] = tbl2;

Tbl1 devrait donc avoir 3 occurence de 0 à 2 avec [toto][tata][titi]. Je me suis rendu compte que dans ce cas il faudrais que je face une copie, donc strncpy(tbl1, tbl2); Et hélas je me retrouve avec un pointeur alors que je n'en ai pas parlé dans mes déclaratif :disappointed_relieved:

error: invalid conversion from ‘char’ to ‘char*’

error: too few arguments to function ‘char* strncpy(char*, const char*, size_t)’

error: at this point in file

Voila donc mon gros point d'interrogation. Donc si on peut avoir un cours du Docteur G ou bien du Docteur Follin sur ce sujet, je veux bien.

  • Comment il fonctionne ?
  • Pourquoi il est question de pointeurs ?
  • Comment travailler sereinement avec les char et tableau de char ?

Bref, j'aimerais convertir à terme tout mes String (quelle horreur) pour du char. Et surtout le maîtrisé. Je précise aussi qu'il serais peut-être intéressant pour la communauté francophone de créer une petite page sur le sujet.

Merci

Un tableau de char est une suite de caractères en mémoire :

chat tab1[10]

Est donc constitué de 10 octets consécutifs en mémoire, 1 par caractère.
tab1[0] est le premier caractère et il est de type char
tab1[9] est le dernier caractère et il est de type char aussi
Ne pas oublier qu'une chaîne de caractères est une suite de caractères se terminant par le caractère invisible '\0' (code ASCII = 0, ne pas confondre avec le caractère représentant le chiffre zéro '0' dont le code ASCII est 48 ou 0x30 en hexa)
Donc pour stocker la chaine de caractère "abcdefhij" de 9 caractères, il faut un char tab[10];

Un tableau ne peut pas changer de taille dynamiquement (contrairement à une String .... quand ca ne plante pas) donc il faut toujours dimensionner les tableaux de manière suffisamment large pour couvrir tous les cas.
Inconvénient : il faut réfléchir car si on se trompe on déborde du tableau et le code crashe.
Avantage : si ton programme compile et se charge, tu es sur qu'il ne te manqueras pas de mémoire durant l'exécution (du moment que tu as bien calculé la taille).
Dit comme cela, on a l'impression qu'on a rien gagné par rapport au String car si on s'est gouré, le programme crashe quand même.

Moi je dis que tu as gagné que le programme ne crashe que si TU t'es trompé dans ton calcul.
Avec les String, le programme crashe sans que tu saches pourquoi.....

Noter aussi que puisque un tableau est une suite de case mémoires, les pointeurs s'appliquent aussi
Et le nom du tableau tab1 est équivalent syntaxiquement à un pointeur sur char pointant sur le 1er élément du tableau

Ainsi

char tab1[10];
char *ptr2;

peuvent être confondus en tant que type.
Mais il y a une grosse différence :
tab1 est un tableau donc de la mémoire a été allouée. Les 10 cases mémoires existes. Mais à partir de la 11eme ca appartient à une autre variable.
ptr2 est un pointeur non initialisé qui pointe n'importe où.

Si tu écris

ptr2 = tab1;

Alors ptr2 devient un pointeur initialisé qui pointe sur la première case du tableau de caractère tab1[]
Et comme on a le droit d'utiliser des crochets avec des pointeurs
tab1 et ptr2 représentent tous les 2 le même élément de type char du tableau
Et si tu fait ++ptr2
Alors tu crée un décalage et ptr2 est équivalent à tab1[i+1]
Par contre il est interdit d'écrire ++tab1 car tab est un tableau et pas un pointeur
Maintenant si tu regarde le code de
*_</em> <em><em>*void strcpy( char *dest, const char *srce ) {   while( *srce != '\0' ) // tant que l'on n'est pas sur le '\0' terminal     *dest++=*srce++;   *dest=*srce; // copie le '\0' terminal }*</em></em> <em>_*
et que tu l'applique à:
```
*char tab1[20] = "abcdef"; // tableau de 20 char initialisé avec une chaine de 6 caractères suivie d'un '\0'
char tab2[10]; // tableau de 10 char non initialisé;

strcpy( tab2, tab1 );
_*_</em> <em>_*- Lors de l'appel de la fonction, tab1 et tab2 sont considérés comme des pointeurs par la fonction.*_</em> <em>_*Mais ce sont des tableaux ce qui assure qu'il y a bien de la mémoire derrière*_</em> <em>_*- La boucle copie les 6 caractères et se termine lorsque srce pointe sur le '\0' final de la chaine. Ce '\0' n'est pas copié*_</em> <em>_*- La dernière ligne de la fonction assure que le '\0' est copié*_</em> <em>_*Tout semble ok.*_</em> <em>_*Mais que ce passe t'il si on a :*_</em> <em>_*
char tab1[20] = "abcdefABCDEF"; // tableau de 20 char initialisé avec une chaine de 12 caractères suivie d'un '\0'
*_</em> <em>_*La boucle va recopier les 12 caractères plus le '\0' (13eme caractère caché) dans une destination qui ne comporte que 10 places.*_</em> <em>_*Résultat on va déborder (on appelle cela en jargon le jardinage mémoire) et écraser les cases mémoires situées après tab2 et qui appartiennent probablement a d'autres variables.*_</em> <em>_*C'est pourquoi il vaut mieux utiliser strncpy() qui compte les caractères :*_</em> <em>_*
*void strncpy( char *dest, const char *srce, int sizedest )
{
  while( (sizedest > 0) && (*srce != '\0') ) // tant que l'on n'est pas sur le '\0' terminal
  {
    *dest++=*srce++;
  sizedest--;
  }
  if ( sizedest > 0 )
    *dest=srce; // copie le '\0' terminal
}

*_</em> <em>_*On ajoute un test sur la taille (compteur qui est décrémenté à chaque copie)*_</em> <em>_*Résultat si le code devient :*_</em> <em>_*
strncpy( tab2, tab1, 10 );
*_</em> <em>_*On est sur que l'on ne déborde pas.*_</em> <em>_*MAIS QUE CONTIENT EXACTEMENT LES 10 caractères de tab2 ?*_</em> <em>_*on a tab2[0] = 'a' jusqu'à tab2[9] = 'D'*_</em> <em>_*Est-ce qu'il ne manque pas quelque chose ?*_</em> <em>_*Si : il manque le '\0' final.*_</em> <em>_*strncpy() s’arrête au dernier caractère qui rentre dans le tableau de destination mais si la source est plus grande, la chaîne n'est pas terminée par un '\0'*_</em> <em>_*Une solution est de coder la copie ainsi :*_</em> <em>_*
strncpy( tab2, tab1, 10 );
tab2[9] = '\0';

*_</em> <em>_*Ainsi on est sur que si la chaine dépasse on s'assure quel tab2 se termine par un '\0' propre*_</em> <em>_*Pour éviter les erreurs de codage, on utilisera systématiquement des macros pour définir les tailles de tableau :*_</em> <em>_*_
#define KEYNAME_LEN    12    // Un nom de clé ne fait pas plus de 12 caractères utiles
char NomClef[KEYNAME_LEN+1]

strncpy( NomClef, xxxx, KEYNAME_LEN );
NomClef[KEYNAME_LEN] = '\0';

_*_</em> <em>_*On peut aussi choisir de d'inclure le caractère terminal dans la macro :*_</em> <em>_*_
#define KEYNAME_LEN    (12+1)    // Un nom de clé ne fait pas plus de 12 caractères utiles + '\0' final
char NomClef[KEYNAME_LEN]

strncpy( NomClef, xxxx, KEYNAME_LEN );
NomClef[KEYNAME_LEN-1] = '\0';

_```
Un petit mot pour finir sur les tableaux à 2 dimensions. Si tu veut mémoriser plusieurs chaines de caractères :_

#define KEYNAME_LEN 12*
#define NBKEYS 5
char MesClefs[NBKEYS][KEYNAME_LEN+1];
MesClefs[0][0] est donc le premier caractère de la 1ère clef
MesClefs[0][12] est donc le 13eme caractère de la 2ère clef (réservé pour le '\0' terminal si le nom de clef fait effectivement 12 caractères)
MesClefs[1][0] est le 1er caractère de la 2eme clef
etc ...
MesClefs[0] est équivalent à un pointeur sur le 1er caractère de la 1ere clef (type char *)
MesClefs est équivalent à un pointeur sur .... char [KEYNAME_LEN+1]
Effectivement MesClefs n'est pas tout a fait équivalent à un char ** mais bien à un pointeur sur tableau car il "conserve" l'information de taille du tableau pointé.
Ca commence à être un peu compliqué mais si besoin je pourrais compléter.
Juste pour terminé une petite question pour voir si j'ai été clair : A quoi est équivalent MesClefs[0][13] ?
(si j'écrit MesClefs[0][13] = 'A', où va s'écrire le caractère 'A' ?)

Pour répondre à ta question, on va être dans { {0,13}} C'est à dire à la treizième case du tableau 2 contenu dans la case 0 du tableau 1.

(case 0 (case 1) (case 2) (case 3) (case 4) (case 6) (case 7) (case 8) (case 9) (case 10) (case 11) (case 12) (case 13 = A))

A condition de pas avoir déclarer tbl[0][12] et de cherché à remplir la treizième. Ceci dit un tableau de type [0][13] reviendrais à faire un tableau type [13]. Enfin pour moi !

Ok pour tout ça, je comprends donc mieux mes erreurs. Un point qui me chagrine, c'est de faire strncpy() puis de rajouter le caractère de fin ensuite. Dans mon esprit, ça devrais se placé avant. J'aimerais comprendre aussi ce détail.

Pour en revenir à mon cas de code. Donc si je comprend bien le cours, je me retrouve à devoir écrire ceci:

#define SIZE_BUFFER 32; //Pour un buffer de 32 caractères
#define SIZE_CONF 10 //Pour 10 commandes de configuration

char tblConfMdle[10]; // Contiendra 10 commandes pleines (nom/true/false/autre info alphanumérique)

/* SETUP */
void setup() {

  fTest(); //Appeler la fonction fTest();
}

/* LOOP */
void loop() {
}

void fTest() {
  
  char bufferConf[SIZE_BUFFER+1]; //Effectuer un buffer de 32 caractères

  //Charger ligne par ligne
  while(condition) {
    
    //Charger le buffer caractère par caractères
    while(condition) {
      bufferConf.read();
    }
    
    //recopie dans le tableau
    strncpy(tblConfMdle, bufferConf, SIZE_CONF);
    /* Ici on charge normalement dans un tableau de 10 case les 32 cases en une (je dit, et je pense que je commais là une erreur). Donc on devrais surement dépassé la taille de 10. Si c'est le cas, une boucle devrais pouvoir résoudre en lisant le premier tableau, placer dans une variable de type char (exemple : char completionBuffer;) et ensuite placer ça dans le tableau qui devrais se charger automatiquement. */
     bufferConf[SIZE_BUFFER] = '\0';
  }
}

Mis à part l'erreur que je crois commettre, je pense que la solution bis semble plus approprié. A moins de réécrire la fonction pour que d'un coup on puisse récupéré caractère par caractère, puis créer une chaine que l'on envoie dans le tableau2. Et c'est même sûr que ça sera beaucoup plus propre. Je ne viens que d'y pensé !

Comme quoi, plus on pratique, plus on a de solution à son arc.

Merci une nouvelle fois pour ce cours et ces précisions. Et merci pour les corrections qui seront apportés.

Pour répondre à ta question, on va être dans { {0,13}} C'est à dire à la treizième case du tableau 2 contenu dans la case 0 du tableau 1.
(case 0 (case 1) (case 2) (case 3) (case 4) (case 6) (case 7) (case (case 9) (case 10) (case 11) (case 12) (case 13 = A))
A condition de pas avoir déclarer tbl[0][12] et de cherché à remplir la treizième. Ceci dit un tableau de type
[13] reviendrais à faire un tableau type [13]. Enfin pour moi !

Je redonne ici la déclaration des tableaux :

#define KEYNAME_LEN       12
#define NBKEYS                 5
char MesClefs[NBKEYS][KEYNAME_LEN+1];   // == MesClefs[5][13]

Le premier indice pour la dimension [5] peut aller de 0 à 4
Le 2nd indice pour la dimension [13] peut aller de 0 à 12

Dont MesClefs[0][13] cherche la 14eme case du tableau MesClefs[0]

Où est cette 14eme case ?

char tblConfMdle[10]; // Contiendra 10 commandes pleines (nom/true/false/autre info alphanumérique)

Je ne sais aps ce qu'est une commande
Ton tableau contient 10 caractères
C'est à dire la place pour une chaine de 9 caractères + le '\0' final

    //recopie dans le tableau

strncpy(tblConfMdle, bufferConf, SIZE_CONF);
    /* Ici on charge normalement dans un tableau de 10 case les 32 cases en une (je dit, et je pense que je commais là une erreur). Donc on devrais surement dépassé la taille de 10. Si c'est le cas, une boucle devrais pouvoir résoudre en lisant le premier tableau, placer dans une variable de type char (exemple : char completionBuffer;) et ensuite placer ça dans le tableau qui devrais se charger automatiquement. */
    bufferConf[SIZE_BUFFER] = '\0';

Tu cherche à recopier 1 chaine de caractères pouvant faire jusqu'à 32 caractères (+ le '\0') dans un tableau de 10 caractères.
Le tout au sein d'une boucle.
Y'a pas un problème là ?

Tu est sur que ton tableau final ne devrait pas être un tableau à 2 dimensions ?

#define SIZE_BUFFER 32; //Pour un buffer de 32 caractères
#define SIZE_CONF 10 //Pour 10 commandes de configuration

char tblConfMdle[10][SIZE_BUFFER+1]; // Contiendra 10 commandes pleines (nom/true/false/autre info alphanumérique)

Soit 10 commandes, chaque commande pouvait faire jusqu'à 32 caractères ?

Ce qui ammène au pseudo code suivant :

void fTest() {
  
  char bufferConf[SIZE_BUFFER+1]; //Effectuer un buffer de 32 caractères
  int indexCommande = 0;
  //Charger ligne par ligne
  while(condition) {
    
    //Charger le buffer caractère par caractères
    while(condition) {
      bufferConf.read();
    }
    
    //recopie dans le tableau
    strncpy(tblConfMdle[indexCommande], bufferConf, SIZE_BUFFER);
     bufferConf[SIZE_BUFFER] = '\0';
   // et d'une commande, on passe à la suivante
   ++indexCommande;
  }
}

En effet, ça fonctionne. J'y voie beaucoup plus clair à présent !

Donc si je résume, j'ai créer un tableau à deux dimensions ayant comme maximum de commande 10 avec un maximum de lettre 32+1.
Ensuite, on copie l'intégralité du tableau avec strncpy() mais sur 32 caractères due au tableau 2 principalement.
Et enfin, on ajoute un '\o' propre afin de ne pas finir sur une chaine infini.

Au niveau des tableau, j'ai noté que char tbl[x] ou x c'est pas null c'est le nombre de caractère. En revanche tab[x] = "toto", prendra x places.
Si je déclare un tableau à deux dimension, tbl[x][y] ou x et y ne sont pas null, y sera le nombre de possibilités avec y nombre de caractères par possibilités.

Important aussi le cas de strncpy(tbl1, tbl2, nombre de char); tbl1 est le tableau de réception, et tbl2 est le tableau d'émission. Ne pas oublier le '\o' à la fin.

Je pense en avoir retenu l'essentiel.

Ok pour tes autres remarques qui sont tout à fait pertinente.

Merci beaucoup y compris pour tes corrections.