Qual é a lógica para seqüências terminadas nulas?

281

Por mais que eu goste de C e C ++, não posso deixar de coçar a cabeça com a escolha de cadeias terminadas nulas:

  • As cadeias de comprimento prefixadas (ie Pascal) existiam antes de C
  • As seqüências de caracteres com prefixo de comprimento tornam vários algoritmos mais rápidos, permitindo uma pesquisa constante de duração.
  • Seqüências de caracteres com prefixo de comprimento tornam mais difícil causar erros de saturação de buffer.
  • Mesmo em uma máquina de 32 bits, se você permitir que a string tenha o tamanho da memória disponível, uma string prefixada de comprimento será apenas três bytes mais larga que uma string terminada nula. Em máquinas de 16 bits, esse é um byte único. Em máquinas de 64 bits, 4 GB é um limite razoável de tamanho de string, mas mesmo que você queira expandi-lo para o tamanho da palavra-máquina, as máquinas de 64 bits geralmente têm memória suficiente, tornando os sete bytes extras como um argumento nulo. Eu sei que o padrão C original foi escrito para máquinas insanamente pobres (em termos de memória), mas o argumento da eficiência não me vende aqui.
  • Praticamente todas as outras linguagens (por exemplo, Perl, Pascal, Python, Java, C # etc.) usam seqüências de caracteres com prefixo de comprimento. Essas linguagens geralmente superam C em benchmarks de manipulação de strings porque são mais eficientes com strings.
  • O C ++ corrigiu isso um pouco com o std::basic_stringmodelo, mas matrizes de caracteres simples que esperam seqüências terminadas nulas ainda são difundidas. Isso também é imperfeito, pois requer alocação de heap.
  • Seqüências terminadas nulas precisam reservar um caractere (nulo), que não pode existir na seqüência, enquanto que as seqüências prefixadas de comprimento podem conter nulos incorporados.

Várias dessas coisas vieram à tona mais recentemente que C, portanto, faria sentido que C não as conhecesse. No entanto, vários foram bem antes de C surgir. Por que seqüências terminadas nulas foram escolhidas em vez do prefixo obviamente de comprimento superior?

EDIT : Como alguns pediram fatos (e não gostaram dos que eu já forneci) no meu ponto de eficiência acima, eles resultam de algumas coisas:

  • Concat usando cadeias terminadas nulas requer complexidade de tempo O (n + m). A prefixação de comprimento geralmente requer apenas O (m).
  • O comprimento usando cadeias terminadas nulas requer complexidade de tempo O (n). A prefixação do comprimento é O (1).
  • Length e concat são de longe as operações mais comuns de strings. Existem vários casos em que seqüências terminadas nulas podem ser mais eficientes, mas ocorrem com muito menos frequência.

Nas respostas abaixo, alguns casos em que seqüências terminadas nulas são mais eficientes:

  • Quando você precisar interromper o início de uma string e passar para algum método. Você não pode realmente fazer isso em tempo constante com o prefixo do comprimento, mesmo que tenha permissão para destruir a cadeia original, porque o prefixo do comprimento provavelmente precisa seguir as regras de alinhamento.
  • Em alguns casos em que você está repetindo a cadeia de caracteres caractere por caractere, poderá salvar um registro da CPU. Observe que isso funciona apenas no caso de você não ter alocado dinamicamente a string (porque você precisaria liberá-la, sendo necessário usar o registro da CPU que você salvou para manter o ponteiro que você recebeu originalmente de malloc e amigos).

Nenhuma das opções acima é quase tão comum quanto o comprimento e a concat.

Há mais uma afirmação nas respostas abaixo:

  • Você precisa cortar o final da corda

mas este está incorreto - é a mesma quantidade de tempo para cadeias terminadas com comprimento nulo e com prefixo. (Seqüências terminadas nulas apenas mantêm um nulo onde você deseja que o novo final esteja, os prefixos de comprimento apenas subtraem do prefixo.)

Billy ONeal
fonte
110
Eu sempre pensei que era um rito de passagem para todos os programadores de C ++ escrever sua própria biblioteca de strings.
Julieta
31
O que é isso sobre esperar explicações racionais agora. Suponho que você queira ouvir uma justificativa para x86 ou DOS a seguir? Para mim, a pior tecnologia vence. Toda vez. E a pior representação de cordas.
jalf
4
Por que você alega que as seqüências de prefixo de comprimento são superiores? Afinal, C se tornou popular porque usava cadeias terminadas em nulo, o que o diferenciava dos outros idiomas.
Daniel C. Sobral
44
@Daniel: C tornou-se popular porque é uma representação simples, eficiente e portátil de programas executáveis ​​em máquinas Von Neumann e porque foi usado no Unix. Certamente não é porque decidiu usar cadeias terminadas nulas. Se fosse uma boa decisão de design, as pessoas teriam copiado e não o fizeram. Eles certamente copiado praticamente tudo o resto a partir de C.
Billy ONeal
4
Concat é apenas O (m) com prefixo de comprimento se você destruir uma das cadeias. Caso contrário, a mesma velocidade. Os usos mais comuns de strings C (historicamente) eram impressão e digitalização. Em ambos, a terminação nula é mais rápida porque salva um registro.
Daniel C. Sobral

Respostas:

195

Da boca do cavalo

Nenhum dos BCPL, B ou C suporta fortemente os dados de caracteres no idioma; cada uma trata as seqüências de caracteres como vetores de números inteiros e complementa as regras gerais de algumas convenções. Tanto em BCPL quanto em B, um literal de cadeia de caracteres indica o endereço de uma área estática inicializada com os caracteres da cadeia de caracteres, empacotada em células. No BCPL, o primeiro byte compactado contém o número de caracteres na sequência; em B, não há contagem e as seqüências são terminadas por um caractere especial, que B soletrou *e. Essa alteração foi feita parcialmente para evitar a limitação no comprimento de uma sequência causada pela retenção da contagem em um slot de 8 ou 9 bits, e em parte porque manter a contagem parecia, em nossa experiência, menos conveniente do que usar um terminador.

Dennis M Ritchie, Desenvolvimento da Linguagem C

Hans Passant
fonte
12
Outra citação relevante: "... a semântica de cordas são totalmente subsumidos por regras mais gerais que regem todas as matrizes, e como resultado, a linguagem é mais simples para descrever ..."
AShelly
151

C não possui uma string como parte do idioma. Uma 'string' em C é apenas um ponteiro para char. Então talvez você esteja fazendo a pergunta errada.

"Qual é a lógica para deixar de fora um tipo de string" pode ser mais relevante. Para isso, gostaria de salientar que C não é uma linguagem orientada a objetos e possui apenas tipos de valores básicos. Uma string é um conceito de nível superior que precisa ser implementado de alguma forma combinando valores de outros tipos. C está em um nível mais baixo de abstração.

à luz da forte tempestade abaixo:

Eu só quero ressaltar que não estou tentando dizer que essa é uma pergunta estúpida ou ruim, ou que a maneira C de representar strings é a melhor escolha. Estou tentando esclarecer que a questão seria colocada de forma mais sucinta se você levar em conta o fato de que C não tem mecanismo para diferenciar uma string como um tipo de dados de uma matriz de bytes. Essa é a melhor escolha, considerando o poder de processamento e memória dos computadores atuais? Provavelmente não. Mas retrospectiva é sempre 20/20 e tudo o que :)

Robert S Ciaccio
fonte
29
char *temp = "foo bar";é uma declaração válida em C ... ei! isso não é uma string? não é nulo encerrado?
Yanick Rochon
56
@Yanick: essa é apenas uma maneira conveniente de dizer ao compilador para criar uma matriz de caracteres com um nulo no final. não é uma 'string'
Robert S Ciaccio
28
@calavera: Mas isso poderia significar simplesmente "Criar um buffer de memória com esse conteúdo de string e um prefixo de dois bytes de comprimento",
Billy ONeal
14
@ Billy: bem, uma vez que uma 'string' é realmente apenas um ponteiro para char, o que equivale a um ponteiro para byte, como você saberia que o buffer com o qual está lidando realmente pretende ser uma 'string'? você precisaria de um novo tipo diferente de char / byte * para denotar isso. talvez uma estrutura?
Robert S Ciaccio
27
Acho que @calavera está certo, C não tem um tipo de dados para strings. Ok, você pode considerar uma matriz de caracteres como uma string, mas isso não significa que é sempre uma string (para string, quero dizer uma sequência de caracteres com um significado definido). Um arquivo binário é uma matriz de caracteres, mas esses caracteres não significam nada para um ser humano.
BlackBear
106

A pergunta é feita como uma coisa Length Prefixed Strings (LPS)vs zero terminated strings (SZ), mas expõe principalmente os benefícios de seqüências de caracteres com prefixo de comprimento. Isso pode parecer esmagador, mas, para ser sincero, também devemos considerar os inconvenientes do LPS e as vantagens do SZ.

Pelo que entendi, a pergunta pode até ser entendida como uma maneira tendenciosa de perguntar "quais são as vantagens de Zero Terminated Strings?".

Vantagens (eu vejo) de Zero Terminated Strings:

  • muito simples, não há necessidade de introduzir novos conceitos na linguagem, os arrays / char pointers podem fazer.
  • a linguagem principal inclui apenas açúcar sintático mínimo para converter algo entre aspas duplas em um monte de caracteres (na verdade, um monte de bytes). Em alguns casos, pode ser usado para inicializar coisas completamente não relacionadas ao texto. Por exemplo, o formato de arquivo de imagem xpm é uma fonte C válida que contém dados de imagem codificados como uma sequência.
  • By the way, você pode colocar um zero em um literal de cadeia, o compilador só vai também adicionar um outro no final do literal: "this\0is\0valid\0C". É uma string? ou quatro cordas? Ou um monte de bytes ...
  • implementação simples, sem indireção oculta, sem número inteiro oculto.
  • nenhuma alocação de memória oculta envolvida (bem, algumas funções não padrão infames como strdup executam alocação, mas isso é principalmente uma fonte de problema).
  • nenhum problema específico para hardware pequeno ou grande (imagine o ônus de gerenciar o comprimento do prefixo de 32 bits em microcontroladores de 8 bits ou as restrições de limitar o tamanho da string a menos de 256 bytes, esse era um problema que eu realmente tive com o Turbo Pascal há eras).
  • A implementação da manipulação de strings é apenas um punhado de funções de biblioteca muito simples
  • eficiente para o uso principal de strings: texto constante lido sequencialmente desde um início conhecido (principalmente mensagens para o usuário).
  • o zero final não é obrigatório, todas as ferramentas necessárias para manipular caracteres como um monte de bytes estão disponíveis. Ao executar a inicialização da matriz em C, você pode até evitar o terminador NUL. Basta definir o tamanho certo.char a[3] = "foo";é C válido (não C ++) e não coloca um zero final em a.
  • coerente com o ponto de vista do unix "tudo é arquivo", incluindo "arquivos" que não têm comprimento intrínseco como stdin, stdout. Lembre-se de que as primitivas de leitura e gravação abertas são implementadas em um nível muito baixo. Não são chamadas de biblioteca, mas chamadas de sistema. E a mesma API é usada para arquivos binários ou de texto. As primitivas de leitura de arquivo obtêm um endereço de buffer e um tamanho e retornam o novo tamanho. E você pode usar strings como buffer para escrever. O uso de outro tipo de representação de cadeia implicaria que você não pode usar facilmente uma cadeia literal como buffer para gerar a saída, ou seria necessário que ela tivesse um comportamento muito estranho ao transmiti-la parachar* . Ou seja, não para retornar o endereço da string, mas para retornar os dados reais.
  • muito fácil de manipular dados de texto lidos de um arquivo no local, sem cópia inútil do buffer, basta inserir zeros nos lugares certos (bem, não com o C moderno, pois as strings de aspas duplas são matrizes de char const hoje em dia geralmente mantidas em dados não modificáveis segmento).
  • preceder alguns valores int de qualquer tamanho implica em problemas de alinhamento. O comprimento inicial deve ser alinhado, mas não há razão para fazer isso para os dados dos caracteres (e, novamente, forçar o alinhamento de cadeias implicaria problemas ao tratá-las como um monte de bytes).
  • O comprimento é conhecido no tempo de compilação por seqüências literais constantes (sizeof). Então, por que alguém iria querer armazená-lo na memória, acrescentando-o aos dados reais?
  • de uma maneira que C está fazendo como (quase) todo mundo, as strings são vistas como matrizes de caracteres. Como o comprimento da matriz não é gerenciado por C, o comprimento lógico também não é gerenciado para seqüências de caracteres. A única coisa surpreendente é que 0 item foi adicionado no final, mas isso é apenas no nível do idioma principal ao digitar uma string entre aspas duplas. Os usuários podem chamar perfeitamente as funções de manipulação de cadeias que ultrapassam o comprimento, ou até usar memcopy simples. SZ são apenas uma instalação. Na maioria dos outros idiomas, o comprimento da matriz é gerenciado, é lógico o mesmo para seqüências de caracteres.
  • de qualquer maneira, nos tempos modernos, os conjuntos de caracteres de 1 byte não são suficientes e você geralmente precisa lidar com cadeias unicode codificadas em que o número de caracteres é muito diferente do número de bytes. Isso implica que os usuários provavelmente vão querer mais do que "apenas o tamanho", mas também outras informações. Manter o comprimento não serve para nada (particularmente nenhum lugar natural para armazená-los) em relação a essas outras informações úteis.

Dito isto, não é necessário reclamar no caso raro em que as seqüências C padrão são realmente ineficientes. Libs estão disponíveis. Se eu segui essa tendência, devo reclamar que o C padrão não inclui nenhuma função de suporte a regex ... mas todo mundo sabe que não é um problema real, pois existem bibliotecas disponíveis para esse fim. Então, quando se deseja eficiência na manipulação de strings, por que não usar uma biblioteca como o bstring ? Ou mesmo cadeias de caracteres C ++?

EDIT : Recentemente, dei uma olhada em D strings . É interessante ver que a solução escolhida não é um prefixo de tamanho, nem uma terminação zero. Como em C, cadeias literais entre aspas duplas são apenas uma mão abreviada para matrizes de caracteres imutáveis, e o idioma também possui uma palavra-chave string que significa isso (matriz de caracteres imutável).

Mas matrizes D são muito mais ricas que matrizes C. No caso de matrizes estáticas, o comprimento é conhecido em tempo de execução, portanto, não há necessidade de armazenar o comprimento. O compilador possui em tempo de compilação. No caso de matrizes dinâmicas, o comprimento está disponível, mas a documentação D não indica onde é mantida. Pelo que sabemos, o compilador pode optar por mantê-lo em algum registro ou em alguma variável armazenada longe dos dados dos caracteres.

Em matrizes char normais ou seqüências de caracteres não literais, não há zero final; portanto, o programador deve se colocar se quiser chamar alguma função C de D. No caso particular de seqüências de caracteres literais, no entanto, o compilador D ainda coloca zero no final de cada sequência (para permitir a conversão fácil de sequências C para facilitar a chamada da função C?), mas esse zero não faz parte da sequência (D não conta no tamanho da sequência).

A única coisa que me decepcionou um pouco é que as strings deveriam ser utf-8, mas o comprimento aparentemente ainda retorna um número de bytes (pelo menos é verdade no meu compilador gdc), mesmo ao usar caracteres de vários bytes. Não está claro para mim se é um bug do compilador ou por objetivo. (OK, eu provavelmente descobri o que aconteceu. Para dizer ao compilador D que sua fonte usa utf-8, você deve colocar alguma ordem estúpida de ordem de bytes no começo. Eu escrevo estúpido porque sei que nenhum editor está fazendo isso, especialmente para UTF- 8 que deve ser compatível com ASCII).

kriss
fonte
7
... Continua ... Acho que vários de seus pontos estão completamente errados, ou seja, o argumento "tudo é um arquivo". Os arquivos são de acesso seqüencial, as seqüências de caracteres C não. A prefixação de comprimento também pode ser feita com o mínimo de açúcar sintático. O único argumento razoável aqui é a tentativa de gerenciar prefixos de 32 bits em hardware pequeno (ou seja, 8 bits); Eu acho que isso poderia ser simplesmente resolvido dizendo que o tamanho do comprimento é determinado pela implementação. Afinal, é o que std::basic_stringfaz.
Billy ONeal
3
@ Billy ONeal: realmente existem duas partes diferentes na minha resposta. Uma é sobre o que faz parte da 'linguagem C principal', a outra é sobre o que as bibliotecas padrão devem oferecer. Em relação ao suporte a cadeias, há apenas um item no idioma principal: o significado de um conjunto de bytes entre aspas duplas. Eu não sou realmente mais feliz do que você com o comportamento C. Sinto-me acrescentando magicamente que zero no final de cada grupo de bytes fechados e fechados é ruim o suficiente. Eu preferiria e explicito \0no final, quando os programadores quiserem, em vez do implícito. Anexar o comprimento é muito pior.
kriss
2
@ Billy ONeal: isso não é verdade, os usos se preocupam com o que é essencial e o que são as bibliotecas. O ponto mais importante é quando C é usado para implementar o SO. Nesse nível, nenhuma biblioteca está disponível. C também é freqüentemente usado em contextos incorporados ou para dispositivos de programação em que você geralmente tem o mesmo tipo de restrições. Em muitos casos Joes de provavelmente não deve usar C em tudo nowaday: "OK, você quer que ele no console Você tem um console Não Too bad ...???"
kriss
5
@ Billy "Bem, para 0,01% dos programadores C implementando sistemas operacionais, tudo bem." Os outros programadores podem fazer uma caminhada. C foi criado para escrever um sistema operacional.
Daniel C. Sobral
5
Por quê? Porque diz que é uma linguagem de propósito geral? Diz o que as pessoas que o escreveram estavam fazendo quando o criaram? Para que foi usada nos primeiros anos de vida? Então, o que diz que não concorda comigo? É uma linguagem de propósito geral criada para escrever um sistema operacional . Isso nega?
Daniel C. Sobral
61

Eu acho que tem razões históricas e encontrou isso na wikipedia :

No momento em que C (e os idiomas de origem) foram desenvolvidos, a memória era extremamente limitada; portanto, era atraente usar apenas um byte de sobrecarga para armazenar o comprimento de uma string. A única alternativa popular na época, geralmente chamada de "string Pascal" (embora também fosse usada pelas versões anteriores do BASIC), usava um byte à esquerda para armazenar o comprimento da string. Isso permite que a string contenha NUL e a localização do comprimento precisa de apenas um acesso à memória (tempo O (1) (constante)). Mas um byte limita o comprimento a 255. Essa limitação de comprimento era muito mais restritiva do que os problemas com a cadeia C, de modo que a cadeia C em geral venceu.

khachik
fonte
2
@muntoo Hmm ... compatibilidade?
Khachik
19
@muntoo: Porque isso quebraria quantidades monumentais de códigos C e C ++ existentes.
Billy ONeal
10
@muntoo: Os paradigmas vão e vêm, mas o código legado é para sempre. Qualquer versão futura do C teria que continuar a suportar cadeias terminadas em 0, caso contrário, mais de 30 anos de código legado teriam que ser reescritos (o que não vai acontecer). E enquanto a maneira antiga estiver disponível, é com isso que as pessoas continuarão a usar, pois é com isso que elas estão familiarizadas.
John Bode
8
@muntoo: Acredite, às vezes eu gostaria de poder. Mas eu ainda prefiro strings com terminação 0 em relação a Pascal.
John Bode
2
Fale sobre legado ... As strings C ++ agora são obrigadas a serem finalizadas por NUL.
Jim Balter
32

Calavera está certa , mas como as pessoas parecem não entender o que quer dizer, fornecerei alguns exemplos de código.

Primeiro, vamos considerar o que C é: uma linguagem simples, onde todo o código tem uma tradução bastante direta para a linguagem de máquina. Todos os tipos se encaixam nos registradores e na pilha, e não requer um sistema operacional ou uma grande biblioteca de tempo de execução para ser executada, pois foi criado para escrever essas coisas (uma tarefa para a qual é extremamente adequada, considerando nem sequer é um provável concorrente até hoje).

Se C tivesse um stringtipo, como intou char, seria um tipo que não se encaixasse em um registro ou na pilha e exigiria que a alocação de memória (com toda a sua infraestrutura de suporte) fosse manipulada de qualquer maneira. Todos os quais vão contra os princípios básicos de C.

Portanto, uma string em C é:

char s*;

Então, vamos supor que esse prefixo tenha comprimento. Vamos escrever o código para concatenar duas strings:

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;
}

Outra alternativa seria usar uma struct para definir uma string:

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

Nesse ponto, toda manipulação de strings exigiria duas alocações, o que, na prática, significa que você passaria por uma biblioteca para lidar com isso.

O engraçado é ... estruturas como essa fazem existir em C! Eles não são usados ​​apenas para o seu dia-a-dia, exibindo mensagens para a manipulação do usuário.

Então, aqui está o argumento de Calavera: não há tipo de string em C . Para fazer qualquer coisa com isso, você teria que pegar um ponteiro e decodificá-lo como ponteiro para dois tipos diferentes, e então se torna muito relevante qual é o tamanho de uma string e não pode ser deixado como "implementação definida".

Agora, C pode manipular a memória de qualquer maneira, e as memfunções da biblioteca ( <string.h>até mesmo!) Fornecem todas as ferramentas necessárias para lidar com a memória como um par de ponteiro e tamanho. As chamadas "strings" em C foram criadas com apenas uma finalidade: mostrar mensagens no contexto de gravação de um sistema operacional destinado a terminais de texto. E, para isso, a rescisão nula é suficiente.

Daniel C. Sobral
fonte
2
1. +1. 2. Obviamente, se o comportamento padrão do idioma tivesse sido feito usando prefixos de comprimento, haveria outras coisas para facilitar isso. Por exemplo, todos os seus elencos teriam sido ocultados por chamadas strlene amigos. Quanto ao problema de "deixar para a implementação", você pode dizer que o prefixo é o que shortestiver na caixa de destino. Então todo o seu elenco ainda funcionaria. 3. Posso criar cenários inventados o dia inteiro que fazem um ou outro sistema parecer ruim.
Billy ONeal
5
@ Billy A coisa da biblioteca é verdadeira o suficiente, além do fato de que o C foi projetado para uso mínimo ou nenhum uso da biblioteca. O uso de protótipos, por exemplo, não era comum desde o início. Dizer que o prefixo shortlimita efetivamente o tamanho da string, o que parece ser uma coisa em que eles não estavam interessados. Eu mesmo, tendo trabalhado com seqüências BASIC e Pascal de 8 bits, seqüências COBOL de tamanho fixo e coisas semelhantes, tornou-se um grande fã de sequências C de tamanho ilimitado rapidamente. Atualmente, um tamanho de 32 bits manipula qualquer string prática, mas a adição desses bytes no início era problemática.
Daniel C. Sobral
1
@ Billy: Primeiro, obrigado Daniel ... você parece entender o que estou falando. Segundo, Billy, acho que você ainda não entendeu o que está sendo discutido aqui. Eu, pelo menos não estou discutindo os prós e os contras de prefixar os tipos de dados da string com seu comprimento. O que estou dizendo, eo que Daniel muito claramente enfatizado, é que não foi uma decisão tomada na implementação de C para não lidar com esse argumento em tudo . As strings não existem no que diz respeito à linguagem básica. A decisão sobre como lidar com seqüências de caracteres é deixada para o programador ... e a terminação nula se tornou popular.
Robert S Ciaccio
1
+1 por mim. Mais uma coisa que eu gostaria de acrescentar; uma estrutura, como você propõe, perde um passo importante em direção a um stringtipo real : não está ciente dos caracteres. É uma matriz de "char" (um "char" na linguagem da máquina é tanto um personagem quanto uma "palavra" é o que os humanos chamariam de palavra em uma frase). Uma cadeia de caracteres é um conceito de nível superior que pode ser implementado no topo de uma matriz charse você introduziu a noção de codificação.
Frerich Raabe
2
@ DanielC.Sobral: Além disso, a estrutura que você mencionou não exigiria duas alocações. Use-o como você o tem na pilha (portanto, bufrequer apenas uma alocação) ou use struct string {int len; char buf[]};e aloque tudo com uma alocação como membro flexível da matriz e passe-o como a string*. (Ou struct string {int capacity; int len; char buf[]};indiscutivelmente , por razões óbvias de desempenho) #
Mooing Duck
20

Obviamente, para desempenho e segurança, você desejará manter o comprimento de uma corda enquanto estiver trabalhando com ela, em vez de executar repetidamente strlenou o equivalente nela. No entanto, armazenar o comprimento em um local fixo imediatamente antes do conteúdo da string é um design incrivelmente ruim. Como Jörgen apontou nos comentários sobre a resposta de Sanjit, ele impede o tratamento da cauda de uma string como uma string, o que, por exemplo, torna muitas operações comuns semelhantes path_to_filenameou filename_to_extensionimpossíveis sem alocar nova memória (e incorrer na possibilidade de falha e manipulação de erros) . E, claro, há a questão de que ninguém pode concordar com quantos bytes o campo de comprimento da string deve ocupar (várias "Pascal string" ruins

O design de C de deixar o programador escolher se / onde / como armazenar o comprimento é muito mais flexível e poderoso. Mas é claro que o programador precisa ser inteligente. O C castiga a estupidez com programas que travam, paralisam ou dão raiz aos inimigos.

R .. GitHub PARE DE AJUDAR O GELO
fonte
+1. Seria bom ter um local padrão para armazenar o comprimento, para que aqueles que desejam algo como prefixação do comprimento não precisem escrever toneladas de "código de cola" em todos os lugares.
quer
2
Não existe um local padrão possível em relação aos dados da string, mas é claro que você pode usar uma variável local separada (recalculando-a em vez de passá-la quando a última não for conveniente e a primeira não for muito dispendiosa) ou uma estrutura com um ponteiro para a string (e melhor ainda, uma flag indicando se a estrutura "possui" o ponteiro para fins de alocação ou se é uma referência a uma string pertencente a outro lugar. E, é claro, você pode incluir um membro flexível da matriz na estrutura para a flexibilidade de alocar a corda com a estrutura quando lhe convier.
R .. GitHub parar de ajudar ICE
13

Preguiça, frugalidade e portabilidade de registro, considerando o intestino de qualquer idioma, especialmente C, que é um passo acima do assembly (herdando assim muito código legado do assembly). Você concorda que um caractere nulo seria inútil naqueles dias ASCII (e provavelmente tão bom quanto um caractere de controle EOF).

vamos ver no pseudo código

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 de 1 uso de registro

caso 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 de 2 registros usados

Isso pode parecer míope naquele momento, mas considerando a frugalidade no código e no registro (que eram PREMIUM naquele momento, no momento em que você sabe, eles usam cartão perfurado). Sendo, portanto, mais rápido (quando a velocidade do processador podia ser contada em kHz), esse "Hack" era muito bom e portátil para registrar um processador sem facilidade com facilidade.

Por uma questão de argumento, implementarei 2 operações de string comuns

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

complexidade O (n) onde, na maioria dos casos, a string PASCAL é O (1) porque o comprimento da string é pré-pendente da estrutura da string (isso também significa que essa operação precisaria ser realizada em um estágio anterior).

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

complexidade O (n) e preceder o comprimento da string não mudariam a complexidade da operação, enquanto admito que levaria três vezes menos tempo.

Por outro lado, se você usar a string PASCAL, teria que redesenhar sua API para levar em consideração o tamanho do registro e a disponibilidade de bits, a string PASCAL terá a conhecida limitação de 255 caracteres (0xFF), porque o comprimento foi armazenado em 1 byte (8 bits). ), e se você quisesse uma string mais longa (16bits-> qualquer coisa), teria que levar em conta a arquitetura em uma camada do seu código, o que significaria, na maioria dos casos, APIs de string incompatíveis se você quisesse uma string mais longa.

Exemplo:

Um arquivo foi gravado com sua API de cadeia de caracteres anexada em um computador de 8 bits e, em seguida, teria que ser lido em um computador de 32 bits, o que o programa lento faria considerando que seus 4 bytes são o comprimento da cadeia de caracteres e depois alocam muita memória tente ler quantos bytes. Outro caso seria a leitura da string de 32 bytes do PPC (little endian) em um x86 (big endian), é claro que se você não souber que um é escrito pelo outro, haverá problemas. O comprimento de 1 byte (0x00000001) se tornaria 16777216 (0x0100000) com 16 MB para a leitura de uma string de 1 byte. É claro que você diria que as pessoas devem concordar com um padrão, mas mesmo o unicode de 16 bits tem pouca e grande utilidade.

É claro que C também teria seus problemas, mas seria muito pouco afetado pelos problemas levantados aqui.

dvhh
fonte
2
@deemoowoor: Concat: O(m+n)com seqüências de caracteres nulos, O(n)típicas em qualquer outro lugar. Comprimento O(n)com cadeias de caracteres nulos, em O(1)qualquer outro lugar. Join: O(n^2)com strings nullterm, em O(n)qualquer outro lugar. Existem alguns casos em que seqüências terminadas nulas são mais eficientes (por exemplo, basta adicionar uma à maiúscula e minúscula), mas concat e length são de longe as operações mais comuns (é necessário pelo menos comprimento para formatação, saída de arquivo, exibição do console etc.) . Se você armazena em cache o comprimento para amortizar o valor, O(n)você apenas afirmou que o comprimento deve ser armazenado com a string.
Billy ONeal
1
Concordo que no código de hoje esse tipo de string é ineficiente e propenso a erros, mas, por exemplo, a exibição do console realmente não precisa saber o comprimento da string para exibi-la com eficiência, a saída do arquivo realmente não precisa saber sobre a string length (apenas alocando cluster em movimento), e a formatação de strings no momento era feita em um comprimento fixo de strings na maioria dos casos. De qualquer forma, você deve estar escrevendo código incorreto se concat em C tiver uma complexidade O (n ^ 2), tenho certeza de que posso escrever um com complexidade O (n) #
dvhh
1
@dvhh: eu não disse n ^ 2 - eu disse m + n - ainda é linear, mas você precisa procurar o final da string original para fazer a concatenação, enquanto que com um prefixo de comprimento não procura É necessário. (Isto é realmente apenas uma outra consequência de comprimento que exige tempo linear)
Billy ONeal
1
@ Billy ONeal: por mera curiosidade, fiz um grep no meu projeto C atual (cerca de 50000 linhas de código) para chamadas de função de manipulação de string. strlen 101, strcpy e variantes (strncpy, strlcpy): 85 (Eu também tenho várias centenas de strings literais usadas para mensagens, cópias implícitas), strcmp: 56, strcat: 13 (e 6 são concatenações para uma string de comprimento zero para chamar strncat) . Concordo que um comprimento prefixado acelerará as chamadas para strlen, mas não para strcpy ou strcmp (talvez se a API strcmp não usar prefixo comum). A coisa mais interessante sobre os comentários acima é que o strcat é muito raro.
kriss
1
@ supercat: na verdade não, olhe para algumas implementações. Seqüências curtas estão usando um buffer baseado em pilha curta (sem alocação de heap) e só usam heap quando ficarem maiores. Mas fique à vontade para fornecer uma implementação real da sua ideia como uma biblioteca. Geralmente, os problemas aparecem apenas quando chegamos aos detalhes, não no design geral.
kriss
9

De muitas maneiras, C era primitivo. E eu adorei.

Foi um passo acima da linguagem assembly, oferecendo quase o mesmo desempenho com uma linguagem muito mais fácil de escrever e manter.

O terminador nulo é simples e não requer suporte especial do idioma.

Olhando para trás, não parece tão conveniente. Mas eu usei a linguagem assembly nos anos 80 e parecia muito conveniente na época. Só acho que o software está em constante evolução e as plataformas e ferramentas se tornam cada vez mais sofisticadas.

Jonathan Wood
fonte
Não vejo o que há de mais primitivo em seqüências terminadas nulas do que qualquer outra coisa. Pascal é anterior a C e usa prefixo de comprimento. Claro, limitava-se a 256 caracteres por string, mas o simples uso de um campo de 16 bits teria resolvido o problema na grande maioria dos casos.
quer
O fato de limitar o número de caracteres é exatamente o tipo de problema que você precisa pensar ao fazer algo assim. Sim, você pode prolongá-lo, mas naquela época os bytes importavam. E um campo de 16 bits será longo o suficiente para todos os casos? Vamos lá, você deve admitir que uma terminação nula é conceitualmente primitiva.
Jonathan Wood
10
Você limita o comprimento da string ou o conteúdo (sem caracteres nulos) ou aceita a sobrecarga extra de uma contagem de 4 a 8 bytes. Não há almoço grátis. No momento do início, a cadeia terminada nula fazia todo o sentido. Às vezes, na montagem eu usava o bit superior de um caractere para marcar o final de uma string, economizando ainda mais um byte!
Mark Ransom
Exatamente, Mark: Não há almoço grátis. É sempre um compromisso. Hoje em dia, não precisamos fazer o mesmo tipo de compromisso. Mas naquela época, essa abordagem parecia tão boa quanto qualquer outra.
Jonathan Wood
8

Supondo por um momento que C implementou seqüências de caracteres da maneira Pascal, prefixando-as por comprimento: uma string de 7 caracteres é o mesmo tipo de dados que uma string de 3 caracteres? Se a resposta for sim, que tipo de código o compilador deve gerar quando atribuo o primeiro ao último? A sequência deve ser truncada ou redimensionada automaticamente? Se redimensionada, essa operação deve ser protegida por uma trava para garantir a segurança da rosca? O lado da abordagem C abordou todos esses problemas, gostemos ou não :)

Cristian
fonte
2
Err .. não, não. A abordagem C não permite atribuir a string de 7 caracteres à string de 3 caracteres.
quer
@ Billy ONeal: por que não? Pelo que entendi nesse caso, todas as strings são do mesmo tipo de dados (char *), portanto, o comprimento não importa. Ao contrário de Pascal. Mas isso era uma limitação de Pascal, em vez de um problema com cadeias de caracteres com prefixo de comprimento.
Oliver Mason
4
@ Billy: Eu acho que você acabou de reafirmar o ponto de vista de Cristian. C lida com essas questões não lidando com elas. Você ainda está pensando em termos de C na verdade contendo uma noção de uma string. É apenas um ponteiro, para que você possa atribuí-lo ao que quiser.
Robert S Ciaccio
2
É como ** a matriz: "não há string".
Robert S Ciaccio
1
@calavera: Não vejo como isso prova alguma coisa. Você pode resolver da mesma maneira com o prefixo do comprimento ... ou seja, não permita a atribuição.
Billy ONeal
8

De alguma forma, entendi que a pergunta implica que não há suporte do compilador para cadeias de caracteres com prefixo de comprimento em C. O exemplo a seguir mostra, pelo menos, você pode iniciar sua própria biblioteca de cadeias C, em que os comprimentos de cadeias são contados no tempo de compilação, com uma construção como esta:

#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;
}

No entanto, isso não ocorre sem problemas, pois você precisa ter cuidado ao liberar especificamente esse ponteiro de seqüência de caracteres e quando ele está alocado estaticamente ( charmatriz literal ).

Edit: Como uma resposta mais direta à pergunta, minha opinião é que este era o modo como C poderia suportar ambos, tendo o comprimento da string disponível (como uma constante de tempo de compilação), caso seja necessário, mas ainda sem sobrecarga de memória, se você quiser usar apenas ponteiros e terminação zero.

É claro que parece que trabalhar com seqüências terminadas em zero era a prática recomendada, uma vez que a biblioteca padrão em geral não usa comprimentos de sequência como argumentos, e como extrair o comprimento não é um código tão simples como char * s = "abc"mostra meu exemplo.

Pyry Jahkola
fonte
O problema é que as bibliotecas não sabem a existência de sua estrutura e ainda lidam com coisas como nulos incorporados incorretamente. Além disso, isso realmente não responde à pergunta que fiz.
Billy ONeal
1
Isso é verdade. Portanto, o maior problema é que não há melhor maneira padrão de fornecer interfaces com parâmetros de string do que simples strings com terminação zero. Eu ainda diria que existem bibliotecas que suportam a alimentação de pares de comprimento de ponteiro (bem, pelo menos você pode construir uma C ++ std :: string com eles).
Pyry Jahkola 12/12
2
Mesmo se você armazenar um comprimento, nunca deverá permitir cadeias com nulos incorporados. Este é o senso comum básico. Se seus dados tiverem nulos, você nunca deve usá-los com funções que esperam seqüências de caracteres.
R .. GitHub Pare de ajudar o gelo
1
@ supercat: Do ponto de vista da segurança, eu gostaria com essa redundância. Caso contrário, programadores ignorantes (ou privados de sono) acabam concatenando dados binários e seqüências de caracteres e passando-os para coisas que esperam seqüências [terminadas em nulo] ...
R .. GitHub parar de ajudar ICE
1
@R ..: Embora os métodos que esperam seqüências terminadas em nulo geralmente esperem a char*, muitos métodos que não esperam a terminação nula também esperam a char*. Um benefício mais significativo da separação dos tipos se relacionaria ao comportamento Unicode. Pode valer a pena uma implementação de string manter sinalizadores para saber se strings contêm certos tipos de caracteres ou se não os contêm [por exemplo, encontrar o ponto de código 999.990 em uma string de milhões de caracteres que não contém qualquer caractere além do plano multilíngue básico terá ordens de magnitude mais rápidas ... #
686
6

"Mesmo em uma máquina de 32 bits, se você permitir que a string tenha o tamanho da memória disponível, uma string prefixada de comprimento será apenas três bytes mais larga que uma string terminada nula."

Primeiro, 3 bytes extras podem ser uma sobrecarga considerável para cadeias curtas. Em particular, uma cadeia de comprimento zero agora leva 4 vezes mais memória. Alguns de nós estão usando máquinas de 64 bits, então precisamos de 8 bytes para armazenar uma sequência de tamanho zero ou o formato da sequência não pode lidar com as sequências mais longas suportadas pela plataforma.

Também pode haver problemas de alinhamento. Suponha que eu tenha um bloco de memória contendo 7 strings, como "solo \ 0segundo \ 0 \ 0four \ 0five \ 0 \ 0seventh". A segunda seqüência começa no deslocamento 5. O hardware pode exigir que números inteiros de 32 bits sejam alinhados em um endereço múltiplo de 4, portanto, você precisa adicionar preenchimento, aumentando ainda mais a sobrecarga. A representação C é muito eficiente em termos de memória em comparação. (A eficiência da memória é boa; ajuda a armazenar em cache o desempenho, por exemplo.)

Brangdon
fonte
Acredito que resolvi tudo isso na questão. Sim, nas plataformas x64, um prefixo de 32 bits não pode caber em todas as sequências possíveis. Por outro lado, você nunca deseja uma string tão grande quanto uma string terminada nula, porque para fazer qualquer coisa, você precisa examinar todos os 4 bilhões de bytes para encontrar o fim de quase todas as operações que deseja fazer. Além disso, não estou dizendo que seqüências terminadas nulas são sempre ruins - se você estiver construindo uma dessas estruturas de blocos e seu aplicativo específico for acelerado por esse tipo de construção, vá em frente. Eu só queria que o comportamento padrão da linguagem não fizesse isso.
Billy ONeal
2
Eu citei essa parte da sua pergunta porque, na minha opinião, subestimava a questão da eficiência. Dobrar ou quadruplicar os requisitos de memória (em 16 e 32 bits, respectivamente) pode ser um grande custo de desempenho. Seqüências longas podem ser lentas, mas pelo menos são suportadas e ainda funcionam. Meu outro ponto, sobre alinhamento, você não menciona nada.
Brangdon
O alinhamento pode ser resolvido especificando que os valores além de UCHAR_MAX devem se comportar como se fossem compactados e descompactados usando acessos de bytes e deslocamento de bits. Um tipo de string adequadamente projetado pode oferecer eficiência de armazenamento essencialmente comparável a strings com terminação zero, além de permitir a verificação de limites nos buffers sem sobrecarga adicional de memória (use um bit no prefixo para dizer se um buffer está "cheio"; se não é e o último byte é diferente de zero, esse byte representaria o espaço restante.Se o buffer não estiver cheio e o último byte for zero, os últimos 256 bytes não serão utilizados, por isso ...
supercat
... pode-se armazenar dentro desse espaço o número exato de bytes não utilizados, com custo adicional de memória zero). O custo de trabalhar com os prefixos seria compensado pela capacidade de usar métodos como fgets () sem precisar ultrapassar o comprimento da string (uma vez que os buffers saberiam o tamanho deles).
Supercat
4

A terminação nula permite operações rápidas baseadas em ponteiro.

Sanjit Saluja
fonte
5
Hã? Quais "operações rápidas do ponteiro" não funcionam com o prefixo de comprimento? Mais importante, outras linguagens que usam prefixo de comprimento são mais rápidas que a manipulação de strings C wrt.
Billy ONeal
12
@billy: Com strings com prefixo de comprimento, você não pode simplesmente pegar um ponteiro de string e adicionar 4 a ele, e esperar que ainda seja uma string válida, porque não possui um prefixo de comprimento (não é válido de qualquer maneira).
Jörgen Sigvardsson
3
@j_random_hacker: A concatenação é muito pior para seqüências de caracteres asciiz (O (m + n) em vez de potencialmente O (n)), e concat é muito mais comum do que qualquer outra operação listada aqui.
Billy ONeal
3
há uma operação pouco tiiny que se torna mais caro, com cordas de terminação nula: strlen. Eu diria que isso é uma desvantagem.
jalf
10
@ Billy ONeal: todo mundo também suporta regex. E daí ? Use bibliotecas para as quais elas foram criadas. C é sobre eficiência máxima e minimalismo, não as baterias incluídas. As ferramentas C também permitem implementar strings prefixadas em comprimento usando estruturas com muita facilidade. E nada o proíbe de implementar os programas de manipulação de cadeias através do gerenciamento de seus próprios buffers de comprimento e de caracteres. Isso é geralmente o que faço quando quero eficiência e uso C, não chamar um punhado de funções que esperam zero no final de um buffer de char não é um problema.
kriss
4

Um ponto ainda não mencionado: quando C foi projetado, havia muitas máquinas em que um 'char' não era de oito bits (ainda hoje existem plataformas DSP onde não é). Se alguém decidir que as strings devem ser prefixadas em comprimento, quantos prefixos de comprimento de caracteres 'char' devem ser usados? O uso de dois imporia um limite artificial no comprimento da seqüência de caracteres para máquinas com caracteres de 8 bits e espaço de endereçamento de 32 bits, enquanto desperdiçaria espaço em máquinas com caracteres de 16 bits e espaço de endereçamento de 16 bits.

Se alguém quiser permitir que seqüências de tamanho arbitrário sejam armazenadas com eficiência, e se 'char' sempre tiver 8 bits, poderá - por alguma despesa em velocidade e tamanho do código - definir um esquema, se uma sequência for prefixada por um número par N teria N / 2 bytes, uma string prefixada por um valor ímpar N e um valor par M (leitura reversa) poderia ser ((N-1) + M * char_max) / 2, etc., e exigir que qualquer buffer que reivindicações para oferecer uma certa quantidade de espaço para armazenar uma seqüência de caracteres devem permitir bytes suficientes antes desse espaço para lidar com o comprimento máximo. O fato de 'char' nem sempre ser 8 bits, no entanto, complicaria esse esquema, pois o número de 'char' necessário para manter o comprimento de uma string variaria dependendo da arquitetura da CPU.

supercat
fonte
O prefixo pode ser facilmente do tamanho definido pela implementação, exatamente como é sizeof(char).
Billy ONeal
@ BillyONeal: sizeof(char)é um. Sempre. Pode-se ter o prefixo com um tamanho definido pela implementação, mas seria estranho. Além disso, não há como saber qual deve ser o tamanho "certo". Se alguém estiver mantendo muitas seqüências de caracteres de 4 caracteres, o preenchimento com zero imporá 25% de sobrecarga, enquanto um prefixo de quatro bytes imporá 100% de sobrecarga. Além disso, o tempo gasto na compactação e descompactação de prefixos de comprimento de quatro bytes pode exceder o custo da verificação de cadeias de caracteres de 4 bytes em busca de zero byte.
Supercat
1
Ah sim. Você está certo. O prefixo poderia facilmente ser algo diferente de char. Tudo o que daria certo aos requisitos de alinhamento na plataforma de destino seria bom. Eu não vou lá - eu já argumentei isso até a morte.
Billy ONeal
Supondo que as strings tivessem prefixo de comprimento, provavelmente a melhor coisa a se fazer seria um size_tprefixo (desperdício de memória, seria o mais sensato - permitir strings de qualquer comprimento possível que pudesse caber na memória). Na verdade, isso é tipo de que D faz; matrizes são struct { size_t length; T* ptr; }e strings são apenas matrizes de immutable(char).
Tim Čas
@ TimČas: A menos que seja necessário alinhar as strings com palavras, o custo do trabalho com strings curtas seria dominado em muitas plataformas pelo requisito de empacotar e descompactar o comprimento; Realmente não vejo isso como prático. Se alguém quiser que as strings sejam arrays de bytes de tamanho arbitrário e independentes de conteúdo, acho que seria melhor manter o comprimento separado do ponteiro para os dados dos caracteres e ter uma linguagem que permita que ambas as informações sejam obtidas para strings literais .
308 supercat On
2

Muitas decisões de design que envolvem C decorrem do fato de que, quando foi originalmente implementado, a passagem de parâmetros era um pouco cara. Dada a escolha entre, por exemplo

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);
}

o último teria sido um pouco mais barato (e, portanto, preferido), pois exigia apenas a passagem de um parâmetro em vez de dois. Se o método chamado não precisasse conhecer o endereço base da matriz nem o índice nela, passar um ponteiro único combinando os dois seria mais barato do que passar os valores separadamente.

Embora existam muitas maneiras razoáveis ​​pelas quais C poderia ter codificado comprimentos de string, as abordagens que foram inventadas até então teriam todas as funções necessárias que deveriam poder trabalhar com parte de uma string para aceitar o endereço base da string e o índice desejado como dois parâmetros separados. O uso da terminação de byte zero tornou possível evitar esse requisito. Embora outras abordagens sejam melhores com as máquinas atuais (os compiladores modernos geralmente passam parâmetros nos registradores e o memcpy pode ser otimizado de maneira que strcpy () - equivalentes não podem)) o código de produção suficiente usa seqüências terminadas de zero byte que é difícil mudar para qualquer outra coisa.

PS - Em troca de uma leve penalidade de velocidade em algumas operações e um pouco de sobrecarga extra em seqüências de caracteres mais longas, seria possível que métodos que trabalhem com sequências de caracteres aceitem ponteiros diretamente para sequências de caracteres, buffers de verificação de limites ou estruturas de dados identificando substrings de outra string. Uma função como "strcat" teria algo parecido com [sintaxe moderna]

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);
  }
}

Um pouco maior que o método K&R strcat, mas suportaria verificação de limites, o que o método K&R não. Além disso, ao contrário do método atual, seria possível concatenar facilmente uma substring arbitrária, por exemplo,

/* 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);
}

Observe que o tempo de vida da string retornada por temp_substring seria limitada por aqueles de se src, o que for menor (por isso o método requerinf ser passado - se fosse local, morreria quando o método retornasse).

Em termos de custo de memória, cadeias e buffers de até 64 bytes teriam um byte de sobrecarga (o mesmo que cadeias terminadas em zero); cadeias mais longas teriam um pouco mais (se uma quantidade permitida de sobrecarga entre dois bytes e o máximo necessário seria uma troca de tempo / espaço). Um valor especial do comprimento / modo de byte seria usado para indicar que uma função de string recebeu uma estrutura contendo um byte de flag, um ponteiro e um tamanho de buffer (que poderia então indexar arbitrariamente em qualquer outra string).

Obviamente, a K&R não implementou nada disso, mas isso é mais provável porque eles não queriam gastar muito esforço no manuseio de cordas - uma área em que até hoje muitas línguas parecem anêmicas.

supercat
fonte
Não há nada que char* arrpossa impedir de apontar para uma estrutura do formulário struct { int length; char characters[ANYSIZE_ARRAY] };ou similar que ainda seria passável como um único parâmetro.
quer
@BillyONeal: Dois problemas com essa abordagem: (1) Permitiria apenas passar a string como um todo, enquanto a abordagem atual também permite passar a cauda de uma string; (2) desperdiçará um espaço significativo quando usado com pequenas cordas. Se a K&R quisesse dedicar algum tempo às cordas, elas poderiam ter tornado as coisas muito mais robustas, mas não acho que elas pretendessem que seu novo idioma fosse usado dez anos depois, muito menos quarenta.
supercat
1
Esse trecho da convenção de chamada é uma história just-so, sem relação com a realidade ... não foi uma consideração no design. E as convenções de chamadas baseadas em registros já haviam sido "inventadas". Além disso, abordagens como dois ponteiros não eram uma opção porque as estruturas não eram de primeira classe ... apenas as primitivas eram atribuíveis ou passáveis; a cópia struct não chegou até o UNIX V7. Precisando de memcpy (que também não existia) apenas para copiar um ponteiro de string é uma piada. Tente escrever um programa completo, não apenas funções isoladas, se estiver pretendendo criar um design de linguagem.
Jim Balter
1
"isso é mais provável porque eles não queriam gastar muito esforço no manuseio de cordas" - bobagem; todo o domínio do aplicativo do UNIX inicial era manipulação de strings. Se não fosse por isso, nunca teríamos ouvido falar.
Jim Balter
1
'Eu não acho que' o buffer char comece com um int contendo o comprimento 'seja mais mágico' - é se você quiser fazer str[n]referência ao char correto. Esses são os tipos de coisas em que as pessoas que discutem isso não pensam .
Jim Balter
2

De acordo com Joel Spolsky nesta postagem no blog ,

É porque o microprocessador PDP-7, no qual o UNIX e a linguagem de programação C foram inventados, tinha um tipo de string ASCIZ. ASCIZ significava "ASCII com um Z (zero) no final".

Depois de ver todas as outras respostas aqui, estou convencido de que, mesmo que isso seja verdade, é apenas parte do motivo de C ter "strings" com terminação nula. Esse post é bastante esclarecedor sobre como coisas simples como strings podem realmente ser bastante difíceis.

BenK
fonte
2
Olha, eu respeito Joel por muitas coisas; mas isso é algo que ele está especulando. A resposta de Hans Passant vem diretamente dos inventores de C.
Billy ONeal
1
Sim, mas se o que Spolsky diz é verdade, seria parte da "conveniência" a que se referiam. É em parte por isso que incluí esta resposta.
BenK
AFAIK .ASCIZera apenas uma instrução assembler para construir uma sequência de bytes, seguida por 0. Significa apenas que zero string terminada era um conceito bem estabelecido na época. Isso não significa que zero seqüências terminadas eram algo relacionado à arquitetura de um PDP- *, exceto que você poderia escrever loops restritos que consistiam em MOVB(copiar um byte) e BNE(ramificar se o último byte copiado não fosse zero).
Adrian W
Ele supõe mostrar que C é uma linguagem antiga, flácida e decrépita.
purec 30/09/18
2

Não é uma justificativa necessariamente, mas um contraponto ao código codificado em comprimento

  1. Certas formas de codificação dinâmica de comprimento são superiores à codificação estática de comprimento no que diz respeito à memória, tudo depende do uso. Veja o UTF-8 como prova. É essencialmente uma matriz de caracteres extensível para codificar um único caractere. Isso usa um único bit para cada byte estendido. A terminação NUL usa 8 bits. Prefixo de comprimento, acho que também pode ser razoavelmente denominado comprimento infinito usando 64 bits. A frequência com que você atinge os bits extras é o fator decisivo. Apenas uma corda extremamente grande? Quem se importa se você estiver usando 8 ou 64 bits? Muitas cordas pequenas (ou seja, cordas de palavras em inglês)? Seus custos de prefixo são uma grande porcentagem.

  2. Sequências com prefixo de comprimento, permitindo economia de tempo, não são reais . Se é necessário que os dados fornecidos tenham o comprimento fornecido, você está contando em tempo de compilação ou realmente está sendo fornecido dados dinâmicos que devem ser codificados como uma sequência. Esses tamanhos são calculados em algum momento do algoritmo. Uma variável separada para armazenar o tamanho de uma sequência terminada nula pode ser fornecida. O que faz a comparação em questão de economia de tempo. Um só tem um NUL extra no final ... mas se a codificação de comprimento não incluir esse NUL, literalmente não haverá diferença entre os dois. Não há nenhuma mudança algorítmica necessária. Apenas um pré-passe, você precisa se projetar manualmente, em vez de um compilador / tempo de execução fazer isso por você. C é principalmente sobre fazer as coisas manualmente.

  3. O prefixo de comprimento sendo opcional é um ponto de venda. Eu nem sempre preciso dessas informações extras para um algoritmo, portanto, ser necessário fazê-lo para cada sequência de caracteres faz com que meu tempo de pré-cálculo + computação nunca seja capaz de cair abaixo de O (n). (Ou seja, gerador de números aleatórios de hardware 1-128. Eu posso extrair de uma "sequência infinita". Digamos que apenas gere caracteres tão rapidamente. Portanto, o comprimento da nossa string muda o tempo todo. Mas meu uso dos dados provavelmente não se importa com o quanto muitos bytes aleatórios que tenho. Ele só quer o próximo byte não utilizado disponível assim que puder obtê-lo após uma solicitação. Eu poderia estar esperando no dispositivo. Mas eu também poderia ter um buffer de caracteres pré-lidos. um desperdício desnecessário de computação. Uma verificação nula é mais eficiente.)

  4. O prefixo de comprimento é uma boa proteção contra o estouro de buffer? O mesmo ocorre com o uso sensato das funções e da implementação da biblioteca. E se eu passar dados malformados? Meu buffer tem 2 bytes, mas digo que a função é 7! Ex: Se o gets () foi projetado para ser usado em dados conhecidos, ele poderia ter uma verificação interna do buffer que testou buffers e malloc ()chamadas compilados e ainda segue as especificações. Se era para ser usado como um tubo para STDIN desconhecido chegar a um buffer desconhecido, então claramente não se pode saber sobre o tamanho do buffer, o que significa que um comprimento arg é inútil, você precisa de algo mais aqui, como uma verificação de canário. Por esse motivo, você não pode prefixar o comprimento de alguns fluxos e entradas, apenas não pode. O que significa que a verificação do comprimento deve ser incorporada ao algoritmo e não uma parte mágica do sistema de digitação.TL; DR NUL-terminado nunca teve que ser inseguro, acabou sendo assim por uso indevido.

  5. ponto de contra-contador: a terminação NUL é irritante no binário. Você precisa fazer o prefixo do comprimento aqui ou transformar bytes NUL de alguma maneira: códigos de escape, remapeamento do intervalo, etc ... o que obviamente significa mais uso da memória / informações reduzidas / mais operações por byte. O prefixo de comprimento vence principalmente a guerra aqui. A única vantagem de uma transformação é que nenhuma função adicional precisa ser gravada para cobrir as seqüências de prefixo de comprimento. O que significa que, em suas rotinas sub-O (n) mais otimizadas, você pode fazer com que elas ajam automaticamente como seus equivalentes O (n) sem adicionar mais código. A desvantagem é, obviamente, desperdício de tempo / memória / compactação quando usado em cadeias pesadas NUL.Dependendo de quanto da sua biblioteca você acaba duplicando para operar com dados binários, pode fazer sentido trabalhar apenas com seqüências de prefixo de comprimento. Dito isso, também é possível fazer o mesmo com seqüências de prefixo de comprimento ... -1 comprimento pode significar terminação NUL e você pode usar cadeias terminadas NUL dentro de terminação comprimento.

  6. Concat: "O (n + m) vs O (m)" Suponho que você esteja se referindo a m como o comprimento total da sequência após concatenar, porque ambos precisam ter esse número de operações mínimo (você não pode simplesmente aderir - na sequência 1, e se você precisar realocar?). E eu suponho que n é uma quantidade mítica de operações que você não precisa mais fazer por causa de uma pré-computação. Nesse caso, a resposta é simples: pré-cálculo.E sevocê está insistindo que sempre terá memória suficiente para não precisar realocar e que é a base da notação big-O; a resposta é ainda mais simples: faça uma pesquisa binária na memória alocada para o final da string 1, claramente existe uma grande amostra de zeros infinitos após a sequência 1 para não nos preocuparmos com o realloc. Lá, facilmente consegui n para registrar (n) e mal tentei. Que, se você se lembrar do log (n), é essencialmente tão grande quanto 64 em um computador real, que é essencialmente como dizer O (64 + m), que é essencialmente O (m). (E sim, essa lógica foi usada na análise em tempo de execução de estruturas de dados reais em uso hoje. Não é besteira demais.)

  7. Concat () / Len () novamente : Memorizar resultados. Fácil. Transforma todos os cálculos em pré-cálculos, se possível / necessário. Esta é uma decisão algorítmica. Não é uma restrição forçada do idioma.

  8. A passagem do sufixo da string é mais fácil / possível com a terminação NUL. Dependendo de como o prefixo de comprimento é implementado, ele pode ser destrutivo na string original e, às vezes, nem ser possível. Exigindo uma cópia e passe O (n) em vez de O (1).

  9. A passagem / des-referência de argumento é menor para o prefixo NUL-terminado do que o comprimento. Obviamente, porque você está passando menos informações. Se você não precisa de comprimento, isso economiza muito espaço e permite otimizações.

  10. Você pode trapacear. É realmente apenas um ponteiro. Quem disse que você deve lê-lo como uma string? E se você quiser lê-lo como um único caractere ou um flutuador? E se você quiser fazer o oposto e ler um float como uma string? Se você for cuidadoso, poderá fazer isso com a terminação NUL. Você não pode fazer isso com prefixo de comprimento, é um tipo de dados distintamente diferente de um ponteiro normalmente. Você provavelmente teria que criar uma string byte a byte e obter o comprimento. É claro que se você quisesse algo como um flutuador inteiro (provavelmente possui um NUL dentro dele), teria que ler byte a byte de qualquer maneira, mas os detalhes são deixados para você decidir.

TL; DR Você está usando dados binários? Se não, a terminação NUL permite mais liberdade algorítmica. Se sim, a quantidade de código versus velocidade / memória / compactação é sua principal preocupação. Uma combinação das duas abordagens ou memorização pode ser a melhor.

Preto
fonte
9 foi meio fora da base / mal representado. A pré-correção do comprimento não tem esse problema. A passagem Lenth como uma variável separada faz. Estávamos conversando sobre pré-fiix, mas eu me empolguei. Ainda é uma coisa boa para pensar, então vou deixar por aí. : d
Preto
1

Eu não compro a resposta "C não tem seqüência". É verdade que C não suporta tipos internos de nível superior, mas você ainda pode representar estruturas de dados em C e é isso que é uma string. O fato de uma string ser apenas um ponteiro em C não significa que os primeiros N bytes não possam ter um significado especial como o comprimento.

Os desenvolvedores do Windows / COM estarão familiarizados com o BSTRtipo exatamente igual a este - uma string C com prefixo de comprimento em que os dados reais dos caracteres começam no byte 0.

Portanto, parece que a decisão de usar a terminação nula é simplesmente o que as pessoas preferem, não uma necessidade do idioma.

Mr. Boy
fonte
-3

O gcc aceita os códigos abaixo:

char s [4] = "abcd";

e tudo bem se tratarmos como uma matriz de caracteres, mas não como string. Ou seja, podemos acessá-lo com s [0], s [1], s [2] e s [3], ou mesmo com memcpy (dest, s, 4). Mas teremos personagens confusos quando tentarmos colocar (s), ou pior, com strcpy (dest).

kkaaii
fonte
@Adrian W. Isso é válido C. As cordas de comprimento exato são especificadas e NUL é omitido. Isso geralmente é uma prática imprudente, mas pode ser útil em casos como preencher estruturas de cabeçalho que usam "strings" FourCC.
Kevin Thibedeau 5/09/19
Você está certo. Este é C válido, será compilado e se comportará como descrito por kkaaii. O motivo dos votos negativos (não o meu ...) provavelmente é que essa resposta não responde à pergunta da OP de forma alguma.
Adrian W