1
votes

Est-il sûr de strncpy dans une chaîne qui n'a pas de place pour le terminateur nul?

Considérez le code suivant:

const char foo[] = "lorem ipsum"; // foo is an array of 12 characters
const auto length = strlen(foo); // length is 11
string bar(length, '\0'); // bar was constructed with string(11, '\0')

strncpy(data(bar), foo, length);
cout << data(bar) << endl;

Je crois comprendre que les chaînes sont toujours allouées avec un élément nul caché. Si tel est le cas, alors bar alloue réellement 12 caractères, le 12 ème étant un '\ 0' caché et c'est parfaitement sûr. .. Si je me trompe, le cout entraînera un comportement non défini car il n'y a pas de terminateur nul.

Quelqu'un peut-il me confirmer? Est-ce légal?


Il y a eu beaucoup de questions sur pourquoi utiliser strncpy au lieu d'utiliser simplement la chaîne (const char *, const size_t) constructeur. Mon intention a été de rendre le code de mon jouet proche de mon code réel qui contient un vsnprintf . Malheureusement, même après avoir obtenu d'excellentes réponses ici, j'ai trouvé que vsnprintf ne se comporte pas de la même manière que strncpy , et j'ai posé une question de suivi ici: Pourquoi vsnprintf n'écrit-il pas le même nombre de caractères que strncpy?


20 commentaires

Comment pourrait-il en être autrement? En supposant bien sûr que vous ne copiez pas plus d'octets qu'il n'y a d'espace tampon disponible.


@TrebuchetMS Oui merci, j'ai corrigé ce commentaire.


Avez-vous un cas d'utilisation réel pour cela? Si vous donnez une chaîne C à un std :: string , il fera la même chose sans se gratter la tête.


@NathanOliver Ouais, j'utilise vsnprintf pour remplir une chaîne . Cela semblait juste ajouter plus de complexité à la question à poser avec cela, et n'a pas forcé la question.


qu'est-ce que string et qu'est-ce que data () ?


@Slava J'ai un mal utilisant namespace std avant cet exemple de jouet. Donc, tout cela est extrait de l'espace de noms standard.


Alors pourquoi pas std :: string bar (foo, length); au lieu de strncpy () ?


@JonathanMee Une raison pour laquelle vous n'utilisez pas de stringstream pour éviter complètement cela?


@Slava En fait, ce n'est pas comme ça que je le fais dans mon vrai code. Mais cela aurait fonctionné. Dans mon vrai code, j'appelle vsnprintf deux fois, une fois pour obtenir la taille, que j'utilise pour allouer une chaîne , puis j'appelle vsnprintf en remplissant à nouveau la chaîne .


Pourquoi ne pas utiliser char buffer [arbitrarySize] puis vsnprintf avec et ensuite créer simplement std :: string en utilisant ce tampon et cette taille? Vous pensez vraiment qu'appeler vsnprintf () deux fois est plus efficace que de créer un tableau de caractères sur la pile?


@NathanOliver La va_list est utilisée pour permettre à une interface C de prendre des informations de journalisation. Il n'est donc pas garanti que l'autre côté de l'interface aura même un stringstream .


@Slava arbitrarySize est peut-être trop petit, non?


Vouliez-vous dire cout << bar << endl ? Sinon, je ne suis pas sûr de l'intérêt de cette partie de la question


Probablement. Quoi qu'il en soit, je remettrais std dans votre exemple et le changerais pour faire snprintf (bar.data (), bar.size (), "format", data) au lieu de strncpy () pour éviter toute confusion.


@Slava Le double appel est une approche assez courante et évite d'avoir à deviner au hasard la taille dont vous avez besoin. Bien que les compromis varient.


@LightnessRacesinOrbit C'est vraiment une question sur la chaîne sous-jacente. Je veux savoir si le 12ème "caractère nul caché" existera même si je le construis comme string (11, '\ 0')


@LightnessRacesinOrbit bien sûr que la question devrait refléter cela au lieu d'un exemple laid d'utilisation de strncpy () qui conduit à la question, pourquoi ne pas créer std :: string directement à partir de celui-ci? Utiliser snprintf () dans cet exemple ne le compliquerait pas du tout.


@Slava Je suis d'accord que l'utilisation de strncpy ici n'est pas nécessaire et devrait être découragée (et j'ai voté pour votre commentaire à cet effet il y a quelque temps). Bien que cela n'invalide pas la question!


@LightnessRacesinOrbit Je n'ai pas dit que oui (et je voterais contre la question si je pense que oui), j'ai juste suggéré des améliorations. Utiliser snprintf ou similaire est compréhensible, utiliser strncpy dans ce cas stimule une discussion sans rapport.


@Slava En effet! :)


4 Réponses :


1
votes

Oui, c'est sûr selon la char * strncpy (char * destination, const char * source , size_t num) :

Copier les caractères de la chaîne

Copie les premiers caractères numériques de la source vers la destination. Si la fin de la chaîne C source (qui est signalée par un caractère nul) est trouvée avant que num caractères aient été copiés, la destination est complétée avec des zéros jusqu'à ce qu'un total de nombres caractères y ait été écrit.


6 commentaires

Le problème est qu'il y a un caractère supplémentaire qui doit être copié.


Pas la question, évidemment le strncpy n'écrira pas hors limites. La question est de savoir si le cout lu est hors limites. Veuillez corriger la réponse pour répondre à la question ou supprimer.


@Matthieu - Pas nécessairement. Si c'est std :: string qui est la cible, il est conscient de sa longueur.


@StoryTeller il manque cependant le \ 0 (lorsqu'il est utilisé avec data (bar) ). C'est en effet très bien lorsque vous utilisez la chaîne elle-même.


@Matthieu - Ce qui n'est un problème que si l'op a besoin du tampon cible pour contenir une chaîne terminée par nul.


@StoryTeller est en effet entièrement d'accord.



3
votes

Il y a deux choses différentes ici.

Tout d'abord, strncpy ajoute-t-il un \ 0 supplémentaire dans cette instance (11 éléments non- \ 0 à copier dans une chaîne de taille 11). La réponse est non:

Copie au plus nombre de caractères de la chaîne d'octets pointée par src (y compris le caractère nul de fin) dans le tableau de caractères pointé par dest.

Si count est atteint avant la copie de la chaîne src entière, le tableau de caractères résultant n'est pas terminé par un nul.

L'appel est donc parfaitement bien.

Ensuite, data () vous donne une chaîne terminée par \ 0 :

c_str () et data () exécutent la même fonction. (depuis C ++ 11)

Il semble donc que pour C ++ 11, vous êtes en sécurité. La question de savoir si la chaîne alloue un \ 0 supplémentaire ou non ne semble pas être indiquée dans la documentation, mais l'API indique clairement que ce que vous faites est parfaitement bien.


2 commentaires

Pour étendre votre déclaration, vous dites que string n'est pas alloué avec un terminateur nul caché?


L'allocation doit fonctionner de cette façon pour que c_str () / data () soit O (1) , étant donné que l'indexation str [str.size ()] vous donne un '\ 0' (depuis C ++ 11). En pratique, ils ont toujours fonctionné de cette façon.



2
votes

Vous avez alloué un std :: string à 11 caractères. Vous n'essayez pas de lire ou d'écrire quoi que ce soit au-delà de cela, donc cette partie sera en sécurité.

La vraie question est donc de savoir si vous avez gâché les éléments internes de la chaîne. Puisque vous n'avez rien fait qui n'est pas autorisé, comment cela serait-il possible? S'il est nécessaire que la chaîne conserve en interne un tampon de 12 octets avec un remplissage nul à la fin afin de remplir son contrat, ce sera le cas quelles que soient les opérations que vous avez effectuées.


0 commentaires

5
votes

Ceci est sûr, tant que vous copiez les caractères [0, size ()) dans la chaîne. Pour [basic.string] / 3

Dans tous les cas, [data (), data () + size ()] est une plage valide, data () + size () pointe vers un objet avec la valeur charT () (un «terminateur nul»), et size () <= capacity () est true .

Donc string bar (length, '\ 0') vous donne une chaîne avec une size () de 11, avec un terminateur nul immuable à la fin (pour un total de 12 caractères en taille réelle). Tant que vous n'écrasez pas ce terminateur nul, ou n'essayez pas d'écrire au-delà, tout va bien.


14 commentaires

Je ne sais pas s'il ira bien - std :: string :: length () donnera des informations "fausses"


@Slava Comment serait-ce faux? La chaîne commence par une taille de 11 qui ne peut pas changer à moins d'utiliser une fonction de chaîne. S'ils ne copient que 5 caractères, ils ont toujours une chaîne de taille 11, il n'y a que 6 nuls supplémentaires qui la terminent.


J'ai mis «faux» entre guillemets. Oui, cela montrera la bonne taille de la mémoire tampon, mais l'utiliser comme chaîne peut conduire à des problèmes horribles qui sont très difficiles à attraper et à corriger plus tard lorsque cette chaîne est utilisée.


@Slava un std :: string est autorisé à contenir des caractères nuls, contrairement à une chaîne C.


strlen ne compte que les chaînes de style C. En effet, la taille donnée par la chaîne est la bonne.


@MarkRansom Je comprends cela, donc je ne prétends pas qu'il sera cassé à coup sûr, mais il y a de fortes chances qu'il y ait des problèmes en aval - car la plupart des développeurs s'attendent à ce que length () vous donne la longueur de la chaîne, pas la taille du tampon. J'éviterais donc un tel code si je ne veux pas de problèmes et j'utiliserais std::vector si j'ai besoin d'un tampon.


@Slava Je peux certainement voir le potentiel de problèmes, mais je pense que vous l'exagérez. length () vous donne vraiment la vraie taille de la chaîne, c'est strlen () qui est confus. Vous ne rencontrerez un problème que lorsque vous mélangez du code qui fonctionne sur string et C-strings, et seulement si vous avez un caractère nul incorporé, ce qui n'est pas le cas dans l'exemple donné.


@MarkRansom "Vous ne rencontrerez un problème que lorsque vous mélangez du code qui opère sur des chaînes et des chaînes C" à droite, donc en disant que vous serez probablement OK pour une personne qui vous demande s'il est bon d'utiliser strncpy () car il est peu probable que vous utilisiez des chaînes C. Oui, je suis en train d'exagérer.


@Slava J'entends le sarcasme dans votre réponse. J'espère que la raison de la copie dans string en premier lieu est de travailler dessus sous cette forme à partir de ce moment-là, à quel point un null intégré est pratiquement inoffensif. Encore une fois, comme le montre la question.


@MarkRansom J'essaie de dire que si quelqu'un essaie de strncpy () dans std :: string alors il y a de fortes chances que cette chaîne soit convertie / traitée comme C- chaîne plus tard. J'éviterais donc d'utiliser std :: string comme tampon si je ne veux pas passer beaucoup de temps à déboguer des problèmes difficiles à attraper. Mais ce n'est que mon humble avis, alors j'ai mis «faux» entre guillemets.


@Slava "comme la plupart des développeurs s'attendent à ce que length () vous donne la longueur de la chaîne, pas la taille du tampon." Mais c'est exactement ce qu'il fait. Une chaîne est une séquence d'octets. Celui-ci a quelques octets nuls vers la fin. Vous devriez perdre l'habitude de penser que "string" est équivalent à "c-string". (Pendant ce temps, le tampon pourrait être un peu plus grand grâce à .reserve () et ainsi de suite)


@Slava "alors il y a de fortes chances que cette chaîne soit convertie / traitée comme chaîne C plus tard" traité peut être un problème, mais converti (en utilisant strlen et strcpy des données ()) doivent être en sécurité.


@LightnessRacesinOrbit "Vous devriez perdre l'habitude de penser que" string "est équivalent à" c-string "." Le problème n'est pas que je sors de cette habitude mais que je force tous les développeurs à le faire. Je n'ai pas ce pouvoir, il est donc plus sûr d'utiliser std::vector quand j'ai besoin de tampon et std :: string quand j'ai besoin de chaîne, ce qui peut être possible traité comme C-string.


@Slava Si les développeurs autour de vous traitent std :: string comme une chaîne C, alors ils ne sont pas des développeurs C ++ .... (même si je conviendrai qu'un vecteur < / code> - ou vector ! - est souvent supérieur, pour un certain nombre de raisons)