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_string
modelo, 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.)
fonte
Respostas:
Da boca do cavalo
Dennis M Ritchie, Desenvolvimento da Linguagem C
fonte
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 :)
fonte
char *temp = "foo bar";
é uma declaração válida em C ... ei! isso não é uma string? não é nulo encerrado?A pergunta é feita como uma coisa
Length Prefixed Strings (LPS)
vszero 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:
"this\0is\0valid\0C"
. É uma string? ou quatro cordas? Ou um monte de bytes ...char a[3] = "foo";
é C válido (não C ++) e não coloca um zero final em a.char*
. Ou seja, não para retornar o endereço da string, mas para retornar os dados reais.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).
fonte
std::basic_string
faz.\0
no final, quando os programadores quiserem, em vez do implícito. Anexar o comprimento é muito pior.Eu acho que tem razões históricas e encontrou isso na wikipedia :
fonte
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
string
tipo, comoint
ouchar
, 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 é:
Então, vamos supor que esse prefixo tenha comprimento. Vamos escrever o código para concatenar duas strings:
Outra alternativa seria usar uma struct para definir uma string:
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
mem
funçõ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.fonte
strlen
e amigos. Quanto ao problema de "deixar para a implementação", você pode dizer que o prefixo é o queshort
estiver 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.short
limita 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.string
tipo 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 matrizchar
se você introduziu a noção de codificação.buf
requer apenas uma alocação) ou usestruct string {int len; char buf[]};
e aloque tudo com uma alocação como membro flexível da matriz e passe-o como astring*
. (Oustruct string {int capacity; int len; char buf[]};
indiscutivelmente , por razões óbvias de desempenho) #Obviamente, para desempenho e segurança, você desejará manter o comprimento de uma corda enquanto estiver trabalhando com ela, em vez de executar repetidamente
strlen
ou 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 semelhantespath_to_filename
oufilename_to_extension
impossí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" ruinsO 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.
fonte
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
total de 1 uso de registro
caso 2
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
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).
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.
fonte
O(m+n)
com seqüências de caracteres nulos,O(n)
típicas em qualquer outro lugar. ComprimentoO(n)
com cadeias de caracteres nulos, emO(1)
qualquer outro lugar. Join:O(n^2)
com strings nullterm, emO(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.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.
fonte
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 :)
fonte
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:
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 (
char
matriz 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.fonte
char*
, muitos métodos que não esperam a terminação nula também esperam achar*
. 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 ... #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.)
fonte
A terminação nula permite operações rápidas baseadas em ponteiro.
fonte
strlen
. Eu diria que isso é uma desvantagem.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.
fonte
sizeof(char)
.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.size_t
prefixo (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ãostruct { size_t length; T* ptr; }
e strings são apenas matrizes deimmutable(char)
.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
versus
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]
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,
Observe que o tempo de vida da string retornada por temp_substring seria limitada por aqueles de
s
esrc
, 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.
fonte
char* arr
possa impedir de apontar para uma estrutura do formuláriostruct { int length; char characters[ANYSIZE_ARRAY] };
ou similar que ainda seria passável como um único parâmetro.str[n]
referência ao char correto. Esses são os tipos de coisas em que as pessoas que discutem isso não pensam .De acordo com Joel Spolsky nesta postagem no blog ,
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.
fonte
.ASCIZ
era apenas uma instrução assembler para construir uma sequência de bytes, seguida por0
. 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 emMOVB
(copiar um byte) eBNE
(ramificar se o último byte copiado não fosse zero).Não é uma justificativa necessariamente, mas um contraponto ao código codificado em comprimento
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.
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.
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.)
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.
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.
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.)
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.
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).
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.
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.
fonte
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
BSTR
tipo 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.
fonte
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).
fonte