web-dev-qa-db-fra.com

Quelle est la raison d'être des chaînes terminées par null?

Bien que j'aime le C et le C++, je ne peux m'empêcher de me gratter la tête devant le choix des chaînes nulles:

  • Les chaînes préfixées en longueur (c’est-à-dire Pascal) existaient avant C
  • Les chaînes préfixées par longueur accélèrent plusieurs algorithmes en permettant une recherche constante de la durée.
  • Les chaînes préfixées en longueur rendent plus difficile la génération d'erreurs de saturation de la mémoire tampon.
  • Même sur un ordinateur 32 bits, si vous autorisez la chaîne à avoir la taille de la mémoire disponible, une chaîne préfixée en longueur n'a que trois octets de plus qu'une chaîne terminée par zéro. Sur les machines 16 bits, il s'agit d'un seul octet. Sur les ordinateurs 64 bits, 4 Go est une limite de longueur de chaîne raisonnable, mais même si vous souhaitez l'étendre à la taille de l'ordinateur Word, les ordinateurs 64 bits disposent généralement de suffisamment de mémoire, ce qui rend l'argument supplémentaire de sept octets d'un argument nul. Je sais que la norme C originale a été écrite pour des machines incroyablement pauvres (en termes de mémoire), mais l'argument de l'efficacité ne me vend pas ici.
  • Pratiquement tous les autres langages (Perl, Pascal, Python, Java, C #, etc.) utilisent des chaînes préfixées en longueur. Ces langues sont généralement supérieures à C dans les tests de manipulation des chaînes, car elles sont plus efficaces avec les chaînes.
  • C++ a corrigé cela un peu avec le std::basic_string modèle, mais les tableaux de caractères simples qui attendent des chaînes terminées par null sont toujours présents. Ceci est également imparfait car il nécessite une allocation de tas.
  • Les chaînes terminées par null doivent réserver un caractère (à savoir null), qui ne peut pas exister dans la chaîne, tandis que les chaînes avec préfixe de longueur peuvent contenir des valeurs null incorporées.

Plusieurs de ces choses ont été révélées plus récemment que C, il serait donc logique que C ne les connaisse pas. Cependant, plusieurs étaient clairs bien avant la naissance de C. Pourquoi aurait-on choisi des chaînes terminées par null au lieu du préfixe de longueur évidemment supérieure?

EDIT : Depuis que certains ont demandé des faits (et n'ont pas aimé ceux J'ai déjà fourni) sur mon point d'efficacité ci-dessus, ils découlent de quelques choses:

  • La concaténation avec des chaînes terminées par null nécessite une complexité temporelle de O (n + m). Le préfixe de longueur nécessite souvent seulement O (m).
  • La longueur utilisant des chaînes terminées par null nécessite la complexité temporelle de O(n)). La longueur du préfixe de longueur est O (1).
  • Length et concat sont de loin les opérations de chaîne les plus courantes. Il existe plusieurs cas où les chaînes terminées par null peuvent être plus efficaces, mais elles se produisent beaucoup moins souvent.

Dans les réponses ci-dessous, voici quelques cas où les chaînes terminées par null sont plus efficaces:

  • Lorsque vous devez couper le début d'une chaîne et que vous devez le passer à une méthode. Vous ne pouvez pas vraiment faire cela en temps constant avec le préfixe de longueur même si vous êtes autorisé à détruire la chaîne d'origine, car le préfixe de longueur doit probablement suivre les règles d'alignement.
  • Dans certains cas, lorsque vous parcourez simplement la chaîne caractère par caractère, vous pouvez éventuellement enregistrer un registre de CPU. Notez que cela ne fonctionne que dans le cas où vous n'avez pas alloué la chaîne de manière dynamique (vous devez alors la libérer, ce qui nécessite d'utiliser le registre de la CPU que vous avez enregistré pour contenir le pointeur que vous avez reçu à l'origine de malloc et de ses amis).

Aucune de ces réponses n’est presque aussi commune que la longueur et la concat.

Il y en a une plus affirmée dans les réponses ci-dessous:

  • Vous devez couper la fin de la chaîne

mais celui-ci est incorrect - la durée est la même pour les chaînes précédées de la chaîne null et préfixées en longueur. (Les chaînes terminées par null collent simplement un null où vous voulez que la nouvelle extrémité soit, les préfixeurs de longueur se soustractent simplement du préfixe.)

269
Billy ONeal

De la bouche du cheval

Aucun de BCPL, B ou C ne prend en charge les données de caractères fortement dans le langage; chacun traite les chaînes beaucoup plus comme des vecteurs d'entiers et complète les règles générales par quelques conventions. En BCPL et en B, un littéral de chaîne indique l'adresse d'une zone statique initialisée avec les caractères de la chaîne, compressés dans des cellules. Dans BCPL, le premier octet condensé contient le nombre de caractères de la chaîne; dans B, il n'y a pas de compte et les chaînes sont terminées par un caractère spécial, que B a orthographié *e. Cette modification a été apportée en partie pour éviter la limitation de la longueur d'une chaîne provoquée par le maintien du compte dans un créneau de 8 ou 9 bits, et en partie parce que le maintien du compte semblait, selon notre expérience, moins pratique que l'utilisation d'un terminateur.

Dennis M Ritchie, Développement du langage C

190
Hans Passant

C n'a pas de chaîne dans le langage. Une 'chaîne' en C est juste un pointeur sur char. Alors peut-être que vous posez la mauvaise question.

"Quelle est la raison pour omettre un type de chaîne" pourrait être plus pertinent. À cela, je ferai remarquer que C n'est pas un langage orienté objet et n'a que des types de valeurs de base. Une chaîne est un concept de niveau supérieur qui doit être implémenté en combinant d'une manière ou d'une autre des valeurs d'autres types. C est à un niveau inférieur d'abstraction.

à la lumière de la rafale ci-dessous:

Je veux juste souligner que je n'essaie pas de dire que c'est une question stupide ou mauvaise, ou que la manière de représenter les chaînes de caractères en C est le meilleur choix. J'essaie de préciser que la question serait plus succincte si vous tenez compte du fait que C ne dispose d'aucun mécanisme pour différencier une chaîne en tant que type de données d'un tableau d'octets. Est-ce le meilleur choix compte tenu de la puissance de traitement et de la mémoire des ordinateurs actuels? Probablement pas. Mais le recul est toujours 20/20 et tout ça :)

151
Robert S Ciaccio

La question est posée comme une chose Length Prefixed Strings (LPS) vs zero terminated strings (SZ), mais expose principalement les avantages des chaînes avec préfixe de longueur. Cela peut sembler accablant, mais pour être honnête, nous devrions également considérer les inconvénients du LPS et les avantages de la SZ.

Si je comprends bien, la question peut même être comprise comme une manière partiale de demander "quels sont les avantages de Zero Terminated Strings?".

Avantages (je vois) de chaînes terminées à zéro:

  • très simple, pas besoin d'introduire de nouveaux concepts en langage, les tableaux de caractères/les pointeurs de caractères peuvent faire.
  • le langage de base n'inclut que le sucre syntaxique minimal pour convertir quelque chose entre guillemets doubles en un tas de caractères (vraiment un tas d'octets). Dans certains cas, il peut être utilisé pour initialiser des éléments totalement indépendants du texte. Par exemple, le format de fichier d'image xpm est une source C valide contenant des données d'image codées sous forme de chaîne.
  • à propos, vous pouvez mettre un zéro dans un littéral de chaîne, le compilateur en ajoutera simplement un autre à la fin du littéral: "this\0is\0valid\0C". Est-ce une ficelle? ou quatre cordes? Ou un tas d'octets ...
  • implémentation à plat, pas d'indirection cachée, pas d'entier caché.
  • aucune allocation de mémoire cachée n'est impliquée (enfin, certaines fonctions non standard infâmes, telles que strdup, effectuent l'allocation, mais c'est principalement une source de problème).
  • pas de problème spécifique pour les petits ou les gros matériels (imaginez la charge de gérer une longueur de préfixe de 32 bits sur des microcontrôleurs de 8 bits, ou les restrictions imposées pour limiter la taille de chaîne à moins de 256 octets, ce qui était un problème que j'avais réellement avec Turbo Pascal il y a bien longtemps).
  • la mise en œuvre de la manipulation de chaîne est juste une poignée de fonction de bibliothèque très simple
  • efficace pour l'utilisation principale de chaînes: texte constant lu séquentiellement à partir d'un début connu (principalement des messages à l'utilisateur).
  • le zéro final n'est même pas obligatoire, tous les outils nécessaires pour manipuler les caractères, comme un tas d'octets, sont disponibles. Lors de l'initialisation du tableau en C, vous pouvez même éviter le terminateur NUL. Il suffit de définir la bonne taille. char a[3] = "foo"; est valide C (pas C++) et ne mettra pas de zéro final dans a.
  • cohérent avec le point de vue unix "tout est fichier", y compris les "fichiers" qui n'ont pas de longueur intrinsèque comme stdin, stdout. N'oubliez pas que les primitives de lecture et d'écriture ouvertes sont implémentées à un niveau très bas. Ce ne sont pas des appels de bibliothèque, mais des appels système. Et la même API est utilisée pour les fichiers binaires ou texte. Les primitives de lecture de fichier obtiennent une adresse de tampon, une taille et renvoient la nouvelle taille. Et vous pouvez utiliser des chaînes comme tampon pour écrire. Utiliser un autre type de représentation de chaîne impliquerait que vous ne pouvez pas facilement utiliser une chaîne littérale comme tampon de sortie, ou que vous deviez lui donner un comportement très étrange lors de son transfert en char*. À savoir ne pas renvoyer l'adresse de la chaîne, mais plutôt renvoyer les données réelles.
  • très facile à manipuler les données textuelles lues à partir d'un fichier sur place, sans copie inutile du tampon, insérez juste des zéros aux bons endroits (enfin, pas vraiment avec le C moderne car les chaînes entre guillemets sont constantes, des tableaux de caractères const sont généralement conservés dans des données non modifiables segment).
  • l'ajout de certaines valeurs int, quelle que soit leur taille, implique des problèmes d'alignement. La longueur initiale doit être alignée, mais il n'y a aucune raison de le faire pour les données de caractères (et encore une fois, forcer l'alignement des chaînes impliquerait des problèmes pour les traiter comme un groupe d'octets).
  • la longueur est connue au moment de la compilation pour des chaînes littérales constantes (sizeof). Alors, pourquoi quelqu'un voudrait-il le stocker en mémoire en l'ajoutant au préalable aux données réelles?
  • d'une manière que C fait comme tout le monde ou presque, les chaînes sont considérées comme des tableaux de caractères. Comme la longueur du tableau n'est pas gérée par C, c'est la longueur logique qui n'est pas gérée pour les chaînes. La seule chose surprenante est que 0 élément a été ajouté à la fin, mais c'est juste au niveau de la langue principale lorsque vous tapez une chaîne entre guillemets. Les utilisateurs peuvent parfaitement appeler des fonctions de manipulation de chaîne en passant de longueur, ou même utiliser plain memcopy à la place. SZ sont juste une installation. Dans la plupart des autres langues, la longueur du tableau est gérée, il en va de même pour les chaînes.
  • de nos jours, de toute façon, les jeux de caractères sur 1 octet ne suffisent pas et vous devez souvent traiter avec des chaînes codées Unicode où le nombre de caractères est très différent du nombre d’octets. Cela implique que les utilisateurs voudront probablement plus que "juste la taille", mais aussi d'autres informations. Garder la longueur ne rien utiliser (en particulier aucun endroit naturel pour les stocker) en ce qui concerne ces autres informations utiles.

Cela dit, nul besoin de se plaindre dans les rares cas où les chaînes C standard sont effectivement inefficaces. Les libs sont disponibles. Si je suivais cette tendance, je devrais me plaindre que la norme C n'inclut aucune fonction de prise en charge des expressions rationnelles ... mais tout le monde sait que ce n'est pas un problème, car il existe des bibliothèques disponibles à cet effet. Ainsi, lorsque l’efficacité de la manipulation des chaînes est souhaitée, pourquoi ne pas utiliser une bibliothèque du type bstring ? Ou même des chaînes C++?

[~ # ~] modifier [~ # ~] : J'ai récemment jeté un œil à chaînes de caractères D . Il est assez intéressant de voir que la solution choisie n’est ni un préfixe de taille, ni une terminaison nulle. Comme en C, les chaînes littérales délimitées par des guillemets ne sont qu'un raccourci pour les tableaux de caractères immuables, et le langage comporte également un mot-clé de chaîne qui signifie que (tableau de caractères immuable).

Mais les tableaux D sont beaucoup plus riches que les tableaux C. Dans le cas de tableaux statiques, la longueur est connue au moment de l'exécution, il n'est donc pas nécessaire de la stocker. Le compilateur l'a au moment de la compilation. Dans le cas des tableaux dynamiques, la longueur est disponible mais la documentation D ne précise pas où elle est conservée. Pour tout ce que nous savons, le compilateur pourrait choisir de le garder dans un registre ou dans une variable stockée loin des données de caractères.

Sur les tableaux de caractères normaux ou les chaînes non littérales, il n'y a pas de zéro final; le programmeur doit donc le définir lui-même s'il souhaite appeler une fonction C à partir de D. Dans le cas particulier des chaînes littérales, le compilateur D met néanmoins un zéro à la fin. fin de chaque chaîne (pour permettre une conversion facile en chaînes C pour faciliter l'appel de la fonction C?), mais ce zéro ne fait pas partie de la chaîne (D ne la prend pas en compte dans la taille de la chaîne).

La seule chose qui m'a un peu déçu, c'est que les chaînes sont supposées être en utf-8, mais length renvoie toujours un certain nombre d'octets (du moins c'est le cas sur mon compilateur gdc), même lorsque vous utilisez des caractères multi-octets. Il m'est difficile de savoir s'il s'agit d'un bogue du compilateur ou d'un problème particulier. (OK, j’ai probablement découvert ce qui s’est passé. Pour dire au compilateur D de votre source que vous utilisez utf-8, vous devez mettre une marque d'ordre d'octet stupide au début. 8 qui est supposé être ASCII).

101
kriss

Je pense, il a des raisons historiques et trouvé cela dans wikipedia :

Au moment où C (et les langages dont il est dérivé) ont été développés, la mémoire était extrêmement limitée, de sorte que l’utilisation d’un seul octet d’overhead pour stocker la longueur d’une chaîne était intéressante. La seule alternative populaire à cette époque, généralement appelée "chaîne Pascal" (bien qu'elle soit également utilisée par les premières versions de BASIC), utilisait un octet de tête pour stocker la longueur de la chaîne. Cela permet à la chaîne de contenir NUL et rend la recherche de la longueur nécessaire avec un seul accès mémoire (O (1) (constant) time). Mais un octet limite la longueur à 255. Cette limitation de longueur était beaucoup plus restrictive que les problèmes posés par la chaîne C, si bien que la chaîne C en général l'a emporté.

60
khachik

Calavera est à droite , mais comme les gens ne semblent pas bien comprendre, je donnerai quelques exemples de code.

Tout d'abord, considérons ce que C est: un langage simple, où tout code a une traduction directe dans le langage machine. Tous les types entrent dans les registres et sur la pile et ne nécessitent ni système d’exploitation ni grande bibliothèque d’exécution, car ils étaient censés écrire ces choses (une tâche à exécuter). qui est superbement bien adapté, considérant qu’il n’ya même pas de concurrent probable à ce jour).

Si C avait un type string, comme int ou char, il s'agirait d'un type qui ne rentrerait ni dans un registre ni dans la pile et nécessiterait une allocation de mémoire. (avec toutes ses infrastructures de support) à gérer de quelque manière que ce soit. Ce qui va à l’encontre des principes de base de C.

Donc, une chaîne en C est:

char s*;

Supposons donc qu'il s'agissait d'un préfixe de longueur. Ecrivons le code pour concaténer deux chaînes:

char* concat(char* s1, char* s2)
{
    /* What? What is the type of the length of the string? */
    int l1 = *(int*) s1;
    /* How much? How much must I skip? */
    char *s1s = s1 + sizeof(int);
    int l2 = *(int*) s2;
    char *s2s = s2 + sizeof(int);
    int l3 = l1 + l2;
    char *s3 = (char*) malloc(l3 + sizeof(int));
    char *s3s = s3 + sizeof(int);
    memcpy(s3s, s1s, l1);
    memcpy(s3s + l1, s2s, l2);
    *(int*) s3 = l3;
    return s3;
}

Une autre alternative serait d'utiliser une structure pour définir une chaîne:

struct {
  int len; /* cannot be left implementation-defined */
  char* buf;
}

À ce stade, toute manipulation de chaîne nécessiterait deux affectations, ce qui signifie en pratique que vous utiliseriez une bibliothèque pour le gérer.

Ce qui est amusant, c’est ... des structures comme celle-là do existent en C! Ils ne sont tout simplement pas utilisés pour l'affichage quotidien des messages destinés aux utilisateurs.

Donc, voici ce que dit Calavera: il n’ya pas de type de chaîne dans C. Pour faire quoi que ce soit avec elle, vous devez prendre un pointeur et le décoder comme un pointeur vers deux types différents, puis il devient très important de déterminer la taille d'une chaîne, et ne peut pas rester comme "implémentation définie".

C can gère de toute façon la mémoire, et les fonctions mem de la bibliothèque (dans <string.h>, même!) fournit tous les outils dont vous avez besoin pour gérer la mémoire comme une paire de pointeur et de taille. Les soi-disant "chaînes" en C ont été créés dans un seul but: afficher des messages dans le contexte de l'écriture d'un système d'exploitation destiné aux terminaux texte. Et, pour cela, la résiliation nulle suffit.

31
Daniel C. Sobral

Évidemment, pour des raisons de performance et de sécurité, vous souhaiterez conserver la longueur d’une chaîne pendant que vous l’utilisez plutôt que d’exécuter de manière répétée strlen ou l’équivalent. Cependant, stocker la longueur dans un emplacement fixe juste avant le contenu de la chaîne est une conception incroyablement mauvaise. Comme Jörgen l'a souligné dans les commentaires sur la réponse de Sanjit, cela empêche de traiter la queue d'une chaîne comme une chaîne, ce qui, par exemple, permet d'effectuer de nombreuses opérations courantes telles que path_to_filename ou filename_to_extension impossible sans allouer une nouvelle mémoire (et avec le risque d'échec et de traitement des erreurs). Et bien sûr, il y a le problème que personne ne peut s'accorder sur le nombre d'octets que doit occuper le champ de longueur de chaîne (beaucoup de mauvais langages "chaîne Pascal" utilisaient des champs de 16 bits ou même des champs de 24 bits qui empêchent le traitement de chaînes longues).

La conception de C consistant à laisser le programmeur choisir si/où/comment stocker la longueur est beaucoup plus flexible et puissante. Mais bien sûr, le programmeur doit être intelligent. C punit la bêtise avec des programmes qui plantent, bloquent ou donnent la racine à vos ennemis.

19
R..

Lazyness, enregistrez la frugalité et la portabilité en tenant compte de l’intestin de toutes les langues, en particulier de C, un peu au-dessus de Assembly (héritant ainsi de nombreux codes hérités de Assembly). Vous conviendrez qu'un caractère NULL serait inutile pendant ces ASCII jours, il (et probablement aussi bon qu'un EOF char) de contrôle).

voyons dans un pseudo-code

function readString(string) // 1 parameter: 1 register or 1 stact entries
    pointer=addressOf(string) 
    while(string[pointer]!=CONTROL_CHAR) do
        read(string[pointer])
        increment pointer

total 1 utilisation du registre

cas 2

 function readString(length,string) // 2 parameters: 2 register used or 2 stack entries
     pointer=addressOf(string) 
     while(length>0) do 
         read(string[pointer])
         increment pointer
         decrement length

total 2 registre utilisé

Cela peut sembler à courte vue à ce moment-là, mais compte tenu de la frugalité en code et en registre (qui étaient PREMIUM à cette époque, le moment où vous savez, ils utilisent une carte perforée). Ainsi, étant plus rapide (lorsque la vitesse du processeur pouvait être comptée en kHz), ce "piratage" était diablement bon et portable pour un processeur sans registre avec facilité.

Par souci d'argument, je vais mettre en œuvre 2 opération de chaîne commune

stringLength(string)
     pointer=addressOf(string)
     while(string[pointer]!=CONTROL_CHAR) do
         increment pointer
     return pointer-addressOf(string)

complexité O(n) où, dans la plupart des cas, chaîne Pascal est O(1) car la longueur de la chaîne est pré-ajoutée à la structure de chaîne (qui signifierait également que cette opération devrait être effectuée à un stade plus précoce).

concatString(string1,string2)
     length1=stringLength(string1)
     length2=stringLength(string2)
     string3=allocate(string1+string2)
     pointer1=addressOf(string1)
     pointer3=addressOf(string3)
     while(string1[pointer1]!=CONTROL_CHAR) do
         string3[pointer3]=string1[pointer1]
         increment pointer3
         increment pointer1
     pointer2=addressOf(string2)
     while(string2[pointer2]!=CONTROL_CHAR) do
         string3[pointer3]=string2[pointer2]
         increment pointer3
         increment pointer1
     return string3

complexité O(n) et l'ajout d'une longueur de chaîne ne modifierait pas la complexité de l'opération, bien que j'avoue que cela prendrait 3 fois moins de temps.

D'autre part, si vous utilisez une chaîne Pascal, vous devrez redéfinir votre API pour prendre en compte la longueur du registre et son endianité. La chaîne Pascal a la limitation bien connue de 255 caractères (0xFF), car la longueur a été stockée dans 1 octet (8 bits). ), et si vous vouliez une chaîne plus longue (16 bits -> n'importe quoi), vous devriez prendre en compte l'architecture dans une couche de votre code, ce qui signifierait dans la plupart des cas des API de chaîne incompatibles si vous vouliez une chaîne plus longue.

Exemple:

Un fichier a été écrit avec votre API de chaîne préposée sur un ordinateur 8 bits et doit ensuite être lu sur un ordinateur 32 bits, ce que le programme paresseux considère que vos 4 octets sont la longueur de la chaîne, puis allouez autant de mémoire. puis essayez de lire autant d'octets. Un autre cas serait PPC chaîne de 32 octets lue (en petit bout) sur un x86 (gros en bout)), bien sûr, si vous ne savez pas que l’un est écrit par l’autre, il y aurait des problèmes. La longueur d'un octet (0x00000001) deviendrait 16777216 (0x0100000), ce qui correspond à 16 Mo pour la lecture d'une chaîne de 1 octet.Bien sûr, vous diriez que les gens devraient s'entendre sur un standard mais que même l'unicode à 16 bits a une grande et petite endianité.

Bien sûr, C aurait aussi ses problèmes, mais serait très peu affecté par les problèmes soulevés ici.

13
dvhh

À bien des égards, C était primitif. Et j'ai adoré ça.

C'était un pas en avant du langage d'assemblage, vous donnant à peu près la même performance avec un langage beaucoup plus facile à écrire et à maintenir.

Le terminateur nul est simple et ne nécessite aucun support particulier de la part du langage.

En regardant en arrière, cela ne semble pas si pratique. Mais j’avais utilisé le langage des assemblées dans les années 80 et cela me semblait très pratique à l’époque. Je pense simplement que les logiciels évoluent continuellement et que les plates-formes et les outils deviennent de plus en plus sophistiqués.

9
Jonathan Wood

Supposons un instant que C implémente les chaînes de la manière Pascal, en les préfixant par la longueur: une chaîne de 7 caractères est-elle le même type de données qu'une chaîne de 3 caractères? Si la réponse est oui, alors quel type de code le compilateur doit-il générer lorsque j'assigne le premier à ce dernier? La chaîne doit-elle être tronquée ou redimensionnée automatiquement? Si redimensionné, cette opération doit-elle être protégée par un verrou afin de sécuriser les threads? L’approche C a résolu tous ces problèmes, qu’on le veuille ou non :)

8
Cristian

En quelque sorte, j'ai compris que la question impliquait qu'il n'y avait pas de support du compilateur pour les chaînes préfixées en longueur en C. L'exemple suivant montre qu'au moins vous pouvez démarrer votre propre bibliothèque de chaînes en C, où les longueurs de chaîne sont comptées lors de la compilation, avec une construction comme celle-ci:

#define PREFIX_STR(s) ((prefix_str_t){ sizeof(s)-1, (s) })

typedef struct { int n; char * p; } prefix_str_t;

int main() {
    prefix_str_t string1, string2;

    string1 = PREFIX_STR("Hello!");
    string2 = PREFIX_STR("Allows \0 chars (even if printf directly doesn't)");

    printf("%d %s\n", string1.n, string1.p); /* prints: "6 Hello!" */
    printf("%d %s\n", string2.n, string2.p); /* prints: "48 Allows " */

    return 0;
}

Cela ne posera toutefois aucun problème, car vous devez faire attention lorsque vous libérez spécifiquement ce pointeur de chaîne et qu'il est alloué statiquement (literal char array).

Edit: Pour répondre plus directement à la question, j’estime que c était ainsi que C pourrait prendre en charge la longueur de chaîne disponible (en tant que constante de compilation), si vous en avez besoin, mais toujours sans surcharge de mémoire si vous souhaitez utiliser uniquement des pointeurs et une terminaison zéro.

Bien sûr, il semble que la pratique recommandée soit de travailler avec des chaînes terminées par un zéro, car la bibliothèque standard en général ne prend pas la longueur de chaîne en tant qu'argument et que l'extraction de la longueur n'est pas aussi simple que le code char * s = "abc", comme le montre mon exemple.

7
Pyry Jahkola

"Même sur une machine 32 bits, si vous autorisez la chaîne à avoir la taille de la mémoire disponible, une chaîne préfixée en longueur n'a qu'une largeur de trois octets supérieure à celle d'une chaîne terminée par un caractère nul."

Premièrement, 3 octets supplémentaires peuvent représenter une surcharge considérable pour les chaînes courtes. En particulier, une chaîne de longueur nulle nécessite désormais 4 fois plus de mémoire. Certains d'entre nous utilisent des machines 64 bits, nous avons donc besoin de 8 octets pour stocker une chaîne de longueur nulle ou le format de chaîne ne peut pas gérer les chaînes les plus longues prises en charge par la plate-forme.

Il peut également y avoir des problèmes d'alignement à traiter. Supposons que je dispose d’un bloc de mémoire contenant 7 chaînes, du type "solo\0 seconde\0\0four\0five\0\0seventh". La deuxième chaîne commence à l'offset 5. Le matériel peut nécessiter que les entiers 32 bits soient alignés sur une adresse multiple de 4. Vous devez donc ajouter un remplissage, ce qui augmente encore la surcharge. La représentation en C est très efficace en termes de mémoire. (L'efficacité de la mémoire est bonne; elle améliore les performances du cache, par exemple.)

5
Brangdon

Un point qui n’a pas encore été mentionné: lors de la conception de C, il existait de nombreuses machines sur lesquelles un "caractère" n’était pas de huit bits (même aujourd’hui, il existe des plates-formes DSP sur lesquelles il ne l’est pas). Si on décide que les chaînes doivent avoir un préfixe de longueur, combien de préfixes de longueur doit avoir une valeur? L'utilisation de deux imposerait une limite artificielle à la longueur de chaîne pour les machines disposant d'un espace d'adressage de 8 bits et de 32 bits, tout en gaspillant de l'espace sur des machines dotées d'un espace d'adressage de 16 bits et de 16 caractères.

Si on veut permettre aux chaînes de longueur arbitraire d'être stockées efficacement, et si 'char' est toujours de 8 bits, on peut - moyennant quelques dépenses en vitesse et en taille de code - définir un schéma est une chaîne préfixée par un nombre pair N aurait une longueur de N/2 octets, une chaîne préfixée par une valeur impaire N et une valeur paire M (lecture en arrière) pourrait être ((N-1) + M * char_max)/2, etc., et exiger que tout tampon les revendications offrant une certaine quantité d’espace pour contenir une chaîne doivent permettre de disposer de suffisamment d’octets précédant cet espace pour gérer la longueur maximale. Cependant, le fait que 'char' ne soit pas toujours 8 bits compliquerait un tel schéma, car le nombre de 'char' requis pour contenir la longueur d'une chaîne varie en fonction de l'architecture de la CPU.

4
supercat

La terminaison nulle permet des opérations rapides basées sur un pointeur.

4
Sanjit Saluja

Selon Joel Spolsky dans cet article de blog ,

C'est parce que le microprocesseur PDP-7, sur lequel UNIX et le langage de programmation C ont été inventés, avait un type de chaîne ASCIZ. ASCIZ signifiait "ASCII avec un Z (zéro) à la fin".

Après avoir vu toutes les autres réponses ici, je suis convaincu que même si cela est vrai, cela ne représente qu'une partie de la raison pour laquelle C a des "chaînes" terminées par un caractère null. Cet article est très éclairant sur la façon dont des choses simples comme des chaînes peuvent être assez difficiles.

2
BenK

pas une justification nécessairement mais un contrepoint à coder en longueur

  1. Certaines formes de codage de longueur dynamique sont supérieures au codage de longueur statique en ce qui concerne la mémoire, tout dépend de l'utilisation. Il suffit de regarder UTF-8 pour preuve. C'est essentiellement un tableau de caractères extensible pour coder un seul caractère. Cela utilise un seul bit pour chaque octet étendu. La terminaison NUL utilise 8 bits. Le préfixe de longueur, je pense, peut raisonnablement être appelé longueur infinie en utilisant 64 bits. Le facteur décisif est la fréquence à laquelle vous frappez la caisse de vos bits supplémentaires. 1 seule corde extrêmement grosse? Qui se soucie si vous utilisez 8 ou 64 bits? Beaucoup de petites chaînes (Ie chaînes de mots anglais)? Ensuite, vos coûts de préfixe représentent un pourcentage élevé.

  2. Les chaînes préfixées en longueur permettant un gain de temps est ce n'est pas une chose réelle. Que la longueur de vos données fournies soit obligatoire, que vous comptiez au moment de la compilation ou que vous receviez réellement des données dynamiques que vous devez coder sous forme de chaîne. Ces tailles sont calculées à un moment donné dans l'algorithme. Une variable distincte permettant de stocker la taille d’une chaîne terminée par un caractère nul peut être fournie. Ce qui rend la comparaison sur le gain de temps discutable. On a juste un NUL supplémentaire à la fin ... mais si la longueur encodée n'inclut pas ce NUL, il n'y a littéralement aucune différence entre les deux. Aucun changement algorithmique n'est requis du tout. Juste un pré-pass que vous devez concevoir manuellement vous-même au lieu d’un compilateur/exécutable le faire pour vous. C consiste surtout à faire les choses manuellement.

  3. Le préfixe de longueur étant optionnel est un argument de vente. Je n'ai pas toujours besoin de ces informations supplémentaires pour un algorithme, alors être obligé de le faire pour chaque chaîne rend mon temps de calcul et de calcul plus long que jamais en deçà de O (n). (Par exemple, le générateur de nombre aléatoire de matériel 1-128. Je peux extraire une "chaîne infinie". Supposons qu’il génère uniquement des caractères aussi rapidement. Ainsi, la longueur de notre chaîne change tout le temps. Mais mon utilisation des données ne me préoccupe probablement pas. beaucoup d’octets aléatoires que j’ai. Je veux juste le prochain octet inutilisé disponible dès qu’il peut l’obtenir après une requête. Je pourrais attendre sur le périphérique. Mais je pourrais aussi avoir un tampon de caractères pré-lu. Une comparaison de longueur est une perte de calcul inutile. Un contrôle nul est plus efficace.)

  4. Le préfixe de longueur est-il une bonne protection contre le débordement de mémoire tampon? Il en va de même pour une utilisation rationnelle des fonctions de la bibliothèque et de son implémentation. Et si je transmets des données malformées? Mon tampon est long de 2 octets mais je dis à la fonction que c'est 7! Ex: Si gets () était destiné à être utilisé sur des données connues, il aurait pu faire l'objet d'une vérification interne du tampon qui testait les tampons compilés et malloc ( ) appelle et suit toujours les spécifications. S'il était censé être utilisé comme canal pour que STDIN inconnu parvienne à un tampon inconnu, il est clair que l'on ne peut pas savoir quelle est la taille du tampon, ce qui signifie qu'un argument de longueur est inutile, vous avez besoin d'autre chose ici, comme une vérification canary. D'ailleurs, vous ne pouvez pas préfixer en longueur certains flux et entrées, vous ne pouvez simplement pas. Ce qui signifie que le contrôle de longueur doit être intégré à l'algorithme et non à une partie magique du système de frappe. TL; DR Les terminaisons NUL n'ont jamais eu à être dangereuses, elles ont finalement abouti de la sorte via une mauvaise utilisation.

  5. compteur de compteur: La terminaison NUL est gênante en binaire. Vous devez soit faire ici le préfixe de longueur, soit transformer les octets NUL: codes d'échappement, remappage de plage, etc., ce qui signifie bien sûr plus d'utilisation de la mémoire/informations réduites/plus d'opérations par octet. Le préfixe de longueur gagne la plupart du temps la guerre ici. Le seul avantage d'une transformation est qu'aucune fonction supplémentaire ne doit être écrite pour couvrir les chaînes de préfixe de longueur. Cela signifie que sur vos sous-routines plus optimisées, vous pouvez les faire agir automatiquement comme leurs équivalents O(n) sans ajouter de code supplémentaire. L’inconvénient est bien sûr le temps, la mémoire et la compression lorsqu’on l’utilise sur des chaînes lourdes NUL. Selon le volume de votre bibliothèque que vous dupliquez pour opérer sur des données binaires, il peut être judicieux de travailler uniquement avec des chaînes de préfixe de longueur. Cela dit, on pourrait également faire de même avec les chaînes de préfixe de longueur ... -1 peut signifier une terminaison NUL et vous pouvez utiliser des chaînes terminées par NUL à l'intérieur d'une terminaison de longueur.

  6. Concat: "O (n + m) vs O (m)" Je suppose que vous faites référence à m comme longueur totale de la chaîne après la concaténation car ils doivent tous les deux avoir ce nombre de opérations minimum (vous ne pouvez pas simplement coller à la chaîne 1, et si vous deviez réallouer?). Et je suppose que n est une quantité mythique d'opérations que vous n'avez plus à faire à cause d'un calcul préalable. Si c'est le cas, la réponse est simple: pré-calculer. Si vous insistez sur le fait que vous aurez toujours assez de mémoire pour ne pas avoir besoin de réallocer et que c'est la base de la notation big-O, la réponse est encore plus simple: effectuez une recherche binaire sur la mémoire allouée pour la fin de la chaîne 1, il existe clairement un grand échantillon de zéros infinis après la chaîne 1 pour que nous ne nous inquiétions pas de realloc. Là, j'ai facilement réussi à me connecter (n) et j'ai à peine essayé. Si vous vous souvenez bien, log (n) n’a en fait que 64 sur un ordinateur réel, ce qui revient essentiellement à dire O (64 + m), qui est essentiellement O (m). (Et oui, cette logique a été utilisée dans l'analyse au moment de l'exécution des structures de données réelles utilisées aujourd'hui. Ce ne sont pas des conneries de ma tête.)

  7. Concat ()/Len () encore: Mémoriser les résultats. Facile. Met tous les calculs en pré-calculs si possible/nécessaire. C'est une décision algorithmique. Ce n'est pas une contrainte imposée par le langage.

  8. Le passage de suffixe de chaîne est plus facile/possible avec la terminaison NUL. En fonction de la manière dont le préfixe de longueur est implémenté, il peut être destructif sur la chaîne d'origine et peut même parfois ne pas être possible. Demande une copie et passe O(n) au lieu de O (1).

  9. Le passage/déréférencement des arguments est inférieur pour le préfixe de longueur par rapport à NUL. Évidemment parce que vous transmettez moins d'informations. Si vous n'avez pas besoin de longueur, cela économise beaucoup de place et permet des optimisations.

  10. Tu peux tricher. C'est vraiment juste un pointeur. Qui a dit que vous deviez le lire comme une chaîne? Que faire si vous voulez le lire en tant que personnage unique ou en tant que float? Que faire si vous voulez faire le contraire et lire un float comme une chaîne? Si vous faites attention, vous pouvez le faire avec une terminaison NUL. Vous ne pouvez pas faire cela avec le préfixe de longueur, il s'agit généralement d'un type de données très différent d'un pointeur. Vous devrez probablement construire une chaîne octet par octet et obtenir la longueur. Bien sûr, si vous vouliez quelque chose comme un flottant entier (vous avez probablement un NUL à l'intérieur), vous devez lire octet par octet de toute façon, mais les détails vous sont laissés.

TL; DR Utilisez-vous des données binaires? Si non, alors la terminaison NUL permet plus de liberté algorithmique. Si oui, la quantité de code par rapport à la vitesse/mémoire/compression est votre principale préoccupation. Un mélange des deux approches ou mémoisation pourrait être préférable.

2
Black

De nombreuses décisions de conception entourant C découlent du fait que, lors de la mise en œuvre initiale, le passage de paramètres était un peu coûteux. Si vous avez le choix entre par exemple.

void add_element_to_next(arr, offset)
  char[] arr;
  int offset;
{
  arr[offset] += arr[offset+1];
}

char array[40];

void test()
{
  for (i=0; i<39; i++)
    add_element_to_next(array, i);
}

versus

void add_element_to_next(ptr)
  char *p;
{
  p[0]+=p[1];
}

char array[40];

void test()
{
  int i;
  for (i=0; i<39; i++)
    add_element_to_next(arr+i);
}

ce dernier aurait été légèrement moins cher (et donc préféré) car il ne fallait que passer d’un paramètre à deux. Si la méthode appelée n'a pas besoin de connaître l'adresse de base du tableau ni son index, passer un seul pointeur combinant les deux revient moins cher que de passer les valeurs séparément.

Bien qu'il existe de nombreuses manières raisonnables pour que C ait codé des longueurs de chaîne, les approches qui avaient été inventées jusque-là auraient toutes les fonctions requises qui devraient pouvoir fonctionner avec une partie de chaîne pour accepter l'adresse de base de la chaîne et l'index souhaité sous forme de deux paramètres distincts. L’utilisation de la terminaison zéro octet a permis d’éviter cette exigence. Bien que d’autres approches soient plus efficaces avec les machines actuelles (les compilateurs modernes transmettent souvent les paramètres dans des registres, et memcpy peut être optimisé de différentes façons strcpy () - les équivalents ne peuvent pas).

PS - En échange d’une légère pénalité de vitesse sur certaines opérations et d’un léger surcroît de charge pour les chaînes plus longues, il aurait été possible d’avoir des méthodes fonctionnant avec des chaînes acceptant les pointeurs directement sur les chaînes, bounds-selected buffers de chaînes ou structures de données identifiant les sous-chaînes d'une autre chaîne. Une fonction comme "strcat" aurait ressemblé à quelque chose comme [syntaxe moderne]

void strcat(unsigned char *dest, unsigned char *src)
{
  struct STRING_INFO d,s;
  str_size_t copy_length;

  get_string_info(&d, dest);
  get_string_info(&s, src);
  if (d.si_buff_size > d.si_length) // Destination is resizable buffer
  {
    copy_length = d.si_buff_size - d.si_length;
    if (s.src_length < copy_length)
      copy_length = s.src_length;
    memcpy(d.buff + d.si_length, s.buff, copy_length);
    d.si_length += copy_length;
    update_string_length(&d);
  }
}

Un peu plus gros que la méthode K & R strcat, mais il prend en charge la vérification des limites, contrairement à la méthode K & R. En outre, contrairement au procédé actuel, il serait possible de concaténer facilement une sous-chaîne arbitraire, par ex.

/* Concatenate 10th through 24th characters from src to dest */

void catpart(unsigned char *dest, unsigned char *src)
{
  struct SUBSTRING_INFO *inf;
  src = temp_substring(&inf, src, 10, 24);
  strcat(dest, src);
}

Notez que la durée de vie de la chaîne renvoyée par temp_substring serait limitée par celles de s et src, qui était toujours plus courte (raison pour laquelle la méthode nécessite le passage de inf in - si c'était local, il mourrait quand la méthode reviendrait).

En termes de coût de la mémoire, les chaînes et les tampons jusqu'à 64 octets auraient un octet de surcharge (identique aux chaînes terminées par un zéro); des chaînes plus longues en auraient un peu plus (qu'une compensation de temps système entre deux octets et le maximum requis constitue un compromis temps/espace). Une valeur spéciale de l'octet de longueur/mode serait utilisée pour indiquer qu'une structure contenant un octet d'indicateur, un pointeur et une longueur de mémoire tampon (pouvant être indexée arbitrairement dans une autre chaîne) a été attribuée à une fonction de chaîne.

Bien sûr, K & R n’a pas mis en œuvre ce genre de chose, mais c’est probablement parce qu’ils ne voulaient pas consacrer beaucoup d’efforts à la manipulation des cordes - un domaine où, de nos jours, de nombreuses langues semblent plutôt anémiques.

2
supercat