Initialiser un caractère [] avec une chaîne est-il une mauvaise pratique?

44

Je lisais un fil de discussion intitulé "strlen vs sizeof" sur CodeGuru , et l' une des réponses indique qu '"il est de toute façon [sic] mauvaise pratique d'initialiser un chartableau avec un littéral de chaîne".

Est-ce vrai ou s'agit-il simplement de son opinion (bien que "membre d'élite")?


Voici la question initiale:

#include <stdio.h>
#include<string.h>
main()
{
    char string[] = "october";
    strcpy(string, "september");

    printf("the size of %s is %d and the length is %d\n\n", string, sizeof(string), strlen(string));
    return 0;
}

droite. la taille devrait être la longueur plus 1 oui?

c'est la sortie

the size of september is 8 and the length is 9

la taille devrait être 10 sûrement. c'est comme si on calculait la taille de la chaîne avant qu'elle ne soit modifiée par strcpy mais par la longueur suivante.

Y at-il quelque chose qui ne va pas avec ma syntaxe ou quoi?


Voici la réponse :

C'est de toute façon une mauvaise pratique d'initialiser un tableau de caractères avec un littéral de chaîne. Alors, faites toujours l’une des choses suivantes:

const char string1[] = "october";
char string2[20]; strcpy(string2, "september");
Cole Johnson
la source
Notez le "const" sur la première ligne. Se pourrait-il que l'auteur assume c ++ au lieu de c? En c ++, il s'agit d'une "mauvaise pratique", car un littéral doit être const et que tout compilateur c ++ récent donnera un avertissement (ou une erreur) sur l'affectation d'un littéral const à un tableau non-const.
André le
@ André C ++ définit les littéraux de chaîne comme des tableaux de constants, car c'est le seul moyen sûr de les traiter. Le problème ne vient pas de C , vous avez donc une règle sociale qui impose le principe de sécurité
Caleth
@ Caleth. Je sais que j’essayais plus de faire valoir que l’auteur de la réponse abordait la "mauvaise pratique" du point de vue de c ++.
André
@ André, ce n'est pas une mauvaise pratique en C ++, parce que ce n'est pas une pratique , c'est une erreur de type tout droit. Cela devrait être une erreur de type en C, mais ce n'est pas le cas, vous devez donc avoir une règle de style qui vous dit "C'est interdit"
Caleth

Réponses:

59

C'est de toute façon une mauvaise pratique d'initialiser un tableau de caractères avec un littéral de chaîne.

L'auteur de ce commentaire ne le justifie jamais vraiment et je trouve la déclaration déconcertante.

En C (et vous l'avez étiqueté C), c'est à peu près la seule façon d' initialiser un tableau charavec une valeur de chaîne (l'initialisation est différente de l'attribution). Vous pouvez écrire soit

char string[] = "october";

ou

char string[8] = "october";

ou

char string[MAX_MONTH_LENGTH] = "october";

Dans le premier cas, la taille du tableau est prise à partir de la taille de l'initialiseur. Les littéraux de chaîne sont stockés en tant que tableaux de char0 octet de fin; la taille du tableau est donc 8 ('o', 'c', 't', 'o', 'b', 'e', ​​'r', 0) Dans les deux autres cas, la taille du tableau est spécifiée dans la déclaration (8 et MAX_MONTH_LENGTHquoi que ce soit).

Ce que vous ne pouvez pas faire, c'est écrire quelque chose comme

char string[];
string = "october";

ou

char string[8];
string = "october";

etc. Dans le premier cas, la déclaration de stringest incomplète car aucune taille de tableau n'a été spécifiée et aucun initialiseur à prendre la taille. Dans les deux cas, =cela ne fonctionnera pas car a) une expression de tableau telle que stringpouvant ne pas être la cible d'une affectation et b) l' =opérateur n'est pas défini pour copier le contenu d'un tableau dans un autre.

De même, vous ne pouvez pas écrire

char string[] = foo;

fooest un autre tableau de char. Cette forme d'initialisation ne fonctionnera qu'avec les littéraux de chaîne.

MODIFIER

Je devrais modifier cela pour dire que vous pouvez également initialiser des tableaux pour contenir une chaîne avec un initialiseur de type tableau, comme

char string[] = {'o', 'c', 't', 'o', 'b', 'e', 'r', 0};

ou

char string[] = {111, 99, 116, 111, 98, 101, 114, 0}; // assumes ASCII

mais il est plus facile pour les yeux d'utiliser des littéraux de chaîne.

EDIT 2

Pour affecter le contenu d'un tableau en dehors d'une déclaration, vous devez utiliser soit strcpy/strncpy(pour les chaînes terminées par 0), soit memcpy(pour tout autre type de tableau):

if (sizeof string > strlen("october"))
  strcpy(string, "october");

ou

strncpy(string, "october", sizeof string); // only copies as many characters as will
                                           // fit in the target buffer; 0 terminator
                                           // may not be copied, but the buffer is
                                           // uselessly completely zeroed if the
                                           // string is shorter!
John Bode
la source
@ KeithThompson: ne désaccordez pas, venez de l'ajouter pour des raisons de complétude.
John Bode
16
Veuillez noter que char[8] str = "october";c'est une mauvaise pratique. Je devais littéralement compter les caractères pour m'assurer que ce n'était pas un débordement et qu'il se cassait sous la maintenance ... par exemple, corriger une erreur d'orthographe de seprateà separatese briserait si la taille n'était pas mise à jour.
Djechlin
1
Je suis d'accord avec djechlin, c'est une mauvaise pratique pour les raisons données. La réponse de JohnBode ne fait aucun commentaire sur l'aspect "mauvaise pratique" (qui est la partie principale de la question !!), elle explique simplement ce que vous pouvez ou ne pouvez pas faire pour initialiser le tableau.
mastov
Mineur: As 'longueur ", la valeur renvoyée par strlen()n'inclut pas le caractère null. Utiliser MAX_MONTH_LENGTHpour conserver la taille maximale requise semblechar string[] souvent incorrect. IMO serait préférable ici.MAX_MONTH_SIZE
chux - Rétablir Monica
10

Le seul problème que je me rappelle est l’assignation du littéral chaîne à char *:

char var1[] = "september";
var1[0] = 'S'; // Ok - 10 element char array allocated on stack
char const *var2 = "september";
var2[0] = 'S'; // Compile time error - pointer to constant string
char *var3 = "september";
var3[0] = 'S'; // Modifying some memory - which may result in modifying... something or crash

Par exemple, prenez ce programme:

#include <stdio.h>

int main() {
  char *var1 = "september";
  char *var2 = "september";
  var1[0] = 'S';
  printf("%s\n", var2);
}

Ceci sur ma plate-forme (Linux) se bloque lorsqu’il essaie d’écrire sur une page marquée en lecture seule. Sur d’autres plates-formes, il peut imprimer «septembre», etc.

Cela dit - l'initialisation par le littéral fait le montant spécifique de la réservation pour que cela ne fonctionne pas:

char buf[] = "May";
strncpy(buf, "September", sizeof(buf)); // Result "Sep"

Mais ce sera

char buf[32] = "May";
strncpy(buf, "September", sizeof(buf));

Dernière remarque - je n’utiliserais pas strcpydu tout:

char buf[8];
strcpy(buf, "very long string very long string"); // Oops. We overwrite some random memory

Bien que certains compilateurs puissent le transformer en appel sécurisé, cela strncpyest beaucoup plus sûr:

char buf[1024];
strncpy(buf, something_else, sizeof(buf)); // Copies at most sizeof(buf) chars so there is no possibility of buffer overrun. Please note that sizeof(buf) works for arrays but NOT pointers.
buf[sizeof(buf) - 1] = '\0';
Maciej Piechotka
la source
Il y a toujours un risque de saturation de mémoire tampon à strncpycause de cela, car la chaîne copiée n'est pas terminée lorsque la longueur something_elseest supérieure à sizeof(buf). J'ai l'habitude de définir le dernier caractère buf[sizeof(buf)-1] = 0à protéger, ou, s'il bufest initialisé à zéro, de l'utiliser sizeof(buf) - 1comme longueur de copie.
syockit
Utilisez strlcpyou strcpy_sou même snprintfsi vous devez.
user253751
Fixé. Malheureusement, il n’ya pas de moyen facile de le faire, sauf si vous avez le luxe de travailler avec les derniers compilateurs ( strlcpyet snprintfne sont pas directement accessibles sur MSVC, du moins sur les commandes et strcpy_sne sont pas sur * nix).
Maciej Piechotka
@ MaciejPiechotka: Dieu merci, Unix a rejeté l'annexe k, sponsorisée par Microsoft.
Déduplicateur
6

Une chose que ni l'un ni l'autre des fils ne soulève est la suivante:

char whopping_great[8192] = "foo";

contre.

char whopping_great[8192];
memcpy(whopping_great, "foo", sizeof("foo"));

Le premier fera quelque chose comme:

memcpy(whopping_great, "foo", sizeof("foo"));
memset(&whopping_great[sizeof("foo")], 0, sizeof(whopping_great)-sizeof("foo"));

Ce dernier ne fait que la mémoire. Le standard C insiste sur le fait que si une partie d’un tableau est initialisée, c’est le cas. Donc, dans ce cas, il vaut mieux le faire soi-même. Je pense que c’est peut-être à cela que voulait arriver Treuss.

Pour sûr

char whopping_big[8192];
whopping_big[0] = 0;

est mieux que soit:

char whopping_big[8192] = {0};

ou

char whopping_big[8192] = "";

ps Pour les points bonus, vous pouvez faire:

memcpy(whopping_great, "foo", (1/(sizeof("foo") <= sizeof(whopping_great)))*sizeof("foo"));

générer une erreur de division du temps de compilation par zéro si vous êtes sur le point de déborder du tableau.

Richard Fife
la source
5

Principalement parce que vous n'avez pas la taille de la char[]variable / construction que vous pouvez facilement utiliser dans le programme.

L'échantillon de code du lien:

 char string[] = "october";
 strcpy(string, "september");

stringest alloué sur la pile sur 7 ou 8 caractères. Je ne me souviens pas si c'est terminé de cette façon ou non - le fil que vous avez lié a déclaré qu'il en était ainsi.

Copier "septembre" sur cette chaîne est un dépassement de mémoire évident.

Un autre défi survient si vous passez stringà une autre fonction afin que cette dernière puisse écrire dans le tableau. Vous devez dire à l'autre fonction combien de temps le tableau est si elle ne crée pas un dépassement. Vous pourriez passer stringavec le résultat de, strlen()mais le fil de discussion explique comment cela peut exploser s'il stringn'est pas terminé par null.

Il vaut mieux allouer une chaîne de taille fixe (définie de préférence comme une constante), puis passer le tableau et la taille fixe à l'autre fonction. Les commentaires de @John Bode sont corrects et il existe des moyens d'atténuer ces risques. Ils nécessitent également plus d’efforts de votre part pour les utiliser.

D'après mon expérience, la valeur que j'ai initialisée char[]à est généralement trop petite pour les autres valeurs que je dois y placer. L'utilisation d'une constante définie permet d'éviter ce problème.


sizeof stringvous donnera la taille du tampon (8 octets); utilisez le résultat de cette expression plutôt que strlenlorsque vous vous préoccupez de mémoire.
De même, vous pouvez faire un chèque avant l'appel strcpypour voir si votre tampon cible est assez grand pour la chaîne source: if (sizeof target > strlen(src)) { strcpy (target, src); }.
Oui, si vous devez passer le tableau à une fonction, vous aurez besoin de passer sa taille physique ainsi: foo (array, sizeof array / sizeof *array);. - John Bode

Communauté
la source
2
sizeof stringvous donnera la taille du tampon (8 octets); utilisez le résultat de cette expression plutôt que strlenlorsque vous vous préoccupez de mémoire. De même, vous pouvez faire un chèque avant l'appel strcpypour voir si votre tampon cible est assez grand pour la chaîne source: if (sizeof target > strlen(src)) { strcpy (target, src); }. Oui, si vous devez passer le tableau à une fonction, vous aurez besoin de passer sa taille physique ainsi: foo (array, sizeof array / sizeof *array);.
John Bode
1
@ JohnBode - merci, et ce sont de bons points. J'ai intégré votre commentaire à ma réponse.
1
Plus précisément, la plupart des références au nom du tableau stringentraînent une conversion implicite en char*, pointant vers le premier élément du tableau. Cela perd les informations sur les limites du tableau. Un appel de fonction n'est que l'un des nombreux contextes dans lesquels cela se produit. char *ptr = string;est un autre. Even en string[0]est un exemple. l' []opérateur travaille sur des pointeurs, pas directement sur des tableaux. Suggestion de lecture: L' article 6 de la FAQ comp.lang.c .
Keith Thompson
Enfin une réponse qui renvoie à la question!
mastov
2

Je pense que l'idée de "mauvaise pratique" vient du fait que cette forme:

char string[] = "october is a nice month";

fait implicitement une strcpy du code de la machine source à la pile.

Il est plus efficace de gérer uniquement un lien vers cette chaîne. J'aime avec:

char *string = "october is a nice month";

ou directement:

strcpy(output, "october is a nice month");

(mais bien sûr dans la plupart des codes cela n'a probablement pas d'importance)

toto
la source
Ne feriez-vous une copie que si vous essayez de la modifier? Je pense que le compilateur serait plus intelligent que cela
Cole Johnson
1
Qu'en est-il des cas tels que celui char time_buf[] = "00:00";où vous allez modifier un tampon? Un char *initialisé sur un littéral de chaîne est défini sur l'adresse du premier octet. Essayer de le modifier entraîne donc un comportement indéfini car la méthode de stockage du littéral de chaîne est inconnue (implémentation définie), tandis que modifier les octets de a char[]est parfaitement légal car l'initialisation copie les octets dans un espace inscriptible alloué sur la pile. Dire que c'est «moins efficace» ou «mauvaise pratique» sans préciser les nuances de char* vs char[]est trompeur.
Braden Best
-3

Il ne faut jamais trop de temps, mais évitez d’initialiser char [] en chaîne, car "chaîne" est const char *, et vous l’assignez à char *. Donc, si vous passez ce caractère [] à la méthode qui modifie les données, vous pouvez avoir un comportement intéressant.

Comme recommandé, j'ai mélangé un peu char [] avec char *, ce n'est pas bien, car ils diffèrent un peu.

Il n'y a rien de mal à assigner des données à un tableau de caractères, mais comme l'intention d'utiliser ce tableau est de l'utiliser comme 'chaîne' (char *), il est facile d'oublier que vous ne devez pas modifier ce tableau.

Dainius
la source
3
Incorrect. L'initialisation copie le contenu du littéral chaîne dans le tableau. L'objet tableau n'est pas, à constmoins que vous ne le définissiez de cette façon. (Et les littéraux de chaîne en C ne le sont pas const, bien que toute tentative de modification d'un littéral de chaîne ait un comportement indéfini.) A char *s = "literal";le genre de comportement dont vous parlez; c'est mieux écrit commeconst char *s = "literal";
Keith Thompson
en effet ma faute, j'ai mélangé char [] avec char *. Mais je ne serais pas si sûr de copier du contenu dans un tableau. La vérification rapide avec le compilateur MS C montre que 'char c [] = "asdf";' va créer 'chaîne' dans le segment const, puis assigner cette adresse à une variable de tableau. C'est en fait une raison pour laquelle j'ai dit d'éviter les assignations à un tableau de caractères non const.
Dainius
Je suis sceptique. Essayez ce programme et laissez-moi savoir quelle sortie vous obtenez.
Keith Thompson le
2
"Et en général" asdf "est une constante, elle devrait donc être déclarée comme const." - Le même raisonnement demanderait constà int n = 42;, car 42est une constante.
Keith Thompson
1
Peu importe la machine sur laquelle vous vous trouvez. La norme de langue garantit que cela cest modifiable. C’est une garantie aussi forte que celle qui a été 1 + 1évaluée 2. Si le programme auquel j'ai lié ci - dessus fait autre chose que l'impression EFGH, il indique une implémentation en C non conforme.
Keith Thompson le