¿Cuál es la justificación de las cadenas terminadas en nulo?

281

Por mucho que ame C y C ++, no puedo evitar rascarme la cabeza al elegir cadenas terminadas en nulo:

  • Las cadenas de longitud prefijadas (es decir, Pascal) existían antes de C
  • Las cadenas prefijadas de longitud hacen que varios algoritmos sean más rápidos al permitir una búsqueda de longitud de tiempo constante.
  • Las cadenas prefijadas de longitud hacen que sea más difícil causar errores de desbordamiento del búfer.
  • Incluso en una máquina de 32 bits, si permite que la cadena sea del tamaño de la memoria disponible, una cadena prefijada de longitud es solo tres bytes más ancha que una cadena terminada en nulo. En máquinas de 16 bits, este es un solo byte. En máquinas de 64 bits, 4 GB es un límite de longitud de cadena razonable, pero incluso si desea expandirlo al tamaño de la palabra de máquina, las máquinas de 64 bits generalmente tienen memoria suficiente, lo que hace que los siete bytes adicionales sean un argumento nulo. Sé que el estándar C original fue escrito para máquinas increíblemente pobres (en términos de memoria), pero el argumento de la eficiencia no me vende aquí.
  • Casi todos los demás idiomas (es decir, Perl, Pascal, Python, Java, C #, etc.) usan cadenas prefijadas de longitud. Estos lenguajes generalmente superan a C en los puntos de referencia de manipulación de cadenas porque son más eficientes con las cadenas.
  • C ++ rectificó esto un poco con la std::basic_stringplantilla, pero las matrices de caracteres simples que esperan cadenas terminadas en nulo siguen siendo dominantes. Esto también es imperfecto porque requiere la asignación del montón.
  • Las cadenas terminadas en nulo deben reservar un carácter (es decir, nulo), que no puede existir en la cadena, mientras que las cadenas con prefijo de longitud pueden contener nulos incrustados.

Varias de estas cosas han salido a la luz más recientemente que C, por lo que tendría sentido que C no las supiera. Sin embargo, varios eran evidentes mucho antes de que C surgiera. ¿Por qué se habrían elegido cadenas terminadas en cero en lugar del prefijo de longitud obviamente superior?

EDITAR : Dado que algunos pidieron datos (y no les gustaron los que ya proporcioné) en mi punto de eficiencia anterior, provienen de algunas cosas:

  • Concat que utiliza cadenas terminadas en nulo requiere una complejidad de tiempo O (n + m). El prefijo de longitud a menudo requiere solo O (m).
  • La longitud que usa cadenas terminadas en nulo requiere una complejidad de tiempo O (n). El prefijo de longitud es O (1).
  • Longitud y concat son, con mucho, las operaciones de cadena más comunes. Hay varios casos en los que las cadenas terminadas en nulo pueden ser más eficientes, pero ocurren con mucha menos frecuencia.

De las respuestas a continuación, estos son algunos casos en los que las cadenas terminadas en nulo son más eficientes:

  • Cuando necesita cortar el inicio de una cadena y necesita pasarla a algún método. Realmente no puede hacer esto en tiempo constante con el prefijo de longitud, incluso si se le permite destruir la cadena original, porque el prefijo de longitud probablemente deba seguir las reglas de alineación.
  • En algunos casos en los que solo está recorriendo la cadena carácter por carácter, es posible que pueda guardar un registro de CPU. Tenga en cuenta que esto funciona solo en el caso de que no haya asignado dinámicamente la cadena (porque entonces tendría que liberarla, necesitando usar ese registro de CPU que guardó para contener el puntero que originalmente obtuvo de malloc y amigos).

Ninguno de los anteriores es tan común como la longitud y la concat.

Hay uno más afirmado en las respuestas a continuación:

  • Necesitas cortar el final de la cuerda

pero este es incorrecto: es la misma cantidad de tiempo para las cadenas con terminación nula y con prefijo de longitud. (Las cadenas terminadas en nulo solo pegan un nulo donde desea que esté el nuevo final, los prefijos de longitud solo se restan del prefijo).

Billy ONeal
fuente
110
Siempre pensé que era un rito de iniciación para todos los programadores de C ++ escribir su propia biblioteca de cadenas.
Julieta
31
¿De qué se trata ahora de esperar explicaciones racionales? Supongo que querrás escuchar una justificación para x86 o DOS a continuación. En lo que a mí respecta, la peor tecnología gana. Cada vez. Y la peor representación de cadena.
jalf
44
¿Por qué afirman que las cadenas de prefijos de longitud son superiores? Después de todo, C se hizo popular porque utilizaba cadenas terminadas en nulo, que lo diferenciaban de los otros lenguajes.
Daniel C. Sobral
44
@Daniel: C se hizo popular porque es una representación simple, eficiente y portátil de programas ejecutables en máquinas Von Neumann, y porque se usó para Unix. Ciertamente no lo es porque decidió usar cadenas terminadas en nulo. Si fue una buena decisión de diseño, la gente la habría copiado, y no lo han hecho. Ciertamente han copiado casi todo lo demás de C.
Billy ONeal
44
Concat es solo O (m) con prefijos de longitud si destruye una de las cadenas. De lo contrario, la misma velocidad. Los usos más comunes de las cadenas C (históricamente) fueron la impresión y el escaneo. En ambos casos, la terminación nula es más rápida porque guarda un registro.
Daniel C. Sobral

Respuestas:

195

De la boca del caballo

Ninguno de BCPL, B o C admite datos de caracteres fuertemente en el lenguaje; cada uno trata las cadenas como vectores de enteros y complementa las reglas generales mediante algunas convenciones. Tanto en BCPL como en B, un literal de cadena denota la dirección de un área estática inicializada con los caracteres de la cadena, empaquetados en celdas. En BCPL, el primer byte empaquetado contiene el número de caracteres en la cadena; en B, no hay conteo y las cadenas se terminan con un carácter especial, que B deletrea *e. Este cambio se realizó parcialmente para evitar la limitación en la longitud de una cadena causada por mantener el conteo en una ranura de 8 o 9 bits, y en parte porque mantener el conteo parecía, en nuestra experiencia, menos conveniente que usar un terminador.

Dennis M Ritchie, Desarrollo del lenguaje C

Hans Passant
fuente
12
Otra cita relevante: "... la semántica de las cadenas está totalmente subsumida por reglas más generales que rigen todas las matrices, y como resultado el lenguaje es más simple de describir ..."
AShelly
151

C no tiene una cadena como parte del lenguaje. Una 'cadena' en C es solo un puntero a char. Entonces tal vez estás haciendo la pregunta equivocada.

"¿Cuál es la razón para omitir un tipo de cadena" podría ser más relevante. A eso señalaría que C no es un lenguaje orientado a objetos y solo tiene tipos de valores básicos. Una cadena es un concepto de nivel superior que debe implementarse de alguna manera combinando valores de otros tipos. C está en un nivel inferior de abstracción.

a la luz de la furiosa tormenta de abajo:

Solo quiero señalar que no estoy tratando de decir que esta es una pregunta estúpida o mala, o que la forma C de representar cadenas es la mejor opción. Estoy tratando de aclarar que la pregunta se plantearía de manera más sucinta si se tiene en cuenta el hecho de que C no tiene ningún mecanismo para diferenciar una cadena como tipo de datos de una matriz de bytes. ¿Es esta la mejor opción a la luz del poder de procesamiento y memoria de las computadoras de hoy? Probablemente no. Pero en retrospectiva siempre es 20/20 y todo eso :)

Robert S Ciaccio
fuente
29
char *temp = "foo bar";es una declaración válida en C ... hey! ¿No es eso una cuerda? ¿No es nulo terminado?
Yanick Rochon
56
@ Yanick: esa es solo una manera conveniente de decirle al compilador que cree una matriz de caracteres con un valor nulo al final. no es una 'cadena'
Robert S Ciaccio
28
@calavera: Pero podría haber significado simplemente "Crear un búfer de memoria con este contenido de cadena y un prefijo de longitud de dos bytes",
Billy ONeal
14
@Billy: bueno, dado que una 'cadena' es realmente solo un puntero a char, que es equivalente a un puntero a byte, ¿cómo sabrías que el búfer con el que estás tratando realmente es una 'cadena'? necesitaría un nuevo tipo que no sea char / byte * para denotar esto. tal vez una estructura?
Robert S Ciaccio
27
Creo que @calavera tiene razón, C no tiene un tipo de datos para cadenas. Ok, puede considerar una serie de caracteres como una cadena, pero esto no significa que siempre sea una cadena (para la cadena me refiero a una secuencia de caracteres con un significado definido). Un archivo binario es un conjunto de caracteres, pero esos caracteres no significan nada para un humano.
BlackBear
106

La pregunta se hace como una cosa Length Prefixed Strings (LPS)vs zero terminated strings (SZ), pero en su mayoría expone los beneficios de las cadenas prefijadas de longitud. Eso puede parecer abrumador, pero para ser sincero, también debemos considerar los inconvenientes de LPS y las ventajas de SZ.

Según tengo entendido, la pregunta puede incluso entenderse como una forma sesgada de preguntar "¿cuáles son las ventajas de las cadenas terminadas en cero?".

Ventajas (veo) de las cadenas terminadas en cero:

  • muy simple, no es necesario introducir nuevos conceptos en el lenguaje, pueden hacer matrices de caracteres / punteros de caracteres.
  • el lenguaje central solo incluye un mínimo azúcar sintáctico para convertir algo entre comillas dobles en un montón de caracteres (realmente un montón de bytes). En algunos casos, se puede utilizar para inicializar cosas completamente ajenas al texto. Por ejemplo, el formato de archivo de imagen xpm es una fuente C válida que contiene datos de imagen codificados como una cadena.
  • por cierto, se puede poner un cero en un literal de cadena, el compilador simplemente añadir también otro al final del literal: "this\0is\0valid\0C". ¿Es una cuerda? o cuatro cuerdas? O un montón de bytes ...
  • implementación plana, sin indirección oculta, sin número entero oculto.
  • no implica la asignación de memoria oculta (bueno, algunas funciones infames no estándar como strdup realizan la asignación, pero eso es principalmente una fuente de problemas).
  • no hay un problema específico para el hardware pequeño o grande (imagine la carga de administrar la longitud del prefijo de 32 bits en microcontroladores de 8 bits, o las restricciones de limitar el tamaño de la cadena a menos de 256 bytes, ese fue un problema que tuve con Turbo Pascal hace eones).
  • la implementación de la manipulación de cadenas es solo un puñado de funciones de biblioteca muy simples
  • eficiente para el uso principal de cadenas: texto constante leído secuencialmente desde un inicio conocido (principalmente mensajes para el usuario).
  • el cero final ni siquiera es obligatorio, todas las herramientas necesarias para manipular caracteres como un montón de bytes están disponibles. Al realizar la inicialización de la matriz en C, incluso puede evitar el terminador NUL. Solo establece el tamaño correcto. char a[3] = "foo";es válido C (no C ++) y no pondrá un cero final en a.
  • coherente con el punto de vista de Unix "todo es archivo", incluidos los "archivos" que no tienen una longitud intrínseca como stdin, stdout. Debe recordar que las primitivas de lectura y escritura abiertas se implementan en un nivel muy bajo. No son llamadas a la biblioteca, sino llamadas al sistema. Y la misma API se utiliza para archivos binarios o de texto. Las primitivas de lectura de archivos obtienen una dirección de búfer y un tamaño y devuelven el nuevo tamaño. Y puede usar cadenas como el búfer para escribir. El uso de otro tipo de representación de cadena implicaría que no puede usar fácilmente una cadena literal como el búfer de salida, o tendría que hacer que tenga un comportamiento muy extraño al enviarlo char*. Es decir, no devolver la dirección de la cadena, sino devolver los datos reales.
  • muy fácil de manipular los datos de texto leídos de un archivo en el lugar, sin una copia inútil del búfer, simplemente inserte ceros en los lugares correctos (bueno, no realmente con la C moderna, ya que las cadenas de comillas dobles son matrices constantes hoy en día generalmente guardadas en datos no modificables segmento).
  • anteponer algunos valores int de cualquier tamaño implicaría problemas de alineación. La longitud inicial debe estar alineada, pero no hay razón para hacerlo para los datos de los caracteres (y nuevamente, forzar la alineación de las cadenas implicaría problemas al tratarlas como un conjunto de bytes).
  • la longitud se conoce en tiempo de compilación para cadenas literales constantes (sizeof). Entonces, ¿por qué alguien querría almacenarlo en la memoria antes de los datos reales?
  • de una manera que C está haciendo como (casi) todos los demás, las cadenas se ven como matrices de caracteres. Como C no maneja la longitud de la matriz, es lógico que la longitud tampoco se administre para las cadenas. Lo único sorprendente es que se agregan 0 elementos al final, pero eso es solo al nivel del lenguaje principal al escribir una cadena entre comillas dobles. Los usuarios pueden llamar perfectamente a las funciones de manipulación de cadenas pasando la longitud, o incluso usar memcopy simple en su lugar. SZ son solo una instalación. En la mayoría de los otros idiomas se administra la longitud de la matriz, es lógico que sea lo mismo para las cadenas.
  • de todos modos, en los tiempos modernos, los conjuntos de caracteres de 1 byte no son suficientes y, a menudo, tiene que lidiar con cadenas unicode codificadas donde el número de caracteres es muy diferente del número de bytes. Implica que los usuarios probablemente quieran más que "solo el tamaño", pero también otra información. Mantener la longitud no proporciona nada (particularmente ningún lugar natural para almacenarlos) con respecto a estos otros datos útiles.

Dicho esto, no es necesario quejarse en el raro caso en que las cadenas C estándar son de hecho ineficientes. Libs están disponibles. Si seguí esa tendencia, debería quejarme de que el estándar C no incluye ninguna función de soporte de expresiones regulares ... pero realmente todos saben que no es un problema real ya que hay bibliotecas disponibles para ese propósito. Entonces, cuando se desea la eficiencia de la manipulación de cadenas, ¿por qué no usar una biblioteca como bstring ? ¿O incluso cadenas de C ++?

EDITAR : Hace poco tuve un vistazo a las cadenas D . Es lo suficientemente interesante como para ver que la solución elegida no es un prefijo de tamaño ni una terminación cero. Al igual que en C, las cadenas literales entre comillas dobles son solo una abreviatura de las matrices de caracteres inmutables, y el lenguaje también tiene una palabra clave de cadena que significa eso (matriz de caracteres inmutable).

Pero las matrices D son mucho más ricas que las matrices C. En el caso de matrices estáticas, la longitud se conoce en tiempo de ejecución, por lo que no es necesario almacenar la longitud. El compilador lo tiene en tiempo de compilación. En el caso de las matrices dinámicas, la longitud está disponible pero la documentación de D no indica dónde se guarda. Por lo que sabemos, el compilador podría optar por mantenerlo en algún registro, o en alguna variable almacenada lejos de los datos de los caracteres.

En matrices de caracteres normales o cadenas no literales no hay un cero final, por lo tanto, el programador tiene que ponerlo solo si quiere llamar a alguna función C desde D. En el caso particular de las cadenas literales, sin embargo, el compilador de D todavía pone un cero en el final de cada cadena (para permitir una fácil conversión a cadenas C para facilitar la llamada a la función C), pero este cero no es parte de la cadena (D no lo cuenta en el tamaño de la cadena).

Lo único que me decepcionó un poco es que se supone que las cadenas son utf-8, pero la longitud aparentemente aún devuelve una cantidad de bytes (al menos es cierto en mi compilador gdc) incluso cuando se usan caracteres de varios bytes. No me queda claro si es un error del compilador o por propósito. (OK, probablemente descubrí lo que sucedió. Para decirle al compilador D que su fuente usa utf-8, tiene que poner una estúpida marca de orden de bytes al principio. Escribo estúpido porque sé que el editor no está haciendo eso, especialmente para UTF- 8 que se supone que es compatible con ASCII).

kriss
fuente
77
... Continúa ... Creo que varios de sus puntos son simplemente incorrectos, es decir, el argumento "todo es un archivo". Los archivos tienen acceso secuencial, las cadenas C no. El prefijo de longitud también se puede hacer con un mínimo de azúcar sintáctico. El único argumento razonable aquí es tratar de administrar prefijos de 32 bits en hardware pequeño (es decir, 8 bits); Creo que eso podría resolverse simplemente diciendo que el tamaño de la longitud está determinado por la implementación. Después de todo, eso es lo que std::basic_stringhace.
Billy ONeal
3
@Billy ONeal: realmente hay dos partes diferentes en mi respuesta. Uno es sobre lo que forma parte del 'lenguaje central C', el otro es sobre lo que las bibliotecas estándar deberían ofrecer. Con respecto al soporte de cadenas, solo hay un elemento del lenguaje central: el significado de un grupo de bytes incluido entre comillas dobles. No estoy realmente más feliz que tú con el comportamiento C. Siento mágicamente que agregar que el cero al final de cada doble cierre de un grupo de bytes es suficientemente malo. Preferiría y explícito \0al final cuando los programadores quieran eso en lugar del implícito. La longitud de espera es mucho peor.
kriss
2
@Billy ONeal: eso simplemente no es cierto, el uso se preocupa por lo que es el núcleo y las bibliotecas. El punto más importante es cuando C se usa para implementar el sistema operativo. En ese nivel no hay bibliotecas disponibles. C también se usa a menudo en contextos integrados o para dispositivos de programación donde a menudo tiene el mismo tipo de restricciones. En muchos casos, Joes probablemente no debería usar C en absoluto hoy en día: "OK, ¿lo quieres en la consola? ¿Tienes una consola? ¿No? Lástima ..."
kriss
55
@Billy "Bueno, para el .01% de los programadores de C que implementan sistemas operativos, está bien". Los otros programadores pueden hacer una caminata. C fue creado para escribir un sistema operativo.
Daniel C. Sobral
55
¿Por qué? ¿Porque dice que es un lenguaje de propósito general? ¿Dice lo que estaban haciendo las personas que lo escribieron cuando se creó? ¿Para qué se usó durante los primeros años de su vida? Entonces, ¿qué es lo que dice que no está de acuerdo conmigo? Es un lenguaje de propósito general creado para escribir un sistema operativo . ¿Lo niega?
Daniel C. Sobral
61

Creo que tiene razones históricas y encontré esto en wikipedia :

En el momento en que se desarrolló C (y los lenguajes de los que se derivaba), la memoria era extremadamente limitada, por lo que era atractivo usar solo un byte de sobrecarga para almacenar la longitud de una cadena. La única alternativa popular en ese momento, generalmente llamada "cadena Pascal" (aunque también utilizada por versiones anteriores de BASIC), utilizaba un byte inicial para almacenar la longitud de la cadena. Esto permite que la cadena contenga NUL y que la búsqueda de la longitud solo necesite un acceso de memoria (tiempo O (1) (constante)). Pero un byte limita la longitud a 255. Esta limitación de longitud fue mucho más restrictiva que los problemas con la cadena C, por lo que la cadena C en general ganó.

Khachik
fuente
2
@muntoo Hmm ... compatibilidad?
khachik
19
@muntoo: Porque eso rompería cantidades significativas de código C y C ++ existente.
Billy ONeal
10
@muntoo: los paradigmas van y vienen, pero el código heredado es para siempre. Cualquier versión futura de C tendría que continuar admitiendo cadenas terminadas en 0, de lo contrario, el código heredado de más de 30 años tendría que reescribirse (lo que no va a suceder). Y mientras la vieja forma esté disponible, eso es lo que las personas seguirán usando, ya que eso es con lo que están familiarizadas.
John Bode
8
@muntoo: Créeme, a veces desearía poder hacerlo. Pero todavía prefiero cadenas terminadas en 0 sobre cadenas de Pascal.
John Bode
2
Hable sobre el legado ... Las cadenas de C ++ ahora tienen el mandato de tener terminación NUL.
Jim Balter
32

Calavera tiene razón , pero como la gente no parece entender su punto, proporcionaré algunos ejemplos de código.

Primero, consideremos qué es C: un lenguaje simple, donde todo el código tiene una traducción bastante directa al lenguaje máquina. Todos los tipos encajan en los registros y en la pila, y no requiere un sistema operativo o una gran biblioteca de tiempo de ejecución para ejecutarse, ya que estaba destinado a escribir estas cosas (una tarea a la que se adapta perfectamente, teniendo en cuenta allí Ni siquiera es un competidor probable hasta el día de hoy).

Si C tuviera un stringtipo, like into char, sería un tipo que no cabía en un registro o en la pila, y requeriría la asignación de memoria (con toda su infraestructura de soporte) para ser manejada de alguna manera. Todo lo cual va en contra de los principios básicos de C.

Entonces, una cadena en C es:

char s*;

Entonces, supongamos que esto tenía un prefijo de longitud. Escribamos el código para concatenar dos cadenas:

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

Otra alternativa sería usar una estructura para definir una cadena:

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

En este punto, toda manipulación de cadenas requeriría que se realicen dos asignaciones, lo que, en la práctica, significa que pasaría por una biblioteca para manejarla.

Lo curioso es ... ¡ existen estructuras como esa en C! Simplemente no se utilizan para el día a día que muestra mensajes al usuario.

Por lo tanto, aquí está el punto de Calavera está haciendo: no hay ningún tipo cadena en C . Para hacer algo con él, tendría que tomar un puntero y decodificarlo como un puntero para dos tipos diferentes, y luego se vuelve muy relevante cuál es el tamaño de una cadena, y no puede dejarse simplemente como "implementación definida".

Ahora, C puede manejar la memoria de todos modos, y las memfunciones en la biblioteca ( <string.h>¡incluso!) Proporcionan todas las herramientas que necesita para manejar la memoria como un par de puntero y tamaño. Las llamadas "cadenas" en C se crearon con un solo propósito: mostrar mensajes en el contexto de escribir un sistema operativo destinado a terminales de texto. Y, para eso, la terminación nula es suficiente.

Daniel C. Sobral
fuente
2
1. +1. 2. Obviamente, si el comportamiento predeterminado del idioma se hubiera hecho utilizando prefijos de longitud, habría habido otras cosas para hacerlo más fácil. Por ejemplo, todos sus lanzamientos allí habrían estado ocultos por llamadas strleny amigos en su lugar. En cuanto al problema de "dejarlo en manos de la implementación", se podría decir que el prefijo es el que shortestá en el cuadro de destino. Entonces todo tu casting aún funcionaría. 3. Puedo idear escenarios artificiales todo el día que hagan que uno u otro sistema se vea mal.
Billy ONeal
55
@Billy Lo de la biblioteca es bastante cierto, aparte del hecho de que C fue diseñado para un uso mínimo o nulo de la biblioteca. El uso de prototipos, por ejemplo, no era común desde el principio. Decir que el prefijo es shortefectivamente limita el tamaño de la cadena, que parece ser una cosa que no estaban interesados. Yo mismo, después de haber trabajado con cadenas BASIC y Pascal de 8 bits, cadenas COBOL de tamaño fijo y cosas similares, me convertí rápidamente en un gran fanático de las cadenas C de tamaño ilimitado. Hoy en día, un tamaño de 32 bits manejará cualquier cadena práctica, pero agregar esos bytes al principio fue problemático.
Daniel C. Sobral
1
@Billy: Primero, gracias Daniel ... parece que entiendes a lo que me refiero. Segundo, Billy, creo que todavía te estás perdiendo el punto que se está haciendo aquí. Por mi parte, no estoy discutiendo los pros y los contras de prefijar los tipos de datos de cadena con su longitud. Lo que estoy diciendo, y lo que Daniel enfatizó muy claramente, es que se tomó una decisión en la implementación de C para no manejar ese argumento en absoluto . Las cadenas no existen en lo que respecta al lenguaje básico. La decisión sobre cómo manejar las cadenas se deja al programador ... y la terminación nula se hizo popular.
Robert S Ciaccio
1
+1 por mí Una cosa más que me gustaría agregar; una estructura, como usted propone, pierde un paso importante hacia un stringtipo real : no es consciente de los caracteres. Es una serie de "char" (un "char" en la jerga de la máquina es tanto un personaje como una "palabra" es lo que los humanos llamarían una palabra en una oración). Una cadena de caracteres es un concepto de nivel superior que podría implementarse en la parte superior de una matriz charsi se introduce la noción de codificación.
Frerich Raabe
2
@ DanielC.Sobral: Además, la estructura que menciona no requeriría dos asignaciones. Úselo como lo tiene en la pila (por lo que solo bufrequiere una asignación), o use struct string {int len; char buf[]};y asigne todo con una asignación como miembro de matriz flexible, y páselo como a string*. (O posiblemente, struct string {int capacity; int len; char buf[]};por obvias razones de rendimiento)
Mooing Duck
20

Obviamente, por su rendimiento y seguridad, querrás mantener la longitud de una cuerda mientras trabajas con ella en lugar de realizarla repetidamente strleno el equivalente en ella. Sin embargo, almacenar la longitud en una ubicación fija justo antes del contenido de la cadena es un diseño increíblemente malo. Como Jörgen señaló en los comentarios sobre la respuesta de Sanjit, impide tratar la cola de una cadena como una cadena, lo que, por ejemplo, hace que muchas operaciones comunes sean imposibles path_to_filenameo filename_to_extensionsin asignar nueva memoria (e incurrir en la posibilidad de fallas y manejo de errores) . Y luego, por supuesto, está el problema de que nadie puede acordar cuántos bytes debe ocupar el campo de longitud de cadena (un montón de "cadena Pascal" incorrecta)

El diseño de C de permitir que el programador elija si / dónde / cómo almacenar la longitud es mucho más flexible y potente. Pero, por supuesto, el programador tiene que ser inteligente. C castiga la estupidez con programas que se bloquean, se detienen o dan raíces a tus enemigos.

R .. GitHub DEJA DE AYUDAR AL HIELO
fuente
+1. Sin embargo, sería bueno tener un lugar estándar para almacenar la longitud para que aquellos de nosotros que queremos algo como prefijos de longitud no tengan que escribir toneladas de "código de pegamento" en todas partes.
Billy ONeal
2
No hay un lugar estándar posible en relación con los datos de la cadena, pero, por supuesto, puede usar una variable local separada (volver a calcularla en lugar de pasarla cuando la última no es conveniente y la primera no es demasiado derrochadora) o una estructura con un puntero a la cadena (e incluso mejor, un indicador que indica si la estructura "posee" el puntero para fines de asignación o si se trata de una referencia a una cadena de propiedad en otro lugar. Y, por supuesto, puede incluir un miembro de matriz flexible en la estructura para la flexibilidad de asignar la cuerda con la estructura cuando más te convenga
R .. GitHub DEJA DE AYUDAR AL HIELO
13

Pereza, registro de frugalidad y portabilidad teniendo en cuenta el instinto de ensamblaje de cualquier lenguaje, especialmente C, que está un paso por encima del ensamblaje (por lo tanto, hereda una gran cantidad de código heredado de ensamblaje). Usted estaría de acuerdo ya que un carácter nulo sería inútil en esos días ASCII (y probablemente tan bueno como un carácter de control EOF).

veamos en pseudocó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 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 2 registros utilizados

Eso puede parecer miope en ese momento, pero teniendo en cuenta la frugalidad en el código y el registro (que eran PREMIUM en ese momento, el momento en que sabes, usan tarjeta perforada). Por lo tanto, siendo más rápido (cuando la velocidad del procesador se podía contar en kHz), este "Hack" era bastante bueno y portátil para registrar el procesador sin facilidad con facilidad.

Por el bien del argumento, implementaré 2 operaciones de cadena común

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

complejidad O (n) donde, en la mayoría de los casos, la cadena PASCAL es O (1) porque la longitud de la cadena está preajustada a la estructura de la cadena (eso también significaría que esta operación debería llevarse a cabo en una etapa 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

complejidad O (n) y anteponer la longitud de la cadena no cambiaría la complejidad de la operación, aunque admito que tomaría 3 veces menos tiempo.

Por otro lado, si usa la cadena PASCAL, tendría que rediseñar su API para tener en cuenta la longitud del registro y la duración de bits, la cadena PASCAL obtuvo la conocida limitación de 255 caracteres (0xFF) porque la longitud se almacenó en 1 byte (8 bits) ), y si quisieras una cadena más larga (16bits-> cualquier cosa) tendrías que tener en cuenta la arquitectura en una capa de tu código, eso significaría en la mayoría de los casos API de cadenas incompatibles si quisieras una cadena más larga.

Ejemplo:

Se escribió un archivo con su api de cadena anexada en una computadora de 8 bits y luego tendría que leerse en una computadora de 32 bits, ¿qué haría el programa perezoso si considera que sus 4 bytes son la longitud de la cadena y luego asignan esa cantidad de memoria? luego intente leer tantos bytes. Otro caso sería PPC 32 byte string read (little endian) en un x86 (big endian), por supuesto, si no sabe que uno está escrito por el otro, habría problemas. La longitud de 1 byte (0x00000001) se convertiría en 16777216 (0x0100000), es decir, 16 MB para leer una cadena de 1 byte. Por supuesto, diría que las personas deberían ponerse de acuerdo en un estándar, pero incluso unicode de 16 bits tiene poca y gran resistencia.

Por supuesto, C también tendría sus problemas, pero se vería muy poco afectado por los problemas planteados aquí.

dvhh
fuente
2
@deemoowoor: Concat: O(m+n)con cadenas nulas, O(n)típicas en cualquier otro lugar. Longitud O(n)con cuerdas nulas, en O(1)cualquier otro lugar. Únete: O(n^2)con cadenas nullterm, en O(n)cualquier otro lugar. Hay algunos casos en los que las cadenas terminadas en nulo son más eficientes (es decir, solo agregue uno al caso del puntero), pero concat y la longitud son, con mucho, las operaciones más comunes (la longitud al menos es necesaria para el formateo, la salida del archivo, la visualización de la consola, etc.) . Si almacena en caché la longitud para amortizar O(n), simplemente ha dicho que la longitud debe almacenarse con la cadena.
Billy ONeal
1
Estoy de acuerdo en que en el código de hoy este tipo de cadena es ineficiente y propenso a errores, pero por ejemplo, la visualización de la consola realmente no tiene que saber la longitud de la cadena para mostrarla de manera eficiente, la salida del archivo realmente no necesita saber sobre la cadena longitud (solo asignando clúster sobre la marcha), y el formateo de cadenas en este momento se realizó en una longitud de cadena fija en la mayoría de los casos. De todos modos, debe estar escribiendo código incorrecto si concat en C tiene una complejidad O (n ^ 2), estoy bastante seguro de que puedo escribir uno en la complejidad O (n)
dvhh
1
@dvhh: no dije n ^ 2 - dije m + n - todavía es lineal, pero debes buscar hasta el final de la cadena original para hacer la concatenación, mientras que con un prefijo de longitud no buscas es requerido. (Esto es realmente solo otra consecuencia de la longitud que requiere tiempo lineal)
Billy ONeal
1
@Billy ONeal: por pura curiosidad, hice un grep en mi proyecto actual de C (alrededor de 50000 líneas de código) para llamadas a funciones de manipulación de cadenas. strlen 101, strcpy y variantes (strncpy, strlcpy): 85 (también tengo varios cientos de cadenas literales utilizadas para mensajes, copias implícitas), strcmp: 56, strcat: 13 (y 6 son concatenaciones de cadena de longitud cero para llamar a strncat) . Estoy de acuerdo en que una longitud prefijada acelerará las llamadas a strlen, pero no a strcpy o strcmp (tal vez si la API strcmp no usa un prefijo común). Lo más interesante con respecto a los comentarios anteriores es que strcat es muy raro.
kriss
1
@supercat: no realmente, mira algunas implementaciones. Las cadenas cortas usan un búfer basado en una pila corta (sin asignación de montón) y solo usan un montón cuando se hacen más grandes. Pero siéntase libre de proporcionar una implementación real de su idea como biblioteca. Por lo general, los problemas aparecen solo cuando llegamos a los detalles, no en el diseño general.
kriss
9

En muchos sentidos, C era primitivo. Y me encantó.

Fue un paso por encima del lenguaje ensamblador, ofreciéndole casi el mismo rendimiento con un lenguaje que era mucho más fácil de escribir y mantener.

El terminador nulo es simple y no requiere soporte especial por parte del idioma.

Mirando hacia atrás, no parece tan conveniente. Pero usé lenguaje ensamblador en los años 80 y me pareció muy conveniente en ese momento. Simplemente creo que el software está en continua evolución, y las plataformas y herramientas se vuelven cada vez más sofisticadas.

Jonathan Wood
fuente
No veo lo que es más primitivo acerca de las cadenas terminadas en nulo que cualquier otra cosa. Pascal es anterior a C y usa prefijos de longitud. Claro, estaba limitado a 256 caracteres por cadena, pero simplemente usando un campo de 16 bits habría resuelto el problema en la gran mayoría de los casos.
Billy ONeal
El hecho de que limitara el número de caracteres es exactamente el tipo de cuestiones en las que debe pensar al hacer algo así. Sí, podría hacerlo más largo, pero en ese entonces los bytes importaban. ¿Y un campo de 16 bits será lo suficientemente largo para todos los casos? Vamos, debes admitir que una terminación nula es conceptualmente primitiva.
Jonathan Wood
10
O limita la longitud de la cadena o limita el contenido (sin caracteres nulos), o acepta la sobrecarga adicional de un recuento de 4 a 8 bytes. No hay almuerzo gratis. En el momento del inicio, la cadena terminada en nulo tenía mucho sentido. En el ensamblaje, a veces utilicé la parte superior de un carácter para marcar el final de una cadena, ¡guardando incluso un byte más!
Mark Ransom
Exactamente, Mark: No hay almuerzo gratis. Siempre es un compromiso. En estos días, no necesitamos hacer el mismo tipo de compromisos. Pero en aquel entonces, este enfoque parecía tan bueno como cualquier otro.
Jonathan Wood
8

Suponiendo por un momento que C implementó las cadenas al estilo Pascal, prefijándolas por longitud: ¿una cadena larga de 7 caracteres tiene el mismo TIPO DE DATOS que una cadena de 3 caracteres? Si la respuesta es sí, ¿qué tipo de código debe generar el compilador cuando asigno el primero al segundo? ¿Se debe truncar la cadena o cambiar su tamaño automáticamente? Si se cambia el tamaño, ¿esa operación debería estar protegida por una cerradura para que sea segura para la rosca? El lado del enfoque C superó todos estos problemas, nos guste o no :)

Cristian
fuente
2
Err ... no, no lo hizo. El enfoque C no permite asignar la cadena larga de 7 caracteres a la cadena larga de 3 caracteres.
Billy ONeal
@Billy ONeal: ¿por qué no? Por lo que yo entiendo en este caso, todas las cadenas son del mismo tipo de datos (char *), por lo que la longitud no importa. A diferencia de Pascal. Pero eso era una limitación de Pascal, en lugar de un problema con las cadenas con prefijo de longitud.
Oliver Mason
44
@ Billy: Creo que acabas de reafirmar el punto de Cristian. C se ocupa de estos problemas al no tratarlos en absoluto. Todavía estás pensando en términos de que C realmente contiene una noción de una cadena. Es solo un puntero, por lo que puede asignarlo a lo que desee.
Robert S Ciaccio
2
Es como ** la matriz: "no hay cadena".
Robert S Ciaccio
1
@calavera: No veo cómo eso prueba algo. Puede resolverlo de la misma manera con el prefijo de longitud ... es decir, no permita la asignación en absoluto.
Billy ONeal
8

De alguna manera, entendí que la pregunta implica que no hay soporte para el compilador para cadenas con prefijo de longitud en C. El siguiente ejemplo muestra que, al menos, puede iniciar su propia biblioteca de cadenas C, donde las longitudes de las cadenas se cuentan en el momento de la compilación, con una construcción 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;
}

Sin embargo, esto no vendrá sin problemas, ya que debe tener cuidado al liberar específicamente ese puntero de cadena y cuando se asigna estáticamente ( charmatriz literal ).

Editar: como una respuesta más directa a la pregunta, mi opinión es que esta era la forma en que C podría admitir que ambos tuvieran una longitud de cadena disponible (como una constante de tiempo de compilación), en caso de que la necesite, pero aún sin sobrecarga de memoria si desea usar solo punteros y terminación cero.

Por supuesto, parece que trabajar con cadenas terminadas en cero fue la práctica recomendada, ya que la biblioteca estándar en general no toma longitudes de cadena como argumentos, y dado que extraer la longitud no es un código tan sencillo como char * s = "abc", como muestra mi ejemplo.

Pyry Jahkola
fuente
El problema es que las bibliotecas no conocen la existencia de su estructura y aún manejan cosas como nulos incrustados incorrectamente. Además, esto realmente no responde la pregunta que hice.
Billy ONeal
1
Es verdad. Entonces, el problema más grande es que no hay una mejor manera estándar de proporcionar interfaces con parámetros de cadena que las cadenas viejas sin terminación. Todavía afirmaría que hay bibliotecas que admiten la alimentación en pares de longitud de puntero (bueno, al menos puede construir una cadena de C ++ std :: con ellas).
Pyry Jahkola
2
Incluso si almacena una longitud, nunca debe permitir cadenas con valores nulos incrustados. Este es el sentido común básico. Si sus datos pueden tener valores nulos, nunca debe usarlos con funciones que esperen cadenas.
R .. GitHub DEJA DE AYUDAR AL HIELO
1
@supercat: Desde el punto de vista de la seguridad, agradecería esa redundancia. De lo contrario, los programadores ignorantes (o con falta de sueño) terminan la concatenación de datos binarios y cadenas y pasarlos a las cosas que esperan [terminada en nulo] cadenas ...
R .. GitHub dejar de ayudar a ICE
1
@R ..: Si bien los métodos que esperan cadenas terminadas en nulo generalmente esperan a char*, muchos métodos que no esperan terminación nula también esperan a char*. Un beneficio más significativo de separar los tipos estaría relacionado con el comportamiento Unicode. Puede valer la pena que una implementación de cadena mantenga marcas para saber si se sabe que las cadenas contienen ciertos tipos de caracteres, o si se sabe que no los contienen [por ejemplo, encontrar el punto de código 999,990 en una cadena de un millón de caracteres que se sabe que no contiene cualquier personaje más allá del plano multilingüe básico será mucho más rápido ...
supercat
6

"Incluso en una máquina de 32 bits, si permite que la cadena sea del tamaño de la memoria disponible, una cadena prefijada de longitud es solo tres bytes más ancha que una cadena terminada en nulo".

Primero, 3 bytes adicionales pueden ser una sobrecarga considerable para cadenas cortas. En particular, una cadena de longitud cero ahora ocupa 4 veces más memoria. Algunos de nosotros estamos usando máquinas de 64 bits, por lo que necesitamos 8 bytes para almacenar una cadena de longitud cero, o el formato de cadena no puede hacer frente a las cadenas más largas que admite la plataforma.

También puede haber problemas de alineación con los que lidiar. Supongamos que tengo un bloque de memoria que contiene 7 cadenas, como "solo \ 0second \ 0 \ 0four \ 0five \ 0 \ 0seventh". La segunda cadena comienza en el desplazamiento 5. El hardware puede requerir que los enteros de 32 bits se alineen en una dirección que sea múltiplo de 4, por lo que debe agregar relleno, lo que aumenta aún más la sobrecarga. La representación C es muy eficiente en memoria en comparación. (La eficiencia de la memoria es buena; por ejemplo, ayuda al rendimiento de la memoria caché).

Brangdon
fuente
Creo que abordé todo esto en la pregunta. Sí, en plataformas x64, un prefijo de 32 bits no puede ajustarse a todas las cadenas posibles. Por otro lado, nunca querrá una cadena tan grande como una cadena terminada en nulo, porque para hacer cualquier cosa, debe examinar los 4 mil millones de bytes para encontrar el final de casi todas las operaciones que desee realizar. Además, no digo que las cadenas terminadas en nulo sean siempre malas; si está construyendo una de estas estructuras de bloques y su aplicación específica se acelera con ese tipo de construcción, hágalo. Solo desearía que el comportamiento predeterminado del idioma no hiciera eso.
Billy ONeal
2
Cité esa parte de su pregunta porque, en mi opinión, subestimó el problema de la eficiencia. Duplicar o cuadruplicar los requisitos de memoria (en 16 bits y 32 bits respectivamente) puede ser un gran costo de rendimiento. Las cadenas largas pueden ser lentas, pero al menos son compatibles y aún funcionan. Mi otro punto, sobre la alineación, no lo mencionas en absoluto.
Brangdon
La alineación puede tratarse especificando que los valores más allá de UCHAR_MAX deben comportarse como empaquetados y desempaquetados utilizando accesos de bytes y desplazamiento de bits. Un tipo de cadena diseñado adecuadamente podría ofrecer una eficiencia de almacenamiento esencialmente comparable a las cadenas terminadas en cero, al tiempo que permite la verificación de límites en los búferes sin sobrecarga de memoria adicional (use un bit en el prefijo para decir si un búfer está "lleno"; si no es y el último byte no es cero, ese byte representaría el espacio restante. Si el búfer no está lleno y el último byte es cero, los últimos 256 bytes no se usarán, así que ...
supercat
... se podría almacenar dentro de ese espacio el número exacto de bytes no utilizados, con cero costo de memoria adicional). El costo de trabajar con los prefijos se compensaría con la capacidad de usar métodos como fgets () sin tener que pasar la longitud de la cadena (ya que los búferes sabrían qué tan grandes eran).
Supercat
4

La terminación nula permite operaciones rápidas basadas en punteros.

Sanjit Saluja
fuente
55
¿Eh? ¿Qué "operaciones de puntero rápido" no funcionan con prefijos de longitud? Más importante aún, otros lenguajes que usan prefijos de longitud son más rápidos que la manipulación de cadenas C wrt.
Billy ONeal
12
@billy: con cadenas prefijadas de longitud, no puede simplemente tomar un puntero de cadena y agregarle 4, y esperar que siga siendo una cadena válida, porque no tiene un prefijo de longitud (de todos modos, no es válido).
Jörgen Sigvardsson
3
@j_random_hacker: la concatenación es mucho peor para las cadenas asciiz (O (m + n) en lugar de potencialmente O (n)), y concat es mucho más común que cualquiera de las otras operaciones enumeradas aquí.
Billy ONeal
3
hay una operación poco tiiny que se hace más caro con cadenas terminadas en cero: strlen. Diría que es un inconveniente.
jalf
10
@Billy ONeal: todos los demás también admiten expresiones regulares. Y qué ? Use bibliotecas para eso están hechas. C se trata de máxima eficiencia y minimalismo, no de baterías incluidas. Las herramientas C también le permiten implementar cadenas prefijadas de longitud usando estructuras muy fácilmente. Y nada le prohíbe implementar los programas de manipulación de cadenas mediante la administración de su propia longitud y buffers de caracteres. Por lo general, eso es lo que hago cuando quiero eficiencia y uso C, no llamar a un puñado de funciones que esperan un cero al final de un buffer de char no es un problema.
kriss
4

Un punto aún no mencionado: cuando se diseñó C, había muchas máquinas donde un 'char' no era de ocho bits (incluso hoy en día hay plataformas DSP donde no lo es). Si uno decide que las cadenas deben tener un prefijo de longitud, ¿cuántos prefijos de longitud de caracteres debería usar? El uso de dos impondría un límite artificial en la longitud de la cadena para máquinas con caracteres de 8 bits y espacio de direccionamiento de 32 bits, mientras que desperdicia espacio en máquinas con caracteres de 16 bits y espacio de direccionamiento de 16 bits.

Si uno quisiera permitir que las cadenas de longitud arbitraria se almacenen de manera eficiente, y si 'char' fuera siempre de 8 bits, uno podría, por algún gasto en velocidad y tamaño de código, definir un esquema que fuera una cadena prefijada por un número par N tendría una longitud de N / 2 bytes, una cadena prefijada con un valor impar N y un valor par M (lectura hacia atrás) podría ser ((N-1) + M * char_max) / 2, etc. y requeriría cualquier búfer que afirma ofrecer una cierta cantidad de espacio para contener una cadena debe permitir suficientes bytes que precedan a ese espacio para manejar la longitud máxima. Sin embargo, el hecho de que 'char' no siempre sea de 8 bits complicaría dicho esquema, ya que el número de 'char' requerido para contener la longitud de una cadena variará dependiendo de la arquitectura de la CPU.

Super gato
fuente
El prefijo podría ser fácilmente del tamaño definido por la implementación, tal como es sizeof(char).
Billy ONeal
@BillyONeal: sizeof(char)es uno. Siempre. Uno podría tener el prefijo de un tamaño definido por la implementación, pero sería incómodo. Además, no hay una forma real de saber cuál debería ser el tamaño "correcto". Si uno tiene muchas cadenas de 4 caracteres, el relleno cero impondría una sobrecarga del 25%, mientras que un prefijo de longitud de cuatro bytes impondría una sobrecarga del 100%. Además, el tiempo dedicado a empacar y desempacar prefijos de longitud de cuatro bytes podría exceder el costo de escanear cadenas de 4 bytes para el byte cero.
supercat
1
Ah, sí. Tienes razón. Sin embargo, el prefijo podría ser fácilmente algo más que char. Cualquier cosa que haga que los requisitos de alineación en la plataforma de destino funcionen estaría bien. Sin embargo, no voy a ir allí, ya lo he argumentado hasta la muerte.
Billy ONeal
Suponiendo que las cadenas tienen un prefijo de longitud, probablemente lo más sensato sería un size_tprefijo ( maldición sea el desperdicio de memoria, sería la más sana --- permitir cadenas de cualquier longitud posible que puedan caber en la memoria). De hecho, esa es la clase de lo que hace D; las matrices son struct { size_t length; T* ptr; }, y las cadenas son solo matrices de immutable(char).
Tim Čas
@ TimČas: a menos que se requiera que las cadenas estén alineadas con palabras, el costo de trabajar con cadenas cortas en muchas plataformas estaría dominado por el requisito de empacar y desempaquetar la longitud; Realmente no veo eso como algo práctico. Si se desea que las cadenas sean matrices de bytes de tamaño arbitrario independientes del contenido, creo que sería mejor mantener la longitud separada del puntero a los datos de caracteres, y tener un lenguaje que permita obtener ambas piezas de información para cadenas literales .
supercat
2

Muchas decisiones de diseño que rodean a C surgen del hecho de que cuando se implementó originalmente, el paso de parámetros era algo costoso. Dada una elección entre, por ejemplo

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

este último habría sido un poco más barato (y por lo tanto preferido) ya que solo requería pasar un parámetro en lugar de dos. Si el método al que se llama no necesita conocer la dirección base de la matriz ni el índice que contiene, pasar un solo puntero combinando los dos sería más barato que pasar los valores por separado.

Si bien hay muchas formas razonables en las que C podría haber codificado longitudes de cadena, los enfoques que se habían inventado hasta ese momento tendrían todas las funciones requeridas que deberían poder trabajar con parte de una cadena para aceptar la dirección base de la cadena y el índice deseado como dos parámetros separados. El uso de la terminación de byte cero permitió evitar ese requisito. Aunque otros enfoques serían mejores con las máquinas actuales (los compiladores modernos a menudo pasan parámetros en los registros, y memcpy se puede optimizar de manera strcpy () - los equivalentes no pueden) suficiente código de producción utiliza cadenas terminadas de cero bytes que es difícil cambiar a cualquier otra cosa.

PD: a cambio de una leve penalización de velocidad en algunas operaciones, y un poco de sobrecarga adicional en cadenas más largas, habría sido posible que los métodos que funcionan con cadenas acepten punteros directamente a cadenas, buffers de cadena con control de límites o estructuras de datos que identifican subcadenas de otra cadena. Una función como "strcat" se habría parecido a [sintaxis 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);
  }
}

Un poco más grande que el método strcat de K&R, pero admitiría la verificación de límites, lo que no hace el método K&R. Además, a diferencia del método actual, sería posible concatenar fácilmente una subcadena arbitraria, p. Ej.

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

Tenga en cuenta que la vida útil de la cadena devuelta por temp_substring estaría limitada por las de sy src, que alguna vez fue más corta (por lo que el método requiereinf ser pasado, si fuera local, moriría cuando el método regresara).

En términos de costo de memoria, las cadenas y las memorias intermedias de hasta 64 bytes tendrían un byte de sobrecarga (igual que las cadenas terminadas en cero); las cadenas más largas tendrían un poco más (si una cantidad permitida de sobrecarga entre dos bytes y el máximo requerido sería una compensación tiempo / espacio). Se usaría un valor especial del byte longitud / modo para indicar que a una función de cadena se le dio una estructura que contiene un byte indicador, un puntero y una longitud de búfer (que luego podría indexarse ​​arbitrariamente en cualquier otra cadena).

Por supuesto, K&R no implementó tal cosa, pero es muy probable porque no querían gastar mucho esfuerzo en el manejo de cadenas, un área donde incluso hoy en día muchos idiomas parecen bastante anémicos.

Super gato
fuente
No hay nada que hubiera impedido char* arrapuntar a una estructura de la forma struct { int length; char characters[ANYSIZE_ARRAY] };o similar que todavía sería aceptable como un solo parámetro.
Billy ONeal
@BillyONeal: Dos problemas con ese enfoque: (1) Solo permitiría pasar la cadena como un todo, mientras que el enfoque actual también permite pasar la cola de una cadena; (2) desperdiciará un espacio significativo cuando se use con cuerdas pequeñas. Si K&R quisiera dedicar un tiempo a las cuerdas, podrían haber hecho las cosas mucho más robustas, pero no creo que pretendieran que su nuevo lenguaje se usara diez años después, mucho menos cuarenta.
supercat
1
Esta parte de la convención de convocatoria es una historia justa sin relación con la realidad ... no fue una consideración en el diseño. Y las convenciones de llamadas basadas en registros ya habían sido "inventadas". Además, los enfoques como dos punteros no eran una opción porque las estructuras no eran de primera clase ... solo las primitivas eran asignables o pasables; La copia de la estructura no llegó hasta UNIX V7. Necesitar memcpy (que tampoco existía) solo para copiar un puntero de cadena es una broma. Intente escribir un programa completo, no solo funciones aisladas, si pretende simular el diseño del lenguaje.
Jim Balter
1
"Eso es muy probable porque no querían gastar mucho esfuerzo en el manejo de cuerdas" - sin sentido; todo el dominio de aplicación de UNIX temprano era manejo de cadenas. Si no hubiera sido por eso, nunca hubiéramos oído hablar de eso.
Jim Balter
1
"No creo que" el buffer de char comience con un int que contenga la longitud "sea más mágico", lo es si vas a hacer str[n]referencia al char correcto. Estas son las cosas en las que la gente que discute esto no piensa .
Jim Balter
2

Según Joel Spolsky en esta publicación de blog ,

Se debe a que el microprocesador PDP-7, en el que se inventaron UNIX y el lenguaje de programación C, tenía un tipo de cadena ASCIZ. ASCIZ significa "ASCII con una Z (cero) al final".

Después de ver todas las otras respuestas aquí, estoy convencido de que incluso si esto es cierto, es solo una de las razones por las que C tiene "cadenas" terminadas en nulo. Esa publicación es bastante esclarecedora sobre cómo las cosas simples como las cadenas pueden ser bastante difíciles.

BenK
fuente
2
Mira, respeto a Joel por muchas cosas; pero esto es algo en lo que está especulando. La respuesta de Hans Passant proviene directamente de los inventores de C.
Billy ONeal
1
Sí, pero si lo que dice Spolsky es cierto, habría sido parte de la "conveniencia" a la que se referían. Eso es en parte por qué incluí esta respuesta.
BenK
AFAIK .ASCIZera solo una declaración de ensamblador para construir una secuencia de bytes, seguida de 0. Simplemente significa que la cadena terminada en cero era un concepto bien establecido en ese momento. No , no quiere decir que cero cadenas terminadas eran algo relacionado con la arquitectura de un PDP *, excepto que se podría escribir bucles apretados que consisten en MOVB(copiar un byte) y BNE(sucursal si el último byte copiado no era cero).
Adrian W
Supone mostrar que C es un lenguaje viejo, flácido y decrépito.
purec
2

No es necesariamente una justificación, sino un contrapunto a la codificación de longitud

  1. Ciertas formas de codificación de longitud dinámica son superiores a la codificación de longitud estática en lo que respecta a la memoria, todo depende del uso. Solo mire UTF-8 como prueba. Es esencialmente una matriz de caracteres extensible para codificar un solo carácter. Esto usa un solo bit para cada byte extendido. La terminación NUL usa 8 bits. Prefijo de longitud Creo que también se puede llamar razonablemente longitud infinita mediante el uso de 64 bits. La frecuencia con la que acierte el caso de sus bits adicionales es el factor decisivo. ¿Solo 1 cuerda extremadamente grande? ¿A quién le importa si estás usando 8 o 64 bits? ¿Muchas cadenas pequeñas (es decir, cadenas de palabras en inglés)? Entonces sus costos de prefijo son un gran porcentaje.

  2. Las cadenas con longitud prefijada que permiten ahorrar tiempo no son reales . Ya sea que se requiera que se proporcione la longitud de sus datos suministrados, está contando en el momento de la compilación, o realmente se le están proporcionando datos dinámicos que debe codificar como una cadena. Estos tamaños se calculan en algún momento del algoritmo. Una variable independiente para almacenar el tamaño de una cadena terminada en nulo puede ser proporcionada. Lo que hace que la comparación en el ahorro de tiempo sea discutible. Uno solo tiene un NUL extra al final ... pero si la codificación de longitud no incluye ese NUL, literalmente no hay diferencia entre los dos. No se requiere ningún cambio algorítmico en absoluto. Solo un pase previo que debe diseñar manualmente usted mismo en lugar de tener un compilador / tiempo de ejecución que lo haga por usted. C se trata principalmente de hacer las cosas manualmente.

  3. El prefijo de longitud es opcional es un punto de venta. No siempre necesito esa información adicional para un algoritmo, por lo que tener que hacerlo para cada cadena hace que mi tiempo de cálculo y precalculación nunca sea inferior a O (n). (Es decir, generador de números aleatorios de hardware 1-128. Puedo extraer de una "cadena infinita". Digamos que solo genera caracteres tan rápido. Por lo tanto, nuestra longitud de cadena cambia todo el tiempo. Pero mi uso de los datos probablemente no me importa cómo tengo muchos bytes aleatorios. Solo quiere el siguiente byte no utilizado disponible tan pronto como pueda obtenerlo después de una solicitud. Podría estar esperando en el dispositivo. Pero también podría tener un búfer de caracteres preleídos. Una comparación de longitud es un desperdicio innecesario de cómputo. Una verificación nula es más eficiente).

  4. ¿El prefijo de longitud es una buena protección contra el desbordamiento del búfer? También lo es el uso sensato de las funciones y la implementación de la biblioteca. ¿Qué pasa si paso datos mal formados? ¡Mi búfer tiene 2 bytes de largo pero le digo a la función que es 7! Ej: Si recibe estaba destinado a ser utilizado en datos conocidos, podría haber tenido una verificación interna del búfer que probara los búferes compilados y malloc () llamadas () y sigue las especificaciones. Si estaba destinado a usarse como una tubería para que STDIN desconocido llegue a un búfer desconocido, entonces claramente uno no puede saber sobre el tamaño del búfer, lo que significa que una longitud arg no tiene sentido, necesita algo más aquí, como un chequeo canario. Para el caso, no puede prefijar la longitud de algunas secuencias y entradas, simplemente no puede. Lo que significa que la verificación de longitud debe integrarse en el algoritmo y no en una parte mágica del sistema de escritura. TL; DR terminado en NUL nunca tuvo que ser inseguro, simplemente terminó de esa manera por mal uso.

  5. contra-counter point: la terminación NUL es molesta en binario. Debe hacer un prefijo de longitud aquí o transformar bytes NUL de alguna manera: códigos de escape, reasignación de rango, etc., lo que por supuesto significa más uso de memoria / información reducida / más operaciones por byte. El prefijo de longitud gana principalmente la guerra aquí. La única ventaja de una transformación es que no es necesario escribir funciones adicionales para cubrir las cadenas de prefijo de longitud. Lo que significa que en sus rutinas sub-O (n) más optimizadas puede hacer que actúen automáticamente como sus equivalentes O (n) sin agregar más código. La desventaja es, por supuesto, el desperdicio de tiempo / memoria / compresión cuando se usa en cadenas pesadas NUL.Dependiendo de cuánto de su biblioteca termine duplicando para operar con datos binarios, puede tener sentido trabajar únicamente con cadenas de prefijo de longitud. Dicho esto, uno también podría hacer lo mismo con las cadenas de prefijo de longitud ... -1 longitud podría significar terminada en NUL y podría usar cadenas terminadas en NUL dentro de terminada en longitud.

  6. Concat: "O (n + m) vs O (m)" Supongo que te refieres a m como la longitud total de la cadena después de la concatenación porque ambos tienen que tener ese número mínimo de operaciones (no puedes agregar -en la cadena 1, ¿qué pasa si tiene que reasignar?). Y supongo que n es una cantidad mítica de operaciones que ya no tiene que hacer debido a un cálculo previo. Si es así, la respuesta es simple: pre-cómputo. Siinsiste en que siempre tendrá suficiente memoria para no necesitar reasignar y esa es la base de la notación big-O, entonces la respuesta es aún más simple: haga una búsqueda binaria en la memoria asignada para el final de la cadena 1, claramente hay una gran muestra de ceros infinitos después de la cadena 1 para que no nos preocupemos por realloc. Allí, fácilmente logré ingresar n (n) y apenas lo intenté. Lo que si recuerda log (n) es esencialmente solo tan grande como 64 en una computadora real, que es esencialmente como decir O (64 + m), que es esencialmente O (m). (Y sí, esa lógica se ha utilizado en el análisis en tiempo de ejecución de estructuras de datos reales en uso hoy en día. No es una mierda de mi cabeza).

  7. Concat () / Len () nuevamente : Memoize results. Fácil. Convierte todos los cálculos en cálculos previos si es posible / necesario. Esta es una decisión algorítmica. No es una restricción forzada del lenguaje.

  8. El paso del sufijo de cadena es más fácil / posible con terminación NUL. Dependiendo de cómo se implemente el prefijo de longitud, puede ser destructivo en la cadena original y, a veces, incluso puede no ser posible. Requerir una copia y pasar O (n) en lugar de O (1).

  9. El paso de argumento / desreferenciación es menor para el prefijo terminado en NUL frente al de longitud. Obviamente porque estás pasando menos información. Si no necesita longitud, esto ahorra mucho espacio y permite optimizaciones.

  10. Puedes hacer trampa. Realmente es solo un puntero. ¿Quién dice que tienes que leerlo como una cadena? ¿Qué pasa si quieres leerlo como un solo personaje o un flotador? ¿Qué pasa si quieres hacer lo contrario y leer un flotador como una cadena? Si tiene cuidado, puede hacerlo con terminación NUL. No puede hacer esto con el prefijo de longitud, es un tipo de datos claramente diferente de un puntero típicamente. Lo más probable es que tenga que construir una cadena byte por byte y obtener la longitud. Por supuesto, si quisieras algo así como un flotador completo (probablemente tiene un NUL dentro), tendrías que leer byte a byte de todos modos, pero los detalles te quedan para decidir.

TL; DR ¿Está utilizando datos binarios? Si no, entonces la terminación NUL permite más libertad algorítmica. En caso afirmativo, su principal preocupación es la cantidad de código frente a la velocidad / memoria / compresión. Una combinación de los dos enfoques o la memorización podría ser la mejor.

Negro
fuente
9 estaba un poco fuera de base / mal representado. La corrección previa de longitud no tiene este problema. Lenth pasando como una variable separada hace. Estábamos hablando de pre-fiix pero me dejé llevar. Todavía es bueno pensar en eso, así que lo dejaré allí. : d
Negro
1

No compro la respuesta "C no tiene cadena". Es cierto que C no admite tipos integrados de nivel superior, pero aún puede representar estructuras de datos en C y eso es lo que es una cadena. El hecho de que una cadena sea solo un puntero en C no significa que los primeros N bytes no puedan tener un significado especial como longitud.

Los desarrolladores de Windows / COM estarán muy familiarizados con el BSTRtipo que es exactamente así: una cadena C con prefijo de longitud donde los datos de caracteres reales no comienzan en el byte 0.

Entonces parece que la decisión de usar terminación nula es simplemente lo que la gente prefiere, no una necesidad del lenguaje.

Señor chico
fuente
-3

gcc acepta los siguientes códigos:

char s [4] = "abcd";

y está bien si tratamos es como una matriz de caracteres pero no como una cadena. Es decir, podemos acceder con s [0], s [1], s [2] y s [3], o incluso con memcpy (dest, s, 4). Pero obtendremos caracteres desordenados cuando lo intentemos con put (s), o peor, con strcpy (dest, s).

kkaaii
fuente
@ Adrian W. Esto es válido C. Las cadenas de longitud exacta están en mayúsculas especiales y se omite NUL para ellas. Esto generalmente es una práctica imprudente, pero puede ser útil en casos como rellenar estructuras de encabezado que usan "cadenas" de FourCC.
Kevin Thibedeau
Tienes razón. Esto es válido C, se compilará y se comportará como se describe en kkaaii. La razón de los votos negativos (no los míos ...) es probablemente más bien que esta respuesta no responde a la pregunta de OP de ninguna manera.
Adrian W